main 函式解析(二)—— Linux-0.11 學習筆記(六)

ARM的程式設計師敲著詩歌的夢發表於2020-04-04

main函式解析(二)——Linux-0.11 學習筆記(六)

4.6 blk_dev_init函式

void blk_dev_init(void)
{
    int i;

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

這裡的request是一個全域性的陣列。

/*
 * The request-struct contains all necessary data
 * to load a nr of sectors into memory
 */
struct request request[NR_REQUEST]; //請求項陣列
#define NR_REQUEST  32
...

struct request {
    int dev;  //裝置號,若為-1,則表示空閒
    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; //指向下一個struct request,構成單向連結串列
};

以上的程式碼要想完全理解,就牽扯到塊裝置驅動程式了。
這裡寫圖片描述

每個塊裝置的當前請求指標與請求項陣列中該裝置的請求項鍊表共同構成了該裝置的請求佇列。項與項之間利用欄位next指標形成連結串列。因此塊裝置項和相關的請求佇列形成如圖所示結構。請求項採用陣列加連結串列結構的主要原因是為了滿足兩個目的:一是利用請求項的陣列結構在搜尋空閒請求塊時可以進行迴圈操作,搜尋訪問時間複雜度為常數,因此程式可以編制得很簡潔;二是為滿足電梯演算法 (Elevator Algorithm) 插入請求項操作,因此也需要採用連結串列結構。圖中畫出了硬碟裝置當前具有4個請求項,軟盤裝置具有1個請求項,而虛擬盤裝置目前暫時沒有讀寫請求項。

對於一個當前空閒的塊裝置,當 ll_rw_block()函式為其建立第一個請求項時,會讓該裝置的當前請求項指標current_request直接指向剛建立的請求項,並且立刻呼叫對應裝置的請求項操作函式開始執行塊裝置讀寫操作。當一個塊裝置已經有幾個請求項組成的連結串列存在,ll_rw_block()就會利用電梯演算法,根據磁頭移動距離最小原則,把新建的請求項插入到連結串列適當的位置處。

以上內容摘自趙炯博士的《Linux核心完全剖析》。

雖然看完不甚理解,但至少明白blk_dev_init函式了。其實很簡單,就是把陣列的每一項的裝置號設為 -1,表示空閒,然後把 next 設為 NULL(因為空閒,所以沒有被插入連結串列)。

4.7 chr_dev_init函式

此函式實現為空。

void chr_dev_init(void)
{
}

4.8 tty_init函式

void tty_init(void)
{
    rs_init(); // 初始化序列中斷程式和序列介面1、2 
    con_init(); // 初始化控制檯終端
}

4.8.1 rs_init函式

void rs_init(void)
{
    set_intr_gate(0x24,rs1_interrupt); // 設定序列口1的中斷門向量
    set_intr_gate(0x23,rs2_interrupt); // 設定序列口2的中斷門向量
    init(tty_table[1].read_q.data); // 初始化序列口1,引數是埠基地址
    init(tty_table[2].read_q.data); // 初始化序列口2,引數是埠基地址
    outb(inb_p(0x21)&0xE7,0x21);    // 允許8259A主片響應IRQ3、IRQ4中斷請求, 0x21 對應 8259A 主片的中斷遮蔽暫存器
}

4.8.1.1 設定中斷門

#define set_intr_gate(n,addr) \
    _set_gate(&idt[n],14,0,addr)

set_intr_gate(n,addr)的巨集展開是_set_gate(&idt[n],14,0,addr)
這裡寫圖片描述

上圖是中斷門描述符的格式,根據 [11:8] = 14,可以知道程式碼中的14表示中斷門。

巨集_set_gate(gate_addr,type,dpl,addr)用於設定門描述符。具體的分析參見 main函式解析(一)——Linux-0.11 學習筆記(五)

這個巨集根據引數中的

  • 門描述符型別 type
  • 描述符特權級 dpl
  • 中斷或異常處理過程的偏移地址地址 addr

設定位於地址 gate_addr 處的門描述符。

所以, set_intr_gate(n,addr)表示在元素idt[n]idt[]是中斷描述符表,其實是陣列,一共有 256 個表項,一個表項佔8位元組。)位置安裝一箇中斷處理過程的偏移地址地址為 addr 的、特權級為0的中斷門描述符。

