IO多路复用是高性能网络编程一个重要的手段。
一,IO多路复用的概念
以前我们用多线程来处理并发的请求,现在可以只用单线程来实现。单线程,通过记录跟踪每个每个I/O流(sock)的状态,来达到同时管理多个I/O流的目的,提高了服务器的吞吐能力。
如图所示,IO多路复用,就如同中间的开关,哪个sock就绪就连上开关,达到了单开关处理了多个I/O流的目的。这就是单线程却能处理多个Sock传输数据的IO多路复用模型。(当Sock请求很多时,可以进行分组,一个线程控制一个组,把多线程与IO复用结合使用)
之前的博客介绍了多线程的方式,两者的区别在于
1. 多线程模型适合于处理短连接,且连接的打开关闭非常频繁的情况,但不适合处理长连接。
2. 多线程毕竟是要耗费资源的,IO多路复用基本不耗费资源,也不必创建,维护线程,使得系统的开销大大减小,效率变得更快。(效率与单线程比较,不一定比多线程快)
I/O复用典型使用在下列网络应用场合:
1. 当客户处理多个描述符(通常是交互式输入和网络套接字)时,必须使用I/O复用。
2. 如果一个TCP服务器既要处理TCP,又要处理UDP,一般就要用I/O复用。
3. 如果一个服务器要处理多个服务或者多个协议,一般就要使用I/O复用。
二,I/O多路复用的实现
在I/O多路复用这个概念被提出以后,select()是第一个实现的(1983年)。
select()原理:
1、使用copy_from_user从用户空间拷贝fd_set到内核空间
2、注册回调函数__pollwait
3、遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)
4、以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。
5、__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。
6、poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。
7、如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。
8、把fd_set从内核空间拷贝到用户空间
如下图流程所示:
通俗点说,select将所有fd放入一个集合中,不停轮询遍历集合,并将未就绪的fd踢出集合,系统可以通过集合来调动就绪fd,只要fd就绪就会被放入集合处理,可以等价于select管理了所有的fd!
当然select细节还有很多,后面提供代码分析。先来总结select的不足之处:
1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。
2. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。
3. select 如果任何一个sock(I/O stream)出现了数据,select 仅仅会返回,但是并不会告诉你是那个sock上有数据,只能自己一个一个的找,数据大时不方便。
4. select不是线程安全的,不同线程不可对同一sock操作。
5. select支持的文件描述符数量太小了,默认是1024。
select代码如下:
#include"../unp.h"
#include
typedef struct server_context_st
{
int cli_cnt;
int clifds[SIZE];
fd_set allfds;
int maxfd;
}server_context_st;
static server_context_st *s_srv_ctx = NULL;
int server_init()
{
s_srv_ctx = (server_context_st*)malloc(sizeof(server_context_st));
if(s_srv_ctx == NULL)
return -1;
memset(s_srv_ctx, 0, sizeof(server_context_st));
for(int i=0; iclifds[i] = -1;
}
return 0;
}
void server_uninit()
{
if(s_srv_ctx)
{
free(s_srv_ctx);
s_srv_ctx = NULL;
}
}
int create_server_proc(const char *ip, short port)
{
int fd;
fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1)
{
perror("socket");
return -1;
}
struct sockaddr_in addrSer;
addrSer.sin_family = AF_INET;
addrSer.sin_port = htons(port);
addrSer.sin_addr.s_addr = inet_addr(ip);
socklen_t addrlen = sizeof(struct sockaddr);
int res = bind(fd, (struct sockaddr*)&addrSer, addrlen);
if(res == -1)
{
perror("bind");
return -1;
}
listen(fd, LISTENQ);
return fd;
}
int accept_client_proc(int srvfd)
{
struct sockaddr_in addrCli;
socklen_t addrlen = sizeof(struct sockaddr);
int clifd;
ACCEPT:
clifd = accept(srvfd, (struct sockaddr*)&addrCli, &addrlen);
if(clifd == -1)
{
goto ACCEPT;
}
printf("accept a new client: %s:%d\n",inet_ntoa(addrCli.sin_addr),addrCli.sin_port);
int i;
for(i=0; iif(s_srv_ctx->clifds[i] == -1)
{
s_srv_ctx->clifds[i] = clifd;
s_srv_ctx->cli_cnt++;
break;
}
}
if(i == SIZE)
{
printf("Server Over Load.\n");
return -1;
}
}
void handle_client_msg(int fd, char *buf)
{
printf("recv buf is:> %s\n",buf);
send(fd, buf, strlen(buf)+1, 0);
}
void recv_client_msg(fd_set *readfds)
{
int clifd;
char buffer[256];
int n;
for(int i=0; icli_cnt; ++i)
{
clifd = s_srv_ctx->clifds[i];
if(clifd < 0)
continue;
if(FD_ISSET(clifd, readfds))
{
n = recv(clifd, buffer, 256, 0);
if(n <= 0)
{
FD_CLR(clifd, &s_srv_ctx->allfds);
close(clifd);
s_srv_ctx->clifds[i] = -1;
s_srv_ctx->cli_cnt--;
continue;
}
handle_client_msg(clifd, buffer);
}
}
}
int handle_client_proc(int srvfd)
{
int clifd = -1;
int retval = 0;
fd_set *readfds = &s_srv_ctx->allfds;
struct timeval tv;
while(1)
{
FD_ZERO(readfds);
FD_SET(srvfd, readfds);
s_srv_ctx->maxfd = srvfd;
tv.tv_sec = 30;
tv.tv_usec = 0;
int i;
for(i=0; icli_cnt; ++i)
{
clifd = s_srv_ctx->clifds[i];
FD_SET(clifd, readfds);
s_srv_ctx->maxfd = (clifd > s_srv_ctx->maxfd ? clifd : s_srv_ctx->maxfd);
}
retval = select(s_srv_ctx->maxfd+1, readfds, NULL, NULL, &tv);
if(retval == -1)
{
perror("select");
return -1;
}
if(retval == 0)
{
printf("server time out.\n");
continue;
}
//accept
if(FD_ISSET(srvfd, readfds))
{
accept_client_proc(srvfd);
}
else
{
recv_client_msg(readfds);
}
}
}
int main(int argc, char *argv[])
{
int sockSer;
if(server_init() < 0)
perror("server_init");
sockSer = create_server_proc(IPADDR, PORT);
if(sockSer < 0)
{
perror("create_server_porc");
goto err;
}
handle_client_proc(sockSer);
return 0;
err:
server_uninit();
return -1;
针对select的不足,1997年提出了poll实现IO复用的模型。
poll模型实现:
和select差别不大,最主要的区别在于:
1. **集合方式不同**select将可操作的sock存入FD_SETSIZE集合中,内核默认32*32=1024。而poll是将对应fd列表由数组保存,大小没有限制。所以poll将可操作的sock数量提升至无限大。
2. **结构不同**select将fd放入集合中后,每次轮询将未就绪的fd删除,有新的就绪的再加入。而poll处理是将每个fd对应的状态更改,就绪为1,未就绪为0,这样就不用每次轮询删除加入fd了。
poll其他与select类似,select的问题除了限制大小poll都存在,比如轮询效率低下,不是线程安全的,没有详细的sock信息。代码如下:
#include"../unp.h"
#include
int sock_bind(const char *ip, short port)
{
int fd;
fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addrSer;
addrSer.sin_family = AF_INET;
addrSer.sin_port = htons(port);
addrSer.sin_addr.s_addr = inet_addr(ip);
socklen_t addrlen = sizeof(struct sockaddr);
bind(fd, (struct sockaddr*)&addrSer, addrlen);
return fd;
}
void handle_connection(struct pollfd *connfds, int num)
{
int n;
char buf[256];
for(int i=1; i<= num; ++i)
{
if(connfds[i].fd == -1)
continue;
if(connfds[i].revents & POLLIN)
{
n = recv(connfds[i].fd, buf, 256, 0);
if(n <= 0)
{
close(connfds[i].fd);
connfds[i].fd = -1;
continue;
}
printf("recv msg:>%s\n",buf);
send(connfds[i].fd, buf, n, 0);
}
}
}
void do_poll(int sockSer)
{
pollfd clientfds[OPEN_SIZE];
clientfds[0].fd = sockSer;
clientfds[0].events = POLLIN;
for(int i=1; i1;
int maxi = 0;
int nready;
struct sockaddr_in addrCli;
socklen_t addrlen = sizeof(struct sockaddr);
int i;
for(;;)
{
nready = poll(clientfds, maxi+1,-1);
if(nready == -1)
{
perror("poll");
exit(1);
}
if(clientfds[0].revents & POLLIN)
{
int sockConn = accept(sockSer, (struct sockaddr*)&addrCli, &addrlen);
if(sockConn == -1)
{
perror("accept");
continue;
}
printf("accept a new client:%s:%d\n",inet_ntoa(addrCli.sin_addr),addrCli.sin_port);
for(i=1; iif(clientfds[i].fd < 0)
{
clientfds[i].fd = sockConn;
break;
}
}
if(i == OPEN_SIZE)
{
printf("Server Over Load.\n");
continue;
}
clientfds[i].events = POLLIN;
maxi = (i > maxi ? i : maxi);
if(--nready <= 0)
continue;
}
handle_connection(clientfds, maxi);
}
}
int main()
{
int sockSer;
sockSer = sock_bind(IPADDR, PORT);
listen(sockSer, LISTENQ);
do_poll(sockSer);
return 0;
}
终于,2002年实现了epoll模式!
epoll可以说是I/O多路复用最新的实现,epoll修复了poll和select绝大部分问题,比如:
epoll 是线程安全的。
epoll 对socket的数量无限制。
epoll 现在不仅告诉sock组里面数据,还会告诉具体哪个sock有数据。
epoll 不是效率低下的轮询模式,是触发模式。
epoll原理是:
epoll不同于select和poll的轮询,只有注册新的fd到epoll句柄中时,才会把所有新的fd拷贝进内核,epoll保证每个fd在整个过程中只被拷贝一次。并且为每个fd指定一个回调函数,当fd就绪时,回调函数就会将就绪的fd加入一个就绪链表中。epoll_wait的工作就是阻塞查看就绪链表中有没有就绪的fd。而如果没有就绪fd,就会把进程加入一个等待队列中。直到有fd就绪,就会调用回调函数,将就绪的fd放入就绪列表中,并唤醒epoll_wail继续等待,直到又有新的fd就绪。。。
可见,这种方式比轮询的效率高出很多,又节约了CPU时间资源。
select,poll,epoll的效率测试图:
代码实现如下:
横轴Dead connections 就是链接数,纵轴是每秒处理请求的数量。
可以看到,epoll每秒处理请求的数量基本不会随着链接变多而下降的,而select和poll当链接数量很多时,效率就低的很多了。
epoll代码实现如下:
#include"../unp.h"
#include"utili.h"
#include
int sock_bind(const char *ip, short port)
{
int fd;
fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addrSer;
addrSer.sin_family = AF_INET;
addrSer.sin_port = htons(port);
addrSer.sin_addr.s_addr = inet_addr(ip);
socklen_t addrlen = sizeof(struct sockaddr);
bind(fd, (struct sockaddr*)&addrSer, addrlen);
return fd;
}
void handle_accept(int epollfd, int listenfd)
{
struct sockaddr_in addrCli;
int sockConn;
socklen_t addrlen = sizeof(struct sockaddr);
sockConn = accept(listenfd, (struct sockaddr*)&addrCli, &addrlen);
if(sockConn == -1)
perror("accept");
else
{
printf("accept a new client:%s:%d\n",inet_ntoa(addrCli.sin_addr),addrCli.sin_port);
add_event(epollfd, sockConn, EPOLLIN);
}
}
void do_read(int epollfd, int fd, char *buf)
{
int nread = read(fd, buf, 256);
if(nread <= 0)
{
printf("Server is Closed.\n");
close(fd);
delete_event(epollfd, fd, EPOLLIN);
}
printf("recv msg:>%s\n",buf);
modify_event(epollfd, fd, EPOLLOUT);
}
void do_write(int epollfd, int fd, char *buf)
{
int nwrite = write(fd, buf, strlen(buf)+1);
if(nwrite <= 0)
{
printf("client is closed.\n");
close(fd);
delete_event(epollfd, fd, EPOLLOUT);
}
else
modify_event(epollfd, fd, EPOLLIN);
}
void handle_events(int epollfd, epoll_event *events, int num, int listenfd, char *buf)
{
int fd;
for(int i=0; iif((fd==listenfd) && (events[i].events & EPOLLIN))
handle_accept(epollfd, listenfd);
else if(events[i].events & EPOLLIN)
do_read(epollfd, fd, buf);
else if(events[i].events & EPOLLOUT)
do_write(epollfd, fd, buf);
}
}
void do_epoll(int listenfd)
{
int epollfd;
epoll_event events[1024];
epollfd = epoll_create(FDSIZE);
add_event(epollfd,listenfd, EPOLLIN);
int res;
char buf[256];
for(;;)
{
res = epoll_wait(epollfd, events, 1024,-1);
if(res == -1)
{
perror("epoll_wait");
exit(1);
}
handle_events(epollfd, events, res, listenfd, buf);
}
close(epollfd);
}
int main()
{
int listenfd;
listenfd = sock_bind(IPADDR, PORT);
listen(listenfd, LISTENQ);
do_epoll(listenfd);
return 0;
}