• 从IO的角度深入理解Select、Poll、Epoll的区别推理


      近期刚学习IO多路复用的知识,还有看了django和flask框架WSGIServer的源码,对源码中使用的selector模块比较好奇,也就去稍微深入看了一下个方面资料和相关视频及底层实现,梳理出这篇文章。

      一、Python中起高可用socket服务端的常用三种方式

      在初始我们写一个socket服务端, 如果要供多人同时连接使用的话,有几大方式如在接收消息部分使用多线程,使用协程, 或者是多进程实现socket服务端 。

    socket客户端实现, 用于连接测试服务端

    import socket
    import time
    
    sc = socket.socket()
    sc.connect(('127.0.0.1', 8000))
    
    while True:
        sc.send(b'hello word')
        data = sc.recv(1024)
        print(data)
        time.sleep(1)
    

      

    1)多进程实现socket服务端

    import socket
    from multiprocessing import Process
    import time
    
    
    sc = socket.socket()
    sc.bind(('127.0.0.1', 8000))
    sc.listen(5)
    sc.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    
    def recv_data(conn):
        while True:
            data = conn.recv(1024)
            if data:
                print(data)
                conn.sendall(data.upper())
    
    
    while True:
        conn, addr = sc.accept()
        if conn:
            Process(target=recv_data, args=(conn,)).start()
        time.sleep(1)
    

      使用多进程实现socket服务端的优缺点

    优点:解决单进程单线程无法多客户端连接的问题

    缺点:开多进程消耗的资源比较大,并且操作系统多进程数量有限制

    2)多线程实现socket服务端

    # 多线程socket服务端

    import socket import threading import time sc = socket.socket() sc.bind(('127.0.0.1', 8000)) sc.listen(5) sc.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) def recv_data(conn): while True: data = conn.recv(1024) if data: print(data) conn.sendall(data.upper()) while True: conn, addr = sc.accept() if conn: threading.Thread(target=recv_data, args=(conn,)).start() time.sleep(1)

      使用多线程实现socket服务端的优缺点

    优点: 可以满足多客户端连接,实现简单, 比多进程更小的资源的消耗

    缺点: 开多线程耗资源,且线程间的切换有性能消耗,不能无限开

    3)使用协程实现socket服务端

    import time
    import socket
    import gevent
    from gevent import monkey
    monkey.patch_all()
    
    
    sc = socket.socket()
    
    sc.bind(('127.0.0.1', 8000))
    sc.listen(5)
    sc.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    
    def recv_data(conn):
        while True:
            data = conn.recv(1024)
            if data:
                print(data)
                conn.sendall(data.upper())
    
    
    while True:
        conn, addr = sc.accept()
        if conn:
            gevent.spawn(recv_data, conn)
        time.sleep(1)
    

      使用协程实现socket服务端优缺点:

    优点: 协程是微线程,多个协程在一个线程内切换,占用资源最少,并且在socket这种IO密集型的服务中效率很高,有时速度优于多线程socket实现

    缺点:比起多进程和多线程实现是基本没有缺点,唯一是无法利用多CPU,在计算密集型服务时吃力

    以上三种socket服务端的实现方式都存在的缺点是,如果有1W个连接时,单次就会有1W次IO操作,会有操作系统层面的1W次系统调用,会有比较大的系统调用切换的消耗,这就引出我们的IO多路复用。

      二、IO多路复用之Select、Poll、Epoll及其区别

    1)Select和Poll和Epoll的用法

    其实Select和Poll的区别不大,唯一区别是Select对有最大连接数限制1024这个数字是可以修改的,而Poll是基于链表结构的没有最大连接数限制。

    import selectors
    import socket
    
    
    select = selectors.DefaultSelector()
    
    
    def recv_data(conn, mask):
        data = conn.recv(1024)
        if data:
            print(data)
            conn.sendall(data.upper())
        else:
            select.unregister(conn)
            conn.close()
    
    
    def accept(sc, mask):
        conn, addr = sc.accept()
        conn.setblocking(False)
        select.register(conn, selectors.EVENT_READ, recv_data)
    
    
    sc = socket.socket()
    sc.bind(('127.0.0.1', 8000))
    sc.listen(5)
    sc.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sc.setblocking(False)
    
    select.register(sc, selectors.EVENT_READ, accept)      # epoll的话相当于向内核开辟一个空间放文件描述符
    
    while True:
        read = select.select(timeout=1)                    # 相当于遍历文件描述符
        for key, mask in read:
            callback = key.data
            callback(key.fileobj, mask)
    

      由于我们使用的python自带的selectors模块,代码中 select = selectors.DefaultSelector() 会根据操作系统的不同实例化合适的Select或者Poll或者Epoll。

      我们知道很多时刻操作系统的进程和线程的调度策略为: 时间片轮转调度,也就是每个线程在时间片内被cpu调度执行,根据这基础我们进行分析,假设我们有1w个线程并发执行(不是并行哦)这样单位时间内就会调度操作系统内核交互1W次,也就产生了1W次IO,如果把这1W次IO变成1次IO,那性能是不是提升很多,而select和epoll就是这样做的,它把这1W个文件描述符(或者理解成调用)放到一个数组或者链表中,一次传递给操作系统内核,然后内核内的线程去循环这个数组,去执行相应的指令,然后执行完毕后,操作系统用户态再拿回这个文件描述符数组,然后遍历取其中的结果,这样就从1W次IO变成了1次IO了。

    多线程下的IO模型

    select和poll下IO图解

      

      我们先说select和epoll的优缺点:

    优点: 可以减少操作系统用户态和内核态IO的次数,统一监控多个IO操作,然后遍历获取结果。

    缺点: 每次都要传递一个大的数组列表, 还有每次都要多数组列表进行遍历获得结果

    所以在上述缺点的情况下Epoll诞生了:

      epoll会在操作系统内核中开辟一个空间,然后每次系统调用就会把新的文件描述,传递给内核(只传递一次),然后内核会开另一个线程去监控内核中的文件描述符,在有返回结果后,它会结果返回放到另一个空间(文件描述符活跃),此时用户态只会遍历活跃状态的文件描述符,这样用空间换时间效率提升,主要体现在:内核多个线程并发处理文件描述符,每次只遍历活跃的文件描述符。

    epoll IO模型

    至此告一段落,后续还需补充挺多东西,如果操作系统的IO知识:

    1、操作系统IO知识,什么是用户态,内核态

    2、操作系统进程线程的调度策略

    3、操作系统的系统调用、中断和异常

    4、还有select函数底层实现 可以在linux中用man函数调用查看解释

    5、什么是文件描述符,操作系统中一切皆文件

    等等一些列操作系统方面的知识

  • 相关阅读:
    微服务架构总结
    微服务-网关服务
    HttpClient-RestTemplate-Feign
    RPC和REST
    Springmvc的拦截器执行顺序及各方法作用
    秒杀系统优化方案(下)吐血整理
    秒杀系统优化方案(上)吐血整理
    分布式session的管理
    缓存设计——缓存和数据库的数据一致性
    个人理解的javascript作用域链与闭包
  • 原文地址:https://www.cnblogs.com/lifei01/p/13143286.html
Copyright © 2020-2023  润新知