1.重定向
首先我们先了解下Linux中的重定向操作,> ,>> ,<可以实现重定向的操作。
1.1 >输出重定向
在Linux下可以使用>将原本输出在显示屏的内容指定输出在文件中,如果指定文件不存在就创建,存在就清除数据,和我们之前使用fopen的w状态一样。
1.2 >>追加重定向
与>不同的是,如果文件存在,他不清除数据,而是在文件末尾追加数据。与fopen的a状态一样。如下代码。
1.3 <输入重定向
我们在使用指令的时候可以加上选项,例如下图 grep指令。这个选项实际上就是从键盘读取的。使用<也可以从文件中读取。如下图
1.4 重定向底层理解
我们知道,当运行一个程序的时候,会默认打开三个文件,分别是标准输入,标准输出,标准错误,他们分别占据着task_struct里文件描述符的0,1,2位置。但是他们也是可以被关闭的。
并且新的文件fd从没有被使用的最小下标开始。
- 标准输入(0)默认为键盘
- 标准输出 (1)默认为显示器
- 标准错误 (2)默认为显示器
如下代码,关闭文件描述符为1的文件,即标准输出,打开新文件的fd就为1,结果如下。
#include<stdio.h>
#include<unistd.h>
#include <fcntl.h>
#include<string.h>
int main()
{
close(1);
int fd=open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0777);
char s[]="hello write\n";
write(fd,s,strlen(s));
return 0;
}
同理在C语言中printf默认向标准输出打印文本,也就是说,如果我们先关闭fd为1的标准输出,在打开一个文件,此时该文件的fd就为1,printf就会向文件中打印文字。如下代码
#include<stdio.h>
#include<unistd.h>
#include <fcntl.h>
#include<string.h>
int main()
{
//先关闭标准输出
close(1);
//再打开文件
FILE * fp=fopen("log.txt","w");
printf("文件fd为:%d\n",fp->_fileno);
printf("文件fd为:%d\n",fp->_fileno);
printf("文件fd为:%d\n",fp->_fileno);
return 0;
}
1.4.1 C语言缓冲区
注意上述代码中关闭文件采用的是系统调用函数close,而没有采用C语言封装的函数fclose。如果采用C语言fclose关闭结果如下。
文件创建出来了,但是里面的内容什么都没有!首先C语言函数封装了系统调用,尽管速度相比于其他语言很快,但还是有消耗的,如果每次输出字符,都向操作系统内核写入,效率就会比较低,所以C语言就设计了一套自己的缓冲区。
一般这个缓冲区直接定义在FILE结构体里面,如下图。
回到最初的问题,我们可以理解为printf内部写死了,在编译之后,只认stdout里面的缓存区。类似下述代码.
write(stdout->_IO_buf_base,.....)
当我们调用fclose关闭时,C语言会把stdout内部的缓冲区释放,此时在调用printf就向已经释放的区域写入,可能引发错误。
但如果我们调用close,stdout这个结构体依然在,他的缓冲区依旧在,我们就可以使用printf向里面写入数据。当刷新缓存区的时候,数据就会被内核拿到,故保留了下来。
1.4.2 系统级刷新缓存区函数
C语言有自己的缓存区,同样OS为了避免频繁的向磁盘写入文件也设有缓存区,原理如下。
由此可见,为了效率,C语言和Linux都做了不少优化。
fflush是C语言刷新缓存区函数,可以将FILE中缓存的数据刷入到内核缓存区里面。
注意当C语言结束或者fclose文件关闭的时候,会自动刷新所打开的文件流的缓存区
我们可以运行下述程序验证下。
#include<stdio.h>
#include<unistd.h>
#include <fcntl.h>
#include<string.h>
int main()
{
//先关闭标准输出
close(1);
//再打开文件
FILE * fp=fopen("log.txt","w+");//以读写方式打开
printf("文件fd为:%d\n",fp->_fileno);
char s[1024]="\0";
fscanf(fp,"%s",s);
perror(s);//我们只关闭了标准输出,标准错误依然是正常的,可以向屏幕打印
return 0;
}
在运行的时候,没有读取到说明此时的字符还在C语言缓存区,或者内核缓存区里面,没有刷新到磁盘里面。
一般如果是显示器文件C语言遇到\n就立刻刷新,普通文件等到缓存区满了才刷新。
此时我们可以用函数控制他刷新缓存区。
此时字符到了内核缓存区里面,对于内核级缓冲区它只有满了的时候才会刷新到磁盘,为了让他刷新到磁盘里面,可以用系统级刷新缓存函数。sync
#include<stdio.h>
#include<unistd.h>
#include <fcntl.h>
#include<string.h>
int main()
{
//先关闭标准输出
close(1);
//再打开文件
FILE * fp=fopen("log.txt","w+");//以读写方式打开
printf("文件fd为:%d\n",fp->_fileno);
fflush(stdout);
sync();
rewind(fp);//使文件下标回到开始
char s[1024]="\0";
fscanf(fp,"%s",s);
perror(s);//我们只关闭了标准输出,标准错误依然是正常的,可以向屏幕打印
return 0;
}
此时需要注意的是,虽然字符被刷新到了磁盘,但文件的指针指向了结尾,如果想要读取内容,要让他回到开头。
执行上述代码结果如下。此时就可以读取到文件内容了。
1.4.3 重定向
所以所谓的重定向,实际上就是将原本进程的标准输入,标准输出换成我们指定的文件罢了。
我们可以采用上述先关闭一个文件,在打开一个文件,利用OS分配文件描述符fd的特点实现重定向,也可以使用函数dup2来实现重定向。
dup2是把文件描述符oldfd拷贝到newfd处,最后保留oldfd。为什么传入两个数字就可以实现文件跳转,原理如下。
上述是进程的逻辑结构,可以抽象为如下图示
我们传入的数字其实就是文件在进程管理数组中的小标,重定向就相当于把文件指针拷贝过去,如下图。
这也说明了一个文件可以被多个文件指针指向,而文件内部采用引用计数的方式,每次关闭文件count减一,如果count为0就释放文件。
我们就可以用dup2改写上述代码,不用那么复杂了。
#include<stdio.h>
#include<unistd.h>
#include <fcntl.h>
#include<string.h>
int main()
{
int fd=open("log.txt",O_TRUNC| O_WRONLY|O_CREAT, 0777);
dup2(fd,1);
char s1[]="aaaaaaaaaaa\n";
char s2[]="bbbbbbbbbbb\n";
write(fd,s1,strlen(s1));
write(1,s2,strlen(s2));//此时fd与1都指向log文件
return 0;
}
1.5 子进程创建
当进程创建子进程的时候,会继承父进程的内核数据结构与代码加数据。并且采用写时拷贝的技术,保持进程间的独立性。
文件描述符表也是task_struct的一部分数据,也就是说当创建子进程的时候,子进程默认打开了父进程打开的文件。
2. 模拟实现C语言文件函数
C语言封装的是系统调用,也就是利用系统函数,模拟封装下fopen,fwrite,fclose,fflush等函数。
2.1头文件 mystdio.h
定义一个FILE结构体,加入属性fd,buf
#pragma once //防止头文件被重复包含
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include <fcntl.h>
#include <stdio.h>
typedef struct FILE
{
int fd;//文件描述符
char buf[1024];//缓冲区
int size;//缓冲区字符个数
}myFILE;
myFILE *mfopen(const char *path, const char *mode);
size_t mfread(void *ptr,size_t size, size_t nmemb,myFILE * stream);
size_t mfwrite(const void* ptr,size_t size, size_t nmemb,myFILE * stream);
int mfflush(myFILE * stream);
2.2 库文件 mystdio.c
在库文件中实现上述函数。
#include"mystdio.h"
myFILE *mfopen(const char *path, const char *mode) {
myFILE *fp = (myFILE *)malloc(sizeof(myFILE));
if (fp == NULL) {
perror("malloc(sizeof(myFILE))");
return NULL;
}
fp->buf[0] = '\0'; // 假设 buf 是字符串缓冲区
fp->size=0;//缓存区字符个数
int flags = 0;
mode_t permissions = 0777;
if (strcmp(mode, "w") == 0) {
flags = O_WRONLY | O_CREAT | O_TRUNC;
} else if (strcmp(mode, "a") == 0) {
flags = O_WRONLY | O_CREAT | O_APPEND;
} else if (strcmp(mode, "r") == 0) {
flags = O_RDONLY;
} else {
fprintf(stderr, "Invalid mode: %s\n", mode);
free(fp);
return NULL;
}
fp->fd = open(path, flags, permissions);
if (fp->fd == -1) {
perror("open");
free(fp);
return NULL;
}
return fp;
}
int mfflush(myFILE * stream)
{
write(stream->fd,stream->buf,stream->size);
stream->size=0;
}
size_t mfread(void *ptr,size_t size, size_t nmemb,myFILE * stream)
{
return read(stream->fd,ptr,size*nmemb);
}
size_t mfwrite(const void* ptr,size_t size, size_t nmemb,myFILE * stream)
{
if(stream->size+size*nmemb>1024)
mfflush(stream);
snprintf(stream->buf+stream->size,size*nmemb,"%s",(char*)ptr);
stream->size+=size*nmemb;
return size*nmemb;
}
2.3 测试代码 tes.c
#include"mystdio.h"
int main()
{
myFILE* fp =mfopen("log.txt","w");
char s[]="sssssssssssssss\n";
char s2[]="bbbbbbbbbbbbbbb\n";
char s3[]="=gggggggggggg\n";
mfwrite(s,1,strlen(s),fp);
mfwrite(s2,1,strlen(s2),fp);
mfwrite(s3,1,strlen(s3),fp);
mfflush(fp);
return 0;
}
运行结果如下图,在指定文件打印字符。