nginx IO模型

折月煮酒 提交于 2019-12-02 23:27:42

今天下班早些来普及下nginx io模型:

用户空间与内核空间:

    现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。

进程切换:

    为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。
从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:
保存处理机上下文,包括程序计数器和其他寄存器。
更新PCB信息。
把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
选择另一个进程执行,并更新其PCB。
更新内存管理的数据结构。
恢复处理机上下文。

注:总而言之就是很耗资源,具体的可以参考这篇文章:

http://guojing.me/linux-kernel-architecture/posts/process-switch/

进程阻塞:

    正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。

文件描述符:

    文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

缓存IO:

    缓存 IO 又被称作标准 IO,大多数文件系统的默认 IO 操作都是缓存 IO。在 Linux 的缓存 IO 机制中,操作系统会将 IO 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

缓存IO的缺点:

    数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。

Linux IO模型:

    网络IO的本质是socket的读取,socket在linux系统中被抽象为流,IO可以理解为对流的操作.对于一次IO访问,数据会先被拷到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间,所以说当一个read操作发生时,它会经理两个阶段:

1
2
第一阶段:等待数据准备 (Waiting for the data to be ready)。
第二阶段:将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。

    对socket流而言:

1
2
第一步:通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区。
第二步:把数据从内核缓冲区复制到应用进程缓冲区。

网络应用需要处理的无非就是两大类问题,网络IO,数据计算。相对于后者,网络IO的延迟,给应用带来的性能瓶颈大于后者。网络IO的模型大致有如下几种:

1
2
3
4
5
6
同步模型(synchronous IO)
   阻塞IO(bloking IO)
   非阻塞IO(non-blocking IO)
   多路复用IO(multiplexing IO)
   信号驱动式IO(signal-driven IO)
异步IO(asynchronous IO)

同步4个:阻塞IO、非阻塞IO、多路复用IO、信号驱使式IO

异步1个:异步IO

注:由于信号驱动式(signal driven IO)在实际中并不常用,所以我这只提及剩下的四种IO Model。

在深入介绍Linux IO各种模型之前,让我们先来探索一下基本 Linux IO 模型的简单矩阵。如下图所示:

 

每个 IO 模型都有自己的使用模式,它们对于特定的应用程序都有自己的优点。

同步和异步主要针对C(client)端
同步:
所谓的同步,就是在c端发出一个功能调用时,在没有得到结果之前,该调用步返回,也就是说必须一件一件事做,等前一件事完了之后才做后一件事。
如:普通的B/S模式(同步):提交请求->等待服务器处理->处理完毕返回,这期间客户端浏览器不能干任何事

异步:
与同步相对。当C端一个异步过程调用发出之后,调用者不能立即得到结果,实际处理这个调用的部件在完成后,通过状态,通知和回调来通知调用者。
如:请求通过事件触发->服务器处理(浏览器仍然可以做其他事情)->处理完毕

阻塞和非阻塞主要针对S端(server)

阻塞:
阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态,cpu不会分配时间片,线程暂停运行)函数只有得到结果返回。
阻塞调用和同步调用的区别:对同步来说,很多时候当前线程还是激活的,只是逻辑上没有返回,如,在socket编程中调用recv函数,如果缓冲区没有数据,这个函数就会一直等待,直到有数据返回。而此前当前线程还有可能继续处理各种各样的消息。

阻塞的例子:比如去取A楼一层(假设是内核缓冲区)取快递,但是比不知道什么时候来,你有不能干别的事情,只能死等着但是可以睡觉(进程处于休眠状态),因为你知道快递把货送来时一定会给比大电话

非阻塞:
非阻塞与阻塞概念想对应,指在不能立即得到结果之前,该函数不会阻塞当前线程,而会立即返回。

非阻塞的例子:还是等快递,如果用轮询的方式,每隔5分钟去A楼一层(内核缓冲区)去看快递来了没,没来,立即返回,如果快递来了,就放到A楼一层,等你去取。

对象是否处于阻塞模式和函数是不是阻塞调用有很强的相关性,但不是一一对应的。阻塞对象上可以有非阻塞的调用方式,我们可以通过轮询状态,在适当的时候调用阻塞函数,就可以避免阻塞,而对于非阻塞对象,调用函数可以进入阻塞调用,对于select:
1:同步 

1
我客户端(C端调用者)一个功能,该功能没有结束前,我死等结果。

2:异步

