Linux管理文件

静态文件与inode

文件存放在磁盘文件系统中,并且以一种固定的形式进行存放,称为静态文件。调用 open 函数的时候,会将文件数据(文件内容)从磁盘等块设备读取到内存中,将文件数据在内存中进行维护,内存中的这份文件数据我们就把它称为动态文件。

硬盘的最小存储单位叫做“扇区”(Sector),每个扇区储存 512 字节(相当于 0.5KB),操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个“块”(block)。这种由多个扇区组成的“块”,是文件存取的最小单位。“块”的大小,最常见的4KB,即连续八个 sector 组成一个 block。

磁盘在进行分区、格式化的时候会将其分为两个区域,一个是数据区,用于存储文件中的数据;
另一个是 inode 区,用于存放 inode table(inode 表),inode table 中存放的是一个一个的 inode(也成为 inode节点),不同的 inode 就可以表示不同的文件,每一个文件都必须对应一个 inode。

2022-11-10-142428.png

打开一个文件,系统内部会将这个过程分为三步:

  1. 系统找到这个文件名所对应的 inode 编号;
  2. 通过 inode 编号从 inode table 中找到对应的 inode 结构体;
  3. 根据 inode 结构体中记录的信息,确定文件数据所在的 block,并读出数据。

调用 open 函数去打开文件的时候,内核会申请一段内存(一段缓冲区),并且将静态文件的数据内容从磁盘这些存储设备中读取到内存中进行管理、缓存(也把内存中的这份文件数据叫做动态文件、内核缓冲区)。打开文件后,以后对这个文件的读写操作,都是针对内存中这一份动态文件进行相关的操作,而并不是针对磁盘中存放的静态文件。

因为磁盘、硬盘、U 盘等存储设备基本都是 Flash 块设备,因为块设备硬件本身有读写限制等特征,块设备是以一块一块为单位进行读写的(一个块包含多个扇区,而一个扇区包含多个字节),一个字节的改动也需要将该字节所在的 block 全部读取出来进行修改,修改完成之后再写入块设备中,所以导致对块设备的读写操作非常不灵活;而内存可以按字节为单位来操作,而且可以随机操作任意地址数据,非常地很灵活。

内核会为每个进程设置一个专门的数据结构用于管理该进程,譬如用于记录进程的状态信息、运行特征等,我们把这个称为进程控制块(Process control block,缩写PCB)。
PCB 数据结构体中有一个指针指向了文件描述符表(File descriptors),文件描述符表中的每一个元素索引到对应的文件表(File table),文件表也是一个数据结构体。

返回错误处理与errno

在 Linux 系统下对常见的错误做了一个编号,每一个编号都代表着每一种不同的错误类型,当函数执行发生错误的时候,操作系统会将这个错误所对应的编号赋值给 errno 变量,每一个进程(程序)都维护了自己的 errno 变量,它是程序中的全局变量,该变量用于存储就近发生的函数执行错误编号,也就意味着下一次的错误码会覆盖上一次的错误码。

errno 本质上是一个 int 类型的变量,用于存储错误编号,但是需要注意的是,并不是执行所有的系统调用或 C 库函数出错时,操作系统都会设置 errno。

常用的函数perror函数

一般用的最多的还是这个函数,调用此函数不需要传入 errno,函数内部会自己去获取 errno 变量的值,调用此函数会直接将错误提示字符串打印出来,而不是返回字符串。

函数原型

1
2
#include <stdio.h>
void perror(const char *s);

函数参数和返回值含义如下:
s:在错误提示字符串信息之前,可加入自己的打印信息,也可不加,不加则传入空字符串即可。
返回值:void 无返回值。

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(void)
{
int fd;
/* 打开文件 */
fd = open("./test_file", O_RDONLY);
if (-1 == fd) {
perror("open error");
return -1;
}
close(fd);
return 0;
}

exit、__exit、_Exit

在 Linux 系统下,进程(程序)退出可以分为正常退出和异常退出,注意这里说的异常并不是执行函数出现了错误这种情况,异常往往更多的是一种不可预料的系统异常,可能是执行了某个函数时发生的、也有可能是收到了某种信号等.