Linux-0.11 系統

  • 把主片的中斷號設定為 0x20~0x27
  • 把從片的中斷號設定為 0x28~0x2f

根據上圖可以知道,序列口1的中斷號是0x24,序列口2的中斷號是0x23;

順便提一下:使用中斷門(interrupt gate) 來構造中斷呼叫機制的,當 processor 進入 interrupt handler 執行前, 會將 eflags 的值壓入棧中儲存並且會清除 eflags.IF 標誌位,這意味著進入中斷後不再響應其他的可遮蔽中斷。

4.8.1.2 串列埠的初始化

static void init(int port) // port 是埠基地址
{
    ...
}

串列埠初始化,說白了就是配置一些暫存器,使串列埠可以收發資料。

說到這個串列埠,內容還挺多,不瞭解的一定要參考我的博文 PC 機 UART(NS8250)詳解

init(tty_table[1].read_q.data);傳入的引數是tty_table[1].read_q.data,如果沒有猜錯的話,tty_table[1].read_q.data一定是埠的基地址。為了驗證猜測,我們找找和tty_table有關的定義。

果然,在kernel\chr_drv\tty_io.c中看到了以下程式碼:

struct tty_struct tty_table[] = {
    {
        ...
    },{
        ...
        {0x3f8,0,0,0,""},       /* rs 1 */
        {0x3f8,0,0,0,""},
        {0,0,0,0,""}
    },{
        ...
        {0x2f8,0,0,0,""},       /* rs 2 */
        {0x2f8,0,0,0,""},
        {0,0,0,0,""}
    }
};

0x3f80x2f8正是串列埠1和2的埠基地址。

static void init(int port) // port 是埠基地址
{
    outb_p(0x80,port+3);    /* set DLAB of line control reg */
    outb_p(0x30,port);      /* LS of divisor (48 -> 2400 bps */
    outb_p(0x00,port+1);    /* MS of divisor */
    outb_p(0x03,port+3);    /* reset DLAB */
    outb_p(0x0b,port+4);    /* set DTR,RTS, OUT_2 */
    outb_p(0x0d,port+1);    /* enable all intrs but writes */
    (void)inb(port);        /* read data port to reset things (?) */
}

目前遇到的讀寫埠的巨集有4個:

outb_p(value,port)—— 把value寫入埠port.

inb_p(port)——讀取埠port的值。

outb(value,port)—— 把value寫入埠port.

inb(port)——讀取埠port的值。

前2個帶延時,後2個不帶延時。

這4個巨集,具體的定義和分析可以參考我的博文 main函式解析(一)——Linux-0.11 學習筆記(五)

第3行:outb_p(0x80,port+3); 把0x80寫入埠(0x3f8+3=0x3fb,即 LCR 暫存器),也就是使 DLAB=1。

第4行:outb_p(0x30,port);寫波特率因子的低位元組為0x30

第5行:outb_p(0x00,port+1);寫波特率因子的高位元組為0x00

所以,波特率因子為 0x0030=48。

波特率和因子之間的關係是:

這裡寫圖片描述

公式變形一下:
這裡寫圖片描述

所以

=1.8432MHz4816=1843200768=2400

波特率 ={1.8432MHz \over 48*16}={1843200\over 768}=2400

第6行:outb_p(0x03,port+3);無奇偶校驗位、8 位資料位和 1 位停止位,同時復位 DLAB;

第7行:outb_p(0x0b,port+4);若要讓 UART 的中斷請求訊號能夠送到 8259A 中斷控制器,就需要把MODEM 控制暫存器 MCR 的位3(OUT2) 置位。因為在PC 機中,該位控制著 INTRPT 引腳到 8259A 的電路。MCR的位 1 和位 0 分別用於控制 MODEM , 當這兩位置位時,UART的資料終端就緒引腳(DTR)和請求傳送引腳(RTS)輸出有效。

第8行:outb_p(0x0d,port+1);寫IER(Interrupt enable register,中斷允許暫存器 )

0x0d = 1101b
[3] = 1 允許modem狀態中斷;
[2] = 1 允許接收器線路狀態中斷;
[1] = 0 禁止傳送保持暫存器空中斷;
[0] = 1 允許接收到資料中斷;

第9行:(void)inb(port);讀接收快取暫存器 ,以進行復位??其實這句話我也不清楚是否必要,先在這裡留個疑問。

4.8.2 con_init()函式

con_init()函式(在檔案linux/kernel/console.c中)首先根據 setup.s 程式取得的系統硬體引數初始化幾個本函式專用的靜態全域性變數。然後根據顯示卡模式(單色還是彩色)和顯示卡型別(EGA/VGA、CGA、MDA 等) 分別設定顯示記憶體起始位置以及顯示索引暫存器和顯示數值暫存器的埠號。最後設定鍵盤中斷陷阱描述符並開啟對鍵盤中斷的遮蔽位。

顯示卡的歷史

磨刀不誤砍柴工,在理解這個函式之前,不妨先了解一下顯示卡的歷史。

顯示卡的前身

MDA

最早的顯示卡稱為顯示介面卡,在“黑底白字”的DOS年代,對顯示的要求是極低的。最早的顯示型別是 MDA(Monochrome Display Adapter),只能區別出黑白兩色。早期的8080、8088,一直到80286都是使用這種型別的顯示介面卡。它的功能極為簡單,一般整合 16KB 視訊記憶體,是不為人關注的電腦配件。

CGA
到了286時,PC上出現了一些和圖形相關的軟體,因此出現了一種四色介面卡,只能識別三原色和黑白。由於這是第一種彩色的顯示介面卡,所以稱為 CGA(Color Graphics Adaptor,彩色圖形介面卡)。CGA 時代對顯示卡的要求已經大幅度提高,但是當時的製作工藝仍然遠遠高於顯示卡晶片的需求,因此 CGA 顯示介面卡依舊被整合在主機板上,以一塊單晶片的方式來實現,所以“顯示卡”尚未誕生。

EGA
CGA 的解析度太低,於是又有了 EGA(Enhanced Graphics Adapter,增強圖形介面卡)。在顯示效能方面(顏色和解析度),EGA 介於 CGA 和 VGA 之間,可以在高達640x350的分辯率下達到16色。

顯示卡的誕生與換代

以上MDA、CGA、EGA 三種標準都是以 TTL 數字訊號輸出,而之後的 VGA 標準採用模擬訊號輸出,因而其彩色顯示能力大大加強,原則上可以顯示無窮多的顏色。VGA 最初代表解析度,在個人電腦的啟蒙時代,能夠輸出 VGA(640×480)這樣的解析度並不是一件易事,VGA 標準的出現對顯示輸出裝置首次提出了較高的要求,於是催生了 VGA Card,顯示卡正式誕生!

第一代顯示卡:VGA Card,支援256色顯示,1988年

第二代顯示卡:Graphics Card,支援Windows圖形加速,1991年

第三代顯示卡:Video Card,支援視訊加速,1994年

第四代顯示卡:3D Accelerator Card,支援3D加速,1994年

第五代顯示卡:GPU圖形處理器,支援硬體 T&L(Transform and Lighting,多邊形轉換與光源處理),1999年

現代和未來顯示卡:GPGPU(General-purpose computing on graphics processing units,簡稱 GPGPU 或 GP²U)通用計算圖形處理器,支援幾何著色、物理加速、高清解碼、科學計算……

和顯示有關的靜態全域性變數的初始化

