Linux内核学习系列(6)——文件系统

前言

《Linux内核完全注释》中对文件系统进行了详细地解析,从文件系统组织结构到代码实现都很好理解。本篇对该块内容进行简单梳理,并通过跟踪open系统调用理解文件系统与读写文件的关系

文件系统

基本概念

通俗地说,文件系统用来辅佐内核与设备进行交互,如读写文件,显示字符等操作。就好比作为一个图书馆,需要设计目录,分区来方便用户查询或放置图书。以往块设备打开并写入一个文件这一过程为例,我们看看内核应该怎么完成

  1. 块设备分块。首先,块设备对于CPU而言也是一块地址空间,如划分内存物理页一样,应该对块设备存储空间进行分块。如,1k为一块
  2. 设置块位图。内存管理中,通过mem_map表示当前物理内存物理页使用情况。同样地,对于块设备的每一块的使用情况也应设计位图进行表示
  3. 设置i节点。在物理内存中,通过连续地址空间存储一个结构体,由于地址是连续的,内核通过起始地址便能索引到该结构体。而块设备如果要存储文件,一个文件有大有小,仍使用连续块进行分配会造成空间浪费。因此,可以通过设计i节点,每个文件对应一个i节点,i节点中存储了该文件所使用的块索引。对于文件,内核只需知道i节点地址即可。同时,i节点应该存在内存和块设备中。
  4. 设置超级块。当块设备更换挂载的内核时,如何让内核快速知道块设备目前情况呢?最好的办法就是设计一个总结构体,其中存放如i节点数量,块数量等信息。该结构体我们成为超级块。
  5. 设置高速缓冲区。上述四步将块设备结构化后,内核已经可以对其进行寻址和访问。但如CPU与磁盘的IO与CPU执行速度相差巨大,会影响用户体验,并且对于一些文件可能会重复使用,理应单次读取多次使用。为此,需要在物理内存中设计一块高速缓冲区。当内核想写文件时,将内容写入高速缓冲区即刻返回。后续交给块设备驱动将缓冲区内容同步至块设备中。该过程其实就是异步,缓冲区相当于一个消息队列。
  6. 实现同步。上述提到,文件内容写入高速缓冲区即刻返回。那怎么将缓冲区内容写回块设备呢?这就是同步刷盘。需要设置程序将缓冲区内容写回块设备,或者读入缓冲区。
  7. 设计消息队列。由于块设备也属于共享资源,不能允许多个程序同时刷盘。因此需要对刷盘请求进行排队。并且,由于写入块设备时,如磁盘寻址是有顺序的,因此必须对刷盘请求进行排序,尽可能地让磁盘一次寻址过程能够同步尽可能多的数据

上述我们简述了完成一个文件系统大致要完成的事情。进一步地我们看看代码实际是如何实现的

MINIX文件系统

MINIX 文件系统与标准 UNIX 的文件系统基本相同,它由 6 个部分组成:①引导块;②超级块;③i 节点位图;④逻辑块位图;⑤i 节点;⑥数据块。对于一个普通的磁盘块设备来说,其各部分的分布见图 所示
在这里插入图片描述
超级块用于存放盘设备上文件系统的结构信息,并说明各部分的大小。其结构见图 12-3 所示。其中,s_ninodes 表示设备上的 i 节点总数。s_nzones 表示设备上以逻辑块为单位的总逻辑块数。s_imap_blocks和 s_zmap_blocks 分别表示 i 节点位图和逻辑块位图所占用的磁盘块数s_firstdatazone 表示设备上数据区开始处占用的第一个逻辑块块号。s_log_zone_size 是使用 2 为底的对数表示的每个逻辑块包含的磁盘块数。对于 MINIX 1.0 文件系统该值为 0,因此其逻辑块的大小就等于磁盘块大小,都是 1KB。s_max_size是以字节表示的最大文件长度,这里不超过 4GB。当然这个长度值将受到磁盘容量的限制。s_magic 是文件系统魔幻数,用以指明文件系统的类型。对于 MINIX 1.0 文件系统,它的魔幻数是 0x137f。
----《Linux内核完全注释》12.1总体功能

