操作系统学习:Linux0.12初始化详细流程-进程1调度与读取硬盘数据

本文参考书籍

1.操作系统真相还原
2.Linux内核完全剖析:基于0.12内核
3.x86汇编语言  从实模式到保护模式
4.Linux内核设计的艺术
ps:基于x86硬件的pc系统

Linux0.12初始化续-时间中断和任务切换

此时操作系统以及开启了时间中断,在进程0在fork出进程1的过程也有可能发生时间中断并发生任务调度,此时,先分析一下时间中断的处理过程和任务调度。

时间中断处理

当硬件发生时间中断时,会调用注册在中断描述符对应的处理函数,此时时间中断处理函数定义在sched_init函数中的;

set_intr_gate(0x20,&timer_interrupt);   

当发生硬件中断时,会调用timer_interrupt处理;

.align 2
_timer_interrupt:
    push %ds        # save ds,es and put kernel data space     保存ds,es到内核数据段
    push %es        # into them. %fs is used by _system_call   fs将用于系统调用
    push %fs
    pushl $-1      # fill in -1 for orig_eax                  填-1,表示不是系统调用
    pushl %edx      # we save %eax,%ecx,%edx as gcc doesn't    保存eax、ecx和edx的值,因为gcc不保存
    pushl %ecx      # save those across function calls. %ebx
    pushl %ebx      # is saved as we use that in ret_sys_call  保存的值将被ret_sys_call使用
    pushl %eax
    movl $0x10,%eax                                           # ds,es指向内核数据段
    mov %ax,%ds
    mov %ax,%es
    movl $0x17,%eax                                           # fs指向局部数据段
    mov %ax,%fs
    incl _jiffies                                              # 减少运行滴答数
    movb $0x20,%al     # EOI to interrupt controller #1       发指令结束该硬件中断
    outb %al,$0x20                     
    movl CS(%esp),%eax                                         # 取系统调用的选择符中的特权级并压入栈
    andl $3,%eax       # %eax is CPL (0 or 3, 0=supervisor)
    pushl %eax
    call _do_timer      # 'do_timer(long CPL)' does everything from  压入参数作为do_timer的参数
    addl $4,%esp       # task switching to accounting ...     
    jmp ret_from_sys_call                                      #跳到ret_from_sys_call处执行

在进行了相应栈设置,参数入栈后就调用了do_timer函数,该函数位于
sched.c文件中,

void do_timer(long cpl)
{
    static int blanked = 0;

    if (blankcount || !blankinterval) {  // 判断是否让屏幕黑屏或者让屏幕恢复显示
        if (blanked)
            unblank_screen();            // 屏幕恢复
        if (blankcount)
            blankcount--;
        blanked = 0;
    } else if (!blanked) {
        blank_screen();                 // 屏幕黑屏
        blanked = 1;
    }
    if (hd_timeout)                     // 判断硬盘访问是否超时
        if (!--hd_timeout)
            hd_times_out();             // 如果硬盘访问超时,则调用该函数

    if (beepcount)                      // 发生器计数
        if (!--beepcount)
            sysbeepstop();              // 如果发生数为0则停止发声

    if (cpl)                            // 如果是用户态代码
        current->utime++;               // 用户态代码计数加1
    else
        current->stime++;               // 内核态代码计数加1

    if (next_timer) {                   // 如果有设置定时器
        next_timer->jiffies--;          // 定时器执行滴答数减1
        while (next_timer && next_timer->jiffies <= 0) {  // 如果定时器滴答数小于0
            void (*fn)(void);                             // 则执行定时器定义的函数
                                                          // 并将定时器往前移动一位
            fn = next_timer->fn;
            next_timer->fn = NULL;
            next_timer = next_timer->next;
            (fn)();
        }
    }
    if (current_DOR & 0xf0)             // 如果当前软盘的输出寄存器中马达启动置位则执行软盘定时程序
        do_floppy_timer();
    if ((--current->counter)>0) return;     // 如果当前任务执行时间未用完则不发生任务调度
    current->counter=0;
    if (!cpl) return;                   // 如果中断当前的为内核态执行也不发生任务调度
    schedule();                         // 当前任务时间片用完,又是用户态中断则发生任务调度
}

