zoukankan      html  css  js  c++  java
  • I/O模型

    网络IO的本质是socket的读取,socket在linux系统被抽象为流,IO可以理解为对流的操作。刚才说了,对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:

    1. 第一阶段:等待数据准备 (Waiting for the data to be ready)。

    2. 第二阶段:将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。

    对于socket流而言

    • 第一步:通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区。
    • 第二步:把数据从内核缓冲区复制到应用进程缓冲区。

    网络应用需要处理的无非就是两大类问题,网络IO,数据计算。相对于后者,网络IO的延迟,给应用带来的性能瓶颈大于后者。网络IO的模型大致有如下几种:

    • 同步模型(synchronous IO)

    • 阻塞IO(bloking IO)

    • 非阻塞IO(non-blocking IO)

    • 多路复用IO(multiplexing IO)

    • 信号驱动式IO(signal-driven IO)

    • 异步IO(asynchronous IO)

    同步阻塞IO:

    在linux中,默认情况下所有的socket都是blocking。它符合人们最常见的思考逻辑。阻塞就是进程 "被" 休息, CPU处理其它进程去了。

    在这个IO模型中,用户空间的应用程序执行一个系统调用(recvform),这会导致应用程序阻塞,什么也不干,直到数据准备好,并且将数据从内核复制到用户进程,最后进程再处理数据,在等待数据到处理数据的两个阶段,整个进程都被阻塞。不能处理别的网络IO。调用应用程序处于一种不再消费 CPU 而只是简单等待响应的状态,因此从处理的角度来看,这是非常有效的。在调用recv()/recvfrom()函数时,发生在内核中等待数据和复制数据的过程,大致如下图:

     

    流程分析

    当用户进程调用了recv()/recvfrom()这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。第二个阶段:当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。所以,blocking IO的特点就是在IO执行的两个阶段都被block了。

    优点:

    • 能够及时返回数据,无延迟;
    • 对内核开发者来说这是省事了;

    缺点:对用户来说处于等待就要付出性能的代价了;

    同步非阻塞IO(nonblocking IO):

    同步非阻塞就是 “每隔一会儿瞄一眼进度条” 的轮询(polling)方式。在这种模型中,设备是以非阻塞的形式打开的。这意味着 IO 操作不会立即完成,read 操作可能会返回一个错误代码,说明这个命令不能立即满足(EAGAIN 或 EWOULDBLOCK)。

     在网络IO时候,非阻塞IO也会进行recvform系统调用,检查数据是否准备好,与阻塞IO不一样,”非阻塞将大的整片时间的阻塞分成N多的小的阻塞, 所以进程不断地有机会 ‘被’ CPU光顾”。

    也就是说非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。

    流程:

    当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。所以,nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。

    优点:能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在同时执行)。

    缺点:任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。

    IO 多路复用(IO multiplexing)

    由于同步非阻塞方式需要不断主动轮询,轮询占据了很大一部分过程,轮询会消耗大量的CPU时间,而 “后台” 可能有多个任务在同时进行,人们就想到了循环查询多个任务的完成状态,只要有任何一个任务完成,就去处理它。如果轮询不是进程的用户态,而是有人帮忙就好了。那么这就是所谓的 “IO 多路复用”。UNIX/Linux 下的 select、poll、epoll 就是干这个的(epoll 比 poll、select 效率高,做的事情是一样的)。

    IO多路复用有两个特别的系统调用select、poll、epoll函数。select调用是内核级别的,select轮询相对非阻塞的轮询的区别在于—前者可以等待多个socket,能实现同时对多个IO端口进行监听,当其中任何一个socket的数据准好了,就能返回进行可读,然后进程再进行recvform系统调用,将数据由内核拷贝到用户进程,当然这个过程是阻塞的。select或poll调用之后,会阻塞进程,与blocking IO阻塞不同在于,此时的select不是等到socket数据全部到达再处理, 而是有了一部分数据就会调用用户进程来处理。如何知道有一部分数据到达了呢?监视的事情交给了内核,内核负责数据到达的处理。也可以理解为"非阻塞"吧。

    I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时(注意不是全部数据可读或可写),才真正调用I/O操作函数。

    IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

    当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

    多路复用的特点是通过一种机制一个进程能同时等待IO文件描述符,内核监视这些文件描述符(套接字描述符),其中的任意一个进入读就绪状态,select, poll,epoll函数就可以返回。对于监视的方式,又可以分为 select, poll, epoll三种方式。

    上面的图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。

     所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。(select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)

    select 实现多路复用:

    ######服务端#######
    
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    # Created by Mona on 2017/7/21
    
    import socket,select
    sock = socket.socket()
    sock.bind(('0.0.0.0',9090))
    sock.listen(5)
    sock.setblocking(False)  # 设置为非阻塞模式
    
    inp = [sock,]   #监视的列表
    
    while True:
        r= select.select(inp,[],[])  # select示例化时第一个元素放监听的对象
        for obj in r[0]:
            if obj == sock:  #如果是sock套接字,则accept,并将新的套接字对象加入监听的列表
                conn,addr = sock.accept()
                inp.append(conn)
            else:            #如果是conn套接字,则recv
                data = obj.recv(1024)
                print(data.decode())
                response = input('>>>:').strip()
                obj.send(response.encode())
    
    #############客户端################
    import socket
    client = socket.socket()
    client.connect(('localhost',9090))
    
    while 1:
        data = input('>>>>>:').strip()
        if data == 'q':
            client.close()
        client.send(data.encode())
        response = client.recv(1024)
        print(response.decode())

    selectors实现multiplexing

    #######服务器#######
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    # Created by Mona on 2017/7/21
    
    import selectors,socket
    sock = socket.socket()
    sock.bind(('0.0.0.0',9090))
    sock.listen(5)
    
    sel = selectors.DefaultSelector()   #示例化对象
    
    def accept(sock,mask):    #定义sock套接字对象的方法
        conn,addr = sock.accept()  #接受conn套接字
        conn.setblocking(False)    #设置为非阻塞模式
        sel.register(conn,selectors.EVENT_READ,recv)   #将sock注册为sel对象
    
    def recv(conn,mask):    #定义sock套接字对象的方法
        data = conn.recv(1024)
        print(data.decode())
        response = input('>>>:').strip()
        conn.send(response.encode())
    
    sel.register(sock,selectors.EVENT_READ,accept)   #将sock注册为sel对象
    
    while True:   #调用sel对象
        events = sel.select()
        for key,mask in events:   
            #SelectorKey(fileobj=<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('0.0.0.0', 9090)>,
            #  fd=3, events=1, data=<function accept at 0x100762e18>)
        
            callback = key.data   #套接字对象的方法函数
            callback(key.fileobj,mask)  #执行该函数
    
    ########客户端########
    
    import socket
    client = socket.socket()
    client.connect(('localhost',9090))
    
    while 1:
        data = input('>>>>>:').strip()
        if data == 'q':
            client.close()
        client.send(data.encode())
        response = client.recv(1024)
        print(response.decode())

    异步非阻塞IO:

    linux下的asynchronous IO其实用得很少。先看一下它的流程:

    相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。Linux提供了AIO库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如libevent、libev、libuv。

    用户进程发起aio_read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal或执行一个基于线程的回调函数来完成这次 IO 处理过程,告诉它read操作完成了。

  • 相关阅读:
    大话设计模式-——简答工厂模式
    大话设计——-单例模式
    首先,编写一个类ChongZai,该类中有3个重载的方法void print();其次, 再编写一个主类来测试ChongZai类的功能
    创建一个Point类,有成员变量x,y,方法getX(),setX(),还有一个构造方 法初始化x和y。创建类主类A来测试它
    正则表达式
    struts(一)
    servlet容器开发要点
    Http协议
    TCP的四次挥手
    建立TCP连接的三次握手
  • 原文地址:https://www.cnblogs.com/mona524/p/7219265.html
Copyright © 2011-2022 走看看