您的位置:首页 > 运维架构 > Linux

深入介绍Linux内核(九)续

2007-05-23 15:09 302 查看
深入介绍Linux内核(九)续
2007年05月08日 21:09
5.8 Linux系统中堆栈的使用方法

本节內容概要描述了Linux內核从开机引导到系统正常执行过程中对堆栈的使用方式。这部分內容的說明与內核代码关系比较密切,可以先跳过。在开始閱读相应代码时再回来仔细研究。

Linux 0.12系统中共使用了四种堆栈。一种是系统开机初始化时临时使用的堆栈;一种己进入保护模式之后提供內核程式初始化使用的堆栈,位於內核代码位址空间固 定位置处。该堆栈也是后来任务0使用的用戶态堆栈;另一种是每个任务透过系统呼叫,执行內核程式时使用的堆栈;我们称之为任务的內核态堆栈。每个任务都有 自己独立的內核态堆栈;最后一种是任务在用戶态执行的堆栈,位於任务(行程)逻辑位址空间近末端处。

使用多个堆栈或在不同情況下使用不同堆栈的主要原因有两个。首先是由於从真实模式进入保护模式,使得CPU对记忆体定址存取方式发生了变化,因此需要重新 调整设置堆栈区域。另外,为了解決不同CPU特权级共用使用堆栈带来的保护问题,执行0级的內核代码和执行3级的用戶代码需要使用不同的堆栈。当一个任务 进入內核态执行时,就会使用其TSS段中给出的特权级0的堆栈指标tss.ss ( )、tss.esp( ),即内核堆栈。原用户堆栈指标会被保存在内核堆栈中。而当初从内核态返回用户态时,就会恢复使用用户态的堆栈。下面分别对它们进行说明。

5.8.1初始化陪段

开机初始化时(bootsect.s,setup.s)

当bootsect代码被ROM BIOS开机载入到实体记忆体0x7c00虞睛,并没有设置堆栈,当然程式也没有使用堆栈。直到bootsect被移勤到Ox9000:O处时,才把堆栈 段暂存器SS设置为Ox9000,堆栈指标esp暂存器设置为Oxff00,也即堆栈顶端在Ox9000:0xff00处,参见 boot/bootsect.s第61、62行。Setup.s程式中也沿用了bootsect设置的堆栈段。这就是系统初始化时临时使用的堆栈。

进入保护模式时fhead.s

从head.s程式起,系统开始正式在保护模式下执行。此时堆栈段被设置为内核资料段(0x10),堆栈指标esp设置成指向user_stack阵列的 顶端(参兄head.s,第3l行),保留了1页记忆体(4K)作为堆栈使用。user_stack阵列定义在sched.c的67- -72行,共含有1024个字。它在实体记忆体中的位置示意图可参见下图5-24所示。此时堆栈是内核程式自己使用的堆栈。其中的给出位址是大约值,它们 与编译时的实际设置参数有关。这些位址位置是从编译内核时生成的system.map楷案中查到的。



