Linux 驱动开发之中断分析1(基于Linux6.6)---基础与注册介绍
一、中断基础概念
1.1、中断的基本概念
中断是一种信号机制,通常由硬件设备发送给 CPU,告知操作系统需要处理的事件。中断会打断 CPU 当前的执行流,转而执行一个特定的中断处理程序(也叫做中断服务程序,ISR),然后再返回继续执行被打断的任务。
中断的工作流程:
- 硬件触发中断:硬件设备(如键盘、网络卡、硬盘、定时器等)通过中断请求(IRQ)信号通知 CPU,表示某个事件需要处理。
- CPU 响应中断:当 CPU 正在执行某个任务时,如果有中断请求到达,CPU 会中断当前的任务,保存当前任务的执行状态(即上下文),然后跳转到特定的中断服务程序(ISR)。
- 中断服务程序(ISR)执行:ISR 处理外部设备的请求,执行与该中断相关的处理逻辑。
- 恢复执行:ISR 执行完毕后,CPU 会恢复之前的执行状态,继续执行被中断的任务。
1.2、中断的分类
中断可以根据触发源、优先级、响应机制等不同特性进行分类:
1. 硬件中断
硬件中断是由硬件设备发出的信号,通常用于通知 CPU 设备的状态变化或需要数据处理的事件。硬件中断的来源包括:
- 外部硬件设备:如网络卡、磁盘、显卡、USB 设备等。
- 定时器中断:由系统时钟或定时器生成,用于时间管理和调度任务。
2. 软件中断
软件中断是由软件程序发出的请求,通常用于向操作系统请求服务,或者在用户空间与内核之间进行通信。软件中断包括:
- 系统调用:当用户程序需要内核服务时,会触发一个软件中断。例如,文件操作、内存管理等。
- 异常(Exception):某些运行时错误,如除零错误、非法内存访问等,会触发异常中断。
3. 内核中断与外部中断
- 内核中断:由操作系统内核触发,通常与内存、进程调度、资源管理等系统功能相关。
- 外部中断:由硬件设备触发,常见的如输入输出设备的请求、外部时钟中断等。
4. 同步中断与异步中断
- 同步中断:中断的触发与程序的执行流密切相关,通常是在程序执行过程中发生的错误(如除零错误、非法指令等)。
- 异步中断:中断与程序的执行流程无关,通常由外部设备或硬件产生(如外部输入设备的信号)。
1.3、中断处理流程
中断处理是由操作系统内核管理的,Linux 的中断处理过程主要包括以下几个步骤:
-
中断向量表(Interrupt Vector Table):
- 中断向量表是一个数组,存储了所有中断源的入口地址。每个中断类型对应一个中断号,系统通过查找中断号来跳转到对应的中断服务程序(ISR)。
-
中断屏蔽(Interrupt Masking):
- 当中断请求到达时,CPU 会根据优先级判断是否允许该中断。Linux 内核通过中断屏蔽技术来控制中断的响应。例如,内核可以屏蔽某些不重要的中断,避免中断处理程序频繁中断当前任务的执行。
-
中断服务程序(ISR):
- 中断服务程序是操作系统为每个中断事件指定的处理函数。当中断发生时,CPU 会跳转到对应的 ISR 执行处理。在 Linux 中,ISR 通常由设备驱动程序提供。
-
中断上下文与进程上下文:
- 中断处理程序执行时,它处于“中断上下文”中。在中断上下文中,CPU 不会进行任务切换,也不能阻塞。因此,中断服务程序必须尽可能快地执行,并尽量避免耗时操作。
-
中断后处理(软中断/工作队列):
- 为了避免中断服务程序的执行时间过长,Linux 将一些需要较长时间的操作推迟到软中断或工作队列中去处理。例如,网络数据的接收和处理通常是通过软中断机制在中断服务程序完成后进行的。
1.4、Linux 中断管理机制
在 Linux 中,内核提供了精细的中断管理机制,主要通过以下几个部分来处理:
-
IRQ(Interrupt Request):
- 每个硬件设备通过指定的 IRQ 号向 CPU 发起中断请求。IRQ 号用于标识中断源,不同的设备通常会分配不同的 IRQ 号。
- Linux 内核为每个硬件设备分配 IRQ 号,并管理中断请求的排队与处理。
-
中断处理函数(Interrupt Handler):
- 当中断发生时,Linux 内核通过查找中断向量表,找到对应的中断处理函数。每个设备驱动程序通常都会定义一个中断处理函数来响应该设备的中断。
-
软中断(Soft IRQ)和任务队列(Tasklet):
- 为了避免中断服务程序的阻塞,Linux 引入了软中断和任务队列。软中断和任务队列用于处理耗时的操作,比如网络数据包的处理。软中断通常在中断上下文中进行,但可以在稍后的时间内执行。
-
中断优先级(Interrupt Priority):
- 中断优先级决定了哪个中断首先被处理。在多核系统中,Linux 会根据不同的优先级对中断进行处理。高优先级的中断会抢占低优先级的中断。
-
中断屏蔽与嵌套:
- Linux 支持中断的屏蔽(禁用某些中断的处理)和嵌套(允许一个中断服务程序被另一个中断打断)。这些操作有助于控制中断的响应时机和执行顺序。
1.5、中断ID
a -- IRQ number
cpu给中断的一个编号,一个IRQ number是一个虚拟的interrupt ID,和硬件无关;
b -- HW interrupt ID
对于中断控制器而言,它收集了多个外设的irq request line,要向cpu传递,GIC要对外设进行编码,GIC就用HW interrupt ID来标示外部中断;
1.6、SMP情况下中断两种形态
1-Nmode :只有一个processor处理器
N-N :所有的processor都是独立收到中断的
GIC:SPI使用1-Nmode PPI 和 sgi使用N-Nmode
二、驱动使用中断
2.1、申请IRQ
在linux内核中用于申请中断的函数是request_irq()。
include/linux/interrupt.h
/**
* request_irq - Add a handler for an interrupt line
* @irq: The interrupt line to allocate
* @handler: Function to be called when the IRQ occurs.
* Primary handler for threaded interrupts
* If NULL, the default primary handler is installed
* @flags: Handling flags
* @name: Name of the device generating this interrupt
* @dev: A cookie passed to the handler function
*
* This call allocates an interrupt and establishes a handler; see
* the documentation for request_threaded_irq() for details.
*/
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);
}
相关参数:
a -- irq是要申请的硬件中断号。另外,这里要思考的问题是,这个irq 是怎么得到的?这里我们在设备树中获取,具体解析见:
b -- handler是向系统注册的中断处理函数,是一个回调函数,中断发生时,系统调用这个函数,dev_id参数将被传递给它。
c -- irqflags是中断处理的属性,若设置了IRQF_DISABLED (老版本中的SA_INTERRUPT,本版已经不支持了),则表示中断处理程序是快速处理程序,快速处理程序被调用时屏蔽所有中断,慢速处理程序不屏蔽;若设置了IRQF_SHARED (老版本中的SA_SHIRQ),则表示多个设备共享中断,若设置了IRQF_SAMPLE_RANDOM(老版本中的SA_SAMPLE_RANDOM),表示对系统熵有贡献,对系统获取随机数有好处。(这几个flag是可以通过或的方式同时使用的)
d -- devname设置中断名称,在cat /proc/interrupts中可以看到此名称。
e -- dev_id在中断共享时会用到,一般设置为这个设备的设备结构体或者NULL。
request_irq()返回0表示成功,返回-INVAL表示中断号无效或处理函数指针为NULL,返回-EBUSY表示中断已经被占用且不能共享。
顶半部 handler 的类型 irq_handler_t 定义为:
include/linux/interrupt.h
typedef irqreturn_t (*irq_handler_t)(int, void *);
参数1:中断号
参数2 :参数
在Interrupt.h (e:\linux-3.14-fs4412\include\linux) 18323 2014/3/31中定义
IRQ_NONE 共享中断,如果不是我的设备产生的中断,就返回该值
IRQ_HANDLED 中断处理函数正确执行了就返回该值
IRQ_WAKE_THREAD = (1 << 1)
2.2、释放IRQ
与request_irq()相对应的函数为 free_irq(),free_irq()的原型为:
include/linux/interrupt.h
extern const void *free_irq(unsigned int, void *);
free_irq()参数的定义与request_irq()相同。
三、中断注册过程分析
每次用中断的时候就是注册一个中断函数。request_irq首先生成一个irqaction结构,其次根据中断号 找到irq_desc数组项(还记得吧,内核中irq_desc是一个数组,没一项对应一个中断号),然后将irqaction结构添加到 irq_desc中的action链表中。当然还做一些其他的工作,注册完成后,中断函数就可以发生并被处理了。
irq_desc 内核中记录一个irq_desc的数组,数组的每一项对应一个中断或者一组中断使用同一个中断号,一句话irq_desc几乎记录所有中断相关的东西,这个结构是中断的核心。其中包括俩个重要的结构irq_chip 和irqaction 。
3.1、irq_chip
irq_chip 里面基本上是一些回调函数,其中大多用于操作底层硬件,设置寄存器,其中包括设置GPIO为中断输入就是其中的一个回调函数。
include/linux/irq.h
struct irq_chip {
const char *name;
unsigned int (*irq_startup)(struct irq_data *data);
void (*irq_shutdown)(struct irq_data *data);
void (*irq_enable)(struct irq_data *data);
void (*irq_disable)(struct irq_data *data);
void (*irq_ack)(struct irq_data *data);
void (*irq_mask)(struct irq_data *data);
void (*irq_mask_ack)(struct irq_data *data);
void (*irq_unmask)(struct irq_data *data);
void (*irq_eoi)(struct irq_data *data);
int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);
int (*irq_retrigger)(struct irq_data *data);
int (*irq_set_type)(struct irq_data *data, unsigned int flow_type);
int (*irq_set_wake)(struct irq_data *data, unsigned int on);
void (*irq_bus_lock)(struct irq_data *data);
void (*irq_bus_sync_unlock)(struct irq_data *data);
#ifdef CONFIG_DEPRECATED_IRQ_CPU_ONOFFLINE
void (*irq_cpu_online)(struct irq_data *data);
void (*irq_cpu_offline)(struct irq_data *data);
#endif
void (*irq_suspend)(struct irq_data *data);
void (*irq_resume)(struct irq_data *data);
void (*irq_pm_shutdown)(struct irq_data *data);
void (*irq_calc_mask)(struct irq_data *data);
void (*irq_print_chip)(struct irq_data *data, struct seq_file *p);
int (*irq_request_resources)(struct irq_data *data);
void (*irq_release_resources)(struct irq_data *data);
void (*irq_compose_msi_msg)(struct irq_data *data, struct msi_msg *msg);
void (*irq_write_msi_msg)(struct irq_data *data, struct msi_msg *msg);
int (*irq_get_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool *state);
int (*irq_set_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool state);
int (*irq_set_vcpu_affinity)(struct irq_data *data, void *vcpu_info);
void (*ipi_send_single)(struct irq_data *data, unsigned int cpu);
void (*ipi_send_mask)(struct irq_data *data, const struct cpumask *dest);
int (*irq_nmi_setup)(struct irq_data *data);
void (*irq_nmi_teardown)(struct irq_data *data);
unsigned long flags;
};
可以看到这里实现的是一个框架,需要我们进一步的填充里面的函数。分析另一个结构irqaction
3.2、irqaction
include/linux/interrupt.h
struct irqaction {
irq_handler_t handler;
void *dev_id;
void __percpu *percpu_dev_id;
struct irqaction *next;
irq_handler_t thread_fn;
struct task_struct *thread;
struct irqaction *secondary;
unsigned int irq;
unsigned int flags;
unsigned long thread_flags;
unsigned long thread_mask;
const char *name;
struct proc_dir_entry *dir;
} ____cacheline_internodealigned_in_smp;
四、实例分析
1. 网络驱动中的中断应用
在网络驱动程序中,中断被广泛应用于处理网络接口卡(NIC)传输数据包的请求。网络驱动依赖中断来实现接收和发送数据的高效处理。
示例:以太网卡接收数据包
-
网络接口卡 (NIC) 触发中断:
网络卡通过硬件产生中断,告诉 CPU 数据包已经到达,并且需要处理。每当 NIC 接收到一个数据包时,它会发出中断请求,通知操作系统。 -
内核中断处理:
内核的网络驱动程序会响应这个中断请求,执行相关的中断服务程序(ISR)。ISR 会从网络卡中读取数据包,并将其放入内核的网络接收队列中。 -
软中断处理:
为了避免阻塞中断处理程序,Linux 会将数据包的进一步处理(如协议栈的解析)推迟到软中断或工作队列中。这使得中断处理程序能够快速完成,避免长时间占用 CPU。 -
示例代码:
-
static irqreturn_t eth_irq_handler(int irq, void *dev_id) { struct net_device *dev = dev_id; struct sk_buff *skb; // 读取 NIC 数据包 skb = nic_receive_packet(dev); // 将数据包传递到网络协议栈 netif_rx(skb); // netif_rx 是将包递交给网络协议栈的函数 return IRQ_HANDLED; } static int __init eth_init(void) { // 注册中断处理程序 request_irq(ETH_IRQ, eth_irq_handler, IRQF_SHARED, "eth_irq", dev); return 0; }在此示例中,网络驱动程序通过
request_irq注册了一个中断处理程序(eth_irq_handler),当接收到网络卡的中断信号时,内核会执行该处理程序。
2. 定时器中断应用:实时任务调度
Linux 系统中的定时器中断通常由系统时钟(通常是硬件定时器)触发,用于管理时间片轮转、任务调度、进程调度等。内核周期性地响应定时器中断来触发任务调度。
示例:Linux 调度器的时间片管理
-
定时器中断触发:
系统时钟(如 PIT、TSC 等硬件定时器)每隔一定时间会触发中断。这个时间间隔通常是 1ms 或 10ms,具体取决于系统的时钟设置。 -
内核调度:
每次定时器中断触发时,内核会检查当前进程的时间片是否已用尽。如果时间片用尽,则会触发调度器进行进程切换。调度器会选择一个新的进程并将 CPU 的控制权交给它。 -
示例代码:
-
static irqreturn_t timer_irq_handler(int irq, void *dev_id) { // 通过定时器中断触发调度 schedule(); // 调度器的函数,决定是否需要进行上下文切换 return IRQ_HANDLED; } static int __init timer_init(void) { // 请求定时器中断 request_irq(TIMER_IRQ, timer_irq_handler, IRQF_TIMER, "timer_irq", NULL); return 0; }在此示例中,定时器中断被用来触发进程调度。每次定时器中断发生时,
timer_irq_handler会调用schedule()来决定是否需要切换任务。
3. USB 设备的中断应用
USB 设备驱动程序也广泛使用中断机制来处理设备事件,例如设备插入、数据传输完成等。
示例:USB 键盘的中断应用
USB 键盘会通过中断来向操作系统报告键盘按键的状态变化。当用户按下一个键时,USB 控制器会发出一个中断信号,通知内核处理键盘输入。
-
USB 控制器发出中断请求:
当用户按下键盘上的某个键时,USB 键盘会通过中断向 CPU 发出请求,表示有新的键盘输入。 -
内核中断处理:
内核中断处理程序会接收到这个请求,并读取 USB 控制器中的数据。数据会被送到用户空间,通常通过输入子系统(input subsystem)传递给相应的应用程序(如终端或图形界面)。 -
示例代码:
-
static irqreturn_t usb_keyboard_irq_handler(int irq, void *dev_id) { struct usb_device *dev = dev_id; struct input_dev *input = dev->input; // 读取键盘输入 usb_keyboard_read(dev, input); input_sync(input); return IRQ_HANDLED; } static int __init usb_keyboard_init(void) { // 请求 USB 键盘的中断 request_irq(USB_KEYBOARD_IRQ, usb_keyboard_irq_handler, IRQF_SHARED, "usb_keyboard_irq", dev); return 0; }在这个例子中,USB 键盘的中断处理程序读取了设备数据,并通过
input_sync()将按键事件传递给用户空间。
4. GPIO 中断应用
GPIO(通用输入输出)是许多嵌入式系统中的常见硬件接口。Linux 通过 GPIO 中断机制处理按钮按下、外部信号等事件。
示例:外部按钮中断
-
外部硬件触发中断:
当用户按下按钮时,GPIO 引脚会触发一个中断请求。这个中断通知内核有外部事件需要处理。 -
内核响应中断:
中断处理程序会读取 GPIO 引脚的状态,并根据按钮的状态触发相应的操作,例如启动一个进程、切换 LED 状态等。 -
示例代码:
static irqreturn_t gpio_button_irq_handler(int irq, void *dev_id) {
// 读取按钮状态
int button_state = gpio_get_value(GPIO_BUTTON_PIN);
if (button_state) {
printk(KERN_INFO "Button pressed!\n");
}
return IRQ_HANDLED;
}
static int __init gpio_button_init(void) {
// 设置 GPIO 引脚
gpio_request(GPIO_BUTTON_PIN, "Button");
// 请求中断
request_irq(GPIO_BUTTON_IRQ, gpio_button_irq_handler, IRQF_TRIGGER_FALLING, "gpio_button_irq", NULL);
return 0;
}
在这个例子中,GPIO 中断处理程序会响应按钮按下的事件并做出相应的响应。
2140

被折叠的 条评论
为什么被折叠?



