Linux中断子系统之软中断、tasklet和工作队列

Linux中断子系统之软中断、tasklet和工作队列

一、软中断

软中断使得内核可以延期执行任务。因为它们的运作方式与上文描述的中断类似,但完全是用软件实现的,所以称为软中断(softIRQ)。

1.1 软中断的初始化

在内核中有一个软件中断的数组:

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

softirq_action 这个数据结构用于描述一个软件中断。默认情况下,系统上只能使用 32 个软中断。

struct softirq_action
{
    void   (*action)(struct softirq_action *);
};
  • action是一个指向处理程序例程的指针,在软中断发生时由内核执行该处理程序例程。

软中断必须先注册,内核才能执行软中断。open_softirq 函数即用于该目的,它在softirq_vec 表中指定的位置写入新的软中断:

void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}

软中断的类型是通过枚举静态定义的,当前有10种软中断类型,从0到9。0的优先级最高,当系统调度到软中断时会最先执行。

enum
{
    HI_SOFTIRQ=0,        /* 高优先级tasklet软中断 */
    TIMER_SOFTIRQ,       /* 定时器软中断 */
    NET_TX_SOFTIRQ,      /* 发送网络包软中断 */
    NET_RX_SOFTIRQ,      /* 接收网络包软中断 */
    BLOCK_SOFTIRQ,       /* 用于处理块设备「block device」的软中断 */
    IRQ_POLL_SOFTIRQ,    /* 用于执行IOPOLL的回调函数 */
    TASKLET_SOFTIRQ,     /* tasklet软中断 */
    SCHED_SOFTIRQ,       /* 用于进程调度与负载均衡的软中断 */
    HRTIMER_SOFTIRQ,     /* 用于高精度定时器的软中断 */
    RCU_SOFTIRQ,         /* 用于RCU服务的软中断 */

    NR_SOFTIRQS
};

在 start_kernel 函数会调用 softirq_init(); 函数来初始化软件中断:

void __init softirq_init(void)
{
    ...
    /* 注册tasklet相关的两个软中断 */
    open_softirq(TASKLET_SOFTIRQ, tasklet_action);
    open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
1.2 软中断的触发

系统通过一个per cpu的 irq_cpustat_t数据结构来记录软中断的触发与否,可以将其理解为软中断状态寄存器,该寄存器实际是一个uint32_t的__softirq_pending变量,使用bitmap的形式,32个bit可以对应32个软中断,当然目前只使用了bit0到bit9这10个。

typedef struct {
    unsigned int __softirq_pending;
} ____cacheline_aligned irq_cpustat_t;

触发软中断其实就是在__softirq_pending相应的bit位上写1。触发软中断的接口有两个raise_softirqraise_softirq_irqoff,前者在接口内部会关本地中断,而后者需要保证调用时已经处于关中断状态。

inline void raise_softirq_irqoff(unsigned int nr)
{
    __raise_softirq_irqoff(nr);

    if (!in_interrupt())
        wakeup_softirqd();
}

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

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

void __raise_softirq_irqoff(unsigned int nr)
{
    lockdep_assert_irqs_disabled();
    trace_softirq_raise(nr);
    or_softirq_pending(1UL << nr);
}
1.3 软中断的执行

因为每个cpu都有一个软中断状态寄存器__softirq_pending,因此每个CPU可以并行执行自己的软中断队列。软中断的执行时机有3个:

  • 触发软中断后(执行raise_softirqraise_softirq_irqoff时);
  • 硬中断退出时(irq_exit时);
  • 任何调用local_bh_enable打开本地软中断的时机。
https

有几种方法可开启软中断处理,但这些都归结为调用 do_softirq 函数。

asmlinkage __visible void do_softirq(void)
{
    __u32 pending;
    unsigned long flags;

    if (in_interrupt())
        return;

    local_irq_save(flags);     /* 保存IF标志的状态值,并禁用本地CPU上的中断 */

    pending = local_softirq_pending();

    if (pending && !ksoftirqd_running(pending))
        do_softirq_own_stack();

    local_irq_restore(flags);  /* 恢复保存的IF标志的状态值并返回 */
}

in_interrupt() 该函数确认当前不处于中断上下文中(当然,即不涉及硬件中断)。如果处于中断上下文,则立即结束。因为软中断用于执行 ISR 中非时间关键部分,所以其代码本身一定不能在中断处理程序内调用。

通过 local_softirq_pending,确定当前 CPU 软中断位图中所有置位的比特位。如果有软中断等待处理,则调用 __do_softirq。

asmlinkage __visible void __softirq_entry __do_softirq(void)
{
    unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
    unsigned long old_flags = current->flags;
    int max_restart = MAX_SOFTIRQ_RESTART;    /* 循环计数器的值初始化为10 */
    struct softirq_action *h;
    bool in_hardirq;
    __u32 pending;
    int softirq_bit;

    /*
     * Mask out PF_MEMALLOC as the current task context is borrowedfor the
     * softirq. A softirq handled, such as network RX, might set PF_MEMALLOC
     * again if the socket is related to swapping.
     */
    current->flags &= ~PF_MEMALLOC;
    /* 将本CPU的__softirq_pending变量复制到局部变量中 */
    pending = local_softirq_pending();
    account_irq_enter_time(current);
    /* 关软中断,防止再次进入软中断执行逻辑 */
    __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
    in_hardirq = lockdep_softirq_start();

restart:
    /* 将__softirq_pending清零,以便可以激活新的软中断 */
    set_softirq_pending(0);
    /* 开启硬件中断 */
    local_irq_enable();

    h = softirq_vec;

    while ((softirq_bit = ffs(pending))) {
        unsigned int vec_nr;
        int prev_count;

        h += softirq_bit - 1;

        vec_nr = h - softirq_vec;
        prev_count = preempt_count();
        /* 对softirq的执行次数计数,这是/proc/softirqs的数据来源 */
        kstat_incr_softirqs_this_cpu(vec_nr);

        trace_softirq_entry(vec_nr);
        /* 某软中断对应的action回调 */
        h->action(h);
        trace_softirq_exit(vec_nr);
        if (unlikely(prev_count != preempt_count())) {
            pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
                   vec_nr, softirq_to_name[vec_nr], h->action,
                   prev_count, preempt_count());
            preempt_count_set(prev_count);
        }
        h++;
        pending >>= softirq_bit;
    }

    if (__this_cpu_read(ksoftirqd) == current)
        rcu_softirq_qs();
    /* 处理完一波软中断后,关闭硬件中断 */
    local_irq_disable();
    /* 读一下当前的__softirq_pending,看这段时间有没有触发新的软中断 */
    pending = local_softirq_pending();
    /* 有新的软中断需要处理:
     * 如果时间没超过2ms && 当前没有进程需要调度 && 这种循环没有超过10次,
     * 此时重新跳到while循环处依次执行软中断的action回调;
     * 否则唤醒ksoftirqd线程执行软中断处理。
     */
    if (pending) {
        if (time_before(jiffies, end) && !need_resched() &&
            --max_restart)
            goto restart;

        wakeup_softirqd();
    }

    lockdep_softirq_end(in_hardirq);
    account_irq_exit_time(current);
    /* 离开软中断上下文时,开启软中断 */
    __local_bh_enable(SOFTIRQ_OFFSET);
    WARN_ON_ONCE(in_interrupt());
    current_restore_flags(old_flags, PF_MEMALLOC);
}

__do_softirq函数只做固定时间或固定次数的循环,然后就返回了,避免出现延迟用户进程的执行。如果还有其余挂起的软中断,内核线程ksoftirqd将会在预期时间里处理它们。

1.4 ksoftirqd内核线程

系统中的每个处理器都分配了自身的守护进程,名为 ksoftirqd。

static void wakeup_softirqd(void)
{
    /* Interrupts are disabled: no need to stop preemption */
    struct task_struct *tsk = __this_cpu_read(ksoftirqd);

    if (tsk && tsk->state != TASK_RUNNING)
        wake_up_process(tsk);
}

wake_up_process唤醒进程后运行run_ksoftirqd()函数。

static void run_ksoftirqd(unsigned int cpu)
{
    local_irq_disable();
    if (local_softirq_pending()) {
        /*
         * We can safely run softirq on inline stack, as we are not deep
         * in the task stack here.
         */
        __do_softirq();
        local_irq_enable();
        cond_resched();
        return;
    }
    local_irq_enable();
}

二、tasklet

linux内核为什么还要引入tasklet机制呢?主要原因是软中断的pending标志位也就32位,一般情况是不随意增加软中断处理的。而且内核也没有提供通用的增加软中断的接口。其次内,软中断处理函数要求可重入,需要考虑到竞争条件比较多,要求比较高的编程技巧。所以内核提供了tasklet这样的一种通用的机制。

tasklet是I/O驱动程序中实现可延迟函数的首选方法。tasklet建立在两个叫做HI_SOFTIRQ和TASKLET_SOFTIRQ的软中断之上。tasklet和高优先级的tasklet分别存放在tasklet_vec和tasklet_hi_vec数组中。二者都包含类型为tasklet_head的NR_CPUS个元素,每个元素都由指向tasklet描述符链表的指针组成。

各个 tasklet 的数据结构称作 tasklet_struct,定义如下:

struct tasklet_struct
{
    struct tasklet_struct *next;   /* 将多个tasklet链接成单向循环链表 */
    unsigned long state;
    atomic_t count;                /* 0:激活tasklet 非0:禁用tasklet */
    bool use_callback;
    union {
        void (*func)(unsigned long data);   /* 指向tasklet函数指针 */
        void (*callback)(struct tasklet_struct *t);
    };
    unsigned long data;            /* 用作函数执行时的参数 */
};

state表示任务的当前状态,有两个标志:

  • TASKLET_STATE_SCHED 表示tasklet是挂起的,等待调度执行时

  • TASKLET_STATE_RUN 表示 tasklet 当前正在执行。

tasklet_schedule处理过程也比较简单,就是把tasklet_struct结构体挂到tasklet_vec链表或者挂接到tasklet_hi_vec链表上,并调度软中断。

static void __tasklet_schedule_common(struct tasklet_struct *t,
                      struct tasklet_head __percpu *headp,
                      unsigned int softirq_nr)
{
    struct tasklet_head *head;
    unsigned long flags;

    local_irq_save(flags);
    head = this_cpu_ptr(headp);
    t->next = NULL;
    *head->tail = t;
    head->tail = &(t->next);
    raise_softirq_irqoff(softirq_nr);
    local_irq_restore(flags);
}

tasklet执行过程tasklet_action_common在软中断被调度到后会被执行,从链表中把tasklet_struct结构体都取下来,然后逐个执行。如果t->count的值等于0,说明这个tasklet在调度之后,被disable掉了,所以会将tasklet结构体重新放回到tasklet_vec链表,并重新调度TASKLET_SOFTIRQ软中断,在之后enable这个tasklet之后重新再执行它。

static void tasklet_action_common(struct softirq_action *a,
                  struct tasklet_head *tl_head,
                  unsigned int softirq_nr)
{
    struct tasklet_struct *list;

    local_irq_disable();
    list = tl_head->head;
    tl_head->head = NULL;
    tl_head->tail = &tl_head->head;
    local_irq_enable();

    while (list) {
        struct tasklet_struct *t = list;

        list = list->next;

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

        local_irq_disable();
        t->next = NULL;
        *tl_head->tail = t;
        tl_head->tail = &t->next;
        __raise_softirq_irqoff(softirq_nr);
        local_irq_enable();
    }
}

因为一个 tasklet 只能在一个处理器上执行一次,但其他的 tasklet 可以并行运行,所以需要特定于 tasklet 的锁。 state 状态用作锁变量。在执行一个 tasklet 的处理程序函数之前,内核使用 tasklet_trylock 检查 tasklet 的状态是否为 TASKLET_STATE_RUN。

三、工作队列

