一、C与汇编程序的相互调用

1.1 C函数调用机制

在Linux内核程序boot/head.s执行完基本初始化操作之后,就会跳转去执行init/main.c程序。那么head.s程序是如何把执行控制转交给init/main.c程序的呢?即汇编程序是如何调用执行C语言程序的?
首先来看一下C函数的调用机制、控制权传递方式:函数调用操作包括从一块代码到另一块代码之间的双向数据传递和执行控制转移。数据传递通过函数参数和返回值来进行,此外在进入函数时需要为函数的局部变量分配存储空间并在退出函数时收回这部分空间。Inter 80x86 CPU为控制传递提供了简单的指令,而数据的传递和局部变量存储空间的分配与回收则通过栈操作来实现。

1. 栈帧结构和控制转移权方式

栈被用来传递函数参数、存储返回信息、临时保存寄存器原有值以备恢复以及用来存储局部数据。单个函数调用操作所使用的栈部分被称为栈帧结构。栈帧结构的两端由两个指针来指定。寄存器ebp通常用做帧指针,而esp则用作栈指针。在函数执行过程中,栈指针esp会随着数据的入栈和出栈而移动,函数中对大部分数据的访问都基于帧指针ebp。
image.png
对于函数A调用函数B,传递给B的参数包含在A的栈帧中,当A调用B时,函数A的返回地址(调用返回后继续执行的指令地址)被压入栈中,栈中该位置也明确指明了A栈帧的结束处。而B的栈帧则从随后的栈部分开始,即图中保存帧指针ebp的地方开始。而随后则用于存放任何保存的寄存器值以及函数的临时值。
栈是往低地址方向扩展的,而esp指向当前栈顶处的元素。通过push和pop指令我们可以把数据压入栈中或从栈中弹出。对于没有指定初始值的数据所需要的存储空间,我们可以通过把栈指针递减适当的值来做到,通过增加栈指针可以回收栈中已分配的空间。

二、Linux内核体系结构

2.1 Linux内核模式

操作系统内核的结构模式主要可分为整体式的单内核模式和层次式的为内核模式。Linux0.12则是采用了单内核模式。
在单内核模式的系统中,操作系统所提供服务的流程为:

  1. 应用主程序使用指定的参数值执行系统调用指令(int 0x80),使CPU从用户态切换到核心态
  2. 操作系统根据具体的参数值调用特定的服务程序。
  3. 这些服务程序则根据需要再调用底层的一些支持函数已完成特定的功能。
  4. 在完成了应用程序所要求的服务后,操作系统又使CPU从核心态切换回用户态,从而返回到应用程序中继续执行后面的指令。

概要地将单内核模式的内核分为3个层次:
调用服务的主程序层——执行系统调用的服务层——支持系统调用的底层函数

2.2 Linux内核系统体系结构

Linux内核主要由5个模块构成,分别是:

  1. 进程调度模块——用来负责控制进程对CPU资源的使用。所采取的调度策略是各进程能够公平合理的访问CPU,同时保证内核能及时地执行硬件操作。
  2. 内存管理模块——用于确保所有进程能够安全的共享机器主内存区,同时还支持虚拟内存管理方式,使得Linux支持进程使用比实际内存空间更多的内存容量。并利用文件系统把暂时不用的内存数据块交换到外部存储设备上去,当需要时再交换回来。
  3. 文件系统模块——用于支持对外部设备的驱动和存储。虚拟文件系统模块通过向所有的外部存储设备提供一个通用的文件接口,隐藏了各种硬件模块子系统的不同细节。从而提供并支持与其他操作系统兼容的多种文件系统格式。
  4. 进程间通信模块——用于支持多种进程间的信息交换方式。
  5. 网络接口模块——提供对多种网络通信标准的访问并支持许多网络硬件。

image.png

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

1. 物理内存

