等待队列是 Linux 内核中实现阻塞式 I/O 操作的基础机制,它允许进程在等待特定事件发生时进入睡眠状态,释放 CPU 资源。当事件发生后,内核会唤醒等待在队列上的进程,使其继续执行。等待队列由struct wait_queue_head结构体表示,进程通过struct wait_queue结构体与等待队列关联。
以一个简单的字符设备驱动为例,展示等待队列的使用:
#include
#include
#include
#include
#include
#include
#define DEVICE_NAME "my_wait_queue_device"
// 定义等待队列头
static wait_queue_head_t my_wait_queue;
// 用于标识数据是否准备好的标志
static int data_ready = 0;
// 模拟数据缓冲区
static char buffer[100];
static ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) {
// 等待数据准备好
wait_event_interruptible(my_wait_queue, data_ready);
if (copy_to_user(buf, buffer, count)) {
return -EFAULT;
}
data_ready = 0;
return count;
}
static ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
if (copy_from_user(buffer, buf, count)) {
return -EFAULT;
}
data_ready = 1;
// 唤醒等待在队列上的进程
wake_up_interruptible(&my_wait_queue);
return count;
}
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.read = my_read,
.write = my_write,
};
static int __init my_driver_init(void) {
// 初始化等待队列头
init_waitqueue_head(&my_wait_queue);
if (register_chrdev(0, DEVICE_NAME, &my_fops) < 0) {
return -1;
}
printk(KERN_INFO "My wait queue device driver initialized\n");
return 0;
}
static void __exit my_driver_exit(void) {
unregister_chrdev(0, DEVICE_NAME);
printk(KERN_INFO "My wait queue device driver removed\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("cmy");
MODULE_DESCRIPTION("A simple driver using wait queue");
在上述代码中,my_read函数使用wait_event_interruptible等待data_ready标志变为真,若条件不满足则进程进入睡眠状态。my_write函数在数据写入后,设置data_ready为真,并调用wake_up_interruptible唤醒等待在队列上的进程。
等待队列适用于设备数据不能立即准备好,需要等待特定条件满足的场景。例如,在读取传感器数据时,传感器可能需要一定时间进行数据采集,此时驱动程序可以使用等待队列让进程进入睡眠,直到传感器数据准备完毕后再唤醒进程进行数据读取。
非阻塞访问允许进程在执行 I/O 操作时,即使数据未准备好,也不会被阻塞,而是立即返回。通过设置文件描述符的O_NONBLOCK标志,驱动程序可以实现非阻塞 I/O。在非阻塞模式下,当 I/O 操作无法立即完成时,驱动会返回特定的错误码(如-EAGAIN),告知用户空间数据尚未准备好。
修改上述字符设备驱动,使其支持非阻塞访问:
static ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) {
if (filp->f_flags & O_NONBLOCK) {
if (!data_ready) {
return -EAGAIN;
}
} else {
wait_event_interruptible(my_wait_queue, data_ready);
}
if (copy_to_user(buf, buffer, count)) {
return -EFAULT;
}
data_ready = 0;
return count;
}
static ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
if (filp->f_flags & O_NONBLOCK) {
if (count > sizeof(buffer)) {
return -EAGAIN;
}
}
if (copy_from_user(buffer, buf, count)) {
return -EFAULT;
}
data_ready = 1;
wake_up_interruptible(&my_wait_queue);
return count;
}
在my_read和my_write函数中,首先检查文件描述符是否设置了O_NONBLOCK标志。如果是,并且数据未准备好(读取时)或缓冲区空间不足(写入时),则立即返回-EAGAIN,而不是阻塞进程。
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_led9_MainActivity_testNonBlockRead(JNIEnv *env, jobject thiz) {
int fd;
char buffer[BUFFER_SIZE];
// 打开设备
fd = open(DEVICE_PATH, O_RDWR | O_NONBLOCK);
if (fd == -1) {
LOGD("testNonBlockRead 无法打开设备");
return 1;
}
// 读取数据
if (read(fd, buffer, sizeof(buffer)) == -1) {
LOGD("testNonBlockRead 读取失败");
close(fd);
return 1;
}
LOGD("testNonBlockRead 读取内容: %s\n", buffer);
// 关闭设备
close(fd);
return 0;
}
应用层代码需要带上O_NONBLOCK标志位。
非阻塞访问适用于对实时性要求较高,不希望在 I/O 操作上被阻塞的场景。例如,网络客户端程序需要不断尝试发送和接收数据,同时还要处理其他任务,此时使用非阻塞 I/O 可以避免在等待数据时被阻塞,提高程序的响应速度。
I/O 多路复用允许一个进程同时监控多个文件描述符,当其中任何一个文件描述符准备好进行 I/O 操作时,进程就会被唤醒并进行相应处理。常见的 I/O 多路复用机制有select、poll和epoll。在驱动层面,需要配合用户空间的 I/O 多路复用函数,通过poll方法实现对设备文件描述符的状态监控。
在字符设备驱动中添加poll方法:
unsigned int my_poll(struct file *filp, poll_table *wait) {
unsigned int mask = 0;
poll_wait(filp, &my_wait_queue, wait);
if (data_ready) {
mask |= POLLIN | POLLRDNORM;
}
printk(KERN_INFO "my_poll\n");
return mask;
}
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.read = my_read,
.write = my_write,
.poll = my_poll,
};
my_poll函数通过poll_wait将等待队列与文件描述符关联起来。然后根据数据准备情况和缓冲区空间,设置相应的事件掩码(如POLLIN表示数据可读,POLLOUT表示可以写入数据),返回给用户空间,以便用户空间的select、poll或epoll函数判断文件描述符的状态。
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_led9_MainActivity_testSelect(JNIEnv *env, jobject thiz) {
int fd, ret;
fd_set readfds;
char buffer[BUFFER_SIZE];
struct timeval timeout;
// 打开设备文件
fd = open(DEVICE_PATH, O_RDWR);
if (fd < 0) {
perror("open failed");
return -1;
}
while (1) {
// 清空文件描述符集合
FD_ZERO(&readfds);
// 添加设备文件描述符
FD_SET(fd, &readfds);
// 设置超时时间(可选)
timeout.tv_sec = 1;
timeout.tv_usec = 0;
// 调用select等待事件
ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
LOGD("testSelect ret = %d", ret);
if (ret < 0) {
LOGD("select failed");
break;
} else if (ret == 0) {
LOGD("select timeout\n");
continue;
}
// 检查是否是设备文件就绪
if (FD_ISSET(fd, &readfds)) {
// 读取数据
ret = read(fd, buffer, sizeof(buffer));
if (ret > 0) {
LOGD("Read %d bytes: %s\n", ret, buffer);
break;
}
}
}
close(fd);
return 0;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_led9_MainActivity_testPoll(JNIEnv *env, jobject thiz) {
int fd, ret;
struct pollfd fds[1];
char buffer[BUFFER_SIZE];
// 打开设备文件
fd = open(DEVICE_PATH, O_RDWR);
if (fd < 0) {
perror("open failed");
return -1;
}
// 设置pollfd结构体
fds[0].fd = fd;
fds[0].events = POLLIN; // 监视可读事件
while (1) {
// 调用poll等待事件
ret = poll(fds, 1, 1000); // 超时时间1秒
if (ret < 0) {
LOGD("poll failed");
break;
} else if (ret == 0) {
LOGD("poll timeout\n");
continue;
}
// 检查是否有事件发生
if (fds[0].revents & POLLIN) {
// 读取数据
ret = read(fd, buffer, sizeof(buffer));
if (ret > 0) {
LOGD("Read %d bytes: %s\n", ret, buffer);
}
}
}
close(fd);
return 0;
}
应用层读取方式。
I/O 多路复用适用于需要同时处理多个 I/O 设备或文件描述符的场景。例如,网络服务器程序需要同时监听多个客户端连接,处理多个套接字的读写操作,使用 I/O 多路复用可以高效地管理这些文件描述符,避免为每个文件描述符创建单独的进程或线程,从而降低系统资源消耗。
信号驱动 I/O 允许设备在 I/O 操作准备好时,向用户空间进程发送信号,通知进程进行相应处理。用户空间进程通过fcntl函数设置文件描述符为信号驱动模式,并使用sigaction函数注册信号处理函数。当设备准备好进行 I/O 操作时,内核会向进程发送SIGIO信号,进程在信号处理函数中执行 I/O 操作。
在驱动中添加对信号驱动 I/O 的支持:
static struct fasync_struct *my_async_queue;
static void my_data_ready_notify(void) {
if (my_async_queue) {
kill_fasync(&my_async_queue, SIGIO, POLL_IN);
}
}
static int my_fasync(int fd, struct file *filp, int on) {
int ret;
ret = fasync_helper(fd, filp, on, &my_async_queue);
return ret;
}
// 在数据准备好的地方(如my_write函数中)调用my_data_ready_notify
static ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
if (copy_from_user(buffer, buf, count)) {
return -EFAULT;
}
data_ready = 1;
printk(KERN_INFO "My wait queue my_write buffer = %s\n", buffer);
my_data_ready_notify();
wake_up_interruptible(&my_wait_queue);
return count;
}
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.read = my_read,
.write = my_write,
.poll = my_poll,
.fasync = my_fasync,
.release = my_release,
};
my_fasync函数用于管理异步通知队列,my_data_ready_notify函数在数据准备好时向用户空间进程发送SIGIO信号。用户空间进程通过注册SIGIO信号处理函数,在接收到信号后执行相应的 I/O 操作。
void signal_handler(int signum) {
char buffer[BUFFER_SIZE];
int fd;
fd = open(DEVICE_PATH, O_RDONLY);
if (fd < 0) {
perror("open failed");
return;
}
int ret = read(fd, buffer, sizeof(buffer));
if (ret > 0) {
LOGD("signal_handler Read %d bytes: %s\n", ret, buffer);
}
close(fd);
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_led9_MainActivity_testSignal(JNIEnv *env, jobject thiz) {
int fd;
// 打开设备文件
fd = open(DEVICE_PATH, O_WRONLY);
if (fd < 0) {
LOGD("open failed");
return 1;
}
// 设置信号处理函数
signal(SIGIO, signal_handler);
// 设置当前进程为文件owner
fcntl(fd, F_SETOWN, getpid());
// 启用异步通知
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_ASYNC);
LOGD("Waiting for data... (PID: %d)\n", getpid());
sleep(1);
// 向设备写入数据(触发信号)
write(fd, "Hello", 5);
// 保持程序运行以接收信号
while (1) {
pause();
}
close(fd);
return 0;
}
信号驱动 I/O 适用于对实时性要求极高,希望在设备数据准备好时能立即得到通知的场景。例如,在实时监控系统中,当传感器有新数据产生时,需要尽快通知应用程序进行处理,使用信号驱动 I/O 可以实现高效的实时数据交互。
I/O 模型 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
等待队列 | 实现简单,适合阻塞式 I/O 操作 | 进程在等待时会阻塞,浪费 CPU 资源 | 数据准备时间不确定,允许进程阻塞等待的场景 |
非阻塞访问 | 不会阻塞进程,提高程序响应速度 | 需要用户空间不断轮询检查,可能浪费 CPU 资源 | 对实时性要求高,不希望被阻塞的场景 |
I/O 多路复用 | 可以同时监控多个文件描述符,高效管理资源 | 在高并发场景下,select和poll性能会下降 | 需要同时处理多个 I/O 设备或文件描述符的场景 |
信号驱动 I/O | 实时性高,无需轮询 | 信号处理函数编写复杂,可能存在信号丢失问题 | 对实时性要求极高的场景 |
在实际的 Linux 驱动开发中,开发者需要根据设备的特性和应用场景的需求,综合考虑各 I/O 模型的优缺点,选择最合适的 I/O 模型,以实现高效、稳定的设备与系统数据交互。同时,也可以结合多种 I/O 模型,发挥它们的优势,进一步提升驱动程序的性能。