《Linux内核分析》期末总结
5228 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
一、博客链接汇总
《Linux内核分析》第一周学习笔记:http://www.cnblogs.com/20135228guoyao/p/5215176.html
《Linux内核分析》第二周学习笔记:http://www.cnblogs.com/20135228guoyao/p/5243214.html
《Linux内核分析》第三周学习笔记:http://www.cnblogs.com/20135228guoyao/p/5270286.html
《Linux内核设计与实现》第一、二章学习笔记:http://www.cnblogs.com/20135228guoyao/p/5274011.html
《Linux内核分析》第四周学习笔记:http://www.cnblogs.com/20135228guoyao/p/5284262.html
《Linux内核设计与实现》第五章学习笔记:http://www.cnblogs.com/20135228guoyao/p/5297421.html
《Linux内核分析》第五周学习笔记:http://www.cnblogs.com/20135228guoyao/p/5312936.html
《Linux内核设计与实现》第十八章学习笔记:http://www.cnblogs.com/20135228guoyao/p/5330236.html
《Linux内核分析》第六周学习笔记:http://www.cnblogs.com/20135228guoyao/p/5334985.html
《Linux内核设计与实现》第三章学习笔记:http://www.cnblogs.com/20135228guoyao/p/5340778.html
《深入理解计算机系统》第七章学习笔记:http://www.cnblogs.com/20135228guoyao/p/5343928.html
《Linux内核分析》第七周学习笔记:http://www.cnblogs.com/20135228guoyao/p/5360264.html
《Linux内核分析》第八周学习笔记:http://www.cnblogs.com/20135228guoyao/p/5383322.html
《Linux内核设计与实现》第四章学习笔记:http://www.cnblogs.com/20135228guoyao/p/5384449.html
二、知识点总结
1. 计算机是如何工作的?
- 存储程序计算机工作模型:冯诺依曼体系结构
- X86汇编基础:CPU的寄存器(通用寄存器、段寄存器、标志寄存器)、常见汇编指令、堆栈
- 汇编一个简单的C程序分析其汇编指令执行过程
2. 操作系统是如何工作的?
- 函数调用堆栈
- 借助Linux内核部分源代码模拟存储程序计算机工作模型及时钟中断
- 在mykernel基础上构造一个简单的操作系统内核
- 三个法宝:
- 存储程序计算机:所有计算机基础性的逻辑框架
- 堆栈:高级语言的起点,函数调用需要堆栈机制
- 中断机制:多道系统的基础,是计算机效率提升的关键
3. 构造一个简单的Linux系统MenuOS
-
Linux内核源代码简介
•arch:支持不同的CPU的源代码,其中的关键目录包括:Documentation、drivers、firewall、fs、include等
•documentation:文档目录
•fs:文件系统
•init:内核启动相关的代码main.c、Makefile等基本都在该目录中。(main.c中的start_ kernel函数是Linux内核启动的起点,即初始化内核的起点)
•kernel:Linux内核核心代码在kernel目录中。
•lib:公用的库文件
•mm:内存管理的代码
•scripts:与脚本相关的代码
•security:与安全相关的代码
•sound目录:与声音相关的代码
•tools目录:与工具相关的代码
•net:与网络相关的代码
•readme:介绍了什么是Linux,Linux能够在哪些硬件上运行,如何安装内核源代码等
•……
-
构造一个简单的Linux系统
cd LinuxKernel/ qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
-
- qemu命令是模拟内核启动虚拟机,启动Linux内核需要三个参数(kernel、initrd、root所在的分区和目录),执行的第一个文件是init。
- -kernel指明内核文件名
- -initrd指明根文件系统,启动其中的init文件。(menuOS源代码编译->init->rootfs.img)其中rootfs.img 为根文件系统,目前只支持help、version、quit功能。
- 启动过程为:启动内核->启动init->启动进程
-
跟踪调试Linux内核的启动过程
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
4. 扒开系统调用的三层皮(上)
(一)用户态、内核态和中断处理过程
- 内核态:一般现代CPU有几种指令执行级别。在高执行级别下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别对应着内核态
- 用户态:在相应的低级别执行状态下,代码的掌控范围有限,只能在对应级别允许的范围内活动
- 中断处理是从用户态进入内核态的主要方式,中断/int指令会在堆栈上保存一些寄存器的值:如用户态栈顶地址、当前的状态字、当时cs:eip的值(当前中断程序的入口)
(二)系统调用概述
- 系统调用是操作系统为用户态进程与硬件设备进行交互提供的一组接口
- 系统调用概述和系统调用的三层皮:xyz(API)、system_ call(中断向量)、sys_xyz(中断向量对应的中断服务程序)
(三)使用库函数API和C代码中嵌入汇编代码触发同一个系统调用
- 使用库函数API获取系统当前时间
- C代码中嵌入汇编代码的方法
- 使用C代码中嵌入汇编代码触发系统调用获取系统当前时间
5. 扒开应用系统的三层皮(下)
(一)给MenuOS增加time和time-asm命令
rm menu -rf //强制删除当前menu git clone http://github.com/mengning/menu.git //重新克隆新版本的menu cd menu ls make rootfs //rootfs是事先写好的一个脚本,自动编译自动生成根文件系统,同时自动启动MenuOS vi test.c //进入test.c文件 MenuConfig("getpid","Show Pid",Getpid); MenuConfig("getpid_asm","Show Pid(asm)",GetpidAsm); //在main函数中增加MenuConfig() int Getpid(int argc,char *argv[]); int GetpidAsm(int argc,char *argv[]); //增加对应的Getpid和GetpidAsm两个函数 make rootfs //编译
(二)使用gdb跟踪系统调用内核函数sys_time
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S gdb (gdb)file linux-3.18.6/vmlinux (gdb)target remote:1234 //连接到需要调试的MenuOS (gdb)b start_kernel //设置断点 (gdb)c //执行,可见程序在start_kernel处停下 list //可查看start_kernel的代码 (gdb)b sys_time //sys_time是13号系统调用对应的内核处理函数,在该函数处设置断点 (gdb)c //如果这里一直按n单步执行,会进入schedule函数。sys_time返回后进入汇编代码处理,gdb无法继续进行追踪 执行int 0x80后执行system call对应的代码(system call不是函数,是一段特殊的汇编代码,gdb还不能进行跟踪)。
(三)系统调用在内核代码中的工作机制和初始化
- 系统调用在内核代码中的工作机制和初始化
- 简化后便于理解的system_call伪代码
- 简单浏览system_call和iret之间的主要代码
6. 进程的描述和进程的创建
(一)进程的描述
进程描述符task_struct数据结构
struct task_struct{ volatile long state; //进程状态,-1表示不可执行,0表示可执行,大于1表示停止 void *stack; //内核堆栈 atomic_t usage; unsigned int flags; //进程标识符 unsigned int ptrace; …… }
(二)进程的创建
- 进程的创建概览及fork一个进程的用户态代码
- 理解进程创建过程复杂代码的方法
- 浏览进程创建过程相关的关键代码
- 创建的新进程是从哪里开始执行的
- 使用gdb跟踪创建新进程的过程
- 道生一(start_ kernel...cpu_ idle),一生二(kernel_ init和kthreadd),二生三(即前面的0、1、2三个进程),三生万物(1号进程是所有用户态进程的祖先,2号进程是所有内核线程的祖先)
- start_ kernel创建了cpu_ idle,也就是0号进程。而0号进程又创建了两个线程,一个是kernel_ init,也就是1号进程,这个进程最终启动了用户态;另一个是kthreadd。0号进程是固定的代码,1号进程是通过复制0号进程PCB之后在此基础上做修改得到的
- iret与int 0x80指令对应,一个是弹出寄存器值,一个是压入寄存器的值
- 如果将系统调用类比于fork();那么就相当于系统调用创建了一个子进程,然后子进程返回之后将在内核态运行,而返回到父进程后仍然在用户态运行
7. 可执行程序的装载
(一)预处理、编译、链接和目标文件的格式
-
可执行程序是怎么得来的
1. C源代码(.c)经过编译器预处理被编译成汇编代码(.asm) 2. 汇编代码由汇编器被编译成目标代码(.o) 3. 将目标代码链接成可执行文件(a.out) 4. 可执行文件由操作系统加载到内存中执行
-
目标文件的格式ELF
目标文件的三种形式: 1. 可重定位文件.o,用来和其他object文件一起创建可执行文件和共享文件 2. 可执行文件,指出应该从哪里开始执行 3. 共享文件,主要是.so文件,用来被链接编辑器和动态链接器链接
-
静态链接的ELF可执行文件和进程的地址空间
可执行文件加载到内存时: 1. 加载效果:将代码段数据加载到内存中,再把数据加载到内存,默认从0x8048000地址开始加载 2. 启动一个刚加载过可执行文件的进程时,可执行文件加载到内存之后执行的第一条代码地址 3. 一般静态链接会将所有代码放在一个代码段,而动态链接的进程会有多个代码段
(二)可执行程序、共享库和动态加载
-
装载可执行程序之前的工作
1. 一般执行一个程序的Shell环境,实验中直接使用execve系统调用 2. Shell本身不限制命令行参数的个数,命令行参数的个数受限于命令自身 3. Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数
-
装载时动态链接和运行时动态链接应用举例
1. 准备.so文件(在Linux下动态链接文件格式,在Windows中是.dll) 2. 编译成.so文件
(三)可执行程序的装载
-
可执行程序的装载相关关键问题分析
1. execve与fork是比较特殊的系统调用: • execve用它加载的可执行文件把当前的进程覆盖掉,返回之后就不是原来的程序而是新的可执行程序起点; • fork函数的返回点ret_ from_fork是用户态起点 2. sys_ execve内核处理过程: • do_ execve -> do_ execve_ common -> exec_ binprm -> search_ binary_handler,最后根据文件头部信息寻找对应的文件格式处理模块
-
sys_execve的内部处理过程
exec一般和fork调用,常规用法是fork出一个子进程,然后在子进程中执行exec,替换为新的代码。
-
使用gdb跟踪sys_execve内核函数的处理过程
1. 开始先更新内核,再用test_exec.c将test.c覆盖掉 2. test.c文件中增加了exec系统调用,Makefile文件中增加了gcc -o hello hello.c -m32 -static 3. 启动内核并验证execv函数 4. 启动gdb调试 5. 先停在sys_execve处,再设置其它断点 6. 进入函数单步执行
-
可执行程序的装载与庄生梦蝶的故事
庄周(调用execve的可执行程序)入睡(调用execve陷入内核),醒来(系统调用execve返回用户态)发现自己是蝴蝶(被execve加载的可执行程序)
-
浅析动态链接的可执行程序的装载
8. 进程的切换和系统的一般执行过程
(一)进程切换的关键代码switch_to分析
1. 进程进度与进程调度的时机分析
schedule()函数实现调度:
- 中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule()
- 内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度
- 用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度
2. 进程上下文切换相关代码分析
- 为了控制进程的执行,内核必须有能力挂起正在CPU上执行的进程,并恢复以前挂起的某个进程的执行,这叫做进程切换、任务切换、上下文切换
- 挂起正在CPU上执行的进程,与中断时保存现场是不同的,中断前后是在同一个进程上下文中,只是由用户态转向内核态执行
- 进程上下文包含了进程执行需要的所有信息
- schedule()函数选择一个新的进程来运行,并调用context_ switch进行上下文的切换,这个宏调用switch_ to来进行关键上下文切换
(二)Linux系统的一般执行过程
1. Linux系统的一般执行过程分析
最一般的情况:正在运行的用户态进程X切换到运行用户态进程Y的过程: 1. 正在运行的用户态进程X 2. 发生中断——save cs:eip/esp/eflags(current) to kernel stack,then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack). 3. SAVE_ALL //保存现场 4. 中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换 5. 标号1之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过因此可以从标号1继续执行) 6. restore_all //恢复现场 7. iret - pop cs:eip/ss:esp/eflags from kernel stack 8.继续运行用户态进程Y
2. Linux系统执行过程中的几个特殊情况
1. 通过中断处理过程中的调度时机,用户态进程与内核线程之间互相切换和内核线程之间互相切换,与最一般的情况非常类似,只是内核线程运行过程中发生中断没有进程用户态和内核态的转换 2. 内核线程主动调用schedule(),只有进程上下文的切换,没有发生中断上下文的切换,与最一般的情况略简略 3. 创建子进程的系统调用在子进程中的执行起点及返回用户态,如fork 4. 加载一个新的可执行程序后返回到用户态的情况,如execve
3. 内核与舞女
1. 进程的地址空间一共有4G,其中0——3G是用户态可以访问,3G以上只有内核态可以访问 2. 内核相当于出租车,可以为每一个“招手”的进程提供内核态到用户态的转换。 3. 没有进程需要“承载”的时候,内核进入idle0号进程进行“空转”。当用户进程有需求时,内核发生中断,帮助用户进程完成请求,然后再返回到用户进程。就好像Taxi将用户载了一圈之后又把用户放下来。 4. 3G以上的部分就是这样的“出租车”,是所有进程共享的,在内核态部分切换的时候就比较容易 5. 内核是各种中断处理程序和内核线程的集合
(三)Linux系统架构和执行过程概览
1. Linux操作系统架构概览
2. 最简单也是最复杂的操作——执行ls操作
3. 从CPU和内存的角度看Linux系统的执行
三、实验总结
在这几周的实验中我遇到的问题一共有三个:一是clone新版本的menu时显示连接超时,解决方法为在实验楼环境中点击“停止实验”然后再重新进入输入命令;二是编译命令make rootfs结果显示失败,原因是该路径下原有的rootfs文件还存在,解决方法为删除原rootfs文件再执行编译指令;三是偶尔target remote:1234这个命令无法执行,显示连接超时,解决方法为重新执行qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S 指令。
四、学习总结
通过八周的视频学习和教材学习,我对于Linux内核有了初步的理解。在视频中老师为我们讲解了部分核心源码,在课后作业里也有尝试着自己分析进程创建过程相关的关键代码、可执行程序的装载、进程上下文切换相关代码、Linux系统架构和执行过程等,虽举步维艰,但也收获颇丰。Linux内核主要包括进程管理、内存管理、设备驱动、文件系统,从分析内核到了解整个系统是如何工作的、如何控制管理资源分配、进程切换并执行、各种策略和结构让系统运行时更有效率等,在日渐深入的学习中我愈发认识到内核源码数据结构和算法的精妙之处。不过我也认识到自身的许多不足,在平时的学习中有一些内容仅仅是浅尝辄止而没有来得及细嚼慢咽,掌握的不够全面。而且在自己虚拟机环境中动手实践的效果不理想。在日后的学习中我会定时查漏补缺,争取有朝一日能够完全领会内核源码的精妙之处并能吸收借鉴用于其它方面。