课程学习总结报告
报告要求:
根据本课程所学内容总结梳理出一个精简的Linux系统概念模型,最大程度统摄整顿本课程及相关的知识信息,模型应该是逻辑上可以运转的、自洽的,并举例某一两个具体例子(比如读写文件、分配内存、使用I/O驱动某个硬件等)纳入模型中验证模型。
以下内容都是本人根据这学期课程视频与课件总结的linux内核的一些知识,才疏学浅,若有问题,还请指正。
计算机基本原理与准备工作
1.冯.诺依曼体系结构

冯·诺依曼体系结构如上图所示,其中运算器、存储器、控制器、输⼊设备和输出设备5⼤基本类型部件组成了计算机硬件;计算机内部采用二进制来表示指令和数据;冯·诺依曼体系结构的核⼼是存储程序计算机。
2.CPU的一些重要寄存器和汇编指令(32位机)
esp:堆栈顶指针,寄存器中存放栈顶地址。
ebp:堆栈基指针,寄存器中存放栈底地址。
eip:指令指针,寄存器中存放着下一条指令的地址。(只能通过call,jmp,ret等跳转指令去进行相关操作)
eax:累加器。
CS:代码段寄存器。
DS:数据段寄存器。
SS:堆栈段寄存器。
重要汇编指令:push,pop,call,ret
3.函数的堆栈框架(call xxx,进入xxx,退出xxx的堆栈和相关寄存器变化过程)
4.如何在C中嵌入汇编代码(asm volatile( );)
5.编写一个精简的linux内核
深入linux内核
1.系统调用

系统调用是一种特殊的中断(异常中的trap),对应的中断向量号为128。在linux操作系统中,有0和3两个执行级别,分布对应内核态与用户态。用户态和内核态很显著的区别方法为CS:EIP的指向范围。当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行system_call(entry_INT80_32或entry_SYSCALL_64)汇编代码,其中根据系统调用号调用对应的内核处理函数。在内核2.6版本之后,Intel处理器还引入了快速系统调用sysenter指令。

通过int $0x80指令发出系统调用/通过iret汇编指令退出系统调用:
传统系统调⽤(int $0x80) 通过中断/异常实现,在执⾏ int 指令时,发⽣ trap。硬件找到在中断描述符表中的表项,在⾃动切换到内核栈 (tss.ss0 : tss.esp0) 后根据中断描述符的 segment selector 在 GDT / LDT 中找到对应的段描述符,从段描述符拿到段的基址,加载到 cs ,将 offset 加载到 eip。最后硬件将 ss / sp / eflags / cs / ip / error code 依次压到内核栈。返回时,iret 将先前压栈的 ss / sp / eflags / cs / ip 弹出,恢复⽤户态调⽤时的寄存器上下⽂。
详细过程可见上次作业:https://www.cnblogs.com/fengyakk/p/13115899.html
2.中断与异常
中断分外部中断(硬件中断)和内部中断(软件中断),内部中断又称为异常,异常又分为故障和陷阱。
当内核正在做一些别的事情的时候,中断会随时到来,正在运行的代码会被打断。内核的目标就是让中断尽可能快的处理完,尽可能把更多的处理往后推迟。
内核在处理一个中断时可以接收一个新的中断,但在内核代码中还存在一些临界区,在临界区中,中断必须被禁止。而且只有高优先级的中断才能够抢占低优先级的中断。
中断占用的是被中断进程的内核栈。
一旦一个中断事件发生了,CPU总能得到一个相对应的中断向量。
中断描述符表IDT中有三种类型:中断门描述符、任务门描述符和陷阱门描述符,linux用中断门处理中断,用陷阱门描述异常。
中断与异常的硬件处理:(32位机)
(1)确定中断向量i。
(2)读由idtr寄存器指向的IDT表中的第i项,找到相应的段选择符和偏移量。(内核启动中断前,必须初始化IDT,然后把IDT的基地址装载到idtr寄存器中)
(3)从gdtr寄存器获得GDT的基地址,并在GDT中查找,以读取IDT表项中的段选择符所标识的段描述符。这个描述符指定中断或异常处理程序所在段的基地址。
(4)确定中断是由授权的发生源发出的。
(5)检查是否发生特权级的变化(判断是否是中断嵌套)
(6)若发生的是故障,用引起异常的指令地址修改cs和eip的值,以使得这条指令能在异常处理结束后能再次执行。
(7)在栈中保存eflags、cs和eip的内容。
(8)如果产生硬件出错码,则将它保存在栈中。
(9)装载cs和eip寄存器,然后就跳转到异常或中断处理程序。
进入中断处理程序后还会进行软件级的保护现场(save all),进一步保存通用寄存器的内容。

