epoll和select/poll源码层上的区别

预备知识

1. 文件描述符fd,inode和file结构体

struct inode:位于内存中的文件索引节点,是linux管理文件的基本单位,对于磁盘中的一个文件或设备内核中只有一个inode,其数据成员的信息取自磁盘上的文件系统;

struct dentry:目录项,是目录树的基本单元,与inode是多对一的关系,因为创建硬链接就会产生新的目录项,inode没多;

struct file:内核打开文件时创建,关闭时释放,用来标识这次打开的读写权限以及偏移量等信息;

连接关系:file -> dentry -> inode;

进程通过一个结构体files_struct来管理所有打开的file结构体,该结构体中有指向file的指针的数组,而文件描述符fd就是这些指针的index;关系: task_struct(PCB) -> files -> fd_array[fd]。

2. linux等待队列

struct __wait_queue_head {
    spinlock_t lock;
    struct list_head task_list;//内核数据结构,自己没有数据只有指向前后的指针,用来连接其他struct
};

typedef struct __wait_queue wait_queue_t; 

struct __wait_queue { 
    unsigned int flags; 
#define WQ_FLAG_EXCLUSIVE   0x01 
    void *private; 
    wait_queue_func_t func; 
    struct list_head task_list; 
};

select和epoll都依赖文件状态改变后内核遍历文件的等待队列调用其回调函数。

select原理

select的真正实现在core_sys_select >> do_select中

int do_select(int n, fd_set_bits *fds, s64 *timeout)    
{    
 struct poll_wqueues table;    
 poll_table *wait;    
 int retval, i;    

 rcu_read_lock();    
 /*根据已经打开fd的位图检查用户打开的fd, 要求对应fd必须打开, 并且返回最大的fd*/   
 retval = max_select_fd(n, fds);    
 rcu_read_unlock();    

 if (retval < 0)    
  return retval;    
 n = retval;    


 /*将当前进程放入自已的等待队列table, 并将该等待队列加入到该测试表wait*/   
 poll_initwait(&table);    
 wait = &table.pt;    

 if (!*timeout)    
  wait = NULL;    
 retval = 0;    

 for (;;) {  
  unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;    
  long __timeout;    

  /*可中断的睡眠状态*/   
  set_current_state(TASK_INTERRUPTIBLE);    

  inp = fds->in; outp = fds->out; exp = fds->ex;    
  rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;    


  for (i = 0; i < n; ++rinp, ++routp, ++rexp) {/*遍历所有fd*/   
   unsigned long in, out, ex, all_bits, bit = 1, mask, j;    
   unsigned long res_in = 0, res_out = 0, res_ex = 0;    
   const struct file_operations *f_op = NULL;    
   struct file *file = NULL;    

   in = *inp++; out = *outp++; ex = *exp++;    
   all_bits = in | out | ex;    
   if (all_bits == 0) {    
    /*   
    __NFDBITS定义为(8 * sizeof(unsigned long)),即long的位数。   
    因为一个long代表了__NFDBITS位,所以跳到下一个位图i要增加__NFDBITS   
    */   
    i += __NFDBITS;    
    continue;    
   }    

   for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {    
    int fput_needed;    
    if (i >= n)    
     break;    
 
    if (!(bit & all_bits))    
     continue;    

    /*得到file结构指针,并增加引用计数字段f_count*/   
    file = fget_light(i, &fput_needed);    
    if (file) {    
     f_op = file->f_op;    
     mask = DEFAULT_POLLMASK;    

     /*对于socket描述符,f_op->poll对应的函数是sock_poll   
     注意第三个参数是等待队列,在poll成功后会将本进程唤醒执行*/   
     if (f_op && f_op->poll)    
      mask = (*f_op->poll)(file, retval ? NULL : wait);    

     /*释放file结构指针,实际就是减小他的一个引用计数字段f_count*/   
     fput_light(file, fput_needed);    

     /*根据poll的结果设置状态,要返回select出来的fd数目,所以retval++。   
     注意:retval是in out ex三个集合的总和*/   
     if ((mask & POLLIN_SET) && (in & bit)) {    
      res_in |= bit;    
      retval++;    
     }    
     if ((mask & POLLOUT_SET) && (out & bit)) {    
      res_out |= bit;    
      retval++;    
     }    
     if ((mask & POLLEX_SET) && (ex & bit)) {    
      res_ex |= bit;    
      retval++;    
     }    
    }    

    /*   
    注意前面的set_current_state(TASK_INTERRUPTIBLE);   
    因为已经进入TASK_INTERRUPTIBLE状态,所以cond_resched回调度其他进程来运行,   
    这里的目的纯粹是为了增加一个抢占点。被抢占后,由等待队列机制唤醒。   

    在支持抢占式调度的内核中(定义了CONFIG_PREEMPT),cond_resched是空操作   
    */     
    cond_resched();    
   }    
   /*根据poll的结果写回到输出位图里*/   
   if (res_in)    
    *rinp = res_in;    
   if (res_out)    
    *routp = res_out;    
   if (res_ex)    
    *rexp = res_ex;    
  }    
  wait = NULL;    
  if (retval || !*timeout || signal_pending(current))/*signal_pending前面说过了*/   
   break;    
  if(table.error) {    
   retval = table.error;    
   break;    
  }    

  if (*timeout < 0) {    
   /*无限等待*/   
   __timeout = MAX_SCHEDULE_TIMEOUT;    
  } else if (unlikely(*timeout >= (s64)MAX_SCHEDULE_TIMEOUT - 1)) {    
   /* 时间超过MAX_SCHEDULE_TIMEOUT,即schedule_timeout允许的最大值,用一个循环来不断减少超时值*/   
   __timeout = MAX_SCHEDULE_TIMEOUT - 1;    
   *timeout -= __timeout;    
  } else {    
   /*等待一段时间*/   
   __timeout = *timeout;    
   *timeout = 0;    
  }    

  /*TASK_INTERRUPTIBLE状态下,调用schedule_timeout的进程会在收到信号后重新得到调度的机会,   
  即schedule_timeout返回,并返回剩余的时钟周期数   
  */   
  __timeout = schedule_timeout(__timeout);    
  if (*timeout >= 0)    
   *timeout += __timeout;    
 }    

 /*设置为运行状态*/   
 __set_current_state(TASK_RUNNING);    
 /*清理等待队列*/   
 poll_freewait(&table);    

 return retval;    
}    

