当前位置: 技术问答>linux和unix
100分难题,高手帮忙。关于printf的问题。
来源: 互联网 发布时间:2015-10-26
本文导语: 先说明,我已经知道了printf在屏幕输出的时候和在重定向到文件输出的时候采取的缓冲方式不一样。一个是行缓冲,一个是全缓冲。 而且我也知道了可以用 setvbuf 来设置缓冲的类型。 比如下面的程序: int main() {...
先说明,我已经知道了printf在屏幕输出的时候和在重定向到文件输出的时候采取的缓冲方式不一样。一个是行缓冲,一个是全缓冲。 而且我也知道了可以用 setvbuf 来设置缓冲的类型。 比如下面的程序:
int main() {
printf("%d %dn",getpid(),getppid());
if( fork()==0 )
printf("%d %dn",getpid(),getppid()):
return 0;
}
将它编译成a.out之后,运行 ./a.out 和 运行 ./a.out > txt 的结果是不一样的。 再说明一下,我已经知道了原因是因为printf所采取的缓冲方式不同。
我现在的问题是,既然a.out是已经编译好了的。对于相同的一个a.out,在屏幕输出和文件输出的时候为什么缓冲的方式不一样,是什么东西(具体机制)改变了printf的缓冲方式?( 可能涉及到unix系统的问题 ) 。 另外,最好还能详细说一下printf是在哪些情况下使用行缓冲,哪些情况下使用全缓冲,哪些情况下不缓冲?
int main() {
printf("%d %dn",getpid(),getppid());
if( fork()==0 )
printf("%d %dn",getpid(),getppid()):
return 0;
}
将它编译成a.out之后,运行 ./a.out 和 运行 ./a.out > txt 的结果是不一样的。 再说明一下,我已经知道了原因是因为printf所采取的缓冲方式不同。
我现在的问题是,既然a.out是已经编译好了的。对于相同的一个a.out,在屏幕输出和文件输出的时候为什么缓冲的方式不一样,是什么东西(具体机制)改变了printf的缓冲方式?( 可能涉及到unix系统的问题 ) 。 另外,最好还能详细说一下printf是在哪些情况下使用行缓冲,哪些情况下使用全缓冲,哪些情况下不缓冲?
|
其实IO库的函数在同样的代码下缓冲的方式是一样的,只是在内核驱动上改变了
比如:首先在open函数的时候就会用一个系统调用open,这时内核得到此信息后,就确定了你打开的是设备还是磁盘文件,当无论你向终端还是向文件打印一写字符,在库函数最后都是用的同一系统调用,就是write系统调用,这时控制权会交给内核,内核已经知道你打算向什么地方输出,自然就采用了默认的缓冲方式,当然你想要内核改变默认的输出方式,就得使用setvbuf库函数,这个库函数最终还是会调用一个ioctl之类的系统调用来通知内核改变其默认工作方式,内核在最终输出时就改变了结果,从而导致了你出现的问题。
LZ一直都认为是库函数改变了你的行为,实际上真正改变的原因是内核中的驱动所用的默认工作方式
分析如下:
父进程打印一些字符时:
终端控制台方式:在库函数中进行了getpid系统调用,并返回了值,然后再连接成一个完整的字符串,这时它就会向内核发出write系统调用,内核掌握控制权,然后经过处理之后直接写入控制台设备,此时buffer1中的字符串被清空(注意,这是在内核空间的驱动一级的buffer,字符设备的处理方式就是:接收一串字符,它就立即向设备写入,写入完成之后就清掉缓冲区中的待写数据,所以buffer1的数据就为空了,并且write系统调用返回,从而返回到用户态,将CPU控制权交给printf库函数,最后返回到应用程序)
用文件方式:在库函数中进行了getpid系统调用,并返回了值,然后再连接成一个完整的字符串,这时它就会向内核发出write系统调用,内核掌握控制权,它发现这是在写一个块设备(也就是磁盘文件),内核驱动对块设备并不是立即处理,也就是说它只是把它放在了高速缓冲区中,并未向实际文件写入数据,此时由于写操作被挂起,所以就不会去清空buffer1中的数据,虽然实际的写操作被挂起,但系统调用还是会成功返回,从而返回到应用程序
派生进程时:
终端方式:共享父进程的地址空间,同样共享了buffer1的数据,但buffer1中已没有数据,再次调用printf,过程与上面一样,在内核驱动一级,将buffer2的数据写到控制台,返回
重定向方式:共享父进程的地址空间,同样共享了buffer1的数据,此时buffer1中还有数据,再次调用printf,与上面过程一样,在内核驱动一级,buffer1的数据还存在,所以在进行块设备驱动上,会将buffer1与buffer2的数据一起放在高速缓冲区,这时在高速缓冲区有三个字符串待写
结束程序时,会刷新高速缓冲区,也就是说在关闭文件描述符时,内核在驱动上会要求将高速缓冲区上的相关数据立即写入设备,之后才允许关闭文件,回收系统资源,这时就会将三个字符串全部写入文件,这就是导至你输出不一致的原因,如果让控制台使用全缓冲,它就跟使用文件时的过程一样,也就没什么差别了
比如:首先在open函数的时候就会用一个系统调用open,这时内核得到此信息后,就确定了你打开的是设备还是磁盘文件,当无论你向终端还是向文件打印一写字符,在库函数最后都是用的同一系统调用,就是write系统调用,这时控制权会交给内核,内核已经知道你打算向什么地方输出,自然就采用了默认的缓冲方式,当然你想要内核改变默认的输出方式,就得使用setvbuf库函数,这个库函数最终还是会调用一个ioctl之类的系统调用来通知内核改变其默认工作方式,内核在最终输出时就改变了结果,从而导致了你出现的问题。
LZ一直都认为是库函数改变了你的行为,实际上真正改变的原因是内核中的驱动所用的默认工作方式
分析如下:
父进程打印一些字符时:
终端控制台方式:在库函数中进行了getpid系统调用,并返回了值,然后再连接成一个完整的字符串,这时它就会向内核发出write系统调用,内核掌握控制权,然后经过处理之后直接写入控制台设备,此时buffer1中的字符串被清空(注意,这是在内核空间的驱动一级的buffer,字符设备的处理方式就是:接收一串字符,它就立即向设备写入,写入完成之后就清掉缓冲区中的待写数据,所以buffer1的数据就为空了,并且write系统调用返回,从而返回到用户态,将CPU控制权交给printf库函数,最后返回到应用程序)
用文件方式:在库函数中进行了getpid系统调用,并返回了值,然后再连接成一个完整的字符串,这时它就会向内核发出write系统调用,内核掌握控制权,它发现这是在写一个块设备(也就是磁盘文件),内核驱动对块设备并不是立即处理,也就是说它只是把它放在了高速缓冲区中,并未向实际文件写入数据,此时由于写操作被挂起,所以就不会去清空buffer1中的数据,虽然实际的写操作被挂起,但系统调用还是会成功返回,从而返回到应用程序
派生进程时:
终端方式:共享父进程的地址空间,同样共享了buffer1的数据,但buffer1中已没有数据,再次调用printf,过程与上面一样,在内核驱动一级,将buffer2的数据写到控制台,返回
重定向方式:共享父进程的地址空间,同样共享了buffer1的数据,此时buffer1中还有数据,再次调用printf,与上面过程一样,在内核驱动一级,buffer1的数据还存在,所以在进行块设备驱动上,会将buffer1与buffer2的数据一起放在高速缓冲区,这时在高速缓冲区有三个字符串待写
结束程序时,会刷新高速缓冲区,也就是说在关闭文件描述符时,内核在驱动上会要求将高速缓冲区上的相关数据立即写入设备,之后才允许关闭文件,回收系统资源,这时就会将三个字符串全部写入文件,这就是导至你输出不一致的原因,如果让控制台使用全缓冲,它就跟使用文件时的过程一样,也就没什么差别了
|
缓冲的缺省设定是:
stderr: 无缓冲
stdin: 行缓冲
stdout: 页缓冲
I/O缓冲并不完全是由编译好的可执行程序决定,而是由运行时程序所连接的具体I/O设备决定。程序只是决定是stderr还是stdout。所以,当你在terminal上打入./a.out时,因为输出设备是tty,它的缺省设定是页缓冲。而当你在terminal上打入./a.out > txt 时,因为shell将‘>’解释为输出转向,目标是文件,此时的std被连接到文件系统,所以具体缓冲由txt所在的文件系统决定。
我们知道file descriptor 0、1、2分别为每个执行中程序的stdin、stdout和stderr。它们和设备对应关系是是可以动态改变的,方法很简单就是关闭那个file descriptor,然后打开新的目标设备:
close(1);
open("txt");
在执行完上面两行程序后以后,stdout被改变到以txt命名的文件中去了。
'>' 在shell 里是用类似于上面的程序来实现输出转向的:
if (fork() == 0){
close(1);
open(filename); //filename已被设成'txt'
exec(...);
}
...
如果你仔细了解一下file descriptor是如何工作的,你可能会更清楚。
stderr: 无缓冲
stdin: 行缓冲
stdout: 页缓冲
I/O缓冲并不完全是由编译好的可执行程序决定,而是由运行时程序所连接的具体I/O设备决定。程序只是决定是stderr还是stdout。所以,当你在terminal上打入./a.out时,因为输出设备是tty,它的缺省设定是页缓冲。而当你在terminal上打入./a.out > txt 时,因为shell将‘>’解释为输出转向,目标是文件,此时的std被连接到文件系统,所以具体缓冲由txt所在的文件系统决定。
我们知道file descriptor 0、1、2分别为每个执行中程序的stdin、stdout和stderr。它们和设备对应关系是是可以动态改变的,方法很简单就是关闭那个file descriptor,然后打开新的目标设备:
close(1);
open("txt");
在执行完上面两行程序后以后,stdout被改变到以txt命名的文件中去了。
'>' 在shell 里是用类似于上面的程序来实现输出转向的:
if (fork() == 0){
close(1);
open(filename); //filename已被设成'txt'
exec(...);
}
...
如果你仔细了解一下file descriptor是如何工作的,你可能会更清楚。
|
同意do_do(do_do) 的说法
我看了《unix操作系统设计》
shell程序的主循环(部分)如下:
/*读命令行直到"文件尾"*/
while(read(stdin,buffer,numcnars))
{
/*分析命令行*/
if(/*命令行含有&*/)
amper=1;//后台
else
amper=0;
/*对于非shell命令语言的命令部分*/
if(fork()==0)
{
/*重定向I/O吗?*/
if(/*输出重定向*/)
{
fd=creat(newfile,fmask);
close(stdout);
dup(fd);
close(fd);
/*标准输出被重定向*/
}
.............
.............
execve(command,command,0);
}
/*父进程继续从此处。。。*/
/*如果需要则等待子进程退出*/
if(amper==0)
retid=wait(&status);
}
为了重新定向标准输出到一个文件。子进程创建命令行定义的输出文件,
然后关闭以前的标准输出文件并复制新的输出文件的文件描述符。标准
输出文件变为重新定向的输出文件。然后子进程关闭创建该输出文件时
得到的那个文件描述符,这是为了节省文件描述符。
所以当shell分析到要重定向到文件时,标准输出由终端变为文件,相应的
缓冲方式也就由行缓冲变为全缓冲了。
我看了《unix操作系统设计》
shell程序的主循环(部分)如下:
/*读命令行直到"文件尾"*/
while(read(stdin,buffer,numcnars))
{
/*分析命令行*/
if(/*命令行含有&*/)
amper=1;//后台
else
amper=0;
/*对于非shell命令语言的命令部分*/
if(fork()==0)
{
/*重定向I/O吗?*/
if(/*输出重定向*/)
{
fd=creat(newfile,fmask);
close(stdout);
dup(fd);
close(fd);
/*标准输出被重定向*/
}
.............
.............
execve(command,command,0);
}
/*父进程继续从此处。。。*/
/*如果需要则等待子进程退出*/
if(amper==0)
retid=wait(&status);
}
为了重新定向标准输出到一个文件。子进程创建命令行定义的输出文件,
然后关闭以前的标准输出文件并复制新的输出文件的文件描述符。标准
输出文件变为重新定向的输出文件。然后子进程关闭创建该输出文件时
得到的那个文件描述符,这是为了节省文件描述符。
所以当shell分析到要重定向到文件时,标准输出由终端变为文件,相应的
缓冲方式也就由行缓冲变为全缓冲了。
|
由于文件不是一次就写入,所以保留有相应的缓冲区,直至./a.out执行完毕,再一并输出至txt
父:string1->buffer1;
此时文件写操作并没有发生,因此buffer1中的东东仍存在。
即使是行缓冲,这时通知接受端可以取走数据。但是“文件写”只是照单全收,挂帐处理。实际并没有取走数据。明白?
然后fork,
子:copy buffer1->buffer2;
string2->buffer2;
shell:buffer1>文件
buffer2>文件
最后就有3个string
如果是屏幕显示,不会象文件写入那样保留缓冲区,所以直接输出
父:string1->buffer1>显示;
此时及时取走数据,清空了buffer1;
子:string2 ->buffer2>显示;
最后只有2个string
结论:定向符‘>’的存在导致
父:string1->buffer1;
此时文件写操作并没有发生,因此buffer1中的东东仍存在。
即使是行缓冲,这时通知接受端可以取走数据。但是“文件写”只是照单全收,挂帐处理。实际并没有取走数据。明白?
然后fork,
子:copy buffer1->buffer2;
string2->buffer2;
shell:buffer1>文件
buffer2>文件
最后就有3个string
如果是屏幕显示,不会象文件写入那样保留缓冲区,所以直接输出
父:string1->buffer1>显示;
此时及时取走数据,清空了buffer1;
子:string2 ->buffer2>显示;
最后只有2个string
结论:定向符‘>’的存在导致
|
你仍然不明白我在说什么。
谁告诉你shell默认用了全缓冲?
shell并不了解缓冲类型,你在此说的缓冲都是指IO库的缓冲,这些缓冲由此库管理,别人,包括其他的库,系统调用,还有你那该死的应用程序,都不知道。
fopen---打开一个流,实际上调用open打开一个文件描述符,然后把一个流类型与此描述符绑定,这个流包括的内容很多,一个就是你希望看到的缓冲区类型,还有当前缓冲区指针,缓冲区大小,是否采用外部缓冲区,等等。
fXXX---操作流
fclose---刷新流,清理流数据结构,并关闭文件描述符。
你只可以看到了open一个文件时,fopen能预先了解到此文件类型,可能是字符设备,也可能是普通文件。那么fopen能相应的采取处理办法。
父进程exec时,会把他之前的文件描述符预先处理,如重定向,这样,如果你用了012的描述符,它们已经被父进程预先处理过了,极可能已经以fopen打开过。然后在子进程传递了它的状态。
谁告诉你shell默认用了全缓冲?
shell并不了解缓冲类型,你在此说的缓冲都是指IO库的缓冲,这些缓冲由此库管理,别人,包括其他的库,系统调用,还有你那该死的应用程序,都不知道。
fopen---打开一个流,实际上调用open打开一个文件描述符,然后把一个流类型与此描述符绑定,这个流包括的内容很多,一个就是你希望看到的缓冲区类型,还有当前缓冲区指针,缓冲区大小,是否采用外部缓冲区,等等。
fXXX---操作流
fclose---刷新流,清理流数据结构,并关闭文件描述符。
你只可以看到了open一个文件时,fopen能预先了解到此文件类型,可能是字符设备,也可能是普通文件。那么fopen能相应的采取处理办法。
父进程exec时,会把他之前的文件描述符预先处理,如重定向,这样,如果你用了012的描述符,它们已经被父进程预先处理过了,极可能已经以fopen打开过。然后在子进程传递了它的状态。