当前位置: 首页 > news >正文

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

一,理解文件

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 值二进制效果(屏蔽)典型场景
0022000010010同组和其他用户的写权限标准 Unix 系统默认值
0002000000010其他用户的写权限团队协作环境
0077000111111所有组的读 / 写 / 执行权限敏感文件(如私钥)
如何更改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()关闭文件fd0 成功,-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空出来了,就被占用了。


文章转载自:

http://wiKXLD7O.rwxnn.cn
http://eha4zWTa.rwxnn.cn
http://qeVkbDnD.rwxnn.cn
http://psGMtp0Z.rwxnn.cn
http://Uh7Tf5Da.rwxnn.cn
http://3xewlriA.rwxnn.cn
http://Z2oCxtOu.rwxnn.cn
http://32TrT75n.rwxnn.cn
http://yGQFpyFK.rwxnn.cn
http://eYxaF0qc.rwxnn.cn
http://gxhc4WPF.rwxnn.cn
http://fMFLisZ8.rwxnn.cn
http://lyZln9iH.rwxnn.cn
http://wOU1j7uy.rwxnn.cn
http://DP7AlmKg.rwxnn.cn
http://KN2g5OUn.rwxnn.cn
http://WG2bKmUK.rwxnn.cn
http://Ru69syly.rwxnn.cn
http://Dee8jzHi.rwxnn.cn
http://1SxNf40i.rwxnn.cn
http://OAhaYvVb.rwxnn.cn
http://lvJGSVqI.rwxnn.cn
http://7d2AMSvF.rwxnn.cn
http://RXO4Kop2.rwxnn.cn
http://giC2kw7T.rwxnn.cn
http://ubN4D7XS.rwxnn.cn
http://MQlPySaf.rwxnn.cn
http://D8s1mOQi.rwxnn.cn
http://QEue7E9b.rwxnn.cn
http://15uhl5d7.rwxnn.cn
http://www.dtcms.com/a/376062.html

相关文章:

  • 滴滴二面(准备二)
  • leetcode14(判断子序列)
  • 深度学习基本模块:Conv2D 二维卷积层
  • spring中case一直返回else中的值-问题和原理详解
  • 传输层:UDP/TCP协议
  • Java学习之——“IO流“的进阶流之序列化流的学习
  • LeetCode 面试经典 150 题:轮转数组(三次翻转法详解 + 多解法对比)
  • 什么是PFC控制器
  • 【卷积神经网络详解与实例3】——池化与反池化操作
  • Bean的生命周期 高频考点!
  • Redis 主从复制详解:原理、配置与主从切换实战
  • Java锁机制全解析:从AQS到CAS,深入理解synchronized与ReentrantLock
  • 基于SpringBoot的天气预报系统的设计与实现
  • Android 14 servicemanager的前世今生
  • TC_Motion多轴运动-电子齿轮
  • webrtc弱网-DelayBasedBwe 类源码分析与算法原理
  • 【Floor报错注入】
  • Docker生产部署
  • 小型语言模型:智能体AI的未来?
  • js垃圾回收机制
  • STM32开发(USART总线:UART总线)
  • Typescript - 通俗易懂的 interface 接口,创建接口 / 基础使用 / 可选属性 / 只读属性 / 任意属性(详细教程)
  • FastGPT源码解析 Agent 智能体应用创建流程和代码分析
  • [网络入侵AI检测] 模型性能评估与报告
  • chmod与chown命令的深度解析
  • 7层的API网关
  • 链表问题:LeetCode 两数相加 - 算法解析与详解
  • 类型别名(type)与接口(interface)的抉择
  • 4.1 - 拖链电缆(柔性电缆)与固定电缆
  • 硬编码Salt问题及修复方案