Linux: 设备节点创建移除过程简析

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 分析背景

本文基于 Linux 4.14 内核源码进行分析。

3. 设备节点的创建和移除

用户空间的应用程序,经常会通过 /dev/XXX 设备节点,和内核空间进行交互,如读取键盘输入、利用串口设备通信等等。那么,这些 /dev/XXX 设备节点是如何创建和删除的呢?接下来就来探讨这些细节。在不同的历史时期,Linux 设备节点的创建删除方式各不相同,本文不会一一展开。最大的变化,大概是从 devfs 变化为现今一直在使用的 devtmpfs ,本文针对 devtmpfs 进行讨论。在不同的场景下,设备节点的创建移除会有些差异,下面我们一一列举这些不同的情形。

3.1 通过 devtmpfs 创建移除设备节点

3.1.1 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/base/devtmpfs.c */

static struct task_struct *thread;

int __init devtmpfs_init(void)
{
	/*
	 * 注册 devtmpfs 文件系统类型.
	 * 从 mount 命令观察到,/dev 目录下的文件 位于 文件系统 devtmpfs 下:
	 * # mount
	 * devtmpfs on /dev type devtmpfs (rw,relatime,size=98144k,nr_inodes=24536,mode=755)
	 *
	 * devtmpfs 是借助于其它文件系统实现的:
	 * . 启用了 CONFIG_TMPFS 的情形下, 
	 *   devtmpfs 借助 tmpfs (mm/shmem.c,shmem_fs_type) 的实现;
	 * . 关闭了 CONFIG_TMPFS 的情形下, 
	 *   devtmpfs 借助 ramfs (fs/ramfs/inode.c,ramfs_fs_type) 的实现.
	 * 细节参考 dev_fs_type.mount = dev_mount()
	 */
	int err = register_filesystem(&dev_fs_type);
	...

	/* 创建 【设备节点创建请求处理线程】 */
	thread = kthread_run(devtmpfsd, &err, "kdevtmpfs");
	if (!IS_ERR(thread)) {
		/*
		 * 等待 "kdevtmpfs" 【设备节点创建请求处理线程】 就绪:
		 * "kdevtmpfs" 线程函数 devtmpfsd() 会在挂载初始化好 devtmpfs 后,
		 * 会调用 
		 * complete(&setup_done);
		 * 宣告已经准备好接收 【设备节点创建请求处理】 了。
		 */
		wait_for_completion(&setup_done);
	} else {
		...
	}

	...

	printk(KERN_INFO "devtmpfs: initialized\n");
	return 0;
}

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;
}

有必要对 devtmpfs 的挂载过程做一进步的分析:

devtmpfsd() /* drivers/base/devtmpfs.c */
	/* 挂载 devtmpfs 文件系统 */
	*err = sys_mount("devtmpfs", "/", "devtmpfs", MS_SILENT, options); /* fs/namespace.c */
		ret = do_mount(kernel_dev, dir_name, kernel_type, flags, options);
			do_new_mount(&path, type_page, sb_flags, mnt_flags, dev_name, data_page);
				struct file_system_type *type;
				struct vfsmount *mnt;
				...
				mnt = vfs_kern_mount(type, sb_flags, name, data);
					root = mount_fs(type, flags, name, data);
						root = type->mount(type, flags, name, data);
							dev_mount(type, flags, name, data) /*  drivers/base/devtmpfs.c */

dev_mount(type, flags, name, data) /*  drivers/base/devtmpfs.c */
#ifdef CONFIG_TMPFS /* 开启了 CONFIG_TMPFS, devtmpfs 基于 tmpfs 实现 */
	return mount_single(fs_type, flags, data, shmem_fill_super);
		struct super_block *s;
		...
		/* 新建一个 super block 对象 */
		s = sget(fs_type, compare_single, set_anon_super, flags, NULL);
		...
		if (!s->s_root) { /* 还没有建立 文件系统 根目录 */
			/* 填充 super block + 建立 文件系统 根目录 */
			error = fill_super(s, data, flags & SB_SILENT ? 1 : 0);
				shmem_fill_super() /* mm/shmem.c */
					struct inode *inode;
					struct shmem_sb_info *sbinfo;
					...
					sbinfo = kzalloc(max((int)sizeof(struct shmem_sb_info),
							L1_CACHE_BYTES), GFP_KERNEL);
					...
					sb->s_fs_info = sbinfo;
					...
					sb->s_blocksize = PAGE_SIZE; /* tmpfs 的 block 大小为 PAGE_SIZE */
					...
					sb->s_op = &shmem_ops;
					...
					/* 建立 tmpfs 的 根目录 */
					inode = shmem_get_inode(sb, NULL, S_IFDIR | sbinfo->mode, 0, VM_NORESERVE); /* 创建 tmpfs 根目录的 inode */
						struct inode *inode;
						...
						inode = new_inode(sb);
						if (inode) {
							inode->i_ino = get_next_ino(); /* 分配 inode 编号 */
							...
							inode->i_atime = inode->i_mtime = inode->i_ctime = current_time(inode);
							...
							switch (mode & S_IFMT) {
							...
							case S_IFDIR:
								inc_nlink(inode);
								inode->i_size = 2 * BOGO_DIRENT_SIZE;
								/* 设置 inode 接口 */
								inode->i_op = &shmem_dir_inode_operations;
								inode->i_fop = &simple_dir_operations;
								break;
							...
							}
						} else 
							...
						
					sb->s_root = d_make_root(inode); /* 创建 tmpfs 根目录对象 */
					...
					return 0;
		}
#else /* 禁用了 CONFIG_TMPFS, devtmpfs 基于 ramfs 实现 */
	return mount_single(fs_type, flags, data, ramfs_fill_super); /* 感兴趣的读者自行分析 */
#endif

从上面的分析看到,devtmpfs 基于 tmpfs (CONFIG_TMPFS=y)ramfs (CONFIG_TMPFS=n) 实现。devtmpfs 的挂载过程,我们这里重点关注其 inode 的操作接口 shmem_dir_inode_operations (CONFIG_TMPFS=y 的情形) 的 .mknod 回调,它在 devtmpfs 创建设备节点时被 vfs_mknod() 回调,具体细节见后面分析。

static const struct inode_operations shmem_dir_inode_operations = {
#ifdef CONFIG_TMPFS
	...
	.mknod  = shmem_mknod,
	...
#endif
	...
};

用户空间通过 ps 命令可以查看到 kdevtmpfs 线程:

root@qemu-ubuntu:~# ps -ef | grep kdevtmpfs | grep -v grep
root        32     2  0 07:58 ?        00:00:00 [kdevtmpfs]

3.1.2 通过 devtmpfs 创建设备节点

3.1.2.1 发出设备创建请求
/* 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;
}
3.1.2.2 处理设备创建请求
/* 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);
	...

	/* 调用具体文件系统的 mknod 接口,如 tmpfs 的 shmem_mknod() */
	err = vfs_mknod(d_inode(path.dentry), dentry, mode, dev->devt);
		...
		error = dir->i_op->mknod(dir, dentry, mode, dev);
			shmem_mknod() /* mm/shmem.c */
				inode = shmem_get_inode(dir->i_sb, dir, mode, dev, VM_NORESERVE);
					struct inode *inode;
					...
					inode = new_inode(sb);
					...
					if (inode) {
						inode->i_ino = get_next_ino(); /* 分配 inode 编号 */
						...
						switch (mode & S_IFMT) {
						default: /* 字符类、块类、FIFO、socket */
							inode->i_op = &shmem_special_inode_operations;
							init_special_inode(inode, mode, dev);
								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)) /* FIFO类设备 默认接口 设置 */
									inode->i_fop = &pipefifo_fops;
								else if (S_ISSOCK(mode))
									; /* leave it no_open_fops */
								else
									...
							break;
						}
					}
				...
		if (!error)
  			fsnotify_create(dir, dentry);
		return error;
	...
	return err;
}
3.1.2.3 通知用户态设备事件监听程序:设备对象添加
/* 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(".");
}

3.1.3 通过 devtmpfs 删除设备节点

3.1.3.1 发出设备移除请求
/* 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);
	}
	...
}
3.1.3.2 处理设备移除请求
/* 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;
}
3.1.3.3 通知用户态设备事件监听程序:设备对象移除
/* 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 ,在监听到添加移除事件后,按照配置的规则,做 移除设备节点符号链接 等动作。

3.2 通过系统调用 sys_mknod()/sys_unlink() 创建移除设备节点

并非所有的设备节点都经由、或必须经由 driver core 创建移除,也可以通过系统调用 sys_mknod()/sys_unlink() 来完成。

3.2.1 通过系统调用 sys_mknod() 创建设备节点

/* 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() */
	...
}

3.2.2 通过系统调用 sys_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(), ... */
	...
}

4. 参考资料

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

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值