IO编程包括: 文件读写 操作 StringIO 和 BytesIO 内存中 操作文件和目录 OS 序列化 json pickling
操作系统内核空间(缓冲区)收发数据: 内核态(内核空间)-------------》用户态用户空间()系统检测到IO操作时切换的速度和效率: 进程切换----------线程切换----------协程切换(最快)文件描述符(套接字对象): 1 是一个非零整数,不会变 2 收发数据的时候,对于接收端而言,数据先到内核空间,然后copy到用户空间,内核空间数据被清掉 对于一个network IO,它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另一个就是系统内核(kernel)。 当一个IO操作发生时,它会经历两个阶段: 等待数据准备 (Waiting for the data to be ready) 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
I/O类型:
同步和异步
阻塞和非阻塞
一次read操作两个阶段:用户空间的进程没有权限访问磁盘的,进程发起IO调用
(1)等待数据准备好:内核从磁盘中的数据加载至内核内存
(2)真正IO的阶段:内核内存数据在复制到进程内存(这个是真正执行IO的阶段)
所以进程需要等待内核到内核内存和内核内存到进程内存的时间
1.同步和异步:synchronous,asyncronnous
关注的是被调用者的消息通知机制,被调用者如何通知调用者,调用者向被调用者进行系统调用函数
同步:调用发出之后不会立即返回,进程会一直询问内核准备好数据没有,但一旦返回,则内核返回最终结果,数据已经从内核内存复制到进程内存了(进程自己去询问内核,直到两阶段都复制完通知进程)没有通知机制
异步:调用发出之后,被调用方立即返回消息,但返回的并非最终结果;被调用者通过状态、通知机制来通知调用者,或通过回调函数来处理(内核自己都把数据复制完了通知进程数据已经复制完)有通知机制
2.阻塞和非阻塞:block,nonblock
关注的是调用者等待被调用者返回调用结果时的状态
阻塞:调用结果返回之前,调用者会被挂起,叫不可中断睡眠,调用者只有在得到返回结果时候才能继续。(两阶段都阻塞)
非阻塞:调用者在结果返回之前不会被挂起,即调用不会阻塞当前进程(第一阶段不阻塞,第二阶段依然阻塞)
**************************************************************************************************************************************************************
同步是进程调用内核,内核从磁盘读取数据存入内核内存这段时间,进程会一直询问内核准备好数据没有,直到全部读进内存准备好了,然后告诉进程来内核内存来复制数据。
异步是进程调用内核,内核从磁盘读取数据存入内核内存这段时间进程是活动的,不需要等待内核准备好数据,当内核从磁盘数据全部读取,然后通知进程过来复制数据到进程内存,或者通过回调函数来处理。
阻塞是进程调用内核,内核从磁盘读取数据的这段时间,进程是被挂起的或者叫不可中断睡眠处于等待状态,只有内核告诉进程准备好之后,才会继续够继续工作(比如从内核内存复制数据)
非阻塞是进程调用内核,内核从磁盘读取数据的这段时间,进程不会被挂起
1 阻塞IO 同步阻塞IO是进程调用内核,内核从磁盘读取数据至内核内存的时候,进程处理等待状态,准备好数据,进程就复活复制数据,第二阶段进程复制数据,进程依然在等待,所以第一第二阶段依然是阻塞的,是同步的IO
2 非阻塞IO
非阻塞是内核不通知进程,进程会几秒钟询问内核数据准备好了没,时间都花在询问的过程了,叫盲等待,第一段非阻塞,但第二阶段依然是阻塞状态,从内核内存复制到进程内存,进程依然是等待状态处于阻塞状态,所以比起阻塞依然没有太大优势,是同步的IO
服务端:
1 import time
2 import socket
3 sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
4
5 sk.bind(('127.0.0.1',6667))
6 sk.listen(5)
7 sk.setblocking(False)
8 while True:
9 try:
10 print ('waiting client connection .......')
11 connection,address = sk.accept() # 进程主动轮询
12 print("+++",address)
13 client_messge = connection.recv(1024)
14 print(type(client_messge,'utf8'))
15 connection.close()
16 except Exception as e:
17 print (e)
18 time.sleep(4)
客户端:
1 import time
2 import socket
3 sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
4 sk.connect(('127.0.0.1',6667))
5 while True:
6
7
8 sk.sendall('hello'.encode("utf8"))
9 data = sk.recv(1024)
10 print(data.decode('utf8'))
优点:能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在同时执行)。缺点:任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。3 IO多路复用 IO复用指的是任何一个进程,在某种情况下进程自身只能处理一个IO,但是web是要处理两路IO的,一个是网络IO,另外一个磁盘IO,内核中开发了多路IO或IO复用,进程需要调用IO的时候,都把IO请求交给内核中的select()函数或者poll()函数,最大不能超过1024个,如果超过1024个性能会减少,函数能够处理两个IO,处理网络和磁盘IO
I/O多路复用指:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
注意:网络操作、文件操作、终端操作等均属于IO操作, 网络IO中:windows只支持Socket操作,其他系统支持其他网络IO操作,但是无法自动检测 普通文件 自从上次读取之后是否已经变化。 注意1:select函数返回结果中如果有文件可读了,那么进程就可以通过调用accept()或recv()来让kernel将位于内核中准备到的数据copy到用户区。注意2: select的优势在于可以处理多个连接,不适用于单个连接 select方法
1 句柄列表11, 句柄列表22, 句柄列表33 = select.select(句柄序列1, 句柄序列2, 句柄序列3, 超时时间)
2
3 参数: 可接受四个参数(前三个必须)
4 返回值:三个列表
5
6 select方法用来监视文件句柄,如果句柄发生变化,则获取该句柄。
7 1、当 参数1 序列中的句柄发生可读时(accetp和read),则获取发生变化的句柄并添加到 返回值1 序列中
8 2、当 参数2 序列中含有句柄时,则将该序列中所有的句柄添加到 返回值2 序列中
9 3、当 参数3 序列中的句柄发生错误时,则将该发生错误的句柄添加到 返回值3 序列中
10 4、当 超时时间 未设置,则select会一直阻塞,直到监听的句柄发生变化
11 当 超时时间 = 1时,那么如果监听的句柄均无任何变化,则select会阻塞 1 秒,之后返回三个空列表,如果监听的句柄有变化,则直接执行。
利用select实现伪同时处理多个Socket客户端请求:服务端
1 import socket
2 import select
3 sk = socket.socket()
4 sk.bind(('127.0.0.1',8801))
5 sk.listen(5)
6 inputs = [sk,]
7 while True:
8 r ,w, e = select.select(inputs,[],[]) #[sk,conn1,conn2,conn3....] 只返回有改变的socket和链接对象
9 for obj in r:
10 if obj == sk:
11 conn,addr = obj.accept()
12 print(conn)
13 inputs.append(conn)
14 else: #将列表中有改变的链接对象循环处理
15 try : #其中一个连接突然断掉时的处理机制 Windows
16 data = obj.recv(1024)
17 # if not data:
18 # inputs.remove(obj)
19 # continue
20 print(data.decode('utf8'))
21 inp = input('>>> :')
22 obj.sendall(inp.encode('utf8'))
23 except Exception:
24 inputs.remove(obj)
25 print(r)
26 # 注意1:select函数返回结果中如果有文件可读了,那么进程就可以通过调用accept()或recv()来让kernel将位于内核中准备到的数据copy到用户区。
27 #
28 # 注意2: select的优势在于可以处理多个连接,不适用于单个连接
利用select实现伪同时处理多个Socket客户端请求:客户端
1 import socket
2 sk = socket.socket()
3 sk.connect(('127.0.0.1',8080))
4 while True:
5 inp = input('>>')
6 sk.sendall(inp.encode('utf8'))
7 data = sk.recv(1024)
8 print(data.decode('utf8'))
此处的Socket服务端相比与原生的Socket,他支持当某一个请求不再发送数据时,服务器端不会等待而是可以去处理其他请求的数据。但是,如果每个请求的耗时比较长时,select版本的服务器端也无法完成同时操作selectors模块服务端:
1 import selectors
2 import socket
3 sock = socket.socket()
4 sock.bind(('127.0.0.1',8801))
5 sock.listen(5)
6
7 sel = selectors.DefaultSelector() #根据平台选择最佳的IO多路复用机制,例如 linux系统 选择epoll 机制
8
9 def accept(sock,mask):
10 conn,addr = sock.accept()
11 print('accepted',conn,'from',addr)
12
13 sel.register(conn,selectors.EVENT_READ,read) #注册 相当于添加到列表
14 def read(conn,mask):
15 try:
16 data = conn.recv(1000)
17 print(data.decode('utf8'))
18 data2 = input('>>')
19 conn.send(data2.encode('utf8'))
20 except Exception:
21 sel.unregister(conn)
22
23
24 sel.register(sock,selectors.EVENT_READ,accept) #注册事件
25
26 while True:
27 events = sel.select() #监听 [(key1,mask),(key2,mask),(key3,mask),]
28 for key,mask in events:
29 print(key.data) #accept read
30 print(key.fileobj) #sock conn
31 key.data(key.fileobj,mask) #触发函数
4 异步IO 用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后, 首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切 都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了 五种IO模型的比较
select/poll/epoll
select:本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理
缺点:1.单个进程可见识的fd数量被限制,即能监听端口的大小有限
cat /proc/sys/fs/file-max
2.对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低
3.需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
poll:本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态;其没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:
1. 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意
2. poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd
epoll:支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次;使用"事件"的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知
优点:
1.没有最大并发连接的限制,能打开的fd的上限远大于1024(1G的内存上能监听越10万个端口)
2.效率提升:非轮询的方式,不会随着fd数目的增加而效率下降;只有活跃可用的fd才会调用callback函数,即epoll最大的优点就在于它只管理"活跃"的连接,而跟连接总数无关
内存拷贝,一用nmap() 文件映射内存加速与内核空间的消息传递;即epoll使用nmap减少复制开销
通过3个函数实现 最大连接数没有上限 第一个函数:创建epoll句柄,将所有的fd拷贝到内核空间,只需要拷贝一次 第二个函数 :回调函数 某一函数或者动作完成之后会触发的函数 为所有的fd绑定一个回调函数,一旦有数据访问则触发该回调函数 回调函数将fd放到链表中 第三个函数 判断链表是否为空 补充:
1 select 2
3 select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。 4 select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。 5 select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。 6 另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。 7
8 poll 9
10 poll在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。 11 poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。 12 另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。 13
14 epoll 15
16 直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。 17 epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。 18 epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,
这样便彻底省掉了这些文件描述符在系统调用时复制的开销。 19 另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,
内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。