个人技术分享

前言:

在计算机编程中,文件操作是基础且至关重要的技能之一。无论是在系统编程、网络编程还是数据处理,文件的读写操作都是不可或缺的。本文将深入探讨文件操作的底层原理,从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); // 设置我们对应的权限掩码
  • writereadcloselseek ,类比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语言自带缓冲区,缓冲到一定程度后再刷新到操作系统的缓冲区中。

  1. 无刷新,无缓冲
  2. 行刷新——显示器
  3. 全缓冲,全部刷新——普通文件,缓冲区被写满,才刷新。 还有两种刷新:强制刷新、进程退出的时候要自动刷新。

具体在哪里?

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库中的文件操作,这不仅加深了对文件操作原理的理解,也提高了编程实践能力。

通过本文的学习,读者应该能够对文件操作有一个全面而深入的理解,无论是在理论层面还是实践层面,都能够更加自信和高效地进行文件相关的编程工作。