且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

更新时间:2022-09-16 10:00:32

3.3 轮转到进程1执行

在分析进程1如何开始执行之前,先回顾一下进程0创建进程1的过程。
在3.1.3节中讲解调用copy_process函数时曾强调过,当时为进程1设置的tss.eip就是进程0调用fork( )创建进程1时int 0x80中断导致的CPU硬件自动压栈的ss、esp、eflags、cs、eip中的EIP值,这个值指向的是int 0x80的下一行代码的位置,即if (__res >= 0)。
前面讲述的ljmp 通过CPU的任务门机制自动将进程1的TSS的值恢复给CPU,自然也将其中的tss.eip恢复给CPU。现在CPU中的EIP指向的就是fork中的if (__res >= 0)这一行,所以,进程1就要从这一行开始执行。
执行代码如下:

//代码路径:include/unistd.h:
#define _syscall0(type,name) \
int fork(void) 
{ 
long __res; 
__asm__ volatile ("int $0x80"     
    : "=a" (__res)     
    : "0" (__NR_ fork));     
    if (__res >= 0)     //现在从这行开始执行,copy_process为进程1做的tss.eip就是指向这一行
    return (int) __res; 
errno= -__res; 
return -1; 
}

回顾前面3.1.3节中的介绍可知,此时的__res值,就是进程1的TSS中eax的值,这个值在3.1.3节中被写死为0,即p->tss.eax = 0,因此,当执行到return (type) __res这一行时,返回值是0,如图3-15所示。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

返回后,执行到main()函数中if (!fork( ))这一行,! 0为“真”,调用init()函数!
执行代码如下:

//代码路径:init/main.c:
void main(void)
{
    …
    if (!fork()) {        //!0为真,
         init();        //这次要执行这一行!代码跨度比较大,请参看3.1.3节
    }
}

进入init()函数后,先调用setup()函数,执行代码如下:

//代码路径:init/main.c:
void init(void)
{
    …
    setup((void *) &drive_info);
    …
}

本章后续的内容都是setup( )函数实现的。这个函数的调用与fork( )、pause( )函数的调用类似;略有区别的是setup( )函数不是通过_syscall0( )而是通过_syscall1( )实现的;具体的实现过程基本类似,也是通过int 0x80、_system_call、call _sys_call_table(,%eax,4)、sys_setup( )。
提醒:前面pause( )函数的那个int 0x80中断还没有返回,现在setup( )又产生了一个中断。
3.3.1 进程1为安装硬盘文件系统做准备
这一节的内容涉及sys_setup( )的大部分代码,包括从函数开始到调用rd_load( )之前的所有代码;技术路线比较长,代码很多,难度比较大,hash_table的部分尤其如此。但这部分代码的目的却很单一:为第5章将要讲述的安装硬盘文件系统做准备。
这个过程大概经过3个步骤;
1)根据机器系统数据设置硬盘参数;
2)读取硬盘引导块;
3)从引导块中获取信息。
1.进程1设置硬盘的hd_info
根据机器系统数据中的drive_info,如硬盘的柱面数、磁头数、扇区数,设置内核的hd_info,如图3-16所示。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

具体的执行代码如下:

//代码路径:kernel/blk_dev/hd.c:
    …
struct hd_i_struct {
    int head,sect,cyl,wpcom,lzone,ctl;
    };
    …
struct hd_i_struct hd_info[]= { {0,0,0,0,0,0},{0,0,0,0,0,0} };
    …
static struct hd_struct {
    long start_sect;    // 起始扇区号
    long nr_sects;        //总扇区数
} hd[5*MAX_HD]={{0,0},};
    …

/* This may be used only once, enforced by 'static int callable' */
int sys_setup(void * BIOS)    //对比调用可以看出BIOS就是drive_info,参看2.1节
{
    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++){//读取drive_info设置hd_info
         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
//一个物理硬盘最多可以分4个逻辑盘,0是物理盘,1~4是逻辑盘,共5个,第1个物理盘是0*5,第2个物理盘是1*5
    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;
    }
  
  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;
    }

//第1个物理盘设备号是0x300,第2个是0x305,读每个物理硬盘的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("");
         }
    …
}

2.读取硬盘的引导块到缓冲区
在Linux 0.11中,硬盘最基础的信息就是分区表,其他信息都可以从这个信息引导出来,这个信息所在的块就是引导块。一块硬盘只有唯一的一个引导块,即硬盘的0号逻辑块。引导块有两个扇区,真正有用的是第一个扇区。我们设定计算机只有一块硬盘。下面把硬盘的引导块读入缓冲区,以便后续程序解读引导块中的信息。这个工作通过调用bread( )函数实现,bread( )可以理解为block read。
执行代码如下:

//代码路径:kernel/blk_dev/hd.c:
    …
//第1个物理盘设备号是0x300,第2个是0x305,读每个物理硬盘的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("");
         }
    …
}

进入bread( )函数后,先调用getblk( )函数,在缓冲区中申请一个空闲的缓冲块。
执行代码如下:

//代码路径:fs/buffer.c:
struct buffer_head * bread(int dev,int block)    //读指定dev、block,第一块硬盘的dev是0x300,block是0
{
    struct buffer_head * bh;

    if (!(bh=getblk(dev,block)))          //在缓冲区得到与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;
}

申请空闲缓冲块的主要步骤,在图3-17中已有形象的说明。以下将结合代码来深入分析这一过程。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

在getblk( )函数中,先调用get_hash_table( )函数查找哈希表,检索此前是否有程序把现在要读的硬盘逻辑块(相同的设备号和块号)已经读到缓冲区。如果已经读到缓冲区,那就不用再费劲从硬盘上读取,直接用现成的,如图3-17中的第一步所示。使用哈希表进行查询的目的是提高查询速度。
执行代码如下:

//代码路径:fs/buffer.c:
    …
//在缓冲区得到与dev、block相符合或空闲的缓冲块。dev:0x300、block:0
struct buffer_head * getblk(int dev,int block) 
{
    struct buffer_head * tmp, * bh;

repeat:
    if (bh=get_hash_table(dev,block)) 
         return bh;
    tmp= free_list;
    do {
         if (tmp->b_count)                    
               continue;
         if (!bh || BADNESS(tmp)<BADNESS(bh)) {        
               bh= tmp;
               if (!BADNESS(tmp))
                     break;
         }
…
}

进入get_hash_table( )函数后,调用find_buffer( )函数查找缓冲区中是否有指定设备号、块号的缓冲块。如果能找到指定缓冲块,就直接用。
执行代码如下:

//代码路径:fs/buffer.c:
    …
//查找哈希表,确定缓冲区中是否有指定dev、block的缓冲块。dev:0x300、block:0
struct buffer_head * get_hash_table(int dev, int block)
{
    struct buffer_head * bh;