从上面的介绍看以看出,软中断运行在中断上下文中,因此不能阻塞和睡眠,而tasklet使用软中断实现,当然也不能阻塞和睡眠。但如果某延迟处理函数需要睡眠或者阻塞呢?没关系工作队列就可以如您所愿了。

工作队列(work queue)是另外一种将工作推后执行的形式。工作队列可以把工作推后,交由一个内核线程去执行—这个下半部分总是会在进程上下文执行,但由于是内核线程,其不能访问用户空间。最重要特点的就是工作队列允许重新调度甚至是睡眠。实际上,工作队列的本质就是将工作交给内核线程处理,因此其可以用内核线程替换。但是内核线程的创建和销毁对编程者的要求较高,而工作队列实现了内核线程的封装,不易出错,所以也推荐使用工作队列。

ttps://
3.1 关键数据结构

工作是实际需要被执行的用户工作,主体是一个工作函数。常用的工作分为两种:普通工作和延迟工作,延迟工作仅仅是添加了一个参数:延时时间,单位是 jiffies,工作队列会在延时时间过后被设置为就绪状态。

普通工作的结构体由 struct work_struct 描述:

struct work_struct {
    atomic_long_t data;               /* 传递给工作函数的参数 */
    struct list_head entry;           /* 链表结构,链接同一工作队列上的工作 */
    work_func_t func;                 /* 工作函数,用户自定义实现 */
#ifdef CONFIG_LOCKDEP
    struct lockdep_map lockdep_map;   /* 死锁检测 */
#endif
};

延迟工作的结构体由 struct delayed_work描述:

struct delayed_work {
    struct work_struct work;
    struct timer_list timer;       /* 定时器,用于实现延迟处理 */

    /* target workqueue and CPU ->timer uses to queue ->work */
    struct workqueue_struct *wq;   /* 当前工作相关的工作队列 */
    int cpu;
};
  • workqueue_struct:用于描述一个工作队列,在创建工作队列时返回该结构
struct workqueue_struct {
    struct list_head    pwqs;        /* 链表头,该链表中挂着所有与当前 workqueue_struct 相关的 pool_workqueue */
    struct list_head    list;        /* 链表节点,通过该链表节点将该 workqueue_struct 链接到全局链表 */

    struct mutex        mutex;        /* protects this wq */
    int            work_color;        /* WQ: current work color */
    int            flush_color;       /* WQ: current flush color */
    atomic_t        nr_pwqs_to_flush; /* flush in progress */
    struct wq_flusher   *first_flusher;  /* WQ: first flusher */
    struct list_head    flusher_queue;    /* WQ: flush waiters */
    struct list_head    flusher_overflow; /* WQ: flush overflow list */

    struct list_head    maydays;      /* 需要 rescue 执行的工作链表 */
    struct worker        *rescuer;    /* rescue 内核线程,这是一个独立的内核线程 */

    int            nr_drainers;       /* WQ: drain in progress */
    int            saved_max_active;  /* WQ: saved pwq max_active */

    struct workqueue_attrs    *unbound_attrs;    /* PW: only for unbound wqs */
    struct pool_workqueue    *dfl_pwq;    /* PW: only for unbound wqs */

#ifdef CONFIG_SYSFS
    struct wq_device    *wq_dev;    /* I: for sysfs interface */
#endif
#ifdef CONFIG_LOCKDEP
    char            *lock_name;
    struct lock_class_key    key;
    struct lockdep_map    lockdep_map;
#endif
    char            name[WQ_NAME_LEN]; /* I: workqueue name */

    /*
     * Destruction of workqueue_struct is RCU protected to allow walking
     * the workqueues list without grabbing wq_pool_mutex.
     * This is used to dump all workqueues from sysrq.
     */
    struct rcu_head        rcu;

    /* hot fields used during command issue, aligned to cacheline */
    unsigned int        flags ____cacheline_aligned; /* WQ: WQ_* flags */
    struct pool_workqueue __percpu *cpu_pwqs; /* 指向 percpu 类型的 pool_workqueue */
    struct pool_workqueue __rcu *numa_pwq_tbl[]; /* 指向 pernode 类型的 pool_workqueue */
};
  • pool_workqueue:属于 worker_pool 和 workqueue_struct 之间的中介,负责将 workqueue_struct 和 worker_pool 联系起来,一个 workqueue_struct 对应多个 pool_workqueue。
struct pool_workqueue {
    struct worker_pool  *pool;    //当前 pool_workqueue 对应的 worker_pool。
    struct workqueue_struct *wq;  //当前 pool_workqueue 对应的 workqueue_struct。

    int         refcnt;           //引用计数,内核的回收机制

    int         nr_active;        //正处于活动状态的 work 数量。
    int         max_active;       //处于活动状态的 work 最大值

    struct list_head    delayed_works;  // delayed_work 链表,delayed_work 因为有个延时,需要记录起来
    struct list_head    pwqs_node;      //节点,用于将当前的 pool_workqueue 结构链接到 wq->pwqs 中。
    struct list_head    mayday_node;    //当前节点会被链接到 wq->maydays 链表中。

    struct work_struct  unbound_release_work;
} __aligned(1 << WORK_STRUCT_FLAG_BITS);
  • struct worker_pool:工作队列池,存在多个,通常是对应 CPU 而存在的,系统默认会为每个 CPU 创建两个 worker_pool,一个普通的一个高优先级的 pool,这些 pool 是和对应 CPU 严格绑定的,同时还会创建两个 ubound 类型的 pool,也就是不与 CPU 绑定的 pool。
struct worker_pool {
    spinlock_t      lock;       //该 worker_pool 的自旋锁
    int         cpu;            //该 worker_pool 对应的 cpu
    int         node;           //对应的 numa node
    int         id;             //当前的 pool ID.
    unsigned int        flags;  //标志位

    unsigned long       watchdog_ts;    //看门狗,主要用作超时检测

    struct list_head    worklist;     //核心的链表结构,当 work 添加到队列中时,实际上就是添加到了当前的链表节点上。
    int         nr_workers;           //当前 worker_pool 上工作的数量

    int         nr_idle;        //休眠状态下的数量

