You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python中为何select判定可读后read仍会阻塞?

问题分析:select判定可读后read仍阻塞的原因及解决

问题背景与现象

众所周知,在网络套接字场景中,如果select(2)判定有可读数据后,出现校验和错误或缓冲区操作导致数据丢弃,read可能会阻塞,但Python处理标准管道时本不该出现这种情况。

但在以下示例代码中,通过subprocess.Popen调用openssl speed,用select监听stdout和stderr的可读状态,select判定可读后fd.read()依然会阻塞。添加os.set_blocking设置非阻塞模式后虽有改善,但程序启动时会出现异常延迟,改用二进制流(text=False)结果一致。

示例代码:

import os
import sys
import select
from io import StringIO
import subprocess

DEBUG = True

with subprocess.Popen(["openssl", "speed"], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as process:
    #  Something weird is going on. `fd.read()` below blocks even if `select` declares
    #  that there is data available. The `set_blocking` calls below partially help this,
    #  but now there is a strange delay at program start.
    os.set_blocking(process.stdout.fileno(), False)
    os.set_blocking(process.stderr.fileno(), False)

    buffer = StringIO()
    while process.poll() is None:
        rfds, _, _ = select.select([process.stdout, process.stderr], [], [])
        for fd in rfds:
            if fd == process.stdout:
                chunk = fd.read()
                if DEBUG:
                    sys.stdout.write(chunk)
                buffer.write(chunk)
            elif fd == process.stderr:
                sys.stdout.write(fd.read())

原因分析

1. Python高层IO对象的缓冲坑

你用的process.stdout/stderr不是直接的操作系统文件描述符,而是Python封装的TextIOWrapper或BufferedReader,它们自带用户态缓冲区:

  • select盯着的是操作系统内核缓冲区的状态,只要内核里有数据,就会返回“可读”;
  • 但Python的IO对象会先把内核里的数据读到自己的缓冲区,之后的read()是从这个用户态缓冲区读。如果select返回后,Python的缓冲区刚好是空的,它会再次尝试从内核读,但此时内核可能已经没有新数据了(或者只有一点),而无参的read()会一直等,直到读到EOF,自然就阻塞了。

2. 非阻塞模式的适配问题

你给内核文件描述符设了非阻塞,但Python的高层IO对象根本不知道这件事,它还是按自己的逻辑来读数据。这就导致启动时,Python的IO对象会反复尝试读取内核缓冲区,直到有数据进来,造成空转延迟。

3. read()无参的致命问题

无参的fd.read()默认是读到文件结束才停,而不是读当前可用的那点数据。哪怕select告诉你有数据,只要没到EOF,它就会一直等更多数据,直接阻塞。

解决方法

方法1:改用os.read()直接操作文件描述符

绕过Python的高层IO对象,直接用内核文件描述符读,每次读固定大小的字节,完全可控:

import os
import sys
import select
from io import StringIO
import subprocess

DEBUG = True

with subprocess.Popen(["openssl", "speed"], text=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as process:
    stdout_fd = process.stdout.fileno()
    stderr_fd = process.stderr.fileno()
    os.set_blocking(stdout_fd, False)
    os.set_blocking(stderr_fd, False)

    buffer = StringIO()
    while process.poll() is None:
        rfds, _, _ = select.select([stdout_fd, stderr_fd], [], [])
        for fd in rfds:
            try:
                if fd == stdout_fd:
                    chunk = os.read(fd, 4096)
                    if not chunk:
                        continue
                    chunk_str = chunk.decode('utf-8')
                    if DEBUG:
                        sys.stdout.write(chunk_str)
                    buffer.write(chunk_str)
                elif fd == stderr_fd:
                    chunk = os.read(fd, 4096)
                    if chunk:
                        sys.stdout.write(chunk.decode('utf-8'))
            except BlockingIOError:
                # 没有更多数据,直接跳过
                pass

方法2:用read1()代替read()

Python的缓冲IO对象提供了read1()方法,它会读取当前内核缓冲区中的所有可用数据,而不是等待到EOF,完全不需要设置非阻塞:

import os
import sys
import select
from io import StringIO
import subprocess

DEBUG = True

with subprocess.Popen(["openssl", "speed"], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as process:
    buffer = StringIO()
    while process.poll() is None:
        rfds, _, _ = select.select([process.stdout, process.stderr], [], [])
        for fd in rfds:
            if fd == process.stdout:
                chunk = fd.read1()
                if not chunk:
                    continue
                if DEBUG:
                    sys.stdout.write(chunk)
                buffer.write(chunk)
            elif fd == process.stderr:
                chunk = fd.read1()
                if chunk:
                    sys.stdout.write(chunk)

方法3:使用subprocess原生方法简化逻辑

如果不需要实时处理输出,直接用process.communicate()一次性读取所有输出;如果需要实时处理,可以用迭代器逐行读取:

import sys
import subprocess

DEBUG = True

process = subprocess.Popen(["openssl", "speed"], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
buffer = []
# 处理stdout
for line in process.stdout:
    if DEBUG:
        sys.stdout.write(line)
    buffer.append(line)
# 处理stderr
for line in process.stderr:
    sys.stdout.write(line)
process.wait()

内容的提问来源于stack exchange,提问作者Benjamin Crawford Ctrl-Alt-Tut

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 02:25:14