热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

Linux网络编程>epoll

今天自己整理一下epoll,网上有很多经典的介绍,看了很多~收藏了很多~还是整理一下做个积累,自己的东西好找~1. epoll 模型简介epoll 是Linux I/O 多路复用接口 select/p

今天自己整理一下epoll,网上有很多经典的介绍,看了很多~收藏了很多~还是整理一下做个积累,

自己的东西好找~

1. epoll 模型简介

epoll 是Linux I/O 多路复用接口 select/poll 的加强版,首字母e(enhacement)中文翻译就 加强/提高

顾名思义,很强epoll模型会显著提高程序在大量并发连接中只有少量活跃CPU系统的CPU利用率,

它把用户关心的文件描述符上的事件放在内核的一个事件表中,无需像select和poll那样每次调用都

重复传入文件描述符集,在获取事件的时候,无需遍历整个被监听的文件描述符集,而是遍历那些

被内核IO事件异步唤醒而加入ready队列的描述符集合。所以epoll是Linux大规模高并发网络程序的

首选模型。

2. epoll 模型API

epoll 通过使用一组函数来完成任务。

2.1 epoll_create / epoll_create1

创建一个epoll句柄(epoll特有),用来唯一标识内核中这个事件表,这个特有的epoll文件描述符将是

其他所有epoll系统调用的第一个参数(epollfd),以指定要访问的内核事件表。

#include sys/epoll.h
int epoll_create(int size);
int epoll_create1(int flags);

我们注意到,上面有两个create函数,它们的作用相同,后面一个create1是Linux内核2.6.27版本中新

增加的函数关于两个函数了解:http://man7.org/linux/man-pages/man2/epoll_create.2.html

size: 提示内核要监听的文件描述符个数,于内存大小有关。目前已被废弃

2.2 epoll_ctl,该函数用来操作epoll的内核事件表

#include sys/epoll.h
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 成功返回0,出错返回-1

参数说明:

epfd: 就是函数epoll_create创建的epoll句柄,唯一

op: 是指定的操作类型有三种

EPOLL_CTL_ADD : 向epfd注册fd上的event

EPOLL_CTL_MOD: 修改fd已注册的event

EPOLL_CTL_DEL:  从epfd上删除fd的event

fd: 是操作的文件描述符

event: 指定内核要监听的事件,它是struct epoll_event结构类型的指针

struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};

events  成员描述事件类型,将以下宏定义通过位或方式组合:

EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭)

POLLOUT:表示对应的文件描述符可以写

EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)

EPOLLERR:表示对应的文件描述符发生错误

EPOLLHUP:表示对应的文件描述符被挂断;

EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的

EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再

次把这个socket加入到EPOLL队列里

data 用户数据变量

用于存储用户数据,是epoll_data_t 结构类型,定义如下,epoll_data_t是一个联合体,fd指定事件从属

目标文件描述符,ptr可以用来指定fd相关的用户数据,但两者不能同时使用。

typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;

2.3 epoll_wait,该函数用来等待监听文件描述符上有事件发生

#include sys/epoll.h
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
// 成功返回就绪的文件描述符个数,若出错返回 -1,超时返回0

参数说明:

epfd:           就是函数epoll_crate创建的句柄,唯一

events:        是一个传入传出参数,是一个epoll_event结构指针,用来从内核得到事件集合

maxevents: 告知内核events的大小,但不能大于epoll_create()时创建的size

timeout:       是超时事件,-1为阻塞,0为立即返回,非阻塞,大于0是指定的微妙。

3. LT和ET触发模式(正文,[临摹] ~千呼万唤始出来~)

3.1 概念

LT(Level_triggered 水平触发): 当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知

处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),       那么下次调用

epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,

它会一直通知你!!!如果系统中有大量你不需要读写的就绪文件描述符,  而它们每次都会返回

这样会大大降低处理程序检索自己关心的就绪文件描述符的效率!!!

ET(Edge_triggered 边沿触发): 当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知

处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),                  那么下次调用

epoll_wait()时,它不会通知你  也就是它只会通知你一次,直到该文件描述符上出现第二次可读写

事件才会通知你 这种模式比水平触发效率高  系统不会充斥大量你不关心的就绪文件描述符!!!

阻塞IO:当你去读一个阻塞的文件描述符时,如果在该文件描述符上没有数据可读,那么它会一直

阻塞(通俗一点就是一直卡在调用函数那里),直到有数据可读。  当你去写一个阻塞的文件描述符时

如果在该文件描述符上没有空间(通常是缓冲区)可写,那么它会一直阻塞  直到有空间可写。以上的

读和写我们统一指在某个文件描述符进行的操作,不单单指真正的读数据,写数据, 还包括接收连

接accept(),发起连接connect()等操作...

非阻塞IO:当你去读写一个非阻塞的文件描述符时,不管可不可以读写,它都会立即返回, 返回成

