课程地址:
http://mooc.study.163.com/course/USTC-1000029000#/info
学习思路
孟宁老师这门课并没有完整的分析Linux内核中代码,而是针对关键部分进行了讲解分析,个人认为内核代码也是存在二八定律的情况,少部分关键代码经常被使用,而理解这部分代码对我们认识操作系统的真实工作细节和建立操作系统工作的流程框架有很好的帮助。
总体来说,整门课程的内容可以分为三个部分:
(1)内核分析所需要的知识基础
(2)系统调用的原理和实现
(3)进程管理
第一部分对X86汇编,函数调用堆栈,存储计算机工作原理等进行了讲解,第二部分主要针对系统调用的实现,包括用户态与内核态,中断上下文的切换等进行了讲解,第三部分主要是针对进程来讲解,进程是操作系统中最重要的抽象,因为他是硬件资源分配的基本单位,其他的抽象都是围绕他来进行的,这部分针对进程的创建,进程的执行环境,进程的切换进行了讲解。
笔记和课件
(1)知识基础
X86汇编
这门课程中主要涉及的硬件就是CPU,选择的CPU指令集是X86汇编,学习的重点是进程管理部分,对内存管理,文件管理比较忽略,相关的硬件细节也被忽略。
学习X86指令集的关键事实上在于以下几个部分:
计算机工作的基础
计算机是如何工作的?(总结)——三个法宝
1.存储程序计算机工作模型,计算机系统最最基础性的逻辑结构;
2.函数调用堆栈,高级语言得以运行的基础,只有机器语言和汇编语言的时候堆栈机制对于计算机来说并不那么重要,但有了高级语言及函数,堆栈成为了计算机的基础功能;
enter
pushl %ebp
movl %esp,%ebp
leave
movl %ebp,%esp
popl %ebp
函数参数传递机制和局部变量存储
3.中断,多道程序操作系统的基点,没有中断机制程序只能从头一直运行结束才有可能开始运行其他程序。
C语言函数调用堆栈框架
1.堆栈是C语言运行时必须的一个记录调用路径和参数的空间
2.相关寄存器和堆栈操作
SP BP PUSH POP
3.函数调用和返回
CALL RET
4.函数堆栈框架
(2)系统调用
操作系统提供的抽象的本质:
操作系统提供了进程,地址空间,文件三个抽象,本质上是对硬件的虚拟,使得硬件由物理上的一个变为逻辑上的多个,利用的就是共享(时间或者空间)。
系统的调用的本质:
系统调用可以看做特殊的函数调用,所以每次进行系统调用都要新建堆栈。
(3)进程管理
文件和地址空间事实上都是依赖进程这个抽象,进程通过系统调用来控制其他的资源。
进程的描述
进程的创建
创建一个新进程在内核中的执行过程
fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建;
Linux通过复制父进程来创建一个新进程,那么这就给我们理解这一个过程提供一个想象的框架:
复制一个PCB——task_struct
err = arch_dup_task_struct(tsk, orig);
要给新进程分配一个新的内核堆栈
ti = alloc_thread_info_node(tsk, node);
tsk->stack = ti;
setup_thread_stack(tsk, orig); //这里只是复制thread_info,而非复制内核堆栈
要修改复制过来的进程数据,比如pid、进程链表等等都要改改吧,见copy_process内部。
从用户态的代码看fork();函数返回了两次,即在父子进程中各返回一次,父进程从系统调用中返回比较容易理解,子进程从系统调用中返回,那它在系统调用处理过程中的哪里开始执行的呢?这就涉及子进程的内核堆栈数据状态和task_struct中thread记录的sp和ip的一致性问题,这是在哪里设定的?copy_thread in copy_process
*childregs = *current_pt_regs(); //复制内核堆栈
childregs->ax = 0; //为什么子进程的fork返回0,这里就是原因!
p->thread.sp = (unsigned long) childregs; //调度到子进程时的内核栈顶
p->thread.ip = (unsigned long) ret_from_fork; //调度到子进程时的第一条指令地址
进程的装载
程序生成可执行文件的过程
可执行文件装载生成进程,并进行静态与动态链接库文件的过程
详细过程略,主要参考书 《程序员的自我修养》
进程的切换和系统的一般执行过程
进程的调度时机与进程的切换
操作系统原理中介绍了大量进程调度算法,这些算法从实现的角度看仅仅是从运行队列中选择一个新进程,选择的过程中运用了不同的策略而已。
对于理解操作系统的工作机制,反而是进程的调度时机与进程的切换机制更为关键。
进程调度的时机
进程的切换
define switch_to(prev, next, last) \
32do { \
33 /* \
34 * Context-switching clobbers all registers, so we clobber \
35 * them explicitly, via unused output variables. \
36 * (EAX and EBP is not listed because EBP is saved/restored \
37 * explicitly for wchan access and EAX is the return value of \
38 * __switch_to()) \
39 */ \
40 unsigned long ebx, ecx, edx, esi, edi; \
41 \
42 asm volatile("pushfl\n\t" /* save flags */ \
43 "pushl %%ebp\n\t" /* save EBP */ \
44 "movl %%esp,%[prev_sp]\n\t" /* save ESP */ \
45 "movl %[next_sp],%%esp\n\t" /* restore ESP */ \
46 "movl $1f,%[prev_ip]\n\t" /* save EIP */ \
47 "pushl %[next_ip]\n\t" /* restore EIP */ \
48 __switch_canary \
49 "jmp __switch_to\n" /* regparm call */ \
50 "1:\t" \
51 "popl %%ebp\n\t" /* restore EBP */ \
52 "popfl\n" /* restore flags */ \
53 \
54 /* output parameters */ \
55 : [prev_sp] "=m" (prev->thread.sp), \
56 [prev_ip] "=m" (prev->thread.ip), \
57 "=a" (last), \
58 \
59 /* clobbered output registers: */ \
60 "=b" (ebx), "=c" (ecx), "=d" (edx), \
61 "=S" (esi), "=D" (edi) \
62 \
63 __switch_canary_oparam \
64 \
65 /* input parameters: */ \
66 : [next_sp] "m" (next->thread.sp), \
67 [next_ip] "m" (next->thread.ip), \
68 \
69 /* regparm parameters for __switch_to(): */ \
70 [prev] "a" (prev), \
71 [next] "d" (next) \
72 \
73 __switch_canary_iparam \
74 \
75 : /* reloaded segment registers */ \
76 "memory"); \
77} while (0)
Linux系统的一般执行过程
最一般的情况:正在运行的用户态进程X切换到运行用户态进程Y的过程
几种特殊情况