linux-0.11初始化程序笔记

启动代码完成了 linux 系统的启动,并获取了大量的硬件信息,供操作系统完成内核初始化的工作。

在 init/ 目录下有一个 main.c 文件,完成了内核初始化的所有工作。这个文件做的工作如下图所示:

一、计算内存边界值

    ROOT_DEV = ORIG_ROOT_DEV;  //根文件系统所在的设备号
    drive_info = DRIVE_INFO;       //硬盘参数表基址,存放着之前 setup.s 获取的硬盘参数信息

 //计算内存边界

   memory_end = (1<<20) + (EXT_MEM_K<<10);  //EXT_MEM_K 为 1M 以后的扩展内存大小,同样是通过 setup.s 获得的
       memory_end &= 0xfffff000;
    if (memory_end > 16*1024*1024)
        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;

 上述代码计算出了三个内存边界值,分别是 main_memory_start、memory_end和buffer_memory_end

    又因为 main_memory_start 等于 buffer_memory_end ,所以其实只计算了两个值,依靠这三个边界值,划分出了两个区域,分别是缓冲区和主内存:

 如果定义了内存虚拟盘,还需要初始化虚拟盘

二、初始化

1.主内存初始化

void main(void) {
    ...
    mem_init(main_memory_start, memory_end);  //输入参数,之前计算出的主内存起始地址和结束地址
    ...
}

 

#define LOW_MEM 0x100000            //内存低端 1M,最低的 1M 主要给内核用。
#define PAGING_MEMORY (15*1024*1024)       //于是分页内存还剩 15M
#define PAGING_PAGES (PAGING_MEMORY>>12)   //4 个字节为一页
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)    //将指定的内存地址映射为页号
#define USED 100                  //页面被占用的标志

static long HIGH_MEMORY = 0;
static unsigned char mem_map[PAGING_PAGES] = { 0, };  //默认所有内存页面都没有被占用

// start_mem = 2 * 1024 * 1024
// end_mem = 8 * 1024 * 1024
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;
}

但是需要置空的页面个数是置占用的个数的 15 倍,如果先把所有内存页面置空,再令缓冲区部分为占用状态,程序效率会更高

2.中断初始化

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);
    for (i=17;i<48;i++)
        set_trap_gate(i,&reserved);
    set_trap_gate(45,&irq13);
    set_trap_gate(39,&parallel_interrupt);
}

这段初始化用到了两个函数,分别是:set_trap_gate、set_system_gate,而这两个函数都来自于 _set_gate 函数:

#define set_trap_gate(n,addr) \
    _set_gate(&idt[n],15,0,addr)

#define set_system_gate(n,addr) \
    _set_gate(&idt[n],15,3,addr)

其中 n 为中断号,addr 为中断程序偏移地址。

_set_gate 的实现如下:

这段代码用 gcc 内联汇编实现。

gate_addr 为描述符地址,通过中断号和 idt 数组选择。

type 为描述符类型阈值

dpl 描述符特权层值

addr 为偏移地址

#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
    "movw %0,%%dx\n\t" \
    "movl %%eax,%1\n\t" \
    "movl %%edx,%2" \
    : \
    : "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \  //%0,dpl 和 type 通过某种方式组合成类型标志字
    "o" (*((char *) (gate_addr))), \          //%1,描述符地址低 4 个字节
    "o" (*(4+(char *) (gate_addr))), \        //%2,描述符地址高 4 个字节
    "d" ((char *) (addr)),              //%3,偏移地址

    "a" (0x00080000))              //%4,a 代表 eax 寄存器(高字中含有段选择符)

 

 关于 idt 表相关寄存器还没有进一步认识,无法进一步分析,总之这段代码将中断号与中断处理程序关联起来,当中断产生时,CPU 根据中断号选择对应的中断处理程序区执行。

3.块设备初始化

读硬盘需要有块设备驱动程序,块设备的初始化只需要把 request 结构体中的 dev 成员赋为 -1,把 next 成员赋为 NULL 即可:

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

关于 request 结构体的介绍如下:

struct request {
    int dev;        /* -1 if no request */
    int cmd;        /* READ or WRITE */
    int errors;          //操作时产生的错误次数
    unsigned long sector;    //起始扇区(1 块 = 2 扇区)
    unsigned long nr_sectors;  //读/写扇区数
    char * buffer;        //数据缓冲区
    struct task_struct * waiting;  //表示发起请求的进程
    struct buffer_head * bh;    //缓冲区头指针
    struct request * next;     //下一请求项
};

4.控制台初始化

由于字符设备初始化在这一版代码中没有实现,所以直接跳到控制台初始化。这个初始化完成后,就可以在屏幕上显示东西了!

void tty_init(void)
{
    rs_init();    //初始化串行中断和串行接口 1 和 2
    con_init();   //初始化控制台终端
}

跳过串行接口的初始化,据说现在不怎么用了,如果最新一期代码还有的话,在阅读最新代码的时候再深究。

