LV05-03-Kernel-05-01-mknod

本文主要是kernel——mknod源码解析的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
https://elixir.bootlin.com/u-boot/latest/source uboot源码

一、mknod命令

1. 命令简介

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

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

2. 基本格式

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

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

例如:

1
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 · 苏木/imx6ull-driver-demo - 码云 - 开源中国

我们可以执行:

1
make ARCH=x86_64

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

image-20241206215028381

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

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

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

1
2
3
4
5
6
7
# ......
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系统调用:

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

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

1
sudo rm -rf /dev/dev_node

二、mknod源码分析

1. makedev

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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函数的帮助信息:

1
2
3
4
5
6
7
8
9
10
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

1
2
3
4
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

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

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

3. do_mknodat函数

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

1
2
3
4
5
6
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

1
2
3
4
5
6
7
8
9
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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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()函数。):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*
* 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()函数中完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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()函数里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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进行如下赋值操作:

1
2
inode->i_fop = &def_chr_fops;
inode->i_rdev = rdev;

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

1
2
3
4
5
6
7
8
9
/*
* 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

1
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博客