3.Linux中的时钟和定时器
80x86体系中,内核要与四种时钟打交道。
(1)实时时钟:RTC
特点:独立于CPU,单独供电,关机也会计时,但是精度不高。Linux只用RTC获取时间和日期。
(2)时间戳计数器:TSC
特点:精度比RTC高,和CPU一起供电,关机会掉电。
(3)可编程间隔定时器:PIT
特点:能够产生一个周期性的中断;中断周期与计数器的大小有关;计数器可通过编程来设定。一般作为一个周期性的定时中断源。
(4)CPU本地定时器、高精度事件定时器(HPET)、ACPI电源管理定时器
Linux计时体系结构
Linux内核周期性地:(通过定时中断源)
(1)更新自系统启动以来经过的时间。(通过计时时钟源读取)
(2)更新时间和日期。(通过计时时钟源读取)
(3)确定当前进程在每个CPU上已运行了多长时间,确认时间片是否用完。
(4)更新资源使用统计数。
(5)检查每个软定时器。
软定时器和延迟函数
动态定时器应用之schedule_timeout
4.进程管理
程序与进程的区别:程序是指令的有序集合(静态),进程是程序在处理机上的一次执行(动态);程序可以长期存在(永久),进程是有一定生命期的(暂时);进程是由PCB、程序段和数据段三部分组成的;进程具有创建其他进程的功能,而程序没有;同一个程序可以对应多个进程。
内核是通过管理进程的PCB来进行进程管理,而linux中PCB为一个名为task_struct任务结构体的数据结构。linux中每一个进程由一个task_struct数据结构来描述。
进程上下文:系统提供给进程的处于动态变化的运行环境总和称为进程上下文。系统中每一个进程都有它的进程上下文。(pt_regs数据结构)
进程切换的本质就是进程的上下文切换。
进程结束后会调用do_exit函数终止执行,这个函数会由编译器在编译时最后自动加上。
通过当前进程的Thread_info就可以获得进程描述符PCB信息。
通过宏对进程链表的扫描、插入与删除。
可运行队列:140个不同优先级的进程链表;bitmap代表每个优先级的进程链表是否为空;活动进程队列(active)和过期进程队列(expire)。
进程调度:
(1)非剥夺方式和剥夺方式。
(2)进程调度算法(FIFO、SCBF、FPF、时间片轮转法)
进程调度的功能:
(1)记录系统中所有进程的执行情况
(2)选择占有处理机的进程
(3)进行进程上下文切换
进程调度的时机:
(1)进程状态发生变化时
(2)进程时间片用完的时候
(3)中断执行完成(包括系统调用)返回用户态的时候
Linux中的进程调度是基于动态优先级的调度
(1)linux的进程分为普通进程和实时进程,实时进程的优先级高于普通进程。
(2)linux中进程的优先级是动态的。
Linux对于实时进程和普通进程的调度策略是不同的
(1)普通进程是按照动态优先级的时间片轮转算法调度的;普通进程还分为交互进程和最普通的进程,交互进程时间片用完后进过期队列还是活动队列取决于过期队列中最古老进程的动态优先级。
(2)实时进程按照FIFO和时间片轮转算法进行调度的,实时进程时间片用完进活动队列尾部。
进程优先级分为静态优先级(创建时自带)与动态优先级(进行调度),时间片的长度与静态优先级有关。
5.设备驱动
驱动是应用软件和硬件的桥梁,它使得应用软件只需要调用系统软件的应用编程接口就可以让硬件去完成要求的工作。

