linux 简单分析

    为了提高自己,不知不觉踏上了linux源码分析之路了!
    我选择linux源码版本是linux 0.11,为什么选择它?因为它代码量少且资料多。

    针对它的分析是建立于网上资料之上(快捷、效果好)。

    项目如图:


  该项目是网上某某已经编译好了,并且对它进行了分析。笔者只是学习他的皮毛。

  首先进入项目的是bootsect:这个程序是linuxkernel的第一个程序,包括了linux自己的bootstrap程序,但是在 说明这个程序前,必须先说明一般IBMPC开机时的动作(此处的开机是指"打开PC的电源" )。它是第一个被读入内存中并执行的程序,现在,我们可以开始来看看到底它做了什么。 

第一步 :bootsect将它"自己"从被ROMBIOS载入的绝对地址0x7C00处搬到0x90000处, 然后利用一个jmpi(jumpindirectly)的指令,跳到新位置的jmpi的下一行去执行…

表示将跳到CS为0x9000,IP为offset"go"的位置(CS:IP=0x9000:offsetgo),其中I NITSEC=0x9000定义于程序开头的部份,而go这个label则恰好是下一行指令所在的位置。

第二步:接着,将其它segmentregisters包括DS,ES,SS都指向0x9000这个位置,与CS看齐 。

第叁步:接着利用BIOS中断服务int13h的第0号功能,重置磁盘控制器,使得刚才的设定发挥功能。

 第四步:完成重置磁盘控制器之后,bootsect就从磁盘上读入紧邻着bootsect的setup程序, 也就是以后将会介绍的setup.S,此读入动作是利用BIOS中断服务int13h的第2号功能。

第五步:再来,就要读入真正linux的kernel了,也就是你可以在linux的根目录下看到的"v mlinuz"。

第六步:接下来做的事是检查root device,之后就仿照一开始的方法,利用indirect jump跳到刚刚已读入的setup部份。

 ;//bootsec.s文件说明如下:磁盘的引导块程序,驻留在磁盘的第一扇区。
 ;//在PC机加电rom bios自检之后,引导扇区由bios加载到内存0x7c00处,然后将自己移动到内存0x90000处。
 ;//该程序的主要作用是首先将setup模块从磁盘加载到内存中,紧接着bootsect的后面位置(0x90200),然后利用bios中断0x13中断去磁盘参数表中当前引导盘的参数,
 ;//然后在屏幕上显示"Loading system..."字符串。再者将system模块从磁盘上加载到内存0x10000开始的地方。随后确定根文件系统的设备号,如果没有指定,
 ;//则根据所保存的引导盘的每类型和种类,并保存设备号与boot_dev,最后长跳转到 setup程序开始处0x90200执行setup程序。 
 
 .model tiny
 .386p
;// SYSSIZE是要加载的节数(16字节为1节)。3000h共为30000h字节=192kB
;// 对当前的版本空间已足够了。
 SYSSIZE = 3000h		;// 指编译连接后system模块的大小。
						;// 这里给出了一个最大默认值。

 SETUPLEN = 4			;// setup程序的扇区数(setup-sectors)值
 BOOTSEG  = 07c0h		;// bootsect的原始地址(是段地址,以下同)
 INITSEG  = 9000h		;// 将bootsect移到这里
 SETUPSEG = 9020h		;// setup程序从这里开始
 SYSSEG   = 1000h		;// system模块加载到10000(64kB)处.
 ENDSEG   = SYSSEG + SYSSIZE		;// 停止加载的段地址

;// DEF_ROOT_DEV:	000h - 根文件系统设备使用与引导时同样的软驱设备.
;//		301 - 根文件系统设备在第一个硬盘的第一个分区上,等等
ROOT_DEV = 301h;//指定根文件系统设备是第1个硬盘的第1个分区。这是Linux老式的硬盘命名
						;//方式,具体值的含义如下:
						;//设备号 = 主设备号*256 + 次设备号 
						;//          (也即 dev_no = (major<<8 + minor)
						;//(主设备号:1-内存,2-磁盘,3-硬盘,4-ttyx,5-tty,6-并行口,7-非命名管道)
						;//300 - /dev/hd0 - 代表整个第1个硬盘
						;//301 - /dev/hd1 - 第1个盘的第1个分区
						;//... ...
						;//304 - /dev/hd4 - 第1个盘的第4个分区
						;//305 - /dev/hd5 - 代表整个第2个硬盘
						;//306 - /dev/hd6 - 第2个盘的第1个分区
						;//... ...
						;//309 - /dev/hd9 - 第1个盘的第4个分区 

;/* ************************************************************************
;	boot被bios-启动子程序加载至7c00h(31k)处,并将自己移动到了
;	地址90000h(576k)处,并跳转至那里。
;	它然后使用BIOS中断将'setup'直接加载到自己的后面(90200h)(576.5k),
;	并将system加载到地址10000h处。
;
;	注意:目前的内核系统最大长度限制为(8*65536)(512kB)字节,即使是在
;	将来这也应该没有问题的。我想让它保持简单明了。这样512k的最大内核长度应该
;	足够了,尤其是这里没有象minix中一样包含缓冲区高速缓冲。
;
;	加载程序已经做的够简单了,所以持续的读出错将导致死循环。只能手工重启。
;	只要可能,通过一次取取所有的扇区,加载过程可以做的很快的。
;************************************************************************ */
code segment		;// 程序从_main标号开始执行。
	assume cs:code
start:					;// 以下10行作用是将自身(bootsect)从目前段位置07c0h(31k)
						;// 移动到9000h(576k)处,共256字(512字节),然后跳转到
						;// 移动后代码的 go 标号处,也即本程序的下一语句处。 
	mov	ax,BYTE PTR BOOTSEG		;// 将ds段寄存器置为7C0h
	mov	ds,ax
	mov	ax,BYTE PTR INITSEG		;// 将es段寄存器置为9000h
	mov	es,ax
	mov	cx,256			;// 移动计数值 = 256字 = 512 字节
	sub	si,si			;// 源地址   ds:si = 07C0h:0000h
	sub	di,di			;// 目的地址 es:di = 9000h:0000h
	rep movsw			;// 重复执行,直到cx = 0;移动1个字
;	jmp INITSEG:[go] 	;// 间接跳转。这里INITSEG指出跳转到的段地址。
    db 0eah				;// 间接跳转指令码
	dw go
	dw INITSEG
go:	mov	ax,cs			;// 将ds、es和ss都置成移动后代码所在的段处(9000h)。
	mov	ds,ax			;// 由于程序中有堆栈操作(push,pop,call),因此必须设置堆栈。
	mov	es,ax
;// put stack at 9ff00.  将堆栈指针sp指向9ff00h(即9000h:0ff00h)处
	mov	ss,ax
	mov	sp,0FF00h		;/* 由于代码段移动过了,所以要重新设置堆栈段的位置。
						;   sp只要指向远大于512偏移(即地址90200h)处
						;   都可以。因为从90200h地址开始处还要放置setup程序,
						;   而此时setup程序大约为4个扇区,因此sp要指向大
						;   于(200h + 200h*4 + 堆栈大小)处。 */

;// 在bootsect程序块后紧跟着加载setup模块的代码数据。
;// 注意es已经设置好了。(在移动代码时es已经指向目的段地址处9000h)。

load_setup:
	;// 以下10行的用途是利用BIOS中断INT 13h将setup模块从磁盘第2个扇区
	;// 开始读到90200h开始处,共读4个扇区。如果读出错,则复位驱动器,并
	;// 重试,没有退路。
	;// INT 13h 的使用方法如下:
	;// ah = 02h - 读磁盘扇区到内存;al = 需要读出的扇区数量;
	;// ch = 磁道(柱面)号的低8位;  cl = 开始扇区(0-5位),磁道号高2位(6-7);
	;// dh = 磁头号;				  dl = 驱动器号(如果是硬盘则要置为7);
	;// es:bx ->指向数据缓冲区;  如果出错则CF标志置位。 
	mov	dx,0000h				;// drive 0, head 0
	mov	cx,0002h				;// sector 2, track 0
	mov	bx,0200h				;// address = 512, in INITSEG
	mov	ax,0200h+SETUPLEN		;// service 2, nr of sectors
	int	13h					;// read it
	jnc	ok_load_setup			;// ok - continue
	mov	dx,0000h
	mov	ax,0000h				;// reset the diskette
	int	13h
	jmp	load_setup

ok_load_setup:
;/* 取磁盘驱动器的参数,特别是每道的扇区数量。
;   取磁盘驱动器参数INT 13h调用格式和返回信息如下:
;   ah = 08h	dl = 驱动器号(如果是硬盘则要置位7为1)。
;   返回信息:
;   如果出错则CF置位,并且ah = 状态码。
;   ah = 0, al = 0,         bl = 驱动器类型(AT/PS2)
;   ch = 最大磁道号的低8位,cl = 每磁道最大扇区数(位0-5),最大磁道号高2位(位6-7)
;   dh = 最大磁头数,       电力= 驱动器数量,
;   es:di -> 软驱磁盘参数表。 */
	mov	dl,00h
	mov	ax,0800h		;// AH=8 is get drive parameters
	int	13h
	mov	ch,00h
;//	seg cs				;// 表示下一条语句的操作数在cs段寄存器所指的段中。
	mov	cs:sectors,cx		;// 保存每磁道扇区数。
	mov	ax,INITSEG
	mov	es,ax			;// 因为上面取磁盘参数中断改掉了es的值,这里重新改回。

;// Print some inane message   在显示一些信息('Loading system ... '回车换行,共24个字符)。

	mov	ah,03h		;// read cursor pos
	xor	bh,bh			;// 读光标位置。
	int	10h
	
	mov	cx,27			;// 共24个字符。
	mov	bx,0007h		;// page 0, attribute 7 (normal)
	mov	bp,offset msg1		;// 指向要显示的字符串。
	mov	ax,1301h		;// write string, move cursor
	int	10h			;// 写字符串并移动光标。

;// ok, we've written the message, now
;// we want to load the system (at 10000h)  现在开始将system 模块加载到10000h(64k)处。

	mov	ax,SYSSEG
	mov	es,ax		;// segment of 010000h  es = 存放system的段地址。
	call read_it			;// 读磁盘上system模块,es为输入参数。
	call kill_motor		;// 关闭驱动器马达,这样就可以知道驱动器的状态了。

;// 此后,我们检查要使用哪个根文件系统设备(简称根设备)。如果已经指定了设备(!=0)
;// 就直接使用给定的设备。否则就需要根据BIOS报告的每磁道扇区数来
;// 确定到底使用/dev/PS0(2,28)还是/dev/at0(2,8)。
;//		上面一行中两个设备文件的含义:
;//		在Linux中软驱的主设备号是2(参加第43行注释),次设备号 = type*4 + nr, 其中
;//		nr为0-3分别对应软驱A、B、C或D;type是软驱的类型(2->1.2M或7->1.44M等)。
;//		因为7*4 + 0 = 28,所以/dev/PS0(2,28)指的是1.44M A驱动器,其设备号是021c
;//		同理 /dev/at0(2,8)指的是1.2M A驱动器,其设备号是0208。

;//	seg cs
	mov	ax,cs:root_dev
	cmp	ax,0
	jne	root_defined	;// 如果 ax != 0, 转到root_defined
;//	seg cs
	mov	bx,cs:sectors		;// 取上面保存的每磁道扇区数。如果sectors=15
						;// 则说明是1.2Mb的驱动器;如果sectors=18,则说明是
						;// 1.44Mb软驱。因为是可引导的驱动器,所以肯定是A驱。
	mov	ax,0208h			;// /dev/ps0 - 1.2Mb
	cmp	bx,15			;// 判断每磁道扇区数是否=15
	je	root_defined	;// 如果等于,则ax中就是引导驱动器的设备号。
	mov	ax,021ch			;// /dev/PS0 - 1.44Mb
	cmp	bx,18
	je	root_defined
undef_root:				;// 如果都不一样,则死循环(死机)。
	jmp undef_root
root_defined:
;//	seg cs
	mov	cs:root_dev,ax		;// 将检查过的设备号保存起来。

;// 到此,所有程序都加载完毕,我们就跳转到被
;// 加载在bootsect后面的setup程序去。

;	jmp	SETUPSEG:[0]		;// 跳转到9020:0000(setup程序的开始处)。
	db 0eah
	dw 0
	dw SETUPSEG

;//------------ 本程序到此就结束了。-------------

;// ******下面是两个子程序。*******

;// 该子程序将系统模块加载到内存地址10000h处,并确定没有跨越64kB的内存边界。
;// 我们试图尽快地进行加载,只要可能,就每次加载整条磁道的数据
;// 
;// 输入:es - 开始内存地址段值(通常是1000h)
;//
sread	dw 1+SETUPLEN	;// 当前磁道中已读的扇区数。开始时已经读进1扇区的引导扇区
head	dw 0				;// 当前磁头号
track	dw 0				;// 当前磁道号

read_it:		;// 测试输入的段值。必须位于内存地址64KB边界处,否则进入死循环。
	mov ax,es	;// 清bx寄存器,用于表示当前段内存放数据的开始位置。
	test ax,0fffh
die:
	jne die			;// es值必须位于64KB地址边界!
	xor bx,bx		;// bx为段内偏移位置。

rp_read:
;// 判断是否已经读入全部数据。比较当前所读段是否就是系统数据末端所处的段(#ENDSEG),如果
;// 不是就跳转至下面ok1_read标号处继续读数据。否则退出子程序返回。
	mov ax,es
	cmp ax,ENDSEG		;// have we loaded all yet? 是否已经加载了全部数据?
	jb ok1_read
	ret
ok1_read:
;// 计算和验证当前磁道需要读取的扇区数,放在ax寄存器中。
;// 根据当前磁道还未读取的扇区数以及段内数据字节开始偏移位置,计算如果全部读取这些
;// 未读扇区,所读总字节数是否会超过64KB段长度的限制。若会超过,则根据此次最多能读
;// 入的字节数(64KB - 段内偏移位置),反算出此次需要读取的扇区数。
;//	seg cs
	mov ax,cs:sectors		;// 取每磁道扇区数。
	sub ax,sread		;// 减去当前磁道已读扇区数。
	mov dx,ax			;// ax = 当前磁道未读扇区数。
	mov cl,9
	shl dx,cl			;// dx = ax * 512 字节。
	add dx,bx			;// cx = cx + 段内当前偏移值(bx)
						;//    = 此次读操作后,段内共读入的字节数。
	jnc ok2_read		;// 若没有超过64KB字节,则跳转至ok2_read处执行。
	je ok2_read
	xor ax,ax			;// 若加上此次将读磁道上所有未读扇区时会超过64KB,则计算
	sub ax,bx			;// 此时最多能读入的字节数(64KB - 段内读偏移位置),再转换
	shr ax,cl			;// 成需要读取的扇区数。
ok2_read:
	call read_track
	mov dx,ax			;// dx = 该此操作已读取的扇区数。
	add ax,sread		;// 当前磁道上已经读取的扇区数。
;//	seg cs
	cmp ax,cs:sectors		;// 如果当前磁道上的还有扇区未读,则跳转到ok3_read处。
	jne ok3_read
;// 读该磁道的下一磁头面(1号磁头)上的数据。如果已经完成,则去读下一磁道。
	mov ax,1
	sub ax,head			;// 判断当前磁头号。
	jne ok4_read		;// 如果是0磁头,则再去读1磁头面上的扇区数据
	inc track			;// 否则去读下一磁道。
ok4_read:
	mov head,ax			;// 保存当前磁头号。
	xor ax,ax			;// 清当前磁道已读扇区数。
ok3_read:
	mov sread,ax		;// 保存当前磁道已读扇区数。
	shl dx,cl			;// 上次已读扇区数*512字节。
	add bx,dx			;// 调整当前段内数据开始位置。
	jnc rp_read			;// 若小于64KB边界值,则跳转到rp_read处,继续读数据。
						;// 否则调整当前段,为读下一段数据作准备。
	mov ax,es
	add ax,1000h		;// 将段基址调整为指向下一个64KB段内存。
	mov es,ax
	xor bx,bx
	jmp rp_read

;// 读当前磁道上指定开始扇区和需读扇区数的数据到es:bx开始处。
;// al - 需读扇区数; es:bx - 缓冲区开始位置。
read_track:
	push ax
	push bx
	push cx
	push dx
	mov dx,track		;// 取当前磁道号。
	mov cx,sread		;// 取当前磁道上已读扇区数。
	inc cx				;// cl = 开始读扇区。
	mov ch,dl			;// ch = 当前磁道号。
	mov dx,head			;// 取当前磁头号。
	mov dh,dl			;// dh = 磁头号。
	mov dl,0			;// dl = 驱动器号(为0表示当前驱动器)。
	and dx,0100h		;// 磁头号不大于1
	mov ah,2			;// ah = 2, 读磁盘扇区功能号。
	int 13h
	jc bad_rt			;// 若出错,则跳转至bad_rt。
	pop dx
	pop cx
	pop bx
	pop ax
	ret
;// 执行驱动器复位操作(磁盘中断功能号0),再跳转到read_track处重试。
bad_rt:	
	mov ax,0
	mov dx,0
	int 13h
	pop dx
	pop cx
	pop bx
	pop ax
	jmp read_track

;///*
;//* 这个子程序用于关闭软驱的马达,这样我们进入内核
;//* 后它处于已知状态,以后也就无须担心它了。
;//*/
kill_motor:
	push dx
	mov dx,3f2h		;// 软驱控制卡的驱动端口,只写。
	mov al,0			;// A驱动器,关闭FDC,禁止DMA和中断请求,关闭马达。
	out dx,al			;// 将al中的内容输出到dx指定的端口去。
	pop dx
	ret

sectors dw 0				;// 存放当前启动软盘每磁道的扇区数。

msg1 db 13,10			;// 回车、换行的ASCII码。
	 db "Loading my system ..."	;// 我加了my,共有27个字符了
	 db 13,10,13,10	;// 共24个ASCII码字符。

org 508		;// 表示下面语句从地址508(1FC)开始,所以root_dev
			;// 在启动扇区的第508开始的2个字节中。
root_dev dw ROOT_DEV	;// 这里存放根文件系统所在的设备号(init/main.c中会用)。
boot_flag dw 0AA55h		;// 硬盘有效标识。

code ends
end

第二个项目是build,他是在windows下分析Linux编译好的文件pe结构信息。该项目就一个目标文件,源码如下:

#include 
#include 
#include 

DWORD g_dwFileHeader[1024] = {0};    //PE文件的标题将读入该缓冲区。

typedef struct __tagFILE_HEADER{
	unsigned char ucNop[4];
	DWORD         dwJmpAddr;
}__FILL_HEADER;

__FILL_HEADER g_FillHeader = {0x90,0x90,0x90,0xe9,0x00000000};    //这种结构将被写入到目标文件。

char* g_lpszTargetPath = "E:\\book\\Temp\\linux011VC\\VC\\Release\\system";  //目标文件的路径和名称。