    for (;;) {
         if (!(bh=find_buffer(dev,block)))
               return NULL;        //现在是第一次使用,肯定没有已经读到缓冲区的块
         bh->b_count++;
         wait_on_buffer(bh);
         if (bh->b_dev== dev && bh->b_blocknr== block)
               return bh;
         bh->b_count--;
    }
}

现在是第一次使用缓冲区,缓冲区中不可能存在已读入的缓冲块,也就是说hash_table中没有挂接任何节点,find_buffer( )返回的一定是NULL。
执行代码如下:

//代码路径:fs/buffer.c:
    …
//NR_HASH是307,对于dev:0x300、block:0而言,_hashfn(dev,block)的值是154
#define _hashfn(dev,block) (((unsigned)(dev^block))%NR_HASH)
#define hash(dev,block) hash_table[_hashfn(dev,block)]
    …
//在缓冲区查找指定dev、block的缓冲块
static struct buffer_head * find_buffer(int dev, int block)
{        
    struct buffer_head * tmp;

    for(tmp= hash(dev,block);tmp!= NULL;tmp= tmp->b_next)//现在tmp->b_next为NULL
         if (tmp->b_dev==dev && tmp->b_blocknr==block)
               return tmp;
    return NULL;
}

从find_buffer( )、get_hash_table( )函数退出后,返回getblk( )函数,在空闲表中申请一个新的空闲缓冲块。现在所有缓冲块都是绑定在空闲表中的,所以要在空闲表中申请新的缓冲块,如图3-17中的第二步所示。
执行代码如下:

//代码路径:fs/buffer.c:
#define BADNESS(bh) (((bh)->b_dirt<<1) + (bh)->b_lock)//现在b_dirt、b_lock是0,
                                //BADNESS(bh)就是00
struct buffer_head * getblk(int dev,int block)
{
    struct buffer_head * tmp, * bh;

repeat:
    if (bh= get_hash_table(dev,block))
         return bh;
    tmp= free_list;
    do {
         if (tmp->b_count)        //tmp-> b_count现在为0
               continue;
         if (!bh || BADNESS(tmp)<BADNESS(bh))     // bh现在为0    
               bh= tmp;
               if (!BADNESS(tmp))    //现在BADNESS(tmp)是00,取得空闲的缓冲块!
                     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;
    }
         
    if (find_buffer(dev,block))    //现在虽然获得了空闲缓冲块,但并没有挂接到hash表中
    goto repeat;
    …

申请到缓冲块后,对它进行初始化设置,并将这个空闲块挂接到hash_table上。
执行代码如下:

//代码路径:fs/buffer.c:
struct buffer_head * getblk(int dev,int block) 
{
    …
    if (find_buffer(dev,block))
         goto repeat;
    bh->b_count=1;        //占用
    bh->b_dirt=0;
    bh->b_uptodate=0;
    remove_from_queues(bh);
    bh->b_dev=dev;
    bh->b_blocknr=block;
    insert_into_queues(bh);
    return bh;
}
为了更容易理解代码,我们将这个过程分步图示出来,如图3-18所示。
//代码路径:fs/buffer.c:
    …
static inline void remove_from_queues(struct buffer_head * bh)
{
/* remove from hash-queue */
    if (bh->b_next)                //bh->b_next现在为NULL
         bh->b_next->b_prev= bh->b_prev;
    if (bh->b_prev)                // bh->b_prev现在为NULL
         bh->b_prev->b_next= bh->b_next;
    if (hash(bh->b_dev,bh->b_blocknr)== bh)    //现在不出现这个情况
         hash(bh->b_dev,bh->b_blocknr)= bh->b_next;
/* remove from free list */
    if (!(bh->b_prev_free) || !(bh->b_next_free))//正常时不会出现
         panic("Free block list corrupted");
    bh->b_prev_free->b_next_free= bh->b_next_free;
    bh->b_next_free->b_prev_free= bh->b_prev_free;
    if (free_list== bh)
         free_list= bh->b_next_free;
}
    …

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行
《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

挂接hash_table的执行代码如下,分步示意图如图3-19所示。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行
《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

//代码路径:fs/buffer.c:
    …
static inline void insert_into_queues(struct buffer_head * bh)
{
/*put at end of free list */
    bh->b_next_free= free_list;
    bh->b_prev_free= free_list->b_prev_free;
    free_list->b_prev_free->b_next_free= bh;
    free_list->b_prev_free= bh;
/*put the buffer in new hash-queue if it has a device */
    bh->b_prev= NULL;
    bh->b_next= NULL;
    if (!bh->b_dev)
         return;
    bh->b_next= hash(bh->b_dev,bh->b_blocknr);
    hash(bh->b_dev,bh->b_blocknr)= bh;
    bh->b_next->b_prev= bh;
}
    …

执行完getblk( )函数后,返回bread( )函数。
3.将找到的缓冲块与请求项挂接
返回bread( )函数后,调用ll_rw_block( )这个函数,将缓冲块与请求项结构挂接,如图3-20所示。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

执行代码如下:

//代码路径:fs/buffer.c:
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;
}

进入ll_rw_block( )函数后,先判断缓冲块对应的设备是否存在或这个设备的请求项函数是否挂接正常。如果存在且正常,说明可以操作这个缓冲块,调用make_request( )函数,准备将缓冲块与请求项建立关系,执行代码如下:

//代码路径:kernel/blk_dev/ll_rw_block.c:
void ll_rw_block(int rw, struct buffer_head * bh)
{
    unsigned int major;

    if((major=MAJOR(bh->b_dev))>=NR_BLK_DEV||    //NR_BLK_DEV是7,主设备号0-6,>=7意味着不存在
    !(blk_dev[major].request_fn)) {
         printk("Trying to read nonexistent block-device\n\r");
         return;
    }
    make_request(major,rw,bh);
}

进程1继续执行,进入make_request( )函数后,先要将这个缓冲块加锁,目的是保护这个缓冲块在解锁之前将不再被任何进程操作,这是因为这个缓冲块现在已经被使用,如果此后再被挪作他用,里面的数据就会发生混乱。如图3-20右边的缓冲块buffer_head所示,其中选中的那个缓冲块对应的buffer_head已加锁。
之后,在请求项结构中,申请一个空闲请求项,准备与这个缓冲块相挂接。值得注意的是,如果是读请求,则从整个请求项结构的最末端开始寻找空闲请求项;如果是写请求,则从整个结构的2/3处,申请空闲请求项。这是因为从用户使用系统的心理角度讲,用户更希望读取的数据能更快地显现出来,所以给读取操作以更大的空间。这时候,请求项结构是第一次被使用,而且是读请求,所以在请求项结构的末端找到一个空闲的请求项,如图3-20中的request[32]结构所示,其中的最后一项已被选中。之后,缓冲块与请求项正式挂接,并对这个请求项各个成员进行初始化。
执行代码如下:

//代码路径:kernel/blk_dev/ll_rw_block.c:
static inline void lock_buffer(struct buffer_head * bh)
{
    cli();
    while (bh->b_lock)            //现在还没加锁
         sleep_on(&bh->b_wait);
    bh->b_lock=1;                //加锁
    sti();
}
    …
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)            // 读从尾端开始,写从2/3处开始
         req= request + NR_REQUEST;
    else
         req= request + ((NR_REQUEST*2)/3);
