目录

一、IO概述与文件本质
1.1 什么是IO
1.2 文件的双重理解
1.3 Linux的"一切皆文件"哲学
二、C标准库文件IO接口
2.1 核心接口回顾
2.2 标准输入输出流
2.3 文件打开模式详解
三、系统级文件IO接口
3.1 核心系统调用接口
3.2 库函数与系统调用的关系
3.3 接口使用示例对比
四、文件描述符(fd)深度解析
4.1 文件描述符的本质
4.2 默认打开的三个fd
4.3 fd的分配规则
4.4 内核中的文件管理模型
五、重定向机制与dup2函数
5.1 重定向的本质
5.2 dup2系统调用详解
5.3 迷你Shell添加重定向功能
六、"一切皆文件"的内核实现
6.1 file结构体
6.2 file_operations结构体
6.3 设备与文件的适配逻辑
七、缓冲区机制深度剖析
7.1 缓冲区的作用
7.2 三种缓冲类型
7.3 缓冲区的验证与刷新
7.4 FILE结构体与用户级缓冲区
八、总结


一、IO概述与文件本质

IO(Input/Output)即输入输出,是进程与外部设备(磁盘、键盘、显示器等)进行数据交互的过程。在Linux系统中,IO操作的核心载体是文件,理解文件的本质是掌握IO的关键。

1.1 什么是IO

从本质上讲,IO是数据在"内存与外部设备"之间的传输:

  • 输入(Input):数据从外部设备传入内存(如键盘输入、磁盘读数据);
  • 输出(Output):数据从内存传入外部设备(如显示器显示、磁盘写数据)。
    磁盘、键盘、显示器、网卡等都是IO设备,对这些设备的操作都属于IO操作。

1.2 文件的双重理解

文件并非仅指磁盘上的普通文件,其定义包含两个层面:

  • 狭义理解:磁盘上的永久性存储实体,是操作系统管理磁盘数据的基本单位;
  • 广义理解:文件是"属性 + 内容"的集合。无论普通文件、设备、管道还是套接字,本质上都是文件,都具备属性(如权限、大小、创建时间)和内容(或数据传输能力)。

注意:即使是0KB的空文件,也会占用磁盘空间存储其属性信息(如inode节点)。

1.3 Linux的"一切皆文件"哲学

Linux系统将所有IO设备都抽象为文件,带来了两大核心优势:

  1. 统一接口:开发者只需掌握一套IO接口(open、read、write等),即可操作所有设备;
  2. 简化设计:操作系统通过统一的文件管理模型,简化对不同设备的适配与管理。

例如,键盘(输入设备)、显示器(输出设备)、磁盘(存储设备)、网卡(网络设备)都可以通过文件描述符和统一的IO函数进行操作。

二、C标准库文件IO接口

C标准库提供了封装后的文件IO接口,屏蔽了底层系统差异,是用户态开发中最常用的IO方式。

2.1 核心接口回顾

C标准库的文件IO接口围绕FILE结构体展开,核心接口包括:

  • fopen:打开文件,返回FILE*指针(文件句柄);
  • fread:从文件读取数据到内存;
  • fwrite:将内存数据写入文件;
  • fclose:关闭文件,释放资源。
示例1:文件写入
#include <stdio.h>
  #include <string.h>
    int main() {
    // 打开文件,若不存在则创建,只写模式
    FILE* fp = fopen("myfile", "w");
    if (!fp) { // 打开失败返回NULL
    printf("fopen error!\n");
    return 1;
    }
    const char* msg = "hello bit!\n";
    int count = 5;
    // 每次写入strlen(msg)字节,共写1次
    while (count--) {
    fwrite(msg, strlen(msg), 1, fp);
    }
    fclose(fp); // 关闭文件,必须调用
    return 0;
    }

在这里插入图片描述

示例2:文件读取与简易cat命令
#include <stdio.h>
  #include <string.h>
    int main(int argc, char* argv[]) {
    if (argc != 2) {
    printf("用法:%s 文件名\n", argv[0]);
    return 1;
    }
    FILE* fp = fopen(argv[1], "r");
    if (!fp) {
    printf("fopen error!\n");
    return 2;
    }
    char buf[1024];
    while (1) {
    // 每次最多读取sizeof(buf)字节
    size_t s = fread(buf, 1, sizeof(buf), fp);
    if (s > 0) {
    buf[s] = '\0'; // 手动添加字符串结束符
    printf("%s", buf);
    }
    if (feof(fp)) { // 检测到文件结束
    break;
    }
    }
    fclose(fp);
    return 0;
    }