初始化阶段(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的用户态堆栈。关于任务0所使用堆栈的详细描述後请见后面说明。

5.8.2任务的堆栈

每个任务都有两个堆栈,分别用于用户态和内核态程式的执行,并且分别称为用户态堆栈和内核态堆栈。除了处于不同CPU特权极中,这两个堆栈之间的主要区别 在於任移的内核太堆栈很小,所保存的资料量最多不能超过(4096 – 任务资料结构区块)个位元组,大约为3K位元组。而任务的 用户态堆栈却可以在用户的64MB空同内延伸。

在用戶态执行时

每个任务(除了任务0任务1)有自己的64MB位址空间。当一个任务(行程)刚被建立时,它的用户态堆栈指标被设置在其位址空间的靠近末端(64MB顶 端)部分。实际上末端部分还要包括执行程式的参数和环境变数,然后才是用戶堆栈空间,见图5-25所示 。应用程式在用戶态下执行时就一直使用这个堆栈。堆栈实际使用的实体记忆则由CPU分页机制确定。由於Linux实现了写时复制功能(Copy on Write),因此在行程被建立后,若该行程及其父行程都沒有使用堆栈,则两者共用同一堆栈对应的实体记忆体页面。只有当其中一个行程执行堆栈写操作(例 如push操作)时內核记忆体管理程式才会为写操作行程分配新的记忆体页面。而行程 0和行程1的用戶堆栈比较特殊,见后面說明。



在内核态执行时

每个任务有其自己的内核态堆栈,用於任务在內核代码中执行期间。其所在线性位址中的位置由该任务TSS段中ss0和esp0两个栏位指定。ss0是任务內 核态堆栈的段选择符,esp0是堆栈栈底指标。因此每当任务从用戶代码转移进入內核代码中执行时,任务的內核态堆栈总是空的。任务內核态堆栈被设置在位於 其任务资料结构所在页面的末端,即与任务的任务资料结构(task_struct)放在同一页面內。这是在建立新任务时,fork( )程式在任务tss段的內核级堆栈栏位(tss.esp0和tss.ss0)中设置的,参见kernel/fork.c,92行:

p->tss.espO = PAGE_SIZE + (1ong)p ;
p->tss.ssO = 0x10 ;

其中p是新任务的任务资料结构指标,tss是任务状态段结构。內核为新任务申请记忆体用作保存其task_struct结构资料,而tss结构(段)是 task_struct中的一个栏位。该任务的 内核堆栈段值tss.ss0也被设置成为0x10(即內核资料段选择符),而tss.esp0 则指向保存task_struct结构页面的末端。见图5-26所示。实际上tss.espO被设置指向该页面(外)上一位元组处(图中堆栈底处)。这是 因为Intel CPU执行堆栈操作时是先递減堆栈指标esp值,然后在esp指标处保存入堆栈內容。



为什麼从主记忆体区申请得来的用於保存任务资料结构的一页记忆体也能被设置成內核资料段中的资料呢,也即tss.ss0为什麼能被设置成0x10呢? 这是因为用戶內核态堆栈仍然属于内核资料空间。我们可以从內核代码段的长度范围来說明。在head.s程式的末端,分別设置了內核代码段和资料段的描述 符,段长度都被设置成16MB。这个长度值是Linux 0.12內核所能支持的最大实体记忆体长度(参见head.s,110行开始的注释)。因此,內核代码可以定址到整个实体记忆体范围中的任何位置,当然也 包括主记忆体区。每当任务执行內核程式而需要使用其內核堆栈时,CPU就会利用TSS结构把它的內核态堆栈设置成由tss.ss0和tss.espO这两 个值构成。在任务切換时,老任务的內核堆栈指标esp0不会被保存。对CPU来讲,这两个值是唯读的。因此每当一个任务进入內核态执行时,其內核态堆栈总 是空。

任务0和任务1的堆栈

任务0(空閒行程idle)和任务1(初始化行程init)的堆栈比较特殊,需要特別予以說明。任务0和任务1的代码段和资料段相同,限长也都是 640KB,但它们被映射到不同的线性位址范围中。任务0的段基底位址从线性位址。开始,而任务1的段基底位址从64MB开始。但是它们全都映射到实体位 址O- -640KB范围中。这个位址范围也就是內核代码和基本资料所存放的地方,在执行了move_to_user_mode( ),任务O和任务1的內核态堆栈分別位於各自
任务资料结构所在页面的末端,而任务0的用戶态堆栈就是前面进入保护模式后所使用的堆栈,即sched.c的user_stack[]阵列的位置。由于任 务1在建立时复制了任务0的用戶堆栈,因此刚开始时任务0和任务l共用使用同一个用戶堆栈空间。但是当任务1开始执行,由于任务1映射到 user_stack[]处的页表项被设置成唯读,使得任务l在执行堆栈操作时将会引起写页面異常,从而內核会使用写时复制机制²为任务1另行分配主记忆 体区页面作为堆栈空间使用。只有到此时,任务1才开始使用自己独立的用戶堆栈记忆体页面。因此任务0的堆栈需要在任务1实际开始使用之前保持“干淨”,即 任务0此时不能使用堆栈,以确保复制的堆栈页面中不含有任务0的资料。

任务0的內核态堆栈是在其人工设置的初始化任务资料结构中指定的,而它的用戶态堆栈是在执行move_to_user_mode( )时,在类此iret返回之前的堆栈中设置的,参见图5-22所示。我们知道,当进行特权级会发生变化的控制权转移时,目的代码会使用新特权级的堆栈,而 原特权级代码堆栈指标将保留在新堆栈中。因此这里先把任务0用户堆栈指标压入当前处於特权级O的堆栈中,同时把代码指标也压入堆栈,然后执行IRET指令 即可实现把控制权从特权级0的代码转移到特权级3的任务O中。在这个人工设置內容的堆栈中,原esp值被设
置成仍然是user_stack中原来的位置值,而原ss段选择符被设置成0x17,即设置成用戶态区域表LDT中的资料段选择符。然后把任务0代码段选 择符0xlf压入堆栈作为堆栈中原CS 段的选择符,把下一条指令的指标作为原EIP压入堆栈。这樣,透过执行IRET指令即可“返回”到任务0的代码中继续执行了。

5.8.3 任务內核态堆栈与用戶态堆栈之间的切換

在Linux 0.12系统中,所有中断服务程式部属於內核代码。如果一个中断產生时任务正在用戶代码中执行,那麼该中断就会引起CPU特权级从3级到O级的变化,此时 CPU就会进行用戶态堆栈到內核态堆栈的切換操作。CPU会从当前任务的任务状态段TSS中取得新堆栈的段选择符和偏移值。因为中断服务程式在內核中,属 於0级特权级代码,所以48Bit的內核态堆栈指标会从TSS的ss0和espO栏位中获得。在定位了新堆栈(內核态堆栈)之后,CPU就会首先把原用戶 态堆栈指标ss和esp压入內核态堆栈,随后把标志寄存器eflags的內容和返回位置cs、eip压入核态堆栈。

內核的系统呼叫是一个软件中断,因此任务呼叫系统呼叫时就会进入內核並执行內核中的中断服务代码。此时內核代码就会使用该任务的內核态堆栈进行操作。同 樣,当进入內核程式时,由於特权级別发生了改变(从用戶态转到內核态),用戶态堆栈的堆栈段和堆栈指标以及eflags会被保存在任务的內核态堆栈中。而 在执行iret退出內核程式返回到用戶程式时,将恢复用戶态的堆栈和eflags。这个过程见图5-27所示。



如果一个任务正在內核态中执行,那么若CPU回应中断就不再需要进行堆栈切換操作,因为此时该任务执行的內核代码已经在使用內核态堆栈,並且不涉及优先级別的变化,所以CPU 直把eflags和中断返回指标cs、eip压入当前內核态堆栈,然后执行中断服务过程。

5.9 Linux 0.12用的档案系统

內核代码若要正常执行就需要档案系统的支援。用於向內核提供最基本资讯和支援的是根档案系统,即Linux系统引导啟动时,预设使用的档案系统是根档案系 统。其中包括作业系统最起码的一些配置档和命令执行程式。对於Linux系统中使用的UNIX类档案系釉 其中主要包括一些规定的目錄、配置档、装置驱动程式、开发程式以及所有其他用戶资料或文字档案等。其中一般都包括以下一些子目錄和档案:

etc/ 目錄主要含有一些系统配置档;
dev/ 含有装置特殊档,用于使用档操作语句操作装置;
bin/ 存放系统执行程式。列如sh、mkfs、fdisk等;
usr/ 存放程式库函数、手册和其他一些文件;
usr/bin 存放用戶常用的普通命令;
var/ 用於存放系统执行时可变的资料或者是日誌等资讯。

存放档案系统的装置就是档案系统装置。比如,对于一般使用的Windows 2000作业系统,硬碟C就是档案系统装置,而硬碟上按一定规则存放的档案就组成档案系统,Windows 2000有NTFS或FAT32等档案系统。而Linux 0.12內核所支援的档案系统是MINIX 1.0档案系统。目前Linux系统上使用最广泛的则是ext2或ext3档案系统。

对於第l章中介绍对于在软碟上执行的Linux 0.12系统,它由简单的2张软碟组成:bootimage磁碟和rootimage磁碟。bootimage是开机啟动Image档,其中主要包括磁片 开机磁区代码、作业系统载入程式和內核执行代码。rootimage就是用於向內核提供最基本支援的根档案系统。这两个磁碟合起来就相当於一张可啟动的 DOS作业系统碟。

当linux啟动磁碟载入根档案系统时,会根据啟动磁碟上开机磁区第509、51O位元组处一个字(ROOT_DEV)中的根档案系统装置号从指定的装置 中载入根档案系统。如果这个装置号是0的话,则表示需要从开机碟所在当前驱动器中载入根档案系统。若该装置号是一个硬碟分区装置号的话,就会从该指定硬碟 分区中载入根档案系统。

5.10內核原始码的目錄结构

由於Linux內核是种內核模式的系统,因此,內核中所有的程式几乎都有紧密的关联,它们之间的依赖和呼叫关系非常密切。所以在閱读一个原始码档时往往需要参閱其他相关的档案。因此有必要在开始閱读內核原始码之前,先熟悉一下原始码档的目錄结构和安排。

这裡我们首先列出Linux內核完整的原始码目錄,包括其中的子目錄。然后逐一介绍各个目錄中所包含程式的主要功能,使得整个內核原始码的安排形式能在我们的头脑中建立起一 大概的框架,以便於下一章开始的原始码閱读工作。

当我们使用tar命令将linux-0.12.tar.gz解开时,內核原始码档被放到了linux/目錄中。其中的目录结构见图5-28所示:



该內核版本的原始码目錄中含有14个子目錄,总共包括102个代码档。下面逐个对这些子目錄中的內容进行描述。

5.10.1 内核目录linux
linux目錄是原始码的主目录,在该主目錄中除了包括所有的14个子目錄以外,还含有唯一的一个Makefile档。该档是编译辅助工具软体make的 参数配置档。make工具软体的主要用途是透过识別哪些档案已被修改过,从而自动地決定在一个含有多个根源程式档的程式系统中哪些档案需要被重新编译。因 此,make工具软体是程式专案的管理软件。

linux目錄下的这个Makefile档还巢状呼叫了所有子目錄中包含的Makefile档,这樣,当linux目錄(包括子目錄)下的任何档被修改过 时,make都会对其进行重新编译。因此为了编译整个內核所有的原始码档,只要在linux目錄下执行一次make软体即可。

5.10.2 开机啟动程式目录boot
boot目錄中含有3个组合语言档,是內核原始码档中最先被编译的程式。这3个程式完成的主要功能是当计算机加电时开机內核啟动,将內核代码载入到记忆体 中,並做一些进入32位元保护执行方式前的系统初始化工作。其中bootsect.s和setup.s程式需要使用as86 软件来编译,使用的是as86的组合语言格式(与微软的类似),而head.s需要用GNU as来编译,使用的是AT&T格式的组合语言。这两种组合语言在下一章的代码注释裡以及代码列表后面的說明中会有简单的介绍。

Bootsect.s程式是磁碟开机区块程式,编译后会驻留在磁碟的第一个磁区中(开机磁区,O磁轨(柱面),0磁头,第l个磁区)。在PC机加电ROM BIOS自检后,将被BIOS载入到记忆体0x7C00处开始执行。
Setup.s程式主要用于读取机器的硬件配置参数,並把內核模组system移动到适当的记忆体位置处。

Head.s程式会被编译连接在system模组的最前部分,主要进行硬体装置的探测设置和记忆体管理页面的初始设置工作。

5.10.3 档案系统目录S

Linux 0.12內核的档案系统採用了1.0版的MINIX档案系统,这是由於Linux是在MINIX系统上开发的,採用MINIX档案系统便於进行交叉编译, 並且可以从MINIX中载入Linux分区。虽然使用的是MINIX档案系统,但Linux对其处理方式与MINIX系统不同。主要的区別在於MINIX 对档案系统採用单执行绪处理方式,而Linux则採用了多执行绪方式。由於採用了多执行绪处理方式,Linux程式就必须处理多执行绪带来的竞爭条件、锁 死等问题,因此Linux档案系统代码要比MINIX系统的复杂得多。为了避免竞爭条件的发生,Linux系统对资源分配进行了严格地检查,並且在內核模 式下执行时,如果任务沒有主动睡眠(呼叫sleep( )),就不让內核切換任务。

Fs/目錄是档案系统实现程式的目錄,共包含18个C语言程式。这些程式之间的主要参照引用关系见 5-29所示图中每个方框代表一个档案,从上到下按基本按引用关系放置。其中各档案名均略去了尾码.c,虛框中是的程式档不属於档案系统,带箭头的线条表 示引用关系,粗線条表示有相互引用关系。



由图可以看出,该目錄中程式可以划分成四个部分:高速缓冲区管理、低层档操作、档资料存取和档高层函数,在对本目錄中档案进行注释說明时,我们也将分成这四个部分来描述。

对於档案系统,我们可以将它看成是记忆体高速缓冲区的扩展部分。所有对档案系统中资料的存取,都需要首先读取到高速缓冲区中。本目錄中的程式主要用来管理 高速缓冲区中缓冲区块的使用分配和区块装置上的档案系统。管理高速缓冲区的程式是buffer.c,而其他程式则主要都是用於档案系统管理。

在file table.c档中,目前仅定义了一个档案控制码(描述符)结构阵列。ioctl.c档将引用kernel/chr_drv/tty.c中的函数,实现字 元装置的io控制功能。Exec.c程式主要包含一个执行程式函数do_execve( ),它是所有exec( )函数簇中的主要函数。fcntl.c程式用于实现档i/o控制的系统呼叫函数。read_write.c程式用於实现档案读/写和定位三个系统呼叫函 数。Stat.c程式中实现了两个获取档案状态的系统呼叫函数。Open.c程式主要包含实现修改档案属性和建立与关闭档案的系统呼叫函数。

char_dev.c 主要包含字元装置读写函数rw_char()。pipe.c程式中包含管道读写函数和建立管道的系统呼叫。file_dev.c程式中包含基於i节点和描 述符结构的档案读写函数。namei.c程式主要包括档案系统中目錄名和档案名的操作函数和系统呼叫函数。block_dev.c套件程式含块资料读和写 函数。Inode.c程式中包含针对档案系统i节点操作的函数。truncate.c程式用於在刪除档案时释放档案所佔用的装置资料空间。 Bitmap.c程式用於处理档案系统中i节点和逻辑资料区块的点阵图。super.c程式中包含对档案系统超级区块的处理函数。buffer.c程式主 要用於对记忆体高速缓冲区进行处理。虛框中的ll_rw_block是区块装置的底层读函数,它並不在fs目錄中,而是 kernel/blk_drv/ll_rw_block.c中的区块装置读写驱动函数。放在这里裡只是让我们清楚的看到,档案系统对於区块装置中资料的读 写,都需要透过高速缓冲区与区块装置的驱动程式(ll_rw_block())来操作来进行,档案系统程式集本身並不直接与区块装置的驱动程式打交道。

在对程式进行注释过程中,我们将另外给出这些档案中各个主要函数之间的呼叫层次关系。

5.10.4 标头档主目錄include

标头档目錄中总共有32个.h标头档。其中主目錄下有13个,asm子目錄中有4个,linux子目錄中有lO个,sys子目錄中有5个。这些标头档各自的功能见如下简述,具体的作用和 所包含的资讯请参见对标头档的注释一章。

a.out标头档,定义了a.out执行档格式和一些巨集。
常数符号标头档,目前仅定义了i节点中i_mode栏位的各标志位元。
字元类号标头档。定义了一些有关字元类型判断和转換的巨集。
错误号标头档。包含系统中各种出错号。(Linus从minix中引进的)。
档案控制标头档。用於档及其描述符的操作控制常数符号的定义。
信号标头档。定义信号符号常数,信号结构以及信号操作函数原型。
标準参数标头档。以巨集的形式定义变数参数列表。主要說明了一个类型 (va_list)和三个巨集(va_start,va_arg和va_end),用于vsprintf、vprintf’Vfprintf函数。
标準定义标头档。定义了NULL,offsetof(TYPE,MEMBER)。
字串标头挡。主要定义了一些有关字串操作的嵌入函数。
终端输入输出标头档。主要定义控制非同步通信口的终端介面。
时间类型标头档。其中最主要定义了tm结构和一些有关时间的函数原形。
Linux标準标头档。定义了各种符号常数和类型,並声明了各种函数。如定义了_IBRARY__,则还包括系统呼叫号和內嵌组合_syscallO( )等。
用戶时间标头档。定义了存取和修改时问结构以及utime( )原型。

体系结构相关标头档子目录include/asm

这些标头档主要定义了一些CPU体系结构密切相关的资料结构、巨集函数和变数。共4个档案。

io标头档。以巨集的嵌入组合语言程式形式定义对io端口操作的函数。
记忆体拷贝标头档。含有memcpy( )嵌入式组合巨集函数。
段操作标头档。定义了有关段寄存器操作的嵌入式组合函数。
系统标头档。定义了设置或修改描述符/中断门等的嵌入式组合巨集。

Linux内核专用标头档子目录include/linux

內核配置标头档。定义键盘语言和硬碟类型(HD_TYPE)可选项。
软盘机标头档。含有软碟控制卡参数的一些定义。
档案系统标头档。定义档案表结构(file,buffer_head,m_inode等)。
硬碟参数标头档。定义存取硬碟寄存器端口,状态码,分区表等信息。
<]inux/head.h> head标头档,定义了段描述符的简单结构,和几个选择符常数。
內核标头档。含有一些內核常用函数的原形定义。
记忆体管理标头档。含有页面大小定义和一些页面释放函数原型。
程式标头档,定义了任务结构task_struct、初始任务0的资料。
还有一些有关描述符参数设置和获取的嵌入式组合函数巨集式。
系统呼叫标头档。含有72个系统呼叫C函数处理程式,以‘sys_’开头。
tty标头档,定义了有关tty_io,串列通信方面的参数、常数。