首先是一段很长的 if-else 语句,通过宏 ORIG_VIDEO_MODE 来判断显示模式,在启动代码中,将显示模式存入地址为 0x90006 的内存区。

 

  选择完显示模式后,接下来就要将字符显示在屏幕上。内存中有一部分区域是和显存映射的,也就是说,将数据写在下图的内存区域中,相当于将数据写入了显存中,而写入显存的数据是可以直接显示在屏幕上的

 

 

 

   初始化代码第一部分是获取显示模式的相关信息,是从内存中读取的:

    video_num_columns = ORIG_VIDEO_COLS;    //显示器显示字符的列数
    video_size_row = video_num_columns * 2;     //每行使用的字节数
    video_num_lines = ORIG_VIDEO_LINES;      //显示器显示的字符行数
    video_page = ORIG_VIDEO_PAGE;          //当前显示页面(从内存 0x90004 地址处获取)
    video_erase_char = 0x0720;            //擦除字符(0x20 显示字符,0x07 是属性)

   第二部分是初始化一些显存相关的参数,分别是 video_mem_start (显存起始地址)、video_port_reg(显示索引寄存器端口)、video_port_val(数据寄存器端口)、video_type (显示类型)、video_mem_end(显存末端地址)。

  第三部分是初始化一些滚屏的变量:

    origin  = video_mem_start;                  //滚屏起始显存地址
    scr_end = video_mem_start + video_num_lines * video_size_row;  //滚屏结束的显存地址
    top = 0;                           //顶部行号
    bottom  = video_num_lines;                  //底部行号

  第四部分定位光标并开启键盘中断。

5.时间初始化

  代码如下所示,需要关注的只有两个函数,分别是 CMOS_READ 和 BCD_TO_BIN:

   CMOS_READ 的实现为将 0x80 | addr 的值写入到 0x70 端口,再从 0x71 端口读数据,这个版本的汇编都有些古老,就不细看了:

  BCD_TO_BIN 就是将获取到的时间数据从 BCD 转成二进制。

  kernel_mktime 函数通过读取到的时间,返回当前时间减去 1970 年的秒数,但是它没有对年份做判断,万一读取的数据出现问题小于 70 ,year 会变成负数

6.进程调度初始化

   sched_init 函数负责初始化进程调度需要用到的数据结构,首先初始化 TSSLDT,这两个结构体位于 GDT 中。其中,TSS 叫做任务状态段,用于保存和恢复进程上下文的,也就是保存各个寄存器的信息,这样在进程切换的时候,才能做到保存和恢复上下文,继续执行;LDT 叫做局部描述符表,内核代码用 GDT 中的代码和数据;进程代码用 LDT 中的代码和数据:

   接下来是一个循环,这个循环做了两件事,一个是为 task_struct 结构体数组赋初值,这个数组里每一个结构体代表每一个进程的信息;把 GDT 剩下的位置填充 0 ,也就是把所有的 TSS 和 LDT 填充 0 ,以后每创建一个进程,就会为一组 TSS 和 LDT 赋值。一开始初始化的 TSS 和 LDT 对应进程 0,也就是正在执行的代码

  接下来为 trldt 寄存器赋初值,这两个寄存器分别指向 TSS 和 LDT 在内存中的位置:

   最后,设置了两个重要中断,分别是时钟中断系统调用中断

7.缓冲区初始化

  buffer_init 函数负责初始化缓冲区,传入参数 buffer_memory_end 是之前计算内存边界值时计算出的。这个函数创建了一个结构体指针和一个空指针,并初始化结构体指针指向的结构体。循环体所做的事情就是从缓冲区起始地址开始初始化每一块缓冲区(1024字节),直到初始化完所有缓冲区。buffer_end 是之前计算出的缓冲区末尾地址,start_buffer 是缓冲区的起始地址,是由链接器计算出的内核代码的末尾地址,用变量 end 存储。初始化就是为 buffer_head 结构体赋 0 值,为 b_prev_free 和 b_next_free 前后空闲指针赋上正确的地址。

   h 是一个双向链表,通过以下三行代码,创建了一个双向环状链表,free_list 指向 h 的第一个结点:

  至此,缓存头的结构已经初始化完成,但如何知道缓冲区中已经有了要读取的块设备中的数据呢,虽然可以遍历双向链表,但是效率太低,所以选择用一个哈希表的结构方便快速查找,于是创建一个数组 hash_table 并初始化:

  

  哈希表加双向链表可以实现 LRU 算法,在缓冲区的管理中起到了重要作用。

8.硬盘初始化

   最后两个初始化分别是硬盘初始化和软盘初始化,但是软盘初始化几乎被淘汰了,所以只看硬盘初始化即可。blk_dev 是操作系统用来管理硬盘的数据结构体。

void hd_init(void)
{
    blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;  //request_fn 函数指针指向 DEVICE_REQUEST 函数,宏函数是 do_hd_request 函数,这是一个请求硬盘设备的函数。
    set_intr_gate(0x2E,&hd_interrupt);             //开中断,0x2E 是硬盘中断号。
    //以下两行是往端口里写数据,配置硬盘功能。
 outb_p(inb_p(0x21)&0xfb,0x21);
    outb(inb_p(0xA1)&0xbf,0xA1);               
}
posted @ 2022-02-09 21:08  一只吃水饺的胡桃夹子  阅读(140)  评论(0编辑  收藏  举报