UNIX再学习 -- 函数 select、poll、epoll

这部分是相当重要的一部分,之前在工作项目中有用到过,特意认真的看过。文章最后会把我项目用到部分的源码贴出。

再有值得纪念的一下,原创文章数终于赶上转载文章数了。说明我找到的学习方法是对了,一开始茫然不知所措时,学习是做加法,将看到的好的文章转载;但到了一定阶段,要懂得做减法了,将之前转载部分系统的分类总结;最后就是举一反三,将难题,转化成自己熟悉的问题解决。

文章开头总是很难的,要讲的东西太多,不知道从哪下笔。我就以下面参看这篇文章为基础开讲:

参看:select、poll、epoll之间的区别总结[整理]

一、相关概念

select、poll、epoll 都是 I/O 多路复用的机制。I/O 多路复用机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但 select、poll、epoll本质上都是同步 I/O,因为它们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步 I/O 则无需自己负责进行读写,异步 I/O 的实现会负责吧数据从内核拷贝到用户空间。

上面这段文字简单介绍了一下我们要讲到的这三个函数。其中有几个概念需要搞清楚。

(1)I/O 多路复用机制

I/O 多路复用,也称为 I/O 多路转接。APUE 上只有这样一句话。
为了使用这种技术,先构建一张我们感兴趣的描述符(通常都不止一个)的列表,然后调用一个函数,直到这些描述符中的一个已准备好进行 I/O 时,该函数才返回。poll、pselect 和 select 这 3 个函数使我们能够执行 I/O 多路转接。在从这些函数返回时,进程会被告知哪些描述符已准备好可以进行 I/O。
这是什么鬼,还是没有理解多路转接到底是怎么回事,然后我各种百度(谷歌虽好但要翻墙)也没查到合适的解释。
I/O 多路转接,英文是 I/O multiplexing,最后在知乎上看到还比较容易理解的解释。
I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Socket (I/O流)的状态(对应空管塔里面的 Fight progress strip槽)来同时管理多个 I/O 流。 发明它的原因,是尽量多的提高服务器的吞吐能力。

(2)同步 I/O

稍后再讲

(3)异步 I/O

稍后再讲

(4)阻塞 I/O 与非阻塞 I/O

比如 read 和 write,通常 IO 操作都是阻塞 I/O 的,也就是说当你调用 read 时,如果没有数据收到,那么线程或者进程就会被挂起,直到收到数据。

这样,当服务器需要处理1000个连接的的时候,而且只有很少连接忙碌的,那么会需要1000个线程或进程来处理1000个连接,而1000个线程大部分是被阻塞起来的。由于CPU的核数或超线程数一般都不大,比如4,8,16,32,64,128,比如4个核要跑1000个线程,那么每个线程的时间槽非常短,而线程切换非常频繁。这样是有问题的:
1、线程是有内存开销的,1个线程可能需要512K(或2M)存放栈,那么1000个线程就要512M(或2G)内存。
2、线程的切换,或者说上下文切换是有CPU开销的,当大量时间花在上下文切换的时候,分配给真正的操作的CPU就要少很多。

那么,我们就要引入非阻塞I/O的概念,非阻塞IO很简单,通过 fcntl(POSIX)或 ioctl(Unix)设为非阻塞模式,这时,当你调用 read 时,如果有数据收到,就返回数据,如果没有数据收到,就立刻返回一个错误,如EWOULDBLOCK。这样是不会阻塞线程了,但是你还是要不断的轮询来读取或写入。


于是,我们需要引入IO多路复用的概念。多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)。


这样在处理1000个连接时,只需要1个线程监控就绪状态,对就绪的每个连接开一个线程处理就可以了,这样需要的线程数大大减少,减少了内存开销和上下文切换的CPU开销。

使用select函数的方式如下图所示:

二、函数 select

   /* According to POSIX.1-2001 */
       #include <sys/select.h>

       /* According to earlier standards */
       #include <sys/time.h>
       #include <sys/types.h>
       #include <unistd.h>

       int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);
       返回值:准备就绪的描述符数目:若超时,返回 0;若出错,返回 -1 

1、参数解析

(1)参数 timeout它指定愿意等待的时间长度,单位为秒和微妙

时间函数,我们讲了好多次了。参看:C语言再学习 -- 时间函数
结构体 timeval 定义如下:
      struct timeval {    
               time_t      tv_sec;     /* seconds */    
               suseconds_t tv_usec;    /* microseconds */    
           };    
timeout 的取值有以下 3 中情况:
    timeout == NULL
        永远等待如果捕捉到一个信号则中断此无限期等待。当所指定的描述符中的已准备好或捕捉到一个信号则返回。如果捕捉到一个信号,则 select 返回 -1,errno 设置为 EINTR。 
    timeout->tv_sec == 0 && timeout->tv_usec == 0
        根本不等待。测试所有指定的描述符并立即返回。这是轮询系统找到多个描述符状态而不阻塞 select 函数的方法。
    timeout->tv_sec != 0 || timeout->tv_usec != 0
        等待指定的秒数和微妙数。当指定的描述符之一已准备好,或当指定的时间值已经超过时立即返回。如果在超时到期时还没有一个描述符准备好,则返回值是 0。(如果系统不提供微秒级的精度,则 timeout->tv_usec 值取整到最近的支持值)与第一种情况一样,这种等待可被捕捉到的信号中断。

(2)中间 3 个参数 readfds(读)、writefds(写) 和 exceptfds(异常) 是指向描述符集的指针。

这 3 个描述符集说明了我们关心的可读、可写或处于异常条件的描述符集合。每个描述符集存储在一个 fd_set 数据类型中。这个数据类型是由实现选择的,它可以为每一个可能的描述符保持以为。我们可以认为它只是一个很大的字节数组。


重点来了,对于 fd_set 数据类型,唯一可以进行的处理是:分配一个这种类型的变量,将这种类型的一个变量值赋给同类型的另一个变量,或对这种类型的变量使用以下 4 个函数中的一个。
       void FD_CLR(int fd, fd_set *set);  //
       int  FD_ISSET(int fd, fd_set *set);
       void FD_SET(int fd, fd_set *set);
       void FD_ZERO(fd_set *set);
这些接口可实现为宏或函数。调用 FD_ZERO 将一个 fd_set 变量的所有位设置为 0要开启描述符集中的一位,可以调用 FD_SET调用 FD_CLR 可以清除一位。最后,可以调用 FD_ISSET 测试描述符集中的一个指定位是否已打开
在声明了一个描述符集之后,必须用 FD_ZERO 将这个描述符集置为 0,然后在其中设置我们关心的各个描述符的位。具体操作如下所示:
fd_set rset;
int fd;
FD_ZERO (&rset);
FD_SET (fd, &rset);
FD_SET (STDIN_FILEND, &rset);
从 select 返回时,可以用 FD_ISSET 测试该集中的一个给定位是否仍处于打开状态。
if (FD_ISSET (fd, &rset))
{
	....
}
select 的中间 3 个参数(指向描述符集的指针)中的任意一个(或全部)可以是空指针,这表示对相应条件并不关心。如果所有 3 个指针都是 NULL,则 select 提供了比 sleep 更精确的定时器。

