4 重定向
大家来看下面这个代码:
close(1); int fd=open(LOG,O_WRONLY | O_CREAT | O_APPEND,0666); if(fd<0) { perror("open:"); exit(1); } umask(0); printf("printf:hello linux\n"); printf("printf:hello linux\n"); printf("printf:hello linux\n"); fprintf(stdout,"fprintf:hello linux\n"); fprintf(stdout,"fprintf:hello linux\n"); fprintf(stdout,"fprintf:hello linux\n"); fprintf(stderr,"stderr:hello linux\n"); fprintf(stderr,"stderr:hello linux\n"); fprintf(stderr,"stderr:hello linux\n");
大家猜猜运行后结果是啥?
我们运行试试:
我们发现了屏幕中只输出打印了stderr的内容,却没有输出stdout以及用printf打印的内容。
我们查看log.txt中内容:
我们发现内容居然输出到了log.txt文件中,这种现象我们在讲解指令时已经说过了,叫做输出重定向(由于我们打开文件用的O_APPEND,所以叫做追加重定向更加合理)
那么这种重定向的原理是什么呢?
我们画个图来分析分析:
从图中我们清晰的看出由于我们先关闭了文件描述符为1的文件(也就是标准输出文件),当我们打开新文件时就将新文件的地址填充到下标为1的数组中,但是操作系统是不会关注下标为1的数组究竟指向的是谁,他只是负责执行。所以当我们使用printf以及fprintf的标准输出时并不会输出到标准输出文件(屏幕),而是重定向到了log.txt文件中。
同理当我们关闭了标准输入文件时,我们进行标准输入文件的读取时(也就是在键盘上读取)变成了从另外一个文件(此时文件下标为0指向的文件)中读取数据。
有了上面的理解,我们立即实操一下:要求将普通信息输出到nor.txt中,将错误信息输出到err.txt
参考代码:
close(1); umask(0); int fd1=open("nor.txt",O_CREAT | O_WRONLY | O_TRUNC,0666); close(2); int fd2=open("err.txt",O_CREAT | O_WRONLY | O_TRUNC,0666); printf("normal file\n"); printf("normal file\n"); printf("normal file\n"); fprintf(stderr,"err file\n"); fprintf(stderr,"err file\n"); fprintf(stderr,"err file\n"); fprintf(stderr,"err file\n");
这样我们就将普通信息输出到nor.txt中,将错误信息输出到err.txt中了。
除了这种方式,我们还可以使用命令行的方式来操作:
在file.c中没有关闭标准输出和标准错误文件,我们通过命令行方式来进行分类:
通过这种方式也能够将文件信息正确的分类,那如果我们想要将错误信息也打印到log.txt中呢?
可以通过下面这种方式:
其实实际上这里是省略了一个1的,完整写法可以是这样:
在平时练习时我们无论使用命令行还是在代码中实现都是可以的。
但是我们想想在代码中关闭文件的写法是不是有点太挫了,明明只需要替换一下文件地址就可以了为啥还要整一个关闭文件的操作呢?所以系统又给我们提供了另外一个接口:dup2
一般我们经常使用dup2这个接口。
我们来看看它的参数:
int dup2(int oldfd, int newfd);
第一个参数是oldfd,第二个参数是newfd,那么假如我们将标准输出文件关闭(1)打开了一个新文件,新文件的文件描述符是fd,那么1和fd谁是oldfd?谁是newfd?
我相信很多人都会说1是oldfd,fd是newfd(也包括我自己刚分析也是这样的)
但是大家一定要认真读读官方文档:
官方文档中是这么说的:newfd是oldfd的一份拷贝,换句话说就是最后只剩下了oldfd,newfd被oldfd所覆盖了。那么我们在回归话题,被覆盖的是谁?很明显是1被覆盖了,所以1就是newfd,那么
fd就是oldfd。参数顺序可不能够写反,不然就达不到我们想要的效果。
所以此时我们可以这样写代码:
//close(1); umask(0); int fd1=open("nor.txt",O_CREAT | O_WRONLY | O_TRUNC,0666); dup2(fd1,1); //close(2); umask(0); int fd2=open("err.txt",O_CREAT | O_WRONLY | O_TRUNC,0666); dup2(fd2,2); printf("normal file\n"); printf("normal file\n"); printf("normal file\n"); fprintf(stderr,"err file\n"); fprintf(stderr,"err file\n"); fprintf(stderr,"err file\n"); fprintf(stderr,"err file\n");
这样也能够很方便的完成我们的需求。
😝😝😝😝谈谈重定向的实现原理?
每个文件描述符都是一个内核中文件描述信息数组的下标,对应有一个文件的描述信息用于操作文件,而重定向就是在不改变所操作的文件描述符的情况下,通过改变描述符对应的文件描述信息进而实现改变所操作的文件
5 缓冲区
在讲述struct file时还有一个小细节要提出,就是OS在维护的struct file时每一个struct都对应着一个缓冲区,可以理解为内核级别的缓冲区,那么这个缓冲区是有何作用?
这个其实是我们进行文件读写操作时将用户空间与内核空间的数据进行来回拷贝,至于何时刷新到用户磁盘中是由操作系统所决定的。
C语言提供的FILE与struct file有关吗?
因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。所以C库当中的FILE结构体内部,必定封装了fd。这个我们之前都已经提及过。
FILE与struct file本质上是没啥关系的,非要扯一个关系的话就是一种上下层的关系。
来看这样的一段程序:
close(1); int fd= open(LOG,O_CREAT | O_APPEND | O_WRONLY,0666); umask(0); printf("hello file\n"); fprintf(stdout,"stdout,hello file\n"); close(fd);
当我们运行时:
这好像跟我们之前讲的不符合吧?我们关闭了标准输出,打开了新文件后应该将数据重定向到了log.txt中呀,为啥log.txt中还是没有数据?
原因其实就是我们代码在之前的基础上在最后一行写了一句close(fd)
写了这一句为啥会造成这样的结果呢?我们来分析分析。
我们用C语言进行文件操作,在语言层面上会给我们提供一个缓冲区,就像之前我们讲解一个进度条一样,语言会提供一个缓冲区给用户,当我们关闭该文件时缓冲区的内容还没有被刷新到文件中,至于为啥此时不刷新呢?是因为一个规定:显示器刷新采用的是行缓冲,而普通文件刷新采用的是全缓冲
,全缓冲表示必须将缓冲区填满才能够刷新,而显然我们刚才写入的那点儿字符是不足以将缓冲区填满的,所以此时我们在关闭文件前刷新一下缓冲区,数据就能够被正确写入了,我们可以来试试:
运行结果:
这样就得到了我们想要的结果了。
那么我们可能还会思考这个缓冲区是在哪儿的呢?其实该缓冲区是在FILE结构体中的,当我们用fopen打开文件时得到的FILE结构体,而缓冲区就在FILE结构体中。
我们可以看看FILE结构体的源码:
//在/usr/include/libio.h struct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ #define _IO_file_flags _flags //缓冲区相关 /* The following pointers correspond to the C++ streambuf protocol. */ /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */ char* _IO_read_ptr; /* Current read pointer */ char* _IO_read_end; /* End of get area. */ char* _IO_read_base; /* Start of putback+get area. */ char* _IO_write_base; /* Start of put area. */ char* _IO_write_ptr; /* Current put pointer. */ char* _IO_write_end; /* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno; //封装的文件描述符 #if 0 int _blksize; #else int _flags2; #endif _IO_off_t _old_offset; /* This used to be _offset but it's too small. */ #define __HAVE_COLUMN /* temporary */ /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; /* char* _save_gptr; char* _save_egptr; */ _IO_lock_t *_lock; #ifdef _IO_USE_OLD_IO_FILE };
我们再来看一段有趣的代码:
const char* str="hello write\n"; printf("hello printf\n"); fprintf(stdout,"hello fprintf\n"); write(1,str,strlen(str)); fork();
当我们运行时:
可以当我们重定向到另外一个文件中时:
奇怪的现象发生了,为啥会比之前多打印两行?为啥hello write没有多打印一行呢?
我们结合上面的思考再来分析分析:我们调用printf和fprintf时是自带缓冲区的,但是当我们重定向到文件中时缓冲区的刷新方式由行缓冲变成了全缓冲,呢我们放在缓冲区的数据就不会被立即刷新,当我们进行fork之后,由于缓冲区的数据也是数据,所以缓冲区的数据也会发生写时拷贝,而当我们退出进程时缓冲区的数据就会被刷新出来,这也就很好的的解释了为啥会多打印两行hello printf和hell fprintf.
至于为啥没有多打印hello write,别忘了write可是系统调用接口,是不会存在什么缓冲区的,会直接将数据刷新到文件对应的缓冲区,所以fork之后就不会存在什么缓冲区数据拷贝的概念了。
综上: printf fprintf 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,
都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。
那这个缓冲区谁提供呢? printf fprint 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统
调用的“封装”,但是 write 没有缓冲区,而 printf fprintf 有,足以说明,该缓冲区是二次加上的,又因为是
C,所以由C标准库提供。