【Linux】基础IO

1. 理解"文件"

1.1 狭义理解

  • 文件在磁盘里
  • 磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的
  • 磁盘是外设
  • 磁盘上的文件本质是对文件的所有操作,都是对外设的输入和输出,简称IO

1.2 广义理解

  • Linux下一切皆文件
  • 对于 0KB 的空文件是占用磁盘空间的(文件 = 属性 + 内容)

1.3 系统角度

  • 对文件的操作本质是进程对文件的操作
  • 磁盘的管理者是操作系统
  • 文件的读写本质不是通过 C语言/ C++ 的库函数来操作的,而是通过文件相关的系统调用接口来实现的

2. C文件接口

2.1 打开文件

cs 复制代码
#include <stdio.h>
#include <errno.h>  // 包含errno定义(perror依赖)

int main()
{
    FILE* fd = fopen("log.txt", "w"); 
    if (!fd) {
        perror("fopen failed");  // 传入字符串作为提示
        return 1; 
    }
    fclose(fd);
    return 0;
}

2.2 写入文件

write 和 fwrite 函数

cs 复制代码
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
write参数说明
  • fd:文件描述符(非负整数),标识打开的文件 / 设备 /socket。常见值:
    • 0:标准输入(STDIN_FILENO
    • 1:标准输出(STDOUT_FILENO
    • 2:标准错误(STDERR_FILENO
    • 其他值由 open() 函数返回(打开文件后获得)
  • buf:指向要写入数据的缓冲区(const 修饰,不可修改
  • count:请求写入的字节数size_t 是无符号整数)
返回值
  • 成功:返回 实际写入的字节数 (可能小于 count,如磁盘满、网络拥堵)
  • 失败:返回 -1,并设置 errno(错误码,需用 <errno.h> 解析)
cs 复制代码
#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
fwrite参数说明
  • ptr指向要写入文件的数据的起始地址(即数据源的指针)
  • size单个数据块的字节数 (如 sizeof(int)sizeof(struct Student))。
  • nmemb:请求写入的数据块个数
  • stream:文件指针(由 fopen() 返回),常见值:
    • stdout:标准输出
    • stderr:标准错误
    • 其他值由 fopen() 打开文件获得(如 FILE *fp = fopen("test.txt", "wb"))。
返回值
  • 成功:返回 实际写入的完整数据块数 (而非字节数)。
    • 总写入字节数 = 实际块数 × size
  • 失败 / 到达文件尾:返回 小于 nmemb 的数 (需通过 ferror()feof() 判断原因
cs 复制代码
#include <stdio.h>
#include <string.h>

int main()
{
    FILE* fd = fopen("log.txt", "w");
    if (!fd) {
        perror("fopen");
        return 1;
    }

    const char *msg = "abcd"; // 要写入的有效字符串
    int cnt = 5;
    while (cnt--) {
        fwrite(msg, strlen(msg), 1, fd);
    }

    fclose(fd);
    return 0;
}

2.3 读取文件

fread 和 read 函数

cs 复制代码
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
  • read的核心:从一个「数据来源」读取字节流,存入程序的内存缓冲区
  • read参数说明:
    • fd:文件描述符(非负整数,如 open 返回的 fd,标准输入是 0,标准输出是 1,标准错误是 2);
    • buf:接收数据的缓冲区指针;
    • count要读取的最大字节数
  • 返回值:成功:返回实际读取的字节数 (可能小于 count,如文件尾、管道无足够数据);失败:返回 -1(errno 存储错误码,如 EBADF 表示 fd 无效);0:表示到达文件尾(EOF
cs 复制代码
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
  • fread参数说明:
    • ptr:接收数据的缓冲区指针;
    • size每个元素的字节数 (如 sizeof(int));
    • nmemb:要读取的元素个数
    • stream:文件流指针(如 fopen 返回的 FILE*)。
  • 返回值:成功读取的元素个数 (不是字节数!);若到达文件尾,返回值可能小于 nmemb;若出错,返回值不确定,需用 ferror(stream) 检查
cs 复制代码
#include<stdio.h>
#include<unistd.h>
#include <string.h>
   int main()
   {
     FILE* fd = fopen("log.txt","r");
     if(!fd){
      perror("fopen");
     }
     char buff[1024];
     const char *msg ="abcd";
  
    while(1)
    {
        ssize_t s= fread(buff,1,strlen(msg),fd);
        if(s>0)
        {
           buff[s]=0;
           printf("%s",buff);
        }
  
        if (feof(fd)) {
              break;
          }     
    }
    fclose(fd);
    return 0;
  }

2.4 stdin & stdout & stderr

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

  • 这三个流的类型都是FILE*, fopen返回值类型,文件指针

    cs 复制代码
    #include <stdio.h>
    extern FILE *stdin;
    extern FILE *stdout;
    extern FILE *stderr;

    2.5 打开文件的方式

    |------|---------------------|-------------------|
    | "r" | 只读打开 | 读取已存在的文件 |
    | "w" | 只写打开 | 新建文件并写入,或覆盖已有文件 |
    | "a" | 追加写入 | 向文件末尾添加内容 |
    | "r+" | 读写打开 | 既要读又要写已存在的文件 |
    | "w+" | 读写打开 | 新建文件读写,或覆盖已有文件后读写 |
    | "a+" | 追加读写(append + read) | 向文件末尾追加内容,同时可读取文件 |

3. 系统文件I/O

3.1 一种传递标志位的方法

cs 复制代码
#include <stdio.h>
#define ONE 0001 //0000 0001
#define TWO 0002 //0000 0010
#define THREE 0004 //0000 0100
void func(int flags) {
 if (flags & ONE) printf("flags has ONE! ");
 if (flags & TWO) printf("flags has TWO! ");
 if (flags & THREE) printf("flags has THREE! ");
 printf("\n");
}
int main() {
 func(ONE);
 func(THREE);
 func(ONE | TWO);
 func(ONE | THREE | TWO);
 return 0;
 }

3.2 写文件

cs 复制代码
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<string.h>
  4 #include <sys/types.h>
  5 #include <sys/stat.h>
  6 #include <fcntl.h>
  7 
  8 int main()
  9 {
 10   umask(0); // 消除系统默认umask的影响                                                                             
 11   int fd=open("log.txt",O_WRONLY|O_CREAT,0666);
 12   if(fd<0)
 13   {
 14     perror("open");
 15     return 1;
 16   }
 17   int cnt =5;
 18   const char* msg="straykids";
 19 
 20   while(cnt)
 21   {
 22    write(fd,msg,strlen(msg));
 23    cnt--;
 24   }
 25   close(fd);
 26   return 0;
 27 }

3.3 读文件

cs 复制代码
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<string.h>
  4 #include <sys/types.h>
  5 #include <sys/stat.h>
  6 #include <fcntl.h>
  7 
  8 int main()
  9 {
 10   int fd=open("log.txt",O_RDONLY);
 11   if(fd<0)
 12   {
 13     perror("open");
 14     return 1;
 15   }
 16 
 17   char buffer[100];
 18   while(1)
 19   {
 20     ssize_t s =read(fd,buffer,sizeof(buffer));
 21     if(s>0)
 22     {
 23       printf("%s",buffer);
 24     }
 25     else{
 26       break;                                                                                 
 27     }
 28   }
 29   close(fd);
 30   return 0;
 31 }

3.4 接口open介绍

man 2 open:

cs 复制代码
1 #include <sys/types.h>
2 #include <sys/stat.h>
3 #include <fcntl.h>
4  int open(const char *pathname, int flags);
5  int open(const char *pathname, int flags, mode_t mode);
5  pathname: 要打开或创建的⽬标⽂件
6  flags: 打开⽂件时,可以传⼊多个参数选项,⽤下⾯的⼀个或者多个常量进⾏"或"运算,构成flags。
7  参数:
8    O_RDONLY: 只读打开
9    O_WRONLY: 只写打开
10   O_RDWR : 读,写打开
11        (这三个常量,必须指定⼀个且只能指定⼀个

12   O_CREAT : 若⽂件不存在,则创建它。需要使⽤mode选项,来指明新⽂件的访问权限
13   O_APPEND: 追加写
14 返回值:
15   成功:新打开的⽂件描述符
16   失败:-1
17 mode_t :
18    是 C 语言中用于表示文件权限和文件类型的标准数据类型

3.5 open返回值

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

  • 库函数: fopen fclose fread fwrite
  • 系统调用接口:open close read write lseek
  • 可以认为, f# 系列的函数,都是对系统调用的封装,方便二次开发

3.6 文件描述符 fd

1) 文件描述符的概念

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

2) 进程与文件的关联

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

3) 文件描述符的本质

本质上,文件描述符就是该数组的下标。因此,只要持有文件描述符,就可以找到对应的文件。

4) 0 & 1 & 2

  • Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0,标准输出1,标准错误2
  • 0,1,2对应的物理设备一般是:键盘,显示器,显示器

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

cs 复制代码
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<string.h>
  4 #include <sys/types.h>
  5 #include <sys/stat.h>
  6 #include <fcntl.h>
  7   
  8 int main()  
  9 {   
 17   char buffer[100];  
 18   while(1)  
 19   {                                     
 21     ssize_t s =read(0,buffer,sizeof(buffer)-1);  
 22     if(s>0)                            
 23     {                                  
 24       buffer[s]=0;          
 25       printf("%s",buffer);  
 26     }                       
 27     else{                   
 28       break;                
 29     }                       
 30   }   
      return 0;
   }                      
 

如何理解read() 是默认读取标准输入(stdin,文件描述符 0)?

  • read() 其核心作用是从一个 「数据来源」 中读取数据 ,存入程序的内存缓冲区, 它的核心特点是:"输入"= 程序的 "数据输入来源",但这个来源不是固定的 ------ 默认是「键盘」,但可以被替换(重定向)
  • 简单说:read读取 "标准输入",本质就是读取「当前进程的 stdin 数据流」中的数据,而 "输入" 就是这个数据流里的内容

如何理解write() 是默认标准输出(stdout,文件描述符 1)?

  • write() 核心作用是:把程序内存中的字节数据,写入到一个「数据目的地」------ 这个目的地同样可以用**文件描述符(fd)**标识
  • 简单说:"输出" 本质是程序从内存中向外传递的字节数据,而 stdout 是程序 "说话的默认渠道",write函数就是 "把这话通过渠道传出去" 的工具

5) 文件描述符的分配规则

观察如下代码:

文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符

**6)**重定向

