《庖丁解牛Linuxn内核分析》 部分摘录和笔记

第2章

  1. 计算机的3个法宝:存储程序计算机、函数调用堆栈机制、中断
  2. 堆栈的具体作用有:
    • 记录程序调用框架
    • 传递函数参数
    • 保存返回值地址
    • 提供函数内部局部变量的存储空间
  3. 操作系统有2把宝剑:中断上下文、进程上下文

第3章

  1. start_kernel中的最后一句为rest_init,内核启动完成后,有一个call_cpu_idle,当系统没有进程需要执行时就调用idle进程。
  2. start_kernel()相当于C语言中的main函数,start_kernel是一切的起点,在此函数被调用前,内核代码主要是用汇编语言写的,用于完成硬件系统的初始化工作。该函数几乎涉及了内核的所有主要模块,如:trap_init()(中断向量的初始化),mm_init()(内存管理的初始化),sched_init()(调度模块的初始化)。
  3. init_task是使用宏初始化的,这是0号进程,是唯一没有通过fork方式产生的进程。而kernel_init已经是通过kernel_thread()函数fork出一个新的进程来执行了,这是1号内核线程。
  4. 调用kernel_thread执行kthreadd,创建PID为2的内核线程。kthreadd()的任务是管理和调度其他内核线程kernel_thread,所有的内核线程都是直接或者间接地以kthreadd为父进程的。

第4章

  1. Inter x86 CPU有4种不同的执行级别,分别是的0、1、2、3,数字越小,特权越高。按照Inter的设想,操作系统内核运行在Ring0级别,驱动程序运行在Ring1和Ring2级别,应用程序运行在Ring3级别,实际的操作系统都没有用到这4个级别。其中Linux操作系统只采用了其中的0和3两个特权级别,分别对应内核态和用户态。
  2. 用户态和内核态很显著的区分方法就是CS:EIP的指向范围。在内核态是,CS:EIP的值可以是任意的地址,在用户态时(假设32位x86机器,有4GB的进程地址空间),则只能访问0xc0000000以下的地址。
  3. 系统调用也是一种中断,中断处理是从用户态进入内核态的主要方式。

第5章

  1. arch/x86/kernel/traps.c中trap_init函数调用了set_system_trap_gate函数将系统调用的中断向量号0x80和system_call中断服务程序入口的函数指针绑定。
  2. system_call在arch/x86/kernel/entry_32.S,它不是一个正常的函数,是一段特殊的汇编代码(起点),内部没有严格遵守函数调用的堆栈机制,因此无法用gdb调试。理解这段代码有助于理解整个Linux运作机制,因为它的执行过程可以类推到其他中断信号触发的中断服务处理过程。
  3. 从系统调用服务程序system_call入口开始,
    • SAVE_ALL保存现场,
    • 然后找到syscall_call和sys_call_table,通过call *sys_call_table(,%eax,4)调用系统调用的内核处理函数,
    • 调用完系统调用后,把eax里的返回值保存到栈中,在退出前判断是否需要一个syscall_exit_work,
    • 需要则进入该函数,该函数里有work_pending,work_pending又有work_notifysig用来处理信号,可能还会调用schedule(work_resced),schedule是非常关键的部分,是进程切换的代码。因此syscall_exit_work是最常见的进程调度时机点。
    • 之后restore_all和最后一个INTERRUPT_RETURN(iret)用于恢复现场并返回系统调用到用户态结束。

第6章

  1. 操作系统内核实现操作系统的三大管理功能:进程管理、内存管理和文件系统,对应操作系统原理课上最重要的三个抽象概念:进程、虚拟内存和文件。
  2. 在Linux内核中用一个数据结构struct task_struct(include/linux/sched.h)来描述进程,即进程描述符。
    • state是运行状态
    • stack是进程堆栈
    • struct list_head tasks把所有进程用双向循环链表链起来,第0个节点自然是init_task(0号进程,其进程描述符结构体变量的初始化是通过硬编码固定的,其他进程都是通过do_fork()复制父进程的方式初始化)
    • struct mm_struct *mm, *active_mm是和进程地址空间、内存管理相关的数据结构指针。每个进程都有若干个数据段、代码段、堆栈段等,都有这个数据结构统领。(每个进程都有独立的逻辑地址空间)
    • (struct task_struct) real_parent、parent为当前进程的父进程,(struct list_head) children为当前进程的子进程,sibling是兄弟进程,这两个都是双向链表。
    • struct thread_struct thread用于保存进程上下文中CPU相关的一些状态信息。
      • 在x86体系中,该结构体定义在arch/x86/include/asm/processor.h。
      • 其中比较关键的是sp和ip,分别用于保存进程上下文中的ESP寄存器状态和EIP寄存器状态。
    • 还有和文件系统相关的数据结构、已打开的文件描述符,和信号处理有关的,和pipe管道相关的。
  3. 要研究Linux内核某一部分的特定内容,进程描述符起提纲挈领的作用。
  4. 操作系统原理中的进程有三种基本状态:就绪、运行、阻塞。实际Linux内核管理的进程状态中的就绪和运行都是TASK_RUNNING。关键在于有没有获得CPU控制权,即是否在CPU中实际执行。
  5. 阻塞态有两种:TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE,前者可被信号和wake_up()唤醒,后者只能被wake_up()唤醒。阻塞条件没了,就进入就绪队列。
  6. start_kernel()中最后的rest_init()通过kernel_thread创建了两个内核线程:
    • kernel_init,最终把用户态的进程init给启动起来。
    • kthreadd,管理所有的内核线程,是所有内核线程的祖先。
  7. 用户态通过库函数fork()系统调用创建一个子进程。fork系统调用把当前进程复制了一个子进程,也就是一个进程变成了两个进程,两个进程执行相同的代码,只是fork系统调用在父进程和子进程中的返回值不同。
    • fork也是系统调用,因此也是通过int $0x80触发中断机制。
  8. 子进程复制了父进程中所有的进程信息,包括内核堆栈、进程描述符等,子进程作为一个独立的进程也会被调度。
    • 绝大部分信息完全一样,有些信息不一样,例如内核堆栈(即会修改)、把新进程链接到各链表、保存进程执行到什么位置、thread数据结构等
    • 复制时采用写时复制技术(Copy On Write),不需要修改进程资源,父子进程共享内存存储空间。
  9. (kernel/fork.c)fork()、vfork()、clone()三个系统调用和kernel_thread()内核函数都可以创建一个新进程,都是通过do_fork()函数创建。
  10. (kernel/fork.c)do_fork()主要完成调用copy_process()复制父进程信息(创建一个进程内容的主要代码,返回一个进程描述符指针)、获得pid、调用wake_up_new_task()将子进程加入调度器队列等待获得分配CPU资源运行,通过clone_flags标志做一些辅助工作。
  11. (kernel/fork.c)copy_process()主要调用dup_task_struct()复制当前(父)进程描述符(struct task_struct)(最关键)、信息检查、初始化、把子进程状态设置为TASK_RUNNING(就绪态)、采用写时复制技术逐一复制所有其他进程资源、调用copy_thread()初始化子进程内核栈、设置子进程pid等。
  12. (在x86_32体系中arch/x86/kernel/process_32.c)copy_thread()完成真正内核栈关键新的初始化,会设置子进程开始执行的起点ret_from_kernel_thread(内核线程)或ret_from_fork(用户态进程),并且将自己成的eax置0,因此在执行pid=fork();后(父子进程都会执行,只不过子进程不是真的完全执行,而是有返回值),fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次。父进程得到的返回值是子进程的process ID(即它本身调用fork的返回值),而子进程是0(eax被设为0)。
  13. 写入时复制是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这个过程对其他的调用者是透明的(transparently)。此作法的主要优点是如果调用者没有修改该资源,就不会有副本(private copy)被建立,因此多个调用者只是读取操作是可以共享同一份资源。