1
2
我(c端调用者)调用一个功能,不知道该功能结果,该功能有结果后通知我,即回调通知
同步和异步主要针对c端,但是跟s端不是完全没关系,同步和异步必须s端配合才能实现,同步和异步由c端控制,但是s端是否为阻塞还是非阻塞,c端不关心。

3:阻塞

1
就是调用我(s端被调用者,函数),我(s端被调用者,函数)没有完全接受完数据或者没有得到结果之前,我不会返回。

4:非阻塞

1
就是调用我(s端被调用者,函数),我(s端被调用者,函数)立即返回,通过select通知调用者

同步I/O与异步I/O的区别在与数据访问的时候进程是否阻塞
阻塞I/O与非阻塞I/O的区别在与:应该程序的调用是否立即返回。

阻塞和非阻塞是指server端的进程访问的数据如果尚未就绪,进程是否需要等待,简单说这相当于函数内部的实现区别,也就是未就绪时时直接返回还是等待就绪。

就同步和异步是指client端访问数据的机制,同步一般指主动请求并等待I/O操作完毕的方式,当数据就绪后再读写额时候必须阻塞,异步则指主动请求数据后便可以继续处理其他任务,随后等待I/O,操作完毕的通知。

一、阻塞I/O模型:
简介:进程会一直阻塞,直到数据拷贝完成
应用程序调用一个I/O函数,导致应用程序阻塞,等待数据准备好,如果数据没有准备好,一直等待。。数据准备好,从内核拷贝到用户空间,I/O函数返回成功

阻塞I/O模型图:在调用recv()/recvfrom(),发生在内核中等待数据和复制数据过程。

 

当调用recv()函数时,系统首先检查是否有准备好的数据,如果数据没有准备好,那么系统就处于等待状态,当数据准备好后,将数据从系统缓冲区复制到用户空间,然后函数返回。在套接应用程序中,当调用recv()函数时,未必用户空间就已经存在数据,那么此时recv()函数处于等待状态

二、非阻塞I/O模型:
简介:我们把一个套接口设置为非阻塞就是告诉内存,当所请求的I/O操作无法完成时,不要惊进程睡眠,而是返回一个错误,河阳I/O函数会不断的测试数据是否准备好,没有准备好,继续测试,直到数据准备好为止。在测试的过程中会占用大量的CPU时间。

 

三、I/O复用模型:
简介:主要是select和epoll;对于一个I/O端口,两次调用,两次返回,比阻塞I/O并没有什么优势,只是能实现同时对多个I/O端口进行监听。

I/O复用模型会调用select,poll函数,这几个函数也会使进程阻塞,但是和阻塞I/O不同的,这个函数可以同时阻塞多个I/O操作,而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。

四、信号驱动I/O:
简介:两次调用,两次返回
首先允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。昂数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。

五、异步I/O模型:
简介:数据拷贝的时候进程无需阻塞
当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态,通知和回调通知调用者输入输出操作。

同步I/O引起进程阻塞,直到I/O操作完成 
异步I/O不会引起进程阻塞 
I/O复用先通过select调用阻塞

 

NGINX:

nginx 支持多种并发模型,并发模型的具体实现根据系统平台而有所不同。
在支持多种并发模型的平台上,nginx 自动选择最高效的模型。但我们也可以使用 use 指令在配置文件中显式地定义某个并发模型。

NGINX中支持的并发模型:

select:

1
IO多路复用、标准并发模型。在编译 nginx 时,如果所使用的系统平台没有更高效的并发模型,select 模块将被自动编译。configure 脚本的选项:--with-select_module 和 --without-select_module 可被用来强制性地开启或禁止 select 模块的编译

poll:

1
IO多路复用、标准并发模型。与 select 类似,在编译 nginx 时,如果所使用的系统平台没有更高效的并发模型,poll 模块将被自动编译。configure 脚本的选项:--with-poll_module 和 --without-poll_module 可用于强制性地开启或禁止 poll 模块的编译

epoll:

1
IO多路复用、高效并发模型,可在 Linux 2.6+ 及以上内核可以使用

kqueue:

1
IO多路复用、高效并发模型,可在 FreeBSD 4.1+, OpenBSD 2.9+, NetBSD 2.0, and Mac OS X 平台中使用

/dev/poll:

1
高效并发模型,可在 Solaris 7 11/99+, HP/UX 11.22+ (eventport), IRIX 6.5.15+, and Tru64 UNIX 5.1A+ 平台使用

