Linux学习笔记(十一)--文件接口与重定向
文件的共识原理:
(1)文件 = 内容 + 属性
(2)文件分为打开文件和未被打开的文件
(3)打开的文件由进程打开,所以本质是研究文件与进程的关系。(文件被打开必须先被加载到内存)
(4)未被打开的文件被放置在磁盘上。
C语言文件接口:
我们先来看C语言文件接口,它主要分为两类:标准I/O(缓冲I/O)和 低级I/O(无缓冲I/O)。
(1)标准I/O
这类函数定义在头文件 <stdio.h>中。它们通过一个指向 FILE结构体的指针(称为文件指针)来操作文件,并提供了缓冲区,使得读写效率更高。
核心流程:打开->读写->关闭
打开文件:
FILE *fopen(const char *filename, const char *mode);功能:打开一个文件,并返回一个指向该文件的 FILE指针。
参数:filename:文件路径;mode:打开文件的方式
mode参数:
(1)"r":只读。文件必须存在。
(2)"w":只写。如果文件存在,内容被清空;如果不存在,则创建。
(3)"a":追加。如果文件存在,数据写入到文件末尾;如果不存在,则创建。
(4)"r+":读写。文件必须存在。
(5)"w+":读写。如果文件存在,内容被清空;如果不存在,则创建。
(6)"a+":读写。如果文件存在,数据追加到文件末尾;如果不存在,则创建。
关闭文件:
int fclose(FILE *stream);功能:关闭已打开的文件流。关闭前会刷新缓冲区。这是一个非常重要的步骤,避免数据丢失和资源泄漏。
返回值:成功返回0,失败返回EOF。
(2)低级I/O(系统调用,无缓冲I/O,也是系统接口)
这类函数是操作系统提供的系统调用,定义在头文件 <unistd.h>和 <fcntl.h>中。它们通过一个整数(称为文件描述符)来操作文件,没有缓冲区,每次读写都直接调用操作系统内核。
核心流程:打开->读写->关闭
打开/创建文件:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
功能:打开或创建一个文件,返回文件描述符(一个小的非负整数)。
参数:pathname:文件路径。
flags:打开标志,必须包含以下之一:(1)O_RDONLY:只读(2)O_WRONLY:只写(3)O_RDWR:读写(4)O_CREAT:文件不存在则创建(5)O_TRUNC:文件存在则清空(6)O_APPEND:追加模式
mode:当使用 O_CREAT时,指定新文件的权限(如 0644)。(可以通过u_mask来修改文件权限的掩码)
open函数返回值:
返回成功情况:
int fd = open("file.txt", O_RDONLY);
if (fd >= 0) {printf("打开成功,文件描述符: %d\n", fd);close(fd);
}返回值为非负整数(0, 1, 2, 3, 4, ...),通常是当前进程可用的最小未使用文件描述符,标准输入(0)、输出(1)、错误(2)已被系统占用,所以从3开始分配。
文件描述符fd:
概念:是Linux系统中用于访问I/O资源的抽象句柄。
系统层面:
文件描述符是一个整数索引,指向内核维护的进程文件描述符表中的条目。
文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的 最小的一个下标,作为新的文件描述符。
文件描述表:
概念:操作系统内核为每个进程维护的一个数据结构,用于管理该进程所有打开的文件描述符。它是进程文件I/O操作的核心管理机制。
定义:文件描述表是一个指针数组,其中每个元素指向一个文件对象(file结构体)。文件描述符(fd)就是这个数组的索引。
可视化理解:
进程A的文件描述表:
索引[0] ─────→ [标准输入文件对象]
索引[1] ─────→ [标准输出文件对象]
索引[2] ─────→ [标准错误文件对象]
索引[3] ─────→ [普通文件对象]
索引[4] ─────→ [套接字对象]
索引[5] ─────→ [管道对象]
...
索引[1023] ──→ [空]如下所示:

关闭文件:
#include <unistd.h>
int close(int fd);C语言会默认打开三个输入输出流:分别是stdin,stdout和stderror。而文件又在磁盘上,磁盘是外部设备,所以访问文件其实是在访问硬件。几乎所有的库只要是访问硬件设备,必须要封装系统调用。
访问文件的本质:
磁盘存放在文件当中,进程想打开文件,一个进程有自己的PCB,一个文件被打开必须要被系统管理,所以操作系统会把被打开的文件属性放在一起,每一个被打开的文件都会在内核当中创建一个内核数据结构(struct file->操作系统内描述一个被打开的文件的信息会直接或间接的包含操作的文件在磁盘的位置),每打开一个文件内核就会创建一个struct file。
原理图如下所示:
在Linux中我们把一号文件描述符关闭后,使用是stdero也能打印信息在屏幕上,这是因为关闭文件描述符 1 不会影响文件描述符 2,因此使用 stderr依然能正常打印。它们是两个独立的通道。确保了即使标准输出被意外关闭或重定向,错误信息仍然有一个独立的通道可以输出(关闭文件是将引用计数减减和自己对应的struct file数组下标滞空)。
重定向:
概念:重定向是改变进程标准输入/输出/错误默认指向的技术,使得程序可以从非常规来源读取输入或向非常规目标输出结果。
本质:修改进程文件描述符表中的指向关系,让特定的文件描述符(如0,1,2)指向不同的文件对象。
重定向前:
fd[0] → 终端设备(标准输入)
fd[1] → 终端设备(标准输出)
fd[2] → 终端设备(标准错误)重定向后:
fd[0] → 文件A(输入重定向)
fd[1] → 文件B(输出重定向)
fd[2] → 文件C(错误重定向)使用dup2系统调用(dup2是Linux中的一个系统调用,用于复制文件描述符)。
这里先简单介绍一下dup2这个接口:
#include <unistd.h>
int dup2(int oldfd, int newfd);核心功能:dup2将 oldfd复制到 newfd上。如果 newfd已经打开,它会先被关闭,然后再复制。
所以在这里我们使用dup2来实现系统级别的重定向:
#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;}所以从这里我们可以理解printf是C库当中的I/O函数,一般往stdout中输出,但是stdout底层访问文件的时候,找的还是fd:1, 但此时,fd:1 下标所表示内容,已经变成了myfile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。
