linux0.11源码 - 初始化


main

  1. 参数的取值和计算:包括根设备ROOT_DEV,以及之前在汇编语言中获取的各个设备的参数信息drive_info,以及通过计算得到的内存边界。

    void main(void)        /* This really IS void, no error here. */
    {                      /* The startup routine assumes (well, ...) this */
         ROOT_DEV = ORIG_ROOT_DEV;
         drive_info = DRIVE_INFO;        // 复制0x90080处的硬盘参数
         memory_end = (1<<20) + (EXT_MEM_K<<10);     // 内存大小=1Mb + 扩展内存(k)*1024 byte
         memory_end &= 0xfffff000;                   // 忽略不到4kb(1页)的内存数
         if (memory_end > 16*1024*1024)              // 内存超过16Mb,则按16Mb计
             memory_end = 16*1024*1024;
         if (memory_end > 12*1024*1024)              // 如果内存>12Mb,则设置缓冲区末端=4Mb 
             buffer_memory_end = 4*1024*1024;
         else if (memory_end > 6*1024*1024)          // 否则若内存>6Mb,则设置缓冲区末端=2Mb
             buffer_memory_end = 2*1024*1024;
         else
             buffer_memory_end = 1*1024*1024;        // 否则设置缓冲区末端=1Mb
         main_memory_start = buffer_memory_end;
      
         main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
         mem_init(main_memory_start,memory_end); // 主内存区初始化。mm/memory.c
         ......
    }
  2. 各种初始化init操作

    void main(void)    
    {    
         ......        
         mem_init(main_memory_start,memory_end); // 主内存区初始化。mm/memory.c
         trap_init();                            // 陷阱门(硬件中断向量)初始化,kernel/traps.c
         blk_dev_init();                         // 块设备初始化,kernel/blk_drv/ll_rw_blk.c
         chr_dev_init();                         // 字符设备初始化, kernel/chr_drv/tty_io.c
         tty_init();                             // tty初始化, kernel/chr_drv/tty_io.c
         time_init();                            // 设置开机启动时间 startup_time
         sched_init();                           // 调度程序初始化(加载任务0的tr,ldtr)(kernel/sched.c)
     
         buffer_init(buffer_memory_end);         // 缓冲管理初始化,建内存链表等。(fs/buffer.c)
         hd_init();                              // 硬盘初始化,kernel/blk_drv/hd.c
         floppy_init();                          // 软驱初始化,kernel/blk_drv/floppy.c
         sti();                                  // 所有初始化工作都做完了,开启中断
         ......
    }
    
  3. 切换到用户态模式

    void main(void)    
    {        
         ......
         move_to_user_mode();                    // 移到用户模式下执行
         if (!fork()) {        /* we count on this going ok */
             init();                             // 在新建的子进程(任务1)中执行。
         }
         ......
    }
    • init函数里会创建出一个进程,设置终端的标准IO,并且再创建出一个执行shell程序的进程用来接受用户的命令。
  4. 死循环:如果没有任何任务可以运行,操作系统会一直陷入这个死循环

    void main(void)    {
         ......
     /*
      *   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转换成可中断等待状态,再执行调度函数。但是调度函数只要发现系统中
         // 没有其他任务可以运行是就会切换到任务0,而不依赖于任务0的状态。
         for(;;) pause();
    }

mem_init

// 内存低端(1MB)
#define LOW_MEM 0x100000
// 分页内存15 MB,主内存区最多15M.
#define PAGING_MEMORY (15*1024*1024)
// 分页后的物理内存页面数(3840)
#define PAGING_PAGES (PAGING_MEMORY>>12)
// 指定地址映射为页号
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)
// 页面被占用标志.
#define USED 100


void mem_init(long start_mem, long end_mem)
{
    int i;

    // 首先将1MB到16MB范围内所有内存页面对应的内存映射字节数组项置为已占用状态,
    // 即各项字节全部设置成USED(100)。PAGING_PAGES被定义为(PAGING_MEMORY>>12),
    // 即1MB以上所有物理内存分页后的内存页面数(15MB/4KB = 3840).
    HIGH_MEMORY = end_mem;                  // 设置内存最高端(16MB)
    for (i=0 ; i<PAGING_PAGES ; i++)
        mem_map[i] = USED;
    // 然后计算主内存区起始内存start_mem处页面对应内存映射字节数组中项号i和主内存区页面数。
    // 此时mem_map[]数组的第i项正对应主内存区中第1个页面。最后将主内存区中页面对应的数组项
    // 清零(表示空闲)。对于具有16MB物理内存的系统,mem_map[]中对应4MB-16MB主内存区的项被清零。
    i = MAP_NR(start_mem);      // 主内存区其实位置处页面号
    end_mem -= start_mem;
    end_mem >>= 12;             // 主内存区中的总页面数
    while (end_mem-->0)
        mem_map[i++]=0;         // 主内存区页面对应字节值清零
}
  • 4K内存通常叫做1页内存,而这种管理方式叫分页管理,就是把内存分成一页一页(4K)的单位去管理。
  • 1M以下的内存这个数组干脆没有记录,这里的内存是无需管理的,或者换个说法是无权管理的,也就是没有权利申请和释放,因为这个区域是内核代码所在的地方,不能被污染。
  • 1M到2M这个区间是缓冲区,2M是缓冲区的末端,缓冲区的开始在哪里之后再说,这些地方不是主内存区域,因此直接标记为USED,产生的效果就是无法再被分配了。
  • 2M以上的空间是主内存区域,而主内存目前没有任何程序申请,所以初始化时统统都是零,未来等着应用程序去申请和释放这里的内存资源。

trap_init

#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))), \
    "o" (*((char *) (gate_addr))), \
    "o" (*(4+(char *) (gate_addr))), \
    "d" ((char *) (addr)),"a" (0x00080000))

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

#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)
void trap_init(void)
{
    int i;
    // 这两个函数均是嵌入式汇编宏程序(include/asm/system.h中)
    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);
    // 下面把int17-47的陷阱门先均设置为reserved,以后各硬件初始化时会重新设置自己的陷阱门。
    for (i=17;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从芯片的IRQ3中断请求。
    set_trap_gate(39,&parallel_interrupt); // 设置并行口1的中断0x27陷阱门的描述符。
}
  • 各个中断在此文件上方定义。
  • 代码在中断描述符表中插入了一个中断描述符。
  • 17到48号中断都批量设置为了reserved函数,这是暂时的,后面各个硬件初始化时要重新设置好这些中断,把暂时的这个给覆盖掉。
  • 代码执行后,来一个中断,CPU 根据其中断号,就能到中断描述符表idt中找到对应的中断处理程序。

blk_dev_init

#define NR_REQUEST    32

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

......

void blk_dev_init(void)
{
    int i;

    for (i=0 ; i<NR_REQUEST ; i++) {
        request[i].dev = -1;
        request[i].next = NULL;
    }
}
  • blk_dev_init:将request这个数组的前32个元素的两个变量dev和next附上没有任何作用时的初始化值。
  • request结构体:

    • dev 表示设备号,-1 就表示空闲。
    • cmd 表示命令,其实就是 READ 还是 WRITE,也就表示本次操作是读还是写。
    • errors 表示操作时产生的错误次数。
    • sector 表示起始扇区。
    • nr_sectors 表示扇区数。
    • buffer 表示数据缓冲区,也就是读盘之后的数据放在内存中的什么位置。
    • waiting 是个 task_struct 结构,这可以表示一个进程,也就表示是哪个进程发起了这个请求。
    • bh 是缓冲区头指针,request会与缓冲区挂钩的。
    • next 指向了下一个请求项。

tty_init


void tty_init(void)
{
    // 初始化串行中断程序和串行接口1和2(serial.c)
    rs_init();
    con_init();     // 初始化控制台终端(console.c文件中)
}

rs_init

// 串口中断的开启,以及设置对应的中断处理程序
void rs_init(void)
{
    set_intr_gate(0x24,rs1_interrupt);      // 设置串行口1的中断门向量(IRQ4信号)
    set_intr_gate(0x23,rs2_interrupt);      // 设置串行口2的中断门向量(IRQ3信号)
    init(tty_table[1].read_q.data);         // 初始化串行口1(.data是端口基地址)
    init(tty_table[2].read_q.data);         // 初始化串行口2
    outb(inb_p(0x21)&0xE7,0x21);            // 允许主8259A响应IRQ3、IRQ4中断请求
}

con_init

// 初始化控制台终端(console.c文件中)
void con_init(void)
{
    // 寄存器变量a为了高效的访问和操作。
    // 若想指定存放的寄存器(如eax),则可以写成:
    // register unsigned char a asm("ax");。
    register unsigned char a;
    char *display_desc = "????";
    char *display_ptr;

    // 首先根据setup.s程序取得系统硬件参数初始化几个本函数专用的静态全局变量。
    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;              // 擦除字符(0x20是字符,0x07属性)
    
    // 根据显示模式是单色还是彩色分别设置所使用的显示内存起始位置以及显示寄存器
    // 索引端口号和显示寄存器数据端口号。如果原始显示模式等于7,则表示是单色显示器。
    if (ORIG_VIDEO_MODE == 7)            /* Is this a monochrome display? */
    {
        video_mem_start = 0xb0000;      // 设置单显映象内存起始地址
        video_port_reg = 0x3b4;         // 设置单显索引寄存器端口
        video_port_val = 0x3b5;         // 设置单显数据寄存器端口
        // 接着我们根据BIOS中断int 0x10 功能0x12获得的显示模式信息,判断显示卡是
        // 单色显示卡还是彩色显示卡。若使用上述中断功能所得到的BX寄存器返回值不等于
        // 0x10,则说明是EGA卡。因此初始显示类型为EGA单色。虽然EGA卡上有较多显示内存,
        // 但在单色方式下最多只能利用地址范围在 0xb0000-0xb8000 之间的显示内存。
        // 然后置显示器描述字符串为 'EGAm'. 并会在系统初始化期间显示器描述字符串将
        // 显示在屏幕的右上角。
        // 注意,这里使用了 bx 在调用中断 int 0x10 前后是否被改变的方法来判断卡的类型。
        // 若BL在中断调用后值被改变,表示显示卡支持 Ah=12h 功能调用,是EGA或后推出来的
        // VGA等类型的显示卡。若中断调用返回值未变,表示显示卡不支持这个功能,则说明
        // 是一般单色显示卡。
        if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10)
        {
            video_type = VIDEO_TYPE_EGAM;       // 设置显示类型(EGA单色)
            video_mem_end = 0xb8000;            // 设置显示内存末端地址
            display_desc = "EGAm";              // 设置显示描述字符串
        }
        else    // 如果 BX 寄存器的值等于 0x10,则说明是单色显示卡MDA。
        {
            video_type = VIDEO_TYPE_MDA;        // 设置显示类型(MDA单色)
            video_mem_end    = 0xb2000;          // 设置显示内存末端地址
            display_desc = "*MDA";              // 设置显示描述字符串
        }
    }
    // 如果显示模式不为7,说明是彩色显示卡。此时文本方式下所用的显示内存起始地址为0xb8000;
    // 显示控制索引寄存器端口地址为 0x3d4;数据寄存器端口地址为 0x3d5。
    else                                /* If not, it is color. */
    {
        video_mem_start = 0xb8000;              // 显示内存起始地址
        video_port_reg    = 0x3d4;                // 设置彩色显示索引寄存器端口
        video_port_val    = 0x3d5;                // 设置彩色显示数据寄存器端口
        // 再判断显示卡类别。如果 BX 不等于 0x10,则说明是EGA/VGA 显示卡。此时实际上我们
        // 可以使用32KB显示内存(0xb8000 -- 0xc0000),但该程序只使用了其中16KB显示内存。
        if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10)
        {
            video_type = VIDEO_TYPE_EGAC;       // 设置显示类型(EGA彩色)
            video_mem_end = 0xbc000;            // 设置显示内存末端地址
            display_desc = "EGAc";              // 设置显示描述字符串
        }
        else    // 如果 BX 寄存器的值等于 0x10,则说明是CGA显示卡,只使用8KB显示内存
        {
            video_type = VIDEO_TYPE_CGA;        // 设置显示类型(CGA彩色)
            video_mem_end = 0xba000;            // 设置显示内存末端地址
            display_desc = "*CGA";              // 设置显示描述字符串
        }
    }

    /* Let the user known what kind of display driver we are using */

    // 然后我们在屏幕的右上角显示描述字符串。采用的方法是直接将字符串写到显示内存
    // 相应位置处。首先将显示指针display_ptr 指到屏幕第1行右端差4个字符处(每个字符
    // 需2个字节,因此减8),然后循环复制字符串的字符,并且每复制1个字符都空开1个属性字节。
    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;              // 最底行号

    // 最后初始化当前光标所在位置和光标对应的内存位置pos,并设置键盘中断0x21陷阱门
    // 描述符,&keyboard_interrupt是键盘中断处理过程地址。取消8259A中对键盘中断的
    // 屏蔽,允许响应键盘发出的IRQ1请求信号。最后复位键盘控制器以允许键盘开始正常工作。
    gotoxy(ORIG_X,ORIG_Y);
    set_trap_gate(0x21,&keyboard_interrupt);
    outb_p(inb_p(0x21)&0xfd,0x21);          // 取消对键盘中断的屏蔽,允许IRQ1。
    a=inb_p(0x61);                          // 读取键盘端口0x61(8255A端口PB)
    outb_p(a|0x80,0x61);                    // 设置禁止键盘工作(位7置位)
    outb(a,0x61);                           // 再允许键盘工作,用以复位键盘
}
  • 此段代码是为了应对不同的显示模式,来分配不同的变量值。

    • 获取 0x90006 地址处的数据,就是获取显示模式等相关信息。

      • 设置显示模式所需的信息内存0x90000处开始,是执行setup.s后保存到此处的。
      • 关于显示模式:在内存中存在一部分区域,是和显存映射的。向这些内存区域中写数据,相当于写在了显存中,而向显存中写数据,相当于在屏幕上输出文本。假设0xB8000与显存映射,如果我们写这一行汇编语句。mov [0xB8000],'h'或者mov [0xB8000],0x68,这段代码往内存中0xB8000这个位置写了一个值h,只要一写,屏幕上就会显示h。我们用两个字节标识一个字符:第一个表示字符的编码,第二个表示字符的颜色。
      • 倘若我们执行这些指令:mov [0xB8000],'h'mov [0xB8002],'e'mov [0xB8004],'l'mov [0xB8006],'l'mov [0xB8008],'o',则会在屏幕上打印出hello。
    • 设置显存映射的内存地址范围,假设是 CGA 类型的文本模式,所以映射的内存是从 0xB8000 到 0xBA000。
    • 设置一些滚动屏幕时需要的参数,定义顶行和底行是哪里,这里顶行就是第一行,底行就是最后一行。
    • 把光标定位到之前保存的光标位置处(取内存地址 0x90000 处的数据),然后设置并开启键盘中断。
  • gotoxy(ORIG_X,ORIG_Y);(倒数第六行):给x y pos三个参数附值。其中x表示光标在哪一列,y表示光标在哪一行,pos表示根据列号和行号计算出来的内存指针,也就是往这个pos指向的地址处写数据,就相当于往控制台的x列y行处写入字符。

    static inline void gotoxy(unsigned int new_x,unsigned int new_y)
    {
      // 首先检查参数的有效性。如果给定的光标列号超出显示器列数,
      // 或者光标行号不低于显示的最大行数,则退出。否则就更新当前
      // 光标变量和新光标位置对应在显示内存中位置pos.
      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);     // 1列用2个字节表示,x<<1.
    }
  • console.c的其他方法:基本原理同写字符。

    // 定位光标
    static inline void gotoxy(unsigned int new_x, unsigned int new_y){}
    // 滚屏,即内容向上滚动一行
    static void scrup(void){}
    // 光标同列位置下移一行
    static void lf(int currcons){}
    // 光标回到第一列
    static void cr(void){}
    ...
    // 删除一行
    static void delete_line(void){}

