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

中断

中断类型

  • 同步中断和异常。这些由CPU自身产生,针对当前执行的程序
  • 异步中断。这是经典的中断类型,由外部设备产生,可能发生在任意时间。

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

  • 调度器是否应该选择一个新进程代替旧的进程。
  • 是否有信号必须投递到原进程。

数据结构

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个抽象层组成:

  • 高层ISR(high-level interrupt service routines,高层中断服服务例程)针对设备驱动程序端(或其他内核组件)的中断,执行由此引起的所有必要的工作。例如,如果设备使用中断通知一些数据已经到达,那么高层ISR的工作应该是将数据复制到适当的位置。
  • 中断电流处理(interrupt flow handling):处理不同的中断电流类型之间的各种差别,如边沿触发(edge-triggering)和电平触发(level-triggering)。

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

  • 芯片级硬件封装(chip-level hardware encapsulation):需要与在电子学层次上产生中断的底层硬件直接通信。该抽象层可以视为中断控制器的某种“设备驱动程序”

用于表示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个抽象层在该结构中表示如下:

  • 电流层ISR由 handle_irq 提供。 handler_data 可以指向任意数据,该数据可以是特定于IRQ或处理程序的。每当发生中断时,特定于体系结构的代码都会调用 handle_irq 。该函数负责使用 chip 中提供的特定于控制器的方法,进行处理中断所必需的一些底层操作。
  • action 提供了一个操作链,需要在中断发生时执行。由中断通知的设备驱动程序,可以将与之相关的处理程序函数放置在此处。
  • 电流处理和芯片相关操作被封装在 chip 中。
  • name 指定了电流层处理程序的名称,将显示在 /proc/interrupts 中。对边沿触发中断,通常。是“ edge ”,对电平触发中断,通常是“ level。

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

  • IRQ_DISABLED 用于表示被设备驱动程序禁用的IRQ电路。该标志通知内核不要进入处理程序。
  • 在IRQ处理程序执行期间,状态设置为 IRQ_INPROGRESS 。与 IRQ_DISABLED 类似,这会阻止其余的内核代码执行该处理程序。
  • 在CPU注意到一个中断但尚未执行对应的处理程序时, IRQ_PENDING 标志位置位。
  • 为正确处理发生在中断处理期间的中断,需要 IRQ_MASKED 标志。
  • 在某个IRQ只能发生在一个CPU上时,将设置 IRQ_PER_CPU 标志位。
  • IRQ_LEVEL 用于Alpha和PowerPC系统,用于区分电平触发和边沿触发的IRQ。
  • IRQ_REPLAY 意味着该IRQ已经禁用,但此前尚有一个未确认的中断。
  • IRQ_AUTODETECT 和 IRQ_WAITING 用于IRQ的自动检测和配置。
  • 如果当前IRQ可以由多个设备共享,不是专属于某一设备,则置位 IRQ_NOREQUEST 标志。

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;
};
  • name 包含一个短的字符串,用于标识硬件控制器。在IA-32系统上可能的值是“XTPIC”和“IO-APIC”,在AMD64系统上大多数情况下也会使用后者。
  • startup 指向一个函数,用于第一次初始化一个IRQ。大多数情况下,初始化工作仅限于启用该IRQ。因而, startup 函数实际上就是将工作转给 enable 。
  • enable 激活一个IRQ。换句话说,它执行IRQ由禁用状态到启用状态的转换。为此,必须向I/O内存或I/O端口中硬件相关的位置写入特定于硬件的数值。
  • disable 与 enable 的相对应,用于禁用IRQ.而 shutdown 完全关闭一个中断源。
  • ack 与中断控制器的硬件密切相关。在某些模型中,IRQ请求的到达(以及在处理器的对应中断)必须显式确认,后续的请求才能进行处理。如果芯片组没有这样的要求,该指针可以指向一个空函数,或NULL指针。 ack_and_mask 确认一个中断,并在接下来屏蔽该中断。
  • 调用 end 标记中断处理在电流层次的结束。如果一个中断在中断处理期间被禁用,那么该函数负责重新启用此类中断。
  • 现代的中断控制器不需要内核进行太多的电流控制,控制器几乎可以管理所有事务。在处理中断时需要一个到硬件的回调,由 eoi 提供, eoi 表示end of interrupt,即中断结束。
  • 在多处理器系统中,可使用 set_affinity 指定CPU来处理特定的IRQ。
  • set_type 设置IRQ的电流类型。该方法主要使用在ARM、PowerPC和SuperH机器上,其他系统不需要该方法,可以将 set_type 设置为 NULL 。

处理程序函数的表示