eventport:

1
高效并发模型,可用于 Solaris 10 平台,PS:由于一些已知的问题,建议 使用/dev/poll替代。

 

为什么epoll快?

比较一下Apache常用的select,和Nginx常用的epoll

select:

1
2
3
1、最大并发数限制,因为一个进程所打开的 FD (文件描述符)是有限制的,由 FD_SETSIZE 设置,默认值是 1024/2048 ,因此 Select 模型的最大并发数就被相应限制了。自己改改这个 FD_SETSIZE ?想法虽好,可是先看看下面吧 …
2、效率问题, select 每次调用都会线性扫描全部的 FD 集合,这样效率就会呈现线性下降,把 FD_SETSIZE 改大的后果就是,大家都慢慢来,什么?都超时了。
3、内核 / 用户空间 内存拷贝问题,如何让内核把 FD 消息通知给用户空间呢?在这个问题上 select 采取了内存拷贝方法,在FD非常多的时候,非常的耗费时间。

总结为:1、连接数受限 2、查找配对速度慢 3、数据由内核拷贝到用户态消耗时间

epoll:

1
2
3
1、Epoll 没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数字一般远大于 2048, 一般来说这个数目和系统内存关系很大 ,具体数目可以 cat /proc/sys/fs/file-max 查看。
2、效率提升, Epoll 最大的优点就在于它只管你“活跃”的连接 ,而跟连接总数无关,因此在实际的网络环境中, Epoll 的效率就会远远高于 select 和 poll 。
3、内存共享, Epoll 在这点上使用了“共享内存 ”,这个内存拷贝也省略了。

 

还不懂?

举个栗子

假设你在大学中读书,要等待一个朋友来访,而这个朋友只知道你在A号楼,但是不知道你具体住在哪里,于是你们约好了在A号楼门口见面.

如果你使用的阻塞IO模型来处理这个问题,那么你就只能一直守候在A号楼门口等待朋友的到来,在这段时间里你不能做别的事情,不难知道,这种方式的效率是低下的.

现在时代变化了,开始使用多路复用IO模型来处理这个问题.你告诉你的朋友来了A号楼找楼管大妈,让她告诉你该怎么走.这里的楼管大妈扮演的就是多路复用IO的角色.

select版大妈做的是如下的事情:比如同学甲的朋友来了,select版大妈比较笨,她带着朋友挨个房间进行查询谁是同学甲,你等的朋友来了,于是在实际的代码中,select版大妈做的是以下的事情:

1
2
3
4
5
6
7
8
9
10
int n = select(&readset,NULL,NULL,100);
 
for (int i = 0; n > 0; ++i)
{
    if (FD_ISSET(fdarray[i], &readset))
    {
        do_something(fdarray[i]);
        --n;
    }
}

epoll版大妈就比较先进了,她记下了同学甲的信息,比如说他的房间号,那么等同学甲的朋友到来时,只需要告诉该朋友同学甲在哪个房间即可,不用自己亲自带着人满大楼的找人了.于是epoll版大妈做的事情可以用如下的代码表示:

1
2
3
4
5
6
n=epoll_wait(epfd,events,20,500);
 
for(i=0;i<n;++i)
{
    do_something(events[n]);
}

在epoll中,关键的数据结构epoll_event定义如下:

1
2
3
4
5
6
7
8
9
10
typedef union epoll_data {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;
struct epoll_event {
    __uint32_t events;     /* Epoll events */
    epoll_data_t data;     /* User data variable */
};

可以看到,epoll_data是一个union结构体,它就是epoll版大妈用于保存同学信息的结构体,它可以保存很多类型的信息:fd,指针,等等.有了这个结构体,epoll大妈可以不用吹灰之力就可以定位到同学甲.别小看了这些效率的提高,在一个大规模并发的服务器中,轮询IO是最耗时间的操作之一.再回到那个例子中,如果每到来一个朋友楼管大妈都要全楼的查询同学,那么处理的效率必然就低下了,过不久楼底就有不少的人了.
对比最早给出的阻塞IO的处理模型, 可以看到采用了多路复用IO之后, 程序可以自由的进行自己除了IO操作之外的工作, 只有到IO状态发生变化的时候由多路复用IO进行通知, 然后再采取相应的操作, 而不用一直阻塞等待IO状态发生变化了.

从上面的分析也可以看出,epoll比select的提高实际上是一个用空间换时间思想的具体应用.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!