void con_init(void)
{
    register unsigned char a;
    // 定義暫存器變數 a,該變數將被儲存在一個暫存器中,以便於高效訪問和操作。若想指定存放的暫存器(如 eax) ,則可寫成 register unsigned char a asm("ax");

    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; //每屏25行
    video_page = ORIG_VIDEO_PAGE; // 我覺得有點問題
    video_erase_char = 0x0720; // 擦除字元(0x20是空格,0x07是屬性)
    ...

在檔案linux/kernel/console.c中有巨集定義:

#define ORIG_X             (*(unsigned char *)0x90000)
#define ORIG_Y             (*(unsigned char *)0x90001)
#define ORIG_VIDEO_PAGE     (*(unsigned short *)0x90004)
#define ORIG_VIDEO_MODE     ((*(unsigned short *)0x90006) & 0xff)
#define ORIG_VIDEO_COLS     (((*(unsigned short *)0x90006) & 0xff00) >> 8)
#define ORIG_VIDEO_LINES    (25)
#define ORIG_VIDEO_EGA_AX   (*(unsigned short *)0x90008)
#define ORIG_VIDEO_EGA_BX   (*(unsigned short *)0x9000a)
#define ORIG_VIDEO_EGA_CX   (*(unsigned short *)0x9000c)

右邊的地址,其實對應 setup.s 中取得的系統硬體引數的存放位置。如果忘了,可以參考我的博文 setup.s 分析—— Linux-0.11 學習筆記(二)

第9行:video_num_columns = (((*(unsigned short *)0x90006) & 0xff00) >> 8);

對比彙編程式碼,也就是把AH的值(字元列數)賦給全域性變數video_num_columns

`setup.s中相關程式碼如下

    ! 獲取顯示卡當前的顯示模式
    ! 呼叫 BIOS 中斷 0x10,功能號 ah = 0x0f
    ! 返回: ah=字元列數; al=顯示模式;bh=當前顯示頁。
    ! ds = 0x9000
    ! 0x90004(l個字)存放當前頁;0x90006(1位元組)存放顯示模式;0x90007(1位元組)存放字元列數。
    mov ah,#0x0f
    int 0x10
    mov [4],bx      ! bh = 當前顯示頁
    mov [6],ax      ! al = 顯示模式, ah = 字元列數(視窗寬度)

第10行:video_size_row = video_num_columns * 2;算出每行字元需使用的位元組數。

第12行:video_page = (*(unsigned short *)0x90004);

注意,video_pagestatic unsigned char型別。

根據彙編程式碼第8行,應該是0x90005處的一個位元組存放當前活動頁碼,所以我認為第12行應該改為:

video_page = (*(unsigned char *)0x90005);

在所有原始碼檔案中搜了一波發現video_page這個變數沒有被用到,好吧,暫且不管,接著往下看。

顯示模式

#define ORIG_VIDEO_MODE ((*(unsigned short *)0x90006) & 0xff)

根據上面彙編程式碼的第9行,ORIG_VIDEO_MODE中的值是顯示模式。

其取值對應的含義如下表。

AL Type Format Cell Colors Adapter Addr Monitor
0 text 40x25 8x8* 16/8 (shades) CGA,EGA b800 Composite
1 text 40x25 8x8* 16/8 CGA,EGA b800 Comp,RGB,Enh
2 text 80x25 8x8* 16/8 (shades) CGA,EGA b800 Composite
3 text 80x25 8x8* 16/8 CGA,EGA b800 Comp,RGB,Enh
4 graphic 320x200 8x8 4 CGA,EGA b800 Comp,RGB,Enh
5 graphic 320x200 8x8 4 (shades) CGA,EGA b800 Composite
6 graphic 640x200 8x8 2 CGA,EGA b800 Comp,RGB,Enh
7 text 80x25 9x14* 3 (b/w/bold) MDA,EGA b000 TTL Mono
8,9,0aH PCjr modes
0bH,0cH (reserved; internal to EGA BIOS)
0dH graphic 320x200 8x8 16 EGA,VGA a000 Enh,Anlg
0eH graphic 640x200 8x8 16 EGA,VGA a000 Enh,Anlg
0fH graphic 640x350 8x14 3 (b/w/bold) EGA,VGA a000 Enh,Anlg,Mono
10H graphic 640x350 8x14 4 or 16 EGA,VGA a000 Enh,Anlg
11H graphic 640x480 8x16 2 VGA a000 Anlg
12H graphic 640x480 8x16 16 VGA a000 Anlg
13H graphic 640x480 8x16 256 VGA a000 Anlg

Notes: With EGA, VGA, and PCjr you can add 80H to AL to initialize a video mode without clearing the screen.

*The character cell size for modes 0-3 and 7 varies, depending on the hardware. On modes 0-3: CGA=8x8, EGA=8x14, and VGA=9x16. For mode 7, MDPA and EGA=9x14, VGA=9x16, LCD=8x8.

    if (ORIG_VIDEO_MODE == 7) // 等於7說明是單色
    {
        video_mem_start = 0xb0000; // 設定記憶體起始地址
        video_port_reg = 0x3b4; // 設定索引暫存器埠
        video_port_val = 0x3b5; // 設定資料暫存器埠

        // 注意,這裡使用了 BL 在呼叫中斷 int 0x10 前後是否被改變的方法來判斷卡的型別。

        if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10)
        {  // BL之前設定的值是0x10,呼叫中斷後發生改變,則說明是EGA
//#define VIDEO_TYPE_EGAM   0x20 /* EGA/VGA in Monochrome Mode*/  
            video_type = VIDEO_TYPE_EGAM; // 設定顯示型別
            video_mem_end = 0xb8000;      // 設定顯示記憶體末端地址
            display_desc = "EGAm";        // 設定顯示描述字串
        }
        else // 沒有改變說明是MDA
        {
            video_type = VIDEO_TYPE_MDA;
            video_mem_end   = 0xb2000;
            display_desc = "*MDA";
        }
    }
    else   // 說明是彩色 
    {
        video_mem_start = 0xb8000;  // 設定記憶體起始地址
        video_port_reg  = 0x3d4;    // 設定索引暫存器埠
        video_port_val  = 0x3d5;    // 設定資料暫存器埠
        if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10)
        {
            video_type = VIDEO_TYPE_EGAC; // 說明是EGA或者VGA顯示卡
            video_mem_end = 0xbc000;   // 設定顯示記憶體末端地址
            display_desc = "EGAc";     // EGA彩色
        }
        else
        {
            video_type = VIDEO_TYPE_CGA; // 設定顯示型別為CGA
            video_mem_end = 0xba000;
            display_desc = "*CGA";
        }
    }

setup.s中相關程式碼是:

    ! 檢查顯示方式(EGA/VGA)並獲取引數。
    ! 呼叫 BIOS 中斷 0x10,功能號: ah = 0xl2,子功能號: bl = 0xl0
    ! 返回:bh=顯示狀態。 0x00-彩色模式,I/O 埠=0x3dX
    !                   0x01-單色模式,I/O 埠=0x3bX
    ! bl = 安裝的顯示記憶體。0x00 - 64k
    !                   0x01 - 128k
    !                   0x02 - 192k
    !                   0x03 - 256k
    ! cx = 顯示卡特性引數。
    !
    mov ah,#0x12   ! 功能號
    mov bl,#0x10   ! 子功能號
    int 0x10
    mov [8],ax      ! 我也不知道這個是什麼(╯︵╰)
    mov [10],bx     ! bh=顯示狀態(單色模式/彩色模式),bl=已安裝的視訊記憶體大小
    mov [12],cx     ! ch=特性聯結器位元位資訊,cl=視訊開關設定資訊

注意,C程式碼使用了 BL 在呼叫中斷 int 0x10 前後是否被改變的方法來判斷卡的型別。BL在呼叫中斷之前被賦值為 0x10 ,在呼叫中斷後,其值可能不變(CGA 或 MDA),也可能變為0~3中的一個(EGA 或 VGA)。

在螢幕右上角顯示顯示卡型別

下面的程式碼作用是在螢幕右上角顯示描述字串。採用的方法是直接將字串寫到顯示記憶體的相應位置處。首先將顯示指標display_ptr指到螢幕第1行最右端起第4個字元處(每個字元需 2 個位元組,因此減 8 ) ,然後迴圈複製字串的字元。

    display_ptr = ((char *)video_mem_start) + video_size_row - 8;
    while (*display_desc) // 迴圈能停止是因為字串末尾的'\0'
    {
        *display_ptr++ = *display_desc++; // 複製字元
        display_ptr++;  // 跳過屬性位元組
    }

在我的實驗環境除錯,截圖如下:
這裡寫圖片描述

和滾屏有關的變數

初始化用於滾屏的變數(主要用於EGA/VGA):

/* 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; // 最底端行號
#define ORIG_X          (*(unsigned char *)0x90000) // 列號
#define ORIG_Y          (*(unsigned char *)0x90001) // 行號
...

// 初始化當前游標所在位置(x,y)和游標對應的視訊記憶體位置 pos
gotoxy(ORIG_X,ORIG_Y);

第1~2行對應的彙編程式碼(setup.s)是

    mov ax,#INITSEG  !INITSEG = 0x9000
    mov ds,ax        ! ds = 0x9000
    mov ah,#0x03     ! 功能號=3,獲取游標的位置
    xor bh,bh        ! bh = 頁號 = 0(輸入)
    int 0x10         ! 輸出: DH=行號,DL=列號
    mov [0],dx       ! 儲存游標的行號和列號到 0x90000,共佔2位元組.

gotoxy函式(kernel\chr_drv\console.c)的實現如下:

/* NOTE! gotoxy thinks x==video_num_columns is ok */
static inline void gotoxy(unsigned int new_x,unsigned int new_y)
{
    if (new_x > video_num_columns || new_y >= video_num_lines)
        return;
    x=new_x; // 記錄下當前游標所在的列
    y=new_y; // 記錄下當前游標所在的行
    pos=origin + y*video_size_row + (x<<1);// 記錄當前游標所在位置對應的視訊記憶體地址。我除錯時,video_size_row = 160
}

第4行:判斷引數是否合法。在我的實驗環境中,video_num_lines = 25,即new_y的取值是[0,24];video_num_columns = 80,即new_x的取值是[0,80],為什麼可以等於80呢?目前還不知道,我想作者這樣寫肯定有他的道理,後面多留個心。

和顯示有關的程式碼就暫時結束了。後面是關於鍵盤的。

允許鍵盤工作

    set_trap_gate(0x21,&keyboard_interrupt); //安裝陷阱門,鍵盤的中斷號是0x21, 對應8259A主片的 IRQ1 
// 已經反覆強調, Linux-0.11 系統把主片的中斷號設定為 `0x20~0x27`;
// 把從片的中斷號設定為 `0x28~0x2f`。
    outb_p(inb_p(0x21)&0xfd,0x21); // 允許鍵盤中斷
    a=inb_p(0x61); //  讀取鍵盤埠 0x61(8255A埠PB)到 a(之前定義的暫存器變數)
    outb_p(a|0x80,0x61); // b7置位,禁止鍵盤工作
    outb(a,0x61);        // 再允許鍵盤工作,用以復位鍵盤

第4行:0x21是 8259A 主片命令字OCW1的埠地址,(注意:不是第1行的那個0x21)用於對其中斷遮蔽暫存器 IMR 進行讀/寫操作。

寫到這裡,儘管篇幅較長,可是才分析了 2 個函式。

    blk_dev_init();
    chr_dev_init(); //實現為空
    tty_init();

數了數,距main函式結束,還有10多個函式呢……好了,今天就到這裡,明日繼續精進。


參考資料

[0]《Linux核心完全剖析》(趙炯,機械工業出版社,2006)
[1] https://blog.csdn.net/mao0514/article/details/24730097

相關文章