系统专用资料结构子目录include/sys

档案状态标头档。含有档或档案系统状态结构stat{ }和常数。
定义了行程中执行时间结构tms以及times( )函数原型。
类型标头档。定义了基本的系统资料类型。
系统名称结构标头档o
等待呼叫标头档。定义系统呼叫wait( )核waitpid( )及相关常数符号。

5.10.5 內核初始化程式目錄init

该目錄中仅包含一个档main.c。用于执行內核所有的初始化工作,然后移到用戶模式建立新行程,并在控台装置上执行shell程式。

程式首先根据机器记忆体的多少对缓冲区记忆体容量进行分配,如果还设置了要使用虛拟碟,则在缓冲区记忆体后面也为它留下空间。之后就进行所有硬件的初始化 工作,包括人工建立第一个任务(task 0) ,並设置了中断允许标志。在执行从核心态移到用戶态之后,系统第一次呼叫建立行程函数fork( ),建立出一个用於执行init( )的行程,在该子行程中,系统将进行主控台环境设置,並且在生成一个子行程用来执行shell程式。

5.10.6 內核程式主目黪kernel

Linux/kernel目錄中共包含12个代码档和一个Makefile档,另外还有3个子目錄。所有处理任务的程式都保存在kernel/目錄中,其 中包括象fork、exit、调度程式以及一些系统呼叫程式等。还包括处理中断異常和陷阱的处理过程。子目錄中包括了低层的装置驱动程式,如 get_hd_block和try_write等。由于这些档中代码之间呼叫关系复杂,因此这裡就不详细列出各档之间的引用关系图,但仍然可以进行大概分 类,见图5-30所示。



