天天看点

linux内核设计与实现(2)-- 中断和中断处理1. 中断2. 下半部和推后执行的工作

1. 中断

       中断就是一些外设硬件发送通知给处理器的通道。外设可以不在cpu的干预下执行一些动作,在完成这些任务后通过中断通知CPU。每个中断都有一个系统唯一的中断号。

1.1 中断上半部和下半部

       为了解决中断处理函数运行得快且完成尽量多的工作量的矛盾,我们一般把中断处理切为两部分。

  • 上半部:会在接受到一个中断后立刻开始执行,但是只是做一些有严格时限的工作,例如中断应答和复位硬件,这些工作都是在所有中断被禁止的情况下完成的。
  • 下半部:可以推迟的工作。比如网卡中断会在上半部分把网络数据拷贝到内存中,提高数据吞吐量。而网路数据协议的解析就可以放到下半部分区执行。

1.2 中断处理函数

       中断处理函数利用request_irp()。

request_irp()函数可能会随眠,不能在中断上下文或者不允许阻塞的代码中调用改函数。

       上半部的中断处理程序无须重入的,因为在执行的时候当前的所有中断线都是被禁止的。

  • 共享中断处理程序

    一个中断号可以注册多个中断处理函数来实现所有的设备共享这个中断。共享中断的处理函数会形成一个表,在中断来的时候都会执行这些链表中的函数,而这些中断处理函数需要能够区分这些设备是否真正产生了中断。

1.3 中断上下文

        进程在进入内核时即进程上下文可以睡眠也可以调用调度程序。因为没有后备进程,中断上下文不可以睡眠。

1.4 中断控制和处理机制的实现

  • /proc/interrupts:查看所注册的中断以及中断设备,控制器,中断次数等信息,可以通过cat这个文件查看内核中断的信息

2. 下半部和推后执行的工作

        下半部的任务就是执行与终端处理密切相关但中断处理程序本身不执行的工作。

        中断处理函数完成与硬件相关等的和时间密切相关的动作。而需要比较多的时间来处理的工作,比如网络数据的协议解析就留给下半部来执行。

        上半部与下半部任务分配的经验:

        如果一个任务对时间非常敏感,那就应该放到中断处理函数中执行

        如果一个任务和硬件相关,那就应该放在中断处理函数中执行

        如果一个任务要保证不被其他中断(特别是相同的中断)打断,那就应该凡在中断处理函数中执行

        其他所有的任务,考虑全部放到下半部去执行。

2.1 下半部实现的方法

一,工作队列

        工作队列是把工作推后,交由一个内核线程去执行—这个下半部分总是在进程上线文中执行(而不是中断上下文)。这样,他就有了进程上下文的优势,可以在工作队列中重新调度或者是休眠。

1,任务队列相关的数据结构

(1)表示线程的数据结构:

struct workqueue_struct { 
    struct cpu_workqueue_struct *cpu_wq; 
    struct list_head list; 
    const char *name; 
    int singlethread; 
    int freezeable; /* Freeze threads during suspend */ 
    int rt;
};
           

       该结构是一个由cpu_workqueue_struct结构组成的数组,它定义在kernel/woekqueue.c,数组的没一项对应系统中的一个处理器。每个处理器有一个工作者线程,所以每工作者线程对应一个这样的cpu_workqueue_struct结构体。

struct cpu_workqueue_struct { 
    spinlock_t lock; //锁保护这种结构 
    struct list_head worklist; //工作列表 
    wait_queue_head_t more_work; 
    struct work_struct *current_work; 
    struct workqueue_struct *wq; //关联工作队列结构 
    struct task_struct *thread; //关联线程 
} ____cacheline_aligned;
           

       每种工作者线程在每个cpu中都有一个线程,该线程的数据结构如上。

(2)表示工作的数据结构:

Struct work_struct{
Atomic_long_t data;
Struct list_head entry;
Work_fun_t func;
}
           

       这些结构体会被作为队列连接成链表,在每个处理器上的美中类型的队列都对应这样的一个链表。当工作者线程被唤醒时,它就执行链表中的工作。当执行完毕过后,它就会从链表中剔除出去。

(3)三种数据接头的关系

       工作者线程位于最上层。系统允许有许多类型的工作者线程存在( workqueue_struct)。而一个类型的工作者线程又在每个cpu上存在一个线程( cpu_workqueue_struct)。而在每个线程者线程中又可以挂很多的工作(work_struct)。

2,创建下半部推后的工作。

DECLARE_WORK(name,void (*func)(void *),void *data)

来创建工作结构体,并形成链表

3,对工作进行调度

在中断处理函数中调用工作调度,实现工作队列下半部

schedule_work(Struct work_struct * work)

