<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<atom:link href="http://gentoo-zh.org/extern.php?action=feed&amp;tid=443&amp;type=rss" rel="self" type="application/rss+xml" />
		<title><![CDATA[Gentoo中文社区 / linux源码解读（二）：文件系统——高速缓存区]]></title>
		<link>http://www.gentoo-zh.org/viewtopic.php?id=443</link>
		<description><![CDATA[linux源码解读（二）：文件系统——高速缓存区 最近发表的帖子。]]></description>
		<lastBuildDate>Sat, 08 Oct 2022 03:21:09 +0000</lastBuildDate>
		<generator>FluxBB</generator>
		<item>
			<title><![CDATA[linux源码解读（二）：文件系统——高速缓存区]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?pid=450#p450</link>
			<description><![CDATA[<p>用户的应用程序会经常读写磁盘文件的数据到内存，但是内存的速度和磁盘的速度理论上差了好几个数量级；为了更高效地解决内存和磁盘的速度差，linux也在内存使用了缓存区（作用类似于cpu内部为了解决寄存器和内存速度差异的的L1、L2、L3 cache）：如果数据要写入磁盘文件，先放在缓存区，等凑够了一定数量后再批量写入磁盘文件，借此减少磁盘寻址的次数，来提升写入效率（这里多说几句：比如U盘插上电脑后，如果要拔出，建议先卸载再拔出，而不是直接拔出，为啥了？U盘的数据也是先放入缓冲区的，缓冲区有自己的管理机制，很久没有使用的块可以给其他进程使用，如果是脏块则要进行写盘。缓冲在某些情况下才会有写盘操作，所以要拔出U盘时，应该先进行卸载，这样才会写盘，否则数据可能丢失，文件系统可能损坏。）；如果从磁盘读数据，也会先放入缓存区暂存，一旦有其他进程或线程读取同样的磁盘文件，这是就可以先从内存的缓存区取数据了，没必要重新从磁盘读取，也提升了效率！linux 0.11的缓冲区是怎么工作的了？</p><p>&#160; 在main.c的main函数中，有设置缓存区的大小，代码如下：内存不同，缓存区的大小也不同，linux是怎么管理和使用这些缓存区了的？</p><div class="codebox"><pre><code>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&lt;&lt;20) + (EXT_MEM_K&lt;&lt;10);
    //取整4k的内存大小
    memory_end &amp;= 0xfffff000;
    if (memory_end &gt; 16*1024*1024)//控制操作系统的最大内存为16M
        memory_end = 16*1024*1024;
    if (memory_end &gt; 12*1024*1024) 
        buffer_memory_end = 4*1024*1024;//设置高速缓冲区的大小，跟块设备有关，跟设备交互的时候，充当缓冲区，写入到块设备中的数据先放在缓冲区里，只有执行sync时才真正写入；这也是为什么要区分块设备驱动和字符设备驱动；块设备写入需要缓冲区，字符设备不需要是直接写入的
    else if (memory_end &gt; 6*1024*1024)
        buffer_memory_end = 2*1024*1024;
    else
        buffer_memory_end = 1*1024*1024;
    main_memory_start = buffer_memory_end;</code></pre></div><p>&#160; 1、cpu有分页机制，硬件上以4KB为单位把内存分割成小块供程序使用；这个颗粒度是比较大的，有些时候可能会浪费比较多的内存，所以linux缓存区采用了1KB的大小来分割整个缓存区；假设缓存区有2MB，那么一共被分割成了2000个小块，这么多的缓存区该怎么管理了？</p><p>&#160; 每个缓存都有各自的属性，比如是否使用、数据是否更新、缓存数据在磁盘的位置、缓存的起始地址等，要想统一管理这么多的属性，最好的办法自然是构建结构体了；一个结构体管理1块（也就是1KB）的缓存区；假设这里有2000个缓存区，就需要2000个结构体，那么问题又来了：这个多的结构体，又该怎么去管理了？ </p><p>&#160; 参考前面的进程task结构体管理方式：用task数组来管理所有的进程task结构体，最大限制为64个进程，但是放在这里显然不适用：不同机器的物理内存大小是不同的，导致缓存区的block数量是不同的，但数组最大的缺点就是定长，无法适应不同的物理内存，那么这里最适合的只剩链表了，所以linux 0.11版本使用的结构体如下：</p><div class="codebox"><pre><code>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;
};</code></pre></div><p>&#160; 每个字段的含义都在注释了，这里不再赘述；既然采用了链表，解决了数组只能定长的缺点，但是链表本身也有缺点：无法直接找到目标实例，需要挨个遍历链表上的每个节点；还是假设有2000个块，好巧的不巧的是程序所需的block刚好在最后一个节点，那么需要遍历1999个节点才能到达，效率非常低，这又该怎么解决了？刚好这种快速寻址（时间复杂度O(1)）是数组的优势，怎么解决数组和链表各自的优势了？-----hash表！</p><p>&#160; linux 0.11版本采用hash表的方式快速寻址，hash映射算法如下：</p><div class="codebox"><pre><code>// 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)]</code></pre></div><p>&#160; 映射的算法也很简单：每个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链表首尾连接！</p><p><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202111/2052730-20211128163552687-1170315130.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160;当这个一整套存储机制建立后，怎么检索了？linux的检索方式如下：先通过dev和block号定位到hash表的偏移，再遍历该偏移处的所有节点，通过比对dev和block号找到目标buffer_head实例！</p><div class="codebox"><pre><code>//// 利用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-&gt;b_next)
        if (tmp-&gt;b_dev==dev &amp;&amp; tmp-&gt;b_blocknr==block)
            return tmp;
    return NULL;
}</code></pre></div><p>&#160; &#160;根据dev和block号找到缓存区的buffer_head并不代表万事大吉，因为该缓存区可能已经被其他进程/线程占用，当前线程如果一定要用这个缓存区，只能等了，所以最终查找缓存区的代码如下：这里增加了wait_on_buffer函数：</p><div class="codebox"><pre><code>//// 利用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-&gt;b_count++;
        wait_on_buffer(bh);
        if (bh-&gt;b_dev == dev &amp;&amp; bh-&gt;b_blocknr == block)
            return bh;
        // 如果在睡眠时该缓冲块所属的设备号或块设备号发生了改变，则撤消对它的
        // 引用计数，重新寻找。
        bh-&gt;b_count--;
    }
}</code></pre></div><p>&#160; wait_on_buffer函数实现：如果发现该缓存区已经上锁，那么调用sleep_on函数让出cpu，阻塞在这里等待；这个sleep_on函数传入的参数是二级指针，并且内部用了tmp变量保存临时变量；由于二级指针是全局的，所以如果有多个task等待同一个缓存区，sleep_on函数是通过先进后出的栈的形式唤醒等待任务的；参考1有详细的说明，感兴趣的小伙伴建议好好看看！</p><div class="codebox"><pre><code>//// 等待指定缓冲块解锁
// 如果指定的缓冲块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-&gt;b_lock)              // 如果已被上锁则进程进入睡眠，等待其解锁
        sleep_on(&amp;bh-&gt;b_wait);
    sti();                          // 开中断
}</code></pre></div><p>&#160; &#160;先进后出的栈形式唤醒等待任务：<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202111/2052730-20211129202959305-1621767149.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; 接下来可能就是buffer.c中最重要的函数之一了：struct buffer_head * getblk(int dev,int block)，根据设备号和块号得到buffer_head的实例，便于后续使用对应的缓存区；</p><div class="codebox"><pre class="vscroll"><code>//// 取高速缓冲中指定的缓冲块
// 检查指定（设备号和块号）的缓冲区是否已经在高速缓冲中。如果指定块已经在
// 高速缓冲中，则返回对应缓冲区头指针退出；如果不在，就需要在高速缓冲中设置一个
// 对应设备号和块好的新项。返回相应的缓冲区头指针。
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-&gt;b_count)
            continue;
        // 如果缓冲头指针bh为空，或者tmp所指缓冲头的标志(修改、锁定)权重小于bh
        // 头标志的权重，则让bh指向tmp缓冲块头。如果该tmp缓冲块头表明缓冲块既
        // 没有修改也没有锁定标志置位，则说明已为指定设备上的块取得对应的高速
        // 缓冲块，则退出循环。否则我们就继续执行本循环，看看能否找到一个BANDNESS()
        // 最小的缓冲块。BADNESS等于0意味着b_block和b_dirt都是0，这块缓存区还没被使用，目标缓存区已经找到，可以跳出循环了
        if (!bh || BADNESS(tmp)&lt;BADNESS(bh)) {
            bh = tmp;
            if (!BADNESS(tmp))
                break;
        }
