系统相关
首页 > 系统相关> > Linux0.11源码学习(四)

Linux0.11源码学习(四)

作者:互联网

Linux0.11源码学习(四)

linux0.11源码学习笔记

参考资料:
https://github.com/sunym1993/flash-linux0.11-talk
https://github.com/Akagi201/linux-0.11
http://xiehongfeng100.github.io/categories/操作系统/

本文贴出的代码注释参考书籍《Linux内核完全注释》,作者赵炯。当然其中加了点私货,属于本人浅薄理解。

源码查看:
https://elixir.bootlin.com/linux/latest/source


目录

前言

吼吼,终于到了激动人心的main.c

先研究研究main函数

长文警告warning!建议结合目录食用。

本文主要涉及main函数中前面十个初始化函数。


/init/main.c

先把全文(含部分中文注释)放出来

/*
 *  linux/init/main.c
 *
 *  (C) 1991  Linus Torvalds
 */


// 定义该变量是为了包括定义在unistd.h 中的内嵌汇编代码等信息。
#define __LIBRARY__    

/*   *.h 头文件所在的默认目录是include/,则在代码中就不用明确指明位置。
     如果不是UNIX 的标准头文件,则需要指明所在的目录,并用双引号括住。
     标准符号常数与类型文件。定义了各种符号常数和类型,并申明了各种函数。
     如果定义了__LIBRARY__,则还包括系统调用号和内嵌汇编代码_syscall0()等。
*/
#include <unistd.h> 

// 时间类型头文件。其中最主要定义了tm 结构和一些有关时间的函数原形。
#include <time.h>

/*
 * we need this inline - forking from kernel space will result
 * in NO COPY ON WRITE (!!!), until an execve is executed. This
 * is no problem, but for the stack. This is handled by not letting
 * main() use the stack at all after fork(). Thus, no function
 * calls - which means inline code for fork too, as otherwise we
 * would use the stack upon exit from 'fork()'.
 *
 * Actually only pause and fork are needed inline, so that there
 * won't be any messing with the stack from main(), but we define
 * some others too.
 */
 /*
 * 我们需要下面这些内嵌语句 - 从内核空间创建进程(forking)将导致没有写时复制(COPY ON WRITE)!!!
 * 直到一个执行execve 调用。这对堆栈可能带来问题。处理的方法是在fork()调用之后不让main()使用
 * 任何堆栈。因此就不能有函数调用 - 这意味着fork 也要使用内嵌的代码,否则我们在从fork()退出
 * 时就要使用堆栈了。
 * 实际上只有pause 和fork 需要使用内嵌方式,以保证从main()中不会弄乱堆栈,但是我们同时还
 * 定义了其它一些函数。
 */

/* 
    是unistd.h 中的内嵌宏代码。以嵌入汇编的形式调用。
    Linux 的系统调用中断0x80。该中断是所有系统调用的入口。
    syscall0 名称中最后的0 表示无参数,1 表示1 个参数。
*/
static inline _syscall0(int,fork)   //该条语句实际上是int fork()创建进程系统调用。
static inline _syscall0(int,pause)  // int pause()系统调用:暂停进程的执行,直到收到一个信号。
static inline _syscall1(int,setup,void *,BIOS)  // int setup(void * BIOS)系统调用,仅用于 linux 初始化(仅在这个程序中被调用)。
static inline _syscall0(int,sync)   // int sync()系统调用:更新文件系统。

/*
    tty 头文件,定义了有关tty_io,串行通信方面的参数、常数。
*/
#include <linux/tty.h> 

/*
    调度程序头文件,定义了任务结构task_struct、第1个初始任务的数据。
    还有一些以宏的形式定义的有关描述符参数设置和获取的嵌入式汇编函数程序。
*/
#include <linux/sched.h>    

/*
    head 头文件,定义了段描述符的简单结构,和几个选择符常量。
*/
#include <linux/head.h>

/*
    系统头文件。
    以宏的形式定义了许多有关设置或修改描述符/中断门等的嵌入式汇编子程序。
*/
#include <asm/system.h>

/*
    io 头文件。以宏的嵌入汇编程序形式定义对io 端口操作的函数。
*/
#include <asm/io.h>

/*
    标准定义头文件。定义了NULL, offsetof(TYPE, MEMBER)。
*/
#include <stddef.h>

/*
    标准参数头文件。以宏的形式定义变量参数列表。
    主要说明了一个类型(va_list)和三个宏(va_start, va_arg 和va_end),vsprintf、vprintf、vfprintf。
*/
#include <stdarg.h>

/*
emmm,上面似乎已经include一遍了
*/
#include <unistd.h>

/*
    文件控制头文件。
    用于文件及其描述符的操作控制常数符号的定义。
*/
#include <fcntl.h>

/*
    类型头文件。
    定义了基本的系统数据类型。
*/
#include <sys/types.h>

/*
    文件系统头文件。
    定义文件表结构(file,buffer_head,m_inode 等)。
*/
#include <linux/fs.h>


static char printbuf[1024];// 静态字符串数组。

extern int vsprintf();// 送格式化输出到一字符串中
extern void init(void);// 函数原形,初始化
extern void blk_dev_init(void);// 块设备初始化子程序
extern void chr_dev_init(void);// 字符设备初始化
extern void hd_init(void);// 硬盘初始化程序
extern void floppy_init(void);// 软驱初始化程序
extern void mem_init(long start, long end);// 内存管理初始化
extern long rd_init(long mem_start, int length);//虚拟盘初始化
extern long kernel_mktime(struct tm * tm);// 建立内核时间(秒)。
extern long startup_time;// 内核启动时间(开机时间)(秒)。

/*
 * This is set up by the setup-routine at boot-time
 */
/*
* 以下这些数据是由setup.s 程序在引导时间设置的。
*/
#define EXT_MEM_K (*(unsigned short *)0x90002)      // 1M 以后的扩展内存大小(KB)。
#define DRIVE_INFO (*(struct drive_info *)0x90080)  // 硬盘参数表基址
#define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)  // 根文件系统所在设备号。

/*
 * Yeah, yeah, it's ugly, but I cannot find how to do this correctly
 * and this seems to work. I anybody has more info on the real-time
 * clock I'd be interested. Most of this was trial and error, and some
 * bios-listing reading. Urghh.
 */
/*
 * 是啊,是啊,下面这段程序很差劲,但我不知道如何正确地实现,而且好象它还能运行。如果有
 * 关于实时时钟更多的资料,那我很感兴趣。这些都是试探出来的,以及看了一些bios 程序,呵!
 */

// 这段宏读取CMOS 实时时钟信息。
#define CMOS_READ(addr) ({ \
outb_p(0x80|addr,0x70); \    /*0x70 是写端口号,0x80|addr 是要读取的CMOS 内存地址。*/
inb_p(0x71); \               /* 0x71 是读端口号。*/
})

#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)// 将BCD 码转换成数字。


// 该子程序取CMOS 时钟,并设置开机时间,startup_time(秒)。
static void time_init(void)
{
	struct tm time;

	do {
		time.tm_sec = CMOS_READ(0);
		time.tm_min = CMOS_READ(2);
		time.tm_hour = CMOS_READ(4);
		time.tm_mday = CMOS_READ(7);
		time.tm_mon = CMOS_READ(8);
		time.tm_year = CMOS_READ(9);
	} while (time.tm_sec != CMOS_READ(0));
	BCD_TO_BIN(time.tm_sec);
	BCD_TO_BIN(time.tm_min);
	BCD_TO_BIN(time.tm_hour);
	BCD_TO_BIN(time.tm_mday);
	BCD_TO_BIN(time.tm_mon);
	BCD_TO_BIN(time.tm_year);
	time.tm_mon--;
	startup_time = kernel_mktime(&time);
}

static long memory_end = 0;         // 机器具有的内存(字节数)。
static long buffer_memory_end = 0;  // 高速缓冲区末端地址。 
static long main_memory_start = 0;  // 主内存(将用于分页)开始的位置。

struct drive_info { char dummy[32]; } drive_info;// 用于存放硬盘参数表信息。

void main(void)		/* This really IS void, no error here. */
{			/* The startup routine assumes (well, ...) this */
            /* 这里确实是void,并没错。在startup 程序(head.s)中就是这样假设的。 */
/*
 * Interrupts are still disabled. Do necessary setups, then
 * enable them
 */
/*
* 此时中断仍被禁止着,做完必要的设置后就将其开启。
*/
 	ROOT_DEV = ORIG_ROOT_DEV;
 	drive_info = DRIVE_INFO;
	memory_end = (1<<20) + (EXT_MEM_K<<10); // 内存大小=1Mb 字节+扩展内存(k)*1024 字节。
	memory_end &= 0xfffff000;               // 忽略不到4Kb(1 页)的内存数。
	if (memory_end > 16*1024*1024)          // 如果内存超过16Mb,则按16Mb 计。
		memory_end = 16*1024*1024;
	if (memory_end > 12*1024*1024)          // 如果内存>12Mb,则设置缓冲区末端=4Mb
		buffer_memory_end = 4*1024*1024;
	else if (memory_end > 6*1024*1024)      // 否则如果内存>6Mb,则设置缓冲区末端=2Mb
		buffer_memory_end = 2*1024*1024;
	else
		buffer_memory_end = 1*1024*1024;    // 否则则设置缓冲区末端=1Mb
	main_memory_start = buffer_memory_end;  // 主内存起始位置=缓冲区末端
#ifdef RAMDISK			                    // 如果定义了虚拟盘,则主内存将减少。
    main_memory_start += rd_init (main_memory_start, RAMDISK * 1024);
#endif
	mem_init(main_memory_start,memory_end);//将可用内存进行了分页(4KB)化处理,并做好了初始化。
	trap_init();//设置一些中断程序
	blk_dev_init();//初始化request链表,为块设备通信做准备
	chr_dev_init();//为空
	tty_init();//字符设备初始化,设置串口、显示屏、键盘中断
	time_init();//设置开机启动时间
	sched_init();//进程调度初始化
	buffer_init(buffer_memory_end);//初始化了内存缓冲区
	hd_init();
	floppy_init();
	sti();
	move_to_user_mode();
	if (!fork()) {		/* we count on this going ok */
		init();
	}
/*
 *   NOTE!!   For any other task 'pause()' would mean we have to get a
 * signal to awaken, but task0 is the sole exception (see 'schedule()')
 * as task 0 gets activated at every idle moment (when no other tasks
 * can run). For task0 'pause()' just means we go check if some other
 * task can run, and if not we return here.
 */
	for(;;) pause();
}

static int printf(const char *fmt, ...)
{
	va_list args;
	int i;

	va_start(args, fmt);
	write(1,printbuf,i=vsprintf(printbuf, fmt, args));
	va_end(args);
	return i;
}

static char * argv_rc[] = { "/bin/sh", NULL };
static char * envp_rc[] = { "HOME=/", NULL };

static char * argv[] = { "-/bin/sh",NULL };
static char * envp[] = { "HOME=/usr/root", NULL };

void init(void)
{
	int pid,i;

	setup((void *) &drive_info);
	(void) open("/dev/tty0",O_RDWR,0);
	(void) dup(0);
	(void) dup(0);
	printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS,
		NR_BUFFERS*BLOCK_SIZE);
	printf("Free mem: %d bytes\n\r",memory_end-main_memory_start);
	if (!(pid=fork())) {
		close(0);
		if (open("/etc/rc",O_RDONLY,0))
			_exit(1);
		execve("/bin/sh",argv_rc,envp_rc);
		_exit(2);
	}
	if (pid>0)
		while (pid != wait(&i))
			/* nothing */;
	while (1) {
		if ((pid=fork())<0) {
			printf("Fork failed in init\r\n");
			continue;
		}
		if (!pid) {
			close(0);close(1);close(2);
			setsid();
			(void) open("/dev/tty0",O_RDWR,0);
			(void) dup(0);
			(void) dup(0);
			_exit(execve("/bin/sh",argv,envp));
		}
		while (1)
			if (pid == wait(&i))
				break;
		printf("\n\rchild %d died with code %04x\n\r",pid,i);
		sync();
	}
	_exit(0);	/* NOTE! _exit, not exit() */
}

void main(void)

接下来就细细品味void main(void)主函数吧(ง •_•)ง

开头

