【Linux】重定向与应用缓冲区

重定向原理
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>

#define filename "log.txt"

int main()
{
int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd : %d\n", fd);
const char *msg = "hello linux\n";
int cnt = 5;
while(cnt--)
{
write(fd, msg, strlen(msg));
}

close(fd);

return 0;
}
AI写代码
cpp
运行

 

下面通过对上面这段代码进行修改,以此来证明重定向原理:

 

 

printf函数底层封装的是stdout文件,指向的是1号文件描述符,是向显示器文件打印的。将1号文件描述符关闭,就无法通过printf函数向显示器文件打印内容。

 

文件描述符对应的分配规则是,从0下标开始,寻找最小的没有被使用的数组位置,其下标就是新文件的文件描述符。

 

 

Linux下一切皆文件,本来应该向显示器文件写入的内容,写到了log.txt文件中,这种情况就类似于输出重定向。

先将1号文件描述符关闭,说明将1号的显示器文件关闭,1号文件描述符就被空出来了。
再打开一个新的文件时,新的文件会占用1号文件描述符
后续再向1号文件描述符写内容时,就是向新打开的文件中写入。

这种现象也被称之为重定向原理。

使用dup2系统调用接口输入和输出重定向

 

dup2()函数会对文件描述符进行拷贝:

dup2(int oldfd, int newfd);
oldfd:需要拷贝的fd
newfd:需要被覆盖的fd
newfd是oldfd的一份拷贝,两个文件描述符中最后全部描述的是oldfd

 

重定向fd到1号文件描述符,将fd号文件描述拷贝到1号文件描述符,这种现象称为输出重定向

 

本来应该需要在键盘中读取,现在改为向文件中读取,这种现象称为输入重定向。

 

无论是系统调用接口,还是库函数都可以使用dup2进行重定向操作。

 

使用指令“>”或者“>>”或者"<"进行输出或者追加或者输入重定向,本质上底层就是使用dup2函数进行重定向操作。

 

进程历史打开的文件与进行的各种重定向关系都和未来进行程序替换无关!也就是说,程序替换并不会影响文件访问。

指定1号2号文件描述符进行重定向

 

stdout底层的文件描述符是1,stderr底层的文件描述符是2。重定向后,会将向stdou输出的内容输出到normal.log文件中;而向stderr文件中写的内容,还是会向显示器输出。

 

正常打印数据的时候,不管正常的消息还是错误消息都会向显示器上打印,通过这种方式可以将常规消息和错误消息进行分离开,以方便查阅。

指令:./mytest 1>normal.log 2>err.log

通过这种方式,可以将1号和2号文件描述的内容输出到一个文件中。

指令:./mytest 1>all.log 2>&1
【理解】输出重定向后,将1号文件描述符指向all.log文件,然后2>&1的意思是将1号文件描述符写到2号文件描述符里面,1号文件描述符里面是all.log文件,然后2号也将指向all.log。

用户缓冲区
【注意】fread和fwrite接口注意事项

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
ptr是一个字符串指针
size指的是字符串的长度,一般是strlen(ptr)
nmemb指的是需要写入的size的个数
返回值size_t 指的是实际写入的nmemb的值,也就是实际的size的值。
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
const char *fstr = "hello fwrite\n";
const char *str = "hello write\n";

// c语言提供的接口
printf("hello printf\n"); // stdout -> 1
fprintf(stdout, "hello fprintf\n"); // stdout -> 1
fwrite(fstr, strlen(fstr), 1, stdout); // fread, stdout -> 1

// 操作系统提供的接口
write(1, str, strlen(str));
return 0;
}
AI写代码
cpp
运行

 

在后面创建进程后:

#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
const char *fstr = "hello fwrite\n";
const char *str = "hello write\n";

// c语言提供的接口
printf("hello printf\n"); // stdout -> 1
fprintf(stdout, "hello fprintf\n"); // stdout -> 1
fwrite(fstr, strlen(fstr), 1, stdout); // fread, stdout -> 1

// 操作系统提供的接口
write(1, str, strlen(str));

// 创建子进程
fork();

return 0;
}
AI写代码
cpp
运行

 

出现了一种特殊的情况,将执行重定向到文件中,log.txt文件中出现了7行内容,C语言接口被打印两次,而操作系统接口被打印1次。

要将这个问题解释清楚,需要将缓冲区理解清楚,继续进行一组测试。

第一种情况:C语言接口,使用close关闭

int main()
{
const char *fstr = "hello fwrite";
const char *str = "hello write";

// c语言提供的接口
printf("hello printf"); // stdout -> 1
fprintf(stdout, "hello fprintf"); // stdout -> 1
fwrite(fstr, strlen(fstr), 1, stdout); // fread, stdout -> 1

close(1);
return 0;
}
AI写代码
cpp
运行

 

第二种情况:系统接口调用,使用close关闭

int main()
{
const char *str = "hello write";

// 操作系统提供的接口
write(1, str, strlen(str));

close(1);
return 0;
}
AI写代码
cpp
运行

 

C语言接口在底层调用的是write接口,但是在实际中,使用C语言的接口却没有打印处出内容,而系统调用接口却打印出来内容。

这个缓冲区一定不在操作系统内部,不是系统级别的缓冲区
C语言会提供缓冲区,一个用户级缓冲区。显示器文件的刷新方案是行刷新,所以在printf执行完就会立即遇到\n的时候,会将数据进行刷新出去。用户刷新的本质,就是将数据通过1 + write写入内核中。
操作系统也会提供一个缓冲区,close将系统缓存区里的内容刷新到硬件中去。
目前可以认为,只要将数据刷新到内核,数据就可以到硬件了,或者说是在C语言层面提供了一个缓冲区。

 

问题1:缓冲区刷新问题(语言层)
无缓冲——直接刷新
行缓冲——不刷新,直到碰到\n(显示器)
全缓冲——缓冲区满了,才刷新(文件)
【注意】当进程退出的时候,进程也会刷新

 

问题2:为什么需要C语言层面的缓冲区
解决用户效率问题
配合格式化
【例如】printf("hello %d\n", a); 这里会将hello %d, a 转换成为"hello 1";

问题3:语言层面的缓冲区在哪里
【答】:FILE里面存在对应打开文件的缓冲区字段和维护信息。

问题4:这个FILE对象是属于用户还是属于系统?缓冲区是否属于用户级缓冲区?
【答】FILE对象属于用户,语言都属于用户,且缓冲区也属于用户级缓冲区。

理解接口为什么打印两次
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
const char *fstr = "hello fwrite\n";
const char *str = "hello write\n";

// c语言提供的接口
printf("hello printf\n"); // stdout -> 1
fprintf(stdout, "hello fprintf\n"); // stdout -> 1
fwrite(fstr, strlen(fstr), 1, stdout); // fread, stdout -> 1

// 操作系统提供的接口
write(1, str, strlen(str));

// 创建子进程
fork();

return 0;
}
AI写代码
cpp
运行

 

使用重定向向文件log.txt中打印,缓冲方案变成了全缓冲。此时,遇到\n不在刷新,而是等待缓冲区写满才刷新。write会直接将内容打印到系统的缓冲区内,而C语言层面的缓冲区中由于缓冲方案是全缓冲,而导致用户层面的缓冲区没有刷新到内核层面,使用fork会将用户层面的缓冲区拷贝一份,当结束进程时,两份缓冲区都会刷新到内核并将内容输出到硬件中去。

【注意】当不使用重定向的时候,使用的是行缓冲,行缓冲每次都会将内容输出出去,而fork的时候内容已经被输出出去。

模拟实现C文件标准库
makefile文件
myfile:main.c Mystdio.c
gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
rm -rf myfile
AI写代码
cpp
运行
Mystdio.h文件
#ifndef _MYSTDIO_H_
#define _MYSTDIO_H_

#include<string.h>

// 缓冲区大小
#define SIZE 1024

// 缓冲方式
#define FLUSH_NOW 1
#define FLUSH_LINE 2
#define FLUSH_ALL 4

// 文件类型的结构体
typedef struct IO_FILE
{
// 文件描述符
int fileno;
// 缓冲方式
int flag;
// 输入缓冲区
// char inbuffer[SIZE];
// int in_pos;
// 输出缓冲区大小
char outbuffer[SIZE];
int out_pos;
}_FILE;

_FILE *_fopen(const char *filename, const char* flag);
int _fwrite(_FILE* fp, const char* s, int len);
void _fclose(_FILE *fp);
void _fflush(_FILE *fp);

#endif
AI写代码
cpp
运行

Mystdio.c文件
#include"Mystdio.h"
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<assert.h>
#include<stdlib.h>
#include<unistd.h>

#define FILE_MODE 0666

_FILE *_fopen(const char* filename, const char* flag)
{
assert(filename);
assert(flag);

int f = 0, fd = -1;
if(strcmp(flag, "w") == 0)
{
f = (O_CREAT | O_WRONLY | O_TRUNC);
fd = open(filename, f, FILE_MODE);
}
else if(strcmp(flag, "r") == 0)
{
f = O_RDONLY;
fd = open(filename, f, FILE_MODE);
}
else if(strcmp(flag, "a") == 0)
{
f = (O_CREAT | O_APPEND | O_WRONLY);
fd = open(filename, f, FILE_MODE);
}
else return NULL;

if(fd == -1) return NULL;

_FILE* fp = (_FILE*)malloc(sizeof(_FILE));
if(fp == NULL) return NULL;

fp->fileno = fd;
fp->flag = FLUSH_LINE;
fp->out_pos = 0;

return fp;
}

int _fwrite(_FILE* fp, const char* s, int len)
{
memcpy(&fp->outbuffer[fp->out_pos], s, len);
fp->out_pos += len;

if(fp->flag == FLUSH_NOW)
{
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0;
}
else if(fp->flag == FLUSH_LINE)
{
if(fp->outbuffer[fp->out_pos - 1] == '\n')
{
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0;
}
}
else if(fp->flag == FLUSH_ALL)
{
if(fp->out_pos == SIZE)
{
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0;
}
}

return len;
}

void _fclose(_FILE* fp)
{
if(fp == NULL) return;
_fflush(fp);
close(fp->fileno);
free(fp);
}

void _fflush(_FILE* fp)
{
if(fp->out_pos > 0)
{
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0;
}
}
AI写代码
cpp
运行

main.c文件
#include "Mystdio.h"
#include <unistd.h>

#define myfile "test.txt"

int main()
{
_FILE *fp = _fopen(myfile, "a");
if(fp == NULL) return 1;

————————————————
版权声明:本文为CSDN博主「大柏怎么被偷了」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/dab112/article/details/155257162

阅读剩余
THE END
阿里云ECS特惠活动
阿里云ECS服务器 - 限时特惠活动

云服务器爆款直降90%

新客首单¥68起 | 人人可享99元套餐,续费同价 | u2a指定配置低至2.5折1年,立即选购享更多福利!

新客首单¥68起
人人可享99元套餐
弹性计费
7x24小时售后
立即查看活动详情
阿里云ECS服务器特惠活动