/* and repeat until we find something good */
    } while ((tmp = tmp-&gt;b_next_free) != free_list);
    // 如果循环检查发现所有缓冲块都正在被使用(所有缓冲块的头部引用计数都&gt;0)中，
    // 则睡眠等待有空闲缓冲块可用。当有空闲缓冲块可用时本进程会呗明确的唤醒。
    // 然后我们跳转到函数开始处重新查找空闲缓冲块。
    if (!bh) {
        sleep_on(&amp;buffer_wait);
        goto repeat;
    }
    // 执行到这里，说明我们已经找到了一个比较合适的空闲缓冲块了。于是先等待该缓冲区
    // 解锁（多任务同时运行，刚找到的缓存块可能已经被其他任务抢先一步找到并使用了，所以要再次检查）。如果在我们睡眠阶段该缓冲区又被其他任务使用的话，只好重复上述寻找过程。
    wait_on_buffer(bh);
    if (bh-&gt;b_count)
        goto repeat;
    // 如果该缓冲区已被修改，则将数据写盘，并再次等待缓冲区解锁。同样地，若该缓冲区
    // 又被其他任务使用的话，只好再重复上述寻找过程。
    while (bh-&gt;b_dirt) {
        sync_dev(bh-&gt;b_dev);
        wait_on_buffer(bh);
        if (bh-&gt;b_count)
            goto repeat;
    }
/* NOTE!! While we slept waiting for this block, somebody else might */
/* already have added &quot;this&quot; 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&#039;s kind, */
/* and that it&#039;s unused (b_count=0), unlocked (b_lock=0), and clean */
    // 于是让我们占用此缓冲块。置引用计数为1，复位修改标志和有效(更新)标志。
    bh-&gt;b_count=1;
    bh-&gt;b_dirt=0;
    bh-&gt;b_uptodate=0;
    // 从hash队列和空闲队列块链表中移出该缓冲区头，让该缓冲区用于指定设备和
    // 其上的指定块。然后根据此新的设备号和块号重新插入空闲链表和hash队列新
    // 位置处。并最终返回缓冲头指针。
    remove_from_queues(bh);
    bh-&gt;b_dev=dev;
    bh-&gt;b_blocknr=block;
    insert_into_queues(bh);
    return bh;
}</code></pre></div><p>&#160; &#160;代码的整体逻辑并不复杂，但是有些细节想展开说说：</p><p>&#160; BADNESS(bh)：从表达式看，b_dirt左移1位后再和b_lock相加，明显b_dirt的权重乘以了2，说明作者认为缓存区是否被使用的权重应该大于是否被锁！但是实际使用的时候，会一直循环查找BADNESS小的缓存区，说明作者认为b_block比b_dirt更重要，也就是缓存区是否上锁比是否被使用了更重要，这个也符合业务逻辑！<br />// 下面宏用于同时判断缓冲区的修改标志和锁定标志，并且定义修改标志的权重要比锁定标志大。<br />//&#160; b_dirt左移1位，权重比b_block高<br />&#160; &#160;#define BADNESS(bh) (((bh)-&gt;b_dirt&lt;&lt;1)+(bh)-&gt;b_lock)<br />&#160; 循环停止的条件如下：tmp初始值就是free_list，这里的停止的条件也是tmp == free_list，说明free_list是个环形循环链表；所以整个do while循环本质上就是在free_list中找BADNESS值最小的buffer_head；如果找到BADNESS等于0（意味着b_block和b_dirt都为0，该缓存区还没被使用）的buffer_head，直接跳出循环！<br />&#160; &#160;while ((tmp = tmp-&gt;b_next_free) != free_list);<br /> 函数结尾处: 再次检查dev+block是否已经在缓存区了，如果在，说明其他任务捷足先登，已经使用了该缓存区，本任务只能重新走查找的流程；如果该缓存块还没被使用，先设置一些标志/属性位，再把该buffer_head节点从旧hash表和free_list队列溢出，再重新加入hash_table和free_list队列，作者是咋想的？为啥要重复干这种事了？</p><div class="codebox"><pre><code>/* already have added &quot;this&quot; 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&#039;s kind, */
/* and that it&#039;s unused (b_count=0), unlocked (b_lock=0), and clean */
    // 于是让我们占用此缓冲块。置引用计数为1，复位修改标志和有效(更新)标志。
    bh-&gt;b_count=1;
    bh-&gt;b_dirt=0;
    bh-&gt;b_uptodate=0;
    // 从hash队列和空闲队列块链表中移出该缓冲区头，让该缓冲区用于指定设备和
    // 其上的指定块。然后根据此新的设备号和块号重新插入空闲链表和hash队列新
    // 位置处。并最终返回缓冲头指针。
    /*将缓冲块从旧的队列移出，添加到新的队列中，即哈希表的头，空闲表的尾，这样能够迅速找到该存在的块，而该缓冲块存在的时间最长*/
    remove_from_queues(bh);
    bh-&gt;b_dev=dev;
    bh-&gt;b_blocknr=block;
    insert_into_queues(bh);
    return bh;</code></pre></div><p>&#160; 先来看看remove_from_queues和insert_into_queu函数代码：remove_from_queues没啥好说的，就是简单粗暴的从hash表和free_list删除，也是常规的链表操作，重点在insert_into_queu函数：</p><p> bh节点加入了free_list链表的末尾，直接减少了后续查询遍历链表的时间，这不就直接提升了查询效率么？<br /> bh节点加入hash表某个偏移的表头，后续通过hash偏移不就能第一个找到该节点了么？又省了遍历链表的操作！</p><div class="codebox"><pre class="vscroll"><code>//// 从hash队列和空闲缓冲区队列中移走缓冲块。
