Linux——poll实现

0、等待队列

   在 Linux内核 中等待队列有很多用途,可用于中断处理、进程同步及定时。等待队列实现了在事件上的条件等待希望等待特定事件的进程把自己放进合适的等待队列,并放弃控制权。因此,等待队列表示一组睡眠的进程,当某一条件为真时,由内核唤醒它们。

  等待队列由循环链表实现,由等待队列头(wait_queue_head_t)和等待队列项(wait_queue)组成,其元素(等待队列项)包含指向进程描述符的指针。linux/include/wait.h)。

等待队列头结构体的定义:

struct __wait_queue_head {

  spinlock_t  lock;          

  struct list_head task_list;  

}; 

typedef struct __wait_queue_head  wait_queue_head_t;

1.通过DECLARE_WAIT_QUEUE_HEAD定义一个wait_queue_head等待队列头,这是静态定义的方法。该宏会定义一个 wait_queue_head_t,并且初始化结构中的以及等待队列。

2.Linux中等待队列的实现思想如下图所示,当一个任务需要在某个wait_queue_head_t上睡眠时,将自己的进程控制块PCB信息封装到wait_queue中,然后挂载到wait_queue的链表中,执行调度睡眠。当某些事件发生后,另一个任务(进程)会唤醒wait_queue_head_t上的某个或者所有任务,唤醒工作也就是将等待队列中的任务设置为可调度的状态,并且从队列中删除。

 


3.等待队列中存放的是在执行设备操作时不能获得资源而挂起的进程:

定义等待对列:

struct __wait_queue{

  unsigned int flags;  

  #define WQ_FLAG_EXCLUSIVE  0x01  

  void * private;          //通常指向当前任务控制块PCB

  wait_queue_func_t func;  //唤醒阻塞任务的函数 ,决定了唤醒的方式

  struct list_head task_list;    

};

typedef struct __wait_queue  wait_queue_t;

-----------------------------------------------

1、poll 内核实现实现分析(通过等待队列实现)

1.主要数据结构:

(1) struct poll_table_entry {

        struct file*  filp;

        wait_queue_t wait; 

        wait_queue_head_t* wait_address; 

     };

(2) struct poll_table_page {

        struct poll_table_page*   next;

        struct poll_table_entry*  entry;

        struct poll_table_entry   entries[0];

     };

---------

(3)处理函数指针,通常指向 __pollwait 或 null

   typedef void (*poll_queue_proc)(struct file *, 

                                     wait_queue_head_t *, 

                                     struct poll_table_struct *);

   封装在结构体 poll_table 中:
   typedef struct poll_table_struct {
       poll_queue_proc qproc;
   } poll_table;

(4)poll 封装的等待队列:

   struct poll_wqueues {   

       poll_table pt;  //__pollwait 或 null

       struct poll_table_page * table;

       int error;

    };

---------

(5)存放用户空间的信息:

   struct poll_list {

      struct poll_list *next;//按内存页连接,因为kmalloc 数据限制

      int len;                //用户空间传入fd的数量

     struct pollfd entries[0];//存放用户空间的数据

   };

   struct pollfd {

      int fd;
      short events;
      short revents;
   };

-----------------------------------------------

2.poll实现[fs/select.c -->sys_poll]

2.1asmlinkage long sys_poll (struct pollfd __user * ufds, unsigned int nfds, long timeout) {
   struct poll_wqueues table;
   struct poll_list *head;  /* 用户空间数据的链表头 */
   struct poll_list *walk;  /* poll 链表最后一个有效项 */
   ……

   poll_initwait(&table); /* 初始化poll等待队列,注册回调函数pt */

  ……

 while(i!=0) {
  struct poll_list *pp;

   /* 分配内存页,存放来自用户空间的信息,通常是4k */

   pp = kmalloc(sizeof(struct poll_list)+ sizeof(struct pollfd

                 (i>POLLFD_PER_PAGE?POLLFD_PER_PAGE:i), 

                GFP_KERNEL));

if (head == NULL)
  head = pp;
else
  walk->next = pp;
walk = pp;
if (copy_from_user(pp->entries,
ufds + nfds-i,

                    sizeof(struct pollfd)*pp->len)) {

  err = -EFAULT;
  goto  out_fds;
}

i -= pp->len;

 }//end while 

 fdcount = do_poll(nfds, head, &table, timeout);

}

2.2 sys_poll 函数说明:

0)拷贝用户空间信息:循环的作用是把用户态的 struct pollfd 拷进 entries 里,通常用户程序的 poll 调用就监控几个fd,所以 head 链表通常也就只需要一个节点,即操作系统的一页。但是,当用户传入的 fd 很多时,由于 poll 系统调用每次都要把所有 struct pollfd 拷进内核,故参数传递和页分配此时就成了poll系统调用的性能瓶颈。