/*
 * Interrupts are still disabled. Do necessary setups, then
 * enable them
 */
 	ROOT_DEV = ORIG_ROOT_DEV;
 	drive_info = DRIVE_INFO;
	memory_end = (1<<20) + (EXT_MEM_K<<10); // 内存大小=1Mb 字节+扩展内存(k)*1024 字节。
	memory_end &= 0xfffff000;               // 忽略不到4Kb(1 页)的内存数。
	if (memory_end > 16*1024*1024)          // 如果内存超过16Mb,则按16Mb 计。
		memory_end = 16*1024*1024;
	if (memory_end > 12*1024*1024)          // 如果内存>12Mb,则设置缓冲区末端=4Mb
		buffer_memory_end = 4*1024*1024;
	else if (memory_end > 6*1024*1024)      // 否则如果内存>6Mb,则设置缓冲区末端=2Mb
		buffer_memory_end = 2*1024*1024;
	else
		buffer_memory_end = 1*1024*1024;    // 否则则设置缓冲区末端=1Mb
	main_memory_start = buffer_memory_end;  // 主内存起始位置=缓冲区末端
#ifdef RAMDISK			                    // 如果定义了虚拟盘,则主内存将减少。
    main_memory_start += rd_init (main_memory_start, RAMDISK * 1024);
#endif

解释:

ROOT_DEV定义在<fs/super.c>,声明在<linux/fs.h>。数据类型为int

ORIG_ROOT_DEV在该文件里define好了,表示根文件系统所在设备号。

drive_info是结构体变量,定义在该文件,就在mian函数前面。

DRIVE_INFO在该文件里define好了,表示硬盘参数表基址。

EXT_MEM_K在该文件里define好了,表示 1M 以后的扩展内存大小(KB)。

这段代码最主要的是三个变量memory_end, buffer_memory_end, main_memory_start

memory_end表示全部内存大小,单位字节。

buffer_memory_end表示高速缓冲区末端地址,单位字节。

main_memory_start表示主内存(将用于分页)开始的位置,等于高速缓冲区末端地址。

从这里可以看出所运行的内存最大只有16Mb,回望<boot/head.s>中启动分页处理子程序的Linus的注释,16Mb应该是他电脑(当时)的极限。


mem_init

第一个调用的函数,主内存区是如何管理和分配的?

这里有个初始化函数,已经在该文件前面extern声明了。

mem_init(main_memory_start,memory_end);

函数传入的参数就是上面刚算出来的主内存开始地址和内存末端地址(单位字节)。

该函数需要在<mm/memory.c>查看

这里我节选了和mem_init有关的东西。


#define LOW_MEM 0x100000	                 // 内存低端(1MB)
#define PAGING_MEMORY (15*1024*1024)	     // 分页内存15MB。主内存区最多15M。
                                             /*内存最开始的1MB不允许访问,因为存有操作系统源码*/
#define PAGING_PAGES (PAGING_MEMORY>>12)	 // 分页后的物理内存页数。
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)  // 指定内存地址映射为页号。 /*4KB=2^12byte为一页*/
#define USED 100                             // 页面被占用标志

static long HIGH_MEMORY = 0;                 // 全局变量,存放实际物理内存最高端地址。

// 内存映射字节图(1 字节代表1 页内存),每个页面对应的字节用于标志页面当前被引用(占用)次数。
static unsigned char mem_map [ PAGING_PAGES ] = {0,};

void mem_init(long start_mem, long end_mem)
{
	int i;

	HIGH_MEMORY = end_mem;              // 设置内存最高端。
	for (i=0 ; i<PAGING_PAGES ; i++)    // 首先置所有页面为已占用(USED=100)状态,
		mem_map[i] = USED;              // 即将页面映射数组全置成USED。
	i = MAP_NR(start_mem);              // 然后计算可使用起始内存的页面号
	end_mem -= start_mem;               // 再计算可分页处理的内存块大小。
	end_mem >>= 12;                     // 从而计算出可用于分页处理的页面数。
	while (end_mem-->0)                 // 最后将这些可用页面对应的页面映射数组清零。
		mem_map[i++]=0;
}

注释已经比较详细了,做个总结,mem_init函数将可用内存进行了分页(4KB)化处理,并做好了初始化。

trap_init

第二个调用的函数

trap_init()函数在<linux/sched.h>做了extern声明,在<kernel/traps.c>有定义。

void trap_init(void)
{
	int i;

	set_trap_gate(0,&divide_error);// 设置除操作出错的中断向量值。以下雷同。
	set_trap_gate(1,&debug);
	set_trap_gate(2,&nmi);
	set_system_gate(3,&int3);	/* int3-5 can be called from all */
	set_system_gate(4,&overflow);
	set_system_gate(5,&bounds);
	set_trap_gate(6,&invalid_op);
	set_trap_gate(7,&device_not_available);
	set_trap_gate(8,&double_fault);
	set_trap_gate(9,&coprocessor_segment_overrun);
	set_trap_gate(10,&invalid_TSS);
	set_trap_gate(11,&segment_not_present);
	set_trap_gate(12,&stack_segment);
	set_trap_gate(13,&general_protection);
	set_trap_gate(14,&page_fault);
	set_trap_gate(15,&reserved);
	set_trap_gate(16,&coprocessor_error);
// 下面将int17-48 的陷阱门先均设置为reserved,以后每个硬件初始化时会重新设置自己的陷阱门。
	for (i=17;i<48;i++)
		set_trap_gate(i,&reserved);
	set_trap_gate(45,&irq13);// 设置协处理器的陷阱门。
	outb_p(inb_p(0x21)&0xfb,0x21);// 允许主8259A 芯片的IRQ2 中断请求。用于控制器2级联
	outb(inb_p(0xA1)&0xdf,0xA1);// 允许从8259A 芯片的IRQ13 中断请求。数字协处理器错误,?DMA?
	set_trap_gate(39,&parallel_interrupt);
}

该函数由使用了其他函数set_trap_gateset_system_gate等,一个一个看叭。

_set_gate

set_trap_gate被define在<asm/system.h>

可以看出,它本质上是一段内嵌汇编代码

/*
	设置门描述符宏函数。
	参数:gate_addr -描述符地址;type -描述符中类型域值;dpl -描述符特权层值;addr -偏移地址。
	%0 - (由dpl,type 组合成的类型标志字);
	%1 - (描述符低4 字节地址);
	%2 - (描述符高4 字节地址);
	%3 - edx(程序偏移地址addr);
	%4 - eax(高字中含有段选择符)。
*/
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \ /*将偏移地址低字与选择符组合成描述符低4字节(eax)*/
	"movw %0,%%dx\n\t" \ /*将类型标志字与偏移高字组合成描述符高4字节(edx)。*/
	"movl %%eax,%1\n\t" \ /*分别设置门描述符的低4字节和高4字节。*/
	"movl %%edx,%2" \
	: \
	: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
	"o" (*((char *) (gate_addr))), \
	"o" (*(4+(char *) (gate_addr))), \
	"d" ((char *) (addr)),"a" (0x00080000))
/*
	设置陷阱门函数。
	参数:n - 中断号;addr - 中断程序偏移地址。
	&idt[n]对应中断号在中断描述符表中的偏移值;
	中断描述符的类型是15,特权级是0。
*/
#define set_trap_gate(n,addr) \
	_set_gate(&idt[n],15,0,addr)

GNU汇编编译器使用AT&T汇编格式,该程序涉及的内嵌汇编知识科普:

内嵌汇编着实难懂,哈哈,其实说白了,本质是4个mov,下面的"i","o","o","d","a"分别是常数(或者说地址?),对应mov语句中的0,1,2,3,4。(3——"d",4——"a"似乎没有明确调用,实际上它们已经执行完了,在mov操作前,就是往edx和eax寄存器存数据,下面我也会详细讲讲)

/*
	设置门描述符宏函数。
	参数:gate_addr -描述符地址;type -描述符中类型域值;dpl -描述符特权层值;addr -偏移地址。
	%0 - (由dpl,type 组合成的类型标志字);
	%1 - (描述符低4 字节地址);
	%2 - (描述符高4 字节地址);
	%3 - edx(程序偏移地址addr);
	%4 - eax(高字中含有段选择符)。
*/
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \ /*将偏移地址低字与选择符组合成描述符低4字节(eax)*/
	"movw %0,%%dx\n\t" \ /*将类型标志字与偏移高字组合成描述符高4字节(edx)。*/
	"movl %%eax,%1\n\t" \ /*分别设置门描述符的低4字节和高4字节。*/
	"movl %%edx,%2" \
	: \
	: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
	"o" (*((char *) (gate_addr))), \
	"o" (*(4+(char *) (gate_addr))), \
	"d" ((char *) (addr)),"a" (0x00080000))
/*
	设置陷阱门函数。
	参数:n - 中断号;addr - 中断程序偏移地址。
	&idt[n]对应中断号在中断描述符表中的偏移值;
	中断描述符的类型是15,特权级是0。
*/
#define set_trap_gate(n,addr) \
	_set_gate(&idt[n],15,0,addr)

详细解释:

首先,由set_trap_gate(0,&divide_error);可知,内嵌汇编中将 divide_error 函数(这个是什么暂且不论)的地址传给edx。为啥呢,看这一段,"d" ((char *) (addr)),"a" (0x00080000)),这一段的意思就是将addr这个形式参数(这里实际上传入的参数是中断程序偏移地址,即&divide_error,32bit)赋值给edx寄存器;将0x00080000(实际上这个数高字(高16bit)中含有段选择符)赋值给eax寄存器。

第一个mov: 将edx的低16位给ax,即 movw %%dx,%%ax。movw中的w就是word,即字,大小16bit。那么,目前eax的高16bit是0x0008(段选择符,为什么是0x0008?这个问题我们先保留),低16bit是中断程序的偏移地址(&divide_error(32bit)仅取低16bit,具体为什么,看 第二个mov 部分贴出的陷阱门描述符结构图和 第三个mov 的讲解),这就合成了陷阱门中断描述符的低4字节(32bit),后续 第三个mov 会把它插入到别的地址处。

第二个mov: 将edx的低16位置为(short) (0x8000+(dpl<<13)+(type<<8)),即movw %0,%%dx。至于dpl和type是什么,它们是传入的参数,这里

#define set_trap_gate(n,addr) \
	_set_gate(&idt[n],15,0,addr)

可知,type=15,dpl=0,由注释可知,tpye对应中断描述符的类型,dpl对应特权级。这里就涉及到陷阱门描述符的结构了,直接看图吧:

陷阱门描述符是中断描述符表中的描述符的一种,总共有64bit(8byte)。结合陷阱门描述符的结构,我们再看(short) (0x8000+(dpl<<13)+(type<<8))。上述第二个mov操作设置了edx的低16bit,而edx的高16bit仍是中断程序偏移地址,即&divide_error(32bit)的高16bit,换一种说法,edx原本就是32bit的中断程序divide_error的地址,现在edx的低16bit被数据覆盖了,啥数据,就是(short) (0x8000+(dpl<<13)+(type<<8)),至于这个16bit数代表的含义?看图可知P<=1,DLP<=00,TYPE<=1111。后续 第四个mov 会把edx(32bit)插入到别的地址处,组成陷阱门描述符的高4字节(高32bit)。结合图片描述的陷阱门描述符结构应该很好理解吧,这个结构才是这一大坨代码(4个mov)的编程根基。

第三个mov: 将eax(32bit,movl就是mov一个双字数据,即doubleword)传到*((char *) (gate_addr))地址处,即movl %%eax,%1。注意,eax在 第一个mov后 就设置完成了。看注释可知,这就设置了门描述符的低4字节,结合上图理解。gate_addr参数向上层寻找发现是&idt[n],对于set_trap_gate(0,&divide_error);应该就是&idt[0],即0号中断在中断描述符表中的偏移地址值

第四个mov: 将edx(32bit)传到到*(4+(char *) (gate_addr))地址处,即movl %%edx,%2。看注释可知,这就设置了门描述符的高4字节,这不难理解,对比两处被传递的地址就能知道,*((char *) (gate_addr))*(4+(char *) (gate_addr))。至此,就在0号中断在中断描述符表中的偏移地址处,设置了一个8字节的门描述符,结合 第二个mov 说明处贴出的结构图理解。

 