/* find an empty request */
    while (--req >= request)    //从后向前搜索空闲请求项,在blk_dev_init中,dev初始化为-1, 即空闲
         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;
    req->nr_sectors= 2;
    req->buffer= bh->b_data;
    req->waiting= NULL;
    req->bh= bh;
    req->next= NULL;
    add_request(major + blk_dev,req);
}

调用add_request( )函数,向请求项队列中加载该请求项,进入add_request( )后,先对当前硬盘的工作情况进行分析,然后设置该请求项为当前请求项,并调用硬盘请求项处理函数(dev->request_fn)( ),即 do_hd_request( )函数去给硬盘发送读盘命令。图3-21中给出了请求项管理结构与do_hd_request( )函数的对应关系。《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

执行代码如下:

//代码路径:kernel/blk_dev/ll_rw_block.c:
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;
         sti();
         (dev->request_fn)();            //do_hd_request()
         return;
    }
    for (;tmp->next;tmp=tmp->next)    // 电梯算法的作用是让磁盘磁头的移动距离最小
         if ((IN_ORDER(tmp,req) ||
             !IN_ORDER(tmp,tmp->next)) &&
             IN_ORDER(req,tmp->next))
               break;
    req->next=tmp->next;                //挂接请求项队列
    tmp->next=req;
    sti();
}

4.读硬盘
进入do_hd_request( )函数去执行,为读盘做最后准备工作。具体的准备过程如图3-22所示。《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

先通过对当前请求项数据成员的分析,解析出需要操作的磁头、扇区、柱面、操作多少个扇区……之后,建立硬盘读盘必要的参数,将磁头移动到0柱面,如图3-22中第二步所示;之后,针对命令的性质(读/写)给硬盘发送操作命令。现在是读操作(读硬盘的引导块),所以接下来要调用hd_out( )函数来下达最后的硬盘操作指令。注意看最后两个实参,WIN_READ表示接下来要进行读操作,read_intr( )是读盘操作对应的中断服务程序,所以要提取它的函数地址,准备挂接,这一动作反映在图3-22中的第三步。请注意,这是通过hd_out( )函数实现的,读盘请求就挂接read_intr( );如果是写盘,那就不是read_intr( ),而是write_intr( )了。
执行代码如下:

//代码路径:kernel/blk_dev/hd.c:
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) {
         end_request(0);
         goto repeat;
    }
    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) {
         reset= 0;        //置位,防止多次执行if (reset)
         recalibrate= 1;    //置位,确保执行下面的if(recalibrate)
         reset_hd(CURRENT_DEV);//将通过调用hd_out向硬盘发送WIN_SPECIFY命令,建立硬盘
                     //读盘必要的参数
         return;
    }
    if (recalibrate) {
         recalibrate= 0;     //置位,防止多次执行if (recalibrate)
         hd_out(dev,hd_info[CURRENT_DEV].sect,0,0,0,
               WIN_RESTORE,&recal_intr); //将向硬盘发送WIN_RESTORE命令,将磁头移动到
                        //0柱面,以便从硬盘上读取数据
         return;
    }    
    if (CURRENT->cmd== WRITE) {
         hd_out(dev,nsect,sec,head,cyl,WIN_WRITE,&write_intr);
         for(i=0;i<3000 && !(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( )函数中去执行读盘的最后一步:下达读盘指令,如图3-23中第一步所示。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

执行代码如下:

//代码路径:kernel/blk_dev/hd.c:
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))    //对比调用的传参WIN_READ,&read_intr
{
    register int port asm("dx");

    if (drive>1 || head>15)
         panic("Trying to write bad sector");
    if (!controller_ready())
         panic("HD controller not ready");
    do_hd= intr_addr;    //根据调用的实参决定是read_intr还是write_intr,现在是read_intr
    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/system_call.s:
_hd_interrupt:
       …
    1:    jmp 1f
    1:    xorl %edx,%edx   
    xchgl _do_hd,%edx
    testl %edx,%edx
    jne 1f
    movl $_unexpected_hd_interrupt,%edx      
    …

其中,do_hd = intr_addr;这一行是把读盘服务程序与硬盘中断操作程序相挂接,这里面的do_hd是system_call.s中_hd_interrupt下面xchgl _do_hd,%edx这一行所描述的内容。
现在要做读盘操作,所以挂接的就是实参read_intr,如果是写盘,挂接的就应该是write_intr( )函数。
下达读盘命令!
硬盘开始将引导块中的数据不断读入它的缓存中,同时,程序也返回了,将会沿着前面调用的反方向,即hd_out( )函数、do_hd_request( )函数、add_request( )函数、make_request( )函数、ll_rw_block( )函数,一直返回bread( )函数中。
现在,硬盘正在继续读引导块。如果程序继续执行,则需要对引导块中的数据进行操作。但这些数据还没有从硬盘中读完,所以调用wait_on_buffer( )函数,挂起等待!
执行代码如下:

//代码路径:fs/buffer.c:
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;
}

进入wait_on_buffer( )函数后,判断刚才申请到的缓冲块是否被加锁。现在,缓冲块确实加锁了,调用sleep_on( )函数。如图3-24中的第二步所示。执行代码如下:

//代码路径:fs/buffer.c:
static inline void wait_on_buffer(struct buffer_head * bh)
{
    cli();
    while (bh->b_lock)            //前面已经加锁
         sleep_on(&bh->b_wait);
    sti();
}

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

进入sleep_on( )函数后,将进程1设置为不可中断等待状态,如图3-24中第三步所示,进程1挂起;然后调用schedule( )函数,准备进程切换,执行代码如下:

//代码路径:kernel/sched.c:
void sleep_on(struct task_struct **p)
{
    struct task_struct *tmp;

    if (!p)
         return;
    if (current== &(init_task.task))
         panic("task[0] trying to sleep");
    tmp= *p;
    *p= current;
    current->state= TASK_UNINTERRUPTIBLE;
    schedule();
    if (tmp)
         tmp->state=0;
}

5.等待硬盘读数据时,进程调度切换到进程0执行
进入schedule( )函数后,切换到进程0去执行。图3-25给出了切换过程的主要步骤。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

具体执行步骤在3.2节中已经说明。但第二次遍历task[64]的时候,与3.2节中执行的结果不一样。此时只有两个进程,进程0的状态是可中断等待状态,进程1的状态也已经刚刚被设置成了不可中断等待状态。常规的进程切换条件是,剩余时间片最多且必须是就绪态,即代码“if ((p)->state == TASK_RUNNING && (p)->counter > c)”给出的条件。现在两个进程都不是就绪态,按照常规的条件无法切换进程,没有进程可以执行。
这是一个非常尴尬的状态。
操作系统的设计者对这种状态的解决方案是:强行切换到进程0!
注意:c的值将仍然是-1,所以next 仍然是0,这个next就是要切换到进程的进程号。可以看出,如果没有合适的进程,next的数值将永远是0,就会切换到进程0去执行!
执行代码如下:

//代码路径:kernel/sched.c:
void schedule(void)
{
    …
    while (1) {
         c= -1;
         cnext= 0;
         ci= NR_TASKS;
         cp= &task[NR_TASKS];
         cwhile (--i) {
               cif (!*--p)
                     continue;
               if ((*p)->state== TASK_RUNNING && (*p)->counter > c)
                     c= (*p)->counter, next= i;
         }
         if (c) break;
         for(p= &LAST_TASK;p > &FIRST_TASK;--p)
               if (*p)
                     (*p)->counter= ((*p)->counter >> 1) +
                                (*p)->priority;
    }
    switch_to(next);//next是0!
}

调用switch_to(0)执行代码如下:

//代码路径:kernel/sched.h:
#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,_current\n\t" \
    "je 1f\n\t" \
    "movw %%dx,%1\n\t" \
    "xchgl %%ecx,_current\n\t" \
    "ljmp %0\n\t" \    //跳转到进程0,参看3.2节中有关switch_to(n)的讲解及代码解释
    "cmpl %%ecx,_last_task_used_math\n\t" \    
    "jne 1f\n\t" \
    "clts\n" \
    "1:" \
    ::"m" (*&__tmp.a),"m" (*&__tmp.b), \
    "d" (_TSS(n)),"c" ((long) task[n])); \
}

switch_to (0)执行完后,已经切换到进程0去执行。前面3.2节中已经说明,当时进程0切换到进程1时,是从switch_to (1)的"ljmp %0nt"这一行切换走的,TSS中保存当时的CPU所有寄存器的值,其中CS、EIP指向的就是它的下一行,所以,现在进程0要从"cmpl %%ecx,_last_task_used_mathnt” 这行代码开始执行,如图3-25中的第三步所示。
将要执行的代码如下:

//代码路径:kernel/sched.h:
#define switch_to(n) {\
struct {long a,b;} __tmp; 
__asm__("cmpl %%ecx,_current\n\t" \
    "je 1f\n\t" \
    "movw %%dx,%1\n\t" \
    "xchgl %%ecx,_current\n\t" \
    "ljmp %0\n\t" \
    "cmpl %%ecx,_last_task_used_math\n\t"\//从这一行开始执行,此时是进程0在执行,0特权级
    "jne 1f\n\t" \
    "clts\n" \
    "1:" \
    ::"m" (*&__tmp.a),"m" (*&__tmp.b), \
    "d" (_TSS(n)),"c" ((long) task[n])); \
}

回顾3.2节,当时进程0切换到进程1是从pause( )、sys_pause( )、schedule( )、switch_to(1)这个调用路线执行过来的。现在,switch_to(1)后半部分执行完毕后,就应该返回sys_pause( )、for(;;)pause( )中执行了。
pause这个函数将在for(;;)这个循环里面被反复调用,所以,会继续调用schedule函数进行进程切换。而再次切换的时候,由于两个进程还都不是就绪态,按照前面讲述过的理由,当所有进程都挂起的时候,内核会执行switch_to强行切换到进程0。
现在,switch_to中情况有些变化,"cmpl %%ecx,_currentnt” “je 1fnt”的意思是:如果切换到的进程就是当前进程,就跳转到下面的“1:”处直接返回。此时当前进程正是进程0,要切换到的进程也是进程0,正好符合这个条件。
执行代码如下:

//代码路径:init/main.c:
void main(void)
{
   …
   for(;;) pause();
}

//代码路径:kernel/sched.h:
#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,_current\n\t" \
    "je 1f\n\t" \
    "movw %%dx,%1\n\t" \
    "xchgl %%ecx,_current\n\t" \
    "ljmp %0\n\t" \
    "cmpl %%ecx,_last_task_used_math\n\t" \
    "jne 1f\n\t" \
    "clts\n" \
    "1:" \
    ::"m" (*&__tmp.a),"m" (*&__tmp.b), \
    "d" (_TSS(n)),"c" ((long) task[n])); \
}

所以,又回到进程0(注意:不是切换到进程0)。
循环执行这个动作,如图3-26所示。
从这里可以看出操作系统的设计者为进程0设计的特殊职能:当所有进程都挂起或没有任何进程执行的时候,进程0就会出来维持操作系统的基本运转,等待挂起的进程具备可执行的条件。业内人士也称进程0为怠速进程,很像维持汽车等待驾驶员踩油门的怠速状态那样维护计算机的怠速状态。
注意:硬盘的读写速度远低于CPU执行指令的速度(2~3个量级)。现在,硬盘仍在忙着把指定的数据读到它的缓存中……

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

6.进程0执行过程中发生硬盘中断
循环执行了一段时间后,硬盘在某一时刻把一个扇区的数据读出来了,产生硬盘中断。CPU接到中断指令后,终止正在执行的程序,终止的位置肯定是在pause( )、sys_pause( )、schedule( )、switch_to (n)循环里面的某行指令处,如图3-27中的第一步所示。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

然后转去执行硬盘中断服务程序。执行代码如下:

//代码路径:kernel/system_call.s:
    …
_hd_interrupt:
    pushl %eax            //保存CPU的状态
    pushl %ecx
    pushl %edx
    push %ds
    push %es
    push %fs
    movl $0x10,%eax
    mov %ax,%ds
    mov %ax,%es
    movl $0x17,%eax
    mov %ax,%fs
    movb $0x20,%al
    outb %al,$0xA0        
    jmp 1f            
1:    jmp 1f
1:    xorl %edx,%edx
    xchgl _do_hd,%edx
    testl %edx,%edx
    jne 1f
    movl $_unexpected_hd_interrupt,%edx
1:    outb %al,$0x20
    call *%edx        
    …

别忘了中断会自动压栈ss、esp、eflags、cs、eip,硬盘中断服务程序的代码接着将一些寄存器的数据压栈以保存程序的中断处的现场。之后,执行_do_hd处的读盘中断处理程序,对应的代码应该是call *%edx这一行。这个edx里面是读盘中断处理程序read_intr的地址,参看hd_out( )函数的讲解及代码注释。
read_intr( )函数会将已经读到硬盘缓存中的数据复制到刚才被锁定的那个缓冲块中(注意:锁定是阻止进程方面的操作,而不是阻止外设方面的操作),这时1个扇区256字(512字节)的数据读入前面申请到的缓冲块,如图3-27中的第二步所示。执行代码如下:

