《linux内核完全剖析》第七章 初始化程序 笔记

1、简介

初始化程序位于init/目录下,该目录只有一个main.c文件,系统执行完boot/目录下的head.s程序后把执行权交给main.c。main.c程序进行了内核初始化的所有工作。

2、main.c功能描述

main.c利用setup.s取得的系统参数设置系统的根文件设备号和一些内存全局变量。这些内存变量包括主内存的开始地址、系统内存容量和高速缓冲区内存的末端地址。如果定义了虚拟盘,主内存会适当减少。内存映射图如下

之后内核进行所有方面的硬件初始化,包含陷阱门、块设备、字符设备和tty,还包括人工设置第一个任务(task 0),当所有初始化工作完成后程序就设置中断允许标志来开启中断。内核完成初始化后,内核把执行权切换到用户模式(task 0),main.c就工作在task 0中,之后系统第一次调用进程创建函数fork()创建出一个用于运行init()的子进程。整个系统初始化过程如下图

3、main.c源代码

/*
 *  linux/init/main.c
 *
 *  (C) 1991  Linus Torvalds
 */
#define __LIBRARY__ /* 为了包括定义在 unistd.h 中的内嵌汇编代码等信息 */
#include <unistd.h>
#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.
 */

/*
 * 我们需要下面这些内嵌语句 - 从内核空间创建进程将导致没有写时复制(COPY ON WRITE)!!!直到执行一
 * 个execve调用。这对堆栈可能带来问题。处理方法是在fork()调用后不让main()使用任何堆栈。因此就不
 * 能有函数调用 - 这意味着fork也要使用内嵌代码,否则我们在从fork()退出时就要使用堆栈了。
 *
 * 实际上只有pause和fork需要使用内嵌方式,以保证从main()中不会弄乱堆栈,但是我们同时还定义了其
 * 他一些函数。
 */

/* Linux在内核空间创建进程时不使用写时复制技术(Copy on write)。main()在移动到用户模式(到任务0)
 后执行内嵌方式的 fork() 和 pause(),因此可保证不使用任务0的用户栈。在执行move_to_user_mode()之
 后,本程序 main() 就以任务0的身份在运行了。而任务0是所有将创建子进程的父进程。当它创建一个子进
 程时(init进程),由于任务1代码属于内核空间,因此没有使用写时复制功能。此时任务0的用户栈就是任务
 1的用户栈,即它们共同使用一个栈空间。因此希望在 main.c 运行在任务0的环境下时不要有对堆栈的任何
 操作,以免弄乱堆栈。而在再次执行fork() 并执行过 execve() 函数后,被加载程序已不属于内核空间,
 因此可以使用写时复制技术了。*/

// int fork() 系统调用:创建进程
_syscall0(int, fork)
// int pause() 系统调用:暂停进程的执行,直到收到一个信号
_syscall0(int, pause)
// int setup(void * BIOS) 系统调用:仅用于linux初始化(仅在这个程序中被调用)
_syscall1(int, setup, void *, BIOS)
// int sync() 系统调用:更新文件系统
_syscall0(int, sync)

#include <linux/tty.h>
#include <linux/sched.h>
#include <linux/head.h>
#include <asm/system.h>
#include <asm/io.h>

#include <stddef.h>
#include <stdarg.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>

#include <linux/fs.h>

#include <string.h>

#include <linux/log_print.h> 	/* 日志打印功能 */

static char printbuf[1024];		/* 静态字符串数组,用作内核显示信息的缓存。*/

extern char *strcpy();
extern int vsprintf();
extern void init(void);							/* 初始化 */
extern void blk_dev_init(void);					/* 块设备初始化blk_drv/ll_re_blk.c */
extern void chr_dev_init(void);					/* 字符设备初始化chr_drv/tty_io.c */
extern void hd_init(void);						/* 硬盘初始化blk_drv/hd.c */
extern void floppy_init(void);					/* 软驱初始化blk_drv/floppy.c */
extern void mem_init(long start, long end);		/* 内存管理初始化mm/memory.c */
extern long rd_init(long mem_start, int length);/* 虚拟盘初始化blk_drv/ramdisk.c */
extern long kernel_mktime(struct tm * tm);		/* 计算系统开机启动时间(秒) */

