```TTY 和缓冲```
TTY and Buffering

原始链接: https://mattrighetti.com/2026/01/12/tty-and-buffering

## 缓冲解释:为什么你的程序不总是立即打印 你是否想过为什么程序的输出不会立即显示?这通常归结于缓冲。向控制台(TTY)写入时,库通常使用*行缓冲*,在每个换行符(`\n`)之后刷新数据。但是,当管道输出或重定向到文件(非TTY)时,使用*完全缓冲*,累积数据 – 通常高达4-8KB – 然后刷新。 标准错误(stderr)通常是无缓冲或行缓冲的,即使在管道传输时也是如此,以确保错误消息能及时显示。像Rust这样的语言清楚地展示了这一点;如果没有手动`flush()`,管道传输时的输出可能会延迟。 这种差异源于程序检测其环境的方式。TTY代表交互式终端会话,而非TTY是数据流。在程序中,你可以使用像Rust的`is_terminal()`这样的函数检测TTY。 这种区别会影响优化和用户体验。像`ripgrep`这样的工具利用TTY检测在终端中启用彩色输出以提高可读性,但在管道传输时禁用它以避免在文件中出现混乱的转义码。 有趣的是,Rust目前在TTY和非TTY环境中都使用行缓冲,这是一个优先考虑一致行为的 deliberate 选择,尽管开发者承认根据环境调整缓冲策略可以获得潜在的性能提升。理解缓冲有助于调试意外的输出延迟,并为不同的场景优化程序行为。

黑客新闻 新 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 TTY 和缓冲 (mattrighetti.com) 13 分,由 mattrighetti 发表于 2 小时前 | 隐藏 | 过去 | 收藏 | 讨论 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

Every developer has at least once in their career stumbled upon the scenario where a program would not print something as they initially thought. A few years ago, you would search for an answer on StackOverflow and find a page stating the dreaded

you have to flush the buffer

I’d never been curious enough to dig into the 'why' until recently. Let’s take a look at a quick example:

#include <stdio.h>
#include <unistd.h>

int main() {
    for (int i = 1; i <= 5; i++) {
        printf("line %d\n", i);
        sleep(1);
    }
    return 0;
}

If we compile and run this in our terminal, we’ll get the following:

$ gcc -o cprint cprint.c
$ ./cprint
line 1       ← appears at t=1
line 2       ← appears at t=2
line 3       ← appears at t=3
line 4       ← appears at t=4
line 5       ← appears at t=5

Something different happens if you pipe the result:

$ ./cprint | cat
             ← nothing for 5 seconds...
line 1       ← appears at t=5
line 2
line 3
line 4
line 5

The answer lies in how buffering is handled in TTY and in non-TTY environments. A libc implementation will use line buffering if it detects that we’re in a TTY; otherwise, it will use full buffering if we’re in a non-TTY. Line buffering flushes data as soon as a \n character is encountered. On the other hand, full buffering typically accumulates data until the buffer is full, often around 4KB to 8KB.

It is worth noting that stderr is an exception to this rule. While stdout changes behavior based on the environment, stderr is typically unbuffered or line buffered even when piped. This ensures that if a program crashes, the error message reaches the screen immediately rather than being stuck in a full buffer.

Our previous example doesn’t demonstrate the effect of line buffering clearly because we used newlines. Let’s look at a Rust example that highlights this:

use std::io::{self, Write};
use std::thread::sleep;
use std::time::Duration;

fn main() {
    print!("Hello");
    sleep(Duration::from_secs(3));
    println!(" World!");
}
$ cargo run
                ← nothing for 3 seconds...
Hello World!

Rust’s print! implementation also performs line buffering, which causes the first print to only show up along with the second one after a \n character is encountered. You won’t notice any difference if you run cargo run | cat in this case as both behave the same: output is printed in a single shot after 3 seconds.

However, we can force a flush to override this behavior:

use std::io::{self, Write};
use std::thread::sleep;
use std::time::Duration;

fn main() {
    print!("Hello");
    io::stdout().flush().unwrap();
    sleep(Duration::from_secs(3));
    println!(" World!");
}
$ cargo run
Hello           ← t=0
Hello World!    ← t=3

We’re forcing a flush this time, and the first print appears immediately upon program execution. Then the final print writes its content after 3 seconds. What happens if we pipe this flushed output?

$ cargo run | cat
Hello           ← t=0
Hello World!    ← t=3

If you thought that | cat would change the result to print everything after 3s, you’re wrong. A manual flush will override the full buffering behavior and push the buffered data through regardless of the environment.

Now we know that different libraries can behave differently when TTY/non-TTY is detected, but what is a TTY?

In short, TTYs are interactive sessions opened by terminals. By contrast, non-TTYs include data streams like pipes and redirects..

