欢迎光临
我们一直在努力

一文掌握 Linux 文件操作:C 语言接口 + 系统调用 + 缓冲区原理

一、理解文件

a.

文件 = 文件内容 + 文件属性(元数据)

对文件操作:**1.**对内容操作 **2.**对属性操作

b.

访问文件之前需要打开文件,更需要找到文件,路径+文件名;

fopen打开文件,谁来打开???程序运行时才打开文件,即进程打开的文件;

访问任何文件都要有路径;有时不写路径,因为进程中有路径的环境变量cwd;

所以思想要从程序与文件的关系转变成进程与文件的关系。

c.

打开文件其实是将文件加载到内存!

d.

Linux中有大量文件,分为:打开的和没打开的

那么文件的位置也就两种:1.内存级被打开的文件;2.磁盘文件。

e.

Linux系统中的大量文件需要管理,那么就需要先描述,在组织,那么肯定就有个结构体来组织。

二、文件操作C语言接口

1.打开文件

FILE* fopen(const char* filename, const char* mode);

  • 返回值:成功返回文件指针,失败返回 NULL
  • 模式
模式含义文件不存在文件已存在
r 只读 报错 从头读
w 只写 创建新文件 清空重写
a 追加 创建新文件 末尾写入不换行
r+ 读写 报错 从头读写
w+ 读写 创建新文件 清空重写
a+ 读写 创建新文件 末尾读写

2.关闭文件

int fclose(FILE* stream);

  • 必须关闭文件,否则会内存泄漏、数据丢失
  • 成功返回 0,失败返回 EOF

3.字符串读写

1)fgets – 读取一行字符串

char* fgets(char* str, int n, FILE* stream);c

  • 读取最多 n-1 个字符,自动加 \\0,遇到换行 / 文件尾停止
  • 成功返回 str,失败 / 到末尾返回 NULL

2)fputs – 写入一行字符串

int fputs(const char* str, FILE* stream);

  • 写入字符串(不含末尾 \\0),成功返回非负数,失败返回 EOF

4.读写位置

理解<位置>:一个文件不论是有换行还是空格也都是由一个个字符拼接的,所以在磁盘中,文件就类似于一个一维数组char[]。文件位置就是这个数组的下标。

1)fseek – 移动文件指针

int fseek(FILE* stream, long offset, int whence);

  • offset:偏移量(正数向后,负数向前)
  • whence:起始位置
    • SEEK_SET:文件开头
    • SEEK_CUR:当前位置
    • SEEK_END:文件末尾

2)ftell – 获取当前指针位置

long ftell(FILE* stream);

  • 返回距离文件开头的字节数

3)rewind – 指针回到文件开头

void rewind(FILE* stream);

示例:

fseek(fp, 5, SEEK_SET); // 从开头偏移5个字节
long pos = ftell(fp); // 获取当前位置
rewind(fp); // 回到开头

三、文件操作系统级调用

1.打开文件

int open(const char *pathname, int flags, mode_t mode);

  • 返回值:文件描述符 fd(整数),失败 – 1
  • 常用 flags
    • O_RDONLY 只读
    • O_WRONLY 只写
    • O_RDWR 读写
    • O_CREAT 不存在则创建
    • O_APPEND 追加模式(文件末尾写)
    • O_TRUNC 清空原有内容
  • mode:权限 0664;当新生成这个文件时需要设置的权限。

2.关闭文件

int close(int fd);

  • 返回值:成功0;失败-1。

3.写文件

ssize_t write(int fd, const void *buf, size_t count);

**注意:**在打开文件时,要加flags的 O_APPEND 追加:永远写末尾,不会自动换行

