前言
通过本课程的学习,我们主要熟悉了Linux基本原理,了解了Linux操作系统框架,对Linux操作系统内核关键技术进行了深入的学习。
从用户的角度对Linux的环境及其使用进行简单的介绍,然后通过Linux操作系统源代码分析了解Linux操作系统与底层硬件、上层应用之间的结构关系、调用关系,熟悉Linux操作系统的配置。
对Linux操作系统的内核关键技术,例如中断和异常处理、地址空间管理、内存分配技术、进程管理、进程切换关键代码、进程间通信机制、文件系统管理等内容,Linux操作系统中设备管理方法以及设备驱动进行了由浅入深的学习。
一、精简的Linux系统概念模型
Linux系统一般有4个主要部分:内核、shell、文件系统和应用程序。内核、shell和文件系统一起形成了基本的操作系统结构,它们使得用户可以运行程序、管理文件并使用系统。
Linux体系结构可以分为两块:
(1)用户空间:用户空间中包含了,用户的应用程序,C库
(2)内核空间:内核空间包括系统调用,内核,以及与平台架构相关的代码
Linux系统的核心是内核。内核控制着计算机系统上的所有硬件和软件,在必要时分配硬件,并根据需要执行软件。内核主要负责以下四种功能:
- 系统内存管理
- 软件程序管理
- 硬件设备管理
- 文件系统管理
二、进程管理
进程是进程实体的运行过程, 是系统进行资源分配和调度的一个独立单位 。进程是由PCB、 程序段和数据段三部分组成的
进程控制块PCB是名字为task_struct的数据结构, 它称为任务结构体 。任务结构体中容纳了一个进程的所有信息, 是系统对进程进行管理和控制的有效手段, 是系统实现进程调度的主要依据。
Linux中每一个进程由一个task_struct数据结构来描述。进程描述符放在动态内存中而且和内核态的进程栈放在一个独立的8KB的内存区中。
linux系统为每个用户进程分配了两个栈: 用户栈和内核栈。 当一个进程在用户空间执行时,系统使用用户栈; 当在内核空间执行时, 系统使用内核栈。 由于内核栈地址空间的限制, 内核栈不会分配很大的空间。 此外, 内核进程只有内核栈, 没有用户栈。
进程的状态如下:
- 运行态: 进程正在使用CPU运行的状态。
- 可运行态: 进程已分配到除CPU外所需要的其它资源, 等待系统把CPU分配给它之后即可投入运行。
- 等待态: 又称睡眠态, 它是进程正在等待某个事件或某个资源时所处的状态。
- 暂停态: 进程需要接受某种特殊处理而暂时停止运行所处的状态。
- 僵死态: 进程的运行已经结束, 但它的任务结构体仍在系统中。
Linux的进程调度是基于优先级的调度:
- Linux的进程分为普通进程和实时进程, 在基于优先级的算法下实时进程的优先级高于普通进程。
- Linux中进程的优先级是动态的, 调度程序周期性的调整他们的优先级, 避免进程饥饿
Linux对实时进程和普通进程采用不同的调度策略:
- SCHED_OTHER 普通进程的时间片轮转算法
- SCHED_FIFO 实时进程的先进先出算法
- SCHED_RR 实时进程的时间片轮转算法
三、中断管理
内核的一个主要功能就是处理硬件外设I/O,中断信号提供了一种特殊的方式, 使得CPU转去运行正常程序之外的代码,中断会改变处理器执行指令的顺序, 通常与CPU芯片内部或外部硬件电路产生的电信号相对应。
初始化过程:
- 系统加载 start_kernel 开始,调用了 trap.c 中的 trap_init()函数对 中 断 进 行 初 始 化 。 并且调用同一文件下的set_trap_gate()/set_system_gate()/set_intr_gate()等对中断描述符进行初始化。
- 在进入保护模式之前, IDT 再次通过 setup_idt()函数进行初始化,在这里使用了 ignore_int()函数,是为了保护未初始化完成时发生异常不出错。 然后调用 init_IRQ()函数,把中断描述附表的中断处理代码段地址设在在 interrupt 数组中,该数组指向同一个函数处理 common_interrup。
明确中断发生时, CPU 硬件级的中断信号处理过程 :
1. 确定与中断或异常相关联的向量 i
2. 读取 idtr 寄存器的值,找到 IDT 的基址,通过查询 IDT,找到第 i 项对应的内容。
3. 从 gdtr 寄存器获得 GDT 的基地址,并在 GDT 中查找,以读取IDT 表项中的段选择符所标识的段描述符。
4. 确定中断是由授权的发生源发出的。
中断:需要比较 CPL 和 GDT 中的 DPL。中断处理程序的特
权不能低于引起中断的程序的特权。
编程异常:需要比较 CPL 和 IDT 中的 DPL。
5. 检查是否发生了特权级的变化,一般指的是用户态陷入内核态。
如果是用户态陷入内核态,控制单元要使用新的特权级堆栈。
保存 ss 和 esp,并用新的堆栈的值填充。
6. 如果是故障,用引起故障的指令修改 cs 和 eip,以便异常处理后再次执行。
7. 在堆栈中保存 eflags/cs/eip 的内容。
8. 如果有硬件出错码,则保存。
9. 使用 IDT 中第 i 项中的段描述符和偏移量填充 cs 和 eip。
四、文件系统
要实现操作系统对其它各种不同文件系统的支持,就要将对各种不同文件系统的操作和管理纳入到一个统一的框架中。 对用户程序隐去各种不同文件系统的实现细节,为用户程序提供一个统一的、抽象的、虚拟的文件系统界面,这就是所谓的虚拟文件系统(VFS)。
Linux支持多种文件系统,包括ext2、ext3、 vfat、 ntfs、 iso9660、 jffs、 romfs和nfs等,为了对各类文件系统进行统一管理, Linux引入了虚拟文件系统VFS。
虚拟文件系统 VFS:
- 是一个软件层,用来处理与Unix标准文件系统相关的所有系统调用。
- 是用户应用程序与文件系统实现之间的抽象层能为各种文件系统提供一个通用的、统一的接口
主要数据结构:
- 超级块对象:保存文件系统信息
- 文件对象:保存已打开的文件和进程的交互信息。
- 目录项对象:存放目录项和文件链接的信息。
- 索引结点对象:存放具体文件的一般信息。
- 在磁盘中的数据结构:
- 引导控制块:操作系统的初始引导块
- 盘控制块:磁盘信息等
- 文件控制块:文件属性。
- 目录结构:组织管理文件。
- 在内存中的数据结构:
- 系统打开文件表
- 进程打开文件表
根文件系统的挂载:
1. 准备一个虚拟的文件系统 rootfs,将其初始化为根文件系统
Start_kernel 后调用 vfs_caches_init()初始化 vfs。调用 mnt_init()创建一个虚拟的
rootfs,调用 init_rootfs 将其向内核注册。通过 init_mount_tree 挂载根文件系统。 ./挂载点就形成了。
2.将 initrd 文件加载到内存。在这里分两种情况。
① 格式为 cpio_initrd。
a)直接将其解压到根文件下。
b)执行/init,如果执行成功则加载成功。如果失败则执行 3。如果没有/init 那么执行用户定义的根文件系统。然后执行 3。
②格式为 image_initrd。
a)将其释放到/image_initrd 文件下。
b)在内核下将其复制到/dev/ram0 文件下。
c)如果/dev/ram0 是根文件系统,那么挂载根文件系统,然后执行 3。
d)否则执行 Linuxrc,加载根文件系统的一些驱动等。而后挂载根文件系统,然后执行 3。
3. 执行/sbin/init,/bin/int,/etc/init 等,如果成功则挂载根文件系统成功。
五、地址空间与地址映射
Linux把进程地址空间分成内核区和用户区两部分:
- 操作系统内核的代码和数据等被映射到内核区
- 进程可执行映像(代码和数据)映射到虚拟内存的用户区
- 一个进程所需的虚拟空间中的各个部分未必连续,这通常会形成若干离散的虚存“区间” (VM area)
- 一个虚拟“区间” 是进程虚拟空间的一部分,这部分的虚拟空间是连续的并且有相同的一些属性
内核中的函数以直接了当的方式获得动态内存
- 内核是操作系统中优先级最高的成分。
- 内核信任自己
- 采用前面介绍的页面级内存分配和小内存分配以及非连续内存区
给用户态进程分配内存时
- 请求被认为是不紧迫的
- 用户进程不可信任
- 因此,当用户态进程请求动态内存时,并没有立即获得实际的物理页框,而仅仅获得对一个新的线性地址区间的使用权 这个线性地址区间会成为进程地址空间的一部分,称作线性区(memory areas)
进程分配页框
- 被访问的⻚可能不在主存中,其原因或者是进程从没访问过该⻚,或者是内核已经回收了相应的⻚框,或者⻚映射了磁盘文件。
- ⻚属于非线性磁盘文件的映射
- 进程已经访问过这个⻚,但是其内容被临时保存在磁盘上。
请求调页是一种动态内存分配技术,它把页框的分配推迟到不能再推迟为止,也就是说一直推迟到进程要访问的页不在 RAM 中为止,由此引起缺页异常
六、系统调用
系统调⽤的意义是操作系统为⽤户态进程与硬件设备进 ⾏交互提供了⼀组接⼝。 系统调⽤的库函数就是我们使⽤的操作系统提供的 API(应⽤程序编程接⼝),API 只是 函数定义。系统调⽤是通过特定的软件中断(陷阱 trap)向内核发出服务请求,int $0x80 和syscall指令的执⾏就会触发⼀个系统调⽤。C库函数内部使⽤了系统调⽤的封装例程, 其主要⽬的是发布系统调⽤,使程序员在写代码时不需要⽤汇编指令和寄存器传递参数来 触发系统调⽤。⼀般每个系统调⽤对应⼀个系统调⽤的封装例程,函数库再⽤这些封装例 程定义出给程序员调⽤的 API,这样把系统调⽤终封装成⽅便程序员使⽤的C库函数。
当⽤户态进程调⽤⼀个系统调⽤时,CPU切换到内核态并开始执⾏ system_call(entry_INT80_32或entry_SYSCALL_64)汇编代码,其 中根据系统调⽤号调⽤对应的内核处理函数。具体来说,在Linux中通 过执⾏int $0x80或syscall指令来触发系统调⽤的执⾏,其中这条int $0x80汇编指令是产⽣中断向量为128的编程异常(trap)。接口调用时,传递参数⽅法⽆法通过参数压栈的⽅式,⽽是通过寄存器 传递参数的⽅式。寄存器传递参数的个数是有限制的,⽽且每个参数的⻓度不能超过寄存 器的⻓度,32位x86体系结构下寄存器的⻓度⼤32位。除了EAX⽤于传递系统调⽤号 外,参数按顺序赋值给EBX、ECX、EDX、ESI、EDI、EBP,参数的个数不能超过6个, 即上述6个寄存器。
七、心得体会
通过本学期对于Linux系统的深入的学习,将之前操作系统中的知识,投入到Linux的操作系统的实际对比中,发现了理论和现实的相同和差距,同时更加深刻的明白了操作系统的关键的部分的具体实现方式:例如进程管理、系统调用等知识点。总的来说,这学期的Linux课程于我来说受益匪浅,同时也感谢孟老师和李老师在疫情期间仍不辞辛劳的教学。