Linux 基础 IO 核心知识总结:从系统调用到缓冲区机制(一)
一,理解文件
1.1普遍理解
- 文件在磁盘里,磁盘是永久储存的,那文件在磁盘上的存储是永久性的
磁盘是外设(即是输出设备也是输入设备)
对文件的操作通过 IO 完成,磁盘作为外设是 IO 操作的对象之一
1.2在Linux角度
Linux下一切皆文件,如 显示器文件,磁盘,键盘都是文件。
1.3内存角度
对于空文件(0 kb)也在要磁盘上占有空间,而文件=内容➕属性,因此所有的操作都是围绕着文件内用与文件属性展开的。
1.4系统角度
对文件的操作本质是进程对文件的操作,磁盘的管理者是操作系统,所以文件的操作不是通过某种语言实现的,而是通过文件相关的系统调用接口来实现的。
总结:文件是逻辑数据单元,磁盘是物理存储载体,进程通过系统调用磁盘驱动 IO 操作文件。
二,C语言的文件
2.1熟悉接口
1.打开文件:fopen
按指定模式打开文件,返回文件操作指针,失败返回
NULL
。
filename
:文件名(如"log.txt"
),可带路径(如"/home/user/log.txt"
)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?
安全默认值:
防止意外创建高权限文件(如所有人可写的配置文件)。
示例:若 umask 为0000
,open("passwd", 0600)
会因疏忽暴露密码文件。用户自定义:
用户可通过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空出来了,就被占用了。