epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
了解epoll本质要从操作系统进程调度的角度来看数据接收。阻塞是进程调度的关键一环,指的是进程在等待某事件(如接收到网络数据)发生之前的等待状态,recv、select和epoll都是阻塞方法。
//创建socket
int s = socket(AF_INET, SOCK_STREAM, 0);
//绑定
bind(s, ...)
//监听
listen(s, ...)
//接受客户端连接
int c = accept(s, ...)
//接收客户端数据
recv(c, ...);
//将数据打印出来
printf(...)
这是一段最基础的网络编程代码,先新建socket对象,依次调用bind、listen、accept,最后调用recv接收数据。recv是个阻塞方法,当程序运行到recv时,它会一直等待,直到接收到数据才往下执行。
操作系统为了支持多任务,实现了进程调度的功能,会把进程分为“运行”和“等待”等几种状态。运行状态是进程获得cpu使用权,正在执行代码的状态;等待状态是阻塞状态,比如上述程序运行到recv时,程序会从运行状态变为等待状态,接收到数据后又变回运行状态。操作系统会分时执行各个运行状态的进程,由于速度很快,看上去就像是同时执行多个任务。
但是简单的方法往往有缺点,主要是:
其一,每次调用select都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历(遍历进程A关心的所有socket,需要注意的是添加从等待队列头部添加,删除通过回调直接实现,所以每个socket的等待队列不用遍历),而且每次都要将整个fds列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。
其二,进程被唤醒后,程序并不知道哪些socket收到数据,还需要遍历一次(这一次遍历是在应用层)。
epoll是在select出现N多年后才被发明的,是select和poll的增强版本。epoll通过以下一些措施来改进效率。
措施一:功能分离
select低效的原因之一是将“维护等待队列”和“阻塞进程”两个步骤合二为一。如下图所示,每次调用select都需要这两步操作,然而大多数应用场景中,需要监视的socket相对固定,并不需要每次都修改。epoll将这两个操作分开,先用epoll_ctl维护等待队列,再调用epoll_wait阻塞进程(解耦)。显而易见的,效率就能得到提升。
相比select,epoll拆分了功能,功能分离,使得epoll有了优化的可能。
措施二:就绪列表
select低效的另一个原因在于程序不知道哪些socket收到数据,只能一个个遍历。如果内核维护一个“就绪列表”,引用收到数据的socket,就能避免遍历。如下图所示,计算机共有三个socket,收到数据的sock2和sock3被rdlist(就绪列表)所引用。当进程被唤醒后,只要获取rdlist的内容,就能够知道哪些socket收到数据。
为方便理解后续的内容,我们先复习下epoll的用法。如下的代码中,先用epoll_create创建一个epoll对象epfd,再通过epoll_ctl将需要监视的socket添加到epfd中,最后调用epoll_wait等待数据。
int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...)
listen(s, ...)
int epfd = epoll_create(...);
epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中
while(1){
int n = epoll_wait(...)
for(接收到数据的socket){
//处理
}
}
服务端代码
/******************************************************************************
* Copyright (C) 2020, 协议森林.
*
* File Name: tcpepoll.c
* Author: 协议森林
* Date: 2020-12-22
* Description:
-------------------------------------------------------------------------------
功能汇总:
1)关于epoll的用法
2)服务端代码
******************************************************************************/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXEVENTS 100
// 初始化服务端的监听端口。
static int CreateServer(int port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if (sock < 0)
{
printf("socket() failed.\n");
return -1;
}
int opt = 1;
unsigned int len = sizeof(opt);
//一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR是让端口释放后立即就可以被再次使用
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, len);
setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &opt, len);
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(port);
if(bind(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0 )
{
printf("bind() failed.\n");
close(sock);
return -1;
}
if(listen(sock, 5) != 0)//最大可连接为5
{
printf("listen() failed.\n");
close(sock);
return -1;
}
return sock;
}
// 把socket设置为非阻塞的方式。
static int setnonblocking(int sockfd)
{
if (fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)|O_NONBLOCK) == -1)
{
return -1;
}
return 0;
}
int main(int argc,char *argv[])
{
//校验输入参数完整性
if(argc != 2)
{
printf("usage:./tcpepoll port,for example:./tcpepoll 8080.\n");
return -1;
}
// 建立监听的socket
int ListenSock = CreateServer(atoi(argv[1]));
printf("ListenSock=%d\n", ListenSock);
if (ListenSock < 0)
{
printf("CreateServer() failed.\n");
return -1;
}
int EpollFd = 0;
char Buffer[1024];
memset(Buffer, 0, sizeof(Buffer));
//epoll创建一个描述符,max_size标识监听数目最大数
EpollFd = epoll_create(1);
// 添加监听描述符事件
struct epoll_event ev;
ev.data.fd = ListenSock;
ev.events = EPOLLIN;
epoll_ctl(EpollFd, EPOLL_CTL_ADD, ListenSock, &ev);
while (1)
{
//存放有事件发生的结构数组
struct epoll_event events[MAXEVENTS];
//等待监视的socket有事件发生
int infds = epoll_wait(EpollFd, events, MAXEVENTS, -1);
// printf("epoll_wait infds=%d\n",infds);
// 返回失败。
if (infds < 0)
{
printf("epoll_wait() failed.\n");
break;
}
// 超时。
if (infds == 0)
{
printf("epoll_wait() timeout.\n"); continue;
}
// 遍历有事件发生的结构数组。
for (int ii = 0; ii < infds; ii++)
{
printf("infds[%d]. ListenSock[%d].events[%d].data.fd[%d]\n",infds,ListenSock,ii,events[ii].data.fd);
if((events[ii].data.fd == ListenSock) &&(events[ii].events & EPOLLIN))
{
// 如果发生事件的是ListenSock,表示有新的客户端连上来。
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientsock = accept(ListenSock, (struct sockaddr*)&client, &len);
printf("~~~~~~clientsock[%d]\n",clientsock);
if (clientsock < 0)
{
printf("accept() failed.\n");
continue;
}
// 把新的客户端添加到epoll中。
memset(&ev, 0, sizeof(struct epoll_event));
ev.data.fd = clientsock;
ev.events = EPOLLIN;
epoll_ctl(EpollFd, EPOLL_CTL_ADD, clientsock, &ev);
printf ("client(socket=%d) connected ok.\n",clientsock);
continue;
}
else if (events[ii].events & EPOLLIN)
{
// 客户端有数据过来或客户端的socket连接被断开。
char Buffer[1024];
memset(Buffer, 0, sizeof(Buffer));
// 读取客户端的数据。
ssize_t isize = read(events[ii].data.fd, Buffer, sizeof(Buffer));
// 发生了错误或socket被对方关闭。
if (isize <=0)
{
printf("client(eventfd=%d) disconnected.\n",events[ii].data.fd);
// 把已断开的客户端从epoll中删除。
memset(&ev,0,sizeof(struct epoll_event));
ev.events = EPOLLIN;
ev.data.fd = events[ii].data.fd;
epoll_ctl(EpollFd, EPOLL_CTL_DEL, events[ii].data.fd, &ev);
close(events[ii].data.fd);
continue;
}
printf("recv(eventfd=%d,size=%d):%s\n",events[ii].data.fd, isize, Buffer);
//把收到的报文发回给客户端。
write(events[ii].data.fd,Buffer,strlen(Buffer));
}//end else if (events[ii].events & EPOLLIN)
else
{
printf("this is nothing.\n");
}
}
}//end while(1)
close(EpollFd);
return 0;
}
客户端代码:
/******************************************************************************
* Copyright (C) 2020, 协议森林.
*
* File Name: clientepoll.c
* Author: 协议森林
* Date: 2021-1-22
* Description:
-------------------------------------------------------------------------------
功能汇总:
1)关于epoll的用法
2)客户端代码
******************************************************************************/
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int sockfd;
struct sockaddr_in servaddr;
char buf[1024];
//校验输入参数是否完整
if (argc != 3)
{
printf("usage:./tcpclient ip port, for example:./tcpclient 127.0.0.1 8080\n");
return -1;
}
//创建TCP套接字
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0))<0)
{
printf("socket() failed.\n");
return -1;
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(argv[2]));
servaddr.sin_addr.s_addr = inet_addr(argv[1]);
if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0)
{
printf("connect(%s:%s) failed.\n", argv[1], argv[2]);
close(sockfd);
return -1;
}
printf("Now network connect ok.\n");
for (int ii=0; ii < 10000; ii++)
{
// 从命令行输入内容。
memset(buf, 0, sizeof(buf));
printf("Please input:");
scanf("%s", buf);
if(write(sockfd, buf, strlen(buf)) <=0)
{
printf("write() failed.\n");
close(sockfd);
return -1;
}
memset(buf, 0, sizeof(buf));
if(read(sockfd, buf, sizeof(buf)) <=0)
{
printf("read() failed.\n");
close(sockfd);
return -1;
}
printf("recv:%s\n",buf);
}
}
1.如果这篇文章说不清epoll的本质,那就过来掐死我吧!