I/O多路复用及select函数解析及实例

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Megustas_JJC/article/details/79145478

概述

在进行解释之前,首先要说明几个概念:
- 用户空间和内核空间
- 进程切换
- 进程的阻塞
- 文件描述符
- 缓存 I/O

用户空间与内核空间

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

进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。

从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:
1. 保存处理机上下文,包括程序计数器和其他寄存器。
2. 更新PCB信息。
3. 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
4. 选择另一个进程执行,并更新其PCB。
5. 更新内存管理的数据结构。
6. 恢复处理机上下文。

是很耗资源的,具体的可以参考这篇文章:进程切换

注:进程控制块(Processing Control Block),是操作系统核心中一种数据结构,主要表示进程状态。其作用是使一个在多道程序环境下不能独立运行的程序(含数据),成为一个能独立运行的基本单位或与其它进程并发执行的进程。或者说,OS是根据PCB来对并发执行的进程进行控制和管理的。 PCB通常是系统内存占用区中的一个连续存区,它存放着操作系统用于描述进程情况及控制进程运行所需的全部信息

进程的阻塞

正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。

文件描述符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 以及内存开销是非常大的。

I/O复用的情况
有如下的情况:当TCP客户端同时处理两个输入:标准输入和TCP套接字。我们遇到的问题就是,在客户阻塞于(标准输入上的)fgets调用期间,服务进程就会被杀死。服务器TCP虽然正确地给客户TCP发送一个FIN,但是既然客户进程正阻塞于从标准输入读入的过程,它将看不到这个EOF,直到从套接字读时为止(可能已经过了很长时间)。这样的进程需要一种预先告知内核的能力,使得内核一旦发现进程指定的一个或多个I/O条件就绪(也就是说已经准备好被读取,或者描述符已能承接更多的输出),它就通知进程。这个能力称为I/O复用(I/O multiplexing),是由select和poll这两个函数支持的。

I/O复用适用如下情况:
  (1)当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
  (2)当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
  (3)如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
  (4)如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
  (5)如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
  与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。select通过单进程实现同时处理多个非阻塞的socket连接。

IO模型

Unix下可用的5种I/O模型:

  • 阻塞 I/O(blocking IO)
  • 非阻塞 I/O(nonblocking IO)
  • I/O 多路复用( IO multiplexing,select和poll)
  • 信号驱动 I/O( signal driven IO)
  • 异步 I/O(asynchronous IO)

具体的各个模型内容解释可以参考《UNIX网络编程》的第六章,这里仅简要说明下I/O 多路复用:

这里写图片描述

有了I/O 多路复用( IO multiplexing),我们就可以调用select或poll,阻塞在这两个系统调用种的某一个之上,而不是阻塞在真正的I/O系统调用上。

我们阻塞于select调用,等待数据报套接字变为可读。当select返回套接字可读这一条件时,我们调用recvfrom把所有数据报复制到应用进程缓冲区。

与I/O复用密切相关的另一种I/O模型是在多线程中使用阻塞式I/O。这种模型与上述模型极为相似,但它没有使用select阻塞在多个文件描述符上,而是使用多个线程(每个文件描述符一个线程),这样每个线程都可以自由地调用诸如recvfrom之类的阻塞式I/O系统调用了。

所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。

select函数

select通过单进程实现同时处理多个非阻塞的socket连接。该函数允许进程指示内核等待多个事件中的任何一个发生,并只在一个或多个事件发生或经历一段指定的时间才唤醒它。

select(rlist, wlist, xlist, timeout=None)

select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符。

作为一个例子,我们可以调用select,告知内核仅在下列情况发生时才返回:

  • 集合{1,4,5}中的任何描述符准备好读
  • 集合{2,7}中的任何描述符准备好写
  • 集合{1,4}中的任何描述符有异常条件待处理
  • 已经经历了10秒

    就是说调用select告知内核对哪些描述符(就读,写或者异常条件)感兴趣及等待多长时间。

select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低(原因:通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。)。

select的一个实例

