前言
我们以前就听过"Linux下一切皆文件",但是说实话我们只是记住了这句话,实质是不理解的!本期我们就会解释!
本期内容介绍
• 回顾C语言文件操作
• 系统I/O操作接口
• 文件描述符fd
• 理解Linux下一切皆文件
• 重定向
• 缓冲区
• stderr
回顾C语言文件操作
我们在C语言的时候就对文件操作进行过介绍,这里是只是稍微回顾一下,详细见C语言专栏的文件操作:
C语言打开文件: fopen(打开文件的方式有:读 r、写 w、追加 a)
C语言写入:fwrite、fprintf、fputs
C语言读取:fread、fscanf、fgets
C语言会默认给我们打开三个流,即标准输入:stdin、标准输出:stdout、标准错误:stderr
注意:这里他们的返回值类型都是FILE*我们当时介绍说他是文件指针类型!
OK,举个例子
#include <stdio.h>
#include <string.h>
int main()
{
//打开文件,以写的方式打开
FILE* fp = fopen("log.txt", "w");
if(fp == NULL)
{
perror("open error\n");
return 1;
}
//写入
const char* msg = "Hello World\n";
int cnt = 5;
while (cnt--)
{
fwrite(msg, strlen(msg), 1, fp);
}
//关闭
fclose(fp);
return 0;
}
OK, 看看效果:
w的特性是文件存在清空原始数据,从头开始写;不存在,在当前进程的工作目录下创建,然后写入!这个和我们在指令那一期介绍过的输出重定向(>)的作用非常像!其实,输出重定向本质就是文件操作!
#include <stdio.h>
#include <string.h>
int main()
{
//打开文件,以追加的方式打开
FILE* fp = fopen("log.txt", "a");
if(fp == NULL)
{
perror("open error\n");
return 1;
}
//写入
const char* msg = "Hello World\n";
int cnt = 5;
while (cnt--)
{
fwrite(msg, strlen(msg), 1, fp);
}
//关闭
fclose(fp);
return 0;
}
a的特性是,文件存在,追加内容;文件不存在,在当前进程的工作路径下新建,在追加写入!这个有换个我们指令那里的追加重定向(>>)的性质一样!其实追加重定向本质也是文件操作
系统文I/O操作接口
我们以前就介绍过:
• 文件 = 内容 + 属性
• 文件在没有被打开之前是存在磁盘地上
• 打开文件的本质是进程打开文件
我们知道,文件存在磁盘,而磁盘是硬件,所以向文件的写入本是上就是对硬件的写入,而我们普通用户是没有权限直接操作硬件的(OS不相信任何人),但是OS必须要给上层提供操作文件的服务,所以他就提供了系统调用接口!下面我们就来学习一下系统的调用接口:
open
作用:打开文件
参数:pathname是要打开/要创建按的文件名称, flag是标记位传参(作用是传递多个标记位),mode是设置新建文件权限的
返回值:如果打开成功,返回一个大于0的整数即文件描述符;失败,返回-1
注意:返回值后面会在介绍!
close
作用:关闭文件
这里你一定最奇怪的是标记位传参,什么是标记位传参?
标记位传参是一种位图的思想,目的是为了传递多个标记!
什么意思呢?举个例子:如果今天叫你给一个函数传递两个标记位,你大概率就是int flag1, int flag2了,而人家写OS的大佬使用的是标记位传参。先看如下代码,看完你就懂什么是标记位传参了:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define ONE 1 // 1 0000 0001
#define TWO (1 << 1) // 2 0000 0010
#define THREE (1 << 2) // 4 0000 0100
#define FOUR (1 << 3) // 8 0000 1000
void print(int flag)
{
if(flag & ONE)
printf("one\n"); //这些都可以完成其他功能
if(flag & TWO)
printf("two\n");
if(flag & THREE)
printf("three\n");
if(flag & FOUR)
printf("four\n");
}
int main()
{
print(ONE);
printf("\n");
print(ONE | TWO);
printf("\n");
print(ONE | THREE | FOUR);
printf("\n");
print(ONE | TWO | THREE | FOUR);
printf("\n");
return 0;
}
看效果:
这样你就可以用一个整形传递多个标记位了, 我们在传递时以按位或的方式就可以传递多个了!!那我们open系统调用的标记位传参有哪些?OK,我们先来介绍一下,然后再使用:
O_RDONLY : 只读打开
O_WRONLY :只写打开
O_RDWR:读写打开
注意:前三个在使用open时必须指定一个,且只能指定一个!
O_CREAT :若文件不存在, 创建它,此时需要第三个参数设置新文件的访问权限
O_APPEND : 追加写
OK,我们下面来使用以下open,我们打开一个已经存在的文件,写权限打开,如果有内容直接清空
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
//以写的方式打开,如果文件存在,清空内容;不存在,创建
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC);
if(fd == -1)
{
perror("open falied\n");
return 1;
}
//....
//关闭文件
close(fd);
return 0;
}
OK,没有问题!我们再来打开一个不存在文件,也是和上面的同样:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
//以写的方式打开,如果文件存在,清空内容;不存在,创建
int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC);
if(fd == -1)
{
perror("open falied\n");
return 1;
}
//....
//关闭文件
close(fd);
return 0;
}
果然创建出来了,但是我们发现他的权限是有问题的!这里他的权限是乱码!这和我的预期不符呀!如何解决呢?OK,这就是我们要介绍的第三个参数:mode
我们上面介绍open时说了,mode是创建不存在文件时,指定新文件访问权限的参数!那我们在创建文件时指定为666(八进制):
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
//以写的方式打开,如果文件存在,清空内容;不存在,创建
int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);//创建权限为666
if(fd == -1)
{
perror("open falied\n");
return 1;
}
//....
//关闭文件
close(fd);
return 0;
}
这次不是乱码了,但是我们发现我们设置的权限时666,这里怎么变成了664了?其实这个问题我们以前介绍过,权限掩码的问题!我们是普通用户所以umask是0002(八进制),他会拿着我们的初始权限0666和~umask按位与一下,然后的是最终的权限,如何避免呢,我就是想666呢?我们可以调用系统提供的umask接口来设置当前进程创建文件的umask:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0000);//将权限掩码设置为000
//以写的方式打开,如果文件存在,清空内容;不存在,创建
int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);//创建权限为666
if(fd == -1)
{
perror("open falied\n");
return 1;
}
//....
//关闭文件
close(fd);
return 0;
}
注意:程序的权限掩码和系统中的权限掩码优先使用,最接近的!这里程序中的近,所以就使用了程序中的!
write
作用:向文件中写入
参数:fd文件描述符(可以理解为向哪个文件写) buf表示写入的文件内容 (注意没有\0) count表示写入内容的长度/字符个数;
返回值:成功,返回写入字节数;失败,返回-1
OK,我们和open配合使用一下,上面我们演示的是以不存在,创建;存在,先清空,再写入!
这里,我们使用一下,追加的方式打开,不存在就创建:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
//以追加的方式打开,如果文件存在,追加;不存在,创建
int fd = open("file.txt", O_WRONLY | O_CREAT | O_APPEND , 0666);//创建权限为666
if(fd == -1)
{
perror("open falied\n");
return 1;
}
const char* msg = "hello write\n";
//写入
int cnt = 3;
while (cnt--)//追加3次
{
write(fd, msg, strlen(msg));
}
//关闭文件
close(fd);
return 0;
}
OK, 没有问题!write很简单的~!
read
ok,举个简单的例子:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
//以只读的方式打开
int fd = open("file.txt", O_RDONLY , 0666);//创建权限为666
if(fd == -1)
{
perror("open falied\n");
return 1;
}
char buff[1024];//简单的模拟一下缓冲区
//读取
ssize_t n = read(fd, buff, sizeof(buff));
if(n > 0)
buff[n] = '\0';
//打印出来
for(int i = 0; i < n; i++)
{
printf("%c ", buff[i]);
}
//关闭文件
close(fd);
return 0;
}
OK,介绍了这么多,我们其实发现这些系统调用和我们以前在C语言学过的很像!其实,我们C语言的库函数都是封装的这些系统调用!
FILE* fopen("f.txt", "r")就是对int open("f.txt", O_RDONLY);的封装
FILE* fopen("f.txt", "w")就是对int open("f.txt", O_WRONLY | O_CREAO_TRUNC, 0666);的封装
FILE* fopen("f.txt", "r")就是对int open("f.txt", O_WRONLY | O_CREAT | O_APPEND , 0666);的封装
以及fwrite和fread等同理!
OK,上面说的理解了,就是不同的语言有不同的文件操作库函数,虽然他们的用法有差别,但是底层都是调用了这些系统调用;但是上面C语言和系统调用他俩的返回值有些不好理解,C语言的返回的是文件指针,倒还好理解,向文件指针指向的文件写嘛,但是系统调用的返回一个数字咋理解?怎么就可以往一个数字里面写呢??它两有啥关系呢?虽然我们目前还没有介绍文件描述符,但是根据上面的介绍有一点我们一定可以确定:FILE*一定是封装了文件描述符的!为什么要封装呢?文件描述符介绍了在解释!
文件描述符
上面我们只说了open的返回值是一个整数fd,叫做文件描述符,他究竟是个啥呢?下面我们隆重的介绍一下文件描述符!
文件描述符的本质
文件描述符的本质是什么呢?直接先说结论,再解释原理!
文件描述符的本质是进程和文件映射关系数组fd_array的下标!
既然是数组的下标那必然是>=0的,这也符合我们前面对open返回值的介绍;OK,我们来写个代码验证一下:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0000);
//打开文件
int fd1 = open("loga.txt.txt", O_WRONLY | O_CREAT, 0666);
printf("fd1 : %d\n", fd1);
int fd2 = open("logb.txt.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
printf("fd2 : %d\n", fd2);
int fd3 = open("logc.txt.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("fd3 : %d\n", fd3);
//关闭文件
close(fd1);
close(fd2);
close(fd3);
return 0;
}
的确是大于等于0的,但是他怎么是从3开始依次增长的呢?0、1、2去哪了呢?上面我们介绍了,我们有一个进程启动的时候OS会给我们默认打开三个流:标准输入stdin、标准输出stdout、标准错误stderr;其实他们分别占用的就是0、1、2下标;
简单验证一下:既然1是标准输出,那我们往1里面写文件不就直接写到显示器了!
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
const char* msg = "hello world\n";
int cnt = 3;
while (cnt--)
{
write(1, msg, strlen(msg));
sleep(1);
}
close(1);
return 0;
}
OK,说了这么多,目前我就记住了fd是进程和文件的映射数组的下标~!但是说实话我还是不太理解,下面我们来介绍一下open打开一个文件的过程:
我们知道打开和操作文件的本质是进程在打开和操作,每一个进程打开一个文件,OS会为其创建一个struct file的结构体对象,而一个进程可以有很多打开的文件,且OS中进程是很多的,所以OS需要对这些struct file的结构体对象管理起来!如何管理?先描述,在组织!OS会对每一个文件的struct file对象以双链表的形式管理起来(并为每一个struct file 对象开辟一个内核寄级文件缓冲区,用来存储该文件的操作内容),但是每个进程在这么多的struct file对象中,如何知道自己打开的是哪一个呢?其实在进程和struct file之间还会一张记录对应进程打开文件的表,叫文件描述符表files_struct,在文件描述符表里面有一个进程文件映射关系的数组fd_array,他是一个struct file*的指针数组,数组中的每个元素指向该进程打开的文件!所以当上层调用open打开文件时,先会在OS中创建struct file的对象和开辟对应的文件缓冲区,之后加载数据(可能会延后),然后查看对应进程的文件描述符表,将该struct file对象的指针放到fd_array的合适位置,最后给上层返回对应的fd_array的下标,然后上层就可以拿着open的返回值fd进行其他操作了,例如当上层在拿着fd去write的时候,因为write是系统调用所以他就知道fd是fd_array的下标,就可以找到相对应的struct file对象,然后将上层要写入的内容(用缓冲区中的内容)拷贝到fd下标位置中的struct file对象指向的file文件的内核级文件缓冲区,然后在由OS定期向磁盘刷新!
上面的内容对用的就是这张图:
OK这就是我们对文件描述符的介绍,上面也介绍了C语言的文件操作是对系统调用的封装!那既然是封装一定有fd等相关信息喽!OK,查看一下:
#include <stdio.h>
int main()
{
printf("stdin->fd : %d\n", stdin->_fileno);
printf("stdout->fd : %d\n", stdout->_fileno);
printf("stderr->fd : %d\n", stderr->_fileno);
FILE* fp1 = fopen("f1.txt", "w");
printf("fp1->fd: %d\n", fp1->_fileno);
FILE* fp2 = fopen("f2.txt", "w");
printf("fp2->fd: %d\n", fp2->_fileno);
FILE* fp3 = fopen("f3.txt", "w");
printf("fp3->fd: %d\n", fp3->_fileno);
fclose(fp1);
fclose(fp2);
fclose(fp3);
return 0;
}
文件描述符的分配规则
文件描述符的规则:查自己的文件描述符表,分配最小的没被使用的fd
ok,验证一下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0000);
//先关掉 0 -> stdin
close(0);
//打开文件
int fd = open("log.txt.txt", O_WRONLY | O_CREAT, 0666);
//看一下新的文件fd
printf("fd : %d\n", fd);
//关闭文件
close(fd);
return 0;
}
这里我们在打开前先把0给关了,然后当open打开的时候去给新的文件分配的时候查找文件fd——array然后发现0是没有给分配所以刚打开的fd就是0!你可能会问,原来打来的0号文件呢?答案是系统会自动回收!
C语言为什么要对底层的系统调用进行封装呢?
我们既然可以使用系统调用问什么C语言还要对系统调用接口进行性封装呢?OK,先说结论,在解释!
• 为了使代码具有跨平台性
• 为用户考虑,方便编码,提高了编码效率
首先解释第一点,不同系统的系统调用接口是不一样的,如果C语言不对OS调用接口封装的话那我们得直接调系统调用,假设你今天在Linux上写的代码想在mac os以及win上跑是不行的,因为不同的系统的OS调用接口不同!这就是使得代码的跨平台性差,历史也会最终淘汰掉!第二点,系统调用时你必须得对系统调用有一定的了解才可以,所以编码的成本变高,工作效率较低!而C语言对不同的系统调用封装后,我们上层用户只管开发,关于跨平台的原因C语言已经解决了,而库函数比系统调用使用的成本简单,开发效率就提升了!
理解Linux下一切皆文件
OK,通过上面的介绍,我们认识到访问文件时OS的内核只认文件描述符fd; 我们说过每个进程在启动的时候OS会默认打开:标准输入stdin(键盘)、标准输出stdout(显示器)和标准错误stderr(显示器),他们分别是fd_array的0、1、2下标,我们上面也演示了可以拿着他们三个去操作文件,但是他们说到底是硬件啊,怎么可以和文件一样操作呢?其实这就是我们要介绍的Linux一些皆文件!
键盘、显示器、鼠标、网卡、磁盘等本质是硬件,这些硬件也是要被管理起来的,如何管理?先描述,在组织!OS会通过驱动层拿到对应硬件的数据,然后通过结构体进行描述,对每个种硬件进行创建相应的对象;然后用某种数据结构(例如双链表管理起来),然后对硬件操作就变成了对数据结构的操作;而这些硬件本质上的功能都是一样的即I/O操作和文件差不多,所以OS就把他们抽象成文件看待,给他们创建struct file,里面一部分他们的属性,一部分是他们I/O方法的指针,指向他们的I/O方法,这些I/O方法通常是硬件的厂商提供的,所以站在OS以及以上的视角看到所有的外设不都是struct file文件吗?所有的外设操作不就是和操作文件一样吗?这就是Linux下一切皆文件!将抽象出来的struct file这一层称为VFS即虚拟文件系统!其实这种方法就是多态,或C语言实现类!
重定向
我们知道每一个进程在启动的时候,操作系统会默认的打开,stdin(键盘)、stdout(显示器)、stderr(显示器),他们分别对应的fd是0、1、2;那我们把1给关闭了会出现什么情况呢?
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0000);
//先关掉 1 -> stdout
close(1);
//打开文件
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
perror("open\n");
return 1;
}
printf("printf, fd %d\n", fd);
fprintf(stdout, "fprintf, fd %d\n", fd);
fflush(stdout);
//关闭文件
close(fd);
return 0;
}
这是什么原因呢?其实稍微一想,本来是默认向显示器写的内容写到了文件!这和我们以前介绍的输出重定向一样!如下:
OK,既然知道了,这是重定向了,下面我们就来介绍一下重定向的原理:
重定向的原理
我们知道printf/fprintf是C语言,他们都是默认向显示器写入的,也就是写到FILE* stdout,FILE* stdout的值stdout->_fileno 默认是等于1的,是不变的,也就是C语言(上层语言)的stdout是无论什么情况都是默认写到fd == 1位置指向的文件的!而上面我们将1号文件给关了,又打开了一个log.txt的文件,根据上面介绍的文件描述符规则,我们可以知道此时的log.txt的fd就是1,但是我们的C语言(上层语言)不知道此时1号位置指向的文件已经改变,当再次写入时依然写到了1号位置指向的文件!所以这就是为什么本应该写到显示器的内容跑到了文件的原因~!
根据上面的介绍,我么可以得出结论:
重定向的本质是:改变内核fd_array数组中特定下标的内容,与上层无关!
OK,那我们是不是以后重定向也得先关1号文件,然后在打开?是不是有点挫呀!再说了我们在使用ls 命令重定向的时候也没有关闭1啊,他们是怎么做到的呢?其实系统为了方便重定向,给我们提供了一个接口:dup2,下面我们就来学习使用一下这个接口!
dup2
dup2的作用就是将内核数据结构fd_arrary的数组的oldfd下标中的内容拷贝到newfd下标中
有了dup2接口我们就不在先关闭,在打开了!OK,我们写个代码看一下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0000);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
perror("open\n");
return 1;
}
//使用dup2重定向
dup2(fd, 1);
printf("printf, fd %d\n", fd);
fprintf(stdout, "fprintf, fd %d\n", fd);
fflush(stdout);
//关闭文件
close(fd);
return 0;
}
追加重定向的话就是,再打开的时候将标记位O_TRUNC换成O_APPEND,然后就变成了追加重定向,OK上面的代码只需要换一下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0000);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
if(fd < 0)
{
perror("open\n");
return 1;
}
//使用dup2重定向
dup2(fd, 1);
printf("printf, fd %d\n", fd);
fprintf(stdout, "fprintf, fd %d\n", fd);
fflush(stdout);
//关闭文件
close(fd);
return 0;
}
缓冲区
在介绍重定向的时候,我们发现我们写的代码总是带一个fflush这是啥呢?可不可以去掉呢?OK,我们去掉试一试:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0000);
//先关掉 1 -> stdout
close(1);
//打开文件
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
perror("open\n");
return 1;
}
printf("printf, fd %d\n", fd);
fprintf(stdout, "fprintf, fd %d\n", fd);
//fflush(stdout);
//关闭文件
close(fd);
return 0;
}
为啥不加个fflush就不显示呢?这是啥情况?其实不光我们的系统内部存在内核级的文件缓冲区,我们的语言层也是存在的!OK,我先画个图,解释一下,上面代码为什么没有写入到文件:
我们知道main函数中在return时代表进程结束,且会在结束前冲刷缓冲区!但是这里在冲刷之前先把文件给关了,等进程结束前刷新时发现找不到要写入的文件了,所以我们就看不到写入log.txt的数据了!那我们把clos给注释掉是不是就可以看到了呢?
果然我们看到了!这也符合我们的介绍!所以这就是为什么上面重定向的时候要到fflush的原因,是想在关闭文件之前将用户级别的缓冲区刷新到内核,然后再由OS刷到外设!OK,上面也只是验证了一下以前的一个代码!下面我们再来重新的介绍一下缓冲区!
什么是缓冲区?
缓冲区就是一段内存空间!
缓冲区分为:用户级缓冲区和内核级缓冲区
为什么要有缓冲区?
上层减少对底层系统调用/硬件I\O接口的调用次数,给上层提供高效的I\O体验,间接提高整体的效率!
这里你可可能会想不管是内核的还是用户的缓冲区,我都是多了次拷贝呀!他怎么还效率高了呢?
举个例子:你买东西是到商场买,还是到工厂买?是不是商场啊!原因是商场的是现货,你到工厂区代价比较大,也不方便!商场不就是一个缓冲区吗?给用户的体验更好,假设没有商场你今天买一个键盘还得去工厂,明天买一个水杯又要去工厂是不是体验很差啊!而你直接去商城那现成的是不是效率高多了?此时虽然多了一次转手,但是比以前体验好了!所以有了缓存可以提高用户的使用效率!另外,假设你现在在北京,要给你云南的朋友送一个键盘,你会拿着键盘自己送过去吗?是不是不会呀,你只需要下楼给楼下的顺丰即可,然后又可以上楼开心的玩原始人了,等到时间他收到即可!顺丰快递会不会立即把你刚邮寄的包裹立刻送?显然不会,顺丰会等到比如100个包裹一起配送!此时顺风不就是一个缓冲区吗?你寄快递不就是调用语言层面的I\O接口吗?此时虽然多了一次转手(拷贝)但是用户的体验变好了,使用者的效率变高了!
缓冲区的刷新策略
立即刷新; 例如:用户级别的刷新:fflush(stdout); 内核级别的刷新:fsync(int fd);
行刷新;例如:显示器(照顾用户的使用习惯)
全缓冲:缓冲区满了才刷新,一般是普通文件;
特殊情况:进程退出,OS自动刷新!强制刷新:\n等
注意:这里说的是缓冲区的刷新策略,并没特质是用户还是内核!
stderr
在介绍stderr之前,我先来谈谈为什么会有stdin\stdout:
我们平时写的程序,本质都是对数据的处理(计算/存储等),这个过程需要知道数据从哪里来,以及数据去了哪里!比如你写个hello world数据是要从键盘获取的,它的执行结果是不是用户也需要看到啊!所以stdin(1)和stdout(1)是方便用户进行动态的与程序进行交互!但是我实在不理解的是为什么还要有stderr(2)呢,他不是对应的也是显示器吗?OK,我们来写一段代码看看:
#include <stdio.h>
int main()
{
fprintf(stdout, "fprintf stdout\n");
fprintf(stderr, "fprintf stderr\n");
return 0;
}
果然这也验证了我们前面的结论是正确的即1和2都是显示器文件!且是同一个显示器文件,不然上面也不可能同时打印!如下图:
为什么要有stderr?
那既然这样不是只有一个就够了吗,为什么还有stderr呢?OK,下面我们先来看看他们的区别:
我们发现我们本来是将他两都重定向到log.txt的但是为什么只有stdout写进去了呢?其实我们介绍的输出重定向叫做标准输出重定向,也就是他只会把1号fd对应的内容给更改了!但是stderr是2号,依然指向显示器的,所以stdout就写到了log.txt文件,stderr就写入了显示器!
这有什么用呢?其实这样可以将程序的正确信息和错误信息分开,可以更加容易的识别错误,对调试很有帮助!如如说,当前程序结束后有很多的输出,你想知道那些是错误的就可以直接用重定向分流!
假设这些都是输出,我们想找错误的就可以,重定向分流:
其实我们语言上学的perror以及cerr本质就是想stderr(2)中打印,而cout和printf是将stdout(1)中打印!
如果我现在也想让fd2和fd1卸载同一个文件中呢?
./test 1 > log.txt 2>&1
什么意思呢?先将./test fd1中的本应该输出到显示器的内容,从定向输出到log.txt中,然后再将fd2中的原本输出到显示器的内容重定向到stdout也就是fd1位置的文件,而fd1位置的文件已经被换成了log.txt所以stdout和stderr写入到了同一个文件!
OK,本期内容就分享到这里!好兄弟,我们下期再见!
结束语:未来可期!