Linux中断子系统10

Linux中断子系统10(基于Linux6.6)---中断之workqueue1

一、前情回顾

在许多情况下,需要异步流程执行上下文,而workqueue(wq)API是此类情况最常用的机制。
当需要这样的异步执行上下文时,描述要执行哪个函数的工作项放在队列中。 独立线程用作异步执行上下文。 该队列称为workqueue,该线程称为worker。 虽然工作队列中有工作项(work),但工作人员(worker)依次执行与工作项关联的功能。 当工作队列中没有剩余工作项时,工作人员变得空闲。 当新工作项排队时,工作再次开始执行。

二、为何需要workqueue?

Workqueue 是内核里面很重要的一个机制,特别是内核驱动,一般的小型任务 (work) 都不会自己起一个线程来处理,而是扔到 Workqueue 中处理。Workqueue 的主要工作就是用进程上下文来处理内核中大量的小任务。

所以 Workqueue 的主要设计思想:一个是并行,多个 work 不要相互阻塞;另外一个是节省资源,多个 work 尽量共享资源 ( 进程、调度、内存 ),不要造成系统过多的资源浪费。

2.1、什么是中断上下文和进程上下文?

在 Linux 内核中,中断上下文(Interrupt Context)和进程上下文(Process Context)是两种不同的执行环境,它们在内核中有不同的特性和限制。了解这两者之间的差异,对于理解内核的执行模型非常重要。

1. 中断上下文(Interrupt Context)

中断上下文是指当硬件中断发生时,内核在处理中断时所处的执行环境。在这种上下文中,内核执行的代码是在硬件中断的处理函数中运行的,通常是由中断服务例程(ISR)触发的。

特点:
  • 不可阻塞:中断上下文中,内核代码不能执行任何会阻塞的操作,比如 sleep、等待资源等。因为如果当前中断处理函数阻塞了,系统就无法处理其他中断,导致系统响应迟缓或死锁。
  • 不能调用进程上下文的函数:中断上下文中不能调用那些涉及进程调度、信号处理等的进程上下文函数,比如 schedule()msleep() 等。
  • 不属于任何进程:中断上下文不关联任何进程,因此在中断上下文中无法访问进程的资源(如用户空间内存、进程局部数据等)。
  • 执行时间短:中断上下文应该尽可能地简短,避免阻塞其他中断的处理。长时间占用中断上下文会影响系统的实时性和性能。
  • 共享资源:中断上下文可以访问共享的内核资源,但需要小心同步(如使用锁)以避免数据竞争。
示例:

中断处理函数通常在中断上下文中执行。例如,当网卡接收到数据包时,它会生成一个中断,内核的中断服务例程(ISR)会在中断上下文中处理这个事件。

static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    // 中断上下文中的代码
    // 处理硬件中断,不允许进行阻塞操作
    pr_info("Interrupt handled\n");
    return IRQ_HANDLED;
}

2. 进程上下文(Process Context)

进程上下文是指内核执行代码时,处于一个进程的上下文中,这种情况下代码是由内核线程或者用户空间的进程触发的。换句话说,进程上下文是在处理用户进程或者内核线程请求时,内核为该进程执行的代码。

特点:
  • 可阻塞:与中断上下文不同,进程上下文中的代码可以执行阻塞操作。比如,可以调用 schedule() 来让当前进程主动放弃 CPU,或者调用 sleep() 来让进程进入休眠。
  • 属于进程:进程上下文中执行的代码属于某个进程(或者内核线程),可以访问该进程的资源(如用户空间的内存、进程局部数据等)。
  • 可以进行进程调度:在进程上下文中,内核可以调用进程调度函数(如 schedule()),允许系统根据需要切换到其他进程。
  • 执行时间较长:进程上下文通常用于处理较为复杂的任务,因为它允许阻塞和较长时间的执行。
示例:

当系统调用(如文件读写)或者内核线程执行时,内核会进入进程上下文。例如,在 read() 系统调用中,内核会进入进程上下文来处理文件读取操作。

ssize_t my_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
    // 这是进程上下文中的代码
    // 这里可以进行阻塞操作,处理文件读取
    pr_info("Reading data from file\n");
    return 0;
}

中断上下文和进程上下文的区别

特性中断上下文进程上下文
执行上下文中断服务例程(ISR)系统调用或内核线程等
阻塞不允许阻塞(不能调用 sleep 等函数)可以阻塞(调用 schedulesleep 等)
进程关联性不关联任何进程关联当前进程或内核线程
可以进行调度不允许调度(不调用 schedule() 等)可以调用 schedule() 来进行进程调度
执行时间应尽量短,避免影响中断处理可以较长时间执行,适合复杂任务
锁机制不允许持有进程锁,必须小心同步可以持有进程锁,并进行进程间同步

3. 中断上下文和进程上下文的切换