image.png
在Linux0.12内核中,为了有效的使用机器中的物理内存,在系统初始化阶段内存被划分为几个功能区域,其中,Linux内核程序占据在物理内存的开始部分。
接下来是供硬盘或软盘等块设备使用的高速缓冲区部分(其中要扣除显示卡内存和ROM BIOS所占用的内存地址范围640KB—1MB)。当一个进程需要读取块设备中的数据时,系统会首先把数据读到高速缓冲区中;当有数据需要写到块设备上去时,系统也是先将数据放到高速缓冲区中,然后由块设备驱动程序写到相应的设备上。
内存的最后部分是可供所有程序随时申请和使用的主内存区。内核程序在使用主内存去时,也同样首先要向内核内存管理模块提出申请,并在申请成功后方能使用。对于含有RAM虚拟盘的系统,主内存区头部还要划去一部分,供虚拟盘存放数据。

2.4 中断机制

1. 中断操作原理

可编程中断控制器PIC是微机系统中管理设备中断请求的管理者。它通过连接到设备的中断请求引脚接受设备发出的中断服务请求信号。当设备激活其中断请求IRQ信号时,PIC立刻会检测到。在同时收到几个设备的中断服务请求的情况下,PIC会对它们进行优先级比较并选出最高优先级的中断请求进行处理。如果此时处理器正在执行一个设备的中断服务过程,那么PIC还需要把选出的中断请求与正在处理的中断请求的优先级进行比较,并基于该比较结果来确定是否向处理器发出一个中断信号。当PIC向处理器的INT引脚发出一个中断信号时,处理器会立刻停下当时所做的事情并询问PIC需要执行那个中断服务请求。PIC则通过向数据总线发出中断请求对应的中断号来告知处理器要执行哪个中断服务过程。处理器则根据读取的中断号通过查询中断向量表(或32位保护模式下的中断描述符表)取得相关设备的中断向量(即中断服务程序的地址)并开始执行中断服务程序。当中断服务程序执行结束,处理器就继续执行被中断信号打断的程序。

2. 中断向量表

CPU是根据中断号获取中断向量值,即对应中断服务程序的入口地址值。为此需要在内存中建立一张查询表,即中断向量表。80x86微机支持256个中断,对应每个中断需要安排一个中断服务程序。在80x86实模式运行模式下,每个中断向量由4字节组成。这4字节指明了一个中断服务程序的段值和段内偏移值。因此整个向量表的长度为1KB。当80x86微机启动时,ROM BIOS中的程序会在物理内存开始地址0x0000:0x0000处初始化并设置中断向量表,而各中断的默认中断服务程序则会在BIOS中给出。由于中断向量表中的向量是按中断号顺序排列,因此给定一个中断号N,那么它对应的中断向量在内存的位置就是0x0000:N×4,及对应的中断服务程序入口地址保存在物理内存0x0000:N×4位置处。

3. Linux内核的中断处理

对于Linux内核来说,中断信号分为两类:硬件中断和软件中断(异常)。每个终端由0~255之间的一个数字来标识。对于中断int0~int31(0x00~0x1f),每个终端的功能由Intel公司固定设定或保留用,属于软件中断。中断int32~int255(0x20~0xff)可以由用户自己设定。
在系统初始化时,内核在head.s程序中首先使用一个哑中断向量对中断描述符表中所有256个描述符进行了默认设置。这个哑中断向量指向一个默认的无中断处理过程。当发生了一个中断而又没有重新设置过该中断向量时就会显示信息“未知中断”。对于系统中需要使用的一些中断,内核会在其继续初始化的处理过程中(init/main.c)重新设置这些中断的中断描述符项,让它们执行对应的实际处理过程。通常,异常中断处理过程(int0~int31)都在trap.c的初始化函数中进行了重新设置,而系统调用中断int128则是在调度程序初始化函数中进行了重新设置(kernel/sched.c)。

4. 标志寄存器的中断标志

为了避免竞争条件和中断对临界代码区的干扰,在Linux 0.12内核代码中许多地方使用了cli和sti指令。cli指令用来复位CPU标志寄存器中的中断标志,使得系统在执行cli指令后不会响应外部中断。sti指令是用来设置标志寄存器中的中断标志,以允许CPU能识别并响应外部设备发出的中断。
当进入可能引起竞争条件的代码区时,内核中就会使用cli指令来关闭对外部中断的响应,而在执行完竞争代码区时内核就会执行sti指令以重新允许CPU响应外部中断。

2.5 Linux的系统调用