/* 内核专用sprintf()函数,产生格式化信息并输出到指定缓冲区str中 */
static int sprintf(char * str, const char *fmt, ...)
{
	va_list args;
	int i;

	va_start(args, fmt);
	i = vsprintf(str, fmt, args);
	va_end(args);
	return i;
}

/*
 * This is set up by the setup-routine at boot-time
 */
/* 
 * 这些数据由内核引导期间的setup.s程序设置。
 */
#define EXT_MEM_K (*(unsigned short *)0x90002)			/* 1MB以后的扩展内存大小(KB) */
#define CON_ROWS ((*(unsigned short *)0x9000e) & 0xff)	/* 选定的控制台屏幕的行数 */
#define CON_COLS (((*(unsigned short *)0x9000e) & 0xff00) >> 8)	/* ...列数 */
#define DRIVE_INFO (*(struct drive_info *)0x90080)		/* 硬盘参数表32字节内容 */
#define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)		/* 根文件系统所在设备号 */
#define ORIG_SWAP_DEV (*(unsigned short *)0x901FA)		/* 交换文件所在设备号 */

/*
 * 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.
 */
/* 这段宏读取CMOS实时时钟数据。outb_p和inb_p是include/asm/io.h中定义的端口输入输出宏 */
#define CMOS_READ(addr) ({		\
	outb_p(0x80 | addr, 0x70);	\
	inb_p(0x71); 				\
})

/* 将BCD码转换成二进制数值 */
#define BCD_TO_BIN(val)	((val)=((val)&15) + ((val)>>4)*10)

/* CMOS的访问时间很慢。为了减小时间误差,在读取了下面循环中所有数值后,若此时CMOS中秒值
 发生了变化,则重新读取。这样能控制误差在1s内 */
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);		/* 当前月份(1~12) */
		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--;						/* ti_mon中的月份范围是 0 ~ 11 */
	startup_time = kernel_mktime(&time);/* 计算开机时间。*/
}

static long memory_end = 0;				/* 机器所具有的物理内存容量 */
static long buffer_memory_end = 0;		/* 高速缓冲区末端地址 */
static long main_memory_start = 0;		/* 主内存开始的位置 */
static char term[32];					/* 终端设置字符串 */

/* 读取并执行/etc/rc文件时所使用的命令行参数和环境参数 */
static char * argv_rc[] = { "/bin/sh", NULL };
static char * envp_rc[] = { "HOME=/", NULL ,NULL };

/* 运行登录shell时所使用的命令行和环境参数 */
/* argv[0]中的字符“-”是传递shell程序sh的一个标示位,通过这个标示位,sh程序会作为shell程序执行 */
static char * argv[] = { "-/bin/sh", NULL };
static char * envp[] = { "HOME=/usr/root", NULL, NULL };

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

/* 内核初始化主程序 (void -> int 去除编译警告,实际为void) */
int main(void)		/* This really is void, no error here. */
					/* 这里真的是 void,没有问题 */
{					/* The startup routine assumes (well, ...) this */
					/* 因为在 head.s 就是这么假设的(把 main 的地址压入堆栈的时候) */
/*
 * Interrupts are still disabled. Do necessary setups, then enable them
 */
/*
 * 此时中断还被禁止的,做完必要的设置后就将其开启。
 */
	ROOT_DEV = ORIG_ROOT_DEV;
	SWAP_DEV = ORIG_SWAP_DEV;
	sprintf(term, "TERM=con%dx%d", CON_COLS, CON_ROWS);
	envp[1] = term;
	envp_rc[1] = term;
	drive_info = DRIVE_INFO;

	/* 根据机器物理内存容量设置高速缓冲区和主内存区的起始地址 */
	memory_end = (1 << 20) + (EXT_MEM_K << 10); /* 1M + 扩展内存大小 */
	memory_end &= 0xfffff000;					/* 忽略不到4K(1页)的内存 */
	if (memory_end > 16 * 1024 * 1024) {		/* 最多管理16M内存 */
		memory_end = 16 * 1024 * 1024;
	}

	if (memory_end > 12 * 1024 * 1024) {
		buffer_memory_end = 4 * 1024 * 1024;
	} else if (memory_end > 6 * 1024 * 1024) {
		buffer_memory_end = 2 * 1024 * 1024;
	} else {
		buffer_memory_end = 1 * 1024 * 1024;
	}
	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);/* 主内存区初始化 */
	trap_init();							/* 陷阱门初始化 */
	blk_dev_init();							/* 块设备初始化 */
	chr_dev_init();							/* 字符设备初始化 */
	tty_init();								/* tty初始化 */
	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 */
		/* 创建任务1(init进程) */
		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.
 */
