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

使用asio::stream_descriptor异步读取raw模式stdin时崩溃及键码解析问题

问题描述

我想用asio::stream_descriptor异步轮询stdin获取终端键盘事件,写了以下代码:

/* class tui contains a member 'stdin_' of type 'asio::stream_descriptor',
   initialized as such : stdin_{io_strand_,STDIN_FILENO}
*/
void tui::a_read_loop(){
    stdin_.async_read_some(buffer_,[this](const auto& ec, size_t n){
        if(ec==asio::error::operation_aborted);//canceled
        else{
            //...treat input...
            a_read_loop();//chainloop
        }
    });
}

同时将终端设置为raw模式:

termattr.c_cflag|=CS8;
termattr.c_iflag|=IGNBRK;
termattr.c_iflag&=~(tcflag_t)(ICRNL|INLCR|IXOFF|IXON|IXANY);
termattr.c_lflag&=~(tcflag_t)(ECHO|ECHOE|ECHOK|ECHONL|ICANON|IEXTEN|ISIG);
termattr.c_oflag|=OPOST;
termattr.c_cc[VMIN]=1;
termattr.c_cc[VTIME]=0;
tcsetattr(STDIN_FILENO,TCSAFLUSH,&termattr);
std::setvbuf(stdin,nullptr,_IONBF,0);

代码大部分情况正常,但输入过多、处理不及时时会崩溃,抛出异常:

terminate called after throwing an instance of 'std::system_error'
  what():  cannot write to file: Resource temporarily unavailable

异常来自异步任务本身,会导致asio::thread_pool崩溃,排查后认为和asio::stream_descriptor遇到EAGAIN有关,尝试过多种non_blocking和native_non_blocking设置组合,高负载下仍报错。

另外还有一个问题:buffer_中有时会包含连续的两个键码,不知道如何解析并确定单个事件的正确长度,无法识别这类情况。

请问使用asio::stream_descriptor异步逐事件读取stdin的正确方式是什么?

解决方案

一、解决EAGAIN导致的崩溃问题

你遇到的Resource temporarily unavailable本质是EAGAIN错误,但asio默认会将部分系统错误转换为异常抛出,尤其是在异步操作的内部处理中。要避免崩溃,需确保所有可能的错误都被正确捕获处理,同时调整流描述符的非阻塞设置和读取逻辑:

  1. 强制设置native_non_blocking为true
    初始化stdin_后,必须将底层文件描述符设置为非阻塞模式,这是asio异步文件IO的必要条件:

    stdin_.native_non_blocking(true);
    

    注意不要混用non_blocking()方法,native_non_blocking直接操作底层文件描述符的O_NONBLOCK标志,是异步IO的核心依赖。

  2. 在回调中处理所有错误码
    原代码只处理了operation_aborted,忽略了其他错误(比如EAGAIN),需要扩展错误处理逻辑,避免未处理的错误触发异常:

    void tui::a_read_loop(){
        stdin_.async_read_some(buffer_,[this](const auto& ec, size_t n){
            if(ec){
                if(ec != asio::error::operation_aborted && ec.value() == EAGAIN){
                    // 资源暂时不可用,直接重新发起读操作
                    a_read_loop();
                }
                // 其他错误可根据需求处理,比如日志输出
                return;
            }
            //...treat input...
            a_read_loop();//chainloop
        });
    }
    

    这里明确捕获EAGAIN错误,直接重新发起异步读,阻止错误向上传播导致崩溃。

  3. 确保strand的正确使用
    你的stdin_绑定了io_strand_,要保证所有对stdin_的操作都在strand上下文中执行,避免并发操作引发异常。

二、解析连续键码的问题

终端raw模式下,特殊按键(比如方向键、功能键)会发送多字节序列(例如方向键上是\x1b[A),单字节则是普通字符。要正确解析单个键盘事件,需维护一个输入缓冲区,逐步累积字节直到组成完整的按键序列:

  1. 维护累积缓冲区
    在tui类中添加成员变量std::vector<char> input_buffer_,用于暂存未解析的字节。

  2. 分步骤解析输入
    在异步读回调中,先将新读取的字节追加到input_buffer_,再尝试从缓冲区中解析完整的按键事件:

    // 在tui类中定义
    std::vector<char> input_buffer_;
    
    void tui::process_input(const char* data, size_t n){
        input_buffer_.insert(input_buffer_.end(), data, data + n);
        while(!input_buffer_.empty()){
            size_t consumed = parse_key_event(input_buffer_);
            if(consumed == 0){
                // 字节不足,无法组成完整事件,等待后续输入
                break;
            }
            input_buffer_.erase(input_buffer_.begin(), input_buffer_.begin() + consumed);
        }
    }
    
    size_t tui::parse_key_event(const std::vector<char>& buf){
        if(buf.empty()) return 0;
        // 普通ASCII字符(单字节)
        if(buf[0] != '\x1b'){
            handle_key(buf[0]);
            return 1;
        }
        // 转义序列(多字节),比如方向键、功能键
        if(buf.size() >= 2){
            if(buf[1] == '['){
                if(buf.size() >=3){
                    switch(buf[2]){
                        case 'A': handle_key(KEY_UP); return 3;
                        case 'B': handle_key(KEY_DOWN); return 3;
                        case 'C': handle_key(KEY_RIGHT); return 3;
                        case 'D': handle_key(KEY_LEFT); return 3;
                        // 其他功能键的转义序列可继续扩展
                    }
                }
            }
        }
        // 字节不足,返回0等待更多输入
        return 0;
    }
    

    这种方式可以逐步累积输入字节,直到能解析出完整的单个键盘事件,避免一次性处理多个键码的问题。

  3. 适配终端转义序列差异
    不同终端的转义序列可能略有不同(比如某些终端的方向键是\x1bOA而非\x1b[A),可根据目标终端调整解析逻辑,或使用ncurses等现成终端库简化键码解析。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 11:13:23