Linux 基础 IO 核心知识总结:从系统调用到缓冲区机制(一)

发布于:2025-09-11 ⋅ 阅读:(20) ⋅ 点赞:(0)

一,理解文件

1.1普遍理解

  1. 文件在磁盘里,磁盘是永久储存的,那文件在磁盘上的存储是永久性的
  2. 磁盘是外设(即是输出设备也是输入设备)

  3. 对文件的操作通过 IO 完成,磁盘作为外设是 IO 操作的对象之一

 1.2在Linux角度

Linux下一切皆文件,如 显示器文件,磁盘,键盘都是文件。

1.3内存角度

对于空文件(0 kb)也在要磁盘上占有空间,而文件=内容➕属性,因此所有的操作都是围绕着文件内用与文件属性展开的。

1.4系统角度

对文件的操作本质是进程对文件的操作,磁盘的管理者是操作系统,所以文件的操作不是通过某种语言实现的,而是通过文件相关的系统调用接口来实现的。

总结:文件是逻辑数据单元,磁盘是物理存储载体,进程通过系统调用磁盘驱动 IO 操作文件。

二,C语言的文件

2.1熟悉接口

1.打开文件:fopen

按指定模式打开文件,返回文件操作指针,失败返回NULL

  1. filename:文件名(如"log.txt"),可带路径(如"/home/user/log.txt"
  2. mode:打开模式,常用值:
  • "r":只读(文件必须存在)。
  • "w":写入(清空文件或创建新文件)。如果不写就清空
  • "a":追加(在文件末尾写入,不清空)

 FILE *fp = fopen("myfile", "w");  为什么直接写myfile文件程序就知道在哪个路径下?

#include <stdio.h>
int main()
{
    FILE *fp = fopen("myfile", "w");
    if(!fp){
        printf("fopen error!\n");
    }
   const char *mag = "erman\n";
      int count=5;                                                     
      while(--count){
          fwrite(mag,strlen(mag),1,fp);
      }
     fclose(fp);
      return 0;

}

 为什么只用传fp,就知道你要放在那因为-》 命令查看当前正在运行进程的信息: 

ls /proc/[进程id] -l

nmemb 的三种常见传法

批量写

char ch = 'A';
fwrite(&ch, sizeof(char), 1, fp);  // 写入1个字符
int num = 100;
fwrite(&num, sizeof(int), 1, fp);  // 写入1个int

写入字符串

char str[] = "Hello";
fwrite(str, sizeof(char), strlen(str), fp);  // 写入5个字符

写入结构体

struct Student {
    char name[20];
    int age;
};

struct Student class[30];
fwrite(class, sizeof(struct Student), 30, fp);  // 写入30个学生数据
 2. 关闭文件:fclose

释放文件资源,关闭文件指针。

  stream:文件指针。    上面👆例子有 

3.二进制写入文件 :fwrite

将内存中的数据按二进制格式写入文件。直接按字节复制。

  • buffer:要写入的数据地址(如字符串指针)。
  • size:每个数据单元的字节数(如sizeof(char))。
  • count:写入的单元个数。
  • stream:文件指针。
    char message[] = "Hello, erman!";
    fwrite(message, sizeof(char), strlen(message), fp);  // 写入字符串到文件
4.二进制读取文件:fread

文件中按二进制格式读取数据到内存。

char buffer[1024] = {0};
fread(buffer, sizeof(char), 1024, fp);  // 从文件读取1024字节到buffer
5.格式化写入文件 :fprintf

按指定格式(如%d%s)将数据写入文件,类似printf但输出到文件。

int num = 100;
fprintf(fp, "数字:%d,字符串:%s\n", num, "测试");  // 格式化写入
6.按行读取字符串:fgets

从文件中读取一行字符串(遇到\n或文件末尾停止)。

char line[100] = {0};
while (fgets(line, 100, fp) != NULL) {  // 逐行读取直到文件末尾
    printf("%s", line);
}
 7.输出信息到显示器
#include <stdio.h>
#include <string.h>
int main()
{
    const char *msg = "hello fwrite\n";
    fwrite(msg, strlen(msg), 1, stdout);
    printf("hello printf\n");
    fprintf(stdout, "hello fprintf\n");
    return 0;
}

 2.2 C默认三个输入输出流

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

标准输入,输出,错误

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

 3.系统文件I/O

打开文件的方式不仅仅是fopen,ifstream等流式,其语言层的方案,底层都封装了系统接口,因此其实系统才是打开文件最底层的方案。

3.1 open 函数:打开 / 创建文件

d

  • pathname:文件路径(绝对路径或相对路径)。
  • flags:打开方式(可通过|组合多个标志),常用选项:
    • O_RDONLY:只读模式
    • O_WRONLY:只写模式
    • O_RDWR:读写模式
    • O_CREAT:若文件不存在则创建
    • O_TRUNC:打开时清空文件内容
    • O_APPEND:追加模式(写入时从文件末尾开始)
  • mode(可选):新建文件的权限(如0666表示所有者 / 组 / 其他用户均可读写),需与O_CREAT配合使用。
3.1.1介绍flags
什么是flags

flag是标记位,而flags是标记组合 用个例子理解

比如电视机Flags 就像电视机遥控器上的 “组合按键”:

  • 每个按键(如 “电源”“音量 +”“菜单”)是一个独立功能,对应一个标记位
  • 当你同时按下多个按键(如 “电源 + 音量 +”),就组合出一个新功能,这就是Flags 标志组合
  • 计算机里的 Flags 本质是用二进制位(0 和 1)表示 “按键是否按下”,比如:
    • 0:按键没按(对应二进制位 0)
    • 1:按键按下(对应二进制位 1)
 位图

 位图(Bitmap)—— 记录所有开关状态的 “表格”

位图就像遥控器的 “开关控制面板”:

  • 你想 “打开电视 + 调大音量”,需要同时按两个键,对应 Flags 组合:
    • “开电视” 标记位:0b0001(二进制)
    • “调大音量” 标记位:0b0010
    • 组合后(| 操作):0b0001 | 0b0010 = 0b0011(同时生效)。

因此对于flags的传参有多种

规则:O_RDONLY/O_WRONLY/O_RDWR:必须三选一 ➕O_APPEND  O_TRUNC  O_CREAT  组合

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    // 场景1:创建新文件(不存在则创建,存在则报错)
    int fd1 = open("new_file.txt", O_WRONLY | O_CREAT, 0644);
    if (fd1 == -1) {
        perror("创建文件失败");
    } else {
        printf("文件创建成功,fd = %d\n", fd1);
        close(fd1);
    }

    // 场景2:打开已有文件(不存在则报错)
    int fd2 = open("existing_file.txt", O_RDONLY);
    if (fd2 == -1) {
        perror("打开文件失败");
    } else {
        printf("文件打开成功,fd = %d\n", fd2);
        close(fd2);
    }

    // 场景3:打开文件并清空内容
    int fd3 = open("temp.txt", O_WRONLY | O_TRUNC);
    if (fd3 == -1) {
        perror("清空文件失败");
    } else {
        printf("文件已清空,fd = %d\n", fd3);
        close(fd3);
    }

    return 0;
}
3.1.2介绍mode
权限控制
  • 仅在使用O_CREAT并是新文件时需要第三个参数(如0644
  • 已经存在的文件只读
int fd3 = open("temp.txt", O_CREAT);
umask条件掩码 
  • 实际权限 = 指定权限 & ~umask(如 umask 为 0002 时,0666 实际为 0664)

为什么需要 umask?
  1. 安全默认值
    防止意外创建高权限文件(如所有人可写的配置文件)。
    示例:若 umask 为0000open("passwd", 0600)会因疏忽暴露密码文件。

  2. 用户自定义
    用户可通过umask命令修改默认掩码,如开发环境中设为0002允许同组用户写入

umask 值 二进制 效果(屏蔽) 典型场景
0022 000010010 同组和其他用户的写权限 标准 Unix 系统默认值
0002 000000010 其他用户的写权限 团队协作环境
0077 000111111 所有组的读 / 写 / 执行权限 敏感文件(如私钥)
如何更改umask 

查看当前 umask

umask  # 返回如0022

临时修改 umask

umask 0002 # 当前shell会话生效

 永久修改:在~/.bashrc/etc/profile中添加: 可以是其他值

umask 0022

 3.2文件写入 (write())

在系统层面的写入并不关心,传入的什么,最终都是二进制。

三、文件读取 (read())

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    int fd = open("input.txt", O_RDONLY);
    if (fd == -1) {
        perror("打开文件失败");
        return 1;
    }

    // 场景1:读取固定大小缓冲区
    char buffer[100];
    ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
    if (bytes_read == -1) {
        perror("读取失败");
    } else if (bytes_read == 0) {
        printf("文件为空\n");
    } else {
        printf("读取 %ld 字节: %.*s\n", bytes_read, (int)bytes_read, buffer);
    }

    // 场景2:循环读取直到文件末尾
    lseek(fd, 0, SEEK_SET); // 重置文件指针
    ssize_t total = 0;
    while ((bytes_read = read(fd, buffer, sizeof(buffer))) > 0) {
        total += bytes_read;
        // 处理读取的数据...
    }
    printf("总共读取 %ld 字节\n", total);

    close(fd);
    return 0;
}