1. 系统调用接口

系统调用(通常称为syscalls)接口是Linux内核与上层应用程序进行交互通信的唯一接口。从中断机制的说明可知,用户程序通过直接或间接(通过库函数)调用中断int 0x80,并在eax寄存器中指定系统调用功能号,即可使用内核资源,包括系统硬件资源。不过通常应用程序都是使用具有标准接口定义的C函数库中的函数间接地使用内核的系统调用。
通常,系统调用使用函数形式进行调用,因此可带有一个或多个参数。对于系统调用执行的结果,它会在返回值中表现出来。通常负值表示错误,而0代表成功。在出错的情况下,错误的类型码被存放在全局变量errno中。通过调用库函数perror(),可以打印出该错误码对应的出错字符串信息。
在Linux内核中,每个系统调用都具有唯一的一个系统调用功能号,这些功能号定义在文件include/unistd.h中。

2. 系统调用处理过程

当应用程序经过库函数向内核发出一个中断调用int 0x80时,就开始执行一个系统调用。其中寄存器eax中存放着系统调用号,而携带的参数可依此存放在寄存器ebx、ecx和edx中。处理系统调用中断int 0x80的过程是程序kernel/system_call.s中的system_call。
为了方便执行系统调用,内核源代码在include/unistd.h文件中定义了宏函数_syscalln(),其中n代表携带的参数个数,可以是0~3。若需要传递大块数据给内核,则可以传递这块数据的指针值。
对于include/unistd.h中给出的每个系统调用宏,都有2+2×n个参数。其中第1个参数对应系统调用返回值的类型;第2个参数是系统调用的名称;随后是系统调用所携带参数的类型和名称。这个宏会被扩展成包含内嵌汇编语句的C函数:

  1. int read(int fd,char *buf,int n){
  2. long _res;
  3. _asm_volatile(
  4. "int $0x80"
  5. : "=a" (_res)
  6. : "0" (_NR_read), "b" ((long)(fd)), "c" ((long)(buf)),"d" ((long)(n)));
  7. if(_res>=0)
  8. return int_res;
  9. errno-=_res;
  10. return -1;
  11. }

2.6 Linux进程控制

1. 进程初始化

在boot/目录中,引导程序把内核从磁盘上加载到内存中,并让系统进入保护模式下运行后,就开始执行系统初始化程序init/main.c。该程序首先确定如何分配使用系统物理内存,然后调用内核各部分的初始化函数分别对内存管理、中断管理、块设备和字符设备、进程管理以及硬盘和软盘硬件进行初始化处理。在完成了这些操作之后,系统各部分已经处于可运行状态。此后程序把自己“手工”移动到任务0(进程0)中运行,并使用fork()函数调用首次创建出进程1。在进程1中程序将继续进行应用环境的初始化并执行shell登录程序。而原进程0则会在系统空闲时被调度执行,此时任务0仅执行pause()系统调用,其中又会去执行调度函数。
“移动到任务0中进行”:由宏move_yo_user_mode(include/asm/system.h)完成。它把main.c程序执行流程从内核态(特权级0)移动到用户态(特权级3)的任务0中继续运行。在移动之前,系统在对调度程序的初始化过程(sched_init())中,首先对任务0的运行环境进行了设置。这包括人工预先设置好任务0数据结构各字段的值(include/linux/sched.h)、在全局描述符表中添加任务0的任务状态段(TSS)描述符和局部描述符表(LDT)的段描述符并把它们分别加载到任务寄存器tr和局部描述符表寄存器ldtr中。
内核初始化代码即是任务0的代码,从任务0数据结构中设置的初始化数据可知,任务0的代码段和数据段的基址是0,段限长是640KB。而内核代码段和数据段的基址是0,段限长是16MB,因此任务0的代码段和数据段分别包含在内核代码段和数据段中。内核初始化程序main.c就是任务0中的代码,只是在移动到任务0之前系统正在以内核态特权级0运行着main.c程序。宏move_to_user_mode的功能就是把运行特权级从内核态的0级变换到用户态的3级,但是仍然继续执行原来的代码指令流。
在移动到任务0的过程中,宏move_to_user_mode使用了中断返回指令造成特权级改变的方法。CPU允许低级别代码通过调用门或中断、陷阱门来调用或转移到高级别代码中运行,但反之则不行。因此内核采用了模拟IRET返回低级别代码的方法。
该方法的主要思想是在堆栈中构筑中断返回指令需要的内容,把返回地址的段选择符设置成任务0代码段选择符,其特权级为3。此后执行中断返回指令iret时将导致系统CPU从特权级0跳转到外层的特权级3上运行。宏move_to_user_mode首先往内核堆栈中压入任务0堆栈段(即数据段)选择符和内核堆栈指针。然后压入标志寄存器内容。最后压入任务0代码段选择符和执行中断返回后需要执行的下一条指令的偏移位置。该偏移位置是iret后的一条指令处。

