《Linux内核设计与实现》读书笔记——下半部和推后执行的工作

中断处理程序的局限

中断处理程序以异步方式执行,有可能打断其它重要代码;

  • 需要避免被打断的代码停止时间过长;

当前中断处理程序正在执行时,其它中断会被屏蔽;

  • 中断处理程序执行越快越好;
  • 被屏蔽的中断会在后续激活,而不是放弃;

中断处理程序往往需要操作硬件;

  • 所以通常有很高的时限要求;

中断处理程序不在进程上下文中运行;

  • 所以不能阻塞;

上半部与下半部

中断处理程序(也叫上半部):一个快速、异步、简单的机制负责对硬件做出迅速响应并完成那些要求很严格的操作;

  • 如果一个任务对时间非常敏感,将其放在中断处理程序中执行
  • 如果一个任务和硬件相关,将其放在中断处理程序中执行;
  • 如果一个任务要保证不被其他中断(特别是相同的中断)打断,将其放在中断处理程序中执行;

下半部:执行与中断比密切相关但中断处理程序本身不执行的工作;

  • 其他所有任务,考虑放在下半部执行。
  • 通常下半部在中断处理程序一返回就会马上执行;
  • 下半部的关键在于当它运行时允许响应所有的中断;

中断分类

BH:Bottom Half,老的下半部,不介绍。

任务队列:老的下半部,不介绍。

软中断和tasklet

  • tasklet通过软中断实现;
  • 大部分下半部可以使用tasklet,只有对性能要求非常高的情况(比如网络)才使用软中断;

工作队列

软中断

软中断有softirq_action结构表示(include\linux\interrupt.h):

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

kernel\softirq.c中定义了一个包含有32个该结构体的数组:

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

目前(Linux 2.6.34)只使用了9个:

Linux 2.6.34.1中实际找到了10个(include\linux\interrupt.h):

/* PLEASE, avoid to allocate new softirqs, if you need not _really_ high
   frequency threaded job scheduling. For almost all the purposes
   tasklets are more than enough. F.e. all serial device BHs et
   al. should be converted to tasklets, not to softirqs.
 */
enum
{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    BLOCK_IOPOLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ,
    RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */
    NR_SOFTIRQS
};

相比图片中多了一个BLOCK_IOPOOL_SOFTIRQ,这也符合书中的说明,即新增的软中断可以插在BLOCK_SOFTIRQTASKLET_SOFTIRQ之间。

软中断是在编译期间静态分配的。

一个注册的软中断必须在被标记后才会执行,这被称作触发软中断

中断处理程序会在返回前标记它的软中断,使其在稍后被执行。

之后,在合适的时刻,该软中断会被执行。

合适的时刻,比如:

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

不管是用什么办法唤起,软中断都要在do_softirq()中执行:

asmlinkage void do_softirq(void)
{
    __u32 pending;
    unsigned long flags;
    if (in_interrupt())
        return;
    local_irq_save(flags);
    pending = local_softirq_pending();
    if (pending)
        __do_softirq();
    local_irq_restore(flags);
}

在中断处理程序中触发软中断是最常见的形式。内核在执行完中断处理程序之后,马上就会调用do_softirq()函数,来执行中断处理程序留给它去完成的剩余任务。

raise_softirq()将一个软中断设置为挂起状态,让它在下次调用do_softirq()函数时投入运行,下面是一个例子:

void run_local_timers(void)
{
    hrtimer_run_queues();
    raise_softirq(TIMER_SOFTIRQ);
    softlockup_tick();
}

对于软中断(包括tasklet),内核不会立即处理重新触发的软中断,而作为改进,当大量软中断出现时,内核会唤醒内核线程ksoftirqd来处理这些负载。

软中断使用

通过open_softirq()函数,往上面的几种软中断中注册处理函数(称为软中断处理程序)。以下是一个例子(block\blk-iopoll.c):

static __init int blk_iopoll_setup(void)
{
    int i;
    for_each_possible_cpu(i)
        INIT_LIST_HEAD(&per_cpu(blk_cpu_iopoll, i));
    open_softirq(BLOCK_IOPOLL_SOFTIRQ, blk_iopoll_softirq);
    register_hotcpu_notifier(&blk_iopoll_cpu_notifier);
    return 0;
}

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