上面讲了一大堆,可能说的有点啰嗦,主要是夹杂了许多新学的汇编知识,我们了解了一些AT&T汇编格式的汇编代码。其实做个总结,简单的讲就是set_trap_gate(0,&divide_error)的作用是设置 0 号中断,对应的中断处理程序是 divide_error。

ok,回归主题,研究void trap_init(void)

void trap_init(void)
{
	int i;

	set_trap_gate(0,&divide_error);// 设置除操作出错的中断向量值。以下雷同。
	set_trap_gate(1,&debug);
	set_trap_gate(2,&nmi);
	set_system_gate(3,&int3);	/* int3-5 can be called from all */
	set_system_gate(4,&overflow);
	set_system_gate(5,&bounds);
	set_trap_gate(6,&invalid_op);
	set_trap_gate(7,&device_not_available);
	set_trap_gate(8,&double_fault);
	set_trap_gate(9,&coprocessor_segment_overrun);
	set_trap_gate(10,&invalid_TSS);
	set_trap_gate(11,&segment_not_present);
	set_trap_gate(12,&stack_segment);
	set_trap_gate(13,&general_protection);
	set_trap_gate(14,&page_fault);
	set_trap_gate(15,&reserved);
	set_trap_gate(16,&coprocessor_error);
// 下面将int17-48 的陷阱门先均设置为reserved,以后每个硬件初始化时会重新设置自己的陷阱门。
	for (i=17;i<48;i++)
		set_trap_gate(i,&reserved);
	set_trap_gate(45,&irq13);// 设置协处理器的陷阱门。
	outb_p(inb_p(0x21)&0xfb,0x21);// 允许主8259A 芯片的IRQ2 中断请求。
	outb(inb_p(0xA1)&0xdf,0xA1);// 允许从8259A 芯片的IRQ13 中断请求。
	set_trap_gate(39,&parallel_interrupt);
}

可以看到,它就是不断地调用set_trap_xxx,即不断地设置了一大堆中断程序。set_trap_gate()我们已经研究过了,至于set_system_gate(),看看它的定义,位置与set_trap_gate()相同。

/*
	设置门描述符宏函数。
	参数:gate_addr -描述符地址;type -描述符中类型域值;dpl -描述符特权层值;addr -偏移地址。
	%0 - (由dpl,type 组合成的类型标志字);
	%1 - (描述符低4 字节地址);
	%2 - (描述符高4 字节地址);
	%3 - edx(程序偏移地址addr);
	%4 - eax(高字中含有段选择符)。
*/
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
	"movw %0,%%dx\n\t" \
	"movl %%eax,%1\n\t" \
	"movl %%edx,%2" \
	: \
	: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
	"o" (*((char *) (gate_addr))), \
	"o" (*(4+(char *) (gate_addr))), \
	"d" ((char *) (addr)),"a" (0x00080000))

/*
	设置系统调用门函数。
	参数:n - 中断号;
	addr - 中断程序偏移地址。
	&idt[n]对应中断号在中断描述符表中的偏移值;
	中断描述符的类型是15,特权级是3。
*/
#define set_system_gate(n,addr) \
	_set_gate(&idt[n],15,3,addr)

看出来,set_system_gate()本质上和set_trap_gate()一样,均是调用_set_gate这个内嵌汇编。区别只是参数不同,设置的中断程序(地址)不同,中断号不同,特权级(DPL)不同,其余均一样。

outb_p / outb

然后就是,outb_p(inb_p(0x21)&0xfb,0x21);outb(inb_p(0xA1)&0xdf,0xA1);。它们均定义在<asm/io.h>

/*
	硬件端口字节输出函数。
	参数:
	value - 欲输出字节;
	port - 端口。
*/
#define outb(value,port) \
__asm__ ("outb %%al,%%dx"::"a" (value),"d" (port))
/*
	硬件端口字节输入函数。
	参数:
	port - 端口。
	返回读取的字节。
*/
#define inb(port) ({ \
unsigned char _v; \
__asm__ volatile ("inb %%dx,%%al":"=a" (_v):"d" (port)); \
_v; \
})
/*
	带延迟的硬件端口字节输出函数。
	参数:
	value - 欲输出字节;
	port - 端口。
*/
#define outb_p(value,port) \
__asm__ ("outb %%al,%%dx\n" \
		"\tjmp 1f\n" \
		"1:\tjmp 1f\n" \
		"1:"::"a" (value),"d" (port))
/*
	带延迟的硬件端口字节输入函数。
	参数:
	port - 端口。
	返回读取的字节。
*/
#define inb_p(port) ({ \
unsigned char _v; \
__asm__ volatile ("inb %%dx,%%al\n" \
	"\tjmp 1f\n" \
	"1:\tjmp 1f\n" \
	"1:":"=a" (_v):"d" (port)); \
_v; \
})		

这段汇编涉及的知识科普:

inb_p(0x21)会读取0x21端口的数据

inb_p(0xA1)会读取0xA1端口的数据

outb_p(inb_p(0x21)&0xfb,0x21);首先会运行inb_p(0x21),然后计算出outb_p的value参数,然后在0x21端口输出。

outb(inb_p(0xA1)&0xdf,0xA1);首先会运行inb_p(0xA1),然后计算出outb_p的value参数,然后在0xA1端口输出。

至于为什么这样,emm这就涉及到8259A的相关知识,具体参考有关微机原理的书籍,我就懒得讲了(忘的差不多了,懒得翻书(~﹃~)~zZ)。简单介绍一下8259A,就是可编程中断控制器(PIC - Programmable Interrupt Controller),是微机系统中管理设备中断请求的管理者。在80X86微机系统中采用了8259A可编程中断控制器芯片。每个8259A芯片可以管理8个中断源。这里两个写操作其实就是开启了主从两个8259A的某中断源的中断请求。

 

ok,关于trap_init其实还有个问题,通过上面,我们了解到,它就是用来设置中断的。但是还有些个东西没看,就是,被设置的各个中断函数还没看,

直接搜某个中断函数,比如 divide_error ,完全找不到它的定义,最后才发现它们定义在<kernel/asm.s> 或<system_call.s>中,以汇编语言的形式实现。这里我看的有点头大,就先跳过吧( ̄▽ ̄)",说出那句话,为什么?这个问题我们先保留。

blk_dev_init

第三个调用的函数,块设备请求项初始化函数。

具体什么情况,一起来看看吧。

blk_dev_init()这个函数在main.c这个文件的前面已经extern声明过了,而具体的定义在<kernel/blk_drv/ll_rw_blk.c>中,照例,我截取了相关的部分。

#include "blk.h"// 块设备头文件。定义请求数据结构、块设备数据结构和宏函数等信息。
/*
 * The request-struct contains all necessary data
 * to load a nr of sectors into memory
 */
/*
 * 请求结构中含有加载 nr 扇区数据到内存的所有必须的信息。
 */
struct request request[NR_REQUEST];
/*
	块设备初始化函数,由初始化程序main.c 调用。
	作用:初始化请求数组,将所有请求项置为空闲项(dev = -1)。有32项(NR_REQUEST = 32)。
*/
void blk_dev_init(void)
{
	int i;

	for (i=0 ; i<NR_REQUEST ; i++) {
		request[i].dev = -1;
		request[i].next = NULL;
	}
}

NR_REQUEST定义在<kernel/blk_drv/blk.h>

/*
* NR_REQUEST is the number of entries in the request-queue.
* NOTE that writes may use only the low 2/3 of these: reads
* take precedence.
*
* 32 seems to be a reasonable number: enough to get some benefit
* from the elevator-mechanism, but not so much as to lock a lot of
* buffers when they are in the queue. 64 seems to be too many (easily
* long pauses in reading when heavy writing/syncing is going on)
*/
/*
* 下面定义的NR_REQUEST 是请求队列中所包含的项数。
* 注意,读操作仅使用这些项低端的2/3;读操作优先处理。
*
* 32 项好象是一个合理的数字:已经足够从电梯算法中获得好处,
* 但当缓冲区在队列中而锁住时又不显得是很大的数。64 就看上
* 去太大了(当大量的写/同步操作运行时很容易引起长时间的暂停)。
*/
#define NR_REQUEST 32

struct request request[NR_REQUEST]也定义在<kernel/blk_drv/blk.h>

/*
 * Ok, this is an expanded form so that we can use the same
 * request for paging requests when that is implemented. In
 * paging, 'bh' is NULL, and 'waiting' is used to wait for
 * read/write completion.
 */
/*
	OK,下面是 request 结构的一个扩展形式,因而当实现以后,我们就可以在分页请求中使用同样的 request 结构。在分页处理中,'bh'是NULL,而'waiting'则用于等待读/写的完成。
*/
struct request {
	int dev;		/* -1 if no request *//* 使用的设备号。*/
	int cmd;		/* READ or WRITE *//*命令(READ 或WRITE)。*/
	int errors;		/*操作时产生的错误次数。*/
	unsigned long sector;	/*起始扇区。(1 块 = 2 扇区)*/
	unsigned long nr_sectors; /*读/写扇区数。*/
	char * buffer;	/*数据缓冲区*/
	struct task_struct * waiting;	/*任务等待操作执行完成的地方*/
	struct buffer_head * bh;	/*缓冲区头指针(include/linux/fs.h)*/
	struct request * next;		/*指向下一请求项*/
};

 

由此看出,blk_dev_init做了一个很简单的工作,将request[]这个结构体数组(实际上是链表)中的所有项的dev置为-1,说明无设备请求,next置为null,说明没有下一请求项。

深入理解一下,这个函数名为块设备请求初始化函数,所需要做的任务就是为之后要运行的一个功能做初始化工作,该功能就是与周边的输入输出设备进行通信。那他是如何进行初始化的呢?看代码就是将request链表做好了初始化工作,方便以后使用。因此,这个request链表才是重中之重,是之后与块设备交互信息的依仗。

学习一下这个链表,注释挺详细的,但我不太理解是buffer这个指针,数据缓冲区是什么意思?

数据缓冲区位于内存中(看图片的可以跳到buffer_init目录下)。需要知道,进程要想与块设备进行通信,必须经过主机内存中的缓冲区。因此,这里所谓块设备请求的意思应该是内存与块设备之间的信息传递(读/写)。

以下是《Linux内核完全注释》对块设备(block device)和字符设备(character device)的简要说明:

操作系统的主要功能之一就是与周边的输入输出设备进行通信,采用统一的接口来控制这些外围设备。操作系统的所有设备可以粗略地分成两种类型:块设备和字符设备。块设备是一种可以以固定大小的数据块(1024B)为单位进行寻址和访问的设备,如硬盘和软盘设备。字符设备是一种以字符流为操作对象的设备,不能进行寻址操作,如打印机、网络接口、终端设备。

chr_dev_init

第四个调用的的函数,看名字叫字符设备初始化函数。

有关字符设备的解释,可以看上面我引用了《Linux内核完全注释》的话。

其定义在<kernel/chr_dev/tty_io.c>中

void chr_dev_init(void)
{
}

尴尬的是,它是空的。目前是为了以后的拓展做准备,大佬都这么有前瞻性吗(= ̄ω ̄=)

tty_init

第五个被调用的函数。被上面那个函数名字骗了,这个才是用于初始化字符设备的。

它同样定义在<kernel/chr_dev/tty_io.c>,声明在<linux/tty.h>

void tty_init(void)
{
	rs_init();//初始化串行中断程序和串行接口。
	con_init();//初始化控制台终端
}

这里它又调用了两个函数,

rs_init

初始化串行中断程序和串行接口
rs_init()声明在<linux/tty.h>,定义在<kernel/chr_drv/serial.c>

/*
* serial.c
*
* This module implements the rs232 io functions
* void rs_write(struct tty_struct * queue);
* void rs_init(void);
* and all interrupts pertaining to serial IO.
*/
/*
* serial.c
* 该程序用于实现rs232 的输入输出功能
* void rs_write(struct tty_struct *queue);
* void rs_init(void);
* 以及与传输IO 有关系的所有中断处理程序。
*/

extern void rs1_interrupt(void);// 串行口 1 的中断处理程序
extern void rs2_interrupt(void);// 串行口 2 的中断处理程序

/*
	初始化串行端口
 	port: 
	串口1 - 0x3F8,串口2 - 0x2F8。
*/
static void init(int port)
{
	outb_p(0x80,port+3);	/* set DLAB of line control reg *//* 设置线路控制寄存器的DLAB位(位7) */
	outb_p(0x30,port);	/* LS of divisor (48 -> 2400 bps *//* 发送波特率因子低字节,0x30->2400bps */
	outb_p(0x00,port+1);	/* MS of divisor *//* 发送波特率因子高字节,0x00 */
	outb_p(0x03,port+3);	/* reset DLAB *//* 复位 DLAB 位,数据位为 8 位 */
	outb_p(0x0b,port+4);	/* set DTR,RTS, OUT_2 *//* 设置DTR,RTS,辅助用户输出2 */
	outb_p(0x0d,port+1);	/* enable all intrs but writes *//* 除了写(写保持空)以外,允许所有中断源中断 */
	(void)inb(port);	/* read data port to reset things (?) *//* 读数据口,以进行复位操作(?) */
}
// 初始化串行中断程序和串行接口。
void rs_init(void)
{
	set_intr_gate(0x24,rs1_interrupt);// 设置串行口1 的中断门向量(硬件IRQ4 信号)。
	set_intr_gate(0x23,rs2_interrupt);// 设置串行口2 的中断门向量(硬件IRQ3 信号)。
	init(tty_table[1].read_q.data);// 初始化串行口1(.data 是端口号)。
	init(tty_table[2].read_q.data);// 初始化串行口2。
	outb(inb_p(0x21)&0xE7,0x21);// 允许主8259A 芯片的IRQ3,IRQ4 中断信号请求。
}

set_intr_gate定义在<asm/system.h>,和trap_init()中调用的set_trap_gate位于同一个文件,本质也是一样,都是_set_gate()这个内嵌汇编代码,其详情请移步上面trap_init的位置了解。与set_trap_gate不同的是,set_intr_gate的type(中断描述符的类型)值是14。

因此rs_init函数的前两行,就是设置了0x24号和0x23号中断,对应的中断处理函数是rs1_interruptrs2_interrupt。这两个中断函数和前面trap_init里设置的中断函数都是用汇编语言写的,均定义在<kernel/chr_drv/rs_io.s>,它们两的作用是是能串口一和串口二的中断,如今PC机上已经极少使用串口了。emmm这两个串口的中断函数还是比较难以理解的。

rs_io.s
/*
* These are the actual interrupt routines. They look where
* the interrupt is coming from, and take appropriate action.
*/
/*
* 这些是实际的中断程序。程序首先检查中断的来源,然后执行相应
* 的处理。
*/
.align 2//2字节强制对齐
//// 串行端口1 中断处理程序入口点。
_rs1_interrupt:
pushl $_table_list+8 // tty 表中对应串口1 的读写缓冲指针的地址入栈(tty_io.c,99)。
jmp rs_int
.align 2
//// 串行端口2 中断处理程序入口点。
_rs2_interrupt:
pushl $_table_list+16 // tty 表中对应串口2 的读写缓冲队列指针的地址入栈。
rs_int:
pushl %edx
pushl %ecx
pushl %ebx
pushl %eax
push %es
push %ds /* as this is an interrupt, we cannot */

···//省略一大堆汇编代码,因为看的实在头大,从rs_int开始就不知所云了(>﹏<)					

汇编语言这里我有个疑问,也是我看之前的文件存在的问题(当时没有细究)

ok,rs1_interruptrs2_interrupt的探寻之路就先到这里吧,至少我知道,它们会在有串口信息的时候起作用(ง •_•)ง

tty_table

接着往下看,是对tty_table内的成员初始化,

static void init(int port)是个内部的init函数,看看它里面只是一些读写操作(关于这些读写操作如何实现的看我前面trap_init目录下outb_p / outb的分析),总的作用就是设置了串口的通信格式并复位。

被init的是tty_table这个变量,定义在<kernel/chr_drv/tty_io.c>,并且上面提到的串口中断程序中所涉及的
table_list[]也定义在<kernel/chr_drv/tty_io.c>,声明在<linux/tty.h>

#include <linux/tty.h>	// tty 头文件,定义了有关tty_io,串行通信方面的参数、常数。

/*
	termios.h应该是在chr_drv目录下的makefile里添加了依赖,因此本文件可以使用其中的定义(目前猜测)
	ICRNL等都是在termios.h文件里定义的
*/
#define _L_FLAG(tty,f)	((tty)->termios.c_lflag & f)// 取termios 结构中的本地模式标志
#define _I_FLAG(tty,f)	((tty)->termios.c_iflag & f)// 取termios 结构中的输入模式标志。
#define _O_FLAG(tty,f)	((tty)->termios.c_oflag & f)// 取termios 结构中的输出模式标志。

// 取termios 结构中本地模式标志集中的一个标志位。
#define L_CANON(tty)	_L_FLAG((tty),ICANON)// 取本地模式标志集中规范(熟)模式标志位。
#define L_ISIG(tty)		_L_FLAG((tty),ISIG)// 取信号标志位。
#define L_ECHO(tty)		_L_FLAG((tty),ECHO)// 取回显字符标志位
#define L_ECHOE(tty)	_L_FLAG((tty),ECHOE)// 规范模式时,取回显擦出标志位。
#define L_ECHOK(tty)	_L_FLAG((tty),ECHOK)// 规范模式时,取KILL 擦除当前行标志位。
#define L_ECHOCTL(tty)	_L_FLAG((tty),ECHOCTL)// 取回显控制字符标志位。
#define L_ECHOKE(tty)	_L_FLAG((tty),ECHOKE)// 规范模式时,取KILL 擦除行并回显标志位。
// 取termios 结构中输入模式标志中的一个标志位。
#define I_UCLC(tty)		_I_FLAG((tty),IUCLC)// 取输入模式标志集中大写到小写转换标志位。
#define I_NLCR(tty)		_I_FLAG((tty),INLCR)// 取换行符NL 转回车符CR 标志位。
#define I_CRNL(tty)		_I_FLAG((tty),ICRNL)// 取回车符CR 转换行符NL 标志位。
#define I_NOCR(tty)		_I_FLAG((tty),IGNCR)// 取忽略回车符CR 标志位。
// 取termios 结构中输出模式标志中的一个标志位。
#define O_POST(tty)		_O_FLAG((tty),OPOST)// 取输出模式标志集中执行输出处理标志。
#define O_NLCR(tty)		_O_FLAG((tty),ONLCR)// 取换行符NL 转回车换行符CR-NL 标志。
#define O_CRNL(tty)		_O_FLAG((tty),OCRNL)// 取回车符CR 转换行符NL 标志。
#define O_NLRET(tty)	_O_FLAG((tty),ONLRET)// 取换行符NL 执行回车功能的标志。
#define O_LCUC(tty)		_O_FLAG((tty),OLCUC)// 取小写转大写字符标志。

/*
	tty 数据结构的tty_table数组。
	其中包含三个初始化项数据,分别对应控制台、串口终端1和串口终端2的初始化数据。
*/
struct tty_struct tty_table[] = {
	{
		{ICRNL,		/* change incoming CR to NL *//* 将输入的CR转换为NL */
		OPOST|ONLCR,	/* change outgoing NL to CRNL *//* 将输出的NL 转CRNL */
		0,											/* 控制模式标志初始化为0。*/
		ISIG | ICANON | ECHO | ECHOCTL | ECHOKE,	/*本地模式标志。*/
		0,		/* console termio */				/*控制台termio。*/
		INIT_C_CC},/*控制字符数组。*/
		0,			/* initial pgrp */				/*所属初始进程组*/
		0,			/* initial stopped */			/*初始停止标志。*/
		con_write,									/*tty 写函数指针。*/
		{0,0,0,0,""},		/* console read-queue *//*tty 控制台读队列。*/
		{0,0,0,0,""},		/* console write-queue *//*tty 控制台写队列。*/
		{0,0,0,0,""}		/* console secondary queue *//*tty 控制台辅助(第二)队列。*/
	},{
		{0, /* no translation */	/*输入模式标志。0,无须转换。*/
		0,  /* no translation */	/* 输出模式标志。0,无须转换。*/
		B2400 | CS8,				/*控制模式标志。波特率2400bps,8 位数据位。*/
		0,							/*本地模式标志0。*/
		0,							/*行规程0。*/
		INIT_C_CC},					/*控制字符数组。*/
		0,							/*所属初始进程组。*/
		0,							/*初始停止标志*/
		rs_write,					/*串口1 tty 写函数指针*/
		{0x3f8,0,0,0,""},		/* rs 1 *//*串行终端1 读缓冲队列。*/
		{0x3f8,0,0,0,""},				/*串行终端1 写缓冲队列。*/
		{0,0,0,0,""}					/*串行终端1 辅助缓冲队列。*/
	},{
		{0, /* no translation */	/*输入模式标志。0,无须转换。*/
		0,  /* no translation */	/*输出模式标志。0,无须转换*/
		B2400 | CS8,				/*控制模式标志。波特率2400bps,8 位数据位。*/
		0,							/*本地模式标志0。*/
		0,							/*行规程0。*/
		INIT_C_CC},					/*控制字符数组。*/
		0,							/*所属初始进程组。*/
		0,							/*初始停止标志*/
		rs_write,					/*串口2 tty 写函数指针*/
		{0x2f8,0,0,0,""},		/* rs 2 *//*串行终端2 读缓冲队列。*/
		{0x2f8,0,0,0,""},				/*串行终端2 写缓冲队列。*/
		{0,0,0,0,""}					/*串行终端2 辅助缓冲队列。*/
	}
};
/*
 * these are the tables used by the machine code handlers.
 * you can implement pseudo-tty's or something by changing
 * them. Currently not done.
 */
 /*
* 下面是汇编程序使用的缓冲队列地址表。通过修改你可以实现
* 伪tty 终端或其它终端类型。目前还没有这样做。
*/
// tty 缓冲队列地址表。rs_io.s 汇编程序使用,用于取得读写缓冲队列地址。
struct tty_queue * table_list[]={
	&tty_table[0].read_q, &tty_table[0].write_q,
	&tty_table[1].read_q, &tty_table[1].write_q,
	&tty_table[2].read_q, &tty_table[2].write_q
	};

可以看到,tty_table[]初始化了三个成员,分别描述控制台、串口终端1和串口终端2。 tty_table[]数组变量的类型是struct tty_struct,定义在<linux/tty.h>。
table_list[]初始化了六个队列指针, table_list[]数组变量的类型是struct tty_queue指针,也定义在<linux/tty.h>

/*
* 'tty.h' defines some structures used by tty_io.c and some defines.
*
* NOTE! Don't touch this without checking that nothing in rs_io.s or
* con_io.s breaks. Some constants are hardwired into the system (mainly
* offsets into 'tty_queue'
*/
/*
* 'tty.h'中定义了tty_io.c 程序使用的某些结构和其它一些定义。
*
* 注意!在修改这里的定义时,一定要检查rs_io.s 或con_io.s 程序中不会出现问题。
* 在系统中有些常量是直接写在程序中的(主要是一些tty_queue 中的偏移值)。
*/
#include <termios.h>		// 终端输入输出函数头文件。主要定义控制异步通信口的终端接口。
#define TTY_BUF_SIZE 1024	// tty 缓冲区大小。

// tty 等待队列数据结构。
struct tty_queue
{
  unsigned long data;		// 等待队列缓冲区中当前数据指针字符数
							// 对于串口终端,则存放串行端口地址。
  unsigned long head;		// 缓冲区中数据头指针。
  unsigned long tail;		// 缓冲区中数据尾指针。
  struct task_struct *proc_list;	// 等待进程列表。task_struct是进程描述符表,包含了一个进程的所有信息
  char buf[TTY_BUF_SIZE];	// 队列的缓冲区。
};

// tty 数据结构。
struct tty_struct
{
  struct termios termios;	// 终端io 属性和控制字符数据结构。
  int pgrp;			// 所属进程组。
  int stopped;			// 停止标志。
  void (*write) (struct tty_struct * tty);	// tty 写函数指针。
  struct tty_queue read_q;	// tty 读队列。
  struct tty_queue write_q;	// tty 写队列。
  struct tty_queue secondary;	// tty 辅助队列(存放规范模式字符序列),
};				// 可称为规范(熟)模式队列。

