Linux内核设计与实现——读书笔记(6)下半部及推后执行的工作

1、下半部

  由于中断处理程序要尽可能快地执行完,所以不能在其中执行繁杂的操作。下半部的工作主要就是完成中断处理程序未完成的所有工作。 对于上下半部工作的划分没有规定,通常按以下分配:

  • 如果一个任务对时间非常敏感,则放在中断处理程序中处理;
  • 如果一个任务和硬件有关,则放在中断处理程序中处理;
  • 如果一个任务要保证不被其他中断打断(同级或者同线),则放在中断处理程序中处理;
  • 其他,下半部

1.1、BH(已被抛弃)

  最初的Linux只提供 “bottom half” 机制用于实现下半部,这种机制也被称为 “BH”“BH” 提供了静态创建、由32个bottom halves组成的链表。上半部通过一个32位整数中的一位来标识出哪一个bottom half可以执行。每个BH在全局范围内同步,即使分属不同的处理器,也不予许任何两个bottom half同时执行。这种机制方便但是不灵活,存在性能瓶颈。

1.2、任务队列(task queue)(已被抛弃)

  内核开发者们定义了一组队列,每个队列都包含由等待调用的函数组成的链表。根据所出队列的位置,这些函数会在某些时刻被执行。驱动程序可以把他们自己的下半部任务注册到合适的队列上。任务队列的机制也不够灵活,对于性能较高的子系统(网络)不能胜任。

1.3、软中断、tasklet和工作队列

  在2.3版本引入了软中断和taskled,如果不考虑旧版本驱动兼容,软中断和tasklet完全可以取代BH机制。
  软中断是一组静态定义的下半部接口,有32个,可以在所有处理器上同时执行。
  tasklet是基于软中断实现的灵活性强的、动态创建的下半部机制。两个不同类型的tasklet可以在不同的处理器上运行,但是类型相同tasklet不能同时执行
  tasklet是一种在性能和易用性之间追求平衡的机制,大部分下半部处理使用tasklet就可以了,像网络这种对性能要求非常高的才需要使用软中断。
  软中断需要在编译期间静态注册,tasklet可以通过代码进行动态注册。
  
  2.5内核版本时,BH机制被废弃,任务队列被工作队列取代。
  在2.6版本的内核中,下半部的实现内核提供了三种接口:软中断、tasklet和工作队列。

2、软中断

2.1、软中断的实现

  软中断是在编译期间静态分配的,相关代码在kernel/softirq.c中。软中断使用softirq_action结构表示,定义在 <linux/interrupt.h> 中。

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

  在kernel/softirq.c中定义了一个包含32个元素的softirq_action结构体数组。

static struct softirq_action softirq_vec[32] __cacheline_aligned_in_smp;

  __cacheline_aligned_in_smp 用来在smp系统中,和一级cache缓存对齐,应该是用来提升性能吧。每个被注册的软中断都占据该数组的一个项。

2.1.1、软中断处理程序

  函数原型如下:

void softirq_handler(struct softirq_action *)

  当内核运行一个软中断处理程序的时候,就会执行softirq_action结构的action成员指向的软中断处理程序。例如,my_softirq指向softirq_vec的某项,内核会用如下方式调用软中断处理程序:

my_softirq->action(my_softirq);

  一个软中断不会抢占另一个软中断,唯一可以抢占软中的的只有中断处理函数

2.1.3、执行软中断

  一个注册的软中断必须被标记之后才会执行。标记动作被称作触发软中断。通常,中断处理程序会在返回前标记它的软中断,于是在适当的时候软中断会被执行。在以下几种情况待处理的软中断会被检查和执行:

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

  不管是什么情况,软中断都要在do_softirq() 中执行。简化后的do_softirq() 如下所示:
在这里插入图片描述
  do_softirq() 主要是用局部变量pending保存local_softirq_pending()宏返回的软中断位图。它是待处理的软中断的位图,然后再用set_softirq_pending()将实际的软中断位图清零。循环中检测位图然后进行相应软中断处理函数调用。

2.1.4、使用软中断

  软中断保留给系统中对时间要求最严格的最重要的下半部使用。在2.6的内核中只有两个子系统(网络和SCSI)直接使用软中断。内核定时器和tasklet都是建立在软中断上的。

2.1.4.1、分配索引

  在 <linux/interrupt.h> 中定义了一个枚举类型来静态声明软中断的优先级,索引小的软中断先执行。
在这里插入图片描述

  如果要建立一个新的软中断,必须根据希望赋予的优先级来决定加入到位置。

