## Linux面试问题汇总
Linux的I/O模型介绍以及同步异步阻塞非阻塞的区别(超级重要)
一般来说,Linux下系统IO主要就是通过以下几个函数open(),close(),read(),write(),send(),recv(),lseek(),今天就以recv()为例来介绍下IO模型中的同步异步,阻塞非阻塞的区别。
同步、异步
同步:用户进程发起IO后,进行就绪判断,轮询内核状态。
异步:用户进程发起IO后,可以做其他事情,等待内核通知。
阻塞:用户进程访问数据时,如果未完成IO,调用的进程一直处于等待状态,直到IO操作完成。
非阻塞:用户进程访问数据时,会马上返回一个状态值,无论是否完成,此时进程可以操作其他事情。
Linux下的五种IO模型
阻塞I/O(blocking I/O)
非阻塞I/O(nonblocking I/O)
I/O复用(select和poll) (I/O multiplexing)
信号驱动I/O(signal driven I/O (SIGIO))
异步I/O (asynchronous I/O (the POSIX aio_functions))
Tip:前四种都是同步,只有最后一种才是异步I/O。
I/O发生时涉及的对象和阶段
Linux为了OS的安全性等的考虑,进程是无法直接操作I/O设备的,其必须通过系统调用请求内核来协助完成I/O动作,而内核会为每个I/O设备维护一个buffer。
对于一个network I/O (这里我们以read举例),它会涉及到两个系统对象,一个是调用这个I/O的process (or thread),另一个就是系统内核(kernel)。当一个read操作发生时,它会经历两个阶段:
- 用户进程发起请求,内核接收到请求,从I/O设备中获取数据到buffer,等待数据准备 (Waiting for the data to be ready)
- 将buffer中的数据copy到用户进程的地址空间,即将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
记住这两点很重要,因为这些I/O Model的区别就是在两个阶段上各有不同的情况。
先说阻塞与非阻塞的区别
recv()函数默认是阻塞的,什么是阻塞呢?就是当你调用recv()函数时,整个进程或者线程就等待在这里了,直到你recv的fd的所有信息都被send过来,这么做好处就是保证所有信息都能够完整的读取了,但劣势也很明显,就是在recv()的过程中你的进程或线程做不了其它事情,由此,引入了非阻塞IO。
非阻塞IO是什么呢,还是以recv()函数为例,当你将其设置为非阻塞时,每次当你recv()时,就直接返回,不管信息有没有完全send进来,好处很明显,recv()了之后进程马上能处理下一行代码,坏处也很明显,就是你不知道你的消息是否读完了,这种问题就是TCP中大名鼎鼎的半包问题(解决办法主要是通过一个buffer缓存所有读进来的消息)
从上图可以看到在整个过程中,当用户进程进行系统调用时,内核就开始了I/O的第一个阶段,准备数据到缓冲区中,当数据都准备完成后,则将数据从内核缓冲区中拷贝到用户进程的内存中,这时用户进程才解除block的状态重新运行。
所以,Blocking I/O的特点就是在I/O执行的两个阶段都被block了。
从上图可以看到在I/O执行的两个阶段中,用户进程只有在第二个阶段被阻塞了,而第一个阶段没有阻塞,但是在第一个阶段中,用户进程需要盲等,不停的去轮询内核,看数据是否准备好了,因此该模型是比较消耗CPU的。
同步与异步的区别
在POSIX定义中把同步IO操作定义为导致进程阻塞直到IO完成的操作,反之则是异步IO,看概念感觉异步跟非阻塞好像也没有什么区别,要好好理解同步和异步,就要详细说明下IO过程:
IO过程主要分两个阶段:
1.数据准备阶段
2.内核空间复制回用户进程缓冲区空间
无论阻塞式IO还是非阻塞式IO,都是同步IO模型,区别就在与第一步是否完成后才返回,但第二步都需要当前进程去完成,异步IO呢,就是从第一步开始就返回,直到第二步完成后才会返回一个消息,也就是说,非阻塞能够让你在第一步时去做其它的事情,而真正的异步IO能让你第二步的过程也能去做其它事情。
这里就在说一下select,poll和epoll这几个IO复用方式,这时你就会了解它们为什么是同步IO了,以epoll为例,在epoll开发的服务器模型中,epoll_wait()这个函数会阻塞等待就绪的fd,将就绪的fd拷贝到epoll_events集合这个过程中也不能做其它事(虽然这段时间很短,所以epoll配合非阻塞IO是很高效也是很普遍的服务器开发模式--同步非阻塞IO模型)。有人把epoll这种方式叫做同步非阻塞(NIO),因为用户线程需要不停地轮询,自己读取数据,看上去好像只有一个线程在做事情,也有人把这种方式叫做异步非阻塞(AIO),因为毕竟是内核线程负责扫描fd列表,并填充事件链表的,个人认为真正理想的异步非阻塞,应该是内核线程填充事件链表后,主动通知用户线程,或者调用应用程序事先注册的回调函数来处理数据,如果还需要用户线程不停的轮询来获取事件信息,就不是太完美了,所以也有不少人认为epoll是伪AIO,还是有道理的。
I/O复用模型(I/O multiplexing)
I/O multiplexing这个词可能有点陌生,但是如果我说select,poll、epoll,大概就都能明白了。有些地方也称这种I/O方式为event driven I/O,也是实际中使用最多的一种I/O模型。我们都知道,select/epoll的好处就在于单个process就可以同时处理多个网络连接的I/O。它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。它的流程如图:
从上图可以看到在I/O复用模型中,I/O执行的两个阶段用户进程都是阻塞的,但是两个阶段是独立的,在一次完整的I/O操作中,该用户进程是发起了两次系统调用。
I/O multiplexing这个词可能有点陌生,但是如果我说select,poll、epoll,大概就都能明白了。有些地方也称这种I/O方式为event driven I/O,也是实际中使用最多的一种I/O模型。我们都知道,select/epoll的好处就在于单个process就可以同时处理多个网络连接的I/O。它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
信号驱动I/O模型(Signal-driven I/O)
首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。(signal driven I/O在实际中并不常用)
该模型也叫作基于事件驱动的I/O模型,可以看到该模型中,只有在I/O执行的第二阶段阻塞了用户进程,而在第一阶段是没有阻塞的。
乍看起来感觉和非阻塞模型很相似,其实不同之处就在于,该模型在I/O执行的第一阶段,当数据准备完成之后,会主动的通知用户进程数据已经准备完成,即对用户进程做一个回调。该通知分为两种,一为水平触发,即如果用户进程不响应则会一直发送通知,二为边缘触发,即只通知一次。