首先回忆一下我们讲操作系统概念时,画的一张图 系统调用接口和库函数的关系,一目了然。 所以,可以认为, f# 系列的函数,都是对系统调用的封装,方便二次开发 也就是说 fopen fclose fread fwrite 都是 C 标准库当中的函数,我们称之为库函数( libc )。 而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口
文件描述符表
通过对 open 函数的学习,我们知道了文件描述符就是一个小整数每个进程在操作系统中都有一个与之关联的文件描述符表(File Descriptor Table)。这个表是一个数组,数组的每个元素都是一个指向打开文件的引用。文件描述符(File Descriptor,简称fd)就是这个数组的索引,通常是非负整数。
在Linux系统中,文件描述符表的前三个索引默认对应以下标准文件:
- 0:标准输入(Standard Input,
stdin
) - 1:标准输出(Standard Output,
stdout
) - 2:标准错误(Standard Error,
stderr
)
当一个进程打开一个新的文件时,操作系统会在文件描述符表中找到最小的未被使用的索引,将其分配给新打开的文件。
而现在知道,文件描述符就是从 0 开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file 结构体。表示一个已经打开的文件对象。而进程执行 open 系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表 files_struct, 该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件重定向
如果我们关闭标准输出(文件描述符1
),会发生什么呢?请看以下代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
close(1);
int fd = open("myfile", O_WRONLY | O_CREAT, 0644);
if (fd < 0) {
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
运行后,我们会发现本应输出到显示器的内容被写入到了文件myfile
中,且文件描述符fd
的值为1
。这种现象称为输出重定向。常见的重定向符号有>
、>>
、<
等。
重定向的本质
重定向的本质是改变文件描述符的指向。可以使用dup2
系统调用来实现。其函数原型如下:
#include <unistd.h>
int dup2(int oldfd, int newfd);
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
int fd = open("./log", O_CREAT | O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
close(1);
dup2(fd, 1);
for (;;) {
char buf[1024] = {0};
ssize_t read_size = read(0, buf, sizeof(buf) - 1);
if (read_size < 0) {
perror("read");
break;
}
printf("%s", buf);
fflush(stdout);
}
return 0;
}
重定向的本质是改变文件描述符在文件描述符表中的指向,使得原本指向某个文件或设备的文件描述符指向其他的文件或设备。
以输出重定向为例,当我们执行命令command > output.txt
时,实际上是将标准输出(文件描述符1)重定向到了output.txt
文件。
具体流程如下:
-
打开目标文件:进程调用
open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644)
打开output.txt
文件,获得一个新的文件描述符,假设为fd_new
。 -
保存标准输出(可选):如果需要恢复标准输出,可以在重定向前保存标准输出的文件描述符。
-
重定向标准输出:使用
dup2(fd_new, 1)
系统调用,将fd_new
复制到文件描述符1的位置。此时,文件描述符1指向output.txt
,而原来的fd_new
被关闭。 -
关闭原文件描述符:如果
fd_new
不等于1,dup2
会自动关闭fd_new
,无需手动关闭。 -
执行命令:进程继续执行,所有写入标准输出(
stdout
)的内容都会写入到output.txt
文件中。
所以也就是说dup2对文件描述符的拷贝本质上是文件描述符下标所对应内容的拷贝
缓冲区
来段代码在研究一下#include <stdio.h>
#include <string.h>
int main()
{
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
运行出结果:
hello printf
hello fwrite
hello write
但如果对进程实现输出重定向呢?
./hello > file
, 我们发现结果变成了
hello write
hello printf
hello fwrite
hello printf
hello fwrite
我们发现
printf
和
fwrite
(库函数)都输出了
2
次,而
write
只输出了一次(系统调用)。为什么呢?肯定和fork有关!
一般
C
库函数写入文件时是全缓冲的,而写入显示器是行缓冲。printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后。但是进程退出之后,会统一刷新,写入文件当中。但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。write 没有变化,说明没有所谓的缓冲
所以我们可以得出以下内容:
缓冲区的类型
根据缓冲策略,C标准库中的缓冲区可以分为以下三种类型:
-
全缓冲(Fully Buffered):
- 只有当缓冲区满时,数据才会被真正写入文件或设备。
- 适用于文件等块设备。
-
行缓冲(Line Buffered):
- 当检测到换行符(
\n
)时,缓冲区的数据会被刷新。 - 适用于交互式设备,如终端(显示器、键盘)。
- 当检测到换行符(
-
无缓冲(Unbuffered):
- 数据不会经过缓冲区,直接写入文件或设备。
- 适用于
stderr
等需要立即输出的场景。
库函数的缓冲机制
C标准库中的输入输出函数,如printf
、fprintf
、fwrite
等,都是基于FILE
结构体实现的。这些函数在底层使用了用户级的缓冲区,以提高I/O效率。
FILE
结构体与缓冲区
FILE
结构体是C标准库中用于表示文件流的类型。它的定义在stdio.h
和libio.h
中,包含了与缓冲区相关的成员。
struct _IO_FILE {
int _flags; /* 标志位 */
char* _IO_read_ptr; /* 当前读取位置的指针 */
char* _IO_read_end; /* 读取缓冲区的结束位置 */
char* _IO_read_base; /* 读取缓冲区的起始位置 */
char* _IO_write_base; /* 写入缓冲区的起始位置 */
char* _IO_write_ptr; /* 当前写入位置的指针 */
char* _IO_write_end; /* 写入缓冲区的结束位置 */
/* 其他成员 */
};
这些缓冲区成员用于暂存数据,直到满足刷新条件才进行实际的I/O操作。
缓冲区的存在主要有以下原因:
- 减少系统调用次数:每次系统调用都会有一定的开销。通过使用缓冲区,可以将多次小的I/O操作合并为一次大的I/O操作。
- 提高磁盘和网络I/O效率:磁盘和网络设备的I/O操作通常比内存慢得多。缓冲区可以协调不同速度的设备之间的数据传输。
标签:文件,重定向,描述符,fd,缓冲区,表与,hello From: https://blog.csdn.net/2301_77754590/article/details/142871993