20135316王剑桥《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC 1000029000
Linux内核期末总结
一、计算机是如何工作的
个人理解:计算机就是通过和用户进行交互,执行用户的指令,这些指令存放在内存中,通过寄存器存储,堆栈变化,来一步步顺序执行。
二、存储程序计算机工作模型
1.冯诺依曼体系结构—存储程序计算机
硬件角度(主板):通过cpu中IP寄存器指向一个代码段运行某些指令;寄存区,指向内存的某一块区域(代码段)
程序员角度:将cpu抽象为一个for循环,只是执行下一条指令,从内存中取到下一条指令的内容。内存保存指令和数据,cpu负责解释和执行,通过总线连接。
三、X86汇编基础
- 1.X86cpu寄存器
32位:(低16位作为16位寄存器AX,BX,CX,DX,BP,SI,DI,SP)。
通用寄存器:EAX(累加器),EBX(基地址寄存器),ECX(计数寄存器),EDX(数据寄存器),EBP(堆栈基指针),ESI(变址寄存器),EDI(变址寄存器),ESP(堆栈顶指针)。
段寄存器:CS(代码段寄存器),DS(数据段寄存器),ES(附加段寄存器),SS(堆栈段寄存器),FS(附加段寄存器),GS(附加段寄存器)。
CPU在实际取指令的时候根据CS:EIP来准确定位一个指令。
标志寄存区,标识当前的一些状态。
64位寄存器:开头带有R的寄存器
- 2.汇编指令 b,w,l,q分别代表8位,16位,32位,64位
寄存器模式,以%开头的寄存器标识符。
立即数是以$开头的数字。
直接寻址是直接访问一个指定的内存地址的数据。
间接寻址是将寄存器的值作为一个内存地址来访问内存。
变址寻址是在间接寻址之时改变寄存器的数值。
Mov指令:寄存器寻址movl %eax, %edx:把eax寄存器的内容放到edx寄存器。
- 3.寻址方式
立即寻址movl $0x123,%edx:把0x123直接放到edx寄存器中;
直接寻址movl 0x123,%edx:把内存地址0x123所指向的数据放到edx寄存器中;
间接寻址movl (%ebx),%edx:把ebx寄存器存储的值作为内存地址,取出数据放到edx寄存器中;
变址寻址movl 4(%ebx),%edx:把ebx寄存器存储的值加4作为内存地址,取出数据放到edx寄存器中
- 4.Push指令:压栈pushl %eax
将栈顶指针减4,然后将eax寄存器中的值放在esp所指向的内存中。
- 5.Pop指令:出栈popl %eax
将栈顶指针所指向的内存中存放的数据放在eax寄存器中,然后将栈顶指针加4。
- 6.Call指令:call 0x12345
把当前的eip压栈,然后把0x12345这个立即数放到eip寄存器中
- 7.Ret指令:ret
将call指令中保存的eip值还原给eip,ret之后执行call之前的eip,即call之前的下一条指令
*号表示伪指令,不能被程序员直接使用,eip寄存器不能被直接修改,只能通过特殊指令间接修改。
四、计算机工作的三大法宝
- 1.存储程序计算机工作模型,计算机系统最最基础性的逻辑结构;
- 2.函数调用堆栈,高级语言得以运行的基础,只有机器语言和汇编语言的时候堆栈机制对于计算机来说并不那么重要,但有了高级语言及函数,堆栈成为了计算机的基础功能;
- 3.中断,多道程序操作系统的基点,没有中断机制程序只能从头一直运行结束才有可能开始运行其他程序。
五、堆栈相关寄存器:esp(栈顶指针)ebp(栈底指针)
ebp在C语言中用作记录当前函数调用基址。
cs:eip:总是指向下一条的指令地址(顺序执行)。
跳转/分支:call,将当前cs:eip的值压入栈顶,cs:eip指向被调用函数的入口地址
Ret,将保存在栈顶的cs:eip的值弹出,放入cs:eip中。
六、计算机工作的两把宝剑:中断上下文和进程上下文切换
七、Linux内核主要特征:
- 1、支持动态加载内核模块;
- 2、支持对称多处理(SMP);
- 3、内核可以抢占(preemptive),允许内核运行的任务有优先执行的能力;
- 4、不区分线程和进程。
八、操作系统与内核
- 内核:响应中断的中断服务程序;管理多个进程,分享处理器时间调度程序;管理进程地址;空间的内存管理程序;网络、进程间通信等其他功能。
- 内核空间:系统态和被保护起来的内存空间。
- 系统调用:应用程序与内核通信。
九、系统调用的三层皮:xyz,system-call和sys-xyz。
什么是系统调用——系统调用就是用户程序和硬件设备之间的桥梁。用户程序在需要的时候,通过系统调用来使用硬件设备。
- 1、内核态:在高执行级别,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别就对应着内核态。而在相应的低级别执行状态下,代码的掌控范围会受到限制。只能在对应级别允许的范围内活动。从而保证真个系统更稳定。
- 2、Intel x86的CPU有四种不同执行级别0—3,Linux只使用其中的0和3来分别表示内核态和用户态。
- 3、Cs寄存器的最低两位表示当前代码的特权级别。
- 4、0xc0000000以上的地址空间只能在内核态下被访问;0x00000000-0xbfffffff的地址空间在两种状态下都能被访问。(逻辑地址)
- 5、中断处理是从用户态进入内核态的主要方式。
- 6、系统调用是一种特殊的中断。
- 7、从用户态切换到内核态时必须保存用户态的寄存器上下文。中断/int指令会在堆栈寄存器上保存一些寄存器的值。(用户态栈顶地址、当时的状态字、当时的cs:eip的值)
- 8、中断发生之后第一件事就是保存现场。保护现场就是进入中断程序,保存需要用到的寄存器的值,恢复现场就是退出中断程序,回复保存寄存器的数据。中断处理结束最后一件事就是恢复现场。
- 9、操作系统为用户态进程与硬件设备进行交互提供了一组接口—系统调用:把用户从底层的硬件编程中解放出来;极大地提高了系统的安全性;使用户程序具有可移植性。
- 10、应用编程接口API(一个函数),不是每一个API都对应一个系统调用。
- 11、当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数
- 12、系统调用号使用eax寄存器。每个系统调用至少有一个参数。
- 13、寄存器传递参数的限制:每个参数的长度不能超过寄存器的长度(32位)、在系统调用号(eax)之外,参数的个数不能超过六个(ebx,ecx,edx,esi,edi,ebp)。如果超过六个则将某一个寄存器参数作为一个指针,指向一块内存。
十、系统调用的存在,有以下重要的意义:
1.为用户空间提供一种硬件的抽象接口;
2.保证系统稳定和安全;
3.除异常和陷入,是内核唯一的合法入口。
十一、系统调用上下文
内核在执行系统调用的时候处于进程上下文。current指针指向当前任务,即引发系统调用的那个进程。在进程上下文中,内核可以休眠并且可以被抢占。这表明即使是在内核空间中,当前进程也可以被其他进程抢占。因为新的进程可以执行相同的系统调用,所以必须保证系统调用是可重入的。当系统调用返回时,控制权仍然在system_call()中,它最终会负责切换到用户空间并让用户继续执行下去。
十二、进程控制块PCB
task_struct又称进程描述符,是操作系统用于管理控制进程的一个专门的数据结构,记录进程的各种属性,描述进程的动态变化过程,而PCB是系统感知进程存在的唯一标志。
十三、操作系统三大功能:进程管理(核心)、内存管理、文件系统。
十四、进程类型
- 1.I/O消耗型进程:大部分时间用来提交I/O请求或是等待I/O请求,经常处于可运行状态,但运行时间短,等待请求过程时处于阻塞状态。如交互式程序。
- 2.处理器消耗型进程:时间大都用在执行代码上,除非被抢占否则一直不停的运行。
- 3.综合型:既是I/O消耗型又是处理器消耗型。
- 4.调度策略要在:进程响应迅速(响应时间短)和最大系统利用率(高吞吐量)之间寻找平衡。
十五、进程状态转换:
十六.进程管理
1.进程描述符及任务结构
进程存放在叫做任务队列(task list)的双向循环链表中。链表中的每一项包含一个具体进程的所有信息,类型为task_struct,称为进程描述符(process descriptor),该结构定义在<linux/sched.h>文件中。
Linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色(cache coloring)的目的。另一方面,为了避免使用额外的寄存器存储专门记录,让像x86这样寄存器较少的硬件体系结构只要通过栈指针就能计算出task_struct的位置,该结构为thread_info,在文件<asm/thread_info.h>中定义。
2.进程状态
task_struct中的state描述进程的当前状态。进程的状态一共有5种,而进程必然处于其中一种状态:
- 1)TASK_RUNNING(运行)——进程是可执行的,它或者正在执行,或者在运行队列中等待执行。这是进程在用户空间中执行唯一可能的状态;也可以应用到内核空间中正在执行的进程。
- 2)TASK_INTERRUPTIBLE(可中断)——进程正在睡眠(也就是说它被阻塞)等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置为运行,处于此状态的进程也会因为接收到信号而提前被唤醒并投入运行。
- 3)TASK_UNINTERRUPTIBLE(不可中断)——除了不会因为接收到信号而被唤醒从而投入运行外,这个状态与可打断状态相同。这个状态通常在进程必须在等待时不受干扰或等待事件很快就会发生时出现。由于处于此状态的任务对信号不作响应,所以较之可中断状态,使用得较少。
- 4)TASK_ZOMBIE(僵死)——该进程已经结束了,但是其父进程还没有调用wait4()系统调用。为了父进程能够获知它的消息,子进程的进程描述符仍然被保留着。一旦父进程调用了wait4(),进程描述符就会被释放。
- 5)TASK_STOPPED(停止)——进程停止执行,进程没有投入运行也不能投入运行。通常这种状态发生在接收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。
需要调整进程的状态,最好使用set_task_state(task, state)函数,在必要的时候,它会设置内存屏障来强制其他处理器作重新排序(SMP)。
3.进程创建
在Linux系统中,所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程。该进程读取系统的初始化脚本(initscript)并执行其他的相关程序,最终完成系统启动的整个进程。
Linux提供两个函数去处理进程的创建和执行:fork()和exec()。首先,fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID(每个进程唯一),PPID(父进程的PID)和某些资源和统计量(例如挂起的信号)。exec()函数负责读取可执行文件并将其载入地址空间开始运行。
fork()使用写时拷贝(copy-on-write)页实现。内核在fork进程时不复制整个进程地址空间,让父进程和子进程共享同一个拷贝,当需要写入时,数据才会被复制,使各进程拥有自己的拷贝。在页根本不会被写入的情况下(fork()后立即exec()),fork的实际开销只有复制父进程的页表以及给子进程创建唯一的task_struct。
4.线程的实现
从Linux内核的角度来说,它并没有线程这个概念。Linux把所有的线程都当作进程来实现,内核并没有准备特别的调度算法或者定义特别的数据结构来表征线程。相反,每个线程都拥有唯一隶属于自己的task_struct,它看起来就像是一个普通的进程,只是该进程和其他一些进程共享某些资源,如地址空间。
5.进程终结
进程在运行结束,或接受到它既不能处理也不能忽略的信号,或异常时,都会被终结。此时,依靠do_exit()(在kernel/exit.c文件中)把与进程相关联的所有资源都被释放掉(假设进程是这些资源的唯一使用者)。进程不可运行(实际上也没有地址空间让它运行)并处于TASK_ZOMBIE状态。它占用的所有资源就是内核栈、thread_info和task_struct。在父进程获得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的task_struct才被释放。
如果父进程在子进程之前退出,必须有机制保证子进程能找到一个新的父类,否则的话这些成为孤儿的进程就会在退出时永远处于僵死状态,白白的耗费内存。解决方法是给子进程在当前线程组内找一个线程作为父亲,如果不行,就让init做它们的父进程。
十七、进程调度
- 1.什么是调度
现在的操作系统都是多任务的,为了能让更多的任务能同时在系统上更好的运行,需要一个管理程序来管理计算机上同时运行的各个任务(也就是进程)。这个管理程序就是调度程序。
它的功能说起来很简单:决定哪些进程运行,哪些进程等待决定每个进程运行多长时间此外,为了获得更好的用户体验,运行中的进程还可以立即被其他更紧急的进程打断。
总之,调度是一个平衡的过程。一方面,它要保证各个运行的进程能够最大限度的使用CPU(即尽量少的切换进程,进程切换过多,CPU的时间会浪费在切换上);另一方面,保证各个进程能公平的使用CPU(即防止一个进程长时间独占CPU的情况)。
- 2.调度实现原理
2.1.关于进程的优先级进程的优先级有2种度量方法
一种是nice值,nice值的范围是-20~+19,值越大优先级越低,也就是说nice值为-20的进程优先级最大。
一种是实时优先级,实时优先级的范围是0~99,与nice值的定义相反,实时优先级是值越大优先级越高。实时进程都是一些对响应时间要求比较高的进程,因此系统中有实时优先级高的进程处于运行队列的话,它们会抢占一般的进程的运行时间。
2.2.关于时间片
有了优先级,可以决定谁先运行了。但是对于调度程序来说,并不是运行一次就结束了,还必须知道间隔多久进行下次调度。于是就有了时间片的概念。时间片是一个数值,表示一个进程被抢占前能持续运行的时间。也可以认为是进程在下次调度发生前运行的时间(除非进程主动放弃CPU,或者有实时进程来抢占CPU)。时间片的大小设置并不简单,设大了,系统响应变慢(调度周期长);设小了,进程频繁切换带来的处理器消耗。默认的时间片一般是10ms
十八、可执行程序的生成
编译器预处理(负责把include的文件包含进来及宏替换等工作);编译成汇编代码;编译器编译成目标代码;再链接成可执行文件;操作系统加载到内存中来执行。
十九、进程调度与进程调度的时机分析
- 1.不同类型的进程有不同需求的调度需求:
第一种分类:
—I/O-bound:频繁的进行I/O,通常会花费很多时间等待I/O操作的完成
—CPU-bound:计算密集型,需要大量的CPU时间进行运算
第二种分类:
—批处理进程:不必与用户交互,通常在后台运行;不必响应很快;
—实时进程:有实时需求,不被低优先级的进程阻塞;响应时间短,稳定;
—交互式进程:需要经常与用户交互;响应时间要快
- 2.调度策略:一组规则,决定什么时候以怎样的方式选择一个新的进程运行。
- 3.Linux进程根据优先级排序,用特定的算法计算出进程优先级,用一个值表示把进程如何适当地分配给CPU。优先级是动态的,根据进程的行为周期性调整,较长时间未分配到CPU的通常提高优先级;已经在CPU上运行较长时间的降低优先级。
- 4.Schedule函数用来实现调度,在队列中找到一个进程,把CPU分配给他。
- 5.进程调度时机
中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();
内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;
用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。用户态进程只能被动调度;
内核进程是只有内核态没有用户态的特殊进程,可以主动调度也可以被动调度。
几种特殊情况:
- 通过中断处理过程中的调度时机,用户态进程与内核线程之间互相切换和内核线程之间互相切换,与最一般的情况非常类似,只是内核线程运行过程中发生中断没有进程用户态和内核态的转换;
- 内核线程主动调用schedule(),只有进程上下文的切换,没有发生中断上下文的切换,与最一般的情况略简略;
- 创建子进程的系统调用在子进程中的执行起点及返回用户态,如fork(next-ip=ret-from-fork);
- 加载一个新的可执行程序后返回到用户态的情况,如execve;
自我总结
经过半个学期的Linux学习,通过老师的这种教学方式,提高了自己的学习能力和发现问题解决问题的能力。在Linux内核方面,掌握了计算机是如何工作的(对堆栈运行过程做了详细的分析)、X86汇编基础的一些基础知识掌握、了解了计算机工作的三大法宝(存储程序计算机、函数调用堆栈、中断)、计算机工作的两把宝剑(中断上下文和进程上下文)、理解了Linux内核主要特征、分析了系统调用的三层皮(xyz、system-call、sys-xyz)、对进程类型、状态、管理以及调度过程做了详细的分析。这些知识的系统学习也使得我对Linux内核有了深一步的理解,明白了内核的工作流程,在接下来的学习中将会更加努力,按照老师的教学思路,认真分析老师所讲内容,多多实践,争取将理论知识运用到实际中,深入理解Linux内核。