高速缓冲区的管理方式

整个高速缓冲区被划分为 1024 字节一块的缓冲块,正好与块设备上的磁盘逻辑块大小相同。在高速缓冲区初始化时,初始化程序分别从缓冲区的两端开始,分别同时设置缓冲头和划分出对应的缓冲块,如图所示:

缓冲区概况

缓冲头是定义在 include/linux/fs.h 中的一个结构体,用于描述对应缓冲块的各种属性,并用于将所用缓冲头连成链表。缓冲块的划分一直持续到缓冲区中没有足够的内存再划分出缓冲块为止

缓冲头的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// include/linux/fs.h Line 68
struct buffer_head {
char * b_data; // 指向对应缓冲块起始地址
unsigned long b_blocknr; // 缓冲块的块号
unsigned short b_dev; // 数据源的设备号
unsigned char b_uptodate; // 更新标志,表示数据是否已经更新
unsigned char b_dirt; // 修改标志,为 0 表示未被修改,为 1 表示已被修改过
unsigned char b_count; // 使用该块的用户数
unsigned char b_lock; // 缓冲块是否被锁定,1 表示被上锁
struct task_struct * b_wait; // 指向等待该缓冲块解锁的任务
struct buffer_head * b_prev; // hash 队列的前一块
struct buffer_head * b_next; // hash 队列的下一块
struct buffer_head * b_prev_free; // 空闲表的前一块
struct buffer_head * b_next_free; // 空闲表的下一块
};

b_blocknrb_dev 唯一确定了缓冲块中的数据对应的块设备和数据块

b_count 字段表示引用该块的进程数,当其不为 0 时,缓冲管理程序就不能释放该块。程序申请读/写硬盘上的一个块时,会先在高速缓冲中申请一个块,若在 hash 表中能得到指定的块,则该块的 b_count 增加 1,否则表示缓冲块是重新申请得到的,该块的 b_count 置为 1。当程序释放一个块时,该块的 b_count 减 1

b_lock 为锁定标志,当其为 1 时,表示驱动程序正在对该缓冲块内容进行修改。更新缓冲块中的数据时,进程会主动睡眠,此时其他进程就有访问同样缓冲块的机会,因此在睡眠前该缓冲块对应缓冲头的 b_lock 字段被置 1

b_dirt 为修改标志,表示缓冲块中的内容是否与块设备上对应数据块的内容不同。b_uptodate 为数据更新标志,用于说明缓冲块中的数据是否有效。

  • 初始化或释放块时,这两个标志均置为 0,表示该缓冲块中的数据无效
  • 当数据被写入缓冲块但还没有被写入块设备中时,b_dirt = 1,b_uptodate = 0
  • 当数据被写入块设备或刚从块设备中读入缓冲块时,b_dirt = 0,b_uptodate = 1
  • 在新申请一个缓冲块时,这两个标志均为 1

b_prev_freeb_next_free 字段用于构建空闲缓冲块对应缓冲头的双向链表,如图:

空闲缓冲头构成双向链表

b_prevb_next 字段用于构建 hash 表。buffer.c 中使用具有 307 个缓冲头指针项的 hash 数组表结构,从而达到快速而有效地在缓冲区中寻找请求的数据块是否已经被读入到缓冲区中的目的。这两个字段就是用于 hash 表中国散列在同一项上多个缓冲块之间的双向链接,如图所示:

缓冲头的hash表

图中的双箭头实线表示散列在同一 hash 表项中缓冲头结构体之间的双向链接指针。虚线表示缓冲区中所有缓冲块组成的一个双向循环链表(即所谓的空闲链表),实际上这个双向链表是最近最少使用链表(LRU)


读取文件的完整过程

前面也铺垫的差不多了,接下来,通过一个文件从打开(open)、读取(read)到关闭(close)的过程来整体把握文件系统,其中涉及的一些较为底层的函数现在只需知道功能即可

首先修改一下 main.c 的 init 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void init(void)
{
int fd;
char msg[80];

setup((void *) &drive_info);
(void) open("/dev/tty0",O_RDWR,0);
(void) dup(0);
(void) dup(0);

fd = open("/usr/root/hello.c", O_RDONLY, 0);
msg[read(fd, msg, 79)] = '\0';
printf("%s", msg);
close(fd);
while(1) ;
}

