linux内核等待队列机制编程框架—内核进程随时随地唤醒休眠

概念

等待队列机制:让用户进程在内核空间随时随地休眠,随时随地被唤醒的一种机制
场景:一个进程能够获取按键的操作状态(按下或者松开)的需求

分析:进程调用 read 系统调用函数来获取按键的操作状态(按下或者松开)由于用户操作按键(按下或者松开)本身就是一个随机过程(开心了按两下,不开心不操作),read 读进程为了能够及时获取到用户的按键操作,首先想到采用轮询方式(死等,while(1)),但是这种方式会大量消耗 CPU 资源,大大降低了 CPU 的利用率(CPU 永远只做一件事),于是乎想到轮训的死对头中断机制也就是说进程调用 read 系统调用函数来获取按键的操作状态,最终进程调用到底层驱动的 read 接口,如果进程在驱动的 read 接口中发现按键无操作(既没有按下也没有松开),干脆让 read进程在驱动的read 接口中进行休眠等待,等待着按键有操作,一旦read 进程在驱动的 read 接口中进行休眠等待,read 进程会释放掉占用的 CPU 资源给其他进程使用(中断不需要给,它会直接抢占),一旦将来用户对按键进行按下或者松开操作,势必产生按键中断,表示按键有操作,此时只需在按键的中断处理函数中去唤醒休眠的 read 进程,一旦 read 进程被唤醒,read 进程再去读取按键的操作状态即可.返回到用户空间,此时此刻,CPU 至少做 2 件事(一个是执行 read 进程,另一个是其他进程),大大提高了 CPU 的利用率。

:在这过程中,如何让 read 进程随时随地休眠,并且中断到来让 read 进程随时随低被唤醒呢?
:同样利用等待队列机制。等待队列诞生的根本原因:外设的处理速度远远慢于 CPU,所以外设没有准备好数据的时候操作外设的进程就需要进行休眠等待。

编程框架

在这里插入图片描述

相关数据结构和函数和宏

1. 定义并初始化一个等待队列头

  • wait_queue_head_t
    定义在include\linux\wait.h
struct __wait_queue_head {
	spinlock_t		lock;
	struct list_head	task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;

功能:用来定义一个等待队列头

  • init_waitqueue_head()
    定义在include\linux\wait.h
#define init_waitqueue_head(q)				\
	do {						\
		static struct lock_class_key __key;	\
							\
		__init_waitqueue_head((q), #q, &__key);	\
	} while (0)

功能:用来初始化等待队列头

2. 定义装载要休眠进程的容器,且把当前要休眠的进程添加到容器中

  • wait_queue_t
    定义在include\linux\wait.h
typedef struct __wait_queue wait_queue_t;
struct __wait_queue {
	unsigned int		flags;
	void			*private;
	wait_queue_func_t	func;
	struct list_head	task_list;
};

功能:用于定义装载休眠进程的容器

  • void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p)
    定义在include\linux\wait.h
static inline void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p)
{
	q->flags	= 0;
	q->private	= p;
	q->func		= default_wake_function;
}

功能:将当前要休眠的进程添加到容器 wait 中,
注意:一个要休眠的进程对应一个容器 q,一般应用是init_waitqueue_entry(&wait, current); 在这里,current是一个内核全局指针变量,对应的数据类型为:struct task_struct,

  • task_struct
    定义在include\linux\sched.h
    此数据结构用来描述进程的信息,只要 fork 一个进程,内核就会用此数据结构定义初始化一个对象来描述 fork 出来的进程信息,current 指针永远指向当前进程,current在等待队列中是移动的
struct task_struct {
	volatile long state;	/* 描述进程的状态-1 unrunnable, 0 runnable, >0 stopped */
	.................

	/* Revert to default priority/policy when forking */
	unsigned sched_reset_on_fork:1;
	unsigned sched_contributes_to_load:1;

	unsigned long atomic_flags; /* Flags needing atomic access. */

	pid_t pid;  /*进程的pid号*/
	pid_t tgid;
	................
	/*保存进程的名称*/
	char comm[TASK_COMM_LEN]; /* executable name excluding path
				     - access with [gs]et_task_comm (which lock
				       it with task_lock())
				     - initialized normally by setup_new_exec */

	............................
};

对应的 struct task_struct 对象 “当前进程” :正在获取 CPU 资源投入运行的进程,调试信息:打印当前进程的 PID 和名称:
printk(“当前进程[%s][%d]\n”, current->comm, current->pid);

