一. 软中断和Tasklets

1.中断的延迟处理

在中断的处理中,有一些不紧急不关键的任务在需要的时候可以被延迟一段时间来执行。对于中断服务程序来说,一般情况下如果它不结束执行,就不应该产生新的中断;而这些延迟的任务可以在打开中断的情形下执行,因而把它们从中断服务程序中提取出来可以降低内核的响应时间。linux支持两种非紧迫的、可中断的内核函数: 可延迟函数(包括软中断和tasklets )和通过工作队列执行的函数。

软中断和takslet是紧密相关的,tasklet在软中断之上实现。但是二者也有区别:
  1. 软中断是静态分配的,而tasklet的分配和初始化可以在运行时进行。
  2. 软中断可以并发的运行在多个CPU上,因而软中断中的代码应该是可重入的并且必须使用自旋锁保护其数据结构。而相同类型的tasklet总是被串行的执行,也就是说不能在两个CPU上同时运行相同类型的tasklet。但是不同类型的tasklet可以在不同的CPU上并发运行。由于tasklet的这个特性,因而tasklet中的函数不必是可重入的。
一般来说可延迟函数有以下几种操作:
  1. 初始化:定义一个新的可延迟函数。一般这个工作在内核初始化或者模块加载时完成。
  2. 激活:标记一个可延迟函数为pengding以便它在可延迟函数的下一轮调度中被执行。激活可以在任何时候被执行。
  3. 屏蔽:有选择的屏蔽一个可延迟函数,这样即便它被激活,内核也不执行它。
  4. 执行:执行一个pending的可延迟函数和同类型的其它所有的pending的可延迟函数。执行是在特定的时间进行的。

2.软中断

Linux使用了有限的软中断。在大多数情况下tasklets是足够用的,而且它更容易编写。

linux定义了如下的软中断:

  1. HI_SOFTIRQ 
  2. TIMER_SOFTIRQ
  3. NET_TX_SOFTIRQ
  4. NET_RX_SOFTIRQ
  5. BLOCK_SOFTIRQ 
  6. BLOCK_IOPOLL_SOFTIRQ
  7. TASKLET_SOFTIRQ
  8. SCHED_SOFTIRQ
  9. HRTIMER_SOFTIRQ
  10. RCU_SOFTIRQ

这些值都是枚举变量,从0开始,最大值为NR_SOFTIRQS,NR_SOFTIRQS表示系统中当前支持的软中断的数目。这些枚举变量也定义了对应的软中断的优先级,值越小优先级越高。软中断都存放在softirq_vec数组中,每个元素包括一个handler指针和一个当作参数的通用数据指针。

但是需要注意的是软中断的优先级只是定义了它们的执行顺序,而不会影响它们相对于其它“任务”的优先级,也不会影响它们被执行的频度。

1.preempt_count

preempt_count字段用于跟踪内核抢占和内核控制路径的嵌套,它存放在每个进程描述父的thread_info字段中。它分为不同的比特子字段,每个子字段有不同的含义:

  • 0-7  : 内核抢占计数器 (max value = 255)
  • 8-15  :软中断计数器Softirq counter (max value = 255).
  • 16-27 :硬件中断计数器Hardirq counter (max value = 4096)
  • 28 :PREEMPT_ACTIVE标记
第一个子字段记录了显式禁用本地CPU内核抢占的次数,第二个子字段记录了可延迟函数被禁用的程度,第三个子字段记录了本地CPU上中断处理程序的嵌套数。当不允许抢占时,或者正处于中断上下文时,必须禁止内核抢占。有了该字段后内核只需要检查该字段即可获得当前内核的状态。

2.软中断的处理

每个CPU包含一个描述处于pending状态的软中断的32比特掩码。它位于irq_cpustat_t数据结构的_ _softirq_pending域。内核会周期性的检查是否有pengding的中断需要处理,如果有就会进行处理,这种检查是在特点的位置进行的(典型的位置是退出中断处理时irq_exit以及ksoftirqd中)。在软中断处理中,由于在执行一个软中断函数时可能出现新的软中断,因而为了保证可延迟函数的低延迟性,软中断处理函数会一直运行到执行完所有pending的软中断或者已经在软中断处理中运行了一定的时间(3.9.4中是2ms)。如果在软中断处理完成后仍有未处理的软中断,ksoftirqd将会处理它(每个CPU都有一个ksoftirqd内核线程)。

