浅析selet机制

    最近突然顿悟,很有必要养成写博文的习惯。其一,相关知识忘记了可以自己查看复习,而不必到处去百度找半天,其二,做IT必须有所积累,不断巩固知识,才可以提高工作效率。


    本篇文章通过select应用层调用和select驱动实现两个部分来分析如何使用select机制。


一、应用层如何使用select函数


int select(int maxfd,fd_set *rdset,fd_set *wrset,fd_set *exset,struct timeval *timeout)

功能说明:

    用来监视多个文件描述符的状态变化。当程序运行到这个函数,如果timeout=0不会阻塞继续往下执行,如果timeout>0则程序阻塞,这时跳出阻塞有两个办法:1、超时后跳出 2、在timeout时间内检测到文件描述符有一个或者多个发生状态变化时立即跳出。

    如果在timeout指定时间内,需要监视的描述符没有事件发生则函数超时返回0; 如果失败,则返回-1,错误原因存于 errno;需要监视的描述符有事件发生则函数返回1。


参数说明:
maxfd:需要监视的最大的文件描述符值+1。
rdset:  需要检测的可读文件描述符的集合。
wrset:   需要检测的可写文件描述符的集合。

exset:    需要检测的异常文件描述符的集合。
timeout:超时时间,其类型是struct timeval *,即一个struct timeval结构的变量的指针,所以我们在程序里要申明一个struct timeval rto;然后把变量rto的地址&rto传递给select函数。struct timeval结构如下:

struct timeval {
long  
tv_sec;     /* seconds */
long   tv_usec;    /* microseconds */
};


    fd_set是一组文件描述字(fd)的集合,它用一位来表示一个fd,对于fd_set类型通过下面四个宏来操作:

FD_ZERO(fd_set *fdset)
功能说明:将指定的文件描述符集清空,在对文件描述符集合进行设置前,必须对其进行初始化,如果不清空,由于在系统分配内存空间后,通常并不作清空处理,所以结果是不可知的。

FD_SET(int fd,fd_set *fdset)
功能说明:用于在文件描述符集合中增加一个新的文件描述符。

FD_CLR(int fd,fd_set *fdset)
功能说明:用于在文件描述符集合中删除一个文件描述符。

FD_ISSET(int fd,fd_set *fdset)
功能说明:检查fd_set联系的文件描述符fd是否可读写,>0表示可读写。   


下面给出一个简单的实例:

int main()
{
      int ret, socketfd_connected, msg_recv_len;
      struct timeval rto;    
      fd_set socket_read_fds;   
      char msg_recv[1024];
      
      while(1)
      {
            /*每次循环都要重新赋值,不能放在大循环外面。可能有人会问,rto是一个全局变量
            ,在大循环前对它赋值一次不就可以了吗?这个估计是因为调用select函数会对它清零             ,所以每次循环都要重新赋值*/
            
            rto.tv_sec = 5; rto.tv_usec = 0; //超时5秒
           
             //清空文件描述符集socket_read_fds
            FD_ZERO(&socket_read_fds);
            
             //把已经建立好连接的socketfd_connected加入到文件描述符集socket_read_fds
            FD_SET(socketfd_connected,&socket_read_fds);
            
             /*进入超时等待消息,如果监测多个文件描述符,则只要检测到其中任何一个描述符可               以读写,该函数都会立刻跳出*/
            ret = select(socketfd_connected+1,&socket_read_fds, NULL, NULL, &rto);
            if(ret == -1)//错误
            {
                if(EINTR == errno)  
                    continue;   
                return 1;
            }
            if(ret == 0) // select 超时
            {
                continue;  
            }
             //判断socketfd_connected是否可读,如果可读,便调用recv进行读取
            if(FD_ISSET(socketfd_connected,&socket_read_fds))
                     msg_recv_len = recv(socketfd_connected, msg_recv, 1024, 0);
      }
}

    到此select函数的应用层调用介绍完了。


二、select函数的驱动实现


用户空间函数对应的内核空间函数

用户空间
内核空间(驱动)
open
open
close
release
read
read
write
write
ioctl
ioctl
lseek
lseek
select
poll