当关闭1时:

cs 复制代码
#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。这种现象称为输出重定向

常见的重定向符号包括:

  • >:覆盖输出到文件
  • >>:追加输出到文件
  • <:从文件读取输入

7) 重定向本质

  • 这是"重定向"的底层逻辑,核心是 "换个文件描述符的指向"
  • 文件描述符表(fd_array)是属于进程自己的、存在于进程控制块(task_struct)里的一个表结构,文件描述符表是 "存指针的数组",文件描述符是这个数组的 "下标",通过下标能快速找到数组里的指针,进而操作文件
  • 简单说:每个 "打开的文件",在内核里都对应一个file对象,文件描述符表里的file* 指针,就是指向这些file对象的 "链接"------ 通过这个链接,进程才能找到要操作的文件

8) 使用dup2系统调用

dup2函数原型:

cs 复制代码
#include <unistd.h>
int dup2(int oldfd, int newfd);
  • oldfd源文件描述符(必须是已打开且有效的文件描述符,否则调用失败)
  • newfd目标文件描述符(需要被替换的文件描述符)
cs 复制代码
//实例
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<string.h>
  4 #include <sys/types.h>
  5 #include <sys/stat.h>
  6 #include <fcntl.h>
  7 
  8 int main()
  9 {
 10   int fd = open("log.txt",O_CREAT,O_RDWR);                                                   
 11   if(fd<0)
 12   {
 13     perror("open");
 14     return 1;
 15   }
 16   close(1);
 17   dup2(fd,1);
 18   while(1)
 19   {
 20     char buff[1024];
 21     ssize_t read_size =read(0,buff,sizeof(buff)-1);
 22     if(read_size<0)
 23     {
 24       perror("read");
 25       break;
 26     }
 27   printf("%s",buff);
 28   fflush(stdout);
 29   }
 30   return 0;
 31 }

9)在minishell中添加重定向功能

https://gitee.com/dwaekkiyo/my_linux_code/edit/master/251206/myshell.c

补充:标准错误

标准错误(stderr,文件描述符为 2)的默认输出目标是终端屏幕,和标准输出的默认目标一致,就解释了为什么平时执行命令时,正常结果(stdout)和错误提示(stderr)会混在一起显示在屏幕上。

可以把标准输出和标准错误分开打印:

4. 理解"一切皆文件"

  1. 核心设计:广泛的文件抽象相较于 Windows,Linux 的文件概念更宽泛:不仅 Windows 中的文件在 Linux 中仍是文件,进程、磁盘、显示器、键盘等硬件设备,以及管道、后续会接触的网络编程中的 socket(套接字),也都被抽象成文件,可通过访问文件的方式获取其信息。

  2. 核心优势:统一的开发接口:该设计让开发者只需掌握一套 API 和开发工具,就能调用 Linux 系统中绝大多数资源 。例如,读操作(读文件、读系统状态、读管道)几乎都可通过read函数实现,更改操作(改文件、改系统参数、写管道)几乎都可通过write函数实现。

  3. 底层管理:file 结构体的作用 当在 Linux 中打开一个文件时,操作系统会创建一个file结构体用于管理该文件,该结构体定义在/usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/fs.h路径下,以下为部分该结构:

    cs 复制代码
    struct file {
     ...
     
     struct inode *f_inode; //缓存的 inode 指针
     const struct file_operations *f_op;
     
     ...
     
     atomic_long_t f_count; // 文件引用计数,多个文件指针指向该文件时数值会增加 
     unsigned int f_flags;  // 表⽰打开⽂件的权限 
     fmode_t f_mode;        // 设置对⽂件的访问模式,例如:只读,只写等。所有
    的标志在头⽂件<fcntl.h> 中定义 
     loff_t f_pos;          // 当前文件的读写位置 
     
     ...
     
    } __attribute__((aligned(4))); // 结构体按 4 字节对齐
  4. 关键关联:f_op指针与file_operations结构体struct file中的f_op是指向file_operations结构体的指针(二者均定义于fs.h);file_operations结构体的成员中,除struct module *owner外,其余均为函数指针

  5. **file_operations**结构体:

    cs 复制代码
    #include <linux/module.h>
    #include <linux/fs.h>
    #include <linux/aio.h>
    #include <linux/mm.h>
    
    struct file_operations {
        struct module *owner;
        // 指向拥有该模块的指针
    
        loff_t (*llseek) (struct file *, loff_t, int);
        // llseek 方法用作改变文件中的当前读/写位置,并且新位置作为(正的)返回值
    
        ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
        // 用来从设备中获取数据。若该指针为 NULL,read 系统调用会以 -EINVAL("Invalid argument")失败;
        // 非负返回值代表成功读取的字节数(返回值是 signed size 类型,通常是目标平台本地的整数类型)
    
        ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
        // 发送数据给设备。若该指针为 NULL,-EINVAL 会返回给调用 write 系统调用的程序;
        // 非负返回值代表成功写入的字节数
    
        ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
        // 初始化一个异步读------可能在函数返回前不结束的读操作
    
        ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
        // 初始化设备上的一个异步写
    
        int (*readdir) (struct file *, void *, filldir_t);
        // 对于设备文件,该成员应当为 NULL;它用来读取目录,仅对文件系统有用
    
        unsigned int (*poll) (struct file *, struct poll_table_struct *);
        // 实现 I/O 多路复用的轮询操作(支撑 select/poll/epoll 等系统调用)
    
        int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
        // 设备控制接口(兼容老版本,需持有大内核锁)
    
        long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
        // 无锁版设备控制接口(推荐使用,无需持有大内核锁)
    
        long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
        // 32 位兼容版 ioctl(用于 64 位内核运行 32 位用户程序)
    
        int (*mmap) (struct file *, struct vm_area_struct *);
        // 用来请求将设备内存映射到进程的地址空间。若该方法为 NULL,mmap 系统调用返回 -ENODEV
    
        int (*open) (struct inode *, struct file *);
        // 打开一个文件/设备(初始化相关资源)
    
        int (*flush) (struct file *, fl_owner_t id);
        // flush 操作在进程关闭它的设备文件描述符的副本时调用
    
        int (*release) (struct inode *, struct file *);
        // 在文件结构被释放时调用该操作。如同 open,release 可以为 NULL(无额外释放逻辑时)
    
        int (*fsync) (struct file *, struct dentry *, int datasync);
        // 用户调用用来刷新任何挂起(未同步)的数据到存储介质
    
        int (*aio_fsync) (struct kiocb *, int datasync);
        // 异步刷新挂起数据(配合异步读写操作使用)
    
        int (*fasync) (int, struct file *, int);
        // 异步 I/O 通知的注册/注销操作
    
        int (*lock) (struct file *, int, struct file_lock *);
        // 用来实现文件加锁;加锁对常规文件是必不可少的特性,但设备驱动几乎从不实现它
    
        ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
        // 高效发送文件页数据(如网络传输场景)
    
        unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
        // 获取进程地址空间中未映射的虚拟地址区域(配合 mmap 操作使用)
    
        int (*check_flags)(int);
        // 校验文件打开标志的有效性
    
        int (*flock) (struct file *, int, struct file_lock *);
        // BSD 风格的文件加锁(兼容 flock 系统调用,设备驱动一般不实现)
    
        ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
        // 管道到文件/设备的高效数据传输(零拷贝)
    
        ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
        // 文件/设备到管道的高效数据传输(零拷贝)
    
        int (*setlease)(struct file *, long, struct file_lock **);
        // 设置文件租约(用于控制文件并发访问,防止冲突)
    };
  6. file_operations 是 Linux 内核中连接用户层系统调用底层设备驱动实现的关键数据结构,是 "一切皆文件" 理念在设备驱动中的核心落地载体。

    它的核心作用是:将标准化的系统调用(如 readwriteioctl 等),通过函数指针映射到底层驱动程序中对应的具体实现函数 ,让内核能够统一调度不同设备的驱动逻辑,同时让用户层无需关注设备差异,只需调用统一 API 即可操作各类设备,一张图总结:

    总结:file 和**file_operations** 二者共同支撑"一切皆文件",s**truct file 管理 "已打开文件 / 设备的实例状态",struct file_operations 提供 "操作该实例的统一接口集合",前者通过指针引用后者,实现 "状态 + 操作" 的绑定**

