Linux内核0.11体系结构 ——《Linux内核完全注释》笔记打卡

0 总体介绍

        一个完整的操作系统主要由4部分组成:硬件、操作系统内核、操作系统服务和用户应用程序,如图0.1所示。操作系统内核程序主要用于对硬件资源的抽象和访问调度。


图0.1 操作系统组成部分

        内核的主要作用是为了与计算机硬件进行交互,实现对硬件部件的编程控制和接口操作,调度对硬件资源的访问,并为计算机上的用户程序提供一个高级的执行环境和对硬件的虚拟接口。

1 Linux内核模式

        操作系统内核的结构模式主要可分为整体式的单内核模式和层次是的微内核模式。Linux 0.11采用了单内核模式。

        如图1.2所示,单内核操作系统所提供的服务流程为:应用主程序使用指定的参数值执行系统调用指令(int x80),使CPU从用户态切换到核心态,然后操作系统根据具体的参数值调用特定的系统调用服务程序,这些服务程序根据需要再调用底层的一些支持函数以完成特定的功能。完成服务后,系统使CPU从核心态回到用户态,执行后续的指令。


图1.1 单内核模式的简单模型结构

2 Linux内核系统体系结构

        Linux内核主要由5个模块构成,分别为:进程调度模块、内存管理模块、文件系统模块、进程间通信模块和网络接口模块。模块之间的依赖关系如图2.1所示,虚线部分表示0.11版本内核中未实现部分(所有的模块都与进程调度模块存在依赖关系)。


图2.1 Linux内核系统模块结构及相互依赖关系

        从单内核模式结构模型出发,Linux 0.11内核源代码的结构将内核主要模块分配如图2.2所示。(除了硬件控制方框,其他粗线分别对应内核源代码的目录组织结构)


图2.2 内核结构框图

3 Linux内核对内存的管理和使用

        对于机器中的物理内存,Linux 0.11内核中,系统初始化阶段将其划分的功能区域如图3.1所示。


图3.1 物理内存使用的功能分布图

虚拟地址:(virtual address)由程序产生的由段选择符合段内偏移地址两个部分组成的地址。(虚拟地址空间由GDT和LDT映射的地址空间组成,最大空间16384*4G=64T)

逻辑地址:(logical address)由程序产生的与段相关的偏移地址部分。(Intel保护模式下就是程序执行代码段限长内的便宜地址)

线性地址:(linear address)虚拟地址到物理地址变换之间的中间层,是处理器可寻址的内存空间中的地址。

物理地址:(physical address)出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。

虚拟存储/虚拟内存:(virtual memory)指计算机呈现出要比实际拥有的内存大得多的内存量。

保护模式下,虚拟地址到物理地址的变换过程如图3.2所示。


图3.2 虚拟地址(逻辑地址)到物理地址的变换过程

内存分段机制:Intel CPU使用了段的概念对程序进行寻址。在保护模式下,段寄存器中存放的是一个段描述符表中某一描述符项在表中的索引值。(索引的段描述符项中有该段描述符选项中指定的段基地址、段长度和段的访问特权级等信息——GDT/IDT/LDT)

在Linux 0.11中,中断描述符表IDT保存在内核代码段中。程序逻辑地址到线性地址的变换过程使用了CPU的全局段描述符表GDT和局部段描述符表LDT。由GDT映射的地址空间称为全局地址空间,LDT映射的地址空间称为局部地址空间,两者共同构成了虚拟地址空间,如图3.3所示。图中显示了两个任务时的情况,每个LDT本身也是由GDT中描述定义的一个内存段,任务状态段TSS也是由GDT中描述符定义的一个内存段。(TSS用于在任务切换时CPU自动保存或恢复相关任务的当前CPU状态)


图3.3 Linux系统中虚拟地址空间分配图

内存分页管理:基本原理是将CPU整个线性内存区域划分成为4096字节为1页的内存页面。对于页目录和页表,格式基本相同,都占用4个字节,每个页目录表或页表必须只能包含1024个页表,一个页目录表或一个页表均占1页内存。图3.4为线性地址到物理地址的变换过程示意图。


