高级OS(十一) - 一个文件系统的设计与实现
- 一、摘要
- 二、引言
- 三、概要设计
- 四、详细设计
- 4.1 注册退出模块
- 4.2 注册模块
- 4.3 自定义文件系统类型模块
- 4.4 遍历文件系统模块
- 4.5 kern_mount函数模块
- 4.6 创建文件夹模块
- 4.7 创建文件模块
- 4.8 myfs_create_by_name()函数模块
- 4.9 lookup_one_len()函数模块
- 4.10 __lookup_hash()函数模块
- 4.11 myfs_mkdir()函数模块
- 4.12 myfs_mknod()函数模块
- 4.13 myfs_get_inode()函数模块
- 4.14 myfs_file_operations定义
- 4.15 myfs_creat()函数模块
- 4.16 open方法
- 4.17 read方法和write方法
- 五、执行流分析
- 六、测试及分析
- 七、结论
一、摘要
随着科学技术的发展,操作系统已经成为必不可少的一面,其中文件系统更是重要。今天,主要研究的是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等数据结构,了解到了编写文件系统的过程实际上就是建立这些数据结构,并将之间关系理清。本次实验从中收获了很多,也发现了自己的不足之处,在设计一个文件系统之前,应该进行充分理解,对各个模块详细设计,否则会浪费时间和精力。本次实验同时让我了解了创建文件夹和文件的一些区别,明白了动手实践代码真的可以取得一定性的进步。