python磁盘io_聊聊 IO

a0494fb57f34f8689c334adb5e19bda0.png

我一直想写一篇关于 IO 的文章。来厘清现在各种形容词组合搭配的 IO 关系(同步 / 异步,阻塞 / 非阻塞)。

要讲清楚 IO 这坨东西. 将按如下章节说明。

1. user space & kernel space

2. multiplexing (select , epoll,AIO[reactor & proactor ] )

3. 各种 IO 的名称

4. 结语

poll 是对 select 的改进,kqueue 与 epoll 类似。

user space & kernel space

这两跟 IO 有很密切的关系。所以放在最前面说喽。

内核空间(kernel space):系统管理的内存。用户可以通过 system call 操作部分内核空间。

用户空间(user space):用户自己能瞎改的内存。

你把 linux 想像成一个超大的类,它暴露了 api 给你调用。但你不能直接修改它的内核数据。

而对 IO 的操作一般为分两步,比方说普通概念里, read 一个文件。

1. 用户调用 系统 api,系统内核将文件字节读入 kernel space。

2. 从 kernel space, 拷贝数据到 user space。

d325191e696666a7872a43dfad91d683.png

multiplexing

multiplexing 翻成中文技术术语叫多路复用。在 IO 相关的文章里,很常见到。

维基这张图很能说明 multiplexing 这个概念

a5c4f6d849531da888714f78039c7ed0.png

就是 n 个信息打包成一个信息,这一个信息还不会丢失 n 个信息的数据。

举个例子

我们经常用的比特操作:

(0001 | 0010 | 0100) = 0111 = 7

那这算是 3 路复用,得到 7. 而 7 又能还原成 0001,0010 ,0100。

而在网络里。一台服务器,有两个客户 socket 要进来,如果你与其中一个客户建立了连接,在普通情况下,你没法在不断开前一个客户连接的情况下,与另一个客户也建立连接。

让 1 个 socket 能够同时为多个 socket 服务,可以理解为 socket 具有 multiplexing 的能力。

怎么实现呢?解决的方法有很多,比如:

1. 你可以为每一个连接开一个线程。但缺点是连接一多,内存压力太大。

2. 不用线程,也可以在非阻塞的 socket 上一直轮询,但缺点是连接一多,cpu 压力太大。

上面两种方法,在不改 linux kernel 的情况下都能达到 multiplexing 目的。比如经典的 tomcat 在很早前就是用 thread 去实现 multiplexing。

而什么可以解决以上的提到的缺点,我们看看 select

select

select 其实是一个函数的名字。但为交流方便,它现在更像是一种 IO 术语了。

1. application 将 n 个 IO 设备写到 fd_set,然后线程阻塞。

2. kernel 会在某个 IO 设备可读可写,或出错时,在 fd_set 上扫一遍,看有没有相应的 IO 设备。有的话将事件写上,并给一个 syscall 让 线程醒来。

3. 线程醒了也需要再扫一遍,看哪个 IO 设备有新事件了。

4. 如此循环。。

所以,两边都瞎,都需要扫。

select 最多 FD_SETSIZE 个 fd(fd 是一个数字,当它是一个 key,关联着对应的 value,通常是一个 file). FD_SETSIZE 是一个编译器的常量,是在 glibc/bits/typesizes.h 里定义的,描述了能放多少 个 fd 到 fd_set。在 32位下,FD_SETSIZE 通常是 1024. 我们看它的这块 buffer 的数据结构:

struct fd_set {
    int count;
    struct fd_record {
        int fd;
        bool check;
    } fds[FD_SETSIZE];
};

看一下 c 里的接口方法:

# nfds: 你要监控的最大的 fd 号。作用是扫描时,可以确定最大值。 从0 到 ndfs,能少则少点。。
# readfds writefds exceptfds:  分别为需要监控的 fd们(读,写,异常)。
# timeout: select 为阻塞函数,timeout可指定阻塞超时时间。
# 返回值为发生事件的 fd值。
int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

其他语言的封装也大同小异,看一下python

# 返回值略微不同,是 3 个数组。可以同时返回多个相同事件的 list。
def select(self, rlist:list, wlist:list, timeout:list)->(list,list,list):

我们写个 demo,用 python 3写一个微缩版的 EventLoop

# 先在命令行起一个简单的服务器
# nc 命令一般自带,没有的话安装一下 netcat
nc -l -k 1234

运行以下 python 代码

import socket
import select
import sys
connection = socket.socket()
connection.connect(('localhost',1234))