    struct list_head    idle_list;  //链表,记录所有处于 idle 的 worker。
    struct timer_list   idle_timer;   //内核定时器用于记录 idle worker 的超时
    struct timer_list   mayday_timer;   // SOS timer

    DECLARE_HASHTABLE(busy_hash, BUSY_WORKER_HASH_ORDER);  //记录正运行的 worker

    struct worker       *manager;   // 管理线程
    struct list_head    workers;    // 当前 worker_pool 上的 worker 线程。
    struct completion   *detach_completion;  //管理所有 worker 的 detach


    struct workqueue_attrs  *attrs;     // worker 的属性
    int         refcnt;     //引用计数
} ____cacheline_aligned_in_smp;
  • worker 用于描述内核线程,由 worker_pool 产生,一个 worker_pool 可以产生多个 worker。
struct worker {
    union {
        struct list_head    entry;    // 如果当前 worker 处于 idle 状态,就使用这个节点链接到 worker_pool 的 idle 链表
        struct hlist_node   hentry;   // 如果当前 worker 处于 busy 状态,就使用这个节点链接到 worker_pool 的 busy_hash 中。
    };

    struct work_struct  *current_work;     //当前需要执行的 worker
    work_func_t     current_func;          //当前执行 worker 的函数
    struct pool_workqueue   *current_pwq;  //当前 worker 对应的 pwq

    struct list_head    scheduled;         //链表头节点,当准备或者运行一个 worker 的时候,将 work 连接到当前的链表中

    struct task_struct  *task;      // 内核线程对应的 task_struct 结构
    struct worker_pool  *pool;      // 该 worker 对应的 worker_pool

    unsigned long       last_active;    //上一个活动 worker 时设置的 timestamp
    unsigned int        flags;      //运行标志位
    int         id;      //worker id 

    char            desc[WORKER_DESC_LEN]; // work 的描述信息,主要是在 debug 时使用

    struct workqueue_struct *rescue_wq;  // 针对 rescue 使用的 workqueue。  
};
3.2 初始化
3.2.1 工作的初始化

内核提供两种类型的接口对工作进行初始化:分别提供静态和动态的初始化。静态方法的第一个参数只需要提供一个工作名,系统就会创建一个 struct work_struct my_work 结构并对其成员。

DECLARE_WORK(my_work, work_callback)

动态初始化需要自定义一个 struct work_struct 类型结构,传入到 INIT_WORK() 中,传入的参数为指针,比如:

struct work_struct *my_work;
INIT_WORK(my_work,work_callback);

而对于延迟工作队列的初始化,也是提供两个接口:

DECLARE_DELAYED_WORK(n, f)
INIT_DELAYED_WORK(_work, _func)

延迟工作队列的初始化其实就是比普通工作队列多了一项:内核定时器的初始化。

3.2.2 工作队列的初始化

workqueue 初始化的开始有两个函数:

  • workqueue_init_early
  • workqueue_init

workqueue_init_early 直接在 start_kernel 中被调用,而 workqueue_init 的调用路径为:

start_kernel
    rest_init
        kernel_init
            kernel_init_freeable
                workqueue_init

从函数名可以看出,workqueue_init_early 负责前期的初始化工作:

int __init workqueue_init_early(void)
{
    int std_nice[NR_STD_WORKER_POOLS] = { 0, HIGHPRI_NICE_LEVEL };
    int i, cpu;

    WARN_ON(__alignof__(struct pool_workqueue) < __alignof__(long long));

    BUG_ON(!alloc_cpumask_var(&wq_unbound_cpumask, GFP_KERNEL));
    cpumask_copy(wq_unbound_cpumask, cpu_possible_mask);
    /* 创建 pool_workqueue 的高速缓存池 */
    pwq_cache = KMEM_CACHE(pool_workqueue, SLAB_PANIC);
    /* 初始化 percpu 类型的 worker_pool */
    for_each_possible_cpu(cpu) {                 //遍历 cpu 操作
        struct worker_pool *pool;

        i = 0;
        for_each_cpu_worker_pool(pool, cpu) {.   //遍历 worker_pool 操作
            BUG_ON(init_worker_pool(pool));      //初始化 worker_pool
            pool->cpu = cpu;                     //绑定 worker_pool 和 cpu
            cpumask_copy(pool->attrs->cpumask, cpumask_of(cpu));  //获取 cpu mask
            pool->node = cpu_to_node(cpu);  //获取 numa node 节点
            worker_pool_assign_id(pool);    //为当前 worker_pool 分配 id 号

            mutex_lock(&wq_pool_mutex);
            BUG_ON(worker_pool_assign_id(pool));
            mutex_unlock(&wq_pool_mutex);
        }
    }

    /* 创建各种类型的工作队列 */
    for (i = 0; i < NR_STD_WORKER_POOLS; i++) {
        struct workqueue_attrs *attrs;

        BUG_ON(!(attrs = alloc_workqueue_attrs(GFP_KERNEL)));
        attrs->nice = std_nice[i];
        unbound_std_wq_attrs[i] = attrs;

        BUG_ON(!(attrs = alloc_workqueue_attrs(GFP_KERNEL)));
        attrs->nice = std_nice[i];
        attrs->no_numa = true;
        ordered_wq_attrs[i] = attrs;
    }

    system_wq = alloc_workqueue("events", 0, 0);
    system_highpri_wq = alloc_workqueue("events_highpri", WQ_HIGHPRI, 0);
    system_long_wq = alloc_workqueue("events_long", 0, 0);
    system_unbound_wq = alloc_workqueue("events_unbound", WQ_UNBOUND,
                        WQ_UNBOUND_MAX_ACTIVE);
    system_freezable_wq = alloc_workqueue("events_freezable",
                          WQ_FREEZABLE, 0);
    system_power_efficient_wq = alloc_workqueue("events_power_efficient",
                          WQ_POWER_EFFICIENT, 0);
    system_freezable_power_efficient_wq = alloc_workqueue("events_freezable_power_efficient",
                          WQ_FREEZABLE | WQ_POWER_EFFICIENT,
                          0);
    BUG_ON(!system_wq || !system_highpri_wq || !system_long_wq ||
           !system_unbound_wq || !system_freezable_wq ||
           !system_power_efficient_wq ||
           !system_freezable_power_efficient_wq);

    return 0;
}
  • 初始化 percpu 类型的 worker_pool

