常见的重定向有:>, >>, <
先来看一串代码:
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
const char *filename = "log.txt";
int main()
{
close(1);
int fd=open(filename,O_CREAT|O_WRONLY|O_TRUNC,0666);
if(fd<0)
{
perror("open");
return 1;
}
printf("printf,fd: %d\n",fd);
fprintf(stdout,"fprintf,fd: %d\n",fd);
fflush(stdout);
close(fd);
return 0;
}
运行结果:
printf,fd: 1
fprintf,fd: 1
代码解释:
在这段代码中,使用了 open
函数打开文件,并通过 fd
文件描述符来引用该文件。然后,您使用 printf
和 fprintf
函数向标准输出写入内容,并使用 fflush
函数刷新标准输出缓冲区,确保内容被写入文件。最后,使用 close
函数关闭文件。
本应该打印在显示器中,但是打印到在指定文件中,这种技术叫做重定向。
重定向原理: 根据文件描述符的分配规则,新打开的这个文件的文件描述符就是 1,即文件描述符表(file*的数组)1 号下标里面存储的就是新打开的文件对象的地址。接下来调用 write 接口,向 1 号文件描述符中进行写入,本来 1 号文件描述符对应的是显示器文件,原本向显示器文件中写入的内容,此时就被写入到新打开的文件中,没有向显示器文件中写入,因此屏幕上就不会出现字符串。
重定向的本质:是在内核中改变文件描述符表特定下标的内容,和上层无关!
为什么需要fflush
函数刷新标准输出缓冲区?
每一个系统中新建的文件都会有方法表和内核文件缓冲区。
操作系统上层有系统调用和语言层,这里我们以C语言为例。在语言中层,有stdin
、stdout
、stderr
,我们使用的printf/fprintf
函数都是调用stdout
。在C语言层面对应的struct FILE*
对应的结构体除了_fileno还有语言级别的文件缓冲区,在使用printf/fprintf
时,并不是通过1号文件操作符将数据直接写到操作系统内部,而是直接写入对应的语言级别的文件缓冲区里,然后C语言通过1号文件操作符刷新到内核文件缓冲区。fflush刷新不是将底层缓冲区刷新到外设上,是把语言级别的缓冲区通过文件操作符写入到内核文件缓冲区中。
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
该函数的作用是将文件描述符 oldfd
复制到文件描述符 newfd
上,并关闭 newfd
(如果 newfd
已经被使用则会先关闭)。该函数返回新的文件描述符 newfd
或者在出错时返回-1
。最终剩的是oldfd
中的内容。
oldfd
不是一个有效的文件描述符,那么调用失败,newfd
不会被关闭。
oldfd
是一个有效的文件描述符,并且newfd
的值与 oldfd
相同,那么 dup2()
不做任何操作,直接返回 newfd
。
该函数主要用于重定向标准输入、标准输出和标准错误输出。
本质是文件描述符下标所对应的内容的拷贝
如下图所示,新打开的文件对应的文件描述符小标为3,将3拷贝到1号文件描述符中,此时1号不再指向标准输出,而是指向了新打开的文件,3号内容会拷贝到1号。dup2(fd,1)
实例:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
const char *filename = "log.txt";
int main()
{
int fd=open(filename,O_CREAT|O_WRONLY|O_TRUNC,0666);
dup2(fd,1);
printf("hello world\n");
fprintf(stdout,"hello world\n");
return 0;
}
dup2()
函数将文件描述符1
(标准输出)复制到文件描述符fd
上,因此后续的输出语句都会被重定向到文件log.txt
中。如果你运行这段代码,你将看不到hello world
的输出在终端上,而是在log.txt
文件中。
追加重定向:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
const char *filename = "log.txt";
int main()
{
//int fd=open(filename,O_CREAT|O_WRONLY|O_TRUNC,0666);
int fd=open(filename,O_CREAT|O_WRONLY|O_APPEND,0666);
dup2(fd,1);
printf("hello world\n");
fprintf(stdout,"hello world\n");
return 0;
}
O_APPEND
标志会将数据追加到文件末尾,而不是覆盖现有内容。
缓冲区好处:
printf
、fprintf
等函数的效率。这样不仅提高使用者的效率,还提高了系统IO的效率。缓冲区:是一段内存空间,给上层提供高效的IO体验,间接提高整体的效率。
缓冲区刷新策略:
fflush(stdout)
、fsync(fd)
等先看一段代码:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
const char *filename = "log.txt";
int main()
{
//C
printf("hello printf\n");
fprintf(stdout,"hello fprintf\n");
const char *msg="hello write\n";
//system
write(1,msg,strlen(msg));
return 0;
}
运行结果:
在上述代码添加一个fork()
:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
const char *filename = "log.txt";
int main()
{
//C
printf("hello printf\n");
fprintf(stdout,"hello fprintf\n");
const char *msg="hello write\n";
//system
write(1,msg,strlen(msg));
fork();
return 0;
}
运行结果:
对比:
./myfile
运行代码,是显示器文件,默认的是行刷新, ./myfile > log.txt
向普通文件写入,刷新策略发生变化,即全缓冲
如果没有fork
,不论重不重定向,最终数据直接写到操作系统内部,但是一旦重定向到文件时,再做写入时,这printf
和fprintf
两个文件缓冲区没有写满,因此这两个文件的缓冲区会把你的数据暂时保存起来。但是write
调用直接写到系统内核里。当在使用fork
时,write
数据已经写到操作系统内部,乃至硬件上,但是printf
和fprintf
的消息依旧在语言级别的stdout
对应的缓冲区中。
#pragma once
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<unistd.h>
#define LINE_SIZE 1024
#define FLUSH_NOW 1
#define FLUSH_LINE 2
#define FLUSH_FULL 4
struct myFILE
{
unsigned flags;
int fileno;
//缓冲区
char cache[LINE_SIZE];
int cap;
int pos;
};
myFILE* my_fopen(const char *path,const char *flag)
{
int flag1=0;
int iscreate=0;
mode_t mode=0666;
if(strcmp(flag,"r")==0)
{
flag1=(O_RDONLY);
}
else if(strcmp(flag,"w")==0)
{
flag1=(O_WRONLY|O_CREAT|O_TRUNC);
iscreate=1;
}
else if(strcmp(flag,"a")==0)
{
flag1=(O_WRONLY|O_CREAT|O_APPEND);
iscreate=1;
}
else
{}
int fd=0;
if(iscreate)
fd=open(path,flag1,mode);
else
fd=open(path,flag1);
if(fd<0)
return NULL;
myFILE *fp=(myFILE*)malloc(sizeof(myFILE));
if(!fp) return NULL;
fp->fileno=fd;
fp->flags=FLUSH_LINE;
fp->cap=LINE_SIZE;
fp->pos=0;
return fp;
}
void my_fflush(myFILE *fp)
{
write(fp->fileno,fp->cache,fp->pos);
fp->pos=0;
}
ssize_t my_fwrite(myFILE *fp,const char *data,int len)
{
memcpy(fp->cache+fp->pos,data,len);
fp->pos+=len;
if((fp->flags&FLUSH_LINE) && fp->cache[fp->pos-1]=='\n')
{
//write(fp->fileno,fp->cache,fp->pos);
//fp->pos=0;
my_fflush(fp);
}
return len;
}
void my_fclose(myFILE *fp)
{
//my_fflush(fp);
close(fp->fileno);
delete fp;
}
C语言为什么要在FILE中提供用户级缓冲区?为了减少底层调用系统调用的次数,让C语言IO函数效率更高
我们写的程序,本质上都是对数据进行处理(计算、存储…)。 那么这些数据从哪里来,去哪里,用户要不要看到这个过程。
文件描述符2默认打开是为了方便我们debug
实例:
#include<stdio.h>
int main()
{
fprintf(stdout,"hello fprintf stdout\n");
fprintf(stderr,"hello fprintf stderr\n");
return 0;
}
对上述程序进行一个追加,将内容追加到log.txt
,就会发现了区别:
>
是标准输出重定向,更改1号fd里面的内容,并没有更改2号下标,此时1号往log.txt
中写
为什么需要有2号下标?我们在写程序时会有两种情况,正确or错误,不管是正确还是错误,都是往1号fd中打,一旦出错,就往2号下标里面打,这样错误的信息和正确的信息在文件层面上就分开了。
比如我们现在打印一大批消息:
将内容分开:
将1号和2号fd从指向原来的显示器文件分别改成ok.txt
和err.txt
中。
全部放在一个文件中的其他写法:
./a.out 1>all.txt 2>&1
是一个命令行的输入,它将程序的标准输出(stdout)重定向到all.txt文件,并将标准错误输出(stderr)也重定向到同一个文件。具体地说,1>表示将stdout重定向到文件all.txt,2>&1表示将stderr重定向到与stdout相同的位置,即all.txt文件。
执行该命令后,程序的所有输出(包括正常输出和错误信息)都将写入到all.txt文件中。
C语言中perror
本质是向2里面打印
在C++中,cout
相当于C语言中的printf
,cerr
相当于C语言中perro
。