linux 等待队列

Posted 古澜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux 等待队列相关的知识,希望对你有一定的参考价值。

一、等待队列

在linux内核中提供了阻塞机制,等待队列(wait queque)。在驱动中使用的也比较多。例如,应用程序去读取设备上的数据时,如果设备还没有准备好数据,可以将这个进程挂起,进入阻塞状态。等到设备准备好数据时才将这个进程唤醒,并且将数据返回给应用程序,继续执行。阻塞进程的实现方法就使用到了等待队列。

二、等待队列的数据结构

1、等待队列头

struct __wait_queue_head {
    /* 自旋锁,主要防止并发访问这个这个等待队列 */
    spinlock_t lock;
    /* 等待队列链表头,这是一个双向链表 */
    struct list_head task_list;
};
/* wait_queue_head_t 等待队列头类型,创建一个等待队列的时,可以使用这个等待队列 */
typedef struct __wait_queue_head wait_queue_head_t;

2、等待队列项

struct __wait_queue {
	unsigned int flags;
#define WQ_FLAG_EXCLUSIVE	0x01
    /* 当前进程的task_sturct对象地址<一个进程由一个task_sturct管理> */
    void *private; 
    /* 用于唤醒这个进程的回调函数, */
    wait_queue_func_t func;
    /* list_head 链表节点 */
    struct list_head task_list;
};

3、等待队列的示意图

队列是由内核链表实现的,在后面查找节点的时候需要做一下转换,如果不了解内核链表的可以百度一下。其实内核链表和企业链表比较像,只不过内核链表的挂钩在下面,企业链表的挂钩在上面。有兴趣的可以去百度一下内核链表、企业链表和普通链表的区别。

二、等待队列的进程睡眠过程

1、睡眠的使用方法

在使用等待队列的时候首先需要定义一个等待队列头;定义等待队列头有两种方式:

  1. 使用 DECLARE_WAIT_QUEUE_HEAD(name) 宏定义,这种方式定义并且初始化好了
  2. 使用 wait_queue_head_t 定义,使用 init_waitqueue_head 函数初始化
    定义完头队列头之后,使用 wait_event* 系列的宏来将进程挂起

2、将进程挂起的函数

宏定义名/方法名 作用 返回值
wait_event(wq, condition) 将进程挂起,直到condition为真才将进程唤醒;使用这种方法将进程挂起,这种方式是不能被打断的(例如接收到ctrl + c的信号也不会被唤醒结束进程)
wait_event_timeout(wq, condition, timeout) 将进程挂起,直到condition为真或者等待超时将进程唤醒,这种方式也是不能被打断的 0:超时返回; >0 被唤醒时,返回剩余的时间
wait_event_interruptible(wq, condition) 将进程挂起,直到condition为真才将进程唤醒;这种方式可以被打断的 -ERESTARTSYS:被信号打断唤醒; 0:condition为真时唤醒
wait_event_interruptible_timeout(wq, condition, timeout) 将进程挂起, 直到condition为真或者等待超时将进程唤醒,这种方式可以被打断的 -ERESTARTSYS:被信号打断唤醒,0:超时结束返回, >0:condition为真被唤醒时,返回剩余的时间

3、实现原理

以比较常用的wait_event_interruptible来分析,源码如下:

/* wait_event_interruptible 是一个带参宏,wq等待队列头,condition唤醒条件 */
#define wait_event_interruptible(wq, condition)				\\
({									\\
    int __ret = 0;
    /* 如果 condition 为0,将进程挂起*/							\\
    if (!(condition))	
        /* __wait_event_interruptible 也是一个带参宏,源码在下面 */					\\
        __wait_event_interruptible(wq, condition, __ret);	\\
    __ret;								\\
})

__wait_event_interruptible

