Linux — 高级IO

本文详细介绍了五种I/O模型,包括阻塞IO、非阻塞IO、信号驱动IO、异步IO及多路转接IO的特点与应用场景,对比了select、poll与epoll等多路转接模型的优势与不足。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

IO分两步

  1. 等待
  2. 数据拷贝

五种IO模型

阻塞IO

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

非阻塞IO

如果内核还未将数据准备好,系统调用会直接返回,并且返回EWOULDBLOCK错误码。非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询。 这对CPU来说是较大的浪费,一 般只有特定场景下才使用。
在这里插入图片描述

信号驱动IO

内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作。
在这里插入图片描述

异步IO

由内核在数据拷贝完成时,通知应用程序。

同步: 为了完成功能发起调用,若当前不具备完成条件,则自己等待完成功能后返回。
异步: 为了完成功能发起调用,但功能由别人来完成。
同步与异步的区别: 功能是否由别人来完成。

同步通常都是阻塞操作
异步阻塞操作: 等待别人完成操作。
异步非阻塞操作: 不等待别人完成操作 。
同步与异步的优缺点: 同步流程控制简单但是效率较低,异步流程控制较难,但是效率相对较高。
在这里插入图片描述

多路转接IO/多路复用IO

是一种IO事件的监控,同时对大量的描述符进行事件监控,监控描述符是否具备IO条件。虽然从流程图上看起来和阻塞IO类似,实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态。
在这里插入图片描述

IO多路转接模型

都是实现对大量描述符进行事件(描述符的可读/可写/异常)默认阻塞监控的操作,当大量的描述符中有就绪时返回。

就绪
对可读事件来说,缓冲区中有数据就是读就绪。
对于可写事件来说,缓冲区中有空闲空间就是写就绪。

作用
TCP服务端程序,同一时间只能与一个客户端进行通信一次。原因就是服务端不知道客户端什么 时候有数据到来,只能实现一个死流程,这个死流程就会导致程序阻塞。但是当用户知道那个描述符的数据什么时候到来,然后再去处理,流程变成了活动的流程(只针对那些就绪描述符进行处理)灵活性增强,程序将不再阻塞。

select模型

通过对几个事件集合中的描述进行各自的事件监控,当对应集合中有描述符事件就绪则返回(返回之前将集合中没有就绪的描述符全部移除)。

select模型中的一些操作

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
    nfds(maxfds)    //监控的描述符中,最大的那个描述符+1,否则会遍历所有描述符
    readfds    //读时间集合
    writefds    //写事件集合
    exceptfds    //异常事件集合
    timeout    //select等待超时事件
    fd_set    //描述符集合------是一个位图----位图大小取决于__FD_SETSIZE = 1024
void FD_CLR(int fd, fd_set* set);    //将指定的描述符从集合中移除
int FD_ISSET(int fd, fd_set* set);    //判断指定的描述符是否在集合中
void FD_SET(int fd, fd_set* set);    //将指定的描述符添加到监控集合中
void FD_ZERO(fd_set* set);    //清空描述符集合

流程

  1. 针对描述符不同的事件定义不同事件描述符集合fd_set(位图)(可读/可写/异常)。
  2. 将指定的描述符按照不同的所关心的事件添加到不同的集合中。
  3. 将集合拷贝到内核进行监控,监控的原理是对所有描述符的轮询遍历。
  4. 当有描述符就绪时,再调用返回之前,将集合中没有就绪的描述符剔除出去。
  5. 用户操作,对所有的描述符进行遍历,看哪一个描述符还在集合中,则这个描述符已经就绪。

select的优缺点分析

优点

  • 遵循posix标准,可以跨平台,移植性好。
  • select监控的超时事件更加精细(微秒)。

缺点

  • select所能监控的描述符是有上限的–默认1024,取决于__FD_SETSIZE。
  • select实现监控的原理是在内核中进行轮询遍历状态,因此性能会随着描述符增多而下降。
  • select返回值每次监控都会修改监控集合(剔除未就绪描述符),需要用户每次监控时重新添加描述符到集合中。
  • select要监控的集合中的描述符数据,需要每次都重新向内核中拷贝。
  • select不会告诉用户具体哪一个描述符就绪,需要用户遍历所有描述符进行查找,性能随着描述符增多而降低,而且增加了代码复杂度。

poll模型

监控实现原理
采用一个描述符事件结构的方式对描述符所关心的事件进行监控。

// pollfd结构
struct pollfd 
{    
	int fd; /* file descriptor */    //用户监控的文件描述符
	short events; /* requested events */    //保存用户关心的事件
	short revents; /* returned events */    //保存当前就绪的事件
};
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
    fds    //事件数组
    nfds    //监控的事件个数
    timeout    //超时等待事件

流程

  1. 用户定义事件数组,对描述符可以添加关心事件,进行监控。
    1. poll实现监控的原理也是将数据拷贝到内核,然后进行轮询遍历监控,性能随着描述符的增多而下降。
    2. 若有事件就绪,则修改这个响应描述符事件结构中的实际就绪事件。
  2. 用户根据返回的revents判断哪一个事件就绪,然后进行操作即可。
  3. poll也不会直接告诉用户哪一个描述符事件就绪,只是告诉了用户有就绪事件,需要用户对事件结构数组进行遍历遍历查询,查看实际发生的事件是否是自己关心的事件,来查找就绪,进而进行操作。

poll的优缺点分析

优点

  • 采用事件结构的方式对描述符进行监控,简化了多个事件集合的监控方式
  • 没有描述符的具体监控上限

缺点

  • 不能跨平台
  • poll采用轮询遍历方式判断就绪,性能随着描述符增多而性能下降
  • poll也不会告诉用户具体那个描述符事件就绪需要用户遍历所有描述符进行查找,性能随着描述符增多而降低,而且增加了代码复杂度

epoll模型

Linux下性能最高的IO多路转接模型

事件就绪

  • 可读事件就绪: 接收缓冲区中的数据大小大于低水位标记(18),就会触发可读事件。
  • 可写事件就绪: 发送缓冲区中的空闲空间大小大于低水位标记(18),就会触发可写事件。
int epoll_create(int size);    //创建一个epoll的句柄
//功能:在内核中创建一个结构体, struct eventpoll rbr--红黑树    rdlist--双向链表
	size:能监控描述符的上限,自从linux2.6.8内核之后,size参数是被忽略的,只要》0就可以
	返回值:文件描述符(非负整数)    epoll的操作句柄
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//向内核中epfd对应的epoll结构的eventpoll结构中添加/移除/修改所监控的事件结构
	epfd:epollcreat返回的epoll操作句柄
	op    用户要进行的操作
	    EPOLL_CTL_ADD :注册新的fd到epfd中;向内核的eventpoll中添加要监控的事件结构
	    EPOLL_CTL_MOD :修改已经注册的fd的监听事件;修改内核中所监控的事件结构
	    EPOLL_CTL_DEL :从epfd中删除一个fd;从内核的eventpoll中移除要监控的事件结构
	fd    用户所要监控的描述符
	event    描述符对应所要监控的事件

在这里插入图片描述

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
//功能:开始监控
	epfd:epoll操作句柄
	events:事件结构体数组,用于保存就绪的描述符对应事件
	maxevents:用于确定一次最多获取的就绪事件个数(防止events数组溢出)
	timeout:超时等待事件设置---毫秒
	返回值: <0 出错   ==0 超时等待  >0 就绪的事件个数


eventpoll
{
    rbr--红黑树--保存用户添加的事件结构节点
    rdlist--双向链表    描述符就绪时,回调函数自动将对应的事件结构地址添加到双向链表中
}

epoll_wait

  1. 告诉内核要开始对描述符进行监控。
  2. 操作系统对描述符进行监控(采用的是事件触发方式进行监控,为每一个要监控的描述符都定义了一个事件,并且对这个事件定义一个事件回调函数)。
  3. 这个事件回调函数做的事就是将就绪的这个描述符所对应的epoll_event事件结构添加到双向链表rdlist中。
  4. epoll_wait并没有立即返回,而是每隔一会就看一下,内核中的eventpoll中双向链表(保存的都是就绪的描述符对应的事件结构)是否为空,进而判断是否有描述符就绪。
  5. 若链表不为空,表示有描述符就绪,而epoll_wait即将返回,在返回之前,将就绪的描述符对应事件结构向用户态的结构体数组(epoll_wait第二个参数)拷贝一份。
  6. epoll会将就绪的描述符对应事件,拷贝一份到用户态,直接告诉用户有哪些描述符就绪。进而用户可以直接操作就绪的描述符。

epoll触发方式

水平触发
只要缓冲区中的数据大小/空闲空间大小大于低水位标记,就会触发可读/可写就绪事件。

边缘触发

  • 可读时间: 每次只有新数据到来时才会触发一次可读事件(不关注缓冲区数据有多少,要求用户最好一次将缓冲区中的数据全部读取)。
  • 可写事件: 每次只有缓冲区空闲空间从0变为大于低水位标记时才会触发可写事件。

通常读写事件混合监控的时候,对于可写事件就会使用边缘触发(防止可写事件每次在不写入数据但是有空闲空间都会触发事件)。
若是可读事件被设置为边缘触发,需要用户一次性将所有数据读取完毕,但是因为不知道数据有多少,因此只能循环从缓冲区中读取数据,当循环读取但是缓冲区中没有数据的时候,recv就会阻塞,因此边缘触发的可读事件的描述符通常需要被设置为非阻塞。

int fcnt(int fd, int cmd, .../*arg*/);
	fd    文件描述符
	cmd
    F_GETFL    获取描述符状态属性信息
    F_SETFL    设置描述符状态属性信息
	arg    用户设置/获取文件描述符的属性信息

void sernonblock(int fd)
{
    fcntl(fd, F_SETFL, fcntl(fd,F_GETFL,0)|O_NONBLOCK);
}

epoll的优缺点分析

优点

  • epoll没有监控的描述符上限。
  • 采用事件结构简化了select这种监控集合的监控流程。
  • epoll是异步阻塞操作,发起调用让操作系统进行描述符监控,操作系统使用的是事件回调方式对描述符进行监控,避免了select的轮询遍历,因此性能不会随着描述符增多而降低。
  • epoll发起调用之后进行等待–循环判断内核中epoll就绪的事件链表是否为空来确定是否有事件就绪,若有事件就绪,则将对应事件拷贝到用户态供用户操作。直接告诉用户那些描述符就绪了,可以直接对描述符进行操作;没有空遍历,提高了性能,并且简化了代码流程。
  • epoll描述符的事件结构只需向内核拷贝一次(内核eventpoll结构体的红黑树中),不需要每次拷贝。

缺点

  • 不能跨平台
  • 超时等待事件只能精确到毫秒

IO多路转接模型的适应场景
有大量的客户端链接,但是同时只有少量活跃的场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值