《Linux内核分析》笔记与课件整理
课程地址:
http://mooc.study.163.com/course/USTC-1000029000#/info
学习思路
孟宁老师这门课并没有完整的分析Linux内核中代码,而是针对关键部分进行了讲解分析,个人认为内核代码也是存在二八定律的情况,少部分关键代码经常被使用,而理解这部分代码对我们认识操作系统的真实工作细节和建立操作系统工作的流程框架有很好的帮助。
总体来说,整门课程的内容可以分为三个部分:
(1)内核分析所需要的知识基础
(2)系统调用的原理和实现
(3)进程管理
第一部分对X86汇编,函数调用堆栈,存储计算机工作原理等进行了讲解,第二部分主要针对系统调用的实现,包括用户态与内核态,中断上下文的切换等进行了讲解,第三部分主要是针对进程来讲解,进程是操作系统中最重要的抽象,因为他是硬件资源分配的基本单位,其他的抽象都是围绕他来进行的,这部分针对进程的创建,进程的执行环境,进程的切换进行了讲解。
笔记和课件
(1)知识基础
X86汇编
这门课程中主要涉及的硬件就是CPU,选择的CPU指令集是X86汇编,学习的重点是进程管理部分,对内存管理,文件管理比较忽略,相关的硬件细节也被忽略。
学习X86指令集的关键事实上在于以下几个部分:
- 记忆理解X86的CPU寄存器
- 程序的逻辑分段(代码段,堆栈段,数据段)
- 函数调用堆栈
计算机工作的基础
计算机是如何工作的?(总结)——三个法宝
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)进程管理
文件和地址空间事实上都是依赖进程这个抽象,进程通过系统调用来控制其他的资源。
进程的描述
- 进程控制块PCB——task_struct
- 进程的标示pid
- 所有进程链表struct list_head tasks;(双向链表)
- 程序创建的进程具有父子关系
- Linux为每个进程分配一个8KB大小的内存区域,用于存放该进程两个不同的数据结构:Thread_info和进程的内核堆栈
进程处于内核态时使用,不同于用户态堆栈,即PCB中指定了内核栈,
内核控制路径所用的堆栈很少,因此对栈和Thread_info来说,8KB足够
进程的创建
创建一个新进程在内核中的执行过程
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; //调度到子进程时的第一条指令地址
进程的装载
程序生成可执行文件的过程
可执行文件装载生成进程,并进行静态与动态链接库文件的过程
详细过程略,主要参考书 《程序员的自我修养》
进程的切换和系统的一般执行过程
进程的调度时机与进程的切换
操作系统原理中介绍了大量进程调度算法,这些算法从实现的角度看仅仅是从运行队列中选择一个新进程,选择的过程中运用了不同的策略而已。
对于理解操作系统的工作机制,反而是进程的调度时机与进程的切换机制更为关键。
进程调度的时机
- 中断处理过程(包括时钟中断、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指向被调度的进程
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的过程
- 正在运行的用户态进程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;
更多推荐
所有评论(0)