printf的归宿-数据打印到哪儿了

本文通过测试Linux内核路由查找算法时的意外发现,深入探讨了printf函数对程序性能的影响,特别是当输出目标为不同类型的终端设备时。并提出了将日志记录任务分离到专门进程中的优化思路。

以下内容转载自 https://www.toutiao.com/i6857334180586455565/

转自《Linux阅马场》

近日在一次测试Linux内核路由查找算法的过程中,发现一个printf语句竟然能将性能降低2/3。当然,使用“竟然”一词并不意味着这个问题是第一次发现,我的想法是,把它记录下来,让没有经验的同学对printf知其所以然,同时导出我对“性能攸关”的这类算法中记录日志的一个观点。

声明

我不会把大段的源代码贴在文章中,而只是希望能通过阐述原理把我的意思表达清楚。诚然,作为程序员没有代码好像一切都会很虚,不过同样的,也是因为代码,总是会把人逼进死胡同,代码只是一种实现,理解了原理,作为一个懂编程的程序员,任何人都可以写出一个自己的实现。 我会给出原理图,但是这图决不是我凭空想象的,来源在哪?当然是UNIX的相关标准以及Linux的具体实现代码。既然原理来自于Linux的代码,为何不贴出来分析一下呢?要知道,代码随着Linux的内核版本,C库的版本以及应用程序的版本变化而变化,不变的是思想!UNIX历经几十年,其思想不还在指导着千千万万的程序员吗?另外,有谁会去通读Linux内核代码呢?对于大多数的人而言,如果想知道printf或者任何其它的接口的原理,肯定不会去摆开架势做出一副要先了解Linux内核架构,C库架构作为前置知识,然后去跟踪调试其实现。在以上这个过程中,你会把大量的精力消耗在理解不相关的内容上,比如函数调用关系,层层嵌套的条件语句,或者调试器怎么使用,诸如此类。

关于printf

printf是一个接口,跟UNIX标准IO的write系统调用类似,但是更像C库的fwrite,因为同系列的函数中还有一个fprintf(至于同系列其它的函数,请自行man)。printf和fwrite的区别在于两点:

1.它可以格式化输出,如果用fwrite,它接受的是一个固定的buffer,你不得不在调fwrite之前先使用sprintf之类的函数格式化buffer;

2.它免除了你的fopen-fwrite-fclose这个序列的调用,因为它直接将格式化的内容写入UNIX进程自然打开的1号文件描述符,即标准输出。

既然printf写入了标准输出,那么接下来就要定义什么是标准输出。在早期UNIX年代,人们在终端或者伪终端操作机器,那时的输入基本都是键盘,磁带更古老的东西,而输出就是一个计算结果,需要展示出来给人看的那种,一般为终端屏幕,也可以是一条纸带,那么程序怎么知道输入和输出到底是什么呢?这就需要程序明确指定。UNIX的“一切皆文件”思想以及“分离抽象”思想彻底改变了这一切。

UNIX定义了抽象文件描述符0,1,2分别为标准输入,标准输出,标准错误输出。至于它们到底对应什么设备,你可以在程序初始化的时候显式重定向到任意设备,也可以在外部shell做类似的重定向,这样就把指明设备这件事从程序分离了出来。

我为什么不统一说一下fwrite调用对程序性能的影响呢?因为该调用之前你必须执行fopen,而fopen的一个参数明确表示了你希望写入的对象是什么,这就不会带来异议,毕竟如果你非要在性能测试的时候写CF卡,那也是你愿意。printf就不同了,它对效率的影响取决于标准输出是什么以及你是如何重定向标准输出的,所谓的标准输出并不是真实的设备,它只是一个抽象层,具体如何解释标准输出,还要依靠外部。

数据都去哪儿了

我以下面这个超级小的程序来说明printf的时候,数据都去哪了:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
        int i = 0;
        int c = atoi(argv[1]);
        for(; i < c; i++) {
                printf("############  %d\n", i);
        }
        return 0;
}

