系统相关
首页 > 系统相关> > Linux内核入门到放弃-内核活动-《深入Linux内核架构》笔记

Linux内核入门到放弃-内核活动-《深入Linux内核架构》笔记

作者:互联网

中断

中断类型

在退出中断中,内核会检查下列事项。

数据结构

IRQ相关信息管理的关键点是一个全局数组,每个数组项对应一个IRQ编号。因为数组位置和中断号是相同的,很容易定位与特定的IRQ相关的数组项:IRQ 0在位置0,IRQ 15在位置15,等等。IRQ最终映射到哪个处理器中断,在这里是不相关的。

该数组定义如下:

<kernel/irq/handle.c>
struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
    [0 ... NR_IRQS-1] = {
        .status = IRQ_DISABLED,
        .chip = &no_irq_chip,
        .handle_irq = handle_bad_irq,
        .depth = 1,
        .lock = __SPIN_LOCK_UNLOCKED(irq_desc->lock),
#ifdef CONFIG_SMP
        .affinity = CPU_MASK_ALL
#endif
    }
};

通用的IRQ子系统。它能够以统一的方式处理不同的中断控制器和不同类型的中断。基本上,它由3个抽象层组成:

边沿触发意味着硬件通过感知线路上的电位差来检测中断。在电平触发系统中,根据特定的电势值检测中断,与电势是否改变无关。从内核的角度来看,电平触发更为复杂,因为在每个中断后,都需要将线路明确设置为一个特定的电势,表示“没有中断”。

用于表示IRQ描述符的结构如下:

<linux/irq.h>
struct irq_desc {
    irq_flow_handler_t  handle_irq;
    struct irq_chip     *chip;
    struct msi_desc     *msi_desc;
    void            *handler_data;
    void            *chip_data;
    struct irqaction    *action;    /* IRQ action list */
    unsigned int        status;     /* IRQ status */

    unsigned int        depth;      /* nested irq disables */
    unsigned int        wake_depth; /* nested wake enables */
    unsigned int        irq_count;  /* For detecting broken IRQs */
    unsigned int        irqs_unhandled;
    unsigned long       last_unhandled; /* Aging timer for unhandled count */
    spinlock_t      lock;
#ifdef CONFIG_SMP
    cpumask_t       affinity;
    unsigned int        cpu;
#endif
#if defined(CONFIG_GENERIC_PENDING_IRQ) || defined(CONFIG_IRQBALANCE)
    cpumask_t       pending_mask;
#endif
#ifdef CONFIG_PROC_FS
    struct proc_dir_entry   *dir;
#endif
    const char      *name;
} ____cacheline_internodealigned_in_smp;

上面介绍的3个抽象层在该结构中表示如下:

IRQ不仅可以在处理程序安装期间改变其状态,而且可以在运行时改变: status 描述了IRQ的当前状态。 <irq.h> 文件定义了各种常数,可用于描述IRQ电路当前的状态。每个常数表示位串中一个置位的标志位,只要不相互冲突,几个标志可以同时设置。

IRQ控制器抽象

<include/linux/irq.h>

/**
 * struct irq_chip - hardware interrupt chip descriptor
 *
 * @name:       name for /proc/interrupts
 * @startup:        start up the interrupt (defaults to ->enable if NULL)
 * @shutdown:       shut down the interrupt (defaults to ->disable if NULL)
 * @enable:     enable the interrupt (defaults to chip->unmask if NULL)
 * @disable:        disable the interrupt (defaults to chip->mask if NULL)
 * @ack:        start of a new interrupt
 * @mask:       mask an interrupt source
 * @mask_ack:       ack and mask an interrupt source
 * @unmask:     unmask an interrupt source
 * @eoi:        end of interrupt - chip level
 * @end:        end of interrupt - flow level
 * @set_affinity:   set the CPU affinity on SMP machines
 * @retrigger:      resend an IRQ to the CPU
 * @set_type:       set the flow type (IRQ_TYPE_LEVEL/etc.) of an IRQ
 * @set_wake:       enable/disable power-management wake-on of an IRQ
 *
 * @release:        release function solely used by UML
 * @typename:       obsoleted by name, kept as migration helper
 */

struct irq_chip {
    const char  *name;
    unsigned int    (*startup)(unsigned int irq);
    void        (*shutdown)(unsigned int irq);
    void        (*enable)(unsigned int irq);
    void        (*disable)(unsigned int irq);