time_init

static void time_init(void)
{
    struct tm time;

    // CMOS的访问速度很慢,为了减少时间误差,在读取了下面循环中的所有数值后,如果此时
    // CMOS中秒值发生了变化,那么就重新读取所有值。这样内核就能把与CMOS时间误差控制在1秒之内。
    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--;                              // tm_mon中月份的范围是0-11
    startup_time = kernel_mktime(&time);        // 计算开机时间。kernel/mktime.c文件
}

读CMOS芯片中保存的时间

sched_init

  1. 向全局描述符表写两个结构:TSS和LDT,作为未来进程0的任务状态段和局部描述符表信息。

    void sched_init(void)
    {
     int i;
     struct desc_struct * p;                 // 描述符表结构指针
    
     // Linux系统开发之初,内核不成熟。内核代码会被经常修改。Linus怕自己无意中修改了
     // 这些关键性的数据结构,造成与POSIX标准的不兼容。这里加入下面这个判断语句并无
     // 必要,纯粹是为了提醒自己以及其他修改内核代码的人。
     if (sizeof(struct sigaction) != 16)         // sigaction 是存放有关信号状态的结构
         panic("Struct sigaction MUST be 16 bytes");
     // 在全局描述符表中设置初始任务(任务0)的任务状态段描述符和局部数据表描述符。
     // FIRST_TSS_ENTRY和FIRST_LDT_ENTRY的值分别是4和5,定义在include/linux/sched.h
     // 中;gdt是一个描述符表数组(include/linux/head.h),实际上对应程序head.s中
     // 全局描述符表基址(_gdt).因此gtd+FIRST_TSS_ENTRY即为gdt[FIRST_TSS_ENTRY](即为gdt[4]),
     // 也即gdt数组第4项的地址。
     set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
     set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
     ......
    }
    
    • set_tss_desc(gdt+4, &(init_task.task.tss));set_ldt_desc(gdt+5, &(init_task.task.ldt));:设置初始任务

      • TSS 叫任务状态段,用于保存和恢复进程的上下文,所谓上下文,其实就是各个寄存器的信息而已,这样进程切换的时候,才能做到保存和恢复上下文,继续执行。

        struct tss_struct{
        long back_link;
        long esp0;
        long ss0;
        long esp1;
        long ss1;
        long esp2;
        long ss2;
        long cr3;
        long eip;
        long eflags;
        long eax, ecx, edx, ebx;
        long esp;
        long ebp;
        long esi;
        long edi;
        long es;
        long cs;
        long ss;
        long ds;
        long fs;
        long gs;
        long ldt;
        long trace_bitmap;
        struct i387_struct i387;
        };
      • LDT 叫局部描述符表,是与GDT全局描述符表相对应的,内核态的代码用GDT里的数据段和代码段,而用户进程的代码用每个用户进程自己的LDT里得数据段和代码段。
  2. 初始化了结构为task_struct的数组,这里将来会存放所有进程的信息,并且我们给数组的第一个位置附上了init_task.init值,作为未来进程 0 的信息。

    struct task_struct * task[NR_TASKS] = {&(init_task.task), }; // 定义任务指针数组
    
    void sched_init(void)
    {
     // 清任务数组和描述符表项(注意 i=1 开始,所以初始任务的描述符还在)。描述符项结构
     // 定义在文件include/linux/head.h中。
     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++;
     }
    
    }
    • 给一个长度为 64,结构为 task_struct 的数组 task 附上初始值,同时给gdt剩下的位置填充上0,也就是把剩下留给TSS和LDT的描述符都先附上空值。以后每创建一个新进程,就会在后面添加一组 TSS 和 LDT 表示这个进程的任务状态段以及局部描述符表信息。
    • 其中task_struct结构体包含着每一个进程的信息。

      struct task_struct {
      /* these are hardcoded - don't touch */
      long state; /* -1 unrunnable, 0 runnable, >0 stopped */
      long counter;
      long priority;
      long signal;
      struct sigaction sigaction[32];
      long blocked; /* bitmap of masked signals */
       /* various fields */
      int exit_code;
      unsigned long start_code,end_code,end_data,brk,start_stack;
      long pid,father,pgrp,session,leader;
      unsigned short uid,euid,suid;
      unsigned short gid,egid,sgid;
      long alarm;
      long utime,stime,cutime,cstime,start_time;
      unsigned short used_math;
       /* file system info */
      int tty;  /* -1 if no tty, so it must be signed */
      unsigned short umask;
      struct m_inode * pwd;
      struct m_inode * root;
      struct m_inode * executable;
      unsigned long close_on_exec;
      struct file * filp[NR_OPEN];
       /* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
      struct desc_struct ldt[3];
       /* tss for this task */
      struct tss_struct tss;
      };
  3. 告诉CPU,TSS与LDT在内存中的位置

    #define ltr(n) __asm__("ltr %%ax"::"a" (_TSS(n)))
    #define lldt(n) __asm__("lldt %%ax"::"a" (_LDT(n)))
    
    void sched_init(void) {
     ...
     ltr(0);
     lldt(0);
     ...
    }
    • ltr给tr寄存器赋值,告诉CPU任务状态段TSS在内存的位置。
    • lldt给ldt寄存器赋值,告诉CPU局部描述符LDT在内存的位置。
    • CPU能通过tr寄存器找到当前进程的任务状态段信息,也就是上下文信息,以及通过ldt寄存器找到当前进程在用的局部描述符表信息。
  4. 设置了时钟中断0x20和系统调用0x80,一个作为进程调度的起点,一个作为用户程序调用操作系统功能的桥梁

    void sched_init(void)
    {
         ......
         // 下面代码用于初始化8253定时器。通道0,选择工作方式3,二进制计数方式。通道0的
         // 输出引脚接在中断控制主芯片的IRQ0上,它每10毫秒发出一个IRQ0请求。LATCH是初始
         // 定时计数值。
         outb_p(0x36,0x43);        /* binary, mode 3, LSB/MSB, ch 0 */
         outb_p(LATCH & 0xff , 0x40);    /* LSB */
         outb(LATCH >> 8 , 0x40);    /* MSB */
         // 设置时钟中断处理程序句柄(设置时钟中断门)。修改中断控制器屏蔽码,允许时钟中断。
         // 然后设置系统调用中断门。这两个设置中断描述符表IDT中描述符在宏定义在文件
         // include/asm/system.h中。
         set_intr_gate(0x20,&timer_interrupt);
         outb(inb_p(0x21)&~0x01,0x21);
         set_system_gate(0x80,&system_call);
    }
    • 四行代码开启可编程定时器的芯片,之后这个定时器变会持续的、以一定频率的向 CPU 发出中断信号。
    • 代码中设置的第一个中断是时钟中断,中断号为 0x20,中断处理程序为 timer_interrupt。那么每次定时器向 CPU 发出中断后,便会执行这个函数。
    • 第二个设置的中断叫系统调用 system_call,中断号是 0x80,这个中断又是个非常重要的中断,所有用户态程序想要调用内核提供的方法,都需要基于这个系统调用来进行。