我先给出结果:
1.在/dev/tty1上直接执行time ./test 1000
...
######### 995
######### 996
######### 997
######### 998
######### 999

real 0m0.414s

user 0m0.003s

sys 0m0.411s


2.在/dev/tty1上执行time ./test 1000 >/dev/tty2
real 0m0.007s
user 0m0.003s
sys 0m0.007s

3.在SecureCRT上执行time ./test 1000
...
######### 997
######### 998
######### 999

real 0m0.010s

user 0m0.002s

sys 0m0.003s

4.在SecureCRT上执行time ./test 100000 >/dev/tty1,此时不切换tty
...
等了几秒,无结果,于是在键盘按下Alt-F2,切换到第二个tty,马上显示出了结果:
real 0m4.276s
user 0m0.066s
sys 0m4.204s

5.在tty1上执行time ./test 100000 >/dev/tty2:
real 0m0.499s
user 0m0.081s
sys 0m0.410s

 

6.在tty1上执行time ./test 100000 >/dev/null

real 0m0.030s

user 0m0.028s

sys 0m0.001s

通过以上的结果数据,我们可以得到以下的结论:a.对于tty终端而言,如果当前终端不是写入的终端,那么开销主要在内核态,且开销不是很大;

b.对于tty终端而言,如果当前终端是写入的终端,那么开销主要在内核态,且开销很大;

c.对于不管是tty还是远程的pty终端,写入/dev/null的开销主要在用户态,开销不大;

d.对于pty远程终端(/dev/pts/X),不管写入的是不是当前的pty终端,开销主要在内核态,且开销不是很大

e.对应上面的结果和结论,下面给出一幅图解,详细解释一下printf冰山下面的秘密:

printf的归宿-数据打印到哪儿了

 

我想上图已经很清楚了,如果不懂什么叫行规程(也叫线路规程)的话,请阅读《UNIX环境高级编程》的终端和伪终端章节,简单来说,它就是一个中间层,用来适配VFS接口和底层的具体驱动,比如解释和处理控制字符等。从上面的图中,我们可以看出,主要的开销几乎都集中在底层,而底层却偏偏是我们不能控制或者很难控制的。之所以上面的测试例子中ssh登录的终端对test性能的测试效果良好,但是那是因为网络环境好,你在一个64kbps相隔5k公里的线路上试一下。

 

小小的printf下面竟然藏着如此多的内容,并且很可能就是它成了你的程序的性能瓶颈,因为最底层的影响因素往往是不可控的。那么是不是就是意味着我要建议大家从来不用printf打印呢?或者说干脆就不要用标准输出呢?并不是这样。但是为何不把打印这种事交给本机的另一个进程呢?事实上,几乎所有的需要记录日志的系统都是这么做的,而syslog则迎合了这个思想。这种思想的背后就是“用可控制的一次IPC替换不可控制冰山之下的茫茫深海”

关于日志记录

日志记录一直都是“薛定谔猫”式的东西,因为日志记录作为一段代码,它已经是程序的一部分,不可能独立地观察程序的行为,如果说用镜像系统的话,那么这种行为就是被动的,你不得不镜像每一条指令,以发现一些关键的信息,要想主动记录关键事件,必须用日志系统。打印日志可以方便信息获取和审计,但是代价有时也是高昂的:1.你要设计一套日志回滚系统,防止存储空间被撑爆;2.你要让日志记录尽快完成,不能降低关键路径的性能;3.你要反复调试代码,确保日志记录的缓冲区不会溢出;4.为了让日志更短,语言能力不好的人组织的日志就像电报一样难以理解。...我认为,日志记录应该遵循以下的原则:1.除非必须要把事件发生的时间记录下来,否则就用计数器代替日志记录,一系列的事件映射成一系列的计数器,由用户决定什么时候查看事件发生了。事实上,Linux的网络子系统就是用的这种方式,所有的/proc/net/netstat就是这个查看接口。