/*
 * 注意!! 对于任何其他的任务,“pause()”将意味着我们必须等待收到信号才会返回就绪态,但任务0
 * 是唯一例外的情况(参见“schedule()”),因为任务0在任何空闲时间里都会被激活,因此对于任务
 * 0 “pause()”仅意味着我们返回来查看是否有其他任务可以运行,如果没有的话,我们就在这里一直循
 * 环执行。
 */

	/* 调度函数发现系统中没有其他程序可以运行就会切换到任务0 */
	for(;;) {
		__asm__("int $0x80"::"a" (__NR_pause));
	}
}


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;
}

/* init()函数主要完成4件事:
 *		1. 安装根文件系统
 *		2. 显示系统信息
 *		3. 运行系统初始资源配置文件rc中的命令
 *		4. 执行用户登录shell程序
*/
void init(void)
{
	int pid, i;

	setup((void *) &drive_info);

	(void) open("/dev/tty1", O_RDWR, 0);	/* stdin */
	(void) dup(0);							/* stdout */
	(void) dup(0);							/* stderr */

	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);

	/* fork出任务2 */
	if (!(pid = fork())) {
		/* 将stdin重定向到/etc/rc文件,shell程序会在运行完/etc/rc中设置的命令后退出 */
		close(0);
		if (open("/etc/rc", O_RDONLY, 0)) {
			_exit(1);
		}
		execve("/bin/sh", argv_rc, envp_rc);
		_exit(2);
	}

	if (pid > 0) {	/* init进程等待任务2退出 */
		while (pid != wait(&i)) {
			/* nothing */;
		}
	}
	/* 系统将始终在这个循环中 */
	while (1) {
		if ((pid = fork()) < 0) {
			printf("Fork failed in init\r\n");
			continue;
		}
		/* 新的子进程,关闭句柄(0,1,2),新创建一个会话并设置进程组号,然后重新打开/dev/tty0作
		 为stdin,并复制成stdout和stderr。以登录方式再次执行/bin/sh */
		if (!pid) {
			close(0);close(1);close(2);
			setsid();
			(void) open("/dev/tty1", O_RDWR, 0);
			(void) dup(0);
			(void) dup(0);
			_exit(execve("/bin/sh", argv, envp));
		}
		/* 然后父进程再次运行wait()等待 */
		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() */
	/* _exit和exit都能用于正常终止一个函数。但_exit()直接是一个sys_exit的系统调用,而exit()则
	 是普通函数库中的一个函数。它会先进行一些清除操作,例如调用执行各终止处理程序,关闭所有标
	 准IO等,然后调用sys_exit。*/
}

4、main.c代码解读

#define __LIBRARY__ 

为了包括定义在unistd.h中的内嵌汇编代码等信息,对应于unistd.h中 #ifdef __LIBRARY__ 到后一个 #endif 的部分代码。

 

static inline _syscall0(int, fork)

_syscall0是定义在unistd.h中 __LIBRARY__  宏部分的的内嵌宏代码。以嵌入汇编的形式调用linux的系统调用中断0x80。该中断是所有系统调用的入口。这条语句实际上是int fork()创建进程系统调用。syscall0结尾0表示无参数,1表示1个参数,以此类推。下面展开该函数的实现。

先来看看_syscall0的定义