在 Linux 内核中,中断和进程之间的切换是由硬件中断和内核调度器控制的。每当发生硬件中断时,内核会从当前进程上下文切换到中断上下文,并执行中断处理函数。当中断处理完成后,内核会返回到中断之前的上下文,通常是进程上下文。

  • 中断上下文到进程上下文:中断处理完毕后,内核会调度相应的进程或者内核线程继续执行。如果在中断处理过程中调用了 tasklet 或者 workqueue 等,内核会将这些任务推迟到进程上下文中执行。
  • 进程上下文到中断上下文:在进程上下文执行时,如果发生硬件中断,内核会暂停当前进程的执行,进入中断上下文处理相应的中断,然后返回进程上下文继续执行。

2.2、如何判定当前的context?

代码如何知道自己的上下文呢?结合代码来进一步分析。in_irq()是用来判断是否在hard interrupt context的,in_irq()是如何定义的:include/linux/preempt.h

  
#define in_irq()		(hardirq_count())
 
 
#define hardirq_count()	(preempt_count() & HARDIRQ_MASK)

top half的处理是被irq_enter()和irq_exit()所包围,在irq_enter函数中会调用preempt_count_add(HARDIRQ_OFFSET),为hardirq count的bit field增加1。在irq_exit函数中,会调用preempt_count_sub(HARDIRQ_OFFSET),为hardirq count的bit field减去1。因此,只要in_irq非零,则说明在中断上下文并且处于top half部分。

解决了hard interrupt context,来看software interrupt context。如何判定代码当前正在执行bottom half(softirq、tasklet、timer)呢?in_serving_softirq给出了答案:

include/linux/preempt.h

#define in_serving_softirq()	(softirq_count() & SOFTIRQ_OFFSET)

in_softirq定义了更大的一个区域,不仅仅包括了in_serving_softirq上下文,还包括了disable bottom half的场景。下图描述:

在进程上下文中,由于内核同步的要求可能会禁止softirq。这时候,kernel提供了local_bf_enable和local_bf_disable这样的接口函数,这种场景下,在local_bf_enable函数中会执行软中断handler(在临界区中,虽然raise了softirq,但是由于disable了bottom half,因此无法执行,只有等到enable的时候第一时间执行该softirq handler)。in_softirq包括了进程上下文中disable bottom half的临界区部分,而in_serving_softirq精准的命中了software interrupt context。

内核中还有一个in_interrupt的宏定义,从它的名字上看似乎是定义了hard interrupt context和software interrupt context,到底是怎样的呢?

include/linux/preempt.h

  
#define in_interrupt()		(irq_count())
 
#define irq_count()	(preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK \
				 | NMI_MASK))

include/linux/preempt.h

#define in_nmi()		(preempt_count() & NMI_MASK)
#define in_task()		(!(preempt_count() & \
				   (NMI_MASK | HARDIRQ_MASK | SOFTIRQ_OFFSET)))
 

HARDIRQ_MASK定义了hard interrupt contxt,NMI_MASK定义了NMI(对于ARM是FIQ)类型的hard interrupt context,SOFTIRQ_MASK包括software interrupt context加上禁止softirq情况下的进程上下文。因此,in_interrupt()除了包括了中断上下文的场景,还包括了进程上下文禁止softirq的场景。

还有一个in_atomic的宏定义,如下。include/linux/preempt.h

 /*
 * Are we running in atomic context?  WARNING: this macro cannot
 * always detect atomic context; in particular, it cannot know about
 * held spinlocks in non-preemptible kernels.  Thus it should not be
 * used in the general case to determine whether sleeping is possible.
 * Do not use in_atomic() in driver code.
 */
#define in_atomic()	(preempt_count() != 0)

2.3、为何中断上下文不能sleep?

中断上下文中不能调用 sleep() 等阻塞操作,主要是因为中断上下文和进程上下文的执行环境和目的不同。以下是具体原因:

1. 中断上下文不能被打断

  • 中断上下文的特性:当一个硬件中断发生时,内核会暂停当前的进程或线程,并转到中断服务例程(ISR)处理当前的硬件中断。在中断上下文中,内核代码通常需要迅速完成,以便尽快返回处理其他中断或继续执行当前进程。如果中断上下文中发生了阻塞操作(如调用 sleep()),就会导致当前中断无法及时返回,也无法继续处理其他中断。这样,系统的响应性和实时性会受到严重影响。
  • 不可打断的执行:中断上下文中不允许调用阻塞操作的原因之一是,执行这些操作会导致中断无法“迅速”返回,而是被长时间占用。这种情况在系统要求高实时性和快速响应时(如嵌入式系统、网络数据包处理等)特别不可接受。

2. 中断上下文中的调度不可能进行

  • 调度行为的限制sleep() 和类似的函数通常会将当前进程或线程置于等待状态,并将 CPU 资源让给其他任务。这个过程通常会调用进程调度机制(例如 schedule())。然而,在中断上下文中,不允许进行调度操作,因为调度本身涉及到进程的切换,而这通常会依赖于进程的上下文。如果在中断处理函数中调用 sleep(),会引起对调度的依赖,这会导致系统不能及时响应新的中断请求,或者在处理其他中断时出现延迟。

