linux内核分析 课程总结
Linux内核分析 链接汇总
Linux内核分析第三周学习总结——构造一个简单的Linux系统MenuOS
Linux内核分析第四周学习总结——扒开应用系统的三层皮(上)
Linux内核分析第五周学习总结——扒开应用系统的三层皮(下)
Linux内核分析第八周学习总结——进程的切换和系统的一般执行过程
读书笔记:
《Linux内核设计与实现》第八周读书笔记——第四章 进程调度
学习总结
一、计算机是如何工作的
计算机大部分都是用冯诺依曼体系结构,即存储程序计算机。
1.两个层面:
硬件角度(主板):通过cpu中IP寄存器指向一个代码段运行某些指令;寄存区,指向内存的某一块区域(代码段)
程序员角度:将cpu抽象为一个for循环,只是执行下一条指令,从内存中取到下一条指令的内容。内存保存指令和数据,cpu负责解释和执行,通过总线连接。
2.CPU解释执行指令
计算机在执行程序时须先将要执行的相关程序和数据放入内存储器中,在执行程序时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之前的下一条指令
三、计算机工作的三大法宝
存储程序计算机工作模型。计算机系统最最基础性的逻辑结构;
函数调用堆栈。高级语言得以运行的基础,只有机器语言和汇编语言的时候堆栈机制对于计算机来说并不那么重要,但有了高级语言及函数,堆栈成为了计算机的基础功能;
中断。多道程序操作系统的基点,没有中断机制程序只能从头一直运行结束才有可能开始运行其他程序。
四、堆栈相关寄存器:esp(栈顶指针)ebp(栈底指针)
ebp在C语言中用作记录当前函数调用基址。
cs:eip:总是指向下一条的指令地址(顺序执行)。
跳转/分支:call,将当前cs:eip的值压入栈顶,cs:eip指向被调用函数的入口地址。
Ret,将保存在栈顶的cs:eip的值弹出,放入cs:eip中。
五、计算机工作的两把宝剑:
中断上下文
进程上下文切换
六、系统调用的三层皮:xyz,system-call和sys-xyz。
什么是系统调用——系统调用就是用户程序和硬件设备之间的桥梁。用户程序在需要的时候,通过系统调用来使用硬件设备。
Intel x86的CPU有四种不同执行级别0—3,Linux只使用其中的0和3来分别表示内核态和用户态。
0xc0000000以上的地址空间只能在内核态下被访问;0x00000000-0xbfffffff的地址空间在两种状态下都能被访问。(逻辑地址)
从用户态切换到内核态时必须保存用户态的寄存器上下文。中断/int指令会在堆栈寄存器上保存一些寄存器的值。(用户态栈顶地址、当时的状态字、当时的cs:eip的值)
中断发生之后第一件事就是保存现场。保护现场就是进入中断程序,保存需要用到的寄存器的值,恢复现场就是退出中断程序,回复保存寄存器的数据。中断处理结束最后一件事就是恢复现场。
系统调用存在的意义:
为用户空间提供一种硬件的抽象接口。
保证系统稳定和安全。
除异常和陷入,是内核唯一的合法入口。
七、系统调用上下文
内核在执行系统调用的时候处于进程上下文。current指针指向当前任务,即引发系统调用的那个进程。在进程上下文中,内核可以休眠并且可以被抢占。这表明即使是在内核空间中,当前进程也可以被其他进程抢占。因为新的进程可以执行相同的系统调用,所以必须保证系统调用是可重入的。当系统调用返回时,控制权仍然在system_call()中,它最终会负责切换到用户空间并让用户继续执行下去。
八、进程类型
I/O消耗型进程:大部分时间用来提交I/O请求或是等待I/O请求,经常处于可运行状态,但运行时间短,等待请求过程时处于阻塞状态。如交互式程序。
处理器消耗型进程:时间大都用在执行代码上,除非被抢占否则一直不停的运行。
综合型:既是I/O消耗型又是处理器消耗型。
九.进程管理
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种,而进程必然处于其中一种状态:
TASK_RUNNING(运行)——进程是可执行的,它或者正在执行,或者在运行队列中等待执行。这是进程在用户空间中执行唯一可能的状态;也可以应用到内核空间中正在执行的进程。
TASK_INTERRUPTIBLE(可中断)——进程正在睡眠(也就是说它被阻塞)等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置为运行,处于此状态的进程也会因为接收到信号而提前被唤醒并投入运行。
TASK_UNINTERRUPTIBLE不可中断)——除了不会因为接收到信号而被唤醒从而投入运行外,这个状态与可打断状态相同。这个状态通常在进程必须在等待时不受干扰或等待事件很快就会发生时出现。由于处于此状态的任务对信号不作响应,所以较之可中断状态,使用得较少。
TASK_ZOMBIE(僵死)——该进程已经结束了,但是其父进程还没有调用wait4()系统调用。为了父进程能够获知它的消息,子进程的进程描述符仍然被保留着。一旦父进程调用了wait4(),进程描述符就会被释放。
TASK_STOPPED(停止)——进程停止执行,进程没有投入运行也不能投入运行。通常这种状态发生在接收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。
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.关于进程的优先级进程的优先级有2种度量方法:
一种是nice值,nice值的范围是-20~+19,值越大优先级越低,也就是说nice值为-20的进程优先级最大。
一种是实时优先级,实时优先级的范围是0~99,与nice值的定义相反,实时优先级是值越大优先级越高。实时进程都是一些对响应时间要求比较高的进程,因此系统中有实时优先级高的进程处于运行队列的话,它们会抢占一般的进程的运行时间。
2.关于时间片
有了优先级,可以决定谁先运行了。但是对于调度程序来说,并不是运行一次就结束了,还必须知道间隔多久进行下次调度。于是就有了时间片的概念。时间片是一个数值,表示一个进程被抢占前能持续运行的时间。也可以认为是进程在下次调度发生前运行的时间(除非进程主动放弃CPU,或者有实时进程来抢占CPU)。时间片的大小设置并不简单,设大了,系统响应变慢(调度周期长);设小了,进程频繁切换带来的处理器消耗。默认的时间片一般是10ms
十一、进程调度的时机分析
中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();
内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;
用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。用户态进程只能被动调度;
内核进程是只有内核态没有用户态的特殊进程,可以主动调度也可以被动调度。
学习体会
经过了这几周的学习,我对Linux内核的构架和原理,有了一些初步的了解,明白了内核的工作流程。在今后的学习中,我将会更加努力,按照老师的教学思路,多多实践,争取将理论知识运用到实际中,深入理解Linux内核。
为了帮助自己理解孟老师的讲解内容,我在课下自学了《Linux内核设计与分析》,收获颇多。
在这几周的学习中,我最大的收获是从孟老师的教学中学会了从实践中学习和验证知识,找到了属于自己的学习方式,提高了自己的自学效率,学以致用。在此,十分感谢孟老师的教导。
最大的遗憾大概就是遗憾网课时间太短,没能在孟老师的教导下继续进一步去学习Linux更深入的知识。今后只能自学来深入学习Linux内核了。
最后,还是要感谢孟老师这几周以来的辛勤教导,老师辛苦了!