2.1.4.2、注册处理程序

  在运行时通过open_softirq() 注册软中断处理程序,该函数有两个参数:第一个为上面提到的索引号,第二个为处理函数。例如网络子系统中,在net/coreldev.c中通过以下方式注册自己的软中断:

open_softirq(NET_TX_SOFTIRQ,net_tx_action);
open_softirq(NET_RX_SOFTIRQ,net_rx_action);

  软中断处理程序执行的时候允许响应中断,但是不能休眠,同一个处理器运行一个处理程序时,软中断会被禁用,但是其他处理器仍可以执行别的软中断。实际上,如果一个软中断在它被执行的同时再次被触发,那么另一个处理器可以同时运行其处理程序,这意味着任何共享数据都需要严格的锁保护。 大部分软中断处理程序都是采取单处理器或其他技巧来避免显式加锁,从而提供更好的性能。
  如果不需要扩展到多个处理器,那么就是用tasklet。

2.1.4.3、触发软中断

  raise_softirq()函数可以将一个软中断设置为挂起状态,让它在下一次调用do_softirq()函数时投入运行。例如,网络子系统可能会调用:

raise_softirq(NET_TX_SOFTIRQ);

void raise_softirq(unsigned int nr) 
{
    unsigned long flags;

    local_irq_save(flags);
    raise_softirq_irqoff(nr);
    local_irq_restore(flags);
}

  这会触发NET_TX_SOFTIRQ 软中断,处理函数net_tx_action 会在内核下一次执行软中断时投入运行。raise_softirq函数在触发一个软中断之前会先禁用中断,触发后再恢复原来的状态。如果中断已经被禁用,可是使用raise_softirq_irqoff()带来一些优化效果。

raise_softirq_irqoff(NET_TX_SOFTIRQ);

  通常在中断处理程序中触发软中断,然后退出,内核会在执行完中断处理程序以后马上调用do_softirq()函数。

3、tasklet

  tasklet和软中断在本质上相似,但是tasklet接口更简单,锁保护也要起更低。通常在下半部中选用tasklet

3.1、tasklet的实现

  tasklet由两类软中断实现:HI_SOFTIRQTASKLET_SOFTIRQ,当一个tasklet被调度时,内核会唤起这两个软中断中的一个。

3.1.1、tasklet的结构体

struct tasklet_struct{
    struct tasklet_struct *next;	//链表中的下一个tasklet
    unsigned long state;			//tasklet的状态
    atomic_t count;					//引用计数器
    void (*func)(unsigned long);	//tasklet处理函数
    unsigned long data;				//传递给处理函数的参数
};  

  结构体中的func成员对应软中断中的action成员;
  state成员只能取值0TASKLET_STATE_SCHEDTASKLET_STATE_RUN。TASKLET_STATE_SCHED表明tasklet已被调度,正准备投入运行;TASKLET_STATE_RUN表示正在运行。TASKLET_STATE_RUN只有在smp系统上才会作为一种优化选项使用。

3.1.2、调度(触发)tasklet

  已调度的tasklet存放在单处理器数据结构:tasklet_vec(普通tasklet)和tasklet_hi_vec(高优先级tasklet)中。tasklet_vec和tasklet_hi_vec都是tasklet_struct结构体组成的链表。tasklet_schedule()(用于调度普通tasklet)和tasklet_hi_schedule()(用于调度高优先级tasklet),函数接受一个指向tasklet_struct的指针做参数。

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

  tasklet_schedule()的主要操作:

  • 1)检查tasklet的状态是否为TASKLET_STATE_SCHED。如果是,说明tasklet已经被调度,函数立即返回。
  • 2)把需要调度的tasklet加到每个处理器的tasklet_vec链表或者tasklet_hi_vec链表的表头。
  • 3)唤起TASKLET_SOFTIRQHI_SOFTIRQ软中断,这样下一次调用do_softirq()时就会执行该tasklet。
      
      因为上面已经触发TASKLET_SOFTIRQHI_SOFTIRQ,所以do_softirq()会执行相应的软中断处理程序。tasklet的话会调用tasklet_action()和tasklet_hi_action()函数,这两个函数主要做了以下操作:
  • 1)获取当前处理器的tasklet_vec或tasklet_hig_vec的立案表头。
  • 2)将处理器上的该链表设置为NULL。
  • 3)循环遍历获得链表上每一个待处理的tasklet。
  • 4)如果是多处理系统,通过检测state值是否为TASKLET_STATE_RUN来判断tasklet是否在其他处理器上运行。
  • 5)如果当前这个tasklet没有执行,将其设置为TASKLET_STATE_RUN。
  • 6)检查count值是否为0,确保tasklet没有被禁止。
  • 7)执行tasklet的处理程序。
  • 8)tasklet运行完毕,清除state成员的TASKLET_STATE_RUN标志。
  • 9)下一个tasklet,直到没有等待处理的tasklet。