void main(int argc,char argv[])
{
	IMAGE_DOS_HEADER*       ImageDosHeader = NULL;
	IMAGE_NT_HEADERS*       ImageNtHeader = NULL;
	IMAGE_OPTIONAL_HEADER*  ImageOptionalHeader = NULL;
	HANDLE                  hFile = INVALID_HANDLE_VALUE;
	DWORD                   dwReadBytes = 0L;
	BOOL                    bResult = FALSE;
	DWORD                   dwActualBytes = 0L;
	DWORD                   dwOffset = 0L;
	UCHAR*                  lpucSource = NULL;
	UCHAR*                  lpucDes    = NULL;
	DWORD                   dwLoop     = 0;

	hFile = CreateFile(                //打开目标文件
		g_lpszTargetPath,
		GENERIC_READ | GENERIC_WRITE,
		0L,
		NULL,
		OPEN_ALWAYS,
		0L,
		NULL);
	if(INVALID_HANDLE_VALUE == hFile)
	{
		printf("Can not open the target file to read.");
		goto __TERMINAL;
	}

	dwReadBytes = 4096;               //4k字节的目标文件。
	bResult = ReadFile(hFile,g_dwFileHeader,dwReadBytes,&dwActualBytes,NULL);
	if(!bResult)
		goto __TERMINAL;

	CloseHandle(hFile);
	hFile = INVALID_HANDLE_VALUE;

	//
	//PE文件的入口点,并修改它。
	//
	ImageDosHeader = (IMAGE_DOS_HEADER*)&g_dwFileHeader[0];
	dwOffset = ImageDosHeader->e_lfanew;

	ImageNtHeader = (IMAGE_NT_HEADERS*)((UCHAR*)&g_dwFileHeader[0] + dwOffset);
	ImageOptionalHeader = &(ImageNtHeader->OptionalHeader);

	g_FillHeader.dwJmpAddr = ImageOptionalHeader->AddressOfEntryPoint;
	printf("    Entry Point : %d\r\n",ImageOptionalHeader->AddressOfEntryPoint);
	g_FillHeader.dwJmpAddr -= sizeof(__FILL_HEADER);    //计算的目标地址将跳转.我们在前面的目标文件已经添加了一些nop指令,所以我们必须调整它。

	lpucSource = (UCHAR*)&g_FillHeader.ucNop[0];
	lpucDes    = (UCHAR*)&g_dwFileHeader[0];

	for(dwLoop = 0;dwLoop < sizeof(__FILL_HEADER);dwLoop ++)  //修改目标文件的头信息
	{
		*lpucDes = *lpucSource;
		lpucDes ++;
		lpucSource ++;
	}

	hFile = CreateFile(                //打开目标文件 并写入信息
		g_lpszTargetPath,
		GENERIC_READ | GENERIC_WRITE,
		0L,
		NULL,
		OPEN_ALWAYS,
		0L,
		NULL);
	if(INVALID_HANDLE_VALUE == hFile)
	{
		printf("Can not open the target file to write.");
		goto __TERMINAL;
	}

	WriteFile(hFile,(LPVOID)&g_dwFileHeader[0],sizeof(__FILL_HEADER),&dwActualBytes,
		NULL);

	printf("SectionAligment : %d\r\n",ImageOptionalHeader->SectionAlignment);
	printf("   FileAligment : %d\r\n",ImageOptionalHeader->FileAlignment);

__TERMINAL:
	if(INVALID_HANDLE_VALUE != hFile)
		CloseHandle(hFile);
}

第三个项目是Documents,其实是一些文档,具体如下:

附:PC I/O地址分配
  
PC只用了10位地址线(A0-A9)进行译码,其寻址的范围为0H-3FFH,共有1024个I/O地址。
这1024个地址中前半段(A9=0,范围为0H-1FFH)是属于主机板I/O译码,
后半段(A9=1,范围为200H-3FFH)则是用来扩展插槽上的I/O译码用。
         I/O端口功能表
———————————————————————————
I/O地址 功能、用途
———————————————————————————
0    DMA通道0,内存地址寄存器(DMA控制器1(8237))
1    DMA通道0, 传输计数寄存器
2    DMA通道1,内存地址寄存器
3    DMA通道1, 传输计数寄存器
4    DMA通道2,内存地址寄存器
5    DMA通道2, 传输计数寄存器
6    DMA通道3,内存地址寄存器
7    DMA通道3, 传输计数寄存器
8    DMA通道0-3的状态寄存器
0AH    DMA通道0-3的屏蔽寄存器
0BH    DMA通道0-3的方式寄存器
0CH    DMA清除字节指针
0DH    DMA主清除字节
0EH    DMA通道0-3的清屏蔽寄存器
0FH    DMA通道0-3的写屏蔽寄存器
19H   DMA起始寄存器
20H-3FH 可编程中断控制器1(8259)使用
40H   可编程中断计时器(8253)使用,读/写计数器0
41H   可编程中断计时器寄存器
42H   可编程中断计时器杂项寄存器
43H   可编程中断计时器,控制字寄存器
44H   可编程中断计时器,杂项寄存器(AT)
47H   可编程中断计时器,计数器0的控制字寄存器
48H-5FH 可编程中断计时器使用
60H-61H 键盘输入数据缓冲区
61H   AT:8042键盘控制寄存器/XT:8255输出寄存器
62H   8255输入寄存器
63H   8255命令方式寄存器
64H   8042键盘输入缓冲区/8042状态
65H-6FH 8255/8042专用
70H   CMOS RAM地址寄存器
71H   CMOS RAM数据寄存器
80H   生产测试端口
81H   DMA通道2,页表地址寄存器
82H   DMA通道3,页表地址寄存器
83H   DMA通道1,页表地址寄存器
87H   DMA通道0,页表地址寄存器
89H   DMA通道6,页表地址寄存器
8AH   DMA通道7,页表地址寄存器
8BH   DMA通道5,页表地址寄存器
8FH   DMA通道4,页表地址寄存器
93H-9FH DMA控制器专用
0A0H   NM1屏蔽寄存器/可编程中断控制器2
0A1H   可编程中断控制器2屏蔽
0C0H   DMA通道0,内存地址寄存器(DMA控制器2(8237))
0C2H   DMA通道0, 传输计数寄存器
0C4H   DMA通道1,内存地址寄存器
0C6H   DMA通道1, 传输计数寄存器
0C8H   DMA通道2,内存地址寄存器
0CAH   DMA通道2, 传输计数寄存器
0CCH   DMA通道3,内存地址寄存器
0CEH   DMA通道3, 传输计数寄存器
0D0H   DMA状态寄存器
0D2H   DMA写请求寄存器
0D4H   DMA屏蔽寄存器
0D6H   DMA方式寄存器
0D8H   DMA清除字节指针
0DAH   DMA主清
0DCH   DMA清屏蔽寄存器
0DEH   DMA写屏蔽寄存器
0DFH-0EFH 保留
0F0H-0FFH 协处理器使用
100H-16FH保留
170H   1号硬盘数据寄存器
171H   1号硬盘错误寄存器
172H   1号硬盘数据扇区计数
173H   1号硬盘扇区数
174H   1号硬盘柱面(低字节)
175H   1号硬盘柱面(高字节)
176H   1号硬盘驱动器/磁头寄存器
177H   1号硬盘状态寄存器
1F0H   0号硬盘数据寄存器
1F1H   0号硬盘错误寄存器
1F2H   0号硬盘数据扇区计数
1F3H   0号硬盘扇区数
1F4H   0号硬盘柱面(低字节)
1F5H   0号硬盘柱面(高字节)
1F6H   0号硬盘驱动器/磁头寄存器
1F7H   0号硬盘状态寄存器
1F9H-1FFH保留
200H-20FH游戏控制端口
210H-21FH扩展单元
278H   3号并行口,数据端口
279H   3号并行口,状态端口
27AH   3号并行口,控制端口
2B0H-2DFH保留
2E0H   EGA/VGA使用
2E1H   GPIP(0号适配器)
2E2H   数据获取(0号适配器)
2E3H   数据获取(1号适配器)
2E4H-2F7H保留
2F8H   2号串行口,发送/保持寄存器(RS232接口卡2)
2F9H   2号串行口,中断有效寄存器
2FAH   2号串行口,中断ID寄存器
2FBH   2号串行口,线控制寄存器
2FCH   2号串行口,调制解调控制寄存器
2FDH   2号串行口,线状态寄存器
2FEH   2号串行口,调制解调状态寄存器
2FFH   保留
300H-31FH原形卡
320H   硬盘适配器寄存器
322H   硬盘适配器控制/状态寄存器
324H   硬盘适配器提示/中断状态寄存器
325H-347H保留
348H-357H DCA3278
366H-36FH PC网络
372H    软盘适配器数据输出/状态寄存器
375H-376H 软盘适配器数据寄存器
377H    软盘适配器数据输入寄存器
378H    2号并行口,数据端口
379H    2号并行口,状态端口
37AH    2号并行口,控制端口
380H-38FH SDLC及BSC通讯
390H-393H Cluster适配器0
3A0H-3AFH BSC通讯
3B0H-3B H MDA视频寄存器
3BCH    1号并行口,数据端口
3BDH    1号并行口,状态端口
3BEH    1号并行口,控制端口
3C0H-3CFH EGA/VGA视频寄存器
3D0H-3D7H CGA视频寄存器
3F0H-3F7H 软盘控制器寄存器
3F8H    1号串行口,发送/保持寄存器(RS232接口卡1)
3F9H    1号串行口,中断有效寄存器
3FAH    1号串行口,中断ID寄存器
3FBH    1号串行口,线控制寄存器
3FCH    1号串行口,调制解调控制寄存器
3FDH    1号串行口,线状态寄存器
3FEH    1号串行口,调制解调状态寄存器
3FFH    保留
// 微机中断的资料如下:

// IRQ0系统时钟(系统保留) 
// IRQ1键盘(系统保留)
// IRQ2系统的第二个中断请示控制器(IRQ8-15)
// IRQ3串行口2(可用) 
// IRQ4串行口1(可用)
// IRQ5并行口2(可用)(一般用来设置声卡)
// IRQ6软盘(系统保留) 
// IRQ7并行口1(一般用作打印机)
// IRQ8实时时钟(系统保留) 
// IRQ9可用 
// IRQ10可用
// IRQ11常用于显示卡 
// IRQ12 PS/2 mouse(可用)
// IRQ13数学协处理器
// IRQ14 IDE1控制器通道 
// IRQ15 IDE2控制器通道(可用)
// 此外还有NMI非正常中断(不可屏蔽中断),如校验错。


第四个项目是setup.s。

setup负责从BIOS获取系统数据,并存储在适当的内存位置。主要步骤如下:

1、利用ROM BIOS 中断读取机器系统数据,并将这些数据保存到0x90000 开始的位置(覆盖掉了bootsect 程序所在的地方),所取得的参数和保留的内存位置见下表3.1 所示。这些参数将被内核中相关程序使用,例如字符设备驱动程序集中的ttyio.c 程序等。

2、然后setup程序将system模块从0x10000-0x8ffff(512K) (当时认为内核系统模块system的长度不会超过此值:512KB)整块向下移动到内存绝对地址0x00000 处。

3、接下来加载中断描述符表寄存器(idtr)和全局描述符表寄存器(gdtr)。

4、开启A20 地址线,重新设置两个中断控制芯片8259A,将硬件中断号重新设置为0x20 - 0x2f。

5、最后设置CPU 的控制寄存器CR0(也称机器状态字),从而进入32 位保护模式运行,并跳转到位于system模块最前面部分的head.s 程序继续运行。

源码如下:

 .model tiny
 .386p

;// setup.s负责从BIOS 中获取系统数据,并将这些数据放到系统内存的适当地方。
;// 此时setup.s 和system 已经由bootsect 引导块加载到内存中。
;// 这段代码询问bios 有关内存/磁盘/其它参数,并将这些参数放到一个
;// “安全的”地方:90000-901FF,也即原来bootsect 代码块曾经在
;// 的地方,然后在被缓冲块覆盖掉之前由保护模式的system 读取。

;// 以下这些参数最好和bootsect.s 中的相同!
 INITSEG  = 9000h	;// 原来bootsect 所处的段
 SYSSEG   = 1000h	;// system 在10000(64k)处。
 SETUPSEG = 9020h	;// 本程序所在的段地址。


code segment
start:

;// ok, 整个读磁盘过程都正常,现在将光标位置保存以备今后使用。

	mov	ax,INITSEG		;// 将ds 置成INITSEG(9000)。这已经在bootsect 程序中
	mov	ds,ax			;// 设置过,但是现在是setup 程序,Linus 觉得需要再重新
						;// 设置一下。
	mov	ah,03h		;// BIOS 中断10 的读光标功能号ah = 03
	xor	bh,bh		;// 输入:bh = 页号
	int	10h			;// 返回:ch = 扫描开始线,cl = 扫描结束线,
	mov	ds:[0],dx		;// dh = 行号(00 是顶端),dl = 列号(00 是左边)。
					;// 将光标位置信息存放在90000 处,控制台初始化时会来取。
	mov	ah,88h		;// 这3句取扩展内存的大小值(KB)。
	int	15h			;// 是调用中断15,功能号ah = 88
	mov	ds:[2],ax		;// 返回:ax = 从100000(1M)处开始的扩展内存大小(KB)。
					;// 若出错则CF 置位,ax = 出错码。

;// 下面这段用于取显示卡当前显示模式。
;// 调用BIOS 中断10,功能号ah = 0f
;// 返回:ah = 字符列数,al = 显示模式,bh = 当前显示页。
;// 90004(1 字)存放当前页,90006 显示模式,90007 字符列数。
	mov	ah,0fh
	int	10h
	mov	ds:[4],bx		;// bh = display page
	mov	ds:[6],ax		;// al = video mode, ah = window width

;// 检查显示方式(EGA/VGA)并取参数。
;// 调用BIOS 中断10,附加功能选择-取方式信息
;// 功能号:ah = 12,bl = 10
;// 返回:bh = 显示状态
;// (00 - 彩色模式,I/O 端口=3dX)
;// (01 - 单色模式,I/O 端口=3bX)
;// bl = 安装的显示内存
;// (00 - 64k, 01 - 128k, 02 - 192k, 03 = 256k)
;// cx = 显示卡特性参数(参见程序后的说明)。
	mov	ah,12h
	mov	bl,10h
	int	10h
	mov	ds:[8],ax		;// 90008 = ??
	mov	ds:[10],bx		;// 9000A = 安装的显示内存,9000B = 显示状态(彩色/单色)
	mov	ds:[12],cx		;// 9000C = 显示卡特性参数。

;// 取第一个硬盘的信息(复制硬盘参数表)。
;// 第1 个硬盘参数表的首地址竟然是中断向量41 的向量值!而第2 个硬盘
;// 参数表紧接第1 个表的后面,中断向量46 的向量值也指向这第2 个硬盘
;// 的参数表首址。表的长度是16 个字节(10)。
;// 下面两段程序分别复制BIOS 有关两个硬盘的参数表,90080 处存放第1 个
;// 硬盘的表,90090 处存放第2 个硬盘的表。
	mov	ax,0000h
	mov	ds,ax
	lds	si,ds:[4*41h]		;// 取中断向量41 的值,也即hd0 参数表的地址 ds:si
	mov	ax,INITSEG
	mov	es,ax
	mov	di,0080h		;// 传输的目的地址: 9000:0080 -> es:di
	mov	cx,10h			;// 共传输10 字节。
	rep movsb

;// Get hd1 data

	mov	ax,0000h
	mov	ds,ax
	lds	si,ds:[4*46h]		;// 取中断向量46 的值,也即hd1 参数表的地址 -> ds:si
	mov	ax,INITSEG
	mov	es,ax
	mov	di,0090h		;// 传输的目的地址: 9000:0090 -> es:di
	mov	cx,10h
	rep movsb
	

;// 检查系统是否存在第2 个硬盘,如果不存在则第2 个表清零。
;// 利用BIOS 中断调用13 的取盘类型功能。
;// 功能号ah = 15;
;// 输入:dl = 驱动器号(8X 是硬盘:80 指第1 个硬盘,81 第2 个硬盘)
;// 输出:ah = 类型码;00 --没有这个盘,CF 置位; 01 --是软驱,没有change-line 支持;
;//  02--是软驱(或其它可移动设备),有change-line 支持; 03 --是硬盘。
	mov	ax,1500h
	mov	dl,81h
	int	13h
	jc	no_disk1
	cmp	ah,3			;// 是硬盘吗?(类型= 3 ?)。
	je	is_disk1
no_disk1:
	mov	ax,INITSEG		;// 第2个硬盘不存在,则对第2个硬盘表清零。
	mov	es,ax
	mov	di,0090h
	mov	cx,10h
	mov	ax,00h
	rep stosb
	
is_disk1:

;// 从这里开始我们要保护模式方面的工作了。

	cli			;// 此时不允许中断。 ;//

;// 首先我们将system 模块移到正确的位置。
;// bootsect 引导程序是将system 模块读入到从10000(64k)开始的位置。由于当时假设
;// system 模块最大长度不会超过80000(512k),也即其末端不会超过内存地址90000,
;// 所以bootsect 会将自己移动到90000 开始的地方,并把setup 加载到它的后面。
;// 下面这段程序的用途是再把整个system 模块移动到00000 位置,即把从10000 到8ffff
;// 的内存数据块(512k),整块地向内存低端移动了10000(64k)的位置。

	mov	ax,0000h
	cld			;// 'direction'=0, movs moves forward
do_move:
	mov	es,ax		;// es:di -> 目的地址(初始为0000:0)
	add	ax,1000h
	cmp	ax,9000h	;// 已经把从8000 段开始的64k 代码移动完?
	jz	end_move
	mov	ds,ax		;// ds:si -> 源地址(初始为1000:0)
	sub	di,di
	sub	si,si
	mov cx,8000h	;// 移动8000 字(64k 字节)。
	rep movsw
	jmp	do_move

;// 此后,我们加载段描述符。
;// 从这里开始会遇到32 位保护模式的操作,因此需要Intel 32 位保护模式编程方面的知识了,
;// 有关这方面的信息请查阅列表后的简单介绍或附录中的详细说明。这里仅作概要说明。
;//
;// lidt 指令用于加载中断描述符表(idt)寄存器,它的操作数是6 个字节,0-1 字节是描述符表的
;// 长度值(字节);2-5 字节是描述符表的32 位线性基地址(首地址),其形式参见下面
;// 219-220 行和223-224 行的说明。中断描述符表中的每一个表项(8 字节)指出发生中断时
;// 需要调用的代码的信息,与中断向量有些相似,但要包含更多的信息。
;//
;// lgdt 指令用于加载全局描述符表(gdt)寄存器,其操作数格式与lidt 指令的相同。全局描述符
;// 表中的每个描述符项(8 字节)描述了保护模式下数据和代码段(块)的信息。其中包括段的
;// 最大长度限制(16 位)、段的线性基址(32 位)、段的特权级、段是否在内存、读写许可以及
;// 其它一些保护模式运行的标志。参见后面205-216 行。
;//

end_move:
	mov	ax,SETUPSEG		;// right, forgot this at first. didn't work :-)
	mov	ds,ax			;// ds 指向本程序(setup)段。因为上面操作改变了ds的值。

	lidt fword ptr idt_48			;// 加载中断描述符表(idt)寄存器,idt_48 是6 字节操作数的位置
						;// 前2 字节表示idt 表的限长,后4 字节表示idt 表所处的基地址。

	lgdt fword ptr gdt_48			;// 加载全局描述符表(gdt)寄存器,gdt_48 是6 字节操作数的位置

;// 以上的操作很简单,现在我们开启A20 地址线。

	call empty_8042		;// 等待输入缓冲器空。
						;// 只有当输入缓冲器为空时才可以对其进行写命令。
	mov	al,0D1h			;// D1 命令码-表示要写数据到8042 的P2 端口。P2 端
	out	64h,al			;// 口的位1 用于A20 线的选通。数据要写到60 口。

	call empty_8042		;// 等待输入缓冲器空,看命令是否被接受。
	mov	al,0DFh			;// A20 on 选通A20 地址线的参数。
	out	60h,al
	call empty_8042		;// 输入缓冲器为空,则表示A20 线已经选通。

;// 希望以上一切正常。现在我们必须重新对中断进行编程 
;// 我们将它们放在正好处于intel 保留的硬件中断后面,在int 20-2F。
;// 在那里它们不会引起冲突。不幸的是IBM 在原PC 机中搞糟了,以后也没有纠正过来。
;// PC 机的bios 将中断放在了08-0f,这些中断也被用于内部硬件中断。
;// 所以我们就必须重新对8259 中断控制器进行编程,这一点都没劲。

	mov	al,11h		;// 11 表示初始化命令开始,是ICW1 命令字,表示边
					;// 沿触发、多片8259 级连、最后要发送ICW4 命令字。
	out	20h,al		;// 发送到8259A 主芯片。
	dw	00ebh,00ebh		;// jmp $+2, jmp $+2  $ 表示当前指令的地址,
								;// 两条跳转指令,跳到下一条指令,起延时作用。
	out	0A0h,al		;// and to 8259A-2 ;// 再发送到8259A 从芯片。
	dw	00ebh,00ebh
	mov	al,20h		;// start of hardware int's (20)
	out	21h,al		;// 送主芯片ICW2 命令字,起始中断号,要送奇地址。
	dw	00ebh,00ebh
	mov	al,28h		;// start of hardware int's 2 (28)
	out	0A1h,al		;// 送从芯片ICW2 命令字,从芯片的起始中断号。
	dw	00ebh,00ebh
	mov	al,04h		;// 8259-1 is master
	out	21h,al		;// 送主芯片ICW3 命令字,主芯片的IR2 连从芯片INT。
	dw	00ebh,00ebh	;// 参见代码列表后的说明。
	mov	al,02h		;// 8259-2 is slave
	out	0A1h,al		;// 送从芯片ICW3 命令字,表示从芯片的INT 连到主芯
						;// 片的IR2 引脚上。
	dw	00ebh,00ebh
	mov	al,01h		;// 8086 mode for both
	out	21h,al		;// 送主芯片ICW4 命令字。8086 模式;普通EOI 方式,
						;// 需发送指令来复位。初始化结束,芯片就绪。
	dw	00ebh,00ebh
	out	0A1h,al		;// 送从芯片ICW4 命令字,内容同上。
	dw	00ebh,00ebh
	mov	al,0FFh		;// mask off all interrupts for now
	out	21h,al		;// 屏蔽主芯片所有中断请求。
	dw	00ebh,00ebh
	out	0A1h,al		;// 屏蔽从芯片所有中断请求。

;// 哼,上面这段当然没劲 ,希望这样能工作,而且我们也不再需要乏味的BIOS 了(除了
;// 初始的加载.。BIOS 子程序要求很多不必要的数据,而且它一点都没趣。那是“真正”的
;// 程序员所做的事。

;// 这里设置进入32 位保护模式运行。首先加载机器状态字(lmsw - Load Machine Status Word),
;// 也称控制寄存器CR0,其比特位0 置1 将导致CPU 工作在保护模式。

	mov	ax,0001h	;// 保护模式比特位(PE)。
	lmsw ax			;// 就这样加载机器状态字
;	jmp 8:0  		;// 跳转至cs 段8,偏移0 处。执行system 中的代码
	db 0eah
	dw 0
	dw 8
;// 我们已经将system 模块移动到00000 开始的地方,所以这里的偏移地址是0。这里的段值
;// 的8 已经是保护模式下的段选择符了,用于选择描述符表和描述符表项以及所要求的特权级。
;// 段选择符长度为16 位(2 字节);位0-1 表示请求的特权级0-3,linux 操作系统只用
;// 到两级:0 级(系统级)和3 级(用户级);位2 用于选择全局描述符表(0)还是局部描
;// 述符表(1);位3-15 是描述符表项的索引,指出选择第几项描述符。所以段选择符
;// 8(00000,0000,0000,1000)表示请求特权级0、使用全局描述符表中的第1 项,该项指出
;// 代码的基地址是0,因此这里的跳转指令就会去执行system 中的代码。


;// 下面这个子程序检查键盘命令队列是否为空。这里不使用超时方法- 如果这里死机,
;// 则说明PC 机有问题,我们就没有办法再处理下去了。
;// 只有当输入缓冲器为空时(状态寄存器位2 = 0)才可以对其进行写命令。
empty_8042:
	dw 00ebh,00ebh	;// jmp $+2, jmp $+2 $ 表示当前指令的地址
						;// 这是两个跳转指令的机器码(跳转到下一句),相当于延时空操作。
	in	al,64h			;// 读AT 键盘控制器状态寄存器。
	test al,2			;// 测试位2,输入缓冲器满?
	jnz	empty_8042		;// yes - loop
	ret

;// 全局描述符表开始处。描述符表由多个8 字节长的描述符项组成。
;// 这里给出了3 个描述符项。第1 项无用,但须存在。第2 项是系统代码段
;// 描述符(208-211 行),第3 项是系统数据段描述符(213-216 行)。每个描述符的具体
;// 含义参见列表后说明。
gdt:
	dw	0,0,0,0		;// 第1 个描述符,不用。
;// 这里在gdt 表中的偏移量为08,当加载代码段寄存器(段选择符)时,使用的是这个偏移值。
	dw	07FFh		;// 8Mb - limit=2047 (2048*4096=8Mb)
	dw	0000h		;// base address=0
	dw	9A00h		;// code read/exec
	dw	00C0h		;// granularity=4096, 386
;// 这里在gdt 表中的偏移量是10,当加载数据段寄存器(如ds 等)时,使用的是这个偏移值。
	dw	07FFh		;// 8Mb - limit=2047 (2048*4096=8Mb)
	dw	0000h		;// base address=0
	dw	9200h		;// data read/write
	dw	00C0h		;// granularity=4096, 386

idt_48:
	dw	0			;// idt limit=0
	dw	0,0			;// idt base=0L

gdt_48:
	dw	800h		;// 全局表长度为2k 字节,因为每8 字节组成一个段描述符项
						;// 所以表中共可有256 项。
	dw	512+gdt,9h	;// 4 个字节构成的内存线性地址:0009<<16 + 0200+gdt
						;// 也即90200 + gdt(即在本程序段中的偏移地址,205 行)。
	
code ends
end

最后一个项目是system,关键核心源码。由于其的重要性,笔者认真总结每个文件。

boot/head.s  

源码如下:

;/* 
; *总体linux启动过程如下:
; *当PC得电源打开之后,80x86结构的CPU将自动进入实时模式,并且从0xFFFF0开始自动执行程序代码,这个地址通常是ROM-BIOS的地址。PC机的BIOS将执行系统的
; *检测,并且在物理地址的0处开始初始化中断向量。此后,它将可启动设备的第一扇区(512字节)读入内存的绝对地址0x7c00处,并且跳转到这个地方。
; *启动设备通常是软盘或者是硬盘。这里的叙述是很简单的,但是这已经足够理解内核的初始化的工作过程。
; *linux的0x9000由BIOS读入到内存的绝对地址0x7c00(31k)处,当它被执行时就会把自己移动到绝对地址0x90000处,并把启动设备中后2kb字节代码
; *(boot/setup.s)读入到内存0x90200处,而内核的其他部分则被读入到从地址0x10000的开始处。
; *在系统的加载期间显示信息?Loading...",然后将控制权传递给boot/setup.s中的代码.这是另一个实时模式汇编程序。
; *系统启动部分识别主机的某些特性以及vga卡的类型。
; *如果需要,它会要求用户为控制台选择显示模式。
; *然后整个系统从地址0x10000移至0x0000处,进入保护模式病跳转至系统的余下部分。
; *此时所有的32位运行方式的设置启动被完成:idt,gdt,ldt被加载,处理器和协处理器也确认,分页的工作也设置好了。
; *最终将调用init/main.c中的main程序。
; *上述的操作的源代码是在boot/head.s中的。
; *这可能是整个内核中最有诀窍的代码了。
; *注意如果在上述任何一步中出现了一步错误。
; *计算机就会死锁。
; *在操作系统还没有完全运转之前是处理不了错误的。
; */
.586p
.model flat
;/*
; *   head.s 含有32 位启动代码。
; *
; * 注意!!! 32 位启动代码是从绝对地址0x00000000 开始的,这里也同样
; * 是页目录将存在的地方,因此这里的启动代码将被页目录覆盖掉。
; * 
; */
extrn _stack_start:far ptr,_main_rename:proc,_printk:proc
public _idt,_gdt,_pg_dir,_tmp_floppy_area
.code
_pg_dir:		;// 页目录将会存放在这里。
_startup_32:			;// 以下5行设置各个数据段寄存器。指向gdt数据段描述符项
	mov eax,10h
;// 再次注意!!! 这里已经处于32 位运行模式,因此这里的$0x10 并不是把地址0x10 装入各
;// 个段寄存器,它现在其实是全局段描述符表中的偏移值,或者更正确地说是一个描述符表
;// 项的选择符。有关选择符的说明请参见setup.s 中的说明。这里$0x10 的含义是请求特权
;// 级0(位0-1=0)、选择全局描述符表(位2=0)、选择表中第2 项(位3-15=2)。它正好指向表中
;// 的数据段描述符项。(描述符的具体数值参见前面setup.s )。下面代码的含义是:
;// 置ds,es,fs,gs 中的选择符为setup.s 中构造的数据段(全局段描述符表的第2 项)=0x10,
;// 并将堆栈放置在数据段中的_stack_start 数组内,然后使用新的中断描述符表和全局段
;// 描述表.新的全局段描述表中初始内容与setup.s 中的完全一样。
	mov ds,ax
	mov es,ax
	mov fs,ax
	mov gs,ax
	lss esp,_stack_start	;// 表示_stack_start -> ss:esp,设置系统堆栈。
							;// stack_start 定义在kernel/sched.c,69 行。
	call setup_idt		;// 调用设置中断描述符表子程序。
	call setup_gdt		;// 调用设置全局描述符表子程序。
	mov eax,10h			;// reload all the segment registers
	mov ds,ax			;// after changing gdt. CS was already
	mov es,ax			;// reloaded in 'setup_gdt'
	mov fs,ax			;// 因为修改了gdt,所以需要重新装载所有的段寄存器。
	mov gs,ax			;// CS 代码段寄存器已经在setup_gdt 中重新加载过了。
	lss esp,_stack_start
;// 以下5行用于测试A20 地址线是否已经开启。采用的方法是向内存地址0x000000 处写入任意
;// 一个数值,然后看内存地址0x100000(1M)处是否也是这个数值。如果一直相同的话,就一直
;// 比较下去,也即死循环、死机。表示地址A20 线没有选通,结果内核就不能使用1M 以上内存。
	xor eax,eax
l1:	inc eax				;// check that A20 really IS enabled
	mov ds:[0],eax	;// loop forever if it isn't
	cmp ds:[100000h],eax
	je l1				;// '1b'表示向后(backward)跳转到标号1 去。
						;// 若是'5f'则表示向前(forward)跳转到标号5 去。
;/*
;* 注意! 在下面这段程序中,486 应该将位16 置位,以检查在超级用户模式下的写保护,
;* 此后"verify_area()"调用中就不需要了。486 的用户通常也会想将NE(;//5)置位,以便
;* 对数学协处理器的出错使用int 16。
;*/
;// 下面这段程序用于检查数学协处理器芯片是否存在。方法是修改控制寄存器CR0,在假设
;// 存在协处理器的情况下执行一个协处理器指令,如果出错的话则说明协处理器芯片不存
;// 在,需要设置CR0 中的协处理器仿真位EM(位2),并复位协处理器存在标志MP(位1)。
	mov eax,cr0				;// check math chip
	and eax,80000011h		;// Save PG,PE,ET
;/* "orl $0x10020,%eax" here for 486 might be good */
	or	 eax,2				;// set MP
	mov cr0,eax
	call check_x87
	jmp after_page_tables

;/*
;* 我们依赖于ET 标志的正确性来检测287/387 存在与否。
;*/
check_x87:
	fninit
	fstsw ax
	cmp al,0
	je l2				;/* no coprocessor: have to set bits */
	mov eax,cr0			;// 如果存在的则向前跳转到标号1 处,否则改写cr0。
	xor eax,6		;/* reset MP, set EM */
	mov cr0,eax
	ret
align 2	;// 这里".align 2"的含义是指存储边界对齐调整。
l2:			;// 即按4 字节方式对齐内存地址。
	 db 0DBh,0E4h		;/* 287 协处理器码。 */
	 ret

;/*
; * 下面这段是设置中断描述符表子程序setup_idt
; *
; * 将中断描述符表idt 设置成具有256 个项,并都指向ignore_int 中断门。然后加载
; * 中断描述符表寄存器(用lidt 指令)。真正实用的中断门以后再安装。当我们在其它
; * 地方认为一切都正常时再开启中断。该子程序将会被页表覆盖掉。
; */
setup_idt:
	lea edx,ignore_int		;// 将ignore_int 的有效地址(偏移值)值 edx 寄存器
	mov eax,00080000h		;// 将选择符0x0008 置入eax 的高16 位中。
	mov ax,dx				;/* selector = 0x0008 = cs */
							;// 偏移值的低16 位置入eax 的低16 位中。此时eax 含
							;// 有门描述符低4 字节的值。
	mov dx,8E00h		;/* interrupt gate - dpl=0, present */
							;// 此时edx 含有门描述符高4 字节的值。
	lea edi,_idt
	mov ecx,256
rp_sidt:
	mov [edi],eax		;// 将哑中断门描述符存入表中。
	mov [edi+4],edx
	add edi,8			;// edi 指向表中下一项。
	dec ecx
	jne rp_sidt
	lidt fword ptr idt_descr		;// 加载中断描述符表寄存器值。
	ret

;/*
; * 下面这段是设置全局描述符表项setup_gdt
; *
; * 这个子程序设置一个新的全局描述符表gdt,并加载。此时仅创建了两个表项,与前
; * 面的一样。该子程序只有两行,“非常的”复杂,所以当然需要这么长的注释了:)。
; */
setup_gdt:
	lgdt fword ptr gdt_descr		;// 加载全局描述符表寄存器(内容已设置好,见232-238 行)。
	ret

;/*
; * Linus 将内核的内存页表直接放在页目录之后,使用了4 个表来寻址16 Mb 的物理内存。
; * 如果你有多于16 Mb 的内存,就需要在这里进行扩充修改。
; */
;// 每个页表长为4 Kb 字节,而每个页表项需要4 个字节,因此一个页表共可以存放1000 个,
;// 表项如果一个表项寻址4 Kb 的地址空间,则一个页表就可以寻址4 Mb 的物理内存。页表项
;// 的格式为:项的前0-11 位存放一些标志,如是否在内存中(P 位0)、读写许可(R/W 位1)、
;// 普通用户还是超级用户使用(U/S 位2)、是否修改过(是否脏了)(D 位6)等;表项的位12-31 
;// 是页框地址,用于指出一页内存的物理起始地址。
org 1000h		;// 从偏移0x1000 处开始是第1 个页表(偏移0 开始处将存放页表目录)。
pg0:

org 2000h
pg1:

org 3000h
pg2:

org 4000h
pg3:

org 5000h		;// 定义下面的内存数据块从偏移0x5000 处开始。
;/*
; * 当DMA(直接存储器访问)不能访问缓冲块时,下面的tmp_floppy_area 内存块
; * 就可供软盘驱动程序使用。其地址需要对齐调整,这样就不会跨越64kB 边界。
; */
_tmp_floppy_area:
	db 1024 dup(0)		;// 共保留1024 项,每项1 字节,填充数值0 。

;// 下面这几个入栈操作(pushl)用于为调用/init/main.c 程序和返回作准备。
;// 前面3 个入栈指令不知道作什么用的,也许是Linus 用于在调试时能看清机器码用的.。
;// 139 行的入栈操作是模拟调用main.c 程序时首先将返回地址入栈的操作,所以如果
;// main.c 程序真的退出时,就会返回到这里的标号L6 处继续执行下去,也即死循环。
;// 140 行将main.c 的地址压入堆栈,这样,在设置分页处理(setup_paging)结束后
;// 执行'ret'返回指令时就会将main.c 程序的地址弹出堆栈,并去执行main.c 程序去了。
after_page_tables:
	push 0			;// These are the parameters to main :-)
	push 0			;// 这些是调用main 程序的参数(指init/main.c)。
	push 0
	push L6			;// return address for main, if it decides to.
	push _main_rename		;// '_main'是编译程序对main 的内部表示方法。
	jmp setup_paging
L6:
	jmp L6			;// main should never return here, but
				;// just in case, we know what happens.

;/* 下面是默认的中断“向量句柄” :-) */
int_msg:
	db "Unknown interrupt\n\r"		;// 定义字符串“未知中断(回车换行)”。
align 2				;// 按4 字节方式对齐内存地址。
ignore_int:
	push eax
	push ecx
	push edx
	push ds			;// 这里请注意!!ds,es,fs,gs 等虽然是16 位的寄存器,但入栈后
	push es			;// 仍然会以32 位的形式入栈,也即需要占用4 个字节的堆栈空间。
	push fs
	mov eax,10h			;// 置段选择符(使ds,es,fs 指向gdt 表中的数据段)。
	mov ds,ax
	mov es,ax
	mov fs,ax
	push int_msg		;// 把调用printk 函数的参数指针(地址)入栈。
	call _printk		;// 该函数在/kernel/printk.c 中。
						;// '_printk'是printk 编译后模块中的内部表示法。
	pop eax
	pop fs
	pop es
	pop ds
	pop edx
	pop ecx
	pop eax
	iretd		;// 中断返回(把中断调用时压入栈的CPU 标志寄存器(32 位)值也弹出)。


;/*
; * Setup_paging
; *
; * 这个子程序通过设置控制寄存器cr0 的标志(PG 位31)来启动对内存的分页处理
; * 功能,并设置各个页表项的内容,以恒等映射前16 MB 的物理内存。分页器假定
; * 不会产生非法的地址映射(也即在只有4Mb 的机器上设置出大于4Mb 的内存地址)。
; *
; * 注意!尽管所有的物理地址都应该由这个子程序进行恒等映射,但只有内核页面管
; * 理函数能直接使用>1Mb 的地址。所有“一般”函数仅使用低于1Mb 的地址空间,或
; * 者是使用局部数据空间,地址空间将被映射到其它一些地方去-- mm(内存管理程序)
; * 会管理这些事的。
; *
; * 对于那些有多于16Mb 内存的家伙- 太幸运了,我还没有,为什么你会有:-)。代码就
; * 在这里,对它进行修改吧。(实际上,这并不太困难的。通常只需修改一些常数等。
; * 我把它设置为16Mb,因为我的机器再怎么扩充甚至不能超过这个界限(当然,我的机 
; * 器很便宜的:-))。我已经通过设置某类标志来给出需要改动的地方(搜索“16Mb”),
; * 但我不能保证作这些改动就行了 :-( )
; */
align 2		;// 按4 字节方式对齐内存地址边界。
setup_paging:	;// 首先对5 页内存(1 页目录+ 4 页页表)清零
	mov ecx,1024*5		;/* 5 pages - pg_dir+4 page tables */
	xor eax,eax
	xor edi,edi			;/* pg_dir is at 0x000 */
							;// 页目录从0x000 地址开始。
	pushf		;// VC内汇编使用cld和std后,需要自己恢复DF的值
	cld
	rep stosd
;// 下面4 句设置页目录中的项,我们共有4 个页表所以只需设置4 项。
;// 页目录项的结构与页表中项的结构一样,4 个字节为1 项。参见上面的说明。
;// "$pg0+7"表示:0x00001007,是页目录表中的第1 项。
;// 则第1 个页表所在的地址= 0x00001007 & 0xfffff000 = 0x1000;第1 个页表
;// 的属性标志= 0x00001007 & 0x00000fff = 0x07,表示该页存在、用户可读写。
	mov eax,_pg_dir
	mov [eax],pg0+7		;/* set present bit/user r/w */
	mov [eax+4],pg1+7		;/*  --------- " " --------- */
	mov [eax+8],pg2+7		;/*  --------- " " --------- */
	mov [eax+12],pg3+7		;/*  --------- " " --------- */
;// 下面6 行填写4 个页表中所有项的内容,共有:4(页表)*1024(项/页表)=4096 项(0 - 0xfff),
;// 也即能映射物理内存4096*4Kb = 16Mb。
;// 每项的内容是:当前项所映射的物理内存地址+ 该页的标志(这里均为7)。
;// 使用的方法是从最后一个页表的最后一项开始按倒退顺序填写。一个页表的最后一项
;// 在页表中的位置是1023*4 = 4092。因此最后一页的最后一项的位置就是$pg3+4092。
	mov edi,pg3+4092		;// edi -> 最后一页的最后一项。
	mov eax,00fff007h		;/*  16Mb - 4096 + 7 (r/w user,p) */
							;// 最后1 项对应物理内存页面的地址是0xfff000,
							;// 加上属性标志7,即为0xfff007.
	std					;// 方向位置位,edi 值递减(4 字节)。
L3:	stosd				;/* fill pages backwards - more efficient :-) */
	sub eax,00001000h	;// 每填写好一项,物理地址值减0x1000。
	jge L3				;// 如果小于0 则说明全添写好了。
	popf
;// 设置页目录基址寄存器cr3 的值,指向页目录表。
	xor eax,eax		;/* 页目录表(pg_dir)在0x0000 处。 */
	mov cr3,eax		;/* cr3 - page directory start */
