- IO:输入输出-- 过程:等待IO就绪,进行数据拷贝
1. 四种典型IO方式
- 典型IO:阻塞IO,非阻塞IO,信号驱动IO, 异步IO
- 阻塞IO:发起IO调用,若IO 未就绪(IO条件不具备),则一直等待
- 非阻塞IO:发起IO调用,若IO未就绪,则立即报错返回
- 信号驱动IO:自定义IO信号处理,等待IO就绪收到信号打断当前操作进行IO
- 异步IO:自定义IO信号处理,发起IO调用,调用立即返回,但让系统完成IO,完成后通过信号通知进程
- 从阻塞IO异步IO是资源利用率以及效率提高的过程,也是流程复杂的过程。
- 阻塞:为了完成某功能,发起一个调用,若完成功能条件不具备,则一直等待;
- 非阻塞:发起一个调用,若完成功能条件不具备,则立即报错返回
- 阻塞与非阻塞:通常用于描述某个接口发起调用后是否能够立即返回
- 同步:一个功能完成后,才能进行下一个,若不能立即完成则一直等待;流程清晰简单,但是效率相较于异步较低。
- 异步:发起一个调用,让别人完成具体功能,不用等待功能完成后才能继续推进。对资源利用率高,效率更高,但是流程较为复杂。
- 同步与异步:通常用于描述功能的完成流程(外部体现就是功能是否是自己完成的)
- 异步阻塞与非阻塞:区别在于进程是否等待系统完成任务
· 异步阻塞:发起一个调用,让系统完成任务,任务不完成进程也一直等着 · 异步非阻塞:发起一个调用,让系统完成任务,进程继续做自己的事情
2. IO多路转接
- 也叫IO多路复用
- 作用:针对大量描述符进行IO就绪事件监控,让进程仅仅针对已经就绪了IO事件的描述符进行IO操作,避免了进程对未就绪的描述符进行操作所带来的性能损失或者阻塞。
- 实现:select、poll、epoll
- IO就绪事件:可读、可写、异常
2.1 select模型
操作流程
- 定义指定IO事件的描述符集合,将需要监控指定事件的描述符添加到对应集合中
- 发起监控调用,将需要监控的事件描述符集合拷贝到内核,进行事件监控。若监控超时了都没有描述符就绪则返回;若有描述符就绪了指定监控的事件则返回。在监控调用返回前,都会将事件描述符集合中没有就绪事件的描述符移除掉,也就是说,调用返回后,集合中保留的只有就绪的描述符。
- 判断哪个描述符在哪个集合中,就知道哪个描述符就绪了什么事件,进而进行对应IO操作
接口认识:
-
定义集合:fd_set rfds, wfds,efds; -
清空集合:void FD_ZERO(fd_set *set) ; -
将描述符添加到集合中:void FD_SET(int fd, fd_set *set); -
发起监控调用:int select(int nfds, fd_set * readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); · nfds:所有集合中最大的描述符的值+1; · readfds/writefds/exceptfds:可读,可写,异常,不监控置空 · timeout:监控超时等待时间 struct timeval{tv_usec, tv_sec}; 一直等待则置空,非阻塞则数据置0 · 返回值:返回实际就绪的描述符个数;出错返回-1;超时返回0; -
调用返回后,判断哪个描述符还在集合中 ,确定哪个描述符就绪了什么事件 · int FD_ISSET(int fd, fd_set *set); -
从指定集合中移除指定的描述符 · void FD_CLR(int fd, fd_set *set); 多路转接模型是针对一个或多个描述符进行IO就绪事件监控的功能
通常应用于tcp服务器端,针对大量套接字描述符进行监控,让程序能够仅仅针对就绪的描述符进行操作,进而提高处理效率
而udp服务端大多针对单个套接字进行操作
大多数情况也会用到多路转接模型,因为多路转接模型不但可以进行IO就绪事件监控,还可以进行超时控制
select特性总结
- 优点:跨平台移植性较好
- 缺点:
- select所能监控的描述符有数量上限,上限取决于宏_FD_SETSIZE
- select每次进行监控都要重新向集合中添加描述符(每次都会修改),且每次都要重新将集合拷贝到内核
- select监控原理实在内核中进行轮训遍历,性能随着描述符的增多而下降。
· 将集合中描述符遍历一遍看看有没有就绪的; · 有就直接移除未就绪返回,没有则挂起等待; · 有描述符就绪/超时后被唤醒,重新遍历一遍移除未就绪后返回。 - select返回的是就绪集合,需要用户判断哪个描述符还在哪个集合中,才能确定哪个描述符就绪了哪个事件。
2.2 poll
- 接口认识
· int pll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd
{
int fd;
short events;
short revents;
};
· fds:描述符事件结构数组 · nfds:数组中有效节点个数; · timeout:超时时间; · 返回值:等于0表示超时;小于0表示出错;有就绪则大于0.
操作流程
- 定义事件结构体数组,为每个需要监控的描述符定义事件结构
- 发起监控调用,将数组中有效节点拷贝到内核进行监控,超时/就绪则调用返回,返回前将描述符实际就绪的事件记录到对应节点的revents成员中
- 调用返回后,遍历事件数组,通过每个节点的revents成员确定对应节点描述是否就绪了某个事件
优缺点:
- 优点
· poll能够监控的描述符数量没有上限限制; · 代码操作流程相较于select较为简单。 -缺点 · 跨平台移植性较差; · 监控原理依然是遍历轮询,性能会随着描述符增多而下降; · 监控返回后依然需要遍历事件结构数组确定描述符是否就绪。
2.3 epoll
接口认识
int epoll_create(int size); – 在内核中创建epoll句柄; · size:监控的数量上限,Linux2.6之后被忽略,大于0即可 · 返回值:成功返回描述符,失败返回-1int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev); · epfd:epoll_create返回的epoll描述符; · op:EPOLL_CTL_ADD / EPOLL_CTL_DEL / EPOLL_CTL_MOD · fd:针对op类型所操作的监控描述符 · ev:针对fd描述符所定义的事件结构体
struct epoll_event
{
uint32_t events;
epoll_data_t{void *ptr, int fd;}data;
};
· 返回值:成功返回0;失败返回-1
int epoll_wait(int epfd, struct epoll_event *evs, int maxe, int to); · epfd:epoll描述符; · evs:epoll_event数组首地址,用于保存就绪的描述符对应事件结构 · maxe:通常evs数组的节点个数-- 指定要获取的事件最大个数 · to:超时时间-- 毫秒 · 返回值:超时返回0;出错返回-1;有就绪返回就绪事件个数
操作流程
- 在内核中创建epoll句柄结构
- 向内核epoll句柄结构中添加要监控的描述符以及对应事件结构
- 传入一个事件结构数组,开始监控,监控是一个异步阻塞操作。
· 告诉系统开始监控,而描述符的监控由系统完成 · 系统为每个描述符的就绪事件做了一个事件回调函数,一旦某个描述符就绪了指定的事件,则会调用事件回调函数,将这个描述符对应的事件结构添加到就绪事件双向链表 · epoll_wait接口每隔一段事件查看epoll句柄结构的rdllist- 就绪双向链表是否为空,就可以判断有没有描述符就绪,超时则直接返回,如果由就绪,则将就绪的事件结构信息拷贝到传入的数组中。 - 监控调用返回后,只需要遍历evs数组,逐个对节点中的描述符进行对应事件的处理即可
epoll的事件触发方式:
- IO事件的就绪:
· 可写:描述符的发送缓冲区中剩余空间大小大于低水位标记 · 可读:描述符的接收缓冲区中数据大小大于低水位标记 · 低水位标记:类似于一个基准值-- 默认1字节 - IO就绪事件的触发方式:水平触发- 默认,边缘触发- epoll特有
· 水平触发: 可读:只要缓冲区中有数据就会触发可读事件 可写:只要缓冲区中有剩余空间就会触发可写事件 · 边缘触发:EPOLLET 可读:只有新数据到来的时候才会触发可读事件 可写:只有缓冲区从没有剩余空间变为有剩余才会触发可写事件 epoll优缺点 - 缺点:跨平台移植性较差
- 优点
· 所能监控的描述符没有数量上限 · 描述符以及事件结构只需要向内核拷贝一次 · 监控原理采用异步阻塞没监控有系统完成,进程只需要判断就绪链表是否为NULL即可,性能不会随着描述符增多而下降 · 直接返回的都是就绪的描述符对应事件结构,减少空遍历
2.4 多路转接模型
- 适用于有大量描述符需要监控,但是同一时间只有少量活跃的场景
- poll/select适用于单个描述符的超时控制
- 单个描述符的临时控制这里不适用epoll
- 在实际使用中,多路转接模型通常搭配线程池一起使用
· 对大量描述符进行监控,就绪事件后则抛入线程池进行处理
|