第三个参数count细节:

  • 字符串
  • char str[] = "hello";
    write(fd, str, strlen(str));

    只写有效字符,不写 \\0

  • 写整个数组(含末尾 0)
  • write(fd, str, sizeof(str));

    会把字符串结束符 \\0 也写入文件

    **注意:**当写入的是一个指针弄的字符串时,不要用sizeof(),读的是指针的字节8。

    四、文件描述符

    在文件操作系统调用中,打开文件open()的返回值是:文件描述符 fd(整数),失败 – 1

    我们试试多打开几个文件:

    在这里插入图片描述

    发现依次递增,那么前面的0、1、2哪里去了?

    其实0、1、2是C默认打开的三个标准流:

    #include <stdio.h>
    extern FILE *stdin;// 0
    extern FILE *stdout;// 1
    extern FILE *stderr;// 2

    那我们要是想在显示器上显示文字,直接把fd参数设置为1就行了!!!

    write(1, buf, strlen(buf));

    文件描述符的本质

    在这里插入图片描述

    文件描述符的分配规则

    1. 核心分配规则

    (1)从小到大找最小空闲整数分配

    (2)关闭某个 fd 后,该数值立刻变为空闲,下次优先复用

    (3)新打开文件,优先填补空缺,再往后递增

    2. 举例演示

    (1)初始占用:0、1、2

    (2)第一次 open → 分配3

    (3)close (1),释放 1 号

    (4)再 open → 优先分配1(最小空闲)

    (5)再 open → 分配4

    3. 关键特性
    • 每个进程独立一套 fd 表,互不干扰
    • 进程最大 fd 数量有限,超出报错
    • 子进程 fork 会拷贝父进程所有文件描述符
    4. 坑点(也可以是重定向)

    fd=1 是标准输出 stdout

    close(1); 可以执行,但极易出问题

    核心坑点

  • printf、puts 全部失效

    输出本质往 fd=1 写,关掉后屏幕看不到打印

  • 后续打开文件优先抢占 1 号描述符

    按最小空闲分配,新 open 直接拿到 1

    此时 printf 会写到这个文件里,不再输出屏幕

  • 标准错误 fd=2 不受影响,perror、stderr 打印仍可用

  • 示例直观演示

    close(1); // 关闭标准输出
    int fd = open("a.txt",O_WRONLY|O_CREAT,0664);
    printf("测试文字");// 不会打印屏幕,全部写入a.txt

    在这里插入图片描述

    衍生常见坑

    • 关闭 0:scanf 读键盘失效
    • 关闭 2:错误提示看不到
    • fork 子进程会继承关闭状态,子进程也无法正常输出
    5. 优雅的重定向

    调整结构体中的指向文件结构的指针数组;

    把要重定向的目标文件结构指针拷贝到1或0中。

    (1)系统提供了函数dup2()

    int dup2(int oldfd, int newfd);

    把oldfd复制覆盖到newfd,剩下oldfd

  • 先关闭 newfd原本占用的文件
  • 让 newfd 和 oldfd 指向同一个文件
  • 返回值:成功返回newfd,失败 – 1
  • (2)输出重定向实例

    int fd = open("a.txt", O_WRONLY|O_CREAT|O_TRUNC, 0664);
    dup2(fd, 1); // 把标准输出1 改成指向文件fd
    printf("内容写到文件,不显示屏幕");
    close(fd);

    (3)关键特点

  • 两个 fd 操作同一个文件,读写位置共享
  • 关闭其中一个,另一个仍可用
  • 执行后原有 newfd 功能失效
  • 五、再看“一切皆文件”

    一台设备有着许多硬件(外部设备):

    键盘、显示器、网卡、其他设备;访问这些硬件一般就是用IO方法,有的也可能没有,每种设备的访问方法都不一样;

    在这里插入图片描述

    本质竟然还是多态!基类(struct file)->派生类(驱动硬件)。

    六、理解缓冲区

    1. 文件打开和读写的基本过程

    在这里插入图片描述

    2. 语言级缓冲区

    read、write等系统级调用的成本会比较高,在语言级比如C/C++中的输入输出函数,为了在频繁的IO下保持效率,封装IO函数时,并不是读一次或写一次就调用系统级IO函数,而是存在一个语言级的缓冲区!

    在这里插入图片描述

    那语言级缓冲区具体在哪呢?

    每一个文件的**FILE**对象中!!!

    struct FILE {
    int fd;
    char inbuffer[];
    char outbuffer[];
    ...
    }

    但观察语言级函数fopen()等:

    在这里插入图片描述

    返回值是FILE*呀,那FILE对象在哪里创建的呢?

    其实是库函数fopen内部封装了创建FILE对象。

    那么fclose()做的事还要释放FILE对象。

    3. 测试缓冲区现象

    (1):

    在这里插入图片描述

    解决:在printf写入语言级之后就刷新缓冲区fflush(stdout);

    在这里插入图片描述

    在这里插入图片描述

    刷新缓冲区的本质

    语言级缓冲区通过write(fd)拷贝到内核级缓冲区中;

    也就是把用户数据交给了操作系统:不一定写进磁盘。

    刷新策略:

  • 进程结束时,会自动刷新(自动调用fclose);
  • 如果目标文件是显示器:行刷新;
  • 普通文件,缓冲区写满了才刷新:全缓冲。
  • (2):

    int main() {
    printf("hello world");
    sleep(2);
    exit(0);// 进程结束
    return 0;
    }

    在这里插入图片描述

    int main() {
    printf("hello world");
    sleep(2);
    _exit(0);// 进程终止
    return 0;
    }

    在这里插入图片描述

    对比exit()和_exit():

    exit()是C语言提供的库函数,会冲刷语言级缓冲区;

    _exit()是系统级函数,不会冲刷语言级缓冲区。因为它是下层函数,没有上层语言级缓冲区的概念。

    (3):

    int main() {
    // 向显示器打印字符串
    // C语言
    printf("hello printf\\n");
    fprintf(stdout, "hello fprintf\\n");
    const char* s = "hello fputs\\n";
    fputs(s, stdout);

    // 系统调用
    const char* s2 = "hello write\\n";
    write(1, s2, strlen(s2));

    fork();
    return 0;
    }

    在这里插入图片描述

    现象原因:

  • 向文件写,全缓冲;
  • 语言级函数有语言级缓冲区,系统函数没有;
  • 分支进程。
  • 具体过程:

    首先系统级函数write直接将内容写到内核级缓冲区里了;

    其次fork后父子进程各自结束,都要将各自的语言级缓冲区刷新至内核级缓冲区,也就刷新了两次语言级缓冲区!

    4. 文件内核缓冲区

    理解(1):只要用户把数据交给了文件内核缓冲区,就相当于交给了系统!!!

    理解(2):OS对文件内核缓冲区的刷新策略:有立即刷新;或等OS不忙了再自主刷新。

    当然也存在强制刷新内核缓冲区的系统函数,如:int fsync(int fd);。(sync:同步)

    fscyc()针对单个文件生效,将该文件对应的所有脏数据(修改的)、文件元信息(文件大小、修改时间、权限属性等)全部写入磁盘,执行过程阻塞程序,直至磁盘写入完成才返回。

    七、标准错误stderr

    1. stderr = 标准错误(文件描述符 2)

    专门用来输出 “错误信息” 的输出通道。

    它和屏幕绑定,但独立于 stdout(1)。

    2. 对比三个标准流

    0 → stdin 标准输入(键盘)

    **1 → stdout 标准输出(屏幕,正常消息) **

    2 → stderr 标准错误(屏幕,错误消息)

    3. out、err完全独立

    stdout(1)

    • 放正常运行信息
    • 默认行缓冲(遇到 \\n 才输出)
    • 可以被重定向到文件

    stderr(2)

    • 放错误、警告、崩溃信息
    • 默认无缓冲(立刻输出)
    • 为了让你立刻看到错误
    • 也可以重定向,但和 stdout 分开重定向

    4. 例子

    printf("hello\\n"); // 走 stdout(1) 正常消息
    perror("open failed"); // 走 stderr(2) 错误信息
    fprintf(stderr, "error"); // 走 stderr(2)

    在shell中运行:

    在这里插入图片描述

    • printf 内容进文件
    • perror 内容仍然显示在屏幕上

    这就是 stderr 的意义:错误信息不跟着普通输出一起被重定向走!

    5. 为什么要设计 stderr?

    如果没有 stderr:

    • 所有信息混在一起
    • 错误信息会被重定向到文件,你看不到崩溃原因
    • 程序崩了,你都不知道为啥崩

    有了 stderr:

    • 正常信息 → 可以重定向走
    • 错误信息 → 永远能看到
    • 错误输出不缓冲、立刻打印,保证崩溃前能看到错误

    4. 例子

    printf("hello\\n"); // 走 stdout(1) 正常消息
    perror("open failed"); // 走 stderr(2) 错误信息
    fprintf(stderr, "error"); // 走 stderr(2)

    在shell中运行:

    [外链图片转存中…(img-DESJxr1U-1779523730488)]

    • printf 内容进文件
    • perror 内容仍然显示在屏幕上

    这就是 stderr 的意义:错误信息不跟着普通输出一起被重定向走!

    5. 为什么要设计 stderr?

    如果没有 stderr:

    • 所有信息混在一起
    • 错误信息会被重定向到文件,你看不到崩溃原因
    • 程序崩了,你都不知道为啥崩

    有了 stderr:

    • 正常信息 → 可以重定向走
    • 错误信息 → 永远能看到
    • 错误输出不缓冲、立刻打印,保证崩溃前能看到错误
    赞(0)
    未经允许不得转载:171主机测评 » 一文掌握 Linux 文件操作:C 语言接口 + 系统调用 + 缓冲区原理
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址