从定义看,table_list[]就是指定了六个struct tty_queue的地址,应该会在上面提到的串口中断函数(汇编)中应用到。。

rs_init()函数中init函数中,所谓对tty_table[x].read_q.data的初始化,就是对一个struct tty_queue(tty 等待队列数据结构)中的data(对于串口终端,存放串行端口地址。)的初始化,并且是读队列的初始化。

outb

rs_init中最后用到了个写端口函数(实际上是汇编实现,详情见trap_init目录下outb_p / outb的分析),允许了主8259A 芯片的IRQ3,IRQ4 中断信号请求。在早期IBM的PC-AT设备中,主8249A的IRQ3和IRQ4分别对应串口2和串口1。

con_init

初始化控制台终端

整个<kernel/chr_drv/console.c>文件用于实现con_initcon_write

/*
* console.c
*
* This module implements the console io functions
* 'void con_init(void)'
* 'void con_write(struct tty_queue * queue)'
* Hopefully this will be a rather complete VT102 implementation.
*
* Beeping thanks to John T Kohl.
*/
/*
* 该模块实现控制台输入输出功能
* 'void con_init(void)'
* 'void con_write(struct tty_queue * queue)'
* 希望这是一个非常完整的VT102 实现。
*
* 感谢John T Kohl 实现了蜂鸣指示。
*/
/*
* void con_init(void);
*
* This routine initalizes console interrupts, and does nothing
* else. If you want the screen to clear, call tty_write with
* the appropriate escape-sequece.
*
* Reads the information preserved by setup.s to determine the current display
* type and sets everything accordingly.
*/
/*
* void con_init(void);
* 这个子程序初始化控制台中断,其它什么都不做。如果你想让屏幕干净的话,就使用
* 适当的转义字符序列调用tty_write()函数。
*
* 读取setup.s 程序保存的信息,用以确定当前显示器类型,并且设置所有相关参数。
*/

函数有点长,我不贴出来了(~﹃~)~zZ

它做的事就是

所谓控制台,就是一般linux系统开机后,在它所连接的屏幕上显示的命令行风格的界面。

总结一下,tty_init所谓字符初始化函数的名称其实并不全面,字符设备初始化为进程与 串行口显示器 以及 键盘 进行 I/O 通信准备工作环境,主要是对这 3 者进行初始化,以及将与这 3 者相关的中断服务程序与 IDT 相挂接。

time_init

第六个被调用的函数,顾名思义,就是设置时间的。

该函数就定义在mian函数的上方,哈哈

/*
 * Yeah, yeah, it's ugly, but I cannot find how to do this correctly
 * and this seems to work. I anybody has more info on the real-time
 * clock I'd be interested. Most of this was trial and error, and some
 * bios-listing reading. Urghh.
 */
/*
 * 是啊,是啊,下面这段程序很差劲,但我不知道如何正确地实现,而且好象它还能运行。如果有
 * 关于实时时钟更多的资料,那我很感兴趣。这些都是试探出来的,以及看了一些bios 程序,呵!
 */

// 这段宏读取CMOS 实时时钟信息。
#define CMOS_READ(addr) ({ \
outb_p(0x80|addr,0x70); \    /*0x70 是写端口号,0x80|addr 是要读取的CMOS 内存地址。*/
inb_p(0x71); \               /* 0x71 是读端口号。*/
})

#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)// 将BCD 码转换成数字。


// 该子程序取CMOS 时钟,并设置开机时间,startup_time(秒)。
static void time_init(void)
{
	struct tm time;

	do {
		time.tm_sec = CMOS_READ(0);//当前秒
		time.tm_min = CMOS_READ(2);//当前分钟
		time.tm_hour = CMOS_READ(4);//当前小时数
		time.tm_mday = CMOS_READ(7);//当前日期
		time.tm_mon = CMOS_READ(8);//当前月份
		time.tm_year = CMOS_READ(9);//当前的年,只有后2位数,例如97表示1997年
	} while (time.tm_sec != CMOS_READ(0));
	BCD_TO_BIN(time.tm_sec);
	BCD_TO_BIN(time.tm_min);
	BCD_TO_BIN(time.tm_hour);
	BCD_TO_BIN(time.tm_mday);
	BCD_TO_BIN(time.tm_mon);
	BCD_TO_BIN(time.tm_year);
	time.tm_mon--;
	startup_time = kernel_mktime(&time);
}

首先,定义了一个结构体变量time,类型为struct tm,该类型定义在<time.h>。

struct tm {
	int tm_sec;// 秒数 [0,59]。
	int tm_min;// 分钟数 [ 0,59]。
	int tm_hour;// 小时数 [0,59]。
	int tm_mday;// 1 个月的天数 [0,31]。
	int tm_mon;// 1 年中月份 [0,11]。
	int tm_year;// 从1900 年开始的年数。
	int tm_wday;// 1 星期中的某天 [0,6](星期天 =0)。
	int tm_yday;// 1 年中的某天 [0,365]。
	int tm_isdst;// 夏令时标志
};

CMOS

接着就用到CMOS_READ这个宏,他就是调用了outb_p和inb_p来读取CMOS的信息的。当然还有些东西,比如为什么要向CMOS写0x80|addr?0x80就是1000 0000(b),将第8位置1有什么作用?我还没搞明白(>﹏<)。(看网上说,这似乎是Linus的冗余操作,因为他当时缺乏CMOS的资料,这在他自己的注释里也写了)

那么什么是CMOS呢?

CMOS 是主板上的一个小存储芯片,PC机的CMOS内存是由电池供电的64或128字节内存块,是系统时钟芯片RTC的一部分,该64字节的CMOS原先在IBM PC-XT机器上用于保存时钟和日期信息,存放的格式是BCD码。CMOS的器件信息如下:

由此可知,通过读CMOS不同的端口分别获取秒、分钟、小时等时间信息,这些信息都被存到time这个时间结构体中。

BCD_TO_BIN

然后就是调用BCD_TO_BIN宏将从CMOS读到的BCD码转换成数字(十进制)。

#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)// 将BCD 码转换成数字。

val和1111做与运算,取val的低四位,然后就将val的高4位取来乘10再和val的低四位相加就是十进制数了。举个简单的栗子:

比如21这个十进制数(用d表示),在CMOS里是以BCD码的格式,BCD码显示数字的范围只能是00000101,即09,因此21(d)就只能使用四位二进制分别表示2(d)和1(d),表示为0010 0001,这里肯定不能直接取过来用,因为我们系统就是普通二进制码表示数值,直接取过来的结果是33(十进制)。因此,采用,取其低四位作个位,其高4位作十分位的方法转换。

回到time_init函数,然后就是将time.tm_mon减一,emm为什么? 减一后月份范围是0~11,方便后面的数组操作。

然后就是调用了kernel_mktime,将返回值传给了startup_time。

kernel_mktime

先来看看kernel_mktime吧,它定义在<kernel/mktime.c>

#include <time.h>

/*
 * This isn't the library routine, it is only used in the kernel.
 * as such, we don't care about years<1970 etc, but assume everything
 * is ok. Similarly, TZ etc is happily ignored. We just do everything
 * as easily as possible. Let's find something public for the library
 * routines (although I think minix times is public).
 */
/*
 * PS. I hate whoever though up the year 1970 - couldn't they have gotten
 * a leap-year instead? I also hate Gregorius, pope or no. I'm grumpy.
 */
 /*
 * 这不是库函数,它仅供内核使用。因此我们不关心小于 1970 年的年份等,但假定一切均很正常。
 * 同样,时间区域 TZ 问题也先忽略。我们只是尽可能简单地处理问题。最好能找到一些公开的库函数
 * (尽管我认为 minix 的时间函数是公开的)。
 * 另外,我讨厌那个设置 1970 年开始的人 - 难道他们就不能选择从一个闰年开始?我也讨厌格雷戈里乌斯,不管是不是教皇。我很暴躁。
 */
#define MINUTE 60// 1 分钟的秒数。
#define HOUR (60*MINUTE)// 1 小时的秒数。
#define DAY (24*HOUR)// 1 天的秒数。
#define YEAR (365*DAY)// 1 年的秒数。

/* interestingly, we assume leap-years */
/* 有趣的是我们考虑进了闰年 */
// 下面以年为界限,定义了每个月开始时的秒数时间数组。
static int month[12] = {
	0,
	DAY*(31),
	DAY*(31+29),
	DAY*(31+29+31),
	DAY*(31+29+31+30),
	DAY*(31+29+31+30+31),
	DAY*(31+29+31+30+31+30),
	DAY*(31+29+31+30+31+30+31),
	DAY*(31+29+31+30+31+30+31+31),
	DAY*(31+29+31+30+31+30+31+31+30),
	DAY*(31+29+31+30+31+30+31+31+30+31),
	DAY*(31+29+31+30+31+30+31+31+30+31+30)
};
// 该函数计算从1970 年1 月1 日0 时起到开机当日经过的秒数,作为开机时间。
long kernel_mktime(struct tm * tm)
{
	long res;
	int year;

	year = tm->tm_year - 70;// 从70 年到现在经过的年数(后两位,19xx),
	// 因此会有2000 年问题。因此本代码仅适用于公元1900~公元2000
/* magic offsets (y+1) needed to get leapyears right.*/
 /* 为了获得正确的闰年数,这里需要这样一个魔法偏值(y+1) */
	res = YEAR*year + DAY*((year+1)/4);// 这些年经过的秒数时间 + 每个闰年时多1 天
	res += month[tm->tm_mon];// 加上当年到当月时的秒数
/* and (y+2) here. If it wasn't a leap-year, we have to adjust */
 /* 以及(y+2)。如果(y+2)不是闰年,那么我们就必须进行调整(减去一天的秒数时间)。 */
	if (tm->tm_mon>1 && ((year+2)%4))
		res -= DAY;
	res += DAY*(tm->tm_mday-1);// 再加上本月过去的天数的秒数时间。
	res += HOUR*tm->tm_hour;// 再加上当天过去的小时数的秒数时间。
	res += MINUTE*tm->tm_min;// 再加上1 小时内过去的分钟数的秒数时间。
	res += tm->tm_sec; //再加上1 分钟内已过的秒数。
	return res;	// 即等于从1970 年以来经过的秒数时间。
}

相信大家应该有和我一样的问题

res = YEAR*year + DAY*((year+1)/4);这一段就比较迷惑。

这里应该是计算1970年到当前年份所经过的秒数,但中间可能有闰年,因此会多出一天,(year+1)/4就是计算有几个闰年,那为什么这么计算呢?

站在Linus的角度,他没有考虑21世纪的事情,从前面的代码可以看出,因此只需考虑1970.1.1到当前年份的1月1日所经过的闰年数,这其实是完全可以枚举出来的

年份 year值 经过闰年数(经过2月29日的个数)
1970,1971,1972 0,1,2 0
1973,1974,1975,1976 3,4,5,6 1
1977,1978,1979,1980 7,8,9,10 2
...... ...... .......

找规律的话就是:闰年数=(year值+1)/4。这也是Linus所抱怨的,为什么不使用一个闰年作为初始值而使用1970?使用闰年的话计算闰年数就会简单些。

res += month[tm->tm_mon],看到这一段就可以理解为什么前面time->tm_mon--的操作了,因为这样便于做数组索引值,month[]这里定义了各个月份距离一月经过的秒数。

tm->tm_mon>1 && ((year+2)%4)也是需要探究的。

结合上表中所找的闰年year值的规律:(year+2)%4==0就代表是闰年,而(year+2)%4!=0才会有可能进if语句,即平年才会有可能进if语句。tm->tm_mon>1就代表当前月份至少是3月,在看看month[]这个数组,注意,它存储的是闰年的数据,因此平年的时候就要减去2月29日这一多出来的一天。(ps:其实我觉得,改一下month数组,判断是闰年加一天的方法可能更有效率?)

res += DAY*(tm->tm_mday-1),这一句,因为是从1月1日开始算的因此经过的天数就是(当前日期-1)。

startup_time

startup_time有声明extern long startup_time;

作为全局变量定义在<kernel/sched.c>

long startup_time=0;

表示内核启动时间(开机时间)(秒)。

sched_init

进程调度初始化

第七个被调用的函数,声明在<linux/sched.h>,定义在<kernel/sched.c>