(3)select 第一个参数 nfds 的意思是“最大文件描述符编号值加 1”

考虑所有 3 个描述符集,在 3 个描述符集中找出最大描述符编号值,然后加 1,这就是第一个参数值。
也可将第一个参数设置为 FD_SETSIZE,这是 <sys/select.h> 中的一个常量,它指定最大描述符数(经常是 1024),但是对大多数应用程序而言,此值太大了。
root@ubuntu:/usr/include# grep "FD_SETSIZE" * -rn
i386-linux-gnu/sys/select.h:79:#define	FD_SETSIZE		__FD_SETSIZE
i386-linux-gnu/bits/typesizes.h:63:#define	__FD_SETSIZE		1024

2、返回值

select 有 3 个可能的返回值。

(1)返回值 -1表示出错。

这是可能发生的,例如,在所指定的描述符一个都没准备好时捕捉到一个信号。在此情况下,一个描述符集都不修改。
错误码有:
       EBADF  An invalid file descriptor was given in one of the sets.  (Perhaps a file descriptor that was already closed, or one on which
              an error has occurred.)

       EINTR  A signal was caught; see signal(7).

       EINVAL nfds is negative or the value contained within timeout is invalid.

       ENOMEM unable to allocate memory for internal tables.

(2)返回值 0 表示没有描述符准备好。

若指定的描述符一个都没准备好,指定的时间就过去了,那么就会发生这种情况。此时,所有描述符集都会置 0.

(3)一个正返回值说明了已经准备好的描述符数。

该值是 3 个描述符集中已准备好的描述符数之和,所以如果同一描述符已准备好读和写,那么在返回值中会对其计两次数。在这种情况下,3 个描述符集中仍旧打开对的位对应于已准备好的描述符。

  对于“准备好”的含义要作一些更具体的说明。
    若对读集(readfds)中的一个描述符进行的 read 操作不会阻塞,则认为此描述符是准备好的。
    若对写集(writefds)中的一个描述符进行的 write 操作不会阻塞,则认为此描述符是准备好的。
    若对异常条件集(exceptfds)中的一个描述符有一个未决异常条件,则认为此描述符是准备好的。(现在,异常条件包括:网络链接上到达带外的数据,或者在处于数据包模式的伪终端上发生了某些条件)。
    对于读、写和异常条件,普通文件的文件描述符总是返回准备好。

3、示例说明

示例一:用来循环读取键盘输入
#include <stdio.h>
#include <sys/select.h>
#include <time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>

int main (void)
{
	int keyboard;
	int ret, i;
	char c;
	fd_set readfd;
	struct timeval timeout;
	//打开 /dev/tty 只读非阻塞
	keyboard = open ("/dev/tty", O_RDONLY | O_NONBLOCK);
	//assert 断言宏
	assert (keyboard > 0);

	while (1)
	{
		timeout.tv_sec = 5;
		timeout.tv_usec = 0;
		FD_ZERO (&readfd);
		FD_SET (keyboard, &readfd);
		ret = select (keyboard + 1, &readfd, NULL, NULL, &timeout);
		//select error when ret = -1
		if (ret == -1)
			perror ("select error");
  
    //data coming when ret>0
		else if (ret)
		{
			if (FD_ISSET (keyboard, &readfd))
			{
				i = read (keyboard, &c, 1);
				if ('\n' == c)
					continue;
				printf ("the input is %c\n", c);
				
				if ('q' == c)
				   break;	
			}
		}
    //time out when ret = 0
		else if (ret == 0)
			printf ("time out\n");
	}
}

输出结果:
(5秒内操作的话)
s
the input is s
d
the input is d
f
the input is f
q
the input is q
(结束)

(5秒内不操作的话)
time out
time out
time out
time out
//示例二:通过select系统调用进行io多路切换,实现异步读取串口数据
#include<stdio.h>  
#include<stdlib.h>  
#include<unistd.h>  
#include<sys/types.h>  
#include<sys/stat.h>  
#include<sys/signal.h>  
#include<fcntl.h>  
#include<termios.h>  
#include<errno.h>  
#include <string.h>
  
#define FALSE -1  
#define TRUE 0  
#define flag 1  
#define noflag 0  
  
int wait_flag = noflag;  
int STOP = 0;  
int res;  
  
int speed_arr[] =  
  { B38400, B19200, B9600, B4800, B2400, B1200, B300, B38400, B19200, B9600,  
B4800, B2400, B1200, B300, };  
int name_arr[] =  
  { 38400, 19200, 9600, 4800, 2400, 1200, 300, 38400, 19200, 9600, 4800, 2400,  
1200, 300, };  
void  
set_speed (int fd, int speed)  
{  
  int i;  
  int status;  
  struct termios Opt;  
  tcgetattr (fd, &Opt);  
  for (i = 0; i < sizeof (speed_arr) / sizeof (int); i++)  
    {  
      if (speed == name_arr[i])  
    {  
      tcflush (fd, TCIOFLUSH);  
      cfsetispeed (&Opt, speed_arr[i]);  
      cfsetospeed (&Opt, speed_arr[i]);  
      status = tcsetattr (fd, TCSANOW, &Opt);  
      if (status != 0)  
        {  
          perror ("tcsetattr fd1");  
          return;  
        }  
      tcflush (fd, TCIOFLUSH);  
    }  
    }  
}  
  
int  
set_Parity (int fd, int databits, int stopbits, int parity)  
{  
  struct termios options;  
  if (tcgetattr (fd, &options) != 0)  
    {  
      perror ("SetupSerial 1");  
      return (FALSE);  
    }  
  options.c_cflag &= ~CSIZE;  
  switch (databits)  
    {  
    case 7:  
      options.c_cflag |= CS7;  
      break;  
    case 8:  
      options.c_cflag |= CS8;  
      break;  
    default:  
      fprintf (stderr, "Unsupported data size\n");  
      return (FALSE);  
    }  
  switch (parity)  
    {  
    case 'n':  
    case 'N':  
      options.c_cflag &= ~PARENB;   /* Clear parity enable */  
      options.c_iflag &= ~INPCK;    /* Enable parity checking */  
      break;  
    case 'o':  
    case 'O':  
      options.c_cflag |= (PARODD | PARENB);  
      options.c_iflag |= INPCK; /* Disnable parity checking */  
      break;  
    case 'e':  
    case 'E':  
      options.c_cflag |= PARENB;    /* Enable parity */  
      options.c_cflag &= ~PARODD;  
      options.c_iflag |= INPCK; /* Disnable parity checking */  
      break;  
    case 'S':  
    case 's':           /*as no parity */  
      options.c_cflag &= ~PARENB;  
      options.c_cflag &= ~CSTOPB;  
      break;  
    default:  
      fprintf (stderr, "Unsupported parity\n");  
      return (FALSE);  
    }  
  
  switch (stopbits)  
    {  
    case 1:  
      options.c_cflag &= ~CSTOPB;  
      break;  
    case 2:  
      options.c_cflag |= CSTOPB;  
      break;  
    default:  
      fprintf (stderr, "Unsupported stop bits\n");  
      return (FALSE);  
    }  
  /* Set input parity option */  
  if (parity != 'n')  
    options.c_iflag |= INPCK;  
  tcflush (fd, TCIFLUSH);  
  options.c_cc[VTIME] = 150;  
  options.c_cc[VMIN] = 0;   /* Update the options and do it NOW */  
  if (tcsetattr (fd, TCSANOW, &options) != 0)  
    {  
      perror ("SetupSerial 3");  
      return (FALSE);  
    }  
  return (TRUE);  
}  
  
