Linux学习总结
钟晶晶+ 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
博客目录
- 实验一:计算机是如何工作的?
- 实验二:一个简单的时间片轮转多道程序内核代码分析
- 实验三:跟踪分析Linux内核启动过程
- 实验四:汇编代码调用系统调用的工作过程
- 实验五:分析system-call中断处理过程
- 实验六:fork函数对应的系统调用处理过程
- 实验七:Linux内核如何装载和启动一个可执行程序
- 实验八:理解进程调度时机跟踪分析进程调度与进程切换的过程
课程总结
1.计算机是如何工作的?
首先,对于单任务计算机来说,不存在中断。程序是运行在线性地址空间当中。每个函数的执行都有自己相应的栈空间。%ebp栈基址寄存器中保存的是当前执行函数的栈空间的基地址,%esp栈顶指针寄存器中保存的是当前的栈的栈顶。当程序执行时,基本原则是从上到下顺序的执行当前执行函数中的每一条指令,当存在函数调用时,主要是通过call指令。call指令会首先保存当前执行函数中的call指令下一条指令的地址,即当前%eip寄存器中的值,然后将所要调用的函数的地址保存到%eip当中,作为实际上将要执行的下一条指令,即为所要跳转到的函数的第一条指令的地址。
其次,多任务计算机的理念也应该大体相同,即多任务中的每一个任务的执行过程与原理,和单任务计算机中的一个任务的执行过程应该是相同的,但是关键在于同时有多个进程在运行,各个进程之间,如果一个进程在执行,另一个进程忽然之间打断了它的执行应该怎么处理?只要当由一个进程切换到另外一个进程执行时,我们可以先将第一个进程的硬件上下文,包括各个寄存器的值,进程所占栈空间的状态等信息保存起来,以保证在由第二个进程恢复到第一个进程时,能够正确的恢复第一个进程的各个寄存器的值以及对应的栈空间的状态。然后切换到第二个进程中去执行,当第二个进程执行结束之后,会恢复栈空间和相应寄存器到进程切换之前时,第一个进程执行所处的状态,然后再从第一个进程被打断处继续执行。
2.操作系统是如何工作的?
- 将当前的用户态堆栈的esp. ebp指针保存在当前进程内核栈。
- 执行save_all, 保存进程a 的各个寄存器的值到其内核中。
- 进入中断处理程序
- 操作系统调用schedule()函数来进行调度,进入进程b 的内核栈;
- 执行RESTALL_ALL,恢复现场,恢复进程b 的寄存器的值;
- 执行IRET,恢复EIP,ESP以及EFLAGS寄存器;
- 系统从内核态返回用户态
3.分析start_kernal函数
start_kernel()类似C程序中的main函数。在start_kernel()函数之前,内核的代码都是用汇编写的,主要工作是完成一些最基本的初始化与环境设置工作;在start_kernel()中Linux将完成整个系统的内核初始化,在start_kernel的最后,是调用rest_init函数,在rest_init函数中,内核将产生第一个真正的进程,即pid=1的1号进程,而在start_kernel函数中init_task是静态制造出来的,pid=0,我们可以在start_kernel函数的开始处,看到其被初始化的代码,它试图将从最早的汇编代码一直到start_kernel的执行都纳入到init_task进程上下文中,在其初始化工作完成后,就会成为系统的idle进程。事实上在更早前的sched_init函数中,通过init_idle(current, smp_processor_id())函数的调用就已经把init_task初始化成了一个idle task,init_idle函数的第一个参数current就是&init_task,在init_idle中将会把init_task加入到cpu的运行队列中,这样当运行队列中没有别的就绪进程时,init_task(也就是idle task)将会被调用,它的核心是一个while(1)循环,在循环中它将会调用schedule函数以便在运行队列中有新进程加入时切换到该新进程上
4.系统调用三层皮
分别是:API、中断向量对应的system_call、中断服务程序sys_xyz
当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数,在Linux中是通过执行int $0x80来执行系统调用的,这条汇编指令产生向量为128的编程异常。(Intel Pentium II中引入了sysenter指令(快速系统调用),2.6已经支持)内核实现了很多不同的系统调用,进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数,使用eax寄存器(系统调用号将xyz()和sys_xyz()关联起来了)
5.分析system-call中断处理过程
通过系统调用号查找系统调用表sys_call_table,软中断指令INT 0x80执行时,系统调用号会被放入 eax 寄存器中,system_call函数可以读取eax寄存器获取,然后将其乘以4,生成偏移地址,然后以sys_call_table为基址,基址加上偏移地址,就可以得到具体的系统调用服务例程的地址了!然后就到了系统调用服务例程了。需要说明的是,系统调用服务例程只会从堆栈里获取参数,所以在system_call执行前,会先将参数存放在寄存器中,system_call执行时会首先将这些寄存器压入堆栈。system_call退出后,用户可以从寄存器中获得(被修改过的)参数。
6.fork函数对应的系统调用处理过程
在start_kernel中,创建了0号进程,0号进程创建了1号进程,1号进程是所有用户态进程的祖先,0号进程是所有内核进程的祖先。之后其他进程都通过clone(),fork(),vfork()创建,是0号或1号的子孙进程。创建新进程通过复制父进程实现,先复制task_struct,再为其分配一个内核栈,然后修改需要改动的地方,比如进程pid,进程链表等等。创建好之后,会跳转到ret_from_fork,在ret_from_fork中会jmp到syscall_exit,此时父进程的内核栈中保存了父进程在执行fork()前的上下文。子进程的内核栈中也从父进程那里拷贝得到了自己的上下文。所以它们现在只有被调度到,都可以正常的执行。接下来可能发生进程调度,如果子进程得到CPU,就可以正常执行,父进程得到CPU也可以正常执行。
***新进程从ret_from_fork开始执行;决定了新进程的第一条指令地址。***
7. 新的可执行程序是从哪里开始执行的?
当execve()系统调用终止且进程重新恢复它在用户态执行时,执行上下文被大幅度改变,要执行的新程序已被映射到进程空间,从elf头中的程序入口点开始执行新程序。
如果这个新程序是静态链接的,那么这个程序就可以独立运行,elf头中的这个入口地址就是本程序的入口地址。
如果这个新程序是动态链接的,那么此时还需要装载共享库,elf头中的这个入口地址是动态链接器ld的入口地址。
8.Linux系统的一般执行过程
- 最一般的情况:正在运行的用户态进程X切换到运行用户态进程Y的过程
-
正在运行的用户态进程X
-
发生中断——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).
-
SAVE_ALL //保存现场
-
中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换
-
标号1之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过因此可以从标号1继续执行)
- restore_all //恢复现场
-
iret - pop cs:eip/ss:esp/eflags from kernel stack
-
继续运行用户态进程Y
- 通过中断处理过程中的调度时机,用户态进程与内核线程之间互相切换和内核线程之间互相切换
内核线程主动调用schedule(),只有进程上下文的切换,没有发生中断上下文的切换,与最一般的情况略简略;
创建子进程的系统调用在子进程中的执行起点及返回用户态,如fork;
加载一个新的可执行程序后返回到用户态的情况,如execve;
学习心得
通过学习这门课,不仅让我加深了对操作系统的进一步理解,接触到了linux底层代码;更让我学习到了一边学习一边总结的学习方法,写了我人生第一篇博客;让我更爱总结,更爱分享。
当然,通过短短两个月的学习linux,时间是肯定不够的,不过这门课给我打开了Linux的大门,对于我来说,linux才刚刚开始。
浙公网安备 33010602011771号