图3.4 线性地址到物理地址的变换示意图

        Linux 0.11内核设置GDT段描述符项数最大为256,2项空闲,2项系统使用,每个进程使用2项,最多再容纳126个任务,内核规定最大任务数64个,每个任务逻辑地址范围为64M。


图3.5 Linux 0.11 线性地址空间的使用示意图

        对于Linux 0.11 内核代码和数据,初始化操作中,对其段长度都设置为16MB,都是从线性地址的0开始到地址0xffffff共16MB的段。该范围中含有内核所有的代码、内核段表(GDT、IDT、TSS),页目录表和内核的二级页表、内核局部数据以及内核临时堆栈。(将被用作任务0的用户堆栈)内核的虚拟地址空间、线性地址空间和物理地址空间的关系如图3.6所示。


图3.6 内核代码和数据段在三种地址空间中的关系

        Intel 80x86 cpu共分为4个保护级,Linux 0.11操作系统使用了cpu的0级和3级,内核代码本身由系统中所有任务共享。当一个任务(进程)被中断程序中断时,此时用户程序也可以象征性的称为处于进程的内核态,因为中断程序使用当前进程的内核栈。多任务结构示意图如图3.7所示。


图3.7 多任务系统

        任务0是系统中一个人工启动的第一个任务,代码段和数据段长度被设置为640KB(TSS0也是手工预设置好的,任务包含在内核代码中,不需要再为其分配内存页)。任务1也是一个特殊任务,系统创建任务1时,为存放任务1的二级页表在主内存区申请了一页内存来存放,并复制了父进程(任务0)的页目录和二级页表项,它占用的线性空间范围64MB-128MB(实际64MB-64MB+640KB)。对于创建的从任务2开始的其他任务,其父进程都是init(任务1)进程,从任务2开始,任务在线性地址空间中的起始位置为任务号*64MB,图3.8为任务2原先复制任务1的代码和数据被shell程序的代码段和数据段替换后的情况。


图3.8 其他任务地址空间中的对应关系

        内核以页面为单位分配和映射物理内存,malloc()函数具体记录用户程序使用了一页内存的多少字节,剩余的容量保留给程序再申请内存时使用。当用户使用内存释放函数free()动态释放申请的内存块时,C库中的内存管理函数会把释放的内存块标记为空闲,在这个过程中内核为该进程分配的这个物理页面不会被释放,只有进程结束时才全面回收已分配和映射到该进程地址空间范围的所有物理内存页面。

4 Linux系统的中断机制

 


图4.1 PC/AT微机级连式8259控制系统

        在PC/AT兼容机种,使用了两块8259A芯片,如图4.1所示。在总线控制器控制下,8259A芯片可以处于编程状态和操作状态。操作时,通过中断判优选择,芯片将选中当前最高优先级的中断请求作为中断服务对象,并通过CPU引脚INT通知CPU外中断请求的到来,CPU响应后,芯片从数据总线D7-D0将设定的当前服务对象的终端号送出。

        当Intel CPU运行在32位保护模式下时,需要使用中断描述符表IDT管理中断或异常。每个中断描述符中包含中断服务程序地址、有关特权级和描述符类别信息。中断信号通常分为两类:硬件中断和软件中断(异常),每个中断由0-255之间的一个数字来标识。对于中断0x00-0x1f,由Intel公司固定设定或保留,属软件中断。中断分类如图4.2所示。


图4.2 中断分类以及中断退出后CPU处理方式

        Linux系统将0x20-0x2f对应于8259A中的中断请求,把程序编程发初的系统调用中断设置为0x80。对于可能引起竞争条件的代码区,内核使用cli指令和sti指令进行中断的屏蔽和允许。

5 Linux的系统调用

        系统调用(syscalls)是Linux内核与上层应用程序进行通信的唯一接口。应用程序、库函数和内核系统调用之间的关系如图5.1所示。Linux内核中,每个系统调用都具有唯一的一个系统调用功能号。系统调用的结果会在返回值中表现出来,负值为错误,0表示正确,出错时,错误信息会在errno中(perror()函数可进行打印)。