上面的代码实现打开 hello.c 并获取其句柄、读取并输出其内容及关闭文件,运行结果:

hello.c内容

open

open 实际上是一个系统调用,在 system_call 中调用 sys_open,参数 filename 为要打开的文件名字符串指针;flag 为打开文件的标志(只读、只写、可读可写等);mode 只有在创建文件时才会被用于指定文件的许可属性(如 0664)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// fs/open.c Line 138
int sys_open(const char * filename,int flag,int mode)
{
struct m_inode * inode;
struct file * f;
int i,fd;

mode &= 0777 & ~current->umask; // umask 的作用在这里体现(参考 umask 指令)
for(fd=0 ; fd<NR_OPEN ; fd++) // 遍历进程打开文件数组 flip
if (!current->filp[fd]) // 寻找一个空闲项
break;
if (fd>=NR_OPEN) // 没有空闲项则返回出错码
return -EINVAL;
// 设置进程执行时关闭文件句柄位图,将找出的空闲项对应的比特位复位
current->close_on_exec &= ~(1<<fd);
f=0+file_table; // f 指向文件表数组起始
for (i=0 ; i<NR_FILE ; i++,f++) // 遍历文件表,寻找空闲项
if (!f->f_count) break;
if (i>=NR_FILE) // 文件表数组没有空闲项,返回出错码
return -EINVAL;
(current->filp[fd]=f)->f_count++; // 该项的文件引用计数加 1
// 获得 filename 对应文件的 i 节点指针,如果出错,释放刚找到的空闲项
if ((i=open_namei(filename,flag,mode,&inode))<0) {
current->filp[fd]=NULL;
f->f_count=0;
return i;
}

sys_open 函数还没有结束,但这里有必要打断一下,来说说 task_struct 中的 filp 字段与文件表数组 file_table 的关系,及 close_on_exec 字段的含义;之后深入 open_namei 函数去查看其实现细节

  1. flip 与 file_table

    os 维护着一张元素个数为 64(NR_FILE)的打开文件表,名为 file_table,该数组的元素类型为 file 结构体,记录着所有已被打开的文件的信息;每个进程的 task_struct 结构体中都有一个元素个数为 20(NR_OPEN)的 file 结构体指针数组,如果其中的某一项非空(NULL),其必定指向 file_table 数组中的一个 file 结构体,表示该进程捏着这个文件的句柄,可以对其进行合法的操作

    那么现在就好解释为什么标准输入的句柄是 0,标准输出的句柄是 1 了。还记得 init 函数中的操作吗:(void) open("/dev/tty0",O_RDWR,0); 该函数以可读可写模式打开终端设备,此时 1 号进程的 filp 数组为空,故 filp[0] 为 sys_open 中找到的空闲项。在 open 系统调用成功返回后,file_table 中就会有一项 tty0 的 file 结构体,而 1 号进程的 filp[0] 就指向该结构体。之后 init 调用 (void) dup(0); 复制文件句柄,即使得 filp[1] 也同样指向 tty0 的 file结构体。以此类推,标准错误的句柄在第二次 dup 后应该为 2。你会发现,所谓的文件句柄,其实是进程 task_struct 结构体中 filp 数组的下标

  2. close_on_exec

    task_struct 中该字段用于确定在调用 execve 时需要关闭的文件句柄,类型为 unsigned long ,每一个比特位对应一个打开着的文件描述符。当进程创建出子进程后,往往会调用 execve 加载新的程序,此时若文件句柄在 close_on_exec 中对应的比特位为 1,则执行 do_execve 时,该文件将被关闭。在打开一个文件时,默认情况下文件句柄在子进程中也处于打开状态

下面来研究 open_namei 是如何通过 pathname 来找到文件对应 i 节点的,粗略的过程及目录项结构体的定义在 Minix 1.0 文件系统 一文中有所提及,此时 pathname 为 “/usr/root/hello.c”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// fs/namei.c Line 337
// 最后一个参数用于保存文件路径 pathname 对应 i 节点的指针(保存函数返回值)
int open_namei(const char * pathname, int flag, int mode,
struct m_inode ** res_inode)
{
const char * basename;
int inr,dev,namelen;
struct m_inode * dir, *inode;
struct buffer_head * bh;
struct dir_entry * de;
// 如果文件访问模式是只读,但文件截零标志 O_TRUNC 置位,则添加只写 O_WRONLY 标志
if ((flag & O_TRUNC) && !(flag & O_ACCMODE))
flag |= O_WRONLY;
// 下面两句作用是产生一个 mode,当指定文件不存在需要创建时,将其作为新文件的属性
mode &= 0777 & ~current->umask;
mode |= I_REGULAR; // I_REGULAR 表示常规文件
// dir_namei 返回值为目录 "/usr/root" 对应的 i 节点指针,
// namelen 为 "hello.c" 长度,basename 指向字符串 "hello.c"
// 相当于将 pathname 进行切割,分成父级及以上目录 "/usr/root" 与 basename "hello.c"
if (!(dir = dir_namei(pathname,&namelen,&basename)))
return -ENOENT;
if (!namelen) { // 如果 basename 的长度为 0,表示操作的是目录
if (!(flag & (O_ACCMODE|O_CREAT|O_TRUNC))) { // 如果操作不是读写、创建、截零
*res_inode=dir; // 直接返回目录对应的 i 节点指针
return 0;
}
iput(dir); // 到这里表示操作非法,放回 i 节点
return -EISDIR; // 返回出错码
}
// find_entry 在 "/usr/root" 目录下查找 "hello.c" 对应的目录项,存放在 de 中,
// 并返回该目录项所在的高速缓冲块对应的缓冲头指针
bh = find_entry(&dir,basename,namelen,&de);
// 如果缓冲头为 NULL,表示没有找到对应文件名的目录项,即只能是创建文件的操作
// 因为 "/usr/root/hello.c" 在硬盘上是存在的,所以不会进下面这个 if
if (!bh) {
if (!(flag & O_CREAT)) { // 如果没有 O_CREAT 标志
iput(dir); // 放回 i 节点,并返回出错码
return -ENOENT;
}
if (!permission(dir,MAY_WRITE)) { // 如果用户在该目录没有写权利
iput(dir); // 放回 i 节点,并返回出错码
return -EACCES;
}
inode = new_inode(dir->i_dev); // 申请一个新 i 节点
if (!inode) { // 如果失败,放回 i 节点,并返回出错码
iput(dir);
return -ENOSPC;
}
inode->i_uid = current->euid; // 设置用户 id、访问模式,置已修改标志
inode->i_mode = mode;
inode->i_dirt = 1;
bh = add_entry(dir,basename,namelen,&de); // 在 "/usr/root" 目录下新建一个目录项
if (!bh) { // 如果失败
inode->i_nlinks--; // 新节点硬连接数减 1
iput(inode); // 放回该 i 节点与目录 i 节点,返回出错码
iput(dir);
return -ENOSPC;
}
de->inode = inode->i_num; // 目录项 i 节点号置为新申请到的 i 节点号
bh->b_dirt = 1; // 置已修改标志
brelse(bh); // 释放该高速缓冲块
iput(dir); // 放回目录的 i 节点
*res_inode = inode; // 返回新文件的 i 节点指针
return 0;
}
inr = de->inode; // 获得 "hello.c" 对应 i 节点号
dev = dir->i_dev; // 获得其所在设备号
brelse(bh); // 释放该高速缓冲块
iput(dir); // 放回目录的 i 节点
if (flag & O_EXCL) // 如果独占操作标志 O_EXCL 置位,返回出错码
return -EEXIST;
if (!(inode=iget(dev,inr))) // 根据 i 节点号获得 "hello.c" 对应 i 节点指针
return -EACCES; // 出错返回出错码
// 如果取得的 i 节点是一个目录的 i 节点,且访问模式是只读或读写,或者没有访问权限
if ((S_ISDIR(inode->i_mode) && (flag & O_ACCMODE)) ||
!permission(inode,ACC_MODE(flag))) {
iput(inode); // 放回 i 节点,返回出错码
return -EPERM;
}
inode->i_atime = CURRENT_TIME; // 更新该 i 节点访问时间字段为当前时间
if (flag & O_TRUNC) // 如果有截零标志,则将文件长度截为 0
truncate(inode);
*res_inode = inode; // 返回 "/usr/root/hello.c" 对应 i 节点指针
return 0;
}

那么根据 “/usr/root/hello.c” 是怎样找到 “/usr/root” 的 i 节点指针的呢?其步骤类似 open_namei 中已知 “/usr/root” 的 i 节点指针(调用完 dir_namei 函数),获取 “hello.c” 的 i 节点指针。现已知根目录 “/“ i 节点指针,通过调用 find_entry,找到 “usr” 对应的目录项,从而得知其 i 节点号,再调用 iget 即可获取 “/usr” 的 i 节点指针;第二次调用 find_entry 在 “/usr” 目录下找到 “root” 对应的目录项,从而得知 “/usr/root” 的 i 节点号,调用 iget 获取 “/usr/root” 的 i 节点指针。理所当然地,能想到应该使用一个 whlie 循环来完成上述操作,下面是 dir_namei 函数的代码,其调用 get_dir 函数,在 get_dir 中通过 while 循环来获取 “/usr/root” 的 i 节点指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// fs/namei.c Line 278
static struct m_inode * dir_namei(const char * pathname,
int * namelen, const char ** name)
{
char c;
const char * basename;
struct m_inode * dir;

if (!(dir = get_dir(pathname))) // 获取 "/usr/root" 的 i 节点指针
return NULL; // 失败返回 NULL
basename = pathname;
while (c=get_fs_byte(pathname++)) // 使得 basename 指向 "hello.c"
if (c=='/')
basename=pathname;
*namelen = pathname-basename-1; // 计算 basename 的长度
*name = basename; // 返回 basename 指针及 "/usr/root" 的 i 节点指针
return dir;
}

// Line 228
static struct m_inode * get_dir(const char * pathname)
{
char c;
const char * thisname;
struct m_inode * inode;
struct buffer_head * bh;
int namelen,inr,idev;
struct dir_entry * de;
// 当前进程的根与当前工作路径的 i 节点需存在且有效
if (!current->root || !current->root->i_count)
panic("No root inode");
if (!current->pwd || !current->pwd->i_count)
panic("No cwd inode");
if ((c=get_fs_byte(pathname))=='/') { // 如果是绝对路径
inode = current->root; // 起始 inode 设置为根目录(或伪根)对应 i 节点指针
pathname++;
} else if (c) // 否则是相对路径
inode = current->pwd; // 起始 inode 设置为当前工作路径对应 i 节点指针
else
return NULL;
inode->i_count++; // 引用计数加 1
while (1) { // 开始逐层解析目录路径
thisname = pathname; // thisname 指向正在处理的目录名
// 如果不是目录或没有进入该目录的权限
if (!S_ISDIR(inode->i_mode) || !permission(inode,MAY_EXEC)) {
iput(inode); // 放回 i 节点
return NULL;
}
// 搜索到下一个 '/',如第一次循环时,namelen = 3(usr),pathname 指向 "root/hello.c"
for(namelen=0;(c=get_fs_byte(pathname++))&&(c!='/');namelen++)
/* nothing */ ;
if (!c) // 如果已经搜索到路径名末尾
return inode; // 返回 i 节点指针
// 在 inode 对应的目录下查找长度为 namelen 的 thisname 对应的目录项
if (!(bh = find_entry(&inode,thisname,namelen,&de))) {
iput(inode); // 失败放回 i 节点
return NULL;
}
inr = de->inode; // 获得 thisname 对应的 i 节点号
idev = inode->i_dev; // 获得设备号
brelse(bh); // 释放含有该目录项的高速缓冲块
iput(inode); // 放回 i 节点
if (!(inode = iget(idev,inr))) // 获得设备号为 idev、i 节点号为 inr 的 i 节点指针
return NULL;
}
}

至此,不再深入,回到 sys_open 函数。之前调用 open_namei 时,返回的 i 节点指针存储在 inode 中,现在要根据 i_mode 字段判断该文件的类型,对于不同类型的文件,需要做一些处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// fs/open.c Line 163
if (S_ISCHR(inode->i_mode)) // 如果打开的是字符设备,无关,不作记录
if (MAJOR(inode->i_zone[0])==4) {
if (current->leader && current->tty<0) {
current->tty = MINOR(inode->i_zone[0]);
tty_table[current->tty].pgrp = current->pgrp;
}
} else if (MAJOR(inode->i_zone[0])==5)
if (current->tty<0) {
iput(inode);
current->filp[fd]=NULL;
f->f_count=0;
return -EPERM;
}

if (S_ISBLK(inode->i_mode)) // 如果打开的是块设备
check_disk_change(inode->i_zone[0]); //检查盘片是否被更换
f->f_mode = inode->i_mode; // 设置 file_table 中 file 结构体的一些属性
f->f_flags = flag;
f->f_count = 1;
f->f_inode = inode;
f->f_pos = 0;
return (fd); // 返回文件句柄
}

