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

开启stdin非阻塞后ANSI控制序列输出异常的排查与修复

问题

尝试在开启stdin非阻塞模式的情况下,在终端左上角绘制一个10列5行的黑框,使用如下C语言代码实现:

#include <sys/fcntl.h>
#include <unistd.h>
#include <wchar.h>

int main() {
    int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
    fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK);

    char s[] = "\e[1;1H\e[1;1H\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[E\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[E\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[E\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[E\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀";
    // char s1[] = "\e[1;1H\e[1;1H▀▀▀▀▀▀▀▀▀▀\e[E▀▀▀▀▀▀▀▀▀▀\e[E▀▀▀▀▀▀▀▀▀▀\e[E▀▀▀▀▀▀▀▀▀▀\e[E▀▀▀▀▀▀▀▀▀▀\e[E";

    write(1, s, sizeof(s)-1);
    fflush(stdout);
    return 0;
}

但实际输出的黑框不完整,结果如下:

▀▀▀▀▀▀▀▀▀▀
▀▀▀▀▀▀▀▀▀▀
▀▀▀▀▀▀▀▀▀▀
▀▀▀▀▀▀▀▀▀▀
▀▀▀▀▀▀▀

该输出字符串通过在每个半块字符前插入\e[38;5;0m\e[48;5;0m生成。已尝试更换不同Shell、不同终端模拟器(Alacritty、Terminal.app、iTerm2),问题依旧;但在阻塞模式下打印、移除颜色控制代码、将半块字符替换为普通ASCII字符时,输出正常,这并非预期效果。

原因分析

  1. write系统调用的部分写入特性:开启stdin非阻塞模式后,终端输出缓冲区的行为会改变。write在写入包含大量ANSI控制序列和多字节Unicode字符的数据时,可能因内核缓冲区暂时不足返回部分写入的字节数,而原代码未处理这种情况,直接忽略了返回值。
  2. 数据量过大与字符结构叠加:每个▀是UTF-8多字节字符(占3字节),加上前缀的14字节ANSI颜色控制序列,单字符总长度达17字节。当write无法一次性写入全部数据时,最后一行的部分字符会被截断,导致输出不完整。
  3. 非阻塞模式对终端IO的影响:开启stdin非阻塞后,终端IO调度机制变化,输出缓冲区可用空间比阻塞模式下更小,大幅增加了部分写入的概率。

修复方案

方案1:循环调用write确保全量写入

处理write的部分返回情况,循环写入直到所有数据都输出:

#include <sys/fcntl.h>
#include <unistd.h>
#include <wchar.h>
#include <string.h>
#include <errno.h>

int main() {
    int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
    fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK);

    char s[] = "\e[1;1H\e[1;1H\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[E\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[E\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[E\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[E\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀\e[38;5;0m\e[48;5;0m▀";
    
    size_t total_len = sizeof(s) - 1;
    ssize_t written;
    const char *buf = s;

    while (total_len > 0) {
        written = write(1, buf, total_len);
        if (written == -1) {
            // 处理EAGAIN错误,重试写入
            if (errno == EAGAIN) continue;
            break;
        }
        buf += written;
        total_len -= written;
    }

    fflush(stdout);
    return 0;
}

方案2:优化ANSI控制序列,减少重复代码

无需为每个字符重复设置颜色,仅在开头设置一次黑底黑字,后续字符自动继承样式,大幅减少输出数据量:

#include <sys/fcntl.h>
#include <unistd.h>
#include <wchar.h>

int main() {
    int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
    fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK);

    // 仅在开头设置一次颜色,后续字符沿用
    char s[] = "\e[1;1H\e[38;5;0m\e[48;5;0m▀▀▀▀▀▀▀▀▀▀\e[E▀▀▀▀▀▀▀▀▀▀\e[E▀▀▀▀▀▀▀▀▀▀\e[E▀▀▀▀▀▀▀▀▀▀\e[E▀▀▀▀▀▀▀▀▀▀";

    write(1, s, sizeof(s)-1);
    fflush(stdout);
    return 0;
}

方案3:设置stdout为行缓冲模式

通过修改stdout的缓冲策略为行缓冲,确保每行数据完整输出:

#include <sys/fcntl.h>
#include <unistd.h>
#include <wchar.h>
#include <stdio.h>

int main() {
    int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
    fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK);

    // 设置stdout为行缓冲
    setvbuf(stdout, NULL, _IOLBF, 0);

    char s[] = "\e[1;1H\e[38;5;0m\e[48;5;0m▀▀▀▀▀▀▀▀▀▀\n▀▀▀▀▀▀▀▀▀▀\n▀▀▀▀▀▀▀▀▀▀\n▀▀▀▀▀▀▀▀▀▀\n▀▀▀▀▀▀▀▀▀▀";
    printf("%s", s);
    fflush(stdout);
    return 0;
}

内容的提问来源于stack exchange,提问作者Austin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 18:24:55