Linux下高性能网络IO-epoll
做网络IO不得不讲的就是epoll,与大家分享一下,即使总结又是提高。
讲得不好,欢迎大家留言指正。
文章目录
前言
epoll的描述,网络上门很多,不再过多的陈述。
摘自网络:
一、epoll的基础知识
- 基本API
epoll调用,基本的API有:epoll_create,epoll_ctl,epoll_wait; epoll_create:
用来创建一个epoll epoll_wait: 监控哪些可读可写,一次性返回的是所有的可读可写的fd,把内核中就绪队列的一次性拷贝出来。
epoll_ctl: 用来向epoll中,增加(add)、修改(mod)、删除(del)文件描述符;
- 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 event */
epoll_data_t data; /* User data variable */
};
- epoll的基本事件
EPOLLIN:表示对应的文件描述符可以读;
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数可读;
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: ET的epoll工作模式;
- epoll中的水平触发和边缘触发(摘自网络)
水平触发(level-trggered)
只要文件描述符关联的读内核缓冲区非空,有数据可以读取,就一直发出可读信号进行通知,
当文件描述符关联的内核写缓冲区不满,有空间可以写入,就一直发出可写信号进行通知
LT模式支持阻塞和非阻塞两种方式。epoll默认的模式是LT。
边缘触发(edge-triggered)
当文件描述符关联的读内核缓冲区由空转化为非空的时候,则发出可读信号进行通知,
当文件描述符关联的内核写缓冲区由满转化为不满的时候,则发出可写信号进行通知
两者的区别在哪里呢?水平触发是只要读缓冲区有数据,就会一直触发可读信号,而边缘触发仅仅在空变为非空的时候通知一次,
个人总结:水平触发没有处理结束可以反复触发;边缘触发则是一次性的;
二、单线程代码演示tcp服务器
实现一个单线程服务器,从客户端接收到任何数据,都进行确认,发送回复“OK”;
1.大致流程图
2.代码展示
代码如下(示例):
/*
File: epoll.cpp
Function: epoll的编程方法使用,使用epoll编写网络tcp服务器
Writer: syq
Time: 2022-08-11
*/
/*
1. 优点
epoll没有文件描述符的限制;工作效率不会随文件描述符数量增大而效率低;内核级优化;
只遍历触发的,而select则遍历所有的fd
2. 水平触发(没有处理就反复发送),边缘触发(只触发一次)
3. 函数
epool_create\epool_ctl\epoll_wait
4. 事件
EPOLLLET、EPOLLIN、EPOLLOUT、EPOLLPRI、EPOLLERR、EPOLLHUP
5. 操作
add\mod\del
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <string.h>
#include <iostream>
#include <unistd.h>
#define MAX_NUM 512
#define MAX_EVENTS 1024
/*
* 初始化一个server socket
* 参数: nPort使用的端口
* 返回: 返回已经初始化完成的socketfd
*/
int Init_ServerSocket(int nPort)
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1){
perror("socket error!");
exit(1);
}
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(nPort);
bzero(&(server_addr.sin_zero),8);
if(bind(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr)) < 0) {
perror("Bind error:");
exit(1);
}
if(listen(sockfd,MAX_NUM) < 0){
perror("Listen error:");
exit(1);
}
return sockfd;
}
/*
* 演示epoll的单线程使用,缺点一次只能响应一个连接,并且处理数据
*/
int main(int argc,char* argv[])
{
char bufin[1024] = {0};
//1. 得到一个监听socket
int m_nAcceptfd = Init_ServerSocket(8888);
//2. 创建一个epoll
int m_nEpollfd = epoll_create(256);
struct epoll_event ev,EVENTS[MAX_EVENTS];
//3. 默认设置为水平触发,可以返回读取
ev.events = EPOLLIN;
ev.data.fd = m_nAcceptfd;
epoll_ctl(m_nEpollfd,EPOLL_CTL_ADD,m_nAcceptfd,&ev);
//4. 进入循环,开始循环等待epoll的事件触发
while(1){
int nAlreadyIn = epoll_wait(m_nEpollfd,EVENTS,MAX_EVENTS,-1);
//5. 采用遍历
for(int i = 0; i < nAlreadyIn; i ++){
//6. 判断触发的是accepted
if(EVENTS[i].data.fd == m_nAcceptfd){
//7.
if(1){
//8. 调用accept,获取到设备描述符
int socketfd = accept(m_nAcceptfd,NULL,NULL);
std::cout<< "get connected :"<<bufin<<std::endl;
//9. 进入循环读取
while(1){
memset(bufin,0,1024);
int nRead = recv(socketfd,bufin,1024,0);
if(nRead >= 0){
//10. 打印数据
std::cout<< "recv:"<<bufin<<std::endl;
send(socketfd,"ok",2,0);
}else{
std::cout<< "error:"<<std::endl;
close(socketfd);
break;
}
}
}
}
}
}
}
3.使用tcp调试工具运行
可以看到能够正常的实现tcp服务器接收到数据后,回发“OK字段”。
3.代码弊端
这样编写的tcp服务器,即使用了epoll,也面临以下问题:
- 不能多客户端处理:至始至终只能有一个客户端进行连接,不能由多个客户端连接;
- recv函数和send函数在同一个处理触发事件中,当send数据量大的时候,会造成缓冲区阻塞,影响整体效率。
二、使用epoll的几种模型
通过学习和总结,列出了以下几种使用epoll开发的模型:
三、常见的使用epoll的服务器
redis: 使用单线程方式,线程内将send\recv操作不放在一个流程中进行,利用“流程异步”;
nginx:多进程方式;
./sbin/nginx -c conf/nginx.conf
可以看到,启动了一个master进程,4个worker process; 并且worker process都是master process的子进程;
memcached: 多线程模式
三、利用epoll+fork实现高性能网络服务器
1. 使用fork多进程的优缺点
缺点:fork出来的进程被长期占用,分配子进程花费的时间长;
优点:进程隔离,不会相互影响,稳定性更高;
2. 做法
- 一次性创建N个子进程,每个子进程创建epoll,并且进入wait流程;而父进程则调用wait函数等待,否则子进程会编程孤儿进程;
2. 代码
/*
File: epoll_fork.cpp
Function: 使用epoll+fork子进程的方式编写高性能的tcp网络服务器
Writer: syq
Time: 2022-08-12
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <string.h>
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/wait.h>
#define MAX_NUM 512
#define MAX_EVENTS 1024
#define PROCESS_NUM 5
/*
* 初始化一个server socket
* 参数: nPort使用的端口
* 返回: 返回已经初始化完成的socketfd
*/
int Init_ServerSocket(int nPort)
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1){
perror("socket error!");
exit(1);
}
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(nPort);
bzero(&(server_addr.sin_zero),8);
if(bind(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr)) < 0) {
perror("Bind error:");
exit(1);
}
if(listen(sockfd,MAX_NUM) < 0){
perror("Listen error:");
exit(1);
}
return sockfd;
}
/*
* 利用fork创建子进程
*/
int main(int argc,char* argv[])
{
int status = -1;
int flags = 1;
int backlog = 10;
pid_t pid = -1;
char bufin[1024] = {0};
//1. 得到一个监听socket
int m_nAcceptfd = Init_ServerSocket(28888);
//2. 进行fork
for(int a=0; a < PROCESS_NUM; a++){
if(pid !=0){
pid = fork();
}
}
//3.子进程执行
if(pid == 0){
//4. 创建一个epoll
int m_nEpollfd = epoll_create(256);
struct epoll_event ev,EVENTS[MAX_EVENTS];
//5. 默认设置为水平触发,可以返回读取
ev.events = EPOLLIN;
ev.data.fd = m_nAcceptfd;
epoll_ctl(m_nEpollfd,EPOLL_CTL_ADD,m_nAcceptfd,&ev);
//6. 进入循环,开始循环等待epoll的事件触发
while(1){
int nAlreadyIn = epoll_wait(m_nEpollfd,EVENTS,MAX_EVENTS,-1);
//5. 采用遍历
for(int i = 0; i < nAlreadyIn; i ++){
//6. 判断触发的是accepted
if(EVENTS[i].data.fd == m_nAcceptfd){
//7. 调用accept,获取到设备描述符
int socketfd = accept(m_nAcceptfd,NULL,NULL);
std::cout<< "get connected :"<<socketfd<<std::endl;
//8.将新创建的socket设置为 NONBLOCK 模式
flags = fcntl(socketfd, F_GETFL, 0);
fcntl(socketfd, F_SETFL, flags|O_NONBLOCK);
//9. 将它放进epoll,并且设置为边缘触发
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = socketfd;
epoll_ctl(m_nEpollfd,EPOLL_CTL_ADD,socketfd,&ev);
}else{
//10. 继续执行代码
if(EVENTS[i].events & EPOLLIN){
//11.读数据
memset(bufin,0,1024);
int nRead = recv(EVENTS[i].data.fd,bufin,1024,0);
if(nRead <= 0){
//13. 做错误数据分类
switch (errno){
case EAGAIN: //说明暂时已经没有数据了,要等通知
break;
case EINTR: //被终断了,再来一次
printf("recv EINTR... \n");
nRead = recv(EVENTS[i].data.fd, bufin, 1024, 0);
break;
default:
printf("the client is closed, fd:%d\n", EVENTS[i].data.fd);
epoll_ctl(m_nEpollfd, EPOLL_CTL_DEL, EVENTS[i].data.fd, &ev);
close(EVENTS[i].data.fd);
;
}
break;
}
if(nRead > 0){
//12. 打印数据
std::cout<< "recv:"<<bufin<<std::endl;
send(EVENTS[i].data.fd,"ok",2,0);
}
}
}
}
}
}else{
std::cout<<"wait"<<std::endl;
waitpid(-1,&status,0);
}
return 0;
}
2. 效果
三、讲到“惊群效应”
惊群现象就是多进程(多线程)在同时阻塞等待同一个事件的时候(休眠状态),如果等待的这个事件发生,那么他就会唤醒等待的所有进程(或者线程),但是最终却只可能有一个进程(线程)获得这个时间的“控制权”,对该事件进行处理,而其他进程(线程)获取“控制权”失败,只能重新进入休眠状态,这种现象和性能浪费就叫做惊群
参考链接:https://www.zhihu.com/question/22756773
三、总结
epoll与select的比较; epoll没有文件描述符的限制;工作效率不会随文件描述符数量增大而效率低;内核级优化;
epoll只遍历触发的,而select则遍历所有的fd;
下一阶段,我们将继续研究redis,nginx等源码。