上述介绍为书中内容,对应基本概念中的前4点。具体地,超级块与inode节点分别定义如下
linux/fs.h

struct m_inode {
	unsigned short i_mode;
	unsigned short i_uid;
	unsigned long i_size;
	unsigned long i_mtime;
	unsigned char i_gid;
	unsigned char i_nlinks;
	unsigned short i_zone[9];
/* these are in memory also */
	struct task_struct * i_wait;
	struct task_struct * i_wait2;	/* for pipes */
	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;
};

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

因此,当挂载一个块设备后,可以通过读取其超级块中存储的内容,快速获取当前块设备统计信息。这里需要注意的是,超级块中关于i节点个数,以及逻辑数据块的块下标等信息都是在创建文件系统时给定的。内核在执行写入等操作时,要保证不越界。

另一方面,可以看到示意图中,i节点直接存储于一整块逻辑块中。实际上不是的,存储于块设备中的i节点内容,只有32个字节,因此一个逻辑块可以存储32个i节点。因此块设备中第n个i节点地址应该为m+n*32,m为当前块地址

根据i节点中zone数组的内容,可以找到i节点表示文件的具体内容。书上很详细,就不展开了,如下图所示。以及关于目录项、目录、寻址等内容也不是本篇重点,可自行阅读书中章节。
在这里插入图片描述

高速缓冲区

这是基本概念第5点。这部分书中讲的也很好,需理解缓冲块与块设备逻辑块的一一对应关系。并且write,read等系统调用,都是与缓冲块有关,不用考虑同步的事情。

具体地,应该怎么实现read?

  1. read作为系统调用,根据待访问设备类型,调用具体函数在这里插入图片描述

(ps:这部分表述较困难,注意点不多,看书即可,不往下记了)

实现同步

负责同步的函数如下,主要通过ll_rw_block将bh对应缓冲块内容同步进目标设备中

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

需要注意的是,两个函数被调用的次数并不多。只出现在panic报错时,或者iput中。因此,有input的地方就意味着要刷盘

设计消息队列

对于每个刷盘或者读盘需求,都会封装成一个请求,加入队列中,等待对应设备控制器执行。这部分在9.1章节

write过程

我们根据write执行流程看看内核与文件系统如何交互的。

1 . 首先从系统调用sys_write开始 fs/read_write.c

int sys_write(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;
	inode=file->f_inode;
	if (inode->i_pipe)
		return (file->f_mode&2)?write_pipe(inode,buf,count):-EIO;
	if (S_ISCHR(inode->i_mode))
		return rw_char(WRITE,inode->i_zone[0],buf,count,&file->f_pos);
	if (S_ISBLK(inode->i_mode))
		return block_write(inode->i_zone[0],&file->f_pos,buf,count);
	if (S_ISREG(inode->i_mode))
		return file_write(inode,file,buf,count);
	printk("(Write)inode->i_mode=%06o\n\r",inode->i_mode);
	return -EINVAL;
}

sys_write根据i_mode,判断执行哪种设备访问。我们以file_write为例进一步跟踪
fs/file_dev.c

int file_write(struct m_inode * inode, struct file * filp, char * buf, int count)
{
	off_t pos;
	int block,c;
	struct buffer_head * bh;
	char * p;
	int i=0;

/*
 * ok, append may not work when many processes are writing at the same time
 * but so what. That way leads to madness anyway.
 */
	if (filp->f_flags & O_APPEND)
		pos = inode->i_size;
	else
		pos = filp->f_pos;
	while (i<count) {
		if (!(block = create_block(inode,pos/BLOCK_SIZE)))
			break;
		if (!(bh=bread(inode->i_dev,block)))
			break;
		c = pos % BLOCK_SIZE;
		p = c + bh->b_data;
		bh->b_dirt = 1;
		c = BLOCK_SIZE-c;
		if (c > count-i) c = count-i;
		pos += c;
		if (pos > inode->i_size) {
			inode->i_size = pos;
			inode->i_dirt = 1;
		}
		i += c;
		while (c-->0)
			*(p++) = get_fs_byte(buf++);
		brelse(bh);
	}
	inode->i_mtime = CURRENT_TIME;
	if (!(filp->f_flags & O_APPEND)) {
		filp->f_pos = pos;
		inode->i_ctime = CURRENT_TIME;
	}
	return (i?i:-1);
}

简单地说,就是找到缓冲块,往里写入用户缓冲buf中的内容。此处操作都是针对缓冲块的。没有刷盘

写完后调用sys_close关闭文件,其中调用了iput

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

关系梳理

进程 task_struct 中有 flip 结构体数组,用于存放该进程已打开的 file 结构体。每个 file 与 一个 inode 进行关联。

文件系统中有超级快 super_block,用于标识文件系统信息。可由 super_block 定位到根目录的 dentry 目录项,根据目录项可以通过文件名找到对应目标的 i 节点号,从而获得其 inode。

打开一个文件时。进程需要判断 flip 是否已满,未满才可新建一个 file 结构体。根据 pathname 获得目标的 inode,将其与 file 进行关联。

open

通过分析 open 系统调用执行流程,梳理上述每个结构体之间的相互关系。首先,open 标准库函数会进一步进行 sys_open 的系统调用。

  1. 遍历 flip 找到空的 file 对应的 fd 文件描述符
  2. 遍历 file_table 文件结构体缓存,获取空的 file 文件结构体,f
  3. 通过 open_namei ,根据 filename 获取目标的 inode 结构体。在该函数中,会根据 mode 对 filename 对应目标执行相应的操作。如果 mode 为创建 CREATE。则会为 filename 目标在指定的位置创建目标项以及申请 inode。
  4. 当 inode 申请成功,将其放入 f 的 f_inode 属性中。返回 fd
  5. 因此,需要写入内容时,只需要通过 fd 而无需 filename ,可以通过 flip[fd] 获取到对应目标的 file->inode 进行相关操作。
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;
	}
/* ttys are somewhat special (ttyxx major==4, tty major==5) */
	if (S_ISCHR(inode->i_mode))
		if (check_char_dev(inode,inode->i_zone[0],flag)) {
			iput(inode);
			current->filp[fd]=NULL;
			f->f_count=0;
			return -EAGAIN;
		}
/* Likewise with block-devices: check for floppy_change */
	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);
}

