在 Linux 中,poll
和 epoll
的驱动层与应用层协作需要内核模块提供事件通知机制(如数据可读/可写),应用层通过文件描述符监控这些事件。以下是驱动层和应用层的完整实现示例及原理说明:
一、驱动层实现(内核模块)
1. 驱动核心逻辑
-
目标: 创建一个字符设备
/dev/mypoll
,当写入数据时触发可读事件。 -
关键技术:
-
实现
file_operations.poll
方法,通过等待队列(wait_queue_head_t
)管理阻塞的进程。 -
使用
wake_up_interruptible
唤醒等待队列,通知应用层事件就绪。
-
2. 完整代码
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/slab.h>
#include <linux/device.h>
#define DEVICE_NAME "mypoll"
static int major;
static struct class *cls;
static struct device *dev;
static DECLARE_WAIT_QUEUE_HEAD(wq); // 等待队列
static bool data_ready = false; // 数据就绪标志
static char buffer[256]; // 模拟数据缓冲区
// ------------------------- 文件操作函数 -------------------------
static ssize_t dev_read(struct file *filp, char __user *buf, size_t count, loff_t *pos) {
if (copy_to_user(buf, buffer, strlen(buffer)))
return -EFAULT;
data_ready = false; // 读取后重置标志
return strlen(buffer);
}
static ssize_t dev_write(struct file *filp, const char __user *buf, size_t count, loff_t *pos) {
if (copy_from_user(buffer, buf, count))
return -EFAULT;
data_ready = true; // 写入后设置标志
wake_up_interruptible(&wq); // 唤醒等待队列
return count;
}
static unsigned int dev_poll(struct file *filp, poll_table *wait) {
unsigned int mask = 0;
poll_wait(filp, &wq, wait); // 将进程加入等待队列
if (data_ready)
mask |= POLLIN | POLLRDNORM; // 标记可读事件
return mask;
}
static struct file_operations fops = {
.owner = THIS_MODULE,
.read = dev_read,
.write = dev_write,
.poll = dev_poll,
};
// ------------------------- 模块初始化/卸载 -------------------------
static int __init mypoll_init(void) {
major = register_chrdev(0, DEVICE_NAME, &fops);
cls = class_create(THIS_MODULE, DEVICE_NAME);
dev = device_create(cls, NULL, MKDEV(major, 0), NULL, DEVICE_NAME);
return 0;
}
static void __exit mypoll_exit(void) {
device_destroy(cls, MKDEV(major, 0));
class_destroy(cls);
unregister_chrdev(major, DEVICE_NAME);
}
module_init(mypoll_init);
module_exit(mypoll_exit);
MODULE_LICENSE("GPL");
二、应用层示例
1. 使用 poll
监控设备
#include <stdio.h>
#include <fcntl.h>
#include <poll.h>
int main() {
int fd = open("/dev/mypoll", O_RDWR);
struct pollfd fds = { .fd = fd, .events = POLLIN };
while (1) {
int ret = poll(&fds, 1, 5000); // 5秒超时
if (ret == -1) {
perror("poll");
break;
} else if (ret == 0) {
printf("Timeout\n");
continue;
}
if (fds.revents & POLLIN) {
char buf[256];
ssize_t len = read(fd, buf, sizeof(buf));
printf("Data received: %.*s\n", (int)len, buf);
}
}
close(fd);
return 0;
}
2. 使用 epoll
监控设备(边缘触发模式)
#include <stdio.h>
#include <fcntl.h>
#include <sys/epoll.h>
int main() {
int fd = open("/dev/mypoll", O_RDWR | O_NONBLOCK);
int epoll_fd = epoll_create1(0);
struct epoll_event ev = { .events = EPOLLIN | EPOLLET }, events[1];
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev);
while (1) {
int nfds = epoll_wait(epoll_fd, events, 1, 5000);
if (nfds == -1) {
perror("epoll_wait");
break;
} else if (nfds == 0) {
printf("Timeout\n");
continue;
}
for (int i = 0; i < nfds; i++) {
if (events[i].events & EPOLLIN) {
char buf[256];
ssize_t len;
while ((len = read(fd, buf, sizeof(buf))) > 0) { // 必须读空缓冲区
printf("Data received (ET): %.*s\n", (int)len, buf);
}
if (len == -1 && errno != EAGAIN) {
perror("read");
}
}
}
}
close(epoll_fd);
close(fd);
return 0;
}
三、交互测试步骤
1. 编译与加载驱动
# 编译内核模块
make
sudo insmod mypoll.ko
# 创建设备节点(若未自动创建)
sudo mknod /dev/mypoll c $(cat /proc/devices | grep mypoll | awk '{print $1}') 0
2. 运行应用程序
# 使用 poll 监控
gcc poll_app.c -o poll_app
./poll_app
# 使用 epoll 监控
gcc epoll_app.c -o epoll_app
./epoll_app
3. 触发事件
# 另开终端写入数据
echo "Hello from driver" | sudo tee /dev/mypoll
四、核心原理说明
层级 | 关键动作 | 协作逻辑 |
---|---|---|
驱动层 | 1. 实现 poll 方法,管理等待队列 | 当应用层调用 poll/epoll 时,内核通过驱动层的 poll 检查设备状态。若数据未就绪,进程被加入等待队列。 |
2. 数据写入后调用 wake_up_interruptible | 驱动写入数据时唤醒队列,通知应用层事件就绪。 | |
应用层 | 1. 调用 poll/epoll_wait 阻塞等待事件 | 内核检测到事件就绪后,唤醒应用层进程,返回就绪的文件描述符。 |
2. 处理就绪事件(如读取数据) | 应用层通过 read 读取驱动数据,驱动层重置状态标志。 |
五、关键注意事项
-
驱动层同步:
-
使用
wait_queue_head_t
和自旋锁保护共享数据(如data_ready
)。 -
避免在中断上下文中直接调用
wake_up
,应使用wake_up_interruptible
。
-
-
应用层非阻塞模式:
-
边缘触发(ET)模式下,必须将文件描述符设置为非阻塞(
O_NONBLOCK
),并循环读取直到返回EAGAIN
。
-
-
事件类型选择:
-
驱动层返回
POLLIN
表示可读,POLLOUT
表示可写。 -
异常事件使用
POLLERR
或POLLHUP
。
-
通过以上实现,驱动层与应用层可通过 poll/epoll
实现高效的事件驱动交互,适用于传感器数据采集、异步通知等场景。