void  
signal_handler_IO (int status)  
{  
  printf ("received SIGIO signale.\n");  
  wait_flag = noflag;  
}  
  
int  
main ()  
{  
  printf ("This program updates last time at %s   %s\n", __TIME__, __DATE__);  
  printf ("STDIO COM1\n");  
  int fd;  
  fd = open ("/dev/ttyUSB0", O_RDWR);  
  if (fd == -1)  
    {  
      perror ("serialport error\n");  
    }  
  else  
    {  
      printf ("open ");  
      printf ("%s", ttyname (fd));  
      printf (" succesfully\n");  
    }  
  
  set_speed (fd, 115200);  
  if (set_Parity (fd, 8, 1, 'N') == FALSE)  
    {  
      printf ("Set Parity Error\n");  
      exit (0);  
    }  
  
  char buf[255];  
  fd_set rd;  
  int nread = 0;  
  while(1)  
  {  
    FD_ZERO(&rd);  
    FD_SET(fd, &rd);  
    while(FD_ISSET(fd, &rd))  
    {  
        if(select(fd+1, &rd, NULL,NULL,NULL) < 0)  
        {  
            perror("select error\n");  
        }  
        else  
        {  
            while((nread = read(fd, buf, sizeof(buf))) > 0)  
            {  
                printf("nread = %d,%s\n",nread, buf);  
                printf("test\n");  
                memset(buf, 0 , sizeof(buf));  
            }  
        }  
    }  
  }  
  close (fd);  
  return 0;  
}  

4、示例解析

示例一:用来循环读取键盘输入。

