Skip to content

LV150-mknod命令简介

一、mknod 命令

1. 命令简介

mknod 命令是 Linux 系统中用于创建设备文件节点和命名管道的命令。设备文件是 Linux 系统中用于表示硬件设备或设备驱动程序的特殊文件,它们允许用户空间程序与内核空间中的驱动程序进行交互。mknod 命令通过指定设备文件的名称、类型(块设备或字符设备)以及主次设备号来创建设备文件节点。

在数据处理和分析中,mknod 命令通常用于与硬件设备交互,例如读取磁盘数据、控制串口通信等。通过创建设备文件节点,用户空间程序可以像操作普通文件一样操作硬件设备,从而实现数据的读取、写入和控制等功能。

2. 基本格式

我们前面知道 mknod 命令用于创建设备节点,命令格式如下:

shell
mknod 设备名 设备类型 主设备号 次设备号

例如:

shell
mkmod /dev/dev_node c 246 0

然后我们就可以看到/dev 目录下出现了一个名为 dev_node 的设备节点。那么这中间是怎样的一个过程?

3. mknod 在干什么?

我们可以使用 strace 命令看一下 mknod 命令执行的时候在干什么。由于在 ARM 开发板上使用的话,还要移植,这里直接在 ubuntu 中测试好啦。用之前写的用 mknod 创建节点的 demo 来编译一个在 ubuntu 下运行的就可以了,源码在这里:04_chrdev_basic/03_chrdev_node/chrdev_node_demo.c

我们可以执行:

shell
make ARCH=x86_64

然后就会使用 ubuntu 中的内核进行编译,编译出的驱动可以直接在 ubuntu 中使用。我们看一下加载模块后的设备号信息:

image-20241206215028381

然后我们用以下命令来追踪一下执行 mknod 的时候发生了什么:

shell
 strace -o syscall mknod /dev/dev_node c 237 0

然后我们会得到一个名为 syscall 文件,文件内容如下:

txt
# ......
close(3)                                = 0
mknod("/dev/dev_node", S_IFCHR|0666, makedev(0xed, 0)) = 0 = 0
close(1)                                = 0
close(2)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++

会发现经过一堆的系统调用后,调用了 mknod 系统调用:

txt
mknod("/dev/dev_node", S_IFCHR|0666, makedev(0xed, 0)) = 0

这里就为我们提供了分析的起始点。最后记得删掉创建的节点:

shell
sudo rm -rf /dev/dev_node

二、mknod 源码分析

1. makedev

makedev 是一个库函数,我们可以使用 man makedev 来查看帮助信息:

shell
MAKEDEV(3)                                                                 Linux Programmer's Manual                                                                 MAKEDEV(3)

NAME
       makedev, major, minor - manage a device number

SYNOPSIS
       #include <sys/sysmacros.h>

       dev_t makedev(unsigned int maj, unsigned int min);

       unsigned int major(dev_t dev);
       unsigned int minor(dev_t dev);

2. mknod 系统调用

我们在 linux 下执行 man 2 mknod 就可以看到 mknod 函数的帮助信息:

shell
NAME
       mknod, mknodat - create a special or ordinary file

SYNOPSIS
       #include <sys/types.h>
       #include <sys/stat.h>
       #include <fcntl.h>
       #include <unistd.h>

       int mknod(const char *pathname, mode_t mode, dev_t dev);

mknod 命令最终也是通过该系统调用去执行的,它的系统调用定义在 namei.c - fs/namei.c - SYSCALL_DEFINE3

c
SYSCALL_DEFINE3(mknod, const char __user *, filename, umode_t, mode, unsigned, dev)
{
	return do_mknodat(AT_FDCWD, filename, mode, dev);
}

Linux 所有系统调用都是通过宏 SYSCALL_DEFINEn 定义的,关于这个宏的详细说明可以看这里:Anatomy of a system call part 1。这个宏定义在:syscalls.h - include/linux/syscalls.h - SYSCALL_DEFINE3

c
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)

看了一下,挺复杂的,先这样吧。

3. do_mknodat 函数

接下来我们看 do_mknodat 函数,它定义在 namei.c - fs/namei.c - do_mknodat

c
long do_mknodat(int dfd, const char __user *filename, umode_t mode,
		unsigned int dev)
{
	struct dentry *dentry;
	struct path path;
	int error;
	unsigned int lookup_flags = 0;

	error = may_mknod(mode);
	if (error)
		return error;
retry:
	dentry = user_path_create(dfd, filename, &path, lookup_flags); // 这里进行路径解析并创建新的 dentry
	if (IS_ERR(dentry))
		return PTR_ERR(dentry);

	if (!IS_POSIXACL(path.dentry->d_inode))
		mode &= ~current_umask();
	error = security_path_mknod(&path, dentry, mode, dev);
	if (error)
		goto out;
	switch (mode & S_IFMT) { // 在这里创建 inode
		case 0: case S_IFREG:
			error = vfs_create(path.dentry->d_inode,dentry,mode,true);
			if (!error)
				ima_post_path_mknod(dentry);
			break;
		case S_IFCHR: case S_IFBLK:
			error = vfs_mknod(path.dentry->d_inode,dentry,mode,
					new_decode_dev(dev));
			break;
		case S_IFIFO: case S_IFSOCK:
			error = vfs_mknod(path.dentry->d_inode,dentry,mode,0);
			break;
	}
out:
	done_path_create(&path, dentry);
	if (retry_estale(error, lookup_flags)) {
		lookup_flags |= LOOKUP_REVAL;
		goto retry;
	}
	return error;
}

其实就两步:(1)创建 dentry;(2)创建 inode。

3.1 may_mknod 函数

一开始调用了这个 may_mknod 函数,它定义在 namei.c - fs/namei.c - may_mknod

c
static int may_mknod(umode_t mode)
{
	switch (mode & S_IFMT) {
	case S_IFREG:
	case S_IFCHR:
	case S_IFBLK:
	case S_IFIFO:
	case S_IFSOCK:
	case 0: /* zero mode translates to S_IFREG */
		return 0;
	case S_IFDIR:
		return -EPERM;
	default:
		return -EINVAL;
	}
}

可以看到这个函数是在检查模式,我们前面传入的是 S_IFCHR|0666 ,所以这里将会返回 0。返回 0 的话,do_mknodat()函数将会继续执行 retry 之后的部分。

3.2 user_path_create 函数

来看一下这个 user_path_create 函数,它用来创建一个 dentry。函数定义在 namei.c - fs/namei.c - user_path_create

c
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);
}
EXPORT_SYMBOL(user_path_create);

可以看到这个是一个内联函数,根据前面的参数传递情况,这里参数对应情况为:filename_create 函数定义在 namei.c - fs/namei.c - filename_create

c
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);
	// ......
}

filename_create 函数用于在文件系统中创建一个新的文件名。这个函数通常在文件系统操作中被调用,例如创建新文件或目录。

(1)其中 filename_parentat()函数主要是完成的是路径解析的工作,它定义在 namei.c - fs/namei.c - filename_parentat,其中调用了 path_parentat()link_path_walk() 函数来完成路径解析工作。

(2)而__lookup_hash()函数(先在系统缓存中查找 dentry,如果找不到)则主要通过调用d_alloc()函数创建新的 dentry 并加入到系统中。

3.3 vfs_mknod

下面重点分析 inode 的创建过程,也就是这一行:namei.c - fs/namei.c - Linux source code v4.19.71 - Bootlin Elixir Cross Referencer

c
error = vfs_mknod(path.dentry->d_inode,dentry,mode, new_decode_dev(dev));

vfs_mknod()函数定义在 namei.c - fs/namei.c - vfs_mknod

c
int vfs_mknod(struct inode *dir, struct dentry *dentry, umode_t mode, dev_t dev)
{
	int error = may_create(dir, dentry);

	if (error)
		return error;

	if ((S_ISCHR(mode) || S_ISBLK(mode)) && !capable(CAP_MKNOD))
		return -EPERM;

	if (!dir->i_op->mknod)
		return -EPERM;

	error = devcgroup_inode_mknod(mode, dev);
	if (error)
		return error;

	error = security_inode_mknod(dir, dentry, mode, dev);
	if (error)
		return error;

	error = dir->i_op->mknod(dir, dentry, mode, dev);
	if (!error)
		fsnotify_create(dir, dentry);
	return error;
}
EXPORT_SYMBOL(vfs_mknod);

先来看一下函数的参数:

  • struct inode *dir:/dev 目录所指向的 inode 信息
  • struct dentry *dentry:我们要创建的设备文件的 dentry
  • umode_t mode:该 mode 是指的我们要创建的设备文件类型,如我们键入的 mknod /dev/dev_node c 237 0 命令,所以我们要创建的是字符设备,也就是说 S_ISCHR(mode)等于 true。
  • dev_t dev:和开始进入 mknod 系统调用一样都是,dev = makedev(237, 0)。

从 vfs_mknod()函数来看,最终会由/dev 所指向的 inode 中的 i_op 中的 mknod 回调函数进行处理,因此到这里算是分析一部分了。而我们的/dev 所指向的 inode 中的 i_op 中的 mknod 回调函数到底是什么呢?

我们来看一下,在 vfs_mknod()函数中调用了文件系统相关的函数:dir→ i_op→ mknod()。这是父目录 /dev 的 i_op→ mknod 函数,这个函数指针指向的是 shmem_mknod() 函数(为什么指向这个函数?是因为/dev 是一个目录,这个目录对应的文件系统是 devtmpfs,而这个 devtmpfs 文件系统所使用的 struct inode_operations 操作是 shmem_dir_inode_operations,因此会调用到 shmem_mknod()函数。):

c
/*
 * File creation. Allocate an inode, and we're done..
 */
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) {
		error = simple_acl_create(dir, inode);
		if (error)
			goto out_iput;
		error = security_inode_init_security(inode, dir,
						     &dentry->d_name,
						     shmem_initxattrs, NULL);
		if (error && error != -EOPNOTSUPP)
			goto out_iput;

		error = 0;
		dir->i_size += BOGO_DIRENT_SIZE;
		dir->i_ctime = dir->i_mtime = current_time(dir);
		d_instantiate(dentry, inode); /* 可简单理解成:  dentry-> d_inode = inode; */
		dget(dentry); /* Extra count - pin the dentry in core */
	}
	return error;
out_iput:
	iput(inode);
	return error;
}

其中主要工作是在 shmem_get_inode() 函数中完成:

c
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 = new_inode(sb); /* 在内核空间创建 inode 结构体(分配内存) */
	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 = prandom_u32();
		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;
		// ......

		lockdep_annotate_inode_mutex_key(inode);
	} else
		shmem_free_inode(sb);
	return inode;
}

可见在这个函数里面,首先通过 new_inode() 函数在内核空间分配内存,这里不再详细展开。然后对各个成员变量进行初始化,这里我们也不关注,需要关注的地方在 init_special_inode() 函数里面:

c
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;
	} else if (S_ISBLK(mode)) {
		inode->i_fop = &def_blk_fops;
		inode->i_rdev = rdev;
	} else if (S_ISFIFO(mode))
		inode->i_fop = &pipefifo_fops;
	else if (S_ISSOCK(mode))
		;	/* leave it no_open_fops */
	else
		printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for"
				  " inode %s:%lu\n", mode, inode->i_sb->s_id,
				  inode->i_ino);
}
EXPORT_SYMBOL(init_special_inode);

从 init_special_inode()函数可以看出来,当执行 mknod /dev/dev_node c 237 0 命令为 /dev/dev_node 设备文件生成的 inode 时,由于指定的设备文件类型为字符类型,所以会为 inode 进行如下赋值操作:

c
inode->i_fop = &def_chr_fops;
inode->i_rdev = rdev;

可见这里保存了两个重要的成员变量:文件操作函数集和设备号。而这个文件操作函数集是一个通用的操作集,所有字符驱动文件打开时都会调用,在这个函数里面,通过设备号来找到真正的该设备的文件操作函数集。先看这个 def_chr_fops 的定义:

c
/*
 * 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,
};

这个 mknod 系统调用无非就是把文件的设备号保存到新创建的 inode 里面,而真正的驱动相关的文件操作函数集并没有保存在这里面,而是保存在 cdev_map→ probes 数组中,但巧妙之处在于我们可以通过文件的设备号轻松的找到驱动相关的文件操作函数集。最后一点需要说明的是我们回到 shmem_mknod() 函数,这里显式的调用了 dget() 函数。其实在 namei.c - fs/namei.c 这里通过调用 d_alloc() 函数创建新的 dentry 时,已经将将其引用计数设置为 1:dcache.c - fs/dcache.c

c
dentry->d_lockref.count = 1

这里再次调用 dget() 函数就是要保证通过 mknod 函数创建的 inode 永远不会被释放掉(除非 rm /dev/dev_node)。这样就保证了 lookup_fast()函数总能成功返回。

4. 总结

当我们输入 mknod 命令时,实际上会创建设备文件 /dev/hello 和所对应的 inode,以及将主设备号和次设备号形成的设备号保存在 inodei_rdev 中。inode 信息怎么使用?我们后面可以看文件描述符分配的相关笔记。

参考资料

Linux 中 mknod 命令实现原理以及源码分析-CSDN 博客