asm.s程式是用於处理系统硬件异常所引起的中断,对各硬件異常的实际处理程式则是在traps.c档中,在各个中断处理过程中,将分別呼叫traps.c中相应的C语言处理函数。

exit.c程式主要包括用於处理行程终止的系统呼叫。包含行程释放、会话(行程组)终止和程式退出处理函数以及杀死行程、终止行程、掛起行程等系统呼叫函数。

fork.c程式给出了sys_fk( )系统呼叫中使用了两个C语言函数:
find_empty_process( )和copy ocess( )。

mktime.c套件程式含一个内核使用的时间函数mktime( ),用於计算从1970年1月l日0时起到开机当日的秒数,作为开机秒时间。仅在init/main.c中被呼叫一次。

panic.套件程式含一个显示内核出错资讯並停机的函数panic( )。
printk.c套件程式含一个内核专用资讯显示函数printk( )。
sched.c程式中包括有关调度的基本函数(sleep_on、wakeup、schedule等)以及一些简单的系统呼叫函数。另外还有几个与定时相关的软碟操作函数。

signal.c程式中包括了有关信号处理的4个系统呼叫以及一个在对应的中断处理程式中处理信号的函数do_signal( )。
sys.c程式包括很多系统呼叫函数,其中有些还沒有实现。
system_call.s程式实现了Linux系统呼叫(int Ox80)的介面处理过程,实际的处理过程则包含在各系统统呼叫相应的C语言处理函数中,这些处理函数分佈在整个Linux內核代码中。