常见的程序片段:
fs_set readset;
FD_ZERO(&readset);
FD_SET(fd,&readset);
select(fd+1,&readset,NULL,NULL,NULL);
if(FD_ISSET(fd,readset){……}
通过 select 返回值,做判断语句。
负值,select 错误;正值,某些文件可读、写或异常;0,等待超时,没有可读、写或异常的文件。

示例二:通过select系统调用进行 io 多路切换,实现异步读取串口数据

首先是串口读取数据,配置是怎么回事? 然后你要了解串口通信。
这个之前有彻底的总结过的, 从新开一篇文章说道说道,在此不做过多解释。

串口编程,除了通过 select 系统调用,在没有数据时阻塞进程,串口有数据需要读时唤醒进程。
还有两种方法,首先是最简单的循环读取程序第二个是通过软中断方式,使用信号 signal 机制读取串口,这里需要注意的是硬件中断是设备驱动层级的,而读写串口是用户级行为,只能通过信号机制模拟中断,信号机制的发生和处理其实于硬件中断无异。
//代码一:循环读取数据
#include<stdio.h>  
#include<stdlib.h>  
#include<unistd.h>  
#include<sys/types.h>  
#include<sys/stat.h>  
#include<fcntl.h>  
#include<termios.h>  
#include<errno.h>  
  
#define FALSE -1  
#define TRUE 0  
  
int speed_arr[] = { B38400, B19200, B9600, B4800, B2400, B1200, B300,B38400, B19200, B9600, B4800, B2400, B1200, B300, };  
int name_arr[] = {38400,  19200,  9600,  4800,  2400,  1200,  300, 38400, 19200,  9600, 4800, 2400, 1200,  300, };  
void set_speed(int fd, int speed){  
  int   i;   
  int   status;   
  struct termios   Opt;  
  tcgetattr(fd, &Opt);   
  for ( i= 0;  i < sizeof(speed_arr) / sizeof(int);  i++) {   
    if  (speed == name_arr[i]) {       
      tcflush(fd, TCIOFLUSH);       
      cfsetispeed(&Opt, speed_arr[i]);    
      cfsetospeed(&Opt, speed_arr[i]);     
      status = tcsetattr(fd, TCSANOW, &Opt);    
      if  (status != 0) {          
        perror("tcsetattr fd1");    
        return;       
      }      
      tcflush(fd,TCIOFLUSH);     
    }    
  }  
}  
  
int set_Parity(int fd,int databits,int stopbits,int parity)  
{   
    struct termios options;   
    if  ( tcgetattr( fd,&options)  !=  0) {   
        perror("SetupSerial 1");       
        return(FALSE);    
    }  
    options.c_cflag &= ~CSIZE;   
    switch (databits)   
    {     
    case 7:       
        options.c_cflag |= CS7;   
        break;  
    case 8:       
        options.c_cflag |= CS8;  
        break;     
    default:      
        fprintf(stderr,"Unsupported data size\n"); return (FALSE);    
    }  
    switch (parity)   
    {     
        case 'n':  
        case 'N':      
            options.c_cflag &= ~PARENB;   /* Clear parity enable */  
            options.c_iflag &= ~INPCK;     /* Enable parity checking */   
            break;    
        case 'o':     
        case 'O':       
            options.c_cflag |= (PARODD | PARENB);   
            options.c_iflag |= INPCK;             /* Disnable parity checking */   
            break;    
        case 'e':    
        case 'E':     
            options.c_cflag |= PARENB;     /* Enable parity */      
            options.c_cflag &= ~PARODD;      
            options.c_iflag |= INPCK;       /* Disnable parity checking */  
            break;  
        case 'S':   
        case 's':  /*as no parity*/     
            options.c_cflag &= ~PARENB;  
            options.c_cflag &= ~CSTOPB;break;    
        default:     
            fprintf(stderr,"Unsupported parity\n");      
            return (FALSE);    
        }    
      
    switch (stopbits)  
    {     
        case 1:      
            options.c_cflag &= ~CSTOPB;    
            break;    
        case 2:      
            options.c_cflag |= CSTOPB;    
           break;  
        default:      
             fprintf(stderr,"Unsupported stop bits\n");    
             return (FALSE);   
    }   
    /* Set input parity option */   
    if (parity != 'n')     
        options.c_iflag |= INPCK;   
    tcflush(fd,TCIFLUSH);  
    options.c_cc[VTIME] = 150;   
    options.c_cc[VMIN] = 0; /* Update the options and do it NOW */  
    if (tcsetattr(fd,TCSANOW,&options) != 0)     
    {   
        perror("SetupSerial 3");     
        return (FALSE);    
    }   
    return (TRUE);    
}  
  
int main()  
{  
    printf("This program updates last time at %s   %s\n",__TIME__,__DATE__);  
    printf("STDIO COM1\n");  
    int fd;  
    fd = open("/dev/ttyS0",O_RDWR);  
    if(fd == -1)  
    {  
        perror("serialport error\n");  
    }  
    else  
    {  
        printf("open ");  
        printf("%s",ttyname(fd));  
        printf(" succesfully\n");  
    }  
  
    set_speed(fd,115200);  
    if (set_Parity(fd,8,1,'N') == FALSE)  {  
        printf("Set Parity Error\n");  
        exit (0);  
    }  
    char buf[] = "fe55aa07bc010203040506073d";  
    write(fd,&buf,26);  
    char buff[512];   
    int nread;    
    while(1)  
    {  
        if((nread = read(fd, buff, 512))>0)  
        {  
            printf("\nLen: %d\n",nread);  
            buff[nread+1] = '\0';  
            printf("%s",buff);  
        }  
    }  
    close(fd);  
    return 0;  
}  
//代码二:通过signal机制读取数据
#include<stdio.h>  
#include<stdlib.h>  
#include<unistd.h>  
#include<sys/types.h>  
#include<sys/stat.h>  
#include<sys/signal.h>  
#include<fcntl.h>  
#include<termios.h>  
#include<errno.h>  
  
#define FALSE -1  
#define TRUE 0  
#define flag 1  
#define noflag 0  
  
int wait_flag = noflag;  
int STOP = 0;  
int res;  
  
int speed_arr[] =  
  { B38400, B19200, B9600, B4800, B2400, B1200, B300, B38400, B19200, B9600,  
B4800, B2400, B1200, B300, };  
int name_arr[] =  
  { 38400, 19200, 9600, 4800, 2400, 1200, 300, 38400, 19200, 9600, 4800, 2400,  
1200, 300, };  
void  
set_speed (int fd, int speed)  
{  
  int i;  
  int status;  
  struct termios Opt;  
  tcgetattr (fd, &Opt);  
  for (i = 0; i < sizeof (speed_arr) / sizeof (int); i++)  
    {  
      if (speed == name_arr[i])  
    {  
      tcflush (fd, TCIOFLUSH);  
      cfsetispeed (&Opt, speed_arr[i]);  
      cfsetospeed (&Opt, speed_arr[i]);  
      status = tcsetattr (fd, TCSANOW, &Opt);  
      if (status != 0)  
        {  
          perror ("tcsetattr fd1");  
          return;  
        }  
      tcflush (fd, TCIOFLUSH);  
    }  
    }  
}  
  
int  
set_Parity (int fd, int databits, int stopbits, int parity)  
{  
  struct termios options;  
  if (tcgetattr (fd, &options) != 0)  
    {  
      perror ("SetupSerial 1");  
      return (FALSE);  
    }  
  options.c_cflag &= ~CSIZE;  
  switch (databits)  
    {  
    case 7:  
      options.c_cflag |= CS7;  
      break;  
    case 8:  
      options.c_cflag |= CS8;  
      break;  
    default:  
      fprintf (stderr, "Unsupported data size\n");  
      return (FALSE);  
    }  
  switch (parity)  
    {  
    case 'n':  
    case 'N':  
      options.c_cflag &= ~PARENB;   /* Clear parity enable */  
      options.c_iflag &= ~INPCK;    /* Enable parity checking */  
      break;  
    case 'o':  
    case 'O':  
      options.c_cflag |= (PARODD | PARENB);  
      options.c_iflag |= INPCK; /* Disnable parity checking */  
      break;  
    case 'e':  
    case 'E':  
      options.c_cflag |= PARENB;    /* Enable parity */  
      options.c_cflag &= ~PARODD;  
      options.c_iflag |= INPCK; /* Disnable parity checking */  
      break;  
    case 'S':  
    case 's':           /*as no parity */  
      options.c_cflag &= ~PARENB;  
      options.c_cflag &= ~CSTOPB;  
      break;  
    default:  
      fprintf (stderr, "Unsupported parity\n");  
      return (FALSE);  
    }  
  
  switch (stopbits)  
    {  
    case 1:  
      options.c_cflag &= ~CSTOPB;  
      break;  
    case 2:  
      options.c_cflag |= CSTOPB;  
      break;  
    default:  
      fprintf (stderr, "Unsupported stop bits\n");  
      return (FALSE);  
    }  
  /* Set input parity option */  
  if (parity != 'n')  
    options.c_iflag |= INPCK;  
  tcflush (fd, TCIFLUSH);  
  options.c_cc[VTIME] = 150;  
  options.c_cc[VMIN] = 0;   /* Update the options and do it NOW */  
  if (tcsetattr (fd, TCSANOW, &options) != 0)  
    {  
      perror ("SetupSerial 3");  
      return (FALSE);  
    }  
  return (TRUE);  
}  
  
void  
signal_handler_IO (int status)  
{  
  printf ("received SIGIO signale.\n");  
  wait_flag = noflag;  
}  
  
int  
main ()  
{  
  printf ("This program updates last time at %s   %s\n", __TIME__, __DATE__);  
  printf ("STDIO COM1\n");  
  int fd;  
  struct sigaction saio;  
  fd = open ("/dev/ttyUSB0", O_RDWR);  
  if (fd == -1)  
    {  
      perror ("serialport error\n");  
    }  
  else  
    {  
      printf ("open ");  
      printf ("%s", ttyname (fd));  
      printf (" succesfully\n");  
    }  
  
  saio.sa_handler = signal_handler_IO;  
  sigemptyset (&saio.sa_mask);  
  saio.sa_flags = 0;  
  saio.sa_restorer = NULL;  
  sigaction (SIGIO, &saio, NULL);  
  
  //allow the process to receive SIGIO  
  fcntl (fd, F_SETOWN, getpid ());  
  //make the file descriptor asynchronous  
  fcntl (fd, F_SETFL, FASYNC);  
  
  set_speed (fd, 115200);  
  if (set_Parity (fd, 8, 1, 'N') == FALSE)  
    {  
      printf ("Set Parity Error\n");  
      exit (0);  
    }  
  
  char buf[255];  
while (STOP == 0)  
    {  
      usleep (100000);  
      /* after receving SIGIO ,wait_flag = FALSE,input is availabe and can be read */  
      if (wait_flag == 0)  
    {  
      memset (buf, 0, sizeof(buf));  
      res = read (fd, buf, 255);  
      printf ("nread=%d,%s\n", res, buf);  
//    if (res ==1)  
//      STOP = 1;       /*stop loop if only a CR was input */  
      wait_flag = flag; /*wait for new input */  
    }  
    }  
  
  
  close (fd);  
  return 0;  
}  

5、select 函数实现

下面我们分两个过程来分析select:

(1) select的睡眠过程

支持阻塞操作的设备驱动通常会实现一组自身的等待队列如读/写等待队列用于支持上层(用户层)所需的 BLOCK 或NONBLOCK 操作。当应用程序通过设备驱动访问该设备时(默认为 BLOCK 操作),若该设备当前没有数据可读或写,则将该用户进程插入到该设备驱动对应的读/写等待队列让其睡眠一段时间,等到有数据可读/写时再将该进程唤醒。
select 就是巧妙的利用等待队列机制让用户进程适当在没有资源可读/写时睡眠,有资源可读/写时唤醒。下面我们看看 select 睡眠的详细过程。
select 会循环遍历它所监测的 fd_set 内的所有文件描述符对应的驱动程序的 poll 函数。驱动程序提供的 poll 函数首先会将调用 select 的用户进程插入到该设备驱动对应资源的等待队列(如读/写等待队列),然后返回一个 bitmask 告诉 select 当前资源哪些可用。当 select 循环遍历完所有 fd_set 内指定的文件描述符对应的 poll 函数后,如果没有一个资源可用(即没有一个文件可供操作),则 select 让该进程睡眠,一直等到有资源可用为止,进程被唤醒(或者timeout )继续往下执行。

下面分析一下代码是如何实现的。
select 的调用 path 如下:sys_select -> core_sys_select -> do_select
查看 kernel/fs/select.c 
其中最重要的函数是 do_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();
         retval = max_select_fd(n, fds);
         rcu_read_unlock();
 
         if (retval < 0)
                   return retval;
         n = retval;
 
         poll_initwait(&table);
         wait = &table.pt;
         if (!*timeout)
                   wait = NULL;
         retval = 0;        //retval用于保存已经准备好的描述符数,初始为0
         for (;;) {
                   unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
                   long __timeout;
 
                   set_current_state(TASK_INTERRUPTIBLE);    //将当前进程状态改为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) { //遍历每个描述符
                            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);
                                     if (file) {
                                               f_op = file->f_op;
                                               MARK(fs_select, "%d %lld",
                                                                 i, (long long)*timeout);
                                               mask = DEFAULT_POLLMASK;
                                               if (f_op && f_op->poll)
/* 在这里循环调用所监测的fd_set内的所有文件描述符对应的驱动程序的poll函数 */
                                                        mask = (*f_op->poll)(file, retval ? NULL : wait);
                                               fput_light(file, fput_needed);
                                               if ((mask & POLLIN_SET) && (in & bit)) {
                                                        res_in |= bit; //如果是这个描述符可读, 将这个位置位
                                                        retval++;  //返回描述符个数加1
                                               }
                                               if ((mask & POLLOUT_SET) && (out & bit)) {
                                                        res_out |= bit;
                                                        retval++;
                                               }
                                               if ((mask & POLLEX_SET) && (ex & bit)) {
                                                        res_ex |= bit;
                                                        retval++;
                                               }
                                     }
                                     cond_resched();
                            }