中断上下文的目标是快速响应硬件事件,并尽量避免影响其他中断的处理。如果允许 sleep() 等阻塞操作,可能会导致调度过程挂起,而这一过程是不可容忍的。

3. 中断上下文与进程上下文的独立性

  • 不属于进程:中断上下文中的代码不属于任何进程,因此无法使用进程上下文的调度机制。sleep() 等函数通常依赖于进程调度,如果中断上下文中调用这些函数,内核无法将其挂起在一个调度队列中。这样会导致内核无法继续有效地调度其他任务,甚至可能发生死锁。

  • 中断与进程调度的分离:内核区分中断上下文和进程上下文,就是为了保证中断处理的效率和及时性。进程上下文中允许进行阻塞操作和调度(例如 sleep()),因为此时内核处于与用户空间进程的上下文中,可以让出 CPU 资源,等待资源的到来或其他条件满足。而中断上下文中的执行时间是非常有限的,不能让出 CPU 资源。

4. 死锁风险

  • 死锁的可能性:如果在中断上下文中允许调用 sleep(),就可能导致死锁的情况。例如,假设一个中断上下文需要获取某个资源(如锁)来完成其工作,但在中断上下文中无法持有锁(锁通常只在进程上下文中使用),如果它被 sleep() 挂起,然后另一个进程请求相同的资源,这时可能会导致死锁,系统无法继续正常运行。

5. 快速响应的要求

  • 中断处理的时间要求:中断上下文的设计要求中断服务函数尽量简短高效,避免进行任何会导致中断延迟的操作。sleep() 等函数会导致处理的时间不确定,因此不允许在中断上下文中调用。

2.4、为何需要workqueue

workqueue和其他的bottom half最大的不同是它是运行在进程上下文中的,它可以睡眠,这和其他bottom half机制有本质的不同,大大方便了中断处理代码。当然,驱动模块也可以自己创建一个kernel thread来解决defering work,但是,如果每个driver都创建自己的kernel thread,那么内核线程数量过多,这会影响整体的性能。因此,最好的方法就是把这些需求汇集起来,提供一个统一的机制,也就是传说中的work queue了。

三、workqueue数据抽象

3.1、workqueue

定义如下:kernel/workqueue.c

/*
 * The externally visible workqueue.  It relays the issued work items to
 * the appropriate worker_pool through its pool_workqueues.
 */
struct workqueue_struct {
	struct list_head	pwqs;		/* WR: all pwqs of this wq */
	struct list_head	list;		/* PR: list of all workqueues */

	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;	/* MD: pwqs requesting rescue */
	struct worker		*rescuer;	/* MD: rescue worker */

	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 __rcu **cpu_pwq; /* I: per-cpu pwqs */
};

static struct kmem_cache *pwq_cache;

workqueue就是一种把某些任务(work)推迟到一个或者一组内核线程中去执行,那个内核线程被称作worker thread(每个processor上有一个work thread)。系统中所有的workqueue会挂入一个全局链表,链表头定义如下:

static LIST_HEAD(workqueues);

list成员就是用来挂入workqueue链表的

singlethread是workqueue的一个特殊模式,一般而言,当创建一个workqueue的时候会为每一个系统内的processor创建一个内核线程,该线程处理本cpu调度的work。但是有些场景中,创建per-cpu的worker thread有些浪费(或者有一些其他特殊的考量),这时候创建single-threaded workqueue是一个更合适的选择。

freezeable成员是一个和电源管理相关的一个flag,当系统suspend的时候,有一个阶段会将所有的用户空间的进程冻结,那么是否也冻结内核线程(包括workqueue)呢?缺省情况下,所有的内核线程都是nofrezable的,当然也可以调用set_freezable让一个内核线程是可以被冻结的。具体是否需要设定该flag是和程序逻辑相关的,具体情况具体分析。

rt用来调整worker_therad线程所在进程的调度策略。

 

3.2、work

定义如下:include/linux/workqueue.h

struct work_struct {
	atomic_long_t data;
	struct list_head entry;
	work_func_t func;
#ifdef CONFIG_LOCKDEP
	struct lockdep_map lockdep_map;
#endif
};

#define WORK_DATA_INIT()	ATOMIC_LONG_INIT((unsigned long)WORK_STRUCT_NO_POOL)
#define WORK_DATA_STATIC_INIT()	\
	ATOMIC_LONG_INIT((unsigned long)(WORK_STRUCT_NO_POOL | WORK_STRUCT_STATIC))

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

所谓work就是异步执行的函数?

如果该函数的代码中有些需要sleep的场景的时候,那么在中断上下文中直接调用将产生严重的问题。这时候,就需要到进程上下文中异步执行。

仔细看看各个成员:func就是这个异步执行的函数,当work被调度执行的时候其实就是调用func这个callback函数,该函数的定义如下:include/linux/workqueue.h

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

work对应的callback函数需要传递该work的struct作为callback函数的参数。work是被组织成队列的,entry成员就是挂入队列的那个节点,data包含了该work的状态flag和挂入workqueue的信息。

四、总结