vsprintf.c程式实现了现在已经归入标準程式库函数中的字串格式化函数。

区块装置驱动程式子目录kernel/blk_drv

通常情況下,用戶是透过档案系统来存取装置的,因此装置驱动程式为档案系统实现了呼叫介面。在使用区块装置时,由於其资料吞吐量大,为了能夠高效率地使用 区块装置上的资料,在用戶行程与区块装置之间使用了高速缓冲机制。在存取区块装置上的资料时,系统首先以资料块的形式把区块装置上的资料读入到高速缓冲区 中,然后在提供给用戶。blk_drv子目錄共包含4个c档和标头档。标头档blk.h由于是区块装置程式专用的,所以与C档放在一起。这几个档之间的大 致关系,见图5-3l所示。



字元装置驱动程式子目錄kernel/chr_drv

字元装置程式子目錄共含有4个C语言程式和2个组合语言程式档。这些档案实现了对序列端口rs-232、串列终端、键盘和主控台终端装置的驱动。图5-32是这些档案之间的大致呼叫层关系。



tty_iov.c程式中包含tty字装置读函数tty_read( )和写函数tty_write( ),为档案系统提供了上层存取介面。另外还包括在串列中断处理过程中呼叫的C函数do_tty_interrupt( ),该函数将会在中断类型为读字元的处理中被呼叫。