//返回结果
                            if (res_in)
                                     *rinp = res_in;
                            if (res_out)
                                     *routp = res_out;
                            if (res_ex)
                                     *rexp = res_ex;
                   }
                   wait = NULL;
/* 到这里遍历结束。retval保存了检测到的可操作的文件描述符的个数。如果有文件可操作,则跳出for(;;)循环,直接返回。若没有文件可操作且timeout时间未到同时没有收到signal,则执行schedule_timeout睡眠。睡眠时间长短由__timeout决定,一直等到该进程被唤醒。
那该进程是如何被唤醒的?被谁唤醒的呢?
我们看下面的select唤醒过程*/
                   if (retval || !*timeout || signal_pending(current))
                            break;
                  if(table.error) {
                            retval = table.error;
                            break;
                   }
 
                   if (*timeout < 0) {
                            /* Wait indefinitely */
                            __timeout = MAX_SCHEDULE_TIMEOUT;
                   } else if (unlikely(*timeout >= (s64)MAX_SCHEDULE_TIMEOUT - 1)) {
                            /* Wait for longer than MAX_SCHEDULE_TIMEOUT. Do it in a loop */
                            __timeout = MAX_SCHEDULE_TIMEOUT - 1;
                            *timeout -= __timeout;
                   } else {
                            __timeout = *timeout;
                            *timeout = 0;
                   }
                   __timeout = schedule_timeout(__timeout);
                   if (*timeout >= 0)
                            *timeout += __timeout;
         }
         __set_current_state(TASK_RUNNING);
 
         poll_freewait(&table);
 
         return retval;
}

(2)select的唤醒过程

前面介绍了 select 会循环遍历它所监测的 fd_set 内的所有文件描述符对应的驱动程序的 poll 函数。驱动程序提供的poll 函数首先会将调用 select 的用户进程插入到该设备驱动对应资源的等待队列(如读/写等待队列),然后返回一个 bitmask 告诉 select 当前资源哪些可用。
一个典型的驱动程序poll函数实现如下:
(摘自《Linux Device Drivers – ThirdEdition》Page 165)
static unsigned int scull_p_poll(struct file *filp, poll_table *wait)
{
    struct scull_pipe *dev = filp->private_data;
    unsigned int mask = 0;
    /*
     * The buffer is circular; it is considered full
     * if "wp" is right behind "rp" and empty if the
     * two are equal.
     */
    down(&dev->sem);
    poll_wait(filp, &dev->inq,  wait);
    poll_wait(filp, &dev->outq, wait);
    if (dev->rp != dev->wp)
        mask |= POLLIN | POLLRDNORM;    /* readable */
    if (spacefree(dev))
        mask |= POLLOUT | POLLWRNORM;   /* writable */
    up(&dev->sem);
    return mask;
}
将用户进程插入驱动的等待队列是通过poll_wait做的。
Poll_wait定义如下:
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);
}
这里的p->qproc在do_select内poll_initwait(&table)被初始化为__pollwait,如下:
void poll_initwait(struct poll_wqueues *pwq)
{
         init_poll_funcptr(&pwq->pt, __pollwait);
         pwq->error = 0;
         pwq->table = NULL;
         pwq->inline_index = 0;
}
__pollwait定义如下:
/* Add a new entry */
static void __pollwait(struct file *filp, wait_queue_head_t *wait_address,
                                     poll_table *p)
{
         struct poll_table_entry *entry = poll_get_entry(p);
         if (!entry)
                   return;
         get_file(filp);
         entry->filp = filp;
         entry->wait_address = wait_address;
         init_waitqueue_entry(&entry->wait, current);
         add_wait_queue(wait_address,&entry->wait);
}
通过 init_waitqueue_entry 初始化一个等待队列项,这个等待队列项关联的进程即当前调用 select 的进程。然后将这个等待队列项插入等待队列 wait_address。Wait_address 即在驱动 poll 函数内调用 poll_wait(filp, &dev->inq,  wait);时传入的该驱动的 &dev->inq 或者 &dev->outq 等待队列。

注: 关于等待队列的工作原理可以参考下面这篇文档:
参看:等待队列(二)

