工作队列
工作队列的基本原理是在进程上下文中将需要推迟执行的work添加到工作队列中交给内核线程,内核线程会调用这个work的回调函数来完成,所以说工作队列需要重新调度和睡眠,而上次分析的tasklet也是处理中断下文的机制,但是它是不存在睡眠状态的,所以我们可以这样说tasklet用来处理花费时间相对多的事情,工作队列处理相对比较复杂且更加花费时间的事。
工作线程池
用来管理多个工作线程,目前工作线程池的类型有两种,普通优先级BOUND,高优先级UNBOUND,线程池中的线程数量是动态分配的,当工作线程睡眠时候是否需要唤醒更多的工作线程,有需要的话会唤醒同一个工作线程池中处于空闲的工作线程。
相关数据结构
work_struct–工作
struct work_struct {
atomic_long_t data;//
struct list_head entry;//将work挂载到其他队列上
work_func_t func;//work的处理函数
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
work_struct
结构体表示一个工作,主要处理工作的函数为func指针。
worker–工作者线程
struct worker {
union {
struct list_head entry; /* 在空闲状态下在空闲列表中 */
struct hlist_node hentry; /* 在繁忙状态下在繁忙哈希表中 */
};
struct work_struct *current_work; /* 当前正在处理的工作项 */
work_func_t current_func; /* current_work 的执行函数 */
struct pool_workqueue *current_pwq; /* current_work 的所属工作队列 */
struct list_head scheduled; /* 已调度的工作项列表 */
struct task_struct *task; /* 工作线程的任务结构 */
struct worker_pool *pool; /* 关联的工作池 */
struct list_head node; /* pool->workers 上 */
unsigned long last_active; /* 上一次活跃的时间戳 */
unsigned int flags; /* 标志位 */
int id; /* 工作线程的 ID */
char desc[WORKER_DESC_LEN]; /* 用于调试和打印的描述信息 */
struct workqueue_struct *rescue_wq; /* 用于救援的工作队列指针 */
work_func_t last_func; /* 用于记录工作线程的上一个任务函数 */
};
work
运行于内核线程中需要工作者线程处理工作队列的各个工作,用worker
结构体描述,其中current_work
是正在处理的work
,current_func
是当前正在处理的work回调函数,pool是将所有被调度且正在准备执行的work
挂到该链表中,node
将该工作线程挂载到worker_pool->workers
链表上。
workqueue_struct–工作队列
struct workqueue_struct {
struct list_head pwqs; //pool-workqueue数据结构挂载到该链表上
struct list_head list; //链表节点
struct mutex mutex; /* 保护该工作队列的互斥锁 */
int work_color;
int flush_color; /* 当前正在进行的刷新操作的颜色 */
atomic_t nr_pwqs_to_flush; /* 正在进行刷新操作的池工作队列数量 */
struct wq_flusher *first_flusher; /* 第一个刷新操作器 */
struct list_head flusher_queue; /* 等待进行刷新操作的池工作队列链表 */
struct list_head flusher_overflow; /* 刷新操作溢出列表 */
struct list_head maydays; //rescuer工作线程,内存不够创建新的工作队列会失败,创建工作队列设置标志位为WQ_MEM_RECLAIM那么由rescuer处理
struct worker *rescuer;
int nr_drainers; /* 正在进行排空操作的池工作队列数量 */
int saved_max_active; /* 保存的池工作队列最大活跃度 */
struct workqueue_attrs *unbound_attrs; /* 仅用于无绑定工作队列 */
struct pool_workqueue *dfl_pwq; /* 仅用于无绑定工作队列 */
#ifdef CONFIG_SYSFS
struct wq_device *wq_dev; /* 用于sysfs接口 */
#endif
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
char name[WQ_NAME_LEN]; /* 工作队列的名称 */
/*
* workqueue_struct的销毁受sched-RCU保护,以允许在不抓取wq_pool_mutex的情况下遍历workqueues列表。
* 这用于从sysrq中转储所有workqueues。
*/
struct rcu_head rcu;
/* 热字段,在发出命令时使用,对齐到缓存行 */
unsigned int flags ____cacheline_aligned; /* 标志位 */
struct pool_workqueue __percpu *cpu_pwqs; /* 每个CPU的池工作队列 */
struct pool_workqueue __rcu *numa_pwq_tbl[]; /* 由节点索引的无绑定池工作队列表 */
};
多个工作组成工作队列,工作队列共享一组工作线程池,当一个work挂载到工作队列中,需要通过工作线程池中的工作线程来处理其回调函数。
pool_workqueue–关联workqueue、worker_pool
struct pool_workqueue {
struct worker_pool *pool; /* 关联的工作者池 */
struct workqueue_struct *wq; /* 拥有该池工作队列的工作队列 */
int work_color;
int flush_color;
int refcnt; /* 引用计数 */
int nr_in_flight[WORK_NR_COLORS];
/* 进行中的工作项数量 */
int nr_active; /* 活跃的工作项数量 */
int max_active; /* 最大活跃的工作项数量 */
struct list_head delayed_works; /* 延迟工作项链表 */
struct list_head pwqs_node; /* 在wq->pwqs上的节点 */
struct list_head mayday_node; /* 在wq->maydays上的节点 */
/*
* 未绑定pwq的释放延迟到system_wq。请参阅put_pwq()和pwq_unbound_release_workfn()以了解详细信息。
* pool_workqueue本身也受sched-RCU保护,以便可以在不抓取wq->mutex的情况下确定第一个pwq。
*/
struct work_struct unbound_release_work;
struct rcu_head rcu;
} __aligned(1 << WORK_STRUCT_FLAG_BITS);
关系图
pool_workqueue
充当桥梁的作用将工作集合和工作线程集合进行关联。
如何调度一个work?
内核提供处理的宏
#define __INIT_WORK(_work, _func, _onstack) \
do { \
__init_work((_work), _onstack); \
(_work)->data = (atomic_long_t) WORK_DATA_INIT(); \
INIT_LIST_HEAD(&(_work)->entry); \
(_work)->func = (_func); \
} while (0)
#endif
#define INIT_WORK(_work, _func) \
__INIT_WORK((_work), (_func), 0)
此时data成员被划分成高位域和低位域,高位域存放work上次执行的worker_pool
的id,低位域存放与work相关的标志位。
work标志位如下
enum {
WORK_STRUCT_PENDING_BIT = 0, /* work item is pending execution */
WORK_STRUCT_DELAYED_BIT = 1, /* work item is delayed */
WORK_STRUCT_PWQ_BIT = 2, /* data points to pwq */
WORK_STRUCT_LINKED_BIT = 3, /* next work is linked to this one */
#ifdef CONFIG_DEBUG_OBJECTS_WORK
WORK_STRUCT_STATIC_BIT = 4, /* static initializer (debugobjects) */
WORK_STRUCT_COLOR_SHIFT = 5, /* color for workqueue flushing */
#else
WORK_STRUCT_COLOR_SHIFT = 4, /* color for workqueue flushing */
#endif
WORK_STRUCT_COLOR_BITS = 4,
WORK_STRUCT_PENDING = 1 << WORK_STRUCT_PENDING_BIT,
WORK_STRUCT_DELAYED = 1 << WORK_STRUCT_DELAYED_BIT,
WORK_STRUCT_PWQ = 1 << WORK_STRUCT_PWQ_BIT,
WORK_STRUCT_LINKED = 1 << WORK_STRUCT_LINKED_BIT,
#ifdef CONFIG_DEBUG_OBJECTS_WORK
WORK_STRUCT_STATIC = 1 << WORK_STRUCT_STATIC_BIT,
#else
WORK_STRUCT_STATIC = 0,
#endif
/*
* The last color is no color used for works which don't
* participate in workqueue flushing.
*/
WORK_NR_COLORS = (1 << WORK_STRUCT_COLOR_BITS) - 1,
WORK_NO_COLOR = WORK_NR_COLORS,
/* not bound to any CPU, prefer the local CPU */
WORK_CPU_UNBOUND = NR_CPUS,
/*
* Reserve 7 bits off of pwq pointer w/ debugobjects turned off.
* This makes pwqs aligned to 256 bytes and allows 15 workqueue
* flush colors.
*/
WORK_STRUCT_FLAG_BITS = WORK_STRUCT_COLOR_SHIFT +
WORK_STRUCT_COLOR_BITS,
/* data contains off-queue information when !WORK_STRUCT_PWQ */
WORK_OFFQ_FLAG_BASE = WORK_STRUCT_COLOR_SHIFT,
__WORK_OFFQ_CANCELING = WORK_OFFQ_FLAG_BASE,
WORK_OFFQ_CANCELING = (1 << __WORK_OFFQ_CANCELING),
/*
* When a work item is off queue, its high bits point to the last
* pool it was on. Cap at 31 bits and use the highest number to
* indicate that no pool is associated.
*/
WORK_OFFQ_FLAG_BITS = 1,
WORK_OFFQ_POOL_SHIFT = WORK_OFFQ_FLAG_BASE + WORK_OFFQ_FLAG_BITS,
WORK_OFFQ_LEFT = BITS_PER_LONG - WORK_OFFQ_POOL_SHIFT,
WORK_OFFQ_POOL_BITS = WORK_OFFQ_LEFT <= 31 ? WORK_OFFQ_LEFT : 31,
WORK_OFFQ_POOL_NONE = (1LU << WORK_OFFQ_POOL_BITS) - 1,
/* convenience constants */
WORK_STRUCT_FLAG_MASK = (1UL << WORK_STRUCT_FLAG_BITS) - 1,
WORK_STRUCT_WQ_DATA_MASK = ~WORK_STRUCT_FLAG_MASK,
WORK_STRUCT_NO_POOL = (unsigned long)WORK_OFFQ_POOL_NONE << WORK_OFFQ_POOL_SHIFT,
/* bit mask for work_busy() return values */
WORK_BUSY_PENDING = 1 << 0,
WORK_BUSY_RUNNING = 1 << 1,
/* maximum string length for set_worker_desc() */
WORKER_DESC_LEN = 24,
};
static inline bool schedule_work(struct work_struct *work)
{
return queue_work(system_wq, work);
}
初始化完成work调用schedule_work 会将 work 添加到默认的BOUND类型工作队列也就是 system_wq
中,如果需要添加到指定的工作队列,可以调用 queue_work
接口,第一个参数就是指定的 workqueue_struct 结构。
static inline bool queue_work(struct workqueue_struct *wq,
struct work_struct *work)
{
return queue_work_on(WORK_CPU_UNBOUND, wq, 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 已经被添加到某个工作队列时,该标志位被置位,在工作队列机制中并不允许同一个 work 同时被加入到一个或多个工作队列中,只有当 work 正在执行或者已经执行完成,才能重新添加该 work 到工作队列中,所以不存在 cpu 并发执行。而softirq 支持多 cpu 的并发执行, tasklet不允许多 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;
// 确保在中断被禁用的情况下执行该函数
lockdep_assert_irqs_disabled();
debug_work_activate(work);
// 销毁工作队列不允许过程中由work加入队列
if (unlikely(wq->flags & __WQ_DRAINING) &&
WARN_ON_ONCE(!is_chained_work(wq)))
return;
retry:
if (req_cpu == WORK_CPU_UNBOUND)
cpu = wq_select_unbound_cpu(raw_smp_processor_id());
// 根据工作队列的属性选择要使用的工作者池
if (!(wq->flags & WQ_UNBOUND))
pwq = per_cpu_ptr(wq->cpu_pwqs, cpu);//本地cpu的pool_workqueue
else
pwq = unbound_pwq_by_node(wq, cpu_to_node(cpu));//本地节点对应的
last_pool = get_work_pool(work);//查询work上一次位于哪个work_pool中运行
if (last_pool && last_pool != pwq->pool) {
struct worker *worker;
spin_lock(&last_pool->lock);
worker = find_worker_executing_work(last_pool, work);//work是否正在某个worker_pool上执行
if (worker && worker->current_pwq->wq == wq) {
pwq = worker->current_pwq;
} else {
// 如果工作项不在另一个工作者池中运行,将其在当前工作者池中排队
spin_unlock(&last_pool->lock);
spin_lock(&pwq->pool->lock);
}
} else {
spin_lock(&pwq->pool->lock);
}
// 工作者池已确定并锁定。对于非绑定的工作者池,可能会与工作者池的释放发生竞争,因此可能已经被释放。如果引用计数为零,则重新选择工作者池
if (unlikely(!pwq->refcnt)) {
if (wq->flags & WQ_UNBOUND) {
spin_unlock(&pwq->pool->lock);
cpu_relax();
goto retry;
}
// 出现错误
WARN_ONCE(true, "workqueue: per-cpu pwq for %s on cpu%d has 0 refcnt",
wq->name, cpu);
}
// 将工作项插入工作者池的工作队列中
trace_workqueue_queue_work(req_cpu, pwq, work);
if (WARN_ON(!list_empty(&work->entry))) {
spin_unlock(&pwq->pool->lock);
return;
}
// 更新工作者池中的计数和标志
pwq->nr_in_flight[pwq->work_color]++;
work_flags = work_color_to_flags(pwq->work_color);
//活跃的work数,大于则加入等待链表worker_pool->worklist否则加入delayed_works
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;
}
insert_work(pwq, work, worklist, work_flags);//添加work到work_pool工作队列中
spin_unlock(&pwq->pool->lock);
}
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);
}
通过获取当前cpu的id,通过id获取当前被使用的工作队列所绑定的pool_workqueue,通过中间介质找到worker_pool。
等待work是否执行完成调用flush_work。
bool flush_work(struct work_struct *work)
{
return __flush_work(work, false);
}
work此时如果并未完成那么将会被阻塞直到work执行完成,如果此时一个work被添加到多个工作队列中,同样需要等待所有的work均已执行完成,返回true,如果进入空闲状态则返回false,存在延迟的work则调用flush_delayed_work。
1 bool flush_delayed_work(struct delayed_work *dwork)
2 {
3 local_irq_disable(); // 禁用本地中断,确保代码执行的原子性和一致性
4 if (del_timer_sync(&dwork->timer)) // 删除定时器并等待其完成
5 __queue_work(dwork->cpu, dwork->wq, &dwork->work); // 将工作项重新添加到工作队列中
6 local_irq_enable(); // 启用本地中断
7 return flush_work(&dwork->work); // 刷新工作项队列,返回刷新状态
8 }
bool cancel_work_sync(struct work_struct *work)
{
return __cancel_work_timer(work, false);
}
或者可以执行取消操作,调用cancel_work_sync
直接取消当前指定的work,如果当前work正在处于运行状态,那么该函数同样会阻塞直到该work执行完成,与此同时将会取消当前工作队列中加入的work。
实践
要求:写一个简单的内核模块,初始化一个工作队列,在write()函数里调用该工作队列回调函数,在回调函数中输出用户程序写入的字符串。并写一个测试程序测试该功能。
内核态代码:
在上次tasklet机制上修改代码。
//私有数据结构体
struct mydemo_private_data {
struct mydemo_device *device;
char name[64];
// struct tasklet_struct tasklet;//tasklet队列
struct work_struct my_work;//工作队列
};
//打开设备文件
static int demodrv_open(struct inode *inode, struct file *file)
{
unsigned int minor = iminor(inode);
struct mydemo_private_data *data;
struct mydemo_device *device = mydemo_device[minor];//从指针数组中获取对应的设备信息
dev_info(device->dev, "%s: major=%d, minor=%d, device=%s\n", __func__,
MAJOR(inode->i_rdev), MINOR(inode->i_rdev), device->name);
// 申请一个 mydemo_private_data 结构体类型的空间,用于存储打开设备的私有数据
data = kmalloc(sizeof(struct mydemo_private_data), GFP_KERNEL);
if (!data)
return -ENOMEM;
sprintf(data->name, "private_data_%d", minor);
// tasklet_init(&data->tasklet, do_tasklet, (unsigned long)device);
INIT_WORK(&data->my_work, do_work);//初始化工作队列
//参数:将软中断对象的指针地址存储在设备数据结构体 、软中断被触发后执行的回调函数,将设备指针转换为无符号长整型,并将其作为参数传递给处理函数 do_tasklet()
//将设备对象或设备数据的指针存储在文件的私有数据字段
data->device = device;
file->private_data = data;
return 0;
}
static ssize_t
demodrv_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
struct mydemo_private_data *data = file->private_data;
struct mydemo_device *device = data->device;
unsigned int actual_write;
int ret;
if (kfifo_is_full(&device->mydemo_fifo)){
if (file->f_flags & O_NONBLOCK)
return -EAGAIN;
dev_info(device->dev, "%s:%s pid=%d, going to sleep\n", __func__, device->name, current->pid);
ret = wait_event_interruptible(device->write_queue,
!kfifo_is_full(&device->mydemo_fifo));
if (ret)
return ret;
}
mutex_lock(&device->lock);
ret = kfifo_from_user(&device->mydemo_fifo, buf, count, &actual_write);
if (ret)
return -EIO;
//选在在FIFO缓冲区有空间可写的时候进行触发
schedule_work(&data->my_work);//触发
mutex_unlock(&device->lock);
if (!kfifo_is_empty(&device->mydemo_fifo)) {
wake_up_interruptible(&device->read_queue);
kill_fasync(&device->fasync, SIGIO, POLL_IN);
printk("%s kill fasync\n", __func__);
}
dev_info(device->dev, "%s:%s pid=%d, actual_write =%d, ppos=%lld, ret=%d\n", __func__,
device->name, current->pid, actual_write, *ppos, ret);
return actual_write;
}
触发的回调函数为my_work
,编写my_work函数实现简单的打印。
static void do_work(struct work_struct *work)//回调函数
{
struct mydemo_private_data *data;
struct mydemo_device *device;
data = container_of(work, struct mydemo_private_data, my_work);
device = data->device;获取设备指针
dev_info(device->dev, "%s: trigger a work\n", __func__);
}
container_of宏计算出结构体的在内存中的起始地址并返回。
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
type:结构体类型
member:结构体内的成员
ptr:结构体内成员 member 的地址
拿出该结构体mydemo_private_data
的成员my_work
的地址,减去my_work
在结构体中的偏移计算得到mydemo_private_data
的首地址,最终返回计算得到的值。
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
偏移量的计算将0强制转换成一个指向TYPE结构体的指针,通过指针访问成员并获取成员的地址,那就是my_work
在mydemo_private_data
中的地址,它的值其实就是我们要计算的偏移量my_work
在mydemo_private_data
中的偏移。
编译一下
make编译并安装内核模块终端会看到当前已经成功注册了一个字符设备。
当前字符设备的主设备号是247,ls -l /dev/下查看设备信息并未发现存在主设备号247的设备手动mknod。
mknod /dev/mydemo0 c 247 1
测试一下
可以看到已经成功运行并打开了编写的字符设备,其主设备号为247,次设备号为1与我们手动创建的一致。
echo命令写入字符串到创建的/dev/mydemo0设备中,发现已经触发了write并写入字符串,触发workqueue的回调函数打印do_work: trigger a work,调用read函数读取到的字符串正是我们写入的。
总结
workqueue工作流程
(1)workqueue由一个或多个worker线程池组成,每个worker线程都会不断地从workqueue中获取需要执行的工作项。
(2)如果此时有延迟的work,将多个work组装成工作项添加到workqueue工作队列中。
(3)工作者线程worker在空闲状态下可以从workqueue工作队列中取出一个work放在自己的私有队列等待执行。
(4)当工作者线程worker完成当前的工作项,将会处理自己私有队列的下一个工作项。
(5)如若此时workqueue已经不存在可用的工作项了,此时工作者线程会处于等待状态,等待新task加入到workqueue工作队列中。