前面提到,当客户阻塞于fgets时,服务器进程若被杀死,虽然会给客户端发送FIN,但客户端看不到这个EOF,直到从套接字读。这样的进程需要一种预先告知内核的能力,使内核一旦发现进程指定的一个或多个I/O条件就绪,就通知进程。这个能力称为I/O复用(multiplexing)。
I/O复用通常用于服务器设计:
(1)处理多个连接,或者同时处理监听套接字和连接套接字
(2)同时处理TCP协议和UDP协议
(3)同时处理多个服务
默认情况下,所有套接字都是阻塞的。为了简单起见,以数据报套接字为例:
前三次调用recvfrom时无数据返回,因此内核返回EWOULDBLOCK错误。第四次调用已有数据报准备好,它被复制到应用进程缓冲区,recvfrom成功返回。
这种轮询(polling)内核查看某个操作是否就绪的方法,比较耗费CPU时间。
我们可以调用select或poll,使进程阻塞在其中一个系统调用上,而不是阻塞在真正的I/O系统调用上。
在多线程中使用阻塞式I/O与这种模型很相似,使用一个线程来控制一个描述符。
我们也可以使用信号,让内核在描述符就绪时发送SIGIO信号通知我们,称为signal-driven I/O:
这种模型的优势在于等待数据报到达期间进程不被阻塞,主循环可以继续执行。
异步I/O(asynchronous I/O)由POSIX规范定义,其工作机制是:告知内核启动某个操作,并在整个操作完成后通知我们。这种模型与前一节的信号驱动模型的主要区别在于:信号驱动式I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。
例如调用aio_read函数,给内核传递描述符、缓冲区指针、缓冲区大小和文件偏移,并告知内核整个操作完成时如何通知我们。
注:异步I/O模型需要支持的系统
前四种模型都是同步I/O模型,因为其中真正的I/O操作(recvfrom)将阻塞进程。
该函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生的时或经历一段指定的时间后才唤醒它。
#include
#include
int select(int maxfdp1, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timeval* timeout);
//若有就绪描述符则返回其数量,超时返回0,出错返回-1
struct timeval{
long tv_sec;
long tv_usec;
};
关于timeval参数意义:
(1)永远等待直至有描述符准备好,参数置为空指针
(2)等待一段固定的时间,参数所指结构体设为需要等待的时间
(3)不等待,立即返回,参数所指结构体时间设为0
中间的三个参数分别表示要测试读、写和异常的描述符集合。描述符集通常是一个整数数组,其中每一个整数中的每一位对应一个描述符。假设使用32位整数,则该数组的第一个元素对应描述符0~31,第二个元素对应描述符32-63,以此类推(实现细节由内核完成)。
有关操作的四个宏:
void FD_ZERO(fd_set* fdset); //清空(重置)fdset
void FD_SET(int fd, fd_set* fdset); //fd加入set(开启set中对应位)
void FD_CLR(int fd, fd_set* fdset); //fd从set中移除(关闭位)
int FD_ISSET(int fd, fd_set* fdset); //测试fd是否被设置
select函数修改readset、writeset和exceptset所指向的描述符集,函数返回时,结果将指示哪些描述符已就绪。函数返回后,可用FD_ISSET测试描述符。描述符集内未就绪描述符对应的位返回时被清0,所以每次重新调用select函数时,都需再次把所有描述符集内锁关心的位置为1。
分为读就绪、写就绪和异常
满足下面任一条件,读就绪
(1)套接字接收缓冲区数据大于等于接收缓冲区低水位标记。TCP和UDP套接字默认值为1。
(2)该连接的读半部关闭(也就是收到FIN的TCP连接)。对这样的套接字读将不阻塞并返回0。
(3)该套接字是一个监听套接字且已完成的连接数不为0。accept这样的套接字通常不阻塞
(4)其上有一个套接字错误待处理。对这样的套接字读将返回-1,同时设置errno。
满足下面任一条件,写就绪
(1)该套接字已连接或不需要连接(如UDP),且该套接字发送缓冲区大于等于发送缓冲区低水位标记。TCP和UDP默认值为2048。
(2)该连接写半部关闭。对这样的套接字写将产生SIGPIPE信号。
(3)使用非阻塞式connect的套接字已建立连接,或connect已失败
(4)其上有一个套接字错误待处理。对这样的套接字写将返回-1,同时设置errno。
若套接字存在带外数据或仍处于带外标记,那么它有异常待处理
注:当某个套接字上发生错误时,则将由select标记为既可读又可写
一般在
#define FD_SETSIZE xxx;
这些值通常不超过1024。
使用close关闭连接有两个限制:
(1)close只是将描述符引用计数减1,该计数为0时才关闭套接字,而shutdown直接引发终止序列
(2)close同时关闭读和写。shutdown可以选择关闭某一端
#include
int shutdown(int sockfd, int howto);
//成功返回0,出错返回-1
关于howto参数:
#include
int poll(struct pollfd* fdarray, unsigned long nfds, int timeout);
//若有就绪描述符则返回其数目,超时返回0,出错返回-1
struct pollfd{
int fd;
short events;
short revents;
};
要测试的条件由events指定,函数在相应的revents成员中返回该描述符的状态。结构体数组中的元素是由nfds参数指定的。
参考这篇博客:
https://blog.csdn.net/wteruiycbqqvwt/article/details/90299610
#include "unp.h"
#include
int main(int argc, char** argv)
{
int i, maxi = 0, listenfd, connfd, sockfd;
int nready;
ssize_t n;
char buf[MAXLEN];
socklen_t clilen;
struct pollfd client[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_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (SA*)(&servaddr), sizeof(servaddr));
Listen(listenfd, LISTENQ);
client[0].fd = listenfd;
client[0].events = POLLRDNORM;
for(i = 1; i < OPEN_MAX; i++){
client[i].fd = -1; //除了监听描述符,其他初始化为-1
}
while(1){
nready = Poll(client, maxi + 1, INFTIM); //一直等待
if(client[0].revents & EPOLLRDNORM){ //处理新连接
clilen = sizeof(cliaddr);
connfd = Accept(client[0].fd, (SA*)(&cliaddr), &clilen);
for(i = 1; i < OPEN_MAX; i++){
if(client[i].fd < 0){
client[i].fd = connfd;
break;
}
}
if(i == OPEN_MAX)
err_quit("too many clients");
client[i].events = POLLRDNORM;
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){ //对方发送RST
Close(sockfd);
client[i] = -1;
}else{
err_sys("read error");
}
}else if(n == 0){ //对方关闭连接
Close(sockfd);
client[i] = -1;
}else{
Writen(sockfd, buf, n);
}
if(--nready <= 0)
break;
}
}//end of for
}//end of while
}