linux内核设计与实现 —— 中断和中断处理(第7章,第8章)

中断和中断处理

中断的目的:让处理器最快地响应外部硬件的请求。

中断本质上是一种特殊的电信号,由硬件设备发向处理器,处理器反映到操作系统中,最后由操作系统处理这个中断电信号。

不同的设备对应的中断不同。每个中断都通过一个唯一的数字标记,这个标记通常被称为中断请求(IRQ)线。

每个中断都有一个中断处理程序,运行在中断上下文中。(中断上下文与进程上下文的区别在于:中断上下文中的执行代码不可阻塞)。

内核必须保证中断处理程序能够快速响应,并且在尽可能短的时间内完成运行。

为了在大量的工作与快速执行之间求得一种平衡,内核把处理中断的工作分为两部分,上半部和下半部。上半部,一般称为中断处理程序,是在接收到一个中断后,就立即开始执行,并且禁止一些或全部中断。下半部,有软中断,tasklet和工作队列,就是执行与中断处理密切相关但中断处理程序本身不执行的工作,执行期间可以响应所有的中断。

上半部 —— 中断处理程序

1. 中断处理

在响应一个特定中断的时候,内核会执行一个函数,该函数叫做中断处理程序(interrupt handler)或中断服务例程(interrupt service routine,ISR)。

中断处理函数

/**
 * irq:处理程序要响应的中断号
 * dev_id:由注册中断处理程序request_irq()的参数dev_id传递,常用来区分共享同一中断处理程序的多个设备
 */
static irqreturn_t intr_handler(int irq, void *dev_id)

注册中断处理函数

/**
 * irq:申请的硬件中断号
 * handler:向系统注册的中断处理函数,当中断发生时会触发该函数,dev_id参数将被传递给它
 * irqflags: 中断处理标志,上升沿触发,下降沿触发等。。。。
 * devname:设置中断名称,通常是设备驱动程序的名称
 * dev_id:在中断共享时会用到,一般设置为这个设备的设备结构体或者NULL
 */
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id)

释放中断处理函数

/**
 * irq:申请的硬件中断号
 * handler:向系统注册的中断处理函数,当中断发生时会触发该函数,dev_id参数将被传递给它
 * irqflags: 中断处理标志,上升沿触发,下降沿触发等。。。。
 * devname:设置中断名称,通常是设备驱动程序的名称
 * dev_id:在中断共享时会用到,一般设置为这个设备的设备结构体或者NULL
 */
void free_irq(unsigned int irq,void *dev_id)

2. 中断控制

Linux内核提供了一组接口用于操作机器上的中断状态。这些接口能够让我们禁止当前处理器上的所有中断,或者屏蔽掉其中的一条中断线。
一般来说,控制中断系统的最根本的原因是需要提供同步。通过禁止中断,可以确保某个中断处理程序不会抢占当前的代码。此外,禁止中断还可以禁止内核抢占。

禁止当前处理器上的中断,宏定义

local_irq_disable()

激活当前处理器上的中断,宏定义

local_irq_disable()

禁止当前处理器上的中断,在禁止前保存中断系统的状态,宏定义

local_irq_save(flags)

中断被恢复到给定的状态,宏定义

local_irq_save(flags)

禁止指定的中断线,函数

/**
 * 禁止给定中断线,并确保该函数返回之前在该中断线上没有处理程序在运行
 */
void disable_irq(unsigned int irq);

/**
 * 禁止给定中断线
 */
void disable_irq_nosync(unsigned int irq);

/**
 * 激活给定中断线
 */
void enable_irq(unsigned int irq);

/**
 * 等待一个特定的中断程序的退出
 */
void synchronize_irq(unsigned int irq);

了解中断系统的状态,宏定义

/**
 * 如果本地中断传递被禁止,则返回非0,否则返回0
 */
irqs_disabled()

/**
 * 如果在中断上下文中,则返回非0;如果在进程上下文中,则返回0
 */
in_interrupt()

/**
 * 如果当前正在执行中断处理程序,则返回非0,否则返回0
 */