//代码路径:kernel/blk_dev/hd.c:
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++;
    if (--CURRENT->nr_sectors) {
         do_hd= &read_intr;
         return;
    }
    end_request(1);
    do_hd_request(); 
}

但是,引导块的数据是1024字节,请求项要求的也是1024字节,现在仅读出了一半,硬盘会继续读盘。与此同时,在得知请求项对应的缓冲块数据没有读完的情况下,内核将再次把read_intr( )绑定在硬盘中断服务程序上,以待下次使用,之后中断服务程序返回。
进程1仍处在被挂起状态,pause( )、sys_pause( )、schedule( )、switch_to(0)循环从刚才硬盘中断打断的地方继续循环,硬盘继续读盘……
整个过程如图3-28所示。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

又过了一段时间后,硬盘剩下的那一半数据也读完了,硬盘产生中断,读盘中断服务程序再次响应这个中断,进入read_intr( )函数后,仍然会判断请求项对应的缓冲块的数据是否读完了,对应代码如下:

//代码路径:kernel/blk_dev/hd.c:
static void read_intr(void)
{
    …
    if (--CURRENT->nr_sectors)
    …
    end_request(1);
    …
}

这次已经将请求项要求的数据量全部读完了,经检验确认完成后,不执行if里面的内容了,跳到end_request( )函数去执行,如图3-29中read_intr( )这个函数所示。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

进入end_request( )后,由于此时缓冲块的内容已经全部读进来了,将这个缓冲块的更新标志b_uptodate置1,说明它可用了,执行代码如下:

//代码路径:kernel/blk_dev/blk.h:
extern inline void end_request(int uptodate) 
{
    DEVICE_OFF(CURRENT->dev);
    if (CURRENT->bh) {
         CURRENT->bh->b_uptodate= uptodate;    // uptodate是参数,为1
         unlock_buffer(CURRENT->bh);
    }
    if (!uptodate) {
         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;
}

之后,调用unlock_buffer( )函数为缓冲块解锁。在unlock_buffer( )函数中调用wake_up( )函数,将等待这个缓冲块解锁的进程(进程1)唤醒(设置为就绪态),并对刚刚使用过的请求项进行处理,如将它对应的请求项设置为空闲……执行代码如下:

//代码路径:kernel/blk_dev/blk.h:
extern inline void unlock_buffer(struct buffer_head * bh)
{
    if (!bh->b_lock)
         printk(DEVICE_NAME ": free buffer being unlocked\n");
    bh->b_lock=0;
    wake_up(&bh->b_wait);
}

//代码路径:kernel/sched.c:
void wake_up(struct task_struct **p)
{
    if (p && *p) {
         (**p).state=0;        //设置为就绪态
         *p=NULL;
    }
}

硬盘中断处理结束,也就是载入硬盘引导块的工作结束后,计算机在pause( )、sys_pause( )、schedule( )、switch_to(0)循环中继续执行,如图3-29中第三步所示。
7.读盘操作完成后,进程调度切换到进程1执行
现在,引导块的两个扇区已经载入内核的缓冲块,进程1已经处于就绪态。注意:虽然进程0一直参与循环运行,但它是非就绪态。现在只有进程0和进程1,当循环执行到schedule函数时就会切换进程1去执行。该过程如图3-30所示。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

切换到进程1后,进程1从下面的代码继续执行:

//代码路径:kernel/sched.h:
#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,_current\n\t" \
    "je 1f\n\t" \
    "movw %%dx,%1\n\t" \
    "xchgl %%ecx,_current\n\t" \
    "ljmp %0\n\t" \
    "cmpl %%ecx,_last_task_used_math\n\t"\    //理由和前面讲述的switch_to一样
    "jne 1f\n\t" \
    "clts\n" \
    "1:" \
    ::"m" (*&__tmp.a),"m" (*&__tmp.b), \
    "d" (_TSS(n)),"c" ((long) task[n])); \
}

可以看出,所有进程间的切换都是这个模式。
进程1是从"ljmp %0nt" 切换走的,所以现在执行它的下一行。现在,返回切换的发起者sleep_on( )函数中,并最终返回bread( )函数中。在bread( )函数中判断缓冲块的b_uptodate 标志已被设置为1,直接返回,bread( )函数执行完毕。执行代码如下:

//代码路径:fs/buffer.c:
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;
}

回到sys_setup函数继续执行,处理硬盘引导块载入缓冲区后的事务。缓冲块里面装载着硬盘的引导块的内容,先来判断硬盘信息有效标志'55AA'。如果第一个扇区的最后2字节不是'55AA',就说明这个扇区中的数据是无效的(我们假设引导块的数据没有问题)。执行代码如下:

//代码路径:kernel/blk_dev/hd.c:
int sys_setup(void * BIOS)
{
    …
    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;            //根据引导块中的分区信息设置hd[]
         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);                    //释放缓冲块(引用计数减1)
    }
    if (NR_HD)
         printk("Partition table%s ok.\n\r",(NR_HD>1)?"s":"");
    …
}

之后,利用从引导块中采集到的分区表信息来设置hd[],如图3-31所示。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

读引导块的缓冲块已经完成使命,调用brelse( )函数释放,以便以后继续程序使用。
根据硬盘分区信息设置hd[],为第5章安装硬盘文件系统做准备的工作都已完成。下面,我们将介绍进程1用虚拟盘替代软盘使之成为根设备,为加载根文件系统做准备。
3.3.2 进程1格式化虚拟盘并更换根设备为虚拟盘
第2章的2.3节设置了虚拟盘空间并初始化。那时的虚拟盘只是一块“白盘”,尚未经过类似“格式化”的处理,还不能当做一个块设备使用。格式化所用的信息就在boot操作系统的软盘上。第1章讲解过,第一个扇区是bootsect,后面4个扇区是setup,接下来的240个扇区是包含head的system模块,一共有245个扇区。“格式化”虚拟盘的信息从256扇区开始。
下面,进程1调用rd_load( )函数,用软盘上256以后扇区中的信息“格式化”虚拟盘,使之成为一个块设备。
执行代码如下:

//代码路径:kernel/blk_dev/hd.c:
int sys_setup(void * BIOS)
{
    …
    if (NR_HD)
         printk("Partition table%s ok.\n\r",(NR_HD>1)?"s":"");
    rd_load();
    mount_root();
    return (0);
}

