Linux内核分析期末总结
Linux内核分析期末总结
李雪琦 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
Linux内核分析学习笔记链接汇总
-
第一周 计算机是如何工作的
-
第二周 操作系统是如何工作的
-
第四周 扒开系统调用的三层皮(上)
-
第五周 扒开系统调用的三层皮(下)
-
第六周 进程的描述与创建
-
第七周 可执行程序的装载
-
第八周 系统的一般执行过程
1.计算机是如何工作的
冯诺依曼体系结构—存储程序计算机
- 硬件角度(主板):通过cpu中IP寄存器指向一个代码段运行某些指令;寄存区,指向内存的某一块区域(代码段)。
- 程序员角度:将cpu抽象为一个for循环,只是执行下一条指令,从内存中取到下一条指令的内容。内存保存指令和数据,cpu负责解释和执行,通过总线连接。
2.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寄存器中
3.计算机工作的三大法宝
- 存储程序计算机工作模型,计算机系统最最基础性的逻辑结构;
- 函数调用堆栈,高级语言得以运行的基础,只有机器语言和汇编语言的时候堆栈机制对于计算机来说并不那么重要,但有了高级语言及函数,堆栈成为了计算机的基础功能;
- 中断,多道程序操作系统的基点,没有中断机制程序只能从头一直运行结束才有可能开始运行其他程序。
4.计算机工作的两把宝剑
- 中断上下文
- 进程上下文切换
5.Linux内核主要特征
- 支持动态加载内核模块;
- 支持对称多处理(SMP);
- 内核可以抢占(preemptive),允许内核运行的任务有优先执行的能力;
- 不区分线程和进程。
6.操作系统与内核
- 内核:响应中断的中断服务程序;管理多个进程,分享处理器时间调度程序;管理进程地址;空间的内存管理程序;网络、进程间通信等其他功能。
- 内核空间:系统态和被保护起来的内存空间。
- 系统调用:应用程序与内核通信。
7.系统调用
1.用户态、内核态区别
- 在高级别的状态下,代码可以执行特权指令,访问任意的物理地址;一般在Linux中,0xc0000000以上的地址(指的是逻辑地址)空间只能在内核态下访问;
- 在相应的低级别执行状态下,代码的掌控范围会受到限制;
2.系统调用的意义
- 把用户从底层的硬件编程中解放出来
- 极大的提高了系统的安全性
- 使用户程序具有可移植性
3.中断处理程序
- 中断指令会在寄存器上保存一些寄存器的值放入内核堆栈,比如:用户态栈顶地址,标志寄存器,cs:eip。同时,将相关联的中端服务历程的入口加载到cs:eip,把当前的堆栈段esp也加载到CPU里面
- 中断发生之后第一件事就是保存现场;中断处理结束前的最后一件事情就是恢复现场
4.系统调用机制的初始化
trap_gate
函数中,涉及到了系统调用的中断向量和system_call
的汇编代码入口;一旦执行int 0x80,CPU直接跳转到system_call
5.简化后便于理解的system_call伪代码
- system_call的位置就在ENTRY(system_call)处;(其他中断的处理过程与此类似)
- syscall_table是系统调用表
- after_call之后,保存返回值
- 要exit的时候,会有一个syscall_exit_work,否则直接返回用户态
8.进程控制块PCB
task_struct又称进程描述符,是操作系统用于管理控制进程的一个专门的数据结构,记录进程的各种属性,描述进程的动态变化过程,而PCB是系统感知进程存在的唯一标志。
9.操作系统三大功能
- 进程管理
- 内存管理
- 文件系统
10.进程类型
- I/O消耗型进程:大部分时间用来提交I/O请求或是等待I/O请求,经常处于可运行状态,但运行时间短,等待请求过程时处于阻塞状态。如交互式程序。
- 处理器消耗型进程:时间大都用在执行代码上,除非被抢占否则一直不停的运行。
- 综合型:既是I/O消耗型又是处理器消耗型。
- 调度策略要在:进程响应迅速(响应时间短)和最大系统利用率(高吞吐量)之间寻找平衡。
11.可执行程序的装载
1.装载可执行文件之前的工作
- 要先fork一个进程,不然会覆盖shell
execlp
加载一个程序shell程序-->execve-->sys_execve
,然后在初始化新程序堆栈的时候拷贝进去- 先传递函数调用参数,再传递系统调用参数
2.sys_execve的内部处理过程
do_execve
-do_open_exec(filename)
打开要加载的文件- 命令行参数,结构体变量copy到bprm结构体中
exce_binprm(bprm)
,关键代码是寻找能解析当前文件的处理模块register_binfmt($elf_format)
注册这个格式到链表里,然后寻找能处理的模块- ELF可执行文件默认映射到0x8048000这个地址
- 需要动态链接的可执行文件先加载连接器ld;否则直接把elf文件entry地址赋值给entry即可。
start_thread(regs,elf_entry,bprm->p)
会将CPU控制权交给ld来加载依赖库并完成动态链接;对于静态链接的文件elf_entry
是新程序执行的起点
12.进程管理
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等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。
需要调整进程的状态,最好使用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.进程终结
进程在运行结束,或接受到它既不能处理也不能忽略的信号,或异常时,都会被终结。此时,依靠do_exit()
(在kernel/exit.c
文件中)把与进程相关联的所有资源都被释放掉(假设进程是这些资源的唯一使用者)。进程不可运行(实际上也没有地址空间让它运行)并处于TASK_ZOMBIE
状态。它占用的所有资源就是内核栈、thread_info
和task_struct
。在父进程获得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的task_struct
才被释放。
如果父进程在子进程之前退出,必须有机制保证子进程能找到一个新的父类,否则的话这些成为孤儿的进程就会在退出时永远处于僵死状态,白白的耗费内存。解决方法是给子进程在当前线程组内找一个线程作为父亲,如果不行,就让init做它们的父进程。
13.进程调度
1.什么是调度
现在的操作系统都是多任务的,为了能让更多的任务能同时在系统上更好的运行,需要一个管理程序来管理计算机上同时运行的各个任务(也就是进程)。这个管理程序就是调度程序。
它的功能说起来很简单:决定哪些进程运行,哪些进程等待决定每个进程运行多长时间此外,为了获得更好的用户体验,运行中的进程还可以立即被其他更紧急的进程打断。
总之,调度是一个平衡的过程。一方面,它要保证各个运行的进程能够最大限度的使用CPU(即尽量少的切换进程,进程切换过多,CPU的时间会浪费在切换上);另一方面,保证各个进程能公平的使用CPU(即防止一个进程长时间独占CPU的情况)。
2.调度实现原理
2.1.关于进程的优先级进程的优先级有2种度量方法
一种是nice值,nice值的范围是-20~+19,值越大优先级越低,也就是说nice值为-20的进程优先级最大。
一种是实时优先级,实时优先级的范围是0~99,与nice值的定义相反,实时优先级是值越大优先级越高。实时进程都是一些对响应时间要求比较高的进程,因此系统中有实时优先级高的进程处于运行队列的话,它们会抢占一般的进程的运行时间。
2.2.关于时间片
有了优先级,可以决定谁先运行了。但是对于调度程序来说,并不是运行一次就结束了,还必须知道间隔多久进行下次调度。于是就有了时间片的概念。时间片是一个数值,表示一个进程被抢占前能持续运行的时间。也可以认为是进程在下次调度发生前运行的时间(除非进程主动放弃CPU,或者有实时进程来抢占CPU)。时间片的大小设置并不简单,设大了,系统响应变慢(调度周期长);设小了,进程频繁切换带来的处理器消耗。默认的时间片一般是10ms。
14.进程调度与进程调度的时机分析
1.不同类型的进程有不同需求的调度需求
第一种分类:
- I/O-bound:频繁的进行I/O,通常会花费很多时间等待I/O操作的完成
- CPU-bound:计算密集型,需要大量的CPU时间进行运算
第二种分类:
- 批处理进程:不必与用户交互,通常在后台运行;不必响应很快;
- 实时进程:有实时需求,不被低优先级的进程阻塞;响应时间短,稳定;
- 交互式进程:需要经常与用户交互;响应时间要快
2.调度策略
一组规则,决定什么时候以怎样的方式选择一个新的进程运行。
3.进程调度时机
- 中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();
- 内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;
- 用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。用户态进程只能被动调度;
- 内核进程是只有内核态没有用户态的特殊进程,可以主动调度也可以被动调度。
总结部分:学习《Linux内核分析》课程中最大的收获?学习完《Linux内核分析》课程后您最大的遗憾是什么?
1.收获
通过这段时间的学习让我对内核有了进一步的认识,对于源码能够自己去搜索查看,能通过阅读部分核心源码来验证老师所说的理论,能系统的了解了操作系统内核的相关结构和设计原理,学会了gdb调试与分析内核代码的方法。另外,自主的学习方式也让我受益匪浅。
2.遗憾
知识未能形成主线,对很多细节方面仍不求甚解,仍需要进一步的消化学习。另外,自己可能过多局限于理论知识,实践动手能力不强。