时间中断处理函数主要是对定时器、屏幕、硬盘处理程序、软盘处理程序等先进行操作判断,如果时间到了就进行相应的处理,如果当前程序运行在用户态并且当前任务时间片用完则执行任务切换。

任务调度

当中断处理时,发生任务调度时,就调用schedule函数;

void schedule(void)
{
    int i,next,c;
    struct task_struct ** p;                // 任务结构指针的指针

/* check alarm, wake up any interruptible tasks that have got a signal */

    for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)                 // 从任务最开始的任务一直检查到任务最后一个
        if (*p) {                                               // 如果任务数组中有任务
            if ((*p)->timeout && (*p)->timeout < jiffies) {     // 当前任务超时,则重置当前任务
                (*p)->timeout = 0;
                if ((*p)->state == TASK_INTERRUPTIBLE)          // 如果当前任务是可中断状态则改为运行态
                    (*p)->state = TASK_RUNNING;
            }
            if ((*p)->alarm && (*p)->alarm < jiffies) {         // 如果任务的alarm值超时则向任务发送SIGALARM信号
                (*p)->signal |= (1<<(SIGALRM-1));
                (*p)->alarm = 0;                                // 重置任务alarm时间
            }
            if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
            (*p)->state==TASK_INTERRUPTIBLE)                    // 如果当前任务中除了阻塞信号还有其他信号,并且该任务处于可中断状态
                (*p)->state=TASK_RUNNING;                       // 则将该任务设置成运行态
        }

/* this is the scheduler proper: */                             // 任务调度的主要代码

    while (1) { 
        c = -1;
        next = 0;
        i = NR_TASKS;                                           // 当前任务数组数
        p = &task[NR_TASKS];                                    // 取最后一个任务数组
        while (--i) {
            if (!*--p)                                          // 循环如果当前任务数组为空则继续循环
                continue;                               
            if ((*p)->state == TASK_RUNNING && (*p)->counter > c)   // 如果任务为运行态,就循环找出剩余时间片最大的那个任务
                c = (*p)->counter, next = i;
        }
        if (c) break;                                           // 如果比较得出的结果不为0,则结束循环                               
        for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)             // 如果比较结果为0则重新循环任务数组
            if (*p)                                             // 如果任务数组值不为空
                (*p)->counter = ((*p)->counter >> 1) +
                        (*p)->priority;                         // 此时就将counter除2,加上任务的优先级作为执行的剩余时间,并继续下一次循环直到找到能调度的任务
    }
    switch_to(next);                                            // 找到能切换的任务后,执行任务切换
}

任务调度函数主要就是找出运行时间最少的任务并调度,如果当前所有任务时间片都用完则重新更新该任务的运行时间值,按照旧运行时间值除2加上任务优先级的值。
任务切换的switch_to是宏定义函数;

#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,_current\n\t" \                 // 比较n是否是当前任务
    "je 1f\n\t" \                                   // 如果是就什么都不作
    "movw %%dx,%1\n\t" \                            // 将新任务的16位选择符存入__tmp.b中
    "xchgl %%ecx,_current\n\t" \                    // 将current存入ecx中
    "ljmp %0\n\t" \                                 // 长跳转到__tmp处,此时会自动发生任务切换
    "cmpl %%ecx,_last_task_used_math\n\t" \         // 判断是否使用了协处理器
    "jne 1f\n\t" \                                  // 没有就退出
    "clts\n" \                                      // 原任务使用过则清理cr0中的任务
    "1:" \
    ::"m" (*&__tmp.a),"m" (*&__tmp.b), \
    "d" (_TSS(n)),"c" ((long) task[n])); \
}

此时完成任务切换后,会返回到时间处理函数中,继续执行ret_from_sys_call,主要是处理相应信号处理函数。

