阻塞,非阻塞,同步IO,异步IO

2017-07-10  本文已影响82人  狗狗胖妞

概念说明

用户空间与内核空间

现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。

文件描述符fd

文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。

文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

缓存 I/O

缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间

缓存 I/O 的缺点:(内核态与用户态)
数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。

阻塞,非阻塞,同步IO,异步IO

参考文章:http://blog.csdn.net/baixiaoshi/article/details/54933902
http://www.cnblogs.com/alex3714/articles/5876749.html

说一下IO发生时涉及的对象和步骤。
对于一个network IO (这里我们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另一个就是系统内核(kernel)。当一个read操作发生时,它会经历两个阶段:
1 等待数据准备 (Waiting for the data to be ready)
2 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
记住这两点很重要,因为这些IO Model的区别就是在两个阶段上各有不同的情况。

blocking IO
non-blocking IO
IO multiplexing
Asynchronous I/O
总结

现在回过头来回答最初的那几个问题:blocking和non-blocking的区别在哪,synchronous IO和asynchronous IO的区别在哪。
调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。

在说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。Stevens给出的定义(其实是POSIX的定义)是这样子的:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operationcompletes;
An asynchronous I/O operation does not cause the requesting process to be blocked;

两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。有人可能会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,在这段时间内,进程是被block的。而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。

各个IO Model的比较如图所示:


I/O 多路复用之select、poll、epoll详解

'''IO指的是输入输出,一部分指的是文件操作,还有一部分
网络传输操作,例如soekct就是其中之一;多路复用指的是
利用一种机制,同时使用多个IO,例如同时监听多个文件句
柄(socket对象一旦传送或者接收信息),一旦文件句柄出
现变化就会立刻感知到
'''
参考文章:http://www.cnblogs.com/alex3714/p/4372426.html

python select

I/O多路复用是在单线程模式下实现多线程的效果,实现一个多I/O并发的效果。

先看一个简单的socket例子:

import socket

client = socket.socket()
client.connect(('localhost', 9900))
while True:
    data = input('>>:').strip()
    if not data:
        continue
    client.send(data.encode())
    print(client.recv(1024))

以上为一个简单的客户端发送一个输入信息给服务端的socket通信的实例,在以上的例子中,服务端是一个单线程、阻塞模式的。如何实现多客户端连接呢,我们可以使用多线程,多进程模式,这个当然没有问题。 使用多线程、阻塞socket来处理的话,代码会很直观,但是也会有不少缺陷。(前面的文章说了)
使用异步io,这种socket只有event触发时,程序才会继续下面的操作,同时由于异步io时非阻塞的,就没有必要再来使用多线程,多进程。它也可以结合多线程一起使用:单线程使用异步socket用于处理服务器的网络部分,多线程可以用来访问其他阻塞资 源,比如数据库。(此处说的异步IO指的是IO多路复用,它其实不是真正的异步IO)

再来看一下


介绍:
Python中的select模块专注于I/O多路复用,提供了select poll epoll三个方法(其中后两个在Linux中可用,windows仅支持select),另外也提供了kqueue方法(freeBSD系统)
select方法:
进程指定内核监听哪些文件描述符(最多监听1024个fd)的哪些事件,当没有文件描述符事件发生时,进程被阻塞;当一个或者多个文件描述符事件发生时,进程被唤醒。

当我们调用select()时:
  1 上下文切换转换为内核态
  2 将fd从用户空间复制到内核空间
  3 内核遍历所有fd,查看其对应事件是否发生
  4 如果没发生,将进程阻塞,当设备驱动产生中断或者timeout时间后,将进程唤醒,再次进行遍历
  5 返回遍历后的fd
  6 将fd从内核空间复制到用户空间(recvfrom)

fd:file descriptor 文件描述符

fd_r_list, fd_w_list, fd_e_list = select.select(rlist, wlist, xlist, [timeout])
 
参数: 可接受四个参数(前三个必须)
rlist: wait until ready for reading
wlist: wait until ready for writing
xlist: wait for an “exceptional condition”
timeout: 超时时间

返回值:三个列表
 
select方法用来监视文件描述符(当文件描述符条件不满足时,select会阻塞),当某个文件描述符状态改变后,会返回三个列表
1、当参数1 序列中的fd满足“可读”条件时,则获取发生变化的fd并添加到fd_r_list中
2、当参数2 序列中含有fd时,则将该序列中所有的fd添加到 fd_w_list中
3、当参数3 序列中的fd发生错误时,则将该发生错误的fd添加到 fd_e_list中
4、当超时时间为空,则select会一直阻塞,直到监听的句柄发生变化
   当超时时间 = n(正整数)时,那么如果监听的句柄均无任何变化,则select会阻塞n秒,之后返回三个空列表,如果监听的句柄有变化,则直接执行。

看一下select例子:

import socket, select, queue

server = socket.socket()
server.bind(('localhost', 9000))
server.listen(1000)
server.setblocking(False)  #设置为非阻塞

inputs = [server,]
outputs = []

message_queues = {}
while True:
    readable , writeable, exceptional = select.select(inputs, outputs, inputs)   #此处阻塞,当有句柄发生变化就会被监听到
    for s in readable:    #每一个s就是一个socket
        if s is server:   #代表来了新连接(上面我们server自己也当做一个fd放在了inputs列表里,传给了select.如果server有活动就表示来了新连接)
            conn, addr = server.accept()
            conn.setblocking(0)
            print('新连接',addr)
            inputs.append(conn)  #不能直接接收数据

            message_queues[conn] = queue.Queue()

        else:          #s不是server的话,那就只能是一个与客户端建立的连接的fd了
            #客户端的数据过来了,在这接收
            try:
                data = s.recv(1024).decode()
                print('收到的数据:',data)
                message_queues[s].put(data.upper())   #即将发送的数据先放到queue里,一会返回给客户端
                if s not in outputs:
                    outputs.append(s)                 #为了不影响处理与其它客户端的连接 , 这里不立刻返回数据给客户端(每一次和客户端的交互都交给select循环)
            except ConnectionResetError:  #客户端断开连接
                s.close()
                print('断开了一个连接')
                if s in outputs:
                    outputs.remove(s)
                inputs.remove(s)
                del message_queues[s]

    for s in writeable:
        try:
            next_message = message_queues[s].get_nowait()
        except queue.Empty:
            # print('queue is empty..')
            # outputs.remove(s)
            pass
        else:
            s.send(next_message.encode())

    for s in exceptional:
        inputs.remove(s)
        if s in outputs:
            outputs.remove(s)
        s.close()
        del message_queues[s]

select的缺点:
1、每次调用都要将所有的文件描述符(fd)拷贝的内核空间,导致效率下降
2、遍历所有的文件描述符(fd)查看是否有数据访问
3、最大链接数限额(1024)

epoll:
1、第一个函数是创建一个epoll句柄,将所有的描述符(fd)拷贝到内核空间,但只拷贝一次。
2、回调函数,某一个函数或某一个动作成功完成之后会触发的函数为所有的描述符(fd)绑定一个回调函数,一旦有数据访问就是触发该回调函数,回调函数将(fd)放到链表中
3、函数判断链表是否为空
4、最大启动项没有限额

参考文章:http://blog.csdn.net/songfreeman/article/details/51179213

selectors模块

selectors模块默认优先使用epoll

import selectors     #基于selectors模块实现的IO多路复用,建议大家使用
import socket

sel = selectors.DefaultSelector()     #根据平台选择最佳的IO多路机制,比如linux就会选择epoll

def accept(sock, mask):   #新建立的连接
    conn, addr = sock.accept()
    conn.setblocking(False)
    sel.register(conn, selectors.EVENT_READ, read)  #...2

def read(conn, mask):  #处理已建立的连接
    try:
        data = conn.recv(1024)
        conn.send(data)
    except Exception:
        print('客户端断开了')
        sel.unregister(conn)

server = socket.socket()
server.bind(('localhost', 9000))
server.listen(1000)
server.setblocking(False)
sel.register(server, selectors.EVENT_READ, accept)   #注册功能 1

while True:
    event = sel.select()        #默认阻塞,有活动连接就返回活动连接列表
    for key, mask in event:
        # print(key.data)       #accept   找出有活动的绑定函数
        # print(key.fileobj)    #sock     找出有活动的文件描述符
        callback = key.data
        callback(key.fileobj, mask)
上一篇下一篇

猜你喜欢

热点阅读