重定向问题

除了人工的分析之外,最简单最直接的调试方法要算printf了。不过,我们这里推荐使用的并不是初学C语言时使用的函数int printf(const char *format, ...),而是稍微复杂一点的fprintf()函数,因为它更方便我们之后重定向错误输出信息到指定的设备。fprintf()函数的原型如下:

int fprintf(FILE *stream, const char *format, ...)

可以看到,它与printf()函数相比多出来了第一个参数FILE *stream,其意义是将打印的内容输出到文件流指针stream指向的流。所谓流,通常是指程序输入或输出的一个连续的字节序列,设备(例如鼠标、键盘、磁盘、屏幕、调制解调器和打印机)的输入和输出都是用流来处理的,在C语言中,所有的流均以文件的形式出现——不一定是物理磁盘文件,还可以是对应于某个输入/输出源的逻辑文件。C语言提供了5种标准的流,你的程序在任何时候都可以使用它们,并且不必打开或关闭它们。以下列出了这5种标准的流。
------------------------------------------------
    名称          描 述             例 子
------------------------------------------------
    stdin        标准输入             键盘
    stdout       标准输出             屏幕
    stderr       标准错误              屏幕
    stdprn       标准打印机          LPT1端口
    stdaux       标准串行设备       COM1端口
------------------------------------------------
    其中,stdprn和stdaux并不总是预先定义好的,因为LPT1和COM1端口在某些操作系统中是没有意义的,而stdin,stdout 和stderr总是预先定义好的。此外,stdin并不一定来自键盘,stdout也并不一定显示在屏幕上,它们都可以重定向到磁盘文件或其它设备上。我们在头文件stdio.h中可以找到stdin,stdout 和stderr的定义如下:

/* Standard streams. */
extern struct _IO_FILE *stdin;      /* Standard input stream. */
extern struct _IO_FILE *stdout;     /* Standard output stream. */
extern struct _IO_FILE *stderr;     /* Standard error output stream. */

 
在使用fprintf()函数时,通常我们可以将第一个参数设为stdout或者stderr,打印出错调试信息的时候则推荐使用stderr而不是stdout,这是一种惯例,同时也由于内核在处理stdout和stderr时的优先级不一样,后者的优先级要高一些,因此有时候如果程序异常退出时,stderr能得到输出,而stdout就不行。
printf(...) 实际上相当于fprintf(stdout, ...),这也是为什么我们不推荐使用它的原因。在输出调试信息的时候,我们推荐使用fprintf(stderr, …),或者使用某个指定的文件流fprintf(some_stream, …)。
那么具体如何在必要的时候重定向 fprintf() 中的调试信息呢?来看看下面的一些方法:

当调试信息的量比较大,需要一些时间或者其他辅助工具来搜索过滤时,仅仅利用显示屏幕来输出调试信息是不够的,这时我们经常将这些信息输出到所谓的日志文件(log)中,之后再仔细的分析log文件来发现问题。
Ø       利用ShellI/O重定向
简单的写log方法可以通过shell的I/O重定向机制来实现,比如下面的代码:

     1 #include <stdio.h>

 
     2
     3 int main()
     4 {
     5      fprintf(stdout, "This is a standard output info!\n");
     6      fprintf(stderr, "This is a standard error output info!\n");
     7      return 0;
     8 }

 

 
在默认条件下,编译运行的结果是打印信息都输出在屏幕上:

$ gcc fprint.c -o fprint

 
$ ./fprint
This is a standard output info!
This is a standard error output info!

 

这是因为默认情况下,shell所打开的stdout和stderr设备都是显示屏幕。不过我们可以通过shell的重定向功能来将打印信息写到文件中去。比如:

$ ./fprint >output.log

 
This is a standard error output info!
$ cat output.log
This is a standard output info!

 

这样,我们把stdout的输出写到了文件output.log中,不过stderr的输出还是在屏幕上。如何重定向stderr呢?这需要用到shell定义的文件描述符。在shell下stdin, stdout, 和stderr的文件描述符分别是0, 1和2,我们可以用下面的方法重定向:

$ ./fprint >output.log 2>error.log

 
$ cat output.log
This is a standard output info!
$ cat error.log
This is a standard error output info!
$
$ ./fprint >output.log 2>&1
$ cat output.log
This is a standard error output info!
This is a standard output info!

 

