Linux 中的I/O复用模型有三种:select、poll、epoll。前面两种在内核中的处理方式是类似的,第三种效率最高。
函数原型
#include
int poll(struct pollfd *fds,nfds_t nfds,int timeout);
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events 感兴趣的事件 */
short revents; /* returned envets 实际返回的事件 */
}
第一个是结构体数组的指针,我们可以把一个结构体数组的首地址传递过来,第二个是监听的文件描述符的个数,第三个参数是超时时间
代码
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
typedef std::vector 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)
{
// -1 远远等待不设定超时
// &*pollfds.begin() vector 首元素的地址
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);
// accept4 有SOCK_NONBLOCK | SOCK_CLOEXEC 功能,accept就没有这个功能
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="<
如果客户端关闭套接字close,而服务器调用一次write,服务器会接收一个RST segment(在TCP传输层),如果服务器再次调用了write,这时候就会产生SIGPIPE信号。SIGPIPE默认的处理方式是结束进程。显然不符合高性能服务器7*24小时特征,所以要忽略掉这个信号。
我们应该避免服务器出现TIME_WAIT 状态,因为TIME_WAIT 状态会在内核当中在一段时间内保留一些资源。应尽可能在服务器端避免出现TIME_WAIT 状态
服务器在什么状态下会出现TIME_WAIT 状态呢?如果服务器端主动断开连接(先于client 调用close),服务器就会进入TIME_WAIT,这样的话在内核当中hold住一些内核资源,使得它的并发能力大大降低了。
协议设计上,应该让客户端主动断开连接,这样就把TIME_WAIT状态分散到大量的客户端,服务器的压力大大减少了。
但是还有一个问题,如果一些恶意的客户端不断开连接,一直保持连接,即使客户端不活跃了,这样就会占用服务器端的连接资源,所以服务器端也要有个机制来踢掉不活跃的连接 close,这样的话服务器就进入TIME_WAIT 状态,但是这毕竟是少量的。
SOCK_NONBLOCK 非阻塞的套接字,也可以用F_SETFL(O_NONBLOCK)设置非阻塞
SOCK_CLOEXEC ,也可以用F_SETFD(FD_CLOEXEC) 表示fork 后用execv(2) 替换后,文件描述符是关闭状态
创建的时候就设置非阻塞、execv后关闭文件描述符,对后续的进程造成最小的影响
现在我们的采用的编程模型是 nonblocking socket + I/O 复用
C++11 vector的首元素的地址 vector.data();
1、接收read
客户端请求数据包大,一个数据包分两次read,read可能并没有把connfd所对应的接收缓冲区数据读完,那么connfd 仍然是活跃的,我们应该将读到的数据保存在connfd的应用接收缓冲区,每个connfd 都要有一个绑定对应的应用层缓冲区,读的时候把数据追加到这缓冲区的末尾,那么至于解析协议,让协议来处理,是一条完整的数据就取出数据来处理。
2、write
对数据进行应答,假如应答的数据比较大,比如说10000个字节,write时候只写了1000个字节,这中情况是可能存在的,在广域网上是存在的,因为如果发送缓冲区满了,指的是connfd 内核发送缓冲区,write操作不会阻塞,因为connfd 是非阻塞套接字,我们在接收的时候设置成非阻塞模式了,如果设置成阻塞,write操作就会把线程阻塞住了,其他套接字产生了事件就没办法处理了就会降低服务器的并发性,所以我们不应该用阻塞套接字,应该用非阻塞套接字。 既然是非阻塞connfd内核发送缓冲区满的时候,write并不能一次把所有数据全部发送出去,那这时候该怎么办?那些数据是不是都丢弃了呢?当然不应该丢弃掉,可想而知我们应该有个应用发送缓冲区。那应该怎么处理呢?用下面这个逻辑处理
什么时候会发生POLLOUT 事件,可写事件?connfd内核发送缓冲区不满的时候, connfd POLLOUT 事件就会触发, 有空间了我们把应用层数据拷贝到内核发送缓冲区当中。
可想而知,我们一开始 connfd = accept(...) 的时候不能一开始就关注 POLLOUT事件,如果一开始就关注POLLOUT事件,那内核发送缓冲区不满一直触发POLLOUT 事件,出现忙等待busy_loop。所以应该在发送缓冲区满的时候才关注POLLOUT事件
poll 不支持ET 边缘触发模式,poll是LT电平触发模式。
accept(2)的时候可能返回EMFILE,太多的文件描述符达到了进程打开文件描述符的上线就会出现EMFILE错误,怎么处理呢?解决办法有以下这些。
1、调高进程文件描述符数目。
治标不治本,不管怎么调高总是有限制的,因为系统的资源是有限的。我们的目标是大并发服务器,发挥机器的极限。比方说我们可以设定一个预值,到达以后就不处理它了,比方说系统极限是20000,到达19001的时候我们就不处理它了,但是这个不好,我们无法预知系统的极限。不能解决问题。
2、死等。
比方说系统的极限是20000,死等,等待有一个客户端关闭套接字,死等效率也比较低的。
3、推出程序。
如果出现EMFILE,就退出程序,这有点小题大作,因为这个错误是暂时性的资源不足,如果某个客户端关闭了,那么资源又多起来了又能够处理了,这时候把程序关闭了,不能满足服务器7*24小时不间断的服务。
4、关闭监听套接字。那什么时候重新打开呢?
关闭监听套接字,下次就不能接收新的客户端连接了,我们需要重新打开它,这种也不现实。
5、如果是epoll模型,可以改用edge trigger。问题是如果漏掉了一次accept(2),程序再也不会收到新的连接。
poll是电平触发模式,如果accept 出现EMFILE并且没有关闭程序,监听套接字处于活跃状态,那么监听套接字处于高电平状态,下一次调用poll 又触发监听套接字可读事件,accept 又会触发EMFILE,这就处于busy_loop状态忙等待。
不可取
6、准备一个空闲的文件描述符。遇到这种情况,先关闭这个空闲文件,获得一个文件描述符名额;再accept(2)拿到socket连接的文件描述符;随后立刻close(2),这样就优雅地断开了与客户端的连接;最后重新打开空闲文件,把“坑”填上,以备再次出现这种情况时使用。
可以使用,