返回值含义

  • -1:错误发生(检查errno
  • 0:已到达文件末尾(EOF)
  • 正数:实际读取的字节数

 四,文件关闭与错误处理

if (close(fd) == -1) {
        perror("关闭文件失败");
        return 1;
    }
五、文件定位 (lseek())
  • SEEK_SET:从文件开头偏移
  • SEEK_CUR:从当前位置偏移
  • SEEK_END:从文件末尾偏移(可为负值)
  • 获取文件大小:lseek(fd, 0, SEEK_END)
  • 创建空文件:lseek(fd, 1024, SEEK_SET);
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    int fd = open("data.bin", O_RDWR | O_CREAT, 0644);
    if (fd == -1) {
        perror("打开文件失败");
        return 1;
    }

    // 写入数据
    write(fd, "ABCDEFGHIJ", 10);

    // 移动指针到文件中间(第5个字节)
    off_t position = lseek(fd, 4, SEEK_SET);
    printf("当前位置: %ld\n", position);

    // 写入新数据(覆盖原内容)
    write(fd, "XYZ", 3);

    // 移动到文件末尾并追加
    position = lseek(fd, 0, SEEK_END);
    write(fd, "123", 3);

    // 获取文件大小
    position = lseek(fd, 0, SEEK_END);
    printf("文件大小: %ld 字节\n", position);

    close(fd);
    return 0;
}
系统调用 核心功能 关键参数 返回值
open() 打开 / 创建文件 路径、flags、mode 文件描述符
write() 写入数据 fd、缓冲区、大小 实际写入字节数
read() 读取数据 fd、缓冲区、大小 实际读取字节数
lseek() 定位文件指针 fd、偏移量、起始点 新位置
close() 关闭文件 fd 0 成功,-1 失败

 3.3文件描述符fd

