05-Linux 文件系统与设备文件

5.1 Linux 文件操作

5.1.1 文件操作系统调用

++用户空间与驱动交互的第一入口是系统调用++ 。++驱动工程师必须清楚每个系统调用最终会落到 **file_operations**的哪个成员++:

系统调用 典型 C 库函数 VFS 层 最终调用的 file_operations
open open()、fopen() do_sys_openat2() .open
read read()、fread() vfs_read() .read 或 .read_iter
write write()、fwrite() vfs_write() .write 或 .write_iter
lseek lseek()、fseek() vfs_llseek() .llseek
ioctl ioctl() do_vfs_ioctl() .unlocked_ioctl(64 位)/ .compat_ioctl(32 位兼容)
mmap mmap() vm_mmap_pgoff() .mmap(7.2.5 中还有新的 .mmap_prepare)
poll/select/epoll poll()、epoll_wait() do_sys_poll() 等 .poll
fsync fsync() vfs_fsync() .fsync
close close()、fclose() filp_close() .flush(每次 close)与 .release(最后一个引用)
fcntl(F_SETOWN/FASYNC) fcntl() do_fcntl() .fasync

关键概念:

  • struct inode 代表"文件本身"(设备号、权限、大小),一个文件只有一份。
  • struct file 代表"一次打开" (偏移量、标志、私有数据),每次 open 一份。
  • file->private_data 是驱动最常用的"把本次打开关联到自己的数据结构"的钩子(第 6 章会用到)。
  • flush vs release :++flush 每次 close() 都调用(因为 dup 出来的多个 fd 各自 close);release 只在最后一个引用消失时调用。不要把资源释放放在 flush 里++。

最小示例:

cpp 复制代码
int fd = open("/dev/globalmem", O_RDWR);
if (fd < 0) { perror("open"); return 1; }

char buf[64] = "hello";
write(fd, buf, 5);                 /* → 驱动 .write */
lseek(fd, 0, SEEK_SET);            /* → 驱动 .llseek */
read(fd, buf, sizeof(buf));        /* → 驱动 .read */
ioctl(fd, MY_CMD, 0);              /* → 驱动 .unlocked_ioctl */
close(fd);                         /* → 驱动 .flush 与 .release */

5.1.2 C 库文件操作

标准 C 库在系统调用之上加了一层用户空间缓冲 (FILE * + setvbuf):

cpp 复制代码
FILE *fp = fopen("/dev/globalmem", "r+");
setvbuf(fp, NULL, _IONBF, 0);      /* 访问设备时建议关闭缓冲,避免"写不进去"的错觉 */
fwrite("abc", 1, 3, fp);
fflush(fp);                        /* 必须:否则数据可能还在用户空间缓冲里 */
fclose(fp);

设备文件场景的经验:

  • 用 stdio 访问设备容易误判:printf/fwrite 之后没有 fflush,驱动根本没收到数据。
  • 用 mmap 访问设备时要按页对齐(offset 与 addr 必须是 sysconf(_SC_PAGESIZE) 的倍数)。
  • 用 O_NONBLOCK、O_SYNC、O_DIRECT 等标志会影响驱动的行为路径(第 8 章)。

5.2 Linux 文件系统

5.2.1 文件系统目录结构

cpp 复制代码
/           根文件系统(rootfs,可以是 initramfs 的 tmpfs)
├── bin, sbin, lib, usr     可执行文件与库
├── dev/                    设备文件(devtmpfs)
│   ├── console, null, zero, tty
│   ├── ttyS0               串口
│   ├── ram0, vda           块设备
│   ├── input/event0        输入设备
│   └── gpiochip0           GPIO 字符设备
├── proc/                   procfs:进程与内核状态
├── sys/                    sysfs:设备模型(/sys/bus, /sys/class, /sys/devices)
│   ├── kernel/config       configfs(挂载后)
│   └── kernel/debug        debugfs(挂载后)
├── etc/                    配置
├── tmp/, run/, mnt/        临时与挂载点
└── lib/modules/            内核模块

启动时的挂载顺序(本手册的 initramfs 脚本):

bash 复制代码
mount -t proc      none /proc
mount -t sysfs     none /sys
mount -t devtmpfs  none /dev
mount -t debugfs   none /sys/kernel/debug
mount -t configfs  none /sys/kernel/config

5.2.2 文件系统与设备驱动

关键认知:设备文件不是"文件系统上的普通文件",它是"设备号的容器"。

