文章目录
- 重定向原理
- 输出重定向
- 关于FILE
- 解释输出重定向原理
- 追加重定向
- 输入重定向
- dup2
- 缓冲区
- 语言级别的缓冲区
- 内核缓冲区
重定向原理
重定向的本质就是修改文件描述符下标对应的struct file*的内容
输出重定向
- 输出重定向就是把本来应该输出到显示器的数据重定向输出到另一个文件当中
假设我们先关闭1标准输入,然后再打开一个文件,按照文件描述符的分配规则,给再打开文件分配的文件描述符fd就是1,那么是不是把输出的内容打印到屏幕上呢
- 由此现象,我们也可以看出,printf的底层是往fd=1的文件中打印内容,不关心该文件是什么
此时把本来应该打印到显示器的内容,显示到文件中,这种行为称为输出重定向
为什么是这样实现输出重定向呢?
- close(1) ->断开了与显示器文件的联系, 相当于把fd_array[1]位置的内容置空
- 然后再打开新文件log.txt,由文件描述符分配规则,fd_array[1]指向log.txt文件(存放log.txt文件的struct file的地址)
所以C语言中的打印函数–printf,本质是往 标准输出(stdout)打印
关于FILE
stdout和stdin和stderr的类型都是FILE*
,是一个文件指针,那FILE
到底 是什么呢?
FILE是C语言层面上的结构体
库函数是对系统调用接口的封装,本质是访问文件都是通过文件描述符进行访问的,所以C库函数中的File结构体内部一定封装了文件描述符fd
以C语言的fwite函数为例子:该函数是向文件流中写入内容,然后实际在底层中 是通过文件描述符fd写到磁盘上
所以我们可以推断, C语言的FILE
结构体一定包含一个fd
, C++中的cin,cout,cerr这些流对象的属性中也一定包含文件描述符fd
struct FILE
{
//一定包含了一个整数,是对应在系统层面这个新打开文件的fd
};
所以:语言上的in/out/err和系统上的012存在联系
- stdin 标准输入,键盘 -> 包含fd = 1
- stdout 标准输出,显示器 -> 包含fd = 2
- stderr 标准输出,显示器 -> 包含fd = 3
我们可以在内核中看一下FILE的定义:
- 其中的
_filno
就是文件描述符
//在/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
};
stdout,stderr,stdin都是结构体FILE指针,所以我们可以把其指向的文件描述符打印出来:
知道了FILE里面封装了文件描述符fd之后,我们可以理解一下C库函数fopen究竟在做什么
FILE *fopen(const char *path, const char *mode)
fopen函数在上层为用户申请一个FILE结构体变量,并返回该结构体的地址(FILE*), 在底层通过系统调用接口open打开对应的文件,得到文件描述符fd,并把这个文件描述符fd填充到FILE结构体的 _fileno
变量当中,至此便完成了文件打开的工作
而C语言中的其它文件操作函数, 比如fputs,fgets等,都是先根据我们传入的文件指针找到对应的FILE结构体,然后再FILE结构体中找到文件描述符fd,最后通过文件描述符fd找到对应的文件,对文件进行一系列的操作
关于echo的重定向:
把本来输出的显示器的内容输出到文件中, 实际上echo命令也是一个进程, 把echo进程的显示器文件stdout关闭掉(close(1)) , 再把log.txt文件打开,于是给log.txt文件分配的文件描述符就是1, echo实际就是向文件描述符为1的文件输出数据, 不关心文件描述符为1的文件是什么, 现在文件描述符为1的文件是log.txt, 于是输出的内容就被打印到log.txt中
解释输出重定向原理
关闭1, 再打开文件,给该文件分配的文件描述符就是1, printf函数是向stdout中打印,stdout的类型是FILE*类型. FILE是一个结构体,该结构体内部存储文件描述符的遍变量,与系统的1对应,而printf只关心这个整数1,printf实际上就是向文件描述符为1的文件输出数据, 并不关心文件描述符为1下标的文件是什么, 而现在我们1下标指向的是log.txt文件,所以printf就是往log.txt文件中写入
追加重定向
- 追加重定向和输出重定向的区别:输出重定向是覆盖式的输出数据,会把原来已经有的数据覆盖掉再写进去,而追加重定向是追加式输出数据,在原来已经有的数据基础上写入数据
追加重定向与输出重定向唯一的差别就是在打开方式上,增加了 O_APPEND
选项
输入重定向
什么叫输入重定向?
把本来应该从键盘中读取内容改成从文件中读取内容
这里以fgets函数为例子:
从特定的文件流中按行读取内容,内容放在缓冲区s中.读取成功返回字符串的起始地址,否则返回NULL 第二个参数:缓冲区大小
本来应该从标准输入(键盘)中获取数据,现在变成从log.txt文件获取数据
解释:
如果我们想让本应该从键盘文件读数据的scanf函数,改为从log.txt文件读取数据,我们只需要先关闭0描述符对应的文件关闭,也就是将键盘文件关闭, 后续我们打开文件log.txt ,给该文件分配的文件描述符就是0, stdin的类型是FILE*类型. FILE是一个结构体,该结构体中存储的文件描述符就是0,与系统的0对应,因此scanf实际就是向文件描述符为0的文件读取数据 而现在我们0下标指向的是log.txt文件,所就是从log.txt文件中读取数据
dup2
上述我们重定向的方法是:关闭文件然后再打开文件的方式进行重定向,但这个只是特殊情况
而更多的是:
例如:现在有两个文件描述符1 和 3被打开了,如何实现输出重定向呢?
因为在语言层调用接口的时候,函数只认数字1下标, 而不关心1下标指向的文件是什么,所以我们可以
- 把文件描述符3中的内容拷贝到文件描述符1中,就实现了原来应该向显示器文件写入,现在往文件描述符3对应的文件写入
dup2函数就是干这个事情的!
#include <unistd.h>
int dup2(int oldfd, int newfd);
dup2() makes newfd be the copy of oldfd, closing newfd first if necessary, but note the
following:
* If oldfd is not a valid file descriptor, then the call fails, and newfd is not closed.
* If oldfd is a valid file descriptor, and newfd has the same value as oldfd, then dup2() does nothing, and returns newfd.
函数功能:dup2会把fd_array[oldfd]的内容拷贝到fd_array[newfd]当中,有必要时我们需要先关闭文件描述符为newfd的文件
- 最终oldfd和newfd都变成oldfd的内容,可以认为把oldfd的内容覆盖原来newfd的内容
函数返回值: 如果函数调用成功,返回newfd,否则返回-1
由上述的翻译可以得出:
- 1.如果oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭
- 2.如果oldfd是一个有效的文件描述符,但是newfd和oldfd是相同的,则dup2不做任何操作,并返回newfd
输出重定向
dup2(fd,1);//oldfd,newfd
我们把打开文件是获得的文件描述符fd和1传入dup2函数,那么dup2会把fd_array[fd]的内容拷贝到fd_array[1]中,我们向stdout输出数据,而stdout是向文件描述符为1的文件输出数据,因此,本来应该输出到显示器的数据就会重定向输出到log.txt文件当中
选项O_TRUNC
的含义:清空原来的内容再写入 O_CREAT
:要打开的文件不存在就先创建,后面要自定义文件的权限
O_WRONLY
: 以写的方式打开文件
- C语言中文件操作的 "w"选项 也会先把原始文件清空,说明上层封装了这个选项
追加重定向
只需在输出只写O_WRONLY
选项的基础上添加O_APPEND
选项
输入重定向
dup2(fd,0); //原本从键盘读取内容,现在从文件中读取内容
问:执行exec* 程序替换,会影响我们曾经打开的文件嘛?
不会!因为替换的是代码和数据,并不会影响进程内核的数据结构
例子:
echo "hello Mango" > log.txt
在命令行上的重定向,会进行字符串分析, 发现>
重定向符号时,会先执行dup2函数,然后进行程序替换,此时echo的内容就会重定向到指定的文件当中,程序替换并不会替换打开的文件
- 相当于是: fork创建子进程 -> 子进程执行dup2(fd,1) ->子进程执行程序替换exec*函数
那子进程会不会和父进程共享文件描述符表呢?
会! 子进程也会形成自己的file_strcut结构体,子进程内核的数据结构task_strcut会以父进程为模板初始化自身,因此父进程和子进程的文件描述符表就是一样的,父子指向同一份文件
父进程如果曾经打开了标准输入,标准输出,标准错误,那么子进程也会继承下去
- 这也是为什么所有的进程都会默认打开标准输入,标准输出,标准错误,就是因为我们命令行上的进程的父进程都是
bash
, 也就是命令行解释器,bash需要打开标准输入进行输入指令,打开标准输出用于打印结果,打开标准错误用于提示错误信息, 创建子进程的时候,默认也继承下去了, 所以在命令行上启动的子进程最终都打开了这三个东西 (引用计数)
缓冲区
标准输出和标准错误都可以向显示器打印内容,这二者有什么区别呢?
- 当我们重定向的时候,我们可以发现,只有标准输出的内容进行了重定向, 因为
>
叫做输出重定向,即把本应该显示到1号文件描述符的内容写入到指定的文件当中,而2号文件描述符并没有发生改变,仍指向标准错误
实际上我们使用重定向时,重定向的是文件描述符为1的标准输出流,并不会对文件描述符为2的标准错误流进行重定向
那如果我们想把标准输出和标准错误的内容都重定向怎么办呢?
./可执行程序 > 文件 2>&1
表示标准错误和标准输出都进行重定向
这样写可以理解为:
先执行前面的语句: 把1本来指向显示器的内容,变成指向特定的文件, 而后面的2>&1指的是:把1里面的内容拷贝到2, 因为1经过重定向了,指向新文件, 所以2也指向新文件
语言级别的缓冲区
刚才我们进行重定向的时候,打开了文件却没有关闭,最后应该close(fd),
没有close(fd)的情况下 和 close(fd)的情况下对比:
我们可以发现: close之后, C语言的写入接口,像printf,fprintf重定向后并没有被刷新出来,这是什么原因呢?
首先我们要知道缓冲区的概念, 我们之前说的缓冲区,都是指语言级别的用户级的缓冲区,也就是由C语言提供的缓冲区
C语言中的printf ,fprintf向标准输出stdout写入的时候,本质都是 写入/拷贝到C语言的缓冲区当中,然后系统会定期的把C语言缓冲区的内容拷贝到内核的缓冲区,OS再把数据更新到硬件上
而C语言缓冲区的内容写入到内核缓冲区当中,相当于把数据写入文件当中, 一定需要 fd
那C语言提供的缓冲区又在哪呢?
前面我们知道FILE
结构体当中封装了一个fd
,其实该结构体里面还 维护了与C语言缓冲区相关的内容
我们可以看一下它的源码:它是用指针来表示缓冲区的区间 (和我们vector的模拟实现相似)
//在/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
};
我们使用stdin/stdout/stderr 和自己打开的文件,拿到的一个文件指针FILE*
,我们的printf和fprintf都是先写到FILE*对应的FILE结构体的文件缓冲区当中, 即暂时存在FILE结构体的内部,并不会直接刷新到外设
有什么时候会把FILE结构体缓冲区的数据刷新到内核呢?
- \n
- 进程退出的时候
从用户级别缓冲区到内核缓冲区的刷新策略种类: (从OS->硬件同样适用)
- 立即刷新 (不缓冲)
- 行刷新 (行缓冲\n),比如显示器就是这种刷新策略
- 全缓冲(缓冲区满了才刷新),比如向磁盘文件写入数据
当发生 重定向的时候 (由刷新到显示器 -> 刷新到文件当中) :隐含着刷新策略由 行缓冲 变为 全缓冲
此时我们再来解释上面的原因: printf/fprintf进行写入,这些写入的消息都被放到用户缓冲区当中,C缓冲区可能并没有被写满,所以这部分内容并没有立即被刷新到内核当中 (全缓冲刷新策略)
-
最后如果在进程退出之前close, 此时我们的数据还在用户级缓冲区, 因为把文件描述符关闭了,所以没有地方去刷新,因此什么也没有看到
-
如果最后没有调close, 则在 **进程退出的时候,**把数据刷新到内核缓冲区当中,然后由OS把数据同步到硬件上,所以我们就能看到由东西输出
如果最后close,也想把数据刷新,要怎么办呢?
close之前 fflush(stdout)
强制刷新缓冲区
总结:为什么提前关闭文件描述符发现刚刚写入的数据没了?
因为这批数据写到了用户层缓冲区即stdout所指向的FILE缓冲区当中,所以里面包含的数据并没有被立即刷新到系统层面上,当我们全部写完之后,数据都没有刷新,最后关闭文件描述符它也不可能刷新了,最终文件里面什么都没有了
写完之后,fflush强制刷新,最后关闭文件描述符这个数据就能被我们看到,根本原因就是这个数据被暂时写到了C语言的缓冲区当中
FILE结构里面包含缓冲区,这个缓冲区是用户及缓冲区,所以printf,等C语言接口,先把数据写到C语言的缓冲区,然后定期让C语言刷新到操作系统当中, 刷新的时候,要通过文件描述符,系统调用的接口,来把数据刷新到操作系统当中,如果提前关闭了文件描述符,这个数据也就不会被刷新.所以这个结果看不到
内核缓冲区
当我们刷新用户缓冲区的数据时,并不是直接把用户缓冲区的数据刷新到磁盘或者是显示器当中,而是先将数据刷新到内核缓冲区,然后再由内核缓冲区将数据刷新到磁盘或者显示器上
最后时候没有close(1)的情况我们就不看了, 不难理解 我们来观察最后close(1)的情况:
- 不管最后有没有close(1), 我们发现重定向之后,标准错误没有重定向,因为我们是重定向1号文件描述符向log.txt文件打印, 文件描述符2对应的-标准错误不受影响,照样向显示器写,并且刷新策略是行刷新
最后close(1)之后, 重定向后只有write写入的内容, 而printf和fprintf的内容经过重定向后都没有被写入到文件当中,这个是什么原因呢?
解析:
1)重定向时,把原本应该显示到显示器的内容显示到文件当中, 潜台词就是刷新策略从 行缓冲 变为 全缓冲, 等进程结束的时候才会把数据刷新到文件当中,而此时我们的数据还在用户级缓冲区, 因为把文件描述符关闭了,所以没有地方去刷新,因此什么也没有看到
2)write是系统调用, 没有通过C语言缓冲区进行暂存,而是直接写到内核缓冲区, 因此关闭文件描述符并不影响重定向
我们再来看下面一段代码: fork创建子进程
当我们往显示器上打印:正常打印 当我们重定向到文件当中: 使用C语言接口的打印重复出现,而系统调用接口并不影响 这是为什么呢?
没有重定向时:
此时每条向显示器写入的信息末尾都有\n
,所以执行可执行程序./myfile向显示器打印的时候, 在fork之前,这批消息就被刷新到显示器上,所以不会打印两份
重定向时:
此时不是向显示器打印,而是写入到文件中**, 潜台词是是刷新策略变了,由行刷新变成了全缓冲** , printf/fprintf/fpus是先写到C语言缓冲区buffer当中,没有写满并不会立即刷新到内核.父进程的缓冲区也是父进程的空间,fork创建子进程,由于父子进程具有独立性,进程退出时要刷新缓冲区,刷新缓冲区的本质就是一种写入,为了维护代码数据独立性,于是就需要对数据进行写时拷贝, 父进程和子进程谁先刷新时就会发生写时拷贝, 因为父子进程都对各自缓冲区的内容刷新, 于是就可以看到文件当中库函数输出的内容有两份
为什么write没有两份呢?
因为它是系统调用接口,直接向内核缓冲区写入,不会暂存在C语言缓冲区, 这也应证了C语言缓冲区不在OS内,而是在用户层 因为如果说这个缓冲区是操作系统自带的,那么printf,fputs,write函数打印的数据重定向到文件都应该打印1次
那么如何修改可以让创建子进程,而输出的消息也只有一份呢?
在fork之前,fflush强制刷新缓冲区