中断概述
除了系统调用外,中断也是一种内核活动;系统调用不是在用户态和系统状态之间切换的唯一途径
中断的处理往往会涉及到汇编等和硬件相关的代码,但是中断处理部分随着时间的演化,已经达到了这样一种状态:高层代码和底层的硬件交互代码,已经尽可能有效而干净
地分隔开了。
中断框架:
老版本的linux,中断代码比较杂乱,新版本,因为其中引入了一个用于中断和IRQ的通用框架。各个平台现在只负责在最低层次上与硬件交互。所有其他功能都由通用代码提供
中断类型:
同步中断和异步中断,两者都是通过中断服务程序(ISR中断处理程序)来处理的
同步中断和异常
由cpu产生,针对当前执行的程序,比如除0,内核可能通过信号机制通知进程,进程做默认或注册行为比如输出错误信息,或者直接结束;
异常情况也可能是如缺页异常,这种由内核自动修复,进程往往无感知;
异步中断
经典的中断,由外部设备产生,可能发生在任意时间。不同于同步中断,
异步中断并不与特定进程关联。它们可能发生在任何时间,而不牵涉系统当前执行的活动。 ①
网卡通过发出一个相关的中断来报告新分组的到达。因为数据可能在任意时刻到达系统,所
以当前执行的很可能是与数据无关的某个进程或其他东西。为避免损害该进程,内核必须确
保中断能够尽快处理完毕(通过缓冲数据),使得CPU时间能够返还给当前进程。这也是内核
需要延期操作机制的原因
中断的一些其他逻辑:
- 禁用中断,应尽量避免;
- 设备共享中断编号–中断共享
硬件中断
由系统自身和与之连接的外设自动产生。它们用于支持更高效地实现设备驱动程序,也用于引起处理器自身对异常或错误的关注,这些是需要与内核代码进行交互的。
中断不能由处理器外部的外设直接产生,而必须借助于一个称为中断控制器(interrupt controller)的标准组件来请求,该组件存在于每个系统中。
/proc/interrupts 这个文件可以查看系统的中断编号和对应中断;
1 | think@think-VirtualBox:~/c++/ptrace$ cat /proc/interrupts |
硬件中断的处理
进入和退出任务:
首先,必须建立一个适当的环境,使得处理程序函数能够在其中执行,接下来调用处理程序自身,最后将系统复原(在当前程序看来)到中断之前的状态。
进入是从用户态切到核心太,退出是从核心态到用户态,而且需要通过调度器是否选择新进程替代旧进程;中断到达时也可能处于核心态,所以这个时候需要判断;
中断处理程序
- 特点:
可以禁用中断来防止被打断,但是只能短时间使用,处理过程短暂;
可以在其他ISR执行期间调用的中断处理程序,不能彼此干扰
可延期的处理,不用在中断处理程序中实现,可以放到类似下半部的软中断;或者tasklet
中断相关数据结构:
总的数组:
在kernel/irq/handle.c,kernel/irq/irqdesc.c:可以看到初始化的中断处理数组;每个都被处理为handle_bad_irq;此时还没赋值;
赋值发生在驱动程序或模块初始化时调用request_irq等类似函数进行注册;1
2
3
4
5
6
7struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
[0 ... NR_IRQS-1] = {
.handle_irq = handle_bad_irq,
.depth = 1,
.lock = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),
}
};但IRQ的最大可能数目是通过一个平台相关的常数NR_IRQS指定的。大多数体系结构下,该常数定义在处理器相关的头文件include/asm-generic/irq.h中
不同处理器可能支持的不同;1
2
3数组每个元素:
看下中断处理数组中每个元素的结构:每个irq都可以由该结构完全描述1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118/**
* struct irq_desc - interrupt descriptor
* @irq_common_data: per irq and chip data passed down to chip functions
* @kstat_irqs: irq stats per cpu
* @handle_irq: highlevel irq-events handler
电流层ISR由handle_irq提供。 handler_data可以指向任意数据,该数据可以是特定于IRQ
或处理程序的。每当发生中断时,特定于体系结构的代码都会调用handle_irq。该函数负责
使用chip中提供的特定于控制器的方法,进行处理中断所必需的一些底层操作。用于不同中
断类型的默认函数由内核提供。
* @preflow_handler: handler called before the flow handler (currently used by sparc)
* @action: the irq action chain irq操作列表
action提供了一个操作链,需要在中断发生时执行。由中断通知的设备驱动程序,可以将与
之相关的处理程序函数放置在此处。有一个专门的数据结构用于表示这些操作
* @status: status information irq状态
/*
* Bit masks for irq_common_data.state_use_accessors
*
* IRQD_TRIGGER_MASK - Mask for the trigger type bits
* IRQD_SETAFFINITY_PENDING - Affinity setting is pending
* IRQD_NO_BALANCING - Balancing disabled for this IRQ
* IRQD_PER_CPU - Interrupt is per cpu
* IRQD_AFFINITY_SET - Interrupt affinity was set
* IRQD_LEVEL - Interrupt is level triggered
* IRQD_WAKEUP_STATE - Interrupt is configured for wakeup
* from suspend
* IRDQ_MOVE_PCNTXT - Interrupt can be moved in process
* context
* IRQD_IRQ_DISABLED - Disabled state of the interrupt
* IRQD_IRQ_MASKED - Masked state of the interrupt
* IRQD_IRQ_INPROGRESS - In progress state of the interrupt
* IRQD_WAKEUP_ARMED - Wakeup mode armed
*/
/*
enum {
IRQD_TRIGGER_MASK = 0xf,
IRQD_SETAFFINITY_PENDING = (1 << 8),
IRQD_NO_BALANCING = (1 << 10),
IRQD_PER_CPU = (1 << 11),
IRQD_AFFINITY_SET = (1 << 12),
IRQD_LEVEL = (1 << 13),
IRQD_WAKEUP_STATE = (1 << 14),
IRQD_MOVE_PCNTXT = (1 << 15),
IRQD_IRQ_DISABLED = (1 << 16),
IRQD_IRQ_MASKED = (1 << 17),
IRQD_IRQ_INPROGRESS = (1 << 18),
IRQD_WAKEUP_ARMED = (1 << 19),
};*/
/* @core_internal_state__do_not_mess_with_it: core internal status information
* @depth: disable-depth, for nested irq_disable() calls
* @wake_depth: enable depth, for multiple irq_set_irq_wake() callers
* @irq_count: stats field to detect stalled irqs
* @last_unhandled: aging timer for unhandled count
* @irqs_unhandled: stats field for spurious unhandled interrupts
* @threads_handled: stats field for deferred spurious detection of threaded handlers
* @threads_handled_last: comparator field for deferred spurious detection of theraded handlers
* @lock: locking for SMP
* @affinity_hint: hint to user space for preferred irq affinity
* @affinity_notify: context for notification of affinity changes
* @pending_mask: pending rebalanced interrupts
* @threads_oneshot: bitfield to handle shared oneshot threads
* @threads_active: number of irqaction threads currently running
* @wait_for_threads: wait queue for sync_irq to wait for threaded handlers
* @nr_actions: number of installed actions on this descriptor
* @no_suspend_depth: number of irqactions on a irq descriptor with
* IRQF_NO_SUSPEND set
* @force_resume_depth: number of irqactions on a irq descriptor with
* IRQF_FORCE_RESUME set
* @dir: /proc/irq/ procfs entry
* @name: flow handler name for /proc/interrupts output
name指定了电流层处理程序的名称,将显示在/proc/interrupts中。对边沿触发中断,通常
是“edge”,对电平触发中断,通常是“level”。
*/
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data irq_data; //其中包含了irq_chip成员:用于表示一个IRQ控制器抽象的具体特征;
//通常在一个系统上只有一种类型的中断控制器会占据支配地位(当然,并没有什么约束条件阻止多个控制器并存),所有irq_desc的 irq_data的chip成员都指向irq_chip的同一实例
unsigned int __percpu *kstat_irqs;
irq_flow_handler_t handle_irq;
irq_preflow_handler_t preflow_handler;
struct irqaction *action; /* IRQ action list */
unsigned int status_use_accessors;
unsigned int core_internal_state__do_not_mess_with_it;
unsigned int depth; /* nested irq disables */
unsigned int wake_depth; /* nested wake enables */
unsigned int irq_count; /* For detecting broken IRQs */
unsigned long last_unhandled; /* Aging timer for unhandled count */
unsigned int irqs_unhandled;
atomic_t threads_handled;
int threads_handled_last;
raw_spinlock_t lock;
struct cpumask *percpu_enabled;
const struct cpumask *affinity_hint;
struct irq_affinity_notify *affinity_notify;
cpumask_var_t pending_mask;
unsigned long threads_oneshot;
atomic_t threads_active;
wait_queue_head_t wait_for_threads;
unsigned int nr_actions;
unsigned int no_suspend_depth;
unsigned int cond_suspend_depth;
unsigned int force_resume_depth;
struct proc_dir_entry *dir;
int parent_irq;
struct module *owner;
const char *name;
} ____cacheline_internodealigned_in_smp;一些结构的解释:
irq控制器抽象:
struct irq_data irq_data; //其中包含了irq_chip成员:用于表示一个IRQ控制器抽象的具体特征;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83/**
* struct irq_chip - hardware interrupt chip descriptor
*
* @name: name for /proc/interrupts
* @irq_startup: start up the interrupt (defaults to ->enable if NULL)
* @irq_shutdown: shut down the interrupt (defaults to ->disable if NULL)
* @irq_enable: enable the interrupt (defaults to chip->unmask if NULL)
* @irq_disable: disable the interrupt
* @irq_ack: start of a new interrupt
* @irq_mask: mask an interrupt source
* @irq_mask_ack: ack and mask an interrupt source
* @irq_unmask: unmask an interrupt source
* @irq_eoi: end of interrupt
* @irq_set_affinity: set the CPU affinity on SMP machines
* @irq_retrigger: resend an IRQ to the CPU
* @irq_set_type: set the flow type (IRQ_TYPE_LEVEL/etc.) of an IRQ
* @irq_set_wake: enable/disable power-management wake-on of an IRQ
* @irq_bus_lock: function to lock access to slow bus (i2c) chips
* @irq_bus_sync_unlock:function to sync and unlock slow bus (i2c) chips
* @irq_cpu_online: configure an interrupt source for a secondary CPU
* @irq_cpu_offline: un-configure an interrupt source for a secondary CPU
* @irq_suspend: function called from core code on suspend once per chip
* @irq_resume: function called from core code on resume once per chip
* @irq_pm_shutdown: function called from core code on shutdown once per chip
* @irq_calc_mask: Optional function to set irq_data.mask for special cases
* @irq_print_chip: optional to print special chip info in show_interrupts
* @irq_request_resources: optional to request resources before calling
* any other callback related to this irq
* @irq_release_resources: optional to release resources acquired with
* irq_request_resources
* @irq_compose_msi_msg: optional to compose message content for MSI
* @irq_write_msi_msg: optional to write message content for MSI
* @irq_get_irqchip_state: return the internal state of an interrupt
* @irq_set_irqchip_state: set the internal state of a interrupt
* @flags: chip specific flags
*/
结构需要考虑内核中出现的各个IRQ实现的所有特性。因而,一个该结构的特定实例,通常只
定义所有可能方法的一个子集
struct irq_chip {
const char *name;//表示硬件控制器,
unsigned int (*irq_startup)(struct irq_data *data);//用于第一次初始化一个IRQ
void (*irq_shutdown)(struct irq_data *data);//完全关闭一个中断源
void (*irq_enable)(struct irq_data *data);//用于激活一个IRQ
void (*irq_disable)(struct irq_data *data);//禁用IRQ
void (*irq_ack)(struct irq_data *data);
//在某些模型中, IRQ请求的到达(以及在处理器的对应中
断)必须显式确认,后续的请求才能进行处理。如果芯片组没有这样的要求,该指针可以指
向一个空函数,或NULL指针。 ack_and_mask确认一个中断,并在接下来屏蔽该中断。
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);//eoi表示end of interrupt,即中断结束
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);
void (*irq_cpu_online)(struct irq_data *data);
void (*irq_cpu_offline)(struct irq_data *data);
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);
unsigned long flags;
};
找一个例子看看:/arch/x86/kernel/i8295.c:
1 | struct irq_chip i8259A_chip = { |
- 处理程序函数的表示:name和dev_id唯一地标识一个中断处理程序。 name是一个短字符串,用于标识设备(例如,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30/**
* struct irqaction - per interrupt action descriptor
* @handler: interrupt handler function
* @name: name of the device
* @dev_id: cookie to identify the device
* @percpu_dev_id: cookie to identify the device
* @next: pointer to the next irqaction for shared interrupts
* @irq: interrupt number
* @flags: flags (see IRQF_* above)
* @thread_fn: interrupt handler function for threaded interrupts
* @thread: thread pointer for threaded interrupts
* @thread_flags: flags related to @thread
* @thread_mask: bitmask for keeping track of @thread activity
* @dir: pointer to the proc/irq/NN/name entry
*/
struct irqaction {
irq_handler_t handler;//函数指针,指向中断处理程序
void *dev_id;
void __percpu *percpu_dev_id;
struct irqaction *next;//用于实现共享的IRQ处理程序,几个irqaction实例聚集到一个链表中。链表的所有元素都
//必须处理同一IRQ编号(处理不同编号的实例,位于irq_desc数组中不同的位置),在发生一个共享中断时,内核扫描该链表找出中断实际上的来源设备。
irq_handler_t thread_fn;
struct task_struct *thread;
unsigned int irq;
unsigned int flags;<interrupt.h>中定义了
unsigned long thread_flags;
unsigned long thread_mask;
const char *name;
struct proc_dir_entry *dir;
} ____cacheline_internodealigned_in_smp;
“e100”、“ncr53c8xx”,等等),而dev_id是一个指针,指向在所有内核数据结构中唯一标识了该设
备的数据结构实例,例如网卡的net_device实例。
上面的flag:
1 | /* |
中断处理的相关操作:
- 设置控制器硬件:
irq.h:1
2
3
4
5
6
7
8/* Set/get chip/data for an IRQ: */
extern int irq_set_chip(unsigned int irq, struct irq_chip *chip);
extern int irq_set_handler_data(unsigned int irq, void *data);
extern int irq_set_chip_data(unsigned int irq, void *data);
extern int irq_set_irq_type(unsigned int irq, unsigned int type);
extern int irq_set_msi_desc(unsigned int irq, struct msi_desc *entry);
extern int irq_set_msi_desc_off(unsigned int irq_base, unsigned int irq_offset,
struct msi_desc *entry); - 初始化和分配irq:
注册irq:
1
2
3
4
5
6kernel/irq/manage.c
int request_irq(unsigned int irq,
irqreturn_t handler,
unsigned long irqflags, const char *devname, void *dev_id)
往往在设备驱动程序初始化时会做这个request_irq操作把处理程序注册进去;
setup_irq2)释放irq:
free_irq注册中断:
中断到达后:找到正确的中断处理程序的通用函数:do_IRQ,此时若处理器在用户态,需要切换到核心态;
linux中do_IRQ是和体系结构相关的函数:
例如:1
2
3
4
5
6
7
8AMD:
irq.c:
do_IRQ
--> set_irq_regs
--> irq_enter
--> generic_handle_irq -->.... handle_irq
--> irq_exit
--> set_irq_regs关键处理:
handle_irq_event1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16irqreturn_t handle_irq_event(struct irq_desc *desc)
{
struct irqaction *action = desc->action;
irqreturn_t ret;
desc->istate &= ~IRQS_PENDING;
irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);
raw_spin_unlock(&desc->lock);
ret = handle_irq_event_percpu(desc, action);//--->res = action->handler(irq, action->dev_id);
raw_spin_lock(&desc->lock);
irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);
return ret;
}IRQ栈:通过内核栈处理不够,所以又衍生了用于硬件IRQ处理的栈和用于软件IRQ处理的栈;
常规的内核栈对每个进程都会分配,而这两个额外的栈是针对各CPU分别分配的。在硬件中断发
生时(或处理软中断时),内核需要切换到适当的栈。1
2
3
4
5
6
7
8
9
10
11下列数组提供了指向额外的栈的指针:
arch/x86/kernel/irq_32.c
static union irq_ctx *hardirq_ctx[NR_CPUS] __read_mostly;
static union irq_ctx *softirq_ctx[NR_CPUS] __read_mostly;
用作栈的数据结构并不复杂:
arch/x86/kernel/irq_32.c
union irq_ctx {
struct thread_info tinfo;
u32 stack[THREAD_SIZE/sizeof(u32)];
};关于中断处理程序:
- 中断是异步执行的。换句话说,它们可以在任何时间发生。
- 例如,对网络驱动程序来说,不能将接收的数据直接转发到等待的应用程序。毕竟,内核无法确定等待数据的应用程序此时是否在运行(事实上,这种可能性很低)。
- 中断上下文中不能调用调度器。因而不能自愿地放弃控制权。
- 处理程序例程不能进入睡眠状态。
中断处理程序类型:irqreturn_t (*handler)(int irq, void *dev_id, struct pt_regs *regs)
一个例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43一个例子:tg3.c这个有napi形式的,故用这个:
```c
module_init(tg3_init);
static int __init tg3_init(void)
{
return pci_module_init(&tg3_driver);
}
static struct pci_driver tg3_driver = {
.name = DRV_MODULE_NAME,
.id_table = tg3_pci_tbl,
.probe = tg3_init_one,
.remove = __devexit_p(tg3_remove_one),
.suspend = tg3_suspend,
.resume = tg3_resume
};
static int __devinit tg3_init_one(struct pci_dev *pdev,
const struct pci_device_id *ent)
{
static int tg3_version_printed = 0;
unsigned long tg3reg_base, tg3reg_len;
struct net_device *dev;
struct tg3 *tp;
int i, err, pci_using_dac, pm_cap;
...
//在init中没有申请中断,直到open时才申请:
static int tg3_open(struct net_device *dev)
{
struct tg3 *tp = dev->priv;
int err;
...
err = request_irq(dev->irq, tg3_interrupt,
SA_SHIRQ, dev->name, dev);
//来看中断处理程序:
static irqreturn_t tg3_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
struct net_device *dev = dev_id;
struct tg3 *tp = dev->priv;
//这里使用新式接口napi:
if (likely(tg3_has_work(dev, tp)))
netif_rx_schedule(dev); /* schedule NAPI poll */
重要的中断处理程序:
电流处理: typedef void fastcall (*irq_flow_handler_t)(unsigned int irq,
struct irq_desc *desc);边沿触发中断
handle_edge_irq,chip.c电平触发中断:
handle_level_irq,
软件中断
用于有效实现内核中的延期操作。完全用软件来实现,但是运作方式和硬件中断类似;softIRQ;
不管是哪种体系结构的do_IRQ,其结尾都会去处理所有待决的软中断,确保软中断可以定期处理;
软中断表
一个包含也许是32个softirq_action类型的数据表;
interrupt.h:
1 | struct softirq_action |
软中断的流程:
先注册,内核才能执行:
open_softirq函数用于注册: 就是往上述表中填入action
kernel/softirq.c:1
2
3
4void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}各个软中断都有一个唯一的编号,这表明软中断是相对稀缺的资源,使用其必须谨慎,不能由各种设备驱动程序和内核组件随意使用。
这个数组也是被定义在softirq.c中;
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
可以看枚举知道先有的以这种方式注册的软中断:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ, 网络的发送
NET_RX_SOFTIRQ, 网络接收
BLOCK_SOFTIRQ,块处理
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,调度器
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};只有重要的情况才能用软中断,其他可以使用tasklet,工作队列或内核定时器的方式来达到延期处理的目的;
发起中断:
硬件中断需要通过硬件设备触发中断控制器向cpu发起中断,然后进入中断处理程序处理,软中断没有这个机制,需要通过软件的方式发起中断:
void raise_softirq(unsigned int nr) 用于引发一个软中断;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
/*
* This function must run with irqs disabled!
*/
inline void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr);
/*
* If we're in an interrupt or softirq, we're done
* (this also catches softirq-disabled code). We will
* actually run the softirq once we return from
* the irq or softirq.
*
* Otherwise we wake up ksoftirqd to make sure we
* schedule the softirq soon.
*/
if (!in_interrupt())
wakeup_softirqd();
}
//如果不在中断上下文调用raise_softirq,则调用wakeup_softirqd来唤醒软中断守护进程,这
//是开启软中断处理的两个可选方法之一。该函数设置各CPU变量irq_stat[smp_processor_id].__softirq_pending中的对应比特位。
该函数将相应的软中断标记为执行,但这个执行是延期执行。通过使用特定于处理器的位图,内核确
保几个软中断(甚至是相同的)可以同时在不同的CPU上执行。流程:
在注册了软中断后,若引发软中断且处理软中断的handler时,就会匹配到对应的函数,进行处理;
软中断处理被归结到do_softirq函数:1
2
3
4
5do_softirq
-->local_softirq_pending
-->__do_softirq
-->h->action--循环处理
--> local_softirq_pending且重启次数没有超过次数->重启软中断因为软中断大部分是用来延迟硬中断的后续处理,所以需要一个软中断守护进程来进行异步执行软中断,一方面不和硬中断冲突,另一方面确保软中断的及时执行;
系统为每个处理器都分配了自身的守护进程: ksoftirqd,为软中断守护进程;
所以实际上raise_softirq是为了唤醒这个守护进程,进行处理软件中断,进行及时处理;软中断守护进程:ksoftirqd
软中断守护进程的任务是,与其余内核代码异步执行软中断。为此,系统中的每个处理器都分配了自身的守护进程,名为ksoftirqd
内核中有两处调用wakeup_softirqd唤醒了该守护进程。
- 在do_softirq中,如前所述。
- 在raise_softirq_irqoff末尾。该函数由raise_softirq在内部调用,如果内核当前停用了
中断,也可以直接使用。
唤醒函数本身只需要几行代码。首先,借助于一些宏,从一个各CPU变量读取指向当前CPU软中
断守护进程的task_struct的指针。如果该进程当前的状态不是TASK_RUNNING,则通过wake_up_
process将其放置到就绪进程的列表末尾(参见第2章)。尽管这并不会立即开始处理所有待决软中断,
但只要调度器没有更好的选择,就会选择该守护进程(优先级为19)来执行。
(1) 软中断守护进程的初始化和启动:
在系统启动时用initcall机制调用init后,就创建了系统的软中断守护进程,初始化后,各个守护进程进行无限循环:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16kernel/softirq.c
static int ksoftirqd(void * __bind_cpu)
...
while (!kthread_should_stop()) {
if (!local_softirq_pending()) {
schedule();
}
__set_current_state(TASK_RUNNING);
while (local_softirq_pending()) {
do_softirq();
cond_resched();
}
set_current_state(TASK_INTERRUPTIBLE);
}
...
}tasklet ,另一种轻量的软中断
软中断是将操作推迟到未来时刻执行的最有效的方法。但该延期机制处理起来非常复杂。因为多
个处理器可以同时且独立地处理软中断,同一个软中断的处理程序例程可以在几个CPU上同时运行。
对软中断的效率来说,这是一个关键,多处理器系统上的网络实现显然受惠于此。
但处理程序例程的设计必须是完全可重入且线程安全的。 另外, 临界区必须用自旋锁保护(或其他IPC机制),
而这需要大量审慎的考虑。
tasklet是“小进程”,执行一些迷你任务,对这些任务使用全功能进程可能比较浪费
创建tasklet:
各个tasklet的中枢数据结构称作tasklet_struct,
1 | interrupt.h: |
tasklet_vec 是一个pcpu变量,也就是每一个cpu上上的tasklet组成一个list.
1 | struct tasklet_head { |
注册tasklet
要使用它,首先必须注册它;
1 | void tasklet_init(struct tasklet_struct *t, |
执行:
tasklet基于软中断实现,它们总是在处理软中断时执行。
tasklet关联到TASKLET_SOFTIRQ软中断。因而,调用raise_softirq(TASKLET_SOFTIRQ),即可
在下一个适当的时机执行当前处理器的tasklet。
interrupt.h:
1 | static inline void tasklet_schedule(struct tasklet_struct *t) |
内核使用tasklet_action作为该软中断的action函数
该函数首先确定特定于CPU的链表,其中保存了标记为将要执行的各个tasklet。它接下来将表头
重定向到函数局部的一个数据项,相当于从外部公开的链表删除了所有表项。接下来,函数在以下循
环中逐一处理各个tasklet:
1 | static void tasklet_action(struct softirq_action *a) |
在while循环中执行tasklet,类似于处理软中断使用的机制。
因为一个tasklet只能在一个处理器上执行一次,但其他的tasklet可以并行运行,所以需要特定于
tasklet 的 锁 。 state 状 态 用 作 锁 变 量 。 在 执 行 一 个 tasklet 的 处 理 程 序 函 数 之 前 , 内 核 使 用
tasklet_trylock检查tasklet的状态是否为TASKLET_STATE_RUN。换句话说,它是否已经在系
统的另一个处理器上运行:
1 | <interrupt.h> |
除了普通的tasklet之外,内核还使用了另一种tasklet,它具有“较高”的优先级。除以下修改之
外,其实现与普通的tasklet完全相同。
- 使用HI_SOFTIRQ作为软中断,而不是TASKLET_SOFTIRQ,相关的action函数是tasklet_
hi_action。 - 注册的tasklet在CPU相关的变量tasklet_hi_vec中排队。这是使用tasklet_hi_schedule完
成的。
在这里,“较高优先级”是指该软中断的处理程序HI_SOFTIRQ在所有其他处理程序之前执行,
尤其是在构成了软中断活动主体的网络处理程序之前执行。
当前,大部分声卡驱动程序都利用了这一选项,因为操作延迟时间太长可能损害音频输出的音质。
而用于高速传输的网卡也可以得益于该机制
等待队列和完成量;
等待队列( wait queue)用于使进程等待某一特定事件发生,而无须频繁轮询。进程在等待期间
睡眠,在事件发生时由内核自动唤醒。 完成量( completion)机制基于等待队列,内核利用该机制等
待某一操作结束。这两种机制使用得都比较频繁,主要用于设备驱动程序,
等待队列:
数据结构:
每个等待队列都有一个队列头:
wait.h:
1 | struct __wait_queue_head { |
因为等待队列也可以在中断时修改,在操作队列之前必须获得一个自旋锁lock。
task_list是一个双链表,用于实现双链表最擅长表示的结构,即队列。
队列中的成员:
1 | struct __wait_queue { |
等待队列的使用分为如下两部分。
(1) 为使当前进程在一个等待队列中睡眠,需要调用wait_event函数(或某个等价函数,在下文
讨论)。进程进入睡眠,将控制权释放给调度器。
内核通常会在向块设备发出传输数据的请求后,调用该函数。因为传输不会立即发生,而在此期
间又没有其他事情可做,所以进程可以睡眠,将CPU时间让给系统中的其他进程。
(2) 在内核中另一处,就我们的例子而言,是来自块设备的数据到达后,必须调用wake_up函数(或
某个等价函数,将在下文讨论)来唤醒等待队列中的睡眠进程。
在使用wait_event使进程睡眠之后,必须确保在内核中另一处有一个对应的wake_up调用
使进程睡眠:
add_wait_queue函数用于将一个进程增加到等待队列,该函数在获得必要的自旋锁后,将工作
委托给__add_wait_queue:
1 | <wait.h> |
在将新进程统计到等待队列时,除了使用标准的list_add链表函数,没有其他工作需要做。
内核还提供了add_wait_queue_exclusive函数。它的工作方式与add_wait_queue相同,但将
进程插入在队列尾部,并将其标志设置为WQ_EXCLUSIVE
另一种方法: prepare_to_wait也是用来使进程睡眠,比上述方法多一个参数,表示进程状态;
初始化一个动态分配的wait_queue_t实例:
1 | static inline void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p) |
或者DEFINE_WAIT创建wait_queue_t的静态实例,它可以自动初始化:
1 |
使进程睡眠,通常不直接调用add_wait_queue,而是调用wait_event;
1 |
|
在条件满足时, finish_wait将进程状态设置回TASK_RUNNING,并从等待队列的链表移除对应的项。
除了wait_event之外,内核还定义了其他几个函数,可以将当前进程置于等待队列中。其实现实际上等同于sleep_on:
1 | <wait.h> |
- wait_event_interruptible使用的进程状态为TASK_INTERRUPTIBLE。因而睡眠进程可以通
过接收信号而唤醒。 - wait_event_timeout等待满足指定的条件,但如果等待时间超过了指定的超时限制(按jiffies
指定)则停止。这防止了进程永远睡眠。 - wait_event_interruptible_timeout使进程睡眠,但可以通过接收信号唤醒。它也注册了
一个超时限制。从内核采用的命名方式来看,一般不会有出人意料之处!
唤醒进程:
1 | <wait.h> |
这里会反复扫描链表,直至没有更多进程需要唤醒,或已经唤醒的独占进程的数目达到了
nr_exclusive。该限制用于避免所谓的惊群( thundering herd)问题。如果几个进程在等待独占访问
某一资源,那么同时唤醒所有等待进程是没有意义的,因为除了其中一个之外,其他进程都会再次睡
眠。 nr_exclusive推广了这一限制。
最常使用的wake_up函数将nr_exclusive设置为1,确保只唤醒一个独占访问的进程
完成量
完成量和信号量有相似之处,但是都是基于等待队列实现的;
1 | <completion.h> |
init_completion初始化一个动态分配的completion实例,而DECLARE_COMPLETION宏用来建立
该数据结构的静态实例。
进程可以用wait_for_completion添加到等待队列,进程在其中等待(以独占睡眠状态),直至
请求被内核的某些部分处理。这函数需要一个completion实例作为参数:
1 | <completion.h> |
在请求由内核的另一部分处理之后,必须调用complete或complete_all来唤醒等待的进程。因
为每次调用只能从完成量的等待队列移除一个进程,对n个等待进程来说,必须调用该函数n次。 另一
方面, complete_all将唤醒所有等待该完成的进程。 complete_and_exit是一个小的包装器,首先
调用complete,接下来调用do_exit结束内核线程。
1 | <completion.h> |
工作队列:
工作队列是将操作延期执行的另一种手段:因为它们是通过守护进程在用户上下文执行,函数可
以睡眠任意长的时间,这与内核是无关的。在内核版本2.5开发期间,设计了工作队列,用以替换此前
使用的keventd机制。
每个工作队列都有一个数组,数组项的数目与系统中处理器的数目相同。每个数组项都列出了将
延期执行的任务。
对每个工作队列来说,内核都会创建一个新的内核守护进程,延期任务使用上文描述的等待队列
机制,在该守护进程的上下文中执行。
- 创建新工作队列:
新的工作队列通过调用create_workqueue或create_workqueue_singlethread函数来创建。前
一个函数在所有CPU上都创建一个工作线程,而后者只在系统的第一个CPU上创建一个线程。