Linux 0.11内核的启动过程

Linux 0.11内核的启动过程
一、Image文件的构成
1.1 Makefile中的相关命令
Linux 0.11的主Makefile文件中,有如下字段:

tools/build: tools/build.c

$(CC) $(CFLAGS) \

-o tools/build tools/build.c

这个是对tools/build.c程序的编译。

Image: boot/bootsect boot/setup tools/system tools/build

objcopy -O binary -R .note -R .comment tools/system tools/kernel

tools/build boot/bootsect boot/setup tools/kernel $(ROOT_DEV) > Image

rm tools/kernel -f

sync

objcopy这条命令首先将tools/system这个编译后的内核代码制作成纯二进制文件,保存在tools/kernel中,然后使用上述编译好的build工具,将boot/bootsect、boot/setup、tools/kernel、根设备号作为build的参数,并将结果重定向输出到Image中。最后强制删除tools/kernel。

1.2 build.c程序的功能
首先根据传入参数的个数,设置根设备号,并填写到第一个扇区的第508,509字节中,也就是说我们可以覆盖根设备号,自主设定根设备号,如果我们没有指定根设备号,build.c程序将使用默认值DEFAULT_MAJOR_ROOT,DEFAULT_MINOR_ROOT,这个值可能是0x21d,也就是第二个软盘。由于使用的是重定向,DEBUG信息只能通过stderr来输出。

然后读取boot/bootsect,先读掉MINIX可执行文件头,再读取512字节二进制代码,并写到标准输出流1中。

接着把boot/setup也输出到标准输出流1中,先读掉MINIX可执行文件头,再继续读取剩下的整个文件,然后补0,直到4个扇区为止。

对于bootsect和setup的编译,有

boot/bootsect:boot/bootsect.s

$(AS86) -o boot/bootsect.o boot/bootsect.s

$(LD86) -s -o boot/bootsect boot/bootsect.o

boot/setup: boot/setup.s

$(AS86) -o boot/setup.o boot/setup.s

$(LD86) -s -o boot/setup boot/setup.o

也就是使用as86和ld86来编译的,可执行文件与gcc编译后的不一样。

接着读取boot/kernel,将其全部输出到标准输出流1中,注意内核的大小不超过0x30000个字节,也就是192KB。

对于system的编译,有

tools/system:boot/head.o init/main.o \

$(ARCHIVES) $(DRIVERS) $(MATH) $(LIBS)

$(LD) $(LDFLAGS) boot/head.o init/main.o \

$(ARCHIVES) \

$(DRIVERS) \

$(MATH) \

$(LIBS) \

-o tools/system

nm tools/system | grep -v 'compiled








|.oKaTeX parse error: Expected group as argument to '\.' at position 4: \. ̲� $ $ \|[aU] [ …
.
.


$
$
|LASH[RL]DI




[


]


'| sort > System.map

很显然,它是将很多内核代码连接在一起的,其中head.o在system的最前面。

最后在主Makefile中,还提供了这样的工具:

disk: Image

dd bs=8192 if=Image of=/dev/fd0

表示将Image拷贝到/dev/fd0这个软盘中

1.3 Image文件的构成

二、boosect代码的作用
2.1 概述
bootsect位于启动盘的第一个扇区,由BIOS自动加载到内存的0x7c00的位置,且只加载第一个扇区,共512字节。加载后将跳到0x7c00来执行代码,此时CS = 0x7c0,IP = 0,即指向第一条指令代码。

bootsect首先将自身移动到0x90000的位置,然后跳到0x90000的go标号处执行,重新设置DS = ES = SS = 0x9000、SP = 0xff00,栈顶在0x9ff00处。

接着使用BIOS 0x13号中断,将setup共4个扇区的代码加载到0x90200开始的位置,驱动器0?。

然后读取软盘(第83行)的每个磁道的最大扇区数,并填到标号sectors两个字节的内存中。这个量可以给读取system使用。接着读取光标位置,在屏幕显示标号msg1的信息:

”\r\nLoading system …\r\n\r\n”,并移动光标。

再读取system的代码到0x10000中,注意system的大小不会超过192KB,所以末端为0x40000。将第6个扇区(从1开始)开始的读取0x30000个字节到内存0x10000中。

接着对第509和510字节值进行检测,如果值不为0,则跳到setup处(CS = 0x9020,IP = 0,139行)执行。事实上,509和510字节中的初始值为0x306,也就是第二个硬盘第一个分区。但我们可以在build时改变它的值。

思考:这里貌似设置了只能在软盘中启动,包括内核代码system,也固定从软盘中加载。

2.2 程序流程图

三、setup代码的作用
3.1 概述
setup利用BIOS 0x10中断,读取光标的位置,扩展内存的大小,显示卡参数,以及两个硬盘参数表信息到起始内存0x90000中,也就是bootsect的代码被覆盖,其中硬盘参数表与硬盘分区表不一样,且起始位置为0x90080和0x90090,每个都有16个字节,第二个硬盘参数表不存在的话,初始化为0。而且0x901FC保存的根设备号没有被覆盖。

接着将内核代码system从0x10000移动到0开始的位置,即0x10000 ~ 0x90000的内容移动到0x0 ~ 0x80000,每次移动0x10000字节,即64KB,共8次。

加载中断描述符的段长&地址和全局描述符的段长&地址(共6字节)到相应寄存器中,然后开启A20地址线,屏蔽8254主从芯片的所有请求,并将保护模式置位(CR0寄存器中的最低位),此时中断标志没有置位,也就是没有开启中断。自此从实模式进入保护模式,但分页未开启。

跳到CS = 8,IP = 0(第193行),即地址为0的地方开始执行,也就是system的head.o开始执行。此时选择子RPL = 0。

注意:GDT位于setup.s的205行(地址为:0x90200 + gdt),共有3项,每项8字节。第一项不用,默认为0。第二项是代码段,基地址是0,长度为8MB,可读执行。第三项为数据段,基地址为0,长度为8MB,可读写。这个GDT是临时的,提供给内核启动使用而已。

上述是为执行内核代码作准备。

3.2 程序流程图

四、head.s代码的作用
4.1 概述
在主Makefile中,有

boot/head.o: boot/head.s

gcc -I./include -traditional -c boot/head.s -m32

mv head.o boot/

显然head.s格式是AT&T汇编语言格式,使用的是gcc编译器,因为它最终要与其他用C语言写的模块进行连接。

由于刚开启保护模式,这时候实模式下的段地址已经不能使用。故head.s首先将各个数据段重新设置为段选择子,且为0x10,RPL = 0。设置堆栈为stack_start(位于sched.c的69行),使用的堆栈起始是user_stack,共一页内存。这个堆栈即为内核初始化时使用的系统堆栈。然后重新设置IDT,使用(0x8, ignore_int)作为所有中断发生的入口地址,初始化idt开始的2KB内存,最后LIDT加载到寄存器中。ignore_int仅仅打印一行”Unknown interrupt!\n\r”。

使用LGDT重新设置GDT,第一项依旧不用,第二项为代码段0x08,基地址为0,长度为16MB,只可以执行。第三项为内核数据段0x10,基地址为0,长度为16MB,可读写。第四项置为0,不用,剩下的252项全部初始化为0。注意DPL = 0。由于GDT重新设置,缓存无效,必须重新更新段寄存器。

接着检查A20地址线是否开启。检查的方式是项0x10000(1MB)逐渐写入1,2,3,4…然后比较内存0处是否为该值,如果是则不断循环,否则说明已经开启,放入0x10000的值不会放入0处。

然后跳到after_page_tables标号处执行(135行)。布置setup_paging执行后执行main函数的环境:

当前栈顶指向main,即SP = user_stack + PAGE_SIZE - 16。然后执行jmp setup_paging,跳到setup_paging处执行,注意该函数有ret,即最后会跳到main函数处执行。

setup_paging主要是初始化一个页目录和4个页表,共有16MB字节的内存,完成低16MB内存的线性地址和物理地址的一一映射,也就是内核态下物理地址和线性地址是一样的。注意这是从高地址到地址完成的(方向位std)。刚好与上述设置的内核代码段和内核数据段的段长一致。然后初始化CR3寄存器为页目录地址0,CR0的第31位(最高位)置1,开启分页模式。

setup_paging这段代码执行后,内存地址0处的head.o部分代码将被页目录覆盖。低5页内存映像如下:

4.2 程序流程图

五、main函数启动任务0,1
前面主要涉及到获取硬件参数,进入保护模式,开启分页模式,初始化中断描述符合和全局描述符等工作,所以用了汇编语言来写。main函数位于/init/main.c中,是用C语言写的。注意在用gcc编译时,要将main改名,这样才能让heas.s位于system模块的开头,否则gcc会认为main才是入口。main主要是设置中断时执行的函数,块设备和字符设备的初始化,tty初始化,以及内存缓冲区链表的初始化,系统开机时间的初始化,硬盘的初始化,以及任务0的初始化,允许中断处理,然后将任务0移动到用户态下执行,启动任务1(init进程),进入无休止的睡眠。任务1挂载根文件系统,设置标准输入输出和错误,并创建shell进程,最后循环等待所有子进程退出,回收僵尸进程。下面的工作事实上都是由任务0完成的,按照main函数调用次序组织。

5.1 页框的初始化
main中对应的函数调用是mem_init(main_memory_start,memory_end)
其中,对于bochs来说,main_memory_start = 4MB,memory_end = 16MB,第二个值为1MB + 从BIOS获得的扩展内存的大小,但不超过16MB。显然主内存通常是1MB以上的内存,也就是扩展内存。这个内存主要是用来规划页框。

这个函数位于mm/memory.c中的第400行(p344):

void mem_init(long start_mem, long end_mem)
{
int i;
HIGH_MEMORY = end_mem;
for (i = 0 ; i < PAGING_PAGES; i++)
mem_map[i] = USED;
i = MAP_NR(start_mem);
end_mem -= start_mem;
end_mem >>= 12;
while (end_mem–>0)
mem_map[i++]=0;
}
对于0.11的内核,其最大可规划的主内存是15MB,所以mem_map对应的是这15MB的使用情况,对于1MB~4MB,其下标从0开始,初始化为0表示未被使用。1MB以下内存用于存放内核代码和显存。

5.2 具体中断的初始化
trap_init();

这个函数位于kernel/traps.c的181行(p80):

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);
for (i=17; i<48; i++)
set_trap_gate(i,&reserved);
set_trap_gate(45,&irq13);
outb_p(inb_p(0x21)&0xfb,0x21);
outb(inb_p(0xA1)&0xdf,0xA1);
set_trap_gate(39,illel_interrupt);
}

