第2章
- 计算机的3个法宝:存储程序计算机、函数调用堆栈机制、中断
- 堆栈的具体做用有:
- 记录程序调用框架
- 传递函数参数
- 保存返回值地址
- 提供函数内部局部变量的存储空间
- 操做系统有2把宝剑:中断上下文、进程上下文
第3章
- start_kernel中的最后一句为rest_init,内核启动完成后,有一个call_cpu_idle,当系统没有进程须要执行时就调用idle进程。
- start_kernel()至关于C语言中的main函数,start_kernel是一切的起点,在此函数被调用前,内核代码主要是用汇编语言写的,用于完成硬件系统的初始化工做。该函数几乎涉及了内核的全部主要模块,如:trap_init()(中断向量的初始化),mm_init()(内存管理的初始化),sched_init()(调度模块的初始化)。
- init_task是使用宏初始化的,这是0号进程,是惟一没有经过fork方式产生的进程。而kernel_init已是经过kernel_thread()函数fork出一个新的进程来执行了,这是1号内核线程。
- 调用kernel_thread执行kthreadd,建立PID为2的内核线程。kthreadd()的任务是管理和调度其余内核线程kernel_thread,全部的内核线程都是直接或者间接地以kthreadd为父进程的。
第4章
- Inter x86 CPU有4种不一样的执行级别,分别是的0、一、二、3,数字越小,特权越高。按照Inter的设想,操做系统内核运行在Ring0级别,驱动程序运行在Ring1和Ring2级别,应用程序运行在Ring3级别,实际的操做系统都没有用到这4个级别。其中Linux操做系统只采用了其中的0和3两个特权级别,分别对应内核态和用户态。
- 用户态和内核态很显著的区分方法就是CS:EIP的指向范围。在内核态是,CS:EIP的值能够是任意的地址,在用户态时(假设32位x86机器,有4GB的进程地址空间),则只能访问0xc0000000如下的地址。
- 系统调用也是一种中断,中断处理是从用户态进入内核态的主要方式。
第5章
- arch/x86/kernel/traps.c中trap_init函数调用了set_system_trap_gate函数将系统调用的中断向量号0x80和system_call中断服务程序入口的函数指针绑定。
- system_call在arch/x86/kernel/entry_32.S,它不是一个正常的函数,是一段特殊的汇编代码(起点),内部没有严格遵照函数调用的堆栈机制,所以没法用gdb调试。理解这段代码有助于理解整个Linux运做机制,由于它的执行过程能够类推到其余中断信号触发的中断服务处理过程。
- 从系统调用服务程序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章
- 操做系统内核实现操做系统的三大管理功能:进程管理、内存管理和文件系统,对应操做系统原理课上最重要的三个抽象概念:进程、虚拟内存和文件。
- 在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管道相关的。
- 要研究Linux内核某一部分的特定内容,进程描述符起提纲挈领的做用。
- 操做系统原理中的进程有三种基本状态:就绪、运行、阻塞。实际Linux内核管理的进程状态中的就绪和运行都是TASK_RUNNING。关键在于有没有得到CPU控制权,便是否在CPU中实际执行。
- 阻塞态有两种:TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE,前者可被信号和wake_up()唤醒,后者只能被wake_up()唤醒。阻塞条件没了,就进入就绪队列。
- start_kernel()中最后的rest_init()经过kernel_thread建立了两个内核线程:
- kernel_init,最终把用户态的进程init给启动起来。
- kthreadd,管理全部的内核线程,是全部内核线程的祖先。
- 用户态经过库函数fork()系统调用建立一个子进程。fork系统调用把当前进程复制了一个子进程,也就是一个进程变成了两个进程,两个进程执行相同的代码,只是fork系统调用在父进程和子进程中的返回值不一样。
- fork也是系统调用,所以也是经过int $0x80触发中断机制。
- 子进程复制了父进程中全部的进程信息,包括内核堆栈、进程描述符等,子进程做为一个独立的进程也会被调度。
- 绝大部分信息彻底同样,有些信息不同,例如内核堆栈(即会修改)、把新进程连接到各链表、保存进程执行到什么位置、thread数据结构等
- 复制时采用写时复制技术(Copy On Write),不须要修改进程资源,父子进程共享内存存储空间。
- (kernel/fork.c)fork()、vfork()、clone()三个系统调用和kernel_thread()内核函数均可以建立一个新进程,都是经过do_fork()函数建立。
- (kernel/fork.c)do_fork()主要完成调用copy_process()复制父进程信息(建立一个进程内容的主要代码,返回一个进程描述符指针)、得到pid、调用wake_up_new_task()将子进程加入调度器队列等待得到分配CPU资源运行,经过clone_flags标志作一些辅助工做。
- (kernel/fork.c)copy_process()主要调用dup_task_struct()复制当前(父)进程描述符(struct task_struct)(最关键)、信息检查、初始化、把子进程状态设置为TASK_RUNNING(就绪态)、采用写时复制技术逐一复制全部其余进程资源、调用copy_thread()初始化子进程内核栈、设置子进程pid等。
- (在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)。
- 写入时复制是一种计算机程序设计领域的优化策略。其核心思想是,若是有多个调用者同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其余调用者所见到的最初的资源仍然保持不变。这个过程对其余的调用者是透明的(transparently)。此做法的主要优势是若是调用者没有修改该资源,就不会有副本(private copy)被创建,所以多个调用者只是读取操做是能够共享同一份资源。
<br> #### 关于内核堆栈和用户堆栈 >转自[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. 进程用户栈和内核栈的切换 + 当进程由于中断或者系统调用而陷入内核态之行时,进程所使用的堆栈也要从用户栈转到内核栈。 + 进程陷入内核态后,先把用户态堆栈的地址保存在内核栈之中,而后设置堆栈指针寄存器的内容为内核栈的地址,这样就完成了用户栈向内核栈的转换;当进程从内核态恢复到用户态之行时,在内核态执行的最后将保存在内核栈里面的用户栈的地址恢复到堆栈指针寄存器便可。这样就实现了内核栈和用户栈的互转。 + 从内核转到用户态时用户栈的地址是在陷入内核的时候保存在内核栈里面的,可是在陷入内核的时候如何知道内核栈的地址的呢?关键在进程从用户态转到内核态的时候,进程的内核栈老是空的。这是由于,当进程在用户态运行时,使用的是用户栈,当进程陷入到内核态时,内核栈保存进程在内核态运行的相关信息,可是一旦进程返回到用户态后,内核栈中保存的信息无效,会所有恢复,所以每次进程从用户态陷入内核的时候获得的内核栈都是空的。因此在进程陷入内核的时候,直接把内核栈的栈顶地址给堆栈指针寄存器就能够了。html