//defined in <linux/sched.h	>
#define NR_TASKS 64// 系统中同时最多任务(进程)数。
//...

//<kernel/sched.c>
union task_union
{				// 定义任务联合(任务结构成员和stack 字符数组程序成员)。
  struct task_struct task;	// 因为一个任务数据结构与其堆栈放在同一内存页中,所以
  char stack[PAGE_SIZE];	// 从堆栈段寄存器ss 可以获得其数据段选择符。
};

static union task_union init_task = { INIT_TASK, };	// 定义初始任务的数据(sched.h 中)。
struct task_struct *current = &(init_task.task);	// 当前任务指针(初始化为初始任务)。
struct task_struct * task[NR_TASKS] = {&(init_task.task), };// 定义任务指针数组。
// 调度程序的初始化子程序。
void sched_init(void)
{
	int i;
	struct desc_struct * p;// 描述符表结构指针。

	if (sizeof(struct sigaction) != 16)// sigaction 是存放有关信号状态的结构。
		panic("Struct sigaction MUST be 16 bytes");
 // 设置初始任务(任务0)的任务状态段描述符和局部数据表描述符(include/asm/system.h,65)。
	set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
	set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
  // 清任务数组和描述符表项(注意i=1 开始,所以初始任务的描述符还在)。
	p = gdt+2+FIRST_TSS_ENTRY;
	for(i=1;i<NR_TASKS;i++) {
		task[i] = NULL;
		p->a=p->b=0;
		p++;
		p->a=p->b=0;
		p++;
	}
/* Clear NT, so that we won't have troubles with that later on */
  /* 清除标志寄存器中的位NT,这样以后就不会有麻烦 */
    // NT 标志用于控制程序的递归调用(Nested Task)。当NT 置位时,那么当前中断任务执行 iret 指令时就会引起任务切换。NT 指出TSS 中的back_link 字段是否有效。
	__asm__("pushfl ; andl $0xffffbfff,(%	esp) ; popfl"); // 复位NT 标志。
	ltr(0); // 将任务0 的TSS 加载到任务寄存器tr。
	lldt(0); // 将局部描述符表加载到局部描述符表寄存器。
 /*注意!!是将GDT 中相应LDT 描述符的选择符加载到ldtr。只明确加载这一次,以后新任务LDT 的加载,是CPU 根据TSS 中的LDT 项自动加载。下面代码用于初始化8253 定时器.*/
	outb_p(0x36,0x43);		/* binary, mode 3, LSB/MSB, ch 0 */
	outb_p(LATCH & 0xff , 0x40);	/* LSB */ // 定时值低字节
	outb(LATCH >> 8 , 0x40);	/* MSB */ // 定时值高字节。
  // 设置时钟中断处理程序句柄(设置时钟中断门)。
	set_intr_gate(0x20,&timer_interrupt);
  // 修改中断控制器屏蔽码,允许时钟中断。
	outb(inb_p(0x21)&~0x01,0x21);
  // 设置系统调用中断门。 
	set_system_gate(0x80,&system_call);
}

这段程序还是很复杂的

struct desc_struct

这里定义了一个结构体指针,这个结构体类型定义在<linux/head.h>

typedef struct desc_struct {
// 定义了段描述符的数据结构。该结构仅说明每个描述符是由8个字节构成,每个描述符表共有256 项。
	unsigned long a,b;
} desc_table[256];

struct sigaction

然后是个if语句,如果struct sigaction的字节大小不等于16,就调用panic函数。struct sigaction定义在<signal.h>

typedef unsigned int sigset_t;		/* 32 bits */

//定义struct sigaction 中的sa_flags
#define SA_NOCLDSTOP	1
#define SA_NOMASK	0x40000000
#define SA_ONESHOT	0x80000000

#define SIG_DFL ((void (*)(int))0)	/* default signal handling */
// 默认的信号处理程序(信号句柄)。
#define SIG_IGN ((void (*)(int))1)	/* ignore signal */
// 忽略信号的处理程序。
// 下面是sigaction 的数据结构。
// sa_handler 是对应某信号指定要采取的行动。可以是上面的SIG_DFL,或者是SIG_IGN 来忽略该信号,也可以是指向处理该信号函数的一个指针。
// sa_mask 给出了对信号的屏蔽码,在信号程序执行时将阻塞对这些信号的处理。
// sa_flags 指定改变信号处理过程的信号集。它是由37-39 行的位标志定义的。
// sa_restorer 恢复过程指针,是用于保存原返回的过程指针。
// 另外,引起触发信号处理的信号也将被阻塞,除非使用了SA_NOMASK 标志。
struct sigaction {
	void (*sa_handler)(int);
	sigset_t sa_mask;
	int sa_flags;
	void (*sa_restorer)(void);
};

这里的判断语句应该就是检查struct sigaction有没有正确的内存大小,panic函数定义在<kenel/panic.c>,它会调用printk函数,完成信息在控制台终端的打印,用以指出主要的出错问题,然后死机:D

/*
 * This function is used through-out the kernel (includeinh mm and fs)
 * to indicate a major problem.
 */
 // 该函数用来显示内核中出现的重大错误信息,并运行文件系统同步函数,然后进入死循环 -- 死机。
// 如果当前进程是任务0 的话,还说明是交换任务出错,并且还没有运行文件系统同步函数。

set_tss_desc

一般问题不大都会运行到这里,set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));,这个函数定义在<asm/system.h>,是不是有点熟悉,之前让我很头疼的_set_gate就定义在这里。

//// 在全局表中设置任务状态段/局部表描述符。
// 参数:n - 在全局表中描述符项n 所对应的地址;addr - 状态段/局部表所在内存的基地址。
// type - 描述符中的标志类型字节。
// %0 - eax(地址addr);%1 - (描述符项n 的地址);%2 - (描述符项n 的地址偏移2 处);
// %3 - (描述符项n 的地址偏移4 处);%4 - (描述符项n 的地址偏移5 处);
// %5 - (描述符项n 的地址偏移6 处);%6 - (描述符项n 的地址偏移7 处);
#define _set_tssldt_desc(n,addr,type) \
__asm__ ("movw $104,%1\n\t" \// 将TSS 长度放入描述符长度域(第0-1 字节)。
	"movw %%ax,%2\n\t" \// 将基地址的低字放入描述符第2-3 字节。
	"rorl $16,%%eax\n\t" \// 将基地址高字移入ax 中。
	"movb %%al,%3\n\t" \// 将基地址高字中低字节移入描述符第4 字节。
	"movb $" type ",%4\n\t" \// 将标志类型字节移入描述符的第5 字节。
	"movb $0x00,%5\n\t" \// 描述符的第6 字节置0。
	"movb %%ah,%6\n\t" \// 将基地址高字中高字节移入描述符第7 字节。
	"rorl $16,%%eax" \// eax 清零。
	::"a" (addr), "m" (*(n)), "m" (*(n+2)), "m" (*(n+4)), \
	 "m" (*(n+5)), "m" (*(n+6)), "m" (*(n+7)) \
	)
//// 在全局表中设置任务状态段描述符。
// n - 是该描述符的指针;addr - 是描述符中的基地址值。任务状态段描述符的类型是0x89。
#define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x89")

呼呼,果然,这里本质上也是通过宏调用一坨内嵌汇编代码

先把涉及的AT&T汇编知识贴一下

研究过_set_gate,这一段汇编应该也不会太难,本质上也是向内存的某个地方写入某些值,组成一个叫做描述符的东西,这个有两个,分别是任务状态段描述符和局部表描述符。

这里是往哪里写东西呢,往GDT(全局描述符表)里写东西,看名字就知道,里面是存描述符的地方,这个和IDT(中断描述符表)类似,IDT里就是存放中断描述符的地方,_set_gate就是用来设置一种中断描述符(陷阱门描述符),对此有点陌生的童鞋可以往上翻到trap_init的解释里查看详情。

那么我们就应该知道要写的描述符的结构定义,不然咋写代码呢,这里贴出TSS(Task-State Segment)描述符的结构:

对着描述符结构再看代码你一定会有一种豁然开朗的感觉

再看sched_init里被调用的set_tss_desc,它传入了两个参数,分别是,gdt+FIRST_TSS_ENTRY&(init_task.task.tss)

前者,gdt是早在<boot/setup.s>里就定义好的全局描述符表,FIRST_TSS_ENTRY是个宏定义,在<linux/sched.h>

/*
* Entry into gdt where to find first TSS. 0-nul, 1-cs, 2-ds, 3-syscall
* 4-TSS0, 5-LDT0, 6-TSS1 etc ...
*/
/*
* 寻找第1 个TSS 在全局表中的入口。0-没有用nul,1-代码段cs,2-数据段ds,3-系统段syscall
* 4-任务状态段TSS0,5-局部表LTD0,6-任务状态段TSS1,等。
*/
// 全局表中第1 个任务状态段(TSS)描述符的选择符索引号。
#define FIRST_TSS_ENTRY 4
// 全局表中第1 个局部描述符表(LDT)描述符的选择符索引号。
#define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1)

因此,gdt+FIRST_TSS_ENTRY就代表gdt的第4号处,这里用于存放TSS描述符(8字节),注意,gdt应该是每8个字节为一段,作为地址索引时,gdt就是指向其第0字节,gdt+1则指向第8字节处。这里的TSS描述符,按索引的话是位于全局描述符表的第4*8字节处,我知道这里可能有点绕,能力有限,欢迎讨论。

后者,也就是第2个参数,则传入了一个函数的地址,作为任务状态段在内存中的基地址。其实这个应该不是一个函数,只是一个数据结构,这个变量里应该是设置了TSS的初值。注意,TSS可不是64bit的描述符,而是含有一大长串数据的结构体,这个东西好长,我望而生畏(~﹃~)~zZ,感兴趣的取<linux/sched.h>里查看struct tss_struct 这个结构体。

set_ldt_desc

这个函数的实现方式和上面那个完全相同,也定义在<asm/system.h>

// 在全局表中设置局部表描述符。
// n - 是该描述符的指针;addr - 是描述符中的基地址值。局部表描述符的类型是0x82。
#define set_ldt_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x82")

同样的调用,_set_tssldt_desc宏,只不过type是0x82。

set_ldt_desc的作用就是设置LDT(局部描述符表)的描述符,LDT描述符存在GDT中,因此想要访问LDT,就必须访问GDT。等级上来讲,LDT描述符可以看成TSS描述符的同级,毕竟,LDT描述符就在TSS描述符后面。

从代码上来看,LDT描述符和TSS描述符有着同样的结构,因为它们的设置用到了同一段汇编代码。

set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));,了解了TSS描述符的设置,这一句也就不在话下了,本质上是一样的。

for

这个for循环就是把task指针数组清零(没任务),然后就是将GDT里LDT描述符后面的清零。

__ asm __

这段汇编没看懂,什么叫 "NT 置位时,那么当前中断任务执行 iret 指令时就会引起任务切换。"??

ltr(0)

定义在<linux/sched.h>

// 全局表中第1 个任务状态段(TSS)描述符的选择符索引号。
#define FIRST_TSS_ENTRY 4
// 宏定义,计算在全局表中第n 个任务的TSS 描述符的索引号(选择符)。
#define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3))
// 宏定义,加载第n 个任务的任务寄存器tr。
#define ltr(n) __asm__("ltr %%ax"::"a" (_TSS(n)))

这里ltr(0),实际应该就是将0号进程init加入TR寄存器方便cpu访问。

lldt

定义在<linux/sched.h>

// 全局表中第1 个局部描述符表(LDT)描述符的选择符索引号。
#define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1)
// 宏定义,计算在全局表中第n 个任务的LDT 描述符的索引号。
#define _LDT(n) ((((unsigned long) n)<<4)+(FIRST_LDT_ENTRY<<3))
// 宏定义,加载第n 个任务的局部描述符表寄存器ldtr。
#define lldt(n) __asm__("lldt %%ax"::"a" (_LDT(n)))

这里lldr(0),实际应该就是将0号进程init加入LDTR寄存器方便cpu访问。

好了,我这里贴出一张图,可能会帮助大家理解

task_struct

说实话,目前我对于ltr和lldt的理解还没有很深入,只是了解到TSS和LDT这两个长长的数据与进程息息相关。