当进程0在生成进程1的过程中,如果发生切换,由于进行1还是不可中断状态,则还是继续执行进行0;当进程1生成完成后处于运行态后,就可以发生任务切换,当进程0执行时间片完成后就会切换到进程1执行。

Linux0.12初始化续-进程1的执行

此时由进程0生成的子进程任务1执行了;

    if (!fork()) {      /* we count on this going ok */  // 在新建的子进程中执行init()函数
        init();
    }

init函数如下;

void init(void)
{
    int pid,i;

    setup((void *) &drive_info);                // 系统调用读取硬盘信息并加载到虚拟盘
    (void) open("/dev/tty1",O_RDWR,0);          // 以读写方式打开tty
    (void) dup(0);                              // 复制句柄,产生句柄1号标准输出设备
    (void) dup(0);                              // 复制句柄,产生句柄2号标准错误输出
    printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS,
        NR_BUFFERS*BLOCK_SIZE);                 // 打印内存信息
    printf("Free mem: %d bytes\n\r",memory_end-main_memory_start);
    if (!(pid=fork())) {                        // 执行/etc/rc中的命令参数 
        close(0);
        if (open("/etc/rc",O_RDONLY,0))
            _exit(1);                           // 若文件打开失败则立刻退出
        execve("/bin/sh",argv_rc,envp_rc);      // 系统调用执行命令
        _exit(2);                               // 若执行出错则退出
    }
    if (pid>0)
        while (pid != wait(&i))                 // 父进程等待子进程执行完成
            /* nothing */;
    while (1) {
        if ((pid=fork())<0) {
            printf("Fork failed in init\r\n");
            continue;                           // 如果fork出错则继续fork
        }
        if (!pid) {                             // 新的子进程
            close(0);close(1);close(2);         
            setsid();                           // 创建一组会话
            (void) open("/dev/tty1",O_RDWR,0);  // 以读写的方式打开终端
            (void) dup(0);                      
            (void) dup(0);
            _exit(execve("/bin/sh",argv,envp)); // 执行shell程序
        }
        while (1)
            if (pid == wait(&i))                // 如果子进程退出则继续循环
                break;                          // 停止循环
        printf("\n\rchild %d died with code %04x\n\r",pid,i);
        sync();                                 // 同步操作,刷新缓冲区
    }
    _exit(0);   /* NOTE! _exit, not exit() */
}
硬盘的初始化-读取硬盘数据

首先会调用setup函数执行,该函数是一个系统调用函数

static inline _syscall1(int,setup,void *,BIOS)      // 带一个参数的系统调用 setup 磁盘初始化的系统调用 

该系统调用函数位于kernel/blk_dev/hd.c中;

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                                                 // 如果定义了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)                                 // 如果系统只有1个硬盘则会将第2个硬盘的16字节全部清空
        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*             // 中间1到4位是分区信息
                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)      // 读出硬盘类型信息,检测是否是AT兼容硬盘
        if (cmos_disks & 0x0f)
            NR_HD = 2;
        else
            NR_HD = 1;
    else
        NR_HD = 0;
    for (i = NR_HD ; i < 2 ; i++) {                 // 如果两个都是则全部重置,如果为1则重置第二个硬盘参数
        hd[i*5].start_sect = 0;
        hd[i*5].nr_sects = 0;
    }
    for (drive=0 ; drive<NR_HD ; drive++) {         // 根据确定的硬盘个数,读取每个硬盘的第1个扇区的分区表信息,用来设置分区结构数组hd[]中硬盘分区的信息
        if (!(bh = bread(0x300 + drive*5,0))) {     // 首先读取硬盘的第1个数据块
            printk("Unable to read partition table of drive %d\n\r",
                drive);
            panic("");
        }
        if (bh->b_data[510] != 0x55 || (unsigned char)  // 读取成功后,根据第1扇区最后两个字节判断扇区中数据的有效性
            bh->b_data[511] != 0xAA) {                  // 来判断分区表是否有效
            printk("Bad partition table on drive %d\n\r",drive);
            panic("");
        }
        p = 0x1BE + (void *)bh->b_data;                 // 分区表位于第1扇区0x1BE 处
        for (i=1;i<5;i++,p++) {                         // 将硬盘信息数据设置到hd[]数组中
            hd[i+5*drive].start_sect = p->start_sect;
            hd[i+5*drive].nr_sects = p->nr_sects;
        }
        brelse(bh);                                     // 释放为存放硬盘数据块申请的缓冲区
    }
    for (i=0 ; i<5*MAX_HD ; i++)
        hd_sizes[i] = hd[i].nr_sects>>1 ;               // 设置硬盘分区总数据块对应硬盘的数据块总数
    blk_size[MAJOR_NR] = hd_sizes;                      // 让设备数据块总数指针指向该数组
    if (NR_HD)
        printk("Partition table%s ok.\n\r",(NR_HD>1)?"s":"");
    rd_load();                                          // 加载虚拟盘
    init_swapping();                                    // 内存交换初始化
    mount_root();                                       // 安装根文件系统
    return (0);
}