从中可以看出,主要是设置了047号中断的入口地址到IDT中,其中3247这16个中断对应的是8259芯片的中断,并开放了8259的两个中断请求。剩下的8259中断,将在各个初始化函数中设置。

其中set_trap_gate(n, addr)位于include/asm/system.h中的第36行(p390),主要是在idt的对应偏移中设置入口地址,制作描述符。

page_fault函数位于mm/page.s(p345)中,主要是缺页时或者写保护时,调用相应的内存处理函数。这个函数是实现写时复制的关键。

5.3 块设备的初始化
blk_dev_init();

这个函数位于kernel/blk_drv/ll_rw_blk.c第157行(p153):

void blk_dev_init(void)
{
int i;
for (i=0 ; i<NR_REQUEST ; i++)
{
request[i].dev = -1;
request[i].next = NULL;
}
}
上述函数中,主要是对32个请求项进行初始化,表示没被使用。

struct request的结构定义在kernel/blk_drv/blk.h(p134):

struct request
{
int dev; /* -1 if no request /
int cmd; /
READ or WRITE */
int errors;
unsigned long sector;
unsigned long nr_sectors;
char * buffer;
struct task_struct * waiting;
struct buffer_head * bh;
struct request * next;
};
定义了完整的请求信息,包括哪个设备读或写请求哪几个扇区,然后将扇区读到哪个缓冲区中,或写哪个缓冲区,同时还有等待当前项的进程链表。request请求是一个链表,通过next连接,linux电梯调度也在这里发生,涉及到底层IO操作。

5.4 字符设备的初始化
chr_dev_init();

该函数为空。

tty_init();

这个函数定义在kernel/chr_drv/tty_io.c的第105行(p218):

void tty_init(void)
{
rs_init();
con_init();
}
其中rs_init()位于kernel/chr_drv/serial.c第37行(p211):

void rs_init(void)
{
set_intr_gate(0x24,rs1_interrupt);
set_intr_gate(0x23,rs2_interrupt);
init(tty_table1.read_q.data);
init(tty_table2.read_q.data);
outb(inb_p(0x21)&0xE7,0x21);
}
这个函数主要设置串口1和串口2的中断处理函数,同时初始化串口1和串口2的一些硬件属性。

con_init()位于kernel/chr_drv/console.c的第617行(p205):

void con_init(void)
{
register unsigned char a;
char *display_desc = “???”;
char *display_ptr;

video_num_columns = ORIG_VIDEO_COLS;
video_size_row = video_num_columns * 2;
video_num_lines = ORIG_VIDEO_LINES;
video_page = ORIG_VIDEO_PAGE;
video_erase_char = 0x0720;
if (ORIG_VIDEO_MODE == 7) /* Is this a monochrome display? */
{
    video_mem_start = 0xb0000;
    video_port_reg = 0x3b4;
    video_port_val = 0x3b5;
    if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10)
    {
        video_type = VIDEO_TYPE_EGAM;
        video_mem_end = 0xb8000;
        display_desc = "EGAm";
    }
    else
    {
        video_type = VIDEO_TYPE_MDA;
        video_mem_end = 0xb2000;
        display_desc = "*MDA";
    }
}
else /* If not, it is color. */
{
    video_mem_start = 0xb8000;
    video_port_reg = 0x3d4;
    video_port_val = 0x3d5;
    if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10)
    {
        video_type = VIDEO_TYPE_EGAC;
        video_mem_end = 0xbc000;
        display_desc = "EGAc";
    }
    else
    {
        video_type = VIDEO_TYPE_CGA;
        video_mem_end = 0xba000;
        display_desc = "*CGA";
    }
}

