【Linux102】58-fs/buffer.c


公粽号「专注Linux」,专注Linux内核开发

Linux102系列会详细讲解Linux0.11版本中的102个文件,本文讲解linux0.11的第58个文件【Linux102】58-fs/buffer.c的文件源码。

1.buffer.c的主要作用

buffer.c程序用于对高速缓冲区(池)进行操作管理。高速缓冲区位于内核代码主内存区 之间,如图:

高速缓冲区

整个高速缓冲区被划分成1024B大小的缓冲块,正好与块设备上的磁盘逻辑块大小一样。高速缓冲区采用hash表空闲缓冲块队列进行操作管理。

在缓冲区初始化过程中,从缓冲区的两端开始,同时分别设置缓冲块头结构和划分出对应的缓冲块。缓冲区的高端被划分成一个个1024B的缓冲块,低端则分别建立起对应各缓冲块的缓冲头结构 buffer_head(include/linux/fs.h,68行),用于描述对应缓冲块属性和将所有缓冲头连接成链表。 直到它们之间已经不能再划分出缓冲块为止,如图所示:

高速缓冲区的内部结构:缓冲头描述缓冲区

而各个buffer_head被链接成一个空闲缓冲块 双向循环链表结构。详细结构如:

各个缓冲头相连,形成一个双向循环链表结构

为了能够快速地在缓冲区中寻找请求的数据块是否已经被读入到缓冲区中,buffer.c程序使用了具有307个buffer _head指针项的散列表结构。上图中buffer_head结构的指针b_prev、b_next就是用于散列表中散列在同一项上多个缓冲块之间的双向链接(解决哈希冲突的,所以内核这是使用的链表法)。散列表所使用的散列函数由设备号逻辑块号组合而成。程序中使用的具体函数是:

(设备号^逻辑块号) Mod307

对于动态变化的散列表结构某一时刻的状态可参见图:

动态变化的散列表结构,本质就是哈希表和其元素

其中,双箭头横线表示散列在同一散列表项中缓冲块头结构之间的双向链接指针(本质就是哈希冲突)。虚线表示当前连接在空闲缓冲块链表中空闲缓冲块之间的链接指针,free_list空闲链表的头指针

函数调用层级

函数调用层级关系:

1、getblk函数

上面提及的顶层的三个函数在执行时都调用了缓冲块搜索函数getbk(),以获取适合的缓冲块。
该函数首先调用get_hash_table()函数,在hash表队列中搜索指定设备号逻辑块号的缓冲开始,对空闲链表进行扫描,寻找一个空闲缓冲块。在寻找过程中还要对找到的空闲缓冲块做比较,根据赋予修改标志和锁定标志组合而成的权值比较哪个空闲块最适合

查找缓冲块的可能结果:

①既没有被修改也没有被锁定

若找到的空闲块既没有被修改也没有被锁定,就不用继续寻找了。

②没有找到空闲块

若没有找到空闲块,则让当前进程进人睡眠状态,待继续执行时再次寻找。

③若该空闲块被锁定

若该空闲块被锁定,则进程也需进人睡眠,等待其他进程解锁。若在睡眠等待的过程中,该缓冲块又被其他进程占用,那么只从头开始搜索缓冲块。否则判断该缓冲块是否已被修改过,若是,则将该块写盘,并等待该块解锁。此时如果该缓冲块又被别的进程占用,那么又一次前功尽弃,只好从头开始执行getblk()。

④进程睡眠时,当前进程的缓冲块被其他进程加入了散列队列

在经历了以上操作后,此时有可能出现另外一个意外情况,也就是在当前进程睡眠时,可能其他进程已经将当前进程所需要的缓冲块加进了散列队列中,因此这里需要最后一次搜索一下散列队列。如果真的在散列队列中找到了我们所需要的缓冲块,那么我们又得对找到的缓冲块。进行以上判断处理,因此,又一次需要从头开始执行getbk()。


最后,我们才算找到了一块没有被进程使用、没有被上锁,而且是干净(修改标志未置位)的空闲缓冲块。


于是我们就将该块的引用次数置1,并复位其他几个标志,然后从空闲表中移出该块的缓冲头结构。在设置了该缓冲块所属的设备号相应的逻辑号后,将其插人散列表对应表项首部并链接到空闲队列的末尾。 最终,返回该缓冲块头的指针。


整个getblk()处理过程如图所示:

getblk()处理过程


2、bread函数