此时会将通过初始化过程中获取的硬盘相关信息进行硬盘数据的保存,然后尝试加载虚拟盘,初始化交换空间和安装根文件系统。
在读取硬盘信息的过程中回调用bread()函数;

struct buffer_head * bread(int dev,int block)
{
    struct buffer_head * bh;                    // 缓冲区头部结构

    if (!(bh=getblk(dev,block)))                // 获取设备相应的数据块数据
        panic("bread: getblk returned NULL\n");
    if (bh->b_uptodate)                         // 是否是更新的,如果是则直接返回
        return bh;
    ll_rw_block(READ,bh);                       // 块设备读写函数
    wait_on_buffer(bh);                         // 等待该数据块内容被读入后被唤醒
    if (bh->b_uptodate)                         // 获取读取后的数据块如果为最新则返回
        return bh;
    brelse(bh);                                 // 如果读取的数据块不是最新,就释放该数据块
    return NULL;
}

该函数主要是从设备上读取指定的数据块并返回含有数据的缓冲区,如果指定的块不在则返回为NULL。当执行到getblk函数就是读取相应的数据块;

#define BADNESS(bh) (((bh)->b_dirt<<1)+(bh)->b_lock)
struct buffer_head * getblk(int dev,int block)
{
    struct buffer_head * tmp, * bh;

repeat:
    if (bh = get_hash_table(dev,block))                 // 搜索hash表该块已经存在则直接返回
        return bh;
    tmp = free_list;
    do {
        if (tmp->b_count)                               // 如果当前表指针技术不为空继续寻找
            continue;
        if (!bh || BADNESS(tmp)<BADNESS(bh)) {          // 如果缓冲区为空,或者tmp所指的缓冲头数据计算小于bh头标志的值
            bh = tmp;                                   // 让bh指向tmp
            if (!BADNESS(tmp))                          // 如果缓冲块即没有修改页没有锁定标志则结束循环否则一直查找直到找到计算值最小的缓冲块
                break;
        }
/* and repeat until we find something good */
    } while ((tmp = tmp->b_next_free) != free_list);
    if (!bh) {                                          // 如果没有找到则主动睡眠然后等待被调用
        sleep_on(&buffer_wait);
        goto repeat;                                    // 被唤醒后继续跳转到函数开头执行
    }
    wait_on_buffer(bh);                                 // 等待该缓冲块解锁
    if (bh->b_count)                                    // 如果被占用则继续寻找
        goto repeat;
    while (bh->b_dirt) {                                // 如果缓冲区已被修改,
        sync_dev(bh->b_dev);                            // 将数据写盘
        wait_on_buffer(bh);                             // 等待该缓冲区解锁
        if (bh->b_count)                                // 如果又被占用则继续寻找
            goto repeat;
    }
/* NOTE!! While we slept waiting for this block, somebody else might */
/* already have added "this" block to the cache. check it */
    if (find_buffer(dev,block))                         // 检查该缓冲块是否已经加入hash表中
        goto repeat;                                    // 继续寻找
/* OK, FINALLY we know that this buffer is the only one of it's kind, */
/* and that it's unused (b_count=0), unlocked (b_lock=0), and clean */
    bh->b_count=1;                                      // 当找到缓冲块后设置引用次数为1
    bh->b_dirt=0;                                       // 是否脏为0
    bh->b_uptodate=0;                                   // 是否更新为0
    remove_from_queues(bh);                             // 从空闲列表中移除该缓冲块
    bh->b_dev=dev;                                      // 设置设备号
    bh->b_blocknr=block;                                // 设置缓冲块
    insert_into_queues(bh);                             // 重新加入hash列表中
    return bh;                                          // 返回寻找到的缓冲块
}