此时返回的 fd 应该为 3


read

read 也是一个系统调用,处理函数为 sys_read,现在要从已打开的 “/usr/root/hello.c” 中读取 79 个字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// fs/read_write.c Line 55
int sys_read(unsigned int fd,char * buf,int count)
{
struct file * file;
struct m_inode * inode;
// 判断参数文件句柄与要读的字符数的合法性
if (fd>=NR_OPEN || count<0 || !(file=current->filp[fd]))
return -EINVAL;
if (!count) // count 为 0,表示一个字符都不读,返回
return 0;
verify_area(buf,count); // 验证存放数据的地址是否存在内存越界等问题
inode = file->f_inode; // 获得文件 i 节点指针
if (inode->i_pipe) // 如果是管道文件,进行管道读操作
return (file->f_mode&1)?read_pipe(inode,buf,count):-EIO;
if (S_ISCHR(inode->i_mode)) // 如果是字符型文件,进行字符设备读操作
return rw_char(READ,inode->i_zone[0],buf,count,&file->f_pos);
if (S_ISBLK(inode->i_mode)) // 如果是块设备文件,进行块设备读操作
return block_read(inode->i_zone[0],&file->f_pos,buf,count);
if (S_ISDIR(inode->i_mode) || S_ISREG(inode->i_mode)) { // 是目录或普通文件
if (count+file->f_pos > inode->i_size) // 如果读写指针加上欲读字符数超出文件末尾
count = inode->i_size - file->f_pos; // 重新计算 count
if (count<=0)
return 0;
return file_read(inode,file,buf,count); // 进行文件读操作
}
printk("(Read)inode->i_mode=%06o\n\r",inode->i_mode);
return -EINVAL;
}

因为 “/usr/root/hello.c” 是一个普通文件,所以应该调用 file_read 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// fs/file_dev.c Line 17
int file_read(struct m_inode * inode, struct file * filp, char * buf, int count)
{
int left,chars,nr;
struct buffer_head * bh;

if ((left=count)<=0) // 判断参数有效性
return 0;
while (left) { // 如果还需要读取的字符数不为 0
// bmap 获取文件当前读写位置的数据块在设备上对应的逻辑块号
if (nr = bmap(inode,(filp->f_pos)/BLOCK_SIZE)) {
if (!(bh=bread(inode->i_dev,nr))) // 从设备上读取该逻辑块
break;
} else // bmap 失败,bh 置为空
bh = NULL;

file_read 还没有结束,先来看看 bread 函数的实现细节,该函数的作用是从指定设备号的设备中读取指定块号的数据到高速缓冲块中,返回值是缓冲块对应的缓冲头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
// fs/buffer.c Line 270
// dev 是设备号,block 是逻辑块号
struct buffer_head * bread(int dev,int block)
{
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); // 调用底层块设备读写函数 ll_rw_block
wait_on_buffer(bh); // 当前进程睡眠,等待缓冲块解锁
if (bh->b_uptodate) // 醒来后如果缓冲区已被更新,则返回
return bh;
brelse(bh); // 否则表示读操作失败,释放该缓冲区
return NULL;
}

// kernel/blk_dev/ll_rw_blk.c Line 145
// 参数 rw 是操作类型(如读/写),bh 对应的缓冲块用于存储从块设备中读出的数据
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); // 否则创建请求项,并加入请求队列
}

