在 Linux 系统中当一个用户程序调用read或recv时数据通常要经过两个阶段等待数据准备好内核缓冲区填充过程。将数据从内核拷贝到用户空间。根据这两个阶段处理方式的不同演化出了五种典型的 I/O 模型阻塞、非阻塞、多路复用、异步与信号驱动。从应用层看read()/write()可能阻塞也可能因为 O_NONBLOCK 而立即返回select / poll / epoll 负责等待“就绪”aio_read() 这类接口属于异步提交F_SETOWN O_ASYNC则让内核通过 SIGIO 主动通知进程。从驱动层看这几类模型通常分别落在这些点上阻塞 / 非阻塞主要体现在 read()/write() 里是否睡眠等待多路复用主要靠驱动实现 poll()信号驱动主要靠驱动实现 fasync(),并在事件到来时调用 kill_fasync();异步 IO本质是“提交请求后立即返回稍后再拿完成结果”它强调的是完成通知而不是“文件描述符就绪通知”。1.阻塞IO2.1 什么叫阻塞 IO阻塞 IO 的特点很简单当应用调用 read(fd, buf, len) 时如果设备当前没有数据可读那么当前线程不会立刻返回而是睡眠一直等到数据到来、被信号打断或者发生错误。所以一般说的阻塞是用户空间阻塞。在 Linux 内核里等待条件成立最常见的方式就是等待队列。2.2驱动里怎样实习阻塞读典型思路是驱动维护一个接收缓冲区或环形队列read () 发现“当前无数据”时调用 wait_event_interruptible() 睡眠中断处理函数、DMA 完成回调、定时器或底半部在数据到来后把数据放进缓冲区/或write时将内容写到缓冲区然后主动唤醒然后调用 wake_up_interruptible() 唤醒等待进程。2.3等待队列在 Linux 内核驱动开发中等待队列Wait Queue是实现“阻塞 I/O”的核心机制。简单来说它就是内核里的一个“等候室”专门用来存放那些因为等待某个硬件条件如串口有数据、按键被按下而暂时无法继续执行的进程。等待队列由两部分组成就像“等候室”和“等候的人”A. 等待队列头 (wait_queue_head_t)它是等候室的门牌。通常定义在驱动的全局变量中。struct wait_queue_head { spinlock_t lock; // 自旋锁保护队列链表 struct list_head head; // 双向链表头挂载所有在该队列等待的进程 };B. 等待队列项 (wait_queue_entry_t)它是每一个正在等候的进程。内核会自动创建它并把它挂到“头”上。2.3.1工作流程与API详解第一步定义与初始化你可以静态定义也可以动态初始化。// 方式1静态定义并初始化 DECLARE_WAIT_QUEUE_HEAD(my_wq); // 方式2动态初始化 wait_queue_head_t my_wq; init_waitqueue_head(my_wq);第二步进入等待睡眠这是最常用的宏。它的逻辑是“如果 condition 为假我就睡如果 condition 为真我就直接过去。”wait_event_interruptible(my_wq, condition);my_wq刚才定义的队列头。condition这是一个布尔表达式。非常重要进程被唤醒后会重新检查这个条件如果条件依然为假它会继续睡一般我们会定义一个flag。第三步唤醒队列通常在中断处理函数或者 write () 函数中调用。wake_up_interruptible(wq_head);它会扫描 my_wq 指向的链表唤醒所有满足条件的进程。2.3等待队列实验驱动程序如下#include linux/init.h #include linux/module.h #include linux/fs.h #include linux/kdev_t.h #include linux/cdev.h #include linux/device.h #include linux/uaccess.h //copy_to_user copy_from_user #include linux/string.h //strlen #include linux/wait.h //等待队列头文件 struct cdev_test_dev{ static dec_t dev_num; int major; int minor; struct cdev cdev_test; struct class* class_test; struct device* device_test; char kbuf[100]; int flag; //等待队列标志 }; struct cdev_test_dev dev1; struct cdev_test_dev dev2; DECLARE_WAIT_QUEUE_HEAD(my_wq); //定义一个等待队列头 static int cdev_test_open(struct inode* inode,struct file* filp) { dev1.minor 0; dev2.minor 1; printk(cdev_test_open\n); //判断是哪个设备被打开了 struct cdev_test_dev* dev container_of(inode-i_cdev,struct cdev_test_dev,cdev_test); filp-private_data dev; //将设备结构体指针保存在文件私有数据中 printk(major %d,minor %d\n,dev-major,dev-minor); return 0; } static int cdev_test_read(struct file* filp,char __user* buf,size_t count,loff_t* offset) { char kbuf[100] hello cdev_test; wait_event_interruptible(my_wq,dev1.flag); //等待队列,当dev1.flag不为0时才会继续执行 copy_to_user(buf,kbuf,strlen(kbuf)); //用于将内核空间的数据复制到用户空间,使用strlen而不是sizeof是为了去掉字符串末尾的\0 printk(cdev_test_read\n); return 0; } static int cdev_test_write(struct file* filp,const char __user* buf,size_t count,loff_t* offset) { struct cdev_test_dev* dev (struct cdev_test_dev*)filp-private_data; if(dev-minor 0) { dev1.flag 1; //修改等待队列标志 wake_up_interruptible(my_wq); //唤醒等待队列中的进程 copy_from_user(dev-kbuf,buf,count); //用于将用户空间的数据复制到内核空间 printk(cdev_test_write to dev1: %s\n,dev-kbuf); } if(dev-minor 1) { copy_from_user(dev-kbuf,buf,count); //用于将用户空间的数据复制到内核空间 printk(cdev_test_write to dev2: %s\n,dev-kbuf); } return count; } static int cdev_test_release(struct inode* inode,struct file* flip) { printk(cdev_test_release\n); return 0; } static struct file_operations fops { .owner THIS_MODULE, .open cdev_test_open, .read cdev_test_read, .write cdev_test_write, .release cdev_test_release, }; //文件操作结构体 static int __init module_cdev_init(void) { int ret; int major,minor; //1.申请设备号 ret alloc_chrdev_region(dev1.dev_num,0,2,cdev_test1); if(ret 0) { printk(alloc_cdev_region failed\n); return ret; } major MAJOR(dev1.dev_num); minor MINOR(dev1.dev_num); printk(major %d,minor %d\n,major,minor); //2.初始化字符设备结构体 cdev_init(dev1.cdev_test,fops); dev1.cdev_test.owner THIS_MODULE; //3.注册字符设备 ret cdev_add(dev1.cdev_test,dev1.dev_num,1); if(ret 0) { printk(cdev_add failed\n); unregister_chrdev_region(dev1.dev_num,1); return ret; }else { printk(cdev_add success\n); } //4.自动创建设备节点(创建类和设备) dev1.class_test class_create(THIS_MODULE,cdev_test_class); dev1.device_test device_create(dev1.class_test,NULL,dev1.dev_num,NULL,cdev_test_device); dev2.dev_num MKDEV(major,minor1); major MAJOR(dev2.dev_num); minor MINOR(dev2.dev_num); printk(major %d,minor %d\n,major,minor); //2.初始化字符设备结构体 cdev_init(dev2.cdev_test,fops); dev2.cdev_test.owner THIS_MODULE; //3.注册字符设备 ret cdev_add(dev2.cdev_test,dev2.dev_num,1); if(ret 0) { printk(cdev_add failed\n); unregister_chrdev_region(dev1.dev_num,1); return ret; }else { printk(cdev_add success\n); } //4.自动创建设备节点(创建类和设备) dev2.device_test device_create(dev1.class_test,NULL,dev2.dev_num,NULL,cdev_test_device1); dev1.flag 0; //初始化等待队列标志 return 0; } static void __exit module_cdev_exit(void) { //1.删除字符设备 cdev_del(dev1.cdev_test); cdev_del(dev2.cdev_test); //2.释放设备号 unregister_chrdev_region(dev1.dev_num,1); unregister_chrdev_region(dev2.dev_num,1); //3.销毁设备节点和类 device_destroy(dev1.class_test,dev1.dev_num); device_destroy(dev1.class_test,dev2.dev_num); class_destroy(dev1.class_test); class_destroy(dev2.class_test); printk(cdev_test exit\n); } module_init(module_cdev_init); module_exit(module_cdev_exit); MODULE_LICENSE(GPL);应用程序如下read.c#include stdio.h #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include string.h int main() { int fd; char buf1[100]; fd open(/dev/cdev_test_device,O_RDWR); //阻塞,读写方式打开设备节点 if(fd 0) { perror(open failed); return -1; } printf(read from device...\n); read(fd,buf1,sizeof(buf1)); //从设备节点读取数据 printf(read over,buf1 %s\n,buf1); close(fd); return 0; }write.c#include stdio.h #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include string.h int main(int argc,char* argv[]) { int fd; char buf[100] hello kernel; fd open(/dev/cdev_test_device,O_RDWR); if(fd 0) { perror(open failed); return -1; } printf(write to device...\n); write(fd,buf,strlen(buf)); printf(write over\n); close(fd); return 0; }接下来我们在Ubuntu虚拟机上进行实验不在rk3568上所以不需要进行交叉编译1.将驱动程序编译为.ko文件Makefileobj-m wq.o # 如果在虚拟机本地实验用下面这行路径 KDIR : /lib/modules/$(shell uname -r)/build # 如果给 RK3568 编译用你之前的路径 # KDIR : /home/topeet/source/linux/rk3568_linux_5.10/kernel PWD : $(shell pwd) all: make -C $(KDIR) M$(PWD) modules # 注意这里make 后面有空格 clean: make -C $(KDIR) M$(PWD) clean # 同样这里也要有空格更改KDIR为本机的内核路径make后生成.ko文件sudo insmod wq.ko导入内核模块。2.将应用程序编译为可执行文件gcc -o read read.c gcc -o write write.c接着分别运行两个可执行文件。2.非阻塞IO非阻塞 IO 的核心是用户把文件描述符设置为 O_NONBLOCK 后凡是“本来会睡眠等待”的操作现在都不能睡了必须立刻返回。注意O_NONBLOCK 只是应用层传给内核的一个“标志位”真正的逻辑必须由你在驱动程序里亲手写出来。内核不会自动帮你把阻塞代码变成非阻塞。2.1非阻塞IO实验要实现非阻塞 I/O (Non-blocking I/O)我们需要在驱动层增加对O_NONBLOCK标志位的判断并在应用层打开文件时指定该标志。非阻塞模式的核心逻辑是如果数据没准备好驱动不准让进程“睡觉”进入等待队列而是立刻返回 -EAGAIN 错误。驱动程序#include linux/init.h #include linux/module.h #include linux/fs.h #include linux/kdev_t.h #include linux/cdev.h #include linux/device.h #include linux/uaccess.h #include linux/string.h #include linux/wait.h struct cdev_test_dev { dev_t dev_num; // 修复类型名 int major; int minor; struct cdev cdev_test; struct class* class_test; struct device* device_test; char kbuf[100]; int flag; }; struct cdev_test_dev dev1; struct cdev_test_dev dev2; DECLARE_WAIT_QUEUE_HEAD(my_wq); static int cdev_test_open(struct inode* inode, struct file* filp) { // 使用 container_of 动态找到对应的结构体 struct cdev_test_dev* dev container_of(inode-i_cdev, struct cdev_test_dev, cdev_test); filp-private_data dev; return 0; } static ssize_t cdev_test_read(struct file* filp, char __user* buf, size_t count, loff_t* offset) { struct cdev_test_dev* dev (struct cdev_test_dev*)filp-private_data; char kbuf[100] hello from kernel; // --- 非阻塞 IO 核心逻辑开始 --- if (filp-f_flags O_NONBLOCK) { // 如果数据没准备好 (flag 0)直接返回 EAGAIN 错误不进入等待队列 if (dev-flag 0) return -EAGAIN; } else { // 如果是阻塞模式则正常进入等待队列 if (wait_event_interruptible(my_wq, dev-flag ! 0)) return -ERESTARTSYS; } // --- 非阻塞 IO 核心逻辑结束 --- if (copy_to_user(buf, kbuf, strlen(kbuf))) return -EFAULT; dev-flag 0; // 读取后重置标志位 return strlen(kbuf); } static ssize_t cdev_test_write(struct file* filp, const char __user* buf, size_t count, loff_t* offset) { struct cdev_test_dev* dev (struct cdev_test_dev*)filp-private_data; if (copy_from_user(dev-kbuf, buf, count)) return -EFAULT; dev-flag 1; // 设置标志位表示数据准备好了 wake_up_interruptible(my_wq); // 唤醒等待队列 printk(write to dev%d: %s\n, dev-minor, dev-kbuf); return count; } static int cdev_test_release(struct inode* inode, struct file* filp) { return 0; } static struct file_operations fops { .owner THIS_MODULE, .open cdev_test_open, .read cdev_test_read, .write cdev_test_write, .release cdev_test_release, }; static int __init module_cdev_init(void) { int ret; // 申请设备号 ret alloc_chrdev_region(dev1.dev_num, 0, 2, cdev_test); if (ret 0) return ret; dev1.major MAJOR(dev1.dev_num); // 注册 dev1 cdev_init(dev1.cdev_test, fops); cdev_add(dev1.cdev_test, dev1.dev_num, 1); dev1.class_test class_create(THIS_MODULE, cdev_test_class); device_create(dev1.class_test, NULL, dev1.dev_num, NULL, cdev_test0); // 注册 dev2 dev2.dev_num MKDEV(dev1.major, 1); cdev_init(dev2.cdev_test, fops); cdev_add(dev2.cdev_test, dev2.dev_num, 1); device_create(dev1.class_test, NULL, dev2.dev_num, NULL, cdev_test1); dev1.flag dev2.flag 0; return 0; } static void __exit module_cdev_exit(void) { device_destroy(dev1.class_test, dev1.dev_num); device_destroy(dev1.class_test, dev2.dev_num); class_destroy(dev1.class_test); cdev_del(dev1.cdev_test); cdev_del(dev2.cdev_test); unregister_chrdev_region(dev1.dev_num, 2); } module_init(module_cdev_init); module_exit(module_cdev_exit); MODULE_LICENSE(GPL);read.c#include stdio.h #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include string.h #include errno.h // 必须包含 errno 来判断错误类型 int main() { int fd; char buf[100]; ssize_t n; // 增加 O_NONBLOCK 标志位 fd open(/dev/cdev_test0, O_RDWR | O_NONBLOCK); if(fd 0) { perror(open failed); return -1; } printf(Non-blocking read start...\n); while(1) { n read(fd, buf, sizeof(buf)); if (n 0) { if (errno EAGAIN) { // 数据还没准备好我们可以在这里干点别的 printf(No data yet, doing other things...\n); sleep(1); // 模拟做其他工作 continue; } else { perror(read error); break; } } else { // 读到数据了 buf[n] \0; printf(Read success! Data: %s\n, buf); break; } } close(fd); return 0; }read.c中我们用到了errnoerrno 是一个在errno.h中定义的全局变量更准确地说是线程局部变量。当 Linux 系统调用如 readwriteopen 发生错误时函数会统一返回一个特殊值通常是 -1。具体的错误原因错误码会被自动写入到 errno 这个变量中。EAGAIN、EINTR、EBADF 等都是预定义的宏代表不同的整数值。write.c#include stdio.h #include fcntl.h #include unistd.h #include string.h int main() { int fd; char buf[] Wake up, Neo!; // 打开同一个设备 fd open(/dev/cdev_test0, O_RDWR); if(fd 0) { perror(open failed); return -1; } printf(Writing to device to trigger wake_up...\n); write(fd, buf, strlen(buf)); printf(Write over\n); close(fd); return 0; }3.IO多路复用理解了阻塞原地等数据和非阻塞不停打电话问之后你会发现它们都有局限性如果你要同时监控 10 个外设比如温湿度传感器、串口、按键、网络阻塞 I/O 需要开 10 个线程而非阻塞 I/O 会让 CPU 忙着循环。I/O 多路复用 (I/O Multiplexing)就像是给这 10 个外设找了一个“值班哨兵”。你只需要问这个哨兵“这 10 个里有谁准备好了”哨兵会告诉你结果如果没有人准备好你进程就去睡觉。3.1核心原理在 Linux 中多路复用的应用层的主流实现是 select pollepoll。驱动层.poll做了什么驱动里的 .poll 函数其实只做两件事而且它本身不阻塞挂号调用 poll_wait() 。这并不是在等数据而是告诉内核“如果这个设备有动静了请通过这个等待队列头唤醒我。”报数检查当前的 flag如果数据准备好了返回一个“可读”掩码POLLIN否则返回0。内核收到你的 poll 请求后会依次调用你关心的所有驱动的 .poll 函数。如果所有驱动都返回 0内核才会让你的进程进入睡眠。一旦其中任何一个驱动调用了 wake_up 内核会再次轮询一遍找出是谁醒了。3.2 poll的IO多路复用实现驱动程序#include linux/poll.h static __poll_t cdev_test_poll(struct file* filp,struct poll_table_struct* wait) { struct cdev_test_dev* dev filp-private_data; __poll_t mask 0; poll_wait(filp,my_wq,wait); if(dev-minor 0) { if(dev1.flag 1) { return POLLIN | POLLRDNORM; //返回可读事件 } } if(dev-minor 1) { if(dev2.flag 1) { return POLLIN | POLLRDNORM; //返回可读事件 } } return 0; //没有事件发生 } static struct file_operations fops { .owner THIS_MODULE, .open cdev_test_open, .read cdev_test_read, .write cdev_test_write, .release cdev_test_release, .poll cdev_test_poll, //.poll }; //文件操作结构体read.c#include stdio.h #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include string.h #include poll.h int main() { int fd; char buf1[100]; char buf2[100]; struct pollfd fds[1]; fd open(/dev/cdev_test_device,O_RDWR); //阻塞,读写方式打开设备节点 if(fd 0) { perror(open failed); return -1; } fds[0].fd fd; fds[0].events POLLIN; //关注可读事件 printf(read from device...\n); while(1) { int ret poll(fds,1,-1); //阻塞等待事件发生 if(ret 0) { perror(poll failed); close(fd); return -1; }else if(fds[0].revents POLLIN) { read(fd,buf1,sizeof(buf1)); printf(data read from device: %s\n,buf1); } } printf(read over,buf1 %s\n,buf1); close(fd); return 0; }write代码可以保持不变。首先poll机制首先肯定是比阻塞IO要好这个是显而易见的那为什么比非阻塞IO要好呢非阻塞IO是套用while循环一直判断poll机制也要套用while循环呀区别在于多路复用的 while 循环在数据来之前会“卡”在 poll() 这一行动弹不得而非阻塞的 while 循环会像疯了一样不停地在里面转圈。特性非阻塞 (Polling)多路复用 (poll/select)进程状态一直在跑 (Running)没数据时睡觉 (Sleeping)CPU 消耗极高 (100% 忙转)极低 (几乎不占 CPU)响应速度取决于轮询频率由内核中断触发极快处理多 FD非常麻烦且低效天生支持极其高效