【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 ……🔚
原创不易,转载请标明出处。
对您有帮助的话可以一键三连,会持续更新的(嘻嘻)。