while True:
    readers, _ ,_ = select.select([sys.stdin,connection],[],[])
    for reader in readers:
        if reader is connection:
            print(connection.recv(1000).decode('utf8'))
        else:
            msg = sys.stdin.readline()
            connection.send(msg.encode('utf8'))

就能实现全双工的聊天了,再多点代码,就能多用户跟服务器聊了。。

66e2f3412c403c778b0b77603e50cb94.gif

select 缺点很明显:

1. 连接数量支持仅支持 FD_SETSIZE 个.

2. 连接越多,性能越差.性能为 O(n).

3. 而且每次都得重置这块空间.

4. 需要用户自己读数据。

而 poll 跟 select 很像,解决了 select 的第 1 个缺点,其他三个缺点依旧。select/poll 的性能是 O(N),能不能到 O(1)呢? epoll 登场。

epoll

要规避 select 的第 2 个缺点,就差一个通知了!某个 IO 设备可读可写或出问题,kernel 你直接通知 application 是哪个 IO 设备不就好了!而且顺带着能优化第 3 个缺点。

我们看 epoll 的 python 关键使用代码。

# server_socket 是服务器监听的 socket 
# server_socket.fileno() 代表 IO 设备号,或者更准备点叫文件描述符
# select.EPOLLIN 代表当有数据可读时通知你
# select.EPOLLET 代表只通知一次. 不读下次就可能没了. (ET 表示 Edge Trigger。相对的还有 Level Trigger。区别就是是只投递一次,还是投 n 次直到你处理完)
epoll.register(server_socket.fileno(),select.EPOLLIN | select.EPOLLET)

while True:
# poll()当 poll 有任何 io 事件时,就会返回 fd_events. 这是一个list.
# 每个 item 是一个 fd 与 event 事件. 
    fd_events = epoll.poll()
    for fd,events in fd_events:
        ...

epoll 与 select/poll 用起来其实差不太多。只不过内部实现 epoll 做到了 O(1)。

epoll 有哪些缺点呢?

1. 性能在某些时候也会有问题。如果有 100 个fd 需要更新的话,则需要 100 次系统调用。

2. epoll 只能用在 file 上。然后并不是所有的东西在 unix/linux 是文件,定时器不是文件,信号也不是文件,在 linux 上,网络设备也不是文件。当然你可以将这些模拟成一个文件,但带来了复杂度。

3. 对磁盘文件支持不好。epoll / select /poll 都是基于 readiness model。epoll 可以很好的监控 socket 的可读性。以便接下来的操作不会 block。但磁盘文件只有在内存中不存在缓存时才会 block,模型不匹配。

4. 需要用户自己读数据。

kqueue 相对的解决上面某些问题。但不细说了,因为他们一个等级的。

那相对 select/poll 来说,就一定选 epoll 么。

也不是,epoll 也带来了新的问题,见 linus Torvalds的邮件列表

怎么权衡看官自己决定喽.

AIO

IO 发展滚滚向前。AIO 出现了。 名字酷炫,异步 IO。

因为前面说的 select / poll / epoll 都有一个共同的"缺点"。

需要用户自己读数据。

而 AIO 可以看起来像是要解决这个问题。

为什么这么说。

现在的 AIO 看起来像是内核的实现,其实不然,它是在 glibc 里实现的。

虽然 glibc 并不属于内核,但做为普通用户来说,也只是用 glibc 的接口而已。

AIO 的实现可以有很多种,可以使用信号,可以通过线程,也可以通过回调。

我们看 wiki 上一个例子:

ios = IO.IOService()
device = IO.open(ios)

def inputHandler(data, err):
    "Input data handler"
    if not err:
        print(data)
// readSome 直接返回
device.readSome(inputHandler)
ios.loop() # wait till all operations have been completed and call all appropriate handlers

假如说这段代码是线程 a 在执行,那在 AIO 加持下,inputHandler 一定不是由线程 a 执行的。

如果是由线程 a 执行的inputHandler,那说明这就是 non-blocking sync IO.

讲 AIO 不能不讲一下 reactor & proactor pattern

reactor & proactor pattern

reactor 和 proactor 它们都是 multiplexing 的架构模型。 意思也就是,要实现 multiplexing 这个功能。有哪种架构。

它们的最大的区别,在于系统是否利用了 AIO。

在上面我们写微缩版的 EventLoop, 就是基于 select 实现的 reactor 模型。

所以 AIO 到底带来了啥好处?以致于可以多一个 multiplexing 模型命名出来。我们复习一下