描述进程的最最重要的数据结构应该就task_struct结构体了,它定义在<linux/sched.h>,tss和ldt均是该结构体内的成员。这个结构体也很长且内涵颇深,浅看一下,日后再品,因为没啥理解就不贴代码了。task[]就是结构体数组指针变量,用于描述所有进程的,目前系统最大支持64个进程,初始化通过了一个宏定义。

//defined in <linux/sched.h	>
#define NR_TASKS 64// 系统中同时最多任务(进程)数。
//...

//<kernel/sched.c>
union task_union
{				// 定义任务联合(任务结构成员和stack 字符数组程序成员)。
  struct task_struct task;	// 因为一个任务数据结构与其堆栈放在同一内存页中,所以
  char stack[PAGE_SIZE];	// 从堆栈段寄存器ss 可以获得其数据段选择符。
};

static union task_union init_task = { INIT_TASK, };	// 定义初始任务的数据(sched.h 中)。
struct task_struct *current = &(init_task.task);	// 当前任务指针(初始化为初始任务)。
struct task_struct * task[NR_TASKS] = {&(init_task.task), };// 定义任务指针数组。

8253 timer

接下来,sched_init是对8253定时器的初始化。8253定时器是一个可编程定时器的芯片。

#define LATCH (1193180/HZ)
//···
	outb_p(0x36,0x43);		/* binary, mode 3, LSB/MSB, ch 0 */
	outb_p(LATCH & 0xff , 0x40);	/* LSB */ // 定时值低字节
	outb(LATCH >> 8 , 0x40);	/* MSB */ // 定时值高字节。
  // 设置时钟中断处理程序句柄(设置时钟中断门)。
	set_intr_gate(0x20,&timer_interrupt);
  // 修改中断控制器屏蔽码,允许时钟中断。
	outb(inb_p(0x21)&~0x01,0x21);

时钟中断是进程 0 及其他由他创建的进程轮询的基础。对时钟中断进行设置的过程分为 3 个步骤

system_call

sched_init的最后一句。系统调用,这个中断函数很重要

// 设置系统调用中断门。 
	set_system_gate(0x80,&system_call);

只要看过trap_init函数,这个函数我们就不陌生。关键在于这个中断函数。

和之前的中断函数一样,这个也是用汇编写的(貌似所有的中断函数都是用汇编写的),啊啊,汇编好难⊙﹏⊙∥

系统调用是操作系统内核提供一组接口,目的是和用户空间对接。换句话说,只要用户想要访问硬件设备和其他操作系统资源,都要通过该接口。( ̄︶ ̄)↗ 

system_call 是整个操作系统中系统调用软中断的总入口。所有用户程序使用系统调用,产生 int 0x80 软中断后,操作系统通过这个总入口找到具体的系统调用函数。

总结一下,sched_init首先设置初始任务(任务0)的任务状态段描述符和局部数据表描述符,将剩余任务的任务指针和描述符清零;然后复位NT 标志(没看懂);然后设置tss和ldt与寄存器的联系,方便cpu访问;然后设置了时钟中断;然后设置了系统调用中断。

buffer_init

第八个被调用的函数,定义于<fs/buffer.c>。用于缓冲区初始化,传入的参数是buffer_memory_end,前面计算出的,如果是16MB内存,那么其值就是4MB。

//`````<include/linux/fs.h>`````
#define NR_BUFFERS nr_buffers//nr_buffers是汇编定义的吧,我搜不到
#define BLOCK_SIZE 1024
#define NR_HASH 307
//`````````````````````````````

extern int end;			// 由连接程序ld 生成的表明程序末端的变量
//这个外部变量 end 并不是操作系统代码写就的,而是由链接器 ld 在链接整个程序时设置的一个外部变量,帮我们计算好了整个内核代码的末尾地址。(链接器 ld?)

struct buffer_head *start_buffer = (struct buffer_head *) &end;
struct buffer_head *hash_table[NR_HASH];	// NR_HASH = 307 项。
static struct buffer_head *free_list;

// 缓冲区初始化函数。
// 参数buffer_end 是指定的缓冲区内存的末端。对于系统有16MB 内存,则缓冲区末端设置为4MB。
// 对于系统有8MB 内存,缓冲区末端设置为2MB。
void buffer_init(long buffer_end)
{
	struct buffer_head * h = start_buffer;
	void * b;//缓冲区高端地址
	int i;
// 如果缓冲区高端等于1Mb,则由于从640KB-1MB 被显示内存和BIOS 占用,因此实际可用缓冲区内存高端应该是640KB。否则内存高端一定大于1MB。
	if (buffer_end == 1<<20)
		b = (void *) (640*1024);
	else
		b = (void *) buffer_end;
// 这段代码用于初始化缓冲区,建立空闲缓冲区环链表,并获取系统中缓冲块的数目。
// 操作的过程是从缓冲区高端开始划分1K 大小的缓冲块,与此同时在缓冲区低端建立描述该缓冲块
// 的结构buffer_head,并将这些buffer_head 组成双向链表。
// h 是指向缓冲头结构的指针,而h+1 是指向内存地址连续的下一个缓冲头地址,也可以说是指向h
// 缓冲头的末端外。为了保证有足够长度的内存来存储一个缓冲头结构,需要b 所指向的内存块
// 地址 >= h 缓冲头的末端,也即要>=h+1。
	while ( (b -= BLOCK_SIZE) >= ((void *) (h+1)) ) {
		h->b_dev = 0;// 使用该缓冲区的设备号。
		h->b_dirt = 0;// 脏标志,也即缓冲区修改标志。
		h->b_count = 0;// 该缓冲区引用计数。
		h->b_lock = 0;// 缓冲区锁定标志。
		h->b_uptodate = 0;// 缓冲区更新标志(或称数据有效标志)。
		h->b_wait = NULL;// 指向等待该缓冲区解锁的进程。
		h->b_next = NULL;// 指向具有相同hash 值的下一个缓冲头。
		h->b_prev = NULL;// 指向具有相同hash 值的前一个缓冲头。
		h->b_data = (char *) b;// 指向对应缓冲区数据块(1024 字节)。
		h->b_prev_free = h-1;// 指向链表中前一项。
		h->b_next_free = h+1;// 指向链表中下一项。
		h++;// h 指向下一新缓冲头位置。
		NR_BUFFERS++;// 缓冲区块数累加。
		if (b == (void *) 0x100000)// 如果地址b 递减到等于1MB,则跳过384KB,即跳过被显示内存和BIOS 占用的区域
			b = (void *) 0xA0000;// 让b 指向地址0xA0000(640KB)处。
	}
	h--;// 让h 指向最后一个有效缓冲头。
	free_list = start_buffer;	// 让空闲链表头指向头一个缓冲区头。
	free_list->b_prev_free = h;// 链表头的b_prev_free 指向前一项(即最后一项)。
	h->b_next_free = free_list;// h 的下一项指针指向第一项,形成一个环链。
	// 初始化hash 表(哈希表、散列表),置表中所有的指针为NULL。
	for (i=0;i<NR_HASH;i++)
		hash_table[i]=NULL;
}	

struct buffer_head

开头设置了一个结构体指针变量,结构体定义在<linux/fs.h>

// 缓冲区头数据结构。(极为重要!!!)
// 在程序中常用bh 来表示buffer_head 类型的缩写。
struct buffer_head {
	char * b_data;			/* pointer to data block (1024 bytes) */
	unsigned long b_blocknr;	/* block number */ //块号。
	unsigned short b_dev;		/* device (0 = free) */// 数据源的设备号
	unsigned char b_uptodate;// 更新标志:表示数据是否已更新。	
	unsigned char b_dirt;		/* 0-clean,1-dirty *///修改标志:0 未修改,1 已修改.
	unsigned char b_count;		/* users using this block */// 使用的用户数。
	unsigned char b_lock;		/* 0 - ok, 1 -locked */ // 缓冲区是否被锁定。
	struct task_struct * b_wait;// 指向等待该缓冲区解锁的任务。
	struct buffer_head * b_prev;// hash 队列上前一块(这四个指针用于缓冲区的管理)。
	struct buffer_head * b_next;// hash 队列上下一块。
	struct buffer_head * b_prev_free;// 空闲表上前一块。
	struct buffer_head * b_next_free;// 空闲表上下一块。
};

这个结构体指针变量 h 被赋值为start_buffer,start_buffer的变量类型和 h 一样,start_buffer指向了end这个外部变量,其值是一个内存值。end并没有在整个操作系统代码中定义,而是通过ld链接器生成的,表示,操作系统代码在内存中的结束位置,众所周知,操作系统代码是存在内存的开始那一部分的,以16MB大小内存为例,用个表格表示内存就是

内存位置 内容
4MB到16MB 主内存
end到4MB 缓冲区
0到end 内核(操作系统)程序

if

设置完变量,就是一段if判断,主要是因为640KB——1MB 被显示内存和BIOS 占用,无法用于缓冲区。

while

该循环用于初始化缓冲区

//<include/linux/fs.h>
#define BLOCK_SIZE 1024//1kb

b指向缓冲区尾端,h指向缓冲区首端,b以1kb为单位向缓冲区内部移动,1kb就是一个缓冲块,与此同时,h指向的内容要建立大小为struct buffer_head的缓冲区头数据结构来描述尾部刚刚划分出的1kb缓冲块。直到缓冲区被填满while结束。
注意,缓冲区前部分的每个struct buffer_head构成了双向链表。

引用《Linux 内核设计的艺术》对缓冲区的介绍

缓冲区是内存与外设(如硬盘)进行数据交互的媒介。内存与硬盘最大的区别在于,硬盘的作用仅仅是对数据信息以很低的成本做大量数据的断电保存,并不参与运算(因为 CPU 无法到硬盘上寻址),而内存除了需要对数据进行保存以外,更重要的是与 CPU、总线配合进行数据运算。缓冲区则介于两者之间,它既对数据信息进行保存,也能够参与一些像查找、组织之类的间接、辅助性运算。有了缓冲区这个媒介后,对外设而言,它仅需要考虑与缓冲区进行数据交互是否符合要求,而不需要考虑内存如何使用这些交互的数据;对内存而言,它也仅需要考虑与缓冲区交互的条件是否成熟,而不需要关心此时外设对缓冲区的交互情况。两者的组织、管理和协调将由操作系统统一操作。

hash_table

while完设置了一下free_list(或者说给了它初值),这是个static struct buffer_head类型的,指向空闲块链表中第一个“最为空闲”的缓冲块,即近期最少使用的缓冲块。所有的缓冲块表头被设置成了双向链表。

接下来是设置哈希表初始化,也是用于缓冲区的管理,具体其实是针对已读入数据的缓冲块的管理。hash表的类型也是struct buffer_head,它是个数组。它可以大大地方便查找某个缓冲块,效率比遍历双向链表要高的多。

总结一下,就是buffer_init初始化了内存缓冲区,并建立了双向链表和哈希表来进行管理。这里其实和blk_dev_init块设备初始化一起看更有理解一些。

hd_init

第十个被调用地函数,硬盘初始化函数。定义在<kernel/blk_drv/hd.c>

#define MAJOR_NR 3		// 硬盘主设备号是3。
// 硬盘系统初始化。
void hd_init (void)
{
  blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;	// do_hd_request()。
  set_intr_gate (0x2E, &hd_interrupt);	// 设置硬盘中断门向量 int 0x2E(46)。
// hd_interrupt 在(kernel/system_call.s,221)。
  outb_p (inb_p (0x21) & 0xfb, 0x21);	// 复位接联的主8259A int2 的屏蔽位,允许从片发出中断请求信号。
  outb (inb_p (0xA1) & 0xbf, 0xA1);	// 复位硬盘的中断请求屏蔽位(在从片上),允许硬盘控制器发送中断请求信号。
}

blk_dev

该数组是struct blk_dev_struct类型,变量定义在<kernel/blk_drv/ll_rw_blk.c>,结构体定义在<kernel/blk_drv/blk.h>,DEVICE_REQUEST是一个宏定义,也在<kernel/blk_drv/blk.h>

//------<kernel/blk_drv/blk.h>------
#define NR_BLK_DEV 7		// 块设备的数量。

