Linux - 基础IO【下】

一、重谈重定向

  • stderr 和 stdout 是打印到显示器文件里面的 , 均访问同一个文件
  • 进行重定向的时候,是把3重定向到1 , 但是2依旧是在显示屏

问 : 为什么有了stdout , 还需要有stderr ?

问:如何把stderr 和 stdout 打印到同一个文件 ?

二、理解"一切皆文件"

Linux 把所有资源都抽象成文件,包括:

  • 普通磁盘文件(log.txt
  • 设备(键盘、显示器、网卡、磁盘)
  • 进程间通信(管道、socket)
  • 甚至内核对象

统一接口的意义

不管操作的是什么资源,都用同一套系统调用接口open/read/write/close)来操作,这就是「一切皆文件」的本质:

  • 对开发者:只需要学一套 API,就能操作所有资源
  • 对内核:通过 struct file_operations 函数指针集,为不同资源实现不同的底层操作(比如键盘的 read 和磁盘的 read 实现完全不同)
  • 对系统:架构简洁,可扩展性极强

骗过进程,相当于骗过用户 , 让进程以为 "一切皆文件" ,屏蔽底层的差异。

file_operation 就是把系统调用和驱动程序关联起来的关键数据结构 , 这个结构的每个成员都对应着一个系统调用。读取file _operation 中相应的函数指针,接着把控制权转交给函数。从而完成了Linux设备驱动程序的工作

上图的外设,每个设备都可以有自己的read , write , 但一定是对应着不同的操作方法!!!但通过struct file下file _operation中的各种函数回调,让我们开发者只用file便可以调取Linux系统中绝大部分的资源!!!这就是"Linux下一切皆文件"的核心概念!

三、缓冲区

3.1 什么是缓冲区

缓冲区是内存中预留的一块空间,用于缓存 IO 数据,分为:

  • 用户级缓冲区由 C 标准库(libc)维护,在用户空间
  • 内核级缓冲区 :**由 Linux 内核维护,**在内核空间

3.2 为什么要引入缓冲区机制

核心原因:CPU 速度 ≫ 内存速度 ≫ 磁盘 / 设备 IO 速度,存在严重的速度不匹配。

缓冲区的价值:

  1. 减少系统调用次数:避免频繁的用户态 ↔ 内核态切换,降低开销
  2. 批量 IO 提升效率:底层设备(如磁盘)更擅长批量读写,缓冲区可以攒够数据再一次性写入
  3. 解耦 CPU 与 IO:CPU 只需要操作内存缓冲区,无需等待低速设备完成 IO

想象一下,如果没有菜鸟驿站,快递员要送快递给取件人,就必须直接联系对方:

  • 快递员每送到一件快递,都要打电话给取件人,等对方下楼来取。

  • 如果取件人不在家,快递员就得改天再跑一趟,或者一直等着。

  • 取件人也可能因为不能及时拿到快递而感到不便。

这就像没有缓冲区的文件读写 :每次应用程序要读一个字节,CPU就得发起一次系统调用,切换到内核态,让磁盘控制器去读一个字节,然后切换回用户态。频繁的系统调用和状态切换会消耗大量时间,就像快递员反复跑腿、等待一样,效率极低。

有了菜鸟驿站,流程就变成了:

  • 快递员(数据源/输入设备) 不再需要挨个打电话等取件人,而是直接把一批快递(数据块)放到驿站的货架上(缓冲区),然后就可以去处理下一批快递了。

  • 取件人(CPU/应用程序) 可以在自己方便的时候(比如下班后)去驿站,一次取走所有自己的快递,或者分次取,完全不用和快递员同步。

  • 驿站(缓冲区) 起到了中间暂存的作用,既让快递员能快速卸货,也让取件人能灵活取货。

3.3 缓冲类型

  • 用户级缓冲区(语言层缓冲区)

    由C标准库提供(如 printffwrite 等函数内部维护的缓冲区)。当我们调用这些函数时,数据并不会立即通过系统调用写入内核,而是先暂存在用户空间的一块内存中(即用户级缓冲区)。

    例如,多次调用 printf("a") ,字符 'a' 可能先被收集到用户缓冲区,直到满足一定条件才一次性通过 write 系统调用发送给内核。

  • 内核级缓冲区(文件内核缓冲区)

    当用户程序通过**系统调用(如 write)**将数据交给操作系统后,操作系统并不一定会立即将数据写入硬件(如磁盘),而是先将数据拷贝到内核空间的缓冲区(即内核缓冲区)。操作系统根据自身的策略(如缓存、合并写入)决定何时真正将数据刷到硬件设备。

缓冲类型 刷新策略 典型场景
全缓冲 缓冲区满才刷新 普通磁盘文件(默认)
行缓冲 遇到 \n 或缓冲区满才刷新 标准输出 stdout(终端)
无缓冲 直接写入,不缓冲 标准错误 stderr

解释:

这是由于我们将1号描述符重定向到磁盘文件后,缓冲区的刷新方式成为了全缓冲。而 我们写入的内容并没有填满整个缓冲区,导致并不会将缓冲区的内容刷新到磁盘文件中。
怎么办呢?
可以使用fflush强制刷新下缓冲区。

还有一种解决方法 , 刚好可以验证 一下stderr是不带缓冲区的 :

这种方式便可以将2号文件描述符重定向至文件由于stderr没有缓冲区 ,****"hello world"不用fflush就可以写入文件

3.4 FLIE(库封装系统调用)

很正常,四条消息,但是由于write是系统调用 , 直接到文件内核缓冲区 ; 其他的库函数是先放在C标准库的的缓冲区,后面才被刷到os。

我们再接着看下面的例子:

3.5 简单设计一下libc库

mystdio.c

复制代码
#include "mystdio.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

static MYFILE *BuyFile(int fd, int flag)
{
    MYFILE *f = (MYFILE*)malloc(sizeof(MYFILE));
    if(f == NULL) return NULL;
    f->bufferlen = 0;
    f->fileno = fd;
    f->flag = 0;
    f->flush_method = LINE_FLUSH;
    memset(f->outbuffer, 0, sizeof(f->outbuffer));
    return f;
}

MYFILE *MyFopen(const char* path, const char *mode)
{
    int fd = -1;
    int flag = 0;
    if(strcmp(mode, "w") == 0)
    {
        flag = O_WRONLY | O_CREAT | O_TRUNC;
        fd = open(path, flag, 0666);
    }
    else if(strcmp(mode, "a") == 0)
    {
        flag = O_WRONLY | O_CREAT | O_APPEND;
        fd = open(path, flag, 0666);
    }
    else if(strcmp(mode, "r") == 0)
    {
        flag = O_RDONLY;
        fd = open(path, flag);
    }
    else
    {
        //todo
    }
    if(fd < 0) return NULL;
    return BuyFile(fd, flag);
}

void *MyClose(MYFILE *file)
{
    if(file->fileno < 0) return NULL;
    MyFflush(file);
    close(file->fileno);
    free(file);
}

int MyFwrite(MYFILE *file, void *str, int len)
{
    //1.写入就是拷贝
    memcpy(file->outbuffer + file->bufferlen, str, len);
    file->bufferlen += len;

    //2.尝试判断是否满足刷新条件?
    if((file->flush_method & LINE_FLUSH) && file->outbuffer[file->bufferlen-1] == '\n')
    {
        MyFflush(file);
    }

    return 0;
}

void MyFflush(MYFILE *file)
{
    if(file->bufferlen <= 0) return;
    int n = write(file->fileno, file->outbuffer, file->bufferlen);
    (void)n;
    fsync(file->fileno);
    file->bufferlen = 0;
}

mystdio.h

复制代码
#pragma once
#include <stdio.h>

#define MAX 1024
#define NONE_FLUSH (1<<0)
#define LINE_FLUSH (1<<1)
#define FULL_FLUSH (1<<2)

typedef struct IO_FILE
{
    int fileno;
    int flag;
    char outbuffer[MAX];
    int bufferlen;
    int flush_method;
}MYFILE;

MYFILE *MyFopen(const char* path, const char *flag);
void *MyClose(MYFILE *file);
int MyFwrite(MYFILE *file, void *str, int len);
void MyFflush(MYFILE *file);

usercode.c

复制代码
#include "mystdio.h"
#include <string.h>

int main()
{
    MYFILE * filep = MyFopen("./log.txt", "a");
    if(!filep)
    {
        printf("fopen error!\n");
        return 1;
    }

    char* msg = "hello myfile!\n";
    MyFwrite(filep, msg, strlen(msg));

    MyClose(filep); //FILE *fp
    return 0;
}
相关推荐
淡泊if2 小时前
eBPF 实战:一次诡异的 Nginx 高延迟,我用 5 分钟在内核里找到了真凶
java·运维·nginx·微服务·ebpf
志栋智能2 小时前
安全超自动化的终极目标:实现自适应安全防护
运维·人工智能·安全·自动化
xyd陈宇阳2 小时前
面向网络协议初学者的入门指南
linux·运维·网络协议
慧天城寻2 小时前
H3C巡检命令与避坑技巧
运维·网络·运维开发
tnuly3 小时前
Linux 云计算运维入门:从 Socket 底层到 Apache 实战,HTTP 服务全知识点拆解
运维
_DCG_3 小时前
用户态和内核态的区别
linux
肖恭伟3 小时前
QtCreator Linux ubuntu24.04问题集合
linux·windows·qt
兮动人3 小时前
Linux 云服务器部署 OpenClaw 全攻略:从环境搭建到 QQ 机器人集成
linux·服务器·机器人·openclaw
linux修理工3 小时前
使用 nextcloud.occ 重置用户密码
linux·运维·服务器