# 8.5 Linux 文件系统与设备文件系统 ## 设备驱动与文件系统的关系 Linux 系统遵循一切皆文件的理念,在用户态看来,所有对设备和真实文件等的操作,都是文件操作。Linux 通过增加 VFS 层来实现该机制。应用程序、VFS 层、文件和设备驱动等的关系如下图: ![文件系统与设备驱动](./imgs/8.5_Linux_文件系统与设备文件系统/001.drawio.png) 应用程序和 VFS 之间的接口是系统调用,而 VFS 与磁盘文件系统以及普通设备之间的接口是 file_operations 结构体成员函数,这个结构体包含对文件进行打开、关闭、读写、控制的一系列成员函数。所有支持用户态访问控制的设备驱动,都需要实现该接口。内核中定义 file_operations 结构如下: ```cpp struct file_operations { struct module *owner; loff_t(*llseek) (struct file *, loff_t, int); ssize_t(*read) (struct file *, char __user *, size_t, loff_t *); ssize_t(*aio_read) (struct kiocb *, char __user *, size_t, loff_t); ssize_t(*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t(*aio_write) (struct kiocb *, const char __user *, size_t, loff_t); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, struct dentry *, int datasync); int (*aio_fsync) (struct kiocb *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t(*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t(*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t(*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void __user *); ssize_t(*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area) (struct file *, unsigned long, unsigned long, unsigned long, unsigned long); }; ``` 而对于应用程序,只需要关心以下接口即可: ```cpp // 创建文件 int creat(const char *filename, mode_t mode); // 打开文件 int open(const char *pathname, int flags, mode_t mode); // 读文件 int read(int fd, const void *buf, size_t length); // 写文件 int write(int fd, const void *buf, size_t length); // 文件定位 int lseek(int fd, offset_t offset, int whence); // 关闭文件 int close(int fd); ``` ## devfs 文件系统 devfs(设备文件系统)是比较老的设备文件系统,最初由 Linux 2.4 内核引入的,引入时被许多工程师给予了高度评价,它的出现使得设备驱动程序能自主地管理它自己的设备文件。具体来说,devfs 具有如下优点。 * 可以通过程序在设备初始化时在/dev 目录下创建设备文件,卸载设备时将它删除。 * 设备驱动程序可以指定设备名、所有者和权限位,用户空间程序仍可以修改所有者和权限位。 * 不再需要为设备驱动程序分配主设备号以及处理次设备号,在程序中可以直接给 register_chrdev() 传递 0 主设备号以动态获得可用的主设备号,并在 devfs_register()中指定次设备号。 ## udev 设备文件系统 尽管 devfs 有这样和那样的优点,但是,在 Linux 2.6 内核中,devfs 被认为是过时的方法,并最终被抛弃,udev 取代了它。Linux VFS 内核维护者 Al Viro 指出了 udev 取代 devfs 的几点原因: * devfs 所做的工作被确信可以在用户态来完成。 * 一些 bug 相当长的时间内未被修复。 * devfs 的维护者和作者停止了对代码的维护工作。 udev 完全在用户态工作,利用设备加入或移除时内核所发送的热插拔事件(hotplug event)来工作。在热插拔时,设备的详细信息会由内核输出到位于/sys 的 sysfs 文件系统。udev 的设备命名策略、权限控制和事件处理都是在用户态下完成的,它利用 sysfs 中的信息来进行创建设备文件节点等工作。 由于 udev 根据系统中硬件设备的状态动态更新设备文件,进行设备文件的创建和删除等,因此,在使用 udev 后,/dev 目录下就会只包含系统中真正存在的设备了。 devfs 与 udev 的另一个显著区别在于:采用 devfs,当一个并不存在的/dev 节点被打开的时候,devfs 能自动加载对应的驱动,而 udev 则不能。这是因为 udev 的设计者认为 Linux 应该在设备被发现的时候加载驱动模块,而不是当它被访问的时候。 udev 的设计者认为 devfs 所提供的打开/dev 节点时自动加载驱动的功能对于一个配置正确的计算机是多余的。系统中所有的设备都应该产生热插拔事件并加载恰当的驱动,而 udev 能注意到这点并且为它创建对应的设备节点。 ### sysfs 文件系统与 Linux 设备模型 Linux 2.6 内核引入了 sysfs 文件系统,sysfs 被看成是与 proc、devfs 和 devpty 同类别的文件系统,该文件系统是一个虚拟的文件系统,它可以产生一个包括所有系统硬件的层级视图,与提供进程和状态信息的 proc 文件系统十分类似。 sysfs 把连接在系统上的设备和总线组织成为一个分级的文件,它们可以由用户空间存取,向用户空间导出内核数据结构以及它们的属性。sysfs 的一个目的就是展示设备驱动模型中各组件的层次关系,其顶级目录包括 block、device、bus、drivers、class、power 和 firmware。 block 目录包含所有的块设备,devices 目录包含系统所有的设备并根据设备挂接的总线类型组织成层次结构,bus 目录包含系统中所有的总线类型,drivers 目录包括内核中所有已注册的设备驱动程序,class 目录包含系统中的设备类型(如网卡设备、声卡设备、输入设备等)。在 /sys 目录运行 tree 会得到一个相当长的树型目录。 ```tree . ├── block │   ├── loop0 -> ../devices/virtual/block/loop0 │   ├── loop1 -> ../devices/virtual/block/loop1 │   ├── loop2 -> ../devices/virtual/block/loop2 │   ├── loop3 -> ../devices/virtual/block/loop3 │   ├── loop4 -> ../devices/virtual/block/loop4 │   ├── loop5 -> ../devices/virtual/block/loop5 │   ├── loop6 -> ../devices/virtual/block/loop6 │   ├── loop7 -> ../devices/virtual/block/loop7 │   ├── loop8 -> ../devices/virtual/block/loop8 │   ├── loop9 -> ../devices/virtual/block/loop9 │   ├── sda -> ../devices/pci0000:00/0000:00:0d.0/ata3/host2/target2:0:0/2:0:0:0/block/sda │   └── sr0 -> ../devices/pci0000:00/0000:00:01.1/ata2/host1/target1:0:0/1:0:0:0/block/sr0 ├── bus │   ├── ac97 │   │   ├── devices │   │   │   └── 0-0:AD1980 -> ../../../devices/pci0000:00/0000:00:05.0/0-0:AD1980 │   │   ├── drivers │   │   ├── drivers_autoprobe │   │   ├── drivers_probe │   │   └── uevent │   ├── acpi │   │   ├── devices │   │   │   ├── ACPI0003:00 -> ../../../devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/ACPI0003:00 │   │   │   ├── APP0001:00 -> ../../../devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:00/APP0001:00 │   │   │   ├── device:00 -> ../../../devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:00 │   │   │   ├── device:01 -> ../../../devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01 │   │   │   ├── device:02 -> ../../../devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/device:02 │   │   │   ├── device:03 -> ../../../devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/LNXVIDEO:00/device:03 │   │   │   ├── device:04 -> ../../../devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:04 │   │   │   ├── LNXCPU:00 -> ../../../devices/LNXSYSTM:00/LNXCPU:00 │   │   │   ├── LNXCPU:01 -> ../../../devices/LNXSYSTM:00/LNXCPU:01 │   │   │   ├── LNXCPU:02 -> ../../../devices/LNXSYSTM:00/LNXCPU:02 │   │   │   ├── LNXCPU:03 -> ../../../devices/LNXSYSTM:00/LNXCPU:03 ``` 在 /sys/bus 的 pci 等子目录下,又会在分出 drivers 和 devices 目录,而 devices 目录中的文件是对 /sys/devices 目录中文件的符号链接。同样地,/sys/class 目录下包含许多对 /sys/devices 下文件的链接。这与设备、驱动、总线和类的现实状况是直接对应的,也正符合 Linux 2.6 内核的设备模型。 ![sysfs 文件系统下的文件相互交织](./imgs/8.5_Linux_文件系统与设备文件系统/002.png) ## 用户态 GPIO 示例 下面以 GPIO 操作为例,演示设备文件系统的使用方法,该示例为 c++ 工程,包含一个 cmake 文件,一个 Gpio 类和一个 main.cpp 文件,其中 Gpio 类封装了 GPIO 端口导出和读写机制。 main.cpp 内容如下: ```cpp /** * @file: main.cpp */ #include #include "Gpio.h" using namespace std; const char* Gpio::GpioPortTbl[] = { "372", }; int main() { Gpio* gpio = new Gpio(0, Gpio::EC_ATTR_R); Gpio::EU_PORT_LEVEL rlevel = gpio->ReadPort(); cout << "Read GPIO Port Level: " << rlevel << endl; return 0; } ``` Gpio.h 内容如下: ```cpp /** * @file: Gpio.h */ #ifndef GPIO_H #define GPIO_H #include #define POLL_TIMEOUT (1 * 1000) using namespace std; class Gpio { public: enum EU_PORT_LEVEL { EC_PORT_N = 0, EC_PORT_L = '0', EC_PORT_H = '1' }; enum EU_PORT_ATTR { EC_ATTR_R, // Readable EC_ATTR_W // Writeable }; static const char* GpioPortTbl[]; Gpio(int id=0, EU_PORT_ATTR attr=EC_ATTR_R); EU_PORT_ATTR GetAttr(void) const { return PortAttr; } const char* GetNo(void) const { return GpioPortTbl[PortID]; } int GetFd(void) const { return Fd; } bool WritePort(EU_PORT_LEVEL val); EU_PORT_LEVEL ReadPort(void); protected: const int PortID; const EU_PORT_ATTR PortAttr; int Fd; char LastStu; char CurtStu; private: void Export(void); }; #endif // GPIO_H ``` Gpio.cpp 内容如下: ```cpp /** * @file: Gpio.cpp */ #include "Gpio.h" #include #include #include #include Gpio::Gpio(int id, EU_PORT_ATTR attr): PortID(id), PortAttr(attr) { Fd = -1; LastStu = EC_PORT_L; CurtStu = EC_PORT_L; Export(); } bool Gpio::WritePort(EU_PORT_LEVEL val) { int ret = -1; char wVal = (char)val; if(0>=Fd) return false; if(EC_ATTR_W!=PortAttr) return false; lseek(Fd, 0, SEEK_SET); ret = write(Fd, &wVal, 1); if(0>ret) return false; return true; } Gpio::EU_PORT_LEVEL Gpio::ReadPort(void) { int ret = -1; if(0>=Fd) { cout << "Read Gpio" << GpioPortTbl[PortID] << "Error Fd=" << Fd; return EC_PORT_N; } lseek(Fd, 0, SEEK_SET); ret = read(Fd, &CurtStu, 1); cout << "Read Gpio" << GpioPortTbl[PortID] << " Ret=" << ret << " Val=" << CurtStu << endl; switch(CurtStu) { case EC_PORT_L: return EC_PORT_L; case EC_PORT_H: return EC_PORT_H; default: return EC_PORT_N; } } void Gpio::Export(void) { // Export. int fd = open("/sys/class/gpio/export", O_WRONLY); if(fd<0) return; write(fd, GpioPortTbl[PortID], strlen(GpioPortTbl[PortID])); close(fd); // Set direction. string dir = string("/sys/class/gpio/gpio")+ string(GpioPortTbl[PortID])+ string("/direction"); fd = open(dir.c_str(), O_WRONLY); if(fd<0) return; if(PortAttr==EC_ATTR_R) write(fd, "in", 2); else write(fd, "out", 3); close(fd); // Set edge. if(PortAttr==EC_ATTR_R) { string edge = string("/sys/class/gpio/gpio")+ string(GpioPortTbl[PortID])+ string("/edge"); fd = open(edge.c_str(), O_WRONLY); if(fd<0) return; write(fd, "both", 4); close(fd); } string name = string("/sys/class/gpio/gpio")+ string(GpioPortTbl[PortID])+ string("/value"); Fd = open(name.c_str(), (PortAttr==EC_ATTR_R) ? O_RDONLY|O_NONBLOCK : O_WRONLY|O_NONBLOCK); cout << "Open:" << name << "Fd=" << Fd << endl; } ``` CMakeLists.txt 如下: ```cmake cmake_minimum_required(VERSION 3.5) project(c08_05 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_executable(c08_05 main.cpp Gpio.h Gpio.cpp) ```