由以上处理可以看到,getblk()返回的缓冲块可能是一个新的空闲块, 也可能正好是含有我们需要数据的缓冲块, 它已经存在高速缓冲区中。因此对于读取数据块操作(bread()),此时就要判断该缓冲块的更新标志,看看所含数据是否有效,如果有效就可以直接将该数据块返回给申请的程序。否则就需要调用设备的底层块读写函数(1l_rw_block()),并同时让自己进人睡眠状态,等待数据被读入缓冲块。在醒来后再判断数据是否有效,如果有效,就可将此数据返给申请的程序,否则说明对设备的读操作失败了,没有取到数据。于是,释放该缓冲块,并返回 NULL值。

下图是bread()函数的框图。

breada()和breadpage()函数与 bread()函数类似。


3、brelse函数

当程序不再需要使用一个缓冲块中的数据时,就调用brelse()函数,释放该缓冲块唤醒因等待该缓冲块而进入睡眠状态的进程。注意,空闲缓冲块链表中的缓冲块,并不都是空闲的。只有当被写盘刷新解锁没有其他进程引用时(引用计数=0),才能挪作他用。


2.源码用到的文件

源码用到的文件

文件名作用
😉【Linux102】12-include/stdarg.hstdarg.h是C 语言标准库中用于处理可变参数函数的头文件,核心作用是提供一套宏定义,让函数能够接收数量和类型不确定的参数(如 printf(const char *fmt, …) 中的 …)。
😉【Linux102】42-include/linux/config.hconfig.h 是一个配置文件,用于设置键盘布局和硬盘参数(备用)。设计目的:提供可配置的选项,适配不同的硬件环境。
😉【Linux102】15-include/linux/sched.h本程序主要定义了进程调度的相关数据结构,如任务数据结构等等,理解这些数据结构是理解进程调度机制的关键,也是理解内核的关键。一句话:**我们常说的进程是XXX,但是进程究竟是什么?**就定义在这个文件里面!源码面前了,没有秘密!
😉【Linux102】20-include/linux/kernel.h本程序主要定义了内核中最常用的一些基础函数声明,是内核运行最基本保障,比如malloc、printk、free、tyy_write、panic等函数声明,这些函数具体的实现则分布在各个.c文件中。
😉【Linux102】36-include/asm/system.h该文件中定义了设置或修改描述符/中断门等嵌入式汇编宏。其中,函数 move_to_user_mode() 用于内核在初始化结束时 “切换”到初始进程(任务0)。所使用的方法是模拟中断调用返回过程,即利用指令iret运行初始任务0。
😉【Linux102】39-include/asm/io.hio.h定义了一系列宏,这些宏是 x86 架构下直接操作硬件 I/O 端口的底层工具。

3.源码版

/*
 *  linux/fs/buffer.c
 *
 *  (C) 1991  Linus Torvalds
 */

/*
 *  'buffer.c' implements the buffer-cache functions. Race-conditions have
 * been avoided by NEVER letting a interrupt change a buffer (except for the
 * data, of course), but instead letting the caller do it. NOTE! As interrupts
 * can wake up a caller, some cli-sti sequences are needed to check for
 * sleep-on-calls. These should be extremely quick, though (I hope).
 */

/*
 * NOTE! There is one discordant note here: checking floppies for
 * disk change. This is where it fits best, I think, as it should
 * invalidate changed floppy-disk-caches.
 */

#include <stdarg.h>

#include <linux/config.h>
#include <linux/sched.h>
#include <linux/kernel.h>
#include <asm/system.h>
#include <asm/io.h>

extern int end;
extern void put_super(int);
extern void invalidate_inodes(int);

struct buffer_head *start_buffer = (struct buffer_head *)&end;
struct buffer_head *hash_table[NR_HASH];
static struct buffer_head *free_list;
static struct task_struct *buffer_wait = NULL;
int NR_BUFFERS = 0;

static inline void wait_on_buffer(struct buffer_head *bh)
{
    cli();
    while (bh->b_lock)
        sleep_on(&bh->b_wait);
    sti();
}

int sys_sync(void)
{
    int i;
    struct buffer_head *bh;

    sync_inodes(); /* write out inodes into buffers */
    bh = start_buffer;
    for (i = 0; i < NR_BUFFERS; i++, bh++)
    {
        wait_on_buffer(bh);
        if (bh->b_dirt)
            ll_rw_block(WRITE, bh);
    }
    return 0;
}

