写在前面

我在学习Java NIO时,看到网上很多资料是从Reactor模式入手,当我继续深挖下去,意识到NIO的本质或许不只Reactor模式那么简单,那又是什么呢?

于是我决定从Linux的系统调用着手,想了解一下Linux系统怎么做到的并发I/O。

所以这篇文章,更多得是对最近学习Linux I/O知识的梳理和总结。

本文主要是想解答一下这样几个问题:

- 什么是非阻塞I/O
- 非阻塞I/O和异步I/O的区别
- epoll的工作原理

文件描述符

文件描述符是一个非负整数,用于标识一个打开的文件。

这里“文件”一词是更宽泛的概念,可以是进程中使用的任何类型的I\O资源,例如常规文件,管道,Socket等。

通常,打开I\O流的系统调用都会返回一个int类型的文件描述符。

例如下面分别是打开一个文件和创建一个Socket的系统调用。

int open(const char *pathname, int flags);
int socket(int domain, int type, int protocol);

(文件描述符File descriptor,在程序中一般简写为fd。)

阻塞模式(Blocking I/O)

默认的,Unix系统的所有文件描述符都是阻塞模式。这意味着有关I/O的操作(如read,write,open)都是阻塞的。

以从Linux终端窗口读取数据为例,如果你在程序中调用了read(),程序会一直阻塞,直到你确实敲键盘输入了字符。

这里的程序阻塞,更准确地讲,是系统内核把该进程设置成了睡眠状态,直到有数据输入才被唤醒。

TCP Socket通信也是一样的道理,如果你尝试着从socket中读取数据,read()会被阻塞,直到socket的另一端发送了数据。

可见,在阻塞模式下程序是不能并发操作的。

对于并发I/O,我们将讨论三个解决办法:

  • 非阻塞模式
  • I/O多路复用系统调用,select和epoll
  • 多线程/多进程

非阻塞模式(Nonblocking I/O)

在打开一个文件时,设置flag参数为O_NONBLOCK,此标志位告诉操作系统对这个文件的操作应该是非阻塞的,或者说该文件描述符处于非阻塞模式。

int open(const char *pathname, int flags, mode_t mode);

这意味着两点:

  1. 如果这个文件不能立即打开,open()系统调用会返回一个错误码,而不是阻塞open()。
  2. 文件打开成功后,后续的I/O操作应该也是非阻塞的,例如如果不能立即完成read或write,返回错误码。

    不能立即完成读写的原因可能是没有数据可读,或者缓存已满没有更多空间可以写。

    (注意,非阻塞模式对常规文件无效,因为系统内核总能保证有足够的缓存让常规文件I/O不阻塞)

非阻塞模式仅仅是Unix系统中一个原始特性,只有它还不能让程序并发执行。还需要在应用程序中写一个死循环,不停的检测文件描述符的状态。

下面以并发访问两个文件描述符为例写一段伪代码。

先解释一下read()系统调用的几个参数:

int read(int fd, void *buffer, size_t count);

  • fd 即读取的文件描述符
  • buffer 用于接收本次读取的数据
  • count 本次读取的最大字节数
  • 返回值 实际读取的字节数,发生错误返回-1
ssize_t nbytes;
for (;;) {
//文件描述符1
if ((nbytes = read(fd1, buf, sizeof(buf))) < 0) {
if (errno != EWOULDBLOCK) {
//发生错误,且错误码为EWOULDBLOCK
//说明文件描述符fd1没有准备好read
}
} else {
handle_data(buf); //处理数据
}
//文件描述符2
if ((nbytes = read(fd2, buf, sizeof(buf))) < 0) {
if (errno != EWOULDBLOCK) {
//fd2不能read
}
} else {
handle_data(buf);
}
nanosleep(sleep_interval, NULL); //再次检测前睡眠片刻
}

以上实现了对两个文件并发I\O,但有很明显的缺点:

  • 循环的频次太低,会导致I/O的响应延迟。
  • 循环的频次太高,当需要并发处理的文件很多时,每次循环都要检测所有文件,性能会变得很差(read()是系统调用)。

多进程/线程方式

并发处理多个I/O流还有一种更原始的方法,使用多进程或多线程,每个I/O流独占一个进程或线程,很容易理解。

也有很多缺点:

  • 在任何时刻,都可能有大量的线程处于空闲状态,造成资源浪费。
  • 线程是占内存的,不可能创建太多的工作线程。
  • 线程/进程的上下文切换也会带来很大的性能开销。

在互联网早期流量比较小,很多服务采用的这种方式,但是它只适用于低并发的服务器。

上面讨论了非阻塞模式和多进程两个方式,在高并发场景都不太好用。

这时,我们就需要Unix的I/O多路复用了。

I/O多路复用(I/O Multiplexing)

类Unix系统有多个实现了I/O多路复用的系统调用,Unix系统的selectpoll,Linux的epoll,以及BSD的kqueue

他们的底层工作原理相似:首先告诉系统内核你想监控哪些文件描述符的哪些事件(典型的read和write事件),然后用户程序被阻塞,直到你感兴趣的事件发生(事件驱动模型)。

Unix的I/O多路复用机制不关心文件描述符是否处于非阻塞模式,你可以把所有文件描述符设置为阻塞模式,epoll和select不会受影响。

这一点很重要!非阻塞模式和I/O多路复用,这两个方法都可以实现并发I/O,但是本质上他们是相互独立的两个解决问题思路,互不依赖

多路复用实现的并发I/O,有时被称为异步I/O(asynchronous I/O)。但是有人也把这种方式称为非阻塞I/O,通过见面的内容,我们知道这种称呼是错误的。

epoll的工作原理

epoll是event poll的简写,是Linux内核提供的一种由事件驱动的I/O通知机制。(注意epoll是Linux特有的,其他类Unix系统可能没有实现。)

另外,epoll本身并不是一个系统调用,而是一组系统调用的统称。

这组系统调用包括:

  • epoll_create()系统调用
  • epoll_ctl()系统调用
  • epoll_wait()系统调用

epoll_create()

方法签名:int epoll_create(int size)

该系统调用用于创建一个epoll实例,该实例存在于系统内核空间。

epoll实例会初始化一个列表,用于存储用户程序感兴趣的文件描述符,下文简称interest list,参数size即为这个列表的初始大小(可以动态扩展)。返回值是epoll实例的文件描述符。

epoll_ctl()

使用epoll_ctl()可以对interest list增删改(ctl应该是control的缩写)

方法签名:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev)

参数说明:

  • epfd: 即epoll_create()创建的epoll实例的文件描述符
  • op: 枚举值,指定操作类型,例如EPOLL_CTL_ADD添加一个文件描述符
  • ev: 结构体类型,是对该文件描述符的设置。

    ev参数最重要的是ev.events字段可以添加感兴趣的事件,例如添加了read事件,表示该文件可以read的时候,通知应用程序。

epoll_wait()

用户程序调用该方法获取ready的文件描述符。

继续之前先解释一下文件描述符什么时候ready

即使在文件描述符处于阻塞模式(没有设置O_NONBLOCK)的情况下,对该文件描述符的read\write等操作扔不会阻塞,就说该文件描述符ready。

方法签名:int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);

参数说明:

  • epfd: epoll实例的文件描述符
  • evlist: 用于接收ready的文件描述符,该数组由用户程序分配。
  • maxevents: 一次调用返回的最大事件数量
  • timeout: epoll_wait系统调用的超时时间,0表示不阻塞,无事件发生立即返回。-1一直阻塞,直到有事件发生。大于0表示超时返回。

为了对epoll的并发I/O编程有个感性的认识,我们来写一段伪代码

  epfd = epoll_create(EPOLL_QUEUE_LEN); //创建epoll实例
static struct epoll_event ev;
int client_sock;
ev.events = EPOLLIN | EPOLLPRI | EPOLLERR | EPOLLHUP; //声明感兴趣的事件
ev.data.fd = client_sock; //文件描述符指向一个Socket连接
//添加要监控的文件描述符和事件类型到interest list
//真实环境中,可能需要添加成百上千个这种事件。
int res = epoll_ctl(epfd, EPOLL_CTL_ADD, client_sock, &ev);
while (1) {
int nfds = epoll_wait(epfd, events, MAX_EVENTS_PER_RUN, TIMEOUT);//获取ready的事件,可能有多个
if (nfds < 0) die("Error in epoll_wait!"); //发生错误,退出程序
for(int i = 0; i < nfds; i++) { //遍历处理每一个已ready的socket
int fd = events[i].data.fd;
handle_io_on_socket(fd);
}
}

从伪代码中可以看到,基于epoll的I/O多路复用的时间复杂度只有O(n),其中n是ready的事件数量。