// hash队列是双向链表结构，空闲缓冲块队列是双向循环链表结构。
static inline void remove_from_queues(struct buffer_head * bh)
{
/* remove from hash-queue */
    if (bh-&gt;b_next)
        bh-&gt;b_next-&gt;b_prev = bh-&gt;b_prev;
    if (bh-&gt;b_prev)
        bh-&gt;b_prev-&gt;b_next = bh-&gt;b_next;
    // 如果该缓冲区是该队列的头一个块（每个hash偏移的头），则让hash表的对应项指向本队列中的下一个
    // 缓冲区。
    if (hash(bh-&gt;b_dev,bh-&gt;b_blocknr) == bh)
        hash(bh-&gt;b_dev,bh-&gt;b_blocknr) = bh-&gt;b_next;
/* remove from free list */
    if (!(bh-&gt;b_prev_free) || !(bh-&gt;b_next_free))
        panic(&quot;Free block list corrupted&quot;);
    bh-&gt;b_prev_free-&gt;b_next_free = bh-&gt;b_next_free;
    bh-&gt;b_next_free-&gt;b_prev_free = bh-&gt;b_prev_free;
    // 如果空闲链表头指向本缓冲区，则让其指向下一缓冲区。
    if (free_list == bh)
        free_list = bh-&gt;b_next_free;
}

