继续上一篇的分析,我们在使用命令: mknod /dev/my_chr_dev0 c $major 0 创建设备文件的时候,内核做了哪些事呢?可以肯定的一点是,内核肯定创建了一个 inode 结构体,并加入到系统里面,要不然在打开设备文件的时候,会因为lookup_fast()函数找不到相应的 inode,从而使得打开失败。也许你会说,lookup_fast()函数失败了,还有lookup_slow()函数呢?这里因为是特殊文件,情况有所不同,如果lookup_fast()函数失败了,那么就会导致打开失败,不会在lookup_slow()函数里面动态创建 inode,而创建inode的工作其实是在 mknod 系统调用里面完成的。下面来简单分析其具体过程。
首先通过 strace 来查看下系统调用的传入参数:
strace -o syscall mknod /dev/test c 243 0
结果如下:
...
mknod("/dev/test", S_IFCHR|0666, makedev(243, 0)) = 0
...
好了,现在来看下内核里面关于 mknod 系统调用的定义,在 source/fs/namei.c 文件中(Linux所有系统调用都是通过宏 SYSCALL_DEFINEn 定义的,关于这个宏的详细说明,参考这里):
SYSCALL_DEFINE3(mknod, const char __user *, filename, umode_t, mode, unsigned, dev)
{
return sys_mknodat(AT_FDCWD, filename, mode, dev);
}
好了,来看 sys_mknodat 的定义:
SYSCALL_DEFINE4(mknodat, int, dfd, const char __user *, filename, umode_t, mode,
unsigned, dev)
{
...
/* 这里进行路径解析并创建新的 dentry */
dentry = user_path_create(dfd, filename, &path, lookup_flags);
...
switch (mode & S_IFMT) {
...
/* 在这里创建 inode */
case S_IFCHR: case S_IFBLK:
error = vfs_mknod(path.dentry->d_inode,dentry,mode,
new_decode_dev(dev));
break;
...
}
可见,其实就两步:1,创建 dentry;2,创建 inode。先看dentry的创建,user_path_create()函数的定义:
inline struct dentry *user_path_create(int dfd, const char __user *pathname,
struct path *path, unsigned int lookup_flags)
{
return filename_create(dfd, getname(pathname), path, lookup_flags);
}
其中函数 filename_create()定义:
static struct dentry *filename_create(int dfd, struct filename *name,
struct path *path, unsigned int lookup_flags)
{
...
name = filename_parentat(dfd, name, lookup_flags, path, &last, &type);
...
dentry = __lookup_hash(&last, path->dentry, lookup_flags);
...
return dentry;
}
其中 filename_parentat()函数主要完成的是路径解析的工作,其中调用了 path_parentat()->link_path_walk()函数来完成路径解析工作,前面文章以及介绍过,这里不再详细分析。而__lookup_hash()函数(先在系统缓存中查找dentry,如果找不到)则主要通过调用d_alloc()函数创建新的 dentry 并加入到系统中。主要所使用的函数在前面也介绍过,这里不再分析。下面重点分析 inode 的创建过程: vfs_mknod()函数:
int vfs_mknod(struct inode *dir, struct dentry *dentry, umode_t mode, dev_t dev)
{
...
error = dir->i_op->mknod(dir, dentry, mode, dev);
...
return error;
}
这里调用了文件系统相关的函数:dir->i_op->mknod()。这是父目录 /dev 的i_op->mknod 函数,这个函数指针指向的是shmem_mknod()函数:
static int
shmem_mknod(struct inode *dir, struct dentry *dentry, umode_t mode, dev_t dev)
{
struct inode *inode;
int error = -ENOSPC;
inode = shmem_get_inode(dir->i_sb, dir, mode, dev, VM_NORESERVE);
if (inode) {
...
d_instantiate(dentry, inode); /* 可简单理解成: dentry->d_inode = inode; */
dget(dentry); /* Extra count - pin the dentry in core */
}
return error;
...
}
其中主要工作是在 shmem_get_inode()函数中完成:
static struct inode *shmem_get_inode(struct super_block *sb, const struct inode *dir,
umode_t mode, dev_t dev, unsigned long flags)
{
struct inode *inode;
struct shmem_inode_info *info;
struct shmem_sb_info *sbinfo = SHMEM_SB(sb);
if (shmem_reserve_inode(sb))
return NULL;
/* 在内核空间创建 inode 结构体(分配内存) */
inode = new_inode(sb);
if (inode) {
/* 下面是各种成员变量的初始化 */
inode->i_ino = get_next_ino();
inode_init_owner(inode, dir, mode);
inode->i_blocks = 0;
inode->i_atime = inode->i_mtime = inode->i_ctime = current_time(inode);
inode->i_generation = get_seconds();
info = SHMEM_I(inode);
memset(info, 0, (char *)inode - (char *)info);
spin_lock_init(&info->lock);
info->seals = F_SEAL_SEAL;
info->flags = flags & VM_NORESERVE;
INIT_LIST_HEAD(&info->shrinklist);
INIT_LIST_HEAD(&info->swaplist);
simple_xattrs_init(&info->xattrs);
cache_no_acl(inode);
/***********************************************/
switch (mode & S_IFMT) {
default:
inode->i_op = &shmem_special_inode_operations;
init_special_inode(inode, mode, dev); /* 我们最感兴趣的在这里 */
break;
...
}
} else
shmem_free_inode(sb);
return inode;
}
可见在这个函数里面,首先通过new_inode()函数在内核空间分配内存,这里不再详细展开。然后对各个成员变量进行初始化,这里我们也不感兴趣,最感兴趣的地方在 init_special_inode()函数里面:
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
inode->i_mode = mode;
if (S_ISCHR(mode)) {
inode->i_fop = &def_chr_fops;
inode->i_rdev = rdev;
}
...
}
可见这里保存了两个重要的成员变量:文件操作函数集和设备号。而这个文件操作函数集是一个通用的操作集,所有字符驱动文件打开时都会调用,在这个函数里面,通过设备号来找到真正的该设备的文件操作函数集。先看这个 def_chr_fops 的定义:
/*
* Dummy default file-operations: the only thing this does
* is contain the open that then fills in the correct operations
* depending on the special file...
*/
const struct file_operations def_chr_fops = {
.open = chrdev_open,
.llseek = noop_llseek,
};
而这个 chrdev_open()函数就是我们上一篇里面分析的函数。可见这个 mknod 系统调用无非就是把文件的设备号保存到新创建的 inode 里面,而真正的驱动相关的文件操作函数集并没有保存在这里面,而是保存在 cdev_map->probes 数组中(上一篇分析过),但巧妙之处在于我们可以通过文件的设备号轻松的找到驱动相关的文件操作函数集。最后一点需要说明的是我们回到 shmem_mknod()函数,这里显式的调用了 dget() 函数。其实在通过调用d_alloc()函数创建新的 dentry 时,已经将将其引用计数设置为1:
dentry->d_lockref.count = 1
这里再次调用 dget() 函数就是要保证通过 mknod 函数创建的 inode 永远不会被释放掉(除非 rm /dev/my_chr_dev0)。这样就保证了 lookup_fast()函数总能成功返回。