深入解析Linux网络IO模型:阻塞、非阻塞、多路复用与异步

方予
方予
发布时间:2026-10-13 | 6人浏览过
原创

本文详细解析Linux环境下的五种网络IO模型,重点对比阻塞、非阻塞、IO多路复用(select/epoll)及异步IO的执行流程。通过剖析等待数据与拷贝数据两个阶段,阐明各模型在进程阻塞状态、系统调用次数及适用场景上的核心差异,帮助开发者根据连接数与性能需求选择合适的IO处理方式。

一、I/O模型

IO在计算机领域代表Input/Output(输入/输出)。由于程序代码及运行时数据均驻留在内存中,由CPU这一高速计算核心执行,因此涉及数据交换的场景(如磁盘读写、网络通信)必须通过IO接口进行。

同步(synchronous)IO与异步(asynchronous)IO,以及阻塞(blocking)IO与非阻塞(non-blocking)IO的具体定义及其区别是什么?

对此问题的回答因知识背景而异。例如,Wiki认为asynchronous IO与non-blocking IO是同一概念。这种差异源于讨论时的上下文(context)不同。因此,本文限定讨论背景为Linux环境下的network IO。

Stevens在相关文献中比较了五种IO Model:

blocking IO(阻塞IO)

nonblocking IO (非阻塞IO)

IO multiplexing (IO多路复用)

asynchronous IO (异步IO)

signal driven IO (信号驱动IO)

由于signal driven IO在实际应用中并不常用,本文仅讨论剩余的四种IO Model。

接下来阐述IO发生时涉及的对象及步骤。

以network IO中的read操作为例,它涉及两个系统对象:发起IO调用的process(或thread)以及系统内核(kernel)。当一个read操作发生时,需经历两个阶段:

等待数据准备 (Waiting for the data to be ready) 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process) 理解这两点至关重要,因为各IO Model的区别正体现在这两个阶段的不同处理上。

二、 blocking IO

在Linux中,默认情况下所有socket均为blocking模式。一个典型的读操作流程如下:

当用户进程调用recvfrom系统调用时,内核开始IO的第一阶段:准备数据。对于network IO,若数据尚未到达(例如未收到完整UDP包),内核需等待足够数据。在此期间,用户进程被完全阻塞。一旦内核数据准备就绪,便将数据从内核拷贝至用户内存,随后内核返回结果,用户进程解除block状态并继续运行。

因此,blocking IO的特点在于IO执行的两个阶段(等待数据与拷贝数据)中,进程均处于被阻塞状态。

三、non-blocking IO

在Linux下,可通过设置socket使其变为non-blocking。对non-blocking socket执行读操作的流程如下:

从流程图中可见,当用户进程发起read操作时,若内核数据未准备好,内核不会阻塞用户进程,而是立即返回error。对用户进程而言,发起read后无需等待,即刻获得结果。若结果为error,用户进程知晓数据未就绪,可再次发送read操作。一旦内核数据准备好且再次收到用户进程的system call,内核立即将数据拷贝至用户内存并返回。因此,用户进程需不断主动询问内核数据是否就绪。

注意:

在网络IO中,非阻塞IO同样进行recvfrom系统调用以检查数据状态。与阻塞IO不同,“非阻塞将大的整片时间的阻塞分成N多小的阻塞”,使得进程有机会频繁被CPU调度。即每次recvfrom调用间,CPU权限仍归进程所有,进程可执行其他任务。若数据未准备好,内核立即返回error。进程返回后可执行其他操作,随后再次发起recvfrom系统调用。此循环往复的过程称为轮询。通过轮询检查内核数据,直至数据就绪再拷贝至进程进行数据处理。需注意,在拷贝数据的全过程中,进程仍处于阻塞状态。

import time
import socket

sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sk.bind(('127.0.0.1', 8080))
sk.listen(5)
sk.setblocking(False)  #设置套接字为非阻塞模式
while True:
  try:
    print('waiting client connection .......')
    connection, address = sk.accept() # 进程主动轮询
    print("+++", address)
    client_messge = connection.recv(1024)
    print(str(client_messge, 'utf8'))
    connection.close()
  except Exception as e:
    print(e)
    time.sleep(4)

#############################client

import time
import socket
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

while True:
  sk.connect(('127.0.0.1', 8080))
  print("hello")
  sk.sendall(bytes("hello", "utf8"))
  time.sleep(2)
  break

上述实例中,服务端每隔4秒轮询一次。若无客户端连接,则抛出错误信息并继续轮询。

非阻塞IO:

优点:能够在等待任务完成期间执行其他任务(包括提交其他任务,即“后台”可同时执行多个任务)。

缺点:任务完成的响应延迟增加,因为仅每隔一段时间轮询一次read操作。任务可能在两次轮询间的任意时刻完成,导致无法实时获取数据,进而降低整体数据吞吐量。

四、IO multiplexing

IO multiplexing一词可能略显陌生,部分地区也称此IO方式为event driven IO。select/epoll的优势在于单个process可同时处理多个网络连接的IO。其基本原理是select/epoll函数不断轮询所有负责的socket,当任一socket有数据到达时,便通知用户进程。其流程如图:

当用户进程调用select时,整个进程会被block。同时,kernel“监视”所有select负责的socket,当任一socket数据准备好,select即返回。此时用户进程再调用read操作,将数据从kernel拷贝至用户进程。

此图与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的优势并非处理单个连接更快,而是能处理更多连接。)

在IO multiplexing Model中,实际应用中,每个socket通常设置为non-blocking。但如上图所示,整个用户process实际上一直处于block状态,只不过是被select函数block,而非被socket IO直接block。

注意:

1.若select函数返回结果中包含可读文件,进程可通过调用accept()或recv()让kernel将内核中已准备好的数据copy到用户区。

2.select的优势在于可处理多个连接,但不适用于单个连接场景。

import select, socket

sock = socket.socket()
sock.bind(('127.0.0.1', 8080))
sock.listen(5)

sock.setblocking(False)
listen_obj = [sock, ]

while True:
  r, w, e = select.select(listen_obj, [], [])

  for obj in r:
    if obj == sock:
      conn, addr = obj.accept()
      print('conn', conn)
      print('addr', addr)
      listen_obj.append(conn)
    else:
      data = obj.recv(1024)
      print(data.decode('utf8'))
      send_data = input('>>>')
      obj.send(send_data.encode('utf8'))

#############################client


import socket

sock = socket.socket()
sock.connect(('127.0.0.1', 8080))


while True:
  data = input('>>>')
  sock.send(data.encode('utf8'))
  recv_data = sock.recv(1024)
  print(recv_data.decode('utf8'))
sock.close()

上述实例中,服务端kernel监听select负责的listen_obj中的所有socket对象。当任一socket对象被激活,系统根据其类型判断是否建立通信。

五、asynchronous I/O

Linux下的asynchronous IO实际应用较少。先观察其流程:

用户进程发起read操作后,可立即执行其他任务。从kernel角度看,收到asynchronous read请求后,首先立即返回,因此不会对用户进程产生任何block。随后,kernel等待数据准备完成,将数据拷贝至用户内存。全部完成后,kernel向用户进程发送signal,告知read操作已完成。

显然,使用异步IO编写的程序性能远高于同步IO,但其缺点是编程模型复杂。

A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;

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时,若kernel数据未准备好,不会阻塞进程;但当kernel数据准备好时,recvfrom将数据从kernel拷贝至用户内存,此时进程被block。而在asynchronous IO中,进程发起IO操作后直接返回,不再关注后续,直到kernel发送信号告知IO完成。在此全过程中,进程完全未被block。

各IO Model的比较如图所示:

经上述介绍,non-blocking IO与asynchronous IO的区别显而易见。在non-blocking IO中,尽管进程大部分时间未被block,但仍需进程主动check,且数据准备完成后,需进程再次主动调用recvfrom将数据拷贝至用户内存。而asynchronous IO则完全不同。它如同用户进程将整个IO操作委托给kernel(他人),完成后由kernel发送信号通知。在此期间,用户进程无需检查IO状态,也无需主动拷贝数据。

以上关于Python基础之I/O模型的浅谈内容即为小编分享给大家的全部信息,希望能对大家有所帮助,也希望大家多多支持本站。

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表