目录
1 Linux 系统如何管理文件
1.1 什么是静态文件
1.2 扇区(Sector)和块(Block)概念?
1.3 inode
1.4 进程控制块(PCB)
2 返回错误处理与 errno
2.1 errno变量介绍
2.3 perror函数介绍
3 exit、 _exit、 _Exit函数
3.1 exit函数
3.2 _exit函数
3.3 _Exit函数
3.4 示例代码
1 Linux 系统如何管理文件
当我们使用open
函数打开一个文件时,操作系统内核会分配一块内存区域作为缓冲区,并将磁盘上存储的静态文件数据加载到这个内存区域中,以便进行管理和缓存。这个加载到内存中的文件数据被称为动态文件或内核缓冲区。此后,对该文件的所有读写操作都是针对内存中的动态文件进行的,而不是直接操作磁盘上的静态文件。当对动态文件进行修改后,内存中的数据与磁盘上的数据就会不同步了,数据的同步是由内核负责的,内核会在适当的时候将内存中的动态文件内容刷新回磁盘,确保磁盘上的文件与内存中的数据保持一致。
在Linux操作系统中,进程控制块(PCB)是操作系统用来存储和管理每个进程信息的核心数据结构,其中包含文件表,记录了进程打开的所有文件的文件描述符和状态信息。文件表中的每个条目都指向一个inode,inode是文件系统中的一个数据结构,包含了文件的元数据和指向文件数据块的指针。inode table是存储所有inode的表,操作系统通过它来访问和管理文件的inode。磁盘是物理存储设备,由扇区和块组成,文件数据最终存储在这些磁盘块中。
文件描述符表、文件表以及 inode 之间的关系(参考正点原子教程)
1.1 什么是静态文件
在Linux系统中,静态文件通常指的是那些在创建后内容不经常改变的文件,例如系统配置文件、编译后的程序可执行文件、库文件等。这些文件在系统运行过程中保持不变,不需要动态生成或频繁更新,因此被称为静态文件。静态文件的特点是它们在磁盘上占用固定的存储空间,并且可以通过文件路径和inode号直接访问。
1.2 扇区(Sector)和块(Block)概念?
在计算机存储设备中,扇区(Sector)和块(Block)是两个基本的概念,它们用于描述数据存储的基本单位:
扇区(Sector):
- 扇区是磁盘上最小的物理存储单元,所有数据都存储在扇区中。
- 扇区的大小通常是固定的,常见的扇区大小有512字节、4KB等。
- 每个扇区都有唯一的地址,操作系统通过扇区地址来访问磁盘上的数据。
- 扇区是磁盘读写操作的最小单位,即使是很小的数据也需要占用一个完整的扇区。
块(Block):
- 块是文件系统中用于分配存储空间的基本单位,块的大小可以由文件系统决定。
- 块的大小通常大于扇区,例如4KB、8KB、16KB等,这样可以减少文件系统中的碎片。
- 文件系统中的块大小是可配置的,较大的块大小可以提高大文件的读写效率,但可能会浪费存储空间。
- 块是文件系统管理数据的方式,操作系统通过块来分配和管理磁盘空间。
扇区和块之间的关系:
- 一个块可以包含一个或多个扇区。例如,如果块的大小是4KB,而扇区大小是512字节,那么一个块将包含8个扇区。
- 文件系统在分配存储空间时,会将文件数据存储在连续的块中,而每个块又是由连续的扇区组成的。
- 当文件大小超过一个块的大小时,文件系统会在磁盘上分配多个块来存储文件数据。
所以由此可以知道,静态文件对应的数据都是存储在磁盘设备不同的“块”中, 那么我们在程序中调用 open 函数是如何找到对应文件的数据存储“块”的呢?
1.3 inode
inode(索引节点)和(inode 表)是什么?我们的磁盘在进行分区、格式化的时候会将其分为两个区域,一个是数据区,用于存储文件中的数据;另一个是 inode 区,用于存放 inode table(inode 表), inode table 中存放的是一个一个的 inode(也成为 inode节点),不同的 inode 就可以表示不同的文件,每一个文件都必须对应一个 inode, inode 实质上是一个结构体,这个结构体中有很多的元素,不同的元素记录了文件了不同信息,譬如文件字节大小、文件所有者、文件对应的读/写/执行权限、文件时间戳(创建时间、更新时间等)、 文件类型、 文件数据存储的 block(块)位置等等信息, 如图所示。
inode table 与 inode(参考正点原子文档)
inode(索引节点)是什么?在Linux系统中,inode(索引节点)是一种存储文件和目录元数据的数据结构,它包含了文件的权限、所有者、大小、创建和修改时间等信息,但不包含文件的实际数据内容。每个文件和目录都有一个唯一的inode号,文件系统通过inode来管理和定位文件。目录项将文件名与inode号关联起来,而硬链接是指向相同inode的多个文件名,共享文件数据;软链接则是指向其他文件inode的文件。了解inode的概念对于管理文件权限、创建链接以及有效管理文件系统至关重要。
如何查看文件的 inode 编号?
每一个文件都有唯一的一个 inode, 每一个 inode 都有一个与之相对应的数字编号,通过这个数字编号就可以找到 inode table 中所对应的 inode。
在Linux系统中,可以使用ls
命令结合-i
选项来查看文件的inode编号。具体命令如下:
ls -i filename
这里,filename
是你想要查看inode编号的文件名。执行这个命令后,ls
会列出文件及其对应的inode编号。
如果你想查看一个目录下所有文件的inode编号,可以省略文件名,直接使用:
ls -i
ls -il
1.4 进程控制块(PCB)
在 Linux 系统中, 内核会为每个进程设置一个专门的数据结构用于管理该进程,我们把这个称为进程控制块(Process control block,缩写PCB) 。进程控制块(PCB)是操作系统中用于存储和管理进程信息的关键数据结构,它包含了进程的唯一标识符(PID)、当前状态、程序计数器、寄存器集合、调度信息、内存管理信息、文件表、I/O状态、信号处理以及上下文切换信息等,是操作系统调度进程、管理资源和处理进程切换的基础,确保了进程的正确执行和管理。
2 返回错误处理与 errno
2.1 errno变量介绍
在Linux中,错误处理通常通过返回一个错误代码来实现,而更具体的错误信息则存储在全局变量errno
中。errno
是一个宏,它在头文件<errno.h>
中定义。当系统调用或库函数失败时,它们会设置errno
为特定的错误代码。
以下是处理错误的一般步骤:
-
调用系统函数:执行如打开文件、读取数据等操作。
-
检查返回值:如果函数返回-1或其他错误指示值,表示函数执行失败。
-
检查
errno
:调用perror()
、strerror()
或strerror_r()
函数来获取errno
对应的错误描述。 -
错误处理:根据错误类型决定后续操作,如重试操作、记录日志、清理资源或向用户报告错误。
以下是一个简单的示例代码,演示了如何在Linux程序中使用错误处理和errno
:
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main() {int fd;char *filename = "file.txt"; // 一个不存在的文件名// 尝试打开文件fd = open(filename, O_RDONLY);if (fd == -1) {// 如果open失败,打印错误信息fprintf(stderr, "Error opening file '%s': %s\n", filename, strerror(errno));return 1; // 返回非零值表示出错}// 关闭文件close(fd);return 0; // 正常退出
}
在这个示例中,我们尝试打开一个名为file.txt
的文件。由于这个文件不存在,open
函数将失败并返回-1。然后,我们检查fd
是否等于-1,如果是,我们使用strerror(errno)
函数来获取与errno
相关的错误消息,并打印出来。strerror
函数将错误代码转换为可读的字符串。运行结果如下:
2.2 strerror函数strerror
strerror
函数用于将错误代码转换成一个人类可读的字符串。这个函数的原型如下:
char *strerror(int errnum);
其中errnum
是错误代码,通常是errno
宏的值,errno
在发生系统调用错误时被设置。
以下是一个简单的strerror
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>int main() {// 假设我们有一个错误的errno值errno = EACCES; // 访问被拒绝// 打印错误消息printf("Error: %s\n", strerror(errno));return 0;
}
在这个示例中,我们手动设置errno
为EACCES
(访问被拒绝),然后使用strerror
获取对应的错误消息,并打印出来。运行结果如下:
2.3 perror函数介绍
perror
函数用于输出当前errno
值对应的错误消息到标准错误(stderr)。这个函数的原型如下:
void perror(const char *str);
其中str
是一个可选的前缀字符串,它会在错误消息之前输出,以提供上下文信息。
以下是一个简单的perror
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>int main() {FILE *fp;// 尝试打开一个不存在的文件fp = fopen("nonexistent_file.txt", "r");if (fp == NULL) {// 文件打开失败,使用perror打印错误消息perror("Failed to open file: ");} else {// 文件成功打开,执行其他操作fclose(fp);}return 0;
}
在这个示例中,我们尝试打开一个不存在的文件nonexistent_file.txt
。如果文件打开失败(fp
为NULL
),perror
将输出一个带有前缀"Failed to open file: "的错误消息。运行结果如下:
3 exit、 _exit、 _Exit函数
在 Linux 系统下, 进程正常退出除了可以使用 return 之外, 还可以使用exit
、_exit
和_Exit
,是3个函数用于终止进程的函数,它们的行为略有不同:
3.1 exit
函数
exit
函数是标准C库函数,当调用时会执行一些清理操作,比如关闭所有打开的文件描述符、刷新标准I/O库的缓冲区、调用所有注册的退出处理程序等。exit
的原型如下:
void exit(int status);
参数status
是一个整数,通常用于表示程序的退出状态,0通常表示成功,非0表示出错。
3.2 _exit
函数
_exit
是POSIX标准的函数,与exit
类似,但它不会执行任何清理操作,直接终止进程。这使得_exit
比exit
更快,因为它跳过了所有清理步骤。_exit
的原型如下:
void _exit(int status);
参数status
同样用于表示程序的退出状态。
3.3 _Exit
函数
_Exit
是_exit
的一个宏定义,行为与_exit
完全相同。在某些系统中,_Exit
可能被定义为_exit
的宏,或者两者都是系统调用。
3.4 示例代码
以下是一个简单的示例,演示了如何使用exit
和_exit
函数:
#include <stdio.h>
#include <stdlib.h>void cleanup(void) {// 这里可以执行一些清理工作printf("Cleanup resources...\n");
}int main() {// 注册exit函数atexit(cleanup);printf("Normal exit using exit()\n");exit(0); // 正常退出,执行清理工作// 下面的代码不会被执行,因为exit已经终止了程序printf("This will not be printed\n");return 0;
}
#include <stdio.h>
#include <stdlib.h>// 一个示例函数,演示_exit的使用
void immediate_exit() {printf("Immediate exit using _exit()\n");_exit(1); // 立即退出,不执行任何清理工作
}int main() {immediate_exit(); // 调用上面的函数// 下面的代码不会被执行,因为_exit已经终止了程序printf("This will not be printed\n");return 0;
}
在这个示例中,main
函数中使用了exit
,它在退出之前会调用所有注册的退出处理程序(例如cleanup
函数)。而immediate_exit
函数中使用了_exit
,它将立即终止程序,不会执行任何清理工作。在实际使用中,如果你需要立即退出程序且不需要执行任何清理工作,可以使用_exit
或_Exit
;如果你希望在退出前执行一些清理工作,应该使用exit
。第一段代码运行结果如下: