I/O多路转接---epoll服务器
程序员文章站
2022-06-14 14:37:48
...
epoll是改进的poll,几乎结合了poll的所有优点,并将poll的缺点加以改进,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
epoll主要做三件事情:
1.创建一个红黑树
红黑树结点内容,保存了用户想要告诉操作系统要监控的哪些文件描述符上的哪些事件。
2.从驱动到操作系统的回调机制
回调机制不需要操作系统一直在等,在事件就绪时,驱动会告诉操作系统,有事件就绪了,操作系统就会处理眼前的事件。这个回调机制在内核中称为epollcallback,它将发生的事件添加到rdlist
3.创建一个就绪队列
在事件就绪后,操作系统将对应的文件描述符上的事件的结点放在就绪队列中,由用户检查就绪队列,来判断是否有事件就绪。
如下图所示:
epoll相关的系统调用
epoll_create
#include <sys/epoll.h>
int epoll_create(int size);
功能:创建一个epoll的句柄。(即创建epoll模型)
说明:
* 从Linux2.6.8之后,size参数是被忽略的。
* 使用完后,必须调用close()关闭。
epoll_ctl
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:epoll的事件注册函数。
说明:
* 在监听事件前,要先注册监听什么类型的事件.
* 第一个参数epfd是epoll的句柄.
* 第二个参数表示动作,有三个宏.
* 第三个参数需要监听的fd.
* 第四个参数是告诉内核需要监听什么事。
其中,op的取值:
* EPOLL_CTL_ADD:注册新的fd到epfd中.
* EPOLL_CTL_MOD:修改已经注册的fd的监听事件.
* EPOLL_CTL_DEL:从epfd中删除一个fd.
struct epoll_event结构:
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event
{
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
} __attribute__ ((__packed__));
events取值
事件 | 描述 |
---|---|
EPOLLIN | 对应的文件描述符可读 |
EPOLLOUT | 对应的文件描述符可写 |
EPOLLPRI | 对应的文件描述符有紧急的数据可以读 |
EPOLLERR | 对应的文件描述符发生错误 |
EPOLLHUP | 对应的文件描述符被挂断 |
EPOLLET | 将epoll设置为边缘触发模式 |
EPOLLONESHOT | 只监听一次事件 |
enum EPOLL_EVENTS
{
EPOLLIN = 0x001,
EPOLLPRI = 0x002,
EPOLLOUT = 0x004,
EPOLLRDNORM = 0x040,
EPOLLRDBAND = 0x080,
EPOLLWRNORM = 0x100,
EPOLLWRBAND = 0x200,
EPOLLMSG = 0x400,
EPOLLERR = 0x008,
EPOLLHUP = 0x010,
EPOLLRDHUP = 0x2000,
EPOLLONESHOT = (1 << 30),
EPOLLET = (1 << 31)
};
其实,这些事件就是位图,是二进制序列,不会重复,要是想让多个事件同时发生,可以用或。
epoll_wait
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
功能:收集epoll监控的事件中已发生的事件。
说明:
* events是已经分配好的epoll_event结构体数组。
* epoll将会把发生的事件赋值到events数组中。(内核只负责将数据复制到这个数组中,不会帮助我们在用户态中分配内存)。
* maxevents的值告诉内核events的大小,但不能超过epoll_create()中的size。
* timeout是超时时间(0立即返回,-1阻塞)。
* 如果函数调用成功,返回对应I/O上已准备好的文件描述符个数(0表示超时,小于0函数失败)。
epoll的使用过程有三步:
* 调用epoll_create创建一个epoll句柄。
* 调用epoll_ctl,将要监控的文件描述符进行注册。(用户告诉操作系统)
* 调用epoll_wait,等待文件描述符就绪。(操作系统告诉用户)
*
epoll的优点
- 文件描述符无上限。通过epoll_ctl()注册一个文件描述符,内核中用红黑树管理所要监控的文件描述符。
- 事件的就绪方式。一旦被监听的文件描述符就绪,内核会采用类似于callback的回调机制,**该文件描述符,随着文件描述符的增加,也不会影响判定就绪的性能。
- 维护一个就绪队列。当文件描述符就绪,就回去被放到内核中的一个就绪队列,调用epoll_wait获取就绪文件描述符时,只需要取就绪队列中的元素就可以了,时间复杂度是O(1)。
我认为网上说的这句话是不对的,内存映射机制(内核直接将就绪队列通过mmap方式映射到用户态,避免了拷贝内存这样的额外性能开销)。
原因有两点:
1.操作系统不相信任何一个人,只会拷贝,不会直接暴露给用户。
2.epoll_wait自己提供了缓冲区,将数据拷贝到缓冲区,不存在映射。
epoll的工作方式
1.水平触发(Lever Triggered)
* 只要有数据,操作系统就会通知用户,epoll会一直处于触发状态。
* epoll可以对数据不做立刻的处理。
* 直到缓冲区上的数据都被处理完,epoll_wait才会返回。
* 支持阻塞读写和非阻塞读写。
2.边缘触发(Eage Triggered)
* 当epoll检测到socket上事件就绪时,必须立刻处理。
* 文件描述符上的事假就绪后,只有一次处理机会。
* ET的性能相对LT高,但是对用户的要求更高。
* Nginx默认下采用ET模式。
* 只支持非阻塞的读写。
水平触发(LT)
epollServer.c(服务器)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int start_up(int port)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock<0)
{
perror("socket");
exit(2);
}
//设置没有TIME_WAIT
int opt=1;
setsockopt(sock, SOL_SOCKET,SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(sock, (struct sockaddr*)&local, sizeof(local));
if(ret < 0)
{
perror("bind");
exit(3);
}
if(listen(sock, 5)<0)
{
perror("listen");
exit(4);
}
return sock;
}
void serviceIO(int efd, struct epoll_event* buf, int num, int listen_sock)
{
struct epoll_event eve;
int i = 0;
for(i=0; i<num; ++i)
{
int fd = buf[i].data.fd;
if((buf[i].events)&EPOLLIN)//判断是否有读事件发生
{
if(buf[i].data.fd == listen_sock)
{
//listen sock is ready
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(listen_sock,\
(struct sockaddr*)&client,&len );
if(new_sock < 0)
{
perror("accept");
continue;
}
printf("get a new connect:%s:%d\n",\
inet_ntoa(client.sin_addr),\
ntohs(client.sin_port));
eve.events = EPOLLIN;
eve.data.fd = new_sock;
int ret = epoll_ctl(efd, EPOLL_CTL_ADD, new_sock, &eve);
if(ret < 0)
{
perror("epoll_ctl");
exit(7);
}
}
// normal sock is ready
else
{
eve.events = EPOLLOUT;
eve.data.fd = fd;
char buf[1024];
ssize_t s = read(fd, buf, sizeof(buf)-1);
if(s > 0)
{
buf[s]=0;
printf("client; %s\n",buf);
epoll_ctl(efd, EPOLL_CTL_MOD, fd, &eve);
}
else if(s == 0)
{
close(fd);
epoll_ctl(efd, EPOLL_CTL_DEL, fd, NULL);
printf("client quit\n");
}
else
{
perror("read");
close(fd);
epoll_ctl(efd, EPOLL_CTL_DEL, fd, NULL);
}
}
}
if((buf[i].events)&EPOLLOUT)//判断写事件是否就绪
{
const char* str="HTTP/1.0 200 Ok\r\n\r\n<html><h1>hello</h1></html>";
write(fd, str, strlen(str));
close(fd);
epoll_ctl(efd, EPOLL_CTL_DEL, fd, NULL);
}
}
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
printf("Usage: ./server [port]\n");
return 1;
}
int listen_sock = start_up(atoi(argv[1]));
int epfd = epoll_create(256);
if(epfd < 0)
{
perror("epoll_create");
return 5;
}
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD,listen_sock, &ev);
if(ret < 0)
{
perror("epoll_ctl");
return 6;
}
for(;;)
{
struct epoll_event buf[200];
int num = epoll_wait(epfd, buf, sizeof(buf), -1);
switch(num)
{
case -1:
perror("epoll_wait");
break;
case 0:
printf("timeout....\n");
break;
default:
serviceIO(epfd, buf, num, listen_sock);
break;
}
}
}
telnet远程登录测试。
用浏览器访问。
上一篇: 微服务中的Zuul网关
下一篇: 高级I/O中多路转接之epoll
推荐阅读
-
一篇文章帮你彻底搞清楚“I/O多路复用”和“异步I/O”的前世今生
-
Linux I/O多路复用详解及实例
-
python 之 并发编程(非阻塞IO模型、I/O多路复用、socketserver的使用)
-
【面试】一篇文章帮你彻底搞清楚“I/O多路复用”和“异步I/O”的前世今生
-
I/O多路复用
-
linux进程间通信---本地socket套接字(五)---多路IO转接服务器实现一个server对应多个client---poll实现
-
7.10 第九章I/O复用高级函数 select poll epoll(lt et)
-
select模块(I/O多路复用)
-
I/O多路转接之epoll(实现epoll版本的TCP服务器)
-
I/O多路转接——select、poll 和 epoll