#define _syscall0(type, name) 						\
type name(void)		 								\
{ 													\
	long __res; 									\
	__asm__ volatile ("int $0x80"  					\
		: "=a" (__res)								\
		: "0" (__NR_##name));						\
	if (__res >= 0)	{								\
		return (type) __res; 						\
	}												\
	errno = -__res;									\
	return -1; 										\
}

不带参数的系统调用宏函数。type name(void)。 name 是系统调用的名称,与 NR_ 组合形成上面的系统调用符号常数,从而用来对系统调用表中函数指针寻址。返回:如果返回值大于等于 0,则返回该值,否则置出错号 errno,并返回-1。在宏定义中,若在两个标记符号之间有两个连续的井号'##',则表示在宏替换时会把这两个标记符号连接在一起。例如  NR_##name,在替换了参数 name(例如是 fork)之后,最后在程序中出现的将会是符号 NR_fork。因为宏语句需要定义在一行上,所以使用“\”把这些语句连成一行。

把_syscall0(int,fork)代入后得到如下代码

static inline int fork(void)
{
    long res;
    asm volatile ("int $0x80" : "=a" ( res) : "0" (  NR_fork)); 
    if ( res >= 0)
        return (int) res; 
    errno = - res;
    return -1;
}

这里对嵌入汇编语法作简单说明。具有输入和输出参数的嵌入汇编语法的基本格式为:asm(“汇编语句”: 输出寄存器: 输入寄存器: 会被修改的寄存器);__asm__是GCC关键字asm的宏定义:#define __asm__ asm。__asm__和asm是一个东西。如果不希望汇编语句被 GCC 优化而作修改,就需要在 asm 符号后面添加关键词 volatile。int $0x80是一条AT&T语法的中断指令,用于Linux的系统调用。翻译成Intel的语法那就是:int 80h。int表示中断,数字0x80是中断号。"=a"中的"a"称为加载代码,"="表示这是输出寄存器,并且其中的值将被输出值替代。加载代码是 CPU 寄存器、内存地址以及一些数值的简写字母代号。“0”(_NR_fork) 意思是将fork在sys_call_table[]中对应的函数编号_NR_fork也就是2,将2传给eax寄存器。这个编号就是sys_fork()函数在sys_call_table中的偏移值,其他的系统调用在sys_call_table均存在偏移值()。

 

mem_init(main_memory_start, memory_end);

主内存区初始化函数,具体实现在kernel/mm/memory.c中,原型如下

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

	/* 首先,将1MB到16MB范围内的内存页面对应的数组项置为USED(100),即已占用状态 */
	HIGH_MEMORY = end_mem;			/* 设置内存最高端 */
	for (i = 0; i < PAGING_PAGES; i++) {
		mem_map[i] = USED;
	}

	/* 找到主内存区起始位置处页面号i */
	i = MAP_NR(start_mem);

	/* 得到主内存区的页面的数量 */								
	end_mem -= start_mem;
	end_mem >>= 12;

	/* 将主内存区对应的页面的使用数置0,即未使用 */
	while (end_mem-- > 0) {
		mem_map[i++] = 0;
	}
}

 

trap_init();

陷阱门初始化,具体实现在kernel/trap.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);			/* 函数未实现 */
	set_trap_gate(17, &alignment_check);
	/* 下面把int17-47的陷阱门先均设置为reserved,以后各硬件初始化时会重新设置自己的陷阱门 */
	for (i = 18; i < 48; i++) {
		set_trap_gate(i, &reserved);
	}
	/* 设置协处理器中断0x2d(45)陷阱门描述符,并允许其产生中断请求。设置并行口中断描述符 */
	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);			/* 设置并行口1的中断0x27陷阱门描述符 */
}

 

blk_dev_init();

块设备初始化,实现在kernel/blk_drv/ll_rw_blk.c中,原型如下

