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, ÷_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, ¶llel_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)