该函数相对复杂一点,主要是寻找一个可用的缓冲块,由于要考虑到多种并发情况,所以相对复杂,该函数的执行流程图如图(Linux0.12截图);
函数执行流程
其中在调用sleep_on函数时,会执行等待该缓冲块的;

static inline void __sleep_on(struct task_struct **p, int state)
{
    struct task_struct *tmp;

    if (!p)                                         // 若指针无效则退出
        return;
    if (current == &(init_task.task))               // 若当前任务为0则司机
        panic("task[0] trying to sleep");
    tmp = *p;                                       // tmp指向当前任务指针
    *p = current;                                   // 保存当前任务保存到p中
    current->state = state;                         // 设置当前任务的运行状态
repeat: schedule();                                 // 执行任务调度
    if (*p && *p != current) {                      // 任务被唤醒时,判断当前任务与保存的任务是否一致
        (**p).state = 0;                            // 如果不一致我们要唤醒该队列
        current->state = TASK_UNINTERRUPTIBLE;      // 将当前进程设置成不可中断
        goto repeat;                                // 重新执行任务调度
    }
    if (!*p)
        printk("Warning: *P = NULL\n\r");
    if (*p = tmp)                                   // 如果是保存的上个任务不为空则设置上个任务为运行状态
        tmp->state=0;
}
...
void sleep_on(struct task_struct **p)
{
    __sleep_on(p,TASK_UNINTERRUPTIBLE);
}

由于sleep_on可能会出现任务等待队列,所以当该任务呗唤醒时,需要将等待队列里面的任务依次唤醒,这样就可以依次唤醒等待该资源的进程。
在getblk函数sleep_on函数唤醒后,就会执行到wait_on_buffer函数;

static inline void wait_on_buffer(struct buffer_head * bh)
{
    cli();                                  // 关中断
    while (bh->b_lock)                      // 如果该缓冲块已经锁上则等待该资源
        sleep_on(&bh->b_wait);              // 进入资源等待队列中
    sti();                                  // 开中断
}

该函数中的sleep_on中实现原理也是实现了资源的等待队列。
当找到可用的缓冲块之后,然后就返回该缓冲块的地址。

此时在bread()函数中得到返回的bh,如果此时bh可用并没有数据的话,就继续带哦用
ll_rw_block函数,块设备读写函数。

void ll_rw_block(int rw, struct buffer_head * bh)    // 低级数据块读写函数
{
    unsigned int major;

    if ((major=MAJOR(bh->b_dev)) >= NR_BLK_DEV ||   
    !(blk_dev[major].request_fn)) {                 // 如果设备主设备号不存在或者设备的请求函数不存在则报错
        printk("Trying to read nonexistent block-device\n\r");
        return;
    }
    make_request(major,rw,bh);                      // 创建请求项并插入请求队列
}

此时调用make_request函数后执行如下代码;