//// 将缓冲块插入空闲链表尾部，同时放入hash队列中。
static inline void insert_into_queues(struct buffer_head * bh)
{
/* put at end of free list */
    bh-&gt;b_next_free = free_list;
    bh-&gt;b_prev_free = free_list-&gt;b_prev_free;
    free_list-&gt;b_prev_free-&gt;b_next_free = bh;
    free_list-&gt;b_prev_free = bh;
/* put the buffer in new hash-queue if it has a device */
    // 请注意当hash表某项第1次插入项时，hash()计算值肯定为Null，因此此时得到
    // 的bh-&gt;b_next肯定是NULL，所以应该在bh-&gt;b_next不为NULL时才能给b_prev赋
    // bh值。
    bh-&gt;b_prev = NULL;
    bh-&gt;b_next = NULL;
    if (!bh-&gt;b_dev)
        return;
    bh-&gt;b_next = hash(bh-&gt;b_dev,bh-&gt;b_blocknr);
    hash(bh-&gt;b_dev,bh-&gt;b_blocknr) = bh;
    bh-&gt;b_next-&gt;b_prev = bh;                // 此句前应添加&quot;if (bh-&gt;b_next)&quot;判断
}</code></pre></div><p>&#160; &#160;当一个block使用完后就要释放了，避免“占着茅坑不拉屎”；释放的逻辑也简单，如下：引用计数count--，并且唤醒正在等待该缓存区的其他任务；</p><div class="codebox"><pre><code>// 释放指定缓冲块。
// 等待该缓冲块解锁。然后引用计数递减1，并明确地唤醒等待空闲缓冲块的进程。
void brelse(struct buffer_head * buf)
{
    if (!buf)
        return;
    wait_on_buffer(buf);
    if (!(buf-&gt;b_count--))
        panic(&quot;Trying to free free buffer&quot;);
    wake_up(&amp;buffer_wait);
}</code></pre></div><p>&#160; 前面很多的操作，尤其是节点的增删改查都涉及到了hash表和链表，那么hash表和链表都是怎么建立的了？这里用的是buffer_init函数：hash表初始化时所有的偏移都指向null；</p><div class="codebox"><pre class="vscroll"><code>// 缓冲区初始化函数
// 参数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&lt;&lt;20)
        b = (void *) (640*1024);
    else
        b = (void *) buffer_end;
    // 这段代码用于初始化缓冲区，建立空闲缓冲区块循环链表，并获取系统中缓冲块数目。
    // 操作的过程是从缓冲区高端开始划分1KB大小的缓冲块，与此同时在缓冲区低端建立
    // 描述该缓冲区块的结构buffer_head,并将这些buffer_head组成双向链表。
    // h是指向缓冲头结构的指针，而h+1是指向内存地址连续的下一个缓冲头地址，也可以说
    // 是指向h缓冲头的末端外。为了保证有足够长度的内存来存储一个缓冲头结构，需要b所
    // 指向的内存块地址 &gt;= h 缓冲头的末端，即要求 &gt;= h+1.
    while ( (b -= BLOCK_SIZE) &gt;= ((void *) (h+1)) ) {
        h-&gt;b_dev = 0;                       // 使用该缓冲块的设备号
        h-&gt;b_dirt = 0;                      // 脏标志，即缓冲块修改标志
        h-&gt;b_count = 0;                     // 缓冲块引用计数
        h-&gt;b_lock = 0;                      // 缓冲块锁定标志
        h-&gt;b_uptodate = 0;                  // 缓冲块更新标志(或称数据有效标志)
        h-&gt;b_wait = NULL;                   // 指向等待该缓冲块解锁的进程
        h-&gt;b_next = NULL;                   // 指向具有相同hash值的下一个缓冲头
        h-&gt;b_prev = NULL;                   // 指向具有相同hash值的前一个缓冲头
        h-&gt;b_data = (char *) b;             // 指向对应缓冲块数据块（1024字节）
        h-&gt;b_prev_free = h-1;               // 指向链表中前一项
        h-&gt;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-&gt;b_prev_free = h;             // 链表头的b_prev_free指向前一项(即最后一项)。
    h-&gt;b_next_free = free_list;             // h的下一项指针指向第一项，形成一个环链
    // 最后初始化hash表，置表中所有指针为NULL。
    for (i=0;i&lt;NR_HASH;i++)
        hash_table[i]=NULL;
}</code></pre></div><p>&#160; 截至目前，前面围绕缓存区做了大量的铺垫，最终的目的就是和磁盘之间读写数据，那么linux又是怎么利用缓存区从磁盘读数据的了？bread函数代码如下：整个逻辑也很简单，先申请缓存区，如果已经更新就直接返回；否则调用ll_rw_block读磁盘数据；读数据是要花时间的，这段时间cpu没必要闲着，可以跳转到其他进程继续执行；等数据读完后唤醒当前进程，检查buffer是否被锁、是否被更新；如果都没有，就可以安心释放了！</p><div class="codebox"><pre><code>//// 从设备上读取数据块。
// 该函数根据指定的设备号 dev 和数据块号 block，首先在高速缓冲区中申请一块
// 缓冲块。如果该缓冲块中已经包含有有效的数据就直接返回该缓冲块指针，否则
// 就从设备中读取指定的数据块到该缓冲块中并返回缓冲块指针。
struct buffer_head * bread(int dev,int block)
{
    struct buffer_head * bh;