<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;
};
  • handler:处理程序函数本身
  • name 和 dev_id 唯一地标识一个中断处理程序。name 是一个短字符串,用于标识设备。而 dev_id 是一个指针,指向在所有内核数据结构中唯一标识了该设备的数据结构实例。
  • flags 是一个标志变量,通过位图描述了IRQ(和相关的中断)的一些特性,位图中的各个标志位照例可通过预定义的常数访问。 <interrupt.h> 中定义了下列常数。
  • 对共享的IRQ设置 IRQF_SHARED ,表示有多于一个设备使用该IRQ电路。
  • 如果IRQ对内核熵池(entropy pool)有贡献,将设置 IRQF_SAMPLE_RANDOM。
  • IRQF_DISABLED 表示IRQ的处理程序必须在禁用中断的情况下执行。
  • IRQF_TIMER 表示时钟中断
  • next 用于实现共享的IRQ处理程序。

中断电流处理

设置控制器硬件

首先,需要提到内核提供的一些标准函数,用于注册 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);
  • set_irq_chip 将 一 个 IRQ 芯 片 以 irq_chip 实 例 的 形 式 关 联 到 某 个 特 定 的 中 断 。 除 了 从irq_desc 选取适当的成员并设置 chip 指针之外,如果没有提供特定于芯片的实现,该函数还将设置默认的处理程序。如果 chip 指针为 NULL ,将使用通用的“无控制器” irq_chip 实例no_irq_chip ,该实现只提供了空操作。
  • set_irq_handler 和 set_irq_chained_handler 为某个给定的IRQ编号设置电流处理程序。
  • set_chip_and_handler 是一个快捷方式,它相当于连续调用上述的各函数。 _name 变体的工作方式相同,但可以为电流处理程序指定一个名称,保存在 irq_desc[irq]->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 函数,它将执行下列步骤。

  • 如果设置了 IRQF_SAMPLE_RANDOM ,则该中断将对内核熵池有所贡献,熵池用于随机数发生器/dev/random 。 rand_initialize_irq 将该IRQ添加到对应的数据结构。
  • 由 request_irq 生成的 irqaction 实例被添加到所属IRQ编号对应的例程链表尾部,该链表表头为 irq_desc[NUM]->action 。在处理共享中断时,内核就通过这种方式来确保中断发生时调用处理程序的顺序与其注册顺序相同。
  • 如果安装的处理程序是该IRQ编号对应链表中的第一个,则调用 handler->startup 初始化函数。 1 如果该IRQ此前已经安装了处理程序,则没有必要再调用该函数。
  • register_irq_proc 在 proc 文件系统中建立目录 /proc/irq/NUM

释放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栈

  • 用于硬件IRQ处理的栈。
  • 用于软件IRQ处理的栈。

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

调用电流处理程序例程

以AMD64结构的do_IRQ为例:

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

调用高层ISR

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

handle_IRQ_event 可能执行下述操作:

  • 如果第一个处理程序函数中没有设置 IRQF_DISABLED ,则用 local_irq_enable_in_hardirq启用(当前CPU的)中断。换句话说,该处理程序可以被其他IRQ中断。但根据电流类型,也可能一直屏蔽刚处理的IRQ。
  • 逐一调用所注册的IRQ处理程序的 action 函数。
  • 如果对该IRQ设置了 IRQF_SAMPLE_RANDOM ,则调用 add_interrupt_randomness ,将事件的时间作为熵池的一个源(如果中断的发生是随机的,那么它们是理想的源)。
  • local_irq_disable 禁用中断。因为中断的启用和禁用是不嵌套的,与中断在处理开始时是否启用是不相关的。 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;
};
  • next 是一个指针,用于建立 tasklet_struct 实例的链表
  • state 表示任务的当前状态,类似于真正的进程,在tasklet注册到内核,等待调度执行时,将设置 TASKLET_STATE_SCHED 。TASKLET_STATE_RUN 表示 tasklet 当前正在执行。
  • 原子计数器 count 用于禁用已经调度的tasklet。如果其值不等于0,在接下来执行所有待决的tasklet时,将忽略对应的tasklet。

注册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;
  • flags 的值或者为 WQ_FLAG_EXCLUSIVE ,或者为0,当前没有定义其他标志。 WQ_FLAG_EXCLUSIVE表示等待进程想要被独占地唤醒
  • private 是一个指针,指向等待进程的 task_struct 实例。该变量本质上可以指向任意的私有数据,但内核中只有很少情况下才这么用,因此这里不会详细讲述这种情形。
  • 调用 func ,唤醒等待进程。
  • task_list 用作一个链表元素,用于将 wait_queue_t 实例放置到等待队列中。

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

  • 为使当前进程在一个等待队列中睡眠,需要调用 wait_event 函数,进程进入睡眠,将控制权释放给调度器。
  • 在内核中另一处,,必须调用 wake_up 函数来唤醒等待队列中的睡眠进程。

使进程睡眠

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

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

  • 在其上进行等待的等待队列。
  • 一个条件,以所等待事件有关的一个C表达式形式给出。

这个宏只确认条件尚未满足。如果条件已经满足,可以立即停止处理,因为没什么可等待的了。主要的工作委托给 __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。

完成量

基于等待队列实现的。

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

工作队列

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

猜你喜欢

转载自www.cnblogs.com/r1ng0/p/10740566.html