Linux内核学习笔记——中断,tasklet和工作队列

本文深入探讨了Linux操作系统中中断处理的机制,包括硬件中断与内核中断号的映射、中断处理函数的注册及调用流程等内容。通过解析/proc/interrupts文件,读者能够了解到不同设备中断的具体实现细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

对于操作系统来说,中断的实现和裸机代码是不一样的,对于裸机代码来说,它的处理过程更符合下面这个流程,我们在编写裸板驱动代码的时候,一般处理的是硬件中断,而且中断号也是硬件中断号,这个中断号是和CPU寄存器的连线方式直接相关的,只要连线方式一确定,那么外设对应的中断号就确定了。

比如在下面这个图中,中断源可以是TIMER或者BUTTON,对应的中断号是6和16,当这些外设产生中断信号的时候,它首先通过中断控制器的判断将信号传给中断核心层,CPU收到该信号以后会做一些保存现场的工作,然后跳到中断向量表查找对应的中断处理程序并进行处理。

但是操作系统的处理方式并不是这样的,内核对这些驱动的架构都进行了抽象,让编写驱动的时候要符合一定的规则,这样一来,让驱动的编写过程变成了对驱动架构的填充过程,这确实省了不少事,所以这里简单记录一下不同驱动架构中,中断的接收和处理过程是怎么穿梭其中的。

对于内核的驱动架构来说,它不在如过家家似的裸机代码了,首先要对中断号进行映射,也要对外设的数据寄存器以及代码寄存器的地址进行映射。比如寄存器接受到中断号是16的中断信号,当这个信号映射到内核后,这个中断号是160了,当然这两个中断信号其实是同一个信号,只是在不同层之间的不同表示而已,他们是键值对的关系。

linux 驱动中断处理相关数据结构

在Linux内核中有一个表存放中断处理程序叫irq_desc[] ,每个个IRQ中断线,Linux都用一个irq_desc_t数据结构来描述,我们把它叫做 IRQ描述符, NR_IRQS 个IRQ形成一个全局数组 irq_desc[],其定义在 include/linux/irq.h 中:

数据结构irq_desc里有一项叫做 irqaction。它里面就包含着 handler() 函数。irq_desc_t 的 action 成员实际是一个 irqaction 结构的链表,因为多个设备可能共享一个中断源, 每个设备都必须提供一个 irqction 结构挂入 action 链表。

struct irqaction 结构记录当中断发生时具体的处理函数,定义在 include/linux/interrupt.h 文件中。

中断号IRQ的确定

不管对于哪种类型的驱动,都可以在 /proc/interrupts下找到映射完以后的kernel中断号:

第一列是kernel map后的中断号, 倒数第三列是中断的触发方式(上升沿、下降沿),最后一列是注册中断处理子程序是我们分配的描述信息

cat /proc/interrupts 
           CPU0       CPU1       CPU2       CPU3       
 20:  109111782 1287889318  833179606 1002449638       GIC  20  arch_timer
 35:          0          0          0          0       GIC  35  watchdog bark
 97:          0          0   30564292          0       GIC  97  edma_eth_tx0
 98:          0          0          6          0       GIC  98  edma_eth_tx1
 99:          0          0     131107          0       GIC  99  edma_eth_tx2
100:          0          0          4          0       GIC 100  edma_eth_tx3
101:          0          0          0      22339       GIC 101  edma_eth_tx4
102:          0          0          0          0       GIC 102  edma_eth_tx5
103:          0          0          0     205881       GIC 103  edma_eth_tx6
104:          0          0          0          0       GIC 104  edma_eth_tx7
105:   28786218          0          0          0       GIC 105  edma_eth_tx8
106:          4          0          0          0       GIC 106  edma_eth_tx9
107:      99063          0          0          0       GIC 107  edma_eth_tx10
108:          2          0          0          0       GIC 108  edma_eth_tx11
109:          0   29040737          0          0       GIC 109  edma_eth_tx12
110:          0          4          0          0       GIC 110  edma_eth_tx13
111:          0      78467          0          0       GIC 111  edma_eth_tx14
112:          0          8          0          0       GIC 112  edma_eth_tx15
127:          2          0          0          0       GIC 127  78b5000.spi
129:    5171604          0          0          0       GIC 129  i2c-msm-v2-irq
133:          0          0          0          0       GIC 133  sps
139:     238076          0          0          0       GIC 139  msm_serial_hsl0
164:          0          0          0          0       GIC 164  xhci-hcd:usb1
168:          0          0          0          0       GIC 168  xhci-hcd:usb3
173:          0          0          0          0       GIC 173  int_msi
174:       1742          0          0   69415235       GIC 174  wlan_pci
183:          0          0          0          0       GIC 183  int_pls_link_down
200:       1924          0   91612296          0       GIC 200  wlan_ahb
201:       1686  235856973          0          0       GIC 201  wlan_ahb
239:          0          0          0          0       GIC 239  sps
270:          0          0          0          0       GIC 270  sps
272:   41376101          0          0          0       GIC 272  edma_eth_rx0
274:          0   40781979          0          0       GIC 274  edma_eth_rx2
276:          0          0   38000530          0       GIC 276  edma_eth_rx4
278:          0          0          0   41502836       GIC 278  edma_eth_rx6
IPI0:          0          0          0          0  CPU wakeup interrupts
IPI1:          0          0          0          0  Timer broadcast interrupts
IPI2:   72607346   70337041   74354456   73214941  Rescheduling interrupts
IPI3:         14         21         17         20  Function call interrupts
IPI4:   28106763      93631   23136542   20144633  Single function call interrupts
IPI5:          0          0          0          0  CPU stop interrupts
IPI6:    6705108    6830143    5972690    5878126  IRQ work interrupts
IPI7:          0          0          0          0  completion interrupts
Err:          0

