高级OS(十二) - 一个文件系统的设计与实现

一、摘要

随着科学技术的发展,操作系统已经成为必不可少的一面,其中文件系统更是重要。今天,主要研究的是linux系统下的文件系统,在linux系统下,一切皆是文件,linux支持多种文件系统,如FAT32、EXT2、JFS、ReiserFS等。本次实验,主要是创建自己的myfs文件系统。
首先,本文简明的介绍了文件系统的定义和功能等;其次,阐述了该文件系统的设计思想;接着,对本次实验的各个功能模块进行了详细的分析。

二、引言

文件系统就是文件命名、存储、组织的总体结构;是操作系统中负责管理持久数据的子系统;是操作系统用于明确存储设备或分区上的文件的方法和数据结构;是操作系统与驱动器之间的接口,当操作系统请求从硬盘里读取一个文件时,会请求相应的文件系统(ext2/xfs等)打开文件。文件系统的存储中,扇区是磁盘最小的物理存储单元,操作系统无法对众多扇区进行寻址,所以将其组合形成一个簇,对簇管理。文件系统功能主要是文件管理、目录管理、共享管理和数据恢复。Linux文件系统中,会为每个文件分配两个数据结构:索引节点(index node)和目录项(directory),主要用来记录文件的元信息和目录层次结构,本次实验中也会讲到如何分配这两个数据结构。

三、概要设计

Linux有一个树状结构来组织文件。树的顶端为根目录(/),节点为目录,而末端的叶子为包含数据的文件。当我们给出一个文件的完整路径时,我们从根目录出发,经过沿途各个目录,最终到达文件。
文件系统类型结构体file_system_type,每个注册的文件系统对应着相应的super_block,用于存储特定文件的信息。索引节点inode,存放具体文件的信息。dentry目录项,方便查找文件。vfsmount用于挂载的mount结构体,新版本将vfsmount放在了mount结构体里边。编写文件系统的过程实际上就是建立这些数据结构,在具体的编程之前,设计好每个功能模块的具体实现方法,从而可以节省时间和精力,提高速率。
在这里插入图片描述

四、详细设计

4.1 注册退出模块

//***********************************************
//				模块注册退出
//***********************************************
static int __init myfs_init(void)
{
	int retval;
	struct dentry * pslot;

	//将文件系统登录到系统中去
	retval = register_filesystem(&my_fs_type); 

	if(!retval)
	{
		//创建super_block根dentry的inode
		myfs_mount = kern_mount(&my_fs_type); 
		//如果装载错误就卸载文件系统
		if(IS_ERR(myfs_mount))
		{
			printk("--ERROR:aufs could not mount!--\n");
			unregister_filesystem(&my_fs_type);
			return retval;
		}
	}

    //创建文件夹和文件
	pslot = myfs_creat_dir("First", NULL);
	//@S_IFREG:表示一个文件
	//@S_IRUGO:用户读|用户组读|其他读
   //@S_IWUSR:代表用户有写的权限,实现write时必须保证文件可写。
	//第一个文件名,第二个文件权限,第三个是父dentry,第四个空指针,第五个相应的操作。
	myfs_creat_file("one", S_IFREG|S_IRUGO|S_IWUSR, pslot, NULL, &myfs_file_operations);
	myfs_creat_file("two", S_IFREG|S_IRUGO|S_IWUSR, pslot, NULL, &myfs_file_operations);

	pslot = myfs_creat_dir("Second", NULL);
	myfs_creat_file("one", S_IFREG|S_IRUGO|S_IWUSR, pslot, NULL, &myfs_file_operations);
	myfs_creat_file("two", S_IFREG|S_IRUGO|S_IWUSR, pslot, NULL, &myfs_file_operations);

	return retval;
}

在该模块中,首先要通过register_filesystem()函数将文件系统登录到系统中,该函数在内核的fs文件夹下的filesystems.c中,见4.2。
kern_mount()函数的参数是定义的文件系统类型的地址,该函数是kern_mount_data的封装,见4.5是对ker_mount()函数一系列的解析。

4.2 注册模块

int register_filesystem(struct file_system_type * fs){
	int res = 0;
	struct file_system_type ** p;

	if (fs->parameters && !fs_validate_description(fs->name, fs->parameters))
		return -EINVAL;

	BUG_ON(strchr(fs->name, '.'));
	if (fs->next)
		return -EBUSY;
	write_lock(&file_systems_lock);
	p = find_filesystem(fs->name, strlen(fs->name));
	if (*p)
		res = -EBUSY;
	else
		*p = fs; //7.find_filesystem()函数后
	write_unlock(&file_systems_lock);
	return res;}

