试想这样一种情况,从一个文件描述符读,然后写到另一个文件描述符,该怎么实现呢?如果从多个文件描述符读,又该如何呢?下面是几种解决方案。
1、多进程。每个进程独自处理一条数据通路,但在进程终止时,需要进行进程间通信,增加了程序的复杂度。
2、多线程。在一个进程内使用多线程,可以避免复杂的进程间通信,但必须考虑线程间的同步问题。
3、轮询。在循环内,使用非阻塞IO处理数据,可以每隔若干时间处理一次,但这种方式浪费了CPU时间,在多任务系统中应当避免使用。
4、异步IO。异步IO用到了信号机制,如系统V的SIGPOLL信号,BSD的SIGIO信号,问题是并非所有系统都支持这种机制,而且这种信号对每个进程而言只有1个,如果使该信号对多个描述符都起作用,那么在接收到此信号时进程无法判断是哪一个描述符已准备好可以进行IO操作,为了确定是哪一个,仍需将这几个描述符都设置为非阻塞并顺序执行。
上面的四种方案欠佳,可以考虑使用另一种技术——IO多路转接。这个是一种比较好的技术,先构造一张有关描述符的列表,然后调用一个函数,如select、pselect、poll,直到这些描述符中的一个已准备好进行IO操作时,该函数才返回,返回时,它告诉进程哪些描述符已准备好可以进行IO操作了。IO多路转接可以避免阻塞IO的弊端,因为有时候需要在多个描述符上读read、写write,如果使用阻塞IO,就有可能长时间阻塞在某个描述符上而影响其它描述符的使用。下面是三个相关的函数:
#include
#include
#include
#include
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timespec *timeout,
const sigset_t *sigmask);
#include
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
select——
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
传向select的参数告诉内核:我们所关心的描述符;对于每个描述符我们所关心的状态,读、写和异常;愿意等待多长时间,不等待、等待若干时间或无限等待。从select返回时,内核告诉我们:已准备好的描述符的数量;对于读、写或异常这三个状态中的一个,哪些描述符已准备好。使用这些返回信息,就可调用相应的IO函数,如read/write,并且确知该函数不会阻塞。select出错返回-1,并设置对应的errno,timeout超时返回0。
select函数的第一个参数nfds取值为最大描述符加1,意思是在后面三个读、写、异常描述符集参数中找到最大描述符,然后加1。
select函数的中间三个参数readfds、writefds、exceptfds是指向描述符集的指针,这三个描述符集说明了我们关心的可读、可写或处于异常条件的各个描述符,每个描述符集存放在一个fd_set数据结构中,为每一可能的描述符保持了一位。对fd_set数据结构可以进行的处理是:分配一个这种类型的变量;将这种类型的一个变量值赋予同类型的另一个变量;或对于这种类型的变量使用下列四个函数中的一个。
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_set变量的指定位fd为1,FD_CLR则将一指定位清除即设置为0,FD_ISSET测试一指定位是否设置即是否为1。
select函数的最后一个参数timeout表示等待时间,为NULL时表示无限等待,但可以被信号中断,被信号中断时返回-1,errno为EINTR;不为NULL时,等待指定的时间,时间值为0时表示不等待,等待时间超时后返回0。
前面提到了准备好的描述符,这是什么意思呢?对于readfds中的一个描述符的read操作将不会操作,则认为是准备好的;对于writefds中的一个描述符的write操作将不会操作,则认为是准备好的;对于exceptfds中的一个描述符有一个未决异常状态,则认为是准备好的;对于读、写和异常状态,普通文件描述符总是返回准备好。
下面举例select函数的用法,例子中我们只设置标准输入描述符0的读状态,所以select函数的第一个参数为1,首先FD_ZERO清空fd_set的各标志位,FD_SET设置fd_set的第0位为1,timeout为5秒,然后调用select,在timeout超时前如果有东西输入到标准输入,将会通过read函数把内容保存到buf变量并打印出来。通过gcc编译,运行程序时在超时前输入“hello select”。
// selectex.c
#include
#include
#include
#include
#include
#include
int main()
{
fd_set rfds;
struct timeval tv;
int retval;
char buf[1024] = { 0 };
FD_ZERO(&rfds);
FD_SET(0, &rfds);
tv.tv_sec = 5;
tv.tv_usec = 0;
retval = select(1, &rfds, NULL, NULL, &tv);
if (retval == -1)
perror("select()");
else if (retval) {
printf("Data is available now.\n");
printf("FD_ISSET(0, fd_set) is %d\n", FD_ISSET(0, &rfds));
read(0, buf, 1024);
printf("buf = %s\n", buf);
} else
printf("No data within five seconds.\n");
exit(EXIT_SUCCESS);
}
$ gcc selectex.c
$ ./a.out
hello select
Data is available now.
FD_ISSET(0, fd_set) is 1
buf = hello select
pselect——
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timespec *timeout,
const sigset_t *sigmask);
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
struct timespec {
long tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
pselect类似于select,不同之处在于timeout的数据结构变为timespec,timespec比select的timeval时间粒度更小,而且timeout为const,不允许pselect函数修改,另一个不同之处在于pselect提供了一个信号屏蔽字,sigmask不为NULL时,调用pselect时原子地安装这个信号屏蔽字,函数返回时恢复原来的信号屏蔽字恢复。
poll——
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
poll功能类似于select,用法不同。poll关心的描述符通过pollfd数组设置,pollfd个数为nfds,其中fd为待处理的描述符,events为我们想要描述符处理的事件,包括读、写事件,如POLLIN表示是否有数据可读,由用户通过按位或操作符指定,revents是poll函数的执行结果,告诉我们响应了哪些事件,可通过按位与操作符检查。timeout单位为秒。下面使用poll实现上面select用例的功能。
#include
#include
#include
int main()
{
struct pollfd pfd;
nfds_t nfds;
int timeout;
int retval;
char buf[1024] = { 0 };
pfd.fd = 0;
pfd.events = POLLIN;
nfds = 1;
timeout = 5000;
retval = poll(&pfd, nfds, timeout);
if (retval == -1)
perror("poll()");
else if (retval) {
printf("Data is available now.\n");
printf("pollfd.revents & POLLIN = %d\n", pfd.revents & POLLIN);
read(0, buf, 1024);
printf("buf = %s\n", buf);
} else
printf("No data within five seconds.\n");
exit(EXIT_SUCCESS);
}
与poll对应,还有个带信号屏蔽字版本的ppoll:
int ppoll(struct pollfd *fds, nfds_t nfds,
const struct timespec *tmo_p, const sigset_t *sigmask);
对于IO多路转接,除了select、pselect、poll、ppoll之外,还有个epoll,包括epoll_create、epoll_ctl和epoll_wait。epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。epoll支持一个进程打开大数目的socket描述符,io效率不随fd数目增加而线性下降,使用mmap加速内核与用户空间的消息传递。