[网络编程]select流程分析

函数原型:
int select(int maxfdp1,fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout)
主要代码路径与文件 linux/fs/select.c         linux/include/linux/poll.h
 
select的调用路径sys_select()->core_sys_select()-> do_select()
最重要的工作就是在 do_select中完成,前面为参数的判断及准备工作

int  do_select (int n, fd_set_bits *fds, struct timespec *end_time)
{
     ktime_t expire, *to = NULL;
     struct poll_wqueues table;                      
     poll_table *wait;
     int retval, i, timed_out = 0;
     unsigned long slack = 0;

     rcu_read_lock();
      retval = max_select_fd(n, fds);     
     rcu_read_unlock();

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

      poll_initwait(&table);             对   poll_wqueues结构进行一些初始化
      wait = &table.pt;                  取得poll_table结构
     if (end_time && !end_time->tv_sec && !end_time->tv_nsec) {
          wait = NULL;
          timed_out = 1;
     }

     if (end_time && !timed_out)
          slack = estimate_accuracy(end_time);

     retval = 0;
     for (;;) {
          unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;

          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) {                遍历每个描述符
               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) {
                    i += __NFDBITS;                     如果这个字没有待查找的描述符, 跳过这个长字(32位)
                    continue;
               }

               for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {           遍历每个长字里的每个位
                    int fput_needed;
                    if (i >= n)
                         break;
                    if (!(bit & all_bits))
                         continue;
                    file = fget_light(i, &fput_needed);               得到指定位对应的fd的file结构
                    if (file) {
                         f_op = file->f_op;
                         mask = DEFAULT_POLLMASK;
                         if (f_op && f_op->poll) {
                              wait_key_set(wait, in, out, bit);
                              mask = (*f_op->poll)(file, wait); 
                              在这里循环调用所监测的fd_set内的所有文件描述符对应的poll函数
                               还记得socket创建那一文中说到:
                               init_file(file, sock_mnt, dentry, FMODE_READ | FMODE_WRITE,  & socket_file_ops );           
                              对file结构成员赋值,并将file->f_op 更新为 socket_file_ops socket类型文件的操作函数集    
                              那poll函数为: .poll = sock_poll,
                              sock_poll()见下面分析
                         }
                         fput_light(file, fput_needed); 
                          if ((mask & POLLIN_SET) && (in & bit)) {             
                              res_in |= bit;        如果是这个描述符可读, 将这个位置位
                              retval++;             返回描述符个数加1
                              wait = NULL;
                         }
                         if ((mask & POLLOUT_SET) && (out & bit)) {   
                              res_out |= bit;         如果是这个描述符有异常错误, 将这个位置位
                              retval++;                返回描述符个数加1
                              wait = NULL;
                         }
                         if ((mask & POLLEX_SET) && (ex & bit)) {   
                              res_ex |= bit;                如果是这个描述符有异常错误, 将这个位置位
                              retval++;                      返回描述符个数加1
                              wait = NULL;

                         }
                    }
               }
               if (res_in)
                    *rinp = res_in;
               if (res_out)
                    *routp = res_out;
               if (res_ex)
                    *rexp = res_ex;
               cond_resched();
          }

到这里遍历结束。
retval保存了检测到的可操作的文件描述符的个数。
如果有文件可操作,则跳出for(;;)循环,直接返回。
若没有文件可操作且timeout时间未到同时没有收到signal,则执行 schedule_timeout睡眠。
睡眠时间长短由__timeout决定,一直等到该进程被唤醒

          wait = NULL;
          if (retval || timed_out || signal_pending(current))
               break;                                                                    跳出循环 返回结果
          if (table.error) {
               retval = table.error;
               break;
          }

          if (end_time && !to) {
               expire = timespec_to_ktime(*end_time);
               to = &expire;
          }

          if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,       睡眠,可由信号唤醒
                            to, slack))
               timed_out = 1;
     }

     poll_freewait(&table);

     return retval;
}

重要结构体
typedef struct {
     unsigned long *in, *out, *ex;
     unsigned long *res_in, *res_out, *res_ex;
fd_set_bits;           这个结构体保存了select在用户态的参数
在select()中,每一个文件描述符用一个位表示,其中1表示这个文件是被监视的。
in,out,ex指向的bit数组表示对应的读,写,异常文件的描述符,
res_in,res_out,res_ex表示对应的读,写,异常文件的描述符的检测结果。

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];
};
每一个调用select()系统调用的应用进程都会存在一个 struct poll_weueues结构体,
用来统一辅佐实现这个进程中所有待监测的fd的轮询工作,
后面所有的工作和都这个结构体有关,所以它非常重要


