古道西风CSDN 2018-11-12
1.1 Linux的诞生与发展
Linux创始人:Linus Toravlds
Linux第一版发布时间:1991年9月
Linux诞生发展的五大支柱:
UNIX操作系统
Ken. Thompson和Dennis Ritchie开发的分时操作系统
MINIX操作系统
AST编写的用于学生学习操作系统原理的操作系统
GNU计划
GNU计划是Richard Stallman在1983年提出的,希望发展出一套完整的开放源代码操作系统来取代Unix,计划中的操作系统,名为GNU(GUN’s Not Unix)。
POSIX标准
该标准是基于UNIX实践和经验,描述了操作系统的调用服务接口。用于保证编写的应用程序在源代码一级上在不同操作系统之间的可移植性。
Internet网络
1.2 内容综述
本书是基于Linux 0.11版本,内容主要分为三个部分:第1至4章是描述内核引导启动和32位运行方式的准备阶段;第5至10章是内核代码主要部分;第11至13章可以作为第二部分代码的参考信息。
2.1 Linux内核模式和体系结构
操作系统内核的结构模式主要分为整体式的单内核结构和层次式的微内核结构。Linux 0.11版内核采用单内核结构,其结构紧凑,执行速度快,不足之处是层次结构性不强。
2.2 Linux中断机制
80x86使用两片8259A可编程中断控制芯片,PC/AT系列也采用了两片8259A,可以管理15级中断向量。其中从芯片的INT引脚连接到主芯片的IR2引脚上。主8259A芯片的端口基地址是0x20,从芯片是0xA0。
8259A分为编程状态和操作状态,编程即CPU通过IN、OUT指令对芯片进行初始化编程,完成后芯片即进入操作状态,当收到外部设备的中断请求时,首先通过中断判优选择其中优先级最高的,并通过CPU引脚INT通知CPU,之后通过数据总线将当前服务对象的中断号送出,CPU就获得了中断向量,并执行中断服务对于LINUX中断分为硬件中断和软件中断(异常),每个中断由0-255的一个数字标识,其中0-31属于软件中断,由intel公司设定/保留。中断int32-int47对应8259A的15个发出中断请求信号。
2.3 Linux系统定时
PC的可编程定时芯片Intel 8253被设置成每隔10毫秒就发出一个时钟中断,称之为系统滴答,每一个滴答都会调用一次时钟中断处理程序,其通过jiffies变量累计自系统启动经过的滴答数。然后从被中断程序的段选择符中取得当前特权级CPL作为参数调用do_timer()函数。do_timer()函数则根据特权级对当前进程运行时间作累计。如果CPL=0,则表示进程是运行在内核态时被中断,因此把进程的内核运行时间统计值stime增1,否则把进程用户态运行时间统计值增1。对于添加过定时器的程序,定时器时间到,就会调用该定时器的处理函数,然后对当前进程运行时间进行处理,把当前进程运行时间片减1。如果此时进程时间片大于0,推出do_timer()函数继续执行当前进程,否则根据特权级判定操作,特权级为0,则退出do_timer(),特权级大于0,则调用schedule()切换到其它进程去运行。
2.4 Linux内核进程控制
Linux操作系统采用分时技术,在宏观上达到进程并发执行的效果,微观上则为进程并行执行。Linux中的进程执行状态分为内核态和用户态,其对应的用于保存运行参数的内核堆栈和用户堆栈是分开的。
2.4.1 任务数据结构
内核程序通过进程表对进程进行管理,进程表项是一个task_struct(定义在/include/linux/sched.h)任务结构指针。主要包括进程当前运行的状态信息、进程优先级、进程号、父进程号、运行时间累计值、正在使用的文件和本任务的局部描述符以及任务状态段信息。
CPU所有寄存器中的值、进程的状态以及堆栈中的内容都被称为进程上下文,保存在进程的任务数据结构中。进程切换时首先保存当前进程上下文,以便在再次执行该进程时,能够恢复到切换时的状态执行下去。
2.4.2 进程运行状态
- 运行状态(TASK_RUNNING):
当进程正在被 CPU 执行,或已经准备就绪随时可由调度程序执行,则称该进程为处于运行状态。
- 可中断睡眠状态(TASK_INTERRUPTIBLE):
当进程处于可中断等待状态时,系统不会调度该进行执行。当系统产生一个中断或者释放了进程正在等待的资源,或者进程收到一个信号,都可以唤醒进程转换到就绪状态(运行状态)。
- 不可中断睡眠状态(TASK_UNINTERRUPTIBLE):
与可中断睡眠状态类似。但处于该状态的进程只有被使用 wake_up()函数明确唤醒时才能转换到可运行的就绪状态。
- 暂停状态(TASK_STOPPED):
当进程收到信号SIGSTOP、SIGTSTP、SIGTTIN 或SIGTTOU 时就会进入暂停状态。可向其发送SIGCONT 信号让进程转换到可运行状态。
- 僵死状态(TASK_ZOMBIE):
当进程已停止运行,但其父进程还没有询问其状态时,则称该进程处于僵死状态。
进程在时间片用完时进行进程切换,或者在内核态运行时等待某个系统资源时会从“运行态”转移到“睡眠态”进行进程切换。只有当进程从“内核运行态”转移到“睡眠状态”时,内核才会进行进程切换操作。在内核态下运行的进程不能被其它进程抢占。
2.4.3 进程初始化
在 boot/目录中引导程序把内核从磁盘上加载到内存中,并让系统进入保护模式下运行后,就开始执行系统初始化程序init/main.c。该程序首先确定如何分配使用系统物理内存,然后调用内核各部分的初始化函数分别进行初始化处理。在完成了这些操作之后,程序把自己“手工”移动到任务0(进程0)中运行,并使用fork()调用首次创建出进程1。在进程1 中程序将继续进行应用环境的初始化并执行shell 登录程序。而原进程0 则会在系统空闲时被调度执行,此时任务0 仅执行pause()系统调用,并又会调用调度函数。
“移动到任务0 中执行”这个过程由宏move_to_user_mode完成。它把main.c程序执行流从内核态(特权级0)移动到了用户态(特权级3)的任务0 中继续运行。宏move_to_user_mode 使用了中断返回指令造成特权级改变的方法。该方法的主要思想是在堆栈中构筑中断返回指令需要的内容,把返回地址的段选择符设置成任务0 代码段选择符,其特权级为3。此后执行中断返回指令iret 时将导致系统CPU 从特权级0 跳转到外层的特权级3 上运行。
2.4.4 创建新进程
Linux 系统中创建新进程使用fork()系统调用。所有进程都是通过复制进程0 而得到的,都是进程0的子进程。
创建新进程的过程:
系统首先在任务数组中找出一个还没有被任何进程使用的空项(空槽),若没有找到则返回错误。
然后系统为新建进程在主内存区中申请一页内存来存放其任务数据结构信息,并复制当前进程任务数据结构中的所有内容作为新进程任务数据结构的模板。
随后对复制的任务数据结构进行修改。
此后系统设置新任务的代码和数据段基址、限长并复制当前进程内存分页管理的页表。如果父进程中有文件是打开的,则应将对应文件的打开次数增1。
接着在GDT 中设置新任务的TSS 和LDT 描述符项,其中基地址信息指向新进程任务结构中的tss 和ldt。
最后再将新任务设置成可运行状态并返回新进程号。
2.4.5 进程调度
Linux 进程是抢占式的。被抢占的进程仍然处于TASK_RUNNING 状态,只是暂时没有被CPU 运行。进程的抢占发生在进程处于用户态执行阶段,在内核态执行时是不能被抢占的。在Linux 0.11 中采用了基于优先级排队的调度策略。
调度程序
schedule()函数首先扫描任务数组。比较就绪态任务的counter 的值,值大表示运行时间还不长,则选择并切换到该进程。若所有处于TASK_RUNNING 状态进程的时间片都已经用完,系统会根据每个进程的优先权值priority,对所有进程(包括正在睡眠的进程)重新计算counter值。重复上述过程,直到选择出一个进程为止,调用switch_to()执行进程切换操作。
进程切换
switch_to()首先检查要切换到的进程是否就是当前进程,如果是则什么也不做,直接退出。否则就首先把内核全局变量current 置为新任务的指针,然后长跳转到新任务的任务状态段TSS 组成的地址处,造成CPU 执行任务切换操作。此时CPU 会把其所有寄存器的状态保存到当前任务寄存器TR 中TSS 段选择符所指向的当前进程任务数据结构的tss 结构中,然后把新任务状态段选择符所指向的新任务数据结构中tss 结构中的寄存器信息恢复到CPU 中,系统就正式开始运行新切换的任务了
2.4.6 终止进程
当一个进程结束了运行或在半途中终止了运行,那么内核就需要释放该进程所占用的系统资源,包括进程运行时打开的文件、申请的内存等。
当一个用户程序调用exit()系统调用时,就会执行内核函数do_exit():
首先释放进程代码段和数据段占用的内存页面,关闭进程打开着的所有文件,对进程使用的当前工作目录、根目录和运行程序的i 节点进行同步操作。
如果进程有子进程,则让init
进程作为其所有子进程的父进程。如果进程是一个会话头进程并且有控制终端,则释放控制终端,并向属于该会话的所有进程发送挂断信号SIGHUP,这通常会终止该会话中的所有进程。
然后把进程状态置为僵死状态TASK_ZOMBIE。并向其原父进程发送SIGCHLD 信号,通知其某个子进程已经终止。
最后do_exit()调用调度函数去执行其它进程。