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

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

 

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

 

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

 

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

1
2
3
4
5
6
7
8
9
10
11
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服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 多线程socket服务端<br><br>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服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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是基于链表结构的没有最大连接数限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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、什么是文件描述符,操作系统中一切皆文件

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

 

posted @   种树飞  阅读(1112)  评论(1编辑  收藏  举报
编辑推荐:
· DeepSeek 解答了困扰我五年的技术问题
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· 用 C# 插值字符串处理器写一个 sscanf
· Java 中堆内存和栈内存上的数据分布和特点
· 开发中对象命名的一点思考
阅读排行:
· PPT革命!DeepSeek+Kimi=N小时工作5分钟完成?
· What?废柴, 还在本地部署DeepSeek吗?Are you kidding?
· DeepSeek企业级部署实战指南:从服务器选型到Dify私有化落地
· 程序员转型AI:行业分析
· 重磅发布!DeepSeek 微调秘籍揭秘,一键解锁升级版全家桶,AI 玩家必备神器!
点击右上角即可分享
微信分享提示