前面说的 IO 的操作一般为分两步,比方说 Input:

1. 内核将输入放入kernel space。

2. 用户从 kernel space。 获得输入数据到 user space。

第 1 步由内核完成。

第 2 步是要占用用户(application)时间的! 如果第 2 步要用很久,select 实现的 reactor 模型虽然可以支持 multiplexing,但一样很慢。但如果是 AIO 机制,第 2 步会也交给内核。是不是就会快很多!内核帮你把数据都写好了,你直接用就行了。

所以 AIO是什么。简单点讲,就是第 2 步由”内核“发起,而非 application 发起。不占用 application 响应时间。

AIO 与其他 IO 模型最大区别在于它占用的不是 application 的时间(资源),它占用的系统时间(资源)。至于整机性能比 select/ poll/ epoll 强不了多少。有时还可能更慢。

关于 reactor & proactor pattern 最初的定义可看看这个 Proactor.pdf,这里面作者就有 ACE 的贡献者,靠谱点。拿几张图。

我们先看一下 reactor client 连接的图:

a7e7b4d7f3a44ee3aa78dc0d745505ac.png

对比一下 proactor client 连接的图:

225131b9851d4154eea96d0f932265b2.png

最大的区别是在第 2 步 proactor 模型就把 Accept 扔给系统了。

总结一把:
1. multiplexing 是一种功能
2. reactor & proactor 是功能的架构
3. select / poll / epoll / AIO ... 是架构的具体实现用到的技术

各种 IO 的名称

现在,我们来看一下,因为上面的技术出现的各种花哨的名字!

sync blocking IO

sync non-blocking IO

async blocking IO

async non-blocking IO

我一直觉得这 4 个组合好牵强.对它们的来源有些好奇。查阅网上资料,维基里有解释,然后IBM developerworks也有篇解释,然后 stackoverflow 上也各种解释.百花齐放.然而各自精彩。。这是wiki的

2a9e3b962c70a24797d8eeeb7b4da65d.png

这是 ibm 的文章

be9244938ad2231c56cee7ac2b9764b7.png

明显矛盾啊。

在 linux 官网我也没找到有关这 4 个组合的解释.

blocking ,non-blocking 的意思还比较没有岐义。等不等。

但 sync async io 官方的定义在哪?还望牛人指点。

目前我觉得可靠的描述就是这本书《The Sockets Networking API: UNIX® Network Programming Volume 1》 这本书作者是 Richard Stevens,参与过很多网络标准的定制,话还是很有份量的。

书里 6.2 章节的图

08dbeb7a057b1bd3da0fda0e340507b3.png

书里说在 POSIX 里对 async io 和 sync io 有明确定义。定义啊! 下图是书里引用的 POSIX 的定义。

877282fde32b526ff301092296a9c738.png

然后我搜了一圈 POSIX 文档。由于这本书是 2003 年出版的。

找到 POSIX 2001-2004年定义如下:

acf4a15ff1c75a3237ec58b13976c8cb.png

找到 POSIX 2001-2018年是这样:

57a6087df3d2db31724f3694fc17e404.png

我们组合看一下。 一开始 async 和 sync io 的意思跟 blocking non-blocking 很类似,可以说是一样的。

后来定义改了,重点在:

process and the I/O operation may be running concrrently.

我猜个原因。 因为单线程也能够non-blocking,比如 c 里的蝇程, 但它是同步的。

再有一个细节是,IO现在一般由DAM硬件单独控制,与cpu process 协作。而古早前,cpu 是要直接负责IO的。

那我们来总结个现在的白话定义。

async IO:process 与 在内核空间的 IO 能够同时运行

non-blocking IO:process 调用内核空间的 IO 函数,能立即返回

之所以加上内核空间,因为在用户空间,你完全可以自己模拟 non-blocking 与 async IO的功能啊。。只是慢啊。

这样再看那 4 个组合,就好理解了。

而我个人也认为 wiki 的图只能说相对更加正确。

所以:啥是非阻塞?异步?阻塞异步?异步非阻塞?你有答案了么。

结语

看完这篇文章,大家应该可以对各种语言或者框架的 IO 亮点有个所以然的了解。

因为它们或多或少就是 select poll epoll AIO 语言层面的封装或者模拟。

JAVA 里的 NIO,基于 NIO 的 Netty 。 Redis ,Nginx 里的 worker,扒开看看,长的都这么像这 4 个祖宗。

再看见单线程并发高到飞起这种描述,心里是不是有点数了哈。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值