看open函数返回值,我们知道了文件描述符就是一个小整数。

fd用处 

内核根据fd找到对应的文件对象,从而确定数据读写的位置、缓冲区等信息。

int fd = open("data.txt", O_RDWR);  // 打开文件获得fd
char buf[100];
read(fd, buf, 100);  // 通过fd读取数据
write(fd, "hello", 5);  // 通过fd写入数据
close(fd);  // 通过fd关闭文件
fd 名称 默认关联对象 常见用途
0 标准输入(stdin) 键盘 / 管道输入 read(fd, ...)读取输入数据
1 标准输出(stdout) 显示器 / 文件输出 write(fd, ...)输出结果
2 标准错误(stderr) 显示器/ 文件错误输出 打印错误信息

了解文件操作的过程

 现在知道了 fd是文件操作不可或缺的,那文件是怎么操作的呢

首先进程是系统资源分配和调度的基本单位,整个系统的运行围绕进程展开,一个进程运行时对应多个文件

打开并读写的过程:前提先申请一块空间buffer存放文件内容

1,打开打开文件后由读进程在众多文件中找到fd对应的文件描述符表(指针数组)地址,

2,在相应的地址存放着结构体文件,而结构体(struct_file)上有存有从磁盘加载到缓冲区的目标文件。

⽂件描述符的分配规则:在files_struct数组当中,找到 当前没有被使⽤的最⼩的⼀个下标,作为新的⽂件描述符。

 文件描述符 = 图书馆座位号

想象操作你在饭馆买饭堂食,系统是一个餐馆,每个文件是一个位置,而文件描述符就是你拿餐时拿到的座位号。餐馆有固定的座位编号(0, 1, 2, 3...),服务员会优先分配最小的空座位给你。

操作系统内部用一个数组(files_struct)记录所有文件描述符的使用情况。数组下标就是座位号(文件描述符),数组元素记录这个座位是否被占用(文件是否被打开)。

在操作系统中,进程通过 “文件描述符”(File Descriptor)管理打开的文件。每个进程维护一个文件描述符表,记录当前打开的文件句柄(如fd=3对应某个已打开的文本文件)。这进一步说明:文件是进程管理的资源,进程是操作文件的主体

代码示例

标准输入 / 输出 / 错误 = 永远预占的 VIP 座位

每个程序启动时,默认占用前三个座位:

  • 0 号座位(标准输入):默认连接键盘
  • 1 号座位(标准输出):默认连接屏幕
  • 2 号座位(标准错误):默认连接屏幕

 正常打开文件

#include <stdio.h>
#include <fcntl.h>

int main() {
    int fd = open("myfile", O_RDONLY);
    printf("fd: %d\n", fd);  // 输出3(0、1、2已被占用)
    close(fd);
    return 0;
}
关闭标准输入后打开文件
#include <stdio.h>
#include <fcntl.h>

int main() {
    close(0);  // 释放0号座位(标准输入)
    int fd = open("myfile", O_RDONLY);
    printf("fd: %d\n", fd);  // 输出0(最小可用座位)
    close(fd);
    return 0;
}

正常打印是3 ,但是0空出来了,就被占用了。


网站公告

今日签到

点亮在社区的每一天
去签到