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

缓冲头是定义在 include/linux/fs.h 中的一个结构体,用于描述对应缓冲块的各种属性,并用于将所用缓冲头连成链表。缓冲块的划分一直持续到缓冲区中没有足够的内存再划分出缓冲块为止
缓冲头的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| struct buffer_head { char * b_data; unsigned long b_blocknr; unsigned short b_dev; unsigned char b_uptodate; unsigned char b_dirt; unsigned char b_count; unsigned char b_lock; struct task_struct * b_wait; struct buffer_head * b_prev; struct buffer_head * b_next; struct buffer_head * b_prev_free; struct buffer_head * b_next_free; };
|
b_blocknr 与 b_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_free 与 b_next_free 字段用于构建空闲缓冲块对应缓冲头的双向链表,如图:

b_prev 与 b_next 字段用于构建 hash 表。buffer.c 中使用具有 307 个缓冲头指针项的 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 并获取其句柄、读取并输出其内容及关闭文件,运行结果:

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
| int sys_open(const char * filename,int flag,int mode) { struct m_inode * inode; struct file * f; int i,fd;
mode &= 0777 & ~current->umask; for(fd=0 ; fd<NR_OPEN ; fd++) if (!current->filp[fd]) break; if (fd>=NR_OPEN) return -EINVAL; current->close_on_exec &= ~(1<<fd); f=0+file_table; 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++; 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 函数去查看其实现细节
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 数组的下标
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
|
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; if ((flag & O_TRUNC) && !(flag & O_ACCMODE)) flag |= O_WRONLY; mode &= 0777 & ~current->umask; mode |= I_REGULAR; if (!(dir = dir_namei(pathname,&namelen,&basename))) return -ENOENT; if (!namelen) { if (!(flag & (O_ACCMODE|O_CREAT|O_TRUNC))) { *res_inode=dir; return 0; } iput(dir); return -EISDIR; } bh = find_entry(&dir,basename,namelen,&de); if (!bh) { if (!(flag & O_CREAT)) { iput(dir); return -ENOENT; } if (!permission(dir,MAY_WRITE)) { iput(dir); return -EACCES; } inode = new_inode(dir->i_dev); if (!inode) { iput(dir); return -ENOSPC; } inode->i_uid = current->euid; inode->i_mode = mode; inode->i_dirt = 1; bh = add_entry(dir,basename,namelen,&de); if (!bh) { inode->i_nlinks--; iput(inode); iput(dir); return -ENOSPC; } de->inode = inode->i_num; bh->b_dirt = 1; brelse(bh); iput(dir); *res_inode = inode; return 0; } inr = de->inode; dev = dir->i_dev; brelse(bh); iput(dir); if (flag & O_EXCL) return -EEXIST; if (!(inode=iget(dev,inr))) return -EACCES; if ((S_ISDIR(inode->i_mode) && (flag & O_ACCMODE)) || !permission(inode,ACC_MODE(flag))) { iput(inode); return -EPERM; } inode->i_atime = CURRENT_TIME; if (flag & O_TRUNC) truncate(inode); *res_inode = inode; 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
| 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))) return NULL; basename = pathname; while (c=get_fs_byte(pathname++)) if (c=='/') basename=pathname; *namelen = pathname-basename-1; *name = basename; return dir; }
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; 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; pathname++; } else if (c) inode = current->pwd; else return NULL; inode->i_count++; while (1) { thisname = pathname; if (!S_ISDIR(inode->i_mode) || !permission(inode,MAY_EXEC)) { iput(inode); return NULL; } for(namelen=0;(c=get_fs_byte(pathname++))&&(c!='/');namelen++) ; if (!c) return inode; if (!(bh = find_entry(&inode,thisname,namelen,&de))) { iput(inode); return NULL; } inr = de->inode; idev = inode->i_dev; brelse(bh); iput(inode); if (!(inode = iget(idev,inr))) 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
| 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; 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
| 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) return 0; verify_area(buf,count); inode = file->f_inode; 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; 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
| 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) { if (nr = bmap(inode,(filp->f_pos)/BLOCK_SIZE)) { if (!(bh=bread(inode->i_dev,nr))) break; } else 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
|
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; }
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); }
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) req = request+NR_REQUEST; else req = request+((NR_REQUEST*2)/3); while (--req >= request) if (req->dev<0) break; if (req < 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); }
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(); }
|
硬盘对应的请求处理函数 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
| nr = filp->f_pos % BLOCK_SIZE; chars = MIN( BLOCK_SIZE-nr , left ); filp->f_pos += chars; left -= chars; if (bh) { char * p = nr + bh->b_data; while (chars-->0) put_fs_byte(*(p++),buf++); brelse(bh); } else { while (chars-->0) put_fs_byte(0,buf++); } } inode->i_atime = CURRENT_TIME; return (count-left)?(count-left):-ERROR; }
|
至此,”/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
| 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; if (filp->f_count == 0) panic("Close: file count is 0"); if (--filp->f_count) return (0); iput(filp->f_inode); return (0); }
|
呼~ 这个系列到此结束啦✿✿ヽ(°▽°)ノ✿,后面也许会搞点内核 pwn,到时候再做记录,收工收工