在当前文件的头部,静态地创建了 percpu 类型的 worker_pool 结构:

static DEFINE_PER_CPU_SHARED_ALIGNED(struct worker_pool [NR_STD_WORKER_POOLS], cpu_worker_pools);

其中,NR_STD_WORKER_POOLS 的值默认为 2,也就是系统初始化时每个 cpu 定义两个静态的 worker_pool。其中,前面的两个 for_each 嵌套表示:以下的操作针对每个 cpu 下的每个 worker_pool。

  • init_worker_pool 负责初始化 worker_pool
static int init_worker_pool(struct worker_pool *pool)
{
    pool->id = -1;
    pool->cpu = -1;
    pool->node = NUMA_NO_NODE;
    pool->flags |= POOL_DISASSOCIATED;
    pool->watchdog_ts = jiffies;
    INIT_LIST_HEAD(&pool->worklist);
    INIT_LIST_HEAD(&pool->idle_list);
    hash_init(pool->busy_hash);

    setup_deferrable_timer(&pool->idle_timer, idle_worker_timeout,
                   (unsigned long)pool);

    setup_timer(&pool->mayday_timer, pool_mayday_timeout,
            (unsigned long)pool);

    INIT_LIST_HEAD(&pool->workers);

    ida_init(&pool->worker_ida);
    INIT_HLIST_NODE(&pool->hash_node);
    pool->refcnt = 1;

    pool->attrs = alloc_workqueue_attrs(GFP_KERNEL);
}   
  • 创建各种类型的工作队列

初始化完成 worker_pool 之后,内核通过传入不同的 flag 多次调用 alloc_workqueue 创建了大量的工作队列:

system_wq = alloc_workqueue("events", 0, 0);
system_highpri_wq = alloc_workqueue("events_highpri", WQ_HIGHPRI, 0);
system_long_wq = alloc_workqueue("events_long", 0, 0);
system_unbound_wq = alloc_workqueue("events_unbound", WQ_UNBOUND,WQ_UNBOUND_MAX_ACTIVE);
system_freezable_wq = alloc_workqueue("events_freezable",WQ_FREEZABLE, 0);
system_power_efficient_wq = alloc_workqueue("events_power_efficient",WQ_POWER_EFFICIENT, 0);
system_freezable_power_efficient_wq = alloc_workqueue("events_freezable_power_efficient",WQ_FREEZABLE | WQ_POWER_EFFICIENT,0); 
  • WQ_HIGHPRI 表示高优先级的工作队列
  • WQ_UNBOUND 表示不和 cpu 绑定的工作队列
  • WQ_FREEZABLE 表示可在挂起的时候,该工作队列也会被冻结
  • WQ_POWER_EFFICIENT 表示节能型的工作队列
  • WQ_MEM_RECLAIM 表示可在内存回收时调用
  • WQ_CPU_INTENSIVE 表示 cpu 消耗型
  • WQ_SYSFS 表示将当前工作队列导出到 sysfs 中

如果你的 work 有特殊的需求,除了调用 schedule_work(),同样可以调用 queue_work() 以指定将 work 加入到上述的某个工作队列中。

  • alloc_workqueue实现
