详细介绍:09.【Linux系统编程】“文件“读写操作,Linux下一切皆文件!

1. 理解"文件"

1.1 什么是文件(狭义理解)

  • 文件在磁盘里。

  • 磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的。

  • 磁盘是外设(即是输出设备也是输入设备)。

  • 对磁盘上文件的所有操作的本质,都是对外设的输入和输出 简称 IO。

1.2 什么是文件(广义理解)

  • Linux 下一切皆文件(键盘、显示器、网卡、磁盘…… 这些都是抽象化的过程)(后面会讲如何去理解)

1.3 文件操作的归类认知

  • 0KB 的空文件是占用磁盘空间的(文件属性占空间)。

  • 文件是文件属性(元数据)和文件内容的集合(文件 = 属性(元数据)+ 内容)。

  • 所有的文件操作本质是文件内容操作和文件属性操作。

1.4 系统角度

  • 访问文件,需要先打开文件!谁打开文件?谁对文件进行操作?

    • 进程打开的文件,对文件的操作本质是进程对文件的操作
  • 磁盘的管理者是操作系统。

  • 文件的读写本质不是通过 C 语言 / C++ 的库函数来操作的(这些库函数只是为用户提供方便),而是通过文件相关的系统调用接口来实现的。

  • 操作系统管理文件:先描述,再组织!

2. 回顾C文件接口(内存级打开文件)

2.1 文件创建路径

#include <stdio.h>
  int main()
  {
  FILE *fp = fopen("log.txt", "w");
  if(!fp)
  {
  printf("fopen error!\n");
  }
  while(1);
  fclose(fp);
  return 0;
  }

打开的log.txt文件在哪个路径下?

  • 在程序的当前路径下,那系统怎么知道程序的当前路径在哪里呢?

  • 可以使用 ls /proc/[进程id] -l 命令查看当前正在运行进程的信息,其中:

    • cwd:指向当前进程运行目录的一个符号链接。
    • exe:指向启动当前进程的可执行文件(完整路径)的符号链接。

打开文件,本质是进程打开,所以,进程知道自己在哪里,即便文件不带路径,进程也知道。由此OS就能知道要创建的文件放在哪里。

2.2 myfile.c写文件

#include<stdio.h>
  #include<string.h>
    int main()
    {
    FILE *fp = fopen("log.txt", "w");
    if(fp == NULL)
    {
    perror("fopen");
    return 1;
    }
    const char *msg = "hello world: ";
    int cnt = 1;
    while(cnt <= 10)
    {
    char buffer[1024];
    snprintf(buffer, sizeof(buffer), "%s%d\n", msg, cnt++);
    fwrite(buffer, strlen(buffer), 1, fp);
    }
    fclose(fp);
    return 0;
    }

2.3 myfile.c读文件

// myfile.c
// gcc -o myfile myfile.c
// ./myfile filename
#include<stdio.h>
  #include<string.h>
    // cat myfile.txt
    int main(int argc, char *argv[])
    {
    // 只有一个参数,直接返回
    if(argc != 2)
    {
    printf("Usage: %s filename\n", argv[0]);
    return 1;
    }
    // 两个参数,第二个参数是要查看的文件
    FILE *fp = fopen(argv[1], "r");
    if(NULL == fp)
    {
    perror("fopen");
    return 2;
    }
    //读文件内容并打印1
    while(1)
    {
    char buffer[128];
    memset(buffer, 0, sizeof(buffer));
    // fread返回读到的元素个数,sizeof(buffer)-1中的-1是为了保存\0
    int n = fread(buffer, 1, sizeof(buffer)-1, fp);
    if(n > 0)
    {
    printf("%s", buffer);
    }
    if(feof(fp))// 判断是否到文件末尾
    break;
    }
    fclose(fp);
    return 0;
    }

2.4 输出信息到显示器的不同方式

#include<stdio.h>
  #include<string.h>
    int main()
    {
    printf("hello world\n");
    fprintf(stdout, "hello fprintf\n");
    const char *msg = "hello fwrite\n";
    fwrite(msg, strlen(msg), 1, stdout);
    return 0;
    }

2.5 stdin & stdout & stderr

• C默认会打开三个输入输出流,分别是stdin, stdout, stderr

• 仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针

#include <stdio.h>
  extern FILE *stdin;
  extern FILE *stdout;
  extern FILE *stderr;

2.6 打开文件的不同方式

文件使用方式含义(读(输入)从文件读到程序,写(输出)从程序写到文件)如果指定文件不存在
“r”(只读)为了输入(写)数据,打开一个已经存在的文本文件出错
“rb”(只读)为了输入(写)数据,打开一个二进制文件出错
“r+”(读写)为了读和写,打开一个文本文件出错
“rb+”(读写)为了读和写打开一个二进制文件出错
“w”(只写)为了输出(读)数据,清空并打开一个文本文件建立一个新的文件
“wb”(只写)为了输出(读)数据,清空并打开一个二进制文件建立一个新的文件
“w+”(读写)为了读和写,清空并打开一个文本文件建立一个新的文件
“wb+”(读写)为了读和写,清空并打开一个二进制文件建立一个新的文件
“a”(追加)打开一个文本文件,向文件尾写数据建立一个新的文件
“ab”(追加)打开一个二进制文件,向文件尾写数据建立一个新的文件
“a+”(读写)打开一个文本文件,在文件尾部进行读写建立一个新的文件
“ab+”(读写)打开一个二进制文件,在文件尾部进行读和写建立一个新的文件

3. 系统文件I/O

打开文件的方式不仅仅是fopen,ifstream等流式,语言层的方案,其实系统才是打开文件最底层的方案。不过,在学习系统文件IO之前,先要了解下如何给函数传递标志位,该方法在系统文件IO接口中会使用到

3.1 系统文件I/O入口,open函数的使用

// 函数原型
#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 - 标志位(核心参数)使用位掩码组合,主要分为三类:
      // mode - 权限模式(仅在使用 O_CREAT 时需要)
      // 返回值:
      //		成功:新打开的文件描述符
      //		失败:-1
      #include <unistd.h>
        int close(int fd);	// 关闭文件
        // 参数:fd文件描述符,即open函数的返回值
  • flags - 标志位分类