How do we detect a TTY programmatically? Different languages provide their own methods. Rust has a simple is_terminal() function as part of the std::io::IsTerminal trait.

use std::io::{self, IsTerminal, Write};

fn main() {
    let is_tty = io::stdout().is_terminal();
    if is_tty {
        write!(&mut io::stdout(), "TTY!").unwrap();
    } else {
        write!(&mut io::stdout(), "NOT TTY!").unwrap();
    }
}
$ cargo run
TTY!

$ cargo run | cat
NOT TTY!

$ cargo run > output && cat output
NOT TTY!

This output confirms the underlying logic: when we pipe to cat or redirect to a file, the program detects a non-TTY environment.

By now, you may have guessed why this distinction matters. Far from being a niche technical detail, TTY detection is the base for a wide range of optimizations and DX decisions that depend on knowing exactly who or what is on the other end of the line.

Let’s take a real example from the ripgrep CLI tool.

let mut printer = StandardBuilder::new()
    .color_specs(ColorSpecs::default_with_color())
    .build(cli::stdout(if std::io::stdout().is_terminal() {
        ColorChoice::Auto
    } else {
        ColorChoice::Never
    }));

This is a classic use case: colored text is essential for human readability in a terminal, but it becomes a nuisance in a non-TTY environment. If you’ve ever tried to grep through a file only to find it littered with messy ANSI escape codes like ^[[31m, you know exactly why ripgrep makes this choice.

/// Returns a possibly buffered writer to stdout for the given color choice.
///
/// The writer returned is either line buffered or block buffered. The decision
/// between these two is made automatically based on whether a tty is attached
/// to stdout or not. If a tty is attached, then line buffering is used.
/// Otherwise, block buffering is used. In general, block buffering is more
/// efficient, but may increase the time it takes for the end user to see the
/// first bits of output.
///
/// If you need more fine grained control over the buffering mode, then use one
/// of `stdout_buffered_line` or `stdout_buffered_block`.
///
/// The color choice given is passed along to the underlying writer. To
/// completely disable colors in all cases, use `ColorChoice::Never`.
pub fn stdout(color_choice: termcolor::ColorChoice) -> StandardStream {
    if std::io::stdout().is_terminal() {
        stdout_buffered_line(color_choice)
    } else {
        stdout_buffered_block(color_choice)
    }
}

This implementation brings us full circle. By checking is_terminal(), ripgrep chooses the best of both worlds: line buffering to output line by line when a human is watching the terminal, and block buffering for max performance when the output is being sent to another file or process.

You may have noticed that I’ve used Rust for every example except the first one. There is a very specific and interesting reason for that.

Our first example’s aim is to show full buffering when the program is run in non-TTYs. Let’s revisit that C example:

#include <stdio.h>
#include <unistd.h>

int main() {
    for (int i = 1; i <= 5; i++) {
        printf("line %d\n", i);
        sleep(1);
    }
    return 0;
}

Now here’s the equivalent Rust code:

use std::thread::sleep;
use std::time::Duration;

fn main() {
    for i in 1..=5 {
        println!("line {}", i);
        sleep(Duration::from_secs(1));
    }
}
$ cargo run
line 1       ← appears at t=1
line 2       ← appears at t=2
line 3       ← appears at t=3
line 4       ← appears at t=4
line 5       ← appears at t=5

$ cargo run | cat
line 1       ← appears at t=1
line 2       ← appears at t=2
line 3       ← appears at t=3
line 4       ← appears at t=4
line 5       ← appears at t=5

Unlike the C version, Rust produces identical output in both TTY and non-TTY environments and line buffering is used in both cases. How’s that even possible?

Well, I previously wrote that this behavior is an implementation detail and not something that happens in every language or library. Surprisingly, Rust, as of now, uses line buffering for both TTYs and non-TTYs. We can see this by looking at the Stdout implementation:

#[stable(feature = "rust1", since = "1.0.0")]
pub struct Stdout {
    // FIXME: this should be LineWriter or BufWriter depending on the state of
    //        stdout (tty or not). Note that if this is not line buffered it
    //        should also flush-on-panic or some form of flush-on-abort.
    inner: &'static ReentrantLock<RefCell<LineWriter<StdoutRaw>>>,
}

As you can see, LineWriter is the default struct used by Stdout. The FIXME comment shows the Rust team acknowledges that ideally they should check if something is executed in TTYs or not and use LineWriter or BufWriter accordingly, but I guess this was not on their priority list.

Next time your output isn’t appearing when you expect it to, you’ll know exactly where to look. And it will be interesting to see if the Rust team eventually addresses that FIXME and implements the best buffering strategy for different environments. Until then, at least Rust’s consistent behavior means one less thing to debug.

If you want to read more about TTYs, here are some really cool resources:

联系我们 contact @ memedata.com