buffer_init

extern int end;
extern void put_super(int);
extern void invalidate_inodes(int);

struct buffer_head * start_buffer = (struct buffer_head *) &end;
struct buffer_head * hash_table[NR_HASH];           // NR_HASH = 307项
static struct buffer_head * free_list;              // 空闲缓冲块链表头指针
static struct task_struct * buffer_wait = NULL;     // 等待空闲缓冲块而睡眠的任务队列


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

    // 首先根据参数提供的缓冲区高端位置确定实际缓冲区高端位置b。如果缓冲区高端等于1Mb,
    // 则因为从640KB - 1MB被显示内存和BIOS占用,所以实际可用缓冲区内存高端位置应该是
    // 640KB。否则缓冲区内存高端一定大于1MB。
    if (buffer_end == 1<<20)
        b = (void *) (640*1024);
    else
        b = (void *) buffer_end;
    // 这段代码用于初始化缓冲区,建立空闲缓冲区块循环链表,并获取系统中缓冲块数目。
    // 操作的过程是从缓冲区高端开始划分1KB大小的缓冲块,与此同时在缓冲区低端建立
    // 描述该缓冲区块的结构buffer_head,并将这些buffer_head组成双向链表。
    // h是指向缓冲头结构的指针,而h+1是指向内存地址连续的下一个缓冲头地址,也可以说
    // 是指向h缓冲头的末端外。为了保证有足够长度的内存来存储一个缓冲头结构,需要b所
    // 指向的内存块地址 >= h 缓冲头的末端,即要求 >= h+1.
    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;                   // 指向具有相同hash值的下一个缓冲头
        h->b_prev = NULL;                   // 指向具有相同hash值的前一个缓冲头
        h->b_data = (char *) b;             // 指向对应缓冲块数据块(1024字节)
        h->b_prev_free = h-1;               // 指向链表中前一项
        h->b_next_free = h+1;               // 指向连表中后一项
        h++;                                // h指向下一新缓冲头位置
        NR_BUFFERS++;                       // 缓冲区块数累加
        if (b == (void *) 0x100000)         // 若b递减到等于1MB,则跳过384KB
            b = (void *) 0xA0000;           // 让b指向地址0xA0000(640KB)处
    }
    h--;                                    // 让h指向最后一个有效缓冲块头
    free_list = start_buffer;               // 让空闲链表头指向头一个缓冲快
    free_list->b_prev_free = h;             // 链表头的b_prev_free指向前一项(即最后一项)。
    h->b_next_free = free_list;             // h的下一项指针指向第一项,形成一个环链
    // 最后初始化hash表,置表中所有指针为NULL。
    for (i=0;i<NR_HASH;i++)
        hash_table[i]=NULL;
}    
  • buffer_head结构的h,代表缓冲头,其指针值start_buffer,地址为内核代码末端地址end,也就是缓冲区开头。
  • b,代表缓冲块,指针值是buffer_end,地址为2M,就是缓冲区结尾。
  • while ((b -= 1024) >= ((void *) (h+1))):循环初始化h, b每次循环-1024,也就是一页的值,缓冲区结尾的h每次循环+1(一个 buffer_head 大小的内存)。使用next和prev指针构成一个双向链表。
  • for (i=0;i<NR_HASH;i++):建立哈希表,方便快速寻找设备缓存:读取块设备的数据(硬盘中的数据),需要先读到缓冲区中,如果缓冲区已有了,就不用从块设备读取了,直接取走。