图5.1 应用程序、库函数和内核系统调用之间的关系

        程序发初中断调用int 0x80时(系统调用开始),寄存器eax存放系统调用号,参数依次存放在ebx、ecx和edx中(该版本内核最多直接传递3个参数)。此外,Intel CPU还提供了系统调用门的参数传递方式,它在进程用户态堆栈和内核态堆栈自动复制传递的参数。

        ex.对于read()系统调用,其定义为int read(int fd, char *buf, int n),系统调用的宏的形式为_syscall3(int,read,int,fd,char*,buf,int,n),系统调用宏有2+2*n个参数,第一个参数为返回值类型,第二个参数为系统调用名称。

6 系统时间和定时

        PC/AT微机系统中提供了用电池供电的实时时钟RT电路支持,使用了MC146818芯片,初始化时,内核读取芯片当前时间和日期信息,转换成从1970年1月1日午夜0时开始计到当前的以秒为单位的时间(UNIX日历时间)。再通过从系统启动开始计数的系统滴答值jiffies,程序就可以唯一确定运行时刻的当前时间值。

        在Linux 0.11内核的初始化过程中,PC机将可编程定时芯片Intel 8254计数器通道0设置成方式3(方波发生器)初始技术支持设置成10ms在输出一个上升沿,触发一次时钟中断请求(IRQ0),这个时间也称为系统滴答或系统时钟周期。每当发生一次时钟中断,jiffies值就增1。时间片的定义就是滴答数。

        进程在内核态运行时不会被调度程序切换(进程在内核态程序中运行时是不可抢占的(nonpreemptive),但当处于用户态程序中运行时是可以被抢占的(preemptive))。Linux系统中可以创建动态定时器,用于软盘马达开启和关闭等定时操作,仅供内核使用,0.11内核中最多可以有64个定时器。

7 Linux进程控制

       上电启动后,引导程序把内核从磁盘上加载到内存中,并让系统进入保护模式,就开始执行初始化程序。系统首先确定如何分配使用系统物理内存,然后调用内核各部分的初始化函数分别对内存管理、中断处理、块设备和字符设备、进程管理以及硬盘和软盘硬件进行初始化处理。Linux 0.11内核中,第一个进程为“手工”建立,其余的进程都由系统调用fork创建新进程,内核程序使用进程标识号(pid)标识每一个进程。对于只有一个cpu的系统,某一时刻只能有一个进程运行,内核通过调度程序分时调度各个进程。Linux内核中,进程通常被称作任务(task),运行在用户空间的程序称作进程。

        内核程序通过进程表对进程进行管理,一个进程在进程表中占有一项。Linux系统中,进程表项是一个task_struct指针,包含用于控制和管理进程的所有信息(进程当前运行的状态信息、信号、进程号、父进程号、运行时间累计值、正在使用的文件和本任务的局部描述符以及任务状态端信息)。


