ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Linux驱动06-Linux中断系统和阻塞io与非阻塞io

Linux驱动06-Linux中断系统和阻塞io与非阻塞io 一、Linux系统中断1.1 中断概念①中断是指CPU在处理事件的过程中遇到了突发事情需要去处理CPU暂停当前程序转去处理突发事件处理完之后又返回原程序被打断的地方继续执行这个过程就叫做中断。② 中断可以分为 内部中断和外部中断。③ 内部中断来源于CPU内部指 CPU 在执行指令过程中由 CPU 内部的运算器、控制器、MMU、标志寄存器、调试机构等检测到的事件。通常与当前指令同步也叫“异常”或“软中断”。④ 外部中断来源自CPU外部指由 CPU 芯片外部的中断源发出通过 CPU 的INTR、NMI等引脚或中断控制器送入 CPU。通常与当前指令无关是异步的也叫“硬件中断”。⑤ 查看中断相关信息/proc/interrupts。⑥ 中断作用处理异步事件随机事件非顺序逻辑的情况。1.2 中断的顶半部和底半部1.2.1 顶半部和底半部介绍① 中断会打断内核进程的正常调度与执行因此在追求高吞吐量的系统设计中必须确保中断服务程序尽可能短小精悍、快速完成。为此Linux采用了一种分层的中断处理架构② 为了在中断执行时间尽量短和中断处理需完成的工作尽量大之间找到一个平衡点 Linux将中断处理程序分解为两个半部 顶半部Top Half 和底半部Bottom Half。③ 上半部也称为中断顶半部负责处理最紧急、最关键的事务通常仅执行少量必要操作且被设计为不可被其他中断打断以确保响应的及时性和可靠性。上半部的使用场景任务不能被其他中断干扰对时间敏感的操作与硬件直接相关的快速响应任务。顶半部Top Half就是驱动通过request_irq()注册的那个handler()函数。硬件中断发生后CPU 会跳转到这个函数执行顶半部运行在硬中断上下文不能睡眠、不能访问用户空间、不能做耗时操作。所以顶半部必须最快尽快让中断线恢复避免丢中断、影响系统实时性。最急只做必须马上做的事。不能休眠一旦睡眠内核无法调度通常直接死锁或崩溃。不能做的事原因msleep()主动睡眠让出 CPU。中断上下文没有进程可调度会死锁。copy_to_user()/copy_from_user()访问用户空间可能触发缺页异常进而睡眠而且中断上下文没有用户地址空间上下文。mutex互斥锁在竞争时会睡眠等待中断上下文不能睡眠。kmalloc(GFP_KERNEL)GFP_KERNEL可能睡眠等待内存回收。耗时循环、复杂计算会长时间关中断或延迟其他中断降低系统响应。可以做的事原因清中断必须立刻做否则中断控制器会持续上报同一中断。读寄存器MMIO 读写通常不睡眠可快速获取硬件状态。唤醒等待队列wake_up()只是把等待进程状态改为可运行并加入运行队列不切换、不睡眠。调度底半部tasklet_schedule()、raise_softirq()、schedule_work()等只是标记“稍后处理”不睡眠。④ 下半部也称为中断底半部用于处理中断事件中绝大多数非紧急、耗时较长的任务。它可被新的中断打断这是其与顶半部的核心区别。由于任务不紧急因此允许延迟执行从而提升系统整体效率。下半部的使用场景处理非实时性要求高的耗时任务可以被中断抢占无需严格保证即时响应。1.2.2中断下半部实现方式中断下半部有以下方式实现(SoftirqTaskletWorkqueue)1软中断Softirq①一种传统的底半部机制适用于系统级任务资源有限执行时机通常在顶半部返回后立即进行。软中断是 Linux 内核核心子系统的“高性能专用底半部”。驱动开发者应该使用 tasklet、工作队列或线程化中断这些上层封装而不是直接碰软中断。②特点运行于中断上下文不允许睡眠、可多核并行但不开放给驱动模块注册。③ 驱动开发者基本不用软中断是因为软中断类型和槽位在内核编译时固定注册接口open_softirq()未导出给内核模块驱动既无法注册自定义软中断通常也没有必要。④ 这段枚举定义在 Linux 内核的include/linux/interrupt.h中是Linux 软中断softirq的静态向量表。每个成员代表一种软中断类型内核在编译时就固定了这些类型数量有限通常不允许驱动开发者随意新增。软中断运行在中断上下文不能睡眠主要用于把中断上半部中耗时的工作延迟到下半部执行。枚举成员编号含义与典型用途HI_SOFTIRQ0高优先级 tasklet使用的软中断。优先级最高最先被处理。TIMER_SOFTIRQ1传统低分辨率定时器timer wheel的超时处理。定时器到期后通过它执行回调。NET_TX_SOFTIRQ2网络发送软中断。处理网络协议栈的发送队列、发送完成清理等。NET_RX_SOFTIRQ3网络接收软中断。处理网卡接收的数据包常用于 NAPI 轮询收包避免在硬中断中处理整个协议栈。BLOCK_SOFTIRQ4块设备软中断。处理块设备 I/O 完成如磁盘请求完成后的后续工作。BLOCK_IOPOLL_SOFTIRQ5块设备 I/O 轮询软中断。用于支持 I/O 轮询模式的块设备如某些高性能 NVMe 场景。TASKLET_SOFTIRQ6普通 tasklet使用的软中断。优先级低于HI_SOFTIRQ用于一般的延迟处理。SCHED_SOFTIRQ7调度器软中断。用于 SMP 系统中的负载均衡、CPU 迁移、调度相关延迟工作。HRTIMER_SOFTIRQ8高精度定时器软中断。处理hrtimer到期回调用于需要高精度时间的场景。RCU_SOFTIRQ9RCU软中断。处理 RCU 回调如call_rcu()注册的延迟释放函数。NR_SOFTIRQS10不是软中断本身而是软中断的总数。用于定义数组大小如softirq_vec[NR_SOFTIRQS]。2Tasklet① 基于软中断实现、专门给驱动用的底半部机制高优先级 tasklet 使用HI_SOFTIRQ普通 tasklet 使用TASKLET_SOFTIRQ② 特点运行在软中断上下文不能休眠不能访问用户空间不能调用可能睡眠的函数。同一个 tasklet 不会在多核并行适合 GPIO、按键等不需要休眠的快速后续处理。③ 由软中断调度顶半部调用tasklet_schedule()后内核在合适的时机执行 tasklet。④ 代码示例//定义 tasklet 处理函数 static void my_tasklet_func(unsigned long data) { struct my_dev *dev (struct my_dev *)data; /* 只做快速、不能休眠的事 */ // 读寄存器、清标志、唤醒等待队列等 } //声明或初始化 tasklet /* 静态声明 */ DECLARE_TASKLET(my_tasklet, my_tasklet_func, (unsigned long)dev); /* 或动态初始化 */ struct tasklet_struct my_tasklet; tasklet_init(my_tasklet, my_tasklet_func, (unsigned long)dev); //顶半部中调度 static irqreturn_t my_handler(int irq, void *dev_id) { struct my_dev *dev dev_id; /* 清中断、读状态 */ writel(...); /* 调度 tasklet */ tasklet_schedule(dev-my_tasklet); return IRQ_HANDLED; } //释放时 tasklet_kill(dev-my_tasklet);3工作队列Workqueue① workqueue 是 Linux 中常用、可休眠的底半部机制运行在内核线程worker thread上下文属于进程上下文。因此允许睡眠适合处理复杂或需要阻塞操作的任务。所以它可以睡眠msleep()、mutex_lock()、等待队列等分配内存kmalloc(GFP_KERNEL)调用可能睡眠的总线操作I2C、SPI、USB 等执行耗时、大量计算② 用法#include linux/workqueue.h 1. 定义工作函数 static void my_work_func(struct work_struct *work) { struct my_dev *dev container_of(work, struct my_dev, my_work); msleep(10); // 可以睡眠 mutex_lock(dev-lock); // 可以用互斥锁 kmalloc(..., GFP_KERNEL); // 可以用可能睡眠的分配 i2c_transfer(...); // 可以调用 I2C/SPI 等可能睡眠的总线 } 2. 声明或初始化 work DECLARE_WORK(my_work, my_work_func); 或者动态初始化 INIT_WORK(dev-my_work, my_work_func); 3. 顶半部中调度 schedule_work(dev-my_work); // 放入系统默认工作队列 // 或 queue_work(system_wq, dev-my_work); // 指定工作队列 4. 取消 cancel_work_sync(dev-my_work);综上三者各有侧重软中断与 tasklet 适用于高实时性、低延迟的中断处理而工作队列则更适合需要灵活性和睡眠能力的场景。1.2.3 硬件中断上下文Hardirq 顶半部① CPU 收到外设硬件电信号立刻打断当前所有任务执行 request_irq 注册的 ISR 中断服务函数运行环境为硬件中断上下文。② 归属狭义中断上下文优先级最高③ 载体独立中断栈无绑定 task_struct 进程④ 对应场景中断上半部 ISR1.2.4 软中断上下文Softirq / Tasklet① 硬中断退出前触发、或空闲时 ksoftirqd 内核线程调度执行 属于广义中断上下文tasklet 是软中断的封装子集。② 归属广义中断上下文优先级低于硬中断、高于普通进程③ 载体无专属进程临时借用 CPU 执行环境④ 对应场景中断底半部 tasklet、原生 softirq网卡 / 块设备1.2.5 内核线程上下文进程上下文 Workqueue 归属此类① 内核线程kthread、 events 工作线程属于纯内核态进程上下文用户进程系统调用open/read/write也属于进程上下文二者同规则。工作队列 workqueue 完全运行在此环境。② 归属进程上下文调度器统一调度③ 载体拥有完整 task_struct 、内核栈、调度属性④ 对应场景 workqueue、自定义 kthread、驱动文件操作接口1.3 中断相关函数接口注册/释放中断request_irq、free_irq、devm_request_irq指定 IRQ 开关disable_irq、disable_irq_nosync、enable_irq当前 CPU 中断开关local_irq_save、local_irq_disable、local_irq_restore函数名功能作用范围/对象关键说明request_irq注册中断处理程序绑定中断号与处理函数指定 IRQ需与free_irq配对失败返回负值free_irq释放已注册的中断资源指定 IRQ与request_irq配对使用disable_irq禁止指定中断指定 IRQ会等待正在执行的中断处理程序结束可能睡眠不能在中断上下文调用disable_irq_nosync禁止指定中断立即返回不等待中断处理结束指定 IRQ可在中断上下文调用与enable_irq配对enable_irq使能指定中断指定 IRQ与disable_irq/disable_irq_nosync配对不能重复使能local_irq_save禁用当前 CPU 所有中断并保存中断状态当前 CPU多 CPU 下只影响本 CPU与local_irq_restore配对local_irq_disable禁止当前 CPU 所有中断当前 CPU多 CPU 下只影响本 CPU需配对恢复local_irq_restore恢复之前保存的中断状态当前 CPU与local_irq_save配对devm_request_irq自动管理资源的中断注册函数指定 IRQ 设备设备卸载时自动释放中断资源通常无需手动free_irq1.3.1request_irq在 Linux 内核中若要使用某个中断必须先进行申请。request_irq函数用于申请中断由于该函数可能引发睡眠因此不能在中断上下文或其他禁止睡眠的代码段中调用。此外request_irq会自动激活使能中断因此无需手动执行中断使能操作。int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev); 功能注册中断处理函数绑定中断号与处理函数 参数 irq要申请的中断号。 handler中断处理函数指针类型为 irq_handler_t即 irqreturn_t (*)(int, void *)。 flags中断管理相关选项的位掩码控制触发方式和共享属性。这些标志定义在 include/linux/interrupt.h 中可以通过按位或|组合使用。如果不需要任何特殊行为也可以传入 0。 name设备名称会出现在 /proc/interrupts 中。 dev共享中断时用于区分设备的唯一标识通常会指向设备私有数据结构 非共享中断可传 NULL但推荐传入设备结构指针。 返回值成功返回 0失败返回负错误码如 -EBUSY 表示中断线已被占用。 flags标识 IRQF_TRIGGER_NONE 0x00000000 不指定触发方式沿用已有的配置如固件或机器初始化时的设置 IRQF_TRIGGER_RISING 0x00000001 上升沿触发 IRQF_TRIGGER_FALLING 0x00000002 下降沿触发 IRQF_TRIGGER_HIGH 0x00000004 高电平触发 IRQF_TRIGGER_LOW 0x00000008 低电平触发 IRQF_TRIGGER_PROBE 0x00000010 用于探测中断 IRQF_SHARED 0x00000080 允许多个设备共享同一中断线。共享时每个驱动都必须设置此标志且 dev_id 必须唯一 IRQF_ONESHOT 0x00002000 中断线在硬中断处理程序完成后保持禁用直到线程化处理程序运行完毕。必须用于线程化中断1.3.2free_irq使用中断时需通过request_irq函数申请使用完毕后应调用free_irq函数释放。若中断非共享free_irq会移除中断处理函数并禁用该中断。共享中断只有在最后一个处理函数被释放时该中断线才会真正被禁止。如果使用了devm_request_irq则不应手动调用free_irq否则会破坏资源管理机制。void free_irq(unsigned int irq, void *dev_id); 功能释放已经注册的中断资源 参数 irq由 request_irq 申请的硬件中断号。 dev_id必须与 request_irq 时传入的 dev_id 完全一致同一个指针否则会触发内核错误。一般为 NULL 返回值无1.3.3enable_irq disable_irqvoid enable_irq(unsigned int irq); void disable_irq(unsigned int irq); 作用enable_irq 和 disable_irq 用于使能和禁止指定的中断必须配对调用 参数 irq就是要禁止的中断号。disable_irq 返回值无 注意 disable_irq 禁止指定中断线并等待当前正在执行的中断处理程序完成后再返回1.3.4disable_irq_nosyncvoid disable_irq_nosync(unsigned int irq); 功能与 disable_irq 相同禁止指定中断线但不等待当前正在执行的处理程序完成立即返回。 参数 irq就是要禁止的中断号。disable_irq 返回值无 关键区别与用途 由于不睡眠可以在中断上下文中调用。 速度更快但可能与正在运行的中断处理程序产生竞态条件使用时需谨慎。 同样遵循嵌套计数规则需与 enable_irq 配对使用。1.3.5local_irq_enable local_irq_disablevoid local_irq_enable(void); void local_irq_disable(void); 功能local_irq_enable 用于使能当前处理器所有中断系统local_irq_disable 用于禁止当前处理器所有中断系统。上述两个函数存在以下问题若任务 A 调用local_irq_disable禁用全局中断持续 10 秒而在禁用 2 秒后任务 B 开始运行并同样调用local_irq_disable再次禁用中断持续 3 秒。3 秒后任务 B 调用local_irq_enable恢复全局中断。此时仅过去 2 3 5 秒全局中断即被打开。然而任务 A 原本需禁用中断长达 10 秒中断过早恢复可能导致系统状态不一致甚至引发系统崩溃。1.3.6local_irq_save local_irq_restorevoid local_irq_save(unsigned long flags); 功能保存当前 CPU 的中断状态到 flags 变量中然后禁用当前 CPU 的所有中断。 void local_irq_restore(unsigned long flags); 功能使用 local_irq_save 保存的 flags 值恢复当前 CPU 的中断状态可能是开启也可能是关闭。 注意两个必须在同一函数中配对且 flags 不能跨函数传递。恢复的状态严格取决于保存时的实际中断使能情况而非简单地将中断打开。1.3.7devm_request_irqint devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id); 功能与 request_irq 功能相同但增加了设备资源管理devres 支持。多出的 dev 参数指定所属设备。 核心优势使用此函数注册的中断会在驱动 detach 时自动释放无需在 remove 函数中显式调用 free_irq。1.3.8of_irq_getof_irq_get和gpio_to_irq是 Linux 驱动开发中从不同途径获取中断号的常用接口。核心区别在于of_irq_get直接解析设备树中设备节点自身的中断属性而gpio_to_irq则是将一个已知的 GPIO 编号映射为其对应的中断号。int of_irq_get(struct device_node *dev, int index); 功能从设备树节点自身获取中断信息。它读取设备节点的 interrupts 属性并根据 interrupt-parent 指定的中断控制器将其解析为 Linux 内核使用的虚拟中断号。 参数 dev指向设备树节点的指针即要获取中断信息的设备节点。 index索引号表示从该节点的 interrupts 属性中获取第几个中断从 0 开始计数。当设备节点只定义了一个中断时通常传入 0。 返回值 成功返回映射后的 Linux 中断号virq。 失败 返回 0表示中断映射失败。 返回 -EPROBE_DEFER表示中断控制器irq domain尚未创建驱动应延迟探测probe。 返回其他负错误码表示发生了其他错误。 注意与 irq_of_parse_and_map 不同of_irq_get 返回的中断号由 devres 自动管理无需手动调用 irq_dispose_mapping() 释放。1.3.9gpio_to_irqint gpio_to_irq(unsigned gpio); 功能将 GPIO 编号转换为中断号它通过 GPIO 控制器底层驱动的 to_irq 回调函数完成映射。当驱动代码中拥有的是一个 GPIO 编号但需要为其注册中断处理时就需要使用此函数。 参数 gpio一个有效的 GPIO 编号无符号整数。 返回值 成功返回该 GPIO 对应的 Linux 中断号。此返回值可直接传递给 request_irq 或 free_irq 使用。 失败返回一个负的错误码errno。需要特别注意的是并非所有 GPIO 都能用作中断源如果该 GPIO 不支持中断功能映射将失败。 注意事项 使用前必须先用 gpio_direction_input() 将该 GPIO 配置为输入模式。 该函数不允许休眠其执行开销很小通常仅需简单的算术运算。 在设备树系统中如果 GPIO 控制器同时也是中断控制器gpio_to_irq 才能正常工作1.4 按键中断适配中断步骤① 适配设备树引用 pinctrl引用gpio引用中断状态 - okay设备树在设备的节点中加入属性 interrupt-parent interruptsvi linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/boot/dts/imx6ull-alientek-emmc.dts putekey { #address-cells 1; #size-cells 1; compatible PT,pute-key; status okay; pinctrl-names default; pinctrl-0 pinctrl_ptkey; gpio-key gpio1 18 GPIO_ACTIVE_LOW; interrupt-parent gpio1; interrupts 18 IRQ_TYPE_EDGE_FALLING; }; interrupt-parent指定父中断。当GPIO1_18 引脚电平发生变化外部中断到来先找GPIO1 控制器。 interrupts需要描述引脚号上升触发(IRQ_TYPE_EDGE_RISING)下降触发(IRQ_TYPE_EDGE_FALLING)双边沿触发(IRQ_TYPE_EDGE_BOTH) 在dtsi文件中 gpio1,关联到了GIC gpio1: gpio0209c000 { compatible fsl,imx6ul-gpio, fsl,imx35-gpio; reg 0x0209c000 0x4000; interrupts GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH //和GIC关联的中断号 GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH; gpio-controller; #gpio-cells 2; interrupt-controller; 可以作为其他 父中断控制器 #interrupt-cells 2; };② misc_register 注册杂项设备③ of_find_node_by_path 函数获得设备树节点④ of_get_named_gpio 获得gpio编号⑤ gpio_request 向系统申请GPIO资源⑥ gpio_direction_output/gpio_direction_input 设置GPIO的输入输出⑦ of_irq_get/gpio_to_irq 从设备树中获得中断号。⑧ request_irq 申请中断填入中断处理函数(重要中断到来后需要做的事情);⑨ 编写中断处理函数需要注意返回值。enum irqreturn { IRQ_NONE , //转给下一个注册的中断处理函数表示这个不是本驱动要处理的中断最终都没有驱动处理内核报 “nobody cared”。因为中断处理的时候有可能中断号共享的状态。 IRQ_HANDLED , //大部分有这个。表示中断正常处理了。内核不再继续传递 IRQ_WAKE_THREAD , //唤醒 threaded irq 线程去处理需要进一步处理 };⑩ 修改app⑪ 继续改进希望阻塞方式读取。需要加入阻塞功能推荐操作 中断方式 等待队列。驱动部分代码//中断处理函数 static irqreturn_t key_irq_handler(int irq_num, void * arg) { if(led_stauts 0) { led_stauts 1; } else { led_stauts 0; } gpio_set_value(led_gpio, led_stauts); return IRQ_HANDLED; } //杂项设备结构体 static struct miscdevice misc_dev { .minor MISC_DYNAMIC_MINOR, //这个宏为255但是次设备号不一定是255;自动获取次设备号 .name DEV_NAME, //设备节点名 和 /sys/class/misc/xxx 两者名称相同 .fops fops //杂项设备文件操作函数集合 }; static int __init mydrv_init(void) { int ret 0; /注册杂项设备 ret misc_register(misc_dev); if(ret) { pr_err(misc_register error, errno:%d\n, ret); return ret; } pnode_key of_find_node_by_path(/putekey); if(!pnode_key) { pr_err(of_find_node_by_path error\n); ret -ENODEV; goto del_misc; } //参数三是0在设备树中代表数组0就代表下标是0对应的GPIO //返回值成功正数 失败负数错误号 key_gpio of_get_named_gpio(pnode_key, gpio-key, 0); if(key_gpio 0) { ret key_gpio; pr_err(of_get_named_gpio error, errno:%d\n, ret); goto del_misc; } //参数二标签随便赋值 ret gpio_request(key_gpio, gpio_for_key); if(ret) { pr_err(gpio_request error, errno:%d\n, ret); goto del_misc; } ret gpio_direction_input(key_gpio); if(ret) { pr_err(gpio_direction_input error, errno:%d\n, ret); goto del_gpio; } /获取中断号 /key_irq_num of_irq_get(pnode_key, 0); key_irq_num gpio_to_irq(key_gpio); if(key_irq_num 0) { ret key_irq_num; pr_err(of_irq_get error, errno:%d\n, ret); goto del_gpio; } pr_info(key_irq_num is %d\n, key_irq_num); //申请中断 ret request_irq(key_irq_num, key_irq_handler, IRQF_TRIGGER_FALLING, key_interrupt, NULL); if(ret 0) { pr_err(request_irq error, errno:%d\n, ret); goto del_gpio; } //要实现按键控制 LED 就得初始化LED以下是LED部分 pnode_led of_find_node_by_path(/puteled); if(!pnode_led) { ret -ENODEV; pr_err(of_find_node_by_path error, errno:%d\n, ret); goto del_gpio; } led_gpio of_get_named_gpio(pnode_led, gpio-led, 0); if(led_gpio 0) { ret led_gpio; pr_err(of_get_named_gpio error, errno:%d\n, ret); goto del_gpio; } ret gpio_request(led_gpio, led-gpio); if(ret) { pr_err(gpio_request error, errno:%d\n, ret); goto del_gpio; } ret gpio_direction_output(led_gpio, 1); if(ret) { pr_err(gpio_direction_output error, errno:%d\n, ret); goto del_led_gpio; } pr_info(key_irq drv init finish!\n); return 0; del_led_gpio: gpio_free(led_gpio); del_gpio: gpio_free(key_gpio); del_misc: misc_deregister(misc_dev); return ret; } static void __exit mydrv_exit(void) { gpio_free(led_gpio); free_irq(key_irq_num, NULL); gpio_free(key_gpio); //卸载杂项设备 misc_deregister(misc_dev); pr_info(hello, bbb\n); return; } module_init(mydrv_init); module_exit(mydrv_exit); MODULE_LICENSE(GPL); MODULE_DESCRIPTION(this drv is key test\n); MODULE_AUTHOR(dl);应用层代码int main(int argc, char **argv) { int fd 0; fd open(/dev/mykey0, O_RDWR); if (fd 0) { perror(open); return -1; } char buffer[10] {0}; int sw 0; int key_status 0; while (1) { read(fd, key_status, sizeof(key_status)); if(key_status 1) { sw 1; printf(key status is on\n); } else if(key_status 0) { sw 0; printf(key status is off\n); } } close(fd); return 0; }开发板执行insmod key_drv.ko cat /proc/interrupts二、阻塞IO 与非阻塞 IO按键驱动的实现可以基于两种方式① 轮训非阻塞IO方式;② 阻塞中断唤醒方式 - 等待队列 中断;2.1 阻塞和非阻塞简介①这里的 IO 指的是 Input/Output也就是输入/输出是应用程序对驱动设备的输入/输出操作。当应用程序对设备驱动进行操作的时候如果不能获取到设备资源那么阻塞式 IO 就会将应用程序对应的线程挂起直到设备资源可以获取为止。对于非阻塞 IO应用程序对应的线程不会挂起它要么一直轮询等待直到设备资源可以使用要么就直接放弃。②阻塞式 IO应用程序调用 read 函数从设备中读取数据当设备不可用或数据未准备好的时候就会进入到休眠态。等设备可用的时候就会从休眠态唤醒然后从设备中读取数据返回给应用程序。②非阻塞 IO应用程序使用非阻塞访问方式从设备读取数据当设备不可用或数据未准备好的时候会立即向内核返回一个错误码表示数据读取失败。应用程序会再次重新读取数据这样一直往复循环直到数据读取成功。Linux 中文件默认就是阻塞模式fd open(/dev/xxx_dev, O_RDWR); // 阻塞方式 fd open(/dev/xxx_dev, O_RDWR | O_NONBLOCK); // 非阻塞方式2.2 等待队列(阻塞io)① 阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态这样可以将 CPU 资源让出来。但是当设备文件可以操作的时候就必须唤醒进程一般在中断函数里面完成唤醒工作。 Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作② 函数接口函数/宏功能关键说明init_waitqueue_head初始化等待队列头动态分配wait_queue_head_t时使用DECLARE_WAIT_QUEUE_HEAD定义并初始化等待队列头静态定义等价于定义变量 init_waitqueue_headDECLARE_WAITQUEUE定义并初始化等待队列元素参数通常为(name, tsk)绑定到指定任务手动等待逻辑中使用add_wait_queue将等待队列元素添加到等待队列头与remove_wait_queue配对常配合手动设置任务状态和调度remove_wait_queue从等待队列头移除等待队列元素与add_wait_queue配对wait_event等待条件成立不可中断不能被信号打断CtrlC也无效只能在进程上下文使用wait_event_interruptible等待条件成立可中断收到信号会返回-ERESTARTSYS驱动中最常用wait_event_timeout等待条件成立不可中断带超时返回剩余 jiffies返回0表示超时wait_event_interruptible_timeout等待条件成立可中断带超时返回剩余 jiffies、0或-ERESTARTSYSwake_up唤醒等待队列中的等待者可唤醒不可中断和可中断睡眠的进程可在中断上下文调用wake_up_interruptible只唤醒可中断睡眠的等待者通常与wait_event_interruptible配对不唤醒不可中断睡眠的进程注意等待队列相关函数会睡眠不能在中断上下文调用但wake_up/wake_up_interruptible可以在中断上下文调用。驱动中常见配对wait_event_interruptible()wake_up_interruptible()若不允许被信号打断则用wait_event()wake_up()。wait_event系列宏内部已经帮你完成了等待队列元素的加入、移除、状态设置和调度一般不需要手动DECLARE_WAITQUEUEadd_wait_queue。③ 等待队列使用步骤1先看wait_event_interruptible内部做了什么再理解整个使用步骤会更清楚。#define wait_event_interruptible(wq, condition) \ ({ \ int __ret 0; \ if (!(condition)) \ __ret __wait_event_interruptible(wq, condition); \ __ret; \ }) 它的核心逻辑是 如果 condition 已经成立直接返回不等待。 如果 condition 不成立把当前进程加入等待队列设置为 TASK_INTERRUPTIBLE然后主动让出 CPU 睡眠。 被 wake_up_interruptible 唤醒后重新检查 condition。 如果条件成立从等待队列移除返回 0。 如果被信号打断返回 -ERESTARTSYS。2定义等待队列头和条件变量static DECLARE_WAIT_QUEUE_HEAD(key_waitq); static int key_pressed 0; /* 条件变量初始为 0 */ DECLARE_WAIT_QUEUE_HEAD 等价于定义 init_waitqueue_head。 key_pressed 0 表示事件还没发生。3初始化等待队列头如果没静态定义static wait_queue_head_t key_waitq; static int __init my_init(void) { init_waitqueue_head(key_waitq); ... } 如果已经用了 DECLARE_WAIT_QUEUE_HEAD这一步就不需要了。4注册中断处理函数ret request_irq(irq, key_handler, IRQF_TRIGGER_FALLING, key_irq, NULL); 中断处理函数中负责 清中断 置 key_pressed 1 调用 wake_up_interruptible(key_waitq)。 static irqreturn_t key_handler(int irq, void *dev_id) { /* 清中断 */ ... key_pressed 1; /* 先置标志 */ wake_up_interruptible(key_waitq); /* 再唤醒 */ return IRQ_HANDLED; }5在需要阻塞的地方等待按键场景通常在 read() 中等待 static ssize_t key_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { int ret; /* 先置 0表示“我要开始等了” */ key_pressed 0; /* 睡眠等待直到 key_pressed 被中断置 1 */ ret wait_event_interruptible(key_waitq, key_pressed); if (ret) return ret; /* 被信号打断返回 -ERESTARTSYS */ /* 事件已发生拷贝数据给用户 */ if (copy_to_user(buf, key_value, sizeof(key_value))) return -EFAULT; return sizeof(key_value); }6中断到来唤醒阻塞进程中断处理函数里 key_pressed 1; wake_up_interruptible(key_waitq); 唤醒后wait_event_interruptible 内部会重新检查 key_pressed发现为 1条件成立于是返回 0read 继续往下执行。flag 的置 0 和置 1 时机操作时机原因flag 0在调用wait_event_interruptible之前表示“事件还没发生我准备睡了”flag 1在中断处理函数中wake_up之前表示“事件发生了条件成立了”关键点先置 0 再等待如果先等待再置 0可能中断已经发生并置 1但你随后又把 0 写回去导致条件永远不成立进程永久睡眠。先置 1 再唤醒wake_up_interruptible唤醒进程后进程会重新检查flag。如果flag还是 0它会继续睡。所以必须先把flag置 1再唤醒。中断可能发生在置 0 和等待之间如果中断恰好在flag 0之后、wait_event_interruptible之前发生那么中断置flag 1并调用wake_up但此时等待队列里还没有这个进程唤醒无效。随后进程调用wait_event_interruptible它会先检查flag发现已经是 1条件成立直接返回不会睡眠。2.3 轮询非阻塞io① 如果用户应用程序以非阻塞的方式访问设备设备驱动程序就要提供非阻塞的处理方式也就是轮询。 poll、 epoll 和 select 可以用于处理轮询应用程序通过 select、 epoll 或 poll 函数来查询设备是否可以操作如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调用 select、 epoll 或 poll 函数的时候设备驱动程序中的 poll 函数就会执行因此需要在设备驱动程序中编写 poll 函数。② Linux 驱动下的 poll 操作函数1当应用程序调用 select 或 poll 函数来对驱动程序进行非阻塞访问的时候驱动程序 file_operations 操作集中的 poll 函数就会执行。所以驱动程序的编写者需要提供对应的 poll 函数 poll函数原型如下所示unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait) 参数 filp 要打开的设备文件(文件描述符)。 wait 结构体 poll_table_struct 类型指针 由应用程序传递进来的。一般将此参数传递给poll_wait 函数。 返回值:向应用程序返回设备或者资源状态可以返回的资源状态如下 POLLIN 有数据可以读取。 POLLPRI 有紧急的数据需要读取。 POLLOUT 可以写数据。 POLLERR 指定的文件描述符发生错误。 POLLHUP 指定的文件描述符挂起。 POLLNVAL 无效的请求。 POLLRDNORM 等同于 POLLIN普通数据可读2我们需要在驱动程序的 poll 函数中调用 poll_wait 函数 poll_wait 函数不会引起阻塞只是将应用程序添加到 poll_table 中 poll_wait 函数原型如下void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p) 参数 wait_address 是要添加到 poll_table 中的等待队列头 p 就是 poll_table就是 file_operations 中 poll 函数的 wait 参数
返回列表