// Line 88
static void make_request(int major,int rw, struct buffer_head * bh)
{
struct request * req;
int rw_ahead;
// 预读取与预写功能并非必要,若缓冲区已上锁,就不用管它,否则它只是一个一般的读/写操作
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: // 给读请求保留一些空间,防止数组被写请求占满,无法读数据
if (rw == READ) // 如果是读请求,就从 request 数组末开始往前搜索空闲项
req = request+NR_REQUEST;
else // 如果是写请求,就从 request 数组 2/3 处开始往前搜索空闲项
req = request+((NR_REQUEST*2)/3);
while (--req >= request) // 搜索空闲项,req->dev 为 -1 表示该项空闲
if (req->dev<0)
break;
if (req < request) { // 如果已经搜索到 request 数组头
if (rw_ahead) { // 如果是预读/写请求,则释放缓冲头并退出
unlock_buffer(bh);
return;
}
sleep_on(&wait_for_request); // 否则就睡眠,过会儿再搜索请求队列
goto repeat;
}
// 设置请求项各字段
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); // 将请求项添加到 blk_dev[3](硬盘) 的请求队列中
}

// Line 64
// 该函数将设置好的请求项 req 加入指定设备的请求处理队列
static void add_request(struct blk_dev_struct * dev, struct request * req)
{
struct request * tmp;

req->next = NULL; // next 字段先置空
cli(); // 关中断
if (req->bh) // 设置缓冲头的已修改标志为 0
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(); // 开中断
}

