工作队列workqueue

工作队列

工作队列的基本原理是在进程上下文中将需要推迟执行的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是正在处理的workcurrent_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_workmydemo_private_data中的地址,它的值其实就是我们要计算的偏移量my_workmydemo_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工作队列中。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值