> 文档中心 > 【Linux】一文看懂多路复用IO模型(select、poll、epoll)

【Linux】一文看懂多路复用IO模型(select、poll、epoll)

目录

一、前言

二、阻塞和非阻塞IO模型

1、阻塞IO模型

2、非阻塞IO模型

三、IO多路复用模型

1、select

2、poll

3、epoll

1)epoll_create

2)epoll_ctl 

3)epoll_wait

4、使用epoll(IO复用模型)来初步设计高并发服务器

1)服务器示例代码

2)客户端示例代码

3)客户端服务器连接测试结果


一、前言

常见的IO模型有如下五种

✅:阻塞I/O

✅:非阻塞I/O

✅:I/O复用

✅:信号驱动I/O

✅:异步I/O

本文重点介绍 epoll 多路复用IO模型,其他几种作为了解。

二、阻塞和非阻塞IO模型

1、阻塞IO模型

        下图为阻塞IO模型,当系统read不到数据,就会一直阻塞在 等待数据 这个阶段,直到 read到数据或者发生错误 ,才会继续往下走,即在系统调用read函数到read结束这个期间都是阻塞的,所以称之为阻塞IO模型。在阻塞期间CPU可以去做其他事。

2、非阻塞IO模型

        当我们把一个套接口设置为非阻塞方式时,即通知内核:当请求的I/O操作非得让进程睡眠不能完成时,不要让进程睡眠,而应 返回一个错误 。也就是系统会一直处在判断read是否成功(数据是否准备好)的循环中(轮询),直到read成功(数据准备好了)才会继续往下走。系统一直轮询,所以称之为非阻塞IO模型。

        应用程序会连续不断地查询内核,看看某操作是否准备好,这对cpu时间是极大的浪费,一般只在专门提供某种功能的系统中才会用到。

三、IO多路复用模型

  • ✍️IO多路复用模型出现缘由

✏️:因为阻塞模型在没有收到数据的时候就会阻塞卡住,如果一次需要接收多个acceptfd的时候(同时上线多个客户端),就会导致必须先处理完前面的fd,才能处理后面的fd,这样就会造成客户端的💦严重延迟💦

✏️:之前我们为了处理多个请求,开了多进程来处理不同的客户端,但是进程开销非常大,而且数据还不共享,于是我们将其改成多线程,但是这样又会启动大量的线程,且一个进程开的线程也是有限的,还会造成资源的浪费。所以这个时候就出现了IO多路复用技术,就是用 一个进程来处理多个fd(客户端) 的请求。

  • ✍️Linux下多路复用的方案有select、poll、epoll

✏️:多路复用IO模型实际上是将上线的fd(客户端)统一放在一个事件队列里,当某个fd(客户端)活跃时,将该客户端移放至就绪队列中,并唤醒cpu去处理该客户端,处理完之后便将该客户端放回事件队列中。

✏️:select、poll、epoll对事件队列的处理方式不一致,3者之间也存在区别

1、select

  • 📝 内部结构

✏️:select结构是fd_set结构,一般是一个整数数组,数组中的每一位对应一个描述符fd

  • 📝时间复杂度

✏️: select仅仅只是知道有哪几个fd(客户端)活跃了,不知道具体是哪个,所以需要无差别的轮询事件队列中的每个fd(客户端),找出活跃的fd(客户端),所以select的轮询时间复杂度为O(n)。

  • 📝缺点

1️⃣:每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

2️⃣:内核需要遍历队列中存放的每个fd,这个开销在fd很多时也会很大

3️⃣:最大连接数有限制,默认是1024

 2、poll

  • 📝 内部结构

✏️:poll使用pollfd结构,poll是链式的,没有最大连接数的限制

  •  📝时间复杂度

✏️:poll的实现和select非常相似,也是无差别轮询事件队列中的所有fd(客户端),所以poll的轮询时间复杂度也为O(n)。

  •  📝缺点

1️⃣:每次调用poll,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

2️⃣:内核需要遍历队列中存放的每个fd,这个开销在fd很多时也会很大

3、epoll

  •  📝 内部结构

✏️:1个红黑树和1个链表,如下图所示

  •  📝时间复杂度

✏️:epoll不是轮询的方式,不会随着fd数目的增加效率下降。只有活跃可用的fd才会调用callback函数,并通知cpu去处理,所以epoll的时间复杂度为O(1)。

  • 📝优点

        epoll解决了select和poll存在的3个缺点。

1️⃣:每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次

2️⃣:不是轮询的方式,不会随着fd数目的增加效率下降。只有活跃可用的fd才会调用callback函数。

3️⃣:虽然连接数有上限,但是很大,能打开的fd的上限远大于1024(1G的内存上能监听约10万个端口)。

既然epoll优点这么多,所以我们来学习一下他的相关函数。

1)epoll_create

  • 函数功能为创建一个epoll句柄

头文件👇

#include

函数原型👇

int epoll_create(int size);

参数👇

✅size:Since Linux 2.6.8, the  size argument is ignored, but must be  greater  than  zero;

返回值👇

✔️成功:return a nonnegative file descriptor(返回一个非负的文件描述符

失败:返回 -1

2)epoll_ctl 

  • 函数功能为注册要监听的事件类型(将fd加入等待队列中)

头文件👇

#include

函数原型👇

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数👇

✅epfd:epoll句柄(即epoll_create返回的句柄)。

✅op:表示fd操作类型,有:添加(EPOLL_CTL_ADD)、修改EPOLL_CTL_MOD)、删除(EPOLL_CTL_DEL)。

✅fd:要监听的描述符。

✅event:要监听的事件。

返回值👇

✔️成功:returns 0

失败:returns -1

  • epoll_event结构体的定义如下

struct epoll_event

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

typedef union epoll_data

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

3)epoll_wait

  • 函数功能为等待事件的就绪

头文件👇

#include

函数原型👇

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

参数👇

✅epfd:epoll句柄(即epoll_create返回的句柄)。

✅events:从内核得到的就绪事件集合。

✅maxevents:events的大小。

✅timeout:等待的超时事件。

返回值👇

✔️成功:返回 就绪的事件数目 

失败:returns -1

4、使用epoll(IO复用模型)来初步设计高并发服务器

1)服务器示例代码

  • 服务器示例代码如下
#include #include   #include #include #include #include #include #include #include using namespace std;int main(){int socketfd = 0;int acceptfd = 0;struct sockaddr_in s_addr;int len = 0;char buf[255] = { 0 };//存放客户端发过来的信息int opt_val = 1;//初始化网络  参数一:使用ipv4  参数二:流式传输socketfd = socket(AF_INET, SOCK_STREAM, 0);if (socketfd == -1){perror("socket error");}else{//原本使用struct sockaddr,通常使用sockaddr_in更为方便,两个数据类型是等效的,可以相互转换struct sockaddr_in s_addr;//确定使用哪个协议族  ipv4s_addr.sin_family = AF_INET;//系统自动获取本机ip地址 也可以是本地回环地址:127.0.0.1s_addr.sin_addr.s_addr = INADDR_ANY;//端口一个计算机有65535个  10000以下是操作系统自己使用的,  自己定义的端口号为10000以后s_addr.sin_port = htons(12345);  //自定义端口号为12345len = sizeof(s_addr);//端口复用 解决 address already user 问题setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR, (const void*)opt_val, sizeof(opt_val));//绑定ip地址和端口号int res = bind(socketfd, (struct sockaddr*)&s_addr, len);if (res == -1){perror("bind error");}else{//监听这个地址和端口有没有客户端来连接  第二个参数现在没有用  只要大于0就行if (listen(socketfd, 2) == -1){perror("listen error");}}int epollfd = 0;int epollwaitefd = 0;struct epoll_event epollEvent;struct epoll_event epollEventArray[5];//事件结构体初始化bzero(&epollEvent, sizeof(epollEvent));//绑定当前准备好的sockedfd(可用网络对象)epollEvent.data.fd = socketfd;//绑定事件为客户端接入事件epollEvent.events = EPOLLIN;//创建epollepollfd = epoll_create(5);//将已经准备好的网络描述符添加到epoll事件队列中epoll_ctl(epollfd, EPOLL_CTL_ADD, socketfd, &epollEvent);while (1){cout << "epoll wait client…………" << endl;epollwaitefd = epoll_wait(epollfd, epollEventArray, 5, -1);if (epollwaitefd < 0){perror("epoll wait error");}for (int i = 0; i < epollwaitefd; i++){//判断是否有客户端上线if (epollEventArray[i].data.fd == socketfd){cout << "网络开始工作,等待客户端上线" << endl;acceptfd = accept(socketfd, NULL, NULL);cout << "acceptfd = " << acceptfd < 0){cout << "服务器收到 fd = " << epollEventArray[i].data.fd << " 发来的数据:buf = " << buf << endl;}else if (res <= 0){cout << "客户端掉线……" << endl;close(epollEventArray[i].data.fd);//从epoll中删除客户端描述符epollEvent.data.fd = epollEvent.data.fd;epollEvent.events = EPOLLIN;epoll_ctl(epollfd, EPOLL_CTL_DEL, epollEventArray[i].data.fd, &epollEvent);}}}}}}

2)客户端示例代码

  • 客户端实现的功能为循环通过控制台输入向服务器发送消息
#include #include    #include #include #include #include #include #include using namespace std;int main(){int socketfd = 0;int acceptfd = 0;int len = 0;int res = 0;char buf[255] = { 0 };//初始化网络socketfd = socket(AF_INET, SOCK_STREAM, 0);if (socketfd == -1){perror("socket error");}else{struct sockaddr_in s_addr;//确定使用哪个协议族  ipv4s_addr.sin_family = AF_INET;//填入服务器的ip地址  也可以是  127.0.0.1 (回环地址)s_addr.sin_addr.s_addr = inet_addr("127.0.0.1");//端口一个计算机有65535个  10000以下是操作系统自己使用的,  自己定义的端口号为10000以后s_addr.sin_port = htons(12345);  //自定义端口号为12345len = sizeof(s_addr);//绑定ip地址和端口号int res = connect(socketfd, (struct sockaddr*)&s_addr, len);if (res == -1){perror("connect error");}else{while (1){cout << "请输入:" <> buf;write(socketfd, buf, sizeof(buf));bzero(buf, sizeof(buf));}}}return 0;}

3)客户端服务器连接测试结果

        在linux下找到工程文件夹下的——>bin——>x64——>Debug目录下的xxx.out可执行文件,在终端输入 ./xxx.out 的方式来运行可执行文件。

  • 同时上线两个客户端,并向服务器发送消息

💦可以发现,在没有开进程或者线程的情况下,同时上线两个客户端,并向服务器发送消息,服务器是可以区别是谁发来了什么消息的。大大节省资源的开销(不用开进程或线程)

😘The end ……🔚

原创不易,转载请标明出处。

对您有帮助的话可以一键三连,会持续更新的(嘻嘻)。