进入rd_load( )函数后,调用breada( )函数从软盘预读一些数据块,也就是“格式化”虚拟盘需要的引导块、超级块。
注意:现在根设备是软盘。
breada( )和bread( )函数类似,不同点在于可以把一些连续的数据块都读进来,一共三块,分别是257、256和258,其中引导块在256(尽管引导块并未实际使用)、超级块在257中。从软盘上读取数据块与bread读硬盘上的数据块原理基本一致,具体情况参看3.3.1节的讲解。读取完成后的状态如图3-32所示。可以看出3个连续的数据块被读入了高速缓冲区的缓冲块中,其中,超级块用红色框标注。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

之后,分析超级块信息,包括判断文件系统是不是minix文件系统、接下来要载入的根文件系统的数据块数会不会比整个虚拟盘区都大……这些条件都通过,才能继续加载根文件系统。分析完毕,释放缓冲块。
整个过程如图3-33所示。
执行代码如下:

//代码路径:kernel/blk_dev/ramdisk.c:
void rd_load(void)
{
    struct buffer_head *bh;
    struct super_block    s;
    int        block= 256;    /* Start at block 256 */
    int        I= 1;
    int        nblocks;
    char        *cp;        /* Move pointer */
    
    if (!rd_length)
         return;
    printk("Ram disk: %d bytes, starting at 0x%x\n", rd_length,
         (int) rd_start);
    if (MAJOR(ROOT_DEV) != 2)        //如果根设备不是软盘
         return;
    bh= breada(ROOT_DEV,block + 1,block,block + 2,-1);
    if (!bh) {
         printk("Disk error while looking for ramdisk!\n");
         return;
    }
    *((struct d_super_block *) &s)= *((struct d_super_block *) bh->b_data);
    brelse(bh);
    if (s.s_magic != SUPER_MAGIC)        //如果不等,说明不是minix文件系统
         /* No ram disk image present, assume normal floppy boot */
         return;
    nblocks= s.s_nzones << s.s_log_zone_size;        //算出虚拟盘的块数
    if (nblocks > (rd_length >> BLOCK_SIZE_BITS)) {
         printk("Ram disk image too big!  (%d blocks, %d avail)\n", 
               nblocks, rd_length >> BLOCK_SIZE_BITS);
         return;
    }
    printk("Loading %d bytes into ram disk... 0000k", 
         nblocks << BLOCK_SIZE_BITS);
    …
}

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

接下来调用breada( )函数,把与文件系统相关的内容,从软盘上拷贝到虚拟盘中,然后及时释放缓冲块,最终完成“格式化”这个过程,如图3-34所示。
复制结束后,将虚拟盘设置为根设备。
执行代码如下:

//代码路径:kernel/blk_dev/ramdisk.c:
void rd_load(void)
{
    …
    printk("Loading %d bytes into ram disk... 0000k", 
         nblocks << BLOCK_SIZE_BITS);
    cp= rd_start;
    while (nblocks) {        //将软盘上准备格式化用的根文件系统复制到虚拟盘上
         if (nblocks > 2) 
               bh= breada(ROOT_DEV, block, block + 1, block + 2, -1);
         else
               bh= bread(ROOT_DEV, block);
         if (!bh) {
               printk("I/O error on block %d, aborting load\n", 
                     block);
               return;
         }
         (void) memcpy(cp, bh->b_data, BLOCK_SIZE);
         brelse(bh);
         printk("\010\010\010\010\010%4dk",i);
         cp += BLOCK_SIZE;
         block++;
         nblocks--;
         i++;
    }
    printk("\010\010\010\010\010done \n");
    ROOT_DEV=0x0101;                //设置虚拟盘为根设备
}

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

下面将要介绍在虚拟盘这个根设备上加载根文件系统。
3.3.3 进程1在根设备上加载根文件系统
操作系统中加载根文件系统涉及文件、文件系统、根文件系统、加载文件系统、加载根文件系统这几个概念。为了更容易理解,这里我们只讨论块设备,也就是软盘、硬盘、虚拟盘(有关块设备的详细讨论请阅读第5、7章)。
操作系统中的文件系统可以大致分成两部分;一部分在操作系统内核中,另一部分在硬盘、软盘、虚拟盘中。
文件系统是用来管理文件的。文件系统用i节点来管理文件,一个i节点管理一个文件,i节点和文件一一对应。文件的路径在操作系统中由目录文件中的目录项管理,一个目录项对应一级路径,目录文件也是文件,也由i节点管理。一个文件挂在一个目录文件的目录项上,这个目录文件根据实际路径的不同,又可能挂在另一个目录文件的目录项上。一个目录文件有多个目录项,可以形成不同的路径。效果如图3-35所示。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

所有的文件(包括目录文件)的i节点最终挂接成一个树形结构,树根i节点就叫这个文件系统的根i节点。一个逻辑设备(一个物理设备可以分成多个逻辑设备,比如物理硬盘可以分成多个逻辑硬盘)只有一个文件系统,一个文件系统只能包含一个这样的树形结构,也就是说,一个逻辑设备只能有一个根i节点。
加载文件系统最重要的标志,就是把一个逻辑设备上的文件系统的根i节点,关联到另一个文件系统的i节点上。具体是哪一个i节点,由操作系统的使用者通过mount命令决定。
逻辑效果如图3-36所示。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

另外,一个文件系统必须挂接在另一个文件系统上,按照这个设计,一定存在一个只被其他文件系统挂接的文件系统,这个文件系统就叫根文件系统,根文件系统所在的设备就叫根设备。
别的文件系统可以挂在根文件系统上,根文件系统挂在哪呢?
挂在super_block[8]上。
Linux 0.11操作系统中只有一个super_block[8],每个数组元素是一个超级块,一个超级块管理一个逻辑设备,也就是说操作系统最多只能管理8个逻辑设备,其中只有一个根设备。加载根文件系统最重要的标志就是把根文件系统的根i节点挂在super_block[8]中根设备对应的超级块上。
可以说,加载根文件系统有三个主要步骤:
1)复制根设备的超级块到super_block[8]中,将根设备中的根i节点挂在super_block[8]中对应根设备的超级块上。
2)将驻留缓冲区中16个缓冲块的根设备逻辑块位图、i节点位图分别挂接在super_block[8]中根设备超级块的s_zmap[8]、 s_imap[8]上。
3)将当前进程的pwd、root指针指向根设备的根i节点。
加载根文件系统和安装硬盘文件系统完成后的总体效果如图3-37所示。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

进程1通过调用mount_root( )函数实现在根设备虚拟盘上加载根文件系统。执行代码如下:

//代码路径:kernel/blk_dev/hd.c:
   int sys_setup(void * BIOS)
{
    …
         brelse(bh);
    }
    if (NR_HD)
         printk("Partition table%s ok.\n\r",(NR_HD>1)?"s":"");
    rd_load();
    mount_root();        //加载根文件系统
    return (0);
}