;// 设置启动使用分页处理(cr0 的PG 标志,位31)
	mov eax,cr0
	or  eax,80000000h	;// 添上PG 标志。
	mov cr0,eax			;/* set paging (PG) bit */
	ret						;/* this also flushes prefetch-queue */
;// 在改变分页处理标志后要求使用转移指令刷新预取指令队列,这里用的是返回指令ret。
;// 该返回指令的另一个作用是将堆栈中的main 程序的地址弹出,并开始运行/init/main.c 
;// 程序。本程序到此真正结束了。

align 2			;// 按4 字节方式对齐内存地址边界。
	dw 0
;//下面两行是lidt 指令的6 字节操作数:长度,基址。
idt_descr:
	dw 256*8-1
	dd _idt			;// idt contains 256 entries
align 2
	dw 0
;// 下面两行是lgdt 指令的6 字节操作数:长度,基址。
gdt_descr:
	dw 256*8-1		;// so does gdt (not that that's any
	dd _gdt			;// magic number, but it works for me :^)

align 4			;// 按8 字节方式对齐内存地址边界。
_idt:
	DQ 256 dup(0)	;// idt is uninitialized // 256 项,每项8 字节,填0。

;// 全局表。前4 项分别是空项(不用)、代码段描述符、数据段描述符、系统段描述符,
;// 其中系统段描述符linux 没有派用处。后面还预留了252 项的空间,用于放置所创建
;// 任务的局部描述符(LDT)和对应的任务状态段TSS 的描述符。
;// (0-nul, 1-cs, 2-ds, 3-sys, 4-TSS0, 5-LDT0, 6-TSS1, 7-LDT1, 8-TSS2 etc...)
_gdt:
	DQ 0000000000000000h	;/* NULL descriptor */
	DQ 00c09a0000000fffh	;/* 16Mb */  // 代码段最大长度16M。
	DQ 00c0920000000fffh	;/* 16Mb */	// 数据段最大长度16M。
	DQ 0000000000000000h	;/* TEMPORARY - don't use */
	DQ 252 dup(0)				;/* space for LDT's and TSS's etc */
end

fs/bitmap.c

/*
	本程序的功能和作用即简单又清晰,主要用于对i 节点位图和逻辑块位图进行释放和
占用处理。操作i 节点位图的函数是free_inode()和new_inode(),操作逻辑块位图的函数
是free_block()和new_block()。
	函数free_block()用于释放指定设备dev 上数据区中的逻辑块block。具体操作是复位
指定逻辑块block对应逻辑块位图中的比特位。它首先取指定设备dev 的超级块,并根据超
级块上给出的设备数据逻辑块的范围,判断逻辑块号block 的有效性。然后在高速缓冲区中
进行查找,看看指定的逻辑块现在是否正在高速缓冲区中,若是,则将对应的缓冲块释放掉。
接着计算block 从数据区开始算起的数据逻辑块号(从1开始计数),并对逻辑块(区段)位图
进行操作,复位对应的比特位。最后根据逻辑块号设置相应逻辑块位图在缓冲区中对应的
缓冲块的已修改标志。
	函数new_block()用于向设备dev 申请一个逻辑块,返回逻辑块号。并置位指定逻辑块
block 对应的逻辑块位图比特位。它首先取指定设备dev 的超级块。然后对整个逻辑块位图
进行搜索,寻找首个是0 的比特位。若没有找到,则说明盘设备空间已用完,返回0。否则
将该比特位置为1,表示占用对应的数据逻辑块。并将该比特位所在缓冲块的已修改标志置位。
接着计算出数据逻辑块的盘块号,并在高速缓冲区中申请相应的缓冲块,并把该缓冲块清零。
然后设置该缓冲块的已更新和已修改标志。最后释放该缓冲块,以便其它程序使用,并返回
盘块号(逻辑块号)。
	函数free_inode()用于释放指定的i 节点,并复位对应的i 节点位图比特位;new_inode()
用于为设备dev建立一个新i 节点。返回该新i 节点的指针。主要操作过程是在内存i 节点表
中获取一个空闲i 节点表项,并从i 节点位图中找一个空闲i 节点。这两个函数的处理过程
与上述两个函数类似,因此这里就不用再赘述。
*/
 将指定地址(addr)处的一块内存清零。嵌入汇编程序宏。
// 输入:eax = 0,ecx = 数据块大小BLOCK_SIZE/4,edi = addr。
extern _inline void clear_block(char *addr)
{_asm{
	pushf
	mov edi,addr
	mov ecx,BLOCK_SIZE/4
	xor eax,eax
	cld
	rep stosd
	popf
}}
//#define clear_block(addr) \
//__asm__("cld\n\t" \  /*清方向位。*/
//	"rep\n\t" \  /*重复执行存储数据(0)。*/
//	"stosl" \
//	::"a" (0),"c" (BLOCK_SIZE/4),"D" ((long) (addr)):"cx","di")

 置位指定地址开始的第nr 个位偏移处的比特位(nr 可以大于32!)。返回原比特位(0 或1)。
// 输入:%0 - eax(返回值),%1 - eax(0);%2 - nr,位偏移值;%3 - (addr),addr 的内容。
extern _inline int set_bit(unsigned long nr,char* addr)
{
//	volatile register int __res;
	_asm{
		xor eax,eax
		mov ebx,nr
		mov edx,addr
		bts [edx],ebx
		setb al
//		mov __res,eax
	}
//	return __res;
}
//#define set_bit(nr,addr) ({\
//register int res __asm__("ax"); \
//__asm__ __volatile__("btsl %2,%3\n\tsetb %%al": \
//"=a" (res):"0" (0),"r" (nr),"m" (*(addr))); \
//res;})

 复位指定地址开始的第nr 位偏移处的比特位。返回原比特位的反码(1 或0)。
// 输入:%0 - eax(返回值),%1 - eax(0);%2 - nr,位偏移值;%3 - (addr),addr 的内容。
extern _inline int clear_bit(unsigned long nr,char* addr)
{
//	volatile register int __res;
	_asm{
		xor eax,eax
		mov ebx,nr
		mov edx,addr
		btr [edx],ebx
		setnb al
//		mov __res,eax
	}
//	return __res;
}
//#define clear_bit(nr,addr) ({\
//register int res __asm__("ax"); \
//__asm__ __volatile__("btrl %2,%3\n\tsetnb %%al": \
//"=a" (res):"0" (0),"r" (nr),"m" (*(addr))); \
//res;})

 从addr 开始寻找第1 个0 值比特位。
// 输入:%0 - ecx(返回值);%1 - ecx(0);%2 - esi(addr)。
// 在addr 指定地址开始的位图中寻找第1 个是0 的比特位,并将其距离addr 的比特位偏移值返回。
extern _inline int find_first_zero(char *addr)
{
//	int __res;
	_asm{
		pushf
		xor ecx,ecx
		mov esi,addr
		cld   /*清方向位。*/
	l1: lodsd   /*取[esi] -> eax。*/
		not eax   /*eax 中每位取反。*/
		bsf edx,eax   /*从位0 扫描eax 中是1 的第1 个位,其偏移值 -> edx。*/
		je l2   /*如果eax 中全是0,则向前跳转到标号2 处(40 行)。*/
		add ecx,edx   /*偏移值加入ecx(ecx 中是位图中首个是0 的比特位的偏移值)*/
		jmp l3   /*向前跳转到标号3 处(结束)。*/
	l2: add ecx,32   /*没有找到0 比特位,则将ecx 加上1 个长字的位偏移量32。*/
		cmp ecx,8192   /*已经扫描了8192 位(1024 字节)了吗?*/
		jl l1  /*若还没有扫描完1 块数据,则向前跳转到标号1 处,继续。*/
//	l3: mov __res,ecx  /*结束。此时ecx 中是位偏移量。*/
	l3: mov eax,ecx
		popf
	}
//	return __res;
}
 释放设备dev 上数据区中的逻辑块block。
// 复位指定逻辑块block 的逻辑块位图比特位。
// 参数:dev 是设备号,block 是逻辑块号(盘块号)。
void free_block(int dev, int block)
向设备dev 申请一个逻辑块(盘块,区块)。返回逻辑块号(盘块号)。
// 置位指定逻辑块block 的逻辑块位图比特位。
int new_block(int dev)
 释放指定的i 节点。
// 复位对应i 节点位图比特位。
void free_inode(struct m_inode * inode)
 为设备dev 建立一个新i 节点。返回该新i 节点的指针。
// 在内存i 节点表中获取一个空闲i 节点表项,并从i 节点位图中找一个空闲i 节点。
struct m_inode * new_inode(int dev)


fs/block_dev.c

 block_dev.c 程序属于块设备文件数据访问操作类程序。该文件包括block_read()和block_write()两个块设备读写函数。这两个函数是供系统调用函数read()和write()调用的,其它地方没有引用。

 数据块写函数- 向指定设备从给定偏移处写入指定长度字节数据。
// 参数:dev - 设备号;pos - 设备文件中偏移量指针;buf - 用户地址空间中缓冲区地址;
//       count - 要传送的字节数。
// 对于内核来说,写操作是向高速缓冲区中写入数据,什么时候数据最终写入设备是由高速缓冲管理
// 程序决定并处理的。另外,因为设备是以块为单位进行读写的,因此对于写开始位置不处于块起始
// 处时,需要先将开始字节所在的整个块读出,然后将需要写的数据从写开始处填写满该块,再将完
// 整的一块数据写盘(即交由高速缓冲程序去处理)。
int block_write(int dev, long * pos, char * buf, int count)
 数据块读函数- 从指定设备和位置读入指定字节数的数据到高速缓冲中。
int block_read(int dev, unsigned long * pos, char * buf, int count)


fs/buffer.c

用于实现缓冲区高速缓存功能。通过不让中断过程改变缓冲区,而是让调用者来执行,避免了竞争条件(当然除改变数据以外)。注意!由于中断可以唤醒一个调用者,因此就需要开关中断指令(cli-sti)序列来检测等待调用返回。但需要非常地快(希望是这样)。

 等待指定缓冲区解锁。
static _inline void wait_on_buffer(struct buffer_head * bh)
 系统调用。同步设备和内存高速缓冲中数据。
int sys_sync(void)//passed
 对指定设备进行高速缓冲数据与设备上数据的同步操作。
int sync_dev(int dev)
 使指定设备在高速缓冲区中的数据无效。
// 扫描高速缓冲中的所有缓冲块,对于指定设备的缓冲区,复位其有效(更新)标志和已修改标志。
void _inline invalidate_buffers(int dev)
/*
 * 该子程序检查一个软盘是否已经被更换,如果已经更换就使高速缓冲中与该软驱
 * 对应的所有缓冲区无效。该子程序相对来说较慢,所以我们要尽量少使用它。
 * 所以仅在执行'mount'或'open'时才调用它。我想这是将速度和实用性相结合的
 * 最好方法。若在操作过程当中更换软盘,会导致数据的丢失,这是咎由自取 :-)
 *
 * 注意!尽管目前该子程序仅用于软盘,以后任何可移动介质的块设备都将使用该
 * 程序,mount/open 操作是不需要知道是否是软盘或其它什么特殊介质的。
 */
 检查磁盘是否更换,如果已更换就使对应高速缓冲区无效。
void check_disk_change(int dev)
 从hash 队列和空闲缓冲队列中移走指定的缓冲块。
static _inline void remove_from_queues(struct buffer_head * bh)
 将指定缓冲区插入空闲链表尾并放入hash 队列中。
static _inline void insert_into_queues(struct buffer_head * bh)
 在高速缓冲中寻找给定设备和指定块的缓冲区块。
// 如果找到则返回缓冲区块的指针,否则返回NULL。
static struct buffer_head * find_buffer(int dev, int block)
/*
 * 代码为什么会是这样子的?我听见你问... 原因是竞争条件。由于我们没有对
 * 缓冲区上锁(除非我们正在读取它们中的数据),那么当我们(进程)睡眠时
 * 缓冲区可能会发生一些问题(例如一个读错误将导致该缓冲区出错)。目前
 * 这种情况实际上是不会发生的,但处理的代码已经准备好了。
 */
struct buffer_head * get_hash_table(int dev, int block)
 取高速缓冲中指定的缓冲区。
// 检查所指定的缓冲区是否已经在高速缓冲中,如果不在,就需要在高速缓冲中建立一个对应的新项。
// 返回相应缓冲区头指针。
struct buffer_head * getblk(int dev,int block)
 释放指定的缓冲区。
// 等待该缓冲区解锁。引用计数递减1。唤醒等待空闲缓冲区的进程。
void brelse(struct buffer_head * buf)
/*
 * 从设备上读取指定的数据块并返回含有数据的缓冲区。如果指定的块不存在
 * 则返回NULL。
 */
 从指定设备上读取指定的数据块。
struct buffer_head * bread(int dev,int block)
 复制内存块。
// 从from 地址复制一块数据到to 位置。
extern __inline void COPYBLK(char* from, char* to)
/*
 * bread_page 一次读四个缓冲块内容读到内存指定的地址。它是一个完整的函数,
 * 因为同时读取四块可以获得速度上的好处,不用等着读一块,再读一块了。
 */
 读设备上一个页面(4 个缓冲块)的内容到内存指定的地址。
void bread_page(unsigned long address,int dev,int b[4])
/*
 * OK,breada 可以象bread 一样使用,但会另外预读一些块。该函数参数列表
 * 需要使用一个负数来表明参数列表的结束。
 */
 从指定设备读取指定的一些块。
// 成功时返回第1 块的缓冲区头指针,否则返回NULL。
struct buffer_head * breada(int dev,int first, ...)
 缓冲区初始化函数。
// 参数buffer_end 是指定的缓冲区内存的末端。对于系统有16MB 内存,则缓冲区末端设置为4MB。
// 对于系统有8MB 内存,缓冲区末端设置为2MB。
void buffer_init(long buffer_end)


fs/char_dev.c

// 定义字符设备读写函数指针类型。
typedef (*crw_ptr)(int rw,unsigned minor,char * buf,int count,off_t * pos);
 串口终端读写操作函数。
// 参数:rw - 读写命令;minor - 终端子设备号;buf - 缓冲区;cout - 读写字节数;
//       pos - 读写操作当前指针,对于终端操作,该指针无用。
// 返回:实际读写的字节数。
static int rw_ttyx(int rw,unsigned minor,char * buf,int count,off_t * pos)
 终端读写操作函数。
// 同上rw_ttyx(),只是增加了对进程是否有控制终端的检测。
static int rw_tty(int rw,unsigned minor,char * buf,int count, off_t * pos)
 内存数据读写。未实现。
static int rw_ram(int rw,char * buf, int count, off_t *pos)
 内存数据读写操作函数。未实现。
static int rw_mem(int rw,char * buf, int count, off_t * pos)
 内核数据区读写函数。未实现。
static int rw_kmem(int rw,char * buf, int count, off_t * pos)
// 端口读写操作函数。
// 参数:rw - 读写命令;buf - 缓冲区;cout - 读写字节数;pos - 端口地址。
// 返回:实际读写的字节数。
static int rw_port(int rw,char * buf, int count, off_t * pos)
 内存读写操作函数。
static int rw_memory(int rw, unsigned minor, char * buf, int count, off_t * pos)
 字符设备读写操作函数。
// 参数:rw - 读写命令;dev - 设备号;buf - 缓冲区;count - 读写字节数;pos -读写指针。
// 返回:实际读/写字节数。
int rw_char(int rw,int dev, char * buf, int count, off_t * pos)

fs/exec.c

/*
 * create_tables()函数在新用户内存中解析环境变量和参数字符串,由此
 * 创建指针表,并将它们的地址放到"堆栈"上,然后返回新栈的指针值。
 */
 在新用户堆栈中创建环境和参数变量指针表。
// 参数:p - 以数据段为起点的参数和环境信息偏移指针;argc - 参数个数;envc -环境变量数。
// 返回:堆栈指针。
static unsigned long * create_tables(char * p,int argc,int envc)

/*
 * count()函数计算命令行参数/环境变量的个数。
 */
 计算参数个数。
// 参数:argv - 参数指针数组,最后一个指针项是NULL。
// 返回:参数个数。
static int count(char ** argv)
 复制指定个数的参数字符串到参数和环境空间。
// 参数:argc - 欲添加的参数个数;argv - 参数指针数组;page - 参数和环境空间页面指针数组。
//       p -在参数表空间中的偏移指针,始终指向已复制串的头部;from_kmem - 字符串来源标志。
// 在do_execve()函数中,p 初始化为指向参数表(128kB)空间的最后一个长字处,参数字符串
// 是以堆栈操作方式逆向往其中复制存放的,因此p 指针会始终指向参数字符串的头部。
// 返回:参数和环境空间当前头部指针。
static unsigned long copy_strings(int argc,char ** argv,unsigned long *page,
		unsigned long p, int from_kmem)
 修改局部描述符表中的描述符基址和段限长,并将参数和环境空间页面放置在数据段末端。
// 参数:text_size - 执行文件头部中a_text 字段给出的代码段长度值;
//       page - 参数和环境空间页面指针数组。
// 返回:数据段限长值(64MB)。
static unsigned long change_ldt(unsigned long text_size,unsigned long * page)
/*
 * 'do_execve()'函数执行一个新程序。
 */
 execve()系统中断调用函数。加载并执行子进程(其它程序)。
// 该函数系统中断调用(int 0x80)功能号__NR_execve 调用的函数。
// 参数:eip - 指向堆栈中调用系统中断的程序代码指针eip 处,参见kernel/system_call.s 程序
// 开始部分的说明;tmp - 系统中断调用本函数时的返回地址,无用;
//                 filename - 被执行程序文件名;argv - 命令行参数指针数组;
//                 envp - 环境变量指针数组。
// 返回:如果调用成功,则不返回;否则设置出错号,并返回-1。
int do_execve(unsigned long * eip,long tmp,char * filename,
	char ** argv, char ** envp)


fs/fcntl.c

 复制文件句柄(描述符)。
// 参数fd 是欲复制的文件句柄,arg 指定新文件句柄的最小数值。
// 返回新文件句柄或出错码。
static int dupfd(unsigned int fd, unsigned int arg)
 复制文件句柄系统调用函数。
// 复制指定文件句柄oldfd,新句柄值等于newfd。如果newfd 已经打开,则首先关闭之。
int sys_dup2(unsigned int oldfd, unsigned int newfd)
 复制文件句柄系统调用函数。
// 复制指定文件句柄oldfd,新句柄的值是当前最小的未用句柄。
int sys_dup(unsigned int fildes)
 文件控制系统调用函数。
// 参数fd 是文件句柄,cmd 是操作命令(参见include/fcntl.h,23-30 行)。
int sys_fcntl(unsigned int fd, unsigned int cmd, unsigned long arg)

fs/file_dev.c

 文件读函数- 根据i 节点和文件结构,读设备数据。
// 由i 节点可以知道设备号,由filp 结构可以知道文件中当前读写指针位置。buf 指定用户态中
// 缓冲区的位置,count 为需要读取的字节数。返回值是实际读取的字节数,或出错号(小于0)。
int file_read(struct m_inode * inode, struct file * filp, char * buf, int count)
 文件写函数- 根据i 节点和文件结构信息,将用户数据写入指定设备。
// 由i 节点可以知道设备号,由filp 结构可以知道文件中当前读写指针位置。buf 指定用户态中
// 缓冲区的位置,count 为需要写入的字节数。返回值是实际写入的字节数,或出错号(小于0)。
int file_write(struct m_inode * inode, struct file * filp, char * buf, int count)

fs/inode.c

 等待指定的i 节点可用。
// 如果i 节点已被锁定,则将当前任务置为不可中断的等待状态。直到该i 节点解锁。
static _inline void wait_on_inode(struct m_inode * inode)
 对指定的i 节点上锁(锁定指定的i 节点)。
// 如果i 节点已被锁定,则将当前任务置为不可中断的等待状态。
// 直到该i 节点解锁,然后对其上锁。
static _inline void lock_inode(struct m_inode * inode)
 对指定的i 节点解锁。
// 复位i 节点的锁定标志,并明确地唤醒等待此i 节点的进程。
static _inline void unlock_inode(struct m_inode * inode)
 释放内存中设备dev 的所有i 节点。