struct  poll_table_page {
     struct poll_table_page * next;
     struct poll_table_entry * entry;
     struct poll_table_entry entries[0];
};
这个表记录了select过程中所有等待队列的节点。
由于select要监视多个fd,并且要把当前进程放入这些fd的等待队列中去,因此要分配等待队列的节点。
这些节点可能如此之多,以至于不可能像通常做的那样,在堆栈中分配它们。
所以,select以动态分配的方式把它保存在poll_table_page中。
保存的方式是单向链表,每个节点以页为单位,分配多个poll_table_entry项。

struct  poll_table_entry {
     struct file *filp;
     unsigned long key;
     wait_queue_t wait;                               内嵌了一个等待队列
     wait_queue_head_t *wait_address;
};
filp是select要监视的struct file结构体,wait_address是文件操作的等待队列的队首,wait是等待队列的节点。


void poll_initwait(struct poll_wqueues *pwq)
{
      init_poll_funcptr(&pwq->pt,  __pollwait);           保存回调函数
     pwq->polling_task = current;                          设置polling_task为当前进程
     pwq->triggered = 0;
     pwq->error = 0;
     pwq->table = NULL;
     pwq->inline_index = 0;
}
static inline void  init_poll_funcptr(poll_table *pt, poll_queue_proc qproc)
{
     pt->qproc = qproc;
     pt->key   = ~0UL; /* all events enabled */
}

static unsigned int  sock_poll(struct file *file, poll_table *wait)
{
     struct socket *sock;
      sock = file->private_data;          得到socket结构,存于file的私有数据区中          
                                                      这里就从fd转化为对对应socket结构的操作了
     return  sock->ops->poll(file, sock, wait);
          在前面socket的创建一文中分析过   sock->ops = answer->ops;        
        以TCP为例 即为   .ops =        &inet_stream_ops,       .poll = tcp_poll,
       以UDP为例 即为   .ops =        & inet_dgram_ops ,       .poll = udp_poll,
       对应的poll函数就是去查看对应的sock结构中     
            struct sk_buff_head     sk_receive_queue;
            struct sk_buff_head     sk_write_queue;  
     这些队列是否可读,可写,以及其他一些状态的判断,具体的不进入分析了 
      我们只大概看下udp_poll
}

unsigned int  datagram_poll(struct file *file, struct socket *sock,
                  poll_table *wait)
{
     struct sock *sk = sock->sk;
     unsigned int mask;

      sock_poll_wait(file, sk->sk_sleep, wait);     将wait加入到sock的sk_sleep等待队列头中
     mask = 0;
                           下面对各种可读,可写,异常错误等状态判断,返回mask
     /* exceptional events? */
     if (sk->sk_err || !skb_queue_empty(&sk->sk_error_queue))
          mask |= POLLERR;
     if (sk->sk_shutdown & RCV_SHUTDOWN)
          mask |= POLLRDHUP;
     if (sk->sk_shutdown == SHUTDOWN_MASK)
          mask |= POLLHUP;

     /* readable? */
     if (!skb_queue_empty(&sk->sk_receive_queue) ||
         (sk->sk_shutdown & RCV_SHUTDOWN))
          mask |= POLLIN | POLLRDNORM;

     /* Connection-based need to check for termination and startup */
     if (connection_based(sk)) {
          if (sk->sk_state == TCP_CLOSE)
               mask |= POLLHUP;
          /* connection hasn't started yet? */
          if (sk->sk_state == TCP_SYN_SENT)
               return mask;
     }

     /* writable? */
     if (sock_writeable(sk))
          mask |= POLLOUT | POLLWRNORM | POLLWRBAND;
     else
          set_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags);

     return mask;
}

static inline void sock_poll_wait(struct file *filp,
          wait_queue_head_t *wait_address, poll_table *p)

{
     if (p && wait_address) {
           poll_wait(filp, wait_address, p);
          smp_mb();
     }
}

static inline void  poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
     if (p && wait_address)
           p->qproc(filp, wait_address, p);  这里qproc即为  init_poll_funcptr(&pwq->pt,  __pollwait);
                                                             保存回调函数
}