struct poll_wqueues {
       poll_table pt;
       struct poll_table_page *table;
       struct task_struct *polling_task; //保存当前调用select的用户进程struct task_struct结构体
       int triggered;         // 当前用户进程被唤醒后置成1,以免该进程接着进睡眠
       int error;               // 错误码
       int inline_index;   // 数组inline_entries的引用下标
       struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES];

};

fds中保存了用户输入的的三个位图加三个res位图,无非是循环遍历位图的每一个bit,找到对应的fd, file结构体。接下来需要检查file结构体中的f_op和f_op中是否有poll函数的实现,要用select监听文件必须有poll的实现。而file.f_op源于文件打开创建file结构体时内核将对应的inode中的file_operations赋给了file,poll的实现一般由文件对应的驱动给出。

可以看到在poll每个文件时,poll的另一个参数是wait,也就是table.pt,这是一个struct poll_table变量,其中只包含了一个函数指针qproc,poll过程会调用这个函数,而qproc在poll_initwait(&table)时被设置成了__pollwait函数,它在内核中有具体定义:

/* Add a new entry */
 static void __pollwait(struct file *filp, wait_queue_head_t *wait_address,
                                 poll_table *pt)
 {
         struct poll_wqueues *pwq = container_of(p, struct poll_wqueues, pt);//通过成员地址找结构体指针
         struct poll_table_entry *entry = poll_get_entry(pwq);
         if (!entry)
                 return;
         entry->filp = get_file(filp);
         entry->wait_address = wait_address;
         entry->key = p->_key;
         init_waitqueue_func_entry(&entry->wait, pollwake);
         entry->wait.private = pwq;
         add_wait_queue(wait_address, &entry->wait); //list_add(&entry->wait.task_list, &wait_address->task_list);
}

entry->wait就是前面介绍的wait_queue_t, 这里重要的两步一是这个等待队列项的回调函数被设置成了pollwake,二是把等待队列项添加到了监听文件的file->private_data->wait_queue_head_t队列中,private_data是一个void *指针,指向的内容由文件类型决定,而其中一般都会含有等待队列。

因此总的来说,select中主动poll文件其实是做了两件事:1. 检查是否有读写就绪;2. 二是让文件状态改变、处理自己的等待队列时调用pollwake函数。pollwake函数调用过程为pollwake->__pollwake->default_wake_function->try_to_wake_up,就是唤醒主动poll它的这个进程,但wait_queue_t是如何找到是哪个进程呢,答案是do_select中创建的poll_wqueues中包含了指向执行select进程的指针,而上面pwq被赋值给了wait.private。

所以,do_select在第一轮遍历poll之后如果没有就绪文件就睡眠自己,而其poll过的文件如果状态改变则会唤醒这个进程,唤醒后select就会进行新一轮的poll。

epoll原理

1. epoll_create