功说明读写操作完成了,返回失败会设置相应errno状态码,根据这个errno可以进一步执行其他处理

它不会像阻塞IO那样,卡在那里不动!!!

3.2 几种IO模型的触发方式

select(), poll()模型都是水平触发模式,信号驱动IO是边沿触发模式,     epoll()模型即支持水平触发

也之处边沿触发,默认水平触发(LT)

3.3 Demo

约定:

这里讨论epoll的LT和ET,以及阻塞IO和非阻塞IO对它们的影响,        对应监听的socket文件描述符

用sockfd,对于accept() 返回的文件描述符(即要读写的文件描述符)用connfd表示。

场景:

1) 水平触发的非阻塞sockfd

2) 边沿触发的非阻塞sockfd

3) 水平触发的阻塞connfd

4) 水平触发的非阻塞connfd

5) 边沿触发的阻塞connfd

6) 边沿触发的非阻塞connfd

以上没有验证阻塞的sockfd,因为epoll_wait()返回必定是已就绪的连接,设不设置阻塞accept()都会立

即返回。例外:UNP里面有个例子,在BSD上,使用select()模型。设置阻塞的监听sockfd时, 当客户

端发起连接请求,由于服务器繁忙没有来得及accept(),此时客户端自己又断开,           当服务器到达

accept()时,会出现阻塞。本机测试epoll()模型没有出现这种情况,我们就暂且忽略这种情况!!!

代码:文件名 epoll_lt_et.c

#include stdio.h
#include stdlib.h
#include string.h
#include errno.h
#include unistd.h
#include fcntl.h
#include arpa/inet.h
#include netinet/in.h
#include sys/socket.h
#include sys/epoll.h
// 最大缓冲区大小
#define MAX_BUFFER_SIZE 5
// epoll最大监听数
#define MAX_EPOLL_EVENTS 20
// LT 模式
#define EPOLL_LT 0
// ET 模式
#define EPOLL_ET 1
// 文件描述符设置为阻塞
#define FD_BLOCK 0
// 文件描述符设置为非阻塞
#define FD_NONBLOCK 1
// 设置fd 为非阻塞
int
set_nonblock(int fd)
{
/* 获取文件的flags */
int flags = fcntl(fd, F_GETFL);
/* 设置文件的flags 为非阻塞*/
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
return flags;
40 }
// 注册文件描述符到epoll,并设置其事件为EPOLLIN(可读事件)
void
addfd_to_epoll(int epoll_fd, int fd, int epoll_type, int block_type)
{
struct epoll_event ep_event;
ep_event.data.fd = fd;
ep_event.events = EPOLLIN;
/* 如果是ET模式,设置EPOLLET */
if(epoll_type == EPOLL_ET)
ep_event.events |= EPOLLET;
/* 设置非阻塞 */
if(block_type == FD_NONBLOCK)
set_nonblock(fd);
/* 注册epoll_fd(epoll_create生成的epoll专用描述符) 文件描述符上的事件
EPOLL_CTL_ADD - 注册
EPOLL_CTL_MOD - 修改
EPOLL_CTL_DEL - 删除
*/
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, ep_event);
// LT 处理流程
void
epoll_lt(int sockfd)
{
char buff[MAX_BUFFER_SIZE];
int ret;
memset(buff, , MAX_BUFFER_SIZE);
printf("====== 水平触发模式开始recv ...\n");
ret = recv(sockfd, buff, MAX_BUFFER_SIZE, );
printf("ret = [%d]\n", ret);
if(ret )
printf("收到消息:%s, 共%d字节\n", buff, ret);
else {
if(ret == )
printf("客户端主动关闭.\n");
close(sockfd);
printf("====== 水平触发处理结束 \n");
// 带循环的ET处理流程
void
epoll_et_loop(int sockfd)
{
char buff[MAX_BUFFER_SIZE];
int ret;
printf("====== 带循环的ET开始recv数据...\n");
while() {
memset(buff, , MAX_BUFFER_SIZE);
ret = recv(sockfd, buff, MAX_BUFFER_SIZE, );
if(ret == -) {
if(errno == EAGAIN || errno == EWOULDBLOCK) {
printf("循环读完所有数据.\n");
break;
}
close(sockfd);
break;
} else if(ret == ) {
printf("客户端主动关闭请求.\n");
close(sockfd);
break;
} else
printf("收到消息:%s, 共%d个字节.\n", buff, ret);
printf("====== 带循环ET处理结束\n");
// 不带循环的ET处理流程,比epoll_et_loop少了while循环
void
epoll_et_nonloop(int sockfd)
{
char buff[MAX_BUFFER_SIZE];
int ret;
printf("====== 不带循环的ET开始recv数据...\n");
memset(buff, , MAX_BUFFER_SIZE);
ret = recv(sockfd, buff, MAX_BUFFER_SIZE, );
if(ret )
printf("收到消息:%s, 共%d个字节.\n", buff, ret);
else {
if(ret == )
printf("客户端主动关闭.\n");
close(sockfd);
printf("====== 不带循环ET处理结束\n");
// 处理epoll的返回结果
void
epoll_process(int epollfd, struct epoll_event *events, int number,
int sockfd, int epoll_type, int block_type)
struct sockaddr_in client_addr;
socklen_t client_addrlen;
int newfd, connfd;
int i;
for(i = ; i number; i++) {
newfd = events[i].data.fd;
if(newfd == sockfd) {
printf("====== 新一轮accept() ======\n");
printf("accept()开始...\n");
/* 休眠3秒,模拟一个繁忙的服务器,不能立即处理accept连接 */
sleep();
client_addrlen = sizeof(client_addr);
cOnnfd= accept(sockfd,(struct sockaddr*) client_addr, client_addrlen);
printf("cOnnfd= [%d]\n", connfd);
/* 注册已连接的socket到epoll,并设置LT还是ET,是阻塞还是非阻塞 */
addfd_to_epoll(epollfd, connfd, epoll_type, block_type);
printf("accept()结束!!!\n");
}else if(events[i].events EPOLLIN) {
/* 可读事件处理流程 */
if(epoll_type == EPOLL_LT) {
printf("====== 水平触发开始...\n");
epoll_lt(newfd);
}else if(epoll_type == EPOLL_ET) {
printf("====== 边沿触发开始...\n");
/* 带循环的ET模式 */
epoll_et_loop(newfd);
/* 不带循环的ET模式*/
//epoll_et_nonloop(newfd);
}
}else
printf("其他事件发生...\n");
// 出错处理函数
void
err_exit(char *msg)
{
perror(msg);
exit();
// 创建socket
int
creat_socket(const char *ip, const int port_number)
{
//struct sockaddr_in server_addr = {AF_INET};
struct sockaddr_in server_addr;
int sockfd, reuse = ;
memset( server_addr, , sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port_number);
if(inet_pton(PF_INET, ip, server_addr.sin_addr) == -)
err_exit("inet_pton() error.");
/* 创建socket */
if((sockfd = socket(PF_INET, SOCK_STREAM, )) == -)
err_exit("socket() error.");
/* 设置复用socket地址 */
if(setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, reuse, sizeof(reuse)) == -)
err_exit("setsockopt() error.");
/* 绑定 */
if(bind(sockfd, (struct sockaddr*) server_addr, sizeof(server_addr)) == -)
err_exit("bind() error.");
/* 监听 */
if(listen(sockfd, ) == -)
err_exit("listen() error.");
return sockfd;
int main(int argc, char *argv[])
{
if(argc ) {
fprintf(stderr,"Usage: %s ip_address port_number\n", argv[]);
exit();
int sockfd, epollfd, number;
sockfd = creat_socket(argv[], atoi(argv[]));
struct epoll_event events[MAX_EPOLL_EVENTS];
/*
linux内核2.6.27版的新函数,和epoll_create(int size)一样的功能
并去掉了无用的size参数
*/
if((epollfd = epoll_create1()) == -) // 注意这里epoll_create1是数字1,不是字母'l'
err_exit("epoll_create1() error.");
/*
* 以下设置针对监听的sockfd, 当epoll_wait返回时,必定有事件发生,
* 所以这里我们忽略罕见的情况外设置阻塞IO没意义,我们设置为非阻塞IO
**/
/* sockfd - 非阻塞LT模式 */
addfd_to_epoll(epollfd, sockfd, EPOLL_LT, FD_NONBLOCK);
/* sockfd - 非阻塞的ET模式 */
//addfd_to_epoll(epollfd, sockfd, EPOLL_ET, FD_NONBLOCK);
while() {
number = epoll_wait(epollfd, events, MAX_EPOLL_EVENTS, -);
if(number == -)
err_exit("epoll_wait() error.");
else {
/* 以下的LT,ET 以及是否阻塞都是针对accept()函数返回的文件描述符,即connfd */
/* connfd: 阻塞式的LT模式 */
epoll_process(epollfd, events, number, sockfd, EPOLL_LT, FD_BLOCK);
/* connfd: 非阻塞式的LT模式 */
//epoll_process(epollfd, events, number, sockfd, EPOLL_LT, FD_NONBLOCK);
/* connfd: 阻塞式的ET模式 */
//epoll_process(epollfd, events, number, sockfd, EPOLL_ET, FD_BLOCK);
/* connfd: 非阻塞式的ET模式 */
//epoll_process(epollfd, events, number, sockfd, EPOLL_ET, FD_NONBLOCK);
close(sockfd);
return ;
}

写一个简单的Makefile 编译运行

epoll_lt_et : epoll_lt_et.c
gcc -W -Wall -o $@ $^
clean:
rm -fr epoll_lt_et

4. 验证(~犹抱琵琶半遮面~)

代码如上,还需要运行才能看到现象,(伍佰曰:来来来,干了这杯,还有三杯~)

1. 验证水平触发的非阻塞sockfd, 关键代码在 254行,编译运行

这是我们运行后再另一个终端快速用5个客户端连接到服务器,可以看到5个终端连接都处理完成了返回

新的connfd为5, 6, 7,8,9

迅速连接5个客户端

上面测试完后,我们批量kill 掉那5个客户端,继续后续测试

2. 边沿触发的非阻塞sockfd, 注释上面 254 行,打开 257 行,同样编译运行,快速在另一个终端里面创建

5个客户连接,或者10个,这里创建了5个连接

我们看服务器反应,5个客户端值处理了3个,说明在高并发时,会出现客户端连接不上的问题

3. 水平触发的阻塞connfd, 我们先把sockfd改回到水平触发,注释 257,打开254, 重点代码在 268, 编译

运行用一个客户端连接,发送123456789,

再看服务器的反应,可以看到水平触发了2次,因为我们代码缓冲区的大小为5个字节,处理代码一次接

收不完水平触发一直触发,直到数据全部读取完毕

4. 水平触发的非阻塞connfd.注释掉268,打开 271,同样上面测试,看结果,

客户端发送12345678

服务器

5. 边沿触发的阻塞connfd, 放开第 274,注释其他,先测试不带循环的的ET模式(即不循环读取数据,

跟水平触发读取一样),在epoll处理函数中注释176,打开179编译运行,开启一个客户端发送123456789,

服务器端,可以看到边沿触只触发了一次,只读取了5个字节

继续在刚才的客户端上发送一个字符a,告诉epoll_wait(),有新的可读事件发生了,

服务器端,服务器又触发了一次新的边沿触发,并继续读取上次没有读完的6789加一个回车符,

这时我们在客户端继续发送一个字符b,

服务器端,这个时候就会读取上次没读完的a加上上次的回车符,2个字节,还剩3个字节的缓冲区

就可以读到本次的b和本次的回车符共4个字节

此时我们就可看到,阻塞的边沿触发,如果不一次性读取一个事件上的数据,就会干扰下一个事件。

下面我们验证一次性读取数据,即带循环的阻塞connfd,ET模式,和上面的读取方式不一样了,一次

性读完注释 179, 放开 176,编译运行,客户端发送 123456789,

服务器端,可以看到一次全部把数据收到了,

但是仔细看,程序没有输出"====== 带循环的ET处理结束 ======", 是因为代码一直卡在

了第 98 行 recv函数上,因为是阻塞IO,如果没有数据可读,它会一直等在哪里,   直到有

数据可读,如果这个时候用另外一个客户端去连接 服务器不会受理这个新的客户请求连接.

6. 边沿触发的非阻塞connfd, 不带循环的ET,注释代码 176,274,打开179,277,

客户端先发送 1234567890,然后服务器端还是数据读取不完,第二次发一个a,通知epoll_wait()

新的可读事件发生了,这次会将上次剩下的67890,5个字节读出,第三次在发一个b, 这们看

到读出了5个字节,  是第一次的1234567890后的回车符+第二次的a和第二次的回车符+b+第

三次的回车符共5个字节

服务器端:

这里我们开始正规的边沿触发的非阻塞connfd, 带循环的ET,注释代码 179, 打开 176,277

编译运行,用一个客户端连接,发送1234567890,

服务器端,可以看到数据全部一次读取完毕,处理函数也退出了,因为非阻塞IO如果没有数据可

读时, 会立即返回,并设置error,我们这里是根据 EAGAIN和EWOULDBLOCK来判断数据全部

读取完毕了,可以退出循环了

这时我们用另一个客户端去连接,服务器依然可以正常接收请求;

5. 总结(~此时无声胜有声~)

1. 对于监听的sockfd,最好使用水平触发模式,边沿触发模式会导致高并发情况下,有的客户端会连接不上,

如果非要使用边沿触发,网上有的方案是用while来循环accept( ).

2. 对于读写的connfd,水平触发模式下,阻塞和非阻塞效果都一样,不过为了防止特殊情况,还是建议设置阻塞。

3.对于读写的connfd,边沿触发模式下,必须使用非阻塞IO,并要一次性全部读写完数据。

6. 鸣谢

重点鸣谢以下二位,十分感谢,本文的参考模板,很适合我~
感谢:https://blog.csdn.net/liu0808/article/details/52980413
感谢:https://blog.csdn.net/men_wen/article/details/53456491

后记:

生活往往是

也仅仅是

我们现在经历的这一刻


推荐阅读
author-avatar
php学者
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有