hd_init

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

......
struct blk_dev_struct {
    void (*request_fn)(void);
    struct request * current_request;
};

extern struct blk_dev_struct blk_dev[NR_BLK_DEV];
extern struct request request[NR_REQUEST];
extern struct task_struct * wait_for_request;
......
// 硬盘系统初始化
// 设置硬盘中断描述符,并允许硬盘控制器发送中断请求信号。
// 该函数设置硬盘设备的请求项处理函数指针为do_hd_request(),然后设置硬盘中断门
// 描述符。Hd_interrupt(kernel/system_call.s)是其中断处理过程。硬盘中断号为
// int 0x2E(46),对应8259A芯片的中断请求信号IRQ13.接着复位接联的主8250A int2
// 的屏蔽位,允许从片发出中断请求信号。再复位硬盘的中断请求屏蔽位(在从片上),
// 允许硬盘控制器发送中断信号。中断描述符表IDT内中断门描述符设置宏set_intr_gate().
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);                      // 复位接联的主8259A int2的屏蔽位
    outb(inb_p(0xA1)&0xbf,0xA1);                        // 复位硬盘中断请求屏蔽位(在从片上)
}
  • 把blk_dev 数组索引 3 位置处的块设备管理结构 blk_dev_struct 的 request_fn 赋值为do_hd_request。因为有很多块设备,所以Linux 0.11内核用了一个blk_dev[]来进行管理,每一个索引表示一个块设备。每个块设备执行读写请求都有自己的函数实现,在上层看来都是一个统一函数request_fn,但具体实现不同,对于硬盘来说,这个实现就是do_hd_request函数。
  • 设置一个新的中断,中断号是 0x2E,中断处理函数是hd_interrupt,即硬盘发生读写时,硬盘会发出中断信号给CPU,之后CPU便会陷入中断处理程序,也就是执行hd_interrupt函数。
  • 向几个IO端口上读写,其作用是允许硬盘控制器发送中断请求信号。

声明:Hello World|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - linux0.11源码 - 初始化


我的朋友,理论是灰色的,而生活之树是常青的!