因此:

  • 设备文件可以放在任何文件系统上(devtmpfs、tmpfs、甚至磁盘上的 ext4)。
  • ++同一个设备可以有多个设备节点++(不同路径、同样 major/minor,指向同一驱动)。
  • ++删除设备节点不影响驱动 ,驱动仍在;反之驱动卸载后设备节点可能仍在(此时 open 返回 ENODEV)++。
  • ++主设备号标识驱动,次设备号标识实例++。
bash 复制代码
# 观察设备号
ls -l /dev/null /dev/zero /dev/ttyS0

# 手工创建设备节点(等价于 udev 做的事)
mknod /tmp/mynull c 1 3        # 1:3 就是 /dev/null
cat /proc/devices              # 查看已注册的主设备号(字符/块分开列)
ls -l /sys/dev/char/1:3/       # 反查该设备号对应的 sysfs 设备

5.3 devfs(历史,已被删除)

原书详细分析了 devfs 的机制与其缺陷。在 7.2.5 中 devfs 已经完全不存在,但理解它的历史很重要,因为它是"内核自动管理设备节点"的第一代方案。

对比项 devfs(已删除) devtmpfs + udev(现代)
出现时间 2.3 时代 devtmpfs 2.6.32;udev 2.6 初
设备节点位置 内核直接创建的虚拟文件系统 内核创建 devtmpfs(设备节点由内核填好)
命名策略 内核决定,难以定制 用户空间(udev 规则)决定
持久化命名 不支持 支持(by-id/by-path/by-uuid)
权限控制 内核 用户空间(规则 + ACL + 组)
热插拔 内核通知 内核 uevent → udev
状态 已删除 标准方案

devfs 失败的核心原因:把策略(怎么命名、给什么权限)放进了内核。现代设计把它交给用户空间,内核只提供事实(uevent)与默认节点(devtmpfs)。

但 devtmpfs 仍然需要内核配合:

bash 复制代码
CONFIG_DEVTMPFS=y           # 内核提供 devtmpfs
CONFIG_DEVTMPFS_MOUNT=y     # 内核自动挂载到 /dev(不是 initramfs 场景时常需要)

设备节点从哪来?驱动注册 device_create()(或 cdev_device_add())时,device 模型的 class devnode 回调会生成默认节点名,devtmpfs 据此创建节点。所以:

写字符设备驱动时,正确做法是注册到设备模型(class + device),而不是自己 mknod。

5.4 udev 用户空间设备管理

5.4.1 udev 与 devfs 的区别

维度 devfs udev
运行位置 内核 用户空间守护进程(systemd-udevd)
触发机制 内核内部 内核发 uevent → netlink → udev
命名规则 内核写死 规则文件(/etc/udev/rules.d/*.rules)
权限设置 内核写死 规则中的 OWNER/GROUP/MODE/TAG
可扩展性 差 好(可调用外部程序、设置符号链接、触发脚本)
依赖 无 sysfs(必须已挂载)

5.4.2 sysfs 文件系统与 Linux 设备模型

sysfs 是设备模型在用户空间的投影(Documentation/filesystems/sysfs.rst)。四层结构:

bash 复制代码
/sys/devices/   按物理拓扑组织的"真实"设备树(唯一的真实来源)
/sys/bus/       按总线分类:/sys/bus/i2c/devices/, /sys/bus/platform/drivers/
/sys/class/     按功能分类:/sys/class/net/, /sys/class/gpio/, /sys/class/leds/
/sys/block/     (兼容符号链接到 /sys/class/block/)
/sys/dev/       按设备号索引:/sys/dev/char/1:3, /sys/dev/block/8:0
/sys/module/    按模块分类

同一设备会出现在多个视图里,都是符号链接:

bash 复制代码
ls -l /sys/class/net/eth0            # 指向 ../../devices/.../net/eth0
ls -l /sys/bus/pci/devices/0000:00:03.0
readlink -f /sys/class/net/eth0

驱动如何暴露属性 (现代写法,优先 dev_groups):

cpp 复制代码
static ssize_t counter_show(struct device *dev,
                            struct device_attribute *attr, char *buf)
{
        struct my_dev *priv = dev_get_drvdata(dev);
        return sysfs_emit(buf, "%u\n", priv->counter);   /* 用 sysfs_emit,不要用 sprintf */
}

static ssize_t counter_store(struct device *dev,
                             struct device_attribute *attr,
                             const char *buf, size_t count)
{
        struct my_dev *priv = dev_get_drvdata(dev);
        unsigned int val;

        if (kstrtouint(buf, 0, &val))
                return -EINVAL;
        priv->counter = val;
        return count;
}
static DEVICE_ATTR_RW(counter);

