IO复用概念
有些进程需要一种预先告知内核的能力,使得内核一旦发现进程指定的一个或者多个I/O条件就绪(即是说,输入已经准备好读取,或者,描述字已经能承接更多的输出),它就通知进程,这个能力就成为I/O复用,是由select和poll这两个函数支持的。其中,关于poll的讲解可以参考http://blog.youkuaiyun.com/pngynghay/article/details/17327179。另外,select和poll的增强版本epoll模型,可以参考http://blog.youkuaiyun.com/pngynghay/article/details/8169455
典型的网络应用场合
-
当客户端需要处理多个描述字(通常是交互式输入和网络套接口)时,必须使用IO复用。
-
一个客户同时处理多个套接口是可能的,不过,比较少见。
-
如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般就要哦使用IO复用。
-
如果一个服务器既要处理TCP,又要处理UDP,一般要使用IO复用。
-
如果一个服务器要处理多个服务或者多个协议,一般就要使用IO复用。
注:IO复用并非只限于网络编程。
select函数
函数原型
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout)
返回值:就绪描述字的正数目,0表示超时,-1表示你出错
参数:
maxfdpl:指定待测试的描述字个数,它的值是待测试的最大描述字加1(因为描述字是从0开始的);select.h中定义的FD_SETSIZE是数据类型fd_set中的描述字总数,通常为1024。
readset:读集合
writeset:写集合
exceptset:异常集合
timeout:告知内核等待所指定描述字中的任何一个就绪可花多长时间,timeval结构体用于指定这段时间的秒数和微秒数。如果该参数为NULL,那么,select函数将一直等待下去,直到有描述字准备好IO返回;如果该参数不为NULL,并且timeout为0(即定时器值的微秒数和秒数均为0),那么,select检查描述字后立即返回,成为轮询;如果该参数不为NULL,且timeout不为0,那么,select等待时间不超过timeout值,且在等待期间,如果有一个IO准备就绪,select也立即返回。
该函数允许进程指示内核等待多个事件中 的任何一个发生,并仅在有一个或者多个事件发生或者经历一段指定的时间后才唤醒它。我们调用select函数告诉内核对哪些描述字感兴趣以及等待多长时间。
注:我们感兴趣的描述字不局限于套接口,任何描述字都可以使用select来测试。
利用select函数,判断套接字上是否存在数据,或者能否向一个套接字写入数据。目的是防止应用程序在套接字处于锁定模式时,调用recv(或send)从没有数据的套接字上接收数据,被迫进入阻塞状态。
fd_set是一个SOCKET队列,以下宏可以对该队列进行操作:
FD_CLR( s, *set) 从队列set删除句柄s;
FD_ISSET( s, *set) 检查句柄s是否存在与队列set中;
FD_SET( s, *set )把句柄s添加到队列set中;
FD_ZERO( *set ) 把set队列初始化成空队列.
注:select函数的三个fd_set参数所指向的描述字集,都是值-结果参数。调用该函数时,我们指定所关心的描述字的值,该函数返回时,结果表示哪些描述字已经准备就绪。该函数返回后,我们使用FD_ISSET宏来测试fd_set数据类型中的描述子。描述字集中任何与未就绪的描述字想对应的位均清0,为此,每次重新调用select函数时,我们都得再次把我们感兴趣的所有描述字集的位置1.
附Linux man部分信息
NAME
select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexing
SYNOPSIS
/* 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);
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);
#define _XOPEN_SOURCE 600
#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);
DESCRIPTION
select() and pselect() allow a program to monitor multiple file descriptors, waiting until one or
more of the file descriptors become "ready" for some class of I/O operation (e.g., input possi-
ble). A file descriptor is considered ready if it is possible to perform the corresponding I/O
operation (e.g., read(2)) without blocking.
The operation of select() and pselect() is identical, with three differences:
(i) select() uses a timeout that is a struct timeval (with seconds and microseconds), while
pselect() uses a struct timespec (with seconds and nanoseconds).
(ii) select() may update the timeout argument to indicate how much time was left. pselect()
does not change this argument.
(iii) select() has no sigmask argument, and behaves as pselect() called with NULL sigmask.
Three independent sets of file descriptors are watched. Those listed in readfds will be watched
to see if characters become available for reading (more precisely, to see if a read will not
block; in particular, a file descriptor is also ready on end-of-file), those in writefds will be
watched to see if a write will not block, and those in exceptfds will be watched for exceptions.
On exit, the sets are modified in place to indicate which file descriptors actually changed sta-
tus. Each of the three file descriptor sets may be specified as NULL if no file descriptors are
to be watched for the corresponding class of events.
Four macros are provided to manipulate the sets. FD_ZERO() clears a set. FD_SET() and FD_CLR()
respectively add and remove a given file descriptor from a set. FD_ISSET() tests to see if a file
descriptor is part of the set; this is useful after select() returns.
nfds is the highest-numbered file descriptor in any of the three sets, plus 1.
timeout is an upper bound on the amount of time elapsed before select() returns. It may be zero,
causing select() to return immediately. (This is useful for polling.) If timeout is NULL (no time-
out), select() can block indefinitely.
sigmask is a pointer to a signal mask (see sigprocmask(2)); if it is not NULL, then pselect()
first replaces the current signal mask by the one pointed to by sigmask, then does the ‘select’
function, and then restores the original signal mask.
Other than the difference in the precision of the timeout argument, the following pselect() call:
ready = pselect(nfds, &readfds, &writefds, &exceptfds,
timeout, &sigmask);
is equivalent to atomically executing the following calls:
sigset_t origmask;
sigprocmask(SIG_SETMASK, &sigmask, &origmask);
ready = select(nfds, &readfds, &writefds, &exceptfds, timeout);
sigprocmask(SIG_SETMASK, &origmask, NULL);
The reason that pselect() is needed is that if one wants to wait for either a signal or for a file
descriptor to become ready, then an atomic test is needed to prevent race conditions. (Suppose
the signal handler sets a global flag and returns. Then a test of this global flag followed by a
call of select() could hang indefinitely if the signal arrived just after the test but just before
the call. By contrast, pselect() allows one to first block signals, handle the signals that have
come in, then call pselect() with the desired sigmask, avoiding the race.)
The timeout
The time structures involved are defined in <sys/time.h> and look like
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
and
struct timespec {
long tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
(However, see below on the POSIX.1-2001 versions.)
Some code calls select() with all three sets empty, n zero, and a non-NULL timeout as a fairly
portable way to sleep with subsecond precision.
On Linux, select() modifies timeout to reflect the amount of time not slept; most other implemen-
tations do not do this. (POSIX.1-2001 permits either behaviour.) This causes problems both when
Linux code which reads timeout is ported to other operating systems, and when code is ported to
Linux that reuses a struct timeval for multiple select()s in a loop without reinitializing it.
Consider timeout to be undefined after select() returns.
RETURN VALUE
On success, select() and pselect() return the number of file descriptors contained in the three
returned descriptor sets (that is, the total number of bits that are set in readfds, writefds,
exceptfds) which may be zero if the timeout expires before anything interesting happens. On
error, -1 is returned, and errno is set appropriately; the sets and timeout become undefined, so
do not rely on their contents after an error.
ERRORS
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.
EINVAL nfds is negative or the value contained within timeout is invalid.
ENOMEM unable to allocate memory for internal tables.
服务器程序如下:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/time.h>
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#define MAXLINE 1024
#define SERV_PORT 5000
#define LISTENQ 100
void err_quit(char * msg)
{
printf("err %s", msg);
exit(0);
}
int main(int argc, char **argv)
{
int i, maxi, maxfd, listenfd, connfd, sockfd;
int nready, client[FD_SETSIZE];
ssize_t n;
fd_set rset, allset;
char buf[MAXLINE];
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
listen(listenfd, LISTENQ);
maxfd = listenfd; /* initialize */
maxi = -1; /* index into client[] array */
for (i = 0; i < FD_SETSIZE; i++)
client[i] = -1; /* -1 indicates available entry */
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
/* end fig01 */
/* include fig02 */
for ( ; ; ) {
rset = allset; /* structure assignment */
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if (FD_ISSET(listenfd, &rset)) { /* new client connection */
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &clilen);
#ifdef NOTDEF
printf("new client: %s, port %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, 4, NULL),
ntohs(cliaddr.sin_port));
#endif
for (i = 0; i < FD_SETSIZE; i++)
if (client[i] < 0) {
client[i] = connfd; /* save descriptor */
break;
}
if (i == FD_SETSIZE)
err_quit("too many clients");
FD_SET(connfd, &allset); /* add new descriptor to set */
if (connfd > maxfd)
maxfd = connfd; /* for select */
if (i > maxi)
maxi = i; /* max index in client[] array */
if (--nready <= 0)
continue; /* no more readable descriptors */
}
for (i = 0; i <= maxi; i++) { /* check all clients for data */
if ( (sockfd = client[i]) < 0)
continue;
if (FD_ISSET(sockfd, &rset)) {
if ( (n = read(sockfd, buf, MAXLINE)) == 0) {
/*4connection closed by client */
close(sockfd);
FD_CLR(sockfd, &allset);
client[i] = -1;
} else
write(sockfd, buf, n);
if (--nready <= 0)
break; /* no more readable descriptors */
}
}
}
}
关系到套接字列表的操作都需要使用循环,在轮询的时候,需要遍历一次,再新的一轮开始时,将列表加入队列又需要遍历一次.也就是说,Select在工作一次时,需要至少遍历2次列表,这是它效率较低的原因之一.在大规模的网络连接方面,还是推荐使用IOCP或EPOLL模型.但是Select模型可以使用在诸如对战类游戏上,比如类似星际这种,因为它小巧易于实现,而且对战类游戏的网络连接量并不大.
客户端程序同 http://blog.youkuaiyun.com/pngynghay/article/details/8169455中的客户端程序