软中断运行时硬件中断是打开的,但是会在本地禁止软件中断,结果就是do_softirq在每个CPU上只能进入一次。在处理软中断时,会在关闭中断的情况下将本地CPU的pending的软中断保存到局部变量并且将本地CPU的软中断掩码清0,然后遍历每个pending的软中断并且先打开本地中断再执行软中断函数,再关闭本地中断,也就是说只有在调用中断处理函数时本地中断是打开的(但是本地的软中断十关闭的)。当把pending的处理完后,会重新读取本地CPU的软中断pending状态,如果仍有pending的就继续处理,直到处理完所有的pending的软中断或已经在软中断处理中运行了一定的时间。

3.软中断相关函数

void open_softirq(int nr, void (*action)(struct softirq_action *))
该函数用于处理软中断的初始化,action为软中断处理函数,nr为软中断号
void raise_softirq(unsigned int nr)
该函数用于激活软中断,nr为软中断号
asmlinkage void do_softirq(void) 
该函数用于执行软中断处理函数

2. Tasklets

Tasklets是I/O驱动中实现可延迟函数的首选方法。tasklets是建立在两个软中断HI_SOFTIRQ 和TASKLET_SOFTIRQ之上的。多个tasklets可以和一个软中断关联起来,每个tasklet有自己的函数。除了HI_SOFTIRQ的 tasklets比TASKLET_SOFTIRQ的tasklets先被执行外,两个类型的tasklet之间没有别的区别。

1.tasklet的表示

Tasklets以及高优先级的tasklets分别存放于tasklet_vec和tasklet_hi_vec数组中。这两个数组都包含NR_CPUS个类型为tasklet_head的元素, 每一个元素都包含一个指向由tasklet描述符组成的链表的指针。tasklet描述符的字段:

  • next:指向列表中的下一个taskletPointer to next descriptor in the list
  • state:tasklet的状态
  • count:atomic_t类型的变量,如果其值不为0,则接下来执行待执行的tasklet时就不会再执行它。它用于保证tasklet在所有的CPU上只有一个实例在运行
  • func:tasklet的执行函数
  • data:tasklet执行函数的参数
Tasklet描述父的state域包含两个标志:
  • TASKLET_STATE_SCHED:设置了该标记时表示tasklet是处于pending状态的
  • TASKLET_STATE_RUN:设置了该标志表示该tasklet正在被执行

2.tasklet的执行

tasklet 是一个特殊的函数, 它在软中断上下文被调度。它可能被调度运行多次,但是tasklet调度不累积,也就是即使在tasklet被执行之前请求了多次来执行该tasklet,它也只运行一次。不会有同一个tasklet的多个实例同时运行。但是tasklet可以与SMP系统上的其他tasklet并行运行。因此, 如果多个tasklet会使用相同的资源, 它们必须采取某类加锁来避免彼此冲突。除非tasklet重新激活自己,否则每次tasklet激活只会运行一次。

在HI_SOFTIRQ 和TASKLET_SOFTIRQ的处理函数中,与当前CPU相关联的tasklet_vec或tasklet_hi_vec元素会被保存到局部变量,并且元素本身会被设置为NULL(在关闭本地中断的情况下)。然后遍历链表中的每一个元素,检查是否已经是运行状态(在其它CPU上),是否是禁止状态,如果都不是,并且tasklet被激活,tasklet就会被运行。

3. Tasklet相关API

API对应的头文件为linux/interrupt.h
DECLARE_TASKLET(name, func, data);声明并定义一个Tasklet
DECLARE_TASKLET_DISABLED(name, func, data);声明并定义一个Tasklet,且其初始状态为禁止的
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
为了使用tasklet必须首选分配一个tasklet_struct的数据结构并使用tasklet_init来初始化它。
static inline void tasklet_disable(struct tasklet_struct *t)
它用于禁止tasklet
static inline void tasklet_enable(struct tasklet_struct *t)
它用于启用tasklet
static inline void tasklet_schedule(struct tasklet_struct *t)
static inline void tasklet_hi_schedule(struct tasklet_struct *t)
它们用于激活tasklet,一个用于高优先级软中断相关联的tasklet,一个用于正常的软中断相关联的tasklet。
void tasklet_kill(struct tasklet_struct *t); 用于确保tasklet不再被调度执行,通常用在设备要关闭或模块要退出是。如果tasklet正在被调度执行,则该函数会先等待其执行完成,然后再开始自己的动作。如果tasklet会重新调度自己,则应该在重新调度时做某些判断,以防止永远无法kill掉。tasklet_kill的代码如下:

