目录
一,引言
本节主要讲解在Linux中文件相关知识,首先先回归一下学C语言时常见的文件函数;讲解操作系统中文件接口,并理解库函数与系统接口的关系以及重定向的原理;谈论文件缓冲区的意义。
二,C语言文件接口
首先在C语言中文件相关函数,大部分函数都有两个参数:FILE* ,mode。第一个参数文件指针,第二个参数是打开模式。打开模式分为如下几种:
|------|----------------------------|
| 模式 | 含义 |
| "r" | 只读打开,文件必须存在 |
| "w" | 只写打开,文件不存在则创建,存在则清空原有内容 |
| "a" | 追加写打开,文件不存在则创建,写入内容追加到文件末尾 |
| "r+" | 读写打开,文件必须存在 |
| "w+" | 读写打开,文件不存在则创建,存在则清空 |
| "a+' | 读写打开,文件不存在则创建,写入仅追加到末尾 |
打开文件:
cpp
FILE *fopen(const char *filename, const char *mode);
filename:传入文件的相对路径或者绝对路径 mode:打开模式如上
关闭文件:
cpp
int fclose(FILE *stream);
stream : 需要关闭的文件指针
行级读写文件:
cpp
char *fgets(char *str, int size, FILE *stream);
str :讲读取的数据存入到str中 size :一般传str的大小,最多读取size-1个
cpp
int fputs(const char *str, FILE *stream);
将指定数据写入指定文件中。
块级读写文件:
cpp
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
cpp
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
ptr:存取数据缓冲区的指针 size:单个数据块的大小 nmemb:数据块的个数
缓冲区刷新文件
cpp
int fflush(FILE *stream);
手动刷新缓冲区。
除了以上的C语言文件处理函数还有随机读写文件如:fessk(),ftell()等等,这里不进行一一举例说明。
三,系统调用接口
上述文件接口都是C语言中封装实现的。但是C语言的接口处于用户层,而文件处于硬件层。这两者就无法进行直接访问。因此,用户想要实现对文件的IO操作必然要使用操作系统提供的接口才可以访问。也就是说上述中C语言的文件函数其实内部都封装了系统调用接口。接下来就讲解一下Linux中有关文件操作的系统调用。
1,标志位
当需要设置一种或多种变量的状态时就需要标志位进行标记。但是若如有多种需要标记的状态时就需要多个参数,这会导致函数传参过多的问题。因此引入位参数作为标志位。通常使用一个整形数据。一个整形数据有32个bit位。这里的每一个位数都代表一个变量的状态的开关,就实现了一个变量实现对多个参数需要标记的变量标记。举个例子如下:
cpp
#include <stdio.h>
// 1. 宏定义:单个位标志(每一位对应一个独立开关)
#define FILE_FLAG_READ (1 << 0) // 0x01(二进制 0001):只读
#define FILE_FLAG_WRITE (1 << 1) // 0x02(二进制 0010):只写
#define FILE_FLAG_APPEND (1 << 2) // 0x04(二进制 0100):追加
#define FILE_FLAG_BINARY (1 << 3) // 0x08(二进制 1000):二进制
// 2. 函数:根据标志位处理文件操作
void handle_file(const char *filename, int flags) {
printf("处理文件:%s\n", filename);
// 3. 判断单个标志位是否开启(用 & 运算)
if ((flags & FILE_FLAG_READ) != 0) {
printf(" - 启用:只读模式\n");
}
if ((flags & FILE_FLAG_WRITE) != 0) {
printf(" - 启用:只写模式\n");
}
if ((flags & FILE_FLAG_APPEND) != 0) {
printf(" - 启用:追加模式\n");
}
if ((flags & FILE_FLAG_BINARY) != 0) {
printf(" - 启用:二进制模式\n");
}
}
int main() {
// 4. 组合多个标志位(用 | 运算):同时开启「只写」+「追加」+「二进制」
int file_flags = FILE_FLAG_WRITE | FILE_FLAG_APPEND | FILE_FLAG_BINARY;
// 5. 传递标志位并处理
handle_file("test.dat", file_flags);
printf("\n修改标志位后:\n");
handle_file("test.dat", file_flags);
return 0;
}
如上图,使用这种方式,一个变量就可以通过传入参数的不同来控制多个变量的开启或者关闭状态。
2,系统调用函数
在了解了标志位,下面了解系统调用函数,在上文的讲解中用户是不能直接和硬件进行对接的,这中间必须依赖于操作系统。因此C语言中的文件调用函数内部就必然封装了操作系统的系统调用函数。下面就讲解一下这些系统调用函数。
打开文件:
cpp
#include <fcntl.h>
#include <unistd.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
在打开文件的系统调用函数中,一共有两种类型,当不需要创建文件时就传入两个参数的open函数;当需要创建文件时就传入三个参数的open函数。这是由于创建文件需要设置权限。mode就是用来设置创建新文件的权限。
pathname:就是打开或者创建文件的路径,可以使用绝对地址或者相对地址。
flags:表示该文件以哪种形式进行打开或者创建,具体格式如下:
|----------|--------------------------------------|
| O_RDONLY | 只读打开 |
| O_WRONLY | 只写打开 |
| O_RDWR | 读,写打开 |
| O_CREAT | 若⽂件不存在,则创建它。需要使⽤ mode 选项,来指明新⽂件的访问权限 |
| O_APPEND | 追加写 |
|---------|-------------|
| O_TRUNC | 文件存在则清空文件内容 |
当打开一个文件时,前三个必须选择一种。后三种可以根据需要选择加或者不加。如下:
cpp
int fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, FILE_MODE);
当需要多种打开方式可以使用 | 进行链接。
读写文件:
cpp
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
fd:在后面进行详细讲解。
buf:指向缓冲区的存储地址
count:存入或者读取数据的大小
使用read或者write函数时要注意count的大小,通常来说buf大小减一 ,最后一个空间 留给/0的位置。但是对于大文件读取,当一次并不能将文件中的全部文件都读到buf中,这时就要注意给/0的位置。
注意 :/0并不是操作系统的概念,而是C语言的概念,因此若不适用C语言打印函数等等,则不需要/0进行处理。否则会导致系统识别错误。
关闭文件:
cpp
#include <unistd.h>
int close(int fd);
传入指定的文件标识符来关闭文件。
3,文件描述符
在上述讲解系统调用的用法中,会发现open函数返回一个int类型的参数,这个参数就是文件描述符。在进程的PCB中,会有一个files指针指向一个struct_files结构体。这个结构体中存在file*的数组。这个数组中就是存储打开文件的地址信息。具体如图:

实际上来说,文件描述符就是这个数组的下标。通常来说当一个进程启动时,会自动打开:标准输入,标准输出,标准错误 。对应下标中的0,1,2 。这就是为什么当创建一个文件的下标是从3开始。在操作系统中,系统选择文件下标的规则是,查找最小没有被占用的下标作为这个打开文件的下标。
4,重定向
也就是说操作系统本质是根据下标进行选择文件的,并不关心这个下标有没有被篡改,这就引出了重定向的概念。
cpp
int main()
{
close(1);
int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
exit(0);
}
在创建文件之前,先关闭了标出输出文件。这时下标为1的位置就空出来了。因此本来要写入在显示器的文件就写入到了myfile文件中。这也就验证了操作系统只认文件描述符。
cpp
ls -l > list.txt
ps aux >> process.txt
如图本应该写入显示器,但是进行重定向写入上图中的两个文件中。这个现象的原理就是第一张图所展示的原理。
在操作系统中,dup2()系统调用可以将文件地址进行覆盖。函数原型如下:
cpp
#include <unistd.h> // 必须包含的头文件
int dup2(int oldfd, int newfd);
将oldfd的文件描述符的文件指针,覆盖到newfd文件描述符的文件指针的位置。当操作系统方法newfd文件描述符的位置时,就会访问到oldfd文件描述符处的位置。举个例子如下:
cpp
int main() {
// 1. 打开目标文件(只写、不存在则创建、存在则清空,权限 0644)
const char *log_file = "stdout_redirect.log";
int fd = open(log_file, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open 日志文件失败");
return 1;
}
printf("重定向前:stdout 指向终端,这句话会打印到屏幕\n");
// 2. 核心:用 dup2() 重定向标准输出(STDOUT_FILENO = 1)到 fd 对应的文件
if (dup2(fd, STDOUT_FILENO) == -1) { // STDOUT_FILENO 是系统宏,等价于 1
perror("dup2 重定向标准输出失败");
close(fd);
return 1;
}
// 3. 关闭原文件描述符 fd(dup2 后,1 已经指向该文件,fd 不再需要)
close(fd);
// 4. 后续所有标准输出操作,都会写入 stdout_redirect.log
printf("重定向后:这句话会写入 %s 文件\n", log_file);
char *msg = "Hello, dup2()! 这是重定向后的输出内容\n";
write(STDOUT_FILENO, msg, strlen(msg)); // 直接操作 1,也会写入文件
return 0;
}
而追加重定向,输入重定向的原理和上图中原理一致,只不过替换的文件不一致。
四,文件缓冲区
由于用户作用在硬件这个过程中需要操作系统这个中间的"管理者"作用。但是操作系统还有其他很多进程需要处理,如果频繁的进行系统调用就降低了操作系统的操作效率。于是就有了缓冲区的概念。
缓冲区大致分为两种:用户级缓冲区,内核级缓冲区

内核级缓冲区由操作系统自己控制什么时候刷新,如何刷新。使用我们主要关注的用户级缓冲区。
用户级缓冲区的刷新方式主要分为三种:
1,强制刷新--C语言中有文件刷新函数可以直接进行刷新
2,条件满足进行刷新--如缓冲区满,或者行刷新等等
3,进程退出也会刷新缓冲区
文件刷新的过程:当打开一个文件,输入数据,首先该数据会进入该语言所封装库函数的缓冲区(也就是上文中的C语言缓冲区)之后,当满足缓冲区刷新的某种规则,会将该缓冲区的数据刷新进入内核缓冲区。最后内核缓冲区有自己的缓冲区刷新规则最后刷新到磁盘之中。

最后使用一个例子来总结一下缓冲区的机制。
当库函数和系统调用作用于显示器时(也就是第一次调用)库函数实现的行刷新,因此当到fork()子进程时,缓冲区已经刷新过了,所以就写入了一遍。
当库函数和系统调用作用于普通文件时(第二次调用)库函数实现的缓冲区满刷新,因此当到fork()函数时,缓冲区还没有刷新,所以写入了两遍。