----------------------------
#纯属个人理解,如有问题敬请谅解!
#kernel version: 2.6.26
#Author: andy wang
----------------------------
一: 概述
在上文中分析了VFS根目录是如何创建的;既然有了根VFS这棵树就能开枝散叶,在内存中慢慢发展壮大;本文将介绍如何在VFS中建立一个目录文件和普通文件.
先回顾一下上文, 在rootfs创建根目录索引节点时有这么一段代码:
case S_IFDIR: //目录文件
inode->i_op = &ramfs_dir_inode_operations; //索引节点的操作方法
inode->i_fop = &simple_dir_operations; //目录文件的操作方法
“/”是一个目录文件,所以在该目录下创建文件就会调用inode->i_op中定义的方法. 所以本文主要研究的就是linix VFS 是如何找到目录文件对应的索引节点 ,然后调用索引节点方法实现文件的创建.
先总体感观一下, 如: 我现在要在 “/home/andy/test/”目录下创建一个hello.c的文件,既 /home/andy/test/hello.c 显然这是一个绝对路径,那么现在就必须从根目录”/”开始一次往下查找, 最后在内存中找到”test”的目录项对象, 我们的目的是在test目录下建立hello.c文件; 所以就需要找到test dentry关联的inode,然后调用inode->i_op创建文件的方法,在内存或者在具体存储介质中建立这个文件.
需要注意的是为了加快查找速度, inode和dentry都是存在 cache中的,而cache的空间是有限的, 所以当文件系统挂载后内存中只有根目录的inode和dentry, 而其于文件的inode和dentry都是动态建立起来的,如果一个目录对应的inode和dentry都在内存中,那么它的父亲对应的inode和dentry也一定在内存中. 了解这点后再看后面的代码就要好理解一点了。
二: 路径查找
从上面的分析可以看出在建立一个文件时,VFS需要做的一个重要的事情就是进行路径查找, 找到目的文件父目录的dentry和inode,最后调用inode->i_op.所以得先从路径查找入手了.
查找路径的函数为do_path_lookup(),这个函数比较烦琐,下面是基本调用流程图:
根据这个主线分析一下路径查找的过程;
首先看看do_path_lookup()代码的片段:
static int do_path_lookup(int dfd, const char *name,
unsigned int flags, struct nameidata *nd)
{
int retval = 0;
int fput_needed;
struct file *file;
struct fs_struct *fs = current->fs; //取得当前进程的fs
nd->last_type = LAST_ROOT; /* if there are only slashes... */
nd->flags = flags;
//这个flag有如下几个宏定义:
//LOOKUP_FOLLOW 路径最后一个分量是符号链接,则追踪它
//LOOKUP_DIRECTORY 路径最后一个分量为目录
//LOOKUP_CONTINUE 路径中还有文件名要检查
//LOOKUP_PARENT 查找路径中最后一个分量所在的目录
nd->depth = 0;
if (*name=='/') { //查找需要从当前根目录开始(绝对路径)
read_lock(&fs->lock);
if (fs->altroot.dentry && !(nd->flags & LOOKUP_NOALT)) {
nd->path = fs->altroot;
path_get(&fs->altroot);
read_unlock(&fs->lock);
if (__emul_lookup_dentry(name,nd))
goto out; /* found in altroot */
read_lock(&fs->lock);
}
nd->path = fs->root; //取得根目录的dentry和vfsmnt
path_get(&fs->root);
read_unlock(&fs->lock);
} else if (dfd == AT_FDCWD) { //查找需要从当前工作目录开始(相对路径)
read_lock(&fs->lock);
nd->path = fs->pwd;
path_get(&fs->pwd);
read_unlock(&fs->lock);
} else {
struct dentry *dentry;
file = fget_light(dfd, &fput_needed);
retval = -EBADF;
if (!file)
goto out_fail;
dentry = file->f_path.dentry; //取得当前目录的dentry
retval = -ENOTDIR;
if (!S_ISDIR(dentry->d_inode->i_mode)) //判断文件是否为目录
goto fput_fail;
retval = file_permission(file, MAY_EXEC); //文件权限
if (retval)
goto fput_fail;
nd->path = file->f_path; //在经过上面判断后成功取得进程当前目录的dentry和mnt
path_get(&file->f_path);
fput_light(file, fput_needed);
}
retval = path_walk(name, nd); //此时路径第一个分量的dentry和mnt 已经保存在了nd->path中,继续查找
………………….
}
在这里我们可以看出来nd其实就像是一个”路标” , 它指向了我们查找到路径的什么位置.
继续跟踪代码path_walk(),这个函数调用了函数link_path_walk(name, nd),最终调用函数__link_path_walk(),来看一下代码片段:
static int __link_path_walk(const char *name, struct nameidata *nd)
{
struct path next;
struct inode *inode;
int err;
unsigned int lookup_flags = nd->flags; //这个flag很重要,决定后面查找方式
while (*name=='/') //如果路径第一个字符为”/”,则跳过
name++;
if (!*name)
goto return_reval;
inode = nd->path.dentry->d_inode; //取得inode是为了后面判断该目录或文件的操作权限
if (nd->depth)
lookup_flags = LOOKUP_FOLLOW | (nd->flags & LOOKUP_CONTINUE);
/* At this point we know we have a real path component. */
for(;;) { //这是一个循环,它会解析出路径中的每个分量
unsigned long hash;
struct qstr this;
unsigned int c;
nd->flags |= LOOKUP_CONTINUE;
err = exec_permission_lite(inode, nd); //文件权限判断
if (err == -EAGAIN)
err = vfs_permission(nd, MAY_EXEC);
if (err)
break;
this.name = name;
c = *(const unsigned char *)name;
hash = init_name_hash();
do {
name++;
hash = partial_name_hash(c, hash); //计算路径hash值
c = *(const unsigned char *)name;
} while (c && (c != '/'));
this.len = name - (const char *) this.name;
this.hash = end_name_hash(hash); //到这里路径中的一个分量(目录名)的散列值就计算完了
//此时在this中保存了保存了目录名,目录名字长度和该目录散列值
if (!c)
goto last_component;
while (*++name == '/');
if (!*name)
goto last_with_slashes;
//上面的代码就已经解析出了路径名中的一个分量,并保存在this中
if (this.name[0] == '.') switch (this.len) { 如果名字第一个字符是”.”
default: //路径名出错
break;
case 2:
if (this.name[1] != '.') //表示上级目录
break;
follow_dotdot(nd);
inode = nd->path.dentry->d_inode;
/* fallthrough */
case 1: //表示当前目录, 立刻继续查找下个路径分量
continue;
}
…………………
/* This does the actual lookups.. */
err = do_lookup(nd, &this, &next); //在cache中查找该分量是否存在,如果不存在会建立一个dentry
if (err)
break;
err = -ENOENT;
inode = next.dentry->d_inode;
if (!inode)
goto out_dput;
err = -ENOTDIR;
if (!inode->i_op)
goto out_dput;
if (inode->i_op->follow_link) {
err = do_follow_link(&next, nd);
if (err)
goto return_err;
err = -ENOENT;
inode = nd->path.dentry->d_inode;
if (!inode)
break;
err = -ENOTDIR;
if (!inode->i_op)
break;
} else
path_to_nameidata(&next, nd);
err = -ENOTDIR;
if (!inode->i_op->lookup)
break;
continue;
/* here ends the main loop */
…………………..
}
可以看到这个函数实现很烦琐,简单的说这个函数就是依次解析路径中的每个分量,如果该目录项不在目录项高速缓存中那么就在目录项高速缓存中建立对于的dentry ,并将最后一个分量的父目录的dentry和mnt赋给路标nd,.
下面看一下怎么在cache中查找该目录项对象:
static int do_lookup(struct nameidata *nd, struct qstr *name,
struct path *path)
{
struct vfsmount *mnt = nd->path.mnt;
struct dentry *dentry = __d_lookup(nd->path.dentry, name);//在父目录所在的散列表中查找该目录项对象
if (!dentry)
goto need_lookup;
………………
need_lookup:
dentry = real_lookup(nd->path.dentry, name, nd); //如果在__d_lookup中未找到,需要进一步查找
if (IS_ERR(dentry))
goto fail;
goto done;
……….
}
进一步看看real_lookup()的代码:
static struct dentry * real_lookup(struct dentry * parent, struct qstr * name, struct nameidata *nd)
{
struct dentry * result;
struct inode *dir = parent->d_inode; //获取父目录对于的inode
mutex_lock(&dir->i_mutex);
result = d_lookup(parent, name); //在这里又进行了一次查找,
if (!result) { //如果确认未找到,则需要在内存中建立一个目录项对象了
struct dentry * dentry = d_alloc(parent, name); //分配一个dentry
result = ERR_PTR(-ENOMEM);
if (dentry) {
result = dir->i_op->lookup(dir, dentry, nd); //这是一个很中要的回调函数了,下面介绍
if (result)
dput(dentry);
else
result = dentry;
}
mutex_unlock(&dir->i_mutex);
return result;
}
……………..
}
在这个函数中又一次调用了d_lookup()对dentry进行查找 ,这里为什么先后两次进行相同的查找操作呢?难道仅仅是为了使查找dentry更加准确?为啥子又要加这样的安全套呢?
好了,如果这次也没在目录项高速缓存中找到dentry,那么就需要在目录项高速缓存中新建一个dentry了.
这里需要关注一个回调函数dir->i_op->lookup(dir, dentry, nd); 这个函数在不同的文件系统实现是不同的, 在以后分析其他文件系统的时候再来感受, 这里只看一下rootfs中这个回调函数是咋个定义的:
其实在rootfs中创建inode时定义了inode操作方法(可参见上文),lookup实现函数为simple_lookup();
下面就来看看这个函数都干了些啥子:
struct dentry *simple_lookup(struct inode *dir, struct dentry *dentry, struct nameidata *nd)
{
static struct dentry_operations simple_dentry_operations = {
.d_delete = simple_delete_dentry,
};
if (dentry->d_name.len > NAME_MAX) //linux下文件名字长度不得超过255
return ERR_PTR(-ENAMETOOLONG);
dentry->d_op = &simple_dentry_operations; //定义新建dentry操作方法
d_add(dentry, NULL);
return NULL;
}
d_add(dentry, NULL)函数调用了d_instantiate(entry, inode)其实就关联dentry和inode了,不过因为这里的inode还没有创建所以就为空了, 另外还调用了一个函数d_rehash(entry);这个函数比较简单,就是计算出dentry对应的散列值,然后把它加到对应的散列表中 ,这样在查找的时候就可以快速的找到这个dentry了,而不会再去新建一个了.
至此路径查找的流程已经简单的跑了一遍了,根据查找的flags, 查找的结果放在了nd中,下面就可以用这个”路标”nd做具体的事情了^^^^^^^^^^^。
三: 建立目录文件
看了上面的软件流程其实已经很清晰了, 我们先看看sys_mkdir()这个系统调用的代码片段:
asmlinkage long sys_mkdirat(int dfd, const char __user *pathname, int mode)
{
int error = 0;
char * tmp;
struct dentry *dentry;
struct nameidata nd;
tmp = getname(pathname); //需拷贝 USER空间的pathname 到KERNEL空间
………
error = do_path_lookup(dfd, tmp, LOOKUP_PARENT, &nd); //查找路径上面已经介绍
if (error)
goto out;
dentry = lookup_create(&nd, 1); //创建目标文件的dentry (路径最后一个分量)
…….
error = mnt_want_write(nd.path.mnt);
if (error)
goto out_dput;
error = vfs_mkdir(nd.path.dentry->d_inode, dentry, mode); //创建目录文件的索引节点
mnt_drop_write(nd.path.mnt);
………
..
}
在调用do_path_lookup()后,路径的最后一个分量(目标文件)的父目录信息已经保存到nd中了, 接下来要做的就是建立目标文件的dentyr了(也就是路径的最后一个分量的目录项对象) .目标文件的dentry是由lookup_create()函数创建的 ,实现方式和上面分析的是差不多的,这里就不列出代码了。
创建目录文件还需要创建目录文件对应的索引节点inode, 下面跟踪一下vfs_mkdir函数的实现:
int vfs_mkdir(struct inode *dir, struct dentry *dentry, int mode)
{
int error = may_create(dir, dentry, NULL);
if (error)
return error;
if (!dir->i_op || !dir->i_op->mkdir) //检查父目录的索引节点操作方法
return -EPERM;
mode &= (S_IRWXUGO|S_ISVTX);
error = security_inode_mkdir(dir, dentry, mode);
if (error)
return error;
DQUOT_INIT(dir);
error = dir->i_op->mkdir(dir, dentry, mode); //调用父目录文件索引节点操作方法创建inode
if (!error)
fsnotify_mkdir(dir, dentry);
return error;
}
这个函数比较简单,最后调用dir->i_op->mkdir(dir, dentry, mode)回调函数,
下面来看看rootfs inode操作方法是如何定义的:
static const struct inode_operations ramfs_dir_inode_operations = {
.create = ramfs_create, //创建普通文件
.lookup = simple_lookup,
.link = simple_link,
.unlink = simple_unlink,
.symlink = ramfs_symlink, //创建符号链接文件
.mkdir = ramfs_mkdir, //创建目录文件
.rmdir = simple_rmdir,
.mknod = ramfs_mknod, //创建特殊文件
.rename = simple_rename,
};
可以看出rootfs 使用的inode操作方法和ramfs inode操作方法是一样的,因为它们都是内存文件系统 .
下面是ramfs_mkdir()代码片段:
static int ramfs_mkdir(struct inode * dir, struct dentry * dentry, int mode)
{
int retval = ramfs_mknod(dir, dentry, mode | S_IFDIR, 0);
if (!retval)
inc_nlink(dir);
return retval;
}
ramfs_mknod()在内存中创建一个inode 模式为DIR,说明了我们创建的是目录文件。
ramfs_mknod(struct inode *dir, struct dentry *dentry, int mode, dev_t dev)
{
struct inode * inode = ramfs_get_inode(dir->i_sb, mode, dev); //在上篇文章rootfs挂载中已经介绍
int error = -ENOSPC;
if (inode) {
if (dir->i_mode & S_ISGID) {
inode->i_gid = dir->i_gid;
if (S_ISDIR(mode))
inode->i_mode |= S_ISGID;
}
d_instantiate(dentry, inode); //关联dentry和indoe
dget(dentry); /* Extra count - pin the dentry in core */
error = 0;
dir->i_mtime = dir->i_ctime = CURRENT_TIME; //设置创建时间
}
return error;
}
到此这个目录文件我们已经创建好了^^^^^^^^^^^.
四: 创建普通文件
熟悉linux驱动的人都知道静态创建一个字符或者块设备是用mknod系统调用实现的.
那下面我们就来看看mknod是咋个创建一个普通文件的.
asmlinkage long sys_mknodat(int dfd, const char __user *filename, int mode,
unsigned dev)
{
int error = 0;
char * tmp;
struct dentry * dentry;
struct nameidata nd;
if (S_ISDIR(mode))
return -EPERM;
tmp = getname(filename);
if (IS_ERR(tmp))
return PTR_ERR(tmp);
error = do_path_lookup(dfd, tmp, LOOKUP_PARENT, &nd);
if (error)
goto out;
dentry = lookup_create(&nd, 0);
if (IS_ERR(dentry)) {
error = PTR_ERR(dentry);
goto out_unlock;
}
if (!IS_POSIXACL(nd.path.dentry->d_inode))
mode &= ~current->fs->umask;
error = may_mknod(mode);
if (error)
goto out_dput;
error = mnt_want_write(nd.path.mnt);
if (error)
goto out_dput;
switch (mode & S_IFMT) {
case 0: case S_IFREG: //普通文件
error = vfs_create(nd.path.dentry->d_inode,dentry,mode,&nd);
break;
case S_IFCHR: case S_IFBLK: //设备文件
error = vfs_mknod(nd.path.dentry->d_inode,dentry,mode,
new_decode_dev(dev));
break;
case S_IFIFO: case S_IFSOCK: //FIFO,SOCKET文件
error = vfs_mknod(nd.path.dentry->d_inode,dentry,mode,0);
break;
}
mnt_drop_write(nd.path.mnt);
……………….
}
从上面的代码可以看出此函数的实现和sys_mkdirat 其实都是差不多的,只是sys_mkdirat函数只能创建目录文件, 而sys_mknodat只能创建普通文件和特殊文件.
创建普通文件调用vfs_create(), 创建特殊文件调用vfs_mknod() ,其实他们的实现最终实现方式都是差不多的,,都需要调用ramfs_mknod() ,只是传入的文件类型参数(mode)不同而已.
五: 小结
通过上面的分析,对目录文件和普通文件的创建有了一个大体的认识.,上面只是分析的是内存文件系统文件的创建, 实际的文件系统也就可以Mount到这些目录下了,但是实际的磁盘文件系统文件的创建要更复杂些了. VFS这棵树也就是这样慢慢发展起来的.