Console.c档主要包含主控台初始化程式和主控台写函数con_write( ),用於被tty装置呼叫。还包含对显示器和键盘中断的初始化设置程式con_init( )。

rs_io.s组合语言程式用于实现两个串列介面的中断处理程式。该中断处理程式会根据从中断标识寄存器(端口0x3fa或0x2fa)中取得的4种中断类型分別进行处理,並在处理中断类型为读字元的代码中呼叫do_tty_interrupt( )。

serial.c用於对非同步串列通信晶片UART进行初始化操作,並设置两个通信端口的中断向量。另外还包括tty用于往串口输出的rs_write( )函数。

tty_ioctl.c程式实现了tty的io控制介面函数tty_ioctl( )以及对termio(s)终端io结构的读写函数,並会在实现系统呼叫sys_ioctl( )的fs/ioctl.c程式中被呼叫。

keyboard.s程式主要实现了键盘中断处理过程keyboard_interrupt。

辅助运算器模拟和操作程式子目录kernel/math

该子目錄中目前仅有一个C程式式math_emulate.c。其中的math_emulate( )函数是中断int7的中断处理程式呼叫的C函数。当机器中沒有数学辅助运算器,而CPU卻又执行了辅助运算器的指令时,就会引发该中断。因此,使用该中 断就可以用软体来模拟辅佐算器的功能。本文章所讨论的內核版本还沒有包含有显辅助运算器的模拟代码。本程式中只是列印一条出错资讯,並向用戶程式发送一个 辅助运算器错误信号SIGFPE。