static struct attribute *my_dev_attrs[] = {
        &dev_attr_counter.attr,
        NULL,
};
ATTRIBUTE_GROUPS(my_dev_attrs);

static struct platform_driver my_driver = {
        .driver = {
                .name  = "my-dev",
                .dev_groups = my_dev_groups,   /* 绑定后自动创建属性 */
        },
};

规则:sysfs 属性是"一个值一个文件" ,读写必须是文本、每次一个值、不做复杂解析;不要把 sysfs 当 ioctl 用(Documentation/filesystems/sysfs.rst 有明确说明)。复杂控制请用 ioctl/netlink/configfs/debugfs。

5.4.3 udev 的组成

bash 复制代码
内核 (kobject_uevent)
    |  通过 netlink 广播 uevent(含 ACTION/DEVPATH/SUBSYSTEM/MAJOR/MINOR/...)
    v
udevd (systemd-udevd)
    |  读取 /etc/udev/udev.conf、/usr/lib/udev/rules.d、/etc/udev/rules.d
    |  按规则匹配 → 设置属性 → 创建符号链接 → 设置权限 → 执行 RUN 程序
    v
/dev 下的设备节点与符号链接

观察 uevent(宿主或 guest 有 udev 时):

bash 复制代码
udevadm monitor --kernel --udev          # 实时观察内核与 udev 事件
udevadm info -a -p /sys/class/net/eth0   # 查看设备的全部属性
udevadm info -q all -n /dev/vda          # 按设备节点查询
udevadm control --reload-rules           # 修改规则后重载
udevadm trigger                          # 重新触发一次事件

在 initramfs 里没有 udev 怎么办?两种方案:

  1. 依赖 devtmpfs 的默认节点 (本手册默认方案):内核会把节点建好,名字就是 dev_name() 的结果。
  2. 用 busybox mdev(轻量替代):
bash 复制代码
# /init 中
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s            # 扫描 /sys 并创建 /dev 下的节点

5.4.4 udev 规则文件

规则语法(详见 man 7 udev):

bash 复制代码
# /etc/udev/rules.d/70-mydevice.rules
# 匹配条件用 ==,赋值用 =,追加用 +=
SUBSYSTEM=="usb", ATTR{idVendor}=="1234", ATTR{idProduct}=="5678", \
        MODE="0660", GROUP="plugdev", SYMLINK+="myusb"

KERNEL=="ttyS[0-9]*", MODE="0666"

# 给 I²C 设备设置稳定名字
SUBSYSTEM=="i2c-dev", KERNELS=="i2c-0", SYMLINK+="i2c/primary"

# 只对特定序列号的存储设备建立 by-id 链接
ACTION=="add", SUBSYSTEM=="block", ATTRS{serial}=="ABC123", SYMLINK+="disk/mydisk"

要点:

  • 匹配键:KERNEL(节点名)、SUBSYSTEM、ATTR/ATTRS(sysfs 属性)、DRIVERS、ENV、TAG、TEST。
  • 动作键:NAME、SYMLINK、MODE、OWNER、GROUP、ENV、RUN、TAG。
  • ATTR 只查当前设备,ATTRS 会沿父设备向上查找(常用来找 USB 的 Vendor/Product)。
  • 规则文件名数字前缀决定顺序(60-、70-、99-)。
  • 调试:udevadm test /sys/class/net/eth0(会打印每条规则的匹配过程)。

持久化命名的价值 :/dev/sda 会随插拔顺序变化,而 /dev/disk/by-id/...、/dev/disk/by-uuid/... 是稳定的。产品代码与脚本都应该用后者。

bash 复制代码
ls -l /dev/disk/by-id/ /dev/disk/by-path/ /dev/disk/by-uuid/
blkid

5.5 总结

  • 系统调用 → VFS → file_operations 是用户空间访问设备的标准路径;inode/file/private_data 是必须理解的三件套。
  • 设备文件本质是"设备号 + 类型",可以手工 mknod,但正确做法是注册到设备模型让内核/udev 自动创建。
  • devfs 已被删除,devtmpfs + udev(或 busybox mdev)是 7.2.5 的标准方案;内核提供事实,用户空间决定策略。
  • sysfs 是设备模型的用户空间视图;驱动用 dev_groups 暴露简单属性,复杂接口用 ioctl/netlink/configfs/debugfs。
  • udev 规则是实现稳定命名、权限控制与自动动作的关键,调试用 udevadm monitor/test。
相关推荐
Snasph3 小时前
04-Linux 内核模块
linux 驱动学习