该函数的参数是struct file_system_type * fs类型,也就是我们自己定义的文件系统类型,见4.3。
再经过一些判断后,通过’BUG_ON(strchr(fs->name, ‘.’));’将会判断注册的文件系统名字中是否有”.”,没有就向下执行,会执行find_filesystem()函数,见4.4。
fs赋给*p后,如果函数都执行正常则res的值为0,接下来执行内核态代码中的kern_mount()函数,见 4.1。

4.3 自定义文件系统类型模块

# define MYFS "myfs"
static struct file_system_type my_fs_type = {
	.owner 	= THIS_MODULE,
	.name 	= MYFS,
	.mount 	= myfs_get_sb,
	.kill_sb 	= kill_litter_super
};

本函数是自定义的文件系统类型模块,文件系统的名称是MYFS,是一个宏,定义为’myfs’,mount函数挂载的是myfs_get_sb()函数,其中sb代表的是super_block,kill_sb()函数挂载的是kill_litter_super()函数。

4.4 遍历文件系统模块

static struct file_system_type **find_filesystem(const char *name, unsigned len){
	struct file_system_type **p;
   	for (p = &file_systems; *p; p = &(*p)->next)
		if (strncmp((*p)->name, name, len) == 0 &&
		    !(*p)->name[len])
			break;
	return p;}

find_filesystem()函数会遍历已经注册的文件系统,此外也可以利用该函数遍历系统中所有的文件系统。所有注册过的文件系统都在file_systems链表中,已经注册则p不为空,没有注册过的话p则为NULL,因为是next域,在register_filesystem()函数中会把fs赋值给*p,见4.2。

4.5 kern_mount函数模块

接下来,我们通过下图对该模块进行详细解析。
在这里插入图片描述
(1)kern_mount_data()函数

struct vfsmount *kern_mount_data(struct file_system_type *type, void *data){
	struct vfsmount *mnt;
//(5)simple_fill_super():
	mnt = vfs_kern_mount(type, SB_KERNMOUNT, type->name, data);
	if (!IS_ERR(mnt)) {
	/** it is a longterm mount, don't release mnt until we unmount before file sys is unregistered**/
		real_mount(mnt)->mnt_ns = MNT_NS_INTERNAL;
	}
	return mnt;}
EXPORT_SYMBOL_GPL(kern_mount_data);

kern_mount_data()函数中,第一个参数文件系统类型,第二个null,接着会执行vfs_kern_mount()分配mount空间。

(2)vfs_kern_mount()函数的第一个参数是类型,第二个是标志位,代表mount操作,第三个是文件系统名称,第四个是空指针,会执行alloc_vfsmnt()函数来分配一个mount结构体的空间,然后经过一些判断进入mount_fs()函数:

(3)mount_fs()函数调用type的mount回调函数,结束后会将根dentry的d_sb指向sb(即super_block)。

(4)

static struct dentry *myfs_get_sb(struct file_system_type *fs_type, int flags,
		       const char *dev_name, void *data)
{
	return mount_single(fs_type, flags, data, myfs_fill_super);
}

内核态代码myfs_get_sb()函数会调用mount_single()函数分配super_block()函数。

(5)mount_single()的第四个参数是myfs_fill_super(),看一下mount_single():内核中的mount_bdev()是针对块设备挂载时使用的函数,还有mount_nodev()。首先执行sget()来查找或者创建一个superblock结构体,之后执行fill_super(),这就是自己挂载的函数myfs_fill_super()。

(6)myfs_fill_super()函数

//每个文件系统需要一个MAGIC number
# define MYFS_MAGIC 0X64668735 //magic码,4.15在include/uapi/linux/magic.h中,有利于文件系统的索引

//*********************************************************
//				注册信息
//*********************************************************
static int myfs_fill_super(struct super_block *sb, void *data, int silent)
{
	//这个结构体如下:
	//struct tree_descr { const char *name; const struct file_operations *ops; int mode; };
	static struct tree_descr debug_files[] = {{""}};

	return simple_fill_super(sb,MYFS_MAGIC,debug_files);
}

内核态代码myfs_fill_super()的第一个参数super_block指针,第二个是magic码,第三个参数是一个tree_descr结构体,描述一些文件,如果不为空,super_block在创建的同时,就会在根目录下创建一些文件,这里为空,不需要创建任何文件。

(7)return_fill_super()函数会分配根inode空间和根dentry空间,首先给一些super_block对象赋值,包括对super_block的操作,接着调用new_inode()来创建一个inode结构,也就是文件系统的根inode。在创建文件和文件夹的时候,inode的i_op和i_fop字段非常重要。之后执行d_make_root()函数创建根dentry,接下来传递参数,用for循环在根目录下创建一系列文件,之前传递的tree_descr结构为空,这里for循环不会执行;不为空会依次执行d_alloc_name()和new_inode()来分配相应的dentry和inode结构。

4.6 创建文件夹模块

见4.1,通过myfs_create_dir()函数创建文件夹,

struct dentry * myfs_creat_dir(const char * name, struct dentry * parent)
{
	//使用man creat查找
	//@S_IFREG:表示一个目录
	//@S_IRWXU:user (file owner) has read,  write,  and  execute permission
	//@S_IRUGO:用户读|用户组读|其他读
	return myfs_creat_file(name, S_IFDIR|S_IRWXU|S_IRUGO, parent, NULL, NULL);
}

myfs_creat_dir()函数会执行myfs_creat_file()函数(见4.7),myfs_creat_dir()函数实际上是对myfs_creat_file()函数的封装,myfs_creat_dir()函数第一个参数是创建文件夹的名字,第二个是父dentry,

4.7 创建文件模块

在创建文件夹中的myfs_creat_file()函数的第一个参数对应的是文件夹的名字,第二个是文件夹的权限;第三个是父dentry,这里设置为null,在根目录下进行创建;第四个是空指针,对应的是inode中的i_private字段;最后一个是fops,赋为NULL。在文件夹的创建中,将标志位设为三个,S_IFDIR代表一个目录,S_IRWXU代表用户有读写执行权限,S_IRUGO代表用户读/用户组读/其他读。

struct dentry * myfs_creat_file(const char * name, mode_t mode,
				struct dentry * parent, void * data,
				struct file_operations * fops)
{
	struct dentry * dentry = NULL;
	int error;

	printk("myfs:creating file '%s'\n",name);

	error = myfs_creat_by_name(name, mode, parent, &dentry); //执行

	if(error)
	{
		dentry = NULL;
		goto exit;
	}

	if(dentry->d_inode)
	{
		if(data)
			dentry->d_inode->i_private = data;
		if(fops)
			dentry->d_inode->i_fop = fops; //创建文件 这里的fop不再为空
	}

exit:
	return dentry;
}

执行myfs_creat_by_name()函数。
创建文件函数的第一个参数是文件名,第二个是文件权限,第三个是父dentry,第四个是空指针,第五个是相应的操作。fop字段赋值为myfs_file_operations,其定义见4.14。

4.8 myfs_create_by_name()函数模块

static int myfs_creat_by_name(const char * name, mode_t mode,
				struct dentry * parent, struct dentry ** dentry)
{
	int error = 0;

//判断是否有父目录
	if(!parent)
	{
		if(myfs_mount && myfs_mount -> mnt_sb)
		{
			parent = myfs_mount->mnt_sb->s_root;
		}
	}

	if(!parent)
	{
		printk("can't find a parent");
		return -EFAULT;
	}

	*dentry = NULL;

	inode_lock(d_inode(parent)); //上锁
	*dentry = lookup_one_len(name,parent,strlen(name)); 
    //12.
	if(!IS_ERR(*dentry))
	{
		if((mode & S_IFMT) == S_IFDIR)
		{
			error = myfs_mkdir(parent->d_inode, *dentry, mode); //
		}
		else
		{
			error = myfs_creat(parent->d_inode, *dentry, mode);
		}
	}
	//error是0才对
	if (IS_ERR(*dentry)) {
		error = PTR_ERR(*dentry);
	}
	inode_unlock(d_inode(parent));

	return error;
}

在该函数中,首先判断是否有父目录,如果没有就赋予一个,也就是root,之后通过inode_lock()函数上锁,之后会调用lookup_one_len()函数,见4.9。
执行lookup_one_len()函数后,在经过一些判断,会进入myfs_mkdir()函数,见4.11。创建文件时会执行myfs_creat()函数,见4.15。

4.9 lookup_one_len()函数模块

该函数的作用是在父目录下根据名字来查找dentry结构,如果存在返回指针,不存在就创建一个dentry结构。在该函数源码中,首先可以看到qstr结构体,是quick string,简化传递参数,更重要的作用是保存关于字符串的元数据,即长度和哈希,接下来会执行full_name_hash(),计算文件的哈希值,最后会执行__lookup_hash(),利用之前生成的哈希值来查找同名字的dentry结构。

4.10 __lookup_hash()函数模块

该函数在dcache中查找名称,并可能重新验证找到的dentry,执行lookup_dcache()函数,如果缓存中没有这个dentry就返回NULL,如果dentry为空,则执行d_alloc()分配新的dentry结构,之后执行lookup_real()函数,在此会调用lookup()函数,查找是否有同名的dentry结构存在,再次查找的目的是防止有其他用户创建。
在这里插入图片描述

4.11 myfs_mkdir()函数模块

//******************************************************
//				创建目录,文件
//******************************************************
static int myfs_mkdir(struct inode * dir, struct dentry * dentry, int mode)
{
	int res;

	res = myfs_mknod(dir, dentry, mode|S_IFDIR, 0); //
	if(!res)
	{
		inc_nlink(dir);

	}
	return res;
}

首先会进入myfs_mknod()函数,见4.12。

4.12 myfs_mknod()函数模块

static int myfs_mknod(struct inode * dir, struct dentry * dentry, int mode, dev_t dev)
{
	struct inode * inode;
	int error = -EPERM;

	if(dentry -> d_inode) //判断inode是否存在
		return -EPERM;
	inode = myfs_get_inode(dir->i_sb, mode, dev);
	if(inode)
	{
		d_instantiate(dentry,inode);
		dget(dentry);
		error = 0;
	}
	return error;
}

该函数将创建的inode和dentry连接起来,首先判断inode是否存在,如果存在就退出函数,如果不存在,就调用myfs_get_inode()(见4.13)根据用户创建inode,之后执行d_instantiate(dentry,inode);dget(dentry);这两个函数将dentry加入到inode的dentry链表头。

4.13 myfs_get_inode()函数模块

//*************************************************
//			底层创建函数
//*************************************************
static struct inode * myfs_get_inode(struct super_block * sb, int mode, dev_t dev)
{
	//先申请一个inode结构,使用new_inode()函数
	struct inode * inode = new_inode(sb);

	if(inode)
	{
		inode -> i_mode = mode; //文件访问权限
		//@i_uid:user id
		inode->i_uid  = current_fsuid();
		//@i_gid:group id组标识符
		inode->i_gid  = current_fsgid();
		//@i_size:文件长度 
		inode -> i_size = VMACACHE_SIZE;
		//@i_blocks:指定文件按块计算的长度
		inode -> i_blocks = 0;
		//@i_atime:最后访问时间
		//@i_mtime:最后修改时间
		//@i_ctime:最后修改inode时间
		inode -> i_atime = inode->i_mtime = inode->i_ctime = current_time(inode);

		switch(mode & S_IFMT)
		{
			default:
				init_special_inode(inode,mode,dev);
				break;
			case S_IFREG:
				printk("creat a file\n");
				break;
			case S_IFDIR:
				printk("creat a content\n");
				//inode_operations
				inode -> i_op = &simple_dir_inode_operations;
				//file_operation	
				inode -> i_fop = &simple_dir_operations;
		//@:文件的链接计数,使用stat命令可以看到Links的值,硬链接数目
				//inode -> i_nlink++;
				inc_nlink(inode); //将文件的链接数加1
				break;			
		}
	}
	return inode; //返回inode
}

先申请一个inode结构,使用new_inode()函数,之后对inode相应的字段进行赋值,
我们创建的是文件夹,在switch中我们进入S_IFDIR,接着通过inc_nlink()函数将文件的链接数加1。

4.14 myfs_file_operations定义

static struct file_operations myfs_file_operations = {
    .open = myfs_file_open,
    .read = myfs_file_read,
    .write = myfs_file_write,
};

在这里定义了三个方法,open、read和write。

4.15 myfs_creat()函数模块

static int myfs_creat(struct inode * dir, struct dentry * dentry, int mode)
{
	return myfs_mknod(dir, dentry, mode|S_IFREG, 0);
}

会执行myfs_mknod()函数,见4.12,经过创建文件夹相同操作,调用myfs_get_inode()函数,在其中的switch语句中,会进入S_IFREG,只打印一行信息’creat a file’,之后返回,返回到myfs_creat_file(),其中fop不再是空的。

4.16 open方法

static int myfs_file_open(struct inode *inode, struct file *file)
{
	printk("已打开文件"); //只打印一行字符

	return 0;
}

4.17 read方法和write方法

在read和write方法中,实现的方法很多,本次实验使用KFIFO环形缓冲区来实现,环形缓冲区通常有一个读指针和一个写指针,读指针指向环形缓冲区可读的数据,写指针指向环形缓冲区可写的数据,通过移动读指针和写指针来实现缓冲区的数据读取和写入,Linux也实现了KFIFO的环形缓冲区,可以在一个读线程和一个写线程并发执行的情况下,不用使用锁机制来保证环形环形缓冲区的数据安全。
file_operations和inode_operations都在inode结构中,inode_operations中有lookup()、mkdir()、rmdir()函数,file_operations是对文件的操作,有read()、write()、open()函数。mkdir()、rmdir()函数是对目录的操作,但不在dentry结构体中,是因为在Linux中一切皆文件,不管是文件夹还是文件都由dentry和inode共同描述,inode用来存放元数据,当是文件夹时,执行inode_operations操作,当是文件时,执行file_operations操作。

//初始化一个环形缓冲区,环形缓冲区有64个字符类型的数据。
DEFINE_KFIFO(mydemo_fifo,char,64);

static ssize_t myfs_file_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
	int actual_readed;
	int ret;
	//将读取出环形缓冲区的数据复制到用户空间中
	ret = kfifo_to_user(&mydemo_fifo,buf, count, &actual_readed);
	if(ret)
		return -EIO;

	printk("%s,actual_readed=%d,pos=%lld\n",__func__,actual_readed,*ppos);

	return actual_readed;
}
//对应于写入的aufs文件的写入方法
static ssize_t myfs_file_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{	
	unsigned int actual_write; //保存成功复制数据的数量,用作返回值
	int ret;

	//用该函数将用户空间的数据写入环形缓冲区
	ret = kfifo_from_user(&mydemo_fifo, buf, count, &actual_write);
	if(ret)
		return -EIO;

	printk("%s: actual_write=%d,ppos=%lld\n",__func__,actual_write,*ppos);

	return actual_write;
}

五、执行流分析

首先进行make,make成功之后会生成.ko文件,通过insmod命令插入模块,接着通过lsmod查看,查看到myfs模块,表明已经成功插入模块,包含其Size和Used by信息。
在这里插入图片描述
接着通过mount命令进行挂载。通过cd / 进入根目录,mkdir myfs创建文件夹,mount -t myfs none /myfs/命令挂载,然后dmesg查看到相关信息。
在这里插入图片描述
接着进入myfs文件系统中,ls查看两个文件夹First和Second,ll命令可以查看两个文件夹的具体权限等信息,图中可以看到权限和我们一样;进入First文件夹,这里遇见了问题:cd权限不够,通过’sudo chmod 755 First/ -R’命令修改权限进入,ls可以查看到两个文件one和two,同样,ll命令可以查看到两个文件的具体信息。
在这里插入图片描述
首先通过’cat one’命令查看文件,再次dmesg可以查看到多了“已打开文件”等信息。
在这里插入图片描述

六、测试及分析

# include <stdio.h>
# include <fcntl.h>
# include <unistd.h>
# include <malloc.h>
# include <string.h>

# define FILE_NAME "/myfs/First/one"

int main() 
{
	char buffer[64];
	int fd;
	int ret;
	size_t len;

	char message[] = "I am myfs";
	char *read_buffer;

	len = sizeof(message);

	fd = open(FILE_NAME,O_RDWR);
	if(fd<0) 
	{
		printf("wrong\n");
		return -1;
	}

	//向设备写数据
	ret = write(fd,message,len);
	if(ret != len)
	{
		printf("wrongd\n");
		return -1;
	}

	read_buffer = malloc(2*len);
	memset(read_buffer,0,2*len);
	//关闭设备

	ret = read(fd,read_buffer,2*len);
	printf("read %d bytes\n",ret);
	printf("read buffer=%s\n",read_buffer);

	close(fd);

	return 0;
}

在用户态测试文件中,依次执行open、write、read。定义reader_buffer,用来保存环形缓冲区读取出的数据,写入和读出的数据是’I am myfs’,’gcc test.c’对文件进行编译,然后运行,可以看到打印出的信息和预想的一样。另外,可以通过’cat /proc/filesystems’命令查看到myfs。
在这里插入图片描述

七、结论

本次实验实现了一个文件系统,Linux以inode的方式,让数据形成文件。文件是数据储存的逻辑载体,了解Linux文件系统必然重要。通过本次实验了解到了vfsmount、super_block等数据结构,了解到了编写文件系统的过程实际上就是建立这些数据结构,并将之间关系理清。本次实验从中收获了很多,也发现了自己的不足之处,在设计一个文件系统之前,应该进行充分理解,对各个模块详细设计,否则会浪费时间和精力。本次实验同时让我了解了创建文件夹和文件的一些区别,明白了动手实践代码真的可以取得一定性的进步。

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值