Linux内核分析期末总结
注:作者:臧文君,原创作品转载请注明出处,《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
一、计算机是如何工作的
1、参考链接:http://www.cnblogs.com/CatherineZang/p/5218570.html
2、编译语句:gcc -S -o main.s main.c -m32
3、“三个法宝”
(1)存储程序计算机工作模型,计算机系统最最基础性的逻辑结构;
(2)函数调用堆栈,高级语言得以运行的基础,只有机器语言和汇编语言的时候堆栈机制对于计算机来说并不那么重要,但有了高级语言及函数,堆栈成为了计算机的基础功能;
(3)中断,多道程序操作系统的基点,没有中断机制程序只能从头一直运行结束才有可能开始运行其他程序。
4、堆栈主要存储三种数据:eip(上一函数的返回地址),ebp(上一函数的基地址),局部变量。
二、操作系统是如何工作的
1、参考链接:http://www.cnblogs.com/CatherineZang/p/5245969.html
2、使用gcc -g命令生成test.c的可执行文件test,
使用objdump -S命令获得test的反汇编代码。
3、操作系统的“两把剑”:中断上下文和进程上下文的切换。
4、操作系统核心功能:进程调度和中断机制,通过与硬件的配合实现多任务处理,再加上上层应用软件的支持,最终变成可以使用户可以很容易操作的计算机系统。
5、多进程的Linux操作系统:进程必须等到正在运行的进程空闲CPU后才能运行。
6、进程切换:当正在运行的进程等待其他的系统资源时,Linux内核将取得CPU的控制权,并将CPU分配给其他正在等待的进程。进程切换机制中包含esp的切换、堆栈的切换。
三、构造一个简单的Linux系统MenuOS
1、参考链接:http://www.cnblogs.com/CatherineZang/p/5270066.html
2、使用自己的Linux系统环境搭建MenuOS的过程
# 下载内核源代码编译内核
cd ~/LinuxKernel/
wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.18.6.tar.xz
xz -d linux-3.18.6.tar.xz
tar -xvf linux-3.18.6.tar(解压)
cd linux-3.18.6
make i386_defconfig
make # 一般要编译很长时间,少则20分钟多则数小时
# 制作根文件系统
cd ~/LinuxKernel/
mkdir rootfs
git clone https://github.com/mengning/menu.git # 如果被墙,可以使用附件menu.zip
cd menu
gcc -o init linktable.c menu.c test.c -m32 -static –lpthread(init是第一个用户态进程,是1号进程,采用的是静态编译的方式)
cd ../rootfs
cp ../menu/init ./
find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img(封装到img镜像文件)
# 启动MenuOS系统
cd ~/LinuxKernel/
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
3、重新配置编译Linux使之携带调试信息
(1)在原来配置的基础上,make menuconfig选中如下选项重新配置Linux,使之携带调试信息
kernel hacking—>
[*] compile the kernel with debug info
(2)make重新编译(时间较长)
4、使用gdb跟踪调试内核
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S # 关于-s和-S选项的说明:
# -S freeze CPU at startup (use ’c’ to start execution)
# -s shorthand for -gdb tcp::1234 若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项
另开一个shell窗口
gdb
(gdb)file linux-3.18.6/vmlinux # 在gdb界面中target remote之前加载符号表 file home/shiyanlou/LinuxKernel/vmlinux
(gdb)target remote:1234 # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行
(gdb)break start_kernel # 断点的设置可以在target remote之前,也可以在之后
四、扒开系统调用的三层皮(上)
1、参考链接:http://www.cnblogs.com/CatherineZang/p/5281675.html
2、系统调用的工作机制是:当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数,由API、中断向量和中断处理程序协调完成。
3、在系统调用的过程中,系统调用号使用eax寄存器传递参数。寄存器在传递参数时也有如下限制:
1)每个参数的长度不能超过寄存器的长度,即32位。
2)在系统调用号(eax)之外,参数的个数不能超过6个(ebx,ecx,edx,esi,edi,ebp),若超过6个,就将某一个寄存器作为指针,指向一块内存,在进入内核态之后,可以访问所有的地址空间,通过内存来传递参数。
4、权限级别:0--内核态,3--用户态。
5、0xc0000000以上的地址空间只能在内核态下访问,0x00000000-0xbfffffff的地址空间在两种状态下都可以访问。
6、中断处理的完整过程
保存:将当前的cs:eip、ss:esp、eflags保存到内核堆栈中。
加载:把当前的中断信号或系统调用相关联的中断处理程序的入口加载到cs:eip中,同时将当前的堆栈段ss和esp也加载到CPU中。
7、系统调用的三层皮:API,中断向量对应的中断服务程序,系统调用服务程序。
例:xyz、system_call和sys_xyz。
8、系统调用号将xyz和sys_xyz关联起来了。
五、扒开系统调用的三层皮(下)
1、参考链接:http://www.cnblogs.com/CatherineZang/p/5327039.html
2、系统调用在内核代码中的工作机制和初始化
(1)xyz()和sys_xyz()通过系统调用号匹配起来,int 0x80和system_call通过中断向量匹配起来。
(2)系统调用机制的初始化:trap_init();
3、在系统调用时,我们需要保存现场:SAVE_ALL,用于保存系统调用时的上下文。接着,根据eax传入的系统调用号,通过sys_call_table查询到调用的系统调用,然后跳转相应的系统调用处理函数后,进行处理。最后,调用restore_all恢复系统调用时的现场,并用iret返回用户态。
六、进程的描述和进程的创建
1、参考链接:http://www.cnblogs.com/CatherineZang/p/5343121.html
2、操作系统的三大功能:进程管理,内存管理和文件系统。
3、进程描述符提供了内核所需了解的进程信息。
task_struct中包含:进程状态,进程打开的文件,进程优先级信息。
4、fork()是用户态用于创建一个子进程的系统调用。
fork()系统调用在父进程和子进程各返回一次,在子进程中,pid的返回值为0,执行else的代码,在父进程中,pid的返回值为子进程的ID,执行else if的代码。
5、Linux系统创建一个新进程主要依靠fork(),fork()是用户态用于创建一个子进程的系统调用,再由fork()调用do_fork来实现进程的创建。
先复制一个PCB——task_struct,再给新进程分配一个新的内核堆栈,并且修改复制过来的进程数据,比如pid、进程链表等等。通过以上步骤,就可以创建一个新进程。
6、新进程的起点是ret_from_fork,通过*childregs = *current_pt_regs()语句复制内核堆栈。
在实验的过程中,会遇到gdb的问题,显示连接超时,此时需要重启gdb即可。另外,调试内核时还需要注意路径的问题。若没有给出路径,则是在当前目录下查找文件。新进程的起点是ret_from_fork,通过*childregs = *current_pt_regs()语句复制内核堆栈。
在实验的过程中,会遇到gdb的问题,显示连接超时,此时需要重启gdb即可。另外,调试内核时还需要注意路径的问题。若没有给出路径,则是在当前目录下查找文件。
七、可执行程序的装载
1、参考链接:http://www.cnblogs.com/CatherineZang/p/5361862.html
2、可执行程序是怎么来的?
例:C语言代码-->编译器预处理-->编译成汇编代码-->汇编器编译成目标代码-->链接成可执行文件,再由操作系统加载到内存中执行。
3、ELF中的三种目标文件:
可重定位文件,可执行文件,可共享文件。
4、静态链接时,ELF可执行文件加载到内存时默认地址是0x8048000。
5、命令行参数和环境变量是如何进入新程序的堆栈的?
Shell程序-->execve-->sys_execve,然后在初始化新程序堆栈时拷贝进去。
先函数调用参数传递,再系统调用参数传递。
6、动态链接分为可执行程序装载时动态链接和运行时动态链接。
7、当前程序执行到execve系统调用时陷入内核态,在内核中用execve加载可执行文件,把当前进程的可执行文件覆盖掉,execve系统调用返回到新的可执行程序的起点。
8、动态链接库的装载过程是一个图的遍历。
ELF格式中的.interp和.dynamic需要依赖动态链接器来解析,entry返回到用户态时不是返回到可执行程序规定的起点,返回到动态链接器的入口。
八、进程的切换和系统的一般执行过程
1、参考链接:http://www.cnblogs.com/CatherineZang/p/5386216.html
2、Linux系统的一般执行过程:首先,在控制台下输入命令;然后由shell程序分析输入的参数;接着,调用系统调用fork,再调用exec系统调用将输入的命令的可执行文件装入内存;最后,从系统调用返回。
3、在调试过程中,由schedule函数调用_schedule函数,再调用了pick_next_ task函数和context_switch函数,根据单步执行的过程可以分析出,pick_ next_ task函数首先指定了调度策略,context_ switch函数进行进程上下文切换。
4、Linux既支持普通的分时进程,也支持实时进程。
5、Linux中的调度是多种调度策略和调度算法的混合。
Linux的调度基于分时和优先级。
6、用户态进程只能被动调度
用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。
7、内核线程是只有内核态没有用户态的特殊进程
内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度。
8、schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换,这个宏调用switch_to来进行关键上下文切换。
9、Linux系统的一般执行过程分析
最一般的情况:正在运行的用户态进程X切换到运行用户态进程Y的过程。
关键:中断上下文的切换和进程上下文的切换。
10、X86_32位系统下,每个进程的地址空间4G。0-3G用户态,3G-4G仅内核态。
所有的进程3G以上的部分是共享的。
内核是各种中断处理过程和内核线程的集合。
总结:
1、收获
在Linux内核分析这门课的学习过程中,我对Linux系统有了初步的了解。因为是第一次接触Linux,所以理解起来有点吃力,不过还是尽力去理解。在学习Linux系统内核的相关结构和设计原理的同时,也学会了在Linux系统内的相关操作,印象最深刻的是gdb调试内核代码的方法。
另外,在学生互评的环节中我也学习了其他人的博客,对知识有了很好的梳理和巩固,可以更全面的理解所学的知识。
2、遗憾
这八周的MOOC学习下来,我觉得只是浅浅的了解了Linux内核的相关知识,在脑海里形成了大致的框架。但是在实际的操作应用层面上,自己还很缺乏。在学习的过程中,大多数实验只能根据老师的指导来进行,对于内核中其他的东西还不是很了解。在这门课结束之后,还有许多东西要去学习。
最后,感谢孟老师这八周来的辛苦教学!
附课本学习:
第一、二章 http://www.cnblogs.com/CatherineZang/p/5286409.html
第三章 http://www.cnblogs.com/CatherineZang/p/5339608.html
第四章 http://www.cnblogs.com/CatherineZang/p/5386274.html
第五章 http://www.cnblogs.com/CatherineZang/p/5286462.html