int sync_dev(int dev)
{
    int i;
    struct buffer_head *bh;

    bh = start_buffer;
    for (i = 0; i < NR_BUFFERS; i++, bh++)
    {
        if (bh->b_dev != dev)
            continue;
        wait_on_buffer(bh);
        if (bh->b_dev == dev && bh->b_dirt)
            ll_rw_block(WRITE, bh);
    }
    sync_inodes();
    bh = start_buffer;
    for (i = 0; i < NR_BUFFERS; i++, bh++)
    {
        if (bh->b_dev != dev)
            continue;
        wait_on_buffer(bh);
        if (bh->b_dev == dev && bh->b_dirt)
            ll_rw_block(WRITE, bh);
    }
    return 0;
}

static void inline invalidate_buffers(int dev)
{
    int i;
    struct buffer_head *bh;

    bh = start_buffer;
    for (i = 0; i < NR_BUFFERS; i++, bh++)
    {
        if (bh->b_dev != dev)
            continue;
        wait_on_buffer(bh);
        if (bh->b_dev == dev)
            bh->b_uptodate = bh->b_dirt = 0;
    }
}

/*
 * This routine checks whether a floppy has been changed, and
 * invalidates all buffer-cache-entries in that case. This
 * is a relatively slow routine, so we have to try to minimize using
 * it. Thus it is called only upon a 'mount' or 'open'. This
 * is the best way of combining speed and utility, I think.
 * People changing diskettes in the middle of an operation deserve
 * to loose :-)
 *
 * NOTE! Although currently this is only for floppies, the idea is
 * that any additional removable block-device will use this routine,
 * and that mount/open needn't know that floppies/whatever are
 * special.
 */
void check_disk_change(int dev)
{
    int i;

    if (MAJOR(dev) != 2)
        return;
    if (!floppy_change(dev & 0x03))
        return;
    for (i = 0; i < NR_SUPER; i++)
        if (super_block[i].s_dev == dev)
            put_super(super_block[i].s_dev);
    invalidate_inodes(dev);
    invalidate_buffers(dev);
}

#define _hashfn(dev, block) (((unsigned)(dev ^ block)) % NR_HASH)
#define hash(dev, block) hash_table[_hashfn(dev, block)]

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;
    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;
}

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;
}

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)
        if (tmp->b_dev == dev && tmp->b_blocknr == block)
            return tmp;
    return NULL;
}

/*
 * Why like this, I hear you say... The reason is race-conditions.
 * As we don't lock buffers (unless we are readint them, that is),
 * something might happen to it while we sleep (ie a read-error
 * will force it bad). This shouldn't really happen currently, but
 * the code is ready.
 */
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--;
    }
}

/*
 * Ok, this is getblk, and it isn't very clear, again to hinder
 * race-conditions. Most of the code is seldom used, (ie repeating),
 * so it should be much more efficient than it looks.
 *
 * The algoritm is changed: hopefully better, and an elusive bug removed.
 */
#define BADNESS(bh) (((bh)->b_dirt << 1) + (bh)->b_lock)
struct buffer_head *getblk(int dev, int block)
{
    struct buffer_head *tmp, *bh;

repeat:
    if ((bh = get_hash_table(dev, block)))
        return bh;
    tmp = free_list;
    do
    {
        if (tmp->b_count)
            continue;
        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);
    if (!bh)
    {
        sleep_on(&buffer_wait);
        goto repeat;
    }
    wait_on_buffer(bh);
    if (bh->b_count)
        goto repeat;
    while (bh->b_dirt)
    {
        sync_dev(bh->b_dev);
        wait_on_buffer(bh);
        if (bh->b_count)
            goto repeat;
    }
    /* NOTE!! While we slept waiting for this block, somebody else might */
    /* already have added "this" block to the cache. check it */
    if (find_buffer(dev, block))
        goto repeat;
    /* OK, FINALLY we know that this buffer is the only one of it's kind, */
    /* and that it's unused (b_count=0), unlocked (b_lock=0), and clean */
    bh->b_count = 1;
    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;
}

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);
}

/*
 * bread() reads a specified block and returns the buffer that contains
 * it. It returns NULL if the block was unreadable.
 */
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;
}

#define COPYBLK(from, to)                      \
    __asm__("cld\n\t"                          \
            "rep\n\t"                          \
            "movsl\n\t" ::"c"(BLOCK_SIZE / 4), \
            "S"(from), "D"(to))

/*
 * bread_page reads four buffers into memory at the desired address. It's
 * a function of its own, as there is some speed to be got by reading them
 * all at the same time, not waiting for one to be read, and then another
 * etc.
 */