main 函数中使用 return 后返回,return 执行后把控制权交给调用函数,结束该进程。调用_exit()函数会清除其使用的内存空间,并销毁其在内核中的各种数据结构,关闭进程的所有文件描述符,并结束进程、将控制权交给操作系统。exit()是一个标准 C 库函数,而_exit()和_Exit()是系统调用。

__exit()函数原型

1
2
#include <unistd.h>
void _exit(int status);

_Exit()函数原型

1
2
#include <stdlib.h>
void _Exit(int status);

exit()函数

1
2
#include <stdlib.h>
void exit(int status);

空洞文件

使用 write()函数对文件进行写入操作,也就是说此时将是从偏移文件头部 6000 个字节处开始写入数据,也就意味着 4096~6000 字节之间出现了一个空洞,因为这部分空间并没有写入任何数据,所以形成了空洞,这部分区域就被称为文件空洞,那么相应的该文件也被称为空洞文件。
文件空洞部分实际上并不会占用任何物理空间,直到在某个时刻对空洞部分进行写入数据时才会为它
分配对应的空间,但是空洞文件形成时,逻辑上该文件的大小是包含了空洞部分的大小的

空洞文件对多线程共同操作文件是及其有用的,有时候我们创建一个很大的文件,如果单个线程从头开始依次构建该文件需要很长的时间,有一种思路就是将文件分为多段,然后使用多线程来操作,每个线程负责其中一段数据的写入。

eg:新建一个空洞文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(void)
{
int fd;
int ret;
char buffer[1024];
int i;
/* 打开文件 */
fd = open("./hole_file", O_WRONLY | O_CREAT | O_EXCL,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 将文件读写位置移动到偏移文件头 4096 个字节(4K)处 */
ret = lseek(fd, 4096, SEEK_SET);
if (-1 == ret) {
perror("lseek error");
goto err;
}
/* 初始化 buffer 为 0xFF */
memset(buffer, 0xFF, sizeof(buffer));
/* 循环写入 4 次,每次写入 1K */
for (i = 0; i < 4; i++) {
ret = write(fd, buffer, sizeof(buffer));
if (-1 == ret) {
perror("write error");
goto err;
}
}
ret = 0;
err:
/* 关闭文件 */
close(fd);
exit(ret);
}

示例代码中,使用 open 函数新建了一个文件 hole_file,在 Linux 系统中,新建文件大小是 0,也就
是没有任何数据写入,此时使用lseek函数将读写偏移量移动到4K字节处,再使用write函数写入数据0xFF,每次写入 1K,一共写入 4 次,也就是写入了 4K 数据,也就意味着该文件前 4K 是文件空洞部分,而后 4K数据才是真正写入的数据。

O_APPEND 和 O_TRUNC 标志

O_TRUNC 这个标志的作用是调用 open 函数打开文件的时候会将文件原本的内容全部丢弃,文件大小变为 0。

O_APPEND标志作用open函数携带了O_APPEND,调用 open 函数打开文件,当每次使用 write()函数对文件进行写操作时,都会自动把文件当前位置偏移量移动到文件末尾,从文件末尾开始写入数据,也就是意味着每次写入数据都是从文件末尾开始。

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char buffer[16];
int fd;
int ret;
/* 打开文件 */
fd = open("./test_file", O_RDWR | O_APPEND);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 初始化 buffer 中的数据 */
memset(buffer, 0x55, sizeof(buffer));
/* 写入数据: 写入 4 个字节数据 */
ret = write(fd, buffer, 4);
if (-1 == ret) {
perror("write error");
goto err;
}
/* 将 buffer 缓冲区中的数据全部清 0 */
memset(buffer, 0x00, sizeof(buffer));
/* 将位置偏移量移动到距离文件末尾 4 个字节处 */
ret = lseek(fd, -4, SEEK_END);
if (-1 == ret) {
perror("lseek error");
goto err;
}
/* 读取数据 */
ret = read(fd, buffer, 4);
if (-1 == ret) {
perror("read error");
goto err;
}
printf("0x%x 0x%x 0x%x 0x%x\n", buffer[0], buffer[1],
buffer[2], buffer[3]);
ret = 0;
err:
/* 关闭文件 */
close(fd);
exit(ret);
}

