vislee / leevis.com

Blog
87 stars 13 forks source link

linux-0.11内核带缓冲的磁盘操作 #177

Open vislee opened 2 years ago

vislee commented 2 years ago

概述

磁盘设备速度和cpu存在着巨大差异,为了提升性能在内存开辟了一块空间作为cpu和磁盘块设备的桥梁,称为磁盘缓冲区。

代码分析

从代码的角度分析一次带缓冲的磁盘操作。涉及到的linux-0.11代码文件有:fs.h、 buffer.c、 blk.h、 hd.c、 ll_rw_blk.c 要操作硬盘需要先了解一点硬盘逻辑结构或者说是文件系统结构。 文件系统由6部分组成,引导块|超级块|i节点位图|逻辑块位图|i 节点|数据区逻辑块 引导块:计算机加电BIOS自动会读入的代码和数据。 超级块:文件系统信息,i 节点数、逻辑块数、i 节点位图所占块、逻辑块位图所在块等 i节点位图:标志i节点是否使用。 逻辑块位图:标志逻辑块位图是否使用。 i 节点:磁盘上文件或目录索引节点。 逻辑块:存放文件或目录数据。

用户上层看到的是目录和文件,实际上读取目录文件是需要经过一系列转化定位到逻辑块的。 转换和定位的过程后面再起一篇来说。先看读写逻辑块bread函数。

buffer.c


struct buffer_head * bread(int dev,int block)
{
    struct buffer_head * bh;

        // 根据设备号和块号获取一个缓冲块
        // 如果没有空闲缓存块,该任务就会被挂在缓冲等待队列buffer_wait睡眠。
        // 所以如果从该函数返回就肯定会分配一个缓冲块,否则就有bug系统异常。
    if (!(bh=getblk(dev,block)))
        panic("bread: getblk returned NULL\n");
        // 缓冲块内容有效。即:和磁盘块内容是一致的。直接返回就可以,不用再从磁盘读取了。
    if (bh->b_uptodate)
        return bh;
        // 否则,从磁盘读取。读取也是异步的。
    ll_rw_block(READ,bh);
        // 跟踪上述函数,返回后。已经把读写磁盘的请求添加到磁盘的电梯队列里了。
        // 等待判断是否已经把数据从磁盘读到了缓冲区。如果没有就把本任务挂到该缓冲区等待队列,让出cpu。
        // 等待电梯队列请求被执行,把磁盘内容读到该缓冲块后唤醒执行。
    wait_on_buffer(bh);
        // 被唤醒调度执行后,从该处恢复执行。首先判断缓冲块内容是否和磁盘一致。
    if (bh->b_uptodate)
        return bh;
        // 如果不一致释放缓冲块返回NULL
    brelse(bh);
    return NULL;
}

bread函数主要是分配缓冲块,调用ll_rw_block函数从磁盘读取数据到缓冲块。 下面看下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_NR是3,回调函数是 DEVICE_REQUEST do_hd_request 在blk.h文件中定义
    !(blk_dev[major].request_fn)) {
        printk("Trying to read nonexistent block-device\n\r");
        return;
    }

        // 构造操作磁盘请求
    make_request(major,rw,bh);
}

// 构造请求,添加到磁盘操作的电梯队列
static void make_request(int major,int rw, struct buffer_head * bh)
{
    struct request * req;
    int rw_ahead;
        ......
        // 读 写
    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.
 */
        // 读请求是优先的,请求队列后1/3只留给读请求
    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;
    req->nr_sectors = 2;
    req->buffer = bh->b_data;   // 缓冲块数据区
    req->waiting = NULL;
    req->bh = bh;
    req->next = NULL;
        // 把请求添加到磁盘电梯队列
        // blk_dev 是支持外设数组,该结构体包含2个元素,回调函数和请求等待队列头指针。在blk.h文件定义。
    add_request(major+blk_dev,req);
}

// 添加请求到电梯队列
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;
        // 磁盘电梯队列为空,则直接调用初始化的回调函数do_hd_request
    if (!(tmp = dev->current_request)) {
        dev->current_request = req;
        sti();
        (dev->request_fn)();
        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();
}

到此,我们已经跟踪完带缓冲的读操作。 大概的流程就是:查找一个缓冲块,构造一个磁盘读请求添加到磁盘电梯队列,挂起本任务,等待磁盘数据读到缓冲块后被唤醒继续执行返回缓冲块内容。

接下来分析一下磁盘操作函数函数do_hd_request的大概流程。

首先在操作系统main.c文件main函数中,调用磁盘初始化函数hd_init注册了磁盘操作回调函数do_hd_request 和 磁盘中断回调函数 hd_interrupt.

hd.c文件

void hd_init(void)
{
    blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;
        // 注册磁盘中断回调函数。 hd_interrupt 在sys_call.s中定义,是个汇编函数_hd_interrupt。
        // 因为gcc编译器会对c函数添加前缀(_),因此 c语言的hd_interrupt 在汇编符号就是_ hd_interrupt.
        // hd_interrupt函数,我们只关注_do_hd这个函数(符号)就可以。大概流程就是磁盘中断最终会调用该函数。
    set_intr_gate(0x2E,&hd_interrupt);
    outb_p(inb_p(0x21)&0xfb,0x21);
    outb(inb_p(0xA1)&0xbf,0xA1);
}

// 该函数被添加到电梯队列的首任务调度。
// 主要的作用就是遍历电梯队列向磁盘发指令。
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;
    }

        ......

    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) {
                // 读磁盘请求,向磁盘发指令,具体涉及到磁盘专业的知识,和本文关系不大。知道大概流程和意思即可。
                // 关键是最后一个参数read_intr,就是读磁盘请求结束后的磁盘中断的回调函数。
                // 在hd_out 中,会把最后一个参数的函数指针付给do_hd,而do_hd就是前面说的磁盘中断回调函数。
        hd_out(dev,nsect,sec,head,cyl,WIN_READ,&read_intr);
    } else
        panic("unknown hd-command");
}

// 磁盘读请求结束后的磁盘中断回调函数
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;
    }
        // 本次请求读取的扇区已经全部读完
        // 设置缓冲块b_uptodate标志,表示缓冲块数据有效。
        // ***并唤醒等待该请求的进程***
    end_request(1);
        // 继续遍历磁盘电梯队列的请求,向磁盘发送操作指令
    do_hd_request();
}

总结

最后在总一个总结,调用bread 函数读取外设数据,先根据外设号和磁盘块获取一个缓冲块,然后调用ll_rw_block函数, ll_rw_block函数根据外设号,找到注册的外设驱动,然后调用 make_request函数创建读外设请求。 make_request函数先从请求数组中获取一个空闲请求结构,赋值后调用add_request函数添加到对应外设驱动的请求对列里。add_request函数中,首先判断对以外设驱动的请求队列是否为空,如果是空则添加进去就调用外设回调函数向外设发指令。如果不为空,则说明已经有向外设发指令,排队等待执行即可。该任务也被挂起,等待磁盘响应后再被调度执行。 硬盘的回调函数是do_hd_request函数,该函数先赋值磁盘中断回调函数,读请求为:read_intr或 写请求为:write_intr。 当磁盘处理完请求后,就会给cpu发送0x2E中断,中断回调函数最后会调用read_intrwrite_intr。 在read_intr函数中,把磁盘的响应数据保存到缓冲块buffer中,唤醒对应任务的请求,等待被再次调度执行然后bread函数返回缓冲块。