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中使用。我们看一下加载模块后的设备号信息:
然后我们用以下命令来追踪一下执行mknod的时候发生了什么:
1 | strace -o syscall mknod /dev/dev_node c 237 0 |
然后我们会得到一个名为syscall文件,文件内容如下:
1 | # ...... |
会发现经过一堆的系统调用后,调用了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 | MAKEDEV(3) Linux Programmer's Manual MAKEDEV(3) |
2. mknod系统调用
我们在linux下执行 man 2 mknod就可以看到mknod函数的帮助信息:
1 | NAME |
mknod命令最终也是通过该系统调用去执行的,它的系统调用定义在namei.c - fs/namei.c - SYSCALL_DEFINE3:
1 | SYSCALL_DEFINE3(mknod, const char __user *, filename, umode_t, mode, unsigned, dev) |
Linux所有系统调用都是通过宏 SYSCALL_DEFINEn 定义的,关于这个宏的详细说明可以看这里:Anatomy of a system call part 1。这个宏定义在:syscalls.h - include/linux/syscalls.h - SYSCALL_DEFINE3:
1 |
看了一下,挺复杂的,先这样吧。
3. do_mknodat函数
接下来我们看do_mknodat函数,它定义在namei.c - fs/namei.c - do_mknodat:
1 | long do_mknodat(int dfd, const char __user *filename, umode_t mode, |
其实就两步:(1)创建 dentry;(2)创建 inode。
3.1 may_mknod函数
一开始调用了这个may_mknod函数,它定义在namei.c - fs/namei.c - may_mknod
1 | static int may_mknod(umode_t mode) |
可以看到这个函数是在检查模式,我们前面传入的是 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 | inline struct dentry *user_path_create(int dfd, const char __user *pathname, |
可以看到这个是一个内联函数,根据前面的参数传递情况,这里参数对应情况为:filename_create函数定义在namei.c - fs/namei.c - filename_create:
1 | static struct dentry *filename_create(int dfd, struct filename *name, |
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 | int vfs_mknod(struct inode *dir, struct dentry *dentry, umode_t mode, dev_t dev) |
先来看一下函数的参数:
- 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 | /* |
其中主要工作是在 shmem_get_inode()函数中完成:
1 | static struct inode *shmem_get_inode(struct super_block *sb, const struct inode *dir, |
可见在这个函数里面,首先通过new_inode()函数在内核空间分配内存,这里不再详细展开。然后对各个成员变量进行初始化,这里我们也不关注,需要关注的地方在 init_special_inode()函数里面:
1 | void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev) |
从init_special_inode()函数可以看出来,当执行mknod /dev/dev_node c 237 0
命令为/dev/dev_node
设备文件生成的inode
时,由于指定的设备文件类型为字符类型,所以会为inode
进行如下赋值操作:
1 | inode->i_fop = &def_chr_fops; |
可见这里保存了两个重要的成员变量:文件操作函数集和设备号。而这个文件操作函数集是一个通用的操作集,所有字符驱动文件打开时都会调用,在这个函数里面,通过设备号来找到真正的该设备的文件操作函数集。先看这个 def_chr_fops 的定义:
1 | /* |
这个 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
,以及将主设备号和次设备号形成的设备号保存在inode
的i_rdev
中。inode信息怎么使用?我们下一节看。
参考资料