一个文件系统的设计

一个文件系统的设计

1、摘要

在操作系统中,文件系统是指文件命名、存储和组织的总体结构。一个文件系统包括文件、目录,以及定位和访问这些文件与目录所必需的信息。文件系统也可以表示操作系统的一部分,它把应用程序对文件操作的要求翻译成低级的、面向扇区的并能控制磁盘的驱动程序所理解的任务。


2、引言

本次设计的文件系统主要是由文件系统的注册、文件系统的挂载、创建目录和文件,以及实现有关文件的操作read、write、open操作,文件系统的注销这些部分组成,每个部分都是整个文件系统的实现不可或缺的一部分。

1、文件系统的注册:文件系统注册到Linux内核中,才能使其成为可用的文件系统。所有已注册的文件系统的file_system_type结构形成一个链表,这个链表称为注册链表。

2、文件系统的挂载:文件系统在进行注册后必须进行挂载才能被访问。

3、创建目录和文件:在linux操作系统里面,一切皆文件,目录也是文件的一种,创建文件需要很多信息,就在这里生成和定义。

4、有关文件的操作:如何使用文件,read、write、open这些操作。

5、文件系统的注销:文件系统如果不使用,就需要及时清理。防止内存泄漏。


3、概要设计

1、文件系统的注册:

文件系统想要在操作系统里面使用,就必须需要内核的维护,也就是需要内核认可,所以在创建一个新的文件系统的时候,需要将文件系统注册进去内核,内核有一套机制,也就是VFS,它是专门用来给管理各种文件系统的一种机制,这种机制是搭建了内核与文件系统之间的一道桥梁,通过这种机制,可以管理更多的文件系统。下面是对于VFS与文件系统之间的关系,进行描述。

文件系统的注册与VFS(Virtual File System,虚拟文件系统)之间存在密切关系。

VFS是Linux内核中的文件管理模块,它使得在Linux操作系统中可以同时有多个不同的文件系统在工作,并且不同文件系统之间的差异性对应用层是完全透明的。VFS本身并不是一种技术,而是多种技术手段和数据结构的集合,比如对进程抽象文件管理、将系统文件管理转换成对文件外存的块管理等。

VFS机制本身并不实现任何文件系统,而是定义了一些接口,用于处理不同文件系统之间的共性部分。VFS机制使得不同的文件系统可以以统一的方式进行访问和操作。

当一个文件系统被注册到Linux内核时,它会被VFS机制所管理。在注册过程中,文件系统会将自己的file_system_type数据结构填写并调用注册函数register_filesystem()进行注册。这样,VFS机制就可以在内核中维护一个注册链表,其中包含了所有已注册的文件系统的信息。所有已注册的文件系统的file_system_type结构形成一个链表,这个链表称为注册链表。

通过将文件系统注册到VFS机制中,可以使得Linux内核能够统一地管理和访问不同的文件系统,提供了更加灵活和可扩展的文件管理方式。

2、文件系统的挂载:

文件系统在进行注册后必须进行挂载才能被访问,在操作系统里面,大部分的服务其实都是围绕进程服务,那么只有挂载到内存中目录树的一个目录下,进程才能访问这个文件系统,这是操作系统设计的时候,它默认的。

如果在内核中。没有任何目录项的时候,注册一个新的文件系统,它会挂载在根目录下,这是因为根目录是linux系统中所有文件和目录的起点。

3、创建目录和文件:

插个小插曲,因为在学习过程中,简单了解了操作系统的启动过程,并且写了一些相关代码,自己觉得内核的代码不就是写在文件里面吗,那么内核和文件又有什么关系?

在内核代码的编写中,通常会有多个文件来组成一个完整的内核。这些文件包括内核的配置文件、设备驱动程序、系统调用等。在编写内核代码时,可以将代码分散到多个文件中,以便更好地组织和管理代码。这些文件通过内核的编译工具链进行编译和链接,最终生成一个可执行的内核映像。

内核和文件系统是密切相关的,内核负责管理和调度系统的资源,包括文件系统的管理和访问。在编写内核代码时,可以将代码分散到多个文件中,以便更好地组织和管理代码。

所以内核想要使用,就必须经过编译和链接,生成一个内核映像,经过一些搜索,发现当我们使用Linux时,我们通过调用系统调用来与内核进行交互。这些系统调用是内核提供的一组API,它们允许用户态程序访问内核的功能,例如文件操作、进程管理、内存管理等。用户态程序通过系统调用接口来访问内核服务,而这些系统调用接口是由内核映像提供的。这个映像是操作系统启动时加载并执行的第一个程序。我们平时使用Linux时,通过调用系统调用来与内核进行交互,而这些系统调用接口是由内核映像提供的。

所以在Linux操作系统里面对“一切皆文件”又有了一个新的认识。

文件只是用来组织和管理代码,内核是负责管理和调度系统的资源。操作系统只是负责管理和调度资源。

其中应该还涉及很多底层的知识和原理,包括编译原理或者其他一些关于系统结构设计的知识,觉得操作系统的诞生,建立在抽象的同时又很具象。

4、文件的操作:

文件建立以后,对于用户或者程序设计者来说,就需要使用文件,对于文件的使用,一般就包括打开、读写这些基础操作。

凡是人类创造出来的东西,果然底层逻辑和思维,都离不开人类的思维模式。

5、文件的注销:

对于一个文件系统,如果不再使用,就应该将其注销,否则会导致内存泄漏,或者其他一些问题。


4、详细设计

1、文件系统的注册

在static int __init myfs_init(void)函数里面,可以看到有

int retval;
retval = register_filesystem(&my_fs_type);

register_filesystem是一个内核函数,它的功能是将一个file_system_type结构体实例注册到系统中。如果注册成功,它将返回0;否则,它将返回一个错误码。

在代码中,&my_fs_typemy_fs_type结构体实例的地址。这是我们创建的文件系统类型的实例,将其传递给register_filesystem函数,让它注册到系统中。然后,retval将包含注册的结果:如果注册成功,retval将为0;否则,retval将包含一个错误码。

另外还定义了一个文件系统类型

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

这段代码的功能是定义一个文件系统类型,该类型可以由内核或者其他模块使用,以在特定的存储设备上挂载和卸载文件系统,这种方法可以使得内核管理和操作不同类型的文件系统,例如:ext4,XFS,NTFS等。

2、文件系统的挂载

在static int __init myfs_init(void)函数里面,可以看到有

if(!retval)
	{
		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;
		}
	}

这段代码是在Linux内核中尝试挂载一个自定义的文件系统(通过kern_mount)函数,如果挂载失败,它会打印一条错误信息(通过printk函数),并尝试注销该文件系统类型(通过unregister_filesystem函数)。

//用于填充超级块 
static int myfs_fill_super(struct super_block *sb, void *data, int silent)
{
	static struct tree_descr debug_files[] = {{""}};
	return simple_fill_super(sb,MYFS_MAGIC,debug_files);
}
//挂载一个文件系统 
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);
}
//该函数的主要目的是尝试挂载特定的文件系统,并返回一个指向新挂载点的目录项结构的指针。

这段代码的功能是:当需要挂载特定的文件系统时,它会尝试挂载该文件系统,并返回一个指向新挂载点的dentry结构体的指针。在这个过程中,它使用了自定义的填充超级块函数来初始化超级块。

发现一个问题,那就是如下这两段代码都讲的是挂载一个文件系统,在init初始化时用的第一段代码,在挂载一个文件系统函数里面,又是第二个代码,那么这两个代码有什么区别?

myfs_mount = kern_mount(&my_fs_type);
mount_single(fs_type, flags, data, myfs_fill_super);

这两个代码片段都与文件系统的挂载有关,但是它们在执行操作和参数上有一些不同。

参考了一些解释,可以理解为在init初始化函数里面,我们需要知道是否挂载成功,所以用的是kern_mount()函数,因为如果挂载失败,它会返回一个错误指针,用(IS_ERR)来检查是否挂载成功。但是经过了解,这两个函数在执行结果都为错误时,都会返回一个错误指针,都可以用IS_ERR宏来检查。这里就不必深究了。

优缺点就是:

第一段代码将挂载结果赋值给了一个变量,因此可以方便地检查挂载是否成功,并进一步处理挂载点。但是,它只尝试挂载一个文件系统类型,如果需要挂载其他类型的文件系统,则需要多次调用该函数或使用其他函数。

第二段代码可以指定多个文件系统类型和其他选项,因此可能更灵活和通用,但需要自己处理返回值或错误情况。

所以,一直到这里,大概能理解,为什么在init用的是第一个代码,而到了挂载函数部分时,又用的是第二段代码,应该就是考虑到代码设计,在init用第一段只是想要实现挂载,并且了解是否挂载成功。而在挂载函数里面,考虑到程序设计的多样性,应该考虑到挂载多个文件系统和其他选项的情况,这个时候就应该考虑用的是第二段代码这样的设计思路。

3、创建目录和文件

//这段代码是在Linux内核中创建一个新的目录的函数
struct dentry * myfs_creat_dir(const char * name, struct dentry * parent)
{
	return myfs_creat_file(name, S_IFDIR|S_IRWXU|S_IRUGO, parent, NULL, NULL);
}

因为在linux内核当中,“一切皆文件”,因此一个目录其实也就是相当于一个文件。myfs_creat_dir()这段代码其实也就相当于对myfs_creat_file()这个函数的封装。创建一个新的目录,就相当于创建一个文件。

代码myfs_creat_file()创建一个新的文件,代码分析如下所示:

//这段代码是在Linux内核中创建一个新的文件系统中的文件的函数。
struct dentry * myfs_creat_file(const char * name, mode_t mode,
				struct dentry * parent, void * data,
				struct file_operations * fops)
{
	struct dentry * dentry = NULL;//dentry是Linux中用来描述目录项的数据结构
	int error;//定义一个整型变量 error 来存储函数执行过程中的错误代码
	printk("myfs:creating file '%s'\n",name);
	error = myfs_creat_by_name(name, mode, parent, &dentry);
	// 调用自定义的 myfs_creat_by_name 函数,用给定的名称、模式、父目录和dentry指针来创建文件。
	if(error)
	{
		dentry = NULL;
		goto exit;
	}
	if(dentry->d_inode)//如果文件存在
	{
		if(data)//如果用户提供了数据(data 非NULL),则将该数据存储在inode的 i_private 字段中。
		//这在某些文件系统或文件操作中是常见的,用于关联文件和特定应用的数据。
			dentry->d_inode->i_private = data;
		if(fops)// 如果用户提供了文件操作函数(fops 非NULL),则将它们存储在inode的 i_fop 字段中。
		//这将允许文件被打开并执行特定的操作
			dentry->d_inode->i_fop = fops;
	}
exit:
	return dentry;
}
//函数返回一个指向新创建的文件的dentry结构体的指针(如果成功创建了文件)或NULL(如果创建失败)
//这个dentry结构体可以被用来在文件系统中引用这个文件

这段代码的主要目的是在自定义的文件系统 myfs 中创建一个新的文件,并允许用户提供额外的数据和操作函数来定制这个文件。

如下代码是myfs_creat_file()函数调用myfs_creat_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;//设置为文件系统的根dentry
		}
	}
	if(!parent)
	{
		printk("can't find a parent");
		return -EFAULT;
	}
	*dentry = NULL;//初始化*dentry,表示在成功创建新文件或目录之前,该指针没有指向任何dentry
	inode_lock(d_inode(parent));//锁定父目录的inode
	*dentry = lookup_one_len(name,parent,strlen(name));//在父目录中查找与给定名称匹配的文件或目录
	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);//普通文件,在父目录中创建新文件
		}
	}
	if (IS_ERR(*dentry)) {//错误处理
		error = PTR_ERR(*dentry);
	}
	inode_unlock(d_inode(parent));
	return error;//如果成功创建文件/目录,则返回0,否则返回相应的错误码。
}

这段代码是一个在自定义文件系统(命名为"myfs")中根据名称创建新文件或目录的函数。

如上述代码分析的都是新创建一个目录和文件,那么如下就继续分析一下在指定的目录中创建一个新的目录的代码,代码如下所示:

static int myfs_mkdir(struct inode * dir, struct dentry * dentry, int mode)
{
	int res;
	res = myfs_mknod(dir, dentry, mode|S_IFDIR, 0);//调用成功,则返回值为0
	if(!res)
	{
		inc_nlink(dir);//增加inc_nlink(目录链接)的计数。
		//链接计数是文件系统中用于跟踪目录中文件和子目录数量的机制。
	}
	return res;//如果创建目录成功,则返回0;否则返回相应的错误代码。
}

在上述段代码中调用了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)
		return -EPERM;
	inode = myfs_get_inode(dir->i_sb, mode, dev);
	if(inode)
	{
		d_instantiate(dentry,inode);//将新的inode和dentry关联起来
		dget(dentry);//并增加其计数
		error = 0;//返回错误码为0,表示操作成功
	}
	return error;
}

在上述代码中调用了myfs_get_node来获取一个新的inode,代码分析如下所示:

static struct inode * myfs_get_inode(struct super_block * sb, int mode, dev_t dev)
{
	struct inode * inode = new_inode(sb);

	if(inode)
	{
		inode -> i_mode = mode;
		//设置inode的访问模式
		inode->i_uid  = current_fsuid();
		inode->i_gid  = current_fsgid();
		//current_fsuid()和current_fsgid()函数获取当前进程的用户ID和群组ID
		inode -> i_size = VMACACHE_SIZE;
	    //设置inode的大小为VMACACHE_SIZE
		inode -> i_blocks = 0;
		//设置inode的块计数为0
		inode -> i_atime = inode->i_mtime = inode->i_ctime = current_time(inode);
		//设置inode的访问时间、修改时间和创建时间都为当前时间
		switch(mode & S_IFMT)//根据传入的mode参数判断新创建的inode对应的文件类型
		{
			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;
				inc_nlink(inode);//增加目录的连接数
				break;			
		}
	}
	return inode;
}

在Linux内核中,inode是文件系统的核心数据结构,它包含了文件或目录的元数据(如权限、大小、创建时间等)。每个文件或目录在文件系统中都有一个对应的inode。

4、对文件的各种操作

//用于定义文件系统中的文件操作 
static struct file_operations myfs_file_operations = {
//这行定义了一个静态的file_operations结构体变量myfs_file_operations。file_operations是一个结构体,通常用于文件系统驱动,以处理不同的文件操作。static关键字表示这个变量只在这个源文件中可见。
    .open = myfs_file_open,//这个函数通常用于处理文件的打开操作
    .read = myfs_file_read,//这个函数通常用于处理文件的读取操作
    .write = myfs_file_write,//这个函数通常用于处理文件的写入操作
};

首先,先分析一下这个数据结构。

这段代码是在定义一个文件操作结构,通常用于操作系统级别的文件系统驱动。代码定义了一个名为myfs_file_operations的结构体,这个结构体是一个文件操作函数集,用于处理文件系统中的文件操作,如打开、读取和写入。

open():打开文件,代码分析如下所示:

static int myfs_file_open(struct inode *inode, struct file *file)
{
	printk("文件打开成功!");
	return 0;
}

read():读文件,代码分析如下所示:

static ssize_t myfs_file_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
//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);
	//kfifo_to_user函数从你的文件系统中的FIFO(先进先出)缓冲区中读取数据,并将其复制到用户空间中的缓冲区
	//kfifo_to_user函数的返回值表示实际复制的字节数。如果返回值不为0,说明出现了错误
	if(ret)
		return -EIO;
	printk("%s,actual_readed=%d,pos=%lld\n",__func__,actual_readed,*ppos);
	//如果读取成功,将打印一条日志消息,其中包含实际读取的字节数和文件的当前位置
	return actual_readed;
}

write():写文件,代码分析如下所示:

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

这个函数的设计思路与myfs_file_read()函数设计类似,此函数仅复制数据到FIFO缓冲区,而不会将数据写入实际的存储介质。数据的实际写入可能会在后台异步执行,或者在需要时由其他函数执行。

在上述代码中,ssize是一个整型数据类型,用于表示一个文件操作返回的数据量。在Linux内核中,ssize_t通常用于表示读写操作返回的数据量,其实际长度取决于系统的位数。

5、文件系统卸载

文件系统的卸载写在了模块或程序退出时进行的清理工作。

static void __exit myfs_exit(void)
{
	simple_release_fs(&myfs_mount,&myfs_mount_count);
	//函数的目的是释放文件系统实例并减少内核对该文件系统的引用计数。
	unregister_filesystem(&my_fs_type);
	//函数用于取消注册你的自定义文件系统
}

代码调用simple_release_fs函数来释放文件系统实例并减少内核对该文件系统的引用计数。myfs_mount是文件系统实例的指针,而myfs_mount_count是内核对文件系统的引用计数。

代码调用unregister_filesystem函数来取消注册你的自定义文件系统。my_fs_type是自定义文件系统的类型。

在Linux内核编程中,文件系统的操作是非常重要的部分。当模块或程序不再需要使用某个文件系统时,应该调用相应的函数释放该文件系统实例并减少内核对该文件系统的引用计数,以防止内存泄漏。同时,如果模块或程序使用了自定义的文件系统类型,也应该在不再需要时取消注册该文件系统类型。


5、执行流分析

对从用户态开始,给出执行流的详细分析

这里所有的执行流分析,都用的是流程图来进行分析,并根据不同的模块,分别来进行描述分析:

1、文件系统的注册
在这里插入图片描述
2、文件系统的卸载
在这里插入图片描述
3、创建一个目录和文件

在这里插入图片描述
4、对文件的操作

在这里插入图片描述
5、文件系统的注销
在这里插入图片描述

6、测试及分析

myfs文件系统插入成功,打印信息如下图所示:
在这里插入图片描述
如下图所示为挂载成功:
在这里插入图片描述
如下图所示为用户态测试程序,显示如下:
在这里插入图片描述

7、结论

本系统设计的优点在于能让人很容易理解一些关于文件系统设计的初衷和知识点,缺点在于本次文件系统的设计,无法动态创建多个目录和文件。

自己的心得体会:

在学习过程中,会衍生出很多的疑问,包括为什么内核代码就写在文件里面,那么内核和文件又是什么关系?了解我们平时调用系统调用接口其实都是内核映像提供的,内核映像的形成又是经过编译和链接,又涉及很多底层的知识和内容,所以就觉得操作系统的诞生,很抽象的同时又很具象。包括对于操作系统里面定义的一些东西,彷佛能理解了,凡是人类设计的东西,都是拥有人类的思维模式和逻辑的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值