字符设备与块设备都被映射到Linux文件系统的文件和目录,通过文件系统的系统调用接口open()、write()、read()、close()等即可访问字符设备和块设备。
用户通过设备驱动程序来对设备驱动文件进行操作,从而可以完成对设备的操作。
Linux内核对字符设备的管理是通过cdev结构体实现的。
驱动程序是以模块化的形式编写(LKM),这样可以维持内核的轻便性,insmod将模块插入内核,rmmod卸载模块。
字符设备驱动模块加载函数:
(1)生成设备号(Linux系统用设备号来标识设备文件,设备号分为主设备号和从设备号)
(2)申请内存(kmalloc)
(3)注册,IO端口初始化
(4)设置cdev结构体(两个很重要的成员变量:设备号,file operations)
字符设备驱动模块卸载函数:
(1)释放占用的设备号
(2)注销设备
内核模型只能调用和使用内核提供的函数,不能使用相关的应用程序函数,内核模块编程与内核的版本密切相关。
通过mknod()函数可以创建设备文件。
设备文件的注册:
(1)如果设备驱动程序被静态编译进内核,则注册发生在内核初始化阶段。
(2)如果作为一个内核模块来编译,则在装入模块的时候注册。
设备文件的open()过程?设备文件的read()过程?(可参考后面文件系统)

7.内核根文件系统挂载
bios的四个功能:
(1)硬件的初始化
(2)拷贝加载,把内核从磁盘加载到内存中
(3)传递启动参数
(4)修改程序指针,让程序指针跳转到内核的入口处
根文件系统的挂载步骤:
(1)挂载虚拟的文件系统rootfs作为初始文件系统
因为让rootfs目录成为了init进程的根目录和当前目录,而init进程是所有进程的父进程,所以这个目录成为了所有进程的根目录,因此之后挂载的文件系统就是根文件系统。
(2)挂载一个真正的根文件系统替换rootfs,不同的文件系统可以通过mount挂载到根文件系统中。
8.文件系统
文件:具有符号名的、在逻辑上具有完整意义的一组相关信息项的有序序列。
文件的符号名称称作文件名。通常,系统为一个正在使用的文件提供读和写指针。
文件建立在外存空间,以便使文件能够长期保存。即:文件一旦建立,就一直存在,直到被删除或者超过事先规定的保存期限。
UNIX文件类型:
(1)正规文件:系统所规定的普通格式的文件。
(2)目录文件:由文件目录构成的一类文件。
(3)符号链接:又称为软链接。
(4)设备文件:包括块设备文件和字符设备文件。
(5)管道文件:系统使用管道文件的目的是希望将一个进程的输出作为另一个进程的输入。
(6)套接字:又称插口,可用于socket编程。
文件结构分为逻辑结构和物理结构。
文件的逻辑结构:文件的组织形式,即从用户角度看到的文件组织形式,用户以这种形式存取、检索和加工有关信息。
文件的物理结构:文件的内部组织形式,即文件在物理存储设备上的存放方法;文件的物理结构决定了文件信息在存储设备上的存储位置。
文件目录:系统为每个文件设置了一个描述性数据结构——文件控制块FCB;文件目录就是文件控制块的有序集合。

FCB是文件存放的标志,记录了系统管理文件所需要的全部信息。
在linux中文件控制块称为inode,采用inode索引的文件目录示意图如下:图中文件A和B映射为同一物理文件。

文件目录与目录文件的区别:
1.文件目录
文件与文件控制块是一一对应的;文件控制块的有序集合构成文件目录,每个目录项即是一个文件控制块;给定一个文件名,通过查找文件目录就可以找到文件对应的目录项FCB。(按名存取)
2.目录文件
文件目录是需要长期保存的;为了实现文件目录的管理,通常将文件目录以文件的形式保存在外存空间,这个文件就被称为目录文件;目录文件是长度固定的记录式文件。
用户在使用文件系统时,可以利用操作系统提供的系统调用。
文件打开open()的执行过程:
1.open->sys_open
(1)open执行后,会找到C库中的函数,引起INT 80 n中断,n为系统调用号
(2)从idtr寄存器读取IDT表基地址,找到中断向量表,根据中断向量号128得到对应的的段选择符
(3)从gdtr寄存器读取GDT表基地址,获取段选择符所标志的段描述符,根据段描述符和偏移量可以找到系统调用system_call的入口地址。
(4)保存现场并执行system_call函数,根据系统调用号在系统调用表中进行查找。
(5)系统调用表中保存了所有系统调用函数的入口地址,找到对应的系统调用函数sys_open()的入口地址并执行sys_open()函数。
2.sys_open的过程
(1)根据文件名进行命名查找,获得文件的文件控制块FCB。
(2)根据FCB获得文件类型,调用相应的文件打开函数。
(3)建立系统文件打开表.
(4)通过FCB将文件信息填表。
(5)返回进程控制块中的文件打开表,从fd数组中找到一个空闲的,将指针指向系统文件打开表。
(6)返回fd数组的下标,也就是fd索引号。
至此文件打开过程结束。
sys_read的过程
(1)找到进程控制块中的进程文件打开表,根据fd索引号找到fd指针,从而指向系统文件打开表。
(2)从系统文件打开表中找到file operations中的读函数。
(3)执行file operations中的读函数。
虚拟文件系统模型(VFS)

