复制内存时检测到可能的io争用条件_24、【yummyfood商城集群版本】Nginx扩展深入研究IO模型...

上一篇文章说到Nginx实际上使用了多路复用IO模型,实际上,多路复用模型是IO模型的一种,而epoll是其具体的一种实现方式。那么本篇文章将深入了解几种常见的IO模型,七千多字长文,耗费我不少时间理解和整理,前方高能。

bbc482ca2f3024020dd484525202272a.png

一、I/O 读写原理

首先,I/O是什么意思?我也有同样的困惑,维基百科上如是说:

70b820f065fbd5339b3ef0604547e101.png

似懂非懂,又找到一个简单解释:

I/O一般指进程通过网络或存储介质读取或写入数据。

那么,也就是说,从磁盘中读取文件、读取网络数据等,都是IO事件。

用户程序进行IO的读写,基本上会用到read&write两大系统调用。可能不同操作系统,名称不完全一样,但是功能是一样的。

read系统调用,是把数据从内核缓冲区复制到进程缓冲区;而write系统调用(什么是系统调用下面会解释),是把数据从进程缓冲区复制到内核缓冲区。这个两个系统调用,都不负责数据在内核缓冲区和磁盘之间的交换。这句话如何理解呢?

首先什么是进程缓冲区和内核缓冲区?

在linux系统中,系统内核有个缓冲区叫做内核缓冲区。每个进程也有自己独立的缓冲区,叫做进程缓冲区。

缓冲区的目的,是为了减少频繁的系统IO调用。大家都知道,系统调用需要保存之前的进程数据和状态等信息,而结束调用之后回来还需要恢复之前的信息,为了减少这种损耗时间、也损耗性能的系统调用,于是出现了缓冲区。

有了缓冲区,操作系统使用read函数把数据从内核缓冲区复制到进程缓冲区,write把数据从进程缓冲区复制到内核缓冲区中。

拿读磁盘文件为例,操作系统将文件读入内核缓冲区,然后从内核缓冲区拷贝到进程缓冲区,我们的应用进程再去处理处理此文件数据,而我们这里的read调用只涉及从内核缓冲区拷贝到进程缓冲区的阶段,至于跟磁盘的交互它是不关心的。其次,进程如何知道什么时候数据已经准备好?数据什么时候已经从内核缓冲区复制到了用户缓冲区?这些按照不同的IO模型会有不同的策略,将在下面IO模型中具体展开说明。

二、用户态和内核态

你一定听说过内核态和用户态(kernel mode和user mode),在内核态可以访问系统资源,比如:

  • 处理器cpu:cpu控制着一个程序的执行。

  • 输入输出IO:linux有句话叫“一切都是流”,也就是所有输入输出设备的数据,包括硬盘,内存,终端都可以像流一样操作。

  • 进程管理:类似对进程的创建,休眠,唤醒,释放之类的调度。比如linux下的fork和windows下的CreateProcess()函数。

  • 内存:包括内存的申请,释放等管理操作。

  • 设备:这个就是常常说的外设了,比如鼠标,键盘。

  • 计时器:计算机能计时是因为晶体振荡器产生的电磁脉冲。那么所有的定时任务都是以它为基础的。

  • 进程间通信IPC:进程之间是不能够互相访问内存的,所以进程与进程之间的交互需要通信,而通信也是一种资源。

  • 网络通信:网络通信可以看做是进程间通信的特殊形式。

内核态到底是什么呢?其实从本质上说就是我们所说的内核,它是一种特殊的软件程序,特殊在哪儿呢?控制计算机的硬件资源,例如协调CPU资源,分配内存资源,并且提供稳定的环境供应用程序运行。

用户态就是提供应用程序运行的空间,为了使应用程序访问到内核管理的资源例如CPU,内存,I/O。内核必须提供一组通用的访问接口,这些接口就叫系统调用。如下图所示:

5539e20d01b38e8663d087e4a9621945.png