    // 在高速缓冲区中申请一块缓冲块。如果返回值是NULL，则表示内核出错，停机。
    // 然后我们判断其中说是否已有可用数据。如果该缓冲块中数据是有效的（已更新）
    // 可以直接使用，则返回。
    if (!(bh=getblk(dev,block)))
        panic(&quot;bread: getblk returned NULL\n&quot;);
    if (bh-&gt;b_uptodate)
        return bh;
    // 否则我们就调用底层快设备读写ll_rw_block函数，产生读设备块请求。然后
    // 等待指定数据块被读入，并等待缓冲区解锁。在睡眠醒来之后，如果该缓冲区已
    // 更新，则返回缓冲区头指针，退出。否则表明读设备操作失败，于是释放该缓
    // 冲区，返回NULL，退出。
    ll_rw_block(READ,bh);
    wait_on_buffer(bh);
    if (bh-&gt;b_uptodate)
        return bh;
    brelse(bh);
    return NULL;
}</code></pre></div><p>&#160; &#160;ll_rw_block：ll全称应该是lowlevel的意思；rw表示读或者写请求，bh用来传递数据或保存数据。先通过主设备号判断是否为有效的设备，同时请求函数是否存在。如果是有效的设备且函数存在，即有驱动，则添加请求到相关链表中；</p><p>&#160; 对于一个当前空闲的块设备，当 ll_rw_block()函数为其建立第一个请求项时，会让该设备的当前请求项指针current_request直接指向刚建立的请求项，并且立刻调用对应设备的请求项操作函数开始执行块设备读写操作。当一个块设备已经有几个请求项组成的链表存在，ll_rw_block()就会利用电梯算法，根据磁头移动距离最小原则，把新建的请求项插入到链表适当的位置处；</p><div class="codebox"><pre><code>void ll_rw_block(int rw, struct buffer_head * bh)
{
    unsigned int major;

    if ((major=MAJOR(bh-&gt;b_dev)) &gt;= NR_BLK_DEV ||
    !(blk_dev[major].request_fn)) {
        printk(&quot;Trying to read nonexistent block-device\n\r&quot;);
        return;
    }
    make_request(major,rw,bh);
}</code></pre></div><p>&#160; 该函数内部继续调用make_request生成request：函数首先判断是否为提前读或者提前写，如果是则要看bh是否上了锁。上了锁则直接返回，因为提前操作是不必要的，否则转化为可以识别的读或者写，然后锁住缓冲区；数据处理结束后在中断处理函数中解锁；如果是写操作但是缓冲区不脏，或者读操作但是缓冲区已经更新，则直接返回；最后构造request实例，调用add_request函数把实例添加到链表！</p><p>&#160; add_request函数用了电梯调度算法，主要是考虑到早期机械磁盘的移臂的时间消耗较大，要么从里到外，要么从外到里，顺着某个方向多处理请求。如果req刚好在磁头移动的方向上，那么可以先处理，这样能节省IO(本质是寻址)的时间；</p><div class="codebox"><pre class="vscroll"><code>/*
 * 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-&gt;next = NULL;
    cli();
    if (req-&gt;bh)
        req-&gt;bh-&gt;b_dirt = 0;
    if (!(tmp = dev-&gt;current_request)) {
        dev-&gt;current_request = req;
        sti();
        (dev-&gt;request_fn)();
        return;
    }
    for ( ; tmp-&gt;next ; tmp=tmp-&gt;next)
        if ((IN_ORDER(tmp,req) || 
            !IN_ORDER(tmp,tmp-&gt;next)) &amp;&amp;
            IN_ORDER(req,tmp-&gt;next))
            break;
    req-&gt;next=tmp-&gt;next;
    tmp-&gt;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&#039;s a normal read */
    if ((rw_ahead = (rw == READA || rw == WRITEA))) {
        if (bh-&gt;b_lock)
            return;
        if (rw == READA)
            rw = READ;
        else
            rw = WRITE;
    }
    if (rw!=READ &amp;&amp; rw!=WRITE)
        panic(&quot;Bad block dev command, must be R/W/RA/WA&quot;);
    lock_buffer(bh);
    if ((rw == WRITE &amp;&amp; !bh-&gt;b_dirt) || (rw == READ &amp;&amp; bh-&gt;b_uptodate)) {
        unlock_buffer(bh);
        return;
    }