    void        (*ack)(unsigned int irq);
    void        (*mask)(unsigned int irq);
    void        (*mask_ack)(unsigned int irq);
    void        (*unmask)(unsigned int irq);
    void        (*eoi)(unsigned int irq);

    void        (*end)(unsigned int irq);
    void        (*set_affinity)(unsigned int irq, cpumask_t dest);
    int     (*retrigger)(unsigned int irq);
    int     (*set_type)(unsigned int irq, unsigned int flow_type);
    int     (*set_wake)(unsigned int irq, unsigned int on);

    /* Currently used only by UML, might disappear one day.*/
#ifdef CONFIG_IRQ_RELEASE_METHOD
    void        (*release)(unsigned int irq, void *dev_id);
#endif
    /*
     * For compatibility, ->typename is copied into ->name.
     * Will disappear.
     */
    const char  *typename;
};

处理程序函数的表示

<linux/interrupt.h>
struct irqaction {
    irq_handler_t handler;
    unsigned long flags;
    cpumask_t mask;
    const char *name;
    void *dev_id;
    struct irqaction *next;
    int irq;
    struct proc_dir_entry *dir;
};

中断电流处理

设置控制器硬件

首先,需要提到内核提供的一些标准函数,用于注册 irq_chip 和设置电流处理程序:

<irq.h>
int set_irq_chip(unsigned int irq, struct irq_chip *chip);//
void set_irq_handler(unsigned int irq, irq_flow_handler_t handle);
void set_irq_chained_handler(unsigned int irq, irq_flow_handler_t handle)
void set_irq_chip_and_handler(unsigned int irq, struct irq_chip *chip,irq_flow_handler_t handle);
void set_irq_chip_and_handler_name(unsigned int irq, struct irq_chip *chip,irq_flow_handler_t handle, const char*name);

电流处理

typedef void fastcall (*irq_flow_handler_t)(unsigned int irq,struct irq_desc *desc);

不同的硬件需要不同的电流处理方式,例如,边沿触发和电平触发就需要不同的处理。内核对各种类型提供了几个默认的电流处理程序。它们有一个共同点:每个电流处理程序在其工作结束后,都要负责调用高层ISR。 handle_IRQ_event 负责激活高层的处理程序。

边沿触发中断

handle_edge_irq

在处理边沿触发的IRQ时无须屏蔽,这与电平触发IRQ是相反的。这对SMP系统有一个重要的含义:当在一个CPU上处理一个IRQ时,另一个同样编号的IRQ可以出现在另一个CPU上,称为第二个CPU。这意味着,当电流处理程序在由第一个IRQ触发的CPU上运行时,还可能被再次调用。但为什么应该有两个CPU同时运行同一个IRQ处理程序呢?内核想要避免这种情况:处理程序只应在一个CPU上运行。 handle_edge_irq 的开始部分必须处理这种情况。如果设置了 IRQ_INPROGRESS 标志,则该IRQ在另一个CPU上已经处于处理过程中。通过设置 IRQ_PENDING 标志,内核能够记录还有另一个IRQ需要在稍后处理。在屏蔽该IRQ并通过 mask_ack_irq 向控制器发送一个确认后,处理过程可以放弃。因而第二个CPU可以恢复正常的工作,而第一个CPU将在稍后处理该IRQ。

请注意,如果IRQ被禁用,或没有可用的ISR处理程序,都会放弃处理。

电平触发中断

与边沿触发中断相比,电平触发中断稍微容易处理一些。这也反映在电流处理程序 handle_level_irq 的代码流程图中。

请注意,电平触发中断在处理时必须屏蔽,因此需要完成的第一件事就是调用 mask_ack_irq 。该辅助函数屏蔽并确认IRQ,这是通过调用 chip->mask_ack ,如果该方法不可用,则连续调用chip->mask 和 chip->ack 。在多处理器系统上,可能发生竞态条件,尽管IRQ已经在另一个CPU上处理,但仍然在当前CPU上调用了 handle_level_irq 。这可以通过检查 IRQ_INPROGRESS 标志来判断,这种情况下,IRQ已经在另一个CPU上处理,因而在当前CPU上可以立即放弃处理。

如果没有对该IRQ注册处理程序,也可以立即放弃处理,因为无事可做。另一个导致放弃处理的原因是设置了 IRQ_DISABLED 。尽管被禁用,有问题的硬件仍然可能发出IRQ,但可以被忽略。