到这里我们明白了select如何当前进程插入所有所监测的fd_set关联的驱动内的等待队列,那进程究竟是何时让出CPU进入睡眠状态的呢?
进入睡眠状态是在do_select内调用schedule_timeout(__timeout)实现的。当select遍历完fd_set内的所有设备文件,发现没有文件可操作时(即retval=0),则调用schedule_timeout(__timeout)进入睡眠状态。
唤醒该进程的过程通常是在所监测文件的设备驱动内实现的,驱动程序维护了针对自身资源读写的等待队列。当设备驱动发现自身资源变为可读写并且有进程睡眠在该资源的等待队列上时,就会唤醒这个资源等待队列上的进程。
举个例子,比如内核的8250 uart driver:
Uart是使用的Tty层维护的两个等待队列, 分别对应于读和写: (uart是tty设备的一种)

struct tty_struct {
         ……
         wait_queue_head_t write_wait;
         wait_queue_head_t read_wait;
         ……
}
当uart设备接收到数据,会调用tty_flip_buffer_push(tty);将收到的数据push到tty层的buffer。
然后查看是否有进程睡眠的读等待队列上,如果有则唤醒该等待会列。
过程如下:

serial8250_interrupt -> serial8250_handle_port -> receive_chars -> tty_flip_buffer_push ->
flush_to_ldisc -> disc->receive_buf
在disc->receive_buf函数内:
if (waitqueue_active(&tty->read_wait)) //若有进程阻塞在read_wait上则唤醒
wake_up_interruptible(&tty->read_wait);
到这里明白了select进程被唤醒的过程。由于该进程是阻塞在所有监测的文件对应的设备等待队列上的,因此在timeout时间内,只要任意个设备变为可操作,都会立即唤醒该进程,从而继续往下执行。这就实现了select的当有一个文件描述符可操作时就立即唤醒执行的基本原理。

6、函数 pselect

#include <sys/select.h>
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
                   fd_set *exceptfds, const struct timespec *timeout,
                   const sigset_t *sigmask);

(1)pselect 与 select 不同之处

select 的超时值用 timeval 结构指定,但 pselect 使用 timespec 结构。timespec 结构以秒和纳秒表示超时值,而非秒和微妙。如果平台支持这样的时间精度,那么 timespec 就能提供精确的超时时间。
结构体 timespec 定义如下:
struct timespec {
               long    tv_sec;         /* seconds */
               long    tv_nsec;        /* nanoseconds */
           };
pselect 的超时值被声明为 const,这保证了调用 pselect 不会改变此值。
pselect 可使用可选信号屏蔽字。若 sigmask 为 NULL,那么在与信号有关的方面,pselect 的运行状况和 select 相同。否则,sigmask 指向一信号屏蔽字,在调用 pselect 时,以原子操作的方式安装该信号屏蔽字。在返回时,恢复以前的信号屏蔽字。

(2)示例说明

参看:pselect()
#include        <time.h>
#include        <stdio.h>
#include        <stdlib.h>
#include        <signal.h>
#include        <unistd.h>
#include        <sys/select.h>
#define BUFFSIZE 80
void sig_int(int signo);
void err_sys(const char *p_error);
void sig_alrm(int signo)
{
    char s[] = "receive";
    psignal(signo, s);
    return;
}
int
main(int argc, char **argv)
{
        int             maxfdp1;
        fd_set          rset;
        sigset_t        sigmask;
        ssize_t         nread;
        char            buf[BUFFSIZE];
      sigset_t sigset;
   struct sigaction act;
   // set SIGALRM signal handler
   act.sa_handler = sig_alrm;
   if (sigemptyset(&act.sa_mask) == -1)       
    err_sys("sigemptyset");
   act.sa_flags = 0;
   if (sigaction(SIGALRM, &act, NULL) == -1)
    err_sys("sigaction");
   // initialize signal set and addition SIGALRM into sigset
   if (sigemptyset(&sigset) == -1)
    err_sys("sigemptyet");
   if (sigaddset(&sigset, SIGALRM) == -1)
    err_sys("sigaddset");
   alarm(1);    
        FD_ZERO(&rset);
        FD_SET(STDIN_FILENO, &rset);
        maxfdp1 = STDIN_FILENO + 1;
        if (pselect(maxfdp1, &rset, NULL, NULL, NULL, &sigset) <= 0)
                err_sys("pselect error");
        if (FD_ISSET(STDIN_FILENO, &rset))
  {
                if ((nread = read(STDIN_FILENO, buf, BUFFSIZE)) == -1)
                        err_sys("read error");
                if (write(STDOUT_FILENO, buf, nread) != nread)
                        err_sys("write error");
        }
        exit(0);
}
void
sig_int(int signo)
{
        char    s[] = "received";
        psignal(signo, s);
        return;
}
void
err_sys(const char *p_error)
{
        perror(p_error);
        exit(1);
}
输出结果:
d
receive: Alarm clock
d
上段代码如果没有 CTRL+C 送上一个 SIGINT 信号,将永远阻塞在与用户的交互上,ALARM 产生的 SIGALRM 信号永远打断不了 PSELECT, ALARM 信号被成功屏蔽。

三、函数 poll

poll 函数类似于 select,但是程序员结构有所不同。虽然 poll 函数时 system V 引入进来支持 STREAMS 子系统的,但是 poll 函数可用于任何类型的文件描述符
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
返回值:准备就绪的描述符数目;若超时,返回 0;若出错,返回 -1.

1、参数解析

与 select 不同,poll 不是为每个条件(可读性、可写性和异常条件)构造一个描述符集,而是构造一个 pollfd 结构的数组,每个数组元素指定一个描述符 编号以及我们队该描述符感兴趣的条件
struct pollfd {
               int   fd;         /* file descriptor */
               short events;     /* requested events */
               short revents;    /* returned events */
           };
fds 数组中的元素数由 nfds 指定。


应将每个数组元素的 events 成员设置为上图所示值的一个或几个,通过这些值告诉内核我们关心的是每个描述符的哪些事件。返回时,revnets 成员由内核设置,用于说明每个描述符发生了哪些事件。
注意,poll 没有更改 events 成员。这与 select 不同,select 修改其参数以指示哪个描述符已准备好了。
上图中的前 4 行测试的是可读性,接下来的 3 行测试的是可写性,最后 3 行测试的是异常条件。最后 3 行是由内核在返回时设置的。即使在 events 字段中没有指定这 3 个值,如果是相应条件发生,在 revents 中也会返回它们。
当一个描述符被挂断(POLLHUP)后,就不能再写该描述符,但是有可能仍然可以从该描述符读取到数据。

POLLIN | POLLPRI 等价于 select() 的读事件,POLLOUT |POLLWRBAND 等价于 select() 的写事件。POLLIN 等价于 POLLRDNORM |POLLRDBAND,而 POLLOUT 则等价于 POLLWRNORM。
查看 /usr/include/asm-generic/poll.h 可看到如下定义:
/* These are specified by iBCS2 */
#define POLLIN		0x0001
#define POLLPRI		0x0002
#define POLLOUT		0x0004
#define POLLERR		0x0008
#define POLLHUP		0x0010
#define POLLNVAL	0x0020

/* The rest seem to be more-or-less nonstandard. Check them! */
#define POLLRDNORM	0x0040
#define POLLRDBAND	0x0080
#ifndef POLLWRNORM
#define POLLWRNORM	0x0100
#endif
#ifndef POLLWRBAND
#define POLLWRBAND	0x0200
#endif