在这里插入图片描述

2.2 标准输入输出流

C标准库默认打开三个文件流,供进程直接使用:

  • stdin:标准输入流(对应键盘),FILE*类型;
  • stdout:标准输出流(对应显示器),FILE*类型;
  • stderr:标准错误流(对应显示器),FILE*类型。

这三个流无需手动fopen,可直接用于IO操作:

在这里插入图片描述

2.3 文件打开模式详解

fopen的第二个参数指定文件打开模式,核心模式如下:

模式功能说明
r只读打开,文件不存在则失败,流定位到文件开头
r+读写打开,文件不存在则失败,流定位到文件开头
w只写打开,文件不存在则创建,存在则清空,流定位到文件开头
w+读写打开,文件不存在则创建,存在则清空,流定位到文件开头
a追加写打开,文件不存在则创建,流定位到文件末尾
a+读写+追加打开,文件不存在则创建,读定位到开头,写定位到末尾

关键注意点:a/a+模式下,所有写操作都会追加到文件末尾,不受fseek调整的读位置影响。

三、系统级文件IO接口

C标准库的IO接口是对系统调用的封装,操作系统提供的底层IO接口才是文件操作的真正实现。

3.1 核心系统调用接口

系统级IO接口包含四个核心函数,需包含<sys/types.h><sys/stat.h><fcntl.h><unistd.h>头文件:

  • open:打开或创建文件,返回文件描述符;
  • read:从文件描述符读取数据;
  • write:向文件描述符写入数据;
  • close:关闭文件描述符。
open函数原型
// 打开已存在文件
int open(const char *pathname, int flags);
// 创建并打开文件(需指定权限)
int open(const char *pathname, int flags, mode_t mode);
  • pathname:文件路径(绝对路径或相对路径);
  • flags:打开标志,需至少指定一个访问模式(O_RDONLY/O_WRONLY/O_RDWR),可搭配其他标志(O_CREAT/O_APPEND/O_TRUNC等);
  • mode:文件创建时的权限(如0644),仅O_CREAT存在时有效;
  • 返回值:成功返回文件描述符(非负整数),失败返回-1

3.2 库函数与系统调用的关系

  • 库函数(fopen/fread等)是C标准库提供的用户态接口,底层通过调用系统调用(open/read等)实现;
  • 库函数在系统调用之上添加了用户级缓冲区,减少系统调用次数,提升IO效率;
  • 系统调用是操作系统提供的内核态接口,是IO操作的底层实现。
    在这里插入图片描述

3.3 接口使用示例对比

用系统调用实现与C库函数相同的文件写入功能:

#include <stdio.h>
  #include <sys/types.h>
    #include <sys/stat.h>
      #include <fcntl.h>
        #include <unistd.h>
          #include <string.h>
            int main() {
            umask(0); // 清除权限掩码,确保创建文件权限为0644
            // 打开文件:只写+创建+清空,权限0644
            int fd = open("myfile", O_WRONLY | O_CREAT | O_TRUNC, 0644);
            if (fd < 0) { // 打开失败返回-1
            perror("open"); // 打印错误信息
            return 1;
            }
            const char* msg = "hello bit!\n";
            int len = strlen(msg);
            int count = 5;
            while (count--) {
            // 向fd对应的文件写入len字节数据
            write(fd, msg, len);
            }
            close(fd); // 关闭文件描述符
            return 0;
            }

在这里插入图片描述

系统调用与库函数的核心差异:

  • 系统调用返回文件描述符(int类型),库函数返回FILE*指针;
  • 系统调用无缓冲区,每次调用直接陷入内核态;
  • 库函数有用户级缓冲区,减少内核态切换开销。

四、文件描述符(fd)深度解析

文件描述符(fd)是系统调用接口的核心,是进程与文件关联的桥梁。

4.1 文件描述符的本质

文件描述符是一个非负整数,本质是进程打开文件表(fd_array)的下标。进程通过这个下标,可找到对应的内核文件对象(file结构体)。

4.2 默认打开的三个fd

Linux进程启动时,默认打开三个文件描述符:

  • 0:标准输入(STDIN_FILENO),对应键盘;
  • 1:标准输出(STDOUT_FILENO),对应显示器;
  • 2:标准错误(STDERR_FILENO),对应显示器。