在一个软中断处理程序运行的时候,当前处理器上的软中断被禁止,但是其它处理器仍可以执行别的(别的?但是下面不是说这个也可以吗?)软中断。

如果同一个软中断在它被执行的同时再次被触发,那么另外一个处理器可以同时运行其处理程序。这意味着共享数据,所以需要做好锁保护。实际上大部分软中断处理程序通过采取单处理器数据或者其它的一些技巧来避免显式得加锁。

如果不需要扩展到多个处理器,就使用tasklet,它的同一个处理程序的多个实例不能在多个处理器上同时运行

tasklet

tasklet是利用软中断实现的一种下半部机制。

相比软中断,更应该使用tasklet。

tasklet有两个软中断代表:HI_SOFTIRQ和TASKLET_SOFTIRQ(前面的表中有说明,前者优先级高于后者)。

tasklet结构体:

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

func成员是tasklet处理程序。

count为0是tasklet才被激活,并在被设置为挂起状态时才能够被执行。

stateTASKLET_STATE_SCHED时表示tasklet已被调度;TASKLET_STATE_RUN用于判断tasklet是否在其它处理器上执行。

每个结构体单独代表一个tasklet。已调度的tasklet(相当于被触发的软中断)存放在两个单处理器数据结构(前面提到过,它用于避免显式地枷锁)tasklet_vec和tasklet_hi_ver(对应两个软件中断代码),这两个数据结构是由tasklet结构体构成的链表。

tasklet_schedule()和tasklet_hi_schedule()函数用来调度tasklet。

tasklet操作

静态创建tasklet:

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

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

调度tasklet:

static inline void tasklet_schedule(struct tasklet_struct *t)

禁止tasklet(会操作count成员):

static inline void tasklet_disable(struct tasklet_struct *t)

激活tasklet(会操作count成员):

static inline void tasklet_enable(struct tasklet_struct *t)

工作队列

工作队列可以把工作推后,交由一个内核线程去执行

这个下半部总是会在进程上下文中执行

如果推后执行的任务需要睡眠,那么就需要选择工作队列。这样的下半部可以:

  1. 获取大量内存;
  2. 需要获取信号量;
  3. 需要执行阻塞式的IO操作;

使用工作者线程来处理工作队列。

内核会创建缺省工作者线程events/n,n表示处理器编号。

许多内核驱动程序都把它们的下半部交给缺省的工作线程去做。

处理器密集型和性能要求严格的任务会拥有自己的工作者线程。

工作者线程用workqueue_struct结构体表示:

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;
#ifdef CONFIG_LOCKDEP
    struct lockdep_map lockdep_map;
#endif
};

由于每个处理器对应一个工作者线程,所以这里还有一个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;

worklist对应具体的工作列表,工作用work_struct表示:

struct work_struct {
    atomic_long_t data;
#define WORK_STRUCT_PENDING 0       /* T if work item pending execution */
#define WORK_STRUCT_STATIC  1       /* static initializer (debugobjects) */
#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
};

工作队列处理函数func

typedef void (*work_func_t)(struct work_struct *work);

工作者线程通过执行worker_thread()函数来执行具体的工作。

 

工作队列的使用

静态创建队列:

#define DECLARE_WORK(n, f)                  \
    struct work_struct n = __WORK_INITIALIZER(n, f)

动态创建队列:

#define INIT_WORK(_work, _func)                 \
    do {                            \
        __INIT_WORK((_work), (_func), 0);       \
    } while (0)

对工作进行调度:

extern int schedule_work(struct work_struct *work);

刷新工作队列:

extern void flush_scheduled_work(void);

 

下半部的比较

 

下半部的禁止和使能

void local_bh_enable(void)
void local_bh_disable(void)

但是这些函数对工作队列无效。

工作队列没有必要禁止,因为是在进程上下文中执行的,不会涉及到异步执行的问题。

 

猜你喜欢

转载自blog.csdn.net/jiangwei0512/article/details/106145550