当然也可以在/proc/irq下搜索更多信息(find -name *wlan*),这个目录会列出所有内核支持的中断号(注意不是硬件中断),对于那些有绑定驱动的中断号,一般都会在对应的中断号目录下显示对应的名称,比如wlan_pci:

/proc/irq/174# ls
affinity_hint      smp_affinity       spurious
node               smp_affinity_list  wlan_pci

对于PCI总线驱动来说,在kernel启动的时候就会对外围设备进行扫描,并将这些外围PCI总线设备添加到链表里面,比如下面的代码,在pci_common_init的时候,在pci_fixup_irqs函数里面指明,当然PCI设备的irq号不是已经在配置空间里明确指定了的,在总线驱动初始化的过程中会从配置空间里面直接读取,如果驱动没有从配置空间里读取,这里也可以保证他不会丢失。

https://blog.youkuaiyun.com/eydwyz/article/details/72285858

关于IRQ号的映射关系,这篇文章介绍比较详细:

Linux kernel的中断子系统之(二):IRQ Domain介绍


  1. void __init pci_common_init(struct hw_pci *hw)  
  2. {  
  3.     struct pci_sys_data *sys;  
  4.   
  5.     INIT_LIST_HEAD(&hw->buses);  
  6.   
  7.     if (hw->preinit)  
  8.         hw->preinit();  
  9.     pcibios_init_hw(hw);  
  10.     if (hw->postinit)  
  11.         hw->postinit();  
  12.   
  13.     pci_fixup_irqs(pcibios_swizzle, pcibios_map_irq);

当然对于一些字符设备也可以从下面的方式,或者查看对应的DTS文件,染后经过一定的转换

$ grep -rns "IRQ" arch/arm/* | grep "UART" | grep "16"

$ vi arch/arm/plat-s5p/include/plat/irqs.h

关于更多中断子系统的资料可以查看这些文章:

Linux中断(interrupt)子系统之五:软件中断(softIRQ)
Linux中断(interrupt)子系统之四:驱动程序接口层 & 中断通用逻辑层
Linux中断(interrupt)子系统之三:中断流控处理层
Linux中断(interrupt)子系统之二:arch相关的硬件封装层
Linux中断(interrupt)子系统之一:中断系统基本原理

中断处理函数的注册

当我们确定一个事件的中断号以后,就可以对这个中断号注册中断处理函数,这也是中断处理的核心。

static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
{
    return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
extern void free_irq(unsigned int, void *);
request_irq 函数的参数如下:
unsigned int irq // 请求的中断号
irqreturn_t (*handler) // 安装的处理函数指针. .
unsigned long flags // 与中断管理相关的选项的位掩码(后面描述).
const char *dev_name //这个传递给 request_irq 的字串用在 /proc/interrupts 来显示中断的拥有者(/proc/interupts 最后一列)
void *dev_id // 用作共享中断线的指针. 它是一个独特的标识, 用在当释放中断线时置为 NULL, 但是使用这个项指向设备结构不管如何是个好主意. 我们将在"实现一个处理"一节中看到 dev_id 的
一个实际应用.


其中第一个参数是上面我们说的kernel的中断号

第二个参数是该终端对应的终端处理子函数

第三个参数用来控制中断类型,比如共享中断和快速中断

dev_id用来给共享中断指定处理线程

中断的处理过程

* 中断信号由外部设备发送到中断控制器,中断控制器根据IRQ号转换成相应的中断向量号传给CPU 。
* CPU接收中断后,保存现场,根据中断向量号到IDT中查找相应的处理函数。对于IRQ n的中断,它的处理函数IRQn_interrutp()。
* 调用do_IRQ()函数。该函数完成对中断控制器确认、设置中断源状态等动作, 接着它会根据IRQ号找到描述中断具体动作的irqaction结构变量action,执行如下代码:
    处理 irq 事件的函数,主要实现在 kernel/irq/handle.c 中,其中 res = action->handler (irq, action->dev_id);
    执行了调用用户注册的 handler() 函数的功能。

    irqreturn_t handle_irq_event(struct irq_desc *desc)
    handle_irq_event_percpu(struct irq_desc *desc, struct irqaction *action)
* 最后do_IRQ()函数要检查是否有软中断,如有则调用do_softirq()执行软中断。
* 跳转到 ret_from_intr退出,恢复中断前的现场。


中断处理我们都知道包括中断处理子程序的上半部分和下半部分,上半部分是需要快速执行并且不能阻塞的,主要的任务交给下半部分处理。我们重点讲讲下半部分,下半部分包括:tastlet,工作队列和软中断。

关于这些buttom half的内容就不赘述了,这些文章有详细分析


linux kernel的中断子系统之(九):tasklet

Concurrency Managed Workqueue之(一):workqueue的基本概念

linux kernel的中断子系统之(八):softirq


 关于这些不同BH驱动架构的实例也可以参照这个PDF去练练

ARM驱动开发



<think>我们正在讨论Linux内核tasklet工作队列(workqueue)的创建方法。根据提供的引用材料,我们可以总结如下:1.**tasklet**:-是一种软中断(softirq)机制,运行在中断上下文中,不能睡眠。-创建方法:使用`tasklet_init()`函数初始化,然后使用`tasklet_schedule()`调度执行。-销毁方法:在模块卸载时使用`tasklet_kill()`。2.**工作队列(workqueue)**:-将工作推迟到内核线程中执行,运行在进程上下文中,可以睡眠。-创建方法:可以使用系统默认的工作队列(如`eventd_wq`)或者自己创建(`create_workqueue`)。-调度方法:使用`schedule_work()`或`queue_work()`将工作添加到队列。下面我将详细说明两者的创建步骤示例代码。###1.Tasklet的创建与使用####步骤:1.**定义tasklet结构体**:`structtasklet_struct`2.**初始化tasklet**:使用`tasklet_init()`函数,指定处理函数参数。3.**调度tasklet**:使用`tasklet_schedule()`将tasklet加入软中断队列等待执行。4.**销毁tasklet**:在模块退出时使用`tasklet_kill()`。####示例代码(基于引用[2]):```c#include<linux/init.h>#include<linux/module.h>#include<linux/interrupt.h>staticstructtasklet_structmy_tasklet;//Tasklet处理函数staticvoidtasklet_handler(unsignedlongdata){printk(KERN_INFO"Taskletisrunning\n");}staticint__initmy_module_init(void){//初始化tasklet,关联处理函数tasklet_init(&my_tasklet,tasklet_handler,0);//调度tasklettasklet_schedule(&my_tasklet);return0;}staticvoid__exitmy_module_exit(void){//销毁tasklettasklet_kill(&my_tasklet);printk(KERN_INFO"Moduleexit\n");}module_init(my_module_init);module_exit(my_module_exit);MODULE_LICENSE("GPL");```###2.Workqueue的创建与使用####两种方式:-**使用系统默认工作队列**:无需自己创建队列,直接定义`work_struct`并使用`schedule_work()`调度。-**创建自定义工作队列**:使用`create_workqueue()`创建,然后使用`queue_work()`调度。####步骤(自定义工作队列):1.**创建工作队列**:`create_workqueue(name)`2.**定义工作结构体**:`structwork_struct`3.**初始化工作**:`INIT_WORK(work,work_func)`4.**调度工作**:`queue_work(workqueue,work)`5.**销毁工作队列**:使用`destroy_workqueue()`####示例代码(自定义工作队列):```c#include<linux/init.h>#include<linux/module.h>#include<linux/workqueue.h>staticstructworkqueue_struct*my_wq;staticstructwork_structmy_work;//工作处理函数staticvoidwork_handler(structwork_struct*work){printk(KERN_INFO"Workqueuehandlerisrunning\n");}staticint__initmy_module_init(void){//创建工作队列my_wq=create_workqueue("my_workqueue");if(!my_wq)return-ENOMEM;//初始化工作,关联处理函数INIT_WORK(&my_work,work_handler);//调度工作queue_work(my_wq,&my_work);return0;}staticvoid__exitmy_module_exit(void){//销毁工作队列destroy_workqueue(my_wq);printk(KERN_INFO"Moduleexit\n");}module_init(my_module_init);module_exit(my_module_exit);MODULE_LICENSE("GPL");```###关键区别总结|特性|Tasklet|Workqueue||---------------------|----------------------------------|----------------------------||执行上下文|中断上下文(不可睡眠)|进程上下文(可睡眠)||并发性|同类型tasklet不能并发执行|可并发执行(多线程)||创建方式|`tasklet_init`+`tasklet_schedule`|`create_workqueue`+`INIT_WORK`+`queue_work`||适用场景|短时间、不可睡眠的任务|可能睡眠或长时间的任务|###注意事项1.**Tasklet安全性**:同一tasklet不会同时在多个CPU上运行,但不同tasklet可以并行[^1]。2.**Workqueue资源**:自定义工作队列会创建独立的内核线程,消耗更多资源[^3]。3.**默认工作队列**:使用`schedule_work(work)`会使用系统共享的工作队列(如`system_wq`),避免资源浪费[^4]。§§相关问题§§1.Linux内核中软中断(softirq)tasklet有什么关系?2.工作队列(workqueue)内核线程(kthread)有何区别与联系?3.如何选择使用tasklet还是workqueue?4.工作队列中的工作是如何被调度的?</think>### Linux内核Tasklet与Workqueue创建方法详解 #### 一、Tasklet创建方法 TaskletLinux内核中用于中断下半部处理的轻量级机制,运行在软中断上下文。 **创建步骤:** 1. **声明tasklet结构体** ```c static struct tasklet_struct my_tasklet; ``` 2. **定义处理函数** ```c static void tasklet_handler(unsigned long data) { printk(KERN_INFO "Tasklet executed\n"); // 处理逻辑 } ``` 3. **初始化tasklet** ```c tasklet_init(&my_tasklet, tasklet_handler, 0); // 0为传入参数 ``` 4. **调度tasklet** ```c tasklet_schedule(&my_tasklet); // 加入执行队列 ``` 5. **销毁tasklet**(模块退出时) ```c tasklet_kill(&my_tasklet); // 确保安全终止[^2] ``` **特性:** - 原子性:同类型tasklet不会并发执行 - 不可睡眠:运行在中断上下文 - 低延迟:适合实时性要求高的场景[^1] #### 二、Workqueue创建方法 Workqueue将工作推迟到内核线程执行,运行在进程上下文,可处理复杂操作。 **两种创建方式:** 1. **使用系统默认工作队列**(推荐) ```c static struct work_struct my_work; // 定义工作处理函数 void work_handler(struct work_struct *work) { printk(KERN_INFO "Work executed in kernel thread\n"); } // 初始化调度 INIT_WORK(&my_work, work_handler); schedule_work(&my_work); // 加入系统队列[^4] ``` 2. **创建自定义工作队列** ```c struct workqueue_struct *custom_wq = create_workqueue("my_wq"); static struct work_struct custom_work; INIT_WORK(&custom_work, work_handler); // 提交到自定义队列 queue_work(custom_wq, &custom_work); // 销毁队列(模块退出时) destroy_workqueue(custom_wq);[^3] ``` **关键特性对比:** | 特性 | Tasklet | Workqueue | |---------------------|-----------------------------|----------------------------| | 执行上下文 | 中断上下文(不可睡眠) | 进程上下文(可睡眠) | | 并发性 | 同类型串行执行 | 可并行执行 | | 适用场景 | 简单快速操作 | 复杂/可能阻塞的操作 | | 资源消耗 | 低 | 较高(需内核线程) | | 超时处理 | 不支持 | 支持延迟工作(delayed_work) | #### 三、使用场景选择 1. **选择Tasklet当:** - 需要极低延迟(如网络包处理) - 操作简单且不可阻塞 - 避免线程调度开销[^1] 2. **选择Workqueue当:** - 需要执行可能阻塞的操作(如I/O) - 需要睡眠或等待资源 - 复杂处理需要进程上下文[^3] #### 四、最佳实践 1. **内存管理:** ```c // Tasklet共享数据需原子操作 atomic_t shared_data = ATOMIC_INIT(0); // Workqueue可使用标准锁机制 DEFINE_SPINLOCK(my_lock); ``` 2. **错误处理:** ```c // Workqueue创建需检查 if (!custom_wq) { printk(KERN_ERR "Workqueue creation failed\n"); return -ENOMEM; } ``` 3. **性能优化:** - 高频任务用`tasklet_hi_schedule()`提高优先级 - CPU密集型工作用`create_singlethread_workqueue()`
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值