本实验聚焦Linux驱动开发中至关重要的阻塞与非阻塞I/O模式,核心目标是解决早期轮询读取设备(如按键)导致的CPU资源过度占用问题——此前轮询读取方式下,应用CPU占用率高达99.6%,而通过阻塞和非阻塞机制,可将CPU占用降至接近0%,大幅提升系统效率。
一、核心基础概念
1. 阻塞与非阻塞I/O本质
- 阻塞I/O:应用访问设备时,若设备资源不可用,进程会进入休眠状态让出CPU,直至设备可用时被唤醒,才执行数据读取。这是设备文件的默认访问模式,代码简单,能避免CPU空转浪费。
- 非阻塞I/O:设备不可用时,应用不会休眠,而是返回错误码,由应用自主选择持续轮询或放弃。非阻塞访问需显式在open时添加`O_NONBLOCK`标志,适合需要主动查询、多设备监控的场景。
2. 关键支撑机制:等待队列
等待队列是实现阻塞I/O的核心,负责管理休眠与唤醒过程,核心要素包括:
- 等待队列头:用`wait_queue_head_t`表示,需通过`init_waitqueue_head`初始化或用`DECLARE_WAIT_QUEUE_HEAD`直接定义初始化,是管理等待进程的入口。
- 等待队列项:用`wait_queue_t`表示,对应具体等待的进程,可通过`DECLARE_WAITQUEUE(name, tsk)`快速创建,tsk通常设为`current`(当前进程)。
- 核心操作:
- 进程休眠:通过`add_wait_queue`将进程对应的队列项加入等待队列头,再将进程设为可中断休眠态(`TASK_INTERRUPTIBLE`),调用`schedule`切换进程,实现休眠。
- 唤醒进程:常用`wake_up_interruptible`,仅唤醒可中断休眠的进程,避免唤醒不可中断进程导致资源浪费,该操作通常在中断处理函数中执行。
- 等待事件:可用`wait_event_interruptible`等函数,让进程等待特定条件满足(如按键有效),条件不满足则阻塞,满足时自动唤醒。
3. 轮询机制与驱动配合
非阻塞访问依赖`select`、`POLL`、`epoll`实现轮询,三者均通过调用驱动的`poll`函数完成设备状态检测:
- select:受文件描述符数量限制(默认1024),需遍历所有描述符检查状态,适合描述符较少的场景。
- poll:无描述符数量限制,通过`pollfd`结构体明确监视的事件,效率优于select,是中小规模场景的常用选择。
- epoll:适合大规模并发,采用事件驱动机制,效率极高,常用于网络编程,本实验以select和poll为主。
当应用调用select或poll时,驱动需提供对应的`poll`函数,核心操作是调用`poll_wait`将等待队列添加到轮询表中,并向应用返回设备状态(如是否可读)。
二、阻塞I/O实验
1. 实验核心诉求
第12章的中断实验中,应用通过while循环+read不断读取按键,导致CPU占用率高达99.6%。阻塞I/O的核心解决思路是:无按键事件时让应用休眠,有事件时唤醒,彻底释放CPU资源。
2. 驱动关键改造
-数据结构补充:在设备结构体中新增`wait_queue_head_t r_wait`,用于管理等待的进程队列。
- 等待队列初始化:在驱动初始化函数中,调用`init_waitqueue_head`初始化等待队列头,为后续休眠唤醒做准备。
- read函数改造:采用`wait_event_interruptible`让进程等待按键有效事件,若按键无效则进入可中断休眠,避免循环轮询;若按键有效,继续执行读取操作。
同时支持另一种手动管理队列的方式:通过`DECLARE_WAITQUEUE`创建队列项,`add_wait_queue`加入队列,`schedule`切换进程,唤醒后用`remove_wait_queue`移除队列项,适配更复杂的场景。
- 中断唤醒逻辑:按键中断服务函数或定时器消抖函数中,检测到有效按键事件后,调用`wake_up_interruptible`唤醒等待队列中的进程,让休眠的应用继续执行读取操作。
3. 应用与测试
- 测试程序:直接复用第12章的应用,无需修改,因为默认open就是阻塞模式,应用会自动在无按键时休眠。
- 运行效果:加载驱动后运行测试程序,按下按键时正常打印键值,查看CPU占用率,从99.6%降至0.0%,仅在按键触发瞬间占用少量CPU,大幅提升系统效率。
三、非阻塞I/O实验
1. 驱动核心适配
- 读取逻辑补充:在read函数中增加非阻塞判断,若open时添加了`O_NONBLOCK`标志,检测到无按键事件时,直接返回`-EAGAIN`错误码,不阻塞进程,让应用自主决定后续操作。
- poll函数实现:新增驱动的`poll`回调函数,核心工作是调用`poll_wait`将等待队列加入轮询表,同时检测按键是否有效,有效时向应用返回`POLLIN`,告知有数据可读,否则返回0,让应用知晓设备不可用。
- 操作集注册:在设备文件操作结构体中,添加`poll`成员变量,指向实现的`poll`函数,确保应用调用select或poll时能触发驱动的对应逻辑。
2. 测试应用实现
测试应用提供两种非阻塞读取方式,适配不同轮询需求:
- poll方式:定义`pollfd`结构体,指定监视可读事件,通过`poll`函数轮询,超时设置为500ms。若返回值大于0,说明设备可读,调用read读取键值;若超时,执行自定义超时处理,实现带超时的轮询,避免长时间空等。
- select方式:定义`fd_set`集合存放待监视的描述符,设置500ms超时,调用`select`函数轮询。根据返回值判断:超时则自定义处理,出错则自定义处理,有数据可读时用`read`读取键值,逻辑清晰,兼容老版本Linux系统。
3. 运行效果
加载驱动并运行测试应用,按下按键时正常打印键值,查看CPU占用率,同样降至0.0%。由于采用了带超时的轮询,避免了死循环空转,仅在轮询和按键触发时消耗少量CPU,兼顾实时性与资源效率。
四、实验总结与实践建议
1. 核心对比
- 阻塞I/O:代码简洁,CPU占用极低,开发难度低,适合单任务、无需主动查询的简单场景,是大多数传感器、按键设备的优先选择。
- 非阻塞I/O:需配合select或poll使用,应用代码复杂度略高,但支持多设备统一监控,适合需要同时管理多个设备、事件驱动的场景,比如同时监控按键、网络和串口的程序。
2. 避坑要点
- 绝对禁止在应用层用while循环+read直接轮询,这是CPU高占用的根源,所有轮询必须通过阻塞或select/poll实现。
- 阻塞I/O需严格配对休眠与唤醒操作,避免只休眠不唤醒导致进程永久阻塞,唤醒操作必须放在中断等确保设备可用的时机执行。
- 非阻塞I/O的poll函数需合理返回设备状态,避免状态判断错误导致应用轮询逻辑失效,超时时间需根据实际场景合理设置,平衡响应速度和资源消耗。