poll 的最后一个参数指定的是我们愿意等待多长时间。如同 select 一样,有 3 种不同的情形。
  timeout == -1
        永远等待。(某些系统在 <stropts.h>中定义了常量 INFTIM,其值通常是 -1)当所指定的描述符中的一个已准备好,或捕捉到一个信号时返回。如果捕捉到一个信号,则 poll 返回 -1,errno 设置为 EINTR。
    timeout == 0
        不等待。测试所有描述符并立即返回。这是一种轮询系统的方法,可以找到多个描述符的状态而不阻塞 poll 函数。
    timeout > 0
        等待 timeout 毫秒。当指定的描述符之一已准备好,或 timeout 到期时还没有一个描述符准备好,则返回值是 0.(如果系统不提供毫秒级精度,则 timeout 值取整到最近的支持值)。

2、返回值和错误码

成功时,poll()返回结构体中 revents 域不为 0 的文件描述符个数;
如果在超时前没有任何事件发生,poll() 返回 0;
失败时,poll()返回 -1,并设置 errno 为下列值之一:
  EBADF         一个或多个结构体中指定的文件描述符无效。
  EFAULTfds   指针指向的地址超出进程的地址空间。
  EINTR      请求的事件之前产生一个信号,调用可以重新发起。
  EINVALnfds  参数超出PLIMIT_NOFILE值。
  ENOMEM       可用内存不足,无法完成请求。

3、示例说明

扩展:poll 的例子

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>

#include <poll.h>
#include <fcntl.h>
#include <unistd.h>
 
#define MAX_BUFFER_SIZE 1024
#define IN_FILES 3
#define TIME_DELAY 60*5
#define MAX(a,b) ((a>b)?(a):(b))
 
int main(int argc ,char **argv)
{
  struct pollfd fds[IN_FILES];
  char buf[MAX_BUFFER_SIZE];
  int i,res,real_read, maxfd;
  fds[0].fd = 0;
  if((fds[1].fd=open("data1",O_RDONLY|O_NONBLOCK)) < 0)
  {
	  fprintf(stderr,"open data1 error:%s",strerror(errno));
	  return 1;
  }
  if((fds[2].fd=open("data2",O_RDONLY|O_NONBLOCK)) < 0)
  {
	  fprintf(stderr,"open data2 error:%s",strerror(errno));
	  return 1;
  }
  for (i = 0; i < IN_FILES; i++)
  {
      fds[i].events = POLLIN;
  }
  while(fds[0].events || fds[1].events || fds[2].events)
  {
	  if (poll(fds, IN_FILES, TIME_DELAY) <= 0)
	  {
		  printf("Poll error\n");
		  return 1;
	  }
	  for (i = 0; i< IN_FILES; i++)
	  {
		  if (fds[i].revents)
		  {
			  memset(buf, 0, MAX_BUFFER_SIZE);
			  real_read = read(fds[i].fd, buf, MAX_BUFFER_SIZE);
			  if (real_read < 0)
			  {
				  if (errno != EAGAIN)
				  {
					  return 1;
				  }
			  }
			  else if (!real_read)
			  {
				  close(fds[i].fd);
				  fds[i].events = 0;
			  }
			  else
			  {
				  if (i == 0)
				  {
					  if ((buf[0] == 'q') || (buf[0] == 'Q'))
					  {
						  return 1;
					  }
           }
				  else
				  {
					  buf[real_read] = '\0';
					  printf("%s", buf);
				  }
			  }
		  }
	  }
  }
  exit(0);
}
创建:
data1  1234567890
data2  abcdefghij

输出结果:
1234567890
abcdefghij

4、poll函数实现

参看:poll调用深入解析    可自行阅读。

四、epoll 函数

epoll 是在 Linux 2.6 中引入的,在其他类 UNIX 操作系统上不可用。它提供了一个类似于 select(2)和 poll(2) 函数的功能:
select(2) 一次可以监测 FD_SETSIZE 数量大小的描述符,FD_SETSIZE 通常是一个在 libc 编译时指定的小数字。
poll(2) 一次可以监测的描述符数量并没有限制
,但撇开其它因素,我们每次都不得不检查就绪通知,线性扫描所有通过描述符,这样时间复杂度为 O(n)而且很慢。
epoll 没有这些固定限制,也不执行任何线性扫描。因此它可以更高效地执行和处理大量事件。

1、epoll 接口

epoll操作过程需要三个接口,分别如下:
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

(1)epoll_create 

创建一个 epoll 的句柄,size 用来告诉内核这个监听的数目一共有多大。这个参数不同于 select 中的第一个参数,给出最大监听的 fd + 1 的值。需要注意的是,当创建好 epoll 句柄后,它就是会占用一个 fd 值,在 Linux 下如果查看 /proc/进程 id/fd/,是能够看到这个 fd 的,所以在使用完 epoll 后,必须调用 close 关闭,否则可能导致 fd 被耗尽。

(2)epoll_ctl

epoll 的时间注册函数,它不同于 select 是在监听事件时告诉内核要监听什么类型的时间,而是在这里先注册要监听的事件类型。
第一个参数是 epoll_create 的返回值
第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的 fd
第四个参数是告诉内核需要监听什么事,struct epoll_event 结构如下:
//保存触发事件的某个文件描述符相关的数据(与具体使用方式有关)  
  
typedef union epoll_data {  
    void *ptr;  
    int fd;  
    __uint32_t u32;  
    __uint64_t u64;  
} epoll_data_t;  
 //感兴趣的事件和被触发的事件  
struct epoll_event {  
    __uint32_t events; /* Epoll events */  
    epoll_data_t data; /* User data variable */  
};  
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

(3)epoll_wait

等待事件的产生,类似于 select 调用。参数 events 用来从内核得到事件的集合,maxevents 告之内核这个 events 有多大,这个 maxevents 的值不能大于创建 epoll_create 时的 size,参数 timeout 是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回 0 表示已超时。

2、工作模式

epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
    LT模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用 epoll_wait 时,会再次响应应用程序并通知此事件。
    ET模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
    ET模式 在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

3、示例说明

#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <errno.h>  
#include <sys/socket.h>  
#include <netdb.h>  
#include <fcntl.h>  
#include <sys/epoll.h>  
#include <string.h>  
  
#define MAXEVENTS 64  
  