时间复杂度不会随着interest list的增加而线性增长,这使得epoll有很好的扩展性。

试想一个高并发服务器同一时刻可能需要监控成千上万甚至更多的socket连接,

如果使用文章开始介绍的非阻塞模式,每一次循环都要对全部的socket连接进行测试,吞吐量可想而知。

但是epoll的压力只与socket通信的繁忙程度有关。

另外提一下,Unix中的poll()和select()有着更差的时间复杂度和空间复杂度,篇幅受限这里不展开说了。

最后,结合图片了解一下epoll的内部结构(图片出处见文章最后)

1.进程483通过epoll_create()创建一个epoll实例,该实例存在于内核空间。

2.进程483通过epoll_ctl()系统调用,添加五个感兴趣的文件描述符到interest list。

3.当有文件描述符ready时,系统内核会把该文件描述符添加到ready list,ready list是interest list的子集。

事件发生后添加到ready list,这个过程是内核完成的。

4. 用户程序调用epoll_wait()时,内核会把ready list返回给用户程序。

写在最后

博主的主力语言是Java,对C一知半解,如文章有理解错误的地方,感谢指正。

学习途中翻阅了很多资料,其中对我帮助最大的是《The Linux Programming Interface》,作者是Linux man-pages的维护者,具有一定的权威性。

这本书并没有像书名一样停留在Linux API层面,而是穿插着讲解了很多原理性的知识,有几个知识点讲的可谓醍醐灌顶。

无论你用的什么语言,这本书都非常值得一读。

其他资料就比较杂乱了,像维基百科、man手册、个人博客都有,链接都放在下面了。

希望本文对你有所帮助。

参考内容

  1. 《The Linux Programming Interface》英文版 4.3章节、5.9章节、 56.2章节、63.1章节、63.2.3章节 (核心参考)
  2. man7关于O_NONBLOCK的权威说明:http://man7.org/linux/man-pages/man2/open.2.html ( 里面有提到,设置O_NONBLOCK与否对select和epoll没有影响。)
  3. 很清晰的解释了非阻塞模式和多路复用的区别和联系:https://eklitzke.org/blocking-io-nonblocking-io-and-epoll
  4. 介绍了epoll的工作原理和内部数据结构(本文中的图片来源):https://medium.com/@copyconstruct/the-method-to-epolls-madness-d9d2d6378642
  5. 对《The Linux Programming Interface》,epoll章节的读书笔记:https://jvns.ca/blog/2017/06/03/async-io-on-linux--select--poll--and-epoll/
  6. 一个epoll编程的小demo : https://kovyrin.net/2006/04/13/epoll-asynchronous-network-programming/
  7. 关于epoll和poll性能的讨论: https://www.win.tue.nl/~aeb/linux/lk/lk-12.html

关于非阻塞I/O、多路复用、epoll的杂谈的更多相关文章

  1. 异步、非阻塞和IO多路复用总结

    Nginx是并发处理框架的代表者,很多后台业务都会放在Nginx容器中运行,以实现高吞吐,而Nginx能够支持高并发也是由于使用了异步非阻塞处理模型,本文将用通俗的话讲解异步.同步.阻塞.非阻塞的区别 ...

  2. IO模型--阻塞IO,非阻塞IO,IO多路复用,异步IO

    IO模型介绍: * blocking IO 阻塞IO * nonblocking IO 非阻塞IO * IO multiplexing IO多路复用 * signal driven IO 信号驱动IO ...

  3. Linux IO模式-阻塞io、非阻塞io、多路复用io

    一 概念说明 在进行解释之前,首先要说明几个概念: - 用户空间和内核空间 - 进程切换 - 进程的阻塞 - 文件描述符 - 缓存 I/O 用户空间与内核空间 现在操作系统都是采用虚拟存储器,那么对3 ...

  4. 并发编程 - IO模型 - 1.io模型/2.阻塞io/3.非阻塞io/4.多路复用io

    1.io模型提交任务得方式: 同步:提交完任务,等结果,执行下一个任务 异步:提交完,接着执行,异步 + 回调 异步不等结果,提交完任务,任务执行完后,会自动触发回调函数同步不等于阻塞: 阻塞:遇到i ...

  5. python开发IO模型:阻塞&非阻塞&异步IO&多路复用&selectors

    一 IO模型介绍 为了更好地了解IO模型,我们需要事先回顾下:同步.异步.阻塞.非阻塞 同步(synchronous) IO和异步(asynchronous) IO,阻塞(blocking) IO和非 ...

  6. python全栈开发,Day44(IO模型介绍,阻塞IO,非阻塞IO,多路复用IO,异步IO,IO模型比较分析,selectors模块,垃圾回收机制)

    昨日内容回顾 协程实际上是一个线程,执行了多个任务,遇到IO就切换 切换,可以使用yield,greenlet 遇到IO gevent: 检测到IO,能够使用greenlet实现自动切换,规避了IO阻 ...

  7. 并发\并行,同步\异步,阻塞\非阻塞,IO多路复用解释

    并发.并行 并发:是指一个时间段内,有几个程序在同一个CPU上运行,但是任意时刻只有一个程序在CPU上运行.由于CPU的运行速度极快,可以在多个程序之间切换,这样造成一个假象就是多个程序同时在运行.并 ...

  8. 非阻塞socket调用connect, epoll和select检查连接情况示例

    转自http://www.cnblogs.com/yuxingfirst/archive/2013/03/08/2950281.html 我们知道,linux下socket编程有常见的几个系统调用: ...

  9. 网络编程基础——学习阻塞,非阻塞(select和epoll)

    <h3 class="xyn" helvetica="" neue',="" helvetica,="" aria ...

  10. 从I/O事件到阻塞、非阻塞、poll到epoll的理解过程

    I/O事件   I/O事件 非阻塞I/O.在了解非阻塞I/O之前,需要先了解I/O事件 我们知道,内核有缓冲区.假设有两个进程A,B,进程B想读进程A写入的东西(即进程A做写操作,B做读操作).进程A ...

随机推荐

  1. Cpython和Jython的对比介绍

    CPython 当我们从Python官方网站下载并安装好Python 3.x后,我们就直接获得了一个官方版本的解释器:CPython.这个解释器是用C语言开发的,所以叫CPython.在命令行下运行p ...

  2. python使用openpyxl操作excel总结

    安装openpyxl pip install openpyxl 简单示例 from openpyxl import Workbook #创建一个工作薄对象,也就是创建一个excel文档 wb = Wo ...

  3. Ansibile之playbook初识

    一.playbook简介 ansiblie的任务配置文件被称为playbook,俗称“剧本”,每一个剧本(playbook)中都包含了一系列的任务,这每个任务在ansible中又被称为“戏剧”(pla ...

  4. 201871010114-李岩松《面向对象程序设计(java)》第十周学习总结

    项目 内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh/ 这个作业的要求在哪里 https://www.cnblogs.com/nwnu-daizh/p ...

  5. PHP程序员-常用工具

    三连问 经常有社区的同学问: “我的PHP程序有没有阻塞,我的PHP程序有没有开启协程(对自己写好的代码表示不自信),我的PHP程序有没有问题”.然后贴出了自己的程序,然后进入了愉快的灌水环节,随着时 ...

  6. 如何基于 PHP-X 快速开发一个 PHP 扩展

    0x01 起步 PHP-X本身基于C++11开发,使用cmake进行编译配置.首先,你需要确定所有依赖项已安装好.包括: gcc-4.8 或更高版本 PHP7.0 或更高版本,需要php7-dev 开 ...

  7. [LC]747题 Largest Number At Least Twice of Others (至少是其他数字两倍的最大数)

    ①中文题目 在一个给定的数组nums中,总是存在一个最大元素 . 查找数组中的最大元素是否至少是数组中每个其他数字的两倍. 如果是,则返回最大元素的索引,否则返回-1. 示例 1: 输入: nums ...

  8. spring源码1

    1.beans核心类 1.DefaultListableBeanFactory xmlBeanFactory xmlBeanFactory继承自DefaultListableBeanFactory,D ...

  9. Cef 因系统时间不正常,导致页面访问空白问题

    当我们的系统时间不正常,比如设置一个日期-1999年9月9日,会引发证书问题. 系统时间不正常-IE有概率能访问 触发NavigateError事件,异常代码INET_E_INVALID_CERTIF ...

  10. 宋宝华: Linux内核编程广泛使用的前向声明(Forward Declaration)

    本文系转载,著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 作者:宋宝华 来源: 微信公众号linux阅码场(id: linuxdev) 前向声明 编程定律 先强调一点:在一切可 ...