IO和Socket编程


何为IO?

从字面意思剖析I/O(Input/Outpu) 即输入/输出

计算机结构

从计算机结构的角度来解读一下I/O,计算机结构分为 5 大部分:运算器、控制器、存储器、输入设备、输出设备。

 

输入设备(比如键盘)和输出设备(比如显示器)都属于外部设备。网卡硬盘这种既可以属于输入设备,也可以属于输出设备。

从计算机结构的视角来看的话, I/O 描述了计算机系统与外部设备之间通信的过程。

操作系统

IO操作涉及到由用户态通过系统调用访问内核态。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的。

当应用程序发起 I/O 调用后,会经历两个步骤:

  1. 内核等待 I/O 设备准备好数据

  2. 内核将数据从内核空间拷贝到用户空间。

我们在平常开发过程中接触最多的就是 磁盘 IO(读写文件)网络 IO(网络请求和响应)

Socket编程

Socket是什么呢?

socket是在应用层和传输层中间的抽象层,它把传输层(TCP/UDP)的复杂操作抽象成一些简单的接口,供应用层调用实现进程在网络中的通信。

Socket通信过程

 

  • 服务端和客户端初始化 socket,得到文件描述符;

  • 服务端调用 bind,将绑定在 IP 地址和端口;

  • 服务端调用 listen,进行监听;

  • 服务端调用 accept,等待客户端连接;

  • 客户端调用 connect,向服务器端的地址和端口发起连接请求;

  • 服务端 accept 返回用于传输的 socket 的文件描述符;

  • 客户端调用 write 写入数据;服务端调用 read 读取数据;

  • 客户端断开连接时,会调用 close,那么服务端 read 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭。

Java中3中IO模型

BIO

BIO (Blocking I/O):同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。

 

缺点:

  1. 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。

  2. 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在read 操作上,此时CPU为空闲状态会造成CPU资源浪费。

NIO

NIO (Non-blocking/New I/O):同步非阻塞IO模型中,通过轮询操作,反复调用read,避免了一直阻塞。

缺点:

应用程序不断轮询,查看数据是否已经准备好的过程是十分消耗 CPU 资源的。

基于上述缺点基于NIO的IO多路复用登场。

I/O多路复用

单个线程就可以同时处理多个网络连接。内核负责轮询所有socket,当某个socket有数据到达了,就通知用户进程。

 

  • select

  • poll

  • epoll

select

概述

它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

实现流程

Select 的实现思路很直接,假如程序同时监视如下图的 Sock1、Sock2 和 Sock3 三个 Socket,那么在调用 Select 之后,操作系统把进程 A 分别加入这三个 Socket 的等待队列中。操作系统把进程 A 分别加入这三个 Socket 的等待队列中,当任何一个 Socket 收到数据后,中断程序将唤起进程。

所谓唤起进程,就是将进程从所有的等待队列中移除,加入到工作队列里面,如下图所示:

将进程 A 从所有等待队列中移除,再加入到工作队列里面。经由这些步骤,当进程 A 被唤醒后,它知道至少有一个 Socket 接收了数据。程序只需遍历一遍 Socket 列表,就可以得到就绪的 Socket。

缺点

  1. 有连接限制

  2. 每次select都需要将进程加入到监视socket的等待队列,每次唤醒都要将进程从socket等待队列移除。这里涉及两次遍历操作,而且每次都要将文件列表传递给内核,有一定的开销。

  3. 进程被唤醒后,只能知道有socket接收到了数据,无法知道具体是哪一个socket接收到了数据,所以需要用户进程进行遍历,才能知道具体是哪个socket接收到了数据。

poll

poll本质上和select没有区别, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.

缺点

除了select的第一点解决了其它两点还是没有解决

epoll

epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。时间复杂度降低到了O(1)

通过红黑树和双链表数据结构,并结合回调机制,造就了epoll的高效。

设计思路

措施一:功能分离

select的添加等待队列和阻塞进程是合并在一起的,每次调用select()操作时都得执行一遍这两个操作,从而导致每次都要将fd[]传递到内核空间,并且遍历fd[]的每个fd的等待队列,将进程放入各个fd的等待队列中。

epoll优化的方案是:将添加等待队列和阻塞进程拆分成两个独立的操作,不用每次都去重新维护等待队列,先用 epoll_ctl 维护等待队列,再调用 epoll_wait 阻塞进程。显而易见地,效率就能得到提升。

措施二:就绪列表

Select 低效的另一个原因在于程序不知道哪些 Socket 收到数据,只能一个个遍历。如果内核维护一个“就绪列表”,引用收到数据的 Socket,就能避免遍历。

select会把整个fd[]返回给用户程序,让用户程序自己去遍历哪个fd有接收到网络数据。epoll只会把接收到网络数据的fd[]返回给用户程序,用户程序不用自己去进行遍历查询。

实现流程

创建Epoll对象

调用epoll_create方式时,会创建一个eventpoll对象(),eventpoll 对象也是文件系统中的一员,和 Socket 一样,它也会有等待队列。创建一个代表该 Epoll 的 eventpoll 对象是必须的,因为内核要维护“就绪列表”等数据,“就绪列表”可以作为 eventpoll 的成员。

维护监控对象

创建 Epoll 对象后,可以用 epoll_ctl 添加或删除所要监听的 Socket。eventpoll会通过一个红黑树来存储所有被监视的socket对象,实现快速查找,删除和添加。

eventpoll 对象相当于 Socket 和进程之间的中介,Socket 的数据接收并不直接影响进程,而是通过改变 eventpoll 的就绪列表来改变进程状态。

接受数据

当 Socket 收到数据后,中断程序会给 eventpoll 的“就绪列表”添加 Socket 引用。

阻塞和唤醒进程

当程序执行到 epoll_wait 时,如果 Rdlist 已经引用了 Socket,那么 epoll_wait 直接返回,如果 Rdlist 为空,阻塞进程。

当 Socket 接收到数据,中断程序一方面修改 Rdlist,另一方面唤醒 eventpoll 等待队列中的进程,进程 A 再次进入运行状态

优点

  • 没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);

  • 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll;

  • 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

缺点

  • epoll只能工作在 linux 下

 selectpollepoll
操作方式 遍历 遍历 回调
数据结构 bitmap 数组 红黑树+双向链表
最大连接数 1024(x86)或 2048(x64) 无上限 无上限
最大支持文件描述符数 一般有最大值限制 65535 65535
fd拷贝 每次调用select,都需要把fd集合从用户态拷贝到内核态 每次调用poll,都需要把fd集合从用户态拷贝到内核态 fd首次调用epoll_ctl拷贝,每次调用epoll_wait不拷贝
工作模式 LT LT 支持ET高效模式
工作效率 每次调用都进行线性遍历,时间复杂度为O(n) 每次调用都进行线性遍历,时间复杂度为O(n) 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1)

总结

select为什么在socket连接很多的情况下性能不佳?

将维护socket监控列表和阻塞进程的操作合并在了一起,每次select()调用都会触发这两个操作,从而导致每次调用select()都需要把全量的fd[]列表从用户空间传递到内核空间,内核线程在阻塞进程前需要遍历fd[]将待阻塞的进程放入到每个fd的等待队列里(第一次遍历)。当有网络数据到来时,并不知道网络数据属于具体哪个socket,只知道收到过网络数据,因此需要遍历fd[]唤醒等待队列里的阻塞进程(第二次遍历),并且把fd[]从内核空间拷贝到用户空间,让用户程序自己去遍历fd[]判别哪个socket收到了网络数据(第三次遍历,发生在用户空间)。fd[]比较大的情况,大量的遍历操作会导致性能急剧下降,所以select会默认限制最大文件句柄数为1024,间接控制fd[]最大为1024。

poll其实内部实现基本跟select一样,区别在于它们底层组织fd[]的数据结构不太一样,从而实现了poll的最大文件句柄数量限制去除了

epoll是怎么优化select上述那些问题?

引入了eventpoll这个中间结构,它通过红黑树(rbr)来组织所有待监控的socket对象,实现高效的查找,删除和添加。当收到网络数据时,会触发对应的fd的回调函数,这时不是去遍历各个fd的等待队列进行唤醒进程的操作了,而是把收到数据的socket加入到就绪列表(底层是一个双向链表)。eventpoll有个单独的等待队列来维护待唤醒的进程,避免了像select那样每次需要遍历fd[]来查找各个fd的等待队列的进程。

epoll有这几个核心的数据结构:

红黑树:存放所有待监听的socket

双向链表:存放收到网络数据的socket

等待队列:待唤醒的等待线程

AIO

异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

 

posted @ 2022-07-29 15:25  雙雙  阅读(51)  评论(0编辑  收藏  举报