/* wq:等待队列头,condition唤醒条件,ret返回值变量 */
#define __wait_event_interruptible(wq, condition, ret)			\\
do {		                                                        \\
    /* 定义一个等待队列节点__wait,并且初始化 */				
     /*
    #define DEFINE_WAIT(name)						
    wait_queue_t name = {	
        /* 指向当前的task_struct */					
        .private	= current,				
        /* 默认的唤醒方法 */
        .func		= autoremove_wake_function,		
        /* 初始化节点的链表指针 */
        .task_list	= LIST_HEAD_INIT((name).task_list),	
    } 
     */		
    DEFINE_WAIT(__wait);						\\
   
    								\\
    for (;;) {							\\
        /* 将等待队列的节点挂到等待队列头里面,并且将任务的状态设置为TASK_INTERRUPTIBLE,源代在下面 */
        prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE);	\\
        /* condition为真,则被唤醒返回 */
        if (condition)						\\
            break;	
        /* 判断是否接收到信号唤醒 */					\\
        if (!signal_pending(current)) {				\\
            /* 如果没有接收到信号则执行调度,进程在这里进入休眠,等待唤醒后又从这里开始执行 */
            schedule();					\\
            continue;					\\
	}							\\
        /* 如果接收到信号返回ERESTARTSYS */
	ret = -ERESTARTSYS;					\\
	break;							\\
    }								\\
    /* 将进程设置为 TASK_RUNNING,并且将等待队列的节点从链表中删除 */
    finish_wait(&wq, &__wait);					\\
} while (0)

prepare_to_wait函数

void fastcall prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
{
    unsigned long flags;
    
    wait->flags &= ~WQ_FLAG_EXCLUSIVE;
    spin_lock_irqsave(&q->lock, flags);
    /* 判断一下等待队列节点是否被初始化 */
    if (list_empty(&wait->task_list))
        /* 将等待队列节点添加到等待队列链表中 */
        __add_wait_queue(q, wait);
	/*
	 * don\'t alter the task state if this is just going to
	 * queue an async wait queue callback
	 */
    /* 判断wait 和  (wait)->private) 不为空*/
    if (is_sync_wait(wait))
        /* 设置当前任务的任务状态 */
        set_current_state(state);
    spin_unlock_irqrestore(&q->lock, flags);
}

finish_wait函数

/* q    : 等待队列头
   wait : 等待对列节点
 */
void fastcall finish_wait(wait_queue_head_t *q, wait_queue_t *wait)
{
    unsigned long flags;
    /* 将当前的任务状态设置为运行态,等待CPU调度 */
    __set_current_state(TASK_RUNNING);
	
    if (!list_empty_careful(&wait->task_list)) {
        spin_lock_irqsave(&q->lock, flags);
        /* 将等待队列节点从等待队列中删除 */
        list_del_init(&wait->task_list);
        spin_unlock_irqrestore(&q->lock, flags);
    }
}

三、等待队列的进程唤醒过程

等待队列的唤醒的方法也有下面的几种,都是对__wake_up带参宏定义,__wake_up_locked__wake_up_locked 的实现和__wake_up 差不多,都是调用__wake_up_common函数,接下来就分析__wake_up函数吧

/* x是调用的时候传进来的等待队列头 */
/* 唤醒挂起模式为TASK_UNINTERRUPTIBLE和TASK_INTERRUPTIBLE的任务(可中断和不可中断类型)  */
#define wake_up(x)			__wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, 1, NULL)
#define wake_up_nr(x, nr)		__wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, nr, NULL)
#define wake_up_all(x)			__wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, 0, NULL)
#define wake_up_interruptible(x)	__wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
#define wake_up_interruptible_nr(x, nr)	__wake_up(x, TASK_INTERRUPTIBLE, nr, NULL)
#define wake_up_interruptible_all(x)	__wake_up(x, TASK_INTERRUPTIBLE, 0, NULL)
#define	wake_up_locked(x)		__wake_up_locked((x), TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE)
#define wake_up_interruptible_sync(x)   __wake_up_sync((x),TASK_INTERRUPTIBLE, 1)

