文章目录
- 🌈 一、前言
- 🌈 二、文件操作的系统接口
- ⭐ 1. 打开文件 open
- ⭐ 2. 关闭文件 close
- ⭐ 3. 写入文件 write
- ⭐ 4. 读取文件 read
- 🌈 三、文件描述符
- ⭐ 1. 文件描述符介绍
- ⭐ 2. 提前被分配的文件描述符 0 1 2
- ⭐ 3. 文件描述符的分配规则
- 🌈 四、重定向和缓冲区
- ⭐ 1. 重定向
- ⭐ 2. 缓冲区
- ⭐ 3. dup2 实现直接重定向
- 🌈 五、文件系统
- ⭐ 1. 对磁盘进行分区管理
- ⭐ 2. 对磁盘分区进行分组管理
- ⭐ 3. 如何进行分组管理
- ⭐ 4. inode 介绍
- 🌈 六、软硬链接
- ⭐ 1. 硬链接
- ⭐ 2. 软链接
- 🌈 七、动静态库
- ⭐ 1. 静态库
- 1.1 静态库介绍
- 1.2 生成静态库
- 1.3 使用静态库
- ⭐ 2. 动态库
- 2.1 动态库介绍
- 2.2 生成动态库
- 2.3 使用动态库
🌈 一、前言
1. 文件的概念
- 文件 = 内容 + 属性,所有对文件的操作无非就是两种:对文件内容操作、对文件属性操作。
- 文件的内容和属性本质上都是数据。存储文件必须既存储内容数据,又存储属性数据。
- 进程想要访问一个文件的时候,都需要先将该文件打开,文件在打开前后处于两种状态。
- 打开文件前:文件处在磁盘当中,就是个普通的文件,进程无权访问。
- 打开文件后:将文件加载到内存,此时进程才有权力访问该文件的数据。
- 进程可以打开多个文件,被打开的文件可能会存在多个,因此就需要管理起来。
- 文件在被打开前,必须要先形成一个用于描述被打开文件的结构体。
- 进程打开文件的操作是通过 OS 执行的,因此 OS 一定要提供系统调用接口。
2. 管理被打开的文件
- 每一个被打开的文件都要创建对应的描述结构体。
- 将这些结构体使用链表管理,因此对被打开文件的管理就变成了对链表的管理。
🌈 二、文件操作的系统接口
- 对文件进行操作,除了调用 C 接口(C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问。
系统接口需要的头文件
- 很显然需要的头文件比直接调用 C 语言的接口需要的多,说明语言会对这些接口进行封装。
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
⭐ 1. 打开文件 open
1. 函数参数
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
int open(
const char* pathname, // 表示要打开的文件的所在路径,如果只有文件名则默认在当前路径
int flags, // 表示打开文件的方式
mode_t mode); // 给文件赋予权限,可不提供该参数 (新文件权限随机,旧文件不变)
2. flags 参数的常用选项及其功能
- 必须的选项: 只能指定其中一个,以下三个选项同时只能存在一个,但每个都可以与可选的选项进行组合。
选项 | 说明 |
---|---|
O_RDONLY | 以只读的方式打开文件 |
O_WRONLY | 以只写的方式打开文件 |
O_RDWR | 以读写的方式打开文件 |
- 可选的选项:可以指定零个或多个,以下选项通过或操作实现执行多个选项。
- 如:O_WRONLY | O_CREAT | O_TRUNC 表示同时执行这 3 个选项的功能。
选项 | 说明 |
---|---|
O_CREAT | 如果文件不存在,则创建它。需要第三个参数 mode 来指定新文件的权限。 |
O_EXCL | 和 O_CREAT 一起使用时,如果文件已存在,则 open 失败,确保文件是新建的。 |
O_TRUNC | 如果文件已存在且以写方式打开,则将文件内容清空。 |
O_APPEND | 以追加的方式打开文件,写入的数据会添加到文件末尾。 |
O_NONBLOCK | 对于设备文件,以非阻塞方式打开 |
O_SYNC | 将写操作同步到磁盘。 |
O_DSYNC | 类 O_SYNC,但只同步写入操作,不包括元数据的更新 |
3. 函数返回值
- open 接口会返回一个文件描述符,如果返回 -1 则表示文件打开失败。
4. 使用实例
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
int main()
{
// 在当前目录以写方式打开文件,如果该文件不存在则创建并赋予 270 的权限
int fd = open("log.txt", O_WRONLY | O_CREAT, 0270);
// 打开文件失败
if (fd < 0)
{
perror("open");
return 1;
}
// 操作文件...
// 关闭文件
close(fd);
return 0;
}
⭐ 2. 关闭文件 close
1. 函数参数
#include <unistd.h>
int close(int fd); // 关闭文件描述符 fd 所表示的文件
2. 函数返回值
- 关闭文件成功:返回 0
- 关闭文件失败:返回 -1
⭐ 3. 写入文件 write
- 向一个文件描述符描述的文件当中进行写入。
1. 函数参数
#include <unistd.h>
ssize_t write(
int fd, // 文件描述符,表示要写入的是哪个文件
const void *buf, // 要写入到文件中的字符串的起始地址
size_t count); // 要写入到文件当中的字节数
2. 函数返回值
- write 函数的返回值表示实际写入到文件中的字节数。
3. 函数用例
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
int main()
{
// 打开文件
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if (fd < 0) // 打开文件失败
{
perror("open");
return 1;
}
// 操作文件 (将 msg 写入 fd 所描述的文件)
const char* msg = "hello file system call\n";
write(fd, msg, strlen(msg));
// 关闭文件
close(fd);
return 0;
}
⭐ 4. 读取文件 read
- 从指定文件当中读取指定字节的内容到字符串中。
1. 函数参数
#include <unistd.h>
ssize read(
int fd, // 从文件描述符 fd 指定的文件中读取内容
void* buf, // 将读取到的内容放到 buf 中
size_t count); // 指明希望从文件中读取的字节数
2. 函数返回值
- 读取成功:read 函数的返回值表示实际从文件中读取到的字节数。
- 读取失败:read 函数的返回值是 -1。
3. 函数用例
- 当前有一个内容为 hello world! 的文件 log.txt,现在要使用 read 读取该文件的内容并放入到指定字符串中。
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#define FILE_NAME "log.txt"
int main()
{
// 以只读方式打开该文件
int fd = open(FILE_NAME, O_RDONLY);
// 从指定文件中读取 100 个字节的内容放到 buffer 中
char buffer[1024];
ssize_t n = read(fd, buffer, 100);
printf("实际读取字节数: %d, 读取内容: %s\n", n, buffer);
// 关闭文件
close(fd);
return 0;
}
🌈 三、文件描述符
⭐ 1. 文件描述符介绍
- 文件描述符就是一个从 0 开始连续的小整数。当进程打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了 file 结构体,表示一个已经打开的文件对象。
- 而进程执行 open 系统调用,必须让进程和文件关联起来。每个进程都有一个指针 *files, 指向一张表 files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向被打开的文件的指针!
- 所以,本质上文件描述符就是该数组下标,只要拿着文件描述符,就可以找到对应的文件。
⭐ 2. 提前被分配的文件描述符 0 1 2
- Linux 进程在默认清空下会有 3 个默认打开的文件描述符,分别是标准输入 0 (stdio),标准输出 1 (stdout),标准错误 2 (stderr)。
- 0、1、2 对应的物理设备一般是:键盘、显示器、显示器。
- 也就是说,我们自己的文件的文件描述符都是从 3 开始计数的。
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
int main()
{
// 分别创建 4 个文件,并用 4 个下标处的文件指针指向这些文件
int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd3 = open("log3.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd4 = open("log4.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
// 打印默认 3 个打开文件和 4 个自己文件的文件描述符
printf("stdin: %d\n", stdin->_fileno); // 标准输入
printf("stdout: %d\n", stdout->_fileno);// 标准输出
printf("stderr: %d\n", stderr->_fileno);// 标准错误
printf("fd1: %d\n", fd1);
printf("fd2: %d\n", fd2);
printf("fd3: %d\n", fd3);
printf("fd4: %d\n", fd4);
// 关闭 4 个文件描述符所指向的文件
close(fd1);
close(fd2);
close(fd3);
close(fd4);
return 0;
}
⭐ 3. 文件描述符的分配规则
1. 分配规则
- 在 files_struct 数组当中,找到当前未被使用的最小下标,作为新的文件描述符。
- 如果直接把 0 号描述符所描述的文件观点,此时最小未被使用下标就是 0,下次再打开新文件,该文件的文件描述符就是 0。
int main()
{
close(0); // 关闭标准输入文件
int fd = open("log.txt", O_RDONLY);
printf("新打开文件的文件描述符: %d\n", fd);
close(fd);
return 0;
}
🌈 四、重定向和缓冲区
⭐ 1. 重定向
1. 关闭 1 号文件
- 由于 1 号文件表示的是标准输出 stdout 即显示器,如果将显示器文件关闭,之后新打开文件的文件描述符就会是 1。
- 那么所有原本要输出到显示器上的内容都会被重定向输出到新打开文件中。
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#define FILE_NAME "log.txt"
int main()
{
close(1); // 关闭显示器文件
int fd = open(FILE_NAME, O_WRONLY); // 以只读方式打开指定文件
printf("fd: %d\n", fd); // printf 只认 1 号文件,向 1 号文件写入
fflush(stdout); // 刷新缓冲区,立马将缓冲区数据交给文件
close(fd); // 关闭新打开的文件
return 0;
}
2. 重定向的本质
- 重定向的本质就是修改文件描述符表特定数组下标中的内容。
- 让某个下标中存着的 FILE* 指针从指向 A 文件,变成指向 B 文件就是重定向。
⭐ 2. 缓冲区
- 输出内容不是直接将内容输出到文件的,而是先将内容存放在缓冲区中,等到缓冲区囤积了一定量的数据之后才一次性交给文件。
- 在重定向的代码中,如果不使用 fflush(stdout) 冲刷缓冲区,那么并不会立马将缓冲区内容输出到自己的 log.txt 文件中。
int main()
{
close(1);
int fd = open(FILE_NAME, O_WRONLY | O_TRUNC);
printf("fd: %d\n", fd);
// fflush(stdout); // 不立马将缓冲区中的数据交给文件
close(fd);
return 0;
}
- printf 输出的内容是会先存放在输出缓冲区 stdout 中的,然后再将缓冲区中的内容刷新到 log.txt 文件中。
- 在进程退出前,调用了 close(fd)。如果不使用 flush 强制将缓冲区中的内容立刻刷新到 log.txt 中,那么在关闭文件之后,缓冲区的内容自然也就进不到文件中。
- 进程在退出时一般是要刷新缓冲区的,但是此时 log.txt 文件已经被关闭了,也无法将缓冲区的内容刷给 log.txt 了,因此 log.txt 文件中就什么也没有了。
⭐ 3. dup2 实现直接重定向
- 如果每次执行重定向都要利用文件描述符的分配规则 (先将文件描述符靠前文件关闭,再打开自己的文件),未免显得太折磨了点。
1. 如何直接实现重定向
- 使用dup2 系统调用函数,更改文件描述符数组下标对应的内容 ( file* 文件指针所指向的文件) 来完成重定向。
- 如:3 号文件指向的是 log.txt 文件,只需要将 3 号下标的内容拷贝覆盖到 1 号,就可以直接实现重定向。
2. dup2 函数介绍
#include <unistd.h>
int dup2(int oldfd, int newfd);
- 将文件描述符数组中 oldfd 下标处的内容拷贝覆盖到 newfd 下标处,使得两个下标处的内容都和 oldfd 的一样。
3. dup2 函数用例
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#define FILE_NAME "log.txt"
int main()
{
// 以追加写的方式打开 log.txt 文件
int fd = open(FILE_NAME, O_WRONLY | O_APPEND);
// 将 fd 下标处的内容覆盖到下标 1 处实现输出重定向
// 这样之后往 1 号文件输出的内容就变成了往 log.txt 文件输出
dup2(fd, 1);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
close(fd);
return 0;
}
🌈 五、文件系统
文件管理工作 (文件系统)
- 对打开的被加载到内存中的文件进行管理。
- 对未打开的在硬盘中的文件也要进行管理。
⭐ 1. 对磁盘进行分区管理
- 没打开的文件都在磁盘上放着,也需要在磁盘中进行管理。
- 未被进程打开的文件在磁盘中按照一定规律存储,方便用户随时读取。
- 这于这部分未打开文件的核心工作是快速定位文件 。
- 快速定位文件是通过查找文件路径实现的。
- 磁盘都是很大的,500G 的空间如果要全部管理起来会很费劲,但是由于所有的磁盘区域都能够使用同一个管理方法,因此可以对磁盘使用分区管理。
- 500G 和 100G 的管理方法一样,将 500G 分成 5 个 100G 管理就会轻松很多。
- 如:将一块磁盘分成 C D E F … 盘,管理起来明显会变轻松。
⭐ 2. 对磁盘分区进行分组管理
- 管理一块 500G 的磁盘可以复用管理一块 100G 区域的管理方式,这 100G 的空间也可以划分成块组进行管理。
- 不同文件系统有不同的分组方式,此时假设以 2G 为一块组对一个 100G 的磁盘分区划分出 50 个块组。
- 只要能管理好 1 个组,就能管理 50 个块组 (1 个磁盘分区),能管理好 1 个磁盘分区就能管理好整个磁盘。
⭐ 3. 如何进行分组管理
1. 每个块组分为如下部分
- 超级块 (Super Block):存放文件系统本身的结构信息,只有个别和文件系统有关的块组有这玩意。
- 超级块记录的信息主要有:block 和 inode 的总量, 未使用的 block 和 inode 的数量,一个 block 和一个 inode 的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。
- 超级块的信息一旦被破坏,整个文件系统的结构就会被破坏。
- 块组描述符表 (Group Descriptor Table):描述块组的使用情况。
- 块位图 (Block Bitmap):用位图记录 Data Blocks 中哪个数据块已经被占用,哪个没被占用。
- inode 位图 (inode Bitmap):每个 bit 都表示在 inode 表中的一个 inode 是否空闲可用。
- inode 表 (inode Table):inode 是一个用来存放文件属性的结构体,而 inode Table 则是用来存放 inode 的一个结构具体数组。
- 数据区 (Data Blocks):存放文件内容。整个数据区被分成若干个 4KB 大小的数据块。
2. 属性和数据分开存放
- 可以看出,每个块组都将属性和数据分开存放,这操作看着简单实际一点也不复杂。
⭐ 4. inode 介绍
1. inode 编号
- 一般情况下,每个文件都有属于自己的 inode 编号,inode 编号在整个分区具有唯一性。
- Linux 在内核当中,识别文件,和文件名无关,只认 inode 编号。
- 如何通过文件名找到 inode 编号?:每个目录内都会保存目录内文件的文件名和 inode 编号的映射关系,通过文件名即可映射找到对应的 inode 编号,这也是为什么同一个目录下不允许有同名文件的原因。
2. inode 结构体
- 每个文件都有自己的属性和内容,属性的种类是有限的 & 每个文件都一样。
- inode 结构体 就是用来存放文件属性的。
- 每个 inode 结构体 的固定大小都是 128 字节。
// 文件属性是通过 inode 结构体保存的
// 每个 struct inode 的大小都是 128 字节
struct inode
{
// 文件属性: 文件大小、权限、所属组、ACM 时间、inode 编号等
blocks[15]; // 记录 Data Blocks 中有哪些数据块属于同一个 inode 编号所表示的文件
};
3. inode 表 (inode table)
- inode 节点表,顾名思义就是一张用来存放 inode 结构体的表 (数组),inode 编号就是这个数组的下标。
🌈 六、软硬链接
1. 链接介绍
- 真正在磁盘上找文件靠的并不是文件名,而是 inode,只不是文件名映射了对应的 inode。
- 在 Linux 中可以让多个文件名对应同一个 inode (通过不同的名称访问相同的文件或目录内容)。这种操作称之为链接。
2. 为什么要存在链接
- 某些文件会存储在很深的路径底下,不方便寻找。可以通过链接的方式链接这个藏很深的文件,然后将链接文件放在桌面 (或其他地方),实现在桌面点击快捷方式直接访问该文件。
3. 链接的使用场景
- 快速定位文件。
⭐ 1. 硬链接
- 硬链接通过 inode 编号引用另一个文件,该链接方式不能应用于目录,只能应用于文件。
- 硬链接不是独立的文件,因为它没有自己的 inode 编号。
0. 硬链接是什么?
- 硬链接是在指定目录内部的一组映射关系 (文件名 <=> inode 编号)。
1. 建立硬链接命令
ln 目标文件路径名 链接名 // 用自己定义的链接文件去链接目标文件
2. 建立硬链接示例
- 让 hello.hard.link 文件去链接 hello 文件,这两个文件都是同一个文件,有相同的 inode 编号。
- 建立硬链接相当于对 2228447 这个文件进行重命名,建立硬链接之后就可以删除老文件名了。
3. 硬链接使用示例
- 往 hello.hard.link 中写入一串字符,使用 cat 打印 hello.hard.link 和 hello 文件中的内容。
4. 删除硬链接命令
unlink 硬链接名
⭐ 2. 软链接
- 不同于硬链接,软链接通过名字引用另一个文件。
- 软链接是独立的文件,因为它有自己的 inode 编号。
1. 建立软链接命令
ln -s 目标文件或目录路径名 链接名 // 用自己定义的链接文件去链接目标文件或目录
2. 建立软链接示例
- 让 log.soft.link 文件去链接 log 文件,这两个文件都是独立的文件,有不同的 inode 编号。
3. 软链接使用示例
- 往 log.soft.link 文件中写入一串字符,log 文件和 log.soft.link 文件都发生了变化。
🌈 七、动静态库
⭐ 1. 静态库
1.1 静态库介绍
- 静态库(.a 文件):编译时链接,程序在编译链接的时候把库的代码链接到可执行文件中。程序运行时将不再需要静态库。
1.2 生成静态库
- 以制作一个简单的计算器为例。
1. 创建相关文件
2. 编写相关文件
- 编写 add.h 和 add.c (剩下几个文件逻辑一样,就不贴出来了)。
3. 编译相关文件
- 只用将 add.c、sub.c、mul.c、div.c、编译成 .o 文件即可。
4. 生成静态库
- 将所有的 .o 文件进行链接形成一个可执行。
- 生成静态库指令:
ar -rc libmylib.a file1.o fil2.o ... filen.o
。- mylib 是你的静态库文件名 (名字可自定义)。
- file1.o ~ filen.o 是生成静态库文件时所依赖的编译文件,在本例中是 add.o、sub.o、mul.o、div.o 文件。
- mylib 就是生成的静态库的真实名字。
1.3 使用静态库
1. 编写 & 编译测试文件
- 创建一个 main.c 文件,在该文件中使用上述加减乘除 4 个模块的库函数。
#include "add.h"
#include "sub.h"
#include "mul.h"
#include "div.h"
int main()
{
int x = 10, y = 20;
printf("%d + %d = %d\n", x, y, add(x, y));
printf("%d - %d = %d\n", x, y, sub(x, y));
printf("%d * %d = %d\n", x, y, mul(x, y));
printf("%d / %d = %d\n", x, y, div(x, y));
return 0;
}
2. 编译测试文件并与静态库链接
- 使用如下指令将 main.c 文件编译并将编译后的文件与静态库 mylib 链接。
- 库搜索路径: -L 指定库所在的路径,-l 表示链接指定的库
g++ -o test.exe main.c -L. -lmylib
3. 删除静态库
- 目标文件生成后,静态库删掉,程序照样可以运行。因为静态库中的代码已经全部加载到生成的可执行文件中了。
⭐ 2. 动态库
2.1 动态库介绍
- 动态库(.so 文件):运行时链接,程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
- 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。
- 操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间
2.2 生成动态库
- 生成动态库的例子依然是制作一个简单的计算器,所建立的相关文件依然是下面这些。
加法模块 : add.h add.c
减法模块 : sub.h sub.c
乘法模块 : mul.h mul.c
除法模块 : div.h div.c
测试模块 : main.c
1. 生成动态库指令
- 编译源代码为位置无关的代码 (PIC):
- 在使用 gcc / g++ 编译文件时,使用 -fPIC 选项编译你的源代码文件,生成 .o 文件。
gcc -fPIC -c source.c -o source.o
g++ -fPIC -c source.c -o source.o
- 生成动态库:使用 -shared 选项 和 gcc / g++ 将对象文件链接成动态库 (.so 文件)。
- 其中,libmylib.so 文件为动态库文件名,开头的 lib 和结尾的 .so 为必带选项,中间的 mylib 才是能让你自定义的动态库名。
- source1.o ~ sourcen.o 是生成动态库所要依赖的编译对象文件,可依赖多个。
gcc -shared -o libmylib.so source1.o source2.o sourcen.o
g++ -shared -o libmylib.so source1.o source2.o sourcen.o
2. 生成动态库实例
- 编译源代码为位置无关代码:将 add.c、sub.c、mul.c、div.c 分别编译成各自对应的 .o 文件。
- 生成动态库:依赖编译出的 4 个 .o 文件生成动态库。
2.3 使用动态库
1. 链接动态库指令
- 在使用 gcc / g++ 编译测试文件时,使用 -L 和 -l 选项链接动态库。
- -L 和 -l 选项的功能和链接静态库时一样,就不过多赘述。
gcc -o myprogram main.c -L. -lmylib
g++ -o myprogram main.c -L. -lmylib
2. 链接动态库实例
3. 运行动态库
- 在运行使用动态库的程序前,需要确保动态库文件(libmymath.so)在库搜索路径中。
- 方法 1:将 .so 文件拷贝到系统共享库路径下, 一般指 /usr/lib (不推荐把自己写的库放进去,推荐放别人的)。
- 方法 2:更改 LD_LIBRARY_PATH 环境变量来指明 .so 文件所在路径。
- 这里就采用第 2 中方法来运行动态库。