同步,异步,阻塞,非阻塞,io多路复用总结。

前段时间微信公众号看了这方面的文章,本来一次没啥东西,没想到越看到后面涉及到的知识越多,还有操作系统底层的东西,做个笔记,免得忘记。

基础知识准备:

1.文件描述符

  以linux为例,我们知道,linux当中所有的东西都可以当成一个文件被打开,而文件描述符是内核为了高效管理系统中被打开文件所创建的一个索引值,表现形式为一个非负整数(通常是一个小整数)。所有的I/O操作的系统调用都是通过文件描述符的。

  每一个进程都有维护一份文件描述符表,当进程打开一个文件的时候,内核会把创建了的文件描述符返回给对应进程,对应进程拿到这个文件描述符之后会加入到自己维护的文件描述符表当中。而系统会为所有打开的文件描述符维护有一个系统级别的文件描述符表。

  这边需要注意的是,不同的文件可以被不同的进程打开,也可以被同一进程打开多次,所以,各个维护表中的文件描述符可能指向的同一个文件。

  虽然说,文件理论上可以被打开无限个,但是系统其实做了文件打开限制,一般的系统级别的文件打开数量限制是系统内存的百分之十,而每个进程也会有限制。linux上面现在的一般是65535个。

  

资料:https://blog.csdn.net/cywosp/article/details/38965239

2.内核态和用户态

  首先看上图,内核是一切操作系统的核心,内核本质上是一套与硬件设备交互的软件,比如键盘,鼠标,打印机等等。基于此,内核提供了一套系统函数供上层应用进行调用,比如cpu处理,i/o处理等等,而上层应用进程调用这些函数的过程叫做系统调用。而公用函数库则是进一步对系统函数进行一次封装,函数库中一个方法可能包含多个系统函数调用。

  既然内核是如此的重要,那么他的地位必然是比较超然的,连带着对于内核的一些操作都是具有“特权“”的,Intel的X86架构的cpu提供了四种等级特权划分,0的特权最大,3的特权最小。而0对应了内核态,而3对应了用户态。特权越高,可以调用系统的资源权限就越大,越多,反之则越小,越少。linux只用到了0和3两个特权等级。

  通常,内存地址分为用户地址空间和内核地址空间,以4GB内存来说,前0~3GB为用户地址空间,为所有进程共享,后3~4GB为内核地址空间,部分内核地址对进程共享,当系统创建一个进程的时候,系统会分别在用户地址空间和内存地址空间为进程创建一个进程栈,进程可以再这两个栈当中分别运动,当进程在内核空间地址运行的时候我们就说当前进程处于内核态。

  一开始所有的用户进程都运行子用户态,使用用户空间地址当中的用户栈,后面进程进行了系统调用,那么此时该进程就进入了内核态,他的权限就是0,并且他会使用内核地址空间中自己的内核栈。

  说了这么多,其实是为了说明进程在用户态和内核态之间是会进行切换的,而且因为权限的不同,内核会做更多的事情,因此这个状态的切换消耗是比较大的。

  为了优化这个情况,在用户态和内核态当中分别有一个数据缓冲区,读取数据首先对这个缓冲区进行操作,以此来减少内核态和用户态的切换频率。那么我们的标准I/O操作自然的就分为了两个步骤。

资料:

https://www.cnblogs.com/yuyang0920/p/7278446.html

https://www.cnblogs.com/dormant/p/5456491.html

https://blog.csdn.net/qq_39823627/article/details/78736650

缓存IO(也叫标准IO)

  衔接上文,标准的I/O操作分为两步,以读取网络数据为例,第一步进程进行系统调用会先询问内核是否读取完成网络包,内核读取网络数据包放到内核缓冲区当中,将读取完毕的一个网络数据包copy到用户空间缓冲区,然后由用户进程进行读取。两个阶段分别为:

1. 等待数据准备 (Waiting for the data to be ready)

2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

缓冲区资料:http://www.360doc.com/content/18/0512/20/36367108_753420333.shtml

 个人认为I/O中是否阻塞的关键就在于在内核缓冲区数据还未准备好的情况下,用户进程是否一直处于等待状态

同步与异步?

  首先同步和异步是对于被调用方来说的,比如说你陪你女朋友去逛商场这个方法,然后看到某家美甲店,她要进去作美甲,这时候你女朋友有两个选择

  1.做完美甲在美甲店等你来接她。

  2.做完美甲打电话给你让你来接她。

   这期间你自己可以选择是在店里等他还是选择去哪里先逛,你女朋友就不管你了,爱干啥干啥去。对于她自己来说,她选择做做完美甲在店里等你,那就是一个同步操作,她的时间和状态停留在了做美甲的前一刻,接下来要和你同步继续逛商场。

   选择做完打电话给你就是一个异步操作。

