3.IO

予早 2024-10-05 11:22:27
Categories: Tags:

IO

I/O指从输入设备读取数据到内存,经处理后,由内存中写入到输出设备的过程。

磁盘和网卡均为输入设备兼输出设备。

程序正常退出可自动关闭文件

编解码,所有的文件都是二进制文件,即字节流,但是二进制文件一些特殊的可以是文本文件,即字符流

缓冲IO与直接IO

缓冲I/O,Buffered I/O

直接I/O,Direct I/O

磁盘IO与网络IO

磁盘IO:以磁盘为IO设备,即从磁盘读取、向磁盘写入,通常采用缓冲IO,根据应用程序需求会选用直接IO。磁盘数据通常由文件系统抽象为文件,由此亦称文件IO。

网络IO:以网卡为IO设备,即从网卡读取、向网卡写入,均采用缓冲IO。网卡数据由TCP/IP四层生产模型层层抽象,其中网络接口层、网络层、传输层三层向上最终在传输层之上整体抽象为socket,应用层即应用程序所处分层,由此亦称Socket IO。

IO in Python

缓冲IO和直接IO的选用取决于应用程序的需求,一般情况使用缓冲IO,若需要避免数据缓存或需要进行低层磁盘操作则使用直接IO

Python中,以open为代表的文件操作为缓冲IO,基于mmap模块可以实现文件的直接IO,网络IO均为缓冲IO

磁盘IO模型

网络IO模型

网络IO采用缓冲IO方式,缓冲IO方式主要思路为:读取时,由应用进程发起读数据系统调用,内核线程先从从网卡读取数据到内核缓冲区,然后将数据从内核缓冲区复制到应用进程内存,写入与之相反。

显然网络IO过程中数据主要存在三个地方,网卡、操作系统内核缓冲区、应用程序内存,以读数据过程为例,第一阶段数据由网卡到内核缓冲区,第二阶段由内核缓冲区到网卡。两个阶段分别采用不同策略组合构成五种网络IO模型。

注意:不要先入为主地将物种IO模型名字中的阻塞非阻塞作为实现同步和异步的手段,而实际上,阻塞非阻塞与同步异步是两种不同范畴的内容,阻塞和非阻塞说的是应用程序从内核缓冲区获取数据这个阶段是否阻塞,而同步异步是说两个阶段是否阻塞

https://zhuanlan.zhihu.com/p/473639031

Blocking IO Model,阻塞IO模型

  1. 应用进程执行recvfrom函数发起读数据系统调用,并在数据获取完成之前阻塞
  2. 内核缓冲区未准备好数据就等待数据传输
  3. 内核缓冲区数据准备好后由内核线程将数据从内核缓冲区复制到应用程序

NonBlocking IO Model,非阻塞IO模型

  1. 应用进程执行recvfrom函数发起读数据系统调用,并直接返回数据是否准备好
  2. 内核缓冲区未准备好数据就等待数据传输,应用程序则反复调用recvfrom直至数据准备好
  3. 内核缓冲区数据准备好后由内核线程将数据从内核缓冲区复制到应用程序

IO Multiplexing Model,IO多路复用模型

  1. 应用进程执行select、poll或epoll监控多个socket文件描述符,在任意一个套接字数据准备好之前进程阻塞
  2. 内核缓冲区未准备好数据就等待数据传输
  3. 内核缓冲区数据准备好后返回可读信息
  4. 应用进程执行recvfrom函数发起读数据系统调用
  5. 由内核线程将数据从内核缓冲区复制到应用程序

IO多路复用方式:

Python中的select模块专注于I/O多路复用,提供了select poll epoll三个方法(其中后两个在Linux中可用,windows仅支持select),另外也提供了kqueue方法(freeBSD系统)。

Signal Driven IO Model,信号驱动IO模型

  1. 应用进程进行sigaction系统调用并返回
  2. 应用进程继续执行
  3. 内核等待数据准备好,准备好后发送SIGIO信号
  4. 应用进程执行recvfrom函数发起读数据系统调用
  5. 由内核线程将数据从内核缓冲区复制到应用程序