我们在驱动中实现poll函数,只需要做两件事:
1.  使用poll_wait()将等待队列添加到poll_table中。
2.  返回描述设备是否可读或可写的掩码。


这里给出本人所写的一个简单的实例:

static unsigned int button_poll(struct file *file, struct poll_table_struct *wait)
{
     unsigned int mask = 0;
     
     //添加等待队列到等待队列表中(poll_table)
     poll_wait(file, &button_waitq_poll, wait);
     
     if(ev_press_poll)
     {
          ev_press_poll = 0;
          
          //标识数据可以获得
          mask |= POLLIN | POLLRDNORM;
     }
     return mask;
}


注意:该mask是返回给do_select函数的,不是返回给用户空间的。(do_select是用户空间中select函数的底层调用,后面会分析这个函数)。mask到底有哪些值呢?请看下表:

标志
含义
POLLIN
如果设备无阻塞的读,返回该值
POLLRDNORM
数据已经准备好了,可以读了,就返回该值。通常的做法是返回POLLIN | POLLRDNORM
POLLRDBAND
如果可以从设备读出外带数据,就返回该值。它只可以在Linux内核的某些网络代码中使用,通常不用在设备驱动程序中。
POLLPRI
如果可以无阻塞的读取高优先级(带外)数据,就返回该值,返回该值会导致select报告文件发生异常,以为select把带外数据当做异常处理。
POLLHUP
当读设备的进程到达文件尾时,驱动程序必须返回该值,依照select的功能描述,调用select的进程被告知进程是可读的。
POLLERR
如果设备发生错误就返回该值
POLLOUT
如果设备可以无阻塞的写 ,就返回该值。
POLLWRNORM
设备以及准备好了,可以写了,就返回该值。通常的做法是返回POLLOUT|POLLWRNORM
POLLWRBAND
与POLLRDBAND类似(一个读,一个写啦)


    可以看出,驱动程序中实现的poll函数并没有阻塞,那应用层调用select函数时,是怎么发生阻塞的呢?还有,当调用select函数时,驱动程序中的poll函数是怎么被调用的呢?问题全在do_select函数。do_select函数是select系统调用所对应的内核函数,它完成了select函数的功能。打开内核代码,找到do_select函数,通读之,可以发现:

    do_select中有个for(;;)大循环,里面还有一个长这样子for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1)的循环,这个循环会把文件描述符集轮询一边,调用各个文件描述符对应驱动中的poll函数,并通过poll函数返回的mask的值判断对应的文件描述符是否可读或者可写,如果否,则调用函数poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,to, slack)将进程阻塞,接下来等待驱动空间将对应的等待队列唤醒(若唤醒,说明了某个文件描述符的状态改变了),唤醒后for(;;)大循环又执行一次,也就是for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1)循环又执行一次,这时至少有一个文件描述符的mask是可以或者可写的,那么使用break跳出循环,也就跳出select函数的阻塞了。若在timeout时间内文件描述符集都没有状态改变,则超时,也使用break跳出循环,跳出select函数的阻塞。

    到这里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);
 wait = &table.pt;
 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;
    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);
    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);
     }
     fput_light(file, fput_needed);
     if ((mask & POLLIN_SET) && (in & bit)) {
      res_in |= bit;
      retval++;
      wait = NULL;
     }
     if ((mask & POLLOUT_SET) && (out & bit)) {
      res_out |= bit;
      retval++;
      wait = NULL;
     }
     if ((mask & POLLEX_SET) && (ex & bit)) {
      res_ex |= bit;
      retval++;
      wait = NULL;
     }
    }
   }
   if (res_in)
    *rinp = res_in;
   if (res_out)
    *routp = res_out;
   if (res_ex)
    *rexp = res_ex;
   cond_resched();
  }
  wait = NULL;
  if (retval || timed_out || signal_pending(current))
   break;
  if (table.error) {
   retval = table.error;
   break;
  }
  /*
   * If this is the first loop and we have a timeout
   * given, then we convert to ktime_t and set the to
   * pointer to the expiry value.
   */
  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;
}


你可能感兴趣的:(Linux操作系统,select机制)