linux 工作队列上睡眠的认识--不要在默认共享队列上睡眠

本文探讨了Linux内核中工作队列与睡眠的关系,通过实验发现,在系统共享队列上进行睡眠会导致内核部分功能阻塞,从而造成系统卡死。解决办法是创建自定义队列。

最近项目组做xen底层,我已经被完爆无数遍了,关键在于对内核、驱动这块不熟悉,导致分析xen代码非常吃力。于是准备细细的将 几本 linux 书籍慢慢啃啃。


正好看到LINUX内核设计与实现,对于内核中中断下半段该如何选择?大牛的原话是这样的:“从根本上来说,你有休眠的需要吗?要是有,工作队列就是你的唯一选择,否则最好用tasklet。……”


书中一直强调 工作队列是可以休眠的,而且翻译的人总是强调”工作队列是运行在进程上下文的”, 对于这个翻译,我不是很理解,进程上下文难道就是指用户态而言吗,完全糊涂了,准备自己做个实验。于是我在网上收了下,并自己写了一个工作队列的例子,基本代码如下:


struct work_struct test_task;

void task_handler(void *data)
{
    char c = 'a';
    int i = 0;
   
    while (task_doing == 1)
    {
        c = 'a'+ i%26;
        printk(KERN_ALERT "---%c\n", c);
        
        if (i++ > 50)
        {
            printk(KERN_ALERT "i beyone so quit");
            break;
        }
       
        //msleep(1000);
        wait_event_interruptible(my_dev->test_queue, my_dev->test_task_step !=0);
    }

    printk(KERN_ALERT "quit task task_doing %d\n",task_doing);
       
}
static int
test_ioctl(struct inode *inode, struct file *filp,
		  unsigned int cmd, unsigned long arg)
{
    switch(cmd)
    {
        case IOCTL_INIT_TASK:
            task_doing = 1;
            INIT_WORK(&test_task, task_handler);
            printk(KERN_ALERT "ioctl init task \n");
            break;
         case IOCTL_DO_TASK:
            printk(KERN_ALERT "ioctl do task \n");
            schedule_work(&test_task);
            break;
        default:
            printk(KERN_ALERT "unknown ioctl cmd\n");
            break;
    }
    return 0;
}

用户态测试程序通过 ioctl 命令发送 IOCTL_INIT_TASK 和 IOCTL_DO_TASK 命令。通过书中介绍,INIT_WORK 是初始化一个工作队列,其后调用schedule_work(&test_task) 后,才会执行工作队列上注册的回调函数。


在回调函数中,我进行了睡眠,开始用的是 msleep ,这个函数会放弃CPU到指定的时间,没想到我的内核居然挂住了,再也无法响应。看看驱动设计的代码,很少看到有人用msleep的,可能是自己用了不恰当的函数,于是换成如下代码:

wait_event_interruptible(my_dev->test_queue, my_dev->test_task_step !=0);


重新将虚拟机恢复后,执行同样的测试,还是不行,一运行注册的回调函数,内核就立刻挂起,再也无法操作。


更加无法理解了,说好的工作队列是可以睡眠的,但是我调用睡眠,内核居然就永远无法醒来啦。已经没有机会执行一个动作让 my_dev->test_task_step == 1 了,那么书中所说的 工作队列可以睡眠是什么意思呢 ?


同时看了设备驱动详解中阻塞IO的例子,书中说在 linux 中一个等待队列头可以如下动态创建:

wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);


可是书后面的手动睡眠有有如下的介绍,手动睡眠的过程是这样的:


第一步是创建和初始化一个等待队列. 这常常由这个宏定义完成:

DEFINE_WAIT(my_wait); 

其中, name 是等待队列入口项的名子. 你可用 2 步来做:

wait_queue_t my_wait;
init_wait(&my_wait);

但是常常更容易的做法是放一个 DEFINE_WAIT 行在循环的顶部, 来实现你的睡眠.

下一步是添加你的等待队列入口到队列, 并且设置进程状态. 2 个任务都由这个函数处理:

void prepare_to_wait(wait_queue_head_t *queue, wait_queue_t *wait, int state); 


这里, queue 和 wait 分别地是等待队列头和进程入口. state 是进程的新状态; 它应当或者是 TASK_INTERRUPTIBLE(给可中断的睡眠, 这常常是你所要的)或者 TASK_UNINTERRUPTIBLE(给不可中断睡眠).

在调用 prepare_to_wait 之后, 进程可调用 schedule -- 在它已检查确认它仍然需要等待之后. 一旦 schedule 返回, 就到了清理时间. 这个任务, 也, 被一个特殊的函数处理:

void finish_wait(wait_queue_head_t *queue, wait_queue_t *wait); 


同时,书中还有一个例子:
/* Wait for space for writing; caller must hold device semaphore. On
 * error the semaphore will be released before returning. */
