zoukankan      html  css  js  c++  java
  • linux中的等待队列

    最近看epoll 和 select 都涉及到一个东西叫做设备等待队列,等待队列是如何工作的,内核是怎么管理的?看这篇文章

    1. 问题:进程是如何组织起来的?
      我们知道,进程是有很多种状态的:include/linux/sched.h
      #define TASK_RUNNING        0
      #define TASK_INTERRUPTIBLE    1
      #define TASK_UNINTERRUPTIBLE    2
      #define __TASK_STOPPED        4
      #define __TASK_TRACED        8
      /* in tsk->exit_state */
      #define EXIT_ZOMBIE        16
      #define EXIT_DEAD        32
      等等。
      那么,对于不同状态的进程,内核是如何来管理的呢?
      • 就绪队列:状态为TASK_RUNNING的进程组成的列表;
      • 处于TASK_STOPPED、EXIT_ZOMBIE或者EXIT_DEAD状态的进程是不需要连接进特定链表的。因为对于这些状态的进程而言,父进程只会通过 PID或者子进程链表来进行访问。
      • 而处于TASK_INTERRUPTIBLE、TASK_UNINTERRUPTIBLE状态的进程分为很多种类型,其每个进程对应一 种特定事件。在这种情况下,进程的状态信息是不能提供足够的信息去快速的检索所需进程,因此有必要介绍一些其他的链表组织结构。比如等待队列。
    2. 等待队列:
      在内核里面,等待队列是有很多用处的,尤其是在中断处理、进程同步、定时等场合。我们主要讨论其在进程同步中的应用。
      有时候,一个进程可能要等待一些事件的发生,如磁盘操作结束、一些系统资源的释放等等。个人理理解:等待队列就是暂时存放等待某些事件发生的进程的集合。如果一个进程要等待一个事件发生,那么该进程便将自身放入相应的等待队列中进入睡眠,而放弃控制权,直到等待事件发生后才会被内核唤醒。

    3. 等待队列的结构:


      • 等待队列是以双循环链表的形式实现的,而且队列中的成分(等待队列项)包含了指向进程描述符task的指针。
      • 等待队列头:include/linux/wait.h
        每一个等待队列是有一个等待队列头,等待队列头是一个wait_queue_head_t的数据结构:
        struct __wait_queue_head {
        spinlock_t lock;
            struct list_head task_list;
        };
        typedef struct __wait_queue_head wait_queue_head_t;
        成员说明:
        lock:因为等待队列是不允许多个进程同时进行访问的,以防产生不可预料的结果,因此在此结构中定义了"自旋锁"以实现访问间的同步。
        task_list:用于实现双向链表形式。
        struct list_head {
            struct list_head *next, *prev;
        };
      • 等待队列项:include/linux/wait.h
        struct __wait_queue {
            unsigned int flags;
        struct task_struct *task;(2.6.25.5中是void *private;)
            wait_queue_func_t func;
            struct list_head task_list;
        };
        typedef struct __wait_queue wait_queue_t;

        等待队列中的每一个成分:等待队列项,代表着一个正在等待特定事件发生的睡眠进程。
        成员解释:
        task:存放着睡眠进程状态描述符的地址;
        task_list:用于将进程链接进等待相同事件发生的进程链表中(等待队列)。
        flag:

        互斥进程(exclusive processes)和非互斥进程:

        我们来考虑一下,如果等待的事件发生了、变为真的了,那么是不是要唤醒等待该事件的所有进程(某个等待队列中)呢?
           总是唤醒所有等待该事件的进程并不一定是合适的。比如考虑这样一种情况:如果队列中的多个进程等待的资源是要互斥访问的,一定时间内只允许一个进程去访问的话,这时候,只需要唤醒一个进程就可以了,其他进程继续睡眠。如果唤醒所有的进程,最终也只有一个进程获得该资源,其他进程让需返回睡眠。

        因此,等待队列中的睡眠进程可被划分为互斥、非互斥进程。
           互斥进程:等待的资源是互斥访问的;互斥进程由内核有选择的唤醒,等待队列项的flag字段为1;
           非互斥进程:等待的资源是可多进程同时访问的。非互斥进程在事件发生时,总是被内核唤醒,等待队列元素的flag字段为0。

        func:
        指定等待队列中的睡眠进程如何被唤醒。



    4. 等待队列的创建:include/linux/wait.h
      DECLARE_WAITQUEUE()
      init_waitqueue_head()
      可以用DECLARE_WAIT_QUEUE_HEAD(name)宏定义一个新的等待队列,该宏静态地声明和初始化名为name的等待队列头变量。 init_waitqueue_head()函数用于初始化已动态分配的wait queue head变量。
      等待队列可以通过 DECLARE_WAITQUEUE()静态创建,也可以用 init_waitqueue_head()动态创建。进程把自己放入等待队列中并设置成不可执行状态。 例:
      The init_waitqueue_entry(q, p) function initializes a wait_queue_t structure q as follows:

          q->flags = 0;
          q->task = p;
          q->func = default_wake_function;
      非互斥进程p(flags = 0)将被default_wake_function函数唤醒,而default_wake_function唤醒函数是try_to_wake_up( )的包装而已。

      DEFINE_WAIT:
      可以用宏DEFINE_WAIT声明一个新的wait_queue_t变量(等待队列项),并且对其进行初始化:
      #define DEFINE_WAIT(name)                       
          wait_queue_t name = {                       
              .private    = current,               
              .func        = autoremove_wake_function,       
              .task_list    = LIST_HEAD_INIT((name).task_list),   
          }

    5. 等待队列的添加和删除:
      • add_wait_queue( ):kernel/wait.c
        add_wait_queue_exclusive( )
        add_wait_queue()函数把一个非互斥进程插入等待队列链表的第一个位置;

        在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(),并且使用了所机制对该操作进行互斥保护。

        add_wait_queue_exclusive( )函数把一个互斥进程插入等待队列链表的最后一个位置;
      • remove_wait_queue( ):
        remove_wait_queue( )函数从等待队列链表中删除一个进程;
      • waitqueue_active( ):
        waitqueue_active( )函数检查一个给定的等待队列是否为空。

    6. 等待队列的使用:睡眠和唤醒:/kernel/sched.c
      该组函数使用任务管理中公用形式的等待队列。
      希望等待一个特定事件的进程能调用下列函数中的任一个:
      • 睡眠操作:思想是更改当前进程(CURRENT)的任务状态,并要求重新调度,因为这时这个进程的状态已经改变,不再在调度表的就绪队列中,因此无法再获得执行机会,进入"睡眠"状态,直至被"唤醒"(wake_up()),即其任务状态重新被修改回就绪态。
        常用的睡眠操作有interruptible_sleep_on和sleep_on,两个函数类似,是把调用进程加入到特定的等待队列中,只不过前者将进程的状态从就绪态 (TASK_RUNNING)设置为TASK_INTERRUPTIBLE,允许通过发送signal唤醒它(即可中断的睡眠状态);而后者将进程的状态 设置为TASK_UNINTERRUPTIBLE,在这种状态下,不接收任何singal。

        在当前进程上操作的sleep_on()函数:
        void sleep_on(wait_queue_head_t *wq)
            {
                wait_queue_t wait;
                /* 构造当前进程对应的等待队列项 */
        init_waitqueue_entry(&wait, current);  //wait.h中
                /* 将当前进程的状态从TASK_RUNNING改为TASK_UNINTERRUPTIBLE */
                current->state = TASK_UNINTERRUPTIBLE;  
                /* 将等待队列项添加到指定链表中 */  
                wq_write_lock_irqsave(&q->lock,flags);
                __add_wait_queue(q, &wait); 
                wq_write_unlock(&q->lock);

             /* 进程重新调度,放弃执行权 */
                 schedule( );


                /* 本进程被唤醒,重新获得执行权,首要之事是将等待队列项从链表中删除 */
                wq_write_lock_irq(&q->lock);
                 __remove_wait_queue(q, &wait);
                wq_write_unlock_irqrestore(&q->lock,flags);
            /* 至此,等待过程结束,本进程可以正常执行下面的逻辑 */
            }
        该函数把当前进程的状态设置为TASK_UNINTERRUPTIBLE,并把它插入到特定的等待队列。然后,它调用调度程序,而调度程序重新开始另一个进程的执行。当睡眠进程被唤醒时,调度程序重新开始执行sleep_on()函数,把该进程队列中删除。

      • interruptible_sleep_on():
        interruptible_sleep_on()与sleep_on()函数基本上是一样的,但是interruptible_sleep_on()把 当前进程的状态设置为TASK_INTERRUPTIBLE而不是TASK_UNINTERRUPTIBLE,因此,接受一个信号就可以唤醒当前进程
      • sleep_on_timeout()interruptible_sleep_on_timeout()于上述两个函数类似,只是他们还允许调用者定义一个时间间隔使得进程被内核唤醒;但是,在这两个函数中调用的是schedule_timeout()来代替schedule()。
      • prepare_to_wait()、prepare_to_wait_exclusive()、finish_wait():在wait.c中:
        是在linux2.6中介绍的,将当前进程放入等待队列的另一种方式。作用同sleep_on()。


      • 对应的唤醒操作包括wake_up_interruptible和wake_up。wake_up函数不仅可以唤醒状态为 TASK_UNINTERRUPTIBLE的进程,而且可以唤醒状态为TASK_INTERRUPTIBLE的进程。 wake_up_interruptible只负责唤醒状态为TASK_INTERRUPTIBLE的进程。这两个宏的定义如下:
        #define wake_up(x)   __wake_up((x),TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, 1)
        #define wake_up_interruptible(x) __wake_up((x),TASK_INTERRUPTIBLE, 1)
        __wake_up函数主要是获取队列操作的锁,具体工作是调用__wake_up_common完成的。
        void __wake_up(wait_queue_head_t *q, unsigned int mode, int nr)
        {
            if (q) {
                unsigned long flags;
                wq_read_lock_irqsave(&q->lock, flags);
                __wake_up_common(q, mode, nr, 0);
                wq_read_unlock_irqrestore(&q->lock, flags);
            }
        }
        参数q表示要操作的等待队列,mode表示要唤醒任务的状态,如TASK_UNINTERRUPTIBLE或TASK_INTERRUPTIBLE等。nr_exclusive是要唤醒的互斥进程数目,在这之前遇到的非互斥进程将被无条件唤醒。sync表示???
        {
            struct list_head *tmp;
            struct task_struct *p;
            CHECK_MAGIC_WQHEAD(q);
            WQ_CHECK_LIST_HEAD(&q->task_list);
            /* 遍历等待队列 */
            list_for_each(tmp,&q->task_list) {
                unsigned int state;
                /* 获得当前等待队列项 */
                wait_queue_t *curr = list_entry(tmp, wait_queue_t, task_list);
                CHECK_MAGIC(curr->__magic);
                /* 获得对应的进程 */
                p = curr->task;
                state = p->state;
                /* 如果我们需要处理这种状态的进程 */
                if (state & mode) {
                    WQ_NOTE_WAKER(curr);
                    if (try_to_wake_up(p, sync) && (curr->flags&WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
                        break;
                }
            }
        }

        其他的还有wake_up_nr, wake_up_all, wake_up_interruptible_nr, wake_up_interruptible_all, wake_up_interruptible_sync, wake_up_locked. 
            

    参考文章:
    http://hi.baidu.com/abigbigman/blog/item/a0a1fb54f2faa85cd009065f.html
    http://linux.chinaunix.net/techdoc/system/2008/03/08/982296.shtml

  • 相关阅读:
    Elasticsearch Query DSL 整理总结(三)—— Match Phrase Query 和 Match Phrase Prefix Query
    Elasticsearch Query DSL 整理总结(二)—— 要搞懂 Match Query,看这篇就够了
    Elasticsearch Query DSL 整理总结(一)—— Query DSL 概要,MatchAllQuery,全文查询简述
    Elasticsearch Java Rest Client API 整理总结 (三)——Building Queries
    Elasticsearch date 类型详解
    python 历险记(五)— python 中的模块
    python 历险记(四)— python 中常用的 json 操作
    python 历险记(三)— python 的常用文件操作
    Elasticsearch Java Rest Client API 整理总结 (二) —— SearchAPI
    Elasticsearch Java Rest Client API 整理总结 (一)——Document API
  • 原文地址:https://www.cnblogs.com/cherri/p/3721766.html
Copyright © 2011-2022 走看看