// 扫描内存中的i 节点表数组,如果是指定设备使用的i 节点就释放之。
void invalidate_inodes(int dev)
 同步所有i 节点。
// 同步内存与设备上的所有i 节点信息。
void sync_inodes(void)
 文件数据块映射到盘块的处理操作。(block 位图处理函数,bmap - block map)
// 参数:inode – 文件的i 节点;block – 文件中的数据块号;create - 创建标志。
// 如果创建标志置位,则在对应逻辑块不存在时就申请新磁盘块。
// 返回block 数据块对应在设备上的逻辑块号(盘块号)。
static int _bmap(struct m_inode * inode,int block,int create)
 根据i 节点信息取文件数据块block 在设备上对应的逻辑块号。
int bmap(struct m_inode * inode,int block)
 创建文件数据块block 在设备上对应的逻辑块,并返回设备上对应的逻辑块号。
int create_block(struct m_inode * inode, int block)
 释放一个i 节点(回写入设备)。
void iput(struct m_inode * inode)
 从i 节点表(inode_table)中获取一个空闲i 节点项。
// 寻找引用计数count 为0 的i 节点,并将其写盘后清零,返回其指针。
struct m_inode * get_empty_inode(void)
 获取管道节点。返回为i 节点指针(如果是NULL 则失败)。
// 首先扫描i 节点表,寻找一个空闲i 节点项,然后取得一页空闲内存供管道使用。
// 然后将得到的i 节点的引用计数置为2(读者和写者),初始化管道头和尾,置i 节点的管道类型表示。
struct m_inode * get_pipe_inode(void)
 从设备上读取指定节点号的i 节点。
// nr - i 节点号。
struct m_inode * iget(int dev,int nr)
 从设备上读取指定i 节点的信息到内存中(缓冲区中)。
static void read_inode(struct m_inode * inode)
 将指定i 节点信息写入设备(写入缓冲区相应的缓冲块中,待缓冲区刷新时会写入盘中)。
static void write_inode(struct m_inode * inode)


fs/ioctl.c

 系统调用函数- 输入输出控制函数。
// 参数:fd - 文件描述符;cmd - 命令码;arg - 参数。
// 返回:成功则返回0,否则返回出错码。
int sys_ioctl(unsigned int fd, unsigned int cmd, unsigned long arg)

fs/ioctl.c

/*
 *	permission()
 *
 * 该函数用于检测一个文件的读/写/执行权限。我不知道是否只需检查euid,还是
 * 需要检查euid 和uid 两者,不过这很容易修改。
 */
 检测文件访问许可权限。
// 参数:inode - 文件对应的i 节点;mask - 访问属性屏蔽码。
// 返回:访问许可返回1,否则返回0。
static int permission(struct m_inode * inode,int mask)

/*
 * ok,我们不能使用strncmp 字符串比较函数,因为名称不在我们的数据空间(不在内核空间)。
 * 因而我们只能使用match()。问题不大。match()同样也处理一些完整的测试。
 *
 * 注意!与strncmp 不同的是match()成功时返回1,失败时返回0。
 */
 指定长度字符串比较函数。
// 参数:len - 比较的字符串长度;name - 文件名指针;de - 目录项结构。
// 返回:相同返回1,不同返回0。
static int match(int len,const char * name,struct dir_entry * de)
/*
 *	find_entry()
 *
 * 在指定的目录中寻找一个与名字匹配的目录项。返回一个含有找到目录项的高速
 * 缓冲区以及目录项本身(作为一个参数- res_dir)。并不读目录项的i 节点- 如
 * 果需要的话需自己操作。
 *
 * '..'目录项,操作期间也会对几种特殊情况分别处理- 比如横越一个伪根目录以
 * 及安装点。
 */
 查找指定目录和文件名的目录项。
// 参数:dir - 指定目录i 节点的指针;name - 文件名;namelen - 文件名长度;
// 返回:高速缓冲区指针;res_dir - 返回的目录项结构指针;
static struct buffer_head * find_entry(struct m_inode ** dir,
	const char * name, int namelen, struct dir_entry ** res_dir)
/*
 *	add_entry()
 *
 * 使用与find_entry()同样的方法,往指定目录中添加一文件目录项。
 * 如果失败则返回NULL。
 *
 * 注意!!'de'(指定目录项结构指针)的i 节点部分被设置为0 - 这表示
 * 在调用该函数和往目录项中添加信息之间不能睡眠,因为若睡眠那么其它
 * 人(进程)可能会已经使用了该目录项。
 */
 根据指定的目录和文件名添加目录项。
// 参数:dir - 指定目录的i 节点;name - 文件名;namelen - 文件名长度;
// 返回:高速缓冲区指针;res_dir - 返回的目录项结构指针;
static struct buffer_head * add_entry(struct m_inode * dir,
	const char * name, int namelen, struct dir_entry ** res_dir)
/*
 *	get_dir()
 *
 * 该函数根据给出的路径名进行搜索,直到达到最顶端的目录。
 * 如果失败则返回NULL。
 */
 搜寻指定路径名的目录。
// 参数:pathname - 路径名。
// 返回:目录的i 节点指针。失败时返回NULL。
static struct m_inode * get_dir(const char * pathname)
/*
 *	dir_namei()
 *
 * dir_namei()函数返回指定目录名的i 节点指针,以及在最顶层目录的名称。
 */
// 参数:pathname - 目录路径名;namelen - 路径名长度。
// 返回:指定目录名最顶层目录的i 节点指针和最顶层目录名及其长度。
static struct m_inode * dir_namei(const char * pathname,
	int * namelen, const char ** name)
/*
 *	namei()
 *
 * 该函数被许多简单的命令用于取得指定路径名称的i 节点。open、link 等则使用它们
 * 自己的相应函数,但对于象修改模式'chmod'等这样的命令,该函数已足够用了。
 */
 取指定路径名的i 节点。
// 参数:pathname - 路径名。
// 返回:对应的i 节点。
struct m_inode * namei(const char * pathname)
/*
 *	open_namei()
 *
 * open()所使用的namei 函数- 这其实几乎是完整的打开文件程序。
 */
 文件打开namei 函数。
// 参数:pathname - 文件路径名;flag - 文件打开标志;mode - 文件访问许可属性;
// 返回:成功返回0,否则返回出错码;res_inode - 返回的对应文件路径名的的i 节点指针。
int open_namei(const char * pathname, int flag, int mode,
	struct m_inode ** res_inode)
 系统调用函数- 创建一个特殊文件或普通文件节点(node)。
// 创建名称为filename,由mode 和dev 指定的文件系统节点(普通文件、设备特殊文件或命名管道)。
// 参数:filename - 路径名;mode - 指定使用许可以及所创建节点的类型;dev - 设备号。
// 返回:成功则返回0,否则返回出错码。
int sys_mknod(const char * filename, int mode, int dev)
 系统调用函数- 创建目录。
// 参数:pathname - 路径名;mode - 目录使用的权限属性。
// 返回:成功则返回0,否则返回出错码。
int sys_mkdir(const char * pathname, int mode)
/*
 * 用于检查指定的目录是否为空的子程序(用于rmdir 系统调用函数)。
 */
 检查指定目录是否是空的。
// 参数:inode - 指定目录的i 节点指针。
// 返回:0 - 是空的;1 - 不空。
static int empty_dir(struct m_inode * inode)
 系统调用函数- 删除指定名称的目录。
// 参数: name - 目录名(路径名)。
// 返回:返回0 表示成功,否则返回出错号。
int sys_rmdir(const char * name)
 系统调用函数- 删除文件名以及可能也删除其相关的文件。
// 从文件系统删除一个名字。如果是一个文件的最后一个连接,并且没有进程正打开该文件,则该文件
// 也将被删除,并释放所占用的设备空间。
// 参数:name - 文件名。
// 返回:成功则返回0,否则返回出错号。
int sys_unlink(const char * name)
 系统调用函数- 为文件建立一个文件名。
// 为一个已经存在的文件创建一个新连接(也称为硬连接- hard link)。
// 参数:oldname - 原路径名;newname - 新的路径名。
// 返回:若成功则返回0,否则返回出错号。
int sys_link(const char * oldname, const char * newname)


fs/open.c

// 取文件系统信息系统调用函数。
int sys_ustat(int dev, struct ustat * ubuf)
 设置文件访问和修改时间。
// 参数filename 是文件名,times 是访问和修改时间结构指针。
// 如果times 指针不为NULL,则取utimbuf 结构中的时间信息来设置文件的访问和修改时间。如果
// times 指针是NULL,则取系统当前时间来设置指定文件的访问和修改时间域。
int sys_utime(char * filename, struct utimbuf * times)
/*
 * 文件属性XXX,我们该用真实用户id 还是有效用户id?BSD 系统使用了真实用户id,
 * 以使该调用可以供setuid 程序使用。(注:POSIX 标准建议使用真实用户ID)
 */
 检查对文件的访问权限。
// 参数filename 是文件名,mode 是屏蔽码,由R_OK(4)、W_OK(2)、X_OK(1)和F_OK(0)组成。
// 如果请求访问允许的话,则返回0,否则返回出错码。
int sys_access(const char * filename,int mode)
 改变当前工作目录系统调用函数。
// 参数filename 是目录名。
// 操作成功则返回0,否则返回出错码。
int sys_chdir(const char * filename)
 改变根目录系统调用函数。
// 将指定的路径名改为根目录'/'。
// 如果操作成功则返回0,否则返回出错码。
int sys_chroot(const char * filename)
 修改文件属性系统调用函数。
// 参数filename 是文件名,mode 是新的文件属性。
// 若操作成功则返回0,否则返回出错码。
int sys_chmod(const char * filename,int mode)
 修改文件宿主系统调用函数。
// 参数filename 是文件名,uid 是用户标识符(用户id),gid 是组id。
// 若操作成功则返回0,否则返回出错码。
int sys_chown(const char * filename,int uid,int gid)
 打开(或创建)文件系统调用函数。
// 参数filename 是文件名,flag 是打开文件标志:只读O_RDONLY、只写O_WRONLY 或读写O_RDWR,
// 以及O_CREAT、O_EXCL、O_APPEND 等其它一些标志的组合,若本函数创建了一个新文件,则mode
// 用于指定使用文件的许可属性,这些属性有S_IRWXU(文件宿主具有读、写和执行权限)、S_IRUSR
// (用户具有读文件权限)、S_IRWXG(组成员具有读、写和执行权限)等等。对于新创建的文件,这些
// 属性只应用于将来对文件的访问,创建了只读文件的打开调用也将返回一个可读写的文件句柄。
// 若操作成功则返回文件句柄(文件描述符),否则返回出错码。(参见sys/stat.h, fcntl.h)
int sys_open(const char * filename,int flag,int mode)
 创建文件系统调用函数。
// 参数pathname 是路径名,mode 与上面的sys_open()函数相同。
// 成功则返回文件句柄,否则返回出错码。
int sys_creat(const char * pathname, int mode)
// 关闭文件系统调用函数。
// 参数fd 是文件句柄。
// 成功则返回0,否则返回出错码。
int sys_close(unsigned int fd)


fs/pipe.c

 管道读操作函数。
// 参数inode 是管道对应的i 节点,buf 是数据缓冲区指针,count 是读取的字节数。
int read_pipe(struct m_inode * inode, char * buf, int count)
 管道写操作函数。
// 参数inode 是管道对应的i 节点,buf 是数据缓冲区指针,count 是将写入管道的字节数。
int write_pipe(struct m_inode * inode, char * buf, int count)
 创建管道系统调用函数。
// 在fildes 所指的数组中创建一对文件句柄(描述符)。这对文件句柄指向一管道i 节点。fildes[0]
// 用于读管道中数据,fildes[1]用于向管道中写入数据。
// 成功时返回0,出错时返回-1。
int sys_pipe(unsigned long * fildes)

fs/read_write.c

 重定位文件读写指针系统调用函数。
// 参数fd 是文件句柄,offset 是新的文件读写指针偏移值,origin 是偏移的起始位置,是SEEK_SET
// (0,从文件开始处)、SEEK_CUR(1,从当前读写位置)、SEEK_END(2,从文件尾处)三者之一。
int sys_lseek (unsigned int fd, off_t offset, int origin)
 读文件系统调用函数。
// 参数fd 是文件句柄,buf 是缓冲区,count 是欲读字节数。
int sys_read (unsigned int fd, char *buf, int count)

fs/stat.c

 复制文件状态信息。
// 参数inode 是文件对应的i 节点,statbuf 是stat 文件状态结构指针,用于存放取得的状态信息。
static void
cp_stat (struct m_inode *inode, struct stat *statbuf)
 文件状态系统调用函数 - 根据文件名获取文件状态信息。
// 参数filename 是指定的文件名,statbuf 是存放状态信息的缓冲区指针。
// 返回0,若出错则返回出错码。
int
sys_stat (char *filename, struct stat *statbuf)
 文件状态系统调用 - 根据文件句柄获取文件状态信息。
// 参数fd 是指定文件的句柄(描述符),statbuf 是存放状态信息的缓冲区指针。
// 返回0,若出错则返回出错码。
int
sys_fstat (unsigned int fd, struct stat *statbuf)


fs/super.c

/* set_bit()使用了setb 指令,因为汇编编译器gas 不能识别指令setc */
 测试指定位偏移处比特位的值(0 或1),并返回该比特位值。(应该取名为test_bit()更妥帖)
// 嵌入式汇编宏。参数bitnr 是比特位偏移值,addr 是测试比特位操作的起始地址。
// %0 - ax(__res),%1 - 0,%2 - bitnr,%3 - addr
/*#define set_bit(bitnr,addr) ({ \
register int __res __asm__( "ax"); \
__asm__( "bt %2,%3;setb %%al": "=a" (__res): "a" (0), "r" (bitnr), "m" (*(addr))); \
__res; })*/
extern _inline int set_bit(int bitnr,char* addr)
 锁定指定的超级块。
static void
lock_super (struct super_block *sb)
 对指定超级块解锁。(如果使用ulock_super 这个名称则更妥帖)。
static void
free_super (struct super_block *sb)
 睡眠等待超级块解锁。
static void
wait_on_super (struct super_block *sb)
 取指定设备的超级块。返回该超级块结构指针。
struct super_block *
get_super (int dev)
 释放指定设备的超级块。
// 释放设备所使用的超级块数组项(置s_dev=0),并释放该设备i 节点位图和逻辑块位图所占用
// 的高速缓冲块。如果超级块对应的文件系统是根文件系统,或者其i 节点上已经安装有其它的文件
// 系统,则不能释放该超级块。
void
put_super (int dev)
 从设备上读取超级块到缓冲区中。
// 如果该设备的超级块已经在高速缓冲中并且有效,则直接返回该超级块的指针。
static struct super_block *
read_super (int dev)
 卸载文件系统的系统调用函数。
// 参数dev_name 是设备文件名。
int
sys_umount (char *dev_name)
 安装文件系统调用函数。
// 参数dev_name 是设备文件名,dir_name 是安装到的目录名,rw_flag 被安装文件的读写标志。
// 将被加载的地方必须是一个目录名,并且对应的i 节点没有被其它程序占用。
int
sys_mount (char *dev_name, char *dir_name, int rw_flag)
 安装根文件系统。
// 该函数是在系统开机初始化设置时(sys_setup())调用的。( kernel/blk_drv/hd.c, 157 )
void
mount_root (void)

fs/truncate.c

 释放一次间接块。
static void free_ind (int dev, int block)
 释放二次间接块。
static void
free_dind (int dev, int block)
 将节点对应的文件长度截为0,并释放占用的设备空间。
void
truncate (struct m_inode *inode)

lib/_exit.c

 内核使用的程序(退出)终止函数。
// 直接调用系统中断int 0x80,功能号__NR_exit。
// 参数:exit_code - 退出码。
//volatile 
void _exit(int exit_code)


lib/close.c

// 关闭文件函数。
// 下面该调用宏函数对应:int close(int fd)。直接调用了系统中断int 0x80,参数是__NR_close。
// 其中fd 是文件描述符。
_syscall1(int,close,int,fd)


lib/dup.c

 复制文件描述符函数。
// 下面该调用宏函数对应:int dup(int fd)。直接调用了系统中断int 0x80,参数是__NR_dup。
// 其中fd 是文件描述符。
_syscall1(int,dup,int,fd)

lib/execve.c

 加载并执行子进程(其它程序)函数。
// 下面该调用宏函数对应:int execve(const char * file, char ** argv, char ** envp)。
// 参数:file - 被执行程序文件名;argv - 命令行参数指针数组;envp - 环境变量指针数组。
// 直接调用了系统中断int 0x80,参数是__NR_execve。参见include/unistd.h 和fs/exec.c 程序。
_syscall3(int,execve,const char *,file,char **,argv,char **,envp)

lib/malloc.c

/*
 * 下面的子程序用于初始化一页桶描述符页面。
 */
 初始化桶描述符。
// 建立空闲桶描述符链表,并让free_bucket_desc 指向第一个空闲桶描述符。
static _inline void init_bucket_desc()
 分配动态内存函数。
// 参数:len - 请求的内存块长度。
// 返回:指向被分配内存的指针。如果失败则返回NULL。
void *malloc(unsigned int len)
/*
 * 下面是释放子程序。如果你知道释放对象的大小,则free_s()将使用该信息加速
 * 搜寻对应桶描述符的速度。
 * 
 * 我们将定义一个宏,使得"free(x)"成为"free_s(x, 0)"。
 */
 释放存储桶对象。
// 参数:obj - 对应对象指针;size - 大小。
void free_s(void *obj, int size)


lib/open.c

 打开文件函数。
// 打开并有可能创建一个文件。
// 参数:filename - 文件名;flag - 文件打开标志;...
// 返回:文件描述符,若出错则置出错码,并返回-1。
int open(const char * filename, int flag, ...)

lib/setsid.c

 创建一个会话并设置进程组号。
// 下面系统调用宏对应于函数:pid_t setsid()。
// 返回:调用进程的会话标识符(session ID)。
_syscall0(pid_t,setsid)

lib/wait.c

 等待进程终止系统调用函数。
// 该下面宏结构对应于函数:pid_t waitpid(pid_t pid, int * wait_stat, int options)
//
// 参数:pid - 等待被终止进程的进程id,或者是用于指定特殊情况的其它特定数值;
//       wait_stat - 用于存放状态信息;options - WNOHANG 或WUNTRACED 或是0。
_syscall3(pid_t,waitpid,pid_t,pid,int *,wait_stat,int,options)

 wait()系统调用。直接调用waitpid()函数。
pid_t wait(int * wait_stat)


lib/write.c

 写文件系统调用函数。
// 该宏结构对应于函数:int write(int fd, const char * buf, off_t count)
// 参数:fd - 文件描述符;buf - 写缓冲区指针;count - 写字节数。
// 返回:成功时返回写入的字节数(0 表示写入0 字节);出错时将返回-1,并且设置了出错号。
_syscall3(int,write,int,fd,const char *,buf,off_t,count)


mm/memory.c

void do_exit(long code);// 进程退出处理函数,在kernel/exit.c。

 显示内存已用完出错信息,并退出。
static _inline void oom(void)
// 复制1 页内存(4K 字节)。
//#define copy_page(from,to) \
//__asm__("cld ; rep ; movsl"::"S" (from),"D" (to),"c" (1024):"cx","di","si")
#define copy_page(from,to) _copy_page((void *)(from),(void *)(to))
_inline void _copy_page(void *from, void *to)
/*
 * 获取首个(实际上是最后1 个:-)物理空闲页面,并标记为已使用。如果没有空闲页面,
 * 就返回0。
 */
 取物理空闲页面。如果已经没有可用内存了,则返回0。
// 输入:%1(ax=0) - 0;%2(LOW_MEM);%3(cx=PAGING PAGES);%4(edi=mem_map+PAGING_PAGES-1)。
// 输出:返回%0(ax=页面起始地址)。
// 上面%4 寄存器实际指向mem_map[]内存字节图的最后一个字节。本函数从字节图末端开始向前扫描
// 所有页面标志(页面总数为PAGING_PAGES),若有页面空闲(其内存映像字节为0)则返回页面地址。
// 注意!本函数只是指出在主内存区的一页空闲页面,但并没有映射到某个进程的线性地址去。后面
// 的put_page()函数就是用来作映射的。
unsigned long get_free_page(void)
/*
 * 释放物理地址'addr'开始的一页内存。用于函数'free_page_tables()'。
 */
 释放物理地址addr 开始的一页面内存。