static int scull_getwritespace(struct scull_pipe *dev, struct file *filp)
{

        while (spacefree(dev) == 0)
        { /* full */
                DEFINE_WAIT(wait);

                up(&dev->sem);
                if (filp->f_flags & O_NONBLOCK)
                        return -EAGAIN;

                PDEBUG("\"%s\" writing: going to sleep\n",current->comm);
                prepare_to_wait(&dev->outq, &wait, TASK_INTERRUPTIBLE);
                if (spacefree(dev) == 0)
                        schedule();
                finish_wait(&dev->outq, &wait);
                if (signal_pending(current))

                        return -ERESTARTSYS; /* signal: tell the fs layer to handle it */
                if (down_interruptible(&dev->sem))
                        return -ERESTARTSYS;
        }
        return 0;

}


问题在于,手动睡眠的方式和上面调用 wait_event_interruptible 有什么区别呢 ?从代码上看,手动睡眠有一个等待队列头,而且有一个等待队列单个元素 wait, prepare_to_wait 函数会将 该单个等待元素挂到等待队列头里面去。 一直想搞明白调用 prepare_to_wait 后,会不会进入睡眠 ? 做了一个实验,答案是肯定的,调用prepare_to_wait后,内核立刻进入睡眠状态,只有在其他地方调用 wake_up_interruptible 后才会通知它醒来。。。而且 不必再每次 prepare_to_wait醒来后都调用 finish_wait ,只需要最后调用一次就可以了,因为prepare_to_wait 的内部会做检查,发现该元素不在头链表上时,才会添加该元素到头链表。

在看看 wait_event_interruptible 的代码:
#define __wait_event_interruptible(wq, condition, ret)			\
do {									\
	DEFINE_WAIT(__wait);						\
									\
	for (;;) {							\
		prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE);	\
		if (condition)						\
			break;						\
		if (!signal_pending(current)) {				\
			schedule();					\
			continue;					\
		}							\
		ret = -ERESTARTSYS;					\
		break;							\
	}								\
	finish_wait(&wq, &__wait);					\
} while (0)

原来这个函数对手动睡眠的过程进行了封装,所以调用的时候只用到工作队列(实际就是等待队列)头,它内部自己封装了一个等待元素。。

现在看来,linux 内核设计与实现中,对工作队列可以睡眠的说法是比较模糊的,工作队列上的回调函数是不能睡眠的。工作队列本身就是一种等待队列,队列是可以睡眠的,但是工作队列的上任务回调函数,看来是不能睡眠的。 今天先睡了,后面还要进一步分析看看。


今天在网上查了下相关的东西,有个家伙写得不错:“使用内核提供的共享列队,列队是保持顺序执行的,做完一个工作才做下一个,如果一个工作内有耗时大的处理如阻塞等待信号或锁,那么后面的工作都不会执行。如果你不喜欢排队或不好意思让别人等太久,那么可以创建自己的工作者线程,所有工作可以加入自己创建的工作列队,列队中的工作运行在创建的工作者线程中。”


问题可能就是出在上面了,如果我使用了内核提供的共享队列,可想而知,如果我进入了睡眠或者阻塞,内核中肯定有其他的工作也在这个共享队列上运行,此时便会阻塞内核的某些工作,当然系统就看起来卡死一样了。这样说,如果我创建自己的工作队列,然后在自己的工作队列上挂起,那样就不会出现卡死现象了。做了下试验,果然是这样。


看来,纸上得来总觉浅,深知此事要恭行。linux 内核设计与实现这本书是比较简洁的,作者只告诉我们,利用工作队列甚至可以睡眠,但是他没有强调:“最好不要在系统提供的共享队列上进行睡眠,如果自己的工作是非阻塞的,可以就近利用默认的共享队列。但是如果自己的工作需要睡眠或者阻塞,此时万万不可使用系统提供的默认共享队列,否则会导致内核中一部分关键工作得不到执行,而陷入系统卡死的状态。



这是一个坑,如果不小心处理,会导致系统挂起。