5. 缓冲区

5.1 什么是缓冲区

缓冲区是内存中预留的一块存储空间,专门用于缓存输入或输出的数据,本质是内存的一部分。根据对应设备类型,可分为输入缓冲区 (适配输入设备)和输出缓冲区(适配输出设备)

5.2 引入缓冲区的核心目的

核心是解决 "设备速度不匹配" 和 "减少系统调用开销" 两大问题,提升整体效率:

  1. 减少系统调用与上下文切换损耗:直接操作磁盘等设备需频繁执行系统调用,每次调用会涉及用户空间与内核空间的 CPU 状态切换,耗时较高。缓冲区可一次性读取 / 写入大量数据,减少系统调用次数,降低切换损耗。
  2. 协调高速 CPU 与低速 I/O 设备 :CPU 运算速度远高于磁盘、打印机等 I/O 设备,缓冲区可作为数据 "中转站":
    • 读操作:先从低速设备(如磁盘)读取大量数据到缓冲区,CPU 后续从缓冲区快速获取,无需等待设备;
    • 写操作:CPU 先将数据写入缓冲区,低速设备(如打印机)再自行从缓冲区读取处理,CPU 可同步执行其他任务,避免被低速设备占用

5.3 标准 I/O 的 3 种缓冲类型

标准 I/O 库提供 3 类缓冲区,适配不同使用场景,核心区别在于 "触发 I/O 系统调用的时机":