使用O_APPEND多次打开同一个文件测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
unsigned char buffer1[4], buffer2[4];
int fd1, fd2;
int ret;
int i;
/* 创建新文件 test_file 并打开 */
fd1 = open("./test_file", O_RDWR | O_CREAT | O_EXCL | O_APPEND,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (-1 == fd1) {
perror("open error");
exit(-1);
}
/* 再次打开 test_file 文件 */
fd2 = open("./test_file", O_RDWR | O_APPEND);
if (-1 == fd2) {
perror("open error");
ret = -1;
goto err1;
}
/* buffer 数据初始化 */
buffer1[0] = 0x11;
buffer1[1] = 0x22;
buffer1[2] = 0x33;
buffer1[3] = 0x44;
buffer2[0] = 0xAA;
buffer2[1] = 0xBB;
buffer2[2] = 0xCC;
buffer2[3] = 0xDD;
/* 循环写入数据 */
for (i = 0; i < 4; i++) {
ret = write(fd1, buffer1, sizeof(buffer1));
if (-1 == ret) {
perror("write error");
goto err2;
}
ret = write(fd2, buffer2, sizeof(buffer2));
if (-1 == ret) {
perror("write error");
goto err2;
}
}
/* 将读写位置偏移量移动到文件头 */
ret = lseek(fd1, 0, SEEK_SET);
if (-1 == ret) {
perror("lseek error");
goto err2;
}
/* 读取数据 */
for (i = 0; i < 8; i++) {
ret = read(fd1, buffer1, sizeof(buffer1));
if (-1 == ret) {
perror("read error");
goto err2;
}
printf("%x%x%x%x", buffer1[0], buffer1[1],
buffer1[2], buffer1[3]);
}
printf("\n");
ret = 0;
err2:
close(fd2);
err1:
/* 关闭文件 */
close(fd1);
exit(ret);
}

复制文件描述符

在 Linux 系统中,open 返回得到的文件描述符 fd 可以进行复制,复制成功之后可以得到一个新的文件描述符,使用新的文件描述符和旧的文件描述符都可以对文件进行 IO 操作,复制得到的文件描述符和旧的文件描述符拥有相同的权限,譬如使用旧的文件描述符对文件有读写权限,那么新的文件描述符同样也具有读写权限。

dup函数原型

1
2
#include <unistd.h>
int dup(int oldfd);

oldfd:需要被复制的文件描述符。
返回值:成功时将返回一个新的文件描述符,由操作系统分配,分配置原则遵循文件描述符分配原则;
如果复制失败将返回-1,并且会设置 errno 值

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
unsigned char buffer1[4], buffer2[4];
int fd1, fd2;
int ret;
int i;
/* 创建新文件 test_file 并打开 */
fd1 = open("./test_file", O_RDWR | O_CREAT | O_EXCL,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (fd1 == -1) {
perror("open error");
exit(-1);
}
/* 复制文件描述符 */
fd2 = dup(fd1);
if (fd2 == -1) {
perror("dup error");
ret = -1;
goto err1;
}
printf("fd1: %d\nfd2: %d\n", fd1, fd2);
/* buffer 数据初始化 */
buffer1[0] = 0x11;
buffer1[1] = 0x22;
buffer1[2] = 0x33;
buffer1[3] = 0x44;
buffer2[0] = 0xAA;
buffer2[1] = 0xBB;
buffer2[2] = 0xCC;
buffer2[3] = 0xDD;
/* 循环写入数据 */
for (i = 0; i < 4; i++) {
ret = write(fd1, buffer1, sizeof(buffer1));
if (ret == -1) {
perror("write error");
goto err2;
}
ret = write(fd2, buffer2, sizeof(buffer2));
if (ret == -1) {
perror("write error");
goto err2;
}
}
/* 将读写位置偏移量移动到文件头 */
ret = lseek(fd1, 0, SEEK_SET);
if (ret == -1) {
perror("lseek error");
goto err2;
}
/* 读取数据 */
for (i = 0; i < 8; i++) {
ret = read(fd1, buffer1, sizeof(buffer1));
if (ret == -1) {
perror("read error");
goto err2;
}
printf("%x%x%x%x", buffer1[0], buffer1[1],
buffer1[2], buffer1[3]);
}
printf("\n");
ret = 0;
err2:
close(fd2);
err1:
/* 关闭文件 */
close(fd1);
exit(ret);
}