上文中描述的各个数据结构集合在一起,具体请参考下图:

自上而下来描述各个数据结构。首先,系统中包括若干的workqueue,最著名的workqueue就是系统缺省的的workqueue了,定义如下:

static struct workqueue_struct *keventd_wq __read_mostly;

如果没有特别的性能需求,那么一般驱动使用keventd_wq就可以了,毕竟系统创建太多内核线程也不是什么好事情(消耗太多资源)。当然,如果有需要,驱动模块可以创建自己的workqueue。因此,系统中存在一个workqueues的链表,管理了所有的workqueue实例。一个workqueue对应一组work thread(先不考虑single thread的场景),每个cpu一个,由cpu_workqueue_struct来抽象,这些cpu_workqueue_struct们共享一个workqueue,毕竟这些worker thread是同一种type。

从底层驱动的角度来看,只关心如何处理deferable task(由work_struct抽象)。驱动程序定义了work_struct,其func成员就是deferred work,然后挂入work list就OK了(当然要唤醒worker thread了),系统的调度器调度到worker thread的时候,该work自然会被处理了。当然,挂入哪一个workqueue的那一个worker thread呢?如何选择workqueue是driver自己的事情,可以使用系统缺省的workqueue,简单,实用。当然也可以自己创建一个workqueue,并把work挂入其中。选择哪一个worker thread比较简单:work在哪一个cpu上被调度,那么就挂入哪一个worker thread。

五、接口以及内部实现

5.1、初始化一个work

静态定义一个work,接口如下:include/linux/workqueue.h

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

#define DECLARE_DELAYED_WORK(n, f)					\
	struct delayed_work n = __DELAYED_WORK_INITIALIZER(n, f, 0)

#define DECLARE_DEFERRABLE_WORK(n, f)					\
	struct delayed_work n = __DELAYED_WORK_INITIALIZER(n, f, TIMER_DEFERRABLE)
 

一般而言,work都是推迟到worker thread被调度的时刻,但是有时候,希望在指定的时间过去之后再调度worker thread来处理该work,这种类型的work被称作delayed work,DECLARE_DELAYED_WORK用来初始化delayed work,它的概念和普通work类似,是由内核定时器实现的。include/linux/workqueue.h

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

动态创建也是OK的,不过初始化的时候需要把work的指针传递给INIT_WORK,定义如下:

 include/linux/workqueue.h

#define INIT_WORK(_work, _func)						\
	__INIT_WORK((_work), (_func), 0)

#define INIT_WORK_ONSTACK(_work, _func)					\
	__INIT_WORK((_work), (_func), 1)

#define INIT_WORK_ONSTACK_KEY(_work, _func, _key)			\
	__INIT_WORK_KEY((_work), (_func), 1, _key)

5.2、调度一个work执行

调度work执行有两个接口,一个是schedule_work,将work挂入缺省的系统workqueue(keventd_wq),另外一个是queue_work,可以将work挂入指定的workqueue。具体代码如下:include/linux/workqueue.h

 /**
 * schedule_work - put work task in global workqueue
 * @work: job to be done
 *
 * Returns zero if @work was already on the kernel-global workqueue and
 * non-zero otherwise.
 *
 * This puts a job in the kernel-global workqueue if it was not already
 * queued and leaves it in the same position on the kernel-global
 * workqueue otherwise.
 */
int schedule_work(struct work_struct *work)
{
	return queue_work(keventd_wq, work);
}

include/linux/workqueue.h

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

 
kernel/workqueue.c
 
/**
 * queue_work_on - queue work on specific cpu
 * @cpu: CPU number to execute work on
 * @wq: workqueue to use
 * @work: work to queue
 *
 * Returns 0 if @work was already on a queue, non-zero otherwise.
 *
 * We queue the work to a specific CPU, the caller must ensure it
 * can't go away.
 */
int
queue_work_on(int cpu, struct workqueue_struct *wq, struct work_struct *work)
{
	int ret = 0;
 
	if (!test_and_set_bit(WORK_STRUCT_PENDING, work_data_bits(work))) {
		BUG_ON(!list_empty(&work->entry));
		__queue_work(wq_per_cpu(wq, cpu), work);        //挂入work list并唤醒worker thread 
		ret = 1;
	}
	return ret;
}

处于pending状态的work不会重复挂入workqueue。假设A驱动模块静态定义了一个work,当中断到来并分发给cpu0的时候,中断handler会在cpu0上执行,在handler中会调用schedule_work将该work挂入cpu0的worker thread,也就是keventd 0的work list

在worker thread处理A驱动的work之前,中断很可能再次触发并分发给cpu1执行,这时候,在cpu1上执行的handler在调用schedule_work的时候实际上是没有任何具体的动作的,也就是说该work不会挂入keventd 1的work list,因为该work还pending在keventd 0的work list中。

 

5.3、创建workqueue

接口如下:include/linux/workqueue.h

#define create_workqueue(name)						\
	alloc_workqueue("%s", __WQ_LEGACY | WQ_MEM_RECLAIM, 1, (name))
