文章目录
- 前言:
- 1. 铺垫
- 1.1. 对文件表述符的理解
- 2. 重新使用C文件接口:对比一下重定向
- 2.1. 什么叫当前路径?
- 2.2. 写入文件
- 2.3. 读文件
- 2.4. 程序默认打开的文件流
- 2.5. 输出
- 2.6. 输入
- 3. 系统调用提供的文件接口
- 3.1. open 打开文件
- 3.2. open函数返回值
- 3.3. C语言 与 系统调用 文件读写操作
- 4. 缓冲区问题
- 总结:
前言:
在计算机编程中,文件操作是基础且至关重要的技能之一。无论是在系统编程、网络编程还是数据处理,文件的读写操作都是不可或缺的。本文将深入探讨文件操作的底层原理,从C语言层面的文件接口到操作系统层面的系统调用,再到缓冲区机制的实现,逐步揭示文件操作的全貌。通过对比C语言的文件接口和系统调用,以及对缓冲区问题的深入分析,本文旨在帮助读者建立一个清晰的文件操作概念框架,从而在实际开发中更加得心应手。
1. 铺垫
a. 文件 = 内容 + 属性
b. 访问文件之前,都得先打开。修改文件,都是通过指向代码的方式完成修改,文件必须加载到内存中
c. 谁打开文件?进程在打开文件
d. 一个进程可以打开多少个文件呢?可以打开多个文件
- 一定时间内,系统中存在多个进程,也可能同时存在更多的被打开文件,OS要不要管理多个被进程打开的文件呢?肯定的
- 如何管理呢?先组织,再描述!
e. 进程和文件的关系,struct task_struct 和 struct XXX?
a~e 被打开文件都是:内存文件
f. 系统中是不是所有的文件都被进程打开了?不是!没有被打开文件?就在磁盘中
1.1. 对文件表述符的理解
在Linux系统中,文件描述符(File Descriptor,FD)是一个用于引用打开文件和其他类型的I/O资源的整数。以下是对Linux文件描述符的几个关键理解:
-
唯一标识:文件描述符为每个打开的文件或I/O资源提供了一个唯一的标识符,通常是一个非负整数。
-
文件描述符表:每个进程都有自己的文件描述符表,这是一个内核数据结构,用于跟踪进程打开的所有文件和I/O资源。
-
系统调用:文件描述符通常通过系统调用如
open
、read
、write
、close
等进行操作。open调用返回一个新的文件描述符,read和write使用文件描述符来读取或写入数据,而close用于释放文件描述符。 -
标准流:Linux为标准输入(stdin)、标准输出(stdout)和标准错误(stderr)分别分配了文件描述符0、1和2。
-
缓冲机制:Linux内核可能会对通过文件描述符进行的I/O操作使用缓冲机制,以提高性能和减少实际的磁盘I/O操作。
-
错误处理:当系统调用失败时,会返回-1,并且全局变量errno会被设置为表示错误的特定值。
-
多路复用:文件描述符可以用于I/O多路复用机制,如select、poll和epoll,允许进程同时监控多个文件描述符上的I/O状态。
-
继承性:当创建新进程时,子进程会继承父进程的文件描述符表中的文件描述符,除非它们在子进程中被显式地关闭。
-
重定向:文件描述符可以通过dup、dup2等函数进行重定向,允许将一个文件描述符的引用复制到另一个文件描述符上。
-
文件锁:文件描述符可以用于对文件加锁,以控制对文件的并发访问。
文件描述符是Linux系统中进程与文件和I/O资源交互的基础,它们提供了一种统一的方式来处理各种类型的I/O操作,包括文件、管道、网络连接等。
2. 重新使用C文件接口:对比一下重定向
FILE *fp = fopen("./log.txt", "w"); //以只写的方式打开会把该文件清空if (fp == NULL){perror("fopen");return 1;}//文件操作const char *str = "hallo file!\n";fputs(str, fp);fclose(fp);return 0;
以 w 方式打开文件的时候,该文件会被自动清空。
echo "hello bit" > log.txt //hello bit,本质上就是写入
> log.txt //文件直接被清空,是因为在输出重定向时需要先把文件打开
以 a 方式打开文件,就类似于重定向中的追加。
FILE *fp = fopen("./log.txt", "a"); //以追加的形式打开echo "hello bit" >> log.txt //对比追加重定向
重定向:
在Linux系统中,重定向是通过文件描述符(File Descriptor)来实现的。文件描述符是内核用来跟踪打开文件和I/O流的一种机制。以下是Linux重定向的实现原理:
- 标准文件描述符:每个进程都有三个预分配的标准文件描述符:
标准输入(stdin,文件描述符为0)
标准输出(stdout,文件描述符为1)
标准错误(stderr,文件描述符为2)
文件描述符表:进程启动时,内核会为每个进程创建一个文件描述符表,表中记录了所有打开的文件和I/O设备的引用。
-
重定向操作:在shell中,可以使用特定的语法来重定向命令的输出或输入。例如,
>
用于将输出重定向到文件,<
用于将输入重定向自文件。 -
文件描述符复制:当执行重定向操作时,shell会使用
dup2
系统调用来复制文件描述符。dup2(oldfd, newfd)
会将oldfd
指向的文件或设备复制到newfd,如果newfd已经打开,它会被关闭并重新指向oldfd
所指向的文件或设备。 -
关闭和打开文件:在重定向过程中,shell可能首先会关闭一个文件描述符(如果它已经打开),然后打开一个新的文件或设备,并将其文件描述符复制到原来的位置。
-
缓冲机制:标准I/O库(如C语言的stdio库)使用缓冲区来提高I/O效率。重定向操作可能会影响这些缓冲区的状态,特别是当改变标准输出或标准错误时。
-
错误处理:在执行重定向操作时,需要检查文件操作和
dup2
调用的返回值,以确保没有错误发生。 -
临时文件描述符:有时,重定向操作会涉及创建一个临时文件描述符,用于在执行重定向之前存储当前文件描述符的状态。
通过这种方式,Linux系统允许用户和程序灵活地控制数据流的方向,无论是将输出写入文件、从文件读取输入,还是将错误消息重定向到不同的目的地。
2.1. 什么叫当前路径?
在进程文件里 ls /proc/29065 -l
在进程启动时,会记录自己启动时所在的路径。
2.2. 写入文件
const char *msg = "hallo file!\n";int cnt = 5;while (cnt){int n = fwrite(msg, strlen(msg), 1, fp);printf("write %d block, pid is : %d\n", n, getpid());cnt--;sleep(20);}
2.3. 读文件
char buffer[64];while(true) {char* r = fgets(buffer, sizeof(buffer), fp); // 按行读if (!r) break;printf("%s", buffer);}
2.4. 程序默认打开的文件流
stdin //标准输入 键盘设备
stdout //标准输出 显示器设备
stderr //标准错误 显示器设备
2.5. 输出
printf("hello printf\n");fputs("hello fputs", stdout);const char *msg = "hello fwrite\n";fwrite(msg, 1, strlen(msg), stdout);fprintf(stdout, "hello fprint\n");
2.6. 输入
char buffer[64];fscanf(stdin, "%s", buffer);
3. 系统调用提供的文件接口
访问文件不仅仅有C语言上的文件接口,OS必须提供对应的访问文件的系统调用?
w
: 清空文件、a
: 追加文件、r
: 读取文件内容
3.1. open 打开文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int open(const char *pathname, int flags); // falgs 是用位图进行传参,//哪个比特位被设置了就传递哪一个
int open(const char *pathname, int flags, mode_t mode); // 这里的mode 为权限掩码 umask
pathname
: 要打开或创建的目标文件flags
: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。- 参数:
O_RDONLY
: 只读打开
O_WRONLY
: 只写打开
O_RDWR
: 读,写打开 这三个常量,必须指定一个且只能指定一个
O_CREAT
: 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND
: 追加写- 返回值:
成功:新打开的文件描述符
失败:-1
设置文件创建的掩码:
#include <sys/types.h>#include <sys/stat.h>mode_t umask(mode_t mask); // 设置我们对应的权限掩码
write
、read
、close
、lseek
,类比C文件相关接口。
3.2. open函数返回值
在认识返回值之前,先来认识一下两个概念: 系统调用 和 库函数
结论1:C语言的文件接口,本质就是封装了系统调用。
FILE: C标准库中自己封装的一个结构体,必须 封装特定的 fd
C语言问什么要封装呢?为了保证自己的跨平台性。
认识 fd:数组下标?
文件描述符的本质就是数组下标。
题外话:如何理一切皆文件
通过struct file {…} 屏蔽掉了各种硬件的底层硬件差异 ,VFS(虚拟文件系统)
文件 fd 的分配规则 && 利用规则实现重定向
fd 的分配规则:从最小的没被使用的数组下标,会分 配给最新打开的文件!
想实现文件描述符的重定向,不用关闭再重新打开,OS必须提供“拷贝”接口。
#include <unistd.h>int dup(int oldfd);int dup2(int oldfd, int newfd);
3.3. C语言 与 系统调用 文件读写操作
C语言文件操作:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>int main()
{FILE* fp;const char* filename = "bite";const char* message = "linux so easy";char buffer[256]; // 用于存储读取的数据size_t bytesRead; // 读取的字节数fp = fopen(filename, "w+");if (fp == NULL) {perror("Error opening file");}fwrite(message, sizeof(char), strlen(message), fp);// 移动文件指针到到文件开头fseek(fp, 0, SEEK_SET);bytesRead = fread(buffer, sizeof(char), strlen(message), fp);buffer[bytesRead] = '\0';printf("%s\n", buffer);fclose(fp);return 0;
}
系统调用文件操作:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#include <fcntl.h>
#include <unistd.h>int main()
{int fd; // 文件描述符const char *filename = "bite";const char *message = "I like Linux!";ssize_t bytesRead;off_t offset;char buffer[256]; // 用于存储读取的数据fd = open(filename, O_RDWR | O_CREAT, 0644);if (fd == -1) {perror("Error opening file");return 1;}// 写入消息到文件bytesRead = write(fd, message, strlen(message));if (bytesRead == -1) {perror("Error writing to file");close(fd);return 1;}// 移动文件指针到文件开头,以便读取offset = lseek(fd, 0, SEEK_SET);if (offset == -1) {perror("Error seeking in file");close(fd);return 1;}// 打印读取的内容到标准输出// 从文件中读取内容bytesRead = read(fd, buffer, sizeof(buffer) - 1); // 确保有空间放置空字符'\0'buffer[bytesRead] = '\0'; // 确保字符串以空字符串结尾printf("%s\n", buffer);// 关闭文件描述符if (close(fd) == -1) {perror("Error closing file");return 1;}return 0;
}
4. 缓冲区问题
缓冲区它就是一块内存区域(用空间换时间)
为什么有? 提高使用者的效率
聚集数据,一次拷贝(刷新),提高整体效率。
调用系统调用是有成本的,时间&&空间
我门一直在说的缓冲区和内核中的缓冲区没有关系(尽管它有),语言层面的缓冲区,C语言自带缓冲区,缓冲到一定程度后再刷新到操作系统的缓冲区中。
- 无刷新,无缓冲
- 行刷新——显示器
- 全缓冲,全部刷新——普通文件,缓冲区被写满,才刷新。 还有两种刷新:强制刷新、进程退出的时候要自动刷新。
具体在哪里?
FILE *fp = fopen("log.txt", "w");
FILE *fp = ??
FILE
: 其实是一个结构体(fd), 缓冲区是被FILE结构来维护的! (stdin
,stdout
,stderr
)
编码模拟:手动模拟一下 C标准库中的方法。
// mystdio.h
#pragma once#include <stdio.h>#define SIZE 4096
#define NONE_FLUSH (1<<1)
#define LINE_FLUSH (1<<2)
#define FULL_FLUSH (1<<3)typedef struct _myFILE
{// char inbuffer[];char outbuffer[SIZE];int pos;int cap;int fileno; int flush_mode;
}myFILE;myFILE* my_fopen(const char* pathname, const char* mode);
int my_fwrite(myFILE *fp, const char* s, int size);
void my_fclose(myFILE* fp);
void my_fflush(myFILE* fp);
void DebugPrint(myFILE *fp);
// mystdio.c
#include "mystdio.h"
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>const char* toString(int flag)
{if(flag & NONE_FLUSH) return "None";else if(flag & LINE_FLUSH) return "Line";else if(flag & FULL_FLUSH) return "FULL";return "Unknow";
}void DebugPrint(myFILE *fp)
{printf("outbuffer: %s\n", fp->outbuffer);printf("fd: %d\n", fp->fileno);printf("pos: %d\n", fp->pos);printf("flush_mode: %s\n", toString(fp->flush_mode));
}myFILE* my_fopen(const char* pathname, const char* mode)
{int flag = 0;if (strcmp(mode, "r") == 0){flag |= O_RDONLY;}else if(strcmp(mode, "w") == 0) {flag |= (O_CREAT| O_WRONLY | O_TRUNC);}else if(strcmp(mode, "a") == 0){flag |= (O_CREAT| O_WRONLY | O_APPEND);}else {return NULL;}int fd = 0;if(flag & O_WRONLY){umask(0);fd = open(pathname, flag, 0666);}else {fd = open(pathname, flag);}if (fd < 0) return NULL;myFILE *fp = (myFILE*)malloc(sizeof(myFILE));if(fp == NULL) return NULL;fp->fileno = fd;fp->cap = SIZE;fp->pos = 0;fp->flush_mode = LINE_FLUSH;return fp;
}void my_fflush(myFILE* fp)
{if (fp->pos == 0) return;write(fp->fileno, fp->outbuffer, fp->pos);fp->pos = 0;
}int my_fwrite(myFILE *fp, const char* s, int size)
{// 1. 写入memcpy(fp->outbuffer + fp->pos, s, size);fp->pos += size;if ((fp->flush_mode & LINE_FLUSH) && fp->outbuffer[fp->pos-1] == '\n'){my_fflush(fp);}else if((fp->flush_mode & FULL_FLUSH) && fp->pos == fp->cap){my_fflush(fp);}return size;
}void my_fclose(myFILE* fp)
{my_fflush(fp);close(fp->fileno);free(fp);
}
// filetest.c
#include "mystdio.h"
#include <string.h>
#include <unistd.h>const char* filename = "./log.txt";int main()
{myFILE *fp = my_fopen(filename, "w");if (fp == NULL) return 1;int cnt = 5;char buffer[64];while (cnt){snprintf(buffer, sizeof(buffer), "helloword,hellohd,%d!!! ",cnt--); my_fwrite(fp, buffer, strlen(buffer));DebugPrint(fp);sleep(2);my_fflush(fp);} my_fclose(fp); return 0;
}
总结:
本文详细介绍了Linux系统中文件操作的基本概念和实践技巧。首先,我们讨论了文件描述符的作用和重要性,它是Linux内核用来跟踪进程打开的文件和I/O资源的关键机制。接着,我们通过对比C语言文件接口和系统调用,揭示了C语言文件操作实际上是对系统调用的封装,以实现跨平台兼容性。此外,我们探讨了文件缓冲区的概念,解释了为什么缓冲区能够提高文件操作的效率,并提供了一个简单的自定义文件操作的示例代码,展示了如何模拟C标准库中的文件操作。
通过本文的学习,读者不仅能够理解Linux文件系统的工作原理,还能够掌握文件操作的基本技巧,包括如何使用文件描述符进行高效的文件读写,如何利用缓冲区优化性能,以及如何通过系统调用来实现底层的文件操作。这些知识对于任何需要进行系统编程的开发者来说都是宝贵的,能够帮助他们编写出更高效、更可靠的文件处理程序。