分类标志位含义
文件访问模式(必选其一)O_RDONLY只读模式
O_WRONLY只写模式
O_RDWR读写模式
文件创建和状态标志(可组合)O_CREAT文件不存在时创建
O_EXCL与O_CREAT一起使用,文件必须不存在
O_TRUNC如果文件存在且为普通文件,将其长度截断为0
O_APPEND追加模式(每次写操作前定位到文件末尾)
文件状态标志O_NONBLOCK非阻塞模式
O_SYNC同步写入(数据立即写入磁盘)
常用组合功能组合方式对应C函数模式
创建、清空并写入`int fd = open(“log.txt”, O_CREATO_WRONLY
创建、追加并写入`int fd = open(“log.txt”, O_CREATO_WRONLY
读文件int fd = open("log.txt", O_RDONLY);"r"

3.2 open传递flags标志位的方法(每个bit位各为一种功能标志,通过‘|’传递多个标志)

使用整型的32个bit位,每个bit位各为一种功能标志,举例如下,给Print函数传不同参数则执行函数中不同的功能。

#include <stdio.h>
  #define ONE_FLAG (1<<0) // 0000 0000 0000...0000 0001
  #define TWO_FLAG (1<<1) // 0000 0000 0000...0000 0010
  #define THREE_FLAG (1<<2) // 0000 0000 0000...0000 0100
  #define FOUR_FLAG (1<<3) // 0000 0000 0000...0000 1000
  void Print(int flags)
  {
  if (flags & ONE_FLAG)   printf("One!\n");
  if (flags & TWO_FLAG)   printf("Two\n");
  if (flags & THREE_FLAG) printf("Three\n");
  if (flags & FOUR_FLAG)  printf("Four\n");
  }
  int main()
  {
  Print(ONE_FLAG);    printf("\n");
  Print(ONE_FLAG | TWO_FLAG);    printf("\n");
  Print(ONE_FLAG | TWO_FLAG | THREE_FLAG);    printf("\n");
  Print(ONE_FLAG | TWO_FLAG | THREE_FLAG | FOUR_FLAG);    printf("\n");
  Print(ONE_FLAG | FOUR_FLAG);    printf("\n");
  return 0;
  }
One!
One!
Two
One!
Two
Three
One!
Two
Three
Four
One!
Four

3.3 C调用Linux系统函数write写文件

操作文件,除了上小节的C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问, 先来直接以系统代码的形式,实现和上面功能一模一样的代码。

#include <stdio.h>
  #include <string.h>
    #include <sys/types.h>
      #include <sys/stat.h>
        #include <fcntl.h>
          #include <unistd.h>
            int main()
            {
            umask(0);	// 修改文件权限掩码
            int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
            //int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
            //int fd = open("log.txt", O_RDONLY);
            if(fd < 0)
            {
            perror("open");
            return 1;
            }
            printf("fd: %d\n", fd);
            int cnt = 5;
            const char *msg = "hello world\n";
            while(cnt)
            {
            write(fd, msg, strlen(msg)); //fd: 后面讲, msg:缓冲区首地址, len: 本次读取,期望写入多少个字节的数据。 返回值:实际写了多少字节数据
            cnt--;
            }
            close(fd);
            return 0;
            }

3.4 C调用Linux系统函数read读文件

#include <stdio.h>
  #include <sys/types.h>
    #include <sys/stat.h>
      #include <fcntl.h>
        #include <unistd.h>
          int main()
          {
          umask(0);
          // int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
          // int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
          int fd = open("log.txt", O_RDONLY);
          if(fd < 0)
          {
          perror("open");
          return 1;
          }
          printf("fd: %d\n", fd);
          while(1)
          {
          char buffer[64];
          int n = read(fd, buffer, sizeof(buffer)-1);
          if(n > 0)
          {
          buffer[n] = 0;
          printf("%s", buffer);
          }
          else if(n == 0)
          {
          break;
          }
          }
          close(fd);
          return 0;
          }

3.5 open函数理解(系统调用&库函数)

在认识返回值之前,先来认识一下两个概念: 系统调用和库函数

  • 上面的fopenfclosefreadfwrite 都是C标准库当中的函数,我们称之为库函数(libc)。

  • openclosereadwritelseek 都属于系统提供的接口,称之为系统调用接口

回忆一下我们讲操作系统概念时,画的一张图

系统调用接口和库函数的关系,一目了然。所以,可以认为, f开头 系列的函数,都是对系统调用的封装,方便二次开发。
在这里插入图片描述

3.6 文件描述符fd

• 通过对open函数的学习,我们知道了文件描述符就是一个小整数

3.6.1 特殊文件描述符0 & 1 & 2

• Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0(stdin), 标准输出1(stdout), 标准错误2(stderr).

• 0,1,2对应的物理设备一般是:键盘,显示器,显示器

所以输入输出还可以采用如下方式:

#include <stdio.h>
  #include <sys/types.h>
    #include <sys/stat.h>
      #include <fcntl.h>
        #include <string.h>
          int main()
          {
          char buf[1024];
          ssize_t s = read(0, buf, sizeof(buf));	// 读键盘
          if(s > 0){
          buf[s] = 0;
          write(1, buf, strlen(buf));	// 输入到显示器
          write(2, buf, strlen(buf));	// 输入到显示器
          }
          return 0;
          }

在这里插入图片描述
而现在知道,文件描述符就是从0开始的小整数。

当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。

而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针!

  • 所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。

(其他问题见3.7:既然有fd文件描述符,那么file类型的文件指针中为什么还要存在list_head结构体?)

3.6.2 文件描述符的分配规则

直接看代码:

#include <stdio.h>
  #include <sys/types.h>
    #include <sys/stat.h>
      #include <fcntl.h>
        int main()
        {
        int fd = open("myfile", O_RDONLY);
        if(fd < 0){
        perror("open");
        return 1;
        }
        printf("fd: %d\n", fd);
        close(fd);
        return 0;
        }
        // 输出发现是 fd: 3
// 关闭0或者2,在看
#include <stdio.h>
  #include <sys/types.h>
    #include <sys/stat.h>
      #include <fcntl.h>
        int main()
        {
        close(0);
        //close(2);
        int fd = open("myfile", O_RDONLY);
        if(fd < 0){
        perror("open");
        return 1;
        }
        printf("fd: %d\n", fd);
        close(fd);
        return 0;
        }
        // 结果是: fd: 0 或者fd 2
  • 结论(文件描述符的分配规则):在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。(当然也可以使用close关闭三个标准文件输入/输出,则再次新建文件则同样遵循此规则)

举例:

#include <stdio.h>
  #include <sys/types.h>
    #include <sys/stat.h>
      #include <fcntl.h>
        #include <unistd.h>
          #include <stdlib.h>
            int main()
            {
            umask(0);
            int fd1 = open("log1.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
            int fd2 = open("log2.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
            int fd3 = open("log3.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
            int fd4 = open("log4.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
            if(fd1 < 0) exit(1);
            if(fd2 < 0) exit(1);
            if(fd3 < 0) exit(1);
            if(fd4 < 0) exit(1);
            // FILE 结构体确实封装了文件描述符 fd,成员为int _fileno; // ✅ 封装的文件描述符fd
            printf("stdin: %d\n", stdin->_fileno);
            printf("stdout: %d\n",stdout->_fileno);
            printf("stderr: %d\n", stderr->_fileno);
            printf("fd1: %d\n", fd1);
            printf("fd2: %d\n", fd2);
            printf("fd3: %d\n", fd3);
            printf("fd4: %d\n", fd4);
            close(fd1);
            close(fd2);
            close(fd3);
            close(fd4);
            return 0;
            }
stdin: 0
stdout: 1
stderr: 2
fd1: 3
fd2: 4
fd3: 5
fd4: 6

3.7 文件管理(task_struct→files_struct管理文件)

3.7.1 了解 struct file数组 - 存放文件指针

  • 文件是通过struct files_struct 结构体来管理的,而struct files_struct结构体编程又存放在struct tast_struct进程控制块中。在Linux内核的task_struct中。

  • struct files_struct 结构体中有一个struct file __rcu *指针数组,下标就是文件描述符fd,内容是文件指针

struct task_struct {
// ...
struct files_struct *files;  // 文件管理结构
// ...
};
struct files_struct {
struct file __rcu * fd_array[NR_OPEN_DEFAULT];  // ✅ 文件指针数组
//// 数组的每个元素都是:struct file __rcu *   // 带RCU注解的file结构体指针
// ...
};

3.7.2 struct file中为何要存在struct list_head?

  • struct files_struct 结构体中可以通过下标fd来找到指定的文件指针struct file,那么为什么struct file结构体中还有struct list_head指针将各个文件链接起来呢?

fd_array[] - 进程视角

  • 目的:让单个进程快速访问自己打开的文件
  • 用法read(fd, ...)fd_array[fd] → 找到文件
  • 技术:数组索引,O(1)时间复杂度
  • 场景:系统调用 read(fd), write(fd)

各种list_head - 系统视角

  • 目的:让内核管理系统中的所有文件关系
  • 用法:文件系统维护、inode引用管理、资源清理等
  • 技术:链表遍历,维护系统关系
  • 场景:文件系统卸载、inode引用管理、资源清理

3.8 重定向

3.8.1 重定向概念

​ 那如果关闭1呢?看代码:

#include <stdio.h>
  #include <sys/types.h>
    #include <sys/stat.h>
      #include <fcntl.h>
        #include <stdlib.h>
          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);
          }
  • 此时,我们发现,本来应该输出到显示器上的内容,输出到了文件myfile当中,其中,fd=1。这种现象叫做输出重定向
  • 命令行指令中,常见的重定向有: > , >> , <
    在这里插入图片描述

3.8.2 使用dup2系统调用实现重定向

函数原型如下:

#include <unistd.h>
  int dup2(int oldfd, int newfd);
  • dup2的功能:就是修改files_struct结构体中保存文件指针的 指针数组 struct file的内容,将oldfd下标中的内容拷贝newfd下标中。

示例:

#include<stdio.h>
  #include<sys/types.h>
    #include<sys/stat.h>
      #include<fcntl.h>
        #include<unistd.h>
          #include<string.h>
            #include<stdlib.h>
              int main()
              {
              int fd = open("log.txt",O_CREAT | O_WRONLY | O_TRUNC, 0666);
              if(fd < 0) exit(1);
              dup2(fd, 1);
              printf("fd:%d\n", fd);
              printf("hello printf\n");
              fprintf(stdout, "hello fprintf\n");
              const char *msg = "hello world\n";
              write(fd, msg, strlen(msg));
              return 0;
              }
$ cat log.txt
hello world
fd:3
hello printf
hello fprintf
  • printf是C库当中的IO函数,一般往 stdout 中输出,但是stdout底层访问文件的时候,找的还是fd:1,但此时,fd:1下标所表示内容,已经变成了log.txt的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。那追加和输入重定向同理。

3.9 文本写入 VS 二进制写入(系统层面只有二进制方式)

  • 文本写入即字符写入

  • 都是语言层面,系统层面只有二进制方式,其他都转成二进制方式写入。

3.10 关于fopen对open的封装(可跨平台移植原因)

  • C库封装系统调用,条件编译实现跨平台

    • 在C语言库中,fopen封装了Linux系统对文件操作的open函数,所以可以使用fopen来完成Linux系统下对文件的操作。
    • 同样的,C语言库中也封装了windows系统中对文件操作的函数,上层也封装成fopen函数。在其他系统中同样,他们使用条件编译来区分不同系统。由此就可以实现C语言文件在不同系统中的移植,即上层都是使用fopen对文件操作,底层由C语言库分别适应不同的操作系统。
  • 目的

    • 目的是提高可移植性和开发效率
    • 技术需求、市场需求、标准推动的共同结果。而是技术进步和经济效益的平衡
posted on 2025-09-29 12:46  ljbguanli  阅读(24)  评论(0)    收藏  举报