一、一个精简的Linux系统概念模型:
从逻辑上理顺linux运行的全过程:
0. 当我们按下开机键,计算机加电启动,cpu开始执行bios(从某个约定好的地址上取指令执行),BIOS完成自检等一系列操作,然后调用BootLoader,BootLoader负责把linux内核加载到内存,(启动层次:BIOS -> BootLoader -> Linux kernel)
1. 然后cpu跳转到head.S的_start处执行,(在start_kernel前执行的汇编代码),执行路径大致为:_start -> start_of_setup -> boot/main.c(在这里由实模式切换到保护模式) -> vmlinux -> head_32.S/startup_32(在这里开启分页机制,初始化IDT/GDT表) -> i386_start_kernel(arch/x86/kernel/head32.c中的 i386_start_kernel 只有一条语句 start_kernel(),将跳转到与体系结构无关的 init/main.c 下的 line 576: start_kernel() ) -> start_kernel
2. 从stat_kernel开始,我们就跳出了与体系结构相关的部分,离开了复杂的汇编代码,开始执行c代码,在start_kernel里,创建了0号进程init_task,并调用了一系列的初始化函数,在start_kernel的结尾处调用了rest_init(),rest_init里通过kernel_thread创建了1号进程kernel_init和2号进程kthreadd,kernel_init最终把用户态的init进程给启动起来,init进程是所有用户进程的祖先。然后调用cpu_idle使cpu进入低功耗空闲状态,等待调度。
3. 到这里linux内核就启动起来了,接着就是上一次实验的linux系统的一般执行过程。比如最初用户态通过fork系统调用创建一个进程,它复制了其父进程(init进程)的大部分内容,然后通过execve加载自己的可执行程序,当execve返回时开始执行新的进程。以后不断通过fork父进程创建更多的进程。
4. 现在我们有了很多的进程,那进程之间不可能是顺序执行的,等你执行完我再执行。假设系统是基于时间片轮转调度的,而且当前进程X正在运行,那一般有三种情况会改变当前进程的执行路径,一是发生中断,二是发生系统调用,三是该进程的时间片用完发生进程调度。第一种情况是外部事件打断了当前进程,第二种情况是进程本身主动陷入内核,请求内核的服务,这两种情况在上一次实验都有讨论,我们现在发生的是第三种情况。
5. 在内核初始化的时候会初始化2个时钟源:中断时钟源和计时时钟源,中断时钟源周期性的产生时钟中断,每产生一次时钟中断就会进入时钟中断的中断服务例程timer_interrupt,而在timer_interrupt里会将当前进程的时间片减一,当减到0时,调用schedule()函数完成进程调度,进程调度包括根据调度规则挑选下一个进程Y,然后完成进程切换,开始执行进程Y。然后进程Y重复这个过程,1)不断从代码段取指执行直到结束,2)或是外部设备产生中断然后交给内核去处理,在中断返回到用户态时可能发生进程调度,3)或是主动发出系统调用然后交给内核去处理,系统调用返回到用户态时可能发生进程调度,4)或是进程的时间片用完发生进程调度,5)或是进程状态发生变化如:进程执行完毕调用do_exit()变成僵死态或运行态进程等待某种资源,也会发生进程调度。
6. 如果CPU无需执行任何程序,并且也没有任何中断、异常信号发过来,CPU便会进入cpu idle状态以降低功耗。否则就按照4和5不断的发生进程切换和中断上下文切换,一直执行下去直到用户关闭计算机。
二、以读写文件为例加入设备驱动程序并验证模型:
1. 进程X读写一个文件首先要使用open系统调用打开这个文件(对应上面模型第5步的第3)小步,可以将其加入到模型中),根据给出的文件路径我们可以知道它在哪个存储设备上(FAT32格式的u盘、NTFS格式的磁盘等等),可以找到该设备对应的设备文件的文件控制块inode,inode里有对应的设备号,根据设备号可以找到对应的驱动程序。在内核初始化时已经将各个设备的设备驱动程序注册到内核,并为他们生成对应的设备文件,设备文件向上提供统一的接口如open()、read()等,方便我们调用。而open系统调用会新建一个file结构,并在进程X的进程打开文件表的fd数组里找到空闲的一项,让他指向刚创建的file结构,然后返回fd数组的下标。假如我们要访问的文件存储在FAT32格式的块设备上,程序员写的驱动程序包括具体的针对FAT32的读写打开关闭等函数,他们已经被注册到内核,我们通过设备号就能找到对应的驱动程序,然后用我们写的具体的操作函数来初始化设备文件的文件控制块inode节点里的cdev,再用inode节点的cdev初始化系统文件打开表file结构里的file_operations。以上都是open系统调用内核干的事,直观上来说就是内核向用户态返回了一个整数(fd数组的下标)。
2. 进程X使用read系统调用读这个文件(也对应上面模型第5步的第3)小步,可以将其加入到模型中),就会根据参数:fd数组的下标 找到fd数组的对应项,进而找到指向第1步创建的file结构的指针,进而找到这个file结构,进而找到file结构里的file_operations里的具体的read函数来读取文件,这是一个从抽象到具体的过程。最后系统调用返回告诉用户态进程X读操作成功与否。
3. 进程X使用write系统调用写数据到这个文件(也对应上面模型第5步的第3)小步,可以将其加入到模型中),就会根据参数:fd数组的下标 找到fd数组的对应项,进而找到指向第1步创建的file结构的指针,进而找到这个file结构,进而找到file结构里的file_operations里的具体的write函数来写文件,这也是一个从抽象到具体的过程。最后系统调用返回告诉用户态进程X写操作成功与否。
三、对课程的心得体会,改进建议:
通过本次课程既从宏观上了解了linux系统的一般执行过程,然后又具体到每一个细节,比如中断和异常的具体处理过程,定时器的初始化和执行,进程的管理,linux的文件系统等。首先在心中有了框架,然后又深入了解了它们的具体实现过程,从而在宏观和微观上对linux系统有了系统的认知。
对课程的意见是:信号机制,进程的同步和通信等内容如果能加上整个知识网络会更加完善;如果能把各个模块整合起来融会贯通,更加强调它们之间的联系会更有利于理解。