接下来开始对IRQ的处理。设置 IRQ_INPROGRESS ,表示该IRQ正在处理中,实际工作委托给handle_IRQ_event 。这触发了高层ISR,在下文讨论。在ISR结束之后,清除 IRQ_INPROGRESS。

最后,需要解除对IRQ的屏蔽。但内核需要考虑到ISR可能禁用中断的情况,在这种情况下,ISR仍然保持屏蔽状态。否则,使用特定于芯片的函数 chip->unmask 解除屏蔽。

初始化和分配IRQ

注册IRQ

<kernel/irq/manage.c>
int request_irq(unsigned int irq, irq_handler_t handler,
        unsigned long irqflags, const char *devname, void *dev_id)

内核首先生成一个新的 irqaction 实例,然后用函数参数填充其内容。当然,其中特别重要的是处理程序函数 handler 。所有进一步的工作都委托给 setup_irq 函数,它将执行下列步骤。

释放IRQ

free_irq

处理IRQ

切换到核心态

到核心态的切换,是基于每个中断之后由处理器自动执行的汇编语言代码的。该代码的任务如上文所述。其实现可以在 arch/arch/kernel/entry.S 中找到, 其中通常定义了各个入口点,在中断发生时处理器可以将控制流转到这些入口点。

在C语言中调用函数时,需要将所需的数据(返回地址和参数)按一定的顺序放到栈上。在用户态和核心态之间切换时,还需要将最重要的寄存器保存到栈上,以便以后恢复。这两个操作由平台相关的汇编语言代码执行。在大多数平台上,控制流接下来传递到C函数 do_IRQ , 其实现也是平台相关的,但情况仍然得到了很大的简化。

arch/arch/kernel/irq.c
fastcall unsigned int do_IRQ(struct pt_regs regs)

IRQ栈

常规的内核栈对每个进程都会分配,而这两个额外的栈是针对各CPU分别分配的。在硬件中断发生时(或处理软中断时),内核需要切换到适当的栈。

调用电流处理程序例程

以AMD64结构的do_IRQ为例:

调用对所述IRQ注册的ISR的任务委托给体系结构无关的函数 generic_handle_irq ,它调用 irq_desc[irq]->handle_irq 来激活电流控制处理程序。

调用高层ISR

回想上文可知,不同的电流处理程序例程都有一个共同点:采用 handle_IRQ_event 来激活与特定IRQ相关的高层ISR。

handle_IRQ_event 可能执行下述操作:

实现处理程序例程

中断上下文与普通上下文的不同之处主要有如下3点:

当然,只确保处理程序例程的直接代码不进入睡眠状态,这是不够的。其中调用的所有过程和函数(以及被这些函数/过程调用的函数/过程,依此类推)都不能进入睡眠状态。对此进行的检查并不简单,必须非常谨慎,特别是在控制路径存在大量分支时。

中断处理程序只能使用两种返回值:如果正确地处理了IRQ则返回 IRQ_HANDLED ,如果ISR不负责该IRQ则返回 IRQ_NONE 。

处理程序例程的任务是什么?为处理共享中断,例程首先必须检查IRQ是否是针对该例程的。 如果相关的外部设备设计得比较现代,那么硬件会提供一个简单的方法来执行该检查,通常是通过一个专门的设备寄存器。如果是该设备引起中断,则寄存器值设置为1。在这种情况下,处理程序例程必须将设备寄存器恢复默认值(通常是0),接下来开始正常的中断处理。如果例程发现设备寄存器值为0,它可以确信所管理的设备不是中断源,因而可以将控制返回到高层代码。

如果设备没有此类状态寄存器,还在使用手工轮询的方案。每次发生一个中断时,处理程序都检查相关设备是否有数据可用。倘若如此,则处理数据。否则,例程结束。

软中断

软中断机制的核心部分是一个表,包含了32个softirq_action类型的数据项。

<linux/interrupt.h>
struct softirq_action
{
    void    (*action)(struct softirq_action *);
    void    *data;
};

软中断必须先注册,然后内核才能执行软中断。 open_softirq 函数即用于该目的。它在 softirq_vec 表中指定的位置写入新的软中断

<kernel/softirq.c>
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{
    softirq_vec[nr].data = data;
    softirq_vec[nr].action = action;
}

软中断只用于少数场合,这些都是相对重要的情况:

<linux/interrupt.h>
enum
{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
#ifdef CONFIG_HIGH_RES_TIMERS
    HRTIMER_SOFTIRQ,
#endif
};

其中两个用来实现tasklet( HI_SOFTIRQ 、 TASKLET_SOFTIRQ ),两个用于网络的发送和接收操作( NET_TX_SOFTIRQ 和 NET_RX_SOFTIRQ ,这是软中断机制的来源和其最重要的应用),一个用于块层,实现异步请求完成( BLOCK_SOFTIRQ ),一个用于调度器( SCHED_SOFTIRQ)以实现SMP系统上周期性的负载均衡。在启用高分辨率定时器时,还需要一个软中断( HRTIMER_SOFTIRQ )

raise_softirq(int nr) 用于引发一个软中断(类似普通中断)。软中断的编号通过参数指定。该函数设置各CPU变量 irq_stat[smp_processor_id].__softirq_pending 中的对应比特位。该函数将相应的软中断标记为执行,但这个执行是延期执行。通过使用特定于处理器的位图,内核确保几个软中断(甚至是相同的)可以同时在不同的CPU上执行。

如果不在中断上下文调用 raise_softirq ,则调用 wakeup_softirqd 来唤醒软中断守护进程。

开启软中断处理

do_softirq:

该函数首先确认当前不处于中断上下文中(当然,即不涉及硬件中断)。如果处于中断上下文,则立即结束。因为软中断用于执行ISR中非时间关键部分,所以其代码本身一定不能在中断处理程序内调用。

通过 local_softirq_pending ,确定当前CPU软中断位图中所有置位的比特位。如果有软中断等待处理,则调用 __do_softirq 。

该函数将原来的位图重置为0。换句话说,清除所有软中断。这两个操作都是在(当前处理器上)禁用中断的情况下执行,以防其他进程对位图的修改造成干扰。而后续代码是在允许中断的情况下执行。这使得在软中断处理程序执行期间的任何时刻,都可以修改原来的位图。

softirq_vec 中的 action 函数在一个 while 循环中针对各个待决的软中断被调用。

在处理了所有标记出的软中断之后,内核检查在此期间是否有新的软中断标记到位图中。要求在前一轮循环中至少有一个没有处理的软中断,而重启的次数没有超过 MAX_SOFTIRQ_RESTART (通常设置为10)。如果是这样,则再次按序处理标记的软中断。这操作会一直重复下去,直至在执行所有处理程序之后没有新的未处理软中断为止。

如果在 MAX_SOFTIRQ_RESTART 次重启处理过程之后,仍然有未处理的软中断,那么应该如何?内核将调用 wakeup_softirqd 唤醒软中断守护进程。

软中断守护进程

ksoftirqd:

每次被唤醒时,守护进程首先检查是否有标记出的待决软中断,否则明确地调用调度器,将控制转交到其他进程。

如果有标记出的软中断,那么守护进程接下来将处理软中断。进程在一个 while 循环中重复调用两个函数 do_softirq 和 cond_resched ,直至没有标记出的软中断为止.cond_resched 确保在对当前进程设置了 TIF_NEED_RESCHED 标志的情况下调用调度器

tasklet

软中断的处理程序例程可以在几个CPU上同时运行。对软中断的效率来说,这是一个关键,多处理器系统上的网络实现显然受惠于此。但处理程序例程的设计必须是完全可重入且线程安全的。另外,临界区必须用自旋锁保护,而这需要大量审慎的考虑。

tasklet和工作队列是延期执行工作的机制,其实现基于软中断。

tasklet是“小进程”,执行一些迷你任务,对这些任务使用全功能进程可能比较浪费。

创建tasklet

tasklet的中枢数据结构称作 tasklet_struct:

<linux/interrupt.h>
struct tasklet_struct
{
    struct tasklet_struct *next;
    unsigned long state;
    atomic_t count;
    void (*func)(unsigned long);
    unsigned long data;
};

注册tasklet

tasklet_shedule将一个tasklet注册到系统中去:

<interrupt.h>
static inline void tasklet_schedule(struct tasklet_struct *t);

如果设置了 TASKLET_STATE_SCHED 标志位,则结束注册过程,因为该tasklet此前已经注册了。