void blk_dev_init(void)
{
	int i;

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

主要是初始化块设备的请求数组request[]

 

chr_dev_init();	

字符设备初始化,实现在kernel/chr_drv/tty_io.c中,原型如下

void chr_dev_init(void)
{

}

空,为以后扩展做准备。

 

tty_init();

tty终端初始化,实现在kernel/chr_drv/tty_io.c,原型如下

void tty_init(void)
{
	int i;

	/* 初始化所有终端的缓冲队列结构,设置初值 */
	for (i = 0; i < QUEUES; i++) {
		tty_queues[i] = (struct tty_queue) {0, 0, 0, 0, ""};
	}
	/* 对于串行终端的读/写缓冲队列,将它们的data字段设置为串行端口基地址值.串口1是
	 0x3f8,串口2是0x2f8 */
	rs_queues[0] = (struct tty_queue) {0x3f8, 0, 0, 0, ""};
	rs_queues[1] = (struct tty_queue) {0x3f8, 0, 0, 0, ""};
	rs_queues[3] = (struct tty_queue) {0x2f8, 0, 0, 0, ""};
	rs_queues[4] = (struct tty_queue) {0x2f8, 0, 0, 0, ""};
	/* 初步设置所有终端的tty结构 */
	for (i = 0; i < 256; i++) {
		tty_table[i] =  (struct tty_struct) {
			{0, 0, 0, 0, 0, INIT_C_CC},
			0, 0, 0, NULL, NULL, NULL, NULL
		};
	}
	con_init();
	for (i = 0; i < NR_CONSOLES; i++) {
		con_table[i] = (struct tty_struct) {
			{ICRNL,		/* change incoming CR to NL */ /* CR转NL */
			OPOST|ONLCR,/* change outgoing NL to CRNL */ /* NL转CRNL */
			0,
			IXON | ISIG | ICANON | ECHO | ECHOCTL | ECHOKE,
			0,			/* console termio */
			INIT_C_CC},
			0,			/* initial pgrp */
			0,			/* initial session */
			0,			/* initial stopped */
			con_write,
			con_queues+0+i*3,con_queues+1+i*3,con_queues+2+i*3
		};
	}
	for (i = 0; i < NR_SERIALS; i++) {
		rs_table[i] = (struct tty_struct) {
			{0, /* no translation */
			0,  /* no translation */
			B2400 | CS8,
			0,
			0,
			INIT_C_CC},
			0,
			0,
			0,
			rs_write,
			rs_queues+0+i*3,rs_queues+1+i*3,rs_queues+2+i*3
		};
	}
	/* 初始化伪终端使用的tty结构 */
	for (i = 0; i < NR_PTYS; i++) {
		mpty_table[i] = (struct tty_struct) {
			{0, /* no translation */
			0,  /* no translation */
			B9600 | CS8,
			0,
			0,
			INIT_C_CC},
			0,
			0,
			0,
			mpty_write,
			mpty_queues+0+i*3,mpty_queues+1+i*3,mpty_queues+2+i*3
		};
		spty_table[i] = (struct tty_struct) {
			{0, /* no translation */
			0,  /* no translation */
			B9600 | CS8,
			IXON | ISIG | ICANON,
			0,
			INIT_C_CC},
			0,
			0,
			0,
			spty_write,
			spty_queues+0+i*3,spty_queues+1+i*3,spty_queues+2+i*3
		};
	}
	/* 初始化串行中断处理程序和串行接口1和2(serial.c),并显示系统含有的虚拟控制台
	 数NR_CONSOLES和伪终端数NR_PTYS。 */
	rs_init();
	printk("%d virtual consoles\n\r", NR_CONSOLES);
	printk("%d pty's\n\r", NR_PTYS);
}

初始化所有终端缓冲队列,初始化串口终端和控制台终端

 

time_init();

设置开机启动时间,实现main.c中,原型如下

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);		/* 当前月份(1~12) */
		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--;						/* ti_mon中的月份范围是 0 ~ 11 */
	startup_time = kernel_mktime(&time);/* 计算开机时间。*/
}

CMOS的访问时间很慢。为了减小时间误差,在读取了下面循环中所有数值后,若此时CMOS中秒值发生了变化,则重新读取。这样能控制误差在1s内。

重点看一下main.c中CMOS_READ()这个函数

#define CMOS_READ(addr) ({		\
	outb_p(0x80 | addr, 0x70);	\
	inb_p(0x71); 				\
})

先看看定义在中include/asm/io.h的outb_p(value, port)函数

#define outb_p(value, port) 										\
	__asm__ ("outb %%al,%%dx\n"										\
			"\tjmp 1f\n"											\
			"1:\tjmp 1f\n" 											\
			"1:"::"a" (value),"d" (port))

硬件端口字节输出函数(带延迟),使用两条跳转语句来延迟一会儿,value为欲输出字节,port为端口。为了让 gcc 编译产生的汇编语言程序中寄存器名称前有一个百分号"%",在嵌入汇编语句寄存器名称前就必须写上两个百分号"%%"。

再看看定义在include/asm/io.h的inb_p(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; 															\
	})

硬件端口字节输入函数(带延迟),使用两条跳转语句来延迟一会儿,port为端口,返回读取的字节。

 

sched_init();

调度程序初始化函数,实现在kernel/sched.c中,原型如下

void sched_init(void)
{
	int i;
	struct desc_struct * p;	/* 描述符表结构指针 */

	/* 这个判断语句并无必要 */
	if (sizeof(struct sigaction) != 16) {
		panic("Struct sigaction MUST be 16 bytes");
	}
	set_tss_desc(gdt+FIRST_TSS_ENTRY, &(init_task.task.tss));
	set_ldt_desc(gdt+FIRST_LDT_ENTRY, &(init_task.task.ldt));
	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 */
	__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");
	ltr(0);
	lldt(0);
	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);
}

我们来看看set_tss_desc(,)函数的实现

#define set_tss_desc(n,addr)	_set_tssldt_desc(((char *) (n)),addr, "0x89")

这里是为了在全局表中设置任务状态段描述符(任务状态段描述符的类型是0x89),n表示该描述符的指针,addr表示描述符项中段的基地址值。现在来看_set_tssldt_desc(n,addr,type)函数的实现

#define _set_tssldt_desc(n,addr,type)								\
__asm__ (															\
	"movw $104,%1\n\t"												\
	"movw %%ax,%2\n\t"												\
	"rorl $16,%%eax\n\t"											\
	"movb %%al,%3\n\t"												\
	"movb $" type ",%4\n\t"											\
	"movb $0x00,%5\n\t"												\
	"movb %%ah,%6\n\t"												\
	"rorl $16,%%eax"												\
	::"a" (addr), "m" (*(n)), "m" (*(n+2)), "m" (*(n+4)),			\
	 "m" (*(n+5)), "m" (*(n+6)), "m" (*(n+7))						\
	)

这是在全局表中设置任务状态段/局部表描述符,n为在全局表中描述符项n所对应的地址,addr为状态段/局部表所在内存的基地址,type为描述符中的标志类型字节。

我们再来看看sched_init()中set_ldt_desc(,)的实现

#define set_ldt_desc(n, addr)	_set_tssldt_desc(((char *) (n)),addr, "0x82")

这里和set_tss_desc(n,addr)唯一不同的是局部表段描述符的类型是0x82,作用同样是在全局表中设置局部表描述符。

 

我们再来看看main.c中buffer_init()的实现

void buffer_init(long buffer_end)
{
	struct buffer_head * h = start_buffer;
	void * b;
	int i;
	/* 跳过640KB~1MB的内存空间,该段空间被显示内存和BIOS占用 */
	if (buffer_end == 1<<20) {
		b = (void *) (640*1024);
	}
	else {
		b = (void *) buffer_end;
	}
	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;
		h->b_prev = NULL;
		h->b_data = (char *) b;
		/* 以下两句形成双向链表 */
		h->b_prev_free = h - 1;
		h->b_next_free = h + 1;
		h ++;
		NR_BUFFERS ++;
		/* 同样为了跳过 640KB~1MB 的内存空间 */
		if (b == (void *) 0x100000)
			b = (void *) 0xA0000;
	}
	h --;						/* 让h指向最后一个有效缓冲块头 */
	free_list = start_buffer;	/* 让空闲链表头指向头一个缓冲块 */
	free_list->b_prev_free = h; /* 链表头的b_prev_free指向前一项(即最后一项) */
	h->b_next_free = free_list; /* 表尾指向表头,形成环形双向链表 */
	/* 初始化hash表 */
	for (i = 0; i < NR_HASH; i++) {
		hash_table[i]=NULL;
	}
}	

这是缓冲管理初始化函数,主要是为了缓冲区初始化,缓冲区低端内存被初始化成缓冲头部,缓冲区高端内存被初始化缓冲区。buffer_end是一个>=1M的值。该初始化函数在init/main.c调用。buffer_end为高速缓冲区结束的内存地址。

 

现在来看main.c中hd_init()的实现

void hd_init(void)
{
	blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;     // do_hd_request()
	set_intr_gate(0x2E,&hd_interrupt);  // 设置中断门中处理函数指针              
	outb_p(inb_p(0x21)&0xfb,0x21);    // 复位主片上接联引脚屏蔽位(位 2)
	outb(inb_p(0xA1)&0xbf,0xA1);    // 复位从片上硬盘中断请求屏蔽位(位 6)
}

硬盘系统初始化。设置硬盘中断描述符,并允许硬盘控制器发送中断请求信号。

 

main.c中floppy_init()的实现

void floppy_init(void)
{
    // 设置软盘中断门描述符。floppy_interrupt 是其中断处理过程,见 kernel/sys_call.s,267 行。
    // 中断号为 int 0x26(38),对应 8259A 芯片中断请求信号 IRQ6。
	blk_size[MAJOR_NR] = floppy_sizes;
	blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;    // = do_fd_request()
	set_trap_gate(0x26, &floppy_interrupt);    // 设置陷阱门描述符
	outb(inb_p(0x21)&~0x40,0x21);    // 复位软盘中断请求屏蔽位
}

软盘初始化。

 

main.c中sti()实现

#define sti() __asm__ ("sti"::)	

开启中断

 

main.c中move_to_user_mode()实现

#define move_to_user_mode()						\
__asm__ (										\
	"movl %%esp,%%eax\n\t"						\    // 保存堆栈指针 eap 到 eax 寄存器中。
	"pushl $0x17\n\t"							\    //	首先将堆栈段选择符(SS)入栈。
	"pushl %%eax\n\t"							\    //	然后将保存的堆栈指针值(esp)入栈。
	"pushfl\n\t"								\    //	将标志寄存器(eflags)内容入栈。
	"pushl $0x0f\n\t"							\    //	将Task0 代码段选择符(cs)入栈。
	"pushl $1f\n\t"								\    //	将下面标号 1 的偏移地址(eip)入栈。
	"iret\n"									\    //	执行中断返回指令,跳转到下面标号 1处。
"1:\tmovl $0x17,%%eax\n\t"						\    //	此时开始执行任务 0,
	"mov %%ax,%%ds\n\t"							\    //	初始化段寄存器指向本局部表的数据段。
	"mov %%ax,%%es\n\t"							\
	"mov %%ax,%%fs\n\t"							\
	"mov %%ax,%%gs"								\
	:::"ax")

主要是利用iret指令实现从内核模式移到用户模式去执行初始任务0

 

main.c中init()实现

void init(void)
{
	int pid, i;

	setup((void *) &drive_info);

	(void) open("/dev/tty1", O_RDWR, 0);	/* stdin */
	(void) dup(0);							/* stdout */
	(void) dup(0);							/* stderr */

	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);

	/* fork出任务2 */
	if (!(pid = fork())) {
		/* 将stdin重定向到/etc/rc文件,shell程序会在运行完/etc/rc中设置的命令后退出 */
		close(0);
		if (open("/etc/rc", O_RDONLY, 0)) {
			_exit(1);
		}
		execve("/bin/sh", argv_rc, envp_rc);
		_exit(2);
	}

	if (pid > 0) {	/* init进程等待任务2退出 */
		while (pid != wait(&i)) {
			/* nothing */;
		}
	}
	/* 系统将始终在这个循环中 */
	while (1) {
		if ((pid = fork()) < 0) {
			printf("Fork failed in init\r\n");
			continue;
		}
		/* 新的子进程,关闭句柄(0,1,2),新创建一个会话并设置进程组号,然后重新打开/dev/tty0作
		 为stdin,并复制成stdout和stderr。以登录方式再次执行/bin/sh */
		if (!pid) {
			close(0);close(1);close(2);
			setsid();
			(void) open("/dev/tty1", O_RDWR, 0);
			(void) dup(0);
			(void) dup(0);
			_exit(execve("/bin/sh", argv, envp));
		}
		/* 然后父进程再次运行wait()等待 */
		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() */
	/* _exit和exit都能用于正常终止一个函数。但_exit()直接是一个sys_exit的系统调用,而exit()则
	 是普通函数库中的一个函数。它会先进行一些清除操作,例如调用执行各终止处理程序,关闭所有标
	 准IO等,然后调用sys_exit。*/
}

init()实现了四个目的,安装根文件系统、显示系统信息、运行系统初始资源配置文件rc中的命令、执行用户登录shell程序。

先来看看open(, , ...)函数

int open(const char * filename, int flag, ...)
{
	register int res;
	va_list arg;

	va_start(arg, flag);
	__asm__(
		"int $0x80"
		:"=a" (res)
		:"0" (__NR_open), "b" (filename), "c" (flag), "d" (va_arg(arg, int))
	);
	if (res >= 0){
		return res;
	}
	errno = -res;
	return -1;
}

打开文件函数。打开一个文件或在文件不存在时创建一个文件。参数:filename - 文件名;flag - 文件打开标志;...返回:文件描述符,若出错则置出错码,并返回-1。

 

main.c中(void)dup(0)的实现。(void)是为了强制无返回值。先看该函数对应的宏(定义在lib/dup.c中)

_syscall1(int, dup, int, fd)

复制文件描述符(句柄)函数。下面该宏对应函数原型:int dup(int fd)。直接调用了系统中断 int 0x80,参数是 NR_dup。其中 fd 是文件描述符。然后把参数代入宏即可。

main.c中sync()实现也类似。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值