前言
后端开发的应该都知道Nginx服务器,Nginx是一个高性能的 HTTP 和反向代理服务器,也是一个 IMAP/POP3/SMTP 代理服务器。后端部署中一般使用的就是Nginx反向代理技术。
Nginx 相较于 Apache 具有占有内存少,稳定性高等优势,并发能力强的优点。它所使用的网络通信模型就是epoll。
*注:epoll模型编程实例需要先了解红黑树、tcp/ip、socket、文件描述符fd、阻塞、回调等概念。
epoll介绍
一、epoll模型概念
传统的并发服务器Apache,使用的是多进程/线程模型,每一个客户端请求都要开启一个进程去处理,占用的资源大。
epoll是一个I/O多路复用模型,可以用一个进程去处理处理多个客户端。
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。关于select和poll是更早的多路复用IO模型,这里不做介绍。
相对于select和poll来说,epoll更加灵活,没有描述符限制。
epoll使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
epoll是基于事件驱动模型。展开应该叫event poll,事件轮询(猜测),所以程序围绕着event运行。
二、epoll模型详细执行过程
在Linux中,epoll模型相关的有3个系统API,通过man 2查看手册。
int epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
*此外还有一个close(),create返回的是一个文件描述符int epoll_fd,结束时和普通fd一样要关闭,保证逻辑的完整性。
1.第一步,创建eventepoll结构体
当某一进程调用epoll_create函数时(参数size是事件最大数量,实际上这只是给内核的一个参考值,Linux2.6.8以后这个参数被忽略,但是api文档仍然建议填写),
Linux内核会创建一个eventpoll结构体,并返回一个int epoll_fd,这就是epoll通过一个文件描述符操作多个文件描述符的方法。
这个结构体中有两个成员与epoll的使用方式密切相关。eventpoll结构体如下所示:
struct eventpoll{ struct rb_root rbr; struct list_head rdlist; };
其中rbr是一个红黑树,它的每个结点用来存储用户关心的事件(用户关心的事件,比如服务端server_fd的accept连接请求就是一个事件)。
rblist是一个双向链表用来存储已发生的事件。
事件的结构体
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 */ };
一个事件应该对应至少着一个文件描述符加I/O操作,代表这个事件对应的文件描述符和它是读事件还是写事件。
I/O是同一个变量events,通过"按位或"操作可以同时添加关心读和写事件,"按位与"操作把它读取。
而它对应的文件描述符在变量data中,epoll_event.data.fd。
2. 第二步,epoll_ctl操作红黑树
当用户调用epoll_ctl向结构体加入event时,会把事件挂在到红黑树rbr中。
而所有添加到epoll中的事件都会与低层接口(设备、网卡驱动程序)建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。
这个回调方法在内核中叫ep_poll_callback,这个过程中,因为用户关心的事件挂载在红黑树上,所以查找效率高只有O(ln(n))的事件复杂度。
然后它会将发生的事件添加到rdlist双链表中。红黑树加上函数回调的机制造就了它的高效。
3. 第三步,epoll_wait检查双向链表
当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。
如果rdlist不为空,则把发生的事件复制到用户态(把内核的双向链表拷贝成一个struct epoll_event数组),同时返回文件描述符的数量。
用户只需要用这个数组去接收就可以。
为什么这里要用双向链表而不是单链表?
就绪列表引用着就绪的Socket,所以它应能够快速的插入数据。程序可能随时调用 epoll_ctl 添加监视Socket,也可能随时删除。
当删除时,若该Socket 已经存放在就绪列表中,它也应该被移除。所以就绪列表应是一种能够快速插入和删除的数据结构。
双向链表就是这样一种数据结构,Epoll 使用双向链表来实现就绪队列。
epoll_event示意图
三、编程实例
具体实现可以用一个进程去处理多客户端请求,而不用
1. 使用的头文件
#include#include <string.h> #include #include #include #include #include in.h> #include #include #include #include /* epoll模型api */
2. 函数声明
#define SERV_PORT 5001 #define SERV_IP_ADDR "192.168.1.7" #define QUIT "quit" /*用户退出指令*/ #define BACKLOG 5 /*监听的最大等待连接队列*/ #define EPOSIZE 100 /*接收已发生事件的最大数量*/
/* 套接字初始化的封装 */ int sock_init(int fd, struct sockaddr_in *sin); /* epoll_wait获得已发生的事件集合之后,具体的业务逻辑 */ void handle_events(int epoll_fd, struct epoll_event *events, int num, int accept_fd); /* 具体操作1,接收客户端连接 */ void do_accpet(int epoll_fd, int accept_fd); /* 具体操作2,读操作 */ void do_read(int epoll_fd, int fd, char *buff); /* 具体操作3,写操作 */ void do_write(int epoll_fd, int fd, char *buff); /*把epoll_ctl函数的操作再封装*/ void event_ctl(int epoll_fd, int fd, int flag, int state); /* argument: flag EPOLL_CTL_ADD 添加事件 EPOLL_CTL_DEL 删除事件 EPOLL_CTL_MOD 修改事件 argument: state EPOLLIN input事件 EPOLLOUT output事件 */
3. demo实现
#include "server.h" int main() { int ret = -1; int accept_fd = socket(AF_INET, SOCK_STREAM, 0); if(accept_fd < 0) { perror("socket"); return 1; } struct sockaddr_in sin; ret = sock_init(accept_fd, &sin); if(ret < 0) { perror("sock_init"); return 2; } int epoll_fd = epoll_create(EPOSIZE); struct epoll_event events[EPOSIZE];/*用户空间数组去接收内核的双向链表*/ event_ctl(epoll_fd, accept_fd, EPOLL_CTL_ADD, EPOLLIN);//先把server_fd accept input事件加入红黑树 while(1) { ret = epoll_wait(epoll_fd, events, EPOSIZE, -1);/*参数4,超时时间,特别的-1为阻塞等待,详见linux api: man 2 epoll_wait*/ handle_events(epoll_fd, events, ret, accept_fd); } close(epoll_fd); close(accept_fd); }
int sock_init(int fd, struct sockaddr_in *sin) { bzero(sin,sizeof(*sin)); sin->sin_family = AF_INET; sin->sin_port = htons(SERV_PORT); sin->sin_addr.s_addr = INADDR_ANY; if(bind(fd, (struct sockaddr*)sin, sizeof(*sin)) < 0) { perror("bind"); return -1; } if(listen(fd, BACKLOG) < 0) { perror("listen"); return -2; } return 0; }
void handle_events(int epoll_fd, struct epoll_event *events, int num, int accept_fd) { int i,fd; char buff[BUFSIZ]; for(i=0; i) { fd = events[i].data.fd; if((fd == accept_fd) && events[i].events & EPOLLIN)/* 对events和EPOLLIN“与”操作,判断这个文件描述符是否有input事件*/ do_accpet(epoll_fd, fd); else if(events[i].events & EPOLLIN) do_read(epoll_fd, fd, buff); else if(events[i].events & EPOLLOUT) do_write(epoll_fd, fd, buff); } }
void do_accpet(int epoll_fd, int accept_fd) { int new_fd; struct sockaddr_in cin; socklen_t len; new_fd = accept(accept_fd, (struct sockaddr*)&cin, &len); if(new_fd < 0) { perror("accpet"); return; } printf("a new client connected!\n");
/*add_event input,
client连接之后,把它的文件描述符的input事件加入到红黑树中*/
*/
event_ctl(epoll_fd, new_fd, EPOLL_CTL_ADD, EPOLLIN); }
void do_read(int epoll_fd, int fd, char *buff) { int ret = -1; bzero(buff, BUFSIZ); ret = read(fd, buff, BUFSIZ-1); if(ret == 0 || !strncmp(buff, QUIT, strlen(QUIT))) { printf("a client quit.\n"); close(fd); event_ctl(epoll_fd, fd, EPOLL_CTL_DEL, EPOLLIN);//delete_event in return; } if(ret < 0) { perror("read"); return; } printf("%s\n", buff); event_ctl(epoll_fd, fd, EPOLL_CTL_MOD, EPOLLOUT);//modif_event out } void do_write(int epoll_fd, int fd, char *buff) { int ret = -1; ret = write(fd, buff, strlen(buff)); if(ret <= 0) perror("write"); event_ctl(epoll_fd, fd, EPOLL_CTL_MOD, EPOLLIN);//modif_event in }
/*
参考的资料中把他分成几个操作,更直观
这里为了方便多加一个参数,增加事件/删除事件/修改事件/,把增删改集成到一个函数。
*/ void event_ctl(int epoll_fd, int fd, int flag, int state) { struct epoll_event ev; ev.events = state; ev.data.fd = fd; epoll_ctl(epoll_fd, flag, fd, &ev); }
四、其它
触发模式
关于epoll的水平触发LT和边缘触发ET还没研究清楚,
应该是类似驱动程序中检测硬件信号中,高/低电平触发,上升沿/下降沿触发。
* !!FIXME
参考资料:
https://www.cnblogs.com/Anker/archive/2013/08/17/3263780.html
https://blog.csdn.net/u011063112/article/details/81771440
https://blog.csdn.net/armlinuxww/article/details/92803381