IO操作概念
在Unix系统中,一切都是文件。文件就是流的概念,在进行信息的交流过程中,对这些流进行数据的收发操作就是IO操作
我们都知道unix(like)世界里,一切皆文件,而文件是什么呢?文件就是一串二进制流而已,不管socket,还是FIFO、管道、终端,对我们来说,一切都是文件,一切都是流。在信息 交换的过程中,我们都是对这些流进行数据的收发操作,简称为I/O操作(input and output),往流中读出数据,系统调用read,写入数据,系统调用write。不过话说回来了 ,计算机里有这么多的流,我怎么知道要操作哪个流呢?对,就是文件描述符,即通常所说的fd,一个fd就是一个整数,所以,对这个整数的操作,就是对这个文件(流)的操作。我们创建一个socket,通过系统调用会返回一个文件描述符,那么剩下对socket的操作就会转化为对这个描述符的操作。不能不说这又是一种分层和抽象的思想。
同步、异步(IO模型中的概念,并非并发模式中的同步、异步)
对于一个套接字上的输入操作,第一步就是通常涉及等待数据从网络中到达,当所等待的分组到达的时候,它就被复制到内核中的某个缓冲区中。第二步就是把数据从内核的缓冲区拷贝到应用进程的缓冲区中。
所谓的同步在这里指的是,等待数据到达或者用轮询的方式去查看数据是否到达,数据到达之后,然后直到IO操作完成后用户进程才不阻塞,也就是说同步模型中,其中真正的IO操作也就是第二步骤会将进程阻塞。
所谓的异步在这里指的是不导致请求进程阻塞的一种操作,一般工作模式就是由用户告知内核一个动作,然后让内核在整个操作完成后再通过用户指定的告知方式告知用户。比如用户在该IO操作过程中,告诉内核用户指定的缓冲区位置,以及IO操作完成之后通知程序的方式。所以在IO操作的期间,用户进程是不用管IO操作的,完全可以进行其他操作,直到内核操作完后接收结束通知就行。
同步概念: 执行一个操作后,进程触发IO操作(其中要么就是等待数据的到达,也就是阻塞模式;要么通过轮询去查看数据是否到达也就是非阻塞忙轮询模式的)接着数据到达后,便阻塞用户进程一直到IO操作完成。即第二步骤是的阻塞。
异步概念: 执行一个操作后,触发IO操作后不会导致请求进程阻塞。也就是说数据从内核到用户缓冲区的整个过程都是交给内核去完成的,用户进程无需阻塞一直等到IO操作完成,它只要执行一个操作触发IO操作后就可以继续执行其他操作,直到IO操作结束后,等到被通知就可以了。所以从根本来说异步从等待数据到把数据从内核空间拷贝到用户空间的过程中没有阻塞,只有发起该操作,和被通知该操作完成。所以异步是真正的没有阻塞在IO操作上的。
两者之间的区别: 我的理解是这样的:是否有CPU深度的参与、是否将所有IO操作交给了内核。这里的同步和异步是针对用户和内核的交互性来看的。同步IO操作的模型里面,比如阻塞式IO、非阻塞式IO、IO复用、信号驱动式IO中,它们的第一步骤是不相同的,但是第二步都阻塞在了将数据从内核拷贝到用户缓冲区的IO操作上,而真正的异步IO的过程只有发起和用户进程被等待通知,中间是没有任何阻塞的,它的IO操作需要CPU深度参与。所以按照严格定义的阻塞来看,异步是不存在阻塞的,而同步在真正的IO操作中将阻塞进程,只有异步IO模型与POSIX定义的异步IO匹配。所以区别二者就是在于用户进程是否将所有IO操作交给CPU去完成。若交给CPU之后就可以什么都不用管,只需要等待通知就可以了,那么就是异步;如果需要等待IO操作的完成,没有将IO操作的权利全都托付给CPU的为同步。
- CPU是否深度参与,也就是IO操作是否全都由内核来操作,也就是第二步是否阻塞,阻塞为同步,不阻塞为第二步。
- 严格意义上来看,异步是非阻塞的。所以同步才分阻塞和非阻塞,异步是不存在阻塞的。
通过以上可以将Unix的IO宏观上大体分为同步和异步,然后再从同步中分出阻塞和非阻塞。
阻塞、非阻塞
在网络编程中,socket在创建的时候默认是阻塞的。但是我们可以通过fcntl系统调用去设置为非阻塞。阻塞和非阻塞的概念应用于所有文件描述符。阻塞和非阻塞会导致一些系统调用出现无法立即完成而被操作系统挂起,直到等到有事件发生(如数据可读,缓冲区可写)为止。socket基础API中,可能被阻塞的系统调用包括accept,send,recv,read,connect等。所以在编程中,要理解并且处理好阻塞和非阻塞的设置。
网上大众的例子就是快递例子,那我也写一个我自己理解的例子——假如你今天制定好了一系列并且有时间顺序的计划,并且没有外界可以打乱你的计划,其中就有一项就是拿到快递,而当你之前的事情都按部就班地做了,就等拿快递的时候,寄存快递的储物箱坏了,但是维修人员还没有到。那么此时你后面还有一堆什么洗衣服的啥计划,第一种选择就类似阻塞,大概就是你就站在储物箱那里等到维修人员来修理好并且打开储物箱给你取出快递了你才去洗衣服做后面的事情;第二种选择类似非阻塞,你现在为了不耽搁时间先回去洗衣服,等衣服洗好的时候,再回去看看存快递的储物箱修理好了没有,好了的话你就可以取出你的快递再继续做洗衣服后面的事情。没有好的话你再继续做洗衣服后面的另一件事情,然后做完后再去看看快递储物箱可以打开了不,一直以这样的模式轮询,直到放快递的储物箱可以打开了,可以准备取件为止。以下放上定义概念。
阻塞: 无数据准备好,系统调用比如read,recvfrom就会挂起,等到有数据准备好或者有数据了才继续执行系统调用,最后才从系统调用的函数中返回。
非阻塞: 这里的非阻塞是通过忙轮询去检测是否有数据准备好,没有数据准备好就一直轮询,直到有数据准备好了可以开始进行数据的复制了为止。
注意: 这里的阻塞和非阻塞是从第一个步骤来看的,而同步和异步的阻塞是从真正的IO操作来也就是第二步来看的。异步是真正意义上的非阻塞,所以异步不分阻塞和非阻塞,只有同步才分阻塞和非阻塞,同步中的阻塞和非阻塞是从第一个步骤来区分的。并非第二步骤,因为第二步骤中同步阻塞和非同步阻塞都将在真正的IO操作上被阻塞。
同步IO模型
- 阻塞IO模型
- 非阻塞IO模型
- 信号驱动式IO
一、阻塞IO模型
所有套接字默认的都是阻塞的,以recvfrom系统调用为例子,它要等到有数据报到达且被复制到应用进程的缓冲区中或者发生了错误才返回。若没有数据到达那么将一直会阻塞。
二、非阻塞IO模型
进程将一个套接字设置为非阻塞就是通知内核:当前所请求的IO操作在请求的过程不需要把进程投入睡眠,而是返回一个错误。(注意这里是指请求IO操作,不是进行IO操作)
当一个应用进程循环调用recvfrom的时候,这种操作叫做轮询。应用进程轮询内核,检查某个操作是否准备就绪,当IO操作准备就绪可以操作的时候就会进行真正的IO操作,就是将数据从内核写入用户空间的过程。但是这样做会导致CPU的大量耗费。
三、IO复用模型
我们可以通过系统调用select、poll、epoll、kqueue实现IO复用模型。此时进程就会组赛在这些系统调用上,而不是阻塞在真正的IO操作上,直到有就绪事件了,这些系统调用就会返回哪些套接字可读写,然后就可以进行把数据包复制到应用进程缓冲区了。IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。
从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
其中select是通过不断的轮询,查看是否有就绪事件。如果有的话,再把所有的流遍历一遍看是哪个流准备就绪。而poll也是采用这样的轮询,只不过poll采用的是链表存储,所以没有最大连接数的限制,epoll是even poll,和忙轮询、无差别轮询不一样,它会把哪个流发生了怎样的I/O事件通知我们,不用全都遍历一遍才知道是哪个流发生了。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1)),而select和poll查找复杂度都是O(n)。
四、信号驱动式IO模型
我们也可以用信号让内核在文件描述符准备就绪的时候通知用户进程,即是告知我们什么时候可以启动IO操作。就如数据准备好了,内核就会以一种形式通知用户进程。
这种模型的优势就在于数据到达之前不被阻塞,主循环可以继续执行,用户进程只需要等到着来自信号的处理函数的通知即可,其中既可以是数据已准备好被处理,也可以是数据报已准备好被读取。
异步IO模型
异步IO由POSIX规范定义的。一般地说,这些函数的工作机制就是:由用户进程告知内核启动一个操作,并且由内核去操作,操作完后给用户进程发一个通知,通知用户进程操作完了(包括数据从内核缓冲区拷贝到用户缓冲区的过程)。该模型与信号驱动式IO模型不同的就是,异步IO模型中,是由内核通知IO操作什么时候完成,而信号驱动式IO是由内核告知何时启动IO操作。
读写(read write)与阻塞和非阻塞
阻塞的read: 在我们一般用read中,如果内核的接收缓冲区没有数据到达,那么将会一直阻塞。所以read函数如果在没有数据的时候,是被挂起不返回的,如果有数据了那么就是可以读多少就读多少。
阻塞的write: write如果是在socket阻塞的情况下就是用户进程有多少数据就要将所有数据都写入内核的可写缓冲区中才返回,这时候就是多路复用中为什么要将socket设置为非阻塞的原因。如果是阻塞的,那么写阻塞的时候,此时内核可写缓冲区可以容纳N个字节,而需要发送的数据有N+1个字节的话,那么write是不会返回的,它会一直阻塞直到那多出来的一个字节装到内核缓冲区了才会返回。所以在select中,返回可写条件的时候,要限制将套接字设置为非阻塞,才可以说一次性写操作返回一个正值。
非阻塞的read: 如果没有数据的话,那么read调用不会挂起,就会立即返回。如果有数据的话就是可以读多少就读多少
非阻塞的write: 内核缓冲区够写多少就写多少,能够写多少要根据网路拥塞情况为标准,当拥塞严重的时候,没有足够的缓冲区去写的话,就会出现写不完的情况。