#### 关于内核堆栈和用户堆栈 >转自[https://www.cnblogs.com/dormant/p/5456491.html](https://www.cnblogs.com/dormant/p/5456491.html),修改了错别字和排版 1. 每一个进程(包括普通进程和内核进程)的地址空间都分为用户地址空间和内核地址空间两部分,在32位的x86机器上,用户地址空间的范围是0~3G,内核地址空间的范围是3G~4G。 2. 对于不同的进程,其用户地址空间会随着进程不同而不同,但所有进程的内核地址空间则都是一样的。 + 对于内核进程,由于其始终运行在内核态,所以没有用户地址空间,其对应的tast_struct结构体中的mm域也就被赋值为NULL。 + 对于用户进程,其既有用户地址空间中的栈,也有它自己的内核栈;而内核进程就只有内核栈。 3. 堆的概念应该是只存在于进程的用户地址空间中,所以内核进程是没有堆一说的。 4. 内核线程可以用kmalloc或vmalloc在运行时申请内存。kmalloc或vmalloc申请到的内存在整个内核中都可以使用。比方说内核线程a申请到了一块内存A,只要把该内存的首地址传给另一个内核线程b,则在b中同样也可以使用这块内存。 5. 所有进程(包括内核进程和普通进程)都有一个内核栈,在x86的32位机器上内核栈大小可以为4KB或8KB,这个可以在编译内核的时候配置。 6. 内核栈的用途有两个: + 当进程陷入内核态,即内核代表进程执行系统调用时,系统调用的参数就放在内核栈上,内核栈记录着进程的在内核中的调用链; + 在内核栈被配置成8KB大小的情况下,当中断服务程序中断当前进程时,它将使用当前被中断进程的内核栈。 7. 进程的堆栈 + 内核在创建进程的时候,在创建task_struct的同时,会为进程创建相应的堆栈。 + 每个进程会有两个栈,一个用户栈,存在于用户空间;一个内核栈,存在于内核空间。 + 当进程在用户空间运行时,cpu堆栈指针寄存器里面的内容是用户堆栈地址,使用用户栈;当进程在内核空间时,cpu堆栈指针寄存器里面的内容是内核栈空间地址,使用内核栈。 8. 进程用户栈和内核栈的切换 + 当进程因为中断或者系统调用而陷入内核态之行时,进程所使用的堆栈也要从用户栈转到内核栈。 + 进程陷入内核态后,先把用户态堆栈的地址保存在内核栈之中,然后设置堆栈指针寄存器的内容为内核栈的地址,这样就完成了用户栈向内核栈的转换;当进程从内核态恢复到用户态之行时,在内核态执行的最后将保存在内核栈里面的用户栈的地址恢复到堆栈指针寄存器即可。这样就实现了内核栈和用户栈的互转。 + 从内核转到用户态时用户栈的地址是在陷入内核的时候保存在内核栈里面的,但是在陷入内核的时候如何知道内核栈的地址的呢?关键在进程从用户态转到内核态的时候,进程的内核栈总是空的。这是因为,当进程在用户态运行时,使用的是用户栈,当进程陷入到内核态时,内核栈保存进程在内核态运行的相关信息,但是一旦进程返回到用户态后,内核栈中保存的信息无效,会全部恢复,因此每次进程从用户态陷入内核的时候得到的内核栈都是空的。所以在进程陷入内核的时候,直接把内核栈的栈顶地址给堆栈指针寄存器就可以了。
posted @ 2019-09-19 20:27  辣条小布丁  阅读(418)  评论(0编辑  收藏  举报