3.2、使用tasklet

3.2.1、声明tasklet

  你可以静态创建tasklet或者动态创建tasklet,静态创建tasklet表示你有一个直接引用,可以使用定义在 <linux/interrupu.h> 的两个宏中的某一个:

#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 }

  这两个宏都能根据给出的名称静态创建tasklet_struct结构,当该tasklet被调度以后,func就会执行,它的参数由data指出。前一个宏把引用计数器的值设置为0,改tasklet处于激活状态;后一个宏把引用计数器设置为1,相应的tasklet处于禁用状态。
  还可以通过一个间接引用赋给一个动态创建的tasklet_struct结构的方式来初始化:

void tasklet_init(struct tasklet_struct *t,
          void (*func)(unsigned long), unsigned long data)

3.2.2、编写tasklet处理程序

void tasklet_handler(unsigned long data);

  因为tasklet是使用软中断实现的,不能睡眠,因此不能使用信号量或者其他阻塞函数。由于tasklet运行时允许响应中断,如果你的tasklet和中断处理程序之间共享了某些数据,必须做好预防工作(比如屏蔽中断然后获取一个锁)。两个相同的tasklet不会同时执行,但是两个不同类型的tasklet可以同时执行。和软中断不同的是,在执行软中断处理程序的时候如果再次触发软中断,不同的处理器可以再次运行此处理程序。

3.2.3、禁止和激活tasklet

  作为一种优化措施,一个tasklet总在调度它的处理器上执行——这样可以更好地利用处理器的高速缓存。
  可以使用以下函数来禁止和激活某个tasklet


tasklet_disable(&my_tasklet);		//禁止,如果my_tasklet正在执行,会等待执行完毕
tasklet_disable_nosync(&my_tasklet);	//禁止,不等待

tasklet_enable(&my_tasklet);	//激活,使用DECLARE_TASKLET_DISABLE()静态创建的tasklet可以激活

  通过调用tasklet_kill() 函数从挂起的队列中去掉一个tasklet。

tasklet_kill(&my_tasklet);

  该函数会引起休眠,禁止在中断上下文使用。

3.2.4、ksoftirqd

  有时候软中断被触发的频率可能很高,甚至处理函数还会自行重复触发,导致用户空间进程无法获得足够的处理器时间。为了较为平衡地解决这个问题,当大量软中断出现的 时候,内核会唤醒一组内核线程来处理这些负载。这些线程会分配较低的优先级(nice值较高)来避免和重要的任务抢占资源。
  每个处理器都有这样一个线程,名字叫做ksoftirqd/n,n是处理器的编号。一旦线程被初始化,他就会执行类似的循环:
在这里插入图片描述
  softirq_pending() 函数用于检测待处理的软中断,如果有就调用do_softirq() 处理,每次迭代都会调用schedule() 让更重要的进程获得CPU。当所有操作都完成后,ksoftirqd线程将自身设置为TASK_INTERRUPTIBLE状态,唤起调度称需选择其他可执行的进程。
  只要do_softirq()函数发现已经执行过的内核线程重新触发了它自己,软中断内核线程就会被唤醒

4、工作队列

  工作队列可以把工作推后,交由一个内核线程执行,通过工作队列执行的代码能占尽进程上下文的所有优势,工作队列允许调度和睡眠。所以,如果推后执行的任务需要重新调度或者睡眠就选择工作队列。
  实际上,工作队列可以使用内核线程替换,但是不推荐创建新的内核线程(可能有坑吧)。

4.1、工作队列的实现

  工作队列子系统是一个用于创建内核线程的接口,通过它创建的线程负责执行由内核其他部分排到队列里的任务。它创建的这些线程称作工作者线程。工作队列子系统提供了一个缺省的工作者线程来处理推后的工作,因此,工作队列最基本的表现形式变为:把需要推后执行的任务交给特定的通用线程接口。当然,你也可以创建属于自己的工作者线程。
  缺省的工作者线程叫做events/n ,n代表处理器编号,每个处理器只有一个缺省工作者线程。

4.1.1、工作者线程的数据结构

  工作者线程类型用workqueue_struct数据结构表示:

/*
 * The externally visible workqueue abstraction is an array of
 * per-CPU workqueues:
 */
struct workqueue_struct {
    struct cpu_workqueue_struct *cpu_wq;
    struct list_head list;
    const char *name;
    int singlethread;
    int freezeable;     /* Freeze threads during suspend */
#ifdef CONFIG_LOCKDEP
    struct lockdep_map lockdep_map;
#endif
};

  工作者线程结构中包含一个cpu_workqueue_struct结构指针(2.6版本中为数组),对应系统中的处理器。cpu_workqueue_struct结构是工作者线程的核心数据结构,如下所示:

/*
 * The per-CPU workqueue (if single thread, we always use the first
 * possible cpu).
 */
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;		//关联线程

    int run_depth;      /* Detect run_workqueue() recursion depth */
} ____cacheline_aligned;			//对齐高速缓存

4.1.2、表示工作的数据结构

  所有工作者线程都是用普通内核线程实现的,它们都要执行worker_thread() 函数。这个函数执行一个死循环后睡眠,当有操作被插入到队列里时,就会被唤醒去执行这些操作,操作执行完后会继续睡眠。

struct work_struct {
    atomic_long_t data;
#define WORK_STRUCT_PENDING 0       /* T if work item pending execution */
#define WORK_STRUCT_FLAG_MASK (3UL)
#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)
    struct list_head entry;
    work_func_t func;
#ifdef CONFIG_LOCKDEP
    struct lockdep_map lockdep_map;
#endif
};

  这个结构被连接成一个链表,每个处理器的每种工作者线程的队列上都有一个这样的链表。当一个工作者线程被唤醒时,它会执行链表上的所有工作,工作被执行完毕时,相应的work_struct 对象会从链表中被移除。当链表上没有对象时,继续睡眠。
  worker_thread() 函数的核心如下:

for(;;){
	prepare_to_wait(&cwq->more_work,&wait,TASK_INTERRUPTIBLE);	//将线程设置为休眠态,state会被设置为TASK_INTERRUPTIBLE
	if(list_empty(&cwq->worklist))		//如果工作链表为空
		schedule();						//调用schedule函数进入睡眠
	finish_wait(&cwq->more_work,&wait);	//被唤醒?
	run_workqueue(cwq);					//调用run_workqueue执行推后工作
}

  run_workqueue() 函数的核心如下:

	while(!list_empty(&cwq->worklist)){		//链表是否为空
		struct work_struct *work;
		work_func_t f;
		void *data;
		work = list_entry(cwq->worklist.next,struct work_struct,entry);		//获取work_struct对象
		f = work->func;		//获取work_struct对象上的处理函数func
		list_del_init(cwq->worklist.next);	//将该节点从链表上删除
		work_clear_pending(work);		//清除待处理标志位pending
		f(work);		//调用处理函数
	}

4.1.3、工作队列实现机制的总结(重要)

在这里插入图片描述

  位于最高一层是工作者线程。系统允许有多种类型的工作者线程存在。对于指定的一个类型,系统的每个CPU上都有一个该类的工作者线程。默认情况下内核只有event这一种类型的工作者线程。每个工作者线程由一个cpu_workqueue_struct结构表示。
  而工作者线程的类型(默认的类型只有event)由结构体workqueue_struct表示。
在这里插入图片描述
  工作处于最底层,由work_struct结构表示。结构体中有一个指针指向一个函数,这个函数负责处理需要推后执行的任务,也就是上面图里所说的延迟函数工作会被提交给某个工作者线程,然后再这个工作者线程会被唤醒并执行这些排好的工作

4.2、工作队列的使用

4.2.1、创建推后的工作

  可以通过DECLARE_WORK在编译时静态创建work_struct结构体。

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

  DECLARE_WORK() 静态创建一个名为name,处理函数为func,参数为data的work_struct结构体。在2.6内核版本,DECLARE_WORK() 有三个形参,后面的版本变为只带name和func形参,形参被固化在初始化代码里。
  也可以使用INIT_WORK() 宏动态初始化一个指向work_struct结构的指针:

INIT_WORK(struct work_struct *work,void (*func)(void *),void *data);

4.2.2、工作队列处理函数

  函数原型和创建时的func形参结构一样。处理函数会由一个工作者线程执行,因此函数运行在进程上下文中。尽管如此,处理函数不能访问用户空间,因为内核线程在用户空间没有相关的内存映射。通常在发生系统调用时,内核会代表用户空间的进行运行,此时才能访问用户空间,才能映射用户空间内存。