为什么要区分用户态和内核态?一方面是安全因素考虑,毕竟有些危险操作是不能让用户进程随意调用的,因此必须做出隔离,这很好想到;另外一方面因为操作系统的资源是有限的,如果访问资源的操作过多,必然会消耗过多的资源,而且如果不对这些操作加以区分,很可能造成资源访问的冲突。所以,为了减少有限资源的访问和使用冲突,Unix/Linux的设计哲学之一就是:对不同的操作赋予不同的执行等级,就是所谓特权的概念。简单说就是有多大能力做多大的事,与系统相关的一些特别关键的操作必须由最高特权的程序来完成。

很多程序开始时运行于用户态,但在执行的过程中,一些操作需要在内核权限下才能执行,这就涉及到一个从用户态切换到内核态的过程。比如C函数库中的内存分配函数malloc(),它具体是使用sbrk()系统调用来分配内存,当malloc调用sbrk()的时候就涉及一次从用户态到内核态的切换。当用户态进程想调用系统资源,就得主动请求切换到内核态。而这对应一些特殊的堆栈和内存环境,必须在系统调用前建立好。而在系统调用结束后,cpu会从内核模式切回到用户模式,而堆栈又必须恢复成用户进程的上下文,这种切换会带来大量资源消耗。

ad50bc7b271ac1e5992a3d582c4459fa.png

用户缓冲区的目的就是是为了减少系统调用次数,从而降低操作系统在用户态与内核态频繁切换所耗费的资源和时间。

三、java IO读写的底层流程实例

3c13758ee34ffbdf85471c3c55492352.png

首先看看一个典型Java 服务端处理网络请求的典型过程:

(1)客户端请求

Linux通过网卡,读取客户端的请求数据,将数据读取到内核缓冲区。

(2)获取请求数据

服务器从内核缓冲区读取数据到Java进程缓冲区。

(1)服务器端业务处理

Java服务端在自己的用户空间中,处理客户端的请求。

(2)服务器端返回数据

Java服务端已构建好的响应,从用户缓冲区写入系统缓冲区。

(3)发送给客户端

Linux内核通过网络 I/O ,将内核缓冲区中的数据,写入网卡,网卡通过底层的通讯协议,会将数据发送给目标客户端。

四、对同步与异步,阻塞与非阻塞的理解

用一个求婚的例子来说明。

93836eee8bf5b3fb0db59ddb75bb095c.png

先来说说同步与异步。当男孩向女孩求婚时,女孩有两种反应:

1、女孩陷入了沉思,思考很久,不过还是当场给了男孩回复。

2、女孩立马说:你先回去吧,等我想好了给你发微信。

第一种就是同步的方式,女孩收到求婚这个请求后,开始处理请求,因为求婚的事情比较大,女孩需要纠结一会,所以等了一会才给男孩最终的回复,期间女孩其他事情也没干,一直在认真考虑这件事。

第二种就是异步的方式,女孩收到求婚这个请求后,只给先给男孩一个收到的回复,等想好了,后面再通知男孩最终结果。

上面的主角是女孩,而阻塞/非阻塞的主角是男孩。男孩也会有不一样的反应。

1、男孩发出求婚后,其他啥都不做,只想着女孩的回复,这个就是阻塞。

2、男孩发出求婚后,不管女孩啥时候给反应,男孩还会去做其他事情,而不是一直死死等待,这个就是非阻塞。

所以从上面的例子就知道了,同步/异步,阻塞/非阻塞实际上是两种不一样的概念,切不可混为一谈,那么这两对名词也可以进行排列组合出四种方式了。

并且阻塞/非阻塞主要是针对调用方的调用状态,看调用方是一直死等还是做其他事情去了。同步/异步实际上描述了一种通信机制:收到一个请求后,等处理完毕再回调给调用方;还是先告诉对方收到然后慢慢等结果出来再通知调用方。

五、IO模型

网络IO的本质是socket的读取,socket在linux系统被抽象为流,IO可以理解为对流的操作。

对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:

第一阶段:等待数据准备 (Waiting for the data to be ready)。

第二阶段:将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。

对于socket流而言,

第一步:通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区。

第二步:把数据从内核缓冲区复制到应用进程缓冲区。

我们看到,这是一个比较麻烦的过程,可能是性能出现瓶颈的地方。那么根据不同阶段的处理机制不同,Unix 下有四种常用的 I/O 模型:

  • 同步阻塞式IO

  • 同步非阻塞式IO

  • 多路复用IO

  • 信号驱动IO

  • 异步IO

