限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
本文基于 Linux 4.14
内核源码进行分析。
用户空间的应用程序,经常会通过 /dev/XXX
设备节点,和内核空间进行交互,如读取键盘输入、利用串口设备通信等等。那么,这些 /dev/XXX
设备节点是如何创建和删除的呢?接下来就来探讨这些细节。在不同的历史时期,Linux 设备节点的创建删除方式各不相同,本文不会一一展开。最大的变化,大概是从 devfs
变化为现今一直在使用的 devtmpfs
,本文针对 devtmpfs
进行讨论。在不同的场景下,设备节点的创建移除会有些差异,下面我们一一列举这些不同的情形。
devtmpfs
创建一个名为 "kdevtmpfs"
内核线程,然后 driver core
在添加(device_add()
)、移除(device_del()
)设备过程中 ,通过 devtmpfs
接口 devtmpfs_create_node()/devtmpfs_delete_node()
,将 设备创建、移除请求信息(struct req)
添加到内核线程 "kdevtmpfs"
的请求队列 requests
,接着唤醒内核线程 "kdevtmpfs"
处理请求,然后陷入睡眠等待直到请求处理完成;内核线程 "kdevtmpfs"
被唤醒后,逐个取出请求信息,按请求类型 创建 (vfs_mknod())、移除(vfs_unlink())
设备节点,之后唤醒等待请求完成的线程。最后,driver core
会给用户空间设备事件监听程序(如 udevd
)发送消息,让监听程序对设备事件做相应处理。
start_kernel()
rest_init()
pid = kernel_thread(kernel_init, NULL, CLONE_FS);
kernel_init()
kernel_init_freeable()
do_basic_setup()
driver_init()
devtmpfs_init() /* 初始化 devtmpfs */
...
usermodehelper_enable()
do_initcalls() /* init 初始化接口: 包括驱动注册加载 */
prepare_namespace() /* 挂载 rootfs */
/* drivers/bash/devtmpfs.c */
static struct task_struct *thread;
int __init devtmpfs_init(void)
{
int err = register_filesystem(&dev_fs_type); /* 注册 devtmpfs 文件系统类型 */
...
/* 创建 【设备节点创建请求处理线程】 */
thread = kthread_run(devtmpfsd, &err, "kdevtmpfs");
if (!IS_ERR(thread)) {
/*
* 等待 "kdevtmpfs" 【设备节点创建请求处理线程】 就绪:
* "kdevtmpfs" 线程函数 devtmpfsd() 会在挂载初始化好 devtmpfs 后,
* 会调用
* complete(&setup_done);
* 宣告已经准备好接收 【设备节点创建请求处理】 了。
*/
wait_for_completion(&setup_done);
} else {
...
}
}
static int devtmpfsd(void *p)
{
char options[] = "mode=0755";
int *err = p;
*err = sys_unshare(CLONE_NEWNS);
if (*err)
goto out;
/* 挂载 devtmpfs 文件系统 */
*err = sys_mount("devtmpfs", "/", "devtmpfs", MS_SILENT, options);
if (*err)
goto out;
sys_chdir("/.."); /* will traverse into overmounted root */
sys_chroot(".");
/*
* 唤醒在 devtmpfs_init() 中等待的线程:
* devtmpfs_init() -> wait_for_completion(&setup_done)
*/
complete(&setup_done);
while (1) { /* 处理设备节点创建请求 */
// 后面细述
...
}
out:
complete(&setup_done);
return *err;
}
通过 ps 命令可以查看到 kdevtmpfs 线程:
root@qemu-ubuntu:~# ps -ef | grep kdevtmpfs | grep -v grep
root 32 2 0 07:58 ? 00:00:00 [kdevtmpfs]
/* drivers/base/core.c */
int device_add(struct device *dev)
{
...
if (MAJOR(dev->devt)) {
error = device_create_file(dev, &dev_attr_dev);
...
error = device_create_sys_dev_entry(dev);
...
/* 向 devtmpfs 的 "kdevtmpfs" 内核线程发出 设备节点创建 请求 */
devtmpfs_create_node(dev);
}
...
}
添加创建请求:
/* drivers/base/devtmpfs.c */
int devtmpfs_create_node(struct device *dev)
{
if (!thread) /* "kdevtmpfs" 内核线程尚未就绪 */
return 0;
...
req.name = device_get_devnode(dev, &req.mode, &req.uid, &req.gid, &tmp);
...
if (req.mode == 0)
req.mode = 0600; /* req.mode != 0 表示设备创建请求,否则是删除请求 */
if (is_blockdev(dev))
req.mode |= S_IFBLK;
else
req.mode |= S_IFCHR;
req.dev = dev;
init_completion(&req.done);
wake_up_process(thread); /* 唤醒 "kdevtmpfs" 线程,处理请求 */
wait_for_completion(&req.done); /* 等待请求处理完成 */
kfree(tmp);
return req.err;
}
/* drivers/bash/devtmpfs.c */
static int devtmpfsd(void *p)
{
...
while (1) { /* 处理设备节点创建请求 */
spin_lock(&req_lock);
while (requests) {
struct req *req = requests;
requests = NULL;
spin_unlock(&req_lock);
while (req) { /* 逐个处理请求 */
struct req *next = req->next;
req->err = handle(req->name, req->mode,
req->uid, req->gid, req->dev);
/*
* 通知请求处理完成:
* devtmpfs_create_node() / devtmpfs_delete_node()
* wait_for_completion(&req.done)
*/
complete(&req->done);
req = next;
}
spin_lock(&req_lock);
}
/* 没有请求期间,陷入睡眠 */
__set_current_state(TASK_INTERRUPTIBLE);
spin_unlock(&req_lock);
schedule();
}
return 0;
out:
...
}
static int handle(const char *name, umode_t mode, kuid_t uid, kgid_t gid,
struct device *dev)
{
if (mode) /* 处理节点创建请求 */
return handle_create(name, mode, uid, gid, dev);
else /* 处理节点删除请求 */
return handle_remove(name, dev); // 后面展开
}
/* 处理设备节点创建请求 */
static int handle_create(const char *nodename, umode_t mode, kuid_t uid,
kgid_t gid, struct device *dev)
{
struct dentry *dentry;
struct path path;
int err;
dentry = kern_path_create(AT_FDCWD, nodename, &path, 0);
...
/* 调用具体文件系统的 mknode 接口,如 ext4 的 ext4_mknod() */
err = vfs_mknod(d_inode(path.dentry), dentry, mode, dev->devt);
...
return err;
}
/* drivers/base/core.c */
int device_add(struct device *dev)
{
...
if (MAJOR(dev->devt)) {
error = device_create_file(dev, &dev_attr_dev);
...
error = device_create_sys_dev_entry(dev);
...
/* 向 devtmpfs 的 "kdevtmpfs" 内核线程发出 设备节点创建 请求 */
devtmpfs_create_node(dev);
}
...
/* 通知用户态设备事件监听程序: 添加了设备对象 */
kobject_uevent(&dev->kobj, KOBJ_ADD);
...
}
典型的用户态设备事件监听程序 udev
,在监听到添加设备事件后,按照配置的规则,做 修改设备节点权限、添加设备节点符号链接
等动作。
到此,系统启动期间,所有的设备节点创建工作已经完成,但是由于我们挂载 devtmpfs
、以及其下设备节点的创建工作,是在 rootfs
挂载前完成的,这样用户空间是无法在根目录 /
下看到这些设备节点的,所以在 rootfs
挂载完成后,系统重新挂载 devtmpfs
到了 rootfs
的 /dev
目录,来看细节:
start_kernel()
rest_init()
pid = kernel_thread(kernel_init, NULL, CLONE_FS);
kernel_init()
kernel_init_freeable()
do_basic_setup()
driver_init()
devtmpfs_init() /* 初始化 devtmpfs */
...
usermodehelper_enable()
do_initcalls() /* init 初始化接口: 包括驱动注册加载 */
prepare_namespace() /* 挂载 rootfs */
/* init/do_mounts.c */
void __init prepare_namespace(void)
{
...
mount_root(); /* 挂载根文件系统(rootfs) */
out:
devtmpfs_mount("dev"); /* 将 devtmpfs 重新挂载到根文件系统的 /dev 目录 */
sys_mount(".", "/", NULL, MS_MOVE, NULL);
sys_chroot(".");
}
/* drivers/base/core.c */
void device_del(struct device *dev)
{
...
if (MAJOR(dev->devt)) {
devtmpfs_delete_node(dev); /* 向 devtmpfs 的 "kdevtmpfs" 内核线程发出 设备节点创建 请求 */
device_remove_sys_dev_entry(dev);
device_remove_file(dev, &dev_attr_dev);
}
...
}
/* drivers/bash/devtmpfs.c */
static int handle_remove(const char *nodename, struct device *dev)
{
struct path parent;
struct dentry *dentry;
int deleted = 0;
int err;
dentry = kern_path_locked(nodename, &parent);
...
if (d_really_is_positive(dentry)) {
...
if (!err && dev_mynode(dev, d_inode(dentry), &stat)) {
...
/* 调用具体文件系统的 unlink 接口,如 ext4 的 ext4_unlink() */
err = vfs_unlink(d_inode(parent.dentry), dentry, NULL);
...
}
}
if (deleted && strchr(nodename, '/'))
delete_path(nodename);
return err;
}
/* drivers/base/core.c */
void device_del(struct device *dev)
{
...
if (MAJOR(dev->devt)) {
devtmpfs_delete_node(dev); /* 向 devtmpfs 的 "kdevtmpfs" 内核线程发出 设备节点移除 请求 */
device_remove_sys_dev_entry(dev);
device_remove_file(dev, &dev_attr_dev);
}
...
/* 通知用户态设备事件监听程序: 设备对象移除了 */
kobject_uevent(&dev->kobj, KOBJ_REMOVE);
...
}
典型的用户态设备事件监听程序 udev
,在监听到添加移除事件后,按照配置的规则,做 移除设备节点符号链接
等动作。
并非所有的设备节点都经由、或必须经由 driver core
创建移除,也可以通过系统调用 sys_mknod()/sys_unlink()
来完成。
/* fs/namei.c */
sys_mknod()
sys_mknodat(AT_FDCWD, filename, mode, dev)
/* 为 @filename 创建 dentry */
dentry = user_path_create(dfd, filename, &path, lookup_flags);
...
switch (mode & S_IFMT) {
...
case S_IFCHR: case S_IFBLK:
error = vfs_mknod(path.dentry->d_inode,dentry,mode,
new_decode_dev(dev));
...
}
int vfs_mknod(struct inode *dir, struct dentry *dentry, umode_t mode, dev_t dev)
{
...
error = dir->i_op->mknod(dir, dentry, mode, dev); /* 如 ext4_mknod() */
...
}
sys_unlink()
do_unlinkat(AT_FDCWD, pathname)
error = vfs_unlink(path.dentry->d_inode, dentry, &delegated_inode)
int vfs_unlink(struct inode *dir, struct dentry *dentry, struct inode **delegated_inode)
{
...
error = dir->i_op->unlink(dir, dentry); /* ext4_unlink(), ... */
...
}
https://lwn.net/Articles/331818/#:~:text=Sievers%20outlines%20the%20differences%20between%20devtmpfs%20and%20Adam,600%20for%20an%20early%20version%20of%20Richter%27s%20mini-devfs.
https://manpages.ubuntu.com/manpages/xenial/man7/udev.7.html