void bread_page(unsigned long address, int dev, int b[4])
{
    struct buffer_head *bh[4];
    int i;

    for (i = 0; i < 4; i++)
        if (b[i])
        {
            if ((bh[i] = getblk(dev, b[i])))
                if (!bh[i]->b_uptodate)
                    ll_rw_block(READ, bh[i]);
        }
        else
            bh[i] = NULL;
    for (i = 0; i < 4; i++, address += BLOCK_SIZE)
        if (bh[i])
        {
            wait_on_buffer(bh[i]);
            if (bh[i]->b_uptodate)
                COPYBLK((unsigned long)bh[i]->b_data, address);
            brelse(bh[i]);
        }
}

/*
 * Ok, breada can be used as bread, but additionally to mark other
 * blocks for reading as well. End the argument list with a negative
 * number.
 */
struct buffer_head *breada(int dev, int first, ...)
{
    va_list args;
    struct buffer_head *bh, *tmp;

    va_start(args, first);
    if (!(bh = getblk(dev, first)))
        panic("bread: getblk returned NULL\n");
    if (!bh->b_uptodate)
        ll_rw_block(READ, bh);
    while ((first = va_arg(args, int)) >= 0)
    {
        tmp = getblk(dev, first);
        if (tmp)
        {
            if (!tmp->b_uptodate)
                ll_rw_block(READA, bh);
            tmp->b_count--;
        }
    }
    va_end(args);
    wait_on_buffer(bh);
    if (bh->b_uptodate)
        return bh;
    brelse(bh);
    return (NULL);
}

void buffer_init(long buffer_end)
{
    struct buffer_head *h = start_buffer;
    void *b;
    int i;

    if (buffer_end == 1 << 20)
        b = (void *)(640 * 1024);
    else
        b = (void *)buffer_end;
    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;
        h->b_prev = NULL;
        h->b_data = (char *)b;
        h->b_prev_free = h - 1;
        h->b_next_free = h + 1;
        h++;
        NR_BUFFERS++;
        if (b == (void *)0x100000)
            b = (void *)0xA0000;
    }
    h--;
    free_list = start_buffer;
    free_list->b_prev_free = h;
    h->b_next_free = free_list;
    for (i = 0; i < NR_HASH; i++)
        hash_table[i] = NULL;
}

4.源码注释版本


/*
 * linux/fs/buffer.c
 *
 * (C) 1991 Linus Torvalds
 */

/*
 * 'buffer.c' 实现了缓冲区缓存功能。
 * 通过 NEVER 让中断改变缓冲区(除了数据本身)来避免竞态条件,而是让调用者来做。
 * 注意!由于中断可以唤醒调用者,需要一些 cli-sti 序列来检查调用导致的睡眠。
 * 这些操作应该非常快速(我希望如此)。
 */

/*
 * 注意!这里有一个不协调的地方:检查软盘是否更换。
 * 我认为这是最适合的地方,因为它应该使已更改的软盘缓存失效。
 */

#include <stdarg.h>

#include <linux/config.h>
#include <linux/sched.h>
#include <linux/kernel.h>
#include <asm/system.h>
#include <asm/io.h>

// 外部变量声明
extern int end;                 // 内核结束地址(由链接器设置)
extern void put_super(int);     // 释放超级块
extern void invalidate_inodes(int); // 使inode失效

// 全局变量定义
struct buffer_head *start_buffer = (struct buffer_head *)&end; // 缓冲区起始地址
struct buffer_head *hash_table[NR_HASH];      // 哈希表,用于快速查找缓冲区
static struct buffer_head *free_list;         // 空闲缓冲区链表
static struct task_struct *buffer_wait = NULL; // 等待缓冲区的进程队列
int NR_BUFFERS = 0;                           // 缓冲区数量

// 等待缓冲区解锁(内联函数)
static inline void wait_on_buffer(struct buffer_head *bh)
{
    cli();                     // 关中断
    while (bh->b_lock)         // 如果缓冲区被锁定
        sleep_on(&bh->b_wait); // 睡眠等待
    sti();                     // 开中断
}

// 同步所有缓冲区到磁盘
int sys_sync(void)
{
    int i;
    struct buffer_head *bh;

    sync_inodes(); /* 将inode写入缓冲区 */
    bh = start_buffer;
    for (i = 0; i < NR_BUFFERS; i++, bh++) // 遍历所有缓冲区
    {
        wait_on_buffer(bh);      // 等待缓冲区解锁
        if (bh->b_dirt)          // 如果缓冲区脏(已修改)
            ll_rw_block(WRITE, bh); // 写入磁盘
    }
    return 0;
}