epoll_create做的事主要是配置eventpoll结构体(用来管理这个epollfd的各种数据)并为epollfd创建对应的inode,dentry和file,使epollfd真的指向了一个文件,而file的private_data就是指向创建的eventpoll结构体。eventpoll中重要的成员有:1. 两个等待队列,一个是挂载调用epoll_wait()的进程, 另一个是用于被监听的等待队列这样epollfd也可以被poll;2. 一个就绪队列,用于存储就绪的文件。

2. epoll_ctl

与select/poll相反,epoll的关键实现不在epoll_wait而是添加监听文件的过程。如果操作是add,则对应的函数实现epoll_insert,当然epoll_ctl会首先检查需要添加的fd是否已经在监听中,这就是为什么epoll为什么要使用红黑树管理监听文件(同理remove和modify也要用到查找)。红黑树节点的结构体创建在cache中,同样也是为了加速访问。

struct eventpoll{
  spinlock_t lock;
  struct mutex mtx;
  wait_queue_head_t wq; // sys_epoll_wait() 在这里等待
  wait_queue_head_t poll_wait; //f_op->poll() 使用的,被其他事件通知机制使用的wait_address
  struct list_head rdlist; //存储已经就绪的epitem的列表
  struct rb_root rbr; //保存fd对应的epitem,并且以红黑树的形式组织起来
  struct epitem *ovlist; //rdlist正在被复制,为就绪文件提供一个暂存的队列
  struct user_struct *user; //该epollfd的使用者用户
  struct file *file; //epollfd对用的内核文件
  int visited; //加快检测效率
  struct list_head visited_list_link
}

epoll_insert()除了创建监听文件对应的红黑树节点,还会poll一次监听文件,前面说到文件被poll的时候会将poll它的进程加入自己的等待队列(这个过程其实是传入poll函数的poll_table的qproc函数执行,文件的poll只负责将队列头传给poll_wait)。poll_table的qproc还负责设置等待的回调函数,与select/epoll不同,epoll的qproc给出的回调函数为ep_poll_callback。这个回调函数做的并不是唤醒epoll的进程而是根据其容器epoll_entry找到epitem(红黑树节点),将其添加到eventpoll的就绪队列,并唤醒等待在epoll_wait的进程,处理epoll文件自己的等待队列。

static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,  poll_table *pt)
{
  struct epitem *epi = ep_item_from_epqueue(pt);
  struct eppoll_entry *pwq;
 
  if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) { 
     init_waitqueue_func_entry(&pwq->wait, ep_poll_callback); ###初始化wait回调函数 
     pwq->whead = whead;   ###即sock->sk_wq->wait
     pwq->base = epi;       ###要监听的文件的epitem
     add_wait_queue(whead, &pwq->wait);   ###添加到sock的等待队列中
     list_add_tail(&pwq->llink, &epi->pwqlist); ###添加到epitem的poll wait queues列表
     epi->nwait++;
   } else {
     /* We have to signal that an error occurred */
    epi->nwait = -1;
   }
 }

3. epoll_wait

前面说到,就绪的文件会在回调函数中被添加到就绪队列,所以epoll_wait并不需要再遍历+poll,而是睡眠被唤醒时处理就绪队列(select 和 poll都是先遍历一次所有的fd,唤醒后再遍历一次),这也是为什么epoll效率比较高的原因。

 

总结

1. select/poll和epoll实现出现分别的地方:

虽然在poll文件的地方不同,但select/poll在睡眠前遍历文件其实也是为了完成添加等待队列和设置回调函数,所以我理解的主要区别在于下面几个地方

 poll_table的qproc函数指针qproc中用来装载wait_queue_t的容器赋予等待队列的回调函数
select/poll__pollwaitstruct poll_table_entrypollwake
epollep_ptable_queue_procstruct epoll_entryep_poll_callback

 

可以说epoll的改进来源于select和poll在实现上的明显不足:既然可以为文件就绪添加回调函数,那么我们完全可以让回调函数记录这是哪一个文件而不是简单地唤醒正在等待事件发生的进程。

2. 内核源码中有许多地方使用了通过container_of通过成员指针获得外层结构体指针的操作,这是因为不像C++有模板,如果想将定义的红黑树、等待队列等基本数据结构应用于其他struct,只能将节点置于struct内部,利用节点来得到外层数据。

3. epoll处理水平沿触发的方式:

上述原理可以看出epoll这样其实是实现了边沿触发,而水平触发依赖于epoll_wait中将就绪队列中设置成水平触发的epitem再放入就绪序列,但是这么做,就绪过的文件不就每次返回都会存在于就绪队列了吗?而epoll_wait在处理就绪队列时不是直接将队列中的文件和事件返回给用户,而是再poll一次文件,确定有就绪时间才处理,这样就能过滤掉上次epoll_wait再次放入就绪队列的水平触发epitem。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值