#define alloc_workqueue(fmt, flags, max_active, args...)        \
    __alloc_workqueue_key((fmt), (flags), (max_active),     \
                  NULL, NULL, ##args)

alloc_workqueue 调用了 __alloc_workqueue_key:

struct workqueue_struct *__alloc_workqueue_key(const char *fmt,unsigned int flags,
                           int max_active,struct lock_class_key *key,const char *lock_name, ...)
{
    struct workqueue_struct *wq;
    struct pool_workqueue *pwq;
    ...
    //申请一个 workqueue_struct 结构。
    wq = kzalloc(sizeof(*wq) + tbl_size, GFP_KERNEL);  

    //如果是 unbound 类型的,就申请默认的 attrs,如果不是,就会使用 percpu worker_pool 的 attrs。
    if (flags & WQ_UNBOUND) {
        wq->unbound_attrs = alloc_workqueue_attrs(GFP_KERNEL);
    }

    //初始化 wq 的成员
    wq->flags = flags;
    wq->saved_max_active = max_active;
    mutex_init(&wq->mutex);
    atomic_set(&wq->nr_pwqs_to_flush, 0);
    INIT_LIST_HEAD(&wq->pwqs);
    INIT_LIST_HEAD(&wq->flusher_queue);
    INIT_LIST_HEAD(&wq->flusher_overflow);
    INIT_LIST_HEAD(&wq->maydays);
    INIT_LIST_HEAD(&wq->list);

    //申请并绑定 pwq 和 wq
    alloc_and_link_pwqs(wq);

    //如果设置了 WQ_MEM_RECLAIM 标志位,需要创建一个 rescue 线程。  
    if (flags & WQ_MEM_RECLAIM) {
        struct worker *rescuer;

        rescuer = alloc_worker(NUMA_NO_NODE);
        if (!rescuer)
            goto err_destroy;

        rescuer->rescue_wq = wq;
        rescuer->task = kthread_create(rescuer_thread, rescuer, "%s",
                           wq->name);
        if (IS_ERR(rescuer->task)) {
            kfree(rescuer);
            goto err_destroy;
        }

        wq->rescuer = rescuer;
        kthread_bind_mask(rescuer->task, cpu_possible_mask);
        wake_up_process(rescuer->task);
    }

    //如果设置了 WQ_SYSFS,将当前 wq 导出到 sysfs 中。
    if ((wq->flags & WQ_SYSFS) && workqueue_sysfs_register(wq))
        goto err_destroy;

    //将当前 wq 添加到全局 wq 链表 workqueues 中。
    list_add_tail_rcu(&wq->list, &workqueues);
}
  • rescue 线程

在上一章结构体的介绍中,经常可以看到 mayday 和 rescue 的身影,从字面意思来看,这是用来"救命"的东西,至于救谁的命呢?

结合 WQ_MEM_RECLAIM 标志以及它的注释可以了解到,当 linux 进行内存回收时,可能导致工作队列运行得没那么顺利,这时候需要有一个线程来保证 work 的持续运行,但是并不建议开发者无理由地添加这个 flag,因为这会实实在在地创建一个内核线程,正如前面的章节所说,过多的内核线程将会影响系统的执行效率。

但是实际上内核中大部分驱动程序的实现都使用了这个标志位。

  • 申请 pwq 并绑定

这一部分由函数 : alloc_and_link_pwqs 完成,

static int alloc_and_link_pwqs(struct workqueue_struct *wq)
{
    bool highpri = wq->flags & WQ_HIGHPRI;
    int cpu, ret;

    if (!(wq->flags & WQ_UNBOUND)) {
        wq->cpu_pwqs = alloc_pe
        rcpu(struct pool_workqueue);
        if (!wq->cpu_pwqs)
            return -ENOMEM;

        for_each_possible_cpu(cpu) {
            struct pool_workqueue *pwq =
                per_cpu_ptr(wq->cpu_pwqs, cpu);
            struct worker_pool *cpu_pools =
                per_cpu(cpu_worker_pools, cpu);

            init_pwq(pwq, wq, &cpu_pools[highpri]);

            mutex_lock(&wq->mutex);
            link_pwq(pwq);
            mutex_unlock(&wq->mutex);
        }
        return 0;
    } else if (wq->flags & __WQ_ORDERED) {
        ret = apply_workqueue_attrs(wq, ordered_wq_attrs[highpri]);
        WARN(!ret && (wq->pwqs.next != &wq->dfl_pwq->pwqs_node ||
                  wq->pwqs.prev != &wq->dfl_pwq->pwqs_node),
             "ordering guarantee broken for workqueue %s\n", wq->name);
        return ret;
    } else {
        return apply_workqueue_attrs(wq, unbound_std_wq_attrs[highpri]);
    }
}

该函数主要包括两个部分:

  • 针对非 unbound 类型的 wq,对每个 CPU 申请一个 pwq 结构,如果设置了 WQ_HIGHPRI(高优先级) 标志位,则将其与 percpu 的高优先级 worker_pool 进行绑定,否则与普通的 worker_pool 进行绑定,worker_pool 的创建在上文中有相应介绍。

  • 初始化和绑定的过程也比较简单,主要是以下几个部分: 设置 pwq->pool 为当前 CPU 的 worker_pool,设置 pwq->wq 为当前被创建的 work_queue_struct. 初始化各种链表头、引用计数置为 1 * 将 pwq->pwqs_node 链接到 wq->pwqs 中,建立索引关系。

  • 针对 unbound 类型的 wq,只需要设置属性即可。

我们继续来看另一个初始化函数:workqueue_init(),我们先看一下这个函数的总体实现,然后再进行局部分析:

int __init workqueue_init(void)
{
    struct workqueue_struct *wq;
    struct worker_pool *pool;
    int cpu, bkt;

    // NUMA 节点的初始化
    wq_numa_init();

    //设置 NUMA node
    for_each_possible_cpu(cpu) {
        for_each_cpu_worker_pool(pool, cpu) {
            pool->node = cpu_to_node(cpu);
        }
    }

    list_for_each_entry(wq, &workqueues, list)
        wq_update_unbound_numa(wq, smp_processor_id(), true);

    //对每个 CPU 的 worker_pool ,创建 worker 线程
    for_each_online_cpu(cpu) {
        for_each_cpu_worker_pool(pool, cpu) {
            pool->flags &= ~POOL_DISASSOCIATED;
            BUG_ON(!create_worker(pool));
        }
    }

    //创建 unbound 线程
    hash_for_each(unbound_pool_hash, bkt, pool, hash_node)
        BUG_ON(!create_worker(pool));

    ...
    return 0;
}
  • 为 worker_pool 创建 worker 线程

通过系统提供的接口:create_worker(pool) 为每一个 worker_pool 构建 worker 并创建内核线程,传入的参数为 worker_pool。

static struct worker *create_worker(struct worker_pool *pool)
{
    struct worker *worker = NULL;
    int id = -1;
    char id_buf[16];

    // id 用作确定内核线程的名称
    id = ida_simple_get(&pool->worker_ida, 0, 0, GFP_KERNEL);

    //申请一个 worker 结构
    worker = alloc_worker(pool->node);
    worker->pool = pool;
    worker->id = id;

    //确定内核线程的名称
    if (pool->cpu >= 0)
        snprintf(id_buf, sizeof(id_buf), "%d:%d%s", pool->cpu, id,
             pool->attrs->nice < 0  ? "H" : "");
    else
        snprintf(id_buf, sizeof(id_buf), "u%d:%d", pool->id, id);

    //创建内核线程
    worker->task = kthread_create_on_node(worker_thread, worker, pool->node,
                          "kworker/%s", id_buf);

    set_user_nice(worker->task, pool->attrs->nice);
    kthread_bind_mask(worker->task, pool->attrs->cpumask);

    worker_attach_to_pool(worker, pool);

    worker->pool->nr_workers++;
    worker_enter_idle(worker);
    //开始执行内核线程
    wake_up_process(worker->task);

    return worker;
}
  • 申请 worker 结构

worker 结构体用于描述 workqueue 相关的内核线程,需要使用时要向系统申请并初始化一个 worker 结构,该操作由 alloc_worker 这个接口完成,参数 node 表示 NUMA node,如果是在 NUMA 系统上,则会在 pool->node 节点上进行申请,然后进行一些成员的初始化。

  • 创建内核线程并绑定

创建内核线程和绑定主要是下面这两个接口:

worker->task = kthread_create_on_node(worker_thread, worker, pool->node,"kworker/%s", id_buf);
worker_attach_to_pool(worker, pool);

使用 kthread_create_on_node 接口创建内核线程以兼容 NUMA 系统,将返回的 task_struct 赋值给 worker,由 worker 对线程进行管理。worker_attach_to_pool 接口会将 worker 绑定到 worker_pool 上,其实就是执行下面这一段代码:

list_add_tail(&worker->node, &pool->workers);

通过 worker->node 节点链接到 worker_pool 的 workers 链表上。

当前 thread 的 thread_func 为 worker_thread ,参数为当前 worker,这是内核线程的执行主体部分,同样的,我们来看看它的源码实现,看看工作线程到底做了哪些事:

static int worker_thread(void *__worker)
{
    struct worker *worker = __worker;
    struct worker_pool *pool = worker->pool;

    worker->task->flags |= PF_WQ_WORKER;
woke_up:
    spin_lock_irq(&pool->lock);
    //在必要的时候删除 worker,退出当前线程。
    if (unlikely(worker->flags & WORKER_DIE)) {
        spin_unlock_irq(&pool->lock);
        WARN_ON_ONCE(!list_empty(&worker->entry));
        worker->task->flags &= ~PF_WQ_WORKER;

        set_task_comm(worker->task, "kworker/dying");
        ida_simple_remove(&pool->worker_ida, worker->id);
        worker_detach_from_pool(worker, pool);
        kfree(worker);
        return 0;
    }

    worker_leave_idle(worker);
recheck:
    //管理 worker 线程
    if (!need_more_worker(pool))
        goto sleep;

    if (unlikely(!may_start_working(pool)) && manage_workers(worker))
        goto recheck;

    //执行 work
    do {
        struct work_struct *work =
            list_first_entry(&pool->worklist,
                     struct work_struct, entry);

        pool->watchdog_ts = jiffies;

        if (likely(!(*work_data_bits(work) & WORK_STRUCT_LINKED))) {
            process_one_work(worker, work);
            if (unlikely(!list_empty(&worker->scheduled)))
                process_scheduled_works(worker);
        } else {
            move_linked_works(work, &worker->scheduled, NULL);
            process_scheduled_works(worker);
        }
    } while (keep_working(pool));

    worker_set_flags(worker, WORKER_PREP);
sleep:
    //处理完成,陷入睡眠
    worker_enter_idle(worker);
    __set_current_state(TASK_IDLE);
    spin_unlock_irq(&pool->lock);
    schedule();
    goto woke_up;
}
  • 管理 worker

从 worker 中获取与其绑定的 worker_pool,在 worker_pool 的基础上对运行在其上的 worker 进行管理,主要分为两个部分:检查删除和检查创建。

检查删除主要是检查 worker->flags & WORKER_DIE 标志位是否被置位,如果该位被置位,将释放当前的 worker ,当前线程返回,回收相应的资源。这个标志位是通过 destroy_worker 函数置位,删除的逻辑就是当前 worker_pool 中有冗余的 worker,长时间进入 idle 状态没有被使用。

检查创建主要由 manage_workers 实现,当 worker_pool 的负载增高时,需要动态地新创建 worker 来执行 work,每个 pool 至少保证有一个 idle worker 以响应即将到来的 work。

worker 的创建和删除完全取决于需要执行 work 数量,而这也是整个 work queue 新框架的核心部分。

  • 执行 work

worker 用于管理线程,而线程自然是用于执行具体的工作,从源码中不难看到,当前 worker 的线程被唤醒后将会从第一个 worker_pool->worklist 中的链表元素,也就是挂入当前 worker_pool 的 work 开始,取出其中的 work,然后执行,执行的接口使用:

move_linked_works(work, &worker->scheduled, NULL);
process_scheduled_works(worker);

move_linked_works 将会在执行前将 work 添加到 worker->scheduled 链表中,该接口和 list_add_tail 不同的是,这个接口会先删除链表中存在的节点并重新添加,保证不会重复添加,且始终添加到最后一个节点。

然后调用 process_scheduled_works 函数正式执行 work,该函数会遍历 worker->scheduled 链表,执行每一个 work,执行之前会做一些必要的检查,比如在同一个 cpu 上,一个 worker 不能在多个 worker 线程中被并发执行(这里的并发执行指的是同时加入到 schedule 链表),是否需要唤醒其它的 worker 来协助执行(碰到 cpu 消耗型的 work 需要这么做),执行 work 的方式就是调用 work->func,。

当执行完 worker_pool->worklist 中所有的 work 之后,当前线程就会陷入睡眠。

3.3 工作队列中添加工作

schedule_work 会将 work 添加到默认的工作队列也就是 system_wq 中,如果需要添加到指定的工作队列,可以调用 queue_work(wq,work) 接口,第一个参数就是指定的 workqueue_struct 结构。

schedule_work 其实就是基于 queue_work 的一层封装:

static inline bool schedule_work(struct work_struct *work)
{
    return queue_work(system_wq, work);
}

接着查看 queue_work 的实现:

static inline bool queue_work(struct workqueue_struct *wq,
                  struct work_struct *work)
{
    return queue_work_on(WORK_CPU_UNBOUND, wq, work);
}

继续调用 queue_work_on ,增加一个参数 WORK_CPU_UNBOUND,这个参数并不是指将当前 work 绑定到 unbound 类型的 worker_pool 中,只是说明调用者并不指定将当前 work 绑定到哪个 cpu 上,由系统来分配 cpu.当然,调用者也可以直接使用 queue_work_on 接口,通过第一个参数来指定当前 work 绑定的 cpu。

bool queue_work_on(int cpu, struct workqueue_struct *wq,
           struct work_struct *work)
{
    bool ret = false;
    unsigned long flags;

    local_irq_save(flags);

    if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(work))) {
        __queue_work(cpu, wq, work);
        ret = true;
    }

    local_irq_restore(flags);
    return ret;
}

