I/O之随笔

6 篇文章 0 订阅
3 篇文章 0 订阅

开篇

1. RocketMQ底层实现竟然是Netty?

2. Kafka通讯采用基于tcp的socket方式,响应式模型,何为响应式模式?

3. Redis的单线程是是采用什么样的IO实现?

4. Netty的底层实现是基于NIO,和Linux系统底层NIO一样吗?

5. 怎么理解阻塞式IO和非阻塞式IO?

6. java的BIO、NIO、AIO区别?

7. 那epoll和select又是什么?

学习最好的方式就是带着问题思考,下面一起分析下。

一. 什么是IO?

传统的 IO 大致可以分为4种类型,拿java举例:

  • InputStream、OutputStream 基于字节操作的 IO
  • Writer、Reader 基于字符操作的 IO
  • File 基于磁盘操作的 IO
  • Socket 基于网络操作的 IO

二. Linux系统五大IO模型

什么是IO模型?

一个输入操作最基本包括两个阶段,以取快递为对比:

  • 等待数据准备好——在门口等待快递送到家
  • 从内核向进程复制数据——把快递拿到屋里

Linux 有五种 I/O 模型:

  • 阻塞式 I/O
  • 非阻塞式 I/O
  • I/O 复用(select 和 poll)
  • 信号驱动式 I/O(SIGIO)
  • 异步 I/O(AIO)

1. 阻塞IO

应用进程通过系统调用recvfrom接收数据,但由于内核还未准备好数据,应用进程就会阻塞住,直到内核准备好。recvfrom完成数据复制,应用进程结束阻塞。也就是说 wait for datacopy data 两个阶段都被阻塞。

此场景就是你坐在家门口,傻傻等者收快递。

2. 非阻塞IO

等待快递的时候是不是可以做点其他事呢,当然是可以的。只要保证定时出门去看下快递员到没到就好了。

映射到Linux操作系统中,就是非阻塞IO模型。应用进程与内核交互,数据未达到之前,不再傻傻等着,而是直接返回。然后通过轮询方式,不停的去看内核数据有没有准备好。如果发现数据到位,那就拷贝到用户空间。

底层实现是应用进程通过recvfrom调用不停的去和内核交互,知道内核准备好数据。如果还没准备好,内核会返回error信号,应用进程收到后返回,下次再来请求。这个期间进程就可以做点别的事情。

3. 信号驱动IO

其实不断惦记去看快递员到没到也挺烦的,那可不可以不用经常去看。快递员到了打个电话不就解决了吗。

这就是信号驱动IO,应用进程在读取文件时通知内核,如果某个socket的某个事件发生时,给我打个电话(发个信号),收到信号后,对应的函数再去做相应的处理。

底层实现是应用进程先向内核注册一个信号处理函数,然后用户进程返回,期间没有阻塞。当内核数据准备好后会发一个信号给进程,用户进程收到信号后便开始拷贝数据到用户空间。

4. IO复用模型

假设都是老年人没有电话,只能家门口等着。而快递员为了提高效率,拿着小本本记下了所有用户的住址姓名,一次可以送多个用户。这就是一个一群大妈在家楼口等一个快递小哥的故事。

此为IO复用模型,多个进程的IO可以注册到同一个管道上,这个管道会统一和内核进行交互。当管道中的某一个请求需要的数据准备好后,进程再把对应的数据拷贝到用户空间。

IO多路复用是多了一个select函数,多个进程的IO可以注册到同一个select上,当用户进程调用该select,select会监听所有注册好的IO,如果所有被监听的IO需要的数据都没有准备好,select调用进程会阻塞。当任意一个IO所需要的数据准备好,select调用就会被返回,然后进程再通过recvfrom来进行拷贝。这个方式并没有向内核注册信号处理函数,所以并不是非阻塞的。

以上的小本本就代表了select函数,所有快递用户都要记录在上面。

以上四种方式都是同步的,原因在于无论以上的哪种模型,真正的数据拷贝都是同步进行的。唯一存在疑问的就是信号驱动IO,这种方式是在内核发信号之前是异步的,但是数据拷贝期间依然是同步的,所以整个IO也不能认为是异步的。