5.10.7内核程式库函数目录lib

与普通用戶程式不同,內核代码不能使用标準C函数库及其他一些函数库。主要原因是由於完整的C函数库很大。因此在內核原始码中有专门一个lib/目錄提供 內核需要用到的一些函数。內核函数库用於为內核初始化程式init/main.c执行在用戶态的行程(行程0、1)提供呼叫支援。它与普通靜态库的实现方 法完全一樣。读者可从中了解一般libc函数库的基本组成原理。在lib/目錄中共有12个C语言档,除了一个由tytso编制的malloc.c程式较 长以外,其他的程式很短,有的只有一二行代码,实现了一些系统呼叫的介面函数。

这些档中主要包括有退出函数_exi.t( ),关闭档案函数close(fd)、复制档案描述符函数dup( )、档案开启函数open( )、写入档案函数write( )、执行程式函数execve( )、记忆体分配由malloc( )、等待子行程状态函数wait( )、建立会话系统呼叫setsid( )以及在include/string.h中实现的所有字串操作函数。

5.10.8 记忆体管主等程式目錄mm

该目錄包括3个代码档。主要用於管理程式对主记忆体区的使用,实现了行程逻辑位址到線性位址以及線性位址到实体记忆体位址的映射操作,並透过记忆体分页管理机制,在行程的虛拟记忆体页与主记忆体区的实体记忆体页之间建立了对应关系,同时还真正实现了虛拟储存技术。

Linux內核对记忆体的处理使用了分页和分段两种方式。首先是将386的4G虛拟位址空间分割成64个段,每个段64MB。所有內核程式佔用其中第一个 段,並且实体位址与该段线位址相同。然后每个任务分配一个段使用。分页机制用於把指定的实体记忆体页面映射到段內,检测fork建立的任何重复的拷贝,並 执行写时复制机制。

Page.s档包括记忆体页面異常中断(int 14)处理程式,主要用於处理程式由於缺页而引起的页面中断和存取非法位址而引起的页保护。

Memory.c程式包括记忆体进行初始化的函数mem_init( ),由page.s的记忆体处理中断程序呼叫的do_no_page( )和do_wp_page( )函数。在建立新行程而执行复制行程操作时,即使用该档中的记忆体处理函数来分配管理记忆体空间。

swap.c程式用於管理主记忆体中实体页面和高速二级储存(硬碟)空间之间的页面交換。当主记忆体空间不够用时就可以先把暂时不用的记忆体页面保存到硬 碟中。当发生缺页異常时就首先在硬碟中查看要求的页面是否在硬碟交換空间中,若存在则把页面从交換空间直接读入记忆体中。

5.10.9 编译內核具私式目錄

该目錄下的build.c程式用于将Linux各个目錄中被分別编译生成的目标代码连接合併成一个可执行的內核映射档image。其具体的功能可参见下一章內容。

5.11 內核系统与应用程式的关系

在Linux系统中,內核为用户程式提供了两方面的支援。其一是系统呼叫介面(在第5章中說明),也即中断呼叫int 0x80;另一方面是透过开发环境程式库函数或內核程式库函数与內核进行资讯交流。不过內核程式库函数仅供內核建立的任务0和任务l使用,它们最终还是去 呼叫系统呼叫。因此內核对所有用戶程式或行程实际上只提供系统呼叫这一种统一的介面。lib/目錄下內核程式库函数代码的实现方法与基本C函数库libc 中类似函数的实现方法基本相同,为了使用内核资源,最终都是透过內嵌组合代码呼叫了內核系统呼叫功能,参见图5-4所示。

系统呼叫主要提供给系统软件程式设计或者用於程式库函数的实现。而一般用戶开发的程式则是透过呼叫象libc等库中函数来存取內核资源。这些程式库中的函 数或资源通常被称为应用程式编成介面(API) 。其中定义了应用程式使用的一组标準编成介面。透过呼叫这些库中的程式,应用程式码能夠完成各种常用工作,例如,打开和关闭对档案或装置的存取、进行科学 计算、出错处理以及存取群组和用戶标识号ID等系统信息。

