IO多路复用机制

从阻塞 I/O 到 I/O 多路复用

  • 阻塞IO:
    阻塞 I/O,是指进程发起调用后,会被挂起(阻塞),直到收到数据再返回。如果调用一直不返回,进程就会一直被挂起。因此,当使用阻塞 I/O 时,需要使用多线程来处理多个文件描述符。多线程切换有一定的开销,因此引入非阻塞 I/O。

  • 非阻塞IO:
    非阻塞 I/O 不会将进程挂起,调用时会立即返回成功或错误,因此可以在一个线程里轮询多个文件描述符是否就绪。
    但是非阻塞 I/O 的缺点是:每次发起系统调用,只能检查一个文件描述符是否就绪。当文件描述符很多时,系统调用的成本很高。

因此我们引入了IO多路复用,可以通过一次系统调用(一个线程),检查多个文件描述符的状态。一旦某个文件描述符准备就绪,就能通知到对应应用程序进行相应读写操作。

这是 I/O 多路复用的主要优点,相比于非阻塞 I/O,在文件描述符较多的场景下,避免了频繁的用户态和内核态的切换,减少了系统调用的开销。

I/O 多路复用相当于将「遍历所有文件描述符、通过非阻塞 I/O 查看其是否就绪」的过程从用户线程移到了内核中,由内核来负责轮询。

文件描述符

上面我们提到了文件描述符,这里就解释下 什么是文件描述符?

这里引用 linux进程.md

file
先说 files,它是一个文件指针数组。一般来说,一个进程会从 files[0] 读取输入,将输出写入 files[1],将错误信息写入 files[2]。

举个例子,以我们的角度 C 语言的 printf 函数是向命令行打印字符,但是从进程的角度来看,就是向 files[1] 写入数据;同理,scanf 函数就是进程试图从 files[0] 这个文件中读取数据。

每个进程被创建时,files 的前三位被填入默认值,分别指向标准输入流、标准输出流、标准错误流。我们常说的「文件描述符」就是指这个文件指针数组的索引,所以程序的文件描述符默认情况下 0 是输入,1 是输出,2 是错误。

对于一般的计算机,输入流是键盘,输出流是显示器,错误流也是显示器,所以现在这个进程和内核连了三根线。因为硬件都是由内核管理的,我们的进程需要通过「系统调用」让内核进程访问硬件资源。

note:不要忘了,Linux 中一切都被抽象成文件,设备也是文件,可以进行读和写。

如果我们写的程序需要其他资源,比如打开一个文件进行读写,这也很简单,进行系统调用,让内核把文件打开,这个文件就会被放到 files 的第 4 个位置:

明白了这个原理,输入重定向就很好理解了,程序想读取数据的时候就会去 files[0] 读取,所以我们只要把 files[0] 指向一个文件,那么程序就会从这个文件中读取数据,而不是从键盘:

$ command < file.txt

同理,输出重定向就是把 files[1] 指向一个文件,那么程序的输出就不会写入到显示器,而是写入到这个文件中:

$ command > file.txt

错误重定向也是一样的,就不再赘述。

管道符其实也是异曲同工,把一个进程的输出流和另一个进程的输入流接起一条「管道」,数据就在其中传递,不得不说这种设计思想真的很优美:

$ cmd1 | cmd2 | cmd3

到这里,你可能也看出「Linux 中一切皆文件」设计思路的高明了,不管是设备、另一个进程、socket 套接字还是真正的文件,全部都可以读写,统一装进一个简单的 files 数组,进程通过简单的文件描述符访问相应资源,具体细节交于操作系统,有效解耦,优美高效。

socket 可以用于同一台主机的不同进程间的通信,也可以用于不同主机间的通信。操作系统将 socket 映射到进程的一个文件描述符上,进程就可以通过读写这个文件描述符来和远程主机通信。
socket 是进程间通信规则的高层抽象,而 fd 提供的是底层的具体实现。socket 与 fd 是一一对应的。后面可以将 socket 和 fd 视为同义词。

IO多路复用的三种模型

进程可以通过 select、poll、epoll发起IO多路复用的系统调用,这些系统调用都是同步阻塞的:如果传入的多个文件描述符中,有描述符就绪,则返回就绪的描述符;否则如果所有文件描述符都未就绪,就阻塞调用进程,直到某个描述符就绪,或者阻塞时长超过设置的 timeout 后,再返回。I/O 多路复用内部使用非阻塞 I/O 检查每个描述符的就绪状态。

I/O 多路复用引入了一些额外的操作和开销,性能更差。但是好处是用户可以在一个线程内同时处理多个 I/O 请求。如果不采用 I/O 多路复用,则必须通过多线程的方式,每个线程处理一个 I/O 请求。后者线程切换也是有一定的开销的。

Select

file

int select(int nfds,
            fd_set *restrict readfds,
            fd_set *restrict writefds,
            fd_set *restrict errorfds,
            struct timeval *restrict timeout);

readfdswritefdserrorfds 是三个文件描述符集合。select 会遍历每个集合的前 nfds 个描述符,分别找到可以读取、可以写入、发生错误的描述符,统称为“就绪”的描述符。然后用找到的子集替换参数中的对应集合,返回所有就绪描述符的总数。

timeout 参数表示调用 select 时的阻塞时长。如果所有文件描述符都未就绪,就阻塞调用进程,直到某个描述符就绪,或者阻塞超过设置的 timeout 后,返回。如果 timeout 参数设为 NULL,会无限阻塞直到某个描述符就绪;如果 timeout 参数设为 0,会立即返回,不阻塞。

找到就绪的文件描述符,触发相应的IO操作。
优点:跨平台性好,几乎在所有平台都支持
缺点:

  • 性能开销大:select系统调用,会将 fd_set 从用户空间拷贝到内核空间。并且内核需要轮询遍历 fd_set的每一位。因此随着 FD 数量的增多会导致性能下降
  • 同时能够监听的文件描述符少。一般是1024个,不同的操作系统不相同。

Poll

file
poll与select的区别只在于,poll在用户态时是通过数组方式传递文件描述符,在内核态时会转为链表存储,所以没有最大数据的限制。

EPoll

select 和 poll 都会因为吞吐量增加而导致性能下降,因此出现了 epoll 模型。
epoll 是对 select 和 poll 的改进,避免了“性能开销大”和“文件描述符数量少”两个缺点。

简而言之,epoll 有以下几个特点:

  • 使用红黑树存储文件描述符集合
  • 使用队列存储就绪的文件描述符
  • 每个文件描述符只需在添加时传入一次;通过事件更改文件描述符状态

它能支持的文件描述符FD上限是操作系统的最大文件描述符数,一般而言1G内存大概支持10万个文件描述符。所以分布式系统中如Redis、Nginx都是优先使用 epoll模型。

select、poll 模型都只使用一个函数,而 epoll 模型使用三个函数:epoll_create、epoll_ctl 和 epoll_wait。

1. epoll_create

epoll_create 会创建一个 epoll 实例,同时返回一个引用该实例的文件描述符。

返回的文件描述符仅仅指向对应的 epoll 实例,并不表示真实的磁盘文件节点。其他 API 如 epoll_ctl、epoll_wait 会使用这个文件描述符来操作相应的 epoll 实例。

当创建好 epoll 句柄后,它会占用一个 fd 值,在 linux 下查看 /proc/进程id/fd/,就能够看到这个 fd。所以在使用完 epoll 后,必须调用 close(epfd) 关闭对应的文件描述符,否则可能导致 fd 被耗尽。当指向同一个 epoll 实例的所有文件描述符都被关闭后,操作系统会销毁这个 epoll 实例。

epoll 实例内部存储:

  • 监听列表:所有要监听的文件描述符,使用红黑树
  • 就绪列表:所有就绪的文件描述符,使用链表
2. epoll_ctl

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll_ctl 会监听文件描述符 fd 上发生的 event 事件。每新建一个连接的时候,会同步更新 epoll 对象中的 FD,并绑定一个 callback 回调函数。

参数说明:

  • epfd 即 epoll_create 返回的文件描述符,指向一个 epoll 实例
  • fd 表示要监听的目标文件描述符
  • event 表示要监听的事件(可读、可写、发送错误…)
  • op 表示要对 fd 执行的操作,有以下几种:
  • EPOLL_CTL_ADD:为 fd 添加一个监听事件 event
  • EPOLL_CTL_MOD:Change the event event associated with the target file descriptor fd(event 是一个结构体变量,这相当于变量 event 本身没变,但是更改了其内部字段的值)
  • EPOLL_CTL_DEL:删除 fd 的所有监听事件,这种情况下 event 参数没用

返回值 0 或 -1,表示上述操作成功与否。

epoll_ctl 会将文件描述符 fd 添加到 epoll 实例的监听列表里,同时为 fd 设置一个回调函数,并监听事件 event。当 fd 上发生相应事件时,会调用回调函数,将 fd 添加到 epoll 实例的就绪队列上。

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

这是 epoll 模型的主要函数,功能相当于 select。轮询所有的callback集合,触发对应的IO操作。

参数说明:

  • epfd 即 epoll_create 返回的文件描述符,指向一个 epoll 实例
  • events 是一个数组,保存就绪状态的文件描述符,其空间由调用者负责申请
  • maxevents 指定 events 的大小
  • timeout 类似于 select 中的 timeout。如果没有文件描述符就绪,即就绪队列为空,则 epoll_wait 会阻塞 timeout 毫秒。如果 timeout 设为 -1,则 epoll_wait 会一直阻塞,直到有文件描述符就绪;如果 timeout 设为 0,则 epoll_wait 会立即返回

返回值表示 events 中存储的就绪描述符个数,最大不超过 maxevents。

selectpollepoll
数据结构数组链表B+树、红黑树
最大连接数1024无上限无上限
FD拷贝每次调用select每次调用pollFD首次调用epoll_ctl拷贝,每次调用 epoll_wait不用拷贝
效率轮询:O(n)轮询:O(n)回调:O(1)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

云淡风轻~~

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

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

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

打赏作者

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

抵扣说明:

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

余额充值