验证示例:直接使用fd进行IO操作
在这里插入图片描述

4.3 fd的分配规则

文件描述符的分配遵循"最小未使用下标"原则:当进程打开新文件时,内核会在fd_array中找到当前未使用的最小下标,作为新的文件描述符。

验证示例:
在这里插入图片描述

4.4 内核中的文件管理模型

内核通过三层数据结构管理进程与文件的关联:

  1. task_struct(进程控制块):每个进程有一个files指针,指向files_struct
  2. files_struct:进程的打开文件表,包含fd_array数组(存储file*指针);
  3. file(文件对象):存储文件的属性(权限、偏移量)、inode指针、文件操作函数指针(file_operations)。
    在这里插入图片描述

五、重定向机制与dup2函数

重定向(如><>>)是Linux中常用的IO操作,其本质是修改fd_array的指针指向。

5.1 重定向的本质

以输出重定向> file为例,核心流程:

  1. 关闭fd=1(标准输出);
  2. 打开文件file,由于fd=1已释放,新文件的fd分配为1
  3. 此后,所有向fd=1的写入操作,都会指向file而非显示器。

验证示例:手动实现输出重定向

#include <stdio.h>
  #include <sys/types.h>
    #include <sys/stat.h>
      #include <fcntl.h>
        #include <unistd.h>
          int main() {
          close(1); // 关闭标准输出fd=1
          // 打开文件,fd分配为1
          int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
          if (fd < 0) {
          perror("open");
          return 1;
          }
          printf("hello redirect\n"); // 本应输出到显示器,实际写入log.txt
          fflush(stdout); // 强制刷新缓冲区
          close(fd);
          return 0;
          }

在这里插入图片描述

5.2 dup2系统调用详解

dup2函数专门用于实现重定向,功能是"将oldfd的文件指针复制到newfd",若newfd已打开则先关闭。

函数原型:

#include <unistd.h>
  int dup2(int oldfd, int newfd);

示例:用dup2实现输出重定向

#include <stdio.h>
  #include <unistd.h>
    #include <fcntl.h>
      int main() {
      // 打开文件获取oldfd
      int oldfd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
      if (oldfd < 0) {
      perror("open");
      return 1;
      }
      // 将oldfd的指向复制到newfd=1,实现输出重定向
      dup2(oldfd, 1);
      printf("hello dup2 redirect\n"); // 写入log.txt
      fflush(stdout);
      close(oldfd);
      return 0;
      }

在这里插入图片描述

5.3 迷你Shell添加重定向功能

结合之前实现的迷你Shell,添加重定向(>>><)支持:

核心步骤:
  1. 解析命令行中的重定向符号(<>>>);
  2. 记录重定向类型和目标文件名;
  3. 子进程中根据重定向类型打开文件,调用dup2完成重定向;
  4. 执行程序替换(execvpe),重定向不受程序替换影响。