/* Let the user known what kind of display driver we are using */
display_ptr = ((char *)video_mem_start) + video_size_row - 8;
while (*display_desc)
{
    *display_ptr++ = *display_desc++;
    display_ptr++;
}
/* Initialize the variables used for scrolling (mostly EGA/VGA) */
origin = video_mem_start;
scr_end = video_mem_start + video_num_lines * video_size_row;
top = 0;
bottom = video_num_lines;

gotoxy(ORIG_X,ORIG_Y);
set_trap_gate(0x21,&keyboard_interrupt);
outb_p(inb_p(0x21)&0xfd,0x21);
a=inb_p(0x61);
outb_p(a|0x80,0x61);
outb(a,0x61);

}

这个程序主要完成显示屏和键盘的初始化,在显示屏显示显卡的类型,设置键盘中断的入口函数。
5.5 系统时间的初始化
time_init() : 读取当前系统启动时的详细时间,如2016.12.05 20:13:14,但是以1970.01.01 00:00:00为起点表示的秒。而jiffies表示的系统运行时间,单位是10ms,每10ms发生一次日时钟中断,而该变量会加一,该变量是计算机世界的“时间”。start_up + jiffies / 100表示的将是实际的时间。

这个函数位于init/main.c第76行(p64):

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

}
主要是从CMOS中读取实时时钟,读取到的是BCD码,设置系统启动时间到startup_time中,单位是秒。其中kernel_mktime()位于kernel/mktime.c(p91),

该函数将从实时时钟得到的年月日时秒转化为秒:

long kernel_mktime(struct tm * tm)
{
long res;
int year;
year = tm->tm_year - 70;
/* magic offsets (y+1) needed to get leapyears right./
res = YEAR
year + DAY*((year+1)/4);
res += month[tm->tm_mon];
/* and (y+2) here. If it wasn’t a leap-year, we have to adjust /
if (tm->tm_mon>1 && ((year+2)%4))
res -= DAY;
res += DAY
(tm->tm_mday-1);
res += HOURtm->tm_hour;
res += MINUTE
tm->tm_min;
res += tm->tm_sec;
return res;
}

5.6 任务0的初始化
sched_init();

这个函数位于kernel/sched.c的385行(p102):

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

}

这个函数主要是利用任务0的任务状态段和局部描述符段的偏移地址对GDT描述符进行设置,同时选择子加载到相应的寄存器中,剩余的63个任务初始化为空,描述符也为空。最后设置时钟中断(32号)的入口地址,并开启。设置128号系统调用中断号的入口地址。其实,一开始的内核代码执行流就是任务0在执行。可以从include/linux/sched.h第115行(p405)找到INIT_TASK的定义:

#define INIT_TASK
/* state etc / { 0,15,15,
/
signals / 0,{{},},0,
/
ec,brk… / 0,0,0,0,0,0,
/
pid etc… / 0,-1,0,0,0,
/
uid etc / 0,0,0,0,0,0,
/
alarm / 0,0,0,0,0,0,
/
math / 0,
/
fs info / -1,0022,NULL,NULL,NULL,0,
/
filp / {NULL,},
{
{0,0},
/
ldt */ {0x9f,0xc0fa00},
{0x9f,0xc0f200},
},
/tss/ {0,PAGE_SIZE+(long)&init_task,0x10,0,0,0,0,(long)&pg_dir,
0,0,0,0,0,0,0,0,
0,0,0x17,0x17,0x17,0x17,0x17,0x17,
_LDT(0),0x80000000,
{}
},
}

从上述任务0的LDT描述符中可以看出,第一个描述符不用,第二个为代码段,第三个为数据段,基地址都为0,段长都为640KB,映射内核代码。任务0的结构体TSS大部分都为0,主要设置了内核态的堆栈,这样当任务切换到内核态时,可以获取到内核态的堆栈,其他都会在任务切换时保存到TSS相应的位置上。还设置了页目录地址,以后的进程的页目录都是一个,可以从sys_fork()从看出来。

5.7 内存高速缓冲区
buffer_init(buffer_memory_end);

由于在bochs中,内存超过16MB,而linux 0.11最大支持16MB的内存。故buffer_memory_end = 4MB。注意高速缓冲区必须跳过显存区域640KB1MB。此时的高速缓冲区为内核代码末端640KB,1MB4MB。主内存区则为4MB16MB。

这个函数位于fs/buffer.c的第351行(p250):

void buffer_init(long buffer_end)
{
struct buffer_head * h = start_buffer;
void * b;
int i;

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++;
    if (b == (void *) 0x100000)
        b = (void *) 0xA0000;
}
h--;
free_list = start_buffer;
free_list->b_prev_free = h;
h->b_next_free = free_list;
for (i=0; i<NR_HASH; i++)
    hash_table[i]=NULL;

}

这个函数首先确定高速缓冲区的位置,内核代码结束的地方是高速缓冲区的开始。跳过显存640KB~1MB。NR_BUFFERS定义在fs/buffer.c的第34行,统计缓冲块的个数。

注意这里缓冲头从高速缓冲区的起始开始分配,而缓冲块则从后往前,从高速缓冲区的末端开始分配,构成一一对应的管理关系。每个缓冲头指向一块高速缓冲区,缓冲头前后项相互指向,构建空闲内存双向环形链表。最后free_list指向第一项,初始化整个hash_table为空。

综合上面内核代码的移动,分页,以及这里的高速缓冲区,内存的映像如下:

5.8 硬盘的初始化
hd_init();

这个函数位于kernel/blk_drv/hd.c的第343行(p146):

void hd_init(void)
{
blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;
set_intr_gate(0x2E,&hd_interrupt);
outb_p(inb_p(0x21)&0xfb,0x21);
outb(inb_p(0xA1)&0xbf,0xA1);
}
主要是设置请求函数,设置46号中断,即硬盘中断的处理函数,同时将主8254的int2开放,允许从片发出中断。复位硬盘中断IRQ14屏蔽码。硬盘的主设备号是MAJOR_NR = 3。

5.9 软盘的初始化
floppy_init();

这个函数位于kernel/blk_drv/floppy.c的第458行(p168):

void floppy_init(void)
{
blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;
set_trap_gate(0x26,&floppy_interrupt);
outb(inb_p(0x21)&~0x40,0x21);
}
这个函数主要是设置软盘中断的设置软盘中断的处理函数,将IRQ6的软盘中断开放。软盘的主设备号是MAJOR_NR = 2。

5.10 任务0切换到用户态下执行
接着,允许系统发生中断,并将任务0切换到用户态下执行:

sti();

move_to_user_mode();

这两个嵌入式汇编宏均在include/asm/system.h(p389):

#define move_to_user_mode()
asm (“movl %%esp,%%eax\n\t”
“pushl $0x17\n\t”
“pushl %%eax\n\t”
“pushfl\n\t”
“pushl $0x0f\n\t”
“pushl $1f\n\t”
“iret\n”
“1:\tmovl $0x17,%%eax\n\t”
“movw %%ax,%%ds\n\t”
“movw %%ax,%%es\n\t”
“movw %%ax,%%fs\n\t”
“movw %%ax,%%gs”
:::“ax”)
#define sti() asm (“sti”:😃
可以发现,在切换到用户态时,中断已经开启。该宏布置了中断返回现场,即SS = 0x17, ESP = 原来的的系统内核栈(user_stack)栈顶(跳转到main时有16字节没退出), EFLAGS, CS = 0xf, EIP = 标号1处的地址。也就是,系统的内核栈,经切换之后变为了任务0用户态下的栈,特权级变为3,切换后使用的是LDT的地址,然而对应的段基地址仍为0,考虑到页目录的基地址为0,故任务0执行的仍然是内核代码。

接着创建出init进程,然后任务0进入无休止的睡眠。