3. 将当前进程添加到等待队列中去

  • void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
    声明在include\linux\wait.h中,定义在kernel\sched\wait.c中
    说明
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
{
	unsigned long flags;

	wait->flags &= ~WQ_FLAG_EXCLUSIVE;
	spin_lock_irqsave(&q->lock, flags);
	__add_wait_queue(q, wait);
	spin_unlock_irqrestore(&q->lock, flags);
}
EXPORT_SYMBOL(add_wait_queue);

如果是使用add_wait_queue()函数将当前进程添加到等待队列中去,那么在remove_wait_queue()阶段的时候,写进程唤醒多个读进程的时是倒序的,即最先添加的进程最后被唤醒。见下操作示例

/tmp/vaccine # ps
  85 0          0:00 -/bin/sh
  252 0          0:01 [kworker/0:1]
  265 0          0:00 [kworker/u:1]
  343 0          0:00 ./btn_test r
  344 0          0:00 ./btn_test r
  345 0          0:00 ./btn_test r
  346 0          0:00 ./btn_test r
  347 0          0:00 [flush-0:11]
  348 0          0:00 ps
/tmp/vaccine # ./btn_test w
[ 2244.352000] 写进程[btn_test][352]唤醒读进程
[ 2244.353000] 读进程[btn_test][346]由于驱动主动引起唤醒
[ 2244.355000] 读进程[btn_test][345]由于驱动主动引起唤醒
[ 2244.357000] 读进程[btn_test][344]由于驱动主动引起唤醒
[ 2244.359000] 读进程[btn_test][343]由于驱动主动引起唤醒
[4]+  Done                       ./btn_test r
[3]+  Done                       ./btn_test r
[2]+  Done                       ./btn_test r
[1]+  Done                       ./btn_test r
/tmp/vaccine # 

===========================

  • void add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *wait)
    声明在include\linux\wait.h中,定义在kernel\sched\wait.c中
    说明
void add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *wait)
{
	unsigned long flags;

	wait->flags |= WQ_FLAG_EXCLUSIVE;
	spin_lock_irqsave(&q->lock, flags);
	__add_wait_queue_tail(q, wait);
	spin_unlock_irqrestore(&q->lock, flags);
}
EXPORT_SYMBOL(add_wait_queue_exclusive);

而如果是使用add_wait_queue_exclusive()函数,那么在remove_wait_queue()阶段的时候,写进程唤醒多个读进程的时是顺序的,即最先添加的进程最先被唤醒。但却是一次只能唤醒一个进程。见下操作示例

373 0          0:00 [kworker/0:2]
  383 0          0:00 ./btn_test r
  384 0          0:00 ./btn_test r
  385 0          0:00 ./btn_test r
  387 0          0:00 ./btn_test r
  388 0          0:00 ps
/tmp/vaccine # ./btn_test w
[ 2464.602000] 写进程[btn_test][390]唤醒读进程
[ 2464.603000] 读进程[btn_test][383]由于驱动主动引起唤醒
[1]   Done                       ./btn_test r
/tmp/vaccine # ./btn_test w
[ 2476.678000] 写进程[btn_test][392]唤醒读进程
[ 2476.678000] 读进程[btn_test][384]由于驱动主动引起唤醒
[2]   Done                       ./btn_test r
/tmp/vaccine # ./btn_test w
[ 2479.154000] 写进程[btn_test][393]唤醒读进程
[ 2479.154000] 读进程[btn_test][385]由于驱动主动引起唤醒
[3]-  Done                       ./btn_test r
/tmp/vaccine # ./btn_test w
[ 2480.735000] 写进程[btn_test][394]唤醒读进程
[ 2480.735000] 读进程[btn_test][387]由于驱动主动引起唤醒
[4]+  Done                       ./btn_test r

=========================================

4. 设置要休眠的当前进程的休眠状态

  • set_current_state();
    定义在include\linux\sched.h
#define set_current_state(state_value)		\
	set_mb(current->state, (state_value))
#define set_mb(var, value)	do { var = value; smp_mb(); } while (0)

#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
这里有两种休眠类型。

  • 一种是可中断的休眠类型(TASK_INTERRUPTIBLE),休眠期间可以立即处理接收的信号 此类休眠进程被唤醒的方法有两种
  1. 接收信号引起唤醒
  2. 驱动主动唤醒
    set_current_state(TASK_INTERRUPTIBLE);
  • 另一种是不可中断的休眠类型(TASK_UNINTERRUPTIBLE),休眠期间如果接收到了信号,不会立即处理,而是在被唤醒以后会处理信号。此类休眠进程被唤醒的方法只有一种:驱动主动唤醒。
    set_current_state(TASK_UNINTERRUPTIBLE);

5. schedule();

  • void schedule(void);
    声明在include\linux\sched.h
asmlinkage void schedule(void);

定义在kernel\sched\core.c

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

	sched_submit_work(tsk);
	__schedule();
}
EXPORT_SYMBOL(schedule);

asmlinkage 的作用参见这篇博文—asmlinkage简要理解

注意:不能单独调用此函数,否则要休眠的当前进程会默认放到内核的默认等待队列中,将来如果要唤醒,做不到随时随地了
当执行到该函数时,当前进程正式进入休眠状态,此时此刻当前进程也会释放所占用的 CPU 资源给其他进程,此时此刻代码停止不前 等待被唤醒,一旦被唤醒,代码继续往下执行。

6. 一旦休眠的进程被唤醒,设置休眠的进程状态为运行态

set_current_state(TASK_RUNNING);

7. 将唤醒的休眠进程从等待队列中移除

  • void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
    声明在include\linux\wait.h
extern void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);

定义在kernel\sched\wait.c

void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
{
	unsigned long flags;

	spin_lock_irqsave(&q->lock, flags);
	__remove_wait_queue(q, wait);
	spin_unlock_irqrestore(&q->lock, flags);
}
EXPORT_SYMBOL(remove_wait_queue);
static inline void
__remove_wait_queue(wait_queue_head_t *head, wait_queue_t *old)
{
	list_del(&old->task_list);
}

8. 一般最好建议判断一下进程被唤醒的原因

static inline int signal_pending(struct task_struct *p)
{
	return unlikely(test_tsk_thread_flag(p,TIF_SIGPENDING));
}
if(signal_pending(current)) {
printk("进程由于接收到了信号引起的唤醒!\n");
return -ERESTARTSYS;
} else {
printk("进程由于驱动主动引起唤醒!\n");
//接下里被唤醒的进程就可以访问硬件
//说明硬件数据准备就绪了
}

9. 驱动主动唤醒休眠进程的方法

  • wake_up(&wq); //唤醒 wq 等待队列中所有的休眠进程
#define wake_up(x)			__wake_up(x, TASK_NORMAL, 1, NULL)
  • wake_up_interruptible(&wq); //唤醒 wq 等待队列中所有的休眠类型为可中断的进程
#define wake_up_interruptible(x)	__wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)

应用实例

写进程来唤醒读进程

驱动程序

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/gpio.h>
#include <mach/platform.h>
#include <linux/uaccess.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/sched.h> //TASK_INTERRUPTIBLE等

//定义一个等待队列头对象(造鸡妈妈)
static wait_queue_head_t rwq;
static ssize_t btn_read(struct file *file,
                        char __user *buf,
                        size_t count,
                        loff_t *ppos)
{
    //1.定义初始化装载休眠进程的容器(构造小鸡)
    //将当前进程添加到容器中
    //一个进程一个容器
    wait_queue_t wait;
    init_waitqueue_entry(&wait, current);

    //2.将当前进程添加到等待队列中去
    add_wait_queue(&rwq, &wait);

    //3.设置当前进程休眠的状态类型为可中断
    set_current_state(TASK_INTERRUPTIBLE);

    //4.当前进程正式进入休眠状态
    //此时代码停止不前
    //当前进程释放CPU资源
    //一旦被唤醒,进程继续往下执行
    printk("读进程[%s][%d]将进入休眠状态...\n",
                    current->comm, current->pid);
    schedule();

    //5.一旦被唤醒,设置进程的状态为运行
    set_current_state(TASK_RUNNING);
    
    //6.将被唤醒的进程从队列中移除
    remove_wait_queue(&rwq, &wait);

    //7.判断进程被唤醒的原因
    if(signal_pending(current)) {
        printk("读进程[%s][%d]由于接收到了信号引起唤醒\n",
                            current->comm, current->pid);
        return -ERESTARTSYS;
    } else {
        printk("读进程[%s][%d]由于驱动主动引起唤醒.\n",
                            current->comm, current->pid);
    }
    return count;
}

