玩命加载中 . . .

5.1-阻塞与非阻塞


  • 一个典型的网络IO接口调用,分为两个阶段,分别是“数据就绪”和“数据读写”,数据就绪阶段分为阻塞和非阻塞,表现的结果就是,阻塞当前线程还是直接返回
  • 同步表示A向B请求调用一个网络IO接口时,数据的读写都是由请求方自己来完成的(不管是阻塞还是非阻塞);
  • 异步表示A向B请求调用一个接口时,向B传入请求的事件以及事件发生时的通知方式,A就可以去处理其他逻辑了,当B监听到事件处理完成后,就用事先约定好的通知方式,通知A处理结果

网络IO包括2个阶段

  1. 数据就绪
  2. 数据读写
  • 阻塞:调用IO方法的线程会进入阻塞状态,会被挂起
  • 非阻塞:不会改变线程状态,通过返回值判断
int len = recv(sockfd, buf, 1024, 0);   // 通过recv的返回值来判断,非阻塞
- len = -1: 出错(EINTR/EAGAIN要特殊判断)
- len = 0: 读取到数据末尾,对方连接关闭
- len > 0: 读取到的数据大小
  • 同步

数据放在TCP接收缓冲区,需要用户调用recv去读取
需要等recv数据接收完成,才能继续执行后面的代码

  • 异步

需要有通知方式,调用异步接口后就可以继续执行后面的代码,内核把数据拷贝到用户空间后会通知应用程序

Linux中的异步接口:aio_read/aio_write

5种IO模型

1、阻塞blocking

调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的去检查这个函数有没有返回,必
须等这个函数返回才能进行下一步动作

2、非阻塞non-blocking

非阻塞等待,每隔一段时间就去检测IO事件是否就绪。没有就绪就可以做其他事。非阻塞I/O执行系统调
用总是立即返回,不管事件是否已经发生,若事件没有发生,则返回-1,此时可以根据 errno 区分这两
种情况,对于accept,recvsend,事件未发生时,errno 通常被设置成 EAGAIN

3、IO复用multiplexing

Linux 用 select/poll/epoll 函数实现 IO 复用模型,这些函数也会使进程阻塞,但是和阻塞IO所不同的是
这些函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检测。直到有数
据可读或可写时,才真正调用IO操作函数

4、信号驱动signal-driven

Linux 用套接口进行信号驱动 IO,安装一个信号处理函数,进程继续运行并不阻塞
当IO事件就绪,进程收到SIGIO 信号,然后处理 IO 事件
内核在第一个阶段是异步,在第二个阶段是同步;与非阻塞IO的区别在于它提供了消息通知机制
不需要用户进程不断的轮询检查,减少了系统API的调用次数,提高了效率

5、异步IO(asynchronous)

Linux中,可以调用aio_read函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序

Reactor模式

主线程(IO处理单元)只负责监听文件描述符是否发生有事件发生,有的话就将该事件通知工作线程(逻辑单元),将socket可读可写事件放入请求队列,交给工作线程处理,除此之外,不做其他工作。数据读写,接收新的连接,以及处理客户端请求都在工作线程中完成

使用同步IO(以epoll_wait为例)实现的 Reactor 模式的工作流程

  1. 主线程往epoll内核事件表中注册socket上的读就绪事件
  2. 主线程调用epoll_wait等待socket上有数据可读
  3. socket上有数据可读时,epoll_wait通知主线程,主线程将socket可读事件放入请求队列
  4. 睡眠在请求队列上的某个工作线程被唤醒,它从socket读取数据,并处理客户请求,然后往epoll内核事件表中注册该socket的写就绪事件
  5. 主线程调用epoll_wait等待socket可写
  6. socket可写时,epoll_wait通知主线程,主线程将socket可写事件放入请求队列
  7. 睡眠在请求队列上的某个工作线程被唤醒,它往socket上写入服务器处理客户请求的结果

Proactor模式

Proactor模式将所有的IO操作都交给主线程和内核来处理,工作线程只负责业务逻辑

使用异步IO模型(以aio_read/aio_write为例)实现的Proactor模式的工作流程

  1. 主线程调用aio_read函数向内核注册socket上的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用进程(以信号为例)
  2. 主线程继续处理其他逻辑
  3. socket上的数据被读入用户缓冲区后,内核向应用程序发送一个信号,通知数据可用
  4. 应用程序事先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求后,调用aio_write函数向内核注册socket上的写完成事件,并告诉内核用户写缓冲区的位置,以及操作完成时如何通知应用程序
  5. 主线程继续处理其他逻辑
  6. 当用户缓冲区的数据被写入socket后,内核向应用程序发送一个信号,通知数据发送完毕
  7. 应用程序事先定义好的信号处理函数选择一个工作线程来处理善后工作,比如决定是否关闭socket

模拟Proactor模式

Proactor 模式中,主线程只负责监听socket,数据读写是由内核通过异步方式完成的,工作线程只负责业务逻辑

可以在主线程中用同步的方式进行数据读写,然后交给工作线程处理

使用同步IO(以epoll_wait为例)模拟出的 Proactor 模式的工作流程

  1. 主线程往epoll内核事件表中注册socket上的读就绪事件
  2. 主线程调用epoll_wait等待socket上有数据可读
  3. socket上有数据可读时,epoll_wait通知主线程,主线程从socket循环读取数据,直到没有更多数据可读,然后将数据封装成一个请求对象并插入请求队列
  4. 睡眠在请求队列上的某个工作线程被唤醒,获得请求数据并进行处理,然后往epoll内核事件中注册socket上的写就绪事件
  5. 主线程调用epoll_wait等待socket可写
  6. socket可写时,epoll_wait通知主线程,主线程往socket上写入服务器处理客户请求的结果

线程池

  • 空间换时间,浪费服务器的硬件资源,换取运行效率
  • 线程池是一组资源的集合,这组资源在服务器启动之初就被完全创建好并被初始化,称为静态资源
  • 服务器运行时,如果需要相关资源,可以直接从池中获取,无需动态分配
  • 当服务器处理完一个客户连接后,可以把相关的资源放回池中,无需执行系统调用释放资源

文章作者: kunpeng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 kunpeng !
  目录