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 章会用到)。flushvsrelease:++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 怎么办?两种方案:
- 依赖 devtmpfs 的默认节点 (本手册默认方案):内核会把节点建好,名字就是
dev_name()的结果。 - 用 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(或 busyboxmdev)是 7.2.5 的标准方案;内核提供事实,用户空间决定策略。- sysfs 是设备模型的用户空间视图;驱动用
dev_groups暴露简单属性,复杂接口用 ioctl/netlink/configfs/debugfs。 - udev 规则是实现稳定命名、权限控制与自动动作的关键,调试用
udevadm monitor/test。