1.复制根设备的超级块到super_block[8]中
进入mount_root( )函数后,初始化内存中的超级块super_block[8],将每一项所对应的设备号加锁标志和等待它解锁的进程全部设置为0。系统只要想和任何一个设备以文件的形式进行数据交互,就要将这个设备的超级块存储在super_block[8]中,这样可以通过super_block[8]获取这个设备中文件系统的最基本信息,根设备中的超级块也不例外,如图3-38
所示。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

执行代码如下:

//代码路径:fs/super.c:
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[64],为后续程序做准备
         file_table[i].f_count=0;     
   if (MAJOR(ROOT_DEV)== 2) {    //2代表软盘,此时根设备是虚拟盘,是1。反之,没有虚拟盘,
                    //则加载软盘的根文件系统
         printk("Insert root floppy and press ENTER");
         wait_for_keypress();
    }

    //初始化super_block[8]
    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");
    …
}

前面的rd_load( )函数已经“格式化”好虚拟盘,并设置为根设备。接下来调用read_super( )函数,从虚拟盘中读取根设备的超级块,复制到super_block[8]中。
执行代码如下:

//代码路径:fs/super.c:
void mount_root(void) 
{
    …
    if (!(p=read_super(ROOT_DEV)))
         panic("Unable to mount root");
    …
}

在read_super( )函数中,先检测这个超级块是不是已经被读进super_block[8]中了。如果已经被读进来了,则直接使用,不需要再加载一次了。这与本章3.3.1节中先通过哈希表来检测缓冲块是否已经存在的道理是一样的。
执行代码如下:

//代码路径:fs/super.c:
static struct super_block * read_super(int dev) 
{
    struct super_block * s;
    struct buffer_head * bh;
    int i,block;

    if (!dev)
         return NULL;
    check_disk_change(dev);        //检查是否换过盘,并做相应处理
    if (s= get_super(dev))
         return s;
     …
}

因为此前没有加载过根文件系统,所以要在super_block[8]中申请一项。从图3-39中可以看出,此时找到的是super_block[8]结构中的第一项。然后进行初始化并加锁,准备把根设备的超级块读出。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

对应的代码如下:

//代码路径:fs/super.c:
static struct super_block * read_super(int dev) 
{
    …
    for (s= 0 + super_block ;; s++) {
         if (s >= NR_SUPER + super_block)    // NR_SUPER是8
               return NULL;
         if (!s->s_dev)
               break;
            }
            s->s_dev= dev;
           s->s_isup= NULL;
             s->s_imount= NULL;
             s->s_time= 0;
             s->s_rd_only= 0;
             s->s_dirt= 0;
            lock_super(s);                //锁定超级块
    …
}

调用bread( )函数,把超级块从虚拟盘上读进缓冲区,并从缓冲区复制到super_block[8]的第一项。 bread( )函数在3.3.1节中已经说明。这里有一点区别,在3.3.1节中提到,如果给硬盘发送操作命令,则调用do_hd_request( )函数,而此时操作的是虚拟盘,所以要调用do_rd_request( )函数。值得注意的是,虚拟盘虽然被视为外设,但它毕竟是内存里面一段空间,并不是实际的外设,所以,调用do_rd_request( )函数从虚拟盘上读取超级块,不会发生类似硬盘中断的情况。
超级块复制进缓冲块以后,将缓冲块中的超级块数据复制到super_block[8]的第一项。从现在起,虚拟盘这个根设备就由super_block[8]的第一项来管理,之后调用brelse( )函数释放这个缓冲块,如图3-40所示。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

执行代码如下:

//代码路径:fs/super.c:
static struct super_block * read_super(int dev) 
{
    …
    if (!(bh= bread(dev,1))) {            //读根设备的超级块到缓冲区
         s->s_dev=0;
         free_super(s);                //释放超级块
         return NULL;
    }
    *((struct d_super_block *) s)=        //将缓冲区中的超级块复制到        
    *((struct d_super_block *) bh->b_data);    // super_block[8]第一项
    brelse(bh);                    //释放缓冲块
    if (s->s_magic != SUPER_MAGIC) {        //判断超级块的魔数(SUPER_MAGIC)是否正确
         s->s_dev= 0;
         free_super(s);                //释放超级块
         return NULL;
    }
    …
}

初始化super_block[8]中的虚拟盘超级块中的i节点位图s_imap、逻辑块位图s_zmap,并把虚拟盘上i节点位图、逻辑块位图所占用的所有逻辑块读到缓冲区,将这些缓冲块分别挂接到s_imap[8]和s_zmap[8]上。由于对它们的操作会比较频繁,所以这些占用的缓冲块并不被释放,它们将常驻在缓冲区内。
如图3-41所示,超级块通过指针与s_imap和s_zmap实现挂接。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

执行代码如下:

//代码路径:fs/super.c:
static struct super_block * read_super(int dev) 
{
    …
    for (i=0;i<I_MAP_SLOTS;i++)    //初始化s_imap[8]、s_zmap[8]
         s->s_imap[i]= NULL;
    for (i=0;i<Z_MAP_SLOTS;i++)
         s->s_zmap[i]= NULL;
    block=2;            //虚拟盘的第一块是超级块,第二块开始是i节点位图和逻辑块位图
    for (i=0;i < s->s_imap_blocks;i++)        //把虚拟盘上i节点位图所占用的所有逻辑块
         if (s->s_imap[i]=bread(dev,block))    //读到缓冲区,分别挂接到s_imap[8]上
               block++;
         else
               break;
    for (i=0;i < s->s_zmap_blocks;i++)        //把虚拟盘上逻辑块位图所占用的所有逻辑块
         if (s->s_zmap[i]=bread(dev,block))    //读到缓冲区,分别挂接到s_zmap[8]上
               block++;
         else
               break;   
    if (block != 2 + s->s_imap_blocks + s->s_zmap_blocks){    //如果i节点位图、逻辑块位
         for(i=0;i<I_MAP_SLOTS;i++)    //图所占用的块数不对,说明操作系统有问题,则应释放前
               brelse(s->s_imap[i]);    // 面获得的缓冲块及超级块
         for(i=0;i<Z_MAP_SLOTS;i++)
               brelse(s->s_zmap[i]);
         s->s_dev=0;
         free_super(s);
         return NULL;
    }s->s_imap[0]->b_data[0] |= 1;    //牺牲一个i节点,以防止查找算法返回0 
    s->s_zmap[0]->b_data[0] |= 1;        //与0号i节点混淆
    free_super(s);
    return s;
}

2.将根设备中的根i节点挂在super_block[8]中根设备超级块上
回到mount_root( )函数中,调用iget( )函数,从虚拟盘上读取根i节点。根i节点的意义在于,通过它可以到文件系统中任何指定的i节点,也就是能找到任何指定的文件。
执行代码如下:

//代码路径:fs/super.c:
void mount_root(void)
{
   …
   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");    
    …
}

进入iget( )函数后,操作系统从i节点表inode_table[32]中申请一个空闲的i节点位置(inode_table[32]是操作系统用来控制同时打开不同文件的最大数)。此时应该是首个i节点。对这个i节点进行初始化设置,其中包括该i节点对应的设备号、该i节点的节点号……
图3-42中给出了根目录i节点在内核i节点表中的位置。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

对应代码如下:

//代码路径:fs/inode.c:
struct m_inode * iget(int dev,int nr) 
{
     struct m_inode * inode, * empty;

    if (!dev)
         panic("iget with dev==0");
    empty= get_empty_inode();        //从inode_table[32]中申请一个空闲的i节点
    inode= inode_table;
    while (inode < NR_INODE + inode_table){    //查找与参数相同的inode
         if (inode->i_dev != dev || inode->i_num != nr) {
               inode++;
               continue;
         }
         wait_on_inode(inode);            //等待解锁
         if (inode->i_dev != dev || inode->i_num != nr){    //如等待期间发生
               inode= inode_table;        //变化,继续查找
               continue;
         }
         inode->i_count++;
         if (inode->i_mount) {
               int i;

               for (i= 0;i<NR_SUPER;i++)        //如是mount点,则查找对应的超级块
                     if (super_block[i].s_imount==inode)
                                break;
               if (i >= NR_SUPER) {
                     printk("Mounted inode hasn't got sb\n");
                     if (empty)
                                iput(empty);
                     return inode;
               }
               iput(inode);
               dev= super_block[i].s_dev;    //从超级块中获取设备号
               nr= ROOT_INO;            // ROOT_INO为1,根i节点号
               inode= inode_table;
               continue;
         }
         if (empty)
               iput(empty);
         return inode;
    }
    if (!empty)
         return (NULL);
    inode=empty;
    inode->i_dev= dev;                //初始化
    inode->i_num= nr;
    read_inode(inode);                //从虚拟盘上读出根i节点
    return inode;
}

在read_inode( )函数中,先给inode_table[32]中的这个i节点加锁。在解锁之前,这个i节点就不会被别的程序占用。之后,通过该i节点所在的超级块,间接地计算出i节点所在的逻辑块号,并将i节点所在的逻辑块整体读出,从中提取这个i节点的信息,载入刚才加锁的i节点位置上,如图3-43所示,注意inode_table[32]中的变化。最后,释放缓冲块并将锁定的i节点解锁。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

执行的代码如下:

//代码路径:fs/inode.c:
static void read_inode(struct m_inode * inode) 
{
    …
    lock_inode(inode);                //锁定inode
    if (!(sb=get_super(inode->i_dev)))        //获得inode所在设备的超级块
    …
    block= 2 + sb->s_imap_blocks + sb->s_zmap_blocks +
         (inode->i_num-1)/INODES_PER_BLOCK;
    if (!(bh=bread(inode->i_dev,block)))        //读inode所在逻辑块进缓冲块
         panic("unable to read i-node block");
    *(struct d_inode *)inode=            //整体复制
         ((struct d_inode *)bh->b_data)
               [(inode->i_num-1)%INODES_PER_BLOCK];
    brelse(bh);                    //释放缓冲块
    unlock_inode(inode);                //解锁
}

回到iget( )函数,将inode指针返回给mount_root( )函数,并赋给mi指针。
下面是加载根文件系统的标志性动作:
将inode_table[32]中代表虚拟盘根i节点的项挂接到super_block[8]中代表根设备虚拟盘的项中的s_isup 、s_imount指针上。这样,操作系统在根设备上可以通过这里建立的关系,一步步地把文件找到。
3.将根文件系统与进程1关联
对进程1的tast_struct中与文件系统i节点有关的字段进行设置,将根i节点与当前进程(现在就是进程1)关联起来,如图3-44所示。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.3 轮转到进程1执行

执行代码如下:

//代码路径:fs/super.c:
void mount_root(void)
{
    …
    if (!(mi=iget(ROOT_DEV,ROOT_INO)))    //根设备的根i节点
         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;            //当前进程(进程1)掌控根文件系统的根i节点, 
    current->root= mi;            //父子进程创建机制将这个特性遗传给子进程
    …
}

得到了根文件系统的超级块,就可以根据超级块中“逻辑块位图”里记载的信息,计算出虚拟盘上数据块的占用与空闲情况,并将此信息记录在3.3.3节中提到的驻留在缓冲区中“装载逻辑块位图信息的缓冲块中”。执行代码如下:

//代码路径:fs/super.c:
void mount_root(void)
{
    …
    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)            //计算虚拟盘中空闲的i节点的总数
         if (!set_bit(i&8191,p->s_imap[i>>13]->b_data))
               free++;
    printk("%d/%d free inodes\n\r",free,p->s_ninodes);
}

到此为止,sys_setup( )函数就全都执行完毕了。因为这个函数也是由于产生软中断才被调用的,所以返回system_call中执行,之后会执行ret_from_sys_call。这时候的当前进程是进程1,所以下面将调用do_signal( )函数(只要当前进程不是进程0,就要执行到这里),对当前进程的信号位图进行检测,执行代码如下:

//代码路径:kernel/system_call.s:
    …
ret_from_sys_call:
    movl     _current,%eax            # task[0] cannot have signals
    cmpl     _task,%eax
    je         3f
    cmpw     $0x0f,CS(%esp)            # was old code segment supervisor ?
    jne         3f
    cmpw     $0x17,OLDSS(%esp)        # was stack segment= 0x17 ?
    jne         3f
    movl     signal(%eax),%ebx        #下面是取信号位图…
    movl     blocked(%eax),%ecx
    notl     %ecx
    andl     %ebx,%ecx
    bsfl     %ecx,%ecx
    je         3f
    btrl     %ecx,%ebx
    movl     %ebx,signal(%eax)
    incl     %ecx
    pushl     %ecx
    call     _do_signal            #调用do_signal()
    …

现在,当前进程(进程1)并没有接收到信号,调用do_signal( )函数并没有实际的意义。
至此,sys_setup( )的系统调用结束,进程1将返回3.3节中讲到的代码的调用点,准备下面代码的执行。

//代码路径:init/main.c:
void init(void)
{
    …
    int pid,i;

    setup((void *) &drive_info);
    (void) open("/dev/tty0",O_RDWR,0);
    (void) dup(0);
    (void) dup(0);
    printf("%d buffers= %d bytes buffer space\n\r",NR_BUFFERS,
         NR_BUFFERS*BLOCK_SIZE);    
    …
}

至此,进程0创建进程1,进程1为安装硬盘文件系统做准备、“格式化”虚拟盘并用虚拟盘取代软盘为根设备、在虚拟盘上加载根文件系统的内容讲解完毕。