2. 创建新进程

Linux系统中创建新进程使用fork()系统调用。所有进程都是通过复制进程0而得到的,都是进程0的子进程。
在创建新进程的过程中,系统首先在任务数组中找出一个还没被任何进程使用的空项。如果系统已经有64个进程在运行,则fork()系统调用会因为任务数组表中没有可用空项而出错返回。然后系统为新建进程在主内存区中申请一页内存来存放其任务数据结构信息,并复制当前进程任务数据结构中的所有内容作为新进程任务数据结构的模板。为了防止这个还未处理完成的新建进程被调度函数执行,此时应该立刻将新进程状态设置为不可中断的等待状态(TASK_UNINTERRUPTIBLE)。
随后对复制的任务数据结构进行修改。把当前进程设置为新进程的父进程,清除信号位图并复位新进程各统计值,并设置初始运行时间片值为15个时间滴答数(150ms)。接着根据当前进程设置任务状态段(TSS)中各寄存器的值。由于创建进程时新进程返回值应为0,所以需要设置tss.eax=0。新建进程内核态堆栈指针tss.esp0被设置成新进程任务数据结构所在内存页面的顶端,而堆栈段tss.ss0被设置成内核数据段选择符。tss.ldt被设置成局部表描述符在GDT中的索引值。如果当前进程使用了协处理器,则还需要把协处理器的完整状态保存到新进程的tss.i387结构中。
此后系统设置新任务的代码和数据段基址、限长,并复制当前进程内存分页管理的页表,注意此时系统并不为新的进程分配实际的物理内存页面,而是让它共享其父进程的内存页面。只有当父进程或新进程中任意一个有写内存操作时,系统才会为执行写操作的进程分配相关的独自使用的内存页面。这种处理方式称为写时复制技术。
随后如果父进程中有文件是打开的,则应将对应文件的打开次数增1。接着在GDT中设置新任务的TSS和LDT描述符项,其中基地址信息指向新进程任务结构中的tss和ldt。最后再将新任务设置成可运行状态并返回新进程号。

3. 进程调度

内核中的调度程序用于选择系统中下一个要运行的进程。Linux进程是抢占式的,但被抢占的进程仍然处于TASK_RUNNING状态,只是暂时没有被CPU运行。进程的抢占发生在进程处于用户态执行阶段,在内核态执行时是不能被抢占的。在Linux 0.12中采用了基于优先级排队的调度策略。

①. 调度程序

schedule()函数首先扫描任务数组。通过比较每个就绪态(TASK_RUNNING)任务的运行时间递减滴答计数counter的值来确定当前哪个进程运行的时间最少。哪一个的值大就表示运行时间不长,于是就选中该进程,并使用任务切换宏函数切换到该进程运行。
如果此时所有处于TASK_RUNNING状态进程的时间片都已经用完,系统就会根据每个进程的优先权priority,对系统中所有进程(包括正在睡眠的进程)重新计算每个任务需要运行的时间片值counter。计算公式是:Linux内核完全刨析0.12——准备环境 - 图4;这样,正在睡眠的进程被唤醒时就具有较高的时间片counter值。然后schedule()函数重新扫描任务数组中所有处于TASK_RUNNING状态的进程,并重复上述过程,直到选择出一个进程为止。最后调用switch_to()执行实际的进程切换操作。
如果此时没有其他进程可运行,系统就会选择进程0运行。对于Linux 0.12来说,进程0会调用pause()把自己置为可中断的睡眠状态并再次调用schedule()。不过在调用进程运行时,schedule()并不在意进程0处于什么状态。只要系统空闲就调度进程0运行。

②. 进程切换

每当选择出一个新的可运行进程时,schedule()函数就会调用定义在include/asm/system.h中的switch_to()宏执行实际进程切换操作。该宏会把CPU的当前进程状态(上下文)替换成新进程的状态。在进行切换之前,switch_to()首先检查要切换到的进程是否就是当前进程,如果是则什么也不做,直接退出;否则,就首先把内核全局变量current置为新任务的指针,然后长跳转到新任务的任务状态段TSS组成的地址处,造成 CPU执行任务切换操作。此时CPU会把其所有寄存器的状态,保存到当前任务寄存器TR中TSS段选择符所指向的当前进程任务数据结构的tss结构中,然后把新任务状态段选择符所指向的新任务数据结构中tss结构中的寄存器信息恢复到CPU中,系统就正式开始运行新切换的任务了。

4. 终止进程

当一个进程结束了运行或在半途终止了运行,那内核就需要释放该进程所占用的系统资源。包括进程运行时打开的文件、申请的内存等。
当一个用户程序调用exit()系统调用时,就会执行内核函数do_exit()。该函数会首先释放进程代码段和数据段占用的内存页面,关闭进程打开着的所有文件,对进程使用的当前工作目录、根目录和运行程序的i节点进行同步操作。如果进程有子进程,则让init进程作为其所有子进程的父进程。如果进程是一个会话头进程并且有控制终端,则释放控制终端,并向属于该会话的所有进程发送挂断信号SIGHUP,这通常会终止该会话中的所有进程。然后把进程状态置为僵死状态TASK_ZOMBIE。并向其原父进程发送SIGCHLD信号,通知其某个子进程已经终止。最后do_exit()调用调度函数去执行其他进程。由此可见在进程被终止时,它的任务数据结构仍然保留着。因为其父进程还需要使用其中的信息。
在子进程执行期间,父进程通常使用wait()或waitpid()函数等待其某个子进程终止。当等待的子进程被终止并处于僵死状态时,父进程就会把子进程运行所使用的时间累加到自己的进程中,最终释放已终止子进程任务数据结构所占用的内存页面,并置空子进程在任务数组中占用的指针项。

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

1. 堆栈类型和为什么使用不同的堆栈

Linux 0.12系统中共使用了4中堆栈:

  1. 系统引导初始化时临时使用的堆栈。
  2. 进入保护模式之后提供内核程序初始化使用的堆栈,位于内核代码地址空间固定位置处,该堆栈也是后来任务0使用的用户态堆栈。
  3. 每个任务通过系统调用,执行内核程序时使用的堆栈,称之为任务的内核态堆栈,每个任务都有自己独立的内核态堆栈。
  4. 任务在用户态执行的堆栈,位于任务(进程)逻辑地址空间近末端处。