dup 系统调用分配的文件描述符是由系统分配的,遵循文件描述符分配原则,并不能自己指定一个文件
描述符,这是 dup 系统调用的一个缺陷;而 dup2 系统调用修复了这个缺陷,可以手动指定文件描述符,而不需要遵循文件描述符分配原则。dup2函数原型:

1
2
#include <unistd.h>
int dup2(int oldfd, int newfd);

oldfd:需要被复制的文件描述符。
newfd:指定一个文件描述符(需要指定一个当前进程没有使用到的文件描述符)。
返回值:成功时将返回一个新的文件描述符,也就是手动指定的文件描述符 newfd;如果复制失败将返回-1,并且会设置 errno 值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int fd1, fd2;
int ret;
/* 创建新文件 test_file 并打开 */
fd1 = open("./test_file", O_RDWR | O_CREAT | O_EXCL,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (fd1 == -1) {
perror("open error");
exit(-1);
}
/* 复制文件描述符 */
fd2 = dup2(fd1, 100);
if (fd2 == -1) {
perror("dup error");
ret = -1;
goto err1;
}
printf("fd1: %d\nfd2: %d\n", fd1, fd2);
ret = 0;
close(fd2);
err1:
/* 关闭文件 */
close(fd1);
exit(ret);
}

文件共享

文件共享指的是同一个文件(譬如磁盘上的同一个文件,对应同一个 inode)被多个独立的读写体同时进行 IO 操作。同时进行 IO 操作指的是一个读写体操作文件尚未调用 close 关闭的情况下,另一个读写体去操作文件。

文件共享的意义有很多,多用于多进程或多线程编程环境中,譬如我们可以通过文件共享的方式来实现
多个线程同时操作同一个大文件,以减少文件读写时间、提升效率。
文件共享的核心是:如何制造出多个不同的文件描述符来指向同一个文件。其实方法在上面的内容中都已经给大家介绍过了,譬如多次调用 open 函数重复打开同一个文件得到多个不同的文件描述符、使用 dup()或 dup2()函数对文件描述符进行复制以得到多个不同的文件描述符。

常见三种文件共享实现方式

(1)同一个进程中多次调用 open 函数打开同一个文件

(2)不同进程中分别使用 open 函数打开同一个文件

(3)同一个进程中通过 dup(dup2)函数对文件描述符进行复制

原子操作和竞争冒险

[原子操作和竞争冒险]: https://copyfuture.com/blogs-details/202208241006207606 “竞争冒险”

截断文件

truncate()或ftruncate()可将普通文件截断为指定字节长度。

1
2
3
4
#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);

这两个函数都可以对文件进行截断操作,将文件截断为参数 length 指定的字节长度,什么是截断?如
果文件目前的大小大于参数 length 所指定的大小,则多余的数据将被丢失,类似于多余的部分被“砍”掉
了;如果文件目前的大小小于参数 length 所指定的大小,则将其进行扩展,对扩展部分进行读取将得到空字节”\0”。

使用 ftruncate()函数进行文件截断操作之前,必须调用 open()函数打开该文件得到文件描述符,并且必
须要具有可写权限,也就是调用 open()打开文件时需要指定 O_WRONLY 或 O_RDWR。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
int fd;
/* 打开 file1 文件 */
if (0 > (fd = open("./file1", O_RDWR))) {
perror("open error");
exit(-1);
}
/* 使用 ftruncate 将 file1 文件截断为长度 0 字节 */
if (0 > ftruncate(fd, 0)) {
perror("ftruncate error");
exit(-1);
}
/* 使用 truncate 将 file2 文件截断为长度 1024 字节 */
if (0 > truncate("./file2", 1024)) {
perror("truncate error");
exit(-1);
}
/* 关闭 file1 退出程序 */
close(fd);
exit(0);
}

Linux下的access函数

1
2
#include <unistd.h>
int access(const char *pathname, int mode);

pathname 是文件的路径名+文件名,mode:指定access的作用,取值如下

1
2
3
4
5
6
7
8
9
F_OK 值为0,判断文件是否存在

X_OK 值为1,判断对文件是可执行权限

W_OK 值为2,判断对文件是否有写权限

R_OK 值为4,判断对文件是否有读权限

注:后三种可以使用或“|”的方式,一起使用,如W_OK|R_OK