在UNIX类作业系统中,最为普遍使用的是基於POSIX标準的API介面。Linux当然也不例外。API与系统呼叫的区別在於:为了实现某一应用程式 介面标準,例如POSIX,其中的API可以与一个系统呼叫对应,也可能由几个系统呼叫的功能共同实现。当然某些API函数可能根本就不需要使用系统呼 叫,即不使用內核功能,因此函数库可看作是实现像POSIX标準的主体介面,应用程式不用管它与系统呼叫之间到底在什麼关系。不管一个作业系统提供的系统 呼叫是多麼的不同或有区別,只要它尊循同一个API标準,那麼应用程式就在这些作业系统之问具有可攜性。

系统呼叫是內核与外界介面的最高层。在內核中,每个系统呼叫都有一个序列号(在include/unistd 标头档中定义),並且常以巨集的形式实现,应用程式不应该直接使用系统呼叫,因为这樣的话,程式的移植性就不好了。因此目前Linux标準库LSB (Lir Standard Base)和许多其他标準都不允许应用程式直接存取系统呼叫巨集。系统呼叫的有关文档可参见Linux作业系统的線上手冊的第2部分。

程式库函数一般包括C语言沒有提供的执行进阶功能的用戶级函数,例如输入/输出和字串处理函数。某些程式库函数只是系统呼叫的增強功能版。例如,标準 I/O程式库函数fopen fclose提供了与系统呼叫open和close类似的功能,但卻是在更高的层次上。这种情況下,系统呼叫通常能提供比程式库函数略微好一些的性能,但 是程式函数卻能提供更多的功能,而且更具检错能力。系统提供的程式库函数有关文档可参见作业系统的線上手冊第3部分。

5.12 linux/Makefile 档

从本节起,我们开始对內核原始码档进行注释。首先注释linux目錄下遇到的第一个档Makefile。后续章节将按照这裡类似的描述结构进行注释。

5.12.1功能描述

Makefile档相当于程式便宜过程中的批次档案。是工具程式make执行时的输入资料档案。只要在含有Makefile的当前目录中键入make命令,它就会依据Makefile档中的设置对根源程式或目标代码档进行编译、连接或进行安装等活动。

make工具程式能[ 地确定一个大程式系统中那些程式档需要被重新编译,並发出命令对这些程式档进行编译。在使用make之前,需要编写Makefile资讯档,该档描述了整 个套件程式中各程式之间的关系,並针对每个需要更新的档案给与具体的控制命令。通常,执行程式是根据其目标档进行更新的,而这些目标档则是由编译程序尽力 的。一旦编写好一个合适的Makefile档,那麼在你每次修改过程式系统中的某些原始码档后,执行make命令就能进行所有必要的重新编译工作。 make程式是使用Makefile资料档案和代码档的最后修改时间
(1ast-modification time)来确定那些档案需要进行更新,对於每一个需要更新的档它会根据Makefile中的讯发出相应的命令。在Makefile档中,开头为‘#’的行是注释行。档案开头部分的‘=’代入语句定义了一些参数或命令的缩写。

这个Makefile档的主要作用是指示make程式最终使用独立编译连接成的tools/目錄中的build执行程式所有內核编译代码连接和合併成一个 可执行的內核映射档image。具体是对tools/中的bootsect.s,setup.s使用8086组合器进行编译,分別生成各自的执行模组。再 对原始码中的其他所有程式使用GNU的编译器gcc/gas进行编译,並链接模组system。最后再用build工具将这三块组合成一个內核映射档 image。Build是由tools/build.c根源程式编译而成的一个独立的执行程式,它本身並沒有被编译链结到內核代码中。基本编译连接/组合 结构如图5-33所示。



5.12.2代码注释











本章小结

本章概述了Linux早期作业系统的内核模式和体系结构。首先带出了Linux 0.12內核使用和管理记忆体的方法、內核堆栈态和用戶态堆栈的设置和使用方法、中断机制、系统时钟定时以及行程建立、调度和终止方法。然后根据原始码的 目录结构形式,详细地介绍了各个子目錄中代码档的基本功能和层次关系。同时說明了Linux 0.12所使用的目标档格式。最后从Linux內核主目錄下的makefile档着手,开始对內核原始码进行注释。

本章內容可以看作是对Linux 0.12内核重要资讯的归纳說明,因此可作为閱读后续章节的参考內容。

To be continued......

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: