NGINX为什么可以做到高并发(二)


在上一篇我们简单介绍了NGINX使用的多路复用I/O模型,这一优秀的设计让NGINX在高并发上表现得非常出色。
优秀的设计向来是有迹可循的,这些I/O模型伴随着计算机前辈们不断优化网络效率问题而产生,这是一个推陈出新的过程。
我们将横向对比,通过进一步学习另外几种模型,加深对多路复用I/O模型的理解。
    
那话题就变成Unix的5种I/O模型介绍,但是呢,关于这个话题的博客已经多如牛毛,我得写点不一样的东西。
不一样的地方在于,这里有我的思考和我的理解,我会尽量还原我思考的过程,因此下文可能不尽准确。
    
    
Unix的5种I/O模型,显然我们学习的是五种设计,这5种设计是针对Unix平台。
I/O是输入输出的缩写,对于计算机而言,我们输入数据,计算机将输出结果。    
    
那么这里的IO模式是什么?
这里的IO模式只讨论网络通信的I/O模式,对于进程间通信可能不适用。

以TCP数据传入举例:
    (1)等待数据报文到达网卡->读取到数据内核缓冲区,标志着数据准备好;
    (2)从内核缓冲区复制数据到用户进程空间。
        
为什么数据报文到达网卡后不直接复制到用户进程空间呢?
    因为Linux不允许这样操作,网卡属于I/O设备,只有kernel可以触碰到这一块资源,故必须通过系统接口调用指令,请求kernel来协助完成I/O动作。


为什么kernel不直接复制到用户进程空间呢(异步的思想)?
    
    因为设备IO速度不仅慢还不稳定,而电脑的线程的处理速度比较快,
    利用内核的缓冲区做中转是比较合适的做法,系统可以更充分调度处理器资源,
    当进程检查到内核缓冲区有数据,先执行复制到进程空间再进行运算。
    因为内核缓冲区是内核空间,程序没办法在上面运算,
    只能通过内核接口调用指令,请求kernel完成复制数据到用户空间操作。
    异步IO只是让kernel将这些步骤连贯起来,并非精简步骤。

5种I/O模型的名字:
    阻塞I/O(blocking IO)、非阻塞I/O(nonblocking IO)、
    I/O多路复用(IO multiplexing)、信号驱动I/O(signal driven IO)、
    异步 I/O(asynchronous IO)

阻塞I/O(blocking IO)、非阻塞I/O(nonblocking IO)这种模型,
看上去把 I/O分成了两个派别,注意这里的阻塞是进程阻塞,而非线程阻塞,进程阻塞要更严重些。

题外话:关于线程和进程阻塞上的区别网上解答比较混乱,因为这里会涉及到线程模型,感兴趣的可以去搜一下这类话题。这里只说一个结论,linux基本上都采用一对一模型,线程调用阻塞时不会导致所属进程阻塞。


阻塞I/O是最流行的I/O模型,进程阻塞于内核recvfrom的调用(监听文件描述符fd),直到发现数据报到达并且复制到应用进程缓冲区中或者发生错误才返回。
最常见的错误是系统调用被信号中断。

非阻塞I/O是进程不阻塞于recvfrom的调用,进程告诉内核,不管如何都要立刻回答。若没准备好,我待会再来问。
可以看到这里是轮询(poll)的思想。虽然效率不高,但是体验比阻塞I/O好很多。
比如用单线程做同样一个耗时的操作,阻塞I/O会表现出假死未响应状态,非阻塞I/O则是不断打印提示请等待。


这里需要补充知识点,文件描述符fd。
文件描述符fd:
    相当于Windows的句柄,Linux把一切资源看作文件,本质上是一个索引号(非负整数),系统用户层可以根据它找到系统内核层的文件数据。
    内核(kernel)利用文件描述符来访问文件。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要文件描述符来指定待读写的文件。


 I/O多路复用(IO multiplexing)
    概念:调用select和poll,阻塞在这两个系统调用中的一个,而不是阻塞在真正的I/O系统调用上。I/O复用的优势在于可以等待多个描述符。
 
 要理解这个概念得对非阻塞I/O的弊端有更深的理解,非阻塞I/O弊端是poll会耗费大量的cpu时间。我们想节省这个时间。举例来说,假如我们检查一次邮箱是否有邮件需要10秒,假定我们有6个邮件账号,poll的做法将是耗时60秒,这着实有点蠢。我们现实生活中会通过foxmail客户端绑定这6个邮件号码,耗时将从60秒降低到10秒。
 而I/O多路复用就是这个意思,非阻塞I/O不断询问内核的recvfrom阻塞状态,I/O多路复用不断询问poll或者select这种代理机制的阻塞状态。
 
 题外话:I/O多路复用除了select、poll还有epoll,他们的区别将在下篇博文中介绍。
 

信号驱动I/O(signal driven IO)
概念:让内核在描述符就绪时发送SIGIO信号通知我们。这种模型优势在于等待期间进程不被阻塞可继续执行,只要等待来自信号处理函数的通知:既可以是数据准备好被处理,也可以是数据报准备好被读取。

这种一般我们不用,因为SIGIO信号产生得过于频繁,我们期望当连接请求已经完成时收到通知,可是如果连接中途断开或者重连也可能发出通知,我们反复确认的过程就像轮询一样浪费没必要的精力。


异步I/O(asynchronous IO)
    概念:告知内核启动某个操作并在整个操作(包括数据从内核复制到进程的缓冲区)完成后通知我们,和信号驱动式IO的区别:信号驱动式IO是内核通知我们何时可以启动一个IO操作,异步IO则是内核通知我们IO操作何时完成。

这种用得不多,因为Linux对其适配得不是很好。
这种大体上可以分为两种,反应式(Reactive)和前摄式(Proactive),我们这里针对后者。

反应式模型:
    传统的 select/epoll/kqueue 模型,以及 Java NIO 模型,都是典型的反应式模型,即应用代码对I/O描述符进行注册,然后等待 I/O事件。

前摄式模型:
    应用代码投递的异步 I/O 操作被系统接管,指定一个回调函数并继续自己的应用逻辑。当该异步操作完成时,系统将发起通知并调用应用代码指定的回调函数。


想必大家看到反应式模型会感到疑惑,这不是IO多路复用模型的内容嘛,为什么现在变成了异步I/O。
原因是异步两个字有误区。异步的定义很严格。

无论是非阻塞I/O(nonblocking IO)或者I/O多路复用(IO multiplexing)都不能算入异步,依然归为同步。
这是因为这两种模式下,程序均需要调用recvfrom来将数据拷贝到用户内存。而异步则是完全交给kernel完成。

Reactor(反应堆)是异步的模型,但是IO多路复用模型拓展了该模型的内容。

本篇小结:    
    Unix的5种I/O模型只有异步I/O(asynchronous IO)算异步,其余四种都是同步I/O。
    同步与异步,本质是用户线程与内核的交互方式不同。
    阻塞与非阻塞,本质是用户线程调用内核IO响应方式不同。


总结:本文对I/O模型进行了各方面的对比,重点关注了同步与异步,阻塞与非阻塞的区别,在对比中加深了5种模型的理解。下一篇 NGINX为什么可以做到高并发 将介绍NGINX的进程模式和处理请求模型,以及IO多路复用的3种机制。

发布了103 篇原创文章 · 获赞 175 · 访问量 25万+

猜你喜欢

转载自blog.csdn.net/ai_64/article/details/104585826