10、I/O相关概念
/xiaoxi/p/6525396.html
10.1、同步和异步
同步就是:如果有多个任务或者事件要发生,这些任务或者事件必须逐个地进行,一个事件或者任务的执行会导致整个流程的暂时等待,这些事件没有办法并发地执行;
异步就是:如果有多个任务或者事件发生,这些事件可以并发地执行,一个事件或者任务的执行不会导致整个流程的暂时等待。
举个简单的例子:
假如有一个任务包括两个子任务A和B,对于同步来说,当A在执行的过程中,B只有等待,直至A执行完毕,B才能执行;而对于异步就是A和B可以并发地执行,B不必等待A执行完毕之后再执行,这样就不会由于A的执行导致整个任务的暂时等待。
同步和异步区别在于:多个任务和事件发生时,一个事件的发生或执行是否会导致整个流程的暂时等待。当多个线程同时访问一个变量时,每个线程访问该变量就是一个事件,对于同步来说,就是这些线程必须逐个地来访问该变量,一个线程在访问该变量的过程中,其他线程必须等待;而对于异步来说,就是多个线程不必逐个地访问该变量,可以同时进行访问。
10.2、阻塞和非阻塞
阻塞就是:当某个事件或者任务在执行过程中,它发出一个请求操作,但是由于该请求操作需要的条件不满足,那么就会一直在那等待,直至条件满足;
非阻塞就是:当某个事件或者任务在执行过程中,它发出一个请求操作,如果该请求操作需要的条件不满足,会立即返回一个标志信息告知条件不满足,不会一直在那等待。
这就是阻塞和非阻塞的区别。也就是说阻塞和非阻塞的区别关键在于当发出请求一个操作时,如果条件不满足,是会一直等待还是返回一个标志信息。
举个简单的例子:
假如我要读取一个文件中的内容,如果此时文件中没有内容可读,对于阻塞来说就是会一直在那等待,直至文件中有内容可读;而对于非阻塞来说,就会直接返回一个标志信息告知文件中暂时无内容可读。
总结
同步和异步着重点在于多个任务的执行过程中,一个任务的执行是否会导致整个流程的暂时等待;
而阻塞和非阻塞着重点在于发出一个请求操作时,如果进行操作的条件不满足是否会返会一个标志信息告知条件不满足。
10.3、阻塞I/O和非阻塞I/O
IO操作包括:对硬盘的读写、对socket的读写以及外设的读写。
当用户线程发起一个IO请求操作(本文以读请求操作为例),内核会去查看要读取的数据是否就绪,对于阻塞IO来说,如果数据没有就绪,则会一直在那等待,直到数据就绪;对于非阻塞IO来说,如果数据没有就绪,则会返回一个标志信息告知用户线程当前要读的数据没有就绪。当数据就绪之后,便将数据拷贝到用户线程,这样才完成了一个完整的IO读请求操作,也就是说一个完整的IO读请求操作包括两个阶段:
1)查看数据是否就绪;
2)进行数据拷贝(内核将数据拷贝到用户线程)。
那么阻塞(blocking IO)和非阻塞(non-blocking IO)的区别就在于第一个阶段,如果数据没有就绪,在查看数据是否就绪的过程中是一直等待,还是直接返回一个标志信息。
传统的IO都是阻塞IO,比如通过socket来读数据,调用read()方法之后,如果数据没有就绪,当前线程就会一直阻塞在read方法调用那里,直到有数据才返回;而如果是非阻塞IO的话,当数据没有就绪,read()方法应该返回一个标志信息,告知当前线程数据没有就绪,而不是一直在那里等待。
10.4、同步I/O和异步I/O
同步IO:如果一个线程请求进行IO操作,在IO操作完成之前,该线程会被阻塞;
异步IO:如果一个线程请求进行IO操作,IO操作不会导致请求线程被阻塞。
事实上,同步IO和异步IO模型是针对用户线程和内核的交互来说的:
同步IO:当用户发出IO请求操作之后,如果数据没有就绪,需要通过用户线程或者内核不断地去轮询数据是否就绪,当数据就绪时,再将数据从内核拷贝到用户线程;
异步IO:只有IO请求操作的发出是由用户线程来进行的,IO操作的两个阶段都是由内核自动完成,然后发送通知告知用户线程IO操作已经完成。也就是说在异步IO中,不会对用户线程产生任何阻塞。
这是同步IO和异步IO关键区别所在,同步IO和异步IO的关键区别反映在数据拷贝阶段是由用户线程完成还是内核完成。所以说异步IO必须要有操作系统的底层支持。注意同步IO和异步IO与阻塞IO和非阻塞IO是不同的两组概念。
阻塞IO和非阻塞IO是反映在当用户请求IO操作时,如果数据没有就绪,是用户线程一直等待数据就绪,还是会收到一个标志信息这一点上面的。也就是说,阻塞IO和非阻塞IO是反映在IO操作的第一个阶段,在查看数据是否就绪时是如何处理的。
11、五种I/O模型
11.1、阻塞I/O
即在读写数据过程中会发生阻塞现象。
当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。如果数据没有就绪,就会一直阻塞在read方法。
阻塞会带来一定的问题,比如在调用send()的时候,线程被阻塞,无法响应任何的网络请求,无法适应多客户机、多业务逻辑的网络编程。所以,对于多客户机的网络应用,可以在服务端使用多线程,这样任何一个连接的阻塞都不会影响其他的连接。
11.2、非阻塞I/O
进程反复调用IO函数(系统多次查询)。
当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。
所以事实上,在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。对于非阻塞IO就有一个非常严重的问题,在while循环中需要不断地去询问内核数据是否就绪,这样会导致CPU占用率非常高,因此一般情况下很少使用while循环这种方式来读取数据。
SOCKET设置为非阻塞模式,当所请求的IO无法完成时,不会进入进程睡眠,而是返回一个错误,系统就不断测试数据是否已经准备好,直到数据准备好为止,在不断测试的过程中,会占用大量的CPU时间。创建的SOCKET都是默认阻塞的,非阻塞套接字需要对查询时数据没有准备好的情况进行处理,有些复杂。但是,相对于阻塞套接字,非阻塞套接字对于建立多个连接、数据收发不均匀、时间不定时,有明显的优势。
11.3、多路复用I/O
I/O多路复用:关键是实现同时对多个IO端口进行监听。I/O是指网络I/O,多路是指多个TCP连接(socket或channel),复用是指复用一个线程或几个线程。就是说一个或一组线程处理多个TCP连接,最大的优势是减少了系统开销,不必创建过多的线程/进程,也不用维护这些进程/线程。
I/O多路复用是使用两个系统调用(select/poll/epoll和recvfrom),blocking I/O只调用了recvfrom。select/poll/epoll核心是可以同时处理多个connection,在连接数不高的情况下,其性能并不一定比多线程+阻塞I/O好,多路复用模型中,每一个socket,都设置为non-blocking,阻塞是被select这个函数阻塞,而不是被socket阻塞。
在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。
1、Select
select 实现多路复⽤的⽅式是,将已连接的 Socket 都放到⼀个⽂件描述符集合,然后调⽤ select 函数将⽂件描述符集合拷⻉到内核⾥,让内核来检查是否有⽹络事件产⽣,检查的⽅式很粗暴,就是通过遍历⽂件描述符集合的⽅式,当检查到有事件产⽣后,将此 Socket 标记为可读或可写, 接着再把整个⽂件描述符集合拷⻉回⽤户态⾥,然后⽤户态还需要再通过遍历的⽅法找到可读或可写的 Socket,然后再对其处理。
所以,对于 select 这种⽅式,需要进⾏2 次「遍历」文件描述符集合,⼀次是在内核态⾥,⼀个次是在⽤户态⾥ ,⽽且还会发⽣2 次「拷贝」文件描述符集合,先从⽤户空间传⼊内核空间,由内核修改后,再传出到⽤户空间中。
Select 使⽤固定⻓度的BitsMap,表示⽂件描述符集合,⽽且所⽀持的⽂件描述符的个数是有限制的,在Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最⼤值为1024,只能监听 0~1023 的⽂件描述符。
客户端操作服务器时就会产生这三种文件描述符(简称fd):writefds(写)、readfds(读)、和exceptfds(异常)。会定时对IO进程查询,没有数据会立即返回。select会阻塞住监视3类文件描述符,等有数据、可读、可写、出异常 或超时、就会返回;返回后通过遍历fdset整个数组来找到就绪的描述符fd,进行对应的IO操作。但是select中的timeval设置成{0,0};就可以变为非阻塞的,服务器就可以处理更多的事务。
1)优点
几乎在所有平台上支持,跨平台支持性好
2)缺点
但是采用的是轮询方式全盘扫描,会随着文件描述符FD数量增加而性能下降;每次调用select(),需要把fd集合从用户态拷贝到内核态,并进行遍历(消息传递都是从内核到用户空间);默认单个进程打开的FD有限制1024个,可以修改宏定义,但是效率依然不高。2、Poll
基本原理与select一致,也是轮询+遍历。唯一的区别是poll没有最大文件描述符的限制,用**动态数据**,以**链表**的方式存储fd,突破了BitsMap的文件描述符的限制。
poll 和 select 并没有太⼤的本质区别,都是使⽤「线性结构」存储进程关注的Socket集合,因此都需要遍历⽂件描述符集合来找到可读或可写的Socket,时间复杂度为O(n),⽽且也需要在⽤户态与内核态之间拷⻉⽂件描述符集合,这种⽅式随着并发数上来,性能的损耗会呈指数级增⻓。
3、epoll
epoll 通过两个⽅⾯,很好解决了 select/poll 的问题。
epoll 在内核⾥使⽤红⿊树来跟踪进程所有待检测的⽂件描述字,把需要监控的 socket 通过epoll_ctl()函数加⼊内核中的红⿊树⾥,红⿊树是个⾼效的数据结构,增删查⼀般时间复杂度是O(logn) ,通过对这棵⿊红树进⾏操作,这样就不需要像 select/poll 每次操作时都传⼊整个 socket 集合,只需要传⼊⼀个待检测的 socket,减少了内核和⽤户空间⼤量的数据拷⻉和内存分配。
epoll 使⽤事件驱动的机制,内核⾥维护了⼀个链表来记录就绪事件,当某个 socket 有事件发⽣时,通过回调函数内核会将其加⼊到这个就绪事件列表中,当⽤户调⽤epoll_wait()函数时,只会返回有事件发⽣的⽂件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,⼤⼤提⾼了检测的效率。
没有fd个数的限制,用户拷贝到内核态只需要一次,使用时间通知机制来触发。通过epoll_ctl注册fd,一旦fd就绪就会通过callback回调机制来激活对应的fd,进行相关的IO操作。
**epoll_create() **系统启动时,在Linux内核里面申请一个红黑树结构文件系统,返回epoll对象,也是一个fd
epoll_ctl()每新建一个连接,都通过该函数操作epoll对象,在这个对象里面修改添加删除对应的链接fd,绑定一个callback函数
epoll_wait()轮询所有的callback集合,完成对应的IO操作
注: epoll_wait 实现的内核代码中调⽤了__put_user函数,这个函数就是将数据从内核拷⻉到⽤户空间。
1)优点
没有fd个数限制,所支持的最大个数是操作系统的最大文件句柄数,1G内存大概支持10万个句柄;效率高,使用回调通知而不是轮询的方式,所以效率不会随着FD数量的增多而下降;内核和用户map是同一块内存实现(map是一种内存映射文件的方法,即将一个文件或者其他对象映射到进程的地址空间)2)epoll两种触发模式
边缘触发ET水平触发LTET和LT具体用法:
使⽤边缘触发模式时,当被监控的 Socket 描述符上有可读事件发⽣时,服务器端只会从epoll_wait中苏醒⼀次,即使进程没有调⽤ read 函数从内核读取数据,也依然只苏醒⼀次,因此我们程序要保证⼀次性将内核缓冲区的数据读取完;
使⽤⽔平触发模式时,当被监控的 Socket 上有可读事件发⽣时,服务器端不断地从epoll_wait中苏醒,直到内核缓冲区数据被read函数读完才结束,⽬的是告诉我们有数据需要读取;
ET和LT具体区别:
⽔平触发的意思是只要满⾜事件的条件,⽐如内核中有数据需要读,就⼀直不断地把这个事件传递给⽤户;⽽边缘触发的意思是只有第⼀次满⾜条件的时候才触发,之后就不会再传递同样的事件了。
ET和LT中I/O事件发生:
如果使⽤LT⽔平触发模式,当内核通知⽂件描述符可读写时,接下来还可以继续去检测它的状态,看它是否依然可读或可写。所以在收到通知后,没必要⼀次执⾏尽可能多的读写操作。
如果使⽤ET边缘触发模式,I/O 事件发⽣时只会通知⼀次,⽽且我们不知道到底能读写多少数据,所以在收到通知后应尽可能地读写数据,以免错失读写的机会。因此,我们会循环从⽂件描述符读写数据,那么如果⽂件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那⾥,程序就没办法继续往下执⾏。所以,边缘触发模式⼀般和⾮阻塞 I/O 搭配使⽤,程序会⼀直执⾏ I/O 操作,直到系统调⽤(如 read 和write )返回错误,错误类型为EAGAIN 或 EWOULDBLOCK。
⼀般来说,边缘触发的效率⽐⽔平触发的效率要⾼,因为边缘触发可以减少 epoll_wait 的系统调⽤次数,系统调⽤也是有⼀定的开销的的,毕竟也存在上下⽂的切换。
总结select、poll、epoll区别
三者的效率对比:
100万个连接,里面有1万个是活跃的
select:需要建立100w/1024 = 977个进程来支持100W个连接,CPU的性能就特别差;
poll:没有连接限制,但是遍历都响应不过来的,而且还有空间的拷贝消耗大量的资源;
epoll:请求进来时就创建一个fd并绑定callback,那么就主要遍历1W个活跃的callback即可,既高效又不需要内存拷贝。
深入理解select、poll、epoll:/wteruiycbqqvwt/article/details/90299610
11.4、信号驱动I/O
在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。
允许接口进行信号驱动I/O,并设置一个信号处理函数,进程继续运行不阻塞,当数据准备好的时候,会收到一个信号,在信号处理函数中调用I/O操作函数处理数据。
11.5、异步I/O
异步IO模型才是最理想的IO模型,在异步IO模型中,当用户线程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它收到一个asynchronous read之后,它会立刻返回,说明read请求已经成功发起了,因此不会对用户线程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read操作完成了。也就说用户线程完全不需要知道实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。
也就说在异步IO模型中,IO操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用IO函数进行具体的读写。
但是需要操作系统提供Asynchronous IO 底层支持。
前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞
12、高性能网络模型
12.1、Reactor
1、Reator模式介绍
Reactor 模式也叫 Dispatcher 模式,即I/O 多路复⽤监听事件,收到事件后,根据事件类型分配(Dispatch)给某个进程 / 线程。
Reactor 模式主要由 Reactor 和处理资源池这两个核⼼部分组成,它俩负责的事情如下:
Reactor 负责监听和分发事件,事件类型包含连接事件、读写事件;处理资源池负责处理事件,如 read -> 业务逻辑 -> send;
Reactor 模式是灵活多变的,可以应对不同的业务场景,灵活在于:
Reactor 的数量可以只有⼀个,也可以有多个;处理资源池可以是单个进程 / 线程,也可以是多个进程 /线程;
2、单 Reactor 单进程/线程
1)进程⾥有Reactor、Acceptor、Handler:
Reactor 对象的作⽤是监听和分发事件;Acceptor 对象的作⽤是获取连接;Handler 对象的作⽤是处理业务;2)具体实现方案:
Reactor 对象通过 select (IO 多路复⽤接⼝) 监听事件,收到事件后通过 dispatch 进⾏分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;如果是连接建⽴的事件,则交由 Acceptor 对象进⾏处理,Acceptor 对象会通过 accept ⽅法 获取连接,并创建⼀个 Handler 对象来处理后续的响应事件;如果不是连接建⽴事件, 则交由当前连接对应的 Handler 对象来进⾏响应;Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。3)缺点
只有⼀个进程,⽆法充分利⽤ 多核CPU的性能;Handler 对象在业务处理时,整个进程是⽆法处理其他连接的事件的,如果业务处理耗时⽐较⻓,那么就造成响应的延迟;4)总结
单 Reactor 单进程的⽅案不适⽤计算机密集型的场景,只适⽤于业务处理⾮常快速的场景3、单Reactor多线程/多进程
1)实现方案
Reactor 对象通过 select (IO 多路复⽤接⼝) 监听事件,收到事件后通过 dispatch 进⾏分发,具体分发给 Acceptor 对象还是Handler 对象,还要看收到的事件类型;如果是连接建⽴的事件,则交由 Acceptor 对象进⾏处理,Acceptor 对象会通过 accept ⽅法 获取连接,并创建⼀个 Handler 对象来处理后续的响应事件;如果不是连接建⽴事件, 则交由当前连接对应的 Handler 对象来进⾏响应;2)与单Reactor单线程不同点
Handler 对象不再负责业务处理,只负责数据的接收和发送,Handler 对象通过 read 读取到数据后,会将数据发给⼦线程⾥的 Processor 对象进⾏业务处理;⼦线程⾥的 Processor 对象就进⾏业务处理,处理完后,将结果发给主线程中的 Handler 对象,接着由 Handler 通过 send ⽅法将响应结果发送给 client;单 Reactor 多进程相⽐单 Reactor 多线程实现起来很麻烦,主要因为要考虑⼦进程 <-> ⽗进程的双向通信,并且⽗进程还得知道⼦进程要将数据发送给哪个客户端。⽽多线程间可以共享数据,虽然要额外考虑并发问题,但是这远⽐进程间通信的复杂度低得多,因此实际应⽤中也看不到单 Reactor 多进程的模式。4、多Reactor多线程/多进程
1)实现方案
主线程中的 MainReactor 对象通过 select 监控连接建⽴事件,收到事件后通过 Acceptor 对象中的accept 获取连接,将新的连接分配给某个⼦线程;⼦线程中的 SubReactor 对象将 MainReactor 对象分配的连接加⼊ select 继续进⾏监听,并创建⼀个Handler ⽤于处理连接的响应事件。如果有新的事件发⽣时,SubReactor 对象会调⽤当前连接对应的 Handler 对象来进⾏响应。Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。2)优点
主线程和⼦线程分⼯明确,主线程只负责接收新连接,⼦线程负责完成后续的业务处理。主线程和⼦线程的交互很简单,主线程只需要把新连接传给⼦线程,⼦线程⽆须返回数据,直接就可以在⼦线程将处理结果发送给客户端。12.2、Proactor
Reactor 是⾮阻塞同步⽹络模式,⽽Proactor是异步⽹络模式。异步 I/O 在「内核数据准备好」和「数据从内核空间拷⻉到
⽤户空间」这两个过程都不⽤等待。
1、工作流程
Proactor Initiator 负责创建 Proactor 和 Handler 对象,并将 Proactor 和 Handler 都通过Asynchronous Operation Processor 注册到内核;Asynchronous Operation Processor 负责处理注册请求,并处理 I/O 操作;Asynchronous Operation Processor 完成 I/O 操作后通知 Proactor;Proactor 根据不同的事件类型回调不同的 Handler 进⾏业务处理;Handler 完成业务处理;12.3、Proactor 和 Reactor 区别
Reactor是⾮阻塞同步⽹络模式,感知的是就绪可读写事件。在每次感知到有事件发⽣(⽐如可读就绪事件)后,就需要应⽤进程主动调⽤ read ⽅法来完成数据的读取,也就是要应⽤进程主动将socket 接收缓存中的数据读到应⽤进程内存中,这个过程是同步的,读取完数据后应⽤进程才能处理数据。
Proactor是异步⽹络模式, 感知的是已完成的读写事件。在发起异步读写请求时,需要传⼊数据缓冲区的地址(⽤来存放结果数据)等信息,这样系统内核才可以⾃动帮我们把数据的读写⼯作完成,这⾥的读写⼯作全程由操作系统来做,并不需要像 Reactor 那样还需要应⽤进程主动发起 read/write来读写数据,操作系统完成读写⼯作后,就会通知应⽤进程直接处理数据。
总结
Reactor可以理解为「来了事件操作系统通知应⽤进程,让应⽤进程来处理」,⽽Proactor可以理解为「来了事件操作系统来处理,处理完再通知应⽤进程」。这⾥的「事件」就是有新连接、有数据可读、有数据可写的这些 I/O 事件这⾥的「处理」包含从驱动读取到内核以及从内核读取到⽤户空间。
⽆论是 Reactor,还是 Proactor,都是⼀种基于「事件分发」的⽹络编程模式,区别在于Reactor模式是基于「待完成」的I/O事件,⽽Proactor模式则是基于「已完成」的I/O事件。