Python的select()方法直接调用操作系统的IO接口,它监控sockets,open files, and pipes(所有带fileno()方法的文件句柄)何时变成readable 和writeable, 或者通信错误,select()使得同时监控多个连接变的简单,并且这比写一个长循环来等待和监控多客户端连接要高效,因为select直接通过操作系统提供的C的网络接口进行操作,而不是通过Python的解释器。

其大致流程个人总结如下:
服务端:
(1)创建TCP/IP连接,创建socket
(2)创建通信列表
(3)对inputs进行主循环

  1. Handle inputs
  2. Handle outputs
  3. Handle “exceptional conditions”

客户端:
(1)创建连接socket
(2)并发的对服务端进行连接
(3)进行数据的发送和接收

可以参考一篇英文文档:https://pymotw.com/2/select/

服务端

#-*- coding: utf-8 -*-
import select
import socket
import sys
import Queue
#通过这个demo来理解一下异步这种设计模式
#---------------------------------------------------创建TCP/IP连接
# Create a TCP/IP socket
"""
AF_INET(又称 PF_INET)是 IPv4 网络协议的套接字类型,AF_INET6 则是 IPv6 的;而 AF_UNIX 则是 Unix 系统本地通信
常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。
流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。
"""
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
"""
如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。
非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常。
"""
server.setblocking(0)

# Bind the socket to the port
server_address = ('localhost', 12300)
print >> sys.stderr, 'starting up on %s port %s' % server_address
server.bind(server_address)

# Listen for incoming connections
server.listen(5)


#---------------------------------------------------通信列表
#select()方法接收并监控3个通信列表, 第一个是所有的输入的data,就是指外部发过来的数据,第2个是监控和接收所有要发出去的data(outgoing data),第3个监控错误信息

#第一个列表,所有客户端的进来的连接和数据将会被server的主循环程序放在这个list中处理
# Sockets from which we expect to read
#开始时仅监视这个server是否有活动,当别人主动去连接的时候才会有活动
inputs = [ server ]


#第二个列表
# Sockets to which we expect to write
outputs = [ ]



# Outgoing message queues (socket:Queue)
message_queues = {}