// 同步指定设备的所有缓冲区到磁盘
int sync_dev(int dev)
{
    int i;
    struct buffer_head *bh;

    bh = start_buffer;
    for (i = 0; i < NR_BUFFERS; i++, bh++) // 第一次遍历
    {
        if (bh->b_dev != dev)    // 只处理指定设备的缓冲区
            continue;
        wait_on_buffer(bh);
        if (bh->b_dev == dev && bh->b_dirt)
            ll_rw_block(WRITE, bh);
    }
    sync_inodes();               // 同步inode
    bh = start_buffer;
    for (i = 0; i < NR_BUFFERS; i++, bh++) // 第二次遍历
    {
        if (bh->b_dev != dev)
            continue;
        wait_on_buffer(bh);
        if (bh->b_dev == dev && bh->b_dirt)
            ll_rw_block(WRITE, bh);
    }
    return 0;
}

// 使指定设备的所有缓冲区失效(内联函数)
static void inline invalidate_buffers(int dev)
{
    int i;
    struct buffer_head *bh;

    bh = start_buffer;
    for (i = 0; i < NR_BUFFERS; i++, bh++)
    {
        if (bh->b_dev != dev)    // 只处理指定设备的缓冲区
            continue;
        wait_on_buffer(bh);      // 等待缓冲区解锁
        if (bh->b_dev == dev)
            bh->b_uptodate = bh->b_dirt = 0; // 标记为非最新和未修改
    }
}

/*
 * 此例程检查软盘是否已更改,如果是,则使所有相关的缓冲区缓存项失效。
 * 这是一个相对较慢的例程,因此我们应尽量减少使用它。
 * 因此它只在'mount'或'open'时被调用。
 * 我认为这是结合速度和效用的最佳方式。
 * 在操作过程中更换磁盘的人应该承受数据丢失的后果 :-)
 *
 * 注意!虽然目前这仅适用于软盘,但思想是任何额外的可移动块设备都将使用此例程,
 * 并且mount/open不需要知道软盘/任何东西是特殊的。
 */
void check_disk_change(int dev)
{
    int i;

    if (MAJOR(dev) != 2)        // 主设备号2是软盘
        return;
    if (!floppy_change(dev & 0x03)) // 检查软盘是否更换
        return;
    for (i = 0; i < NR_SUPER; i++) // 遍历所有超级块
        if (super_block[i].s_dev == dev)
            put_super(super_block[i].s_dev); // 释放超级块
    invalidate_inodes(dev);      // 使inode失效
    invalidate_buffers(dev);     // 使缓冲区失效
}

// 哈希函数宏定义
#define _hashfn(dev, block) (((unsigned)(dev ^ block)) % NR_HASH)
#define hash(dev, block) hash_table[_hashfn(dev, block)]

// 从队列中移除缓冲区(内联函数)
static inline void remove_from_queues(struct buffer_head *bh)
{
    /* 从哈希队列中移除 */
    if (bh->b_next)
        bh->b_next->b_prev = bh->b_prev;
    if (bh->b_prev)
        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;
    
    /* 从空闲列表中移除 */
    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;
}

// 将缓冲区插入队列(内联函数)
static inline void insert_into_queues(struct buffer_head *bh)
{
    /* 放到空闲列表末尾 */
    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;
    
    /* 如果缓冲区有设备,将其放入新的哈希队列 */
    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;
}

// 在哈希表中查找指定设备和块号的缓冲区
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)
        if (tmp->b_dev == dev && tmp->b_blocknr == block)
            return tmp;
    return NULL;
}

/*
 * 为什么这样做,我听到你说...原因是竞态条件。
 * 由于我们不锁定缓冲区(除非正在读取),当我们睡眠时可能会发生某些事情
 * (例如读取错误会将其标记为坏)。目前这不应该真正发生,但代码已准备好。
 */
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--;           // 如果不是我们要的,减少引用计数
    }
}

/*
 * 这是getblk,它不是很清晰,再次是为了防止竞态条件。
 * 大部分代码很少使用(例如重复),所以它应该比看起来更高效。
 *
 * 算法已更改:希望更好,并移除了一个难以发现的错误。
 */
