imx6ull-驱动开发篇25——Linux 中断上半部/下半部
目录
上半部与下半部
下半部机制
软中断
softirq_action结构体
open_softirq 函数
raise_softirq 函数
softirq_init 函数
tasklet
tasklet_struct 结构体
tasklet_init 函数
tasklet_schedule 函数
工作队列
work_struct 结构体
workqueue_struct 结构体
worker 结构体
INIT_WORK 宏
schedule_work函数
上半部与下半部
linux内核的中断处理过程分为了两部分:
上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。
Linux 内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快出,那些对时间敏感、执行速度快的操作可以放到中断处理函数中,也就是上半部;剩下的所有工作都可以放到下半部去执行。
编写驱动的时候怎么判断用哪个呢?有一些可以借鉴的参考点:
- 如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
- 如果要处理的任务对时间敏感,可以放到上半部。
- 如果要处理的任务与硬件有关,可以放到上半部
- 除了上述三点以外的其他任务,优先考虑放到下半部。
下半部机制
上半部处理很简单,直接编写中断处理函数就行了。
而在linux中,有多种下半部机制:
软中断
Linux 内核中,使用结构体 softirq_action 表示软中断。
softirq_action结构体
softirq_action结构体定义在文件 include/linux/interrupt.h 中,内容如下:
struct softirq_action {void (*action)(struct softirq_action *);
};
在 kernel/softirq.c 文件中一共定义了 10 个软中断,
static struct softirq_action softirq_vec[NR_SOFTIRQS];
NR_SOFTIRQS 是枚举类型,定义在文件 include/linux/interrupt.h 中,定义如下:
enum
{HI_SOFTIRQ=0, /* 高优先级软中断 */TIMER_SOFTIRQ, /* 定时器软中断 */NET_TX_SOFTIRQ, /* 网络数据发送软中断 */NET_RX_SOFTIRQ, /* 网络数据接收软中断 */BLOCK_SOFTIRQ, /* 块设备软中断 */BLOCK_IOPOLL_SOFTIRQ, /* 块设备轮询软中断 */TASKLET_SOFTIRQ, /* tasklet 软中断 */SCHED_SOFTIRQ, /* 调度软中断 */HRTIMER_SOFTIRQ, /* 高精度定时器软中断 */RCU_SOFTIRQ, /* RCU 软中断 */NR_SOFTIRQS
}
可以看出,一共有 10 个软中断,因此 NR_SOFTIRQS 为 10,因此数组 softirq_vec 有 10 个元素。
softirq_action 结构体中的 action 成员变量就是软中断的服务函数,数组 softirq_vec 是个全局数组,因此所有的 CPU(对于 SMP 系统而言)都可以访问到,每个 CPU 都有自己的触发和控制机制,并且只执行自己所触发的软中断。
枚举值 | 对应软中断 | 用途说明 |
---|---|---|
| 高优先级软中断 | 处理高优先级的延迟任务(如高优先级 Tasklet) |
| 定时器软中断 | 处理定时器回调函数( |
| 网络数据发送软中断 | 处理网络协议栈的数据包发送(如 TCP/IP 栈的发送队列) |
| 网络数据接收软中断 | 处理网络协议栈的数据包接收(如网卡 DMA 数据的上传) |
| 块设备软中断 | 处理块设备请求(如磁盘 I/O 完成后的回调) |
| 块设备轮询软中断 | 用于块设备的轮询模式 I/O(较少使用) |
| Tasklet 软中断 | 调度和执行 Tasklet(通用的小任务延迟机制) |
| 调度软中断 | 处理进程调度相关的延迟任务(如负载均衡) |
| 高精度定时器软中断 | 处理高精度定时器( |
| RCU 软中断 | 处理 RCU(Read-Copy-Update)内存回收的延迟任务 |
| 软中断总数 | 不是实际软中断,表示当前内核支持的软中断类型总数(用于数组边界检查) |
open_softirq 函数
要使用软中断,必须先使用 open_softirq 函数注册对应的软中断处理函数。
open_softirq 函数原型如下:
void open_softirq(int nr, void (*action)(struct softirq_action *))
- nr:要开启的软中断,在数组 softirq_vec 中定义的 action 函数中选择一个。
- action:软中断对应的处理函数。
raise_softirq 函数
注册好软中断以后需要通过 raise_softirq 函数触发。
raise_softirq 函数原型如下:
void raise_softirq(unsigned int nr)
- nr:要触发的软中断,在数组 softirq_vec 中定义的 action 函数中选择一个。
softirq_init 函数
软中断必须在编译的时候静态注册。
Linux 内核使用 softirq_init 函数初始化软中断,。
softirq_init 函数 定义在 kernel/softirq.c 文件里面,函数内容如下:
void __init softirq_init(void)
{int cpu;// 初始化每个CPU的Tasklet链表(普通优先级和高优先级)for_each_possible_cpu(cpu) {// 初始化普通优先级Tasklet链表:// 将tail指针指向head,表示空链表per_cpu(tasklet_vec, cpu).tail = &per_cpu(tasklet_vec, cpu).head;// 初始化高优先级Tasklet链表:// 同样将tail指针指向head,表示空链表per_cpu(tasklet_hi_vec, cpu).tail = &per_cpu(tasklet_hi_vec, cpu).head;}// 注册软中断处理函数:// 将TASKLET_SOFTIRQ(普通优先级Tasklet)与tasklet_action函数绑定open_softirq(TASKLET_SOFTIRQ, tasklet_action);// 将HI_SOFTIRQ(高优先级Tasklet)与tasklet_hi_action函数绑定open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
可以看出, softirq_init 函数默认会打开 TASKLET_SOFTIRQ 和HI_SOFTIRQ。
tasklet
tasklet 是利用软中断来实现的另外一种下半部机制,在软中断和 tasklet 之间,建议大家使用 tasklet。
tasklet_struct 结构体
Linux 内核使用 tasklet_struct 结构体来表示 tasklet:
struct tasklet_struct {struct tasklet_struct *next; /* 链表指针:指向下一个tasklet */unsigned long state; /* 状态标志:TASKLET_STATE_SCHED等 */atomic_t count; /* 引用计数器:0表示激活,非0表示禁用 */void (*func)(unsigned long); /* 处理函数指针:实际要执行的函数 */unsigned long data; /* 函数参数:传递给func的参数 */
};
func 函数就是 tasklet 要执行的处理函数,用户定义函数内容,相当于中断处理函数。
tasklet_init 函数
如果要使用 tasklet,必须先定义一个 tasklet,然后使用 tasklet_init 函数初始化 tasklet。
taskled_init 函数原型如下:
void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long),unsigned long data);
也可以使用宏 DECLARE_TASKLET 来 一 次 性 完 成 tasklet 的 定 义 和 初 始 化 , DECLARE_TASKLET 定义在 include/linux/interrupt.h 文件中,定义如下:
DECLARE_TASKLET(name, func, data)
- name 为要定义的 tasklet 名字,这个名字就是一个 tasklet_struct 类型的时候变量,。
- func就是 tasklet 的处理函数。
- data 是传递给 func 函数的参数。
tasklet_schedule 函数
在上半部,也就是中断处理函数中,调用 tasklet_schedule 函数就能使 tasklet 在合适的时间运行,。
tasklet_schedule 函数 原型如下:
void tasklet_schedule(struct tasklet_struct *t)
- t:要调度的 tasklet,也就是 DECLARE_TASKLET 宏里面的 name。
关于 tasklet 的参考使用示例如下所示:
/* 定义 tasklet */
struct tasklet_struct testtasklet;/* tasklet 处理函数 */
void testtasklet_func(unsigned long data)
{/* tasklet 具体处理内容 */
}/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{/* 硬件相关操作 *//* 调度 tasklet */tasklet_schedule(&testtasklet);......
}/* 驱动入口函数 */
static int __init xxxx_init(void)
{/* 初始化 tasklet */tasklet_init(&testtasklet, testtasklet_func, data);/* 注册中断处理函数 */request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
......}
工作队列
工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。
因此如果要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet。
work_struct 结构体
Linux 内核使用 work_struct 结构体表示一个工作,内容如下(省略掉条件编译):
struct work_struct {atomic_long_t data; // 存储工作项的状态和标志位struct list_head entry; // 用于将工作项链接到工作队列的链表节点work_func_t func; // 工作队列处理函数指针
};
这些工作组织成工作队列。
workqueue_struct 结构体
工作队列使用 workqueue_struct 结构体表示,内容如下(省略掉条件编译):
struct workqueue_struct {/* 工作队列的物理工作池(pool_workqueue)链表 */struct list_head pwqs; // 所有关联的pool_workqueuestruct list_head list; // 全局工作队列链表节点/* 同步保护锁 */struct mutex mutex; // 保护工作队列结构的互斥锁/* 刷新状态管理 */int work_color; // 当前工作项颜色(用于flush机制)int flush_color; // 正在刷新的颜色atomic_t nr_pwqs_to_flush; // 待刷新的pool_workqueue计数struct wq_flusher *first_flusher; // 第一个刷新请求struct list_head flusher_queue; // 刷新等待队列struct list_head flusher_overflow; // 溢出刷新队列/* 应急机制 */struct list_head maydays; // 救援请求列表struct worker *rescuer; // 救援线程(内存紧张时使用)int nr_drainers; // 正在排空的worker计数int saved_max_active; // 保存的最大活跃工作项数/* 非绑定工作队列属性 */struct workqueue_attrs *unbound_attrs; // NUMA/CPU亲和性等属性struct pool_workqueue *dfl_pwq; // 默认的pool_workqueue/* 标识信息 */char name[WQ_NAME_LEN]; // 工作队列名称(如"kworker/my_queue")struct rcu_head rcu; // RCU回调头/* 标志位(缓存行对齐) */unsigned int flags ____cacheline_aligned; // 常见标志:// WQ_MEM_RECLAIM - 内存紧张时可回收// WQ_HIGHPRI - 高优先级工作队列// WQ_UNBOUND - 无CPU亲和性/* 每CPU工作池 */struct pool_workqueue __percpu *cpu_pwqs; // 绑定型工作队列的每CPU池/* NUMA节点工作池 */struct pool_workqueue __rcu *numa_pwq_tbl[]; // 非绑定型工作队列的NUMA节点池
};
worker 结构体
Linux 内核使用工作者线程(worker thread)来处理工作队列中的各个工作, Linux 内核使用worker 结构体表示工作者线程。
worker 结构体内容如下:
struct worker {union {struct list_head entry; // 用于链接到worker_pool的空闲链表struct hlist_node hentry; // 用于链接到worker_pool的忙碌哈希表};struct work_struct *current_work; // 当前正在处理的工作项work_func_t current_func; // 当前工作项的处理函数struct pool_workqueue *current_pwq; // 当前所属的pool_workqueuebool desc_valid; // worker描述符是否有效struct list_head scheduled; // 被调度的工作项链表struct task_struct *task; // 关联的内核线程(如kworker/u4:0)struct worker_pool *pool; // 归属的worker线程池struct list_head node; // 用于全局worker链表unsigned long last_active; // 最后活动时间戳(jiffies)unsigned int flags; // 状态标志:// WORKER_STARTED - 线程已启动// WORKER_DIE - 线程应退出int id; // worker的唯一IDchar desc[WORKER_DESC_LEN]; // 描述符(显示在ps/top中)struct workqueue_struct *rescue_wq; // 所属的救援工作队列(内存紧张时使用)
};
每个 worker 都有一个工作队列,工作者线程处理自己工作队列中的所有工作。
在实际的驱动开发中,我们只需要定义工作(work_struct)即可,关于工作队列和工作者线程我们基本不用去管。
INIT_WORK 宏
简单创建工作很简单,直接定义一个 work_struct 结构体变量即可,然后使用 INIT_WORK 宏来初始化工作。
INIT_WORK 宏定义如下:
#define INIT_WORK(_work, _func)
_work 表示要初始化的工作, _func 是工作对应的处理函数。
也可以使用 DECLARE_WORK 宏一次性完成工作的创建和初始化,宏定义如下:
#define DECLARE_WORK(n, f)
- n 表示定义的工作(work_struct)
- f 表示工作对应的处理函数。
schedule_work函数
工作也是需要调度才能运行的,工作的调度函数为 schedule_work
schedule_work 函数原型如下所示:
bool schedule_work(struct work_struct *work)
- work: 要调度的工作。
- 返回值: 0 成功,其他值 :失败。
关于工作队列的参考使用示例如下所示:
/* 定义工作(work) */
struct work_struct testwork;
/* work 处理函数 */
void testwork_func_t(struct work_struct *work);
{/* work 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
....../* 调度 work */schedule_work(&testwork);
......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
....../* 初始化 work */INIT_WORK(&testwork, testwork_func_t);/* 注册中断处理函数 */request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
......
}