// 1MB 以下的内存空间用于内核程序和缓冲,不作为分配页面的内存空间。
void free_page(unsigned long addr)
/*
 * 下面函数释放页表连续的内存块,'exit()'需要该函数。与copy_page_tables()
 * 类似,该函数仅处理4Mb 的内存块。
 */
 根据指定的线性地址和限长(页表个数),释放对应内存页表所指定的内存块并置表项空闲。
// 页目录位于物理地址0 开始处,共1024 项,占4K 字节。每个目录项指定一个页表。
// 页表从物理地址0x1000 处开始(紧接着目录空间),每个页表有1024 项,也占4K 内存。
// 每个页表项对应一页物理内存(4K)。目录项和页表项的大小均为4 个字节。
// 参数:from - 起始基地址;size - 释放的长度。
int free_page_tables(unsigned long from,unsigned long size)
/*
 * 好了,下面是内存管理mm 中最为复杂的程序之一。它通过只复制内存页面
 * 来拷贝一定范围内线性地址中的内容。希望代码中没有错误,因为我不想
 * 再调试这块代码了 :-)
 *
 * 注意!我们并不是仅复制任何内存块- 内存块的地址需要是4Mb 的倍数(正好
 * 一个页目录项对应的内存大小),因为这样处理可使函数很简单。不管怎样,
 * 它仅被fork()使用(fork.c)
 *
 * 注意!!当from==0 时,是在为第一次fork()调用复制内核空间。此时我们
 * 不想复制整个页目录项对应的内存,因为这样做会导致内存严重的浪费- 我们
 * 只复制头160 个页面- 对应640kB。即使是复制这些页面也已经超出我们的需求,
 * 但这不会占用更多的内存- 在低1Mb 内存范围内我们不执行写时复制操作,所以
 * 这些页面可以与内核共享。因此这是nr=xxxx 的特殊情况(nr 在程序中指页面数)。
 */
 复制指定线性地址和长度(页表个数)内存对应的页目录项和页表,从而被复制的页目录和
 页表对应的原物理内存区被共享使用。
// 复制指定地址和长度的内存对应的页目录项和页表项。需申请页面来存放新页表,原内存区被共享;
// 此后两个进程将共享内存区,直到有一个进程执行写操作时,才分配新的内存页(写时复制机制)。
int copy_page_tables(unsigned long from,unsigned long to,long size)
/*
 * 下面函数将一内存页面放置在指定地址处。它返回页面的物理地址,如果
 * 内存不够(在访问页表或页面时),则返回0。
 */
 把一物理内存页面映射到指定的线性地址处。
// 主要工作是在页目录和页表中设置指定页面的信息。若成功则返回页面地址。
unsigned long put_page(unsigned long page,unsigned long address)
 取消写保护页面函数。用于页异常中断过程中写保护异常的处理(写时复制)。
// 输入参数为页表项指针。
// [ un_wp_page 意思是取消页面的写保护:Un-Write Protected。]
void un_wp_page(unsigned long * table_entry)
/*
 * 当用户试图往一个共享页面上写时,该函数处理已存在的内存页面,(写时复制)
 * 它是通过将页面复制到一个新地址上并递减原页面的共享页面计数值实现的。
 *
 * 如果它在代码空间,我们就以段错误信息退出。
 */
 页异常中断处理调用的C 函数。写共享页面处理函数。在page.s 程序中被调用。
// 参数error_code 是由CPU 自动产生,address 是页面线性地址。
// 写共享页面时,需复制页面(写时复制)。
void do_wp_page(unsigned long error_code,unsigned long address)
 写页面验证。
// 若页面不可写,则复制页面。在fork.c 第34 行被调用。
void write_verify(unsigned long address)
 取得一页空闲内存并映射到指定线性地址处。
// 与get_free_page()不同。get_free_page()仅是申请取得了主内存区的一页物理内存。而该函数
// 不仅是获取到一页物理内存页面,还进一步调用put_page(),将物理页面映射到指定的线性地址
// 处。
void get_empty_page(unsigned long address)
/*
 * try_to_share()在任务"p"中检查位于地址"address"处的页面,看页面是否存在,是否干净。
 * 如果是干净的话,就与当前任务共享。
 *
 * 注意!这里我们已假定p !=当前任务,并且它们共享同一个执行程序。
 */
 尝试对进程指定地址处的页面进行共享操作。
// 同时还验证指定的地址处是否已经申请了页面,若是则出错,死机。
// 返回1-成功,0-失败。
static int try_to_share(unsigned long address, struct task_struct * p)
/*
 * share_page()试图找到一个进程,它可以与当前进程共享页面。参数address 是
 * 当前数据空间中期望共享的某页面地址。
 *
 * 首先我们通过检测executable->i_count 来查证是否可行。如果有其它任务已共享
 * 该inode,则它应该大于1。
 */
 共享页面。在缺页处理时看看能否共享页面
// 返回1 - 成功,0 - 失败。。
static int share_page(unsigned long address)
 页异常中断处理调用的函数。处理缺页异常情况。在page.s 程序中被调用。
// 参数error_code 是由CPU 自动产生,address 是页面线性地址。
void do_no_page(unsigned long error_code,unsigned long address)
 物理内存初始化。
// 参数:start_mem - 可用作分页处理的物理内存起始位置(已去除RAMDISK 所占内存空间等)。
// end_mem - 实际物理内存最大地址。
// 在该版的linux 内核中,最多能使用16Mb 的内存,大于16Mb 的内存将不于考虑,弃置不用。
// 0 - 1Mb 内存空间用于内核系统(其实是0-640Kb)。
void mem_init(long start_mem, long end_mem)
// 计算内存空闲页面数并显示。
void calc_mem(void)


mm/page.s

;/* passed
; *  该文件包括页异常中断处理程序(中断14),主要分两种情况处理。
; * 一是由于缺页引起的页异常中断,通过调用do_no_page(error_code, address)来处理;
; * 二是由页写保护引起的页异常,此时调用页写保护处理函数do_wp_page(error_code, address)
; * 进行处理。其中的出错码(error_code)是由CPU 自动产生并压入堆栈的,出现异常时访问的
; * 线性地址是从控制寄存器CR2 中取得的。CR2 是专门用来存放页出错时的线性地址。
; */
.586p
.model flat
;/*
; * page.s 程序包含底层页异常处理代码。实际的工作在memory.c 中完成。
; */
extrn _do_no_page:proc,_do_wp_page:proc
public _page_fault

.code
_page_fault:
	xchg ss:[esp],eax	;// 取出错码到eax。
	push ecx
	push edx
	push ds
	push es
	push fs
	mov edx,10h		;// 置内核数据段选择符。
	mov ds,dx
	mov es,dx
	mov fs,dx
	mov edx,cr2			;// 取引起页面异常的线性地址
	push edx			;// 将该线性地址和出错码压入堆栈,作为调用函数的参数。
	push eax
	test eax,1			;// 测试标志P,如果不是缺页引起的异常则跳转。
	jne l1
	call _do_no_page	;// 调用缺页处理函数(mm/memory.c,365 行)。
	jmp l2			
l1:	call _do_wp_page	;// 调用写保护处理函数(mm/memory.c,247 行)。
l2:	add esp,8		;// 丢弃压入栈的两个参数。
	pop fs
	pop es
	pop ds
	pop edx
	pop ecx
	pop eax
	iretd
end
;/*
; * 当处理器在转换线性地址到物理地址的过程中检测到以下两种条件时,
; * 就会发生页异常中断,中断14。
; *   o 当CPU 发现对应页目录项或页表项的存在位(Present)标志为0。
; *   o 当前进程没有访问指定页面的权限。
; * 对于页异常处理中断,CPU 提供了两项信息用来诊断页异常和从中恢复运行。
; * (1) 放在堆栈上的出错码。该出错码指出了异常是由于页不存在引起的还是违反了访问权限引起的;
; * 		在发生异常时CPU 的当前特权层;以及是读操作还是写操作。出错码的格式是一个32 位的长
; * 		字。但只用了最后的3 个比特位。分别说明导致异常发生时的原因:
; * 		位2(U/S) - 0 表示在超级用户模式下执行,1 表示在用户模式下执行;
; * 		位1(W/R) - 0 表示读操作,1 表示写操作;
; * 		位0(P) - 0 表示页不存在,1 表示页级保护。
; * (2) CR2(控制寄存器2)。CPU 将造成异常的用于访问的线性地址存放在CR2 中。异常处理程序可以
; * 		使用这个地址来定位相应的页目录和页表项。如果在页异常处理程序执行期间允许发生另一
; * 		个页异常,那么处理程序应该将CR2 压入堆栈中。
; */

kernel/asm.s

;/*
;* asm.s contains the low-level code for most hardware faults.
;* page_exception is handled by the mm, so that isn't here. This
;* file also handles (hopefully) fpu-exceptions due to TS-bit, as
;* the fpu must be properly saved/resored. This hasn't been tested.
;* eax = -1
;* 系统中断调用(eax=调用号)
;* ebx,ecx,edx 中放有调用参数
;* 调用号超范围?
;* 中断返回
;* 寄存器入栈
;* ds,es 指向内核代码段
;* fs 指向局部数据段(用户数据)
;* 调用对应的C 处理函数
;* 任务状态?
;* 调用schedule() 时间片=0?
;* 初始任务?
;* 弹出入栈的寄存器
;* 超级用户程序?
;* 用户堆栈?
;* 根据进程信号位图取进程的最
;* 小信号量,调用do signal()
;*/
;/*
;* asm.s 程序中包括大部分的硬件故障(或出错)处理的底层次代码。页异常是由内存管理程序
;* mm 处理的,所以不在这里。此程序还处理(希望是这样)由于TS-位而造成的fpu 异常,
;* 因为fpu 必须正确地进行保存/恢复处理,这些还没有测试过。
;*/

;// 本代码文件主要涉及对Intel 保留的中断int0--int16 的处理(int17-int31 留作今后使用)。
;// 以下是一些全局函数名的声明,其原形在traps.c 中说明。
extrn _do_divide_error:far, _do_int3:far, _do_nmi:far, _do_overflow:far
extrn _do_bounds:far, _do_invalid_op:far, _do_coprocessor_segment_overrun:far
extrn _do_reserved:far, _coprocessor_error:far ptr, _do_double_fault:far
extrn _do_invalid_TSS:far, _do_segment_not_present:far
extrn _do_stack_segment:far, _do_general_protection:far

public _divide_error,_debug,_nmi,_int3,_overflow,_bounds,_invalid_op
public _double_fault,_coprocessor_segment_overrun
public _invalid_TSS,_segment_not_present,_stack_segment
public _general_protection,_irq13,_reserved

;// int0 -- (下面这段代码的含义参见图4.1(a))。
;// 下面是被零除出错(divide_error)处理代码。标号'_divide_error'实际上是C 语言函
;// 数divide_error()编译后所生成模块中对应的名称。'_do_divide_error'函数在traps.c 中。
.code
_divide_error:
	push dword ptr _do_divide_error ;// 首先把将要调用的函数地址入栈。这段程序的出错号为0。
no_error_code: ;// 这里是无出错号处理的入口处,见下面第55 行等。
	xchg [esp],eax ;// _do_divide_error 的地址 -> eax,eax 被交换入栈。
	push ebx
	push ecx
	push edx
	push edi
	push esi
	push ebp
	push ds ;// !!16 位的段寄存器入栈后也要占用4 个字节。
	push es
	push fs
	push 0 ;// "error code" ;// 将出错码入栈。
	lea edx,[esp+44] ;// 取原调用返回地址处堆栈指针位置,并压入堆栈。
	push edx
	mov edx,10h ;// 内核代码数据段选择符。
	mov ds,dx
	mov es,dx
	mov fs,dx
	call eax ;// 调用C 函数do_divide_error()。
	add esp,8 ;// 让堆栈指针重新指向寄存器fs 入栈处。
	pop fs
	pop es
	pop ds
	pop ebp
	pop esi
	pop edi
	pop edx
	pop ecx
	pop ebx
	pop eax ;// 弹出原来eax 中的内容。
	iretd

;// int1 -- debug 调试中断入口点。处理过程同上。
_debug:
	push _do_int3 ;// _do_debug C 函数指针入栈。以下同。
	jmp no_error_code

;// int2 -- 非屏蔽中断调用入口点。
_nmi:
	push _do_nmi
	jmp no_error_code

;// int3 -- 同_debug。
_int3:
	push _do_int3
	jmp no_error_code

;// int4 -- 溢出出错处理中断入口点。
_overflow:
	push _do_overflow
	jmp no_error_code

;// int5 -- 边界检查出错中断入口点。
_bounds:
	push _do_bounds
	jmp no_error_code

;// int6 -- 无效操作指令出错中断入口点。
_invalid_op:
	push _do_invalid_op
	jmp no_error_code

;// int9 -- 协处理器段超出出错中断入口点。
_coprocessor_segment_overrun:
	push _do_coprocessor_segment_overrun
	jmp no_error_code

;// int15 – 保留。
_reserved:
	push _do_reserved
	jmp no_error_code

;// int45 -- ( = 0x20 + 13 ) 数学协处理器(Coprocessor)发出的中断。
;// 当协处理器执行完一个操作时就会发出IRQ13 中断信号,以通知CPU 操作完成。
_irq13:
	push eax
	xor al,al ;// 80387 在执行计算时,CPU 会等待其操作的完成。
	out 0F0h,al ;// 通过写0xF0 端口,本中断将消除CPU 的BUSY 延续信号,并重新
;// 激活80387 的处理器扩展请求引脚PEREQ。该操作主要是为了确保
;// 在继续执行80387 的任何指令之前,响应本中断。
	mov al,20h
	out 20h,al ;// 向8259 主中断控制芯片发送EOI(中断结束)信号。
	jmp l1 ;// 这两个跳转指令起延时作用。
l1: jmp l2
l2: out 0A0h,al ;// 再向8259 从中断控制芯片发送EOI(中断结束)信号。
	pop eax
	jmp _coprocessor_error ;// _coprocessor_error 原来在本文件中,现在已经放到
							;// (kernel/system_call.s, 131)

;// 以下中断在调用时会在中断返回地址之后将出错号压入堆栈,因此返回时也需要将出错号弹出。
;// int8 -- 双出错故障。(下面这段代码的含义参见图4.1(b))。
_double_fault:
	push _do_double_fault ;// C 函数地址入栈。
error_code:
	xchg [esp+4],eax ;// error code <-> %eax,eax 原来的值被保存在堆栈上。
	xchg [esp],ebx ;// &function <-> %ebx,ebx 原来的值被保存在堆栈上。
	push ecx
	push edx
	push edi
	push esi
	push ebp
	push ds
	push es
	push fs
	push eax ;// error code ;// 出错号入栈。
	lea eax,[esp+44] ;// offset ;// 程序返回地址处堆栈指针位置值入栈。
	push eax
	mov eax,10h ;// 置内核数据段选择符。
	mov ds,ax
	mov es,ax
	mov fs,ax
	call ebx ;// 调用相应的C 函数,其参数已入栈。
	add esp,8 ;// 堆栈指针重新指向栈中放置fs 内容的位置。
	pop fs
	pop es
	pop ds
	pop ebp
	pop esi
	pop edi
	pop edx
	pop ecx
	pop ebx
	pop eax
	iretd

;// int10 -- 无效的任务状态段(TSS)。
_invalid_TSS:
	push _do_invalid_TSS
	jmp error_code

;// int11 -- 段不存在。
_segment_not_present:
	push _do_segment_not_present
	jmp error_code

;// int12 -- 堆栈段错误。
_stack_segment:
	push _do_stack_segment
	jmp error_code

;// int13 -- 一般保护性出错。
_general_protection:
	push _do_general_protection
	jmp error_code

;// int7 -- 设备不存在(_device_not_available)在(kernel/system_call.s,148)
;// int14 -- 页错误(_page_fault)在(mm/page.s,14)
;// int16 -- 协处理器错误(_coprocessor_error)在(kernel/system_call.s,131)
;// 时钟中断int 0x20 (_timer_interrupt)在(kernel/system_call.s,176)
;// 系统调用int 0x80 (_system_call)在(kernel/system_call.s,80)
end


kernel/exit.c

 释放指定进程(任务)。
void release (struct task_struct *p)
 向指定任务(*p)发送信号(sig),权限为priv。
static _inline int
send_sig (long sig, struct task_struct *p, int priv)
 终止会话(session)。
static void kill_session (void)
/*
* 为了向进程组等发送信号,XXX 需要检查许可。kill()的许可机制非常巧妙!
*/
 kill()系统调用可用于向任何进程或进程组发送任何信号。
// 如果pid 值>0,则信号被发送给pid。
// 如果pid=0,那么信号就会被发送给当前进程的进程组中的所有进程。
// 如果pid=-1,则信号sig 就会发送给除第一个进程外的所有进程。
// 如果pid < -1,则信号sig 将发送给进程组-pid 的所有进程。
// 如果信号sig 为0,则不发送信号,但仍会进行错误检查。如果成功则返回0。
int sys_kill (int pid, int sig)
 通知父进程 -- 向进程pid 发送信号SIGCHLD:子进程将停止或终止。
// 如果没有找到父进程,则自己释放。
static void tell_father (int pid)
 程序退出处理程序。在系统调用的中断处理程序中被调用。
int do_exit (long code)		// code 是错误码。
 系统调用exit()。终止进程。
int sys_exit (int error_code)
 系统调用waitpid()。挂起当前进程,直到pid 指定的子进程退出(终止)或者收到要求终止
// 该进程的信号,或者是需要调用一个信号句柄(信号处理程序)。如果pid 所指的子进程早已
// 退出(已成所谓的僵死进程),则本调用将立刻返回。子进程使用的所有资源将释放。
// 如果pid > 0, 表示等待进程号等于pid 的子进程。
// 如果pid = 0, 表示等待进程组号等于当前进程的任何子进程。
// 如果pid < -1, 表示等待进程组号等于pid 绝对值的任何子进程。
// [ 如果pid = -1, 表示等待任何子进程。]
// 若options = WUNTRACED,表示如果子进程是停止的,也马上返回。
// 若options = WNOHANG,表示如果没有子进程退出或终止就马上返回。
// 如果stat_addr 不为空,则就将状态信息保存到那里。
int sys_waitpid (pid_t pid, unsigned long *stat_addr, int options)


kernel/fork.c

 进程空间区域写前验证函数。
// 对当前进程的地址addr 到addr+size 这一段进程空间以页为单位执行写操作前的检测操作。
// 若页面是只读的,则执行共享检验和复制页面操作(写时复制)。
void verify_area (void *addr, int size)
// 设置新任务的代码和数据段基址、限长并复制页表。
// nr 为新任务号;p 是新任务数据结构的指针。
int copy_mem (int nr, struct task_struct *p)
/*
* OK,下面是主要的fork 子程序。它复制系统进程信息(task[n])并且设置必要的寄存器。
* 它还整个地复制数据段。
*/
// 复制进程。
int copy_process (int nr, long ebp, long edi, long esi, long gs, long none,
				  long ebx, long ecx, long edx,
				  long fs, long es, long ds,
				  long eip, long cs, long eflags, long esp, long ss)
// 为新进程取得不重复的进程号last_pid,并返回在任务数组中的任务号(数组index)。
int find_empty_process (void)


kernel/mktime.c

// 该函数计算从1970 年1 月1 日0 时起到开机当日经过的秒数,作为开机时间。
long
kernel_mktime (struct tm *tm)


kernel/panic.c

// 该函数用来显示内核中出现的重大错误信息,并运行文件系统同步函数,然后进入死循环 -- 死机。
// 如果当前进程是任务0 的话,还说明是交换任务出错,并且还没有运行文件系统同步函数。
void panic (const char *s)

kernel/printk.c

// 内核使用的显示函数。
int printk (const char *fmt, ...)


kernel/sched.c