硬盘对应的请求处理函数 dev->request_fn 为 do_hd_request,在上一篇文章 块设备驱动 中已经给出注释。add_request 函数将请求添加到队列中后,会返回到 bread 中,执行 wait_on_buffer,等待读请求完成,再返回到 file_read 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// fs/file_dev.c Line 30
nr = filp->f_pos % BLOCK_SIZE; // 计算文件当前读写指针在数据块中的偏移 nr
chars = MIN( BLOCK_SIZE-nr , left ); // 获得二者之间较小的值
filp->f_pos += chars; // 设置文件当前读写指针
left -= chars; // 剩余未读字符数减去将拷贝字符数
if (bh) {
char * p = nr + bh->b_data;
while (chars-->0) // buf 为 read 第二个参数
put_fs_byte(*(p++),buf++); // 将高速缓冲块中的数据拷贝到目标地址处
brelse(bh); // 释放缓冲头
} else { // 如果上面 bread 失败,bh 为 NULL,则将 buf 填 0
while (chars-->0)
put_fs_byte(0,buf++);
}
}
inode->i_atime = CURRENT_TIME; // 设置 i 节点访问时间为当前时间
return (count-left)?(count-left):-ERROR; // 返回读到 buf 的字符数
}

至此,”/usr/root/hello.c” 文件的内容已经被读入 msg 中,最后是文件的关闭


close

close 系统调用就比较简单了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// fs/open.c Line 192
int sys_close(unsigned int fd)
{
struct file * filp;

if (fd >= NR_OPEN) // 判断参数的合法性
return -EINVAL;
current->close_on_exec &= ~(1<<fd); // 复位执行时关闭位图中对应的位
if (!(filp = current->filp[fd])) // 如果该文件句柄不存在,返回错误码
return -EINVAL;
current->filp[fd] = NULL; // 将 filp 数组该项置空
if (filp->f_count == 0) // 如果句柄引用计数已经为 0,说明内核出现错误,停机
panic("Close: file count is 0");
if (--filp->f_count) // 否则将引用计数减一
return (0);
iput(filp->f_inode); // 放回该 i 节点
return (0);
}

呼~ 这个系列到此结束啦✿✿ヽ(°▽°)ノ✿,后面也许会搞点内核 pwn,到时候再做记录,收工收工