// 块设备结构。
struct blk_dev_struct {
	void (*request_fn)(void);// 请求操作的函数指针。	
	struct request * current_request;// 请求信息结构。
};
//·······
#elif (MAJOR_NR == 3)		// 硬盘主设备号是3。
/* harddisk */
#define DEVICE_NAME "harddisk"	// 硬盘名称harddisk。
#define DEVICE_INTR do_hd	// 设备中断处理程序do_hd()。
#define DEVICE_REQUEST do_hd_request	// 设备请求函数do_hd_request()。
#define DEVICE_NR(device) (MINOR(device)/5)	// 设备号(0--1)。每个硬盘可以有4 个分区。
#define DEVICE_ON(device)	// 硬盘一直在工作,无须开启和关闭。
#define DEVICE_OFF(device)

//----------------------------------
//------<kernel/blk_drv/ll_rw_blk.c>------
/* blk_dev_struct is:
* do_request-address
* next-request
*/
/* blk_dev_struct 块设备结构是:(kernel/blk_drv/blk.h,23)
* do_request-address //对应主设备号的请求处理程序指针。
* current-request // 该设备的下一个请求。
*/
// 该数组使用主设备号作为索引(下标)。
struct blk_dev_struct blk_dev[NR_BLK_DEV] = {//NR_BLK_DEV=7
	{ NULL, NULL },		/* no_dev */ // 0 - 无设备。
	{ NULL, NULL },		/* dev mem */ // 1 - 内存。	
	{ NULL, NULL },		/* dev fd */ // 2 - 软驱设备
	{ NULL, NULL },		/* dev hd */ // 3 - 硬盘设备。
	{ NULL, NULL },		/* dev ttyx */ // 4 - ttyx 设备。
	{ NULL, NULL },		/* dev tty */ // 5 - tty 设备。
	{ NULL, NULL }		/* dev lp */ // 6 - lp 打印机设备。
};
//--------------------------------------

blk_dev数组用于存放主设备的信息。

hd_init第一句主要是对3号硬盘主设备指定设备请求函数do_hd_request()

do_hd_request

先简单了解一下吧,就定义在hd_init的上方,

#define MAX_HD 2		// 系统支持的最多硬盘数。
     static int recalibrate = 1;	// 重新校正标志。
     static int reset = 1;	// 复位标志。
/*
* This struct defines the HD's and their types.
*/
/* 下面结构定义了硬盘参数及类型 */
// 各字段分别是磁头数、每磁道扇区数、柱面数、写前预补偿柱面号、磁头着陆区柱面号、控制字节。
     struct hd_i_struct
     {
       int head, sect, cyl, wpcom, lzone, ctl;
     };

#ifdef HD_TYPE			// 如果已经在include/linux/config.h 中定义了HD_TYPE…
	struct hd_i_struct hd_info[] =
	{
		HD_TYPE
	};			// 取定义好的参数作为hd_info[]的数据。

	#define NR_HD ((sizeof (hd_info))/(sizeof (struct hd_i_struct)))	// 计算硬盘数。
#else // 否则,都设为0 值。
	struct hd_i_struct hd_info[] =
	{
	{
	0, 0, 0, 0, 0, 0
	}
	,
	{
	0, 0, 0, 0, 0, 0
	}
	};
	static int NR_HD = 0;
#endif
// 定义硬盘分区结构。给出每个分区的物理起始扇区号、分区扇区总数。
// 其中5 的倍数处的项(例如hd[0]和hd[5]等)代表整个硬盘中的参数。
static struct hd_struct
{
  long start_sect;
  long nr_sects;
}
hd[5 * MAX_HD] =
{
  {	0, 0 },
};
// 执行硬盘读写请求操作。
void do_hd_request(void)
{
	int i,r;
	unsigned int block,dev;
	unsigned int sec,head,cyl;
	unsigned int nsect;

	INIT_REQUEST;// 检测请求项的合法性(参见kernel/blk_drv/blk.h,127)。
// 取设备号中的子设备号(见列表后对硬盘设备号的说明)。子设备号即是硬盘上的分区号。
	dev = MINOR(CURRENT->dev);//#define MINOR(a) ((a)&0xff)取低八位
	block = CURRENT->sector;// 请求的起始扇区。
// 如果子设备号不存在或者起始扇区大于该分区扇区数-2,则结束该请求,并跳转到标号repeat 处
// (定义在INIT_REQUEST 开始处)。因为一次要求读写2 个扇区(512*2 字节),所以请求的扇区号
// 不能大于分区中最后倒数第二个扇区号。
//hd数组的初始化应该在sys_setup中,此时还没有运行,因此,该数组在hd_init时内容是0.
	if (dev >= 5*NR_HD || block+2 > hd[dev].nr_sects) {
		end_request(0);
		goto repeat;//该标号就是INIT_REQUEST宏定义所在处
	}
	block += hd[dev].start_sect;// 将所需读的块对应到整个硬盘上的绝对扇区号。
	dev /= 5;// 此时dev 代表硬盘号(0 或1)。
// 下面嵌入汇编代码用来从硬盘信息结构中根据起始扇区号和每磁道扇区数计算在磁道中的
// 扇区号(sec)、所在柱面号(cyl)和磁头号(head)。
	__asm__("divl %4":"=a" (block),"=d" (sec):"0" (block),"1" (0),
		"r" (hd_info[dev].sect));
	__asm__("divl %4":"=a" (cyl),"=d" (head):"0" (block),"1" (0),
		"r" (hd_info[dev].head));
	sec++;
	nsect = CURRENT->nr_sectors;// 欲读/写的扇区数。
// 如果reset 置1,则执行复位操作。复位硬盘和控制器,并置需要重新校正标志,返回。
	if (reset) {
		reset = 0;
		recalibrate = 1;
		reset_hd(CURRENT_DEV);
		return;
	}
// 如果重新校正标志(recalibrate)置位,则首先复位该标志,然后向硬盘控制器发送重新校正命令。
	if (recalibrate) {
		recalibrate = 0;
		hd_out(dev,hd_info[CURRENT_DEV].sect,0,0,0,
			WIN_RESTORE,&recal_intr);
		return;
	}	
// 如果当前请求是写扇区操作,则发送写命令,循环读取状态寄存器信息并判断请求服务标志DRQ_STAT 是否置位。DRQ_STAT 是硬盘状态寄存器的请求服务位(include/linux/hdreg.h,27)。
	if (CURRENT->cmd == WRITE) {
		hd_out(dev,nsect,sec,head,cyl,WIN_WRITE,&write_intr);
		for(i=0 ; i<3000 && !(r=inb_p(HD_STATUS)&DRQ_STAT) ; i++)
			/* nothing */ ;
// 如果请求服务位置位则退出循环。若等到循环结束也没有置位,则此次写硬盘操作失败,去处理下一个硬盘请求。否则向硬盘控制器数据寄存器端口HD_DATA 写入1 个扇区的数据。
		if (!r) {
			bad_rw_intr();
			goto repeat;
		}
// 如果当前请求是读硬盘扇区,则向硬盘控制器发送读扇区命令。
		port_write(HD_DATA,CURRENT->buffer,256);
	} else if (CURRENT->cmd == READ) {
		hd_out(dev,nsect,sec,head,cyl,WIN_READ,&read_intr);
	} else
		panic("unknown hd-command");
}

这个函数很长哈

INIT_REQUEST

这个函数一上来有点懵的可能就是这个了,它是个宏定义,定义在<kernel/blk_drv/blk.h>

#define CURRENT (blk_dev[MAJOR_NR].current_request)	// CURRENT 为指定主设备号的当前请求结构。


// 定义初始化请求宏。
#define INIT_REQUEST \
repeat: \ //标号(大概)
	if (!CURRENT) \ // 如果当前请求结构指针为null 则返回。
		return; \
	if (MAJOR(CURRENT->dev) != MAJOR_NR) \
		panic(DEVICE_NAME ": request list destroyed"); \
	if (CURRENT->bh) { \
		if (!CURRENT->bh->b_lock) \
			panic(DEVICE_NAME ": block not locked"); \
	}

首先,CURRENT是个宏,就是blk_dev数组里的current_request结构体指针,结构体类型为struct request,这个结构体在之前blk_dev_init目录下有提到过,并且所谓的blk_dev_init,主要目的就是初始化这个结构体类型的一个名为request的数组。

/*
 * Ok, this is an expanded form so that we can use the same
 * request for paging requests when that is implemented. In
 * paging, 'bh' is NULL, and 'waiting' is used to wait for
 * read/write completion.
 */
/*
	OK,下面是 request 结构的一个扩展形式,因而当实现以后,我们就可以在分页请求中使用同样的 request 结构。在分页处理中,'bh'是NULL,而'waiting'则用于等待读/写的完成。
*/
struct request {
	int dev;		/* -1 if no request *//* 使用的设备号。*/
	int cmd;		/* READ or WRITE *//*命令(READ 或WRITE)。*/
	int errors;		/*操作时产生的错误次数。*/
	unsigned long sector;	/*起始扇区。(1 块 = 2 扇区)*/
	unsigned long nr_sectors; /*读/写扇区数。*/
	char * buffer;	/*数据缓冲区*/
	struct task_struct * waiting;	/*任务等待操作执行完成的地方*/
	struct buffer_head * bh;	/*缓冲区头指针(include/linux/fs.h)*/
	struct request * next;		/*指向下一请求项*/
};

回到INIT_REQUEST这个宏,如果CURRENT为空,即主设备号的当前请求结构为空,就返回NULL。

接着看,MAJOR什么意思,这也是个宏,定义在<linux/fs.h>

#define MAJOR(a) (((unsigned)(a))>>8)// 取高字节(主设备号)

然后能理解,如果主设备号的当前请求结构中的设备号的高字节不等于当前的设备号,(在这段程序中,当前设备号MAJOR_NR=3,即硬盘),那么就会调用panic报个警告,然后死机。

然后,如果主设备号的当前请求结构中的缓冲区头指针(struct buffer_head类型,前面buffer_init有介绍)不为空并且缓冲器被锁定,那么就会调用panic报个警告,然后死机。

这么一看,INIT_REQUEST就是检查一下设备请求号对不对,缓冲器可不可用。

剩下的看注释理解一下,该函数有涉及了<kernel/blk_drv/hd.c>中的其他几个函数,可谓是牵一发而动全身啊。

算了,还是回到hd_init吧,只需记得do_hd_request是执行硬盘读写请求操作的。

set_intr_gate

接下来就是老朋友了,这个就是设置了hd_interrupt中断函数(与 IDT 相挂接).

8259A

操作端口读写操作,开启复位接联的主8259A int2 的屏蔽位,允许从片发出中断请求信号。

Hard Disk IT

操作端口读写操作,复位硬盘的中断请求屏蔽位(在从片上),允许硬盘控制器发送中断请求信号。

总结,hd_init首先将块设备数组变量(用于存放主设备的信息),中的3号成员(硬盘),与执行硬盘读写请求操作函数联系起来。然后开启hd_interrupt中断函数,最后,允许从片发出中断请求信号,允许硬盘控制器发送中断请求信号。

floppy_init

第十个被调用的函数,软盘初始化函数,定义在<kernel/blk_drv/floppy.c>。虽然据说软盘现在已经不用了

// 软盘系统初始化。
// 设置软盘块设备的请求处理函数(do_fd_request()),并设置软盘中断门(int 0x26,对应硬件
// 中断请求信号IRQ6),然后取消对该中断信号的屏蔽,允许软盘控制器FDC 发送中断请求信号。
void floppy_init(void)
{
	blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;// = do_fd_request()。
	set_trap_gate(0x26,&floppy_interrupt);//设置软盘中断门 int 0x26(38)。
	outb(inb_p(0x21)&~0x40,0x21);// 复位软盘的中断请求屏蔽位,允许软盘控制器发送中断请求信号。
}

如果对比hd_init,就会发现,这两个初始化函数结构思路完全一致,只是对应绑定的是软盘读写请求项处理函数,以及开启的是floppy_interrupt中断函数和软盘中断请求。


 

总结

好了,这篇文章就到这里,以上仅为一个小辣鸡初学的简单理解,还请各位看官多多指教。

本文对main函数开始的十个初始化函数做了简单的学习,根据自己的理解大致可以这样分类:

上一篇:
Linux0.11源码学习(三)

标签:tty,set,struct,void,描述符,学习,init,源码,Linux0.11
来源: https://www.cnblogs.com/Baiyug/p/16614691.html