static void make_request(int major,int rw, struct buffer_head * bh)
{
    struct request * req;
    int rw_ahead;

/* WRITEA/READA is special case - it is not really needed, so if the */
/* buffer is locked, we just forget about it, else it's a normal read */
    if (rw_ahead = (rw == READA || rw == WRITEA)) {     // 判断操作是否是读写
        if (bh->b_lock)                                 // 判断缓冲块是否上锁
            return;                                     // 如果上锁就直接返回
        if (rw == READA)                                
            rw = READ;
        else
            rw = WRITE;
    }
    if (rw!=READ && rw!=WRITE)                          // 如果操作既不是读又不是写则报错
        panic("Bad block dev command, must be R/W/RA/WA");
    lock_buffer(bh);                                    // 给该缓冲块上锁
    if ((rw == WRITE && !bh->b_dirt) || (rw == READ && bh->b_uptodate)) {
        unlock_buffer(bh);                              // 如果该缓冲块是写并且是数据不脏的情况和是读该缓冲块是更新的情况下就解锁该缓冲块直接返回
        return;
    }
repeat:
/* we don't allow the write-requests to fill up the queue completely:
 * we want some room for reads: they take precedence. The last third
 * of the requests are only for reads.
 */
    if (rw == READ)                                     // 如果是读的话,则指向队列尾部
        req = request+NR_REQUEST;
    else
        req = request+((NR_REQUEST*2)/3);               // 如果是写的话指向队列2/3处
/* find an empty request */
    while (--req >= request)                            // 此时查找出一个空的请求项
        if (req->dev<0)
            break;
/* if none found, sleep on new requests: check for rw_ahead */
    if (req < request) {                                // 如果以搜索到头部
        if (rw_ahead) {                                 // 若是提前读写请求则解锁缓冲块并退出
            unlock_buffer(bh);
            return;
        }
        sleep_on(&wait_for_request);                    // 如果是读写请求则进入睡眠,等待唤醒后继续查找
        goto repeat;
    }
/* fill up the request-info, and add it to the queue */
    req->dev = bh->b_dev;                               // 找到空闲的请求项后,设置设备号
    req->cmd = rw;                                      // 设置请求
    req->errors=0;                                      // 操作时产生的错误次数
    req->sector = bh->b_blocknr<<1;                     // 其实扇区,块号转为扇区(512乘以2为1024)
    req->nr_sectors = 2;                                // 请求项需要读写的扇区数
    req->buffer = bh->b_data;                           // 请求项缓冲区指针指向需读写的数据缓冲区
    req->waiting = NULL;                                // 任务等待完成的地方
    req->bh = bh;                                       // 缓冲块指针
    req->next = NULL;                                   // 下一个请求
    add_request(major+blk_dev,req);                     // 请求项加入队列中
}

该函数就是初始化一个req,读操作是优先的,请求队列的后1/3用于读请求项,当初始化完成后,就调用add_request函数添加该请求到任务队列中。

static void add_request(struct blk_dev_struct * dev, struct request * req)
{
    struct request * tmp;

    req->next = NULL;
    cli();                                      // 关闭中断
    if (req->bh)
        req->bh->b_dirt = 0;                    // 置为缓冲区脏标志
    if (!(tmp = dev->current_request)) {        // 如果设备当前的请求项为空
        dev->current_request = req;             // 设置当前设备请求指向该req请求
        sti();                                  // 开中断
        (dev->request_fn)();                    // 执行设备请求函数
        return;
    }
    for ( ; tmp->next ; tmp=tmp->next) {        // 如果设备请求项不为空,利用电梯算法搜索最佳查找位置
        if (!req->bh)                           
            if (tmp->next->bh)
                break;
            else
                continue;
        if ((IN_ORDER(tmp,req) ||
            !IN_ORDER(tmp,tmp->next)) &&
            IN_ORDER(req,tmp->next))
            break;
    }
    req->next=tmp->next;                        // 找到位置后将req加入tmp之后
    tmp->next=req;
    sti();                                      // 开启中断
}

此时将req添加到相关请求队列中执行,此时执行request_fn,硬盘对应的为do_hd_request函数;

void do_hd_request(void)                    // 执行硬盘读写请求
{
    int i,r;
    unsigned int block,dev;
    unsigned int sec,head,cyl;
    unsigned int nsect;

    INIT_REQUEST;                          
    dev = MINOR(CURRENT->dev);              // 取设备号中的子设备号
    block = CURRENT->sector;                // 读取块的大小
    if (dev >= 5*NR_HD || block+2 > hd[dev].nr_sects) {  // 因为一次读写2个扇区所以不能超过倒数第二个扇区
        end_request(0);                     // 结束当前请求   
        goto repeat;                        // 跳转到blk.h中进行检查
    }
    block += hd[dev].start_sect;            // 获取开始的块数
    dev /= 5;                               // 取硬盘号
    __asm__("divl %4":"=a" (block),"=d" (sec):"0" (block),"1" (0),
        "r" (hd_info[dev].sect));
    __asm__("divl %4":"=a" (cyl),"=d" (head):"0" (block),"1" (0),
        "r" (hd_info[dev].head));           // 根据传入的参数计算扇区号柱面号和磁头号
    sec++;
    nsect = CURRENT->nr_sectors;            // 打算读写的扇区数
    if (reset) {                            // 是否重置硬盘
        recalibrate = 1;                    // 重置标志位
        reset_hd();                         // 磁盘复位操作
        return;
    }
    if (recalibrate) {                      // 重新置位
        recalibrate = 0;
        hd_out(dev,hd_info[CURRENT_DEV].sect,0,0,0,
            WIN_RESTORE,&recal_intr);
        return;
    }   
    if (CURRENT->cmd == WRITE) {            // 如果是写操作
        hd_out(dev,nsect,sec,head,cyl,WIN_WRITE,&write_intr);
        for(i=0 ; i<10000 && !(r=inb_p(HD_STATUS)&DRQ_STAT) ; i++)
            /* nothing */ ;
        if (!r) {
            bad_rw_intr();
            goto repeat;
        }
        port_write(HD_DATA,CURRENT->buffer,256);
    } else if (CURRENT->cmd == READ) {      // 如果是读操作
        hd_out(dev,nsect,sec,head,cyl,WIN_READ,&read_intr);
    } else
        panic("unknown hd-command");
}

此时进行读写后都会执行hd_out函数进行处理,此时我们继续查看该函数的操作;

static void hd_out(unsigned int drive,unsigned int nsect,unsigned int sect,
        unsigned int head,unsigned int cyl,unsigned int cmd,
        void (*intr_addr)(void))                    // 向硬盘控制器发送命令块
{
    register int port asm("dx");                    // 定义局部寄存器变量放在寄存器dx中

    if (drive>1 || head>15)                         // 如果驱动器号大于1或者磁头号大于15则不支持
        panic("Trying to write bad sector");
    if (!controller_ready())                        // 等待驱动器是否就位
        panic("HD controller not ready");
    SET_INTR(intr_addr);                            // 设置do_hd=intr_addr在中断中使用
    outb_p(hd_info[drive].ctl,HD_CMD);              // 向控制寄存器输出控制字节
    port=HD_DATA;                                   // 设置寄存器端口
    outb_p(hd_info[drive].wpcom>>2,++port);         // 柱面号
    outb_p(nsect,++port);                           // 读写扇区总数
    outb_p(sect,++port);                            // 起始扇区
    outb_p(cyl,++port);                     
    outb_p(cyl>>8,++port);
    outb_p(0xA0|(drive<<4)|head,++port);
    outb(cmd,++port);                               // 硬盘控制命令
}

此时硬盘读命令发送到了磁盘控制器,然后就等待磁盘控制器将读出的数据,通过硬盘中断返回数据。

当硬盘数据读取完后后,此时会执行硬盘中断程序位于kernel/sys_call.s中的_hd_interrupt;