这是一个非常好的问题:**`printf` 会把内容打印到哪里?** 答案取决于你所处的 **运行环境和标准输出(stdout)的重定向情况**。我们来详细分析不同场景下 `printf` 的输出去向。 --- ### 默认情况下,`printf` 输出到 **标准输出(stdout)** ```c printf(fmt, ##__VA_ARGS__); ``` 这行代码调用的是 C 标准库中的 `printf` 函数,它将格式化后的字符串输出到 **标准输出流(stdout)**。 #### 在不同的系统中,stdout 指向的位置不同: | 环境 | printf 输出到哪里 | |------|------------------| | **普通 PC 上的 Linux / Windows 可执行程序** | 终端(Terminal)或控制台(Console) | | **Linux Shell 执行的程序** | 当前终端,除非被重定向(如 `./app > log.txt`) | | **嵌入式系统(如裸机、RTOS)** | 可能无输出,或需要手动实现 `_write` / `fputc` 等底层函数 | | **单片机 + 串口调试** | 通常通过串口(UART)输出,需重定向 stdout 到串口 | | **内核模块 / Bootloader / FAT 文件系统驱动** | 若未配置输出设备,则 `printf` 可能无效甚至导致崩溃 | --- ### 关键点:`printf` 是否有效,依赖于底层 I/O 实现 在某些环境中(尤其是嵌入式系统),虽然你可以写 `printf`,但如果没有实现底层的字符输出函数,它是**不会工作的**。 #### 常见的底层挂钩函数包括: - **newlib(嵌入式常用 C 库)**: ```c int _write(int file, char *ptr, int len); ``` 必须由开发者实现,否则 `printf` 调用会卡住或忽略。 - **ARM Cortex-M 单片机示例(使用 UART)**: ```c int _write(int file, char *ptr, int len) { for (int i = 0; i < len; i++) { uart_send_byte(ptr[i]); // 发送到串口 } return len; } ``` > ✅ 这样之后,所有 `printf` 都会通过串口输出到电脑上的串口助手(如 SecureCRT、Putty)。 --- ### 如何确认 `printf` 输出到了哪里? 1. **桌面程序(Linux/macOS/Windows)** - 直接输出到终端。 - 可以用重定向查看: ```bash ./my_program # 输出到屏幕 ./my_program > output.log # 输出到文件 ./my_program | grep "Value" ``` 2. **嵌入式系统(如 STM32、ESP32、RTOS)** - 查看是否连接了串口线(TX/RX) - 使用串口调试工具接收数据 - 波特率必须匹配(如 115200) 3. **内核态代码(如 FAT 驱动在 OS 内核中)** - 不能直接用 `printf` - 通常使用专用日志函数,例如: - Linux Kernel: `printk()` - Zephyr RTOS: `LOG_INF()` - FreeRTOS: `FreeRTOS_printf()` - 如果你在内核中用了 `printf`,很可能链接失败或运行异常! --- ### 特别注意:你的宏名字是 `_FAT_DBGPRT_INFO` 从命名来看,这很可能是用于 **FAT 文件系统的调试输出**,常见于: - Bootloader(如 U-Boot) - 嵌入式文件系统(如 FatFs) - 自定义操作系统组件 👉 在这些环境下,`printf` 是否可用,完全取决于开发人员是否已经将标准输出重定向到某个物理接口(通常是串口)。 --- ### 如何验证和控制输出目标? #### ✅ 方法一:显式重定向 stdout(适用于嵌入式) ```c // 示例:让 printf 输出到串口 #include <stdio.h> // 重定向 stdout int fputc(int ch, FILE *f) { uart_putc(ch); // 写入串口寄存器 return ch; } ``` > 只要实现了 `fputc`,`printf` 就会自动走这个函数。 #### ✅ 方法二:修改宏,支持更灵活的日志后端 ```c #define _FAT_DBGPRT_INFO(fmt, ...) my_log_func("[FAT] " fmt, ##__VA_ARGS__) void my_log_func(const char* fmt, ...) { va_list args; va_start(args, fmt); vfprintf(stderr, fmt, args); // 或者输出到 ring buffer / 串口 va_end(args); } ``` 这样可以集中管理输出行为。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值