使用多个栈或不同情况下使用不同栈的主要原因有两个:

  1. 由于从实模式进入保护模式,使得CPU对内存寻址访问方式发生了变化,因此需要重新调整设置栈区域。
  2. 为了解决不同CPU特权级共享使用堆栈带来的保护问题,执行0级的内核代码和执行3级的用户代码需要使用不同栈。当一个任务进入内核态运行时,就会使用其TSS段中给出的特权级0的堆栈指针tss.ss0、tss.esp0,即内核态。原用户栈指针会被保存在内核栈中。而当从内核态返回用户态时,就会恢复使用用户态堆栈。

    2. 初始化阶段

    ① 开机初始化(bootsect.S setup.s)

    当bootsect代码被ROM BIOS引导加载到物理内存0x7c00处时,并没有设置堆栈段,当然程序也没有使用堆栈。直到bootsect被移动到0x9000:0处时,才把堆栈段寄存器SS设置为0x9000,堆栈指针esp寄存器设置为0xff00,即堆栈顶端在0x9000:0xff00处。setup.s程序中也沿用了bootsect中设置的堆栈段。这就是系统初始化时临时使用的堆栈。

    ② 进入保护模式时(head.s)

    从head.s程序起,系统开始正式在保护模式下运行。此时堆栈段被设置为内核数据段(0x10),堆栈指针esp设置成指向user_stack数组的顶端,保留了1页内存(4KB)作为堆栈使用。user_stack数组定义在sched.c中,共含有1024个长字。它在物理内存中的位置示意图如图所示。此时该堆栈是内核程序自己使用的堆栈。其中给出的地址是大约值,它们与编译时的实际设置参数有关,这些地址位置是从编译内核时生成的system.map文件中查到的。
    image.png

    ③ 初始化时(main.c)

    在init/main.c程序中,在执行move_to_user_mode()代码把控制权移交给任务0之前,系统一直使用上述堆栈。而在执行过move_to_user_mode()之后,main.c的代码被切化成任务0中执行。通过执行fork()系统调用,main.c中的init()将在任务1中执行,并使用任务1的堆栈。而main()本身则在被切换成为任务0之后,仍然继续使用上述内核程序自己的堆栈作为任务0的用户态堆栈。

    3. 任务的堆栈

    每个任务都有两个堆栈,分别位于用户态和内核态程序的执行,并且分别称为用户态堆栈和内核态堆栈。除了处于不同CPU特权级中,这两个堆栈之间的主要区别在于任务的内核态堆栈很小,所保存的数据量最多不能超过3KB。而任务的用户态堆栈却可以在用户的64MB空间内延申。

    ①在用户态运行

    每个任务(除了任务0和任务1)都有自己的64MB地址空间。当一个任务(进程)刚被创建时,他的用户态堆栈指针被设置在其他地址空间的靠近末端(64MB顶端)部分。实际上末端部分还要包括执行程序的参数和环境变量,然后才是用户堆栈空间。应用程序在用户态下运行时就一直使用这个堆栈。堆栈实际使用的物理内存则是由CPU分页机制确定。由于Linux实现了写时复制功能,因此在进程被创建后,若该进程及其父进程都没有使用堆栈,则两者共享同一堆栈对应的物理内存页面。只有当其中一个进程执行堆栈写操作时内核内存管理程序才会为写操作进程分配新的内存页面。
    image.png

    ②在内核态运行

    每个任务都有自己的内核态堆栈,用于任务在内核代码中执行期间。其所在线性地址中的位置由该任务TSS段中ss0和esp0两个字段指定。ss0是任务内核态堆栈的段选择符,esp0是堆栈栈底指针。因此每当任务从用户代码转移进入内核代码中执行时,任务的内核态栈总是空的。任务内核态堆栈被设置在位于其任务数据结构所在页面的末端,即与任务的任务数据结构(task_struct)放在同一页面内。这是在建立新任务时,fork()程序在任务tss段的内核级堆栈字段中设置的。
    1. p->tss.esp0=PAGE_SIZE+(long)p;
    2. p->tss.ss0=0x10;
    其中,p是新任务的任务数据结构指针,tss是任务状态段结构。内核为新任务申请内存用作保存其task_struct数据结构,而tss结构是task_struct中的一个字段。该任务的内核堆栈段值tss.ss0也被设置成0x10(即内核数据段选择符),而tss.esp0则指向保存task_struct结构页面的末端。

    ③ 任务0和任务1的堆栈

    任务0(空闲进程idle)和任务1(初始化进程init)的堆栈比较特殊。任务0和任务1的代码段和数据段相同,限长也都是640KB,但它们被映射到不同的线性地址范围中。
    任务0的段基地址从线性地址0开始,任务1的段基地址从64MB开始,但是他们全都映射到物理地址0~640KB范围中。这个地址范围就是内核代码和基本数据所存放的地方。
    在执行了move_to_user_mode()之后,任务0和任务1的内核态堆栈分别位于各自任务数据结构所在页面的末端,而任务0的用户态堆栈就是前面进入保护模式后所使用的堆栈,即sched.c的user_stack[]数组的位置。由于任务1在创建时复制了任务0的用户堆栈,因此刚开始时任务0和任务1共享使用同一个用户堆栈空间。但是当任务1开始运行时,由于任务1映射到user_stack[]处的页表项被设置成只读,使得任务1在执行堆栈操作时将会引起写页面异常,从而内核会使用写时复制机制为任务1另行分配主内存去页面作为堆栈空间使用。因此任务0的堆栈需要在任务1开始使用之前保持干净,即任务0此时不能使用堆栈,以确保复制的对战页面中不含有任务0的数据。

    2.8 Linux内核源代码的目录结构

    | linux | | | —- | —- | | |——boot | 系统引导汇编程序 | | |——fs | 文件系统 | | |——include | 头文件(*.h) | | | |——asm | 与CPU体系结构相关的部分 | | | |——linux | Linux内核专用部分 | | | |——sys | 系统数据结构部分 | | |——init | 内核初始化程序 | | |——kernel | 内核初始化程序、信号处理、系统调用等程序 | | | |——blk_drv | 块设备驱动程序 | | | |——chr_drv | 字符设备驱动程序 | | | |——math | 数学协处理器仿真处理程序 | | |——lib | 内核库函数 | | |——mm | 内存管理程序 | | |——tools | 生成内核Image文件的工具程序 |

