这三个机制在Linux驱动及内核中被广泛使用,可以说任何一个内核程序都会用到;虽然它们的原理可能很复杂,但是作为使用者,我们只关心何时及如何使用它。
等待队列
关键在“等待”二字,它用于使进程(线程)等待某一特定事件的发生(进程在等待期间会睡眠。),在事件发生时由内核唤醒;
在名称上要区分工作队列,在使用场景上要区分完成量;我们很快就会看到它们的区别。
等待队列的使用:
等待队列的使用方式非常简单,我们以NVMe驱动中使用等待队列的方式为例;
1、 首先定义等待队列头:wait_queue_head_t state_wq;
2、然后初始化:init_waitqueue_head(&state_wq);
3、在需要的位置使进程睡眠:
wait_event(state_wq,
nvme_change_ctrl_state(ctrl, NVME_CTRL_RESETTING) | nvme_state_terminal(ctrl));
调用wait_event()的进程会在此处睡眠,此函数有两个参数,第一个是定义的等待队列,第二个被唤醒的条件;
4、在内核其他地方唤醒进程:wake_up_all(&state_wq);
wake_up_all()唤醒所有睡眠的进程;wake_up()唤醒一个睡眠的进程;在唤醒前,要设置wait_event()的第二个参数为true;
对于使用者来说这四步,这四步就完成了等待队列的使用,工作原理会在后面文章介绍。
完成量
完成量跟等待队列的使用场景非常类似,其实完成量就是通过等待队列实现的,我们在后续介绍原理的时候看完成量的定义就一目了然了。
考虑一个场景,如果要等待某件事情,我们知道要用等待队列,但如果我们在开始等待时,事情可能已经发生,这样就永远无法唤醒睡眠的进程了;所以在事情可能已经发生的场景下应该使用完成量。
完成量就在等待队列的基础上增加一个计数,当事件发生时计数累加;当开始等待事件时先判断计数是否>0,如果>0说明事件已经发生,就不再等待。
完成量的使用,仍然以NVMe驱动为例:
1、定义完成量:struct completion disable_done;
2、初始化: init_completion(&disable_done);
3、等待事件: wait_for_completion(&disable_done);(如果事件已经发生过了就不再等待,否则进程睡眠)。
4、当内核其他地方完成事件时: complete(&ns->disable_done);
完成量与等待队列的行为完全一致。
工作队列
在名称上区分于“等待队列”;工作队列用于处理要稍后异步处理的工作,它由内核专门的线程去执行;通常用于中断的“下半部”;
也就是说工作队列是多线程机制;首先创建一个工作队列,然后加入工作项,这些工作项稍后由内核线程去执行;工作项的执行是在另外线程异步进行的,不会阻塞本线程。
工作队列的工作方式:(以NVMe驱动为例)
1、声明一个工作队列:struct workqueue_struct *nvmet_wq;
2、创建工作队列: nvmet_wq = alloc_workqueue("nvmet-wq", WQ_MEM_RECLAIM, 0);
3、真正工作的是工作项:
a) 定义工作项 : struct work_struct async_event_work;
b)工作项初始化:INIT_WORK(&async_event_work, nvme_async_event_work);
4、向工作队列添加工作项:
queue_work(nvme_wq, &async_event_work);它的调用时机由调度器确定;
另一个函数是:queue_work_delayed(),它确保在延期工作执行之前,至少经过dalay指定的一段时间;
有另外几个常用的函数:
flush_work(&async_event_work); //等待工作执行完成
flush_workqueue(wq); //参数是工作队列,等待队列上的所以工作都完成
以上是我们自定义了工作队列,其实系统自带了标准的工作队列,我们只需要将新的工作项添加到该队列就行了,使用如下两个函数:
schedule_work(&ctrl->failfast_work);
or
schedule_delayed_work(&ctrl->failfast_work, time);