1. 程式人生 > >Linux 0.11核心的啟動過程

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\)\|\(\.o$$\)\|\( [aU] \)\|\(\.\.ng$$\)\|\(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);
}

從中可以看出,主要是設定了0~47號中斷的入口地址到IDT中,其中32~47這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_table[1].read_q.data);
    init(tty_table[2].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 += HOUR*tm->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。注意高速緩衝區必須跳過視訊記憶體區域640KB~1MB。此時的高速緩衝區為核心程式碼末端~640KB,1MB~4MB。主記憶體區則為4MB~16MB。

這個函式位於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();
}

forkpause均為核心提供的(dupwait等也是),用於任務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_info[1].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核心完全註釋--趙炯》這本書中的頁碼。