前文中我们已经分析了文件系统,而文件系统的精髓所在是让用户可以通过文件描述符来对指定的inode
进行一系列的操作。
本章开始学习伪文件系统,伪文件系统和普通文件系统的区别在于,其inode
对用户不可访问,即仅在内核态可见,从用户层的视角来看该文件系统并不存在。伪文件系统的作用是对一些操作系统中的元素进行封装,和普通的文件统一接口,如块设备bdevfs
,管道文件pipefs
,套接字socketfs
等。通过这种方式的统一封装,才实现了Linux一切皆文件的思想。如下图中红色的都应该算是伪文件系统,本章就学习其中的procfs/sysfs/pipefs。
1 proc文件系统
1.1 数据结构
proc文件系统由proc_dir_entry结构体实例组成,内核各子系统可通过接口函数向proc文件系统添加目录和文件。proc_dir_entry结构体定义在fs/proc/internal.h头文件
在VFS中proc文件系统节点由proc_inode结构体表示,其定义在fs/proc/internal.h
1.2 初始化
内核在启动函数start_kernel()中调用proc_root_init()函数完成proc文件系统的初始化工作,函数定义在/fs/proc/root.c文件内,代码如下:
proc_root_init()函数的主要工作是创建proc_inode结构体slab缓存,注册proc_fs_type文件系统类型,为各子系统添加目录项和文件。
proc文件系统需要挂载到根文件系统(通常是/proc挂载点),才能对用户进程可见。那么如何对proc文件系统的挂载,以及用户进程对proc文件系统中文件的操作。
1.3 挂载文件系统
proc文件系统类型挂载函数proc_mount()定义如下(fs/proc/root.c)
其与前面介绍过的文件系统挂载类似,主要是通过proc_fill_super填充超级块实例,创建设置denry、proc_inode实例等,主要来看看proc_fill_super函数实现(fs/proc/inode.c)
内核在/fs/proc/root.c文件内静态创建了proc文件系统根目录项的proc_dir_entry实例proc_root:
proc_fill_super()函数首先对super_block实例进行初始化,设置其超级块操作结构实例为 proc_sops ,然后调用proc_get_inode()函数创建proc_inode实例,并建立其与proc_root实例的关联,创建文件系统根目录项dentry实例,并关联proc_inode.vfs_inode成员(inode实例),最后在根目录下创建self和thread-self目录项。所以其数据结构如下图所示
proc文件系统需要由用户挂载,才能对用户可见。通常在操作系统启动脚本中会将proc文件系统挂载到/proc目录下。
- 挂载操作将创建proc文件系统超级块super_block实例,文件系统根目录项dentry实例,以及proc_inode实例。proc_inode实例中包含节点inode结构体成员,其i_op、i_fop成员分别指向proc_root实例中proc_iops、proc_fops指向的实例。
- 对于根节点,proc_iops、proc_fops指向的实例是专用的,它们分别为proc_root_inode_operations、proc_root_operations。
- 用户打开proc文件系统中文件时,将为每个路径分量(目录项)创建dentry和proc_inode实例,并建立proc_inode实例与proc_dir_entry实例之间的关联。
1.4 添加普通目录项
向proc文件系统添加普通目录项的接口函数为proc_mkdir_data(),定义如下(fs/proc/generic.c)
设置普通目录文件的节点操作结构和文件操作结构实例为proc_dir_inode_operations和proc_dir_operations,这两个实例都是通用的实例,最后调用proc_register()函数注册proc_dir_entry实例,主要是将其添加到父目录项管理的红黑树中。
向proc文件系统添加文件的操作与添加普通目录项的操作类似,主要区别是添加文件时,需要定义文件操作结构file_operations实例,传递给proc_dir_entry实例。
对于普通目录项,proc_inode内嵌inode实例i_op和i_fop成员赋值为proc_dir_entry实例proc_iops、proc_fops成员值。对于文件目录项,proc_inode内嵌inode实例i_op赋值为proc_dir_entry实例proc_iops成员值,而i_fop成员指向 proc_reg_file_ops 实例,
1.6 打开文件
proc文件系统中路径搜索都是从其根目录项开始的,根目录项与普通目录项关联inode_operations实例不同,搜索函数也不同,如下图所示。
根目录项关联inode_operations实例定义如下(fs/proc/root.c):
普通目录项关联inode_operations实例定义如下(fs/proc/generic.c):
在打开文件的操作中将调用proc_get_inode()函数为proc_dir_entry实例创建proc_inode实例,如果实例代表的是普通文件,则inode实例关联的文件操作结构实例设为proc_reg_file_ops,定义如下:
打开文件后,需要读写文件,其也就可以调用该接口的read/write接口,其整个流程如下
2. sysfs
sys文件系统与proc文件系统一样,通过内核数据结构实例,组织成文件系统,它由kernfs_node结构体实例构成。
kernfs文件系统并不是一个完整的文件系统,它只是提供构建文件系统的基础组件及相关的接口函数,使用kernfs文件系统的子系统需要定义并注册文件系统类型file_system_type实例。其过程与procfs文件基本类似就不单独介绍。
3 pipefs
管道(pipe)和命名管道是进程间通信的机制,用于进程间的单向数据传输。管道和命名管道的两端分别是写进程和读进程,本质上是一种特殊的文件,文件的内容保存在一个内存的缓冲区中(FIFO)。
管道没有文件名称,由内核管理,只能用于同源的进程间(fork()出来的进程间)通信,对其它非同源的进程不可见。命名管道与普通文件一样具有文件名称,文件保存在具体文件系统中,导出到内核根文件系统,对用户可见,可用于任意进程之间的通信。
管道/命名管道包含一个缓存区,缓存区可认为有一个进口和一个出口,写进程从进口写入数据,读进程从出口读取数据,数据在缓存区中按写入时间先后顺序排列,先进的数据先读出,只能按顺序读取,不能任意读取。
父进程创建子进程后,表示管道的两个文件描述符将传递给子进程。如果父进程要通过管道向子进程传递数据,则关闭父进程读端文件描述符和子进程写端文件描述符。父进程则可以向管道写入数据,子进程可从管道读取数据。
pipefs伪文件系统类型pipe_fs_type实例定义如下(/fs/pipe.c)
pipefs伪文件系统初始化函数init_pipe_fs()中注册了文件系统类型,并执行了内核挂载,函数定义如下:
其pipefs_mount比较简单,主要工作是创建挂载mount结构体实例,调用文件系统类型中定义的mount()函数,创建超级块super_block、根目录项dentry和inode结构体实例,并建立以上数据结构实例之间的关系,执行结果如下图所示。
我们来学习下如何使用的管道,用户进程创建管道的系统调用为pipe和pipe2,实现如下
pipe2()系统调用实现函数定义如下:
pipe()/pipe2()系统调用将返回2个文件描述符,参数fildes指向的数组用于存放返回的文件描述符,fildes[0]表示读取端文件描述符(只读形式),fildes[1]表示写入端文件描述符(只写形式)。
系统调用内通过__do_pipe_flags()函数创建表示管道的dentry和inode实例,分配并初始化表示管道的pipe_inode_info结构体实例,分配2个file实例,2个文件描述符,2个file实例关联到同一个inode实例,file和inode关联的文件操作结构实例为 pipefifo_fops 。