##---------------------------------------------------主循环程序
while inputs:

    # Wait for at least one of the sockets to be ready for processing
    #当检测所有文件句柄都没有活动的时候,会阻塞状态,并打印如下结果
    print >>sys.stderr, '\nwaiting for the next event'
    #select检测这几个列表里面有没有就绪的文件描述符,返回三个列表,即循环这三个列表并返回可用的(就绪的)列表
    readable, writable, exceptional = select.select(inputs, outputs, inputs)

    #---------------------------------------------------select之后的处理
    # Handle inputs
    for s in readable:
        # --------------------------------------------------- 第一种是
        """
        如果这个socket是main "server" socket,它负责监听客户端的连接
        如果是一个server,代表有客户端连接过来了,这是一个新连接,是一个新实例
        如果这个main server socket出现在readable里,那代表这是server端已经ready来接收一个新的连接进来了
        """
        if(s is server):
            # A "readable" server socket is ready to accept a connection
            connection, client_address = s.accept()
            print >> sys.stderr, 'new connection from', client_address
            #为了能同时处理多个连接,在下面的代码里,我们把这个设置为非阻塞模式
            connection.setblocking(0)
            #把这个新的连接放到input列表中,accept之后不会直接进行recv,避免阻塞(因为后面可能还有连接在等待,如果进行recv则阻塞)
            inputs.append(connection)
            #字典将socket对象作为key,并为之配一个队列,即为每一个连接都配一个Queue
            # Give the connection a queue for data we want to send
            message_queues[connection] = Queue.Queue()
            #进行下一次轮循
        else:
            # ---------------------------------------------------第二种
            """
            socket是已经建立了的连接,它把数据发了过来,这个时候你就可以通过recv()来接收它发过来的数据,
            然后把接收到的数据放到queue里,这样你就可以把接收到的数据再传回给客户端了
            """
            data = s.recv(1024)
            if data:
                # A readable client socket has data
                print >> sys.stderr, 'received "%s" from %s' % (data, s.getpeername()) #getpeername()返回与某个套接字关联的本地协议地址
                message_queues[s].put(data)
                # Add output channel for response
                # 把连接放到outputs列表中
                if s not in outputs:
                    outputs.append(s)
                    # ---------------------------------------------------当连接已经在outputs列表中
            else:
                #如果没有data,需要把对应的链接从列表中删除,否则会造成while inputs的死循环,不断打印waiting for the next event
                # Interpret empty result as closed connection
                print >> sys.stderr, 'closing', client_address, 'after reading no data'
                # Stop listening for input on the connection
                if s in outputs:
                    outputs.remove(s)  # 既然客户端都断开了,我就不用再给它返回数据了,所以这时候如果这个客户端的连接对象还在outputs列表中,就把它删掉
                inputs.remove(s)  # inputs中也删除掉
                s.close()  # 把这个连接关闭掉

                # Remove message queue
                """
                由于python都是引用,而python有GC机制,
                所以del语句作用在变量上,而不是数据对象上,del删除的是变量,而不是数据
                """
                del message_queues[s] #删除变量message_queues[s],解除其对对应数据的引用

    # Handle outputs
    #循环已经准备好了的,可以发数据的连接列表
    for s in writable:
        """
        try:
            <语句>
        except <name>:
            <语句>          #如果在try部份引发了名为'name'的异常,则执行这段代码
        else:
            <语句>          #如果没有异常发生,则执行这段代码
        """
        try:
            """
            Queue模块:
            queue.put_nowait():无阻塞的向队列中添加任务
            queue.get_nowait():无阻塞的向队列中get任务
            """
            next_msg = message_queues[s].get_nowait()
        except Queue.Empty:
            # No messages waiting so stop checking for writability.
            print >>sys.stderr, 'output queue for', s.getpeername(), 'is empty'
            outputs.remove(s)
        else:
            print >>sys.stderr, 'sending "%s" to %s' % (next_msg, s.getpeername())
            s.send(next_msg.upper()) #取到数据进行发送

    # Handle "exceptional conditions",处理错误列表,例如链接出现通信错误等问题,先在inputs和outputs列表中找到并删除即可
    for s in exceptional:
        print('handling exceptional condition for', s.getpeername())
        # Stop listening for input on the connection
        inputs.remove(s)
        if s in outputs:
            outputs.remove(s)
        s.close()
        # Remove message queue
        del message_queues[s]

客户端

#-*- coding: utf-8 -*-
#并发的去链接服务端,本例子是两个并发的向服务端没有阻塞的发送信息
#达到了用多线程进行网络通信的一个效果
import socket
import sys

messages = ['This is the message. ',
            'It will be sent ',
            'in parts.',
            ]
server_address = ('localhost', 12300)

# Create a TCP/IP socket
#客户端起了两个链接,并将链接放入列表中,之后循环列表
socks = [socket.socket(socket.AF_INET, socket.SOCK_STREAM),
         socket.socket(socket.AF_INET, socket.SOCK_STREAM),
         ]

# Connect the socket to the port where the server is listening
print >> sys.stderr, 'connecting to %s port %s' % server_address
for s in socks:
    s.connect(server_address)

for message in messages:

    # Send messages on both sockets
    for s in socks:
        print >> sys.stderr, '%s: sending "%s"' % (s.getsockname(), message)
        s.send(message)

    # Read responses on both sockets
    for s in socks:
        data = s.recv(1024)
        print >> sys.stderr, '%s: received "%s"' % (s.getsockname(), data)
        if not data:
            print >> sys.stderr, 'closing socket', s.getsockname()
            s.close()

运行结果:

服务端:
这里写图片描述

客户端:

这里写图片描述

poll

poll的相关内容本人还没有进行学习了解,可以参加如下一些资料及demo:

猜你喜欢

转载自blog.csdn.net/Megustas_JJC/article/details/79145478