如上所述,sys_open 中主要执行流程为 open_namei ,为 filename 对应目标获得相应 inode。进一步地,看看 open_namei 具体做了什么

  1. 首先,通过 dir_namei 获取 pathname 最接近目标的目录文件 inode。例如 pathname 为 /user/etc/ssh ,会获取到 etc 这一目录文件 inode。以及 basename 为 ssh,namelen 为 3。
  2. 经由 find_entry ,通过目录文件 inode 寻找 etc 这一目录文件下的目录项,并判断其与 basename 的关系,返回 bh 高速缓存。例如,etc/ 下可能有两个文件 ssh 与 aab,那么就会包括 {name: ssh,inode_n:1} 和 {name:aab,inode_n:2}这两个 dentry。如果 mode 为CREATE ,并且不存在 {name: ssh,inode_n:1} ,则需要为其创建对应 inode 和为其添加目录项。
  3. 因此,如果 bh 为空,说明 find_entry 匹配不到 basename 。因此要通过 new_inode 创建一个 inode。并由 add_entry 为其添加目录项。
  4. 此时新建的 inode 与 bh 新建目录项缓存没有建立关系,需要为其建立关系,即通过 bh 修改目录项为 {name:ssh,inode_n:x}。对应 de->inode = inode->i_num;bh->b_dirt = 1;
  5. 由于 bh 是缓存,还需要调用 brelse 进行同步,以便于将 bh 内容刷入块设备磁盘中。
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,NULL)))
		return -ENOENT;
	if (!namelen) {			/* special case: '/usr/' etc */
		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);
	if (flag & O_EXCL) {
		iput(dir);
		return -EEXIST;
	}
	if (!(inode = follow_link(dir,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;
}

理解上述过程的关键在于,bh 高速缓存应该怎么用。对此再梳理一下。inode 中只通过 i_zone 记录具体文件内容所在盘块号。如果要查询具体盘块号内容,必须先为其申请一个 bh 高速缓存,并将盘块内容读入 bh 中。这样才能通过 bh 间接访问具体盘块内容。当需要修改文件内容时,应先修改文件内容对应 bh ,将其刷盘,同步到磁盘中才能完成修改。

因此在 open_namei 中。通过 dir_namei 获取到了 etc 目录文件的 inode,但不知道里面具体的内容,因此需要通过 find_entry 获取其具体内容,在 bh 中显示。此时目录项内容就在 bh 中,因此可以在 bh 中进行 basename 的匹配,直到找出 basename 对应的 inode。如果找不到 inode 则需要创建,并且还要修改目录项。

同理。新建 inode 时也需要考虑同步的问题。首先, inode 在内存中有记录,同时块设备内部也有记录。因此,新建的过程应该是,先从内存的 inode_table 缓存中申请一个空白的 inode ,通过 bh 修改超级块中 inode 的位图信息。刷新 bh 后,块设备的超级块信息被同步,由于 inode 位图信息已经被更新,因此文件系统能够感知到 inode 被分配出去了。

write

经过 open 的分析,我们可以大致理解 inode 如何被创建,并绑定于进程上。那么当需要修改文件内容时,可以通过进程获取到 inode ,并根据 inode 中 i_zone 指向的块设备号,根据其建立一个 bh,修改 bh 并同步它,则能够实现对具体块设备磁盘内容的修改

可以看到 sys_write 会根据 i_mode 类型调用不同的 write 实现。具体地,跟踪一下 file_write

int sys_write(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;
	inode=file->f_inode;
	if (inode->i_pipe)
		return (file->f_mode&2)?write_pipe(inode,buf,count):-EIO;
	if (S_ISCHR(inode->i_mode))
		return rw_char(WRITE,inode->i_zone[0],buf,count,&file->f_pos);
	if (S_ISBLK(inode->i_mode))
		return block_write(inode->i_zone[0],&file->f_pos,buf,count);
	if (S_ISREG(inode->i_mode))
		return file_write(inode,file,buf,count);
	printk("(Write)inode->i_mode=%06o\n\r",inode->i_mode);
	return -EINVAL;
}

pos 为 file 中属性。我们可以知道,之所以每个进程访问同一个文件的游标不同,是因为每个进程使用了不同的 file ,并且 pos 不同。但 file 对应的是同一个 inode。

  1. 如前所述,通过 bread 可以根据 inode 和 block 号创建一个 bh ,用来映射具体地磁盘文件内容。
  2. 将 p 指向 bh 的数据域。通过 get_fs_byte 将 buf 内容写入 bh 数据域中。
  3. 调用 brelse 将 bh 内容同步至磁盘中。
int file_write(struct m_inode * inode, struct file * filp, char * buf, int count)
{
	off_t pos;
	int block,c;
	struct buffer_head * bh;
	char * p;
	int i=0;

/*
 * ok, append may not work when many processes are writing at the same time
 * but so what. That way leads to madness anyway.
 */
	if (filp->f_flags & O_APPEND)
		pos = inode->i_size;
	else
		pos = filp->f_pos;
	while (i<count) {
		if (!(block = create_block(inode,pos/BLOCK_SIZE)))
			break;
		if (!(bh=bread(inode->i_dev,block)))
			break;
		c = pos % BLOCK_SIZE;
		p = c + bh->b_data;
		bh->b_dirt = 1;
		c = BLOCK_SIZE-c;
		if (c > count-i) c = count-i;
		pos += c;
		if (pos > inode->i_size) {
			inode->i_size = pos;
			inode->i_dirt = 1;
		}
		i += c;
		while (c-->0)
			*(p++) = get_fs_byte(buf++);
		brelse(bh);
	}
	inode->i_mtime = CURRENT_TIME;
	if (!(filp->f_flags & O_APPEND)) {
		filp->f_pos = pos;
		inode->i_ctime = CURRENT_TIME;
	}
	return (i?i:-1);
}

小结

初始版本的文件系统实现比较简单,由于内存过小,因此基本没有考虑使用缓存。在后续版本的改进中,随着内存的增加,缓存的使用,数据结构变得更复杂,例如 dentry 等。而 inode , super_block 等结构也作为了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值