in_irq()

下半部 —— 推后的执行的工作

中断处理程序的上半部在接收到一个中断时就立即执行,但只做比较紧急的工作,这些工作都是在所有中断被禁止的情况下完成的,所以要快,否则其它的中断就得不到及时的处理。那些耗时又不紧急的工作被推迟到下半部去。

中断处理程序的下半部分(如果有的话)几乎做了中断处理程序所有的事情。它们最大的不同是上半部分不可中断,而下半部分可中断

在理想的情况下,最好是中断处理程序上半部分将所有工作都交给下半部分执行,这样的话在中断处理程序上半部分中完成的工作就很少,也就能尽可能快地返回。但是,中断处理程序上半部分一定要完成一些工作,例如,通过操作硬件对中断的到达进行确认,还有一些从硬件拷贝数据等对时间比较敏感的工作。剩下的其他工作都可由下半部分执行。

通常下半部在中断处理程序一返回就会马上运行。下半部执行的关键在于当他们运行的时候,允许响应所有的中断。
对于上半部分和下半部分之间的划分没有严格的规则,靠驱动程序开发人员自己的编程习惯来划分,不过还是有一些习惯供参考:

  • 如果该任务对时间比较敏感,将其放在上半部中执行
  • 如果该任务和硬件相关,一般放在上半部中执行
  • 如果该任务要保证不被其他中断打断,放在上半部中执行(因为这是系统关中断)
  • 其他不太紧急的任务, 一般考虑在下半部执行

Linux内核实现下半部的机制主要有软中断tasklet工作队列,下面对他们的实现做简单介绍。

1. 软中断

软中断处理程序

软中断是一组静态定义的下半部接口,有32个,可以在所有处理器上同时执行,即使两个类型相同也可以。
软中断保留给系统中对时间要求最严格以及最重要的下半部使用。目前,只有两个子系统(网络和SCSI)直接使用软中断。此外,内核定时器和tasklet都是建立在软中断上的。
软中断由softirq_struct结构表示,它定义在 linux/interrupt.h 中:

struct softirq_action {
    void (*action)(struct softirq_action *);
}

在 kernel/softirq.c 中定义了一个包含有32个该结构体的数组:

static struct softirq_action softirq_vec[NR_SOFTIRQS];

每个被注册的软中断都占据该数组的一项,因此最多可能有32个软中断。
软中断处理程序action的函数原型如下:

void softirq_handler(struct softirq_action *)
添加软中断

(1) 分配索引
在编译期间,通过在 linux/interrupt.h 中定义的一个枚举类型来静态地声明软中断。建立一个新的软中断必须在此枚举类型中加入新的项。

(2) 注册中断处理程序
接着,在运行时通过调用open_softirq()注册软中断处理程序。

/**
 * nr:软中断的索引号
 * action:处理函数
 */
void open_softirq(int nr, void (*action)(struct softirq_action *));

(3) 触发软中断
raise_softirq()函数可以将一个软中断设置为挂起状态,让它在下次调用do_softirq()函数时投入运行。

执行软中断

一个注册的软中断必须在被标记后才会执行,这就是触发软中断(raising the softirq)。中断处理程序绘制返回前标记它的软中断,使其稍后被执行。在下面情况中,待处理的软中断会被检查和执行:

  • 从一个硬件中断代码处返回时
  • 在ksoftirqd内核线程中
  • 在那些显式检查和执行待处理的软中断的代码中,如网络子系统

不管是用什么办法唤起,软中断都要在do_softirq()中执行,该函数很简单,如果有待处理的软中断,do_softirq()会循环遍历每一个,调用它们的处理程序。以下是do_softirq()函数经过简化后的核心部分:

u32 pending;

pending = local_softirq_pending();
if (pending) {
    struct softirq_action *h;
    set_softirq_pending(0);
    h = softirq_vec;
    do {
        if (pending & 1)
            h->action(h);
        h++;
        pending >>= 1;
    } while (pending);
}

2. tasklet

