文章目录
- 前言:
- 1. 铺垫
- 2. 重新使用C文件接口:对比一下重定向
- 2.1. 什么叫当前路径?
- 2.2. 写入文件
- 2.3. 读文件
- 2.4. 程序默认打开的文件流
- 2.5. 输出
- 2.6. 输入
- 3. 系统调用提供的文件接口
- 3.1. open 打开文件
- 3.2. open函数返回值
- 4. 缓冲区问题
- 总结:
前言:
在计算机编程中,文件操作是基础且至关重要的技能之一。无论是在系统编程、网络编程还是数据处理,文件的读写操作都是不可或缺的。本文将深入探讨文件操作的底层原理,从C语言层面的文件接口到操作系统层面的系统调用,再到缓冲区机制的实现,逐步揭示文件操作的全貌。通过对比C语言的文件接口和系统调用,以及对缓冲区问题的深入分析,本文旨在帮助读者建立一个清晰的文件操作概念框架,从而在实际开发中更加得心应手。
1. 铺垫
a. 文件 = 内容 + 属性
b. 访问文件之前,都得先打开。修改文件,都是通过指向代码的方式完成修改,文件必须加载到内存中
c. 谁打开文件?进程在打开文件
d. 一个进程可以打开多少个文件呢?可以打开多个文件
- 一定时间内,系统中存在多个进程,也可能同时存在更多的被打开文件,OS要不要管理多个被进程打开的文件呢?肯定的
- 如何管理呢?先组织,再描述!
e. 进程和文件的关系,struct task_struct 和 struct XXX?
a~e 被打开文件都是:内存文件
f. 系统中是不是所有的文件都被进程打开了?不是!没有被打开文件?就在磁盘中
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 //对比追加重定向
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);
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;
}
总结:
本文首先介绍了文件操作的基本概念,包括文件的定义、访问文件前的打开过程、以及进程与文件的关系。接着,通过C语言的文件接口示例,详细讨论了文件的打开、写入、读取以及默认文件流的使用。文章进一步探讨了系统调用层面的文件接口,特别是open函数的使用方法和返回值,揭示了C语言文件接口背后封装的系统调用机制。
在缓冲区问题部分,文章解释了缓冲区的作用、类型以及与内核缓冲区的关系,并提供了一个简单的缓冲区管理模拟实现。通过这个模拟实现,读者可以更直观地理解缓冲区在文件操作中的重要性和工作机制。
最后,文章通过一个实际的文件测试程序,展示了如何使用自定义的文件操作函数来模拟标准C库中的文件操作,这不仅加深了对文件操作原理的理解,也提高了编程实践能力。
通过本文的学习,读者应该能够对文件操作有一个全面而深入的理解,无论是在理论层面还是实践层面,都能够更加自信和高效地进行文件相关的编程工作。