__wake_up函数

/*
 * q    : 等待队列头
 * mode : 唤醒那种挂起方式的任务
 * nr_exclusive: 唤醒任务的个数,为0唤醒所有的任务
 * key: 传递给唤醒函数的参数,在默认的唤醒函数里面没有使用到
 */
void fastcall __wake_up(wait_queue_head_t *q, unsigned int mode,
			int nr_exclusive, void *key)
{
    unsigned long flags;
    spin_lock_irqsave(&q->lock, flags);
    /* 唤醒任务,在这个函数里面 */
    __wake_up_common(q, mode, nr_exclusive, 0, key);
    spin_unlock_irqrestore(&q->lock, flags);
}

__wake_up_common函数

static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
			     int nr_exclusive, int sync, void *key)
{
    struct list_head *tmp, *next;
    /* 遍历等待队列的所有节点 */
    list_for_each_safe(tmp, next, &q->task_list) {
        /* 找到等待队列的节点的偏移指针,因为内核链表的特点,找到这个节点后都要计算一个偏移量。
        不知道这个的可以百度一下,内核链表和企业链表区别和普通链表的区别 */
        wait_queue_t *curr = list_entry(tmp, wait_queue_t, task_list);
        unsigned flags = curr->flags;
        /* 对于每个等待队列的节点调用唤醒方法,nr_exclusive是唤醒的个数,为0时每一个节点都调用 */
        
        if (curr->func(curr, mode, sync, key) &&
                (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
            break;
    }
}

curr->func在wait_event的时候被定义等待队列节点时候初始化为 autoremove_wake_function 函数:

int autoremove_wake_function(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
    /* 默认的唤醒函数 */
    int ret = default_wake_function(wait, mode, sync, key);
    
    if (ret)
        /* 删除等待队列节点 */
        list_del_init(&wait->task_list);
    return ret;
}

default_wake_function函数

/* 函数也比较简单,之调用了try_to_wake_up函数 */
int default_wake_function(wait_queue_t *curr, unsigned mode, int sync,
			  void *key)
{
    
    return try_to_wake_up(curr->private, mode, sync);
}

try_to_wake_up函数

static int try_to_wake_up(struct task_struct *p, unsigned int state, int sync){
{
	int cpu, this_cpu, success = 0;
	unsigned long flags;
	long old_state;
	struct rq *rq;
#ifdef CONFIG_SMP
	struct sched_domain *sd, *this_sd = NULL;
	unsigned long load, this_load;
	int new_cpu;
#endif
        /* 中间做了比较多的处理,比较复杂,就不分析了 */
	...
        ...
        /* 在这里将任务的状态设置为TASK_RUNNING(可运行转台),等待cpu的调度后,任务就被唤醒了 */
	p->state = TASK_RUNNING;
out:
	task_rq_unlock(rq, &flags);

	return success;
}

四、总结

一、等待队列的使用时需要定义一个等待队列头和一个唤醒条件,然后调用wait_event*的宏来将进程休眠,调用 wake_up* 宏将进程唤醒。在linux中断管理四有使用的例子,这里就不过多啰嗦了

二、wait_event*里面主要做了下面几件事情

  1. 构建一个等待队列的节点,并且初始化
  2. 将等待队列节点链入到等待队列,并且设置任务状态
  3. 调用schedule函数进行任务调度,任务开始休眠

三、wake_up*做了下面几件事情

  1. 遍历等待队列的的节点,节点数可以设定
  2. 调用默认的唤醒函数 default_wake_function
  3. default_wake_function 里面讲任务的任务状态设置为TASK_RUNNING

以上是关于linux 等待队列的主要内容,如果未能解决你的问题,请参考以下文章

linux中的等待队列-51

linux中的等待队列-51

linux中的等待队列-51

linux中的等待队列-51

linux 等待队列

Linux 工作队列和等待队列的区别