🌈前言🌈
本期【Linux杂货铺】,主要讲解什么是IO多路复用,通过了解IO模式,了解为什么要有多路复用,以及其他的IO模型,学习多路复用的3中模型。
📁 五种IO模型
📂 阻塞IO
在内核将数据准备好之前,系统调用会一直等待。默认是阻塞方式。
** 📂 非阻塞IO**
如果内核还没有将数据准备好,系统调用会直接返回,并且返回**EWOULDBLOCK错误码**。此时,就需要特殊判错误码,究竟是异常错误,还是非阻塞IO。
非阻塞IO常常需要循环式的读写这个文件描述符,这个过程叫做轮询,对CPU是一个巨大的浪费,只有在特定场景下才会使用。
** 📂 信号驱动IO**
内核将数据准备好,使用SIGIO信号通知应用程序进行IO操作。
** **📂 多路复用
同时等待多个文件描述符的就绪状态,如果有一个或多个文件描述符事件触发,就会返回。
** **📂 异步IO
由内核在数据拷贝完成时,通知应用程序,上层只需要完成数据处理即可。但是异步IO往往伴随着代码逻辑复杂,难以理解,编程困难等问题。
在IO过程中,需要经过两个步骤,1. 等待;2. 拷贝。等待的时间往往高于拷贝时间,因此提高IO效率,最核心的方法就是缩小等待时间。
此外,**同步和异步关注的消息通知机制**。
同步,就是发出一个调用时,在没有得到结果之前,该调用就不返回。一旦调用返回,就得到返回值,即由调用者主动等待这个调用结果;
异步,调用发出之后,这个调用会直接返回,没有返回结果,即一个异步调用发出后,调用者不会立刻得到结果,而是调用发出后,被调用者通过,信号等通知调用者,通过回调函数处理这个调用。
**阻塞和非阻塞关注的是程序等待调用结果时状态。**
阻塞调用指的是调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果才会返回。
非阻塞调用指的是不能立刻得到结果之前,该调用不会阻塞当前线程。
📁 非阻塞IO实现
文件描述符默认都是阻塞的。使用fcntl系统调用可以修改文件描述为非阻塞。
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd ,int cmd , ...);
fcntl函数有5种功能:
1. 复制现有的描述符(cmd = F_DUPFD) 2. 获得/设置文件描述符标记(cmd = F_GETFD 或 F_SETFD) 3. 获得/设置文件状态标记(cmd = F_GETFL 或 F_SETFL) 4. 获得/设置异步IO所有权(cmd = F_GETOWN或F_SETOWN) 5. 获得/设置锁(cmd = F_GETLK,F_SETLK或F_SETLKW)
void SetNoBlock(int fd)
{
int fl = fcntl(fd , F_GETFL);
if(fl < 0)
{
prror("fcntl");
return ;
}
fcntl(fd , F_SETFL , fl | O_NONBLOCK);
}
📁 select
** 📂 接口使用**
include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set
*exceptfds, struct timeval *timeout);
• 参数 nfds 是需要监视的最大的文件描述符值+1;
• rdset,wrset,exset 分别对应于需要检测的可读文件描述符的集合, 可写文件描述符的集合及异常文件描述符的集合;
• 参数 timeout 为结构体 timeval, 用来设置 select()的等待时间。timeout为NULL时,表示select会一直被阻塞,直到事件发生;timeout为0时,仅检测描述符集合的状态,然后立刻返回,并不会等待外部事件的发生;timeout为特定时间值,如果指定时间段内没有事件发生,select会超时返回。
fd_set结构表示为1个位图,一个bit位表示要监视文件描述符。select系统调用的三个fd_set参数是输入输出型参数,输入表示要监听哪些描述符,输出表示哪些描述符事件就绪。
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组 set 中相关fd的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组 set 中相关fd的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组 set 中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组 set 的全部位
select系统调用返回值:执行成功返回文件描述符状态已改变的个数;如果返回值为0表示超时;如果返回-1,错误原因存在于errno。
select系统调用常常搭配一个数组来使用。将需要监听的描述符添加至数组,通过遍历数组来给fd_set赋值。
** 📂 缺点**
1. select函数,支持的文件描述符数量太小;
2. select函数,每次都需要手动设置fd集合,使用不方便;
3. 每次调用select都需要在内核遍历传递进来所有的fd,这个开销在fd很多时也很大。
4. 每次调用select,都需要吧fd集合从用户态拷贝到内核态,这个开销在fd很多时很大。
📁 poll
** 📂 接口使用**
include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// pollfd 结构
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
• fds 是一个 poll 函数监听的结构列表. 每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返回的事件集合.
• nfds 表示 fds 数组的长度.
• timeout 表示 poll 函数的超时时间, 单位是毫秒(ms)
如果底层事件就绪,revents会被赋值。
events和revents的取值:
poll函数返回值小于0,表示出错;返回值等于0,表示poll等待超时;返回值小于0,表示由于poll由于文件描述符就绪而返回。
** 📂 优缺点**
1. poll没有描述符的数量限制;接口比select使用更方便,pollfd结构体包含了要监视的event和发生的revent,不在使用select输入输出型参数传递方式。
2. 同select一样,poll返回后,需要遍历pollfd数组来获取就绪的描述符。
3. poll需要把大量的pollfd结构体从用户态拷贝至内核态。
📁 epoll
** 📂 接口使用**
1. 创建epoll文件描述符(句柄)
int epoll_create(int size);
2.操作epoll模型
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
3. 等待时间
int epoll_wait(int epfd , struct epoll_event* events,int maxevents , int timeout);
op参数表示动作,用3个宏来表示:
• EPOLL_CTL_ADD: 注册新的 fd 到 epfd 中;
• EPOLL_CTL_MOD: 修改已经注册的 fd 的监听事件;
• EPOLL_CTL_DEL: 从 epfd 中删除一个 fd;
uint32_t events可以是一下几个宏的集合:
• EPOLLIN : 表示对应的文件描述符可以读 (包括对端 SOCKET 正常关闭);
• EPOLLOUT : 表示对应的文件描述符可以写;
• EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
• EPOLLERR : 表示对应的文件描述符发生错误;
• EPOLLHUP : 表示对应的文件描述符被挂断;
• EPOLLET : 将 EPOLL 设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
• EPOLLONESHOT: 只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个 socket 的话, 需要再次把这个 socket 加入到 EPOLL 队列里.
epoll_wait系统调用会将底层已经就绪的事件拷贝至数组中,系统调用返回值就是已经就绪的事件数量n,只需要遍历数组前n个元素即可。
** 📂 底层原理**
** epoll模型中,节点采用了与类型无关的数据结构,即一个节点可以被连入到不同的数据结构中,既可以在红黑树中,也可以在就绪队列中。**
** 当节点既在红黑树中中,又在就绪队列中,就表示该节点监视的文件描述中有事件触发。**
** 📂 工作模式 ET vs LT**
水平触发(LT):epoll**默认状态下就是LT模式**。当epoll检测到事件就绪,如果没有处理,或者处理不完全,**再次调用epoll_wait时会立即返回并通知事件就绪**,直到事件被处理完成,如缓冲区上所有数据被处理完成,epoll_wait才不会立刻返回。**支持阻塞读写或非阻塞读写。**
边缘触发(ET):epoll检测到有事件就绪,必须立刻处理。如果没有处理,或者处理不完全,**下次调用epoll_wait不会再通知**,而是等到有新的数据到来,事件再次就绪,再次通知上层。即事件就绪后,只有一次处理机会。ET模式比LT模式性能更高,Nginx默认采用ET模式。**ET模式只支持非阻塞读写。**
ET模式的epoll,需要将文件描述符设置为非阻塞,这不是接口上的要求,而是“工程实践”上的要求。
如果面对下面这种情况,ET模式采用阻塞读取是会出现问题的:
• 服务器只读到 1k 个数据, 要 10k 读完才会给客户端返回响应数据.
• 客户端要读到服务器的响应, 才会发送下一个请求
• 客户端发送了下一个请求, epoll_wait 才会返回, 才能去读缓冲区中剩余的数据
因此,为了解决上述问题,需要将ET模式改为非阻塞轮询式读取底层数据,一次将数据全部读取完毕。
** 📂 优点**
1. 接口使用方便。
2. 数据拷贝轻量。
3. 事件回调机制,避免了使用遍历,而是使用回调函数的方式,将就绪的文件描述符结构加入就绪队列中。
4. 没有数量限制。
📁 总结
以上,就是本期【Linux杂货铺】的主要内容了,主要讲解了五种IO模型,重点讲解了select,epoll接口,学习了epoll底层原理,以及epoll的工作模式,了解了poll接口,其中epoll是尤为重要的多路复用接口。
如果感觉本期内容对你有帮助,欢迎点赞,关注,收藏Thanks♪(・ω・)ノ
版权归原作者 秋刀鱼的滋味@ 所有, 如有侵权,请联系我们删除。