1. 内核主目录linux

linux目录是源代码的主目录,在该主目录中除了包括所有的14个子目录以外,还含有唯一的一个Makefile文件。该文件是编译辅助工具软件make的参数配置文件。make工具软件的主要用途是通过识别哪些文件已经被修改过,从而自动地决定在一个含有多个源程序文件的程序系统中哪些文件需要被重新编译。

2. 引导启动程序目录boot

boot目录中含有3个汇编语言文件,是内核源代码文件中最先被编译的程序。

  1. bootsect.S程序是磁盘引导块程序,编译后会驻留在磁盘的第一个扇区中(引导扇区:0磁道(柱面),0磁头,第1个扇区)。在PC加电ROM BIOS自检后,将被BIOS加载到内存0x7C00处运行。
  2. setup.s程序主要用于读取机器的硬件配置参数,并把内核模块system移动到适当的内存位置处。
  3. head.s程序会被编译链接在system模块的最前部分,主要进行硬件设备的探测设置和内存管理页面的初始设置工作。

    3. 文件系统目录fs

    Linux使用的是MINIX文件系统,但是又做了改变,MINIX对文件系统采用单线程处理方式,但是Linux采用了多线程方式。fs/目录是文件系统实现程序的目录,共包含18个C语言程序。可将其划分为4个部分:

  4. 高速缓冲区管理

  5. 底层文件操作
  6. 文件数据访问
  7. 文件高层函数。

对于文件系统可以将它看成是内存高速缓冲区的扩展部分。所有对文件系统中数据的访问都要首先读取到高速缓冲区中。本目录中的程序主要用来管理高速缓冲区中缓冲块的使用分配(buffer.c)和块设备上的文件系统。

4. 头文件主目录include

头文件目录中共有32个.h头文件。主目录下有13个,asm子目录有4个,linux子目录有10个,sys子目录有5个。

5. 内核初始化程序目录init

仅包含一个文件main.c。用于执行内核所有的初始化工作,然后移到用户模式创建新进程,并在控制台设备上运行shell程序。

6. 内核程序主目录kernel

linux/kernel目录中共包含12个代码文件和一个Makefile文件,另外还有3个子目录。所有处理任务的程序都保存在kernel/目录中。子目录中包括了低层的设备驱动程序。

  1. 块设备驱动程序子目录kernel/blk_drv
  2. 字符设备驱动程序子目录kernel/chr_drv
  3. 协处理器仿真和操作程序子目录kernel/math

    7. 内核库函数目录lib

    内核库函数用于为内核初始化程序init/main.c运行在用户态的进程提供调用支持。

    8. 内存管理程序目录mm

    主要用于管理程序对主内存的使用,实现了进程逻辑地址线性地址以及线性地址到物理内存地址的映射操作,并通过内存分页管理机制,在进程的虚拟内存页与主内存区的物理内存页之间建立了对应关系,同时还真正实现了虚拟存储技术。

    9. 编译内核工具程序目录tools

    该目录下的build.c程序用于将Linux各个目录中被分别编译生成的目标代码连接合并成一个可运行的内核映像文件image。