5. 异步IO模型

老年人生病了怎么办,腿脚不利索,孩子都在工作。那就是有好心的快递小哥直接送货到家,门都不用出。

应用进程把IO请求传给内核后,完全由内核去操作文件拷贝。内核完成相关操作后,会发信号告诉应用进程本次IO已经完成。

在底层,用户进程发起aio_read操作之后,给内核传递描述符、缓冲区指针、缓冲区大小等,告诉内核当整个操作完成时,如何通知进程,然后就立即去做其他事了。当内核收到aio_read后,会立刻返回,然后内核开始等待数据准备,数据准备好以后,直接把数据拷贝到用户空间,然后再通知进程本次IO完成。

三. 什么是select/poll/epoll?

这三兄弟都是 I/O 多路复用的具体实现,select 出生最早,之后是 poll,再是 epoll。

int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

1. select

select 允许应用程序监视一组文件描述符,等待一个或者多个描述符成为就绪状态,从而完成 I/O 操作。

  • fd_set 使用数组实现,数组大小使用 FD_SETSIZE 定义,所以只能监听少于 FD_SETSIZE 数量的描述符。有三种类型的描述符类型:readset、writeset、exceptset,分别对应读、写、异常条件的描述符集合。

  • timeout 为超时参数,调用 select 会一直阻塞直到有描述符的事件到达或者等待的时间超过 timeout。

  • 成功调用返回结果大于 0,出错返回结果为 -1,超时返回结果为 0。

2. poll

int poll(struct pollfd *fds, unsigned int nfds, int timeout);

poll 的功能与 select 类似,也是等待一组描述符中的一个成为就绪状态。

两者对比:

1. 功能

select 和 poll 的功能基本相同,不过在一些实现细节上有所不同。

  • select 会修改描述符,而 poll 不会;
  • select 的描述符类型使用数组实现,FD_SETSIZE 大小默认为 1024,因此默认只能监听少于 1024 个描述符。如果要监听更多描述符的话,需要修改 FD_SETSIZE 之后重新编译;而 poll 没有描述符数量的限制;
  • poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高。
  • 如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定。

2. 速度

select 和 poll 速度都比较慢,每次调用都需要将全部描述符从应用进程缓冲区复制到内核缓冲区。

3. 可移植性

几乎所有的系统都支持 select,但是只有比较新的系统支持 poll。

3. epoll

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符。

从上面的描述可以看出,epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次,并且进程不需要通过轮询来获得事件完成的描述符。

epoll 仅适用于 Linux OS。

epoll 比 select 和 poll 更加灵活而且没有描述符数量限制。

epoll 对多线程编程更有友好,一个线程调用了 epoll_wait(), 另一个线程关闭了同一个描述符,也不会产生像 select 和 poll 的不确定情况。

四. Netty的实现NIO

BIO、NIO、AIO

  • BIO 就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用时可靠的线性顺序。它的有点就是代码比较简单、直观;缺点就是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。
  • NIO 是 Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。
  • AIO 是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

所以NIO其实是New I/O,机制和Linux的多路复用IO是一样的,在linux系统上,Netty使用的是epoll系统调用。

五. 阻塞和非阻塞

阻塞与非阻塞主要是从 CPU 的消耗上来说的。

阻塞就是 CPU 停下来等待一个慢的操作完成 CPU 才接着完成其它的事。

非阻塞就是在这个慢的操作在执行时 CPU 去干其它别的事,等这个慢的操作完成时,CPU 再接着完成后续的操作。

虽然表面上看非阻塞的方式可以明显的提高 CPU 的利用率,但是也带了另外一种后果就是系统的线程切换增加。

六. 什么是Socket

socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口,通过Socket,我们才能使用TCP/IP协议。

实际上,Socket跟TCP/IP协议没有必然的联系。Socket编程接口在设计的时候,就希望也能适应其他的网络协议。所以说,Socket的出现只是使得程序员更方便地使用TCP/IP协议栈而已,是对TCP/IP协议的抽象,从而形成了我们知道的一些最基本的函数接口,比如create、listen、connect、accept、send、read和write等等。

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值