4.2.3、工作调度

  想要把给定的工作处理函数提交给缺省的events工作线程,只需要调用:

schedule_work(&work);

  work会马上被调度,一旦处理器的工作者线程被唤醒,就会被执行。
  如果不希望马上被执行,可以在调度时指定延后时间:

schedule_delayed_work(&work,delay);

  delay参数是以时钟节拍为标准的。

4.2.4、刷新操作

  排入队列的工作会在工作者线程下一次被唤醒的时候执行。有时,在进行下一步操作之前,必须保证一些操作已经执行完。内核提供了一个用于刷新events/n工作队列的函数:

void flush_scheduled_work(void);

  函数会一直等待,直到队列中所有的对象都被执行完后才返回,等待期间函数会进入休眠,因此只能在进程上下文中使用。
  在一个驱动模块卸载前、内核中防止竞争条件的出现或者确保不再有等待处理的工作,都可能会执行此函数。在内核中检索这个函数,其中一些用如下:

//在drivers/net/sungem.c中
static struct pci_driver gem_driver = {
	...
    .suspend    = gem_suspend,  
    ...                                                                                                                                                       
};
static int gem_suspend(struct pci_dev *pdev, pm_message_t state)
{
	...
	flush_scheduled_work();
	...
}
//在drivers/infiniband/core/device.c中
static void __exit ib_core_cleanup(void)
{
    ib_cache_cleanup();
    ib_sysfs_cleanup();
    /* Make sure that any pending umem accounting work is done. */
    flush_scheduled_work();
}
                                                                                                                                                                                        
module_init(ib_core_init);
module_exit(ib_core_cleanup);

  注意:函数并不会取消延迟执行的工作。就是说,任何通过schedule_delayed_work进行延迟调度的工作,不会因为flush_scheduled_work冲刷掉,所以,如果使用schedule_delayed_work延迟调度工作,那么在使用flush_scheduled_work函数之前要使用cancel_delayed_work函数取消延迟执行的工作。

4.3、创建自己的工作队列类型

  如果缺省的工作队列不能满足你的需求,可以自己创建一个新的工作队列和工作者线程。但是要明确一点,就是自己创建的一套线程能否提高性能?
  创建一个新的工作队列和相关工作者线程只需调用一个简单的函数:

struct workqueue_struct * create_workqueue(const char *name);

  name就是工作者线程的名字。缺省的工作队列和工作者线程events就是这样创建的:
在这里插入图片描述
  与schedule_work()和schedule_delayed_work()对应的函数为:

int queue_work(struct workqueue_struct *wq,struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *wq,struct work_struct *work,unsigned long delay);

  刷新操作使用以下函数:

flush_queue(struct workqueue_struct *wq);

5、禁止和激活下半部

  禁止和激活所有下半部处理(所有软中断和tasklet)使用如下函数:

void local_bh_disable()
void local_bh_enable()

  禁止和激活成对存在,禁止几次就要激活几次,最后一次激活才真正激活。
  函数通过preempt_count为每个进程维护一个计数器。(第二篇中内核抢占也是使用preempt_count)当计数器变为0时,下半部才能被处理。
  这些函数和硬件体系有关,位于 <asm/softirq.h> 中,通常由复杂的宏实现。C语言版本的近似描述:
在这里插入图片描述
  禁止和激活下半部和工作队列无瓜,因为工作队列是在进程上下文中的。

6、总结(重要)

  软中断在编译期间静态创建,最多只有32个。软中断提供的执行序列化保障最少,相应的操作要注意和确保共享数据的安全性。相同类别的软中断有可能在不同的处理器上同时执行。
  tasklet接口简单,两个相同类别的tasklet不能同时执行,不能并发运行;两个不同类别的tasklet(HI_SOFTIRQ和TASKLET_SOFTIRQ类型)可以同时执行。tasklet自己负责执行的序列化保障,无需处理同步问题,但是如果两个不同类型的tasklet共享同一数据,就要处理好锁和同步。
  工作队列适用于推后工作需要睡眠、重新调度、获取大量内存、获取信号量等进程中才能完成的工作。工作者线程都是普通内核线程实现的。
  如果进程上下文和下半部共享数据,在访问数据之前,需要禁止下半部的处理并得到锁的使用权。
  如果中断上下文和下半部共享数据,在访问数据之前,需要禁止中断并得到锁的使用权。
  禁止和激活下半部和工作队列无瓜,因为工作队列是在进程上下文中的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr_zhangsq

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值