VFS是一个软件层,用来处理与Unix标准文件系统相关的所有系统调用,能为各类文件系统提供一个统一的操作界面和应用编程接口。
VFS提供的sys_read,sys_write等等都是通过fd索引找到系统文件打开表,调用file operations中的相应函数。打开一个文件都需要创建系统文件打开表。
为什么读写文件之前需要打开文件?
通过open函数可以获得fd索引号,sys_write,sys_read根据fd索引号找到fd数组中的指针指向系统文件打开表,从而由系统文件打开表调用file operations中的相应函数。
VFS引入的通用文件模型:
(1)超级块对象:存放文件系统相关信息(文件系统控制块,与磁盘的盘控制块对应,仅存在内存之中)
(2)索引节点对象:存放具体文件的一般信息(inode节点,与磁盘的FCB结构对应)
(3)文件对象:存放已打开文件和进程之间的交互信息(系统文件打开表)
(4)目录项对象:存放目录项与文件的链接信息(与磁盘的目录结构对应)
为什么VFS能对多个文件系统兼容:因为VFS的超级块中含有各个具体文件系统的私有结构。
9.进程地址空间
内存描述符:与进程地址空间有光的全部信息都包含在一个叫做内存描述符的数据结构中。(mm_struct)每个进程有且仅有一个mm_struct结构,描述进程的虚拟内存。
线性区:描述地址空间内连续区间上的一个独立内存范围;线性区的开始和结束都必须4KB对齐;进程只能访问某个有效的线性区。(由vm_area_struct线性区描述符来描述的)
如果进程试图访问一个area之外的地址或者用不正确的方式访问一个有效的area,内核将通过段异常杀死这个进程。
缺页异常:当用户态向内核申请空间时,内核只是通过mmap()等调用分配了一些线性地址空间给进程,并没有真正把实际物理页框分配给进程。当进程试图访问这些分配给它的地址空间时,由于没有物理页框对应于这些线性地址,从而会引发一个缺页异常。
通过缺页异常处理程序可以判断出这是不是一个合法的缺页异常,如果是,则负责给这段线性地址分配一些物理页框并把磁盘中对应的文件写入这些物理页框,这样进程就得以正常运行。

请求调页:这是一种动态内存分配技术,它把页框的分配推迟到不能再推迟为止,也就是说一直推迟到进程要访问的页不在内存中为止,由此引起缺页异常。
写时复制:
子进程复制父进程的整个地址空间非常耗时,因此父进程与子进程共享页框而不是复制页框。而且共享的页框不能被修改。但是,当父进程和子进程何时试图写一个共享的页框,就产生一个异常,这时内核就把这个页复制到一个新的页框中并标记为可写。原来的共享页框仍然是写保护的,当其他进程试图进入时,内核检查写进程是否是这个页框的唯一属主,如果是就把这个页框分配给它。这样一段时间后进程就会有自己的空间,并且按需分配。
总结
通过这学期的linux课程,我学到了很多知识,通过做孟老师布置的实验,我进一步了解到一个精简的操作系统是什么样的,以及通过gdb来跟踪调试系统调用,让我受益匪浅。非常荣幸能有这样学习的机会,孟老师、李老师还有助教辛苦了!
浙公网安备 33010602011771号