Asynchronous IO Model,异步IO模型

  1. 应用进程调用aio_read函数,告知内核线程socket描述符后返回
  2. 应用进程不阻塞
  3. 内核等待数据,数据准备好后将数据由内核空间复制到应用进程
  1. 第一阶段为从网卡到内核缓冲区,第二阶段为从内核缓冲区到应用程序
  2. 若当程序读取数据时无论是在第一阶段阻塞还是第二阶段阻塞亦或两个阶段都阻塞,都是在保证应用进程和内核线程间同步通信,故阻塞IO、非阻塞IO、IO多路复用、信号驱动IO均为同步IO,而只有异步IO实质上并未在应用进程和内核线程间同步通信
  3. 对于同步IO,在第二阶段应用进程均由于recvfrom系统调用而阻塞(等待内核复制数据),根据第一阶段不同实现方式分为阻塞IO、非阻塞IO、IO多路复用、信号驱动IO,阻塞IO即第一阶段应用进程阻塞,非阻塞IO即第一阶段不阻塞而采用轮询方式,IO多路复用第一阶段应用程序阻塞但监控多个socket,信号驱动IO采用异步方式
  4. 对于异步IO,第一阶段和第二阶段均为异步方式
  5. 阻塞IO和非阻塞IO名称中“阻塞”和“非阻塞”仅指第一阶段对内核线程等待数据期间应用进程采取的策略。

文件读写

open

open()为内置函数,用于读写文件

mode

示例文本

file.txt12
你好9
世界9
。6
HELLO WORLD15
不念过往, 不畏将来30

读写文件

f = open('./file.txt', mode='r+', encoding='utf-8')  # Unicode字符集,UTF-8编码方式
print(f.readable())  # True 是否可读
print(f.writable())  # True 是否可写

print(f.tell())  # 0 光标位置,按字节,最开始是0,0表示所有内容之前,不指向任何字节
print(f.read(11))  # file.txt12 按字符数量读取,默认读取所有,示例文本中第一行有12个字节
print(f.tell())  # 12 英文字母和标点占一个字节、换行(Windows CRLF)占两个字节
print(f.seek(15, 0))  # 15 移动光标到第15个字节 whence 0 :将开头作为参考位置 1 :将当前作为参考位置 2 :将末尾作为参考位置
print(f.tell())  # 15
print(f.readline())  # 好9 从当前位置向后读取一行,并不一定是完整一行 中文占3个字节
print(f.tell())  # 21
print(f.readlines())  # ['世界9\n', '。6\n', 'HELLO WORLD15\n', '不念过往, 不畏将来30'] 从当前位置向后读取若干行
print(f.tell())  # 79
print(f.seek(0))  # 0 回到最开始
print(f.write("不念过往, 不畏将来\n\n"))  # 12 写入12个字符,写30个字节,写多少覆盖多少
print(f.tell())  # 30
print(f.truncate(12))  # 23 截断文件,返回截断后的文件字节数,在文件某个位置截断,若文件末尾在截断位置之后,截断位置之后所有内容清除,若文件末尾在截断位置之前,不足的部分补空格,默认在当前光标位置截断,size = f.tell()
print(f.tell())  # 30 截断操作不改变光标位置
print(f.truncate())  # 30 不足补充空格
print(f.tell())  # 30 光标位置不变
f.close()  # 关闭文件


# 最终示例文本会变为"不念过往                  "(后面是空格共18个)。

注:

  1. 在Python中\n表示换行,但针对不同平台python的\n对应不同的字符。
  2. Windows CRLF \r\n、Unix LF \n、MacOS CR \r
  3. 把Windows的一个文本文件直接上传Linux,再打开会出现格式问题。
  4. Git作为分布式版本控制系统,同一份代码要在各个平台协作开发,其实现了代码源文件中换行字符的自动转换。
  5. 上述源码在WIndows下运行没问题,在Linux或MacOS运行需要根据上述规则修改光标移动参数。

用户交互

# 用户交互
id = input('请输入ID:')
print(id, type(id))

序列化

序列化是将内存中的对象转换成一定格式的二进制数据或文本数据,便于存储在文件中或者通过网络传输,后续根据该格式可从文件或者网络读入数据并重建对象的过程称之为反序列化。

json

https://www.json.org/json-en.html

# 序列化为json
import json

d = {1: "a", 2: "b"}
print(d, type(d))
d_dump = json.dumps(d)
print(d_dump, type(d_dump))
d_rd = json.loads(d_dump)
print(d_rd, type(d_rd))

d = {1: "键值对", 2: "b"}
f = open("./dump.txt", "w", encoding="utf-8")
json.dump(d, f, ensure_ascii=False)
f.close()
f = open("./dump.txt", encoding="utf-8")
d = json.load(f)
f.close()
print(d, type(d))

pickle

# 序列化为pickle定义的二进制格式
import pickle

d = {1: "a", 2: "b"}
print(d, type(d))
d_dump = pickle.dumps(d)
print(d_dump, type(d_dump))
d_rd = pickle.loads(d_dump)
print(d_rd, type(d_rd))

