本文主要是介绍Linux内核有什么之块设备驱动有什么第三回 —— 邂逅的三个文件系统之一:devtmpfs,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
接前一篇文章:Linux内核有什么之块设备驱动有什么第二回 —— 块设备驱动初始化流程 vs 字符设备驱动初始化流程
本文内容参考:
34 | 块设备(上):如何建立代理商销售模式?-趣谈Linux操作系统-极客时间
Linux内核——块设备总结_linux do_open-CSDN博客
【Linux驱动】块设备驱动(一)—— 注册块设备_创建块设备-CSDN博客
【linux kernel】devtmpfs文件系统分析
特此致谢!
上一回借着与字符设备初始化流程的对比,讲解了块设备初始化的一般流程。本回开始对于此流程以及整个块设备驱动的细节进行深入解析。
和字符设备一样,块设备也是mknod创建设备结点、打开设备,以及读写设备流程。但与字符设备流程不同的是,块设备的流程多了文件系统及底层硬盘设备的读写。
块设备一般会被格式化为文件系统,而且还不止一个,而是一共三个,这就意味着有三套dentry和inode。因此,这是块设备的一个不好理解、容易搞晕的地方,同时也是块设备中的一个重点。也就是说,这是一个重点暨难点。
三个文件系统都是哪三个?分别是:(1)devtmpfs文件系统;(2)挂载(mount)时选择的具体文件系统(ext2/3/4、XFS、F2FS等);(3)bdev伪文件系统。由于挂载时选择的文件系统可能各种各样,因此严格来说,并不是三个文件系统,而是三类文件系统。接下来,跟随块设备的操作步骤一个一个来看。
和字符设备一样,块设备的mknod还是会在/dev/下创建设备结点。例如,笔者电脑Ubuntu虚拟机下/dev/下的各设备结点如下:
$ ls /dev/
autofs disk hugepages loop14 loop-control ptmx sg2 tty10 tty21 tty32 tty43 tty54 tty8 ttyS17 ttyS28 uhid vcsa vcsu5
block dma_heap hwrng loop15 mapper pts shm tty11 tty22 tty33 tty44 tty55 tty9 ttyS18 ttyS29 uinput vcsa1 vcsu6
bsg dmmidi initctl loop16 mcelog random snapshot tty12 tty23 tty34 tty45 tty56 ttyprintk ttyS19 ttyS3 urandom vcsa2 vfio
btrfs-control dri input loop17 mem rfkill snd tty13 tty24 tty35 tty46 tty57 ttyS0 ttyS2 ttyS30 userfaultfd vcsa3 vga_arbiter
bus ecryptfs kmsg loop2 midi rtc sr0 tty14 tty25 tty36 tty47 tty58 ttyS1 ttyS20 ttyS31 userio vcsa4 vhci
cdrom fb0 log loop3 mqueue rtc0 sr1 tty15 tty26 tty37 tty48 tty59 ttyS10 ttyS21 ttyS4 vcs vcsa5 vhost-net
char fd loop0 loop4 net sda stderr tty16 tty27 tty38 tty49 tty6 ttyS11 ttyS22 ttyS5 vcs1 vcsa6 vhost-vsock
console fd0 loop1 loop5 null sda1 stdin tty17 tty28 tty39 tty5 tty60 ttyS12 ttyS23 ttyS6 vcs2 vcsu vmci
core full loop10 loop6 nvram sda2 stdout tty18 tty29 tty4 tty50 tty61 ttyS13 ttyS24 ttyS7 vcs3 vcsu1 vsock
cpu fuse loop11 loop7 port sda3 tty tty19 tty3 tty40 tty51 tty62 ttyS14 ttyS25 ttyS8 vcs4 vcsu2 zero
cpu_dma_latency hidraw0 loop12 loop8 ppp sg0 tty0 tty2 tty30 tty41 tty52 tty63 ttyS15 ttyS26 ttyS9 vcs5 vcsu3 zfs
cuse hpet loop13 loop9 psaux sg1 tty1 tty20 tty31 tty42 tty53 tty7 ttyS16 ttyS27 udmabuf vcs6 vcsu4
其中,就既包括字符设备,又包括块设备。比如其中的ttyS0为字符设备(第一个字母为c,代表char),而/dev/sda1为块设备(第一个字母为b,代表block),如下所示:
$ ls /dev/ttyS0 -l
crw-rw---- 1 root dialout 4, 64 1月 26 21:34 /dev/ttyS0$ ls /dev/sda1 -l
brw-rw---- 1 root disk 8, 1 1月 26 21:34 /dev/sda1
这一点经常玩Linux尤其是驱动的想必不陌生。
/dev路径下是/devtmpfs文件系统,如下所示:
$ mount | grep devtmpfs
udev on /dev type devtmpfs (rw,nosuid,relatime,size=2972608k,nr_inodes=743152,mode=755,inode64)
这就是块设备邂逅的第一个文件系统。
这里先来了解一下devtmpfs的相关知识。
devtmpfs简介
devtmpfs文件系统与传统的tmpfs文件系统类似,都是基于内存的文件系统。它将设备结点以文件的形式表示,并提供了对设备结点的访问和管理。通过devtmpfs文件系统,内核可以自动创建和删除设备节点,而无需依赖外部工具。
devtmpfs文件系统的主要作用是为了方便设备的管理和访问。在Linux系统中,每个设备都对应一个设备结点,通过设备结点可以与设备进行通信和操作。devtmpfs文件系统可以自动创建和管理这些设备结点,使得设备的管理更加方便和高效。
devtmpfs的作用是在Linux内核启动早期建立一个初步的/dev,令一般启动程序不用等待udev(udev 是Linux Kernel 2.6系列的设备管理器。它主要的功能是管理/dev目录底下的设备结点),从而缩短GNU/Linux的开机时间。
devtmpfs允许内核在初始化时,即在驱动程序核心设备注册之前创建tmpfs。每个主/次设备都将在这个tmpfs实例中创建它的一个设备结点。当rootfs被内核挂载后,被填充的tmpfs被挂载在/dev路径下。在initramfs中,执行/sbin/init之前,可以将它移动到指定的根文件系统中。
devtmpfs初始化
在Linux内核中,对devtmpfs文件系统的初始化由devtmpfs_init函数完成,该函数会创建devtmpfs文件系统实例,然后各个驱动模块核心会将设备结点添加到该文件系统中。devtmpfs_init函数在drivers/base/devtmpfs.c中,代码如下:
/** Create devtmpfs instance, driver-core devices will add their device* nodes here.*/
int __init devtmpfs_init(void)
{char opts[] = "mode=0755";int err;mnt = vfs_kern_mount(&internal_fs_type, 0, "devtmpfs", opts);if (IS_ERR(mnt)) {pr_err("unable to create devtmpfs %ld\n", PTR_ERR(mnt));return PTR_ERR(mnt);}err = register_filesystem(&dev_fs_type);if (err) {pr_err("unable to register devtmpfs type %d\n", err);return err;}thread = kthread_run(devtmpfsd, &err, "kdevtmpfs");if (!IS_ERR(thread)) {wait_for_completion(&setup_done);} else {err = PTR_ERR(thread);thread = NULL;}if (err) {pr_err("unable to create devtmpfs %d\n", err);unregister_filesystem(&dev_fs_type);thread = NULL;return err;}pr_info("initialized\n");return 0;
}
从devtmpfs_init函数代码可见,与几乎所有的文件系统注册一样,在函数中都会调用register_filesystem函数向Linux内核注册文件系统。devtmpfs文件系统类型描述符dev_fs_type定义如下(同文件中):
static struct file_system_type dev_fs_type = {.name = "devtmpfs",.mount = public_dev_mount,
};
对于devtmpfs文件系统的知识补强就到这里。回到块设备驱动中来。
上边提到,mknod会为每一个块设备文件创建一个设备结点,也就意味着为其分配一个特殊的inode(索引节点),这一点也和字符设备一样。这一步的具体实现函数为init_special_inode,在fs/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)) {if (IS_ENABLED(CONFIG_BLOCK))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 */elseprintk(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函数用于初始化一个特殊类型的inode结构。inode(索引结点)是Linux文件系统中的一个重要概念,用于表示文件或目录的元数据信息。init_special_inode函数通常在文件系统实现中被调用,用于创建特殊类型的文件或设备结点。例如,在ext4文件系统中,可以通过调用init_special_inode函数来创建字符设备或块设备结点。
在Linux内核源码下搜索“init_special_inode”关键字,会发现各个文件系统中都调用了它。
init_special_inode函数中为字符设备分配特殊的inode,走的是S_ISCHR分支;而为块设备分配特殊inode,则走的是S_ISBLK分支。为了方便,再次贴一下init_special_inode函数的代码:
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)) {if (IS_ENABLED(CONFIG_BLOCK))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 */elseprintk(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);
这里要注意,inode中的i_rdev即inode->i_rdev被设置成了块设备的设备号rdev,其类型为dev_t,这一点后文书还会用到,这里埋一个伏笔。dev_t的定义在include/linux/types.h中:
typedef __kernel_dev_t dev_t;
__kernel_dev_t的定义也在同文件中,如下:
typedef u32 __kernel_dev_t;
对于块设备来说,特殊inode的默认file_operations即inode->i_fop是def_blk_fops(字符设备是def_chr_fops)。
def_blk_fops的定义和初始化在block/fops.c中,如下:
const struct file_operations def_blk_fops = {.open = blkdev_open,.release = blkdev_release,.llseek = blkdev_llseek,.read_iter = blkdev_read_iter,.write_iter = blkdev_write_iter,.iopoll = iocb_bio_iopoll,.mmap = blkdev_mmap,.fsync = blkdev_fsync,.unlocked_ioctl = blkdev_ioctl,
#ifdef CONFIG_COMPAT.compat_ioctl = compat_blkdev_ioctl,
#endif.splice_read = filemap_splice_read,.splice_write = iter_file_splice_write,.fallocate = blkdev_fallocate,
};
与字符设备一样,块设备也有打开、关闭、读写函数(指针)。不过与字符设备不同,虽然块设备有这些函数,但常规操作不会这样“raw”操作,而是会将(这个)块设备文件mount到一个文件夹下面。所谓“raw”操作,就是指直接打开/dev/下的块设备结点进行操作,而不是通过其挂载点进行相关操作。笔者的职业生涯中,这样干的情况见过,但确实不多,一般都是文件系统解决不了、无法进行块设备的相关操作需求时才这么干。
简单看一下open函数指针所指向的clkdev_open函数,其也在block/fops.c中,代码如下:
static int blkdev_open(struct inode *inode, struct file *filp)
{struct bdev_handle *handle;blk_mode_t mode;/** Preserve backwards compatibility and allow large file access* even if userspace doesn't ask for it explicitly. Some mkfs* binary needs it. We might want to drop this workaround* during an unstable branch.*/filp->f_flags |= O_LARGEFILE;filp->f_mode |= FMODE_BUF_RASYNC | FMODE_CAN_ODIRECT;mode = file_to_blk_mode(filp);handle = bdev_open_by_dev(inode->i_rdev, mode,mode & BLK_OPEN_EXCL ? filp : NULL, NULL);if (IS_ERR(handle))return PTR_ERR(handle);if (bdev_nowait(handle->bdev))filp->f_mode |= FMODE_NOWAIT;filp->f_mapping = handle->bdev->bd_inode->i_mapping;filp->f_wb_err = filemap_sample_wb_err(filp->f_mapping);filp->private_data = handle;return 0;
}
至此,块设备驱动邂逅的第一个文件系统devtmpfs就讲解到这里了,下回讲其邂逅的第二个文件系统。
这篇关于Linux内核有什么之块设备驱动有什么第三回 —— 邂逅的三个文件系统之一:devtmpfs的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!