//函数:  
//功能:创建和绑定一个TCP socket  
//参数:端口  
//返回值:创建的socket  
static int  
create_and_bind (char *port)  
{  
  struct addrinfo hints;  
  struct addrinfo *result, *rp;  
  int s, sfd;  
  
  memset (&hints, 0, sizeof (struct addrinfo));  
  hints.ai_family = AF_UNSPEC;     /* Return IPv4 and IPv6 choices */  
  hints.ai_socktype = SOCK_STREAM; /* We want a TCP socket */  
  hints.ai_flags = AI_PASSIVE;     /* All interfaces */  
  
  s = getaddrinfo (NULL, port, &hints, &result);  
  if (s != 0)  
    {  
      fprintf (stderr, "getaddrinfo: %s\n", gai_strerror (s));  
      return -1;  
    }  
  
  for (rp = result; rp != NULL; rp = rp->ai_next)  
    {  
      sfd = socket (rp->ai_family, rp->ai_socktype, rp->ai_protocol);  
      if (sfd == -1)  
        continue;  
  
      s = bind (sfd, rp->ai_addr, rp->ai_addrlen);  
      if (s == 0)  
        {  
          /* We managed to bind successfully! */  
          break;  
        }  
  
      close (sfd);  
    }  
  
  if (rp == NULL)  
    {  
      fprintf (stderr, "Could not bind\n");  
      return -1;  
    }  
  
  freeaddrinfo (result);  
  
  return sfd;  
}  
  
  
//函数  
//功能:设置socket为非阻塞的  
static int  
make_socket_non_blocking (int sfd)  
{  
  int flags, s;  
  
  //得到文件状态标志  
  flags = fcntl (sfd, F_GETFL, 0);  
  if (flags == -1)  
    {  
      perror ("fcntl");  
      return -1;  
    }  
  
  //设置文件状态标志  
  flags |= O_NONBLOCK;  
  s = fcntl (sfd, F_SETFL, flags);  
  if (s == -1)  
    {  
      perror ("fcntl");  
      return -1;  
    }  
  
  return 0;  
}  
  
//端口由参数argv[1]指定  
int  
main (int argc, char *argv[])  
{  
  int sfd, s;  
  int efd;  
  struct epoll_event event;  
  struct epoll_event *events;  
  
  if (argc != 2)  
    {  
      fprintf (stderr, "Usage: %s [port]\n", argv[0]);  
      exit (EXIT_FAILURE);  
    }  
  
  sfd = create_and_bind (argv[1]);  
  if (sfd == -1)  
    abort ();  
  
  s = make_socket_non_blocking (sfd);  
  if (s == -1)  
    abort ();  
  
  s = listen (sfd, SOMAXCONN);  
  if (s == -1)  
    {  
      perror ("listen");  
      abort ();  
    }  
  
  //除了参数size被忽略外,此函数和epoll_create完全相同  
  efd = epoll_create1 (0);  
  if (efd == -1)  
    {  
      perror ("epoll_create");  
      abort ();  
    }  
  
  event.data.fd = sfd;  
  event.events = EPOLLIN | EPOLLET;//读入,边缘触发方式  
  s = epoll_ctl (efd, EPOLL_CTL_ADD, sfd, &event);  
  if (s == -1)  
    {  
      perror ("epoll_ctl");  
      abort ();  
    }  
  
  /* Buffer where events are returned */  
  events = calloc (MAXEVENTS, sizeof event);  
  
  /* The event loop */  
  while (1)  
    {  
      int n, i;  
  
      n = epoll_wait (efd, events, MAXEVENTS, -1);  
      for (i = 0; i < n; i++)  
        {  
          if ((events[i].events & EPOLLERR) ||  
              (events[i].events & EPOLLHUP) ||  
              (!(events[i].events & EPOLLIN)))  
            {  
              /* An error has occured on this fd, or the socket is not 
                 ready for reading (why were we notified then?) */  
              fprintf (stderr, "epoll error\n");  
              close (events[i].data.fd);  
              continue;  
            }  
  
          else if (sfd == events[i].data.fd)  
            {  
              /* We have a notification on the listening socket, which 
                 means one or more incoming connections. */  
              while (1)  
                {  
                  struct sockaddr in_addr;  
                  socklen_t in_len;  
                  int infd;  
                  char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];  
  
                  in_len = sizeof in_addr;  
                  infd = accept (sfd, &in_addr, &in_len);  
                  if (infd == -1)  
                    {  
                      if ((errno == EAGAIN) ||  
                          (errno == EWOULDBLOCK))  
                        {  
                          /* We have processed all incoming 
                             connections. */  
                          break;  
                        }  
                      else  
                        {  
                          perror ("accept");  
                          break;  
                        }  
                    }  
  
                                  //将地址转化为主机名或者服务名  
                  s = getnameinfo (&in_addr, in_len,  
                                   hbuf, sizeof hbuf,  
                                   sbuf, sizeof sbuf,  
                                   NI_NUMERICHOST | NI_NUMERICSERV);//flag参数:以数字名返回  
                                  //主机地址和服务地址  
  
                  if (s == 0)  
                    {  
                      printf("Accepted connection on descriptor %d "  
                             "(host=%s, port=%s)\n", infd, hbuf, sbuf);  
                    }  
  
                  /* Make the incoming socket non-blocking and add it to the 
                     list of fds to monitor. */  
                  s = make_socket_non_blocking (infd);  
                  if (s == -1)  
                    abort ();  
  
                  event.data.fd = infd;  
                  event.events = EPOLLIN | EPOLLET;  
                  s = epoll_ctl (efd, EPOLL_CTL_ADD, infd, &event);  
                  if (s == -1)  
                    {  
                      perror ("epoll_ctl");  
                      abort ();  
                    }  
                }  
              continue;  
            }  
          else  
            {  
              /* We have data on the fd waiting to be read. Read and 
                 display it. We must read whatever data is available 
                 completely, as we are running in edge-triggered mode 
                 and won't get a notification again for the same 
                 data. */  
              int done = 0;  
  
              while (1)  
                {  
                  ssize_t count;  
                  char buf[512];  
  
                  count = read (events[i].data.fd, buf, sizeof(buf));  
                  if (count == -1)  
                    {  
                      /* If errno == EAGAIN, that means we have read all 
                         data. So go back to the main loop. */  
                      if (errno != EAGAIN)  
                        {  
                          perror ("read");  
                          done = 1;  
                        }  
                      break;  
                    }  
                  else if (count == 0)  
                    {  
                      /* End of file. The remote has closed the 
                         connection. */  
                      done = 1;  
                      break;  
                    }  
  
                  /* Write the buffer to standard output */  
                  s = write (1, buf, count);  
                  if (s == -1)  
                    {  
                      perror ("write");  
                      abort ();  
                    }  
                }  
  
              if (done)  
                {  
                  printf ("Closed connection on descriptor %d\n",  
                          events[i].data.fd);  
  
                  /* Closing the descriptor will make epoll remove it 
                     from the set of descriptors which are monitored. */  
                  close (events[i].data.fd);  
                }  
            }  
        }  
    }  
  
  free (events);  
  
  close (sfd);  
  
  return EXIT_SUCCESS;  
}  
在一个终端运行此程序:./a.out PORT
另一个终端:telnet  127.0.0.1 PORT
输出结果:

五、总结

这一章,有点难讲。select 我用过,所以讲起来还好。poll 和 epoll 这两个都没有使用过,而且手头没用相关书籍参看。网上的示例也是以服务器居多。中间经过了端午节,好多东西都忘了总结到哪了。 这部分到,讲网络通信时,再深入研究。
然后,还有几个知识点还要深入去讲。如,阻塞与非阻塞。异步I/O与同步I/O。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

聚优致成

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值