切记切记,男孩就是下面图中的application,即用户进程或者说应用进程。女孩就是下面的kernel即内核,即内核态。

5.1 同步阻塞式 I/O

男孩提出求婚,女孩当场陷入沉思,男孩只能死死等待女孩回复。

两个阶段中,应用进程都被阻塞,直到数据复制到应用进程缓冲区中才结束阻塞的状态。

应该注意到,在本进程阻塞的过程中,本进程阻塞不意味着整个操作系统都被阻塞,其它进程还可以正常执行。由于阻塞期间本进程不消耗 CPU 时间,这种模型的执行效率会比较高

52931c7ec5174bfc8341d46a5e665284.png

当用户进程调用了recv()/recvfrom()这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。

第二个阶段:当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。

优点是简单,缺点是调用方就得一直等待做不了其他事情了。

5.2 同步非阻塞式 I/O

男孩提出求婚,女孩当场陷入沉思,男孩趁着女孩沉思的时间,刷刷抖音看看电视剧,男孩需要时不时观察女孩有没有思考完毕,毕竟担心女孩思考完了他还在刷抖音就尴尬了。

同步非阻塞就是 “每隔一会儿瞄一眼进度条” 的轮询(polling)方式。每次recvform系统调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。

优点是能够在等待任务完成的时间里干其他活了,但是由于 CPU 要处理更多的系统调用,因此这种模型是比较低效的。

1b0e29d5f08dafb5884be86b137cc119.png

5.3 I/O 多路复用

跟同步非阻塞有点像,只不过当你的女朋友考虑好了,就会有个人提醒你不要刷抖音啦。。。太好了太好了,男孩可以放心大胆玩了,甚至还能有空闲同时等待其他几个女孩的结果。。。

有些地方也称这种IO方式为event driven IO。

由于同步非阻塞方式需要不断主动轮询某个socket是否可读或可写(即准备好),轮询占据了很大一部分过程,轮询会消耗大量的CPU时间,而后台可能有多个任务在同时进行,人们就想到了循环查询多个任务的完成状态,只要有任何一个任务完成,就去处理它。如果轮询不是进程的用户态,而是有人帮忙就好了。那么这就是所谓的 “IO 多路复用”。UNIX/Linux 下的 select、poll、epoll 就是干这个的(epoll 比 poll、select 效率高,做的事情是一样的)。

如何做的呢?

我们先来看看同步阻塞的服务器是如何做的,来一个用户请求连接,我们的服务器就会开启一个线程来处理连接。当用户请求佷多时,为了减少阻塞,可能会开启大量的线程来提高处理效率,不过由于线程间的切换也会耗费系统资源,因此效率得不到提升。

而多路复用IO,顾名思义,就是一个单独的进程或线程,可以监视多个连接,通过上篇文章我们知道,实际上就是监听多个文件描述符。这个单独监听的进程或线程就是上面说的帮助我们轮询的帮手。当有某些文件描述符数据已经准备好了,这个时候帮手会帮我们轮询找到这个准备好的socket,继而通知用户进程进行第二步的操作。这就是select或者poll干的事情。

而epoll就更加强大了,我们知道了select和poll是轮询文件描述符,当文件描述符非常多的时候,显然效率会降低佷多。epoll就比较聪明了,它把文件描述符注册到内核的一个地方,当数据准备就绪,会通过回调的方式把准备好的文件描述符放到一起,告诉用户进程,用户进程就可以获取到准备好的描述符执行第二阶段调用了。最后还会再详细说明这三者。

总结一下就是:多路复用的特点是通过一种机制一个进程能同时等待IO文件描述符,内核监视这些文件描述符(套接字描述符),其中的任意一个进入读就绪状态,select, poll,epoll函数就可以返回。对于监视的方式,又可以分为 select, poll, epoll三种方式。

46665e7be81a02a02dc88fc11ec4bafc.png

上面的图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以用更少的系统资源同时处理多个connection。