// 坏度计算宏:脏缓冲区比锁定缓冲区更不适合重用
#define BADNESS(bh) (((bh)->b_dirt << 1) + (bh)->b_lock)
struct buffer_head *getblk(int dev, int block)
{
    struct buffer_head *tmp, *bh;

repeat:
    if ((bh = get_hash_table(dev, block))) // 首先检查是否已在缓存中
        return bh;
    
    // 在空闲列表中寻找最佳缓冲区
    tmp = free_list;
    do
    {
        if (tmp->b_count)        // 如果正在使用,跳过
            continue;
        if (!bh || BADNESS(tmp) < BADNESS(bh)) // 寻找"坏度"最低的缓冲区
        {
            bh = tmp;
            if (!BADNESS(tmp))   // 找到完美的缓冲区(未锁定且未修改)
                break;
        }
        /* 重复直到找到合适的 */
    } 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)) // 再次检查是否已存在
        goto repeat;
    
    /* 好的,最终我们知道这个缓冲区是唯一的,并且未使用(b_count=0),未锁定(b_lock=0),且干净 */
    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;
}

// 释放缓冲区(减少引用计数,必要时唤醒等待进程)
void brelse(struct buffer_head *buf)
{
    if (!buf)
        return;
    wait_on_buffer(buf);         // 等待缓冲区解锁
    if (!(buf->b_count--))       // 减少引用计数,检查是否已为0
        panic("Trying to free free buffer");
    wake_up(&buffer_wait);       // 唤醒等待缓冲区的进程
}

/*
 * bread() 读取指定块并返回包含它的缓冲区。
 * 如果块不可读,则返回NULL。
 */
struct buffer_head *bread(int dev, int block)
{
    struct buffer_head *bh;

    if (!(bh = getblk(dev, block))) // 获取缓冲区
        panic("bread: getblk returned NULL\n");
    if (bh->b_uptodate)          // 如果数据是最新的
        return bh;
    ll_rw_block(READ, bh);       // 从设备读取数据
    wait_on_buffer(bh);          // 等待读取完成
    if (bh->b_uptodate)          // 如果读取成功
        return bh;
    brelse(bh);                  // 否则释放缓冲区
    return NULL;                 // 返回NULL表示失败
}

// 块复制宏(使用汇编优化)
#define COPYBLK(from, to)                      \
    __asm__("cld\n\t"                          \ // 清除方向标志(向前复制)
            "rep\n\t"                          \ // 重复
            "movsl\n\t" ::"c"(BLOCK_SIZE / 4), \ // 复制次数(块大小/4,因为movsl每次复制4字节)
            "S"(from), "D"(to))                // 源地址,目标地址

/*
 * bread_page 将四个缓冲区读入内存中所需地址。
 * 它是一个独立的函数,因为通过同时读取它们可以获得一些速度,
 * 而不是等待一个被读取,然后再读取另一个等等。
 */
void bread_page(unsigned long address, int dev, int b[4])
{
    struct buffer_head *bh[4];
    int i;

    // 第一阶段:请求所有块的读取
    for (i = 0; i < 4; i++)
        if (b[i]) // 如果有块号
        {
            if ((bh[i] = getblk(dev, b[i]))) // 获取缓冲区
                if (!bh[i]->b_uptodate)      // 如果数据不是最新的
                    ll_rw_block(READ, bh[i]); // 发起读取请求
        }
        else
            bh[i] = NULL;
    
    // 第二阶段:等待读取完成并复制数据
    for (i = 0; i < 4; i++, address += BLOCK_SIZE)
        if (bh[i])
        {
            wait_on_buffer(bh[i]);           // 等待读取完成
            if (bh[i]->b_uptodate)           // 如果读取成功
                COPYBLK((unsigned long)bh[i]->b_data, address); // 复制数据
            brelse(bh[i]);                   // 释放缓冲区
        }
}

/*
 * Ok, breada 可以像bread一样使用,但另外标记其他块也要读取。
 * 用负数结束参数列表。
 */
struct buffer_head *breada(int dev, int first, ...)
{
    va_list args;
    struct buffer_head *bh, *tmp;

    va_start(args, first);
    if (!(bh = getblk(dev, first))) // 获取第一个缓冲区
        panic("bread: getblk returned NULL\n");
    if (!bh->b_uptodate)            // 如果数据不是最新的
        ll_rw_block(READ, bh);      // 发起读取请求
    
