Linux I/O复用
高性能的网络服务器需要同时并发处理大量的客户端,而采用以前的那种对每个连接使用一个分开的线程或进程方法效率不高,因为处理大量客户端的时候,资源的使用及进程上下文的切换将会影响服务器的性能。一个可替代的方法是在一个单一的线程中使用非阻塞的I/O(non-blocking I/O)。
1.poll 函数原型:
#include<poll.h>
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 使用基本流程:
listenfd POLLIN事件到来 —〉connfd = accpt(…);
关注connfd的POLLIN事件
遍历已连接字套接字集
connfd POLLIN事件到来
read(connfd, ….);
write(connfd, ….);
遍历已连接字套接字集
connfd POLLIN事件到来
read(connfd, ….);
ret = write(connfd, buf, 10000);
if(ret < 10000)
{
将未发完的数据添加应用层缓冲区OutBuffer
关注connfd的POLLOUT事件
}
connfd POLLOUT事件到来
取出应用层缓冲区中的数据发送 write(connfd, ….);
如果应用层缓冲区中的数据发送完毕,取消关注POLLOUT事件
2.accept 返回EMFILE(太多的文件,打开的文件描述符超出上限)的处理
1)调高进程文件描述符数目(治标不治本)
2)死等
3)退出程序(不能满足服务器7*24小时不间断工作)
4)关闭监听套接字。那什么时候重新打开?(不现实)
5)如果是epoll模型,可以改用edge trigger(边沿触发).问题是如果漏掉一次accept,程序再也不会收到新连接。
6)准备一个空闲的文件文件描述符。遇到这种情况,先关闭这个空闲文件,获得一个文件描述符名额;再accpet拿到socket连接的文件描述符;随后立刻close,这样就优雅地断开了与客户端的连接;最后重新打开空闲文件,把‘坑’填上,以备再次出现这种情况时使用。
服务器参考程序:
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <poll.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <vector>
#include <iostream>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
typedef std::vector<struct pollfd> PollFdList;
int main(void)
{
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
int idlefd = open("/dev/null", O_RDONLY | O_CLOEXEC);
int listenfd;
//if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
if ((listenfd = socket(PF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, IPPROTO_TCP)) < 0)
ERR_EXIT("socket");
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
int on = 1;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
ERR_EXIT("setsockopt");
if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("bind");
if (listen(listenfd, SOMAXCONN) < 0)
ERR_EXIT("listen");
struct pollfd pfd;
pfd.fd = listenfd;
pfd.events = POLLIN;
PollFdList pollfds;
pollfds.push_back(pfd);
int nready;
struct sockaddr_in peeraddr;
socklen_t peerlen;
int connfd;
while (1)
{
nready = poll(&*pollfds.begin(), pollfds.size(), -1);
if (nready == -1)
{
if (errno == EINTR)
continue;
ERR_EXIT("poll");
}
if (nready == 0) // nothing happended
continue;
if (pollfds[0].revents & POLLIN)
{
peerlen = sizeof(peeraddr);
connfd = accept4(listenfd, (struct sockaddr*)&peeraddr,
&peerlen, SOCK_NONBLOCK | SOCK_CLOEXEC);
/* if (connfd == -1)
ERR_EXIT("accept4");
*/
if (connfd == -1)
{
if (errno == EMFILE)
{
close(idlefd);
idlefd = accept(listenfd, NULL, NULL);
close(idlefd);
idlefd = open("/dev/null", O_RDONLY | O_CLOEXEC);
continue;
}
else
ERR_EXIT("accept4");
}
pfd.fd = connfd;
pfd.events = POLLIN;
pfd.revents = 0;
pollfds.push_back(pfd);
--nready;
// 连接成功
std::cout<<"ip="<<inet_ntoa(peeraddr.sin_addr)<<
" port="<<ntohs(peeraddr.sin_port)<<std::endl;
if (nready == 0)
continue;
}
//std::cout<<pollfds.size()<<std::endl;
//std::cout<<nready<<std::endl;
for (PollFdList::iterator it=pollfds.begin()+1;
it != pollfds.end() && nready >0; ++it)
{
if (it->revents & POLLIN)
{
--nready;
connfd = it->fd;
char buf[1024] = {0};
int ret = read(connfd, buf, 1024);
if (ret == -1)
ERR_EXIT("read");
if (ret == 0)
{
std::cout<<"client close"<<std::endl;
it = pollfds.erase(it);
--it;
close(connfd);
continue;
}
std::cout<<buf;
write(connfd, buf, strlen(buf));
}
}
}
return 0;
}
2.signal(SIGPIPE, SIG_IGN)
如果客户端关闭套接字close
而服务器端调用一次write,服务器会接收一个RST segment(TCP传输层)
如果服务器再次调用write,这个时候会产生SIGPIPE信号。如果我们不处理这个信号,默认处理方式就是关闭服务器。
因为大并发服务器需要 7 * 24 小时不间断,所以我们需要对这个信号处理。
3.TIME_WAIT 状态对大并发服务器的影响
应尽可能在服务器端避免出现TIME_WAIT状态
如果服务器端 主动断开连接(先与client 调用close),服务器端就会进入TIME_WAIT
协议设计上,应该让客户端主动断开连接,这样把TIME_WAIT状态分散到大量的客户端
如果客户端不活跃了,一些客户端不断开连接,这样子就会占用服务器端的连接资源,服务器也要有个机制来踢掉不活跃的连接,进行close。
epoll LT
它提供了一种类似select或poll函数的机制:
Select(2)只能够同时管理FD_SETSIZE数目的文件描述符
poll(2)没有固定的描述符上限这一限制,但是每次必须遍历所有的描述符来检查就绪的描述符,这个过程的时间复杂度为O(N)。
每次调用poll函数,都需要把监听套接字与已连接套接字所感兴趣的事件数组,copy到内核,效率比较低。
epoll/poll/select 对比:
一个进程中所能打开最多的连接数:
select: 32位: 32 * 32 64位:32 * 64
poll: poll本质与select没什么区别,但是他没有最大连接数的限制,原因是他是基于链表存储的
epoll:虽然连接数有限制,但是很大,1 G内存的机器上可以打开10万左右的连接,2 G是20万左右。
epoll没有select这样对文件描述符上限的限制,也不会像poll那样进行线性的遍历,不需要进行数据copy。因此epoll处理大并发连接有着更高的性能
epoll 处理流程图:
epoll的示例程序:
/****
*头文件略
****/
typedef std::vector<struct epoll_event> EventList;
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(void)
{
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
int idlefd = open("/dev/null", O_RDONLY | O_CLOEXEC);
int listenfd;
//if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
if ((listenfd = socket(PF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, IPPROTO_TCP)) < 0)
ERR_EXIT("socket");
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
int on = 1;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
ERR_EXIT("setsockopt");
if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("bind");
if (listen(listenfd, SOMAXCONN) < 0)
ERR_EXIT("listen");
std::vector<int> clients;
int epollfd;
epollfd = epoll_create1(EPOLL_CLOEXEC); //返回一个文件描述符,也就是说epoll是以特殊文件方式体现给用户
struct epoll_event event;
event.data.fd = listenfd;
event.events = EPOLLIN/* | EPOLLET*/;
epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &event); //增加或删除被epoll监听的文件描述符
EventList events(16);
struct sockaddr_in peeraddr;
socklen_t peerlen;
int connfd;
int nready;
while (1)
{
//等待发生在监听描述符上的事件,保存在events中,它会一直阻塞直到事件发生
nready = epoll_wait(epollfd, &*events.begin(), static_cast<int>(events.size()), -1);
if (nready == -1)
{
if (errno == EINTR)
continue;
ERR_EXIT("epoll_wait");
}
if (nready == 0) // nothing happended
continue;
if ((size_t)nready == events.size())
events.resize(events.size()*2);
for (int i = 0; i < nready; ++i)
{
if (events[i].data.fd == listenfd)
{
peerlen = sizeof(peeraddr);
connfd = ::accept4(listenfd, (struct sockaddr*)&peeraddr,
&peerlen, SOCK_NONBLOCK | SOCK_CLOEXEC);
if (connfd == -1)
{
if (errno == EMFILE)
{
close(idlefd);
idlefd = accept(listenfd, NULL, NULL);
close(idlefd);
idlefd = open("/dev/null", O_RDONLY | O_CLOEXEC);
continue;
}
else
ERR_EXIT("accept4");
}
std::cout<<"ip="<<inet_ntoa(peeraddr.sin_addr)<<
" port="<<ntohs(peeraddr.sin_port)<<std::endl;
clients.push_back(connfd);
event.data.fd = connfd;
event.events = EPOLLIN/* | EPOLLET*/;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event);
}
else if (events[i].events & EPOLLIN)
{
connfd = events[i].data.fd;
if (connfd < 0)
continue;
char buf[1024] = {0};
int ret = read(connfd, buf, 1024);
if (ret == -1)
ERR_EXIT("read");
if (ret == 0)
{
std::cout<<"client close"<<std::endl;
close(connfd);
event = events[i];
epoll_ctl(epollfd, EPOLL_CTL_DEL, connfd, &event);
clients.erase(std::remove(clients.begin(), clients.end(), connfd), clients.end());
continue;
}
std::cout<<buf;
write(connfd, buf, strlen(buf));
}
}
}
return 0;
}
Epoll的两种模式:
1. 水平触发(LT):使用此种模式,当数据可读的时候,epoll_wait()将会一直返回就绪事件。如果你没有处理完全部数据,并且再次在该epoll实例上调用epoll_wait()才监听描述符的时候,它将会再次返回就绪事件,因为有数据可读。ET只支持非阻塞socket。
2. 边缘触发(ET):使用此种模式,只能获取一次就绪通知,如果没有处理完全部数据,并且再次调用epoll_wait()的时候,它将会阻塞,因为就绪事件已经释放出来了。
ET的效能更高,但是对程序员的要求也更高。在ET模式下,我们必须一次干净而彻底地处理完所有事件。LT两种模式的socket都支持。
传递给epoll_ctl(2)的Epoll事件结构体如下所示:
typedefunionepoll_data
{
void*ptr;
intfd;
__uint32_t u32;
__uint64_t u64;
}epoll_data_t;
structepoll_event
{
__uint32_t events;/* Epoll events */
epoll_data_t data;/* User data variable */
};