EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
本帖最后由 Heaven_1 于 2022-5-24 14:17 编辑
4 r( p) r O6 G& ^
, Q0 g$ S. h) {8 j( P关于printfprintf是一个接口,跟UNIX标准IO的write系统调用类似,但是更像[color=inherit !important]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的时候,数据都去哪了:
* H5 c! I P7 p#include <stdio.h>
) U0 o: E+ d3 Y0 q1 [+ Y I/ s#include <stdlib.h>' p) `: g5 _6 _ h
int main(int argc, char **argv), ?/ @6 e$ U; q
{ int i = 0;
$ b- A! L! J5 |( A7 L$ s0 v2 Aint c = atoi(argv[1]);' j" S! T+ k g% U3 C3 G
for(; i < c; i++) { 0 t, ^/ J1 f H
printf("############ %d\n", i);
/ @5 b6 N. O: O- w5 t/ d" B} return 0;
6 P+ C: x2 l, k; [} 我先给出结果:
5 o0 o1 K6 z: Z6 h- m5 R1.在/dev/tty1上直接执行time ./test 1000
D/ I$ p, { z% y... ######### 995; g. s: n( ~9 b& M7 Y% n
######### 996
2 g8 R G4 I( y######### 9977 c, t/ d. S* ^
######### 998. w1 e# J' N- Y. b; f, D1 \
######### 999* L) A: j) B) Q2 g. r/ o# h: L
real 0m0.414s
/ x( a, E# W$ k! v& p9 N. Auser 0m0.003s
2 \/ i: l4 t2 v2 asys 0m0.411s
0 i/ Z! }9 h# |# U4 w6 F6 D5 |# l
2.在/dev/tty1上执行time ./test 1000 >/dev/tty28 J6 @3 _1 C9 H8 v; i$ r4 c/ w
real 0m0.007s* _0 c; T# n. M% O
user 0m0.003s
) H/ f( l; ]3 o3 g7 I, C. hsys 0m0.007s
4 D6 t1 M4 H5 u$ \) c A2 K3.在SecureC[color=inherit !important]RT上执行time ./test 1000
* d/ a( Q$ M$ x7 N* D8 P...% w, W# R/ H7 X4 K8 j) ~
######### 997( ]: e# l0 j+ K- {
######### 998# Y X$ K4 T% s$ F) ] \
######### 999
- C! [. }: m# j9 f. E: Ireal 0m0.010s
; a9 a& d, m( d; l9 J4 e4 v. Uuser 0m0.002s
3 c5 V, ~" m4 s4 \sys 0m0.003s
; l' N) ?' }3 B
. z( N% \5 s+ B" G3 i+ B# \% }在SecureCRT上执行time ./test 100000 >/dev/tty1,此时不切换tty2 H$ l5 U6 _; ]8 n
...
2 T' p" [# t( ~/ B9 _等了几秒,无结果,于是在键盘按下Alt-F2,切换到第二个tty,马上显示出了结果:
1 s$ Y- o6 N( U4 t+ C% V0 Areal 0m4.276s
& y+ B% b) V4 F* ?% r+ suser 0m0.066s" q! [% W9 R; n T
sys 0m4.204s
# l* H' r0 N, R2 U+ j; \5 o5.在tty1上执行time ./test 100000 >/dev/tty2:
5 E+ {; }$ ]& Ereal 0m0.499s
' z. }) B Z; t$ S+ ]user 0m0.081s
# ~9 }* R5 O3 e9 o' k9 tsys 0m0.410s# E+ A# o7 W" t. M- V% G, i
6.在tty1上执行time ./test 100000 >/dev/null/ w. z+ z* ~- ]1 c# t- I
real 0m0.030s q6 m: F4 D/ }6 h1 k; S2 x
user 0m0.028s$ r! P5 f7 t+ ]0 P
sys 0m0.001s 通过以上的结果数据,我们可以得到以下的结论:0 b6 h0 y7 n. f; W+ J
a.对于tty终端而言,如果当前终端不是写入的终端,那么开销主要在内核态,且开销不是很大;
+ f* W4 {6 ~9 |! A" nb.对于tty终端而言,如果当前终端是写入的终端,那么开销主要在内核态,且开销很大;" Q: [4 q9 }. U1 I5 S3 a
c.对于不管是tty还是远程的pty终端,写入/dev/null的开销主要在用户态,开销不大;
! i }9 I2 h3 ]8 ?# ud.对于pty远程终端(/dev/pts/X),不管写入的是不是当前的pty终端,开销主要在内核态,且开销不是很大3 e+ a! h0 O2 n8 @1 A& R8 d0 r
e.对应上面的结果和结论,下面给出一幅图解,详细解释一下printf冰山下面的秘密:
3 B. d5 _1 l! U9 K, m( [ 线路规程串口举例:
简易图如下:
我想上图已经很清楚了,如果不懂什么叫行规程(也叫线路规程)的话,请阅读《UNIX环境高级编程》的终端和伪终端章节,简单来说,它就是一个中间层,用来适配VFS接口和底层的具体驱动,比如解释和处理控制字符等。从上面的图中,我们可以看出,主要的开销几乎都集中在底层,而底层却偏偏是我们不能控制或者很难控制的。之所以上面的测试例子中ssh登录的终端对test性能的测试效果良好,但是那是因为网络环境好,你在一个64kbps相隔5k公里的线路上试一下。 小小的printf下面竟然藏着如此多的内容,并且很可能就是它成了你的程序的性能瓶颈,因为最底层的影响因素往往是不可控的。那么是不是就是意味着我要建议大家从来不用printf打印呢?或者说干脆就不要用标准输出呢?并不是这样。但是为何不把打印这种事交给本机的另一个进程呢?事实上,几乎所有的需要记录日志的系统都是这么做的,而syslog则迎合了这个思想。这种思想的背后就是“用可控制的一次IPC替换不可控制冰山之下的茫茫深海 关于日志记录日志记录一直都是“薛定谔猫”式的东西,因为日志记录作为一段代码,它已经是程序的一部分,不可能独立地观察程序的行为,如果说用镜像系统的话,那么这种行为就是被动的,你不得不镜像每一条指令,以发现一些关键的信息,要想主动记录关键事件,必须用日志系统。打印日志可以方便信息获取和审计,但是代价有时也是高昂的:
! [9 O; D$ ?& s O! p4 f1.你要设计一套日志回滚系统,防止存储空间被撑爆;( X* Y' U/ B2 R G& \$ ?
2.你要让日志记录尽快完成,不能降低关键路径的性能;, L* M$ ^! N( a$ N6 G
3.你要反复调试代码,确保日志记录的缓冲区不会溢出;
0 z) g. O; Z! @ Z' l4.为了让日志更短,语言能力不好的人组织的日志就像电报一样难以理解。
7 j! \8 z/ u6 p, S+ m...4 _* G0 d1 z8 X4 I0 y$ \
我认为,日志记录应该遵循以下的原则: 1.除非必须要把事件发生的时间记录下来,否则就用计数器代替日志记录,一系列的事件映射成一系列的计数器,由用户决定什么时候查看事件发生了。事实上,[color=inherit !important]Linux的网络子系统就是用的这种方式,所有的/proc/net/netstat就是这个查看接口。 2.一定要有一个日志级别控制选项,用户可以决定是否记录日志,以及记录的日志详细到什么程度。 tty层接口 驱动代码摘自: lichee/linux-3.10/drivers/tty/serial/sunxi-uart.c 接收数据 static unsigned int sw_uart_handle_rx(struct sw_uart_port *sw_uport, unsigned int lsr) { …. tty_flip_buffer_push(&sw_uport->port.state->port); … } tty_termios_baud_rate(termios) tty_termios_encode_baud_rate(termios, baud, baud); 发送数据 % J" q* c# R% w! c. w
|