其中./fprint >output.log 2>error.log分别将stdout和stderr的输出写入到文件output.log和error.log中,而./fprint >output.log 2>&1则表示将stderr的输出追加到stdout的文件output.log中(结果是output.log中既有stdout输出也有stderr输出)。
一些常用的shell I/O语法如下:
cmd > file  把 stdout 重定向到 file 文件中
cmd >> file  把 stdout 重定向到 file 文件中(追加)
cmd 1> fiel  把 stdout 重定向到 file 文件中
cmd > file 2>&1  把 stdout 和 stderr 一起重定向到 file 文件中
cmd 2> file  把 stderr 重定向到 file 文件中
cmd 2>> file  把 stderr 重定向到 file 文件中(追加)
cmd >> file 2>&1  把 stderr 和 stderr 一起重定向到 file 文件中(追加)
在平时的简单调试中,我们可以灵活利用这些方法来快速得到log文件。
 
Ø        freopen()进行重定向
有时候我们要求在程序中能够控制标准流的重定向,这时可以利用标准C库函数freopen()。freopen()的函数原型如下:

FILE *freopen(const char *filename, const char *mode, FILE *stream)

 

 

 
下面的代码用来测试用函数freopen()重定向stderr:

     1 #include <stdio.h>

 
     2
     3 int main()
     4 {
     5      if (freopen("err.log", "w", stderr)==NULL)
     6          fprintf(stderr, "error redirecting stderr\n");
     7      fprintf(stdout, "This is a standard output info!\n");
     8      fprintf(stderr, "This is a standard error output info!\n");
     9      fclose(stderr);
    10      return 0;
    11 }

 

在第5行我们用freopen()函数将stderr重定向到了”err.log”文件,这样得到的结果如下:

$ gcc print_log.c -o print_log

 
$ ./print_log
This is a standard output info!
$ cat err.log
This is a standard error output info!

 

可见第 8 行打印到 stderr 的信息被重定向到了 err.log 文件中,而第 7 stdout 的打印信息则还是输出到了屏幕上


#include<unistd.h>

  int dup(int fd);

  int dup2(int fd1,int fd2);
  两个均为 复制一个现存 的文件的描述
  两个函数的返回:若成功为新的文件描述,若出错为-1;

  由dup返回的新文件描述符一定是当前可用文件描述中的最小数值。用dup2则可以用fd2参数指定新的描述符数值。如果fd2已经打开,则先关闭。若fd1=fd2,则dup2返

回fd2,而不关闭它。通常使用这两个系统调用来重定向一个打开的文件描述符。


函数简介

  函数名:  dup
  功 能: 复制一个文件句柄
  用 法: int   dup(int handle);
  相关函数: dup2、fdopen、freopen、mbsdup、wcsdup、strdup

  1. #include <string.h>  
  2. #include <stdio.h>  
  3.   
  4. void flush(FILE *stream);  
  5.   
  6. int main(void)  
  7. {  
  8.     FILE    *fp;  
  9.     char    msg[] = "This is a test";  
  10.       
  11.     /*create a file*/  
  12.     fp = fopen("DUMMY.FIL","w");  
  13.       
  14.     /*write some date to the file*/  
  15.     fwrite(msg,strlen(msg),1,fp);  
  16.     printf("Press any key to flush DUMMY.FIL");  
  17.     getchar();  
  18.   
  19.     /*flush the data to DUMMY.FIL without closing it*/  
  20.     flush(fp);  
  21.     printf("\n File was flushed,Press any key to quit:");  
  22.     getchar();  
  23.     return 0;  
  24. }  
  25.   
  26. void flush(FILE *stream)  
  27. {  
  28.     int duphandle;  
  29.   
  30.     /*flush TC's internal buffer*/  
  31.     fflush(stream);  
  32.       
  33.     /* make a duplicate file handle*/  
  34.     duphandle = dup(fileno(stream));  
  35.     /* close the duplicate handle to flush the DOS buffer*/  
  36.     close(DUPHANDle);  
  37. }  


一、单个进程内的dup和dup2

假设进程A拥有一个已打开的文件描述符fd3,它的状态如下
进程A的文件描述符表(before dup2)

[plain]  view plain copy print ?
  1.    ------------  
  2. fd0 0   | p0  
  3.    ------------  
  4. fd1 1   | p1 -------------> 文件表1 ---------> vnode1  
  5.    ------------  
  6. fd2 2   | p2  
  7.    ------------  
  8. fd3 3   | p3 -------------> 文件表2 ---------> vnode2  
  9.    ------------  
  10. ... ...  
  11. ... ...  
  12.    ------------  

经下面调用:
n_fd = dup2(fd3, STDOUT_FILENO);后进程状态如下:
进程A的文件描述符表(after dup2)
[plain]  view plain copy print ?
  1.    ------------  
  2. fd0 0   | p0  
  3.    ------------  
  4. n_fd 1   | p1 ------------  
  5.    ------------               \  
  6. fd2 2   | p2                  \  
  7.    ------------                 _\|  
  8. fd3 3   | p3 -------------> 文件表2 ---------> vnode2  
  9.    ------------  
  10. ... ...  
  11. ... ...  
  12.    ------------  

解释如下:n_fd = dup2(fd3, STDOUT_FILENO)表示n_fd与fd3共享一个文件表项(它们的文件表指针指向同一个文件表项),n_fd在文件描述符表中的位置为 STDOUT_FILENO的位置,而原先的STDOUT_FILENO所指向的文件表项被关闭,我觉得上图应该很清晰的反映出这点。按照上面的解释我们就可以解释CU中提出的一些问题:
(1) "dup2的第一个参数是不是必须为已打开的合法filedes?" -- 答案:必须。
(2) "dup2的第二个参数可以是任意合法范围的filedes值么?" -- 答案:可以,在Unix其取值区间为[0,255]。

(每一个进程都对应一个结构体  task_struct ,里面包含一个数据成员 :

  1. /* open file information */  
  2.         struct files_struct *files;  


files_struct 结构体定义如下:

  1. /* 
  2.  * Open file table structure 
  3.  */  
  4. struct files_struct {  
  5.   /* 
  6.    * read mostly part 
  7.    */    
  8.         atomic_t count;  
  9.         struct fdtable __rcu *fdt;  
  10.         struct fdtable fdtab;  
  11.   /* 
  12.    * written part on a separate cache line in SMP 
  13.    */    
  14.         spinlock_t file_lock ____cacheline_aligned_in_smp;  
  15.         int next_fd;  
  16.         struct embedded_fd_set close_on_exec_init;  
  17.         struct embedded_fd_set open_fds_init;  
  18.         struct file __rcu * fd_array[NR_OPEN_DEFAULT];  
  19. };  
 对于上面的两个句柄 n_fd  fd3 他们在 files_struct 里面的数组 fd_array 里面对应的数值 是相等的。



二、重定向后恢复
CU上有这样一个帖子,就是如何在重定向后再恢复原来的状态?首先大家都能想到要保存重定向前的文件描述符。那么如何来保存呢,象下面这样行么?
int s_fd = STDOUT_FILENO;
int n_fd = dup2(fd3, STDOUT_FILENO);
还是这样可以呢?
int s_fd = dup(STDOUT_FILENO);
int n_fd = dup2(fd3, STDOUT_FILENO);
这两种方法的区别到底在哪呢?答案是第二种方案才是正确的,分析如下:按照第一种方法,我们仅仅在"表面上"保存了相当于fd_t(按照我前面说的理解方法)中的index,

而在调用dup2之后,ptr所指向的文件表项由于计数值已为零而被关闭了,我们如果再调用dup2(s_fd, fd3)  (注意此时s_fd对应的文件表项已经关闭)就会出错。而第二种方法我们首先做一下复制(这样对应的文件表项不会被关闭),复制后的状态如下图所示:
进程A的文件描述符表(after dup)

[plain]  view plain copy print ?
  1.    ------------  
  2. fd0 0   | p0  
  3.    ------------  
  4. fd1 1   | p1 -------------> 文件表1 ---------> vnode1  
  5.    ------------                 /|  
  6. fd2 2   | p2                /  
  7.    ------------             /  
  8. fd3 3   | p3 -------------> 文件表2 ---------> vnode2  
  9.    ------------          /  
  10. s_fd 4   | p4 ------/  
  11.    ------------  
  12. ... ...  
  13. ... ...  
  14.    ------------  
调用dup2后状态为:
进程A的文件描述符表(after dup2)
[plain]  view plain copy print ?
  1.    ------------  
  2. fd0 0   | p0  
  3.    ------------  
  4. n_fd 1   | p1 ------------  
  5.    ------------               \  
  6. fd2 2   | p2                 \  
  7.    ------------                _\|  
  8. fd3 3   | p3 -------------> 文件表2 ---------> vnode2  
  9.    ------------  
  10. s_fd 4   | p4 ------------->文件表1 ---------> vnode1  
  11.    ------------  
  12. ... ...  
  13. ... ...  
  14.    ------------  

dup(fd)的语意是返回的新的文件描述符与fd共享一个文件表项。就如after dup图中的s_fd和fd1共享文件表1一样。




用了freopen,却想恢复控制台输出的方法

有时用了freopen,怎么搞都不能从控制台输入输出了,相当郁闷。于是总结了一下用freopen后恢复到控制台的方法。

方法一、

        在freopen前,调用dup把标准输出的文件描述符保存起来

              old = _dup( 1 );

             FILE *fp=freopen("a.out","w",stdout);

             fflush(fp);//将输出缓冲区清空
             _dup2( old, 1 );//恢复标准输出文件描述符

             printf("方法一 OK!\n");

方法二、(适用在windows下)

            FILE *fp=freopen("a.out","w",stdout);

            fflush(fp);//将输出缓冲区清空

            freopen( "CON", "w", stdout ); //定向输出到控制台

方法三、(使用在linux下)

           FILE *fp=freopen("a.out","w",stdout);

          fflush(fp);//将输出缓冲区清空

           freopen("/dev/tty", "w", stdout); 
           freopen("/dev/tty", "r", stdin); 


今天在做一个C项目时用到多线程,调试用到printf,但是发现每次打印信息都是程序执行完成后才全部打印,打印信息被阻塞了。
最终找到原因,是由于C的printf是默认带缓冲输出,所以只需要把缓冲关闭即可实时显示打印信息。方法如下:
在主程序打印之前执行

[cpp]  view plain copy
  1. setbuf(stdout,NULL);  

在UNIX系统中,用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell进程的控制终端(Controlling Terminal),在第 1 节 “引言”讲过,控制终端是保存在PCB中的信息,而我们知道fork会复制PCB中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端。默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。此外在第 33 章 信号还讲过,在控制终端输入一些特殊的控制键可以给前台进程发信号,例如Ctrl-C表示SIGINT,Ctrl-\表示SIGQUIT

第 28 章 文件与I/O中讲过,每个进程都可以通过一个特殊的设备文件/dev/tty访问它的控制终端。事实上每个终端设备都对应一个不同的设备文件,/dev/tty提供了一个通用的接口,一个进程要访问它的控制终端既可以通过/dev/tty也可以通过该终端设备所对应的设备文件来访问。ttyname函数可以由文件描述符查出对应的文件名,该文件描述符必须指向一个终端设备而不能是任意文件。下面我们通过实验看一下各种不同的终端所对应的设备文件名。

例 34.1. 查看终端对应的设备文件名

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

int main()
{
    printf("fd 0: %s\n", ttyname(0));
    printf("fd 1: %s\n", ttyname(1));
    printf("fd 2: %s\n", ttyname(2));
    return 0;
}

在图形终端窗口下运行这个程序,可能会得到

$ ./a.out
fd 0: /dev/pts/0
fd 1: /dev/pts/0
fd 2: /dev/pts/0

再开一个终端窗口运行这个程序,可能又会得到

$ ./a.out
fd 0: /dev/pts/1
fd 1: /dev/pts/1
fd 2: /dev/pts/1

用Ctrl-Alt-F1切换到字符终端运行这个程序,结果是

$ ./a.out
fd 0: /dev/tty1
fd 1: /dev/tty1
fd 2: /dev/tty1

读者可以再试试在Ctrl-Alt-F2的字符终端下或者在telnetssh登陆的网络终端下运行这个程序,看看结果是什么。


  如果对一个程序中的print等输出进行重定向,则应该在printf后面加下输出的刷新fflush(stdout),不然输出不会马上写入到重定向的文件里,因为重定向后输出只有满4K之后才会写入一次文件中,如下:

  #include <stdio.h>

#include <stdlib.h>

int main( int argc, char ** argv )

{

while (1)

{

printf( "hello, world!/n" );

fflush(stdout);

sleep(2);

}

 

return 0;

}

 

编译程序:gcc –o test test.c

执行重定向:./test > info

如果没有fflush(stdout),则文件info中到等很长很长时间之后才会看到相关的输出信息(等输出信息满4K),而有fflush(stdout)时查看info会马上看到输出信息。





  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值