static void  __pollwait(struct file *filp, wait_queue_head_t *wait_address,
                    poll_table *p)
{
     从poll_table结构得到poll_wqueues 结构
     struct poll_wqueues *pwq = container_of(p, struct poll_wqueues, pt);        
     获得一个poll_table_entry 
     struct poll_table_entry * entry = poll_get_entry(pwq);          
     if (!entry)
          return;
     get_file(filp);
     entry->filp = filp;              保存file结构变量
     entry->wait_address = wait_address;            这里wait_address为sk->sk_sleep结构
     entry->key = p->key;
     初始化等待队列项,pollwake是唤醒该等待队列项时候调用的函数
      init_waitqueue_func_entry(&entry->wait, pollwake);      
       将poll_wqueues作为该等待队列项的私有数据,后面使用
     entry->wait.private = pwq;                                           
     add_wait_queue(wait_address, &entry->wait);              
将该等待队列项添加到从驱动程序中传递过来的等待队列头中去sk->sk_sleep结构
}
该函数首先通过container_of宏来得到结构体poll_wqueues的地址,然后调用poll_get_entry()函数来获得一个poll_table_entry结构体,这个结构体是用来连接驱动和应用进程的关键结构体,其实联系很简单,这个结构体中内嵌了一个等待队列项wait_queue_t,和一个等待队列头 wait_queue_head_t,它就是驱动程序中定义的等待队列头,应用进程就是在这里保存了每一个硬件设备驱动程序中的等待队列头( 当然每一个fd都有一个poll_table_entry结构体)。

我们看下睡眠
poll_schedule_timeout(&table, TASK_INTERRUPTIBLE, to, slack)
int  poll_schedule_timeout(struct poll_wqueues *pwq, int state,
                 ktime_t *expires, unsigned long slack)
{
     int rc = -EINTR;

     set_current_state(state);
     if (!pwq->triggered)            这个triggered在什么时候被置1的呢?只要有一个fd对应的设备将当前应用进程唤醒后将会把它设置成1
          rc = schedule_hrtimeout_range(expires, slack, HRTIMER_MODE_ABS);
     __set_current_state(TASK_RUNNING);
     set_mb(pwq->triggered, 0);
     return rc;
}

看下唤醒过程:
前面介绍了select会循环遍历它所监测的fd_set内的所有文件描述符对应的驱动程序的poll函数。
驱动程序提供的poll函数首先会将调用select的用户进程插入到该设备驱动对应资源的等待队列(如读/写等待队列), 然后返回一个bitmask告诉select当前资源哪些可用。

上面poll函数中已经将wait 即当前进程插入到了等待队列中。

唤醒该进程的过程通常是在所监测文件的设备驱动内实现的,驱动程序维护了针对自身资源读写的等待队列。
当设备驱动发现自身资源变为可读写并且有进程睡眠在该资源的等待队列上时, 就会唤醒这个资源等待队列上的进程。
在这里,比如UDP,有udp数据包来了后,挂载到了对应sock的接收队列上时,会查看是否有进程正在睡眠等待, 如果有的话就调用注册的 唤醒函数进行唤醒,我们这里注册的唤醒函数就是上面提到的 pollwake.

static int  pollwake(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
     struct poll_table_entry *entry;

     entry = container_of(wait, struct poll_table_entry, wait);    从wait得到poll_table_entry 结构
     if (key && !((unsigned long)key & entry->key))             判断key,检查是否有错误唤醒
          return 0;
     return  __pollwake(wait, mode, sync, key);
}


static int __pollwake(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
      struct poll_wqueues *pwq = wait->private;
     DECLARE_WAITQUEUE(dummy_wait, pwq->polling_task);

     smp_wmb();
     pwq->triggered = 1;

     return  default_wake_function(&dummy_wait, mode, sync, key);  
将等待进程从等待队列上摘下,加入运行进程队列等一系列复杂操作,达到唤醒目的
}
到这里明白了select进程被唤醒的过程。
由于该进程是阻塞在所有监测的文件对应的设备等待队列上的,因此在timeout时间内,只要任意个设备变为可操作,
都会立即唤醒该进程,从而继续往下执行。这就实现了select的当有一个文件描述符可操作时就立即唤醒执行的基本原理。

这篇文章对select(poll)分析的很好,大家共同学习

poll与select实现了相同的功能,只是参数类型不同。它的原型是:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
可以看到,poll的参数中,直接列出了要监视的文件描述符的信息,而不像select一样要列出从0开始到nfds-1的所有文件描述符。这样的好处是,poll不需要查询很多无关的文件描述符的信息,在一定场合下效率会有所提高。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值