【课程总结】Linux内核分析课程总结
程涵
原创博客
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
每周实验报告:
反汇编一个简单的C程序http://www.cnblogs.com/ClareOhno/p/5223874.html
操作系统是如何工作的http://www.cnblogs.com/ClareOhno/p/5248098.html
跟踪分析Linux内核的启动过程http://www.cnblogs.com/ClareOhno/p/5273066.html
使用库函数API和C代码中嵌入汇编代码触发同一个系统调用http://www.cnblogs.com/ClareOhno/p/5299817.html
分析system_call中断处理过程http://www.cnblogs.com/ClareOhno/p/5321221.html
进程的描述和进程的创建http://www.cnblogs.com/ClareOhno/p/5338586.html
可执行程序的装载http://www.cnblogs.com/ClareOhno/p/5373813.html
进程的切换和系统的一般执行过程http://www.cnblogs.com/ClareOhno/p/5390687.html
读书笔记:
Linux内核设计与实现(第一章&第二章)http://www.cnblogs.com/ClareOhno/p/5287883.html
Linux内核设计与实现(第五章)http://www.cnblogs.com/ClareOhno/p/5314651.html
Linux内核设计与实现(第十八章) http://www.cnblogs.com/ClareOhno/p/5330905.html
Linux内核设计与实现(第三章)http://www.cnblogs.com/ClareOhno/p/5359096.html
深入理解计算机系统(第七章) http://www.cnblogs.com/ClareOhno/p/5363680.html
知识点总结:
一、计算机是如何工作的
存储程序计算机工作模型
冯诺依曼体系结构
- 从硬件角度来看:CPU和内存,由总线连接,CPU中有一个名为IP的寄存器,总是指向内存的某一块:CS,代码段,执行命令时就取IP指向的一条指令,然后IP自加1,就指向下一条指令。
- 从程序员角度来看:即存储程序计算机,内存存储数据和指令,CPU就是一个for循环,总是在执行下一条指令,CPU负责解释和执行这些指令。
X86汇编基础
- CPU寄存器
- 通用寄存器+段寄存器+标志寄存器
- X86汇编指令
- mov指令:b,w,l,q分别代表8位,16位,32位,64位
- 寻址方式:寄存器寻址,立即数寻址,直接寻址,间接寻址,变址寻址
- 汇编指令:push,pop,call,ret
- leave
- 分析汇编指令片段
汇编一个简单的C程序分析其汇编指令执行过程
- 汇编一个简单的C程序
- C程序和对应的汇编指令
- 完整汇编程序执行过程分析
二、操作系统是如何工作的
函数调用堆栈
- 计算机三大法宝
- 存储程序计算机工作模型
- 函数调用堆栈
- 中断
- 深入理解函数调用堆栈
- 函数调用框架
- 传递参数
- 保存返回地址
- 提供局部变量空间
- 堆栈相关寄存器:esp堆栈指针ebp基址指针,记录当前函数调用基址
- 堆栈操作:push,pop
- cs:eip:总是指向下一条的指令地址,执行call的时候,保存下一条指令地址到栈顶,然后cs:eip指向调用函数入口地址。
- 参数传递与局部变量
借助Linux内核部分源代码模拟存储程序计算机工作模型及时钟中断
- mykernel实验背后涉及的思想
- 利用mykernel实验模拟计算机硬件平台
- 由CPU和内核代码共同实现了保存现场和恢复现场
在mykernel基础上构造一个简单的操作系统内核
- C代码中嵌入汇编代码的写法
- 一个简单的操作系统内核源代码
- mypcb.h:头文件
- mymain.c: 内核初始化和进程的启动
- myinterrupt.c:进程的切换
- 运行这个精简的操作系统内核
三、构造一个简单的Linux系统MenuOS
Linux内核源代码简介
- 操作系统两把宝剑
- 中断上下文的切换
- 进程上下文的切换
- Linux内核源代码
构造一个简单的Linux系统
构造一个简单的Linux系统MenuOS
- qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
- qemu:启动一个虚拟机平台
- -kernel:给一个内核,操作系统
- -initrd:驱动所需的硬盘
- rootfs.img:放一个可执行文件由menuOS源代码编译成的init
跟踪调试Linux内核的启动过程
- 使用gdb跟踪调试Linux内核的方法
- qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
- -S freeze CPU at startup (use ’c’ to start execution)指在CPU初始化之前(刚启动的时候)将其冻结
- -s shorthand for -gdb tcp::1234 指在1234这个端口上创建的gdb server,若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项
- gdb
- (gdb)file linux-3.18.6/vmlinux # 在gdb界面中targe remote之前加载符号表
- (gdb)target remote:1234 # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行
- (gdb)break start_kernel # 断点的设置可以在target remote之前,也可以在之后
- 符号:全局函数名,全局变量名,汇编时没有,不需要符号只是一个地址,但调试时需要,所以有符号表:符号与在内存中地址的一一对应。
- 简单分析一下start_kernel
- 全局变量init_task,即手工创建的PCB,0号进程即最终的idle进程
- trap_init涉及到中断,初始化一些中断向量
- mm_init:内存管理模块初始化
- sched_init:调用进程调度初始化
- rest_init中:
- kernel_init:创建1号进程
- 第一个用户态进程:init_process,1号进程,找默认路径下的程序作为1号进程
- kthreadd:内核线程,用来管理系统资源
- 当系统没有进程需要执行时就调度idle进程,即0号进程,一直存在。
四、系统调用
用户态、内核态和中断
- 用户态&内核态(CPU执行级别)
- 内核态:高级别执行,可以使用特权指令,访问任意的物理地址。对应x86 0级
- 用户态:低级别执行,代码范围受到限制。对应x86 3级(x86CPU有0-3四个级别)
- 如何区分用户态&内核态?
- 0xc0000000以上的地址只能在内核态下访问
- 0x00000000-0xbfffffff两种状态都行
- 中断处理是从用户态进入内核态主要的方式
- 从用户态进入内核态:必须保存用户态的寄存器上下文
- 中断/int指令在堆栈上保存寄存器的值:用户态/内核态栈顶地址(ss:esp)、状态字(eflags)、cs:eip值(内核态时指向中断服务程序入口)
系统调用概述
- 系统调用的意义
- 用户不管硬件编程
- 提高系统安全性
- 用户程序可移植
- API和系统调用
- API:应用编程接口,是一个函数定义
- 系统调用:通过软中断向内核发出明确请求
- 不是每个API都对应一个特定的系统调用
- 应用程序、封装例程、系统调用处理程序、系统调用服务例程之间的关系
- 系统调用三层皮:
- API(xyz)
- 中断向量(system_call)
- 中断服务程序(sys_xyz)
使用库函数API和C代码中嵌入汇编代码触发同一个系统调用
- 使用库函数API获取系统当前时间
- 使用C代码中嵌入汇编代码触发系统调用获取系统当前时间
- 传递了一个系统调用号 - eax
- 传递了参数 - ebx
给MenuOS增加time和time-asm命令
- 更新menu代码到最新版
- 在main函数中增加MenuConfig(一个命令一行,与上面的格式一样)
- 增加对应的time和time-asm函数(就是上周写的两个函数)
- make rootfs (rootfs是一个脚本,可以自动生成,编译)
使用gdb跟踪系统调用内核函数sys_time
- c:继续执行,停在断点处
- n/s:单步运行,s进入函数,n不进入
系统调用在内核代码中的处理过程
- int 0x80触发一个系统调用 ->执行系统调用处理函数system_call ->返回到用户态
- 返回到用户态之前有一个进程调度的时机,会跟踪到schedule
- 中断向量:int 0x80 ->system_call,初始化时绑定
- 系统调用号:将xyz和中断服务程序sys_xyz关联起来
- 系统调用机制的初始化
- main.c中start_kernel函数:trap_init()
- set_system_trap_gate(SYSCALL_VECTOR,&system_call)
- SYSCALL_VECTOR:系统调用的中断向量
- &system_call:汇编代码入口
- 理解system_call到iret之间的主要代码
- 从系统调用入口开始:ENTRY(system_call)
- SAVE_ALL //保存现场
- system_call中:call *system_ call_table(,%eax,4) //调用了系统调用处理函数,有系统调用号eax中,是实际的系统调用处理程序。
- 当前任务syscall_ exit_ work里面有work_ pending里面有work_ notifysig //处理pending信号,不用管
- 重要的是work_ resched:call schedule //决定了进程调度的代码,调用完会跳转到restore_all
- restore_all //恢复现场
- INTERRUPT_ RETURN //irp_return宏,中断处理过程在这结束
五、进程的描述和进程的创建
进程的描述
- 操作系统的三大管理功能:
- 进程管理(最重要的)
- 内存管理
- 文件系统
- 进程控制块PCB task_struct:
- 进程状态
- 进程打开的文件
- 进程优先级信息
- 进程描述符task_struct数据结构
进程的创建
- 进程的创建概览
- 道生一(start_ kernel....cpu_idle)
- 一生二(kernel_init和kthreadd)
- 二生三(即前面0、1和2三个进程)
- 三生万物(1号进程是所有用户态进程的祖先,2号进程是所有内核线程的祖先)
- fork创建一个子进程
- fork在父进程和子进程各返回一次
- 创建一个新进程在内核中的执行过程
- 新进程从哪里开始执行?
- fork出来的子进程是从
ret_from_fork
开始执行的,然后跳转到syscall_exit
,从系统调用中返回。
六、可执行程序的装载
预处理、编译、链接和目标文件的格式
- 可执行程序是怎么得来的?
- C代码
- 编译器预处理
- 编译成汇编代码
- 汇编器编译成目标代码
- 链接成可执行文件
- 操作系统加载到内存执行
- 目标文件的格式ELF
- 静态链接的ELF可执行文件和进程的地址空间
- 默认加载起始地址是0x8048000
- ELF头部大小不同,程序入口点也将不同,程序入口在头部有定义:
Entry point address
,即是可执行文件加载到内存中开始执行的第一行代码
可执行程序、共享库和动态链接
- 装载可执行程序之前的工作
- 可执行程序的执行环境
- 命令行参数和环境变量是如何保存和传递的?
- 参数传递过程:shell程序 -> execve系统调用 -> sys_execve 内核处理函数在初始化新程序堆栈时拷贝进去
- 装载时动态链接和运行时动态链接应用举例
- 准备.so文件
- 分别以共享库和动态加载共享库的方式使用libshlibexample.so文件和libdllibexample.so文件
可执行程序的装载
- 可执行程序的装载相关关键问题分析
- 特殊系统调用:fork、execve
- sys_execve内部会解析可执行文件格式
- execve系统调用返回到用户态从哪里开始执行?load_ elf_ binary -> start_thread
- sys_execve的内部处理过程
- 使用gdb跟踪sys_execve内核函数的处理过程
- 可执行程序的装载与庄生梦蝶的故事
- 浅析动态链接的可执行程序的装载
七、进程的切换和系统的一般执行过程
进程切换的关键代码switch_to分析
- 进程调度与进程调度的时机分析
- 中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();
- 内核线程(只有内核态没有用户态的特殊进程)可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;
- 用户态进程无法实现主动调度,只能被动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。
- 进程上下文切换相关代码分析
- 为了控制进程的执行,内核必须有能力挂起正在CPU上执行的进程,并恢复以前挂起的某个进程的执行,这叫做进程切换、任务切换、上下文切换;
- 挂起正在CPU上执行的进程,与中断时保存现场是不同的,中断前后是在同一个进程上下文中,只是由用户态转向内核态执行;
- 进程上下文包含了进程执行需要的所有信息
- 用户地址空间:包括程序代码,数据,用户堆栈等
- 控制信息:进程描述符,内核堆栈等
- 硬件上下文(注意中断也要保存硬件上下文只是保存的方法不同)
- schedule()函数选择一个新的进程来运行,并调用
context_switch
进行上下文的切换,这个宏调用switch_to
来进行关键上下文切换
- next = pick_ next_task(rq, prev);//进程调度算法都封装这个函数内部
- context_switch(rq, prev, next);//进程上下文切换
- switch_to利用了prev和next两个参数:prev指向当前进程,next指向被调度的进程
Linux系统的一般执行过程
- Linux系统的一般执行过程分析
- 中断上下文的切换(中断和中断返回时CPU进行上下文切换)
- 进程上下文的切换(进程调度过程中,从一个进程的内核堆栈切换到另一个进程的内核堆栈)
- Linux系统执行过程中的几个特殊情况
- 通过中断处理过程中的调度时机,用户态进程与内核线程之间互相切换和内核线程之间互相切换,与最一般的情况非常类似,只是内核线程运行过程中发生中断没有进程用户态和内核态的转换;
- 内核线程主动调用schedule(),只有进程上下文的切换,没有发生中断上下文的切换,与最一般的情况略简略;//用户态进程不能主动调用
- fork:创建子进程的系统调用在子进程中的执行起点(next_ ip = ret_ from_ fork)返回用户态,进程返回不是从标号1开始执行,直接跳转到ret_ from_fork执行然后返回到用户态;
- 加载一个新的可执行程序后返回到用户态的情况,如execve,只是中断上下文在execve系统调用内部被修改了;
Linux系统架构和执行过程概览
- Linux操作系统架构概览
- 最简单也是最复杂的操作--执行ls命令
- 从CPU和内存的角度看Linux系统的执行
学习心得:
通过学习这门课程,我把之前《深入理解计算机系统》以及接触过的Linux相关基础知识联系在了一起,通过跟踪调试加深理解,并对 Linux内核工作流程有了更加深刻的了解。在学习的过程中我也发现了自己的诸多不足,对汇编学习的不够扎实让我在部分的汇编语言学习中遇到了困难,但是我坚持每周看视频、写博客、读相关书籍,到现在已经有十周,收获还是很丰富的。课程结束了,学习Linux的路程还刚刚启程。
遗憾:在学习过程中还是有不求甚解的坏习惯,在实验过程中遇到一些问题并没有真正弄懂根源,并没有自己亲手透彻理解问题并解决。以后的学习中会加强锻炼自己解决问题的能力。