zoukankan      html  css  js  c++  java
  • linux源码解读(二):文件系统——高速缓存区

      用户的应用程序会经常读写磁盘文件的数据到内存,但是内存的速度和磁盘的速度理论上差了好几个数量级;为了更高效地解决内存和磁盘的速度差,linux也在内存使用了缓存区(作用类似于cpu内部为了解决寄存器和内存速度差异的的L1、L2、L3 cache):如果数据要写入磁盘文件,先放在缓存区,等凑够了一定数量后再批量写入磁盘文件,借此减少磁盘寻址的次数,来提升写入效率(这里多说几句:比如U盘插上电脑后,如果要拔出,建议先卸载再拔出,而不是直接拔出,为啥了?U盘的数据也是先放入缓冲区的,缓冲区有自己的管理机制,很久没有使用的块可以给其他进程使用,如果是脏块则要进行写盘。缓冲在某些情况下才会有写盘操作,所以要拔出U盘时,应该先进行卸载,这样才会写盘,否则数据可能丢失,文件系统可能损坏。);如果从磁盘读数据,也会先放入缓存区暂存,一旦有其他进程或线程读取同样的磁盘文件,这是就可以先从内存的缓存区取数据了,没必要重新从磁盘读取,也提升了效率!linux 0.11的缓冲区是怎么工作的了?

      在main.c的main函数中,有设置缓存区的大小,代码如下:内存不同,缓存区的大小也不同,linux是怎么管理和使用这些缓存区了的?

    void main(void)        /* This really IS void, no error here. */
    {            /* The startup routine assumes (well, ...) this */
    /*
     * Interrupts are still disabled. Do necessary setups, then
     * enable them
     */
    //前面这里做的所有事情都是在对内存进行拷贝
         ROOT_DEV = ORIG_ROOT_DEV;//设置操作系统的根文件
         drive_info = DRIVE_INFO;//设置操作系统驱动参数
         //解析setup.s代码后获取系统内存参数
        memory_end = (1<<20) + (EXT_MEM_K<<10);
        //取整4k的内存大小
        memory_end &= 0xfffff000;
        if (memory_end > 16*1024*1024)//控制操作系统的最大内存为16M
            memory_end = 16*1024*1024;
        if (memory_end > 12*1024*1024) 
            buffer_memory_end = 4*1024*1024;//设置高速缓冲区的大小,跟块设备有关,跟设备交互的时候,充当缓冲区,写入到块设备中的数据先放在缓冲区里,只有执行sync时才真正写入;这也是为什么要区分块设备驱动和字符设备驱动;块设备写入需要缓冲区,字符设备不需要是直接写入的
        else if (memory_end > 6*1024*1024)
            buffer_memory_end = 2*1024*1024;
        else
            buffer_memory_end = 1*1024*1024;
        main_memory_start = buffer_memory_end;

      1、cpu有分页机制,硬件上以4KB为单位把内存分割成小块供程序使用;这个颗粒度是比较大的,有些时候可能会浪费比较多的内存,所以linux缓存区采用了1KB的大小来分割整个缓存区;假设缓存区有2MB,那么一共被分割成了2000个小块,这么多的缓存区该怎么管理了?

      每个缓存都有各自的属性,比如是否使用、数据是否更新、缓存数据在磁盘的位置、缓存的起始地址等,要想统一管理这么多的属性,最好的办法自然是构建结构体了;一个结构体管理1块(也就是1KB)的缓存区;假设这里有2000个缓存区,就需要2000个结构体,那么问题又来了:这个多的结构体,又该怎么去管理了? 

      参考前面的进程task结构体管理方式:用task数组来管理所有的进程task结构体,最大限制为64个进程,但是放在这里显然不适用:不同机器的物理内存大小是不同的,导致缓存区的block数量是不同的,但数组最大的缺点就是定长,无法适应不同的物理内存,那么这里最适合的只剩链表了,所以linux 0.11版本使用的结构体如下:

    struct buffer_head {
        char * b_data;            /* pointer to data block (1024 bytes):单个数据块大小1KB */
        unsigned long b_blocknr;    /* block number */
        unsigned short b_dev;        /* device (0 = free) */
        unsigned char b_uptodate;    /*数据是否更新*/
        unsigned char b_dirt;        /* 0-clean空现,1-dirty已被占用*/
        unsigned char b_count;        /* users using this block */
        /*如果缓冲区的某个block被锁,上层应用是没法从这个block对应的磁盘空间读数据的,这里有个漏洞:
        A进程锁定了某block,B进程想办法解锁,然后就能监听A进程从磁盘读写了哪些数据
        */
        unsigned char b_lock;        /* 0 - ok, 1 -locked:锁用于多进程/多线程之间同步,避免数据出错*/
        struct task_struct * b_wait;/*A正在使用这个缓存,并已经锁定;B也想用,就用这个字段记录;等A用完后从这里找到B再给B用*/
        struct buffer_head * b_prev;
        struct buffer_head * b_next;
        struct buffer_head * b_prev_free;
        struct buffer_head * b_next_free;
    };

      每个字段的含义都在注释了,这里不再赘述;既然采用了链表,解决了数组只能定长的缺点,但是链表本身也有缺点:无法直接找到目标实例,需要挨个遍历链表上的每个节点;还是假设有2000个块,好巧的不巧的是程序所需的block刚好在最后一个节点,那么需要遍历1999个节点才能到达,效率非常低,这又该怎么解决了?刚好这种快速寻址(时间复杂度O(1))是数组的优势,怎么解决数组和链表各自的优势了?-----hash表!

      linux 0.11版本采用hash表的方式快速寻址,hash映射算法如下:

    // hash表的主要作用是减少查找比较元素所花费的时间。通过在元素的存储位置与关
    // 键字之间建立一个对应关系(hash函数),我们就可以直接通过函数计算立刻查询到指定
    // 的元素。建立hash函数的指导条件主要是尽量确保散列在任何数组项的概率基本相等。
    // 建立函数的方法有多种,这里Linux-0.11主要采用了关键字除留余数法。因为我们
    // 寻找的缓冲块有两个条件,即设备号dev和缓冲块号block,因此设计的hash函数肯定
    // 需要包含这两个关键值。这两个关键字的异或操作只是计算关键值的一种方法。再对
    // 关键值进行MOD运算就可以保证函数所计算得到的值都处于函数数组项范围内。
    #define _hashfn(dev,block) (((unsigned)(dev^block))%NR_HASH)
    #define hash(dev,block) hash_table[_hashfn(dev,block)]

      映射的算法也很简单:每个buffer_head结构体都有dev和block两个字段,这两个字段组合起来本身是不会重复的,所以把这两个字段异或后模上hash表的长度,就得到了hash数组的偏移;现在问题又来了:这个版本的hash_table数组长度设定为NR_HASH=307,远不如buffer_head的实例个数,肯定会发生hash冲突,这个该怎么解决了?--这里就要用上链表变长的优点了:把发生hash冲突的bufer_head实例首位相接不久得了么?最终的hash_table示意图如下:hash表本身用数组,存储buffer_head实例的地址;如果发生hash冲突,相同hash偏移的实例通过b_next和b_prev链表首尾连接!

       

       当这个一整套存储机制建立后,怎么检索了?linux的检索方式如下:先通过dev和block号定位到hash表的偏移,再遍历该偏移处的所有节点,通过比对dev和block号找到目标buffer_head实例

    //// 利用hash表在高速缓冲区中寻找给定设备和指定块号的缓冲区块。
    // 如果找到则返回缓冲区块的指针,否则返回NULL。
    static struct buffer_head * find_buffer(int dev, int block)
    {        
        struct buffer_head * tmp;
    
        // 搜索hash表,寻找指定设备号和块号的缓冲块。
        for (tmp = hash(dev,block) ; tmp != NULL ; tmp = tmp->b_next)
            if (tmp->b_dev==dev && tmp->b_blocknr==block)
                return tmp;
        return NULL;
    }

       根据dev和block号找到缓存区的buffer_head并不代表万事大吉,因为该缓存区可能已经被其他进程/线程占用,当前线程如果一定要用这个缓存区,只能等了,所以最终查找缓存区的代码如下:这里增加了wait_on_buffer函数:

    //// 利用hash表在高速缓冲区中寻找指定的缓冲块。若找到则对该缓冲块上锁
    // 返回块头指针。
    struct buffer_head * get_hash_table(int dev, int block)
    {
        struct buffer_head * bh;
    
        for (;;) {
            // 在高速缓冲中寻找给定设备和指定块的缓冲区块,如果没有找到则返回NULL。
            if (!(bh=find_buffer(dev,block)))
                return NULL;
            // 对该缓冲块增加引用计数,并等待该缓冲块解锁。由于经过了睡眠状态,其他任务可能会更改这个缓存区对应的dev和block号
            // 因此有必要在验证该缓冲块的正确性,并返回缓冲块头指针。
            bh->b_count++;
            wait_on_buffer(bh);
            if (bh->b_dev == dev && bh->b_blocknr == block)
                return bh;
            // 如果在睡眠时该缓冲块所属的设备号或块设备号发生了改变,则撤消对它的
            // 引用计数,重新寻找。
            bh->b_count--;
        }
    }

      wait_on_buffer函数实现:如果发现该缓存区已经上锁,那么调用sleep_on函数让出cpu,阻塞在这里等待;这个sleep_on函数传入的参数是二级指针,并且内部用了tmp变量保存临时变量;由于二级指针是全局的,所以如果有多个task等待同一个缓存区,sleep_on函数是通过先进后出的栈的形式唤醒等待任务的;参考1有详细的说明,感兴趣的小伙伴建议好好看看!

    //// 等待指定缓冲块解锁
    // 如果指定的缓冲块bh已经上锁就让进程不可中断地睡眠在该缓冲块的等待队列b_wait中。
    // 在缓冲块解锁时,其等待队列上的所有进程将被唤醒。虽然是在关闭中断(cli)之后
    // 去睡眠的,但这样做并不会影响在其他进程上下文中影响中断。因为每个进程都在自己的
    // TSS段中保存了标志寄存器EFLAGS的值,所以在进程切换时CPU中当前EFLAGS的值也随之
    // 改变。使用sleep_on进入睡眠状态的进程需要用wake_up明确地唤醒。
    static inline void wait_on_buffer(struct buffer_head * bh)
    {
        cli();                          // 关中断
        while (bh->b_lock)              // 如果已被上锁则进程进入睡眠,等待其解锁
            sleep_on(&bh->b_wait);
        sti();                          // 开中断
    }

       先进后出的栈形式唤醒等待任务:

           

      接下来可能就是buffer.c中最重要的函数之一了:struct buffer_head * getblk(int dev,int block),根据设备号和块号得到buffer_head的实例,便于后续使用对应的缓存区;

    //// 取高速缓冲中指定的缓冲块
    // 检查指定(设备号和块号)的缓冲区是否已经在高速缓冲中。如果指定块已经在
    // 高速缓冲中,则返回对应缓冲区头指针退出;如果不在,就需要在高速缓冲中设置一个
    // 对应设备号和块好的新项。返回相应的缓冲区头指针。
    struct buffer_head * getblk(int dev,int block)
    {
        struct buffer_head * tmp, * bh;
    
    repeat:
        // 搜索hash表,如果指定块已经在高速缓冲中,则返回对应缓冲区头指针,退出。
        if ((bh = get_hash_table(dev,block)))
            return bh;
        // 扫描空闲数据块链表,寻找空闲缓冲区。
        // 首先让tmp指向空闲链表的第一个空闲缓冲区头
        tmp = free_list;
        do {
            // 如果该缓冲区正被使用(引用计数不等于0),则继续扫描下一项。对于
            // b_count = 0的块,即高速缓冲中当前没有引用的块不一定就是干净的
            // (b_dirt=0)或没有锁定的(b_lock=0)。因此,我们还是需要继续下面的判断
            // 和选择。例如当一个任务该写过一块内容后就释放了,于是该块b_count()=0
            // 但b_lock不等于0;当一个任务执行breada()预读几个块时,只要ll_rw_block()
            // 命令发出后,它就会递减b_count; 但此时实际上硬盘访问操作可能还在进行,
            // 因此此时b_lock=1, 但b_count=0.
            if (tmp->b_count)
                continue;
            // 如果缓冲头指针bh为空,或者tmp所指缓冲头的标志(修改、锁定)权重小于bh
            // 头标志的权重,则让bh指向tmp缓冲块头。如果该tmp缓冲块头表明缓冲块既
            // 没有修改也没有锁定标志置位,则说明已为指定设备上的块取得对应的高速
            // 缓冲块,则退出循环。否则我们就继续执行本循环,看看能否找到一个BANDNESS()
            // 最小的缓冲块。BADNESS等于0意味着b_block和b_dirt都是0,这块缓存区还没被使用,目标缓存区已经找到,可以跳出循环了
            if (!bh || BADNESS(tmp)<BADNESS(bh)) {
                bh = tmp;
                if (!BADNESS(tmp))
                    break;
            }
    /* and repeat until we find something good */
        } while ((tmp = tmp->b_next_free) != free_list);
        // 如果循环检查发现所有缓冲块都正在被使用(所有缓冲块的头部引用计数都>0)中,
        // 则睡眠等待有空闲缓冲块可用。当有空闲缓冲块可用时本进程会呗明确的唤醒。
        // 然后我们跳转到函数开始处重新查找空闲缓冲块。
        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 */
        // 在高速缓冲hash表中检查指定设备和块的缓冲块是否乘我们睡眠之际已经被加入
        // 进去(毕竟是多任务系统,有可能被其他任务抢先使用并放入has表)。如果是的话,就再次重复上述寻找过程。
        if (find_buffer(dev,block))
            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 */
        // 于是让我们占用此缓冲块。置引用计数为1,复位修改标志和有效(更新)标志。
        bh->b_count=1;
        bh->b_dirt=0;
        bh->b_uptodate=0;
        // 从hash队列和空闲队列块链表中移出该缓冲区头,让该缓冲区用于指定设备和
        // 其上的指定块。然后根据此新的设备号和块号重新插入空闲链表和hash队列新
        // 位置处。并最终返回缓冲头指针。
        remove_from_queues(bh);
        bh->b_dev=dev;
        bh->b_blocknr=block;
        insert_into_queues(bh);
        return bh;
    }

       代码的整体逻辑并不复杂,但是有些细节想展开说说:

    •   BADNESS(bh):从表达式看,b_dirt左移1位后再和b_lock相加,明显b_dirt的权重乘以了2,说明作者认为缓存区是否被使用的权重应该大于是否被锁!但是实际使用的时候,会一直循环查找BADNESS小的缓存区,说明作者认为b_block比b_dirt更重要,也就是缓存区是否上锁比是否被使用了更重要,这个也符合业务逻辑
    // 下面宏用于同时判断缓冲区的修改标志和锁定标志,并且定义修改标志的权重要比锁定标志大。
    //  b_dirt左移1位,权重比b_block高
    #define BADNESS(bh) (((bh)->b_dirt<<1)+(bh)->b_lock)
    •   循环停止的条件如下:tmp初始值就是free_list,这里的停止的条件也是tmp == free_list,说明free_list是个环形循环链表;所以整个do while循环本质上就是在free_list中找BADNESS值最小的buffer_head;如果找到BADNESS等于0(意味着b_block和b_dirt都为0,该缓存区还没被使用)的buffer_head,直接跳出循环
     while ((tmp = tmp->b_next_free) != free_list);
    •  函数结尾处: 再次检查dev+block是否已经在缓存区了,如果在,说明其他任务捷足先登,已经使用了该缓存区,本任务只能重新走查找的流程;如果该缓存块还没被使用,先设置一些标志/属性位,再把该buffer_head节点从旧hash表和free_list队列溢出,再重新加入hash_table和free_list队列,作者是咋想的?为啥要重复干这种事了
    /* already have added "this" block to the cache. check it */
        // 在高速缓冲hash表中检查指定设备和块的缓冲块是否乘我们睡眠之际已经被加入
        // 进去(毕竟是多任务,期间可能会被其他任务抢先使用并放入hash表)。如果是的话,就再次重复上述寻找过程。
        if (find_buffer(dev,block))
            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 */
        // 于是让我们占用此缓冲块。置引用计数为1,复位修改标志和有效(更新)标志。
        bh->b_count=1;
        bh->b_dirt=0;
        bh->b_uptodate=0;
        // 从hash队列和空闲队列块链表中移出该缓冲区头,让该缓冲区用于指定设备和
        // 其上的指定块。然后根据此新的设备号和块号重新插入空闲链表和hash队列新
        // 位置处。并最终返回缓冲头指针。
        /*将缓冲块从旧的队列移出,添加到新的队列中,即哈希表的头,空闲表的尾,这样能够迅速找到该存在的块,而该缓冲块存在的时间最长*/
        remove_from_queues(bh);
        bh->b_dev=dev;
        bh->b_blocknr=block;
        insert_into_queues(bh);
        return bh;

      先来看看remove_from_queues和insert_into_queu函数代码:remove_from_queues没啥好说的,就是简单粗暴的从hash表和free_list删除,也是常规的链表操作,重点在insert_into_queu函数:

    •  bh节点加入了free_list链表的末尾,直接减少了后续查询遍历链表的时间,这不就直接提升了查询效率么?
    •  bh节点加入hash表某个偏移的表头,后续通过hash偏移不就能第一个找到该节点了么?又省了遍历链表的操作!
    //// 从hash队列和空闲缓冲区队列中移走缓冲块。
    // hash队列是双向链表结构,空闲缓冲块队列是双向循环链表结构。
    static inline void remove_from_queues(struct buffer_head * bh)
    {
    /* remove from hash-queue */
        if (bh->b_next)
            bh->b_next->b_prev = bh->b_prev;
        if (bh->b_prev)
            bh->b_prev->b_next = bh->b_next;
        // 如果该缓冲区是该队列的头一个块(每个hash偏移的头),则让hash表的对应项指向本队列中的下一个
        // 缓冲区。
        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;
    }
    
    //// 将缓冲块插入空闲链表尾部,同时放入hash队列中。
    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 */
        // 请注意当hash表某项第1次插入项时,hash()计算值肯定为Null,因此此时得到
        // 的bh->b_next肯定是NULL,所以应该在bh->b_next不为NULL时才能给b_prev赋
        // bh值。
        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;                // 此句前应添加"if (bh->b_next)"判断
    }

       当一个block使用完后就要释放了,避免“占着茅坑不拉屎”;释放的逻辑也简单,如下:引用计数count--,并且唤醒正在等待该缓存区的其他任务;

    // 释放指定缓冲块。
    // 等待该缓冲块解锁。然后引用计数递减1,并明确地唤醒等待空闲缓冲块的进程。
    void brelse(struct buffer_head * buf)
    {
        if (!buf)
            return;
        wait_on_buffer(buf);
        if (!(buf->b_count--))
            panic("Trying to free free buffer");
        wake_up(&buffer_wait);
    }

      前面很多的操作,尤其是节点的增删改查都涉及到了hash表和链表,那么hash表和链表都是怎么建立的了?这里用的是buffer_init函数:hash表初始化时所有的偏移都指向null;

    // 缓冲区初始化函数
    // 参数buffer_end是缓冲区内存末端。对于具有16MB内存的系统,缓冲区末端被设置为4MB.
    // 对于有8MB内存的系统,缓冲区末端被设置为2MB。该函数从缓冲区开始位置start_buffer
    // 处和缓冲区末端buffer_end处分别同时设置(初始化)缓冲块头结构和对应的数据块。直到
    // 缓冲区中所有内存被分配完毕。
    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;
    }    

      截至目前,前面围绕缓存区做了大量的铺垫,最终的目的就是和磁盘之间读写数据,那么linux又是怎么利用缓存区从磁盘读数据的了?bread函数代码如下:整个逻辑也很简单,先申请缓存区,如果已经更新就直接返回;否则调用ll_rw_block读磁盘数据;读数据是要花时间的,这段时间cpu没必要闲着,可以跳转到其他进程继续执行;等数据读完后唤醒当前进程,检查buffer是否被锁、是否被更新;如果都没有,就可以安心释放了!

    //// 从设备上读取数据块。
    // 该函数根据指定的设备号 dev 和数据块号 block,首先在高速缓冲区中申请一块
    // 缓冲块。如果该缓冲块中已经包含有有效的数据就直接返回该缓冲块指针,否则
    // 就从设备中读取指定的数据块到该缓冲块中并返回缓冲块指针。
    struct buffer_head * bread(int dev,int block)
    {
        struct buffer_head * bh;
    
        // 在高速缓冲区中申请一块缓冲块。如果返回值是NULL,则表示内核出错,停机。
        // 然后我们判断其中说是否已有可用数据。如果该缓冲块中数据是有效的(已更新)
        // 可以直接使用,则返回。
        if (!(bh=getblk(dev,block)))
            panic("bread: getblk returned NULL\n");
        if (bh->b_uptodate)
            return bh;
        // 否则我们就调用底层快设备读写ll_rw_block函数,产生读设备块请求。然后
        // 等待指定数据块被读入,并等待缓冲区解锁。在睡眠醒来之后,如果该缓冲区已
        // 更新,则返回缓冲区头指针,退出。否则表明读设备操作失败,于是释放该缓
        // 冲区,返回NULL,退出。
        ll_rw_block(READ,bh);
        wait_on_buffer(bh);
        if (bh->b_uptodate)
            return bh;
        brelse(bh);
        return NULL;
    }

       ll_rw_block:ll全称应该是lowlevel的意思;rw表示读或者写请求,bh用来传递数据或保存数据。先通过主设备号判断是否为有效的设备,同时请求函数是否存在。如果是有效的设备且函数存在,即有驱动,则添加请求到相关链表中

      对于一个当前空闲的块设备,当 ll_rw_block()函数为其建立第一个请求项时,会让该设备的当前请求项指针current_request直接指向刚建立的请求项,并且立刻调用对应设备的请求项操作函数开始执行块设备读写操作。当一个块设备已经有几个请求项组成的链表存在,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生成request:函数首先判断是否为提前读或者提前写,如果是则要看bh是否上了锁。上了锁则直接返回,因为提前操作是不必要的,否则转化为可以识别的读或者写,然后锁住缓冲区;数据处理结束后在中断处理函数中解锁;如果是写操作但是缓冲区不脏,或者读操作但是缓冲区已经更新,则直接返回;最后构造request实例,调用add_request函数把实例添加到链表!

      add_request函数用了电梯调度算法,主要是考虑到早期机械磁盘的移臂的时间消耗较大,要么从里到外,要么从外到里,顺着某个方向多处理请求。如果req刚好在磁头移动的方向上,那么可以先处理,这样能节省IO(本质是寻址)的时间

    /*
     * add-request adds a request to the linked list.
     * It disables interrupts so that it can muck with the
     * request-lists in peace.
     */
    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)();
            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();
    }
    
    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);
    /* 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;
        add_request(major+blk_dev,req);
    }

       add_request中定义了宏IN_ORDER,真正的电梯调度算法体现在这里了:read请求排在写请求前面,先处理读请求,再处理写请求;同一读或写请求先处理设备号小的设备请求,再处理设备号大的设备请求;同一读或写请求,同一设备,按先里面的扇区再到外面的扇区的顺序处理。

    /*
     * This is used in the elevator algorithm: Note that
     * reads always go before writes. This is natural: reads
     * are much more time-critical than writes.
     */
    #define IN_ORDER(s1,s2) \
    ((s1)->cmd<(s2)->cmd || ((s1)->cmd==(s2)->cmd && \
    ((s1)->dev < (s2)->dev || ((s1)->dev == (s2)->dev && \
    (s1)->sector < (s2)->sector))))

      

      

    参考:

    1、https://blog.csdn.net/jmh1996/article/details/90139485     linux-0.12源码分析——缓冲区等待队列(栈)sleep_on+wake_up分析2

    2、https://blog.csdn.net/ac_dao_di/article/details/54615951   linux 0.11 块设备文件的使用

    3、https://cloud.tencent.com/developer/article/1749826 Linux文件系统之 — 通用块处理层

  • 相关阅读:
    ThreadLocal的设计理念与作用
    生产者消费者模式
    Java 线程池
    对象锁(包括方法锁)和类锁
    C++入门经典-例2.12-求逻辑表达式的值
    C++入门经典-例2.11-流输出小数控制
    C++入门经典-例2.10-控制输出精确度
    C++入门经典-例2.9-输出十六进制数以及大写的十六进制数
    C++入门经典-例2.8-输出整数,控制打印格式
    C++入门经典-例2.7-控制cout打印格式程序
  • 原文地址:https://www.cnblogs.com/theseventhson/p/15602994.html
Copyright © 2011-2022 走看看