环境:Linux 0.11 / Linux 3.4.2
参考书籍:Linux内核完全剖析基于0.11内核-赵炯
一、Linux中使用文件系统的部分
1.1关于Linux中高速缓冲区的管理程序
1.2文件系统的底层通用函数(对于硬盘的读写,分配释放等,对于目录节点inode的管理,以及内存和磁盘的映射
1.3文件数据进行读写的操作模块(vfs:虚拟文件系统 硬件驱动和文件系统的关系)
1.4文件系统与其他程序接口的实现(fopen fclose等等)
二、文件系统的基本概念
对于一个硬盘设备,通常都会划分出几个盘片,每个盘片存放着一个不同的文件系统。比如下图将一个硬盘分成了4个分区,包含了4个不同的文件系统。其中主引导扇区中存放着磁盘引导程序和分区表信息(指明硬盘上的每个分区的类型)。
对于Linux0.11内核采用的文件系统是MINIX文件系统,它的分布如下图哦所示:
① 引导块:用来引导设备的,通常在上电时由BIOS自动读入运行的数据。对于非引导设备的盘引导块内容为空。
② 超级快:相当于文件系统的描述符,定义如下:
struct super_block {
unsigned short s_ninodes;
unsigned short s_nzones;
unsigned short s_imap_blocks;
unsigned short s_zmap_blocks;
unsigned short s_firstdatazone;
unsigned short s_log_zone_size;
unsigned long s_max_size;
unsigned short s_magic;
/* These are only in memory */
struct buffer_head * s_imap[8];
struct buffer_head * s_zmap[8];
unsigned short s_dev;
struct m_inode * s_isup;
struct m_inode * s_imount;
unsigned long s_time;
struct task_struct * s_wait;
unsigned char s_lock;
unsigned char s_rd_only;
unsigned char s_dirt;
};
s_ninode表示当前块设备的inode节点数。每个inode代表一个文件。
s_nzones表示当前块设备上以逻辑块为大小 (1KB)的逻辑块数。
s_imap_blocks与s_zmap_blocks分别表示当前块设备的inode节点位图和逻辑块位图所占用的逻辑块数。
逻辑块位图用于描述当前设备每个磁盘块的使用情况。如果为0,表示对应的磁盘块是空闲的,可以分配使用。当一个磁盘块被分配占用后,对应的逻辑块位图的比特位被置1
根据超级块数据结构中定义,s_zmap是一个数组,它占用了8块磁盘块大小,每个块大小 是1024字节,因此总共可以管理8192*8个比特位,每个比特位分别对应一个数据磁盘块,总计这8个磁盘块大小可以管理655356数据磁盘块。
inode节点位图是用来标记iNode节点的使用情况。与逻辑块位图 类似,当创建一个文件时候,我们分配一个iNode数据结构,并且些iNode实例对应的inode位图数组对应的位需要被置1。
s_firstdatazones表示当前块设备上数据区开始位置占用的第一个逻辑块的块号。
s_max_size表示当前块设备上,以字节为单位的最大文件 的长度。
s_magic表示文件系统的魔数,标示了文件系统的类型。
④ i节点位图:类似逻辑块位图。
③ 逻辑块位图:每一位对应于逻辑块的使用情况,如果对应逻辑块使用了,则逻辑块位图的位置1.
⑤ i节点:是目录与磁盘的桥接, 文件的属性描述。
⑥ 逻辑块:用来存储数据的逻辑单元。
对于i节点定义在文件/include/linux/fs.h中,如下所示:
struct m_inode {
unsigned short i_mode; //文件的类型和属性
unsigned short i_uid; //宿主用户id
unsigned long i_size; //文件的大小
unsigned long i_mtime; //文件的修改时间
unsigned char i_gid; //用户组id
unsigned char i_nlinks; //硬链接数
unsigned short i_zone[9]; //表示文件和磁盘的映射关系
//i_zone[6]如果你的文件大小只只用了7个逻辑块大小以内,那么这个数组每一个单源存储了一个逻辑块号
//i_zone[7]一次间接块号,如果占用的逻辑块大小大于7,小于512+7则占用一次逻辑块号
//i_zone[8]二次间接块号, 如果占用的逻辑块大小大于512 + 7,小于512 * 512 + 7则启动二次逻辑块号
/* these are in memory also */
struct task_struct * i_wait;
unsigned long i_atime;
unsigned long i_ctime;
unsigned short i_dev;
unsigned short i_num;
unsigned short i_count;
unsigned char i_lock;
unsigned char i_dirt;
unsigned char i_pipe;
unsigned char i_mount;
unsigned char i_seek;
unsigned char i_update;
};
i_mode是一个us类型的变量,用来保存文件的类型和属性,具体定义如下所示:
1、高速缓冲区(buffer.c)
高速缓冲区是文件系统访问块设备数据的桥梁也是必经之道。如果内核对块设备进行数据读写,每次读写的I/O操作的时间与cpu自身的处理速度相比是非常慢的。因此内核会在内存中开辟一块高速缓冲区,并将其分成一个个与磁盘数据块大小相同的缓冲块进行管理。
在高速缓冲区中会存放着最近使用过的块设备中的数据块。当内核需要读取块设备上的数据块的时候,会先在高速缓冲区进行查找,如果相应数据已经在缓冲中,就无需从块设备上读。如果数据不在缓冲区中,就会发出读块设备的命令,将数据读到高速缓冲区中。当需要把数据写到块设备中时,内核会在高速缓冲区中申请一块空闲缓冲块临时存放这些数据,当执行设备数据同步命令的时候,才会真正的写入到块设备之中。
高速缓冲区的结构如下图所示:
对于linux 0.11内核,每个缓冲块的大小都是1024字节,与磁盘上的逻辑块大小相同。缓冲头结构的定义在文件/include/linux/fs.h中
struct buffer_head {
char * b_data; //指向缓冲块的指针
unsigned long b_blocknr; //块号
unsigned short b_dev; //数据源的设备号(0==free)
unsigned char b_uptodate; //表示数据是否被更新
unsigned char b_dirt; //0-未修改,1-已修改
unsigned char b_count; //使用该数据块的用户数
unsigned char b_lock; //缓冲区是否被锁定
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; //空闲表上后一块
};
缓冲区的维护依靠的是一个hash列表和一个free_list空闲列表。在buffer.c程序中定义了一个有307个buffer_head指针的hash数组。hash数组的hash函数是由(设备号^逻辑块号)% 307得到。
在buffer_head中定义的b_prev和b_next指针就是用于散列在同一个hash表项时用于双向链接的。而b_prev_free和b_next_free指针就是用于维护一个所有缓冲块的双向链表。
某一时刻的hash表和free_list状态如下图所示:
内核程序在使用高速缓冲区的缓冲块时,需要指定设备号(dev)和访问设备数据的逻辑块号(block),然后调用函数bread()、bread_page()和breada()操作,这三个函数的本质都是调用缓冲区搜索函数getblk()来搜索缓冲块中最为空闲的缓冲块。
getblk()函数代码如下:
//用来判断缓冲区的修改和锁定标志
#define BADNESS(bh) (((bh)->b_dirt<<1)+(bh)->b_lock)
//传入参数dev和block
struct buffer_head * getblk(int dev,int block)
{
struct buffer_head * tmp, * bh;
repeat:
if (bh = get_hash_table(dev,block)) //搜索哈希表,如果对应缓冲块已经存在在哈希表中,直接返回该指针。
return bh;
//tmp指向空闲队列
tmp = free_list;
do {
//如果缓冲区的b_count不等于0表示在使用,直接跳过
//即使一个缓冲区的b_count=0也不一定是未使用过。
//比如一个进程执行breada()函数读取几个块,在执行完ll_rw_block()命令后就会释放b_count,但此时读取的操作还在进行,因此b_count=0,b_lock=1.
if (tmp->b_count)
continue;
//找到一个BADNESS值最小的缓冲区
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;
}
//此时已经找到了一个BADNESS最小的缓冲块,等待该缓冲区解锁
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 */
//最后得到的缓冲块b_count=0,b_dirt=0,b_uptodate=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;
}
2、文件系统底层操作函数(super.c,bitmap.c,truncate.c,inode.c和namei.c)
bitmap.c
free_block(int dev, int block)
释放设备dev上数据区的逻辑块block,复位逻辑块block对应的逻辑块位图比特位
new_block(int dev)
向设备申请一个逻辑块,该函数会在获取该其超级块,并在超级块中的逻辑位图寻找第一个值为0的bit,然后在对应逻辑块位图置位该bit。为该逻辑块取得在高速缓冲区取得一个对应缓冲块,并更新标志,最后返回逻辑块号。
free_inode(struct m_inode* inode)
释放指定inode节点
new_inode(int dev)
为指定设备创建一个Inode节点
truncate.c
truncate(struct m_inode* inode)
将节点对应的文件长度截为0,主要调用free_ind和free_dind释放一次和二次间接块。
inode.c
wait_on_inode(struct m_inode * inode)
等待一个inode节点空闲
lock_inode(struct m_inode * inode)
锁定一个inode节点
unlock_inode(struct m_inode * inode)
解锁一个inode节点,并唤醒等待队列
invalidate_inodes(int dev)
扫描inode表数组,如果是指定设备使用的i节点就释放
sync_inodes(void)
把i节点表上的所有节点写入到高速缓冲区等待同步
bmap
文件数据块映射到盘块的处理操作,该函数把指定的文件数据块block对应到设备上逻辑块上,并返回逻辑块号。如果创建标志 置位,则在设备上对应逻辑块不存在时就申请新磁盘块,返回文件数据块block对应在设备上的逻辑块号(盘块号)。
iput、iget
iput 释放一个inode节点。
主要是对i_count引用次数进行操作,把i节点引用数值减1并且若是管道i节点,则唤醒等待进程若是块设备文件i节点则刷新设备若i节点的链接计数为0,则释放该i节点占用的所有磁盘逻辑块,并释放该节点
iget 获得一个inode节点。
从设备上读取指定节点号的i节点到内存i节点表中,并返回该i节点指针首先在位于高速缓冲区的i节点表中寻找,若找到指定节点号的i节点则再经过一些判断处理后返回i节点指针否则通过设备号和指定i节点号,从设备中读取的i节点信息,放入在i节点表中申请的空闲节点中,并返回该i节点指针
read_inode write_inode
找到指定的设备,通过设备号找到他的超级块,超级块计算要读写的块号,调用bread将其读写入高速缓冲区中,读的话将高速缓冲区的b_data读到内存,释放高速缓冲区。写的话将数据写到高速缓冲区的b_data并设置dirt置位,等待系统sys_sync进行写盘,释放高速缓冲区。
super.c
此文件中主要存放这对文件系统超级块的管理函数。
get_super()函数在指定了设备号的情况下,返回对应的超级块的指针
put_super()函数用于释放超级块,在调用函数umount()函数的会调用此函数
read_super()函数用于把指定设备的文件系统超级块读入缓冲区,并登记到超级块数组,最后返回该超级块的指针
sys_umount()系统调用用于卸载一个设备文件名的文件系统
sys_mount()用于往一个目录上挂载一个文件系统
mount_root()用于挂载根文件系统
namei.c
实现了根据目录名或文件名寻找对应i节点的函数namei(),以及关于目录的建立和删除、目录项的建立和删除等操作和系统调用
3、文件数据进行读写的操作模块
本部分是文件系统的第三个部分,主要包含对块设备,字符设备,管道文件,普通文件的读写函数(用作给系统调用提供读写接口)以及系统调用sys_read()和sys_write()。关系如下图所示:
block_dev.c
block_write(int dev, long *pos, char *buf, int count)
int block_write(int dev, long * pos, char * buf, int count)
{
int block = *pos >> BLOCK_SIZE_BITS;
int offset = *pos & (BLOCK_SIZE-1);
int chars;
int written = 0;
struct buffer_head * bh;
register char * p;
while (count>0) {
chars = BLOCK_SIZE - offset;
if (chars > count)
chars=count;
if (chars == BLOCK_SIZE)
bh = getblk(dev,block);
else
bh = breada(dev,block,block+1,block+2,-1);
block++;
if (!bh)
return written?written:-EIO;
p = offset + bh->b_data;
offset = 0;
*pos += chars;
written += chars;
count -= chars;
while (chars-->0)
*(p++) = get_fs_byte(buf++);
bh->b_dirt = 1;
brelse(bh);
}
return written;
}
数据块写函数,向指定设备dev的偏移量pos处,从buf缓冲区开始写入count字节的数据。返回成功写入的字节数。
写数据流程:
① 根据偏移量pos会计算出开始写的盘号block,和在第一个数据块中的偏移量offset。
② 针对要写入的字节数count开始循环进行写操作。
2.1首先计算当前数据块剩余可写入数据的大小chars。如果剩余大小chars大于需要写入的数据count,则chars = count。
2.2 如果剩余可写入数据大小为一块数据的内容,则直接申请1块高速缓冲块。
2.3 否则就需要读入将被写入的数据的数据块,并预读取下两块数据,然后将块号递增1,为循环写入做准备。
block_read(int dev, unsigned long *pos, char *buf, int count)
数据块读函数,从指定设备dev的pos处读取数据,和write函数流程类似。
file_dev.c
提供普通文件的读写函数,供系统调用read()和write()使用
file_read(struct m_inode * inode, struct file * filp, char * buf, int count)
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;
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;
}
file文件读数据流程:
① 若要读取的字节数count <= 0直接返回,否则用left保存要读取的字节数开始循环读取。
② 利用bmap()函数获取该文件的读写指针所在位置的逻辑块号nr,若nr!=0则读取该逻辑块,若nr=0表示数据块不存在,缓冲块bh指向空。
③ 计算读写指针在该逻辑块中的偏移量nr,将该数据块剩余的数据BLOCK_SIZE-nr和剩余需要读取字节数left进行比较,如果BLOCK_SIZE-nr > left表示该快是最后一个数据块,反之还需要读取下一个数据块,调整文件指针后移。
file_write(struct m_inode * inode, struct file * filp, char * buf, int count)
和读函数类似
pipe.c
管道文件是多进程通信进行数据交互的一种基本方式,在本文件中提供了read_pipe(),write_pipe()同时实现了系统调用sys_pipe()。
在创建管道的时候,程序会专门申请一个管道i节点,并为管道分配一页缓冲区(4kb),i节点的i_size字段保存着管道缓冲区地址,管道数据头指针存放在i_zone[0]字段中,管道数据尾指针存放在i_zone[1]指针中,如下图所示:
char_dev.c
该文件包含字符设备的访问函数。主要包含rw_ttyx()串口中端设备读写函数,rw_tty()控制台终端读写函数,rw_memory()内存设备读写函数。以及rw_char()字符设备读写接口函数。
4.文件系统操作和管理部分
....