_hd_interrupt:
    pushl %eax
    pushl %ecx
    pushl %edx
    push %ds
    push %es                                    # 参数压栈
    push %fs
    movl $0x10,%eax                            # ds,es置为内核数据段
    mov %ax,%ds
    mov %ax,%es
    movl $0x17,%eax                            # 置为调用程序的局部数据段
    mov %ax,%fs
    movb $0x20,%al
    outb %al,$0xA0     # EOI to interrupt controller #1
    jmp 1f          # give port chance to breathe
1:  jmp 1f
1:  xorl %edx,%edx
    movl %edx,_hd_timeout                       # 将hd_timeout置为0,表示控制器在规定时间产生了中断
    xchgl _do_hd,%edx                           # 将压入的处理函数指针复制给_do_hd
    testl %edx,%edx
    jne 1f                                      # 如果为空则将_unexpected_hd_interrupt赋值给edx
    movl $_unexpected_hd_interrupt,%edx
1:  outb %al,$0x20                                 # 结束中断
    call *%edx      # "interesting" way of handling intr.  # 执行调用函数
    pop %fs
    pop %es
    pop %ds
    popl %edx
    popl %ecx
    popl %eax
    iret

此时在前面传入的是read_intr函数,此时执行;


static void read_intr(void)                     // 读操作中断调用函数
{
    if (win_result()) {                         // 若控制器忙、读写出错等
        bad_rw_intr();                          // 进行读写硬盘失败处理
        do_hd_request();                        // 再次请求硬盘处理
        return;
    }
    port_read(HD_DATA,CURRENT->buffer,256);     // 读数据到请求结构结构缓冲区
    CURRENT->errors = 0;                        // 清出错次数
    CURRENT->buffer += 512;                     // 调整缓冲区指针,指向新的空区
    CURRENT->sector++;                          // 起始扇区加1
    if (--CURRENT->nr_sectors) {                // 如果所需要读的扇区数还没读完则继续读
        SET_INTR(&read_intr);
        return;
    }
    end_request(1);                             // 扇区数读完后更新本次读请求,并唤醒等待该缓冲区的进程
    do_hd_request();                            // 处理其他磁盘请求
}

此时在end_request函数中就有唤醒等待该缓冲块的进程;

extern inline void end_request(int uptodate)                     // 结束请求处理
{
    DEVICE_OFF(CURRENT->dev);                                    // 关闭设备
    if (CURRENT->bh) {                                           // 当前请求项指针
        CURRENT->bh->b_uptodate = uptodate;                      // 置更新标志
        unlock_buffer(CURRENT->bh);                              // 解锁缓冲区
    }
    if (!uptodate) {                                             // 如果更新标志为0则显示出错信息
        printk(DEVICE_NAME " I/O error\n\r");
        printk("dev %04x, block %d\n\r",CURRENT->dev,
            CURRENT->bh->b_blocknr);
    }
    wake_up(&CURRENT->waiting);                                  // 唤醒等待该请求项的进程
    wake_up(&wait_for_request);                                  // 唤醒等待空闲请求项的进程
    CURRENT->dev = -1;                                           // 释放该请求项
    CURRENT = CURRENT->next;                                     // 指向下一个请求项
}

此时当在sys_setup函数中读取了硬盘第一扇区的数据后,比较之后就会执行到brelse;

void brelse(struct buffer_head * buf)
{
    if (!buf)
        return;
    wait_on_buffer(buf);                                // 等待该缓冲区
    if (!(buf->b_count--))                              // 缓冲区引用次数减1
        panic("Trying to free free buffer");
    wake_up(&buffer_wait);                              // 唤醒等待的进程
}

此时,系统的第一次读硬盘数据的流程便执行完成,接下来分析虚拟盘和根文件系统的加载。

阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_33339479/article/details/80694668
文章标签: 操作系统
个人分类: 操作系统
上一篇操作系统学习:Linux0.12初始化详细流程-首个子进程
下一篇操作系统学习:Linux0.12初始化详细流程-进程1加载虚拟盘和根文件系统安装
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