一、select, poll的区别 select()系统调用提供一个机制来实现同步多元I/O: #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); FD_CLR(int fd, fd_set *set); FD_ISSET(int fd, fd_set *set); FD_SET(int fd, fd_set *set); FD_ZERO(fd_set *set); 调用select()将阻塞,直到指定的文件描述符准备好执行I/O,或者可选参数timeout指定的时间已经过去。 监视的文件描述符分为三类set,每一种对应等待不同的事件。readfds中列出的文件描述符被监视是否有数据可供读取(如果读取操作完成则不会阻 塞)。writefds中列出的文件描述符则被监视是否写入操作完成而不阻塞。最后,exceptfds中列出的文件描述符则被监视是否发生异常,或者无 法控制的数据是否可用(这些状态仅仅应用于套接字)。这三类set可以是NULL,这种情况下select()不监视这一类事件。 select()成功返回时,每组set都被修改以使它只包含准备好I/O的文件描述符。例如,假设有两个文件描述符,值分别是7和9,被放在 readfds中。当select()返回时,如果7仍然在set中,则这个文件描述符已经准备好被读取而不会阻塞。如果9已经不在set中,则读取它将 可能会阻塞(我说可能是因为数据可能正好在select返回后就可用,这种情况下,下一次调用select()将返回文件描述符准备好读取)。 第一个参数n,等于所有set中最大的那个文件描述符的值加1。因此,select()的调用者负责检查哪个文件描述符拥有最大值,并且把这个值加1再传递给第一个参数。 timeout参数是一个指向timeval结构体的指针,timeval定义如下: #include <sys/time.h> struct timeval { long tv_sec; /* seconds */ long tv_usec; /* 10E-6 second */ }; 如果这个参数不是NULL,则即使没有文件描述符准备好I/O,select()也会在经过tv_sec秒和tv_usec微秒后返回。当 select()返回时,timeout参数的状态在不同的系统中是未定义的,因此每次调用select()之前必须重新初始化timeout和文件描述 符set。实际上,当前版本的Linux会自动修改timeout参数,设置它的值为剩余时间。因此,如果timeout被设置为5秒,然后在文件描述符 准备好之前经过了3秒,则这一次调用select()返回时tv_sec将变为2。 如果timeout中的两个值都设置为0,则调用select()将立即返回,报告调用时所有未决的事件,但不等待任何随后的事件。 文件描述符set不会直接操作,一般使用几个助手宏来管理。这允许Unix系统以自己喜欢的方式来实现文件描述符set。但大多数系统都简单地实现set为位数组。FD_ZERO移除指定set中的所有文件描述符。每一次调用select()之前都应该先调用它。 fd_set writefds; FD_ZERO(&writefds); FD_SET添加一个文件描述符到指定的set中,FD_CLR则从指定的set中移除一个文件描述符: FD_SET(fd, &writefds); /* add 'fd' to the set */ FD_CLR(fd, &writefds); /* oops, remove 'fd' from the set */ 设计良好的代码应该永远不使用FD_CLR,而且实际情况中它也确实很少被使用。 FD_ISSET测试一个文件描述符是否指定set的一部分。如果文件描述符在set中则返回一个非0整数,不在则返回0。FD_ISSET在调用select()返回之后使用,测试指定的文件描述符是否准备好相关动作: if (FD_ISSET(fd, &readfds)) /* 'fd' is readable without blocking! */ 因为文件描述符set是静态创建的,它们对文件描述符的最大数目强加了一个限制,能够放进set中的最大文件描述符的值由FD_SETSIZE指定。在Linux中,这个值是1024。本章后面我们还将看到这个限制的衍生物。 返回值和错误代码 select()成功时返回准备好I/O的文件描述符数目,包括所有三个set。如果提供了timeout,返回值可能是0;错误时返回-1,并且设置errno为下面几个值之一: EBADF 给某个set提供了无效文件描述符。 EINTR 等待时捕获到信号,可以重新发起调用。 EINVAL 参数n为负数,或者指定的timeout非法。 ENOMEM 不够可用内存来完成请求。 -------------------------------------------------------------------------------------------------------------- poll()系统调用是System V的多元I/O解决方案。它解决了select()的几个不足,尽管select()仍然经常使用(多数还是出于习惯,或者打着可移植的名义): #include <sys/poll.h> int poll (struct pollfd *fds, unsigned int nfds, int timeout); 和select()不一样,poll()没有使用低效的三个基于位的文件描述符set,而是采用了一个单独的结构体pollfd数组,由fds指针指向这个组。pollfd结构体定义如下: #include <sys/poll.h> struct pollfd { int fd; /* file descriptor */ short events; /* requested events to watch */ short revents; /* returned events witnessed */ }; 每一个pollfd结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。每个结构体的events域是监视该 文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码。内核在调用返回时设置这个域。events域中请求的任何 事件都可能在revents域中返回。合法的事件如下: POLLIN 有数据可读。 POLLRDNORM 有普通数据可读。 POLLRDBAND 有优先数据可读。 POLLPRI 有紧迫数据可读。 POLLOUT 写数据不会导致阻塞。 POLLWRNORM 写普通数据不会导致阻塞。 POLLWRBAND 写优先数据不会导致阻塞。 POLLMSG SIGPOLL消息可用。 此外,revents域中还可能返回下列事件: POLLER 指定的文件描述符发生错误。 POLLHUP 指定的文件描述符挂起事件。 POLLNVAL 指定的文件描述符非法。 这些事件在events域中无意义,因为它们在合适的时候总是会从revents中返回。使用poll()和select()不一样,你不需要显式地请求异常情况报告。 POLLIN | POLLPRI等价于select()的读事件,POLLOUT | POLLWRBAND等价于select()的写事件。POLLIN等价于POLLRDNORM | POLLRDBAND,而POLLOUT则等价于POLLWRNORM。 例如,要同时监视一个文件描述符是否可读和可写,我们可以设置events为POLLIN | POLLOUT。在poll返回时,我们可以检查revents中的标志,对应于文件描述符请求的events结构体。如果POLLIN事件被设置,则文 件描述符可以被读取而不阻塞。如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。这些标志并不是互斥的:它们可能被同时设置,表示这个文件描 述符的读取和写入操作都会正常返回而不阻塞。 timeout参数指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定为负数值表示无限超时;timeout为0指示 poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。 返回值和错误代码 成功时,poll()返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1,并设置errno为下列值之一: EBADF 一个或多个结构体中指定的文件描述符无效。 EFAULT fds指针指向的地址超出进程的地址空间。 EINTR 请求的事件之前产生一个信号,调用可以重新发起。 EINVAL nfds参数超出PLIMIT_NOFILE值。 ENOMEM 可用内存不足,无法完成请求。 -------------------------------------------------------------------------------------------------------------- 以上内容来自《OReilly.Linux.System.Programming - Talking.Directly.to.the.Kernel.and.C.Library.2007》 -------------------------------------------------------------------------------------------------------------- 二、select poll使用 如何管理多个连接? “我想同时监控一个以上的文件描述符(fd)/连接(connection)/流(stream),应该怎么办?” 使用 select() 或 poll() 函数。 注意:select() 在BSD中被引入,而poll()是SysV STREAM流控制的产物。因此,这里就有了平台移植上的考虑:纯粹的BSD系统可能仍然缺少poll(),而早一些的SVR3系统中可能没有 select(),尽管在SVR4中将其加入。目前两者都是POSIX. 1g标准,(译者注:因此在Linux上两者都存在) select()和poll()本质上来讲做的是同一件事,只是完成的方法不一样。两者都通过检验一组文件描述符来检测是否有特定的时间将在上面发生并在一定的时间内等待其发生。 [重要事项:无论select()还是poll()都不对普通文件起很大效用,它们着重用于套接口(socket)、管道(pipe)、伪终端(pty)、终端设备(tty)和其他一些字符设备,但是这些操作都是系统相关(system-dependent)的。] 2.1.1. 我如何使用select()函数? select()函数的接口主要是建立在一种叫'fd_set'类型的基础上。它('fd_set') 是一组文件描述符(fd)的集合。由于fd_set类型的长度在不同平台上不同,因此应该用一组标准的宏定义来处理此类变量: fd_set set; FD_ZERO(&set); /* 将set清零 */ FD_SET(fd, &set); /* 将fd加入set */ FD_CLR(fd, &set); /* 将fd从set中清除 */ FD_ISSET(fd, &set); /* 如果fd在set中则真 */ 在 过去,一个fd_set通常只能包含少于等于32个文件描述符,因为fd_set其实只用了一个int的比特矢量来实现,在大多数情况下,检查 fd_set能包括任意值的文件描述符是系统的责任,但确定你的fd_set到底能放多少有时你应该检查/修改宏FD_SETSIZE的值。*这个值是系 统相关的*,同时检查你的系统中的select() 的man手册。有一些系统对多于1024个文件描述符的支持有问题。[译者注: Linux就是这样的系统!你会发现sizeof(fd_set)的结果是128(*8 = FD_SETSIZE=1024) 尽管很少你会遇到这种情况。] select的基本接口十分简单: int select(int nfds, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout); 其中: nfds 需要检查的文件描述符个数,数值应该比是三组fd_set中最大数 更大,而不是实际文件描述符的总数。 readset 用来检查可读性的一组文件描述符。 writeset 用来检查可写性的一组文件描述符。 exceptset 用来检查意外状态的文件描述符。(注:错误并不是意外状态) timeout NULL指针代表无限等待,否则是指向timeval结构的指针,代表最 长等待时间。(如果其中tv_sec和tv_usec都等于0, 则文件描述符 的状态不被影响,但函数并不挂起) 函数将返回响应操作的对应操作文件描述符的总数,且三组数据均在恰当位置被修改,只有响应操作的那一些没有修改。接着应该用FD_ISSET宏来查找返回的文件描述符组。 这里是一个简单的测试单个文件描述符可读性的例子: int isready(int fd) { int rc; fd_set fds; struct timeval tv; FD_ZERO(&fds); FD_SET(fd,&fds); tv.tv_sec = tv.tv_usec = 0; rc = select(fd+1, &fds, NULL, NULL, &tv); if (rc < 0) return -1; return FD_ISSET(fd,&fds) ? 1 : 0; } 当然如果我们把NULL指针作为fd_set传入的话,这就表示我们对这种操作的发生不感兴趣,但select() 还是会等待直到其发生或者超过等待时间。 [译者注:在Linux中,timeout指的是程序在非sleep状态中度过的时间,而不是实际上过去的时间,这就会引起和非Linux平台移植上的时 间不等问题。移植问题还包括在System V风格中select()在函数退出前会把timeout设为未定义的 NULL状态,而在BSD中则不是这样, Linux在这点上遵从System V,因此在重复利用timeout指针问题上也应该注意。] 2.1.2. 我如何使用poll()? poll ()接受一个指向结构'struct pollfd'列表的指针,其中包括了你想测试的文件描述符和事件。事件由一个在结构中事件域的比特掩码确定。当前的结构在调用后将被填写并在事件发生后 返回。在SVR4(可能更早的一些版本)中的 "poll.h"文件中包含了用于确定事件的一些宏定义。事件的等待时间精确到毫秒 (但令人困惑的是等待时间的类型却是int),当等待时间为0时,poll()函数立即返回,-1则使poll()一直挂起直到一个指定事件发生。下面是 pollfd的结构。 struct pollfd { int fd; /* 文件描述符 */ short events; /* 等待的事件 */ short revents; /* 实际发生了的事件 */ }; 于select()十分相似,当返回正值时,代表满足响应事件的文件描述符的个数,如果返回0则代表在规定事件内没有事件发生。如发现返回为负则应该立即查看 errno,因为这代表有错误发生。 如果没有事件发生,revents会被清空,所以你不必多此一举。 这里是一个例子 /* 检测两个文件描述符,分别为一般数据和高优先数据。如果事件发生 则用相关描述符和优先度调用函数handler(),无时间限制等待,直到 错误发生或描述符挂起。*/ #include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <stropts.h> #include <poll.h> #include <unistd.h> #include <errno.h> #include <string.h> #define NORMAL_DATA 1 #define HIPRI_DATA 2 int poll_two_normal(int fd1,int fd2) { struct pollfd poll_list[2]; int retval; poll_list[0].fd = fd1; poll_list[1].fd = fd2; poll_list[0].events = POLLIN|POLLPRI; poll_list[1].events = POLLIN|POLLPRI; while(1) { retval = poll(poll_list,(unsigned long)2,-1); /* retval 总是大于0或为-1,因为我们在阻塞中工作 */ if(retval < 0) { fprintf(stderr,"poll错误: %s/n",strerror(errno)); return -1; } if(((poll_list[0].revents&POLLHUP) == POLLHUP) || ((poll_list[0].revents&POLLERR) == POLLERR) || ((poll_list[0].revents&POLLNVAL) == POLLNVAL) || ((poll_list[1].revents&POLLHUP) == POLLHUP) || ((poll_list[1].revents&POLLERR) == POLLERR) || ((poll_list[1].revents&POLLNVAL) == POLLNVAL)) return 0; if((poll_list[0].revents&POLLIN) == POLLIN) handle(poll_list[0].fd,NORMAL_DATA); if((poll_list[0].revents&POLLPRI) == POLLPRI) handle(poll_list[0].fd,HIPRI_DATA); if((poll_list[1].revents&POLLIN) == POLLIN) handle(poll_list[1].fd,NORMAL_DATA); if((poll_list[1].revents&POLLPRI) == POLLPRI) handle(poll_list[1].fd,HIPRI_DATA); } } 2.1.3. 我是否可以同时使用SysV IPC和select()/poll()? *不能。* (除非在AIX上,因为它用一个无比奇怪的方法来实现这种组合) 一般来说,同时使用select()或poll()和SysV 消息队列会带来许多麻烦。SysV IPC的对象并不是用文件描述符来处理的,所以它们不能被传递给select()和 poll()。这里有几种解决方法,其粗暴程度各不相同: 完全放弃使用SysV IPC。 :-) 用fork(),然后让子进程来处理SysV IPC,然后用管道或套接口和父进程 说话。父进程则使用select()。 同上,但让子进程用select(),然后和父亲用消息队列交流。 安排进程发送消息给你,在发送消息后再发送一个信号。*警告*:要做好 这个并不简单,非常容易写出会丢失消息或引起死锁的程序。 另外还有其他方法。