void tasklet_kill(struct tasklet_struct *t)
{
        if (in_interrupt())
                printk("Attempt to kill tasklet from interrupt\n");


        while (test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
                do {
                        yield();
                } while (test_bit(TASKLET_STATE_SCHED, &t->state));
        }
        tasklet_unlock_wait(t);
        clear_bit(TASKLET_STATE_SCHED, &t->state);
}

tasklet执行时的核心代码如下:

                if (tasklet_trylock(t)) {
                        if (!atomic_read(&t->count)) {
                                if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
                                        BUG();
                                trace_irq_tasklet_low_entry(t);
                                t->func(t->data);
                                trace_irq_tasklet_low_exit(t);
                                tasklet_unlock(t);
                                continue;
                        }
                        tasklet_unlock(t);
                }

二、workqueue

1.工作队列的概念

工作队列用于替换任务队列。它们都允许内核函数被激活,之后由特殊的称为工作者线程的特殊内核线程执行。
工作队列和可延迟函数非常相似,但是也有不同:
  • 可延迟函数运行于中断上下文,工作队列运行于进程上下文
  • 工作队列可以阻塞,但是可延迟函数不能,因为只有在进程上下文运行时才能执行可阻塞函数(中断上下文不可能发生进程切换)
类似于可延迟函数,工作队列中的函数也不能访问进程的用户地址空间。而且由于工作队列由内核线程来执行,因而也不存在用户态地址空间。
使用工作队列时需要先定义工作队列,然后将需要延迟执行的函数插入到工作队列。每个工作者线程在worker_thread函数内部循环执行,大部分时间该线程都处于睡眠状态并等待某些工作被插入到队列。工作线程一旦被唤醒就调用run_workqueue()函数,该函数从工作者线程的工作队列链表中删除所有的work_struct描述父并执行相应的函数。工作者线程可以阻塞,并且可以睡眠。
系统预定义了一些工作队列,大部分情况下都可以直接使用它:
预定义工作队列函数                  等价的标准工作队列函数
schedule_work(w)                   queue_work(keventd_wq,w)
schedule_delayed_work(w,d)         queue_delayed_work(keventd_wq,w,d) (on any CPU)
schedule_delayed_work_on(cpu,w,d)  queue_delayed_work(keventd_wq,w,d) (on a given CPU)
flush_scheduled_work( )            flush_workqueue(keventd_wq)
但是需要注意的是如果你的函数会长期阻塞则不要使用预定义工作队列,因为这会对使用预定义队列的其它函数造成影响。

从执行的角度上说,workqueue中的函数是由内核线程调用的。

数据结构workqueue_struct用于表示工作队列,work_struct用于定义需要由工作队列(的线程)执行的任务。

2.相关API

API对应的头文件为 linux/workqueue.h

1.工作者队列API

create_workqueue(name)
这实际上是一个宏,它返回新创建的工作队列的描述父地址。该函数还创建n个工作者线程(n是系统中CPU数目),并根据name为工作者线程命名。
create_singlethread_workqueue(name)
这也是一个宏,它完成和create_workqueue类似的工作,但是只创建一个工作者线程。
void destroy_workqueue(struct workqueue_struct *wq);
该函数用于销毁工作者队列,其参数为指向工作者队列数据结构的指针。

2.工作相关API

DECLARE_WORK(name, void (*function)(void *), void *data);
INIT_WORK(struct work_struct *work, void (*function)(void *), void *data);
PREPARE_WORK(struct work_struct *work, void (*function)(void *), void *data);
以上三个API用于初始化“工作”结构
int queue_work(struct workqueue_struct *wq, struct work_struct *work);
该函数用于将函数插入到工作队列,wq为工作队列数据结构指针,work为“工作”。
int queue_work_on(int cpu, struct workqueue_struct *wq, struct work_struct *work);
类似于queue_work,但是指定了该函数由那个CPU执行。
int queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *work, unsigned long delay);
该函数在指定的时间后将函数插入到工作者队列
int queue_delayed_work_on(int cpu, struct workqueue_struct *wq, struct delayed_work *work, unsigned long delay);
类似于queue_delayed_work,但是指定了该函数由哪个CPU执行
void flush_workqueue(struct workqueue_struct *wq);
该函数强制工作队列中的在调用该函数之前就被加入到其中的所有函数都被执行并且在它们都执行完之前一直阻塞
GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