关键代码实现:
#include <stdio.h>
  #include <stdlib.h>
    #include <string.h>
      #include <unistd.h>
        #include <sys/wait.h>
          #include <sys/stat.h>
            #include <fcntl.h>
              #include <ctype.h>
                extern char **environ;
                // 重定向类型
                #define NONE_REDIR 0
                #define INPUT_REDIR 1
                #define OUTPUT_REDIR 2
                #define APPEND_REDIR 3
                // 全局变量
                char *g_argv[64];
                int g_argc = 0;
                int g_redir = NONE_REDIR;
                char g_redir_file[256] = {0};
                // 工具函数:去除字符串前后空格
                void trim_space(char *str) {
                if (!str) return;
                char *start = str;
                while (isspace((unsigned char)*start)) start++;
                char *end = str + strlen(str) - 1;
                while (end >= start && isspace((unsigned char)*end)) end--;
                memmove(str, start, end - start + 1);
                str[end - start + 1] = '\0';
                }
                // 工具函数:去除字符串前后引号(解决文件名/参数引号问题)
                void remove_quotes(char *str) {
                if (!str) return;
                int len = strlen(str);
                if (len >= 2 && str[0] == '"' && str[len-1] == '"') {
                str[len-1] = '\0';
                memmove(str, str+1, len-1); // 去掉前后引号
                }
                }
                // 解析重定向符号
                void parse_redir(char *cmd) {
                int len = strlen(cmd);
                for (int i = len-1; i >= 0; i--) {
                if (cmd[i] == '>') {
                if (i > 0 && cmd[i-1] == '>') { // >>
                g_redir = APPEND_REDIR;
                cmd[i-1] = '\0';
                strcpy(g_redir_file, cmd + i + 1);
                } else { // >
                g_redir = OUTPUT_REDIR;
                cmd[i] = '\0';
                strcpy(g_redir_file, cmd + i + 1);
                }
                trim_space(g_redir_file);
                remove_quotes(g_redir_file);
                trim_space(cmd);
                break;
                } else if (cmd[i] == '<') { // <
                g_redir = INPUT_REDIR;
                cmd[i] = '\0';
                strcpy(g_redir_file, cmd + i + 1);
                trim_space(g_redir_file);
                remove_quotes(g_redir_file);
                trim_space(cmd);
                break;
                }
                }
                }
                // 解析命令参数(处理引号)
                void parse_cmd(char *cmd) {
                g_argc = 0;
                char *tok = strtok(cmd, " ");
                while (tok && g_argc < 63) {
                remove_quotes(tok);
                g_argv[g_argc++] = tok;
                tok = strtok(NULL, " ");
                }
                g_argv[g_argc] = NULL;
                }
                // 执行重定向
                void do_redir() {
                int fd;
                switch (g_redir) {
                case INPUT_REDIR:
                fd = open(g_redir_file, O_RDONLY);
                if (fd < 0) { perror("打开文件失败"); exit(1); }
                dup2(fd, 0); close(fd);
                break;
                case OUTPUT_REDIR:
                fd = open(g_redir_file, O_CREAT|O_WRONLY|O_TRUNC, 0666);
                if (fd < 0) { perror("打开文件失败"); exit(1); }
                dup2(fd, 1); close(fd);
                break;
                case APPEND_REDIR:
                fd = open(g_redir_file, O_CREAT|O_WRONLY|O_APPEND, 0666);
                if (fd < 0) { perror("打开文件失败"); exit(1); }
                dup2(fd, 1); close(fd);
                break;
                }
                }
                // 执行命令
                void exec_cmd() {
                pid_t pid = fork();
                if (pid < 0) { perror("fork失败"); return; }
                if (pid == 0) {
                do_redir();
                execvpe(g_argv[0], g_argv, environ);
                perror("命令执行失败"); exit(1);
                } else {
                waitpid(pid, NULL, 0);
                }
                }
                int main() {
                char cmd[1024];
                while (1) {
                memset(cmd, 0, sizeof(cmd));
                memset(g_argv, 0, sizeof(g_argv));
                g_argc = 0;
                g_redir = NONE_REDIR;
                memset(g_redir_file, 0, sizeof(g_redir_file));
                printf("[minishell]$ ");
                fflush(stdout);
                if (!fgets(cmd, sizeof(cmd), stdin)) {
                printf("\n退出\n"); break;
                }
                cmd[strcspn(cmd, "\n")] = '\0';
                if (strlen(cmd) == 0) continue;
                parse_redir(cmd);
                parse_cmd(cmd);
                exec_cmd();
                }
                return 0;
                }

测试样例:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

六、"一切皆文件"的内核实现

Linux"一切皆文件"的哲学,本质是通过统一的内核数据结构,将不同设备抽象为文件,实现接口统一。

6.1 file结构体

每个打开的文件(包括设备)在内核中都对应一个file结构体,存储文件的核心信息:

  • f_inode:指向文件的inode节点(存储文件属性);
  • f_pos:文件当前读写位置;
  • f_flags:文件打开标志(如O_RDONLY);
  • f_op:指向file_operations结构体(存储文件操作函数指针)。

6.2 file_operations结构体

file_operations是内核实现设备抽象的关键,其成员是一组函数指针,对应文件的各种操作(readwrite等)。不同设备(磁盘、键盘、显示器)会实现自己的操作函数,通过f_op指针注册到file结构体中。

核心结构示例:

struct file_operations {
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
// 其他操作函数...
};

6.3 设备与文件的适配逻辑

无论是什么设备,只要实现了file_operations中的对应函数,就能通过统一的IO接口(readwrite)操作:
在这里插入图片描述

这种设计让开发者无需关心设备底层实现,只需使用统一的IO接口,实现了"设备无关性"。

