技术知识库
共享设备状态与每次 open 私有状态如何决定进程能否互相唤醒
共享设备状态与每次 open 私有状态如何决定进程能否互相唤醒
摘要
字符设备的每次 open() 都产生独立的 struct file,但这些文件是否能够互相传递数据和唤醒,取决于它们的 file->private_data 是否最终指向同一个共享设备对象。结构体成员地址可以理解为“对象基地址 + 成员偏移”:基地址相同就会得到同一个 KFIFO 和等待队列地址;基地址不同则是互不关联的对象。这里的关键是对象身份和显式指针关系,而不是内存是否连续或代码是否在同一次注册中执行。
问题/背景
在实验 6-7 的多个设备 I/O 多路复用和实验 6-8“为什么不能唤醒读写进程”中,需要回答两个问题:
write()为什么能够唤醒正在等待同一设备的read()或epoll_wait()?- 相同设备节点被两个进程分别打开后,为什么有时调用了
wake_up_interruptible()仍无法唤醒读者?
三种对象及其生命周期
| 对象 | 典型生命周期 | 是否应共享 |
|---|---|---|
struct kfifo_device | 模块创建设备到设备注销 | 同一设备的打开者共享 |
struct file | 每次 open() 到对应 close() | 每次打开独立 |
file->private_data | 随 struct file 存在 | 可以指向共享设备,也可以指向每次打开的私有上下文 |
同一设备被打开两次时,两个 struct file 一定不同;决定通信关系的是驱动放入 private_data 的指针。
基地址加成员偏移
访问结构体成员:
&device->read_queue
可以抽象为:
read_queue 地址
= device 对象基地址
+ offsetof(struct kfifo_device, read_queue)
假设 read_queue 的偏移为 0x20。
共享同一个设备对象
cat 的 private_data = 0x1000
echo 的 private_data = 0x1000
双方的 read_queue = 0x1000 + 0x20 = 0x1020
读者等待 0x1020,写者也唤醒 0x1020,因此能够配对。
每次打开分别创建对象
cat 的 private_data = 0x1000
echo 的 private_data = 0x2000
cat 的 read_queue = 0x1020
echo 的 read_queue = 0x2020
内核能够分别找到两个等待队列,但不会推断它们在业务上应当关联。wake_up_interruptible() 只处理传入地址代表的那个队列。
正确的共享设备模型
模块为每个设备创建一份长期存在的状态:
/dev/epoll_kfifo_mailbox_3
│
▼
kfifo_device 3
├── misc_kfifo
├── read_queue
└── write_queue
每次打开设备 3 都有自己的 struct file,但 open() 最终令它们指向同一个 kfifo_device 3:
cat 的 struct file ──┐
├──→ kfifo_device 3
echo 的 struct file ──┘
于是 echo 写入的正是 cat 读取的 KFIFO,写者唤醒的也正是读者等待的队列。
实验 6-8 的故意错误
实验 6-8 故意把 KFIFO、read_queue 和 write_queue 移入每次 open() 分配的私有数据:
struct mydemo_private_data {
struct mydemo_device *device;
struct kfifo mydemo_fifo;
wait_queue_head_t read_queue;
wait_queue_head_t write_queue;
};
这会形成:
cat open() → private A → fifo A、read_queue A
echo open() → private B → fifo B、read_queue B
cat 睡在队列 A,而 echo 写入 FIFO B 并唤醒队列 B,所以读者不会醒来。问题不是 wake_up_interruptible() 失效,而是驱动选择了错误的状态归属和对象生命周期。
poll 与 epoll 如何定位具体设备
8 个设备是 8 个事件源,不等于 8 个消费者。每个设备拥有独立等待队列:
fd0 → device0 → read_queue0
fd1 → device1 → read_queue1
...
fd7 → device7 → read_queue7
.poll 通过 poll_wait() 把对应文件的 epoll 回调登记到本设备的队列。写入 device3 后,驱动只调用:
wake_up_interruptible(&device3->read_queue);
内核由该等待队列中的 epoll 注册记录找到对应的文件事件,并把用户在 epoll_event.data 中保存的文件描述符、编号或指针返回给 epoll_wait()。
多个消费者监听同一个设备
若多个进程监听同一个共享 FIFO,它们会登记在同一个等待队列上。普通唤醒可能使多个等待者变为可运行,但共享数据通常只会被先执行读取的消费者取走;其他消费者需要重新检查条件。若业务要求每个消费者都收到一份消息,驱动必须维护订阅者列表并逐个投递,内核不会自动复制消息。
指针成员与嵌入成员
嵌入成员可以直接通过基地址和固定偏移定位:
struct example {
struct miscdevice misc;
};
这时可以用 container_of() 从 misc 的地址反推出 example 的基地址:
结构体基地址 = 成员地址 - 成员偏移
若结构体保存的是指针:
struct example {
struct miscdevice *misc;
};
miscdevice 可以位于另一次分配的内存中,二者依靠显式指针连接,不能仅靠成员偏移互相反推。
证据与来源
- 实验手册 v2.0.7,第 6.7 节“向虚拟设备中添加 I/O 多路复用支持”。
- 实验手册 v2.0.7,第 6.8 节“为什么不能唤醒读写进程”。
- 实验内核:Running Linux Kernel 5.0,ARM64,
5.0.0+。 - 当前多设备驱动:
/home/martin-zou/workspace/gcc-lab/char_mod/epollkfifo_mailbox/epollkfifo_mailbox.c。 - Linux 5.0 epoll 等待队列回调:
fs/eventpoll.c::ep_ptable_queue_proc()、ep_poll_callback()。 - 已观察到阻塞邮箱在 FIFO 为空时
cat等待,写入后通过同一设备等待队列继续读取。
关联
- 字符设备驱动 MOC
- 字符设备如何把设备节点连接到 file_operations
- O_NONBLOCK 如何改变字符设备读写语义
- KFIFO 如何用环形缓冲区管理字符设备数据
- 奔跑吧 Linux 内核入门篇第二版实验指导手册
待验证
- 在目标 ARM64 Debian 中加载
epollkfifo_mailbox.ko,确认 8 个 misc 节点均创建成功。 - 编写并运行用户空间 poll/epoll 程序,验证写入设备 0 和设备 1 时只返回对应事件源。
- 按实验 6-8 故意把 KFIFO 和等待队列移入每次打开的私有对象,实际复现“写者唤醒错误队列”的现象。