if (!fork()) /* we count on this going ok */
{
init();
}
for(;😉
{
pause();
}
fork,pause均为内核提供的(dup,wait等也是),用于任务0在用户态下启动进程的内核库函数。

5.11 任务0的流程图

六、任务1(init进程)的执行过程
init进程主要是挂载文件系统,设置标准输入输出错误句柄,同时创建出进程2,运行shell。init进程作为所有孤儿进程的父进程,最后不断取回用户进程的退出码,回收僵尸进程,撤销进程表项。此时所有的进程都处在用户态下运行。

6.1 挂载根文件系统
setup((void*)&drive_info)

drive_info主要来源于bios对硬盘参数表的读取,两个硬盘共32字节。

init是由任务0fork出来的,而drive_info本来就在编译后的内核数据段中,类似的还有argv_rc, envp_rc, argv, envp等,这些东西最后都是被共享了,虚拟地址空间不同而已,引用的物理页地址是一样的,都是在上述一开始建立页目录和4个页表时建立的。注意fork的机制,只需重新复制页表就可以了,物理页是一样的,只要不对数据进行写,则不会发生重新建立物理内存页的现象(写时复制)。

该函数对应的系统调用时sys_setup,位于kernel/blk_drv/hd.c(p140)第71行:

int sys_setup(void * BIOS)
{
static int callable = 1;
int i,drive;
unsigned char cmos_disks;
struct partition *p;
struct buffer_head * bh;

if (!callable)
    return -1;
callable = 0;

#ifndef HD_TYPE
for (drive=0 ; drive<2 ; drive++)
{
hd_info[drive].cyl = *(unsigned short *) BIOS;
hd_info[drive].head = *(unsigned char *) (2+BIOS);
hd_info[drive].wpcom = *(unsigned short *) (5+BIOS);
hd_info[drive].ctl = *(unsigned char *) (8+BIOS);
hd_info[drive].lzone = (unsigned short ) (12+BIOS);
hd_info[drive].sect = (unsigned char ) (14+BIOS);
BIOS += 16;
}
if (hd_info1.cyl)
NR_HD=2;
else
NR_HD=1;
#endif
for (i=0 ; i<NR_HD ; i++)
{
hd[i
5].start_sect = 0;
hd[i
5].nr_sects = hd_info[i].head

hd_info[i].sect
hd_info[i].cyl;
}

/*
We querry CMOS about hard disks : it could be that
we have a SCSI/ESDI/etc controller that is BIOS
compatable with ST-506, and thus showing up in our
BIOS table, but not register compatable, and therefore
not present in CMOS.
Furthurmore, we will assume that our ST-506 drives
<if any> are the primary drives in the system, and
the ones reflected as drive 1 or 2.
The first drive is stored in the high nibble of CMOS
byte 0x12, the second in the low nibble.  This will be
either a 4 bit drive type or 0xf indicating use byte 0x19
for an 8 bit type, drive 1, 0x1a for drive 2 in CMOS.
Needless to say, a non-zero value means we have
an AT controller hard disk for that drive.
*/

if ((cmos_disks = CMOS_READ(0x12)) & 0xf0)
    if (cmos_disks & 0x0f)
        NR_HD = 2;
    else
        NR_HD = 1;
else
    NR_HD = 0;
for (i = NR_HD ; i < 2 ; i++)
{
    hd[i*5].start_sect = 0;
    hd[i*5].nr_sects = 0;
}
for (drive=0 ; drive<NR_HD ; drive++)
{
    if (!(bh = bread(0x300 + drive*5,0)))
    {
        printk("Unable to read partition table of drive %d\n\r",
               drive);
        panic("");
    }
    if (bh->b_data[510] != 0x55 || (unsigned char)
            bh->b_data[511] != 0xAA)
    {
        printk("Bad partition table on drive %d\n\r",drive);
        panic("");
    }
    p = 0x1BE + (void *)bh->b_data;
    for (i=1; i<5; i++,p++)
    {
        hd[i+5*drive].start_sect = p->start_sect;
        hd[i+5*drive].nr_sects = p->nr_sects;
    }
    brelse(bh);
}
if (NR_HD)
    printk("Partition table%s ok.\n\r",(NR_HD>1)?"s":"");
rd_load();
mount_root();
return (0);

}

显然,这个函数利用从BIOS中读取的32个字节(保存在0x90080),获取到两个硬盘驱动的柱面数、磁头数、每磁道扇区数、控制字等,然后我们可以计算出每个硬盘的扇区总数,hd[0],hd[5]分别代表第一、二块硬盘,存储起始扇区为0,以及整块硬盘的扇区总数。如果第二块硬盘参数中有出现0,则表示不存在,设置NR_HD = 1。我们可以在linclude/linux/config.h中注释掉HD_TYPE,这样就可以自定义两块硬盘的上述参数。

然后通过读取每块硬盘的第一个扇区,读取硬盘分区表,初始化每个分区的起始扇区和扇区数,0x301~0x304, 0x306~0x309。注意,如果硬盘不存在,则不会执行这个步骤。

执行rd_load(),如果我们没有定义虚拟硬盘,则不会以虚拟硬盘作为根设备启动。最后执行mount_root(),这个函数位于fs/super.c第242行(p270):

void mount_root(void)
{
int i,free;
struct super_block * p;
struct m_inode * mi;

if (32 != sizeof (struct d_inode))
    panic("bad i-node size");
for(i=0; i<NR_FILE; i++)
    file_table[i].f_count=0;
if (MAJOR(ROOT_DEV) == 2)
{
    printk("Insert root floppy and press ENTER");
    wait_for_keypress();
}
for(p = &super_block[0] ; p < &super_block[NR_SUPER] ; p++)
{
    p->s_dev = 0;
    p->s_lock = 0;
    p->s_wait = NULL;
}
if (!(p=read_super(ROOT_DEV)))
    panic("Unable to mount root");
if (!(mi=iget(ROOT_DEV,ROOT_INO)))
    panic("Unable to read root i-node");
mi->i_count += 3 ; /* NOTE! it is logically used 4 times, not 1 */
p->s_isup = p->s_imount = mi;
current->pwd = mi;
current->root = mi;
free=0;
i=p->s_nzones;
while (-- i >= 0)
    if (!set_bit(i&8191,p->s_zmap[i>>13]->b_data))
        free++;
printk("%d/%d free blocks\n\r",free,p->s_nzones);
free=0;
i=p->s_ninodes+1;
while (-- i >= 0)
    if (!set_bit(i&8191,p->s_imap[i>>13]->b_data))
        free++;
printk("%d/%d free inodes\n\r",free,p->s_ninodes);

}

这里面的ROOT_DEV定义在fs/super.c第29行,然而它会在main.c中第110行被重新赋值,其值取自0x901fc,也就是启动扇区的508,509字节,这个值编译结束后是固定的。对于以软盘作为根设备而言,一般是第二个软盘,即0x21d,系统启动后会提示:Insert root floppy and press ENTER”

这个函数首先设置全局文件描述符表为未使用,然后初始化超级块数组。读取根设备的超级块,取超级块数组的第一项,超级块位于第三四扇区,即第二数据块,且读取超级块时会把i节点位图和数据块位图都读到高速缓冲区来。然后读取该超级块的第一个节点,作为根节点,同时将该超级块代表的文件系统的挂载点设置为根节点。然后读取该超级块中文件系统的数据块空闲块数,空闲i节点数,并打印出统计信息。

该函数执行结束后,会打印出NR_BUFFERS * BLOCK_SIZE,表示可用的缓冲区字节数,不包含缓冲头。再打印出主内存数,也就是用于分页的内存。

3450 buffers = 3532800 bytes buffer space

Free mem: 12582912 bytes

6.2 启动任务2(Shell进程)
shell进程是任务1使用fork创建的,继承了进程1的文件句柄,故它首先将标准输入关闭,并以/etc/rc作为标准输入,然后使用环境变量和参数,执行/bin/sh可执行文件,启动shell进程。

注:上述页码pxxx均表示《Linux内核完全注释–赵炯》这本书中的页码
————————————————
版权声明:本文为CSDN博主「ac_dao_di」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https原文链接:https://blog.csdn.net/ac_dao_di/article/details/52144608@TOC

欢迎使用Markdown编辑器

你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。

新的改变

我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:

  1. 全新的界面设计 ,将会带来全新的写作体验;
  2. 在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;
  3. 增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示;
  4. 全新的 KaTeX数学公式 语法;
  5. 增加了支持甘特图的mermaid语法1 功能;
  6. 增加了 多屏幕编辑 Markdown文章功能;
  7. 增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间;
  8. 增加了 检查列表 功能。

功能快捷键

撤销:Ctrl/Command + Z
重做:Ctrl/Command + Y
加粗:Ctrl/Command + B
斜体:Ctrl/Command + I
标题:Ctrl/Command + Shift + H
无序列表:Ctrl/Command + Shift + U
有序列表:Ctrl/Command + Shift + O
检查列表:Ctrl/Command + Shift + C
插入代码:Ctrl/Command + Shift + K
插入链接:Ctrl/Command + Shift + L
插入图片:Ctrl/Command + Shift + G
查找:Ctrl/Command + F
替换:Ctrl/Command + G

合理的创建标题,有助于目录的生成

直接输入1次#,并按下space后,将生成1级标题。
输入2次#,并按下space后,将生成2级标题。
以此类推,我们支持6级标题。有助于使用TOC语法后生成一个完美的目录。

如何改变文本的样式

强调文本 强调文本

加粗文本 加粗文本

标记文本

删除文本

引用文本

H2O is是液体。

210 运算结果是 1024.

插入链接与图片

链接: link.

图片: Alt

带尺寸的图片: Alt

居中的图片: Alt

居中并且带尺寸的图片: Alt

当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。

如何插入一段漂亮的代码片

博客设置页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的 代码片.

// An highlighted block
var foo = 'bar';

生成一个适合你的列表

  • 项目
    • 项目
      • 项目
  1. 项目1
  2. 项目2
  3. 项目3
  • 计划任务
  • 完成任务

创建一个表格

一个简单的表格是这么创建的:

项目Value
电脑$1600
手机$12
导管$1

设定内容居中、居左、居右

使用:---------:居中
使用:----------居左
使用----------:居右

第一列第二列第三列
第一列文本居中第二列文本居右第三列文本居左

SmartyPants

SmartyPants将ASCII标点字符转换为“智能”印刷标点HTML实体。例如:

TYPEASCIIHTML
Single backticks'Isn't this fun?'‘Isn’t this fun?’
Quotes"Isn't this fun?"“Isn’t this fun?”
Dashes-- is en-dash, --- is em-dash– is en-dash, — is em-dash

创建一个自定义列表

Markdown
Text-to- HTML conversion tool
Authors
John
Luke

如何创建一个注脚

一个具有注脚的文本。2

注释也是必不可少的

Markdown将文本转换为 HTML

KaTeX数学公式

您可以使用渲染LaTeX数学表达式 KaTeX:

Gamma公式展示 Γ ( n ) = ( n − 1 ) ! ∀ n ∈ N \Gamma(n) = (n-1)!\quad\forall n\in\mathbb N Γ(n)=(n1)!nN 是通过欧拉积分

Γ ( z ) = ∫ 0 ∞ t z − 1 e − t d t   . \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. Γ(z)=0tz1etdt.

你可以找到更多关于的信息 LaTeX 数学表达式here.

新的甘特图功能,丰富你的文章

2014-01-07 2014-01-09 2014-01-11 2014-01-13 2014-01-15 2014-01-17 2014-01-19 2014-01-21 已完成 进行中 计划一 计划二 现有任务 Adding GANTT diagram functionality to mermaid
  • 关于 甘特图 语法,参考 这儿,

UML 图表

可以使用UML图表进行渲染。 Mermaid. 例如下面产生的一个序列图:

张三 李四 王五 你好!李四, 最近怎么样? 你最近怎么样,王五? 我很好,谢谢! 我很好,谢谢! 李四想了很长时间, 文字太长了 不适合放在一行. 打量着王五... 很好... 王五, 你怎么样? 张三 李四 王五

这将产生一个流程图。:

链接
长方形
圆角长方形
菱形
  • 关于 Mermaid 语法,参考 这儿,

FLowchart流程图

我们依旧会支持flowchart的流程图:

Created with Raphaël 2.3.0 开始 我的操作 确认? 结束 yes no
  • 关于 Flowchart流程图 语法,参考 这儿.

导出与导入

导出

如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。

导入

如果你想加载一篇你写过的.md文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,
继续你的创作。


  1. mermaid语法说明 ↩︎

  2. 注脚的解释 ↩︎

带中文注释可成功编译运行的Linux0.11+Bochs2.62实验环境说明 此注释以网上获得的“linux带中文注释的0.11版本”为基础,对照赵炯博士《Linux内核完全注释(0.11) 》V3.0版(http://oldlinux.org/download/clk011c-3.0.pdf)编辑而成。作为对赵博士感谢,以及对Linux初学者的回馈,特发布在CSDN上。 此注释可以在http://oldlinux.org/Linux.old/bochs/提供的Linux-0.11-devel-XXXXXX实验环境下正确编译成功,使用:"make disk"命令重启Bochs虚拟机后,新编译源码直接生效,便于学习者直接阅读源码,直接进行实验。 注意事项: 1、为了使注释版与实验环境上的Linux0.11内核保持一致,达到对应文件可以互换的目的,与Linux0.11原始版本相比,加入了15个系统调用函数(参见include/Linux/sys.h第78-92行。赵博士原书没有这部分注释,我不敢班门弄斧),其它相关的文件加入了相应的定义。新加入的代码只有函数体定义,没有具体实现,对其它原始代码没有改变、没有影响。 2、键盘定义改成了美式键盘(原始代码中是芬兰键盘,会导致个别键出问题,调试的时候我曾被迷糊了好久,以为自己把程序搞乱了)。 3、把网上VC版的注释统一改成了 “/* */” 格式的注释。经测试,在Linux0.11实验环境中(gcc1.40),只有标准C注释语法可以正常编译。 4、由于《Linux内核完全注释(0.11) 》原书版本更新的原因,注释中提到的图、表可能与V3.0版书中不一致。 5、由于代码中加入注释,代码行号发生变化,注释中提到的代码行号会出现不一致,建议对照3.0版查询对应内容。 6、实验方法:请先安装附带的Bochs2.62版安装包,双击Test.bxrc即可启动实验系统,执行命令:sh t,即可完成对linuxcn的编译。 7、linux目录中是此实验系统中/usr/src/linux提取出来的不含中文注释的linux0.11源码(此版本比原始的0.11版多15个系统调用函数),linuxcn是加入了中文注释的源码。 8、diskb.img是实验系统与Windows环境下进行文件交换的1.44M软盘映像,执行脚本命令"sh t"时会自动从此映像中读取linux.tar、linuxcn.tar包,解包并编译,编译结果在:/usr/root/zw/linuxcn目录下。为了方便文件交换,建议使用7zip为压缩/解压缩工具(7zip可以直接生成tar包),用WinImage实现Windows环境与软件映像交换文件。 9、实验系统下 .profile中加入了几个命令,请读者注意。 10、若实验环境的启动盘被破坏,请用压缩包中的bootimage-0.11-hd覆盖对应文件即可。 11、若实验环境的要命文件系统被破坏,请用压缩包中的hdc-0.11-new.img覆盖对应文件即可。 2014-5-4 cyfx2288
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值