[Linux] 详析 Linux下的 文件重定向 以及 文件缓冲区
文件描述符的分配规则
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
umask(0);
close(0); // 什么都不干, 先关闭fd=0的文件
int fd = open("new_log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); //以清空只写方式打开文件, 若文件不存在则创建文件
if(fd < 0) {
perror("open");
}
printf("打开文件的fd为: %d\n", fd);
close(fd);
return 0;
}
0
. 如果我们关闭0、2文件, 再打开两个文件, 新打开文件的fd会怎么分配呢?打开文件分配fd的规则其实是, 从头遍历fd_array[]数组, 将没有使用的最小下标分配给新打开的文件
重定向
理解重定向
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main() { umask(0); close(0); int fd = open("new_log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); //以清空只写方式打开文件, 若文件不存在则创建文件 if(fd < 0) { perror("open"); } fprintf(stdout, "打开文件成功, fd: %d\n", fd); close(fd); return 0; }
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
umask(0);
close(1); // 什么都不干, 先关闭fd=1的文件
int fd = open("new_log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); //以清空只写方式打开文件, 若文件不存在则创建文件
if(fd < 0) {
perror("open");
}
fprintf(stdout, "打开文件成功, fd: %d\n", fd);
fflush(stdout); // 刷新文件缓冲区操作
close(fd);
return 0;
}
关闭fd=1, fprintf()之后, 必须要手动刷新文件缓冲区, 不过暂时不做解释
本文章后面详析介绍文件缓冲区
原来应该打印到标准输出流的信息 打印到了刚刚打开的文件中
这是否就是重定向?
Linux系统的上层只认0、1、2、3等这样的fd值, 我们在OS内部通过一定的方式 调整 数组特定下标的指向, 这样的操作就是重定向
如何重定向
dup2()
int dup2(int oldfd, int newfd);
复制一个文件描述符, 将 newfd 变为 oldfd 的复制
oldfd
和 newfd
. 并且 会将newfd变为oldfd的复制, 也就是说dup2()操作的是两个已经打开的文件
dup2()会将一个fd描述的文件重定向为另一个文件
dup2()其实会将 newfd 变为 oldfd
, 也就是说执行过dup2()之后, 其实进程中已经不在维护原来的newfd, newfd和oldfd 只剩了oldfd
. 从这个结果可以看出来, oldfd其实是重定向之后的文件, 而newfd是被重定向掉的文件
即, dup2(oldfd, newfd); 是将 newfd位置的内容变成了oldfd位置的内容.
dup2()实现输出、追加重定向
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
umask(0);
int fd = open("new_log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); //以清空只写方式打开文件, 若文件不存在则创建文件
if(fd < 0) {
perror("open");
}
dup2(fd, 1); // 将标准输出重定向到只写打开文件, 实现输出重定向
const char *buffer = "Hello world, hello July\n";
write(stdout->_fileno, buffer, strlen(buffer));
close(fd);
return 0;
}
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
umask(0);
int fd = open("new_log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); //以清空只写方式打开文件, 若文件不存在则创建文件
if(fd < 0) {
perror("open");
}
dup2(fd, 1); // 将标准输出重定向到只写打开文件, 实现输出重定向
const char *buffer = "Hello world, hello July\n";
write(stdout->_fileno, buffer, strlen(buffer));
close(fd);
return 0;
}
dup2()实现输入重定向
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd = open("new_log.txt", O_RDONLY); //以只读方式打开文件, 只读打开一般都是已存在的文件
if(fd < 0) {
perror("open");
}
dup2(fd, 0); // 将标准输入重定向到只读方式打开的文件, 实现输入重定向
char buffer[128];
// 从stdin中获取数据到buffer中
while(fgets(buffer, sizeof(buffer), stdin) != NULL) {
printf("%s", buffer);
}
close(fd);
return 0;
}
文件缓冲区
什么是文件缓冲区
文件缓冲区其实就是一块内存空间
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main() {
printf("Hello world");
write(stdout->_fileno, "I am a process", strlen("I am a process"));
sleep(3);
return 0;
}
可以说明文件缓冲区的存在, 并且只有在刷新文件缓冲区时, 文件缓冲区内的数据才会写入系统内核中
为什么要存在文件缓冲区
-
我们知道, 进程在等待硬件资源时是会进入
阻塞状态
的处于阻塞状态的进程, 无法执行其他代码, 只能等到阻塞结束
而进程向屏幕输出信息时, 其实也是需要屏幕资源的
如果此时屏幕资源已经被占用满了, 并且没有文件缓冲区的存在, 输出语句执行就会进入阻塞状态等待屏幕资源
而如果有文件缓冲区的存在, 即使屏幕资源已经被占满了, 输出语句执行之后, 会将需要输出的信息存入文件缓冲区中, 然后进程继续执行其他代码. 等到合适的时候, 再刷新文件缓冲区 将需要打印的信息打印到屏幕上
这样看, 其实
文件缓冲区的存在, 在一定程度上节省了进程使用缓冲区(此缓冲区非文件缓冲区)的时间
-
如果没有文件缓冲区的存在, 我们打印信息就会立马在屏幕上打印出来.
这样看起来似乎不错, 但其实会加重操作系统的负担.
如果没有限制的、死循环地向屏幕上打印信息, 那么数据就会在操作系统与硬件之间疯狂地I/O操作.
这样显然会加重操作系统的负担.
而有了文件缓冲区地存在, 在不满足刷新文件缓冲区地条件时, 我们需要打印的信息就会先存放在文件缓冲区中, 暂时不与硬件发生I/O操作. 直到达成刷新文件缓冲区的条件时, 再将文件缓冲区内的所有数据刷新到屏幕上.
这样,
文件缓冲区的存在, 其实可以集中处理数据刷新, 有效的减少操作系统与硬件之间的I/O次数, 进而提高整机的效率
文件缓冲区在什么地方
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main() {
printf("Hello world");
write(stdout->_fileno, "I am a process", strlen("I am a process"));
sleep(3);
return 0;
}
"I am a process"
, 然后在3s之后输出了"Hello world"
系统接口wirte(), 是不存在文件缓冲区的, 所以用write()向标准输出写数据, 会直接在屏幕中打印出来
printf()最终可以在屏幕上打印数据, 一定是在内部调用了write()接口. 但是 write()打印数据是会直接打印出来的
.文件缓冲区在什么地方使用了?
文件缓冲区一定是在printf()内部使用了
.文件缓冲区就是由语言本身提供的, 与操作系统无关
.包括fd(_fileno) 和 文件缓冲区
typedef struct _IO_FILE FILE;
:struct _IO_FILE{}
具体是什么呢?struct _IO_FILE{}
相关内容:FILE结构体
中. 与操作系统无关文件缓冲区在FILE结构体中描述着.
而使用C语言的文件接口, 每打开一个文件就会返回一个FILE*
那么是否, **
C语言每个打开的文件都有自己独立的文件缓冲区
**呢?是的.
printf
、fprintf
、fputs
等C语言提供的 均封装了write()
的 向其他文件中写入数据的接口时, 其实都会使用到C语言提供的文件缓冲区.writr()
接口向文件中写入数据:#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main() {
printf("Hello July");
fprintf(stdout, "Hello July");
fputs("Hello July", stdout);
// 如果没有关闭stdout, 这三个语句会在进程结束时正常在屏幕上输出
// 进程退出会自动刷新缓冲区
// 我们在这里将 stdout 关闭
close(stdout->_fileno);
return 0;
}
"Hello July"
在刷新缓冲区之前, 关闭指定的fd, 缓冲区内的数据就不能被写入到指定的fd中了
缓冲区的刷新策略
- 无缓冲, 即立即刷新. 每次存储到缓冲区的内容都会被立即写入系统内核数据, 并刷新缓冲区
- 行刷新, 即遇到
'\n'
时刷新. 我们使用的printf() 这种向显示器文件中写入数据的 一般就采用的行刷新策略, 当输出的内容结尾处有'\n'时, 会将'\n'及之前的数据打印出来
- 全刷新, 即
缓冲区满再刷新
. 全刷新策略一般在向块设备对应的文件(例如磁盘文件)中写入数据时
会采用
- 当进程退出的时候, 文件缓冲区会自动刷新
- 用户可以强制刷新文件缓冲区,
fflush()
函数就是这个作用
* 奇怪的问题
include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main() {
const char* str1 = "Hello printf\n";
const char* str2 = "Hello fprintf\n";
const char* str3 = "Hello fputs\n";
const char* str4 = "Hello write\n";
// C库函数
printf("%s", str1);
fprintf(stdout, str2);
fputs(str3, stdout);
// 系统接口
write(stdout->_fileno, str4, strlen(str4));
fork();
return 0;
}
-
正常编译运行:
-
输出重定向到文件中
直接运行屏幕上输出了4句话, 但是如果是输出重定向到文件中, 文件中会被写入7句话
文件缓冲区是由FILE结构体维护的, 是属于父进程内部的数据
文件缓冲区的刷新策略就改变了
, 上面介绍过向文件中写入数据, 文件缓冲区的刷新策略是全刷新
. 所以在执行前三个语句时, 会将三句话都存储到文件缓冲区内 且不刷新
. 而执行系统接口 wirte()是没有缓冲区
的, 所以会率先写入到文件中.共享一份代码和数据
的. 而无论父子进程谁要修改数据, 就会发生写时拷贝
. 子进程被创建时, 很明显父进程的文件缓冲区还没有被刷新. 那么也就是说 子进程创建出来时, 是与父进程共享同一份文件缓冲区的
. 那么接下来, 无论是子进程先终止, 还是父进程先终止, 都需要清除共享的文件缓冲区. 而fork()父子进程修改数据的机制是, 只要修改就会发生写时拷贝
, 所以在 进程要清除文件缓冲区时, 另一个进程会先拷贝一份
. 拷贝完成之后, 先终止的进程就会刷新文件缓冲区, 将缓冲区内的数据写入到文件中, 然后另一个进程终止, 将拷贝的文件缓冲区也刷新掉, 将相同的数据写入到文件中.写一份自己的C文件操作库, 并实现文件缓冲区
myFILE结构
#define SIZE 1024 // 缓冲区大小
typedef struct _myFILE {
int _fileno; // 首先需要存储文件的fd
char _buffer[SIZE]; // 设置一个1024字节的缓冲区
int _end; // 用来记录缓冲区目前长度, 即结尾
int _flags; // 用来选择缓冲区刷新策略
}myFILE;
my_fopen()函数
// 宏定义缓冲策略, 以便执行
#define NONE_FLUSH 0x0
#define LINE_FLUSH 0x1
#define FULL_FLUSH 0x2
myFILE* my_open(const char* filename, const char* method) {
// 两个参数, 一个文件名, 一个打开模式
assert(filename);
assert(method);
int flag = O_RDONLY; //打开文件方式 默认只读
if(strcmp(method, "r") == 0) {} // 只读传参, 不对flag做修改
else if(strcmp(method, "r+") == 0) {
flag = O_RDWR; // 读写, 文件不存在打开失败
}
else if(strcmp(method, "w") == 0) {
flag = O_WRONLY | O_CREAT | O_TRUNC; // 清空只写, 文件不存在创建文件
}
else if(strcmp(method, "w+") == 0) {
flag = O_RDWR | O_CREAT | O_TRUNC;
}
else if(strcmp(method, "a") == 0) {
flag = O_WRONLY | O_CREAT | O_APPEND; // 追加只写, 文件不存在创建文件
}
else if(strcmp(method, "a+") == 0) {
flag = O_RDWR | O_CREAT | O_APPEND;
}
int fileno = open(filename, flag, 0666); // 封装系统接口打开文件
if(fileno < 0) {
return NULL; // 打开文件失败
}
// 打开文件成功, 则为myFILE开辟空间
myFILE* fp = (myFILE*)malloc(sizeof(myFILE)); // 有开辟失败的可能
if(fp == NULL) {
return fp;
}
memset(fp, 0, sizeof(myFILE)); // 将开辟的空间全部置0
fp->_fileno = fileno; // 更新 myFILE里的_fileno
fp->_flags |= LINE_FLUSH; // 默认行刷新
fp->_end = 0; // 默认缓冲区为空
return fp;
}
int main() {
myFILE* pf = my_open("newlog.txt", "w+");
printf("打卡文件的fd:%d\n", pf->_fileno);
printf("打卡文件缓冲区占用:%d\n", pf->_end);
return 0;
}
my_fclose()函数
void my_fflush(myFILE* fp) {
assert(fp);
if(fp->_end > 0) { // _end记录的是 缓冲区内数据的长度
write(fp->_fileno, fp->_buffer, fp->_end); // 向fd中写入缓冲区数据
// 这里不在判断是否写入成功
fp->_end = 0;
syncfs(fp->_fileno); // 我们只向内核中写入了数据, 数据可能存储到了操作系统中 文件系统的缓冲区中, 需要刷新一下文件系统的缓冲区
// 与文件缓冲区不同
}
}
void my_fclose(myFILE* fp) { // 暂时忽略返回值
// 再关闭文件之前, 需要先刷新缓冲区, 所以可以先写一个刷新缓冲区的函数
my_fflush(fp);
close(fp->_fileno); // 封装系统接口close()关闭文件
free(fp); // 记得free掉 malloc出来的空间
}
my_fclose()
的实现, 重点在关闭文件前缓冲区的刷新, 和 free()掉malloc的空间my_fwrite()函数
'\n'
就将'\n'
及以前的所有数据刷新出去.'\n'
就刷新我们模拟实现C文件操作是为了加深对文件缓冲区的理解, 而不是为了完善函数和算法模拟
void my_fwrite(myFILE* fp, const char* start, int len) {
assert(fp);
assert(start);
assert(len > 0);
strncpy(fp->_buffer + fp->_end, start, len); // 将start 追加到_buffer原内容之后
fp->_end += len; // 更新一下 _end;
// 刷新缓冲区
if(fp->_flags & NONE_FLUSH) {
// 无缓冲
my_fflush(fp);
}
else if(fp->_flags & LINE_FLUSH) {
// 行刷新
if(fp->_end > 0 && fp->_buffer[fp->_end-1] == '\n') { // 需要访问_end-1位置, 所以要先判断_end > 0
my_fflush(fp);
}
}
else if(fp->_flags & FULL_FLUSH) {
if(fp->_end == SIZE) { // SIZE是缓冲区的大小
my_fflush(fp);
}
}
}
int main() {
myFILE* pf = my_open("newlog.txt", "a+");
const char* buf1 = "Hello world, hello July";
const char* buf2 = "Hello world, hello July\n";
my_fwrite(pf, buf2, strlen(buf2));
my_fwrite(pf, buf1, strlen(buf1));
// my_fflush(pf); // 可以看一下词语执行与否的差别
return 0;
}
再谈重定向
实现命令行重定向
>
>>
<
实现重定向呢?简易shell博客地址
命令行重定向的用法
标准输出与标准错误
一般都是显示器
. 也就是说, 不论是向fd=1还是fd=2写入数据, 一般情况下都是向显示器写入数据#include <cstdio>
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstring>
#include <cerrno>
int main() {
// stdout
printf("hello printf fd=1\n");
fprintf(stdout, "hello fprintf fd=1\n");
fputs("hello printf fd=1\n", stdout);
// stderr
fprintf(stderr, "hello fprintf fd=2\n");
fputs("hello fputs fd=2\n", stderr);
perror("hello perror fd=2");
// cout
std::cout << "hello cout fd=1" << std::endl;
// cerr
std::cerr << "hello cerr fd=2" << std::endl;
return 0;
}
./out_err > out_err.txt
重定向的完整用法
./out_err 1> out_err.txt
./out_err 2> out_err.txt
1> 重定向
是输出重定向, 而使用 2> 重定向
则是错误重定向命令 fd> 文件
fd>
:0>
是输入重定向, 1>
是输出重定向, 2>
是错误重定向, >>
是追加重定向重定向其他用法
1>
是输出重定向, 2>
是错误重定向.可不可以同时输出重定向和错误重定向
**?./out_err 1> out.txt 2> err.txt
可以分离程序的运行日志, 可以将运行错误日志分离出来以便分析
将输出、错误重定向同时重定向到同一个文件中
?./out_err 1> all.txt 2>&1
2>&1的操作, 可以看作 是
将标准错误输出重定向
作者: 哈米d1ch 发表日期:2023 年 3 月 17 日