如果没有为当前的 work 设置 WORK_STRUCT_PENDING_BIT 标志位,当当前 work 已经被添加到某个工作队列时,该标志位被置位,与 tasklet 和 softirq 的区别在于,softirq 支持多 cpu 上的并发执行,所以要求执行函数可重入,而 tasklet 不允许多 cpu 上的并发执行,编程相对简单。workqueue 机制中,不允许同一个 work 同时被加入到一个或多个工作队列中,只有当 work 正在执行或者已经执行完成,才能重新添加该 work 到工作队列中,所以也不存在多 cpu 并发执行的问题。

static void __queue_work(int cpu, struct workqueue_struct *wq,
             struct work_struct *work)
{
    struct pool_workqueue *pwq;
    struct worker_pool *last_pool;
    struct list_head *worklist;
    unsigned int work_flags;
    unsigned int req_cpu = cpu;

    /*
     * While a work item is PENDING && off queue, a task trying to
     * steal the PENDING will busy-loop waiting for it to either get
     * queued or lose PENDING.  Grabbing PENDING and queueing should
     * happen with IRQ disabled.
     */
    lockdep_assert_irqs_disabled();

    /* if draining, only works from the same workqueue are allowed */
    if (unlikely(wq->flags & __WQ_DRAINING) &&
        WARN_ON_ONCE(!is_chained_work(wq)))
        return;
    rcu_read_lock();
retry:
    /* pwq which will be used unless @work is executing elsewhere */
    if (wq->flags & WQ_UNBOUND) {
        if (req_cpu == WORK_CPU_UNBOUND)
            cpu = wq_select_unbound_cpu(raw_smp_processor_id());
        pwq = unbound_pwq_by_node(wq, cpu_to_node(cpu));
    } else {
        if (req_cpu == WORK_CPU_UNBOUND)
            cpu = raw_smp_processor_id();
        pwq = per_cpu_ptr(wq->cpu_pwqs, cpu);
    }