七、缓冲区机制深度剖析

缓冲区是提升IO效率的核心,分为用户级缓冲区(C标准库提供)和内核级缓冲区(操作系统提供),我们重点讨论用户级缓冲区。

7.1 缓冲区的作用

缓冲区是内存中的一块临时存储区域,核心作用是减少系统调用次数:

  • 写操作:先将数据写入缓冲区,缓冲区满或主动刷新时,再调用write写入内核;
  • 读操作:先从内核读取大量数据到缓冲区,用户后续读取直接从缓冲区获取。

减少系统调用意味着减少内核态与用户态的切换开销,大幅提升IO效率。

7.2 三种缓冲类型

C标准库提供三种缓冲类型,由FILE结构体的_flags字段控制:

  1. 全缓冲:缓冲区满时才刷新(如磁盘文件);
  2. 行缓冲:遇到换行符\n或缓冲区满时刷新(如stdout,默认行缓冲);
  3. 无缓冲:无缓冲区,数据直接写入内核(如stderr)。

7.3 缓冲区的验证与刷新

验证1:行缓冲与全缓冲的差异
#include <stdio.h>
  #include <unistd.h>
    int main() {
    printf("hello printf"); // 行缓冲,无\n,不刷新
    fprintf(stdout, "hello fprintf"); // 行缓冲,无\n,不刷新
    write(1, "hello write\n", 12); // 系统调用,无缓冲区,直接输出
    sleep(3); // 休眠3秒,观察输出顺序
    return 0;
    }

在这里插入图片描述

验证2:fork对缓冲区的影响
#include <stdio.h>
  #include <string.h>
    #include <unistd.h>
      #include <sys/wait.h>
        int main() {
        const char *msg0 = "hello printf\n";
        const char *msg1 = "hello fwrite\n";
        const char *msg2 = "hello write\n";
        printf("%s", msg0); // 行缓冲,有\n,直接刷新
        fwrite(msg1, strlen(msg1), 1, stdout); // 行缓冲,有\n,直接刷新
        write(1, msg2, strlen(msg2)); // 系统调用,无缓冲
        fork(); // 创建子进程
        return 0;
        }
  • 直接运行:三种输出各一次;
    在这里插入图片描述
  • 重定向到文件(./test > file):printffwrite输出两次,write输出一次。
    在这里插入图片描述
    原因:重定向后stdout变为全缓冲,fork时父进程缓冲区数据未刷新,子进程通过写时拷贝获得相同缓冲区,进程退出时父子进程各自刷新,导致重复输出;write无缓冲区,直接写入内核,仅输出一次。
缓冲区的刷新时机
  1. 缓冲区满时;
  2. 主动调用fflush函数;
  3. 进程正常退出时;
  4. 行缓冲遇到\n时。

7.4 FILE结构体与用户级缓冲区

C标准库的FILE结构体内部封装了用户级缓冲区和文件描述符:

struct _IO_FILE {
char* _IO_read_ptr;   // 读缓冲区当前指针
char* _IO_read_end;   // 读缓冲区结束指针
char* _IO_write_ptr;  // 写缓冲区当前指针
char* _IO_write_end;  // 写缓冲区结束指针
char* _IO_buf_base;   // 缓冲区起始地址
char* _IO_buf_end;    // 缓冲区结束地址
int _fileno;          // 封装的文件描述符
// 其他字段...
};
  • _fileno存储系统调用返回的文件描述符;
  • _IO_write_ptr指向当前写入位置,_IO_write_end指向缓冲区末尾;
  • 调用fwrite时,数据先拷贝到_IO_buf_base_IO_buf_end之间的缓冲区,满足刷新条件时调用write写入内核。

八、总结

  1. 文件本质:文件是"属性+内容"的集合,Linux下一切皆文件,通过file_operations实现设备抽象;
  2. IO接口:C标准库接口(fopen/fread)封装系统调用(open/read),添加用户级缓冲区;
  3. 文件描述符:fd是fd_array的下标,默认0/1/2对应标准输入/输出/错误,分配规则为"最小未使用下标";
  4. 重定向:本质是修改fd_array的指针指向,dup2函数是实现核心;
  5. 缓冲区:C标准库提供三种缓冲类型,核心作用是减少系统调用,刷新时机包括缓冲区满、fflush、进程退出、行缓冲遇\n