<think>我们正在讨论Linux内核中的工作队列(workqueue)的使用方法。根据引用[1]和[2]的内容,我们可以总结如下:1.工作队列是一种将工作推后执行的机制,它允许重新调度甚至睡眠(因为工作是在内核线程中执行的)。2.使用工作队列的基本步骤包括:初始化工作任务(work),将工作任务挂入工作队列,以及可能需要的取消和刷新操作。具体使用方法:###1.定义并初始化工作任务在Linux内核中,工作任务由`structwork_struct`表示。我们可以使用`INIT_WORK`宏来初始化一个工作任务。```c#include<linux/workqueue.h>structwork_structmy_work;//通常会在模块初始化函数中初始化staticint__initmy_module_init(void){INIT_WORK(&my_work,my_work_handler);//my_work_handler是处理函数return0;}```其中,`my_work_handler`是工作处理函数,其函数签名为:```cvoidmy_work_handler(structwork_struct*work){//处理工作}```###2.调度工作任务初始化后,可以将工作任务调度到工作队列中。有两种方式:-调度到系统默认工作队列(system_wq):使用`schedule_work`或`schedule_work_on`(指定CPU)。-调度到自定义的工作队列:使用`queue_work`或`queue_work_on`(指定CPU)。####使用系统默认工作队列```cschedule_work(&my_work);//调度到默认工作队列schedule_work_on(cpu,&my_work);//指定CPU```####使用自定义工作队列首先需要创建自己的工作队列:```cstructworkqueue_struct*my_wq=alloc_workqueue("my_wq",flags,max_active);```其中,`flags`是创建标志(如`WQ_MEM_RECLAIM`等),`max_active`是最大活动任务数。然后,将工作任务加入自定义工作队列:```cqueue_work(my_wq,&my_work);queue_work_on(cpu,my_wq,&my_work);```###3.取消和刷新工作任务如果需要取消一个已经调度但尚未执行的工作任务,可以使用`cancel_work_sync`(同步取消,会等待任务完成):```ccancel_work_sync(&my_work);```如果要刷新(等待完成)系统默认工作队列中的所有任务,使用:```cflush_scheduled_work();```对于自定义工作队列,使用:```cflush_workqueue(my_wq);```###4.销毁工作队列在模块卸载时,需要销毁自定义工作队列:```cdestroy_workqueue(my_wq);```###注意事项-使用系统默认工作队列时,不需要自己创建和销毁工作队列,但要注意默认工作队列共享的,长时间运行的任务可能会影响系统性能。-对于需要长时间运行或需要特定调度要求的工作,建议创建专用工作队列。###示例代码下面是一个简单的示例,展示如何使用系统默认工作队列:```c#include<linux/init.h>#include<linux/module.h>#include<linux/workqueue.h>staticstructwork_structmy_work;voidmy_work_handler(structwork_struct*work){printk(KERN_INFO"Workhandlerisrunning\n");}staticint__initmy_init(void){INIT_WORK(&my_work,my_work_handler);schedule_work(&my_work);printk(KERN_INFO"Workscheduled\n");return0;}staticvoid__exitmy_exit(void){//确保工作完成flush_scheduled_work();printk(KERN_INFO"Moduleexit\n");}module_init(my_init);module_exit(my_exit);```§§相关问题§§1.工作队列(workqueue)和任务队列(tasklet)有什么区别?2.如何创建工作队列并设置其属性(如并发级别、优先级等)?3.工作队列中的任务是否可以睡眠?为什么?4.在什么情况下应该使用自定义工作队列而不是系统默认工作队列?</think>### Linux内核中工作队列(workqueue)的使用方法 工作队列(workqueue)是Linux内核中用于异步执行任务的机制,特别适合需要睡眠或重新调度的场景[^1]。以下是详细使用步骤: #### 1. **定义工作任务** 使用`struct work_struct`表示工作任务,并通过`INIT_WORK()`宏初始化: ```c #include <linux/workqueue.h> struct work_struct my_work; // 初始化工作任务(静态方式) static DECLARE_WORK(my_work, work_handler); // 或动态初始化(在模块初始化函数中) INIT_WORK(&my_work, work_handler); ``` 其中`work_handler`是任务处理函数: ```c void work_handler(struct work_struct *work) { printk(KERN_INFO "Work executed\n"); // 可在此处执行需要睡眠的操作 } ``` #### 2. **调度工作任务** - **默认工作队列(system_wq)** 使用内核预定义的全局队列: ```c schedule_work(&my_work); // 调度到当前CPU schedule_work_on(cpu_id, &my_work); // 指定目标CPU[^2] ``` - **自定义工作队列** 创建专用队列(适用于高频任务): ```c struct workqueue_struct *my_wq = alloc_workqueue("my_queue", WQ_MEM_RECLAIM, 1); queue_work(my_wq, &my_work); // 提交任务 ``` #### 3. **任务管理** - **取消任务** 同步取消未执行的任务: ```c cancel_work_sync(&my_work); // 等待任务完成[^2] ``` - **刷新队列** 确保所有任务完成: ```c flush_workqueue(my_wq); // 自定义队列 flush_scheduled_work(); // 默认队列[^2] ``` #### 4. **销毁资源** 卸载模块时释放资源: ```c destroy_workqueue(my_wq); // 销毁自定义队列 ``` ### 关键特性 1. **睡眠支持** 工作队列在进程上下文中运行,允许调用`sleep()`等可能阻塞的函数[^1]。 2. **并发控制** 通过`alloc_workqueue()`的`max_active`参数控制并发任务数。 3. **CPU绑定** 使用`schedule_work_on()`或`queue_work_on()`指定目标CPU[^2]。 ### 典型应用场景 - 中断下半部处理(需睡眠时) - 异步文件操作 - 定时任务执行 - 驱动程序的延迟初始化 > **注意**:对实时性要求高的场景应优先选择Tasklet或SoftIRQ,因工作队列涉及线程调度开销。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值