    // 预读其他块
    while ((first = va_arg(args, int)) >= 0)
    {
        tmp = getblk(dev, first);   // 获取缓冲区但不增加引用计数
        if (tmp)
        {
            if (!tmp->b_uptodate)   // 如果数据不是最新的
                ll_rw_block(READA, bh); // 发起预读请求(异步)
            tmp->b_count--;         // 减少引用计数(我们不持有它)
        }
    }
    va_end(args);
    
    wait_on_buffer(bh);             // 等待第一个缓冲区读取完成
    if (bh->b_uptodate)             // 如果读取成功
        return bh;
    brelse(bh);                     // 否则释放缓冲区
    return (NULL);                  // 返回NULL表示失败
}

// 缓冲区初始化函数
void buffer_init(long buffer_end)
{
    struct buffer_head *h = start_buffer;
    void *b;
    int i;

    // 确定缓冲区内存区域的结束位置
    if (buffer_end == 1 << 20)      // 如果缓冲区结束在1MB处
        b = (void *)(640 * 1024);   // 则从640KB开始(避开显存区域)
    else
        b = (void *)buffer_end;     // 否则从buffer_end开始
    
    // 初始化所有缓冲区头和数据区域
    while ((b -= BLOCK_SIZE) >= ((void *)(h + 1))) // 为每个缓冲区分配内存
    {
        h->b_dev = 0;               // 设备号:0表示空闲
        h->b_dirt = 0;              // 脏标志:0表示未修改
        h->b_count = 0;             // 引用计数:0表示未使用
        h->b_lock = 0;              // 锁定标志:0表示未锁定
        h->b_uptodate = 0;          // 最新标志:0表示数据可能过时
        h->b_wait = NULL;           // 等待队列:初始为空
        h->b_next = NULL;           // 哈希链表下一个指针
        h->b_prev = NULL;           // 哈希链表前一个指针
        h->b_data = (char *)b;      // 数据区域指针
        h->b_prev_free = h - 1;     // 空闲链表前一个指针
        h->b_next_free = h + 1;     // 空闲链表下一个指针
        h++;                        // 下一个缓冲区头
        NR_BUFFERS++;               // 增加缓冲区计数
        if (b == (void *)0x100000)  // 如果到达1MB边界
            b = (void *)0xA0000;    // 跳到0xA0000(640KB),避开显存
    }
    h--;                            // 调整指针
    
    // 设置空闲链表为循环链表
    free_list = start_buffer;
    free_list->b_prev_free = h;
    h->b_next_free = free_list;
    
    // 初始化哈希表
    for (i = 0; i < NR_HASH; i++)
        hash_table[i] = NULL;
}

5.源码图像版

【Linux102】58-fs/buffer.c

6.源码注释版图像

【Linux102】58-fs/buffer.c



Linux102系列

本系列将精讲Linux0.11内核中的每一个文件,共计会发布100+文章。


0.一些辅助文件

😉【Linux102】1-Makefile

😉【Linux102】2-Makefile.header

😉【Linux102】3-system.map


1.内核引导启动程序

😉【Linux102】4-bootsect.s

😉【Linux102】5-setup.s

😉【Linux102】6-head.s

😉【Linux102-D】/boot


2.内核初始化过程

😉【Linux102】7-main.c


3.进程调度与系统调用

😉【Linux102】8-kernel/asm.s

😉【Linux102】9-kernel/traps.c

😉【Linux102】10-kernel/printk.c

😉【Linux102】11-kernel/vsprintf.c

😉【Linux102】12-include/stdarg.h

😉【Linux102】13-kernel/mktime.c

😉【Linux102】14-kernel/system_call.s

😉【Linux102】15-include/linux/sched.h

😉【Linux102】16-kernel/sched.c

😉【Linux102】17-kernel/signal.c

😉【Linux102】18-include/signal.h

😉【Linux102】19-include/sys/types.h

😉【Linux102】20-include/linux/kernel.h

😉【Linux102】21-include/asm/segment.h

😉【Linux102】22-include/linux/head.h

😉【Linux102】23-include/linux/mm.h

😉【Linux102】24-include/linux/fs.h

😉【Linux102】25-include/errno.h

😉【Linux102】26-include/sys/wait.h

😉【Linux102】27-include/inux/tty.h

😉【Linux102】28-include/termios.h

😉【Linux102】29-kernel/panic.c

😉【Linux102】30-include/sys/times.h

😉【Linux102】31-include/sys/utsname.h

😉【Linux102】32-include/stddef.h

😉【Linux102】33-include/linux/sys.h

😉【Linux102】34-kernel/sys.c

😉【Linux102】35-kernel/fork.c