// 显示任务号nr 的进程号、进程状态和内核堆栈空闲字节数(大约)。
void show_task (int nr, struct task_struct *p)
// 显示所有任务的任务号、进程号、进程状态和内核堆栈空闲字节数(大约)。
void show_stat (void)
/*
 * 将当前协处理器内容保存到老协处理器状态数组中,并将当前任务的协处理器
 * 内容加载进协处理器。
 */
// 当任务被调度交换过以后,该函数用以保存原任务的协处理器状态(上下文)并恢复新调度进来的
// 当前任务的协处理器执行状态。
void math_state_restore ()
/*
 * 'schedule()'是调度函数。这是个很好的代码!没有任何理由对它进行修改,因为它可以在所有的
 * 环境下工作(比如能够对IO-边界处理很好的响应等)。只有一件事值得留意,那就是这里的信号
 * 处理代码。
 * 注意!!任务0 是个闲置('idle')任务,只有当没有其它任务可以运行时才调用它。它不能被杀
 * 死,也不能睡眠。任务0 中的状态信息'state'是从来不用的。
 */
void schedule (void)
 pause()系统调用。转换当前任务的状态为可中断的等待状态,并重新调度。
// 该系统调用将导致进程进入睡眠状态,直到收到一个信号。该信号用于终止进程或者使进程调用
// 一个信号捕获函数。只有当捕获了一个信号,并且信号捕获处理函数返回,pause()才会返回。
// 此时pause()返回值应该是-1,并且errno 被置为EINTR。这里还没有完全实现(直到0.95 版)。
int sys_pause (void)
// 把当前任务置为不可中断的等待状态,并让睡眠队列头的指针指向当前任务。
// 只有明确地唤醒时才会返回。该函数提供了进程与中断处理程序之间的同步机制。
// 函数参数*p 是放置等待任务的队列头指针。(参见列表后的说明)。
void sleep_on (struct task_struct **p)
// 将当前任务置为可中断的等待状态,并放入*p 指定的等待队列中。参见列表后对sleep_on()的说明。
void interruptible_sleep_on (struct task_struct **p)
// 唤醒指定任务*p。
void wake_up (struct task_struct **p)
// 指定软盘到正常运转状态所需延迟滴答数(时间)。
// nr -- 软驱号(0-3),返回值为滴答数。
int ticks_to_floppy_on (unsigned int nr)
// 等待指定软驱马达启动所需时间。
void floppy_on (unsigned int nr)
// 置关闭相应软驱马达停转定时器(3 秒)。
void floppy_off (unsigned int nr)
// 软盘定时处理子程序。更新马达启动定时值和马达关闭停转计时值。该子程序是在时钟定时
// 中断中被调用,因此每一个滴答(10ms)被调用一次,更新马达开启或停转定时器的值。如果某
// 一个马达停转定时到,则将数字输出寄存器马达启动位复位。
void do_floppy_timer (void)
// 添加定时器。输入参数为指定的定时值(滴答数)和相应的处理程序指针。
// jiffies – 以10 毫秒计的滴答数;*fn()- 定时时间到时执行的函数。
void add_timer (long jiffies, void (*fn) ())
 时钟中断C 函数处理程序,在kernel/system_call.s 中的_timer_interrupt(176 行)被调用。
// 参数cpl 是当前特权级0 或3,0 表示内核代码在执行。
// 对于一个进程由于执行时间片用完时,则进行任务切换。并执行一个计时更新工作。
void do_timer (long cpl)
// 系统调用功能 - 设置报警定时时间值(秒)。
// 如果已经设置过alarm 值,则返回旧值,否则返回0。
int sys_alarm (long seconds)
// 系统调用功能 -- 降低对CPU 的使用优先权(有人会用吗??)。
// 应该限制increment 大于0,否则的话,可使优先权增大!!
int sys_nice (long increment)
// 调度程序的初始化子程序。
void sched_init (void)


kernel/signal.c

volatile void do_exit (int error_code);	// 前面的限定符volatile 要求编译器不要对其进行优化。

// 获取当前任务信号屏蔽位图(屏蔽码)。
int sys_sgetmask ()
// 设置新的信号屏蔽位图。SIGKILL 不能被屏蔽。返回值是原信号屏蔽位图。
int sys_ssetmask (int newmask)
// 复制sigaction 数据到fs 数据段to 处。。
static _inline void save_old (char *from, char *to)
// 把sigaction 数据从fs 数据段from 位置复制到to 处。
static _inline void get_new (char *from, char *to)
// signal()系统调用。类似于sigaction()。为指定的信号安装新的信号句柄(信号处理程序)。
// 信号句柄可以是用户指定的函数,也可以是SIG_DFL(默认句柄)或SIG_IGN(忽略)。
// 参数signum --指定的信号;handler -- 指定的句柄;restorer –原程序当前执行的地址位置。
// 函数返回原信号句柄。
int sys_signal (int signum, long handler, long restorer)
// sigaction()系统调用。改变进程在收到一个信号时的操作。signum 是除了SIGKILL 以外的任何
// 信号。[如果新操作(action)不为空]则新操作被安装。如果oldaction 指针不为空,则原操作
// 被保留到oldaction。成功则返回0,否则为-1。
int sys_sigaction (int signum, const struct sigaction *action,
					struct sigaction *oldaction)
// 系统调用中断处理程序中真正的信号处理程序(在kernel/system_call.s,119 行)。
// 该段代码的主要作用是将信号的处理句柄插入到用户程序堆栈中,并在本系统调用结束
// 返回后立刻执行信号句柄程序,然后继续执行用户的程序。
void do_signal (long signr, long eax, long ebx, long ecx, long edx,
			long fs, long es, long ds,
			long eip, long cs, long eflags, unsigned long *esp, long ss)


kernel/sys.c

// 返回日期和时间。
int sys_ftime ()
// 用于当前进程对子进程进行调试(degugging)。
int sys_ptrace ()
// 改变并打印终端行设置。
int sys_stty ()
// 取终端行设置信息。
int sys_gtty ()
// 设置当前任务的实际以及/或者有效组ID(gid)。如果任务没有超级用户特权,
// 那么只能互换其实际组ID 和有效组ID。如果任务具有超级用户特权,就能任意设置有效的和实际
// 的组ID。保留的gid(saved gid)被设置成与有效gid 同值。
int sys_setregid (int rgid, int egid)
// 设置进程组号(gid)。如果任务没有超级用户特权,它可以使用setgid()将其有效gid
// (effective gid)设置为成其保留gid(saved gid)或其实际gid(real gid)。如果任务有
// 超级用户特权,则实际gid、有效gid 和保留gid 都被设置成参数指定的gid。
int sys_setgid (int gid)
// 返回从1970 年1 月1 日00:00:00 GMT 开始计时的时间值(秒)。如果tloc 不为null,则时间值
// 也存储在那里。
int sys_time (long *tloc)
/*
* 无特权的用户可以见实际用户标识符(real uid)改成有效用户标识符(effective uid),反之也然。
*/
// 设置任务的实际以及/或者有效用户ID(uid)。如果任务没有超级用户特权,那么只能互换其
// 实际用户ID 和有效用户ID。如果任务具有超级用户特权,就能任意设置有效的和实际的用户ID。
// 保留的uid(saved uid)被设置成与有效uid 同值。
int sys_setreuid (int ruid, int euid)
// 设置任务用户号(uid)。如果任务没有超级用户特权,它可以使用setuid()将其有效uid
// (effective uid)设置成其保留uid(saved uid)或其实际uid(real uid)。如果任务有
// 超级用户特权,则实际uid、有效uid 和保留uid 都被设置成参数指定的uid。
int sys_setuid (int uid)
// 设置系统时间和日期。参数tptr 是从1970 年1 月1 日00:00:00 GMT 开始计时的时间值(秒)。
// 调用进程必须具有超级用户权限。
int sys_stime (long *tptr)
// 当参数end_data_seg 数值合理,并且系统确实有足够的内存,而且进程没有超越其最大数据段大小
// 时,该函数设置数据段末尾为end_data_seg 指定的值。该值必须大于代码结尾并且要小于堆栈
// 结尾16KB。返回值是数据段的新结尾值(如果返回值与要求值不同,则表明有错发生)。
// 该函数并不被用户直接调用,而由libc 库函数进行包装,并且返回值也不一样。
int sys_brk (unsigned long end_data_seg)
/*
* 下面代码需要某些严格的检查…
* 我只是没有胃口来做这些。我也不完全明白sessions/pgrp 等。还是让了解它们的人来做吧。
*/
// 将参数pid 指定进程的进程组ID 设置成pgid。如果参数pid=0,则使用当前进程号。如果
// pgid 为0,则使用参数pid 指定的进程的组ID 作为pgid。如果该函数用于将进程从一个
// 进程组移到另一个进程组,则这两个进程组必须属于同一个会话(session)。在这种情况下,
// 参数pgid 指定了要加入的现有进程组ID,此时该组的会话ID 必须与将要加入进程的相同(193 行)。
int sys_setpgid (int pid, int pgid)
// 获取系统信息。其中utsname 结构包含5 个字段,分别是:本版本操作系统的名称、网络节点名称、
// 当前发行级别、版本级别和硬件类型名称。
int sys_uname (struct utsname *name)
// 设置当前进程创建文件属性屏蔽码为mask & 0777。并返回原屏蔽码。
int sys_umask (int mask)


kernel/system_call.s

;/*
;* system_call.s 文件包含系统调用(system-call)底层处理子程序。由于有些代码比较类似,所以
;* 同时也包括时钟中断处理(timer-interrupt)句柄。硬盘和软盘的中断处理程序也在这里。
;*
;* 注意:这段代码处理信号(signal)识别,在每次时钟中断和系统调用之后都会进行识别。一般
;* 中断信号并不处理信号识别,因为会给系统造成混乱。
;*
;* 从系统调用返回('ret_from_system_call')时堆栈的内容见上面19-30 行。
;*/
SIG_CHLD = 17 ;// 定义SIG_CHLD 信号(子进程停止或结束)。

R_EAX = 00h ;// 堆栈中各个寄存器的偏移位置。
R_EBX = 04h
R_ECX = 08h
R_EDX = 0Ch
R_FS = 10h
R_ES = 14h
R_DS = 18h
R_EIP = 1Ch
R_CS = 20h
EFLAGS = 24h
OLDR_ESP = 28h ;// 当有特权级变化时。
OLR_DSS = 2Ch

;// 以下这些是任务结构(task_struct)中变量的偏移值,参见include/linux/sched.h,77 行开始。
state = 0 ;// these are offsets into the task-struct. ;// 进程状态码
counter = 4 ;// 任务运行时间计数(递减)(滴答数),运行时间片。
priority = 8 ;// 运行优先数。任务开始运行时counter=priority,越大则运行时间越长。
signal = 12 ;// 是信号位图,每个比特位代表一种信号,信号值=位偏移值+1。
sigaction = 16 ;// MUST be 16 (=len of sigaction) // sigaction 结构长度必须是16 字节。
;// 信号执行属性结构数组的偏移值,对应信号将要执行的操作和标志信息。
blocked = (33*16) ;// 受阻塞信号位图的偏移量。

;// 以下定义在sigaction 结构中的偏移量,参见include/signal.h,第48 行开始。
;// offsets within sigaction
sa_handler = 0 ;// 信号处理过程的句柄(描述符)。
sa_mask = 4 ;// 信号量屏蔽码
sa_flags = 8 ;// 信号集。
sa_restorer = 12 ;// 返回恢复执行的地址位置。

nr_system_calls = 72 ;// Linux 0.11 版内核中的系统调用总数。

;/*
;* Ok, I get parallel printer interrupts while using the floppy for some
;* strange reason. Urgel. Now I just ignore them.
;*/
;/*
;* 好了,在使用软驱时我收到了并行打印机中断,很奇怪。呵,现在不管它。
;*/
;// 定义入口点。
extrn _schedule:proc,_do_signal:proc,_math_error:proc
extrn _math_state_restore:proc,_math_emulate:proc,_jiffies:proc
extrn _do_timer:proc,_do_execve:proc
extrn _find_empty_process:proc,_copy_process:proc
extrn _do_floppy:proc,_unexpected_floppy_interrupt:proc
extrn _do_hd:proc,_unexpected_hd_interrupt:proc

extrn _current:dword,_task:dword,_sys_call_table:dword

public _system_call,_sys_fork,_timer_interrupt,_sys_execve
public _hd_interrupt,_floppy_interrupt,_parallel_interrupt
public _device_not_available, _coprocessor_error

.code

;// 错误的系统调用号。
align 4 ;// 内存4 字节对齐。
bad_sys_call:
	mov eax,-1 ;// eax 中置-1,退出中断。
	iretd
;// 重新执行调度程序入口。调度程序schedule 在(kernel/sched.c,104)。
align 4
reschedule:
	push ret_from_sys_call ;// 将ret_from_sys_call 的地址入栈(101 行)。
	jmp _schedule
; int 0x80 --linux 系统调用入口点(调用中断int 0x80,eax 中是调用号)。
align 4
_system_call:
	cmp eax,nr_system_calls-1 ;// 调用号如果超出范围的话就在eax 中置-1 并退出。
	ja bad_sys_call
	push ds ;// 保存原段寄存器值。
	push es
	push fs
	push edx ;// ebx,ecx,edx 中放着系统调用相应的C 语言函数的调用参数。
	push ecx ;// push %ebx,%ecx,%edx as parameters
	push ebx ;// to the system call
	mov edx,10h ;// set up ds,es to kernel space
	mov ds,dx ;// ds,es 指向内核数据段(全局描述符表中数据段描述符)。
	mov es,dx
	mov edx,17h ;// fs points to local data space
	mov fs,dx ;// fs 指向局部数据段(局部描述符表中数据段描述符)。
;// 下面这句操作数的含义是:调用地址 = _sys_call_table + %eax * 4。参见列表后的说明。
;// 对应的C 程序中的sys_call_table 在include/linux/sys.h 中,其中定义了一个包括72 个
;// 系统调用C 处理函数的地址数组表。
	call [_sys_call_table+eax*4]
	push eax ;// 把系统调用号入栈。
	mov eax,_current ;// 取当前任务(进程)数据结构地址??eax。
;// 下面97-100 行查看当前任务的运行状态。如果不在就绪状态(state 不等于0)就去执行调度程序。
;// 如果该任务在就绪状态但counter[??]值等于0,则也去执行调度程序。
	cmp dword ptr [state+eax],0 ;// state
	jne reschedule
	cmp dword ptr [counter+eax],0 ;// counter
	je reschedule
;// 以下这段代码执行从系统调用C 函数返回后,对信号量进行识别处理。
ret_from_sys_call:
;// 首先判别当前任务是否是初始任务task0,如果是则不必对其进行信号量方面的处理,直接返回。
;// 103 行上的_task 对应C 程序中的task[]数组,直接引用task 相当于引用task[0]。
	mov eax,_current ;// task[0] cannot have signals
	cmp eax,_task
	je l1 ;// 向前(forward)跳转到标号l1。
;// 通过对原调用程序代码选择符的检查来判断调用程序是否是超级用户。如果是超级用户就直接
;// 退出中断,否则需进行信号量的处理。这里比较选择符是否为普通用户代码段的选择符0x000f
;// (RPL=3,局部表,第1 个段(代码段)),如果不是则跳转退出中断程序。
	cmp word ptr [R_CS+esp],0fh ;// was old code segment supervisor ?
	jne l1
;// 如果原堆栈段选择符不为0x17(也即原堆栈不在用户数据段中),则也退出。
	cmp word ptr [OLR_DSS+esp],17h ;// was stack segment = 0x17 ?
	jne l1
;// 下面这段代码(109-120)的用途是首先取当前任务结构中的信号位图(32 位,每位代表1 种信号),
;// 然后用任务结构中的信号阻塞(屏蔽)码,阻塞不允许的信号位,取得数值最小的信号值,再把
;// 原信号位图中该信号对应的位复位(置0),最后将该信号值作为参数之一调用do_signal()。
;// do_signal()在(kernel/signal.c,82)中,其参数包括13 个入栈的信息。
	mov ebx,[signal+eax] ;// 取信号位图??ebx,每1 位代表1 种信号,共32 个信号。
	mov ecx,[blocked+eax] ;// 取阻塞(屏蔽)信号位图??ecx。
	not ecx ;// 每位取反。
	and ecx,ebx ;// 获得许可的信号位图。
	bsf ecx,ecx ;// 从低位(位0)开始扫描位图,看是否有1 的位,
;// 若有,则ecx 保留该位的偏移值(即第几位0-31)。
	je l1 ;// 如果没有信号则向前跳转退出。
	btr ebx,ecx ;// 复位该信号(ebx 含有原signal 位图)。
	mov dword ptr [signal+eax],ebx ;// 重新保存signal 位图信息??current->signal。
	inc ecx ;// 将信号调整为从1 开始的数(1-32)。
	push ecx ;// 信号值入栈作为调用do_signal 的参数之一。
	call _do_signal ;// 调用C 函数信号处理程序(kernel/signal.c,82)
	pop eax ;// 弹出信号值。
l1: pop eax
	pop ebx
	pop ecx
	pop edx
	pop fs
	pop es
	pop ds
	iretd

; int16 -- 下面这段代码处理协处理器发出的出错信号。跳转执行C 函数math_error()
;// (kernel/math/math_emulate.c,82),返回后将跳转到ret_from_sys_call 处继续执行。
align 4
_coprocessor_error:
	push ds
	push es
	push fs
	push edx
	push ecx
	push ebx
	push eax
	mov eax,10h ;// ds,es 置为指向内核数据段。
	mov ds,ax
	mov es,ax
	mov eax,17h ;// fs 置为指向局部数据段(出错程序的数据段)。
	mov fs,ax
	push ret_from_sys_call ;// 把下面调用返回的地址入栈。
	jmp _math_error ;// 执行C 函数math_error()(kernel/math/math_emulate.c,37)

; int7 -- 设备不存在或协处理器不存在(Coprocessor not available)。
;// 如果控制寄存器CR0 的EM 标志置位,则当CPU 执行一个R_ESC 转义指令时就会引发该中断,这样就
;// 可以有机会让这个中断处理程序模拟R_ESC 转义指令(169 行)。
;// CR0 的TS 标志是在CPU 执行任务转换时设置的。TS 可以用来确定什么时候协处理器中的内容(上下文)
;// 与CPU 正在执行的任务不匹配了。当CPU 在运行一个转义指令时发现TS 置位了,就会引发该中断。
;// 此时就应该恢复新任务的协处理器执行状态(165 行)。参见(kernel/sched.c,77)中的说明。
;// 该中断最后将转移到标号ret_from_sys_call 处执行下去(检测并处理信号)。
align 4
_device_not_available:
	push ds
	push es
	push fs
	push edx
	push ecx
	push ebx
	push eax
	mov eax,10h ;// ds,es 置为指向内核数据段。
	mov ds,ax
	mov es,ax
	mov eax,17h ;// fs 置为指向局部数据段(出错程序的数据段)。
	mov fs,ax
	push ret_from_sys_call ;// 把下面跳转或调用的返回地址入栈。
	clts ;// clear TS so that we can use math
	mov eax,cr0
	test eax,4h ;// EM (math emulation bit)
;// 如果不是EM 引起的中断,则恢复新任务协处理器状态,
	je  goto_math_state_restore;// 执行C 函数math_state_restore()(kernel/sched.c,77)。
	push ebp
	push esi
	push edi
	call _math_emulate ;// 调用C 函数math_emulate(kernel/math/math_emulate.c,18)。
	pop edi
	pop esi
	pop ebp
	ret ;// 这里的ret 将跳转到ret_from_sys_call(101 行)。
goto_math_state_restore:
	jmp _math_state_restore

; int32 -- (int 0x20) 时钟中断处理程序。中断频率被设置为100Hz(include/linux/sched.h,5),
;// 定时芯片8253/8254 是在(kernel/sched.c,406)处初始化的。因此这里jiffies 每10 毫秒加1。
;// 这段代码将jiffies 增1,发送结束中断指令给8259 控制器,然后用当前特权级作为参数调用
;// C 函数do_timer(long CPL)。当调用返回时转去检测并处理信号。
align 4
_timer_interrupt:
	push ds ;// save ds,es and put kernel data space
	push es ;// into them. %fs is used by _system_call
	push fs
	push edx ;// we save %eax,%ecx,%edx as gcc doesn't
	push ecx ;// save those across function calls. %ebx
	push ebx ;// is saved as we use that in ret_sys_call
	push eax
	mov eax,10h ;// ds,es 置为指向内核数据段。
	mov ds,ax
	mov es,ax
	mov eax,17h ;// fs 置为指向局部数据段(出错程序的数据段)。
	mov fs,ax
	inc dword ptr _jiffies
