socket网络编程-IO模型

IO模型

同步/异步 阻塞/非阻塞

阻塞:进程/线程需要访问的资源如果没有就位则进程/线程阻塞

非阻塞:进程/线程需要访问的资源如果没有就位则进程/线程不会阻塞,而是继续执行,并会轮询看资源是否就位

同步:同步是指当数据准备就绪后,需要主动读写数据,在读写数据的过程中还是会阻塞的

异步:异步是指数据准备就绪后,会直接完成IO操作,并不需要去主动读写,由操作系统内核来完成数据的读写

阻塞和非阻塞是针对进程/线程的状态而言的

同步和异步是针对访问数据的方式而言的

一个IO的过程其实可以分成两次,第一次是数据从对端送达,并从协议栈拷贝到内核缓冲区,第二次则是数据从内核缓冲区拷贝拷贝到应用层缓冲区,对于阻塞非阻塞而言,主要是针对第一个阶段而言,即进程/线程在第一个阶段如果内核缓冲区中没有数据则是否阻塞,而同步异步则是针对第二个阶段而言,同步还是需要自己去将数据从内核拷贝到应用缓冲区中,但是异步则是系统内核直接将数据拷贝到应用缓冲区中,然后通知再通知用户。

UNIX五大IO模式
  1. 阻塞式IO(BIO),程序发起读写等请求后会阻塞,直到有数据到达内核缓冲区,然后将内核缓冲区中的数据拷贝到应用层缓冲区后返回

  2. 非阻塞IO(NIO),程序发起请求后,如果此时有数据则直接返回结果,否则将返回一个错误,而不是将线程投入睡眠,同时在一定时间后再次发送请求(一般是轮询)直到返回结果

  3. IO复用(事件驱动),IO复用是针对多个套接字采用的一种IO模型,在第一阶段通过epoll/select/poll等系统调用在多个套接字中寻找一个或者多个数据已经准备好的套接字,然后返回表示有套接字可读/可写,然后调用相应的IO函数从相应可读/可写的套接字进行读写操作。其和阻塞式IO最大的区别是可以同时针对多个套接字进行阻塞式IO操作,而普通的BIO则是通过多个线程来完成各个套接字的阻塞式IO

  4. 信号驱动式IO模型,当有IO请求时并不会阻塞,而是等到套接字就绪时内核产生一个sigio信号,通知去读/写数据(通过sigaction注册一个系统调用函数即可)

  5. 异步IO模型,调用者直接向内核发送自己的缓冲区地址,缓冲区大小等信息,告诉内核自己去启动某个操作,然后立即返回去做别的事情,此时内核会自动完成第一阶段和第二阶段,等到第二阶段完成,即数据被复制到了用户空间才通知进程去处理。这种方式和信号的最大区别是前者直接将把IO操作已经完成再通知用户,而后者是通知用户套接字已经就绪,可以进行一个IO操作

注意我们可以将阻塞过程分为两个阶段,第一阶段是等待内核接收缓冲区中有数据,第二阶段是将内核缓冲区中的数据拷贝到应用层缓冲区,我们可以发现在上面的5种方案中,前四种方案虽然不一定在第一阶段阻塞,但是实际上都会在第二阶段阻塞(虽然将数据从内核缓冲区拷贝到应用层缓冲区很快,但是还是需要时间的),而异步IO模型是唯一在两个阶段都不会阻塞的IO模式(windows下的IOCP就是基于这种模型)

这里就可以看出来非阻塞和异步的区别了,同步是用户自己要经常去看能不能读了,如果能则读出来,而异步不需要用户去看,系统直接帮你读完了然后通知你,即阻塞是我必须要读,非阻塞是我可以读了,异步是我读完了

img

BIO和NIO的几种常见的网络框架
  1. BIO,由于BIO在read/accept时没有数据或者没有连接会阻塞,那么为了处理多个连接,可以选择为每个新到来的连接创建一个进程/线程来处理该连接,当该连接释放时同时也释放该线程,但是这种方案在并发量高的时候可能要创建过多的线程,而且频繁的创建和释放线程会导致系统开销巨大。但是可以通过线程池来优化
  2. NIO与IO多路复用一般配合在一起
    • IO多路复用是指通过epoll/select/poll来监视多个文件描述符,普通的IO是每个连接阻塞在各自的线程/进程上等待IO,而多路复用可以使用单进程/线程同时等待多个文件描述符就绪,然后通过轮询来处理各个描述符上的IO事件
    • IO多路复用为什么要和NIO一起使用?
      • 从某种意义上说,当epoll返回时,一般表示可读/可写了,那么read/write是不会在阻塞的,但是实际情况并不是如此
      • 惊群现象,可能多个进程/线程都阻塞在epoll上,当某个连接到来时所有进程被唤醒,但是实际只有一个进程获得了连接,如果此时是BIO那么其他进程都会阻塞在accept
      • 边缘触发导致
      • 当接收缓冲区有新的数据到达时,epoll可读,但是后来协议栈检查到该数据校验和错误然后丢弃,这样如果是BIO会导致read阻塞
reactor模式

reactor模式其实就是一种使用了同步非阻塞的IO多路复用机制,IO多路复用实际上是使用了一个线程来检查多个文件描述符的就绪状态,当有文件描述符就绪时,返回,否则阻塞直到超时,得到就绪状态后进行真正的操作可以在同一个线程执行,也可以启动其他线程执行(比如线程池),此时需要一个事件分发器,事件分发器的作用就是将IO事件分发给读写事件的处理者,而涉及到事件分发器的两种模式即为Reactor和Proactor模式

reactor模式又分为单线程、多线程、主从reactor

单线程:当epoll/select/poll监视的描述符上有事件发生,则通过轮询机制逐一执行发生IO事件的描述符

多线程:多线程又分为

  • 任务-线程:即当有IO事件发生,则创建一个线程(一般通过线程池优化线程创建和销毁的开销)去处理该IO任务,即一个任务对应一个线程,当任务执行完毕,将线程交给线程池,这样做的弊端在于,一个连接上的多个任务可能不会按序获得结果,当一个连接上突然到来一连串任务时,可能会占满线程池
  • 连接-线程:即为每个连接分配一个线程,当epoll监听到某个连接上有事件发生时,则将该任务该线程去处理(即某个连接上的任务固定交给其所属的线程来处理),但是如果有很多连接将需要多个线程,而且如果某个连接上突发多个请求,可能会使得该线程跑满,导致CPU负载不均衡

主从式reactor:该方案又分为两种

  • 采用主从reactor来进行处理,主reactor(主线程)负责监听有没有新的连接到来,如果有新的连接到来,则将其分配给某个从reactor(某个子线程,一般采用轮询的方式分配从reactor)那么该连接则属于该子线程,其上的IO事件由从reactor来监视(注意一个从reactor可以处理多个连接,但是一个连接只能对应一个从reactor),但是如果某个从reactor上的某个连接上出现了CPU密集型任务,或者读写数据库/磁盘等任务,则可能导致该从reactor上的其他连接的任务无法及时执行
  • 为了解决上述问题,采用主从reactor+线程池的方式来处理,将连接上需要处理很长时间的任务交给线程池来处理,使得此时从reactor可以继续执行其他连接上的任务
发布了23 篇原创文章 · 获赞 4 · 访问量 2127

猜你喜欢

转载自blog.csdn.net/hdadiao/article/details/104614591