图7.1 进程状态及转换关系

        进程状态及转换关系如图7.1所示。当一个进程的运行时间片用完,系统就会使用调度程序切换到其他程序去执行。同时,只有当进程总“内核态”转移到“睡眠态”时,内核才会进行进程切换操作。为了避免进程切换时造成内核数据错误,内核在执行临街区代码时会禁止一切中断。

        对于进程的初始化,系统处于可运行状态时,程序把自己“手工”移动到任务0(进程0)中运行,并使用fork()调用创建进程1(在进程1中程序继续对应用环境进行初始化,执行shell登录程序)。“移动到任务0中执行”这个过程由宏move_to_user_mode完成,移动之前,系统在对调度程序初始化的过程中,首先对任务0的运行环境进行了设置(任务0数据结构各字段的值)。宏move_to_user_mode的功能就是把运行特权级从内核态的0级变换到用户态的3级,移动过程中,宏move_to_user_mode使用了中断返回指令造成特权级改变的方法。在使用fork()创建新进程时,所有进程都是通过复制进程0得到的,都是进程0的子进程。

        对于新进程的创建,系统首先在任务数组中找出一个还没有被任何进程使用的空项(如果已有64个进程,则fork()系统调用会返回错误),然后系统为新建进程在主内存区中申请一页存放任务的数据结构信息,复制当前进程任务数据结构作为模板(防止未处理完成的新建进程被调度函数执行,设置其状态为不可中断的等待状态),接着修改任务数据结构(父进程、时间片、GDT、代码和数据段基址、限长、内存分页管理页表等),随后若父进程打开则对应文件打开次数增1,GDT中设置TSS,LDT,最后将其设置成可运行状态。

        对于进程的调度,首先扫描任务数组,选中就绪状态中(TASK_RUNNING)运行时间最少的进程,使用任务切换宏函数切换到该进程运行。若所有处于就绪状态进程时间片都用完,系统就根据每个进程的优先级权值priority,从新计算counter=counter/2+priority。每当选出一个新的可运行进程,便进行进程切换。switch_to()宏会把CPU当前状态替换成新进程的状态(current置为新任务指针->长跳转新任务TSS地址出->CPU保存当前任务状态后更新到新任务的状态),整个过程如图7.2所示。


图7.2 任务切换操作示意图

        对于进程的终止,在子进程执行期间,父进程通常使用wait()或waitpid()函数等待某子进程的终止,当被等待子进程进入僵死状态时,父进程把子进程运行使用的时间累加到自己进程中后释放已终止子进程任务数据结构所占用的内存页面,置空子进程在任务数组中的指针项,释放进程所占用的系统资源。

8 Linux系统中堆栈的使用方法

        Linux 0.11系统中使用了四种堆栈。一种是系统引导初始化时临时使用的堆栈;一种是进入保护模式后提供内核程序初始化使用的堆栈;另一种是每个任务通过系统调用,执行内核程序使用的堆栈;最后一种是任务在用户态执行的堆栈。

        使用多个栈或在不同情况下使用不同栈的原因主要有两个:首先是从实模式进入保护模式,CPU对寻址访问方式发生了变化,需要重新设置栈区域;为了解决不同CPU特权级共享使用堆栈带来的保护问题,0级和3级的代码需要不同的栈。

9 Linux 0.11采用的文件系统

        Linux系统引导启动时,默认使用的文件系统是根文件系统。其中包括规定的目录、配置文件、设备驱动程序、开发程序以及所有其他用户数据或文本文件等。一般包括一些子目录如图9.1所示。


图9.1 根文件系统示意图

        Linux 0.11内核所支持的文件系统是MINIX 1.0文件系统,目前Linux系统上使用最广泛的则是ext3或ext4文件系统。当Linux启动盘加载根文件系统时,会根据启动盘上引导扇区第509、510字节处一个字(ROOT_DEV)中的跟文件系统设备号从指定设备中加载根文件系统(若设备号是0,则从引导盘所在的当前驱动器中加载根文件系统)。

10 Linux内核源代码目录结构

        图10.1所示是Linux内核源代码目录结构图,Linux是一种单内核模式的系统,内核中程序练习紧密,依赖和调用关系非常密切。该内核版本含有14个子目录,总共包括102个代码文件,linux努力是源代码主目录,除了图示以外,还有唯一一个Makefile文件。


图10.1 Linux内核源代码目录结构图

11 内核系统与应用程序的关系

        Linux系统中,内核为用户程序提供两方面的支持:系统调用接口和通过开发环境库函数或内核函数与内核进行信息交流。在UNIX类操作系统中,最为普遍使用的是基于POSIX标准的API接口。API与系统调用区别在于:为了实现某一应用程序接口标准,API可以与一个或几个系统调用对应,也可以不使用系统调用。统一的标准保证了程序再不同系统之间的可移植性。

        系统调用是内核与外界接口的最高层,在内核中,每个系统调用都有一个序列号,并且常以宏的形式实现。目前Linux标准库LSB和许多其他标准都不允许应用程序直接访问系统调用宏。

posted @ 2018-08-09 22:41  雾封尘  阅读(2319)  评论(0编辑  收藏  举报