😉【Linux102】36-include/asm/system.h

😉【Linux102】37-kernel/exit.c

😉【Linux102】38-include/linux/fdreg.h

😉【Linux102】39-include/asm/io.h


4.输入输出系统--块设备驱动程序

😉【Linux102】40-kernel/blk_drv/blk.h

😉【Linux102】41-kernel/blk_drv/hd.c

😉【Linux102】42-include/linux/config.h

😉【Linux102】43-include/linux/hdreg.h

😉【Linux102】45-kernel/blk_drv/ramdisk.c

😉【Linux102】46-include/asm/memory.h

😉【Linux102】47-include/string.h

😉【Linux102】48-kernel/blk_drv/floppy.c


5.输入输出系统——字符设备驱动程序

😉【Linux102】49-kernel/chr_drv/keyboard.S

😉【Linux102】50-kernel/chr_drv/console.c

😉【Linux102】51-kernel/chr_drv/serial.c

😉【Linux102】52-kernel/chr_drv/rs_io.s

😉【Linux102】53-kernel/chr_drv/tty_io.c

😉【Linux102】54-include/ctype.h

😉【Linux102】55-lib/ctype.c

😉【Linux102】56-kernel/chr_drv/tty_ioctl.c


Linux内核精讲系列

和Linux内核102系列不同,本系列将会从全局描绘Linux内核的各个模块,而非逐行源码分析,适合想对Linux系统有宏观了解的家人阅读。

😉【Linux】学习Linux前必备的知识点

😉【Linux】Linux内核对进程的内存抽象

😉【Linux】Linux概述1-linux对物理内存的使用

😉【Linux】软件从写下到运行的全部流程

😉【Linux】CPU的三寻址:实模式、保护模式、长模式

😉【Linux】实模式与保护模式的寻址, 这次讲明白了!

😉【Linux】linux0.11的源码目录架构

😉【Linux】Makefile机制及基础详解

😉【Linux】编译并运行Linux0.11

😉【Linux】“进进内网文”—Linux的内核结构全貌

😉【Linux】linux的中断机制

😉【Linux】linux进程描述


汇编语言

本系列将带领大家从0开始循序渐进学习汇编语言,直至完全掌握这门底层语言。同时给出学习平台DOSBox的使用教程。

😉【汇编语言】1—基础硬件知识

😉【汇编语言】2—寄存器基础知识

😉【汇编语言】3-寄存器与内存的交互

😉【汇编语言】4-第一个完整的汇编程序

😉【汇编语言】5-[BX]和loop指令

😉【汇编语言】6-包含多个段的程序

😉【汇编语言】7-灵活的5大寻址方式

😉【汇编语言】8-1-数据的位置

😉【汇编语言】8-2-数据的长度

😉【汇编语言】8-数据处理的两个基本问题(位置与长度)

😉【DOSBox】1-debug

😉【DOSBox】2-debug可执行文件

😉【DOSBox】3-完整开发流程


C语言

本系列将直击C语言的本质基础,流利处理出各个易错、实用的实战点,并非从零开始学习C。

😉【C语言】C Token(C89 C99 C11)

😉【C语言】指针基础

😉【C语言】数组基础

😉【C语言】结构体对齐

😉【C语言】华为C语言进阶测试

😉【C语言】触发刷新到磁盘的方式总结

😉【C语言】C语言文件操作的mode详解

😉【C语言】C语言文件知识全讲解

😉【C语言】从extern到头文件包含的最优实践

😉【C语言】C语言的关键字与重载机制

😉【C语言】长字符串的2种处理方式

😉【C语言】C语言嵌入汇编程序

😉【C语言】指针数组 VS 数组指针 原来这么简单!

😉【C语言】深浅拷贝、传参、赋值 本质剖析

😉【C语言】find-in-linux递归搜索文件名函数

😉【C陷阱与缺陷】-1-词法陷阱

😉【C陷阱与缺陷】-2-语法陷阱

😉【C陷阱与缺陷】-3-语义陷阱


关于小希

😉嘿嘿嘿,我是小希,专注Linux内核领域,同时讲解C语言汇编等知识。

我的微信:C_Linux_Cloud,期待与您学习交流!

加微信请备注哦


小希的座右铭:别看简单,简单也是难。别看难,难也是简单。我的文章都是讲述简单的知识,如果你喜欢这种风格:

不妨关注、评论、转发,让更多朋友看到哦~~~🙈

下一期想看什么?在评论区留言吧!我们下期见!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值