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

一、理解文件

a.

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

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

b.

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

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

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

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

c.

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

d.

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

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

e.

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

二、文件操作C语言接口

1.打开文件

c 复制代码
FILE* fopen(const char* filename, const char* mode);
  • 返回值 :成功返回文件指针,失败返回 NULL
  • 模式
模式 含义 文件不存在 文件已存在
r 只读 报错 从头读
w 只写 创建新文件 清空重写
a 追加 创建新文件 末尾写入不换行
r+ 读写 报错 从头读写
w+ 读写 创建新文件 清空重写
a+ 读写 创建新文件 末尾读写

2.关闭文件

c 复制代码
int fclose(FILE* stream);
  • 必须关闭文件,否则会内存泄漏、数据丢失
  • 成功返回 0,失败返回 EOF

3.字符串读写

1) fgets - 取一行字符串

c 复制代码
char* fgets(char* str, int n, FILE* stream);c
  • 读取最多 n-1 个字符,自动加 \0,遇到换行 / 文件尾停止
  • 成功返回 str,失败 / 到末尾返回 NULL

2) fputs - 入一行字符串

c 复制代码
int fputs(const char* str, FILE* stream);
  • 写入字符串(不含末尾 \0),成功返回非负数,失败返回 EOF

4.读写位置

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

1) fseek - 移动文件指针

c 复制代码
int fseek(FILE* stream, long offset, int whence);
  • offset:偏移量(正数向后,负数向前)
  • whence:起始位置
    • SEEK_SET:文件开头
    • SEEK_CUR:当前位置
    • SEEK_END:文件末尾

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

c 复制代码
long ftell(FILE* stream);
  • 返回距离文件开头的字节数

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

c 复制代码
void rewind(FILE* stream);

示例

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

三、文件操作系统级调用

1.打开文件

c 复制代码
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.关闭文件

c 复制代码
int close(int fd);
  • 返回值:成功0;失败-1。

3.写文件

c 复制代码
ssize_t write(int fd, const void *buf, size_t count);

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

第三个参数count细节

  1. 字符串
c 复制代码
char str[] = "hello";
write(fd, str, strlen(str));

只写有效字符,不写 \0

  1. 写整个数组(含末尾 0)
c 复制代码
write(fd, str, sizeof(str));

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

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

四、文件描述符

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

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

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

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

c 复制代码
#include <stdio.h>
extern FILE *stdin;	// 0
extern FILE *stdout;// 1
extern FILE *stderr;// 2

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

c 复制代码
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); 可以执行,但极易出问题

核心坑点

  1. printf、puts 全部失效

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

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

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

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

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

示例直观演示

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

衍生常见坑

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

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

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

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

c 复制代码
int dup2(int oldfd, int newfd);

oldfd 复制覆盖到newfd ,剩下oldfd

  1. 关闭 newfd原本占用的文件
  2. 让 newfd 和 oldfd 指向同一个文件
  3. 返回值:成功返回newfd,失败 - 1

(2)输出重定向实例

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

(3)关键特点

  1. 两个 fd 操作同一个文件,读写位置共享
  2. 关闭其中一个,另一个仍可用
  3. 执行后原有 newfd 功能失效

五、再看"一切皆文件"

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

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

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

六、理解缓冲区

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

2. 语言级缓冲区

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

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

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

c 复制代码
struct FILE {
    int fd;
    char inbuffer[];
    char outbuffer[];
    ...
}

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

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

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

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

3. 测试缓冲区现象

(1)

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

刷新缓冲区的本质

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

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

刷新策略:

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

(2)

c 复制代码
int main() {
	printf("hello world");
	sleep(2);
	exit(0);	// 进程结束
	return 0;
}
c 复制代码
int main() {
	printf("hello world");
	sleep(2);
	_exit(0);	// 进程终止
	return 0;
}

对比exit()_exit()

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

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

(3)

c 复制代码
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;
}

现象原因:

  1. 向文件写,全缓冲
  2. 语言级函数有语言级缓冲区,系统函数没有;
  3. 分支进程。

具体过程:

首先系统级函数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. 例子

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

在shell中运行:

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

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

5. 为什么要设计 stderr?

如果没有 stderr:

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

有了 stderr:

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

4. 例子

c 复制代码
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:

  • 正常信息 → 可以重定向走
  • 错误信息 → 永远能看到
  • 错误输出不缓冲、立刻打印,保证崩溃前能看到错误
相关推荐
子榆.12 小时前
CANN自定义GEMM算子(Ascend C手写高性能矩阵乘法)
c语言·开发语言·矩阵
代码中介商13 小时前
Git 版本控制完全指南:从分支管理到远程协作
linux·git
s_w.h13 小时前
【 linux 】进程的调度算法
linux·运维·服务器
c++逐梦人13 小时前
多路转接epoll
linux·网络·epoll
r-t-H13 小时前
KVM虚拟化与Docker基础实践-第三章
linux·运维·nginx·docker·容器
仰泳之鹅13 小时前
【C语言】动态内存管理
c语言·数据结构·算法
嘿嘿嘿x313 小时前
Linux-知识点1-$-POSIX等
linux·ubuntu
艾莉丝努力练剑13 小时前
【Linux网络】Linux 网络编程:传输层UDP
linux·运维·服务器·网络·计算机网络·udp
陈eaten13 小时前
centos 7等保整改学习
linux·运维·服务器·网络安全·centos·等保