#define create_freezable_workqueue(name)				\
	alloc_workqueue("%s", __WQ_LEGACY | WQ_FREEZABLE | WQ_UNBOUND |	\
			WQ_MEM_RECLAIM, 1, (name))
#define create_singlethread_workqueue(name)				\
	alloc_ordered_workqueue("%s", __WQ_LEGACY | WQ_MEM_RECLAIM, name)

create_workqueue是创建普通workqueue,也就是每个cpu创建一个worker thread的那种。当然,作为“普通”的workqueue,在freezeable属性上也是跟随缺省的行为,即在suspend的时候不冻结该内核线程的worker thread。create_freezeable_workqueue和create_singlethread_workqueue都是创建single thread workqueue,只不过一个是freezeable的,另外一个是non-freezeable的。的代码如下:kernel/workqueue.c


__printf(1, 4)
struct workqueue_struct *alloc_workqueue(const char *fmt,
					 unsigned int flags,
					 int max_active, ...)
{
	va_list args;
	struct workqueue_struct *wq;
	struct pool_workqueue *pwq;

	/*
	 * Unbound && max_active == 1 used to imply ordered, which is no longer
	 * the case on many machines due to per-pod pools. While
	 * alloc_ordered_workqueue() is the right way to create an ordered
	 * workqueue, keep the previous behavior to avoid subtle breakages.
	 */
	if ((flags & WQ_UNBOUND) && max_active == 1)
		flags |= __WQ_ORDERED;

	/* see the comment above the definition of WQ_POWER_EFFICIENT */
	if ((flags & WQ_POWER_EFFICIENT) && wq_power_efficient)
		flags |= WQ_UNBOUND;

	/* allocate wq and format name */
	wq = kzalloc(sizeof(*wq), GFP_KERNEL);
	if (!wq)
		return NULL;

	if (flags & WQ_UNBOUND) {
		wq->unbound_attrs = alloc_workqueue_attrs();
		if (!wq->unbound_attrs)
			goto err_free_wq;
	}

	va_start(args, max_active);
	vsnprintf(wq->name, sizeof(wq->name), fmt, args);
	va_end(args);

	max_active = max_active ?: WQ_DFL_ACTIVE;
	max_active = wq_clamp_max_active(max_active, flags, wq->name);

	/* init 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);

	wq_init_lockdep(wq);
	INIT_LIST_HEAD(&wq->list);

	if (alloc_and_link_pwqs(wq) < 0)
		goto err_unreg_lockdep;

	if (wq_online && init_rescuer(wq) < 0)
		goto err_destroy;

	if ((wq->flags & WQ_SYSFS) && workqueue_sysfs_register(wq))
		goto err_destroy;

	/*
	 * wq_pool_mutex protects global freeze state and workqueues list.
	 * Grab it, adjust max_active and add the new @wq to workqueues
	 * list.
	 */
	mutex_lock(&wq_pool_mutex);

	mutex_lock(&wq->mutex);
	for_each_pwq(pwq, wq)
		pwq_adjust_max_active(pwq);
	mutex_unlock(&wq->mutex);

	list_add_tail_rcu(&wq->list, &workqueues);

	mutex_unlock(&wq_pool_mutex);

	return wq;

err_unreg_lockdep:
	wq_unregister_lockdep(wq);
	wq_free_lockdep(wq);
err_free_wq:
	free_workqueue_attrs(wq->unbound_attrs);
	kfree(wq);
	return NULL;
err_destroy:
	destroy_workqueue(wq);
	return NULL;
}
EXPORT_SYMBOL_GPL(alloc_workqueue);

解析:

  • flags 的检查和处理

  • if ((flags & WQ_UNBOUND) && max_active == 1)
        flags |= __WQ_ORDERED;
    
    • 这里的判断表示:如果工作队列是无绑定(unbound)并且 max_active 为 1,那么将 flags 加上 __WQ_ORDERED,即将其设置为有序工作队列。过去的行为是 unboundmax_active == 1 会自动成为有序的,但现在有些机器由于 per-pod pools(每个 CPU 核心上的工作队列池)导致不再是这样,所以需要显式地设置。
  • 内存分配和初始化

  • wq = kzalloc(sizeof(*wq), GFP_KERNEL);
    if (!wq)
        return NULL;
    
    • 使用 kzalloc 分配 workqueue_struct 结构体的内存,内存清零。如果分配失败,返回 NULL
  • 处理 unbound 属性

  • if (flags & WQ_UNBOUND) {
        wq->unbound_attrs = alloc_workqueue_attrs();
        if (!wq->unbound_attrs)
            goto err_free_wq;
    }
    
    • 如果工作队列是 unbound 类型,那么就为其分配额外的属性 unbound_attrs。如果分配失败,跳转到错误处理部分,释放已经分配的内存。
  • 格式化工作队列名称

  • va_start(args, max_active);
    vsnprintf(wq->name, sizeof(wq->name), fmt, args);
    va_end(args);
    
    • 使用 vsnprintf 格式化工作队列的名称,并将可变参数写入 wq->name
  • 最大并发工作数调整

  • max_active = max_active ?: WQ_DFL_ACTIVE;
    max_active = wq_clamp_max_active(max_active, flags, wq->name);
    
    • 这里使用了 C 语言中的条件运算符:max_active 为 0 时,使用默认值 WQ_DFL_ACTIVE。然后通过 wq_clamp_max_active 函数调整 max_active,确保它符合工作队列的属性限制。
  • 初始化工作队列结构

  • 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);
    
    • 设置工作队列的标志和最大并发数。
    • 初始化互斥锁 wq->mutex 和原子变量 wq->nr_pwqs_to_flush,用于同步和控制工作队列的状态。
    • 初始化多个双向链表,管理不同的队列和任务,例如:pwqsflusher_queue 等。
  • 初始化锁依赖

  • wq_init_lockdep(wq);
    INIT_LIST_HEAD(&wq->list);
    
    • 初始化锁依赖关系,用于跟踪锁的使用,以避免死锁。
  • 为工作队列分配并链接池(pwqs

  • if (alloc_and_link_pwqs(wq) < 0)
        goto err_unreg_lockdep;
    
    • 调用 alloc_and_link_pwqs 函数分配并连接工作队列池(pwqs),如果失败,跳转到错误处理部分。
  • 初始化工作队列的 "rescuer" 线程

  • if (wq_online && init_rescuer(wq) < 0)
        goto err_destroy;
    
    • 如果工作队列是在线的(wq_online),则初始化工作队列的 "rescuer" 线程,负责抢救失效的工作队列任务。
  • 注册工作队列的 SysFS 接口

  • if ((wq->flags & WQ_SYSFS) && workqueue_sysfs_register(wq))
        goto err_destroy;
    
    • 如果工作队列支持 SysFS 接口,尝试注册该接口。
  • 调整最大并发数并将工作队列添加到全局工作队列列表

mutex_lock(&wq_pool_mutex);
mutex_lock(&wq->mutex);
for_each_pwq(pwq, wq)
    pwq_adjust_max_active(pwq);
mutex_unlock(&wq->mutex);
list_add_tail_rcu(&wq->list, &workqueues);
mutex_unlock(&wq_pool_mutex);
  • 获取全局锁 wq_pool_mutex,然后调整工作队列池中每个池的最大并发数,并将工作队列添加到全局的工作队列链表中。

在 Linux 6.x 版本中,工作队列机制进行了改进,主要的变化包括:

  • 内核线程管理的变化:原来使用 create_workqueue_thread() 创建的工作线程,现在已经被新的线程管理机制所取代。新的工作队列机制使用 workqueues 结构来管理任务,而不再直接依赖于手动创建和管理工作线程。内核会根据负载和需要自动管理工作线程。

  • 工作队列的改进:在 Linux 6.x 中,工作队列的实现细节发生了一些变化,工作队列的线程不再是显式创建的,而是通过内核的工作队列框架动态地管理。这意味着,开发者在使用工作队列时,不再需要手动管理工作线程的生命周期。内核会自动为工作队列分配和调度内核线程。

  • 内存管理和资源优化:内核对于工作队列的管理进行了性能优化,包括对线程池的更好管理以及对内存资源的优化。在 Linux 6.x 中,工作队列的处理更高效,支持更高并发性和更低的开销。

5.4、work执行的时机

work执行的时机是和调度器相关的,当系统调度到worker thread这个内核线程后,该thread就会开始工作。每个cpu上执行的worker thread的内核线程的代码逻辑都是一样的,在worker_thread中实现:

而worker_thread也则是workqueue_init中实现创建的。kernel/workqueue.c

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

	wq_cpu_intensive_thresh_init();

	mutex_lock(&wq_pool_mutex);

	/*
	 * Per-cpu pools created earlier could be missing node hint. Fix them
	 * up. Also, create a rescuer for workqueues that requested it.
	 */
	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) {
		WARN(init_rescuer(wq),
		     "workqueue: failed to create early rescuer for %s",
		     wq->name);
	}

	mutex_unlock(&wq_pool_mutex);

	/* create the initial workers */
	for_each_online_cpu(cpu) {
		for_each_cpu_worker_pool(pool, cpu) {
			pool->flags &= ~POOL_DISASSOCIATED;
			BUG_ON(!create_worker(pool));
		}
	}

	hash_for_each(unbound_pool_hash, bkt, pool, hash_node)
		BUG_ON(!create_worker(pool));

	wq_online = true;
	wq_watchdog_init();
}

kernel/workqueue.c

/**
 * create_worker - create a new workqueue worker
 * @pool: pool the new worker will belong to
 *
 * Create and start a new worker which is attached to @pool.
 *
 * CONTEXT:
 * Might sleep.  Does GFP_KERNEL allocations.
 *
 * Return:
 * Pointer to the newly created worker.
 */
static struct worker *create_worker(struct worker_pool *pool)
{
	struct worker *worker;
	int id;
	char id_buf[23];