static ssize_t btn_write(struct file *file,
                        const char __user *buf,
                        size_t count,
                        loff_t *ppos)
{
    //1.唤醒休眠的读进程
    printk("写进程[%s][%d]唤醒读进程\n",
                        current->comm, current->pid);
    wake_up(&rwq);
    return count;
}

//定义初始化硬件操作接口对象
static struct file_operations btn_fops = {
    .owner = THIS_MODULE,
    .read = btn_read,
    .write = btn_write
};

//定义初始化混杂设备对象
static struct miscdevice btn_misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "mybtn",
    .fops = &btn_fops
};

static int btn_init(void)
{
    //注册混杂设备对象
    misc_register(&btn_misc);
    //初始化等待队列头(武装鸡妈妈)
    init_waitqueue_head(&rwq);
    return 0;
}

static void btn_exit(void)
{
    //卸载混杂设备对象
    misc_deregister(&btn_misc);
}
module_init(btn_init);
module_exit(btn_exit);
MODULE_LICENSE("GPL");

应用程序

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
    int fd;

    fd = open("/dev/mybtn", O_RDWR);
    if (fd < 0) 
        return -1;

    if(argc != 2) {
        printf("用法:%s <r|w>\n", argv[0]);
        return -1;
    }
    if(!strcmp(argv[1], "r"))
        read(fd, NULL, 0); //启动一个读进程
    else if(!strcmp(argv[1], "w"))
        write(fd, NULL, 0); //启动一个写进程

    close(fd);
    return 0;
}

操作实例

insmod btn_drv.ko
./btn_test r &  //启动读进程A,A利用等待队列在驱动中休眠

然后使用ps 命令查看进程,如下

  73 0          0:00 [mmcqd/0]
   74 0          0:00 [mmcqd/0boot0]
   75 0          0:00 [mmcqd/0boot1]
   76 0          0:00 [kworker/u:2]
   77 0          0:00 [krfcommd]
   78 0          0:00 [deferwq]
   85 0          0:00 -/bin/sh
  129 0          0:01 [kworker/0:2]
  169 0          0:00 [flush-0:11]
  224 0          0:00 ./btn_test r
  225 0          0:00 ps

然后使用top命令查看进程的状态如下

129     2 0        SW       0  0.0   0  0.4 [kworker/0:2]
  231    85 0        R     3024  0.3   0  0.2 top
   85     1 0        S     3024  0.3   0  0.0 -/bin/sh
    1     0 0        S     3020  0.3   0  0.0 {linuxrc} init
  224    85 0        S     1676  0.2   0  0.0 ./btn_test r
    2     0 0        SW       0  0.0   0  0.0 [kthreadd]
    3     2 0        SW       0  0.0   0  0.0 [ksoftirqd/0]
    4     2 0        SW       0  0.0   0  0.0 [kworker/0:0]
    5     2 0        SW       0  0.0   0  0.0 [kworker/u:0]
    6     2 0        SW       0  0.0   0  0.0 [migration/0]
    7     2 0        SW       0  0.0   0  0.0 [watchdog/0]
    8     2 0        SW<      0  0.0   0  0.0 [khelper]
    9     2 0        SW       0  0.0   0  0.0 [kdevtmpfs]
   10     2 0        SW       0  0.0   0  0.0 [sync_supers]
   11     2 0        SW       0  0.0   0  0.0 [bdi-default]
   12     2 0        SW<      0  0.0   0  0.0 [kblockd]
   13     2 0        SW       0  0.0   0  0.0 [ion_noncontig_h]
   14     2 0        SW       0  0.0   0  0.0 [khubd]
   15     2 0        SW       0  0.0   0  0.0 [irq/270-axp_mfd]
   17     2 0        SW<      0  0.0   0  0.0 [cfg80211]

发现上述PID为224的进程的状态为S,S代表TASK_INTERRUPTIBLE,是可中断睡眠状态

如果是执行 kill 244

/tmp/vaccine # [ 1770.165000] 读进程[btn_test][295]接收到信号引起唤醒

[1]+  Terminated                 ./btn_test r

如果是执行 ./btn_test w

[ 1238.459000] 写进程[btn_test][238]唤醒读进程
[ 1238.459000] 读进程[btn_test][224]由于驱动主动引起唤醒
[1]+  Done                       ./btn_test r
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值