否则,将该tasklet置于一个链表的起始,其表头是特定于CPU的变量 tasklet_vec 。该链表包含了所有注册的tasklet,使用 next 成员作为链表元素。在注册了一个tasklet之后,tasklet链表即标记为即将进行处理。

执行tasklet

因为tasklet基于软中断实现,它们总是在处理软中断时执行。

内核使用 tasklet_action 作为该软中断的 action函数。

该函数首先确定特定于CPU的链表,其中保存了标记为将要执行的各个tasklet。它接下来将表头重定向到函数局部的一个数据项,相当于从外部公开的链表删除了所有表项。接下来,函数在以下循环中逐一处理各个tasklet 。

因为一个tasklet只能在一个处理器上执行一次,但其他的tasklet可以并行运行,所以需要特定于tasklet 的 锁 。 state 状 态 用 作 锁 变 量 。 在 执 行 一 个 tasklet 的 处 理 程 序 函 数 之 前 , 内 核 使 用tasklet_trylock 检查tasklet的状态是否为 TASKLET_STATE_RUN 。换句话说,它是否已经在系统的另一个处理器上运行:

如果 count 成员不等于0,则该tasklet已经停用。在这种情况下,不执行相关的代码。

在 两 项 检 查 都 成 功 通 过 之 后 , 内 核 用 对 应 的 参 数 执 行 tasklet 的 处 理 程 序 函 数 , 即 调 用t->func(t->data) 。最后,使用 tasklet_unlock 清除tasklet的 TASKLET_SCHED_RUN 标志位。

除了普通的tasklet之外,内核还使用了另一种tasklet,使用 HI_SOFTIRQ 作为软中断,而不是 TASKLET_SOFTIRQ ,相关的 action 函数是 tasklet_hi_action 。注册的tasklet在CPU相关的变量 tasklet_hi_vec 中排队。这是使用 tasklet_hi_schedule 完成的。

等待队列和完成量

等待队列(wait queue)用于使进程等待某一特定事件发生,而无须频繁轮询。进程在等待期间睡眠,在事件发生时由内核自动唤醒。完成量(completion)机制基于等待队列,内核利用该机制等待某一操作结束。

等待队列

每个等待队列都有一个队列头:

<linux/wait.h>
struct __wait_queue_head {
    spinlock_t lock;
    struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;

task_list用于连接队列:

<linux/wait.h>
struct __wait_queue {
    unsigned int flags;
#define WQ_FLAG_EXCLUSIVE   0x01
    void *private;
    wait_queue_func_t func;
    struct list_head task_list;
};
typedef struct __wait_queue wait_queue_t;

等待队列的使用分为如下两部分:

使进程睡眠

add_wait_queue 函数用于将一个进程增加到等待队列,该函数在获得必要的自旋锁后,将工作委托给__add_wait_queue

add_wait_queue 通常不直接使用。更常用的是 wait_event 。这是一个宏,需要如下两个参数。

这个宏只确认条件尚未满足。如果条件已经满足,可以立即停止处理,因为没什么可等待的了。主要的工作委托给 __wait_event :

#define __wait_event(wq, condition)                     \
do {                                    \
    DEFINE_WAIT(__wait);                        \
                                    \
    for (;;) {                          \
        prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE);    \
        if (condition)                      \
            break;                      \
        schedule();                     \
    }                               \
    finish_wait(&wq, &__wait);                  \
} while (0)

在用 DEFINE_WAIT 建立等待队列成员之后,这个宏产生了一个无限循环。使用 prepare_to_wait使进程在等待队列上睡眠。每次进程被唤醒时,内核都会检查指定的条件是否满足,如果条件满足则退出无限循环。否则,将控制转交给调度器,进程再次睡眠。

在条件满足时, finish_wait 将进程状态设置回 TASK_RUNNING ,并从等待队列的链表移除对应的项。

唤醒进程

内核定义了一系列宏,可用于唤醒等待队列中的进程。它们基于同一个函数:_wake_up

在获得了用于保护等待队列首部的锁之后, _wake_up 将工作委托给 _wake_up_common。

完成量

基于等待队列实现的。

场景:一个在等待某操作完成,而另一个在操作完成时发出声明。

工作队列

工作队列是将操作延期执行的另一种手段。因为它们是通过守护进程在用户上下文执行,函数可以睡眠任意长的时间。

标签:入门,中断,IRQ,Linux,int,处理程序,内核,irq,tasklet
来源: https://www.cnblogs.com/r1ng0/p/10740566.html