d = {1: "细节", 2: "b"}
f = open("./dump.txt", "wb")
pickle.dump(d, f)
f.close()
f = open("./dump.txt", "rb")
d = pickle.load(f)
f.close()
print(d, type(d))

shelve

# 序列化为shelve定义的二进制格式
import shelve

f = shelve.open('open_file')
f['key'] = {'int': 11, 'float': 1.2, 'string': 'ssd'}
f.close()

f = shelve.open('open_file', flag='r')
e = f['key']
f.close()
print(e)

ini文件操作

import configparser

config = configparser.ConfigParser()
config["DEFAULT"] = {
    "ServerAliveInterval": 45,
    "Compression": "yes"
}
config["bitbucket.org"] = {"user": "da"}

with open('config.ini', 'w') as configfile:
    config.write(configfile)

config = configparser.ConfigParser()
config.read('config.ini')
print(config.sections())
print("bitbucket.org" in config)
print("b.org" in config)
print(config["bitbucket.org"]["user"])
print(config["DEFAULT"]["Compression"])
config.add_section("add-section")
config.remove_section("bitbucket.org")
config.set("add-section", "k1", "111")
config.set("add-section", "k2", "222")

toml

# 这是一个 TOML 文档

title = "TOML 示例"

[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00

[database]
enabled = true
ports = [8000, 8001, 8002]
data = [["delta", "phi"], [3.14]]
temp_targets = { cpu = 79.5, case = 72.0 }

[servers]

[servers.alpha]
ip = "10.0.0.1"
role = "前端"

[servers.beta]
ip = "10.0.0.2"
role = "后端"
# 注释
# 最基本元素是键值对
# https://toml.io/cn/
#
# 键可以分为裸键和引号键,裸键仅由纯ASCII字符构成且不能为空,引号键由单引号或双引号以及Unicode字符构成且允许为空。
#
# 值可以分为基本类型和容器类型,基本类型有布尔值、整数、浮点数、字符串、日期时间,容器类型有数组、表(一般表和内联表)、表数组
#
# BNF
# https://www.jianshu.com/p/15efcb0c06c8


import tomllib

print(tomllib.load(open("demo.toml","rb")))

print(tomllib.loads("""
title = "TOML 示例"
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00
"""))

数据压缩

https://docs.python.org/zh-cn/3/library/zipfile.html

import zipfile

ret_zip_path = 'xxx_xxx_xxx_2023-09-08.zip'
with zipfile.ZipFile(ret_zip_path, mode="w", compression=zipfile.ZIP_DEFLATED) as ret_zip:
    ret_zip.write('xxx_xxx_xxx_2023-09-08.xls', arcname='xxx_xxx_xxx_2023-09-08.xls')
import zipfile

f = zipfile.ZipFile("file.zip", 'r')

# 会返回压缩包内所有文件名的列表
print(f.namelist())

# 指定文件详细信息
print(f.getinfo("__pycache__/lua_script_for_redis.cpython-311.pyc"))

# 所有文件详细信息
print(f.infolist())

# 解压文件
for file in f.namelist():
    f.extract(file, r'e:/work')  # 在d:/Work中解压文件
f.close()

# w覆盖压缩,会删除其他文件,a追加压缩
f = zipfile.ZipFile("file.zip", 'a')

f.write("./zipfile_demo.py", "zipfile_domo.py", zipfile.ZIP_DEFLATED)
f.close()


# ZipFile.getinfo(name) 方法返回的是一个ZipInfo对象,表示zip文档中相应文件的信息。它支持如下属性:
#
# ZipInfo.filename: 获取文件名称。
# ZipInfo.date_time: 获取文件最后修改时间。返回一个包含6个元素的元组:(年, 月, 日, 时, 分, 秒)
# ZipInfo.compress_type: 压缩类型。
# ZipInfo.comment: 文档说明。
# ZipInfo.extr: 扩展项数据。
# ZipInfo.create_system: 获取创建该zip文档的系统。
# ZipInfo.create_version: 获取 创建zip文档的PKZIP版本。
# ZipInfo.extract_version: 获取 解压zip文档所需的PKZIP版本。
# ZipInfo.reserved: 预留字段,当前实现总是返回0。
# ZipInfo.flag_bits: zip标志位。
# ZipInfo.volume: 文件头的卷标。
# ZipInfo.internal_attr: 内部属性。
# ZipInfo.external_attr: 外部属性。
# ZipInfo.header_offset: 文件头偏移位。
# ZipInfo.CRC: 未压缩文件的CRC-32。
# ZipInfo.compress_size: 获取压缩后的大小。
# ZipInfo.file_size: 获取未压缩的文件大小。