阻塞IO与非阻塞IO
一、阻塞IO/非阻塞IO 与 进程
进程有三个基本状态,运行态、就绪态和阻塞态(睡眠态)。
所谓阻塞式IO,就是进程等待IO时会进入阻塞态,让出CPU。

对于一个想要进行IO操作的进程来说,可以选择阻塞IO和非阻塞IO两种方式。如果选择阻塞IO,在等待IO时进程就会进入阻塞态,进程也就不会在CPU上继续运行了,可以让出CPU,使CPU可以切换到其他进程运行;如果选择非阻塞IO,就不会一直等待IO,也就不会进入阻塞态,继而可以继续在CPU上运行,这样在等待IO时发起其他IO或处理已经完成的IO。
二、阻塞IO与非阻塞IO
文件IO是否为阻塞式IO,取决于用open()函数以什么方式打开。
2.1 阻塞IO
open()函数默认以阻塞IO的方式打开文件,也就是进程默认在等待IO时会进入阻塞态,不会继续在CPU上运行。
int fd;
char buffer;
fd = open("/dev/sda", O_RDWR);
ret = read(fd, &buffer, sizeof(buffer));
2.2 非阻塞IO
open()函数可以通过添加非阻塞标志位O_NONBLOCK,以非阻塞IO的方式打开文件。
int fd;
int buffer;
fd = open("/dev/sda", O_RDWR | O_NONBLOCK);
ret = read(fd, &buffer, sizeof(buffer));
应用程序通过poll()、epoll()和select()三个函数来轮询设备是否可用,从而实现非阻塞IO。
2.2.1 select()函数
select()函数把想要读的文件放到一个集合里,把想要写的文件放到一个集合里,把需要监测异常的文件也放到一个集合里,然后再设置一个超时时间。在超时时间内,如果这三个集合里出现了可用的文件,select()就返回一个大于0的数表明可用。

select()通过宏定义 FD_XXX 来往监测文件集合里增加、删除文件。
void FD_ZERO(fd_set *set) // 把文件集合清空
void FD_SET(int fd, fd_set *set) // 往集合里加入一个文件
void FD_CLR(int fd, fd_set *set) // 从集合里删除一个文件
int FD_ISSET(int fd, fd_set *set) // 判断一个文件在不在这个集合里
select()默认最多能够监视1024个文件。
2.2.2 poll()函数
poll()函数对于监视时间的数量没有限制。
select()函数是将监视文件按读、写、异常分类监视,而poll()函数把所有文件放到一个集合里,然后设置每个文件要监视的属性。

2.2.3 epoll()函数
select()和poll()都会随着文件数量的增加,效率变低,为了解决高并发问题发明了epoll()函数,一般在大规模并发服务器网络编程中使用。
因此,一般使用select()和poll()函数来做非阻塞IO。
三、阻塞IO与非阻塞IO的底层实现原理
3.1 阻塞IO的底层实现原理
要想实现阻塞IO,就需要编写对应设备文件的驱动来实现阻塞IO。
- 阻塞IO的实现方式:等待队列
等待队列是Linux内核提供的一种可以实现阻塞IO的方式。等待队列通过把想要访问该设备文件的进程1、进程2、进程3放入一个队列里排队等待;等到设备可以使用时,就可以依次唤醒等待队列里的进程1、进程2、进程3;阻塞态的进程是通过中断唤醒的,所以进程一定设置成TASK_INTERRUPTIBLE状态。

3.2 非阻塞IO的底层实现原理
应用程序通过poll()、epoll()和select()三个函数来轮询设备是否可用,对应的都是设备文件驱动的poll()函数。
- 非阻塞IO的实现方式:poll()函数轮询

四、阻塞IO和非阻塞IO的特点总结
采用阻塞IO还是非阻塞IO主要取决于,进程切换成本 与 CPU轮询成本 哪个更小!
阻塞IO会让出CPU,可以让给其他进程执行,所以适合连接数少、间隔较长时间才可用的设备。比如,按键驱动,只有用户按下时才会有效,非阻塞IO需要一直轮询,CPU占用率极高,而阻塞IO可以很好地解决这种情况;
非阻塞IO会继续占用CPU,进程处于忙等待,但可以让给其他线程执行,所以适合连接数多(高并发)、间隔时间较短就可用的设备。比如,DMA数据采集,完成DMA数据采集不需要很长时间,因此隔一小段时间就会可用,所以阻塞IO切换进程会很耗时,而非阻塞IO可以很好地解决这种情况。
总结,进程进行阻塞IO会进入睡眠态,IO完成后会被唤醒,底层采用等待队列实现,非阻塞IO适合连接数少的情况;
进程进行非阻塞IO,会不断轮询所有监测文件集合里的文件是否可用,适合连接数多的情况。