所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。(还是那句话,select/epoll的优势并不是对于单个连接能处理得更快,而是在于能用较小的系统开销同时处理更多的连接。)

IO多路复用的第一阶段是阻塞在select,epoll这样的系统调用之上,跟上面介绍的两种还不一样,注意区别。

5.4 信号驱动 I/O

实际中并不常用。

应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。

相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。

4c3f454ea0f6008b1eba3dbc3cd1b44d.png

5.5 异步I/O

男孩求婚,女孩让男孩回家等消息了,男孩可以尽情打游戏、刷抖音,只要注意接听电话或者查看微信消息即可。

异步IO就是完全自动化,我们可以看到,前面的四种IO,应用进程都在第二步拷贝数据的过程中阻塞了。那么来到异步IO中的时候,我们的应用进程就完全不会有阻塞期。第一步发起请求开始,立马收到内核的回复。内核将等待数据和拷贝数据全部完成后,返回最终结果给应用进程。

这就是真正的异步IO,用户进程只需要发起和等待结果即可,中间没有任何其他操作。因此我们可以理解,真正的异步IO中,用户进程是不存在阻塞的,也就是说不区分阻塞非阻塞,都是非阻塞的。

5d275bd1b7193f871fbe7414c9349fda.png

5.6 五大 I/O 模型比较

前四种 I/O 模型的主要区别在于第一个阶段,而第二个阶段是一样的:将数据从内核复制到应用进程过程中,应用进程会被阻塞。

而异步非阻塞IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。

5976f12e27a46c0a4a8b8013e31e431e.png

同步非阻塞IO和异步IO差别还是挺大的,在同步非阻塞IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而异步IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。

回到Nginx为什么并发性比较高?还有异步IO这么牛逼为什么不用?以下是我的理解:

异步IO本质上还是属于BIO,只是没有阻塞点了,不过该花费的线程资源一样都不少,来一个用户请求,就需要一个线程去处理,线程多了还是有问题。那为什么要用多路复用IO模型呢?Nginx和redis都很像,最核心的工作就是处理网络IO,以Nginx为例,可能会发生一直无法成功建立起连接,即在accept的地方阻塞住,我们知道Nginxworker进程如果被其中一个请求阻塞住的话就没得工作了。那么为了避免IO可能会阻塞在某个客户端请求处理上,采用epoll来让内核去监听socket状态,让nginx不会阻塞在某个用户请求上,从而大大提升了并发。所以,Nginx适合用大量连接的网络IO处理上,而不是擅长连接较少或者是CPU计算型的场景。

六、select/poll/epoll

这三个都是 I/O 多路复用的具体实现,select 出现的最早,之后是 poll,再是 epoll

6.1 select

5b850c4879fa1cb6488ce7fa2f8c194b.png

fd_set 表示描述符集合类型,有三个参数:readsetwriteset 和 exceptset,分别对应读、写、异常条件的描述符集合。

timeout 参数告知内核等待所指定描述符中的任何一个就绪可花多少时间;

成功调用返回结果大于 0;出错返回结果为 -1;超时返回结果为 0。

每次调用 select 都需要将 fd_set \*readfds, fd_set \*writefds, fd_set \*exceptfds 链表内容全部从应用进程缓冲复制到内核缓冲。

返回结果中内核并没有声明 fd_set 中哪些描述符已经准备好,所以如果返回值大于 0 时,应用进程需要遍历所有的 fd_set

select 最多支持 1024 个描述符,其中 1024 由内核的 FD_SETSIZE 决定。如果需要打破该限制可以修改 FD_SETSIZE,然后重新编译内核。

2bf6f5d99c6d1a0d096f705bdcf00b0c.png

6.2 poll

28b16c90f41b4f8717f8f5c9535c80e6.png

cfc058f0f2b51ffb9893a1f0a1b0d15d.png

它和 select 功能基本相同。同样需要每次将描述符从应用进程复制到内核,poll 调用返回后同样需要进行轮询才能知道哪些描述符已经准备好。

poll 取消了 1024 个描述符数量上限,但是数量太大以后不能保证执行效率,因为复制大量内存到内核十分低效,所需时间与描述符数量成正比。

poll 在描述符的重复利用上比 select 的 fd_set 会更好。

