前言
I/O复用采用轮询的方式处理多个描述符,当有文件准备好时,就通知进程。
关注点
- I/O复用的应用场合
- 采用I/O复用的客户端和服务器程序
I/O复用的应用场合
1. 当客户处理多个描述符时(通常是交互式输入和网络套接字),必须使用I/O复用,才能即使告知用户程序套接字的情况
2. 如果一个TCP服务器既要处理监听又要处理连接套接字,一般要用I/O复用
3. 如果既要处理TCP,又要处理UDP,一般要用I/O复用
4. 如果一个服务器要处理多个服务或多个协议如inet守护进程,一般要用I/O复用
I/O复用
select函数
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
void FD_ZERO(fd_set *fdset);
void FD_SET(int fd, fd_set *fdset);
void FD_CLR(int fd, fd_set *fdset);
int FD_ISSET(int fd, fd_set *fdset);
//返回就绪描述符数目,若超时则为0,出错为1
批量输入问题:
close终止了读和写两个方向的数据传送
shutdown可以通过SHUT_RD等参数实现读半关闭从而使得还在网络中的数据传输完整
兑现了TCP的可靠传输的性质。
select和缓冲区的关系:
select只认是否有描述符准备好,也就是观察内核,但是stdio和sendline(用户级)有自己的缓冲区,并且读写有可能导致自己缓冲区中留有数据,select没有看到,因此还是阻塞在等待内核描述符准备好,导致stdio和sendline中的数据没有完全处理掉(结合程序看比较清楚,也可以看[这里](http://www.cppblog.com/mysileng/archive/2013/01/15/197284.html))。
struct timeval{
long tv_sec;
long tv_usec;
}
timeval结构中的微妙级数分辨率一般不可能达到,一般都是向上舍入10ms的倍数,另外还涉及调度的延迟。
- 采用I/O复用的客户端和服务器程序
使用selecet的客户端程序
版本一:
void str_cli(FILE * fp, int sockfd){
char sendline[MAXLINE], recvline[MAXLINE];
int maxfdp1;
fd_set rset;
FD_ZERO(&rset);
for( ; ; ){
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(fp), sockfd) + 1;
Select(maxfdp1, &rset, NULL, NULL, NULL);
if(FD_ISSET(sockfd, &rset)){
if(Readline(sockfd, recvline, MAXLINE) == 0)
err_quit("str_cli: server terminated prematurely");
Fputs(recvline, stdout);
}
if(FD_ISSET(fileno(fp), &rset)){
if(Fgets(sendline, MAXLINE, fp) == NULL)
return ;
Writen(sockfd, sendline, strlen(sendline));
}
}
}
版本二:
void str_cli(FILE * fp, int sockfd){
int maxfdp1, stdineof;
fd_set rset;
char buf[MAXLINE];
int n;
stdineof = 0;
FD_ZERO(&rset);
for( ; ; ){
if(stdineof == 0){
FD_SET(fileno(fp), &rset);
}
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(fp), sockfd) + 1;
Select(maxfdp1, &rset, NULL, NULL, NULL);
if(FD_ISSET(sockfd, &rset)){
if((n = Read(sockfd, buf, MAXLINE)) == 0){
if(stdineof == 1)
return ;
else
err_quit("str_cli: server terminated prematurely");
}
Write(fileno(stdout), buf, n);
}
if(FD_ISSET(fileno(fp), &rset)){
if((n = Read(fileno(fp), buf, MAXLINE)) == 0){
stdineof = 1;
Shutdown(sockfd, SHUT_WR);
FD_CLR(fileno(fp), &rset);
continue;
}
Writen(sockfd, buf, n);
}
}
}
版本一中调用了Fets,Fputs,Readline等有自己缓冲区的函数,select看不到,这将导致缓冲区中的数据来不及消费。
版本二改用了Read,Write,解决这一问题;并且改用了shutdown来关闭连接而不是用close。修复了批量输入的问题。
- 使用select的服务端程序
#include "unp.h"
int main(int argc, char const *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_port = htons(SERV_PORT);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
Bind(listenfd, (SA *)&servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
maxfd = listenfd;
maxi = -1;
for (i = 0; i < FD_SETSIZE; i++){
client[i] = -1;
}
FD_ZERO (&allset);
FD_SET(listenfd, &allset);
for( ; ; ){
rset = allset;
nready = Select(maxfd + 1, &rset, NULL, NULL, NULL);
if(FD_ISSET(listenfd, &rset)){
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *)&cliaddr, &clilen);
for(i = 0; i < FD_SETSIZE; i++)
if(client[i] < 0){
client[i] = connfd;
break;
}
if(i == FD_SETSIZE)
err_quit("too many clients");
FD_SET(connfd, &allset);
if(connfd > maxfd)
maxfd = connfd;
if(i > maxi)
maxi = i;
if(--nready <= 0)
continue;
}
for(i = 0; i <= maxi; i++){
if((sockfd = client[i]) < 0)
continue;
if(FD_ISSET(sockfd, &rset)){
if((n = Read(sockfd, buf, MAXLINE)) == 0){
Close(sockfd);
FD_CLR(sockfd, &allset);
client[i] = -1;
}
else
Writen(sockfd, buf, n);
if(--nready <= 0)
break;
}
}
}
}
实例图:
if(--nready <= 0)
continue;
if(--nready <= 0)
break;
//这两段代码一开始让我很困惑,这两段代码的作用是提高效率,避免了i到maxi的重复循环。
采用client数组存储连接描述符
使用poll的客户端程序
#include <poll.h>
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
第一个参数是指向一个结构数组第一个元素的指针。每个数组元素都是一个pollfd结构,用于指定测试某个给定描述符fd的条件。
struct pollfd{
int fd;
short events;
short revents;
}
回射服务器poll版本:
#include "unp.h"
#include <linux/fs.h>
int main(int argc, char const *argv[]) {
int i, maxi, listenfd, connfd, sockfd;
int nready;
ssize_t n;
char buf[MAXLINE];
socklen_t clilen;
struct pollfd client[INR_OPEN_MAX];
struct sockaddr_in cliaddr, servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
client[0].fd = listenfd;
client[0].events = POLLRDNORM;
for(i = 1; i < INR_OPEN_MAX; i++)
client[i].fd = -1;
maxi = 0;
for( ; ; ){
nready = Poll(client, maxi + 1, INFTIM);
if(client[0].revents & POLLRDNORM){
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA*)&cliaddr, &clilen);
for(i = 1; i < INR_OPEN_MAX; i++){
if(client[i].fd < 0){
client[i].fd = connfd;
break;
}
}
if(i == INR_OPEN_MAX)
err_quit("too many clients");
client[i].events = POLLRDNORM;
if(i > maxi)
maxi = i;
if(--nready <= 0)
continue;
}
for(i = 1; i <= maxi; i++){
if((sockfd = client[i].fd) < 0){
continue;
}
if(client[i].revents & (POLLRDNORM | POLLERR)){
if((n = read(sockfd, buf, MAXLINE)) < 0){
if(errno == ECONNRESET){
Close(sockfd);
client[i].fd = -1;
} else
err_sys("read error");
} else if(n == 0){
Close(sockfd);
client[i].fd = -1;
} else
Writen(sockfd, buf, n);
}
if(--nready <= 0)
break;
}
}
return 0;
}
总结poll和select的区别:
1) poll不用对最大描述符做+1操作
2)poll在应对大数目的文件描述符时更快,因为select要求内核需要检查大量描述符对应的fd_set中的每一个bit位,比较费时。
3)select可以监控的文件描述符数目是固定的,相对来说教少(1024或2048)。如果需要监控较大的文件描述符或分布较稀疏的的较少的描述符,效率也会很低。而对于poll来说,可以创建特定大小的数组来保存监控的描述符,而不受文件描述符值大小的影响,而且poll可以监控的文件描述符数目远大于select。
http://www.cnblogs.com/Anker/p/3265058.html