    /* 检查当前的 work 是不是在这之前被添加到其他 worker_pool 中,
     如果是,就让它继续在原本的 worker_pool 上运行 */
    last_pool = get_work_pool(work);
    if (last_pool && last_pool != pwq->pool) {
        struct worker *worker;

        raw_spin_lock(&last_pool->lock);

        worker = find_worker_executing_work(last_pool, work);

        if (worker && worker->current_pwq->wq == wq) {
            pwq = worker->current_pwq;
        } else {
            /* meh... not running there, queue here */
            raw_spin_unlock(&last_pool->lock);
            raw_spin_lock(&pwq->pool->lock);
        }
    } else {
        raw_spin_lock(&pwq->pool->lock);
    }

    /*
     * pwq is determined and locked.  For unbound pools, we could have
     * raced with pwq release and it could already be dead.  If its
     * refcnt is zero, repeat pwq selection.  Note that pwqs never die
     * without another pwq replacing it in the numa_pwq_tbl or while
     * work items are executing on it, so the retrying is guaranteed to
     * make forward-progress.
     */
    if (unlikely(!pwq->refcnt)) {
        if (wq->flags & WQ_UNBOUND) {
            raw_spin_unlock(&pwq->pool->lock);
            cpu_relax();
            goto retry;
        }
        /* oops */
        WARN_ONCE(true, "workqueue: per-cpu pwq for %s on cpu%d has 0 refcnt",
              wq->name, cpu);
    }

    /* pwq determined, queue */
    trace_workqueue_queue_work(req_cpu, pwq, work);

    if (WARN_ON(!list_empty(&work->entry)))
        goto out;

    pwq->nr_in_flight[pwq->work_color]++;
    work_flags = work_color_to_flags(pwq->work_color);
    //如果超过 pwq 支持的最大的 work 数量,将work添加到 pwq->delayed_works 中,否则就添加到 pwq->pool->worklist 中。  
    if (likely(pwq->nr_active < pwq->max_active)) {
        trace_workqueue_activate_work(work);
        pwq->nr_active++;
        worklist = &pwq->pool->worklist;
        if (list_empty(worklist))
            pwq->pool->watchdog_ts = jiffies;
    } else {
        work_flags |= WORK_STRUCT_DELAYED;
        worklist = &pwq->delayed_works;
    }

    debug_work_activate(work);
    //添加 work 到队列中。
    insert_work(pwq, work, worklist, work_flags);

out:
    raw_spin_unlock(&pwq->pool->lock);
    rcu_read_unlock();
}

insert_work 的源码实现也比较简单:

static void insert_work(struct pool_workqueue *pwq, struct work_struct *work,
            struct list_head *head, unsigned int extra_flags)
{
    struct worker_pool *pool = pwq->pool;
    //设置 work 的 pwq 和 flag。
    set_work_pwq(work, pwq, extra_flags);
    //将 work 添加到 worklist 链表中
    list_add_tail(&work->entry, head);
    //为 pwq 添加引用计数
    get_pwq(pwq);
    //添加内存屏障,防止 cpu 将指令乱序排列
    smp_mb();

    //唤醒 worker 对应的内核线程
    if (__need_more_worker(pool))
        wake_up_worker(pool);
}

这个添加的过程中,pwq 和 workqueue_struct、worker_pool 是如何进行交互的?

答案其实也很简单,获取当前 cpu 的 id,通过该 cpu id 获取当前被使用的 wq 绑定的 pwq,通过 pwq 就可以找到对应的 worker_pool,在这里 pwq 相当于 worker_pool 和 wq 之间的媒介。

等待执行完成

如果需要确定指定的 work 是否执行完成,可以使用内核提供的接口:

bool flush_work(struct work_struct *work)

这个接口在 work 未完成时会被阻塞直到 work 执行完成,返回 true,但是如果指定的 work 进入了 idle 状态,会返回 false。

需要注意的是:一个 work 在执行期间可能会被添加到多个工作队列中,flush_work 将会等待所有 work 执行完成。

针对延迟工作而言,内核接口使用 flush_delayed_work:

bool flush_delayed_work(struct delayed_work *dwork)

同时需要注意的是,在延迟工作对象上调用 flush 将会取消 delayed_work 的延时时间,也就是会将 delayed_work 立即添加到工作队列并调度执行。

取消工作

另一个类似的接口是:

bool cancel_work_sync(struct work_struct *work)

这个接口会取消指定的工作,如果该工作已经在运行,该函数将会阻塞直到它完成,对于其它添加到工作队列的工作,将会取消它们。

  • 12
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值