如果在多线程下,如果一个线程对某个描述符调用了 poll 系统调用,但是另一个线程关闭了该描述符,会导致 poll 调用结果不确定,该问题同样出现在 select 中。

c42da0081d51d14617ea76a6863576d4.png

6.3 epoll

6eb047c71c8a505dabca4a5e1500adb5.png

epoll 仅仅适用于 Linux OS

它是 select 和 poll 的增强版,更加灵活而且没有描述符数量限制。

它将用户关心的描述符放到内核的一个事件表中,从而只需要在用户空间和内核空间拷贝一次。

select 和 poll 方式中,进程只有在调用一定的方法后,内核才对所有监视的描述符进行扫描。而 epoll 事先通过 epoll_ctl() 来注册描述符,一旦基于某个描述符就绪时,内核会采用类似 callback 的回调机制,迅速激活这个描述符,当进程调用 epoll_wait() 时便得到通知。

epoll_ctl() 执行一次系统调用,用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理。

epoll_wait() 取出在内核中通过链表维护的 I/O 准备好的描述符,将他们从内核复制到应用进程中,不需要像 select/poll 对注册的所有描述符遍历一遍。

epoll 对多线程编程更有友好,同时多个线程对同一个描述符调用了 epoll_wait() 也不会产生像 select/poll 的不确定情况。或者一个线程调用了 epoll_wait 另一个线程关闭了同一个描述符也不会产生不确定情况。

a58790bdcd3b37da243b79b65d95bacb.png

6.4 select 和 poll 比较

功能上,它们提供了几乎相同的功能,select 默认只能监听 1024 个描述符,如果要监听更多的话,需要修改 FD_SETSIZE 之后重新编译。

速度上poll 和 select 在速度上都很慢。

  • 它们都采取轮询的方式来找到 I/O 完成的描述符,如果描述符很多,那么速度就会很慢;

  • select 只使用每个描述符的 3 位,而 poll 通常需要使用 64 位,因此 poll 需要复制更多的内核空间。

可移植性上,几乎所有的系统都支持 select,但是只有比较新的系统支持 poll

七、select poll epoll 应用场景

很容易产生一种错觉认为只要用 epoll 就可以了,select poll 都是历史遗留问题,并没有什么应用场景,其实并不是这样的。

7.1 select 应用场景

select() poll() epoll_wait() 都有一个 timeout参数,在 select() 中 timeout 的精确度为 1ns,而 poll() 和 epoll_wait() 中则为 1ms。所以 select更加适用于实时要求更高的场景,比如核反应堆的控制。

select 历史更加悠久,它的可移植性更好,几乎被所有主流平台所支持。

7.2 poll 应用场景

poll 没有最大描述符数量的限制,如果平台支持应该采用 poll 且对实时性要求并不是十分严格,而不是 select

需要同时监控小于 1000 个描述符。那么也没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。

7.3 epoll 应用场景

程序只需要运行在 Linux 平台上,有非常大量的描述符需要同时轮询,而且这些连接最好是长连接。

八、select poll epoll 形象化对比

举例说明:老师收学生作业,相当于应用层调用I/O操作。

1、老师逐个收学生作业,学生没有做完,只能阻塞等待,收了之后,再去收下一个学生的作业。这显然存在性能问题。

2、怎么解决上面的问题?老师找个班长,班长负责收作业,班长的做法是:遍历问学生作业写好了吗,写好的,收起来交给老师。休息一会,再去遍历。。。这个班长就是select

存在问题

  • 这个班长还有一个能力问题,最多只能管理1024个学生。

  • 很多学生的作业没有写好,而且短时间写不好,班长还是不停地遍历去问,影响效率。

怎么解决问题1班长的能力问题?

  • 换一个能力更强的班长,可以管理更多的学生,这个班长就是poll

怎么解决问题1、2,存在的能力问题和效率问题?

  • 换一个能力超级强的班长,可以管理无限多的学生,同时班长的做法是:遍历一次所有的学生,如果作业没有写完,告诉学生写好之后,放在一个固定的地方。这样的话,班长只需要定期到这个地方取作业就好了。这就是epoll

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值