;// 由于初始化中断控制芯片时没有采用自动EOI,所以这里需要发指令结束该硬件中断。
	mov al,20h ;// EOI to interrupt controller ;//1
	out 20h,al ;// 操作命令字OCW2 送0x20 端口。
;// 下面3 句从选择符中取出当前特权级别(0 或3)并压入堆栈,作为do_timer 的参数。
	mov eax,dword ptr [R_CS+esp]
	and eax,3 ;// %eax is CPL (0 or 3, 0=supervisor)
	push eax
;// do_timer(CPL)执行任务切换、计时等工作,在kernel/shched.c,305 行实现。
	call _do_timer ;// 'do_timer(long CPL)' does everything from
	add esp,4 ;// task switching to accounting ...
	jmp ret_from_sys_call

; 这是sys_execve()系统调用。取中断调用程序的代码指针作为参数调用C 函数do_execve()。
;// do_execve()在(fs/exec.c,182)。
align 4
_sys_execve:
	lea eax,[R_EIP+esp]
	push eax
	call _do_execve
	add esp,4 ;// 丢弃调用时压入栈的R_EIP 值。
	ret

; sys_fork()调用,用于创建子进程,是system_call 功能2。原形在include/linux/sys.h 中。
;// 首先调用C 函数find_empty_process(),取得一个进程号pid。若返回负数则说明目前任务数组
;// 已满。然后调用copy_process()复制进程。
align 4
_sys_fork:
	call _find_empty_process ;// 调用find_empty_process()(kernel/fork.c,135)。
	test eax,eax
	js l2
	push gs
	push esi
	push edi
	push ebp
	push eax
	call _copy_process ;// 调用C 函数copy_process()(kernel/fork.c,68)。
	add esp,20 ;// 丢弃这里所有压栈内容。
l2: ret

; int 46 -- (int 0x2E) 硬盘中断处理程序,响应硬件中断请求IRQ14。
;// 当硬盘操作完成或出错就会发出此中断信号。(参见kernel/blk_drv/hd.c)。
;// 首先向8259A 中断控制从芯片发送结束硬件中断指令(EOI),然后取变量do_hd 中的函数指针放入edx
;// 寄存器中,并置do_hd 为NULL,接着判断edx 函数指针是否为空。如果为空,则给edx 赋值指向
;// unexpected_hd_interrupt(),用于显示出错信息。随后向8259A 主芯片送EOI 指令,并调用edx 中
;// 指针指向的函数: read_intr()、write_intr()或unexpected_hd_interrupt()。
_hd_interrupt:
	push eax
	push ecx
	push edx
	push ds
	push es
	push fs
	mov eax,10h ;// ds,es 置为内核数据段。
	mov ds,ax
	mov es,ax
	mov eax,17h ;// fs 置为调用程序的局部数据段。
	mov fs,ax
;// 由于初始化中断控制芯片时没有采用自动EOI,所以这里需要发指令结束该硬件中断。
	mov al,20h
	out 0A0h,al ;// EOI to interrupt controller ;//1 ;// 送从8259A。
	jmp l3 ;// give port chance to breathe
l3: jmp l4 ;// 延时作用。
l4: xor edx,edx
	xchg edx,dword ptr _do_hd ;// do_hd 定义为一个函数指针,将被赋值read_intr()或
;// write_intr()函数地址。(kernel/blk_drv/hd.c)
;// 放到edx 寄存器后就将do_hd 指针变量置为NULL。
	test edx,edx ;// 测试函数指针是否为Null。
	jne l5 ;// 若空,则使指针指向C 函数unexpected_hd_interrupt()。
	mov edx,dword ptr _unexpected_hd_interrupt ;// (kernel/blk_drv/hdc,237)。
l5: out 20h,al ;// 送主8259A 中断控制器EOI 指令(结束硬件中断)。
	call edx ;// "interesting" way of handling intr.
	pop fs ;// 上句调用do_hd 指向的C 函数。
	pop es
	pop ds
	pop edx
	pop ecx
	pop eax
	iretd

; int38 -- (int 0x26) 软盘驱动器中断处理程序,响应硬件中断请求IRQ6。
;// 其处理过程与上面对硬盘的处理基本一样。(kernel/blk_drv/floppy.c)。
;// 首先向8259A 中断控制器主芯片发送EOI 指令,然后取变量do_floppy 中的函数指针放入eax
;// 寄存器中,并置do_floppy 为NULL,接着判断eax 函数指针是否为空。如为空,则给eax 赋值指向
;// unexpected_floppy_interrupt (),用于显示出错信息。随后调用eax 指向的函数: rw_interrupt,
;// seek_interrupt,recal_interrupt,reset_interrupt 或unexpected_floppy_interrupt。
_floppy_interrupt:
	push eax
	push ecx
	push edx
	push ds
	push es
	push fs
	mov eax,10h ;// ds,es 置为内核数据段。
	mov ds,ax
	mov es,ax
	mov eax,17h ;// fs 置为调用程序的局部数据段。
	mov fs,ax
	mov al,20h ;// 送主8259A 中断控制器EOI 指令(结束硬件中断)。
	out 20h,al ;// EOI to interrupt controller ;//1
	xor eax,eax
	xchg eax,dword ptr _do_floppy ;// do_floppy 为一函数指针,将被赋值实际处理C 函数程序,
;// 放到eax 寄存器后就将do_floppy 指针变量置空。
	test eax,eax ;// 测试函数指针是否=NULL?
	jne l6 ;// 若空,则使指针指向C 函数unexpected_floppy_interrupt()。
	mov eax,dword ptr _unexpected_floppy_interrupt
l6: call eax ;// "interesting" way of handling intr.
	pop fs ;// 上句调用do_floppy 指向的函数。
	pop es
	pop ds
	pop edx
	pop ecx
	pop eax
	iretd

; int 39 -- (int 0x27) 并行口中断处理程序,对应硬件中断请求信号IRQ7。
;// 本版本内核还未实现。这里只是发送EOI 指令。
_parallel_interrupt:
	push eax
	mov al,20h
	out 20h,al
	pop eax
	iretd
end

kernel/traps.c

// 以下语句定义了三个嵌入式汇编宏语句函数。有关嵌入式汇编的基本语法见列表后或参见附录。
// 取段seg 中地址addr 处的一个字节。
//#define get_seg_byte(seg,addr) ({ \
//register char __res; \
//__asm__("push %%fs;mov %%ax,%%fs;movb %%fs:%2,%%al;pop %%fs" \
//	:"=a" (__res):"0" (seg),"m" (*(addr))); \
//__res;})
_inline char get_seg_byte(unsigned short segm, void *addr)
// 取段seg 中地址addr 处的一个长字(4 字节)。
_inline long 
get_seg_long(unsigned short segm,long *addr) 
// 取fs 段寄存器的值(选择符)。
_inline unsigned short _fs() 
// 该子程序用来打印出错中断的名称、出错号、调用程序的EIP、EFLAGS、ESP、fs 段寄存器值、
// 段的基址、段的长度、进程号pid、任务号、10 字节指令码。如果堆栈在用户数据段,则还
// 打印16 字节的堆栈内容。
static void die(char * str,long esp_ptr,long nr)
// 以下这些以do_开头的函数是对应名称中断处理程序调用的C 函数。
void do_double_fault(long esp, long error_code)
// 下面是异常(陷阱)中断程序初始化子程序。设置它们的中断调用门(中断向量)。
// set_trap_gate()与set_system_gate()的主要区别在于前者设置的特权级为0,后者是3。因此
// 断点陷阱中断int3、溢出中断overflow 和边界出错中断bounds 可以由任何程序产生。
// 这两个函数均是嵌入式汇编宏程序(include/asm/system.h,第36 行、39 行)。
void trap_init(void)


kernel/vsprintf.c

// 该函数将字符数字串转换成整数。输入是数字串指针的指针,返回是结果数值。另外指针将前移。
static int skip_atoi (const char **s)
// 除操作。输入:n 为被除数,base 为除数;结果:n 为商,函数返回值为余数。
// 参见4.5.3 节有关嵌入汇编的信息。
#define do_div(n,base) _do_div(&(n),base)
extern _inline int _do_div(int *n,int base)
// 将整数转换为指定进制的字符串。
// 输入:num-整数;base-进制;size-字符串长度;precision-数字长度(精度);type-类型选项。
// 输出:str 字符串指针。
static char *
number (char *str, int num, int base, int size, int precision, int type)
// 下面函数是送格式化输出到字符串中。
// 为了能在内核中使用格式化的输出,Linus 在内核实现了该C 标准函数。
// 其中参数fmt 是格式字符串;args 是个数变化的值;buf 是输出字符串缓冲区。
// 请参见本代码列表后的有关格式转换字符的介绍。
int
vsprintf (char *buf, const char *fmt, va_list args)

kernel/blk_drv/floppy.c

// 字节直接输出(嵌入汇编语言宏)。
//#define immoutb_p(val,port) \
//__asm__ ("outb %0,%1\n\tjmp 1f\n1:\tjmp 1f\n1:"::"a" ((char) (val)),"i" (port))
void _inline immoutb_p(unsigned char val,unsigned short port)
 释放(取消选定的)软盘(软驱)。
// 数字输出寄存器(DOR)的低2 位用于指定选择的软驱(0-3 对应A-D)。
void
floppy_deselect (unsigned int nr)
/*
* floppy-change()不是从中断程序中调用的,所以这里我们可以轻松一下,睡觉等。
* 注意floppy-on()会尝试设置current_DOR 指向所需的驱动器,但当同时使用几个
* 软盘时不能睡眠:因此此时只能使用循环方式。
*/
 检测指定软驱中软盘更换情况。如果软盘更换了则返回1,否则返回0。
int
floppy_change (unsigned int nr)
 复制内存块。
//#define copy_buffer(from,to) \
// __asm__( "cld ; rep ; movsl" \
// :: "c" (BLOCK_SIZE/4), "S" ((long)(from)), "D" ((long)(to)) \
// : "cx", "di", "si")
void _inline copy_buffer(void* from, void* to)
 设置(初始化)软盘DMA 通道。
static void
setup_DMA (void)
 向软盘控制器输出一个字节数据(命令或参数)。
static void
output_byte (char byte)
 读取FDC 执行的结果信息。
// 结果信息最多7 个字节,存放在reply_buffer[]中。返回读入的结果字节数,若返回值=-1
// 表示出错。
static int
result (void)
 软盘操作出错中断调用函数。由软驱中断处理程序调用。
static void
bad_flp_intr (void)
/*
* OK,下面该中断处理函数是在DMA 读/写成功后调用的,这样我们就可以检查执行结果,
* 并复制缓冲区中的数据。
*/
 软盘读写操作成功中断调用函数。。
static void
rw_interrupt (void)
 设置DMA 并输出软盘操作命令和参数(输出1 字节命令+ 0~7 字节参数)。
_inline void
setup_rw_floppy (void)
/*
* 该子程序是在每次软盘控制器寻道(或重新校正)中断后被调用的。注意
* "unexpected interrupt"(意外中断)子程序也会执行重新校正操作,但不在此地。
*/
 寻道处理中断调用函数。
// 首先发送检测中断状态命令,获得状态信息ST0 和磁头所在磁道信息。若出错则执行错误计数
// 检测处理或取消本次软盘操作请求项。否则根据状态信息设置当前磁道变量,然后调用函数
// setup_rw_floppy()设置DMA 并输出软盘读写命令和参数。
static void
seek_interrupt (void)
/*
* 该函数是在传输操作的所有信息都正确设置好后被调用的(也即软驱马达已开启
* 并且已选择了正确的软盘(软驱)。
*/
 读写数据传输函数。
static void
transfer (void)
/*
* 特殊情况 - 用于意外中断(或复位)处理后。
*/
 软驱重新校正中断调用函数。
// 首先发送检测中断状态命令(无参数),如果返回结果表明出错,则置复位标志,否则复位重新
// 校正标志。然后再次执行软盘请求。
static void
recal_interrupt (void)
 意外软盘中断请求中断调用函数。
// 首先发送检测中断状态命令(无参数),如果返回结果表明出错,则置复位标志,否则置重新
// 校正标志。
void
unexpected_floppy_interrupt (void)
 软盘重新校正处理函数。
// 向软盘控制器FDC 发送重新校正命令和参数,并复位重新校正标志。
static void
recalibrate_floppy (void)
 软盘控制器FDC 复位中断调用函数。在软盘中断处理程序中调用。
// 首先发送检测中断状态命令(无参数),然后读出返回的结果字节。接着发送设定软驱参数命令
// 和相关参数,最后再次调用执行软盘请求。
static void
reset_interrupt (void)
/* FDC 复位是通过将数字输出寄存器(DOR)位2 置0 一会儿实现的 */
 复位软盘控制器。
static void
reset_floppy (void)
 软驱启动定时中断调用函数。
// 首先检查数字输出寄存器(DOR),使其选择当前指定的驱动器。然后调用执行软盘读写传输
// 函数transfer()。
static void
floppy_on_interrupt (void)
 软盘读写请求项处理函数。
void
do_fd_request (void)
 软盘系统初始化。
// 设置软盘块设备的请求处理函数(do_fd_request()),并设置软盘中断门(int 0x26,对应硬件
// 中断请求信号IRQ6),然后取消对该中断信号的屏蔽,允许软盘控制器FDC 发送中断请求信号。
void
floppy_init (void)


kernel/blk_drv/hd.c

// 读端口port,共读nr 字,保存在buf 中。
//#define port_read(port,buf,nr) \
//__asm__( "cld;rep;insw":: "d" (port), "D" (buf), "c" (nr): "cx", "di")
_inline void port_read(unsigned short port, void* buf,unsigned long nr)
// 写端口port,共写nr 字,从buf 中取数据。
//#define port_write(port,buf,nr) \
//__asm__( "cld;rep;outsw":: "d" (port), "S" (buf), "c" (nr): "cx", "si")
_inline void port_write(unsigned short port, void* buf,unsigned long nr)
/* 下面该函数只在初始化时被调用一次。用静态变量callable 作为可调用标志。*/
// 该函数的参数由初始化程序init/main.c 的init 子程序设置为指向0x90080 处,此处存放着setup.s
// 程序从BIOS 取得的2 个硬盘的基本参数表(32 字节)。硬盘参数表信息参见下面列表后的说明。
// 本函数主要功能是读取CMOS 和硬盘参数表信息,用于设置硬盘分区结构hd,并加载RAM 虚拟盘和
// 根文件系统。
int sys_setup (unsigned char *BIOS)
 判断并循环等待驱动器就绪。
// 读硬盘控制器状态寄存器端口HD_STATUS(0x1f7),并循环检测驱动器就绪比特位和控制器忙位。
static int controller_ready (void)
 检测硬盘执行命令后的状态。(win_表示温切斯特硬盘的缩写)
// 读取状态寄存器中的命令执行结果状态。返回0 表示正常,1 出错。如果执行命令错,
// 则再读错误寄存器HD_ERROR(0x1f1)。
static int win_result (void)
 向硬盘控制器发送命令块(参见列表后的说明)。
// 调用参数:drive - 硬盘号(0-1); nsect - 读写扇区数;
// sect - 起始扇区; head - 磁头号;
// cyl - 柱面号; cmd - 命令码;
// *intr_addr() - 硬盘中断处理程序中将调用的C 处理函数。
static void hd_out (unsigned int drive, unsigned int nsect, unsigned int sect,
		    unsigned int head, unsigned int cyl, unsigned int cmd,
		    void (*intr_addr) (void))
 等待硬盘就绪。也即循环等待主状态控制器忙标志位复位。若仅有就绪或寻道结束标志
// 置位,则成功,返回0。若经过一段时间仍为忙,则返回1。
static int drive_busy (void)
 诊断复位(重新校正)硬盘控制器。
static void reset_controller (void)
 复位硬盘nr。首先复位(重新校正)硬盘控制器。然后发送硬盘控制器命令“建立驱动器参数”,
// 其中recal_intr()是在硬盘中断处理程序中调用的重新校正处理函数。
static void reset_hd (int nr)
 意外硬盘中断调用函数。
// 发生意外硬盘中断时,硬盘中断处理程序中调用的默认C 处理函数。在被调用函数指针为空时
// 调用该函数。参见(kernel/system_call.s,241 行)。
void unexpected_hd_interrupt (void)
 读写硬盘失败处理调用函数。
static void bad_rw_intr (void)
 读操作中断调用函数。将在执行硬盘中断处理程序中被调用。
static void read_intr (void)
 写扇区中断调用函数。在硬盘中断处理程序中被调用。
// 在写命令执行后,会产生硬盘中断信号,执行硬盘中断处理程序,此时在硬盘中断处理程序中调用的
// C 函数指针do_hd()已经指向write_intr(),因此会在写操作完成(或出错)后,执行该函数。
static void write_intr (void)
 硬盘重新校正(复位)中断调用函数。在硬盘中断处理程序中被调用。
// 如果硬盘控制器返回错误信息,则首先进行硬盘读写失败处理,然后请求硬盘作相应(复位)处理。
static void recal_intr (void)
// 执行硬盘读写请求操作。
void do_hd_request (void)
// 硬盘系统初始化。
void hd_init (void)

kernel/blk_drv/ll_rw_blk.c

// 锁定指定的缓冲区bh。如果指定的缓冲区已经被其它任务锁定,则使自己睡眠(不可中断地等待),
// 直到被执行解锁缓冲区的任务明确地唤醒。
static _inline void
lock_buffer (struct buffer_head *bh)
// 释放(解锁)锁定的缓冲区。
static _inline void
unlock_buffer (struct buffer_head *bh)
/*
* add-request()向连表中加入一项请求。它关闭中断,
* 这样就能安全地处理请求连表了 
*/
 向链表中加入请求项。参数dev 指定块设备,req 是请求的结构信息。
  static void
add_request (struct blk_dev_struct *dev, struct request *req)
 创建请求项并插入请求队列。参数是:主设备号major,命令rw,存放数据的缓冲区头指针bh。
static void
make_request (int major, int rw, struct buffer_head *bh)
 低层读写数据块函数。
// 该函数主要是在fs/buffer.c 中被调用。实际的读写操作是由设备的request_fn()函数完成。
// 对于硬盘操作,该函数是do_hd_request()。(kernel/blk_drv/hd.c,294)
void ll_rw_block (int rw, struct buffer_head *bh)
 块设备初始化函数,由初始化程序main.c 调用(init/main.c,128)。
// 初始化请求数组,将所有请求项置为空闲项(dev = -1)。有32 项(NR_REQUEST = 32)。
void blk_dev_init (void)

kernel/blk_drv/ramdisk.c

// 执行虚拟盘(ramdisk)读写操作。程序结构与do_hd_request()类似(kernel/blk_drv/hd.c,294)。
void
do_rd_request (void)
/* 返回内存虚拟盘ramdisk 所需的内存量 */
// 虚拟盘初始化函数。确定虚拟盘在内存中的起始地址,长度。并对整个虚拟盘区清零。
long
rd_init (long mem_start, int length)
/*
* 如果根文件系统设备(root device)是ramdisk 的话,则尝试加载它。root device 原先是指向
* 软盘的,我们将它改成指向ramdisk。
*/
 加载根文件系统到ramdisk。
void
rd_load (void)


kernel/blk_drv/math_emulate.c

 协处理器仿真函数。
// 中断处理程序调用的C 函数,参见(kernel/math/system_call.s,169 行)。
void
math_emulate (long edi, long esi, long ebp, long sys_call_ret,
	      long eax, long ebx, long ecx, long edx,
	      unsigned short fs, unsigned short es, unsigned short ds,
	      unsigned long eip, unsigned short cs, unsigned long eflags,
	      unsigned short ss, unsigned long esp)
 协处理器出错处理函数。
// 中断处理程序调用的C 函数,参见(kernel/math/system_call.s,145 行)。
void
math_error (void)

学习的目标是成熟!~~

你可能感兴趣的:(linux)