schedule_delayed_work(Struct work_struct * work)

queue_work(struct workqueue_struct *wq,Struct work_struct * work)

queue_delayed_work(struct workqueue_struct *wq,Struct work_struct * work)

使用以上四个函数可以让工作进行调度,前两个是缺省的work,后面两个是指定events队列。

可以在内核源码中查看工作队列的使用,加深对其的理解

二,软中断

内核定义了的软中断的中断号

enum
{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ,
    RCU_SOFTIRQ,  /*Preferable RCU should always be the last softirq */
    NR_SOFTIRQS
};
           

       从中可以看出,网络发送接收下半部用到了软中断,以及中断下半部的tasklet也是采用软中断实现的。软中断的具体实现在kernel/softirq.c中。

1,软中断实现

(1)软中断处理函数

Void softirq_handler(struct softirq_action *)

(2)执行软中断

一个注册的软中断必须被标记后才会执行,这个标记就是触发软中断。在合适的时机,标记过后的软中断就会被执行。在以下情况,待处理的软中断会被检查和执行:

①,从一个硬件中断返回时

②,在ksoftirqd内核线程中

③,在显示检查和执行待处理的软中断的代码中,如网络子系统。

不管是用什么办法,软中断都是通过do_softirq()中去检查然后执行

2,使用软中断

       目前,只要网络和调度子系统直接使用了软中断,同时,内核定时器和tasklet也是基于软中断实现的。如果想加入一个软中断,可以先问一下tasklet为什么不能实现。

(1)分配索引

       前面的枚举值定义了现在已经使用的软中断,如果要使用软中断。则需要在其中添加索引,一般是在BLOCK_SOFTIRQ与 TASKLET_SOFTIRQ,之间添加自己的软中断

(2)注册软中断处理函数

Open_softirq(NET_TX_SOFTIRQ, net_tx_axtion);

(3)触发软中断

Raise_softirq(NET_TX_SOFTIRQ)

       在中断处理程序中触发软中端是最常见的形式。在这种情况下,中断处理函数执行硬件设备中断后触发软中断,当退出中断后就执行软中断的处理。

具体可以参考内核源码中的网络软中断实现

三,Tasklet

结构体数据

struct tasklet_struct
{
    struct tasklet_struct *next;
    unsigned long state;
    atomic_t count;
    void (*func)(unsigned long);
    unsigned long data;
};
           

1,tasklet的实现与使用

(1)声明自己的tasklet

DECLARE_TASKLET(name,func,data) //静态的创建

Tasklet_init(t, tasklet_hander, dev) //动态的创建

(2)编写自己的tasklet处理函数

Void tasklet_handler(unsigned long data)

(3)调度自己的tasklet

tasklet_schedule(struct tasklet_struct *t);

2,tasklet的执行—ksoftirqd

前面说到tasklet的实现也是通过软中断来实现的,并且在ksoftirqd中会执行软中断。

每个处理器都有一个辅助处理软中断的内核线程—ksoftirqd/n n就是处理器的编号。

线程的实现在 kernel/softirq.c中,被设计为以下的死循环

For(;;)
{
    If(!softirq_pending(CPU))
    Schedule();
    Set_current_state(TASK_RUNNING);
    While(softirq_pending(cpu)
    {
	    Do_doftirq();
	    If(need_resched())
	    {
	   	 	Schedule();
	    }
    }
    Set_current_state()TASK_INTERRUPTIBLE);
}
           

       只要有待处理的软中低,ksoftirq就会调用do_softirq()去处理他们。同时tasklet是软中断实现的,也就会同时被调用。

3.在下半部加锁

       在不通的进程,或者进程和中断之间共享数据的时候都需要锁来保证数据的同步。

       因为软中断和tasklet是中断上下文,所以在有这些环境相关的情况下,在获取锁之后需要禁止中断下半部或者中断,保证不被死锁。如果下半部获取了锁之后被其它中断打断,并且中断也需要这个锁,那么就会出现死锁的情况。

4. 三种实现方法的区别

1,tasklet与软中断不能睡眠,是中断上下文环境

2,工作队列可以睡眠,可以产出调度,是进程上下文环境

当选择方法的时候,如果需要睡眠,则选择任务队列;当不需要睡眠的时候,选择tasklet,当tasklet无法实现时才考虑使用软中断。

3,软中断处理程序执行的时候,允许响应中断,但自己不能休眠。

4,如果软中断在执行的时候再次触发,则别的处理器可以同时执行,所以加锁很关键。

5,tasklet负责执行的序列化保障,两个类型的tasklet不允许同时执行,即使在不同的处理器上也不行。所以相同tasklet之间不需要操心同步的问题,只需要考虑不同tasklet之间的同步

继续阅读