	/* ID is needed to determine kthread name */
	id = ida_alloc(&pool->worker_ida, GFP_KERNEL);
	if (id < 0) {
		pr_err_once("workqueue: Failed to allocate a worker ID: %pe\n",
			    ERR_PTR(id));
		return NULL;
	}

	worker = alloc_worker(pool->node);
	if (!worker) {
		pr_err_once("workqueue: Failed to allocate a worker\n");
		goto fail;
	}

	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);
	if (IS_ERR(worker->task)) {
		if (PTR_ERR(worker->task) == -EINTR) {
			pr_err("workqueue: Interrupted when creating a worker thread \"kworker/%s\"\n",
			       id_buf);
		} else {
			pr_err_once("workqueue: Failed to create a worker thread: %pe",
				    worker->task);
		}
		goto fail;
	}

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

	/* successful, attach the worker to the pool */
	worker_attach_to_pool(worker, pool);

	/* start the newly created worker */
	raw_spin_lock_irq(&pool->lock);

	worker->pool->nr_workers++;
	worker_enter_idle(worker);
	kick_pool(pool);

	/*
	 * @worker is waiting on a completion in kthread() and will trigger hung
	 * check if not woken up soon. As kick_pool() might not have waken it
	 * up, wake it up explicitly once more.
	 */
	wake_up_process(worker->task);

	raw_spin_unlock_irq(&pool->lock);

	return worker;

fail:
	ida_free(&pool->worker_ida, id);
	kfree(worker);
	return NULL;
}
static int worker_thread(void *__worker)
{
	struct worker *worker = __worker;
	struct worker_pool *pool = worker->pool;

	/* tell the scheduler that this is a workqueue worker */
	set_pf_worker(true);
woke_up:
	raw_spin_lock_irq(&pool->lock);

	/* am I supposed to die? */
	if (unlikely(worker->flags & WORKER_DIE)) {
		raw_spin_unlock_irq(&pool->lock);
		set_pf_worker(false);

		set_task_comm(worker->task, "kworker/dying");
		ida_free(&pool->worker_ida, worker->id);
		worker_detach_from_pool(worker);
		WARN_ON_ONCE(!list_empty(&worker->entry));
		kfree(worker);
		return 0;
	}

	worker_leave_idle(worker);
recheck:
	/* no more worker necessary? */
	if (!need_more_worker(pool))
		goto sleep;

	/* do we need to manage? */
	if (unlikely(!may_start_working(pool)) && manage_workers(worker))
		goto recheck;

	/*
	 * ->scheduled list can only be filled while a worker is
	 * preparing to process a work or actually processing it.
	 * Make sure nobody diddled with it while I was sleeping.
	 */
	WARN_ON_ONCE(!list_empty(&worker->scheduled));

	/*
	 * Finish PREP stage.  We're guaranteed to have at least one idle
	 * worker or that someone else has already assumed the manager
	 * role.  This is where @worker starts participating in concurrency
	 * management if applicable and concurrency management is restored
	 * after being rebound.  See rebind_workers() for details.
	 */
	worker_clr_flags(worker, WORKER_PREP | WORKER_REBOUND);

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

		if (assign_work(work, worker, NULL))
			process_scheduled_works(worker);
	} while (keep_working(pool));

	worker_set_flags(worker, WORKER_PREP);
sleep:
	/*
	 * pool->lock is held and there's no work to process and no need to
	 * manage, sleep.  Workers are woken up only while holding
	 * pool->lock or from local cpu, so setting the current state
	 * before releasing pool->lock is enough to prevent losing any
	 * event.
	 */
	worker_enter_idle(worker);
	__set_current_state(TASK_IDLE);
	raw_spin_unlock_irq(&pool->lock);
	schedule();
	goto woke_up;
}

 导致worker thread进入sleep状态有三个条件:(a)电源管理模块没有请求冻结该worker thread。(b)该thread没有被其他模块请求停掉。(c)work list为空,也就是说没有work要处理。

下面看一下任务是怎么被执行的:

asmlinkage __visible void __sched schedule(void)
{
	struct task_struct *tsk = current;

#ifdef CONFIG_RT_MUTEXES
	lockdep_assert(!tsk->sched_rt_mutex);
#endif

	if (!task_is_running(tsk))
		sched_submit_work(tsk);
	__schedule_loop(SM_NONE);
	sched_update_worker(tsk);
}
EXPORT_SYMBOL(schedule);

schedule() 负责实现任务调度。它的主要功能包括:

  1. 上下文切换(Context Switching):决定当前任务是否需要被切换,以及应该切换到哪个任务。
  2. 任务调度(Task Scheduling):根据任务的优先级、状态等信息,选择下一个要执行的任务。
  3. 任务提交(Task Submission):如果任务不在运行,可能会将任务提交给调度器,等待调度。
  4. 状态更新:更新当前任务或工作线程的状态,确保调度器能够正确跟踪任务。

这里说一下,带延时功能的等待队列,其实原理很简单。include/linux/workqueue.h

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

