Linux高级IO

前言

内存和其他设备进行数据交互的过程叫做IO,IO可以理解成两个过程,等待和拷贝数据。其中等待的当作消耗的时间相对较长,提高IO效率的思路就是减少等待时间所占的比重

概念区分:
阻塞和非阻塞:
阻塞是指调用结果如果没有返回,当前线程会被挂起进行等待。
非阻塞指在不能立刻得到结果时候,该调用不会阻塞当前线程。
同步IO/异步IO:
同步IO指IO发起者参与等待或数据拷贝的过程,异步IO指等待和数据拷贝的任务由系统完成,IO发起者只等待最终完成结果的通知。

下文介绍高级IO中的非阻塞IO和多路复用

一、五种IO模型

  1. 阻塞IO

在内核将数据准备好之前,系统调用会一直等待。所有的套接字,默认IO方式都是阻塞式。
阻塞IO是最常见的IO模型
在这里插入图片描述

  1. 非阻塞IO

如果内核未将数据准备好, 系统调用直接返回, 并且返回EWOULDBLOCK错误码。
所以需要以循环的方式反复读,也叫轮询,比较浪费CPU资源
在这里插入图片描述

  1. 信号驱动IO
    内核将数据准备好的时候,使用SIGIO信号通知进程进行IO操作

在这里插入图片描述

  1. 多路复用
    在这里插入图片描述
    和阻塞IO的过程有点相似,但是可以同时等待多个文件描述符就绪。

  2. 异步IO
    数据拷贝由内核完成,内核在数据拷贝完成时, 通知进程。
    在这里插入图片描述

二、非阻塞IO

文件描述符默认是阻塞IO
设置文件描述符为非阻塞:

//自定义函数
void SetNoBlock(int fd) { 
 int fl = fcntl(fd, F_GETFL); 
 if (fl < 0) { 
 perror("fcntl");
 return; 
 }
 fcntl(fd, F_SETFL, fl | O_NONBLOCK); 
}

使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图).
然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数.

三、多路复用

select

多路复用的一种方式,调用select可以同时关心多个文件描述符,程序会等待直到关心的文件描述里有状态发生改变的。

函数原型:

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
 fd_set *exceptfds, struct timeval *timeout);

参数:
nfds为关心的文件描述符里的最大值+1
接下来的三个参数分别代表关心的读文件描述符集合、写文件描述符集合、异常文件描述符集合。都是输入输出型参数,用户通过这个集合告诉内核自己关心的文件描述符有哪些,内核也通过这个集合告诉用户哪些文件描述符就绪了。
timeout参数用来设置等待时间

timeout取值:
NULL:select将一直被阻塞,直到某个文件描述符上发生了事件;
0:仅检测描述符集合的状态,然后立即返回;
特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。

fd_set结构:
在这里插入图片描述
在这里插入图片描述
直观地看fd_set是一个整数数组,实际上是用位图的形式存储关心的文件描述符。

操作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代表已超过timeout时间
当有错误发生时则返回-1,错误原因存于errno

select特点:
可关心的文件描述符个数有上限,和fd_set的容量有关
要额外使用一个数组保存关心的文件描述符,数组的功能如下

  1. 方便select返回时候判断哪个文件描述符就绪了。
  2. 方便下一次调用select时候设置关心的文件描述符。
  3. 方便获取select第一个参数。

select的缺点:

  1. 每次调用select都要手动设置关心的文件描述符集合,使用不方便。
  2. 每次调用select都要把文件描述符集合拷贝到内核态,内核也要遍历文件描述符集合,文件描述符比较多时候开销较大。
  3. 可以关心的文件描述符数量有限。

poll

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是pollfd类型的数组,也是输入输出型参数
nfds表示数组长度
timeout表示poll函数的超时时间(毫秒)

pollfd结构:
fd代表文件描述符,events表示关心的事件集合,revents表示返回的就绪事件的集合

事件的取值:
在这里插入图片描述
在这里插入图片描述

函数返回值:
小于0, 表示出错
等于0, 表示poll函数等待超时
大于0, 表示就绪的文件描述符个数

poll的优缺点:

  1. 和select比起来,通过pollfd数组记录关心的文件描述符和对应的事件,不需要每次调用都重新设置,接口使用更加方便。
  2. 每次调用poll都要把pollfd数组拷贝到内核,poll返回后也需要遍历pollfd数组获取就绪的文件描述符
  3. 可以关心的文件描述符没有数量限制,但是同一时间连接的客户端可能只有少数就绪,文件描述符数量多的时候效率是不理想的。

epoll

epoll是为了处理大量文件描述符而改进的poll,是一种性能比较好的多路复用方式。epoll适用于有多个连接但是只有一部分连接活跃的场景。
epoll的三个系统调用:
1.创建epoll模型

int epoll_create(int size);    

返回值是一个文件描述符
2.注册关心的事件

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

参数:
epfd为刚才创建的epoll文件描述符
op为本次操作的类型,有增删改三种

EPOLL_CTL_ADD :注册新的fd到epfd中;
EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
EPOLL_CTL_DEL :从epfd中删除一个fd;

fd为事件对应的文件描述符
第四个参数是epoll_event类型的数组,代表关心fd上的哪些事件
epoll_event结构:
在这里插入图片描述
events可以是以下几个宏的集合:

EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
EPOLLOUT : 表示对应的文件描述符可以写;
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
EPOLLERR : 表示对应的文件描述符发生错误;
EPOLLHUP : 表示对应的文件描述符被挂断;
EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里

3.获取已经就绪的事件

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

第二个和第三个参数都是输出型参数,events为就绪的事件的数组,不可以为空,内核会把就绪事件拷贝到这里,maxevents代表数组有效元素个数。
timeout(毫秒,0会立即返回,-1是永久阻塞)
函数调用成功,返回就绪的文件描述符数目,如返回0表示超时, 返回小于0表示调用失败
epoll模型:
在这里插入图片描述
调用epoll_create时,Linux内核会创建一个eventpoll结构体

struct eventpoll{ 
 .... 
 /*红黑树的根节点*/ 
 struct rb_root rbr; 
 /*双链表就绪队列*/ 
 struct list_head rdlist; 
 .... 
};

调用epoll_ctl,就是修改红黑树的节点,设置回调方法
调用epoll_wait,就是从双链表就绪队列里读取就绪的事件

epoll的优点:

  1. 和select相比不需要每次调用都设置关心的文件描述符,可关心的文件描述符个数也没有上限
  2. 不需要每次都把文件描述符数组拷贝到内核,只在注册新事件的时候进行拷贝
  3. select和poll都要求操作系统去检测文件描述符上的某些事件是否就绪,会在没有就绪的文件描述符和事件上浪费一定时间,而epoll是在底层为关心的文件描述符和时间注册回调方法,哪一个就绪了就添加到就绪队列,效率较高。

epoll工作方式:

epoll的工作方式有水平触发(LT)和边缘触发(ET)

LT:
epoll默认是LT方式,当epoll检测到某个文件描述符上的事件就绪,我们可以不处理或者处理一部分,下次调用epoll_wait时候仍然会通知该事件就绪直到我们把事件处理完。支持阻塞和非阻塞读写。select和poll是LT工作方式。
ET:
epoll检测到某事件就绪,我们必须立刻处理完,因为下次调用epoll_wait不会再通知该事件就绪。所以epoll_wait返回的次数少了很多,性能更高,但编写程序的复杂度也相对提高了一些。ET模式下必须使用非阻塞读写,因为我们不知道底层数据有多少,是循环地一部分一部分地读取,最后一次读到的数据量肯定小于等于我们期望这一次读到的数据量,如果小于我们可以知道这次读完就结束了,如果等于我们不能知道已经读完,所以采用阻塞读取这里就会被挂起进行等待。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

江南无故人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值