1)poll_initwait 初始化变量 table,注意该 table 在整个执行 poll 的过程中是很关键的变量:

   void poll_initwait (struct poll_wqueues *pwq) 

     &(pwq->pt)->qproc = __pollwait/* 主要功能:设置回调函数 */

     ……

  }

》驱动中自定义 xxx_poll 函数中的调用函数:

  void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p);/* __pollwait 不仅是 poll 系统调用需要,select 系统调用也一样是用这个__pollwait,是操作系统的异步操作的御用回调函数。epoll 没有用这个,另外新增了一个回调函数,以达到其高效运转的目的 */

3)do_poll 函数说明

  static int do_poll(unsigned int nfds, 

                      struct poll_list *list,

                      struct poll_wqueues *wait, 

                      long timeout) 

 {

   int count = 0;
     poll_table* pt = &wait->pt;/* 回调函数 */
     for (;;) {/* 阻塞于此 */
       struct poll_list *walk;
       set_current_state(TASK_INTERRUPTIBLE);
       walk = list;
       while (walk != NULL) {

       /* 针对每个传进来的fd,调用各自对应的 xxx_poll 函数 */
         do_pollfd( walk->len, walk->entries, &pt, &count); 
         walk = walk->next;
       }
       if (count || !timeout || signal_pending(current))
          break;
       timeout = schedule_timeout(timeout); /* 挂起 current */
       }//end while 
      __set_current_state( TASK_RUNNING );
      return count;
   }

NOTE:注意 set_current_state(TASK_INTERRUPTIBLE)和 signal_pending,保障了当用户程序在调用 poll 后挂起时,发信号可以让程序迅速退出poll调用,而通常的系统调用是不会被信号打断的。纵览do_poll函数,主要是在循环内等待,直到count大于0才跳出循环,而count主要是靠do_pollfd函数处理。注意while循环,当用户传入的fd很多时(比如1000个),对 do_pollfd 就会调用很多次,poll效率瓶颈的另一原因就在这里

4)do_pollfd() 针对每个传进来的fd,调用它们各自对应的 xxx_poll函数,如下:

    [fs/select.c-->sys_poll()-->do_poll()-->do_pollfd()]
    static void do_pollfd(unsigned int num, struct pollfd * fdpage, poll_table ** pwait, int *count){

   ……

   file = fget_light(fd, &fput_needed);
   if (file->f_op && file->f_op->poll) {
     if (pwait)
       pwait->key = pollfd->events |POLLERR | POLLHUP;
       mask = file->f_op->poll(file, pwait);
    }

   return mask;
    } 

NOTE:如果 fd 对应的是某个socketdo_pollfd调用的就是网络设备驱动实现的poll;如果fd对应的是某个ext3文件系统上的一个打开文件,那do_pollfd调用的就是ext3文件系统驱动实现的poll,即file->f_op->poll是设备驱动程序实现的。设备驱动程序的标准实现是:调用poll_wait,即以设备自己的等待队列为参数(通常设备都有自己的等待队列,避免不支持异步操作的设备)调用 struct poll_table的回调函数。

5)例:socket tcp时的实例:[net/ipv4/tcp.c-->tcp_poll]

unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait){

     poll_wait(file, sk->sk_sleep, wait);

  }
Note:tcp_poll的核心实现就是poll_wait,而poll_wait就是调用 struct poll_table 对应的回调函数,那 poll 系统调用对应的回调函数就是__poll_wait,所以几乎可以把tcp_poll理解为一个语句:
__pollwait (file, sk->sk_sleep, wait); 

由此也可以看出,每个socket自己都带有一个等待队列(head) sk_sleep,所以上面所说的设备的等待队列,其实不止一个。
6) __poll_wait的实现:[fs/select.c-->__poll_wait()]
 void __pollwait (struct file *filp, wait_queue_head_t *wait_address, poll_table *_p){

    ……

 }

Note:__pollwait 的作用:一次设备poll调用只创建一个 poll_table_entry,并通过struct poll_table_entry 的 wait 成员,把current 挂在了设备的等待队列上,此处的等待队列是 wait_address,对应 tcp_poll 里的 sk->sk_sleep

7)poll系统调用的原理:

1.先注册回调函数 __pollwait,再初始化 table 变量(类型为struct poll_wqueues) ;

2.拷贝用户传入的struct pollfd(其实主要是fd(瓶颈1) ;

3.do_poll轮流调用所有fd对应的 poll__poll_waitcurrent挂到各个fd对应的设备等待队列上)(瓶颈2);

4.在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上的进程,即current被唤醒.

8)select/poll 缺点

1.每次调用时要重复地从用户态读入参数;
2.每次调用时要重复地扫描文件描述符;
3.每次在调用开始时,要把当前进程放入各个文件描述符的等待队列, 在调用结束后,又把进程从各个等待队列中删除。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值