设备文件节点的生成
-- |
设备文件节点的生成
在Linux系统下,设备文件是种特殊的文件类型,其存在的主要意义是沟通用户空间程序和内核空间驱动程序。换句话说,用户空间的应用程序要想使用驱动程序提供的服务,需要经过设备文件来达成。当然,如果你的驱动程序只是为内核中的其他模块提供服务,则没有必要生成对应的设备文件。
按照通用的规则,Linux系统所有的设备文件都位于/dev目录下。/dev目录在Linux系统中算是一个比较特殊的目录,在Linux系统早期还不支持动态生成设备节点时,/dev目录就是挂载的根文件系统下的/dev,对这个目录下所有文件的操作使用的是根文件系统提供的接口。比如,如果Linux系统挂载的根文件系统是ext3,那么对/dev目录下所有目录/文件的操作都将使用ext3文件系统的接口。随着后来Linux内核的演进,开始支持动态设备节点的生成[1],使得系统在启动过程中会自动生成各个设备节点,这就使得/dev目录不必要作为一个非易失的文件系统的形式存在。因此,当前的Linux内核在挂载完根文件系统之后,会在这个根文件系统的/dev目录上重新挂载一个新的文件系统devtmpfs,后者是个基于系统RAM的文件系统实现。当然,对动态设备节点生成的支持并不意味着一定要将根文件系统中的/dev目录重新挂载到一个新的文件系统上,事实上动态生成设备节点技术的重点并不在文件系统上面。
动态设备节点的特性需要其他相关技术的支持,在后续的章节中会详细描述这些特性。目前先假定设备节点是通过Linux系统下的mknod命令静态创建。为方便叙述,下面用一个具体的例子来描述设备文件产生过程中的一些关键要素,这个例子的任务很简单:在一个ext3类型的根文件系统中的/dev目录下用mknod命令来创建一个新的设备文件节点demodev,对应的驱动程序使用的设备主设备号为2,次设备号是0,命令形式为:
root@LinuxDev:/home/dennis#
mknod /dev/demodev c 2 0
上述命令成功执行后,将会在/dev目录下生成一个名为demodev的字符设备节点。如果用strace工具来跟踪一下上面的命令,会发现如下输出(删去了若干不相关部分):
root@LinuxDev:/home/dennis#
strace mknod /dev/demodev c 2 0
execve("/bin/mknod",
["mknod", "/dev/demodev", "c",
"30","0"], [/* 36 vars */]) = 0
…
mknod("/dev/demodev",
S_IFCHR|0666, makedev(30,0)) = 0
…
可见Linux下的mknod命令最终是通过调用mknod函数来实现的,调用时的重要参数有两个,一是设备文件名("/dev/demodev"),二是设备号(makedev(30,0))。设备文件名主要在用户空间使用(比如用户空间程序调用open函数时),而内核空间则使用inode来表示相应的文件。本书只关注内核空间的操作,对于前面的mknod命令,它将通过系统调用sys_mknod进入内核空间,这个系统调用的原型是:
<include/linux/syscalls.h>
long sys_mknod(const char __user *filename, int mode, unsigned dev);
注意sys_mknod的最后一个参数dev,它是由用户空间的mknod命令构造出的设备号。sys_mknod系统调用将通过/dev目录上挂载的文件系统接口来为/dev/demodev生成一个新的inode[2],设备号将被记录到这个新的inode对象上。
图2-7展示了通过ext3文件系统在/dev目录下生成一个新的设备节点/dev/demodev的主要流程。
图2-7
ext3文件系统mknod的主要流程
完整了解设备节点产生的整个过程需要知晓VFS和特定文件系统的技术细节。然而从驱动程序员的角度来说,没有必要知道文件系统相关的所有细节,只需关注文件系统和驱动程序间是如何建立上关联的就足够了。
sys_mknod首先在根文件系统ext3的根目录“/”下寻找dev目录所对应的inode,图中对应的inode编号为168,ext3文件系统的实现会通过某种映射机制,通过inode编号最终得到该inode在内存中的实际地址(图中由标号1的线段表示)。接下来会通过dev的inode结构中的i_op成员指针所指向的ext3_dir_inode_operations(这是个struct inode_operations类型的指针),来调用该对象中的mknod方法,这将导致ext3_mknod函数被调用。
ext3_mknod函数的主要作用是生成一个新的inode(用来在内核空间表示demodev设备文件节点,demodev设备节点文件与新生成的inode之间的关联在图2-7中由标号5的线段表示)。在ext3_mknod中会调用一个和设备驱动程序关系密切的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)) {
inode->i_fop =
&def_blk_fops;
inode->i_rdev = rdev;
} else if (S_ISFIFO(mode))
inode->i_fop =
&def_fifo_fops;
else if (S_ISSOCK(mode))
inode->i_fop =
&bad_sock_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);
}
这个函数最主要的功能便是为新生成的inode初始化其中的i_fop和i_rdev成员。设备文件节点inode中的i_rdev成员用来表示该inode所对应设备的设备号,通过参数rdev为其赋值。设备号在由sys_mknod发起的整个内核调用链中进行传递,最早来自于用户空间的mknod命令行参数。
i_fop成员的初始化根据是字符设备还是块设备而有不同的赋值。对于字符设备,fop指向def_chr_fops,后者主要定义了一个open操作:
<fs/char_dev.c>
const struct file_operations def_chr_fops = {
.open = chrdev_open,
…
};
相对于字符设备,块设备的def_blk_fops的定义则要有点复杂:
<fs/block_dev.c>
const struct file_operations def_blk_fops = {
.open = blkdev_open,
.release = blkdev_close,
.llseek = block_llseek,
.read = do_sync_read,
.write = do_sync_write,
.aio_read = generic_file_aio_read,
.aio_write = blkdev_aio_write,
.mmap = generic_file_mmap,
.fsync = blkdev_fsync,
.unlocked_ioctl = block_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = compat_blkdev_ioctl,
#endif
.splice_read = generic_file_splice_read,
.splice_write = generic_file_splice_write,
};
关于块设备,将在本书第11章“块设备驱动程序”中详细讨论,这里依然把考察的重点放在字符设备上。字符设备inode中的i_fop指向def_chr_fops。至此,设备节点的所有相关铺垫工作都已经结束,接下来可以看看打开一个设备文件到底意味着什么。
——本段文字节选自《深入Linux设备驱动程序内核机制》
图书详细信息: