个人认为同步和异步是更抽象的概念,是相对的任务而言的。而阻塞和非阻塞就就字面的意思是对当前的进程或线程而言的。
同步与异步在不同的场景下有不同的概念,在IO模型中的同步异步,主要区别在当任务A调用任务B的过程中,进程A是否继续进行。
如果A等待B的结果,则为同步;
如果A不等待B的结果,则为异步;同步状态下任务A的执行时依赖于任务B的,任务A成功是依赖于成功B的。而异步模式下两者是不相关的。
异步的实现方式大概有三种:状态、通知和回调。状态就是任务A去查询任务B的结果如何 ;通知就是等任务B执行完成之后通知任务A来实现;回调就是任务A定义个回调函数,当任务B结束后会自动调用回调函数;
阻塞和非阻塞的主要区别在,任务A等待B的结果的过程中,任务A是否会被挂起? 如果A等待的过程中不会挂起,则为阻塞; 如果A不等待的过程中不会挂起,则为非阻塞。
同步阻塞的情况下,任务A会挂起,同步非阻塞的情况下任务A并不挂起。不挂起的情况下任务A保留有响应信号的能力。
非阻塞的情况下并不会导致线程切换(只是不强制进行线程切换,如果该线程的时间片用完还是会切换的),可能效率更高,cpu利用率也更高,但是cpu可能会无意义空转,这样又会导致性能降低,所以使用何种方式需要看当前的系统情况。
上面两点似乎被分的很清楚,但是实际上这两个概念我认为指的是同一件事情,站的角度不同而已,过分的强调概念是无意义的。同步和异步更多的是两个任务之间数据通信方式,而阻塞非阻塞,则是站在当前线程自身的角度考虑是否可以在保留进程不挂起而继续进行任务来看的。
要了解IO模型先要理解一linux类的系统下计算机的IO基本概念,5种IO模型实际上指的都是网络编程中的IO,数据从网络读取后先会被放入内核区,而后从内核区传入用户区。
5种IO模型中前四种全部都是同步IO只有异步IO,这里的同步和异步区别在于同步IO会在数据到达内核区的等待过程中进行各种方式的检查,如果查到了有数据到达了内核区则进行阻塞,因此整个IO的流程是分成2个阶段的。而异步IO的模式是只有数据完成了从内核区到用户区的复制之后才有通知。 而同步IO中根据第一阶段的不同策略又分成很多的不同模型。
阻塞式I/O
应用进程在调用recvfrom,后阻塞直至数据到达用户区之后才恢复。从应用进程的角度上说这是很合理而高效的,从性能的角度来说可以见上文中的讨论,首先他会引起线程切换,其次其cpu利用率低。
非阻塞式I/O
应用进程在调用recvfrom的情况下并阻塞,而是返回一个为准备好的返回值,这时候应用进程可以继续运行,处理一些其他事情。通过这种轮询的方式查询是否有数据到达内核区,如果内核区有数据则需要阻塞应用线程,等待数据读取至用户区之后进行处理。
非阻塞I/O看起来很傻,不停的循环,但是这样有两个好处,第一个是不会强制进行线程的切换,线程切换的代价是很大的,其次在两次查询直接可以用来做一些其他的事情,用户线程保有一定的响应能力。
I/O复用模型
非阻塞式I/O中说到了采用轮询的方式查看是否有数据到达内核区,单线程的来看这个问题其实很傻,但是网络IO通常不是单个线程的。会有很多线程同时进行I/O读写,因此我们可以依次检测多个I/O读写任务,如果有某个任务所指定的数据到达则返回这样效率就高得多了,这就是所谓的I/O复用。
其实这是网络模型中最为常用的模式,平时所谓的select、poll和epoll都是I/O复用模型,只是细节上略有区别,具体的区别在后面讨论。
读取的两个阶段有2次调用2次返回,在第一个阶段是很多I/O读取任务共用的,因此效率还是比较高的。
信号驱动I/O
异步I/O
异步I/O的模型其实就是两个阶段均为异步的方式,用户进程异步调用函数后,检测数据的信号,但是用户线程自身并不挂起,而是继续运行,当数据完成了2个阶段的过程,读取至用户区之后才会通知应用程序。
这种方式和阻塞式I/O的区别仅仅在于用户线程是否在两个阶段均阻塞。
模型之间的讨论
其实对于典型的服务器环境,基本上都是默认采用I/O复用模型的,这一点从模型自身的特性就可以看出。网络I/O中即使采用非阻塞的方式其实本身也没有太大的意义,因为没有数据到来,那处理程序也没有其他的事情要做。而且从编程的角度上来说,I/O复用这种同步的编程模式也更利于理解,不会使得程序显得很混乱。下面附上几中不同的I/O状态比较图:
首先select只有一个函数,创建、注册等待都是一次完成的。
特性
首先将fd_set从用户空间拷贝到内核区,然后注册回调函数 __pollwait。
回调函数的主要工作就是把当前线程挂到设备的等待队列中,不同的设备有不同的等待队列,如果该设备有个响应的响应(比如网络中读取的I/O)则会唤醒该设备等待队列上的进程,___pollwait方法会返回一个描述读写操作是否就行的mask掩码,根据这个掩码给fd_set赋值。 如果遍历fd_set都没有一个可读写的mask掩码,这调用会 schedule_timeout将select线程进入睡眠,如果设备驱动自身资源可读或者,超时一定时间限都没人唤醒,则会唤醒当代队列上的线程重新遍历fd_set判断有没有就绪的fd。
缺点
每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大 ;同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大 ,尤其是在连接数量很多,但是活跃连接并不多的情况下。 select支持的文件描述符数量太小了,默认是1024(64位系统是2048)。
poll和select基本上类似,区别是采用链表的形式去组织数据,所以没有数量限制。但是前两个问题依然存在
epoll的实现和上面两者有很大的不同,epoll分为三个函数,分别表示创建,注册和阻塞三个情况。
这样在注册时会将epoll的句柄从用户区拷贝到内核区,所以只有一次复制,而不是每次等待都要复制,不存在第一个问题。
epoll为每个fd指定一个回调函数,当设备唤醒的时候就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表,epoll_wait要做的工作其实就是定期查看这个链表有没有就绪的fd就好,所以不需要遍历fd,不存在第二个问题。
而且由于不需要遍历也不存在低活跃连接数量下效率低下的问题。 epoll没有fd上限,一般1G内存可以有10w个连接,与内存大小相关,而且可以修改。不存在第三个问题。
此外还有另一种说法,epoll采用mmap内存映射技术将内核区与用户区映射为同一片地方以减少系统复制代价,不清楚具体用在哪里。
epoll和select、poll之间最主要的区别还是前者基于回调机制进行响应,而select和poll基于系统调用,让内核遍历所有fd进行查询是否有设备就绪。
epoll本身还分为电平触发和边缘触发
电平触发(条件触发)LT模式:就是当fd就绪后进行通知,如果此次通知后没有操作响应,则下次依然通知,可用在阻塞模式也可以用在非阻塞模式。
边缘触发ET模式:就是当fd就绪后进行通知,但是如果此次没有响应操作,则下次不会通知。但只可以用在非阻塞模式下。
通常来说边缘触发效率更高,因为可以减少重复epoll的次数。