就是在普通的work_struct 上增加了一个内核定时器,先注册内核定时器,等定时器时间到,定时任务触发后,再在定时器任务里面添加work到keventd_wq工作队列中。

注:延时工作队列,只能加入到keventd_wq的工作队列之中。

include/linux/workqueue.h

 /**
 * schedule_delayed_work - put work task in global workqueue after delay
 * @dwork: job to be done
 * @delay: number of jiffies to wait or 0 for immediate execution
 *
 * After waiting for a given time this puts a job in the kernel-global
 * workqueue.
 */
int schedule_delayed_work(struct delayed_work *dwork,
					unsigned long delay)
{
	return queue_delayed_work(keventd_wq, dwork, delay);    //延时任务挂到keventd_wq任务
}
 
/**
 * queue_delayed_work - queue work on a workqueue after delay
 * @wq: workqueue to use
 * @dwork: delayable work to queue
 * @delay: number of jiffies to wait before queueing
 *
 * Equivalent to queue_delayed_work_on() but tries to use the local CPU.
 */
static inline bool queue_delayed_work(struct workqueue_struct *wq,
				      struct delayed_work *dwork,
				      unsigned long delay)
{
	return queue_delayed_work_on(WORK_CPU_UNBOUND, wq, dwork, delay);
}

 
 kernel/workqueue.c
bool queue_delayed_work_on(int cpu, struct workqueue_struct *wq,
			   struct delayed_work *dwork, unsigned long delay)
{
	struct work_struct *work = &dwork->work;
	bool ret = false;
	unsigned long flags;

	/* read the comment in __queue_work() */
	local_irq_save(flags);

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

	local_irq_restore(flags);
	return ret;
}
EXPORT_SYMBOL(queue_delayed_work_on);
 
static void __queue_delayed_work(int cpu, struct workqueue_struct *wq,
				struct delayed_work *dwork, unsigned long delay)
{
	struct timer_list *timer = &dwork->timer;
	struct work_struct *work = &dwork->work;

	WARN_ON_ONCE(!wq);
	WARN_ON_ONCE(timer->function != delayed_work_timer_fn);
	WARN_ON_ONCE(timer_pending(timer));
	WARN_ON_ONCE(!list_empty(&work->entry));

	/*
	 * If @delay is 0, queue @dwork->work immediately.  This is for
	 * both optimization and correctness.  The earliest @timer can
	 * expire is on the closest next tick and delayed_work users depend
	 * on that there's no such delay when @delay is 0.
	 */
	if (!delay) {
		__queue_work(cpu, wq, &dwork->work);
		return;
	}

	dwork->wq = wq;
	dwork->cpu = cpu;
	timer->expires = jiffies + delay;

	if (unlikely(cpu != WORK_CPU_UNBOUND))
		add_timer_on(timer, cpu);
	else
		add_timer(timer);
}
 
 
//在定时器任务中,执行work挂入,cpu_workqueue_struct的worklist链表中
void delayed_work_timer_fn(struct timer_list *t)
{
	struct delayed_work *dwork = from_timer(dwork, t, timer);

	/* should have been called from irqsafe timer with irq already off */
	__queue_work(dwork->cpu, dwork->wq, &dwork->work);
}
EXPORT_SYMBOL(delayed_work_timer_fn);

 

当然也可以销毁创建的工作队列,使用下面函数:include/linux/workqueue.h

extern void destroy_workqueue(struct workqueue_struct *wq);

六、举例

下面是一个简单的示例,展示了如何使用 Linux 工作队列来异步执行任务:

示例:使用工作队列

  1. 定义一个工作队列
  2. 创建一个工作项(work item)
  3. 初始化并排队工作项
  4. 定义工作函数来处理任务
  5. 清理工作项和工作队列

代码示例

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/workqueue.h>
#include <linux/delay.h>

// 定义一个工作队列
static struct workqueue_struct *my_wq;

// 定义一个工作项结构体
static struct work_struct my_work;

// 工作函数:执行工作队列中的任务
static void my_work_function(struct work_struct *work)
{
    pr_info("工作队列任务正在执行...\n");
    msleep(1000);  // 模拟一个延迟操作
    pr_info("工作队列任务执行完成\n");
}

// 模块初始化函数
static int __init workqueue_example_init(void)
{
    pr_info("模块初始化,创建工作队列...\n");

    // 创建工作队列
    my_wq = create_workqueue("my_workqueue");
    if (!my_wq) {
        pr_err("创建工作队列失败\n");
        return -ENOMEM;
    }

    // 初始化工作项
    INIT_WORK(&my_work, my_work_function);

    // 将工作项加入工作队列
    queue_work(my_wq, &my_work);

    return 0;
}

// 模块清理函数
static void __exit workqueue_example_exit(void)
{
    pr_info("模块退出,销毁工作队列...\n");

    // 等待工作队列中的所有任务完成
    flush_workqueue(my_wq);

    // 销毁工作队列
    destroy_workqueue(my_wq);
}

module_init(workqueue_example_init);
module_exit(workqueue_example_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("一个简单的工作队列示例");

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值