阻塞与非阻塞?

   还是上面那个例子,阻塞与非阻塞是对于调用方来说的,也就是你自己的选择。女朋友要做美甲,你也有连个选择

  1.在店里等他

  2.闲得无聊就先去商场晃晃呗。

  你在店里等他就是阻塞住了,啥事也干不了,就等她做美甲了,如果先去商场逛,那就美滋滋了,那就是非阻塞的了。

基于同步,异步,阻塞和非阻塞,还有标准IO的两阶段读取,IO发展出了这么几种模型。

  -- 阻塞 I/O(blocking IO)
  -- 非阻塞 I/O(nonblocking IO)
  -- I/O 多路复用( IO multiplexing)
  -- 信号驱动 I/O( signal driven IO)
  -- 异步 I/O(asynchronous IO)

同步阻塞I/O

  根据上面的结论,内核尚未读取完成数据,内核缓冲区数据一直没有准备好,进程处于等待阻塞状态,分为两步:

 1.进程发起read,进行recvfrom系统调用。

 2.内核开始第一阶段,准备数据,等待网络数据包写入到内核缓冲区。

 3.在第二步的同时,调用者也就是我们的用户进程阻塞。等待内核将数据写入用户缓冲区。

 4.直接内核将数据写入用户缓冲区,内核返回结果,进程等待解除。

也就是说内核准备数据数据从内核拷贝到用户空间这两个过程都是阻塞的。

所以,同步阻塞I/O的特点是两次读取阶段中,进程都被阻塞了。

同步非阻塞I/O

 1.当用户进程发出read操作时,如果kernel中的数据还没有准备好;

 2.那么它并不会block用户进程,而是立刻返回一个error,从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果;

 3.用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call;

 4.那么它马上就将数据拷贝到了用户内存,然后返回。

所以,nonblocking IO的特点是用户进程内核准备数据的阶段需要不断的主动询问数据好了没有

异步非阻塞I/O

 当进程调用aio_read函数后,进程就立刻返回做其他的事情去了,并且会让内核将数据包copy到用户缓冲区之后,向进程发起实现指定好的函数通知进程。

https://www.cnblogs.com/euphie/p/6376508.html

https://www.cnblogs.com/zingp/p/6863170.html

I/O多路复用

上面所说的io模型基本上都是多线程,所以我们经常说一个socket对应一个线程,但是当多线程之后,本身多线程编程就是比较复杂的事情,而多路复用则是在单线程当中,监听多个socket,跟踪每个socket状态,来管理多个IO流。因此,多路复用的好处是在于可以提高服务的吞吐量,而非处理速度。

在同一个线程里面, 通过拨开关的方式,来同时传输多个I/O流,系统内核都有提供select,poll,epoll函数对多路复用的实现。

select函数

用户线程调用select函数,将IO操作的socket添加到select函数中,然后线程阻塞住select函数返回。当内核读取完网络数据之后,将socket通过select函数返回,用户线程正式发起read请求。流程上和同步阻塞IO区别不大, 甚至还多了调用select函数的步骤,按理说效率更差一些,但是因为内核中是同一线程来处理多个IO的,因此提高了吞吐量。

伪代码如下

{

    select(socket);

    while(1) {

      sockets = select();

      for(socket in sockets) {

        if(can_read(socket)) {

          read(socket, buffer);

          process(buffer);

        }

      }

    }

}

然而,select函数返回的是所有的socket,你需要采用轮询的方式去找到自己的的socket,这个也太麻烦了,效率实在是低。而且select函数监听的socket数量是有限的,一般是1024个,当出现高并发的情况下,系统依旧会崩。

因此,后来出现了poll函数,对于select函数的一些问题,进行了修复,比如poll函数取消了socket监听数量的限制,但是poll函数依旧需要对于所有的socket进行轮询。效率依旧还是比较低下。

直到后来epoll的出现。

epoll

epoll最大的改动就是在poll的基础之上,采用了事件回调函数来通知用户线程,而不用再对所有的socket进行轮询处理

epoll使用了Reactor模式来实现这一机制。

用户线程向Reactor注册事件处理,然后用户线程去作其他的事情(异步),而Reactor线程不断调用select,所以,这边Reactor本身是阻塞的状态,内核socket可读,select函数返回,然后轮询注册事件,找出对应的用户线程注册的回调函数通知用户线程,用户线程正式发起请求读取数据。在这个过程中,用户线程读取数据本身是阻塞的。

http://www.cnblogs.com/fanzhidongyzby/p/4098546.html

https://www.cnblogs.com/jeakeven/p/5435916.html

https://www.zhihu.com/question/32163005

猜你喜欢

转载自blog.csdn.net/helianus/article/details/86534664
今日推荐