Advanced IO

本文介绍了高级IO的相关知识,包括非阻塞I/O、异步I/O和I/O多路复用。详细阐述了Linux支持的I/O多路复用系统调用select、poll、epoll之间的区别,以及它们的函数使用和参数说明。重点分析了epoll高效的原因、原理、流程和实现细节。

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

Advanced IO

Nonblocking I/O

系统调用分为低速系统调用 (slow system calls) 和其他,低速系统调用会永远阻塞。它们包含:

  • 如果某些文件类型 (pipes, terminal devices 和 network devices) 的数据不存在,read操作可能一直阻塞调用者。
  • 对于相同的文件类型 (e.g., 管道或网络流控制中没有空间),如果数据没有被立即接受,write操作则可能会永远阻塞调用者。
  • 对于某些类型的文件,opens阻塞直到某些情况发生。(例如打开一个终端设备,它会一直等待直到调制解调器应答,或者当没有其他进程为读打开FIFO时,为写打开一个FIFO)

Asynchronous I/O

进程告诉内核:当描述符准备好进行I/O时,使用一个信号通知它。

I/O Multiplexing

创建一个文件描述符集合,当集合中的某个文件描述符准备好进行I/O操作时,不再阻塞,转而进行I/O操作。Linux支持I/O多路复用的系统调用 select, poll, epoll 这些调用都是内核级别的。但 select, poll, epoll 本质上都是同步I/O,先是block住以等待就绪的socket,再block住,将数据从内核拷贝到用户的内存空间。
I/O Multiplexing 模型

select, poll, epoll之间的区别
selectpollepoll
操作方式遍历遍历回调
底层实现数组链表哈希表
IO效率每次调用都进行线性遍历,时间复杂度为O(n)每次调都进行线性遍历,时间复杂度为O(n)事件通知方式,每当fd就绪,系统注册回调函数就会被调用,将就绪的fd放到rdlist里面,时间复杂度为O(1)
fd拷贝每次调用select,都需要把fd集合从用户态拷贝到内核态每次调用poll,都需要把fd集合从用户态拷贝到内核态调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝
函数select和pselect

select中使用的数据结构fd_set的实现为数组,数组下标表示文件描述符,其值被设为1说明已设置,0表示未设置。向 readfdswritefdsexceptfds 设置文件描述符 (FD_SET) 所作的工作是将指定的fd_set中指定的位设置为1,反之,清除某文件描述符 (FD_CLR) 是将fd_set指定位的值设为0。当执行select时,内核会通过遍历检测 fd_set 中关注的文件描述符是否准备就绪,在已有文件描述符准备好或者超时时函数返回。我们可以使用 FD_ISSET 检测一个文件描述符是否准备就绪。
关于内存数据,fd_set由用户创建,存在于用户内存空间,调用select后,被转移至内核内存空间,当有文件描述符准备好时,内核会修改 fd_set 中对应的内容。然后将 fd_set 转移回内核,并从select返回,之后可以使用 FD_ISSET 进行测试。

#include <sys/select.h>

int select(int maxfdpl, fd_set* restrict readfds, 
		   fd_set* restrict writefds, fd_set* restrict exceptfds,
		   struct timeval* restrict tvptr);
// 返回值:准备好的文件描述符的个数;若超时,返回0;若出错,返回-1;

FD_ZERO(int fd, fd_set* fds)   //清空集合
FD_SET(int fd, fd_set* fds)    //将给定的描述符加入集合
FD_ISSET(int fd, fd_set* fds)  //将给定的描述符从文件中删除  
FD_CLR(int fd, fd_set* fds)    //判断指定描述符是否在集合中
参数说明

tvptr:

  • NULL: 永远等待,当所指定的描述符中的一个或以上准备好,或者捕捉到一个信号则返回。如果捕捉到一个信号,则 select 返回-1, errno 设置为EINTR
  • tvptr->tv_sec == 0 && tvptr->usec == 0:根本不等待。测试所有指定的描述符并立即返回。
  • tvptr->tv_sec != 0 && tvptr->usec != 0:等待指定的秒数和微秒数,当指定的描述符之一已准备好或当指定的时间值已经超过时立即返回。

readfds, writefds, exceptfds:
中间3个参数readfds, writefds, exceptfds是指向描述符的指针。这3个描述符集说明了我们关心的可读,可写或处于异常条件的描述符集合,如果不需要测试,可以设为空。
第一个参数max_fd指待测试的fd的个数,文件描述符从0开始开max_fd-1都将被测试。

函数poll

poll的实现和使用本质上和select并无区别,管理多个描述符也是进行轮询。poll取消了文件描述符同时监听的最大数量的限制。使用pollfd数组作为文件描述符集合描述,其中 fd 表示关注的文件描述符,events 表示与对该描述符关注的事件,revents表示在该描述符上实际发生的事件。

#include <poll.h>

int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);
// 返回值:准备就绪的描述符数目;若超时,返回0;若出错,返回-1

与select不同,poll不是为每个条件(可读性,可写性和异常条件)构造一个描述符集,而是构造一个pollfd结构的数组,每个数组元素指定一个描述符编号和我们对其感兴趣的条件。

struct pollfd {
	int fd;			// file descriptor to check, or < 0 to ignore
	short events;	// event of interest on fd
	short revents;	// events that occured on fd
};

返回值:成功返回已准备就绪的文件描述符数量,超时返回0,出错返回-1;
参数:

  • fdarray[]: 关注的文件描述符的数组,fd, event, revent。内核不修改event,而是设置revent表示当前文件描述符的状态,这与select修改 fd_set 的操作不同。
  • nfds表示数组大小。
  • timeout表示超时时间。

IO Multiplexing 实现单一线程监听多个文件描述符,当有文件描述符准备就绪,就从函数返回。

epoll详解

epoll 是基于事件驱动的IO方式,相对于selectpoll 来说,epoll没有描述符个数的限制,使一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放待内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。优点如下:

  • 没有最大并发连接的限制,能打开的fd上限远大于1024
  • 采用回调的方式,效率提升。只有活跃可用的fd才会调用callback函数,与连接的总数无关。
  • 内存拷贝。使用mmap()文件映射内存来加速与内核空间的消息传递,减少复制开销。
epoll为什么高效
  1. 通过硬件传输,网卡接收的数据存放到内存中,操作系统可以去读取它们。
  2. 当网卡把数据写入内存后,网卡向cpu发出一个中断的信号,操作系统便能得知有新数据的到来,再通过网卡中断程序去处理数据。
  3. 中断程序主要有两项功能,先将网络数据写入到对应socket的接收缓冲区里面,再唤醒进程A,重新将进程A放入工作队列中。
    在这里插入图片描述

select的流程
select在所有进程A需要监听的套接字的等待队列中都加入进程A,这里需要进行一次套接字的遍历,以确定哪些套接字的等待队列需要加入进程A (依据 fd_set)。当某一被监听的套接字准备就绪时,需要将进程A从所有的套接字进程等待队列中移除,这里又需要遍历一次。当 select 返回时,需要再遍历一次 fd_set 使用 FD_ISSET 来确定哪些套接字准备就绪。

为了避免 select 的多次遍历,提出了事件驱动的 I/O 复用接口: epoll

epoll 通过以下一些措施来改进效率。

措施一:功能分离
select低效的原因之一是将维护等待队列阻塞进程两个步骤合二为一。

int select(int maxfdpl, fd_set* restrict readfds, 
		   fd_set* restrict writefds, fd_set* restrict exceptfds, 
		   struct timeval* restrict); 

可以看出,等待队列由 fd_set 结构确定,调用 select 则会阻塞进程。

epoll维护等待队列阻塞进程 两个步骤分开。使用 epoll_ctl 维护等待队列,使用 epoll_wait 阻塞进程。

int s = socket(AF_INET, SOCKET_STREAM, 0); /* 协议族, 套接字类型,使用的协议 */
bind(s, ...)
listen(s, ...)

int epfd = epoll_create(...);
epoll_ctl(epfd, ...); // 将所有需要监听的socket添加到epfd中

while(1) {
	int n = epoll_wait(...)
	for(/*接收到数据的socket*/) {
		// 处理
	}
}

措施二:就绪列表
select低效的另一个原因是程序不知道哪些socket收到数据,只能一个个遍历。如果内核需要维护一个“就绪列表” (rdlist),引用收到数据的socket,就能避免遍历。

epoll 的原理和流程

创建epoll对象:

#include <sys/epoll.h>

int epoll_create(int size);

当进程调用epoll_create方法时,内核会创建一个 eventpoll 对象 ( 就是epollfd指代的对象 )。eventpoll 对象也是文件系统的一员,和socket一样,它也会有等待队列。
在这里插入图片描述
创建一个代表该 epolleventpoll 对象是必须的, 因为内核要维护 “就绪列表” 等数据,“就绪列表”可以作为eventpoll的成员。

维护监视列表

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

创建 epoll 对象后,可以用 epoll_ctl 添加或删除所要监听的 socket。以添加 socket 为例,如下图,如果通过 epoll_ctl 添加 sock1sock2sock3 的监视,内核会将 eventpoll 添加到这3个socket 的等待队列中。
在这里插入图片描述
socket 收到数据后,中断程序会操作 eventpoll 对象,而不是直接操作进程。

接收数据
当有套接字准备就绪时,中断处理程序将该套接字引用添加到 eventpollrdlist 就绪列表中,并且通知 eventpoll 等待队列中的进程有准备好的套接字。
在这里插入图片描述
进程A通过 rdlist 获取已经准备好的套接字。

epoll 的实现细节

如下图所示,eventpoll包含了lock、mtx、wq(等待队列)、rdlist等成员。rdlist和rbr是我们所关心的。
在这里插入图片描述

  • lockmtx 保证了 epoll 是线程安全的,
  • wq 是进程的等待队列,
  • rdlist的实现是双向链表,元素是epitem, 表示,
  • rbr 的实现是红黑树, 元素是epitem, 表示需要监听的套接字.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值