repeat:
/* we don&#039;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 &gt;= request)
        if (req-&gt;dev&lt;0)
            break;
/* if none found, sleep on new requests: check for rw_ahead */
    if (req &lt; request) {
        if (rw_ahead) {
            unlock_buffer(bh);
            return;
        }
        sleep_on(&amp;wait_for_request);
        goto repeat;
    }
/* fill up the request-info, and add it to the queue */
    req-&gt;dev = bh-&gt;b_dev;
    req-&gt;cmd = rw;
    req-&gt;errors=0;
    req-&gt;sector = bh-&gt;b_blocknr&lt;&lt;1;
    req-&gt;nr_sectors = 2;
    req-&gt;buffer = bh-&gt;b_data;
    req-&gt;waiting = NULL;
    req-&gt;bh = bh;
    req-&gt;next = NULL;
    add_request(major+blk_dev,req);
}</code></pre></div><p>&#160; &#160;add_request中定义了宏IN_ORDER，真正的电梯调度算法体现在这里了：read请求排在写请求前面，先处理读请求，再处理写请求；同一读或写请求先处理设备号小的设备请求，再处理设备号大的设备请求；同一读或写请求，同一设备，按先里面的扇区再到外面的扇区的顺序处理。</p><div class="codebox"><pre><code>/*
 * 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)-&gt;cmd&lt;(s2)-&gt;cmd || ((s1)-&gt;cmd==(s2)-&gt;cmd &amp;&amp; \
((s1)-&gt;dev &lt; (s2)-&gt;dev || ((s1)-&gt;dev == (s2)-&gt;dev &amp;&amp; \
(s1)-&gt;sector &lt; (s2)-&gt;sector))))</code></pre></div><p>参考：</p><p>1、https://blog.csdn.net/jmh1996/article/details/90139485&#160; &#160; &#160;linux-0.12源码分析——缓冲区等待队列（栈）sleep_on+wake_up分析2</p><p>2、https://blog.csdn.net/ac_dao_di/article/details/54615951&#160; &#160;linux 0.11 块设备文件的使用</p><p>3、https://cloud.tencent.com/developer/article/1749826 Linux文件系统之 — 通用块处理层</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Sat, 08 Oct 2022 03:21:09 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?pid=450#p450</guid>
		</item>
	</channel>
</rss>