tasklet是通过软中断实现的,所以它们本身也是软中断。tasklet由两类软中断代表:HI_SOFTIRQ和TASKLET_SOFTIRQ,这两者之间唯一的实际区别在于,HI_SOFTIRQ类型的软中断先于TASKLET_SOFTIRQ类型的软中断执行。
tasklet由tasklet_struct结构表示,每个结构体单独代表一个tasklet,它在 linux/interrupt.h 中定义:

struct tasklet_struct {
    struct tasklet_struct *next;  /*链表中的下一个tasklet */
    unsigned long state;          /* tasklet的状态,有三种:0, TASKLET_STATE_SCHED, TASKLET_STATE_RUN */
    atomic_t count;               /* 引用计数器,为0时,tasklet才被激活 */
    unsigned long data;           /* 给tasklet处理函数的参数 */  
}
调度tasklet

已调度的tasklet存放在两个单处理器数据结构:tasklet_vec(普通tasklet)和tasklet_hi_vec(高优先级的tasklet)。这两个数据结构都是由tasklet_struct结构体构成的链表,链表中的每个tasklet_struct代表一个不同的tasklet。
tasklet由tasklet_schedule()和tasklet_hi_schedule函数进行调度。
所有的tasklet都通过重复运用HI_SOFTIRQ和TASKLET_SOFTIRQ这两个软中断实现。当一个tasklet被调度时,内核就会唤起这两个软中断函数中的一个。随后,do_softirq()被执行,调用函数tasklet_action()或者tasklet_hi_action()处理tasklet。

使用tasklet

(1) 创建tasklet
可以静态创建,使用宏DECLARE_TASKLET或者DECLARE_TASKLET_DISABLED

#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }

#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }

也可以直接使用结构体struct tasklet_struct动态创建,然后使用函数tasklet_init初始化:

/**
 * t:tasklet_struct结构体
 * func:tasklet处理函数
 * data: tasklet处理函数的传递参数
 */
void tasklet_init(struct tasklet_struct *t,
             void (*func)(unsigned long), unsigned long data)

(2) tasklet处理程序
tasklet处理函数必须符合规定的函数类型,如下:

void tasklet_handler(unsigned long data)

(3) 调度tasklet
通过调用tasklet_schedule()函数并传递给它相应的tasklet_struct的指针,该tasklet就会被调度以便执行:

tasklet_schedule(&my_tasklet);  /* 把my_tasklet标记为挂起 */

3. 中断处理

对于软中断(和tasklet),内核会在几个特殊的时机执行(注意执行和调度的区别,调度软中断只是对软中断打上待执行的标记,并没有真正执行),而在中断处理程序返回时处理是最常见的。软中断的触发频率有时可能会很高(例如进行大流量网络通信期间)。更不利的是,软中断的执行函数有时还会调度自身,所以如果软中断本身出现的频率较高,再加上他们又有将自己重新设置为可执行状态的能力,那么就会导致用户空间的进程无法获得足够的处理时间,因而处于饥饿状态。

为了避免用户进程的饥饿。内核开发者做了一些折中,最终在内核的实现方案中是不会立即处理由软中断自身重新触发的软中断(不允许软中断嵌套)。而作为改进,内核会唤醒一组内核线程来处理这些过多的软中断,这些内核线程在最低优先级上运行(nice值是19),这能避免它们跟其他重要的任务抢夺资源,但它们最终肯定会被执行,所以这个方案能够保证软中断负载很重的时候,用户进程不会因为得不到处理时间而处于饥饿状态,相应的,也能保证过量的软中断终究会得到处理。

每个处理器都有一个这样的线程,来辅助处理软中断。所有线程的名字都叫做ksoftirqd/n,区别在于n,它对应的是处理器的编号。线程执行代码类似下面:

for (;;) {
    if (!softirq_pending(cpu))
        schedule();

    set_current_state(TASK_RUNNING);

    while (softirq_pending(cpu)) {
        do_softirq();
        if (need_resched())
            schedule();
    }

    set_current_state(TASK_INTERRUPTIBLE);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值