Cwww3's Blog

Record what you think

0%

IO

I/O

  • 可以进行I/O操作的内核对象

  • 文件、管道、套接字等都是流

  • 流的入口:文件描述符(fd)

    I/O操作

  • 对流的读写操作称为I/O操作

同步异步

  • 同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。
  • 异步就是发一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时可以处理其他的请求,被调用者通常依靠事件、回调等机制来通知调用者其返回结果。

阻塞和非阻塞

  • 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。不占用CPU资源。
  • 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他的事情。占用CPU资源(轮询)

同步异步与阻塞非阻塞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 老张烧开水的故事
# 老张爱喝茶,废话不说,煮开水。
# 出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。

# 同步阻塞
老张把水壶放到火上,立等水开。
老张觉得自己有点傻

# 同步非阻塞
老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。
老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~的噪音。

# 异步阻塞
老张把响水壶放到火上,立等水开。
老张觉得这样傻等意义不大

# 异步非阻塞
老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。
老张觉得自己聪明了。

# 所谓同步异步,只是对于水壶而言
普通水壶:同步;响水壶:异步。
虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了,这是普通水壶所不能及的。
同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。

# 所谓阻塞非阻塞,仅仅对于老张而言
立等的老张:阻塞;看电视的老张:非阻塞。
虽然情况3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。

阻塞IO - Blocking IO

image-20211118105721114
  • 一请求一应答
  • 通常由一个独立的 Acceptor 线程负责监听客户端的连接。我们一般通过在 while(true) 循环中服务端会调用 accept() 方法等待客户端连接的方式监听请求,请求一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待当前连接的客户端的操作执行完成,不过可以通过多线程来支持多个客户端的连接
image-20211118103446987

伪异步 I/O

当客户端并发访问量增加后阻塞IO模型会出随着并发访问量增加会导致线程数急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务。

线程是宝贵的资源。线程的创建和销毁成本很高,线程本身占用较大内存,线程的切换成本也很高,容易造成锯齿状的系统负载。

  • 解决方案

后端通过一个线程池来处理多个客户端的请求接入,设置线程的最大值,防止由于海量并发接入导致线程耗尽。

image-20211118105303658

非阻塞IO - NoneBlocking IO

当用户线程发起一个 IO 操作后,并不需要等待,而是马上就得到一个结果。如果结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 IO 操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。

在非阻塞IO 模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。

image-20211118110037961
  • 非阻塞式主要体现在用户进程发起recvfrom系统调用的时候,这个时候系统内核还没有接收到数据报,直接返回错误给用户进程,告诉“当前还没有数据报可达,晚点再来”
  • 用户进程接收到信息,但是用户进程不知道什么时候数据报可达,于是就开始不断轮询(polling)向系统内核发起recvfrom的系统调用“询问数据来了没”,如果没有则继续返回错误
  • 用户进程轮询发起recvfrom系统调用直至数据报可达,这个时候需要等待系统内核复制数据报到用户进程的缓冲区,复制完成之后将返回成功提示

IO多路复用 - IO multiplexing

所谓 I/O 多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。这种机制的使用需要 selectpollepoll 来配合。

image-20211118110236775

在多路复用IO模型中,会有一个内核线程不断地去轮询多个 socket 的状态,只有当真正读写事件发送时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有真正有读写事件进行时,才会使用IO资源,所以它大大减少来资源占用

  • IO复用模式是使用select或者poll函数向系统内核发起调用,阻塞在这两个系统函数调用,而不是真正阻塞于实际的IO操作(recvfrom调用才是实际阻塞IO操作的系统调用)
  • 阻塞于select函数的调用,等待数据报套接字变为可读状态
  • 当select套接字返回可读状态的时候,就可以发起recvfrom调用把数据报复制到用户空间的缓冲区

信号驱动IO - signal driven IO

在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。这个一般用于UDP中,对TCP套接字几乎没用,原因是该信号产生得过于频繁,并且该信号的出现并没有告诉我们发生了什么请求。

image-20211118110908521

异步IO - asynchronous IO

前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是从内核拷贝数据到用户态的过程都会让用户线程阻塞。

image-20211118110819248
  • 由POSIX规范定义,告知系统内核启动某个操作,并让内核在整个操作包含数据等待以及数据复制过程的完成之后通知用户进程数据已经准备完成,可以进行读取数据;
  • 与上述的信号IO模型区分在于异步是通知我们何时IO操作完成,而信号IO是通知我们何时可以启动一个IO操作

IO模型对比

image-20211118111021556

处理多I/O请求

阻塞+多进程/多线程

非阻塞+轮询

多路复用

  • select 最多监听1024个 且不会通知具体哪个流接收到数据 需要遍历全部流,进行处理。 平台无关性。
1
2
3
4
5
6
7
8
9
for {
select() //阻塞

for _, in := ins {
if has {
// 处理
}
}
}
  • poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.

  • epoll 监听的数量与操作系统能打开的文件数相同,且返回收到数据的流。不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。Linux操作系统的方法。

1
2
3
4
5
6
7
for {
ins = epoll() //阻塞

for _, in := ins {
// 处理
}
}
  • image-20211118111811931
Donate comment here.