缓冲类型 触发系统调用的条件 典型应用场景
全缓冲区 缓冲区被填满时 磁盘文件操作(如普通文件读写)
行缓冲区 遇到换行符,或缓冲区填满(默认大小 1024 字节) 终端相关流(如标准输入 stdin、标准输出 stdout)
无缓冲区 不缓存数据,直接执行系统调用 标准出错

缓冲区额外刷新场景

除上述默认触发条件外,以下情况会主动刷新缓冲区:

  1. 缓冲区数据填满时

  2. 主动执行flush语句时

  3. 示例如下:

    cs 复制代码
    // 示例1:stdout重定向到文件(全缓冲,未flush导致内容未写入)
    void test_no_flush() {
        // 关闭1号描述符(stdout,默认对应显示器)
        close(1);
        // 打开log1.txt,fd自动分配为1(最小未使用fd),实现stdout重定向
        int fd = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
        if (fd < 0) {
            perror("open log1.txt failed");
            exit(1);
        }
    
        // printf写入stdout(已重定向到文件,缓冲类型变为全缓冲)
        printf("test_no_flush: hello world, fd=%d\n", fd);
        // 未手动flush,内容未填满全缓冲区,数据仅存于内存
        close(fd);
    }
    
    // 示例2:stdout重定向+fflush强制刷新(内容成功写入)
    void test_with_flush() {
        close(1);
        int fd = open("log2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
        if (fd < 0) {
            perror("open log2.txt failed");
            exit(1);
        }
    
        printf("test_with_flush: hello world, fd=%d\n", fd);
        fflush(stdout);  // 强制刷新stdout缓冲区,数据写入文件
        close(fd);
    }
    
    // 示例3:stderr重定向(无缓冲,无需flush直接写入)
    void test_stderr_no_buffer() {
        // 关闭2号描述符(stderr,标准错误流)
        close(2);
        // 打开log3.txt,fd自动分配为2,实现stderr重定向
        int fd = open("log3.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
        if (fd < 0) {
            perror("open log3.txt failed");
            exit(1);
        }
    
        // perror默认写入stderr(无缓冲特性),直接触发系统调用
        perror("test_stderr_no_buffer: hello world");
        close(fd);
    }
    
    int main() {
        // 依次执行三个测试用例
        test_no_flush();
        test_with_flush();
        test_stderr_no_buffer();
    
        return 0;
    }

    5.4 FILE

  • 因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通 过fd访问的
  • 所以C库当中的FILE结构体内部,必定封装了fd

验证代码:

cs 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>  // 包含 fork() 声明

int main() {
    const char *msg0 = "hello printf\n";
    const char *msg1 = "hello fwrite\n";
    const char *msg2 = "hello write\n";

    printf("%s", msg0);
    fwrite(msg1, strlen(msg1), 1, stdout);  

    write(1, msg2, strlen(msg2));

    // 创建子进程,触发写时拷贝
    fork();

    return 0;
}

结果对比

场景 1:直接输出到显示器(无重定向)

cs 复制代码
运行命令
./hello
cs 复制代码
输出结果
hello printf
hello fwrite
hello write

原因解析

  • 输出目标为显示器时,stdout的缓冲类型是行缓冲
  • msg0msg1均包含换行符\n,触发行缓冲立即刷新,数据直接输出到显示器;
  • fork执行时,库函数的缓冲区已为空,父子进程无额外数据可刷新,因此无重复输出;
  • write是系统调用,无缓冲区,数据直接写入,仅输出 1 次

场景 2:重定向到文件(./hello > file

Delphi 复制代码
运行命令
./hello > file
cat file
Delphi 复制代码
输出结果
hello write
hello printf
hello fwrite
hello printf
hello fwrite

原因解析(核心逻辑)

  1. 缓冲类型切换 :重定向到普通文件后,stdout的缓冲类型从行缓冲 变为全缓冲
  2. 库函数数据未刷新printffwrite的输出内容较短,未填满全缓冲区,数据暂存于用户级缓冲区(由 C 标准库提供),未触发系统调用;
  3. fork 的写时拷贝fork创建子进程时,会复制父进程的所有内存数据(包括用户级缓冲区)。此时父子进程各自持有一份相同的缓冲区数据(未刷新);
  4. 进程退出时刷新缓冲区:main 函数返回后,父子进程相继退出,C 标准库会自动刷新缓冲区,将数据写入文件。因此库函数的内容被输出 2 次;
  5. 系统调用无缓冲write是系统调用,无用户级缓冲区,数据直接写入内核,fork时无数据可复制,因此仅输出 1 次
  6. 为了避免重复fork前用fflush(stdout)手动刷新缓冲区

5.5 简单设计lib库

mystdio.h

cs 复制代码
  1 #pragma once 
  2 #include<stdio.h>
  3                                                                                          
  4 #define MAX 1024         
  5 #define NONE_FLUSH (1<<0)
  6 #define LINE_FLUSH (1<<1)
  7 #define FULL_FLUSH (1<<2)
  8                       
  9 typedef struct IO_FILE
 10 {                           
 11   int fileno;   //文件描述符         
 12   int flag;     //文件状态(r,w,a..
 13   char outbuffer[MAX];  //输出缓冲区
 14   int bufferlen;        //缓冲区长度
 15   int flush_method;
 16         
 17 }MyFile;
 18                                                    
 19 MyFile* MyFopen(const char *path,const char *mode);
 20 void MyFclose(MyFile*);
 21 int MyFwrite(MyFile*,void* str,int len);           
 22 void MyFFlush(MyFile*);        

mystdio.c

cs 复制代码
  1 #include "mystdio.h"
    2 #include <string.h>
    3 #include <stdlib.h>
    4 #include <sys/types.h>
    5 #include <sys/stat.h>
    6 #include<unistd.h>
    7 #include <fcntl.h>
    8 static MyFile* BuyFile(int fd,int flag)
    9 {
   10   MyFile *f =(MyFile*)malloc(sizeof(MyFile));
   11   if(f==NULL)
   12     return NULL;
   13   f->bufferlen=0;
   14   f->fileno=fd;                                                                        
   15   f->flag=0;
   16   f->flush_method=LINE_FLUSH;
   17   memset(f->outbuffer,0,sizeof(f->outbuffer));
   18   return f;
   19 }
   20 
   21 MyFile* MyFopen(const char *path,const char *mode)
   22 {
   23   int fd = -1;
   24   int flag = 0;
   25   if(strcmp(mode,"w")==0)
   26   {
   27      flag = O_CREAT | O_WRONLY | O_TRUNC;                                              
   28      fd =open(path,flag,0666);
   29   }
   30   else if(strcmp(mode,"a")==0)
   31   {
   32      flag = O_CREAT | O_WRONLY | O_APPEND;
   33      fd =open(path,flag,0666);
   34   }
   35   else if(strcmp(mode,"r")==0)
   36   {
   37     flag= O_RDWR;
   38     fd =open(path,flag);
   39   }
   40   else 
   41   {
   42    //                                                                                  
   43   }
   44   if(fd<0)
   45     return NULL;
   46   return BuyFile(fd,flag);
   47 }
   48 void MyFclose(MyFile* file)
   49 {
   50   if(file->fileno<0) return ;
   51   MyFFlush(file);
   52   close(file->fileno);
   53   free(file);
   54 }
   55 int MyFwrite(MyFile* file,void* str,int len)
      56 {
   57   //1.拷贝
   58   memcpy(file->outbuffer+file->bufferlen,str,len);
   59   file->bufferlen +=len;
   60   //2.尝试判断是否满足刷新条件
   61   if((file->flush_method & LINE_FLUSH) && file->outbuffer[file->bufferlen-1]=='\n')
   62   {
   63     MyFFlush(file);                                                                    
   64   }
   65   return 0;
   66 }
   67 void MyFFlush(MyFile* file)
   68 {
   69   if(file->fileno<=0)return ;
   70   int n = write(file->fileno,file->outbuffer,file->bufferlen);
   71   //fsync(file->fileno); // 刷新到外设
   72   file->bufferlen=0;
   73 }

main.c

cs 复制代码
  1 #include"mystdio.h"
  2 #include<string.h>
  3 
  4 int main()
  5 {
  6   MyFile* filep= MyFopen("./log.txt","a");                                               
  7   if(!filep)
  8   {
  9     printf("fopen error\n");
 10     return 1;
 11   }
 12   char *msg=(char*)"hello myfile";
 13   MyFwrite(filep,msg,strlen(msg));
 14 
 15   MyFclose(filep);
 16   return 0;
 17 }
相关推荐
信创天地2 小时前
信创日志全流程管控:ELK国产化版本与华为日志服务实战应用
运维·安全·elk·华为·rabbitmq·dubbo
qq_406176142 小时前
吃透JS异步编程:从回调地狱到Promise/Async-Await全解析
服务器·开发语言·前端·javascript·php
卡卡大怪兽2 小时前
服务器远程连接,后台运行程序
运维·服务器
wdfk_prog2 小时前
[Linux]学习笔记系列 -- [drivers][base]dd
linux·笔记·学习
blueSatchel2 小时前
bus_register源码研究
linux·c语言
小李独爱秋2 小时前
计算机网络经典问题透视:RTP首部三剑客——序号、时间戳与标记的使命
服务器·计算机网络·web安全·信息与通信·rtsp
小李独爱秋2 小时前
计算机网络经典问题透视:RTP协议能否提供应用分组的可靠传输?
服务器·计算机网络·web安全·信息与通信·rtsp
AOwhisky2 小时前
iSCSI 网络存储服务从入门到精通
linux·运维·网络
刘叨叨趣味运维2 小时前
服务器硬件全面解析:从CPU到网卡的运维必备知识
linux