回顾c语言的文件操作

相关接口

简单的回顾下在c语言中,我们是如何使用相关的文件接口的。
C语言提供了一套强大而灵活的文件接口,使得程序能够在磁盘上读取和写入数据。这个文件接口是操作系统提供的API(应用程序编程接口)的一部分,这里则是简单介绍一下C语言中常用的文件接口,包括文件的打开、读取、写入、关闭等操作。

打开文件

在C语言中,要对一个文件进行操作,首先需要将其打开。这可以通过使用fopen函数来实现。

1
2
FILE *fptr;  // 声明一个文件指针
fptr = fopen("example.txt", "r"); // 打开名为example.txt的文件以供读取

声明了一个文件指针fptr,使用fopen函数将名为example.txt的文件以只读模式打开,函数返回一个指向该文件的指针。

读取文件

一旦文件打开成功,我们可以使用fread函数来读取文件内容。

1
2
char buffer[100];  // 声明一个用于存储数据的缓冲区
fread(buffer, sizeof(char), 100, fptr); // 从文件中读取100个字符到缓冲区中

上使用fread函数从打开的文件中读取了100个字符,然后将其存储到名为buffer的字符数组中。

写入文件

如果需要将数据写入文件,可以使用fwrite函数。

1
2
char data[] = "Hello, World!";
fwrite(data, sizeof(char), strlen(data), fptr); // 将数据写入文件

将字符串"Hello, World!"写入到已经打开的文件中。

关闭文件

在完成文件操作后,应该及时关闭文件,以释放资源。

1
fclose(fptr);  // 关闭文件

错误处理

在实际使用中,我们需要考虑可能发生的错误情况。可以通过检查函数的返回值来判断文件是否成功打开或操作是否成功。

1
2
3
4
5
6
FILE *fptr;
fptr = fopen("example.txt", "r");
if (fptr == NULL) {
printf("无法打开文件\n");
return 1;
}

c语言的输入输出流

1
2
3
4
5
6
7
8
int main()
{
const char *str = "hello world\n";
fwrite(str, strlen(str), 1, stdout);
printf("hello linux\n");
fprintf(stdout, "hello world\n");
return 0;
}

fwrite函数将字符串 “hello world\n” 写入标准输出。fwrite 函数的参数依次为字符串的指针(str),字符串的长度(strlen(str)),写入次数(1),和目标输出(stdout)。
rintf 函数来打印 “hello linux”。printf 函数将格式化字符串写入标准输出。
fprintf 函数来打印 “hello world”。fprintf 函数的参数依次为目标输出(stdout)和格式化字符串。

上面三个函数都提到了stdout,那么stdout到底是什么?

在C语言中,stdout是指向标准输出的文件指针。标准输出是一个特殊的文件流,通常用于将程序的输出打印到屏幕上。
在这个函数中,使用了不同的输出函数来将字符串打印到标准输出上。fwrite、printf和fprintf都可以接受文件指针作为参数,用于指定输出的目标。
使用stdout可以方便地将输出打印到屏幕上,而不需要指定具体的文件或设备。这样,程序的输出就可以在控制台上可见,并且可以通过重定向等方式将输出保存到文件中。

C语言默认会打开三个输入输出流,分别是stdin, stdout, stderr
stdout已经在上文解释过,当涉及到输入和错误输出时,C语言提供了两个额外的标准流:stdin和stderr。

stdin是指向标准输入的文件指针。标准输入用于接收来自用户的输入,通常是通过键盘输入。例如,使用scanf函数可以从标准输入中读取用户的输入。

stderr是指向标准错误的文件指针。标准错误用于输出程序的错误消息或其他诊断信息。与标准输出不同,标准错误的输出通常被发送到屏幕上的错误流中,而不会被重定向到文件。例如,使用fprintf(stderr, …)函数可以将错误消息输出到标准错误流。

linux下的文件操作

系统文件io

除了上面提到的C语言来进行文件操作,同样的,我们可以通过系统调用来对文件进行读或者写等等操作。
当使用系统调用进行文件I/O时,有这么几个常用的函数:

  1. open():用于打开文件。它接受文件路径和一些标志作为参数,并返回一个文件描述符(file descriptor),表示打开的文件。
  2. read():用于从文件中读取数据。它接受文件描述符、数据缓冲区和读取字节数作为参数,并返回实际读取的字节数。
  3. write():用于向文件中写入数据。它接受文件描述符、数据缓冲区和写入字节数作为参数,并返回实际写入的字节数。
  4. close():用于关闭文件。它接受文件描述符作为参数,并在操作完成后关闭文件。

打开一个名为 “example.txt” 的文件(如果不存在则创建),然后写入字符串 “Hello, world!”。最后关闭文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <fcntl.h>
#include <unistd.h>

int main() {
int fd = open("example.txt", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
if (fd == -1) {
// 处理打开文件失败的情况
return 1;
}

const char *data = "Hello, world!";
ssize_t bytes_written = write(fd, data, strlen(data));
if (bytes_written == -1) {
// 处理写入文件失败的情况
return 1;
}

close(fd);

return 0;
}

四个常用接口介绍

open

1
2
3
4
5
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
O_RDONLY: 只读打开。
O_WRONLY: 只写打开。
O_RDWR : 读,写打开。
这三个常量,必须指定一个且只能指定一个。
O_CREAT : 若文件不存在,则创建它。
O_APPEND: 追加写。
返回值:
成功:新打开的文件描述符(什么是文件描述符会在下文讲)。
失败:-1

剩下的三个也都大差不差:

close

1
int close(int fd);

这个函数用于关闭文件。它接受文件描述符(fd)作为参数,在操作完成后关闭文件。
参数:
fd:要关闭的文件描述符。
返回值:
成功:0。
失败:-1。

write

1
ssize_t write(int fd, const void *buf, size_t count);

这个函数用于向文件中写入数据。它接受文件描述符(fd),数据缓冲区指针(buf)和要写入的字节数(count)作为参数。
参数:
fd:要写入的文件描述符。
buf:指向要写入的数据的缓冲区的指针。
count:要写入的字节数。
返回值:
成功:实际写入的字节数。
失败:-1。

read

1
ssize_t read(int fd, void *buf, size_t count);

这个函数用于从文件中读取数据。它接受文件描述符(fd),数据缓冲区指针(buf)和要读取的最大字节数(count)作为参数。

参数:
fd:要读取的文件描述符。
buf:指向存储读取数据的缓冲区的指针。
count:要读取的最大字节数。
返回值:
成功:实际读取的字节数。
失败:-1。

接口演示

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
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
int fd = open("example.txt", O_RDONLY);
if (fd == -1) {
// 处理文件打开失败的情况
return 1;
}

char buffer[100];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read == -1) {
// 处理读取文件失败的情况
close(fd);
return 1;
}

close(fd);

// 打印读取的数据
write(STDOUT_FILENO, buffer, bytes_read);

return 0;
}

可以看到上文很多地方都出现了fd和文件描述符的概念(二者是一个东西),那么什么是文件描述符呢?

文件描述符

在文章开篇时,我提到了c语言会打开三个输入输出流,那我们是如何得知这三个流被打开了呢?

文件描述符是一个非负整数,用于唯一标识一个打开的文件或I/O流。它是一个抽象的概念,可以是文件、管道、套接字等。

标准文件描述符

Linux系统通常会为每个进程自动分配三个标准文件描述符:
0(stdin):标准输入,通常用于从键盘或其他输入设备读取数据。
1(stdout):标准输出,通常用于向终端或其他输出设备输出数据。
2(stderr):标准错误,通常用于输出错误消息。
0,1,2对应的物理设备一般是:键盘,显示器,显示器

文件描述符主要用于进行文件和I/O操作,通常通过以下系统调用来操作文件描述符:

open():打开一个文件并返回一个文件描述符。
close():关闭一个文件描述符,释放相关资源。
read():从文件描述符中读取数据。
write():向文件描述符中写入数据。
lseek():移动文件描述符的读/写位置。
dup()和dup2():复制文件描述符,创建一个新的文件描述符与之关联。

也就是说,可以这样输出:

1
2
3
4
5
6
7
8
9
10
11
int main()
{
char buffer[1024];
ssize_t s = read(0, buffer, sizeof(buffer));
if(s > 0){
buffer[s] = 0;
write(1, buffer, strlen(buffer));
write(2, buffer, strlen(buffer));
}
return 0;
}

上面的代码实现了从标准输入读取数据,并将其写入标准输出和标准错误。
那么现在就可以理解,文件描述符就是从0开始的整数。当打开文件时,操作系统在内存中要创建相应的数据结构来
描述目标文件,也就是file结构体来表示一个已经打开的文件对象。
例如,当进程执行open系统调用时,进程和文件必须有所关联。每个进程都有一个指针*files, 指向一张表files_struct,该表有一个指针数组,每个元素都是一个指向打开文件的指针。所以,本质上,文件描述符就是该数组的下标。所以,只要有文件描述符,就可以找到对应的文件。

1
2
3
4
5
6
7
8
9
10
11
12
+-------------------+         +------------------------+
| | | |
| Process (进程) | | File Struct (file) |
| | | |
| +------------+ | | +----------------+ |
| | *files | | | | File* 0 | |
| | (指针) |----------> | | File* 1 | |
| +------------+ | | | File* 2 | |
| | | +----------------+ |
+-------------------+ +------------------------+

0,1,2分别是这个指针数组的下标,从上往下分别指向stdin,stdout,stderr所代表的三个已打开的文件。

注意,上图的File*列表仅存储打开的文件,也就是说,如果此时新建一个文件,那么分配给他的下标就会是3,如果在创建此文件前关闭了1,那么此文件的文件描述符就会被分配为1。这就是Linux系统下文件描述符基本的分配规则。并且File Struct这张表是唯一的,哪怕你在进程中打开了一个子进程,这张表也不会被拷贝,而是以共享的形式存在。

重定向

当我们关闭掉1:

1
2
3
4
5
6
7
8
9
10
11
int main()
{
close(1);
int fd = open("file", O_WRONLY|O_CREAT, 00644);

printf("fd: %d\n", fd);

close(fd);
exit(0);
}

按照我们才说的,那么屏幕就不会打印我们想要输出的内容,而是将其输入到file文件中。
事实确实如此,因为printf底层封装的仍然是输出到下标1,哪怕stdout被关闭,系统也不清楚,换句话说,系统只认识1,他要做的就是将需要打印的内容输出到1代表的这个文件。这也是我认为的linux环境下文件管理的一个显著特征,层层封装,再由系统统一调用,有点多态的意思在里面。

将原本该打印到屏幕的内容打印到file里,这就叫重定向。

文件描述符的重定向允许将一个文件描述符与另一个文件或设备相关联。例如,可以使用>将命令的输出重定向到文件,或使用<将文件内容作为输入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//输出重定向:将命令的标准输出保存到文件。
//将 "Hello, World!" 写入到文件 output.txt
echo "Hello, World!" > output.txt
//输入重定向:从文件中读取数据作为命令的标准输入。
//从文件 input.txt 中读取数据并将其作为命令的输入
cat < input.txt
//追加:将命令的标准输出追加到文件末尾。
//追加 "Hello again!" 到文件 output.txt
echo "Hello again!" >> output.txt
//错误输出重定向:将命令的标准错误输出保存到文件。
//将命令的标准错误输出保存到 error.log 文件
ls non_existent_directory 2> error.log
//标准输出和标准错误重定向到同一文件:
//将标准输出和标准错误输出都重定向到同一个文件
ls non_existent_directory > output_and_error.log 2>&1
//使用管道:将一个命令的输出传递给另一个命令的输入。
//列出当前目录下的文件,并将结果通过管道传递给 grep 命令以筛选文件名中包含 "example" 的文件
ls | grep "example"

但在本质上,其更改的是文件描述符所指向的内容。在上面我画了一张关于File Struct的图,当执行了close(1)操作,再执行新建file文件,那么此时文件描述符为1的坑位就指向了file而不是stdout。
可以这么说,重定向的魅力在于操作文件描述符,将它们连接到不同的位置,从而改变了命令的输入和输出源,使得命令行操作更加灵活多变。
举一个例子:

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
// 打开或创建文件 "output.txt",并获取文件描述符
int file_fd = open("output.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);

if (file_fd == -1) {
perror("open");
exit(1);
}

// 备份标准输出文件描述符
int stdout_backup = dup(1);

if (stdout_backup == -1) {
perror("dup");
exit(1);
}

// 使用 dup2 将文件描述符 "file_fd" 复制到标准输出文件描述符 "1"
if (dup2(file_fd, 1) == -1) {
perror("dup2");
exit(1);
}

// 现在标准输出已经被重定向到 "output.txt"
printf("This will be written to output.txt\n");

// 恢复标准输出
if (dup2(stdout_backup, 1) == -1) {
perror("dup2");
exit(1);
}

// 关闭文件描述符
close(file_fd);
close(stdout_backup);

// 现在标准输出已经恢复,继续输出到屏幕
printf("This will be shown on the screen\n");

return 0;
}

小tip:因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。所以C库当中的FILE结构体内部,必定封装了fd

缓冲区

1
2
3
4
5
6
7
8
9
10
11

int main()
{
const char *p1="fwrite\n";
const char *p2="write\n";
printf("printf");
fwrite(p1, strlen(p1), 1, stdout);
write(1, p2, strlen(p2));
fork();
return 0;
}

执行三个输出,将信息打印到屏幕上,再执行fork新建一个子进程,这就是目前代码所执行的逻辑。
但当你将此文件重定向到一个普通文件中时,输出会变成这样:

1
2
3
4
5
write
printf
fwrite
printf
fwrite

看起来write仍然只执行一次,而printf和fwrite被执行了两次。

首先,需要明白一个概念:printf和fwrite库函数有自带的缓冲区,而write作为系统调用则没有缓冲区。
这些缓冲区都是用户级缓冲区。
printf和fwrite都是库函数,这个缓冲区由C的标准库来提供。而write是系统调用,库函数则是在系统调用的“上层”, 是对系统调用的“封装”,所以write没有这个缓冲区也不足为奇。
一般C库函数写入文件时是全缓冲(进程结束统一刷新)的,而写入显示器是行缓冲(遇到\n刷新)。当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。那么缓冲区中的数据就不会被立即刷新,甚至fork之后也不会刷新。当进程退出之后,就会被统一刷新,再写入文件当中。
当进行fork时,父子数据会发生写时拷贝,缓冲区也是不被共享的,但数据会被拷贝,那么就有了两个缓冲区,分别在等待进程结束时进行刷新,将数据刷新至内核级别的缓冲区。
目前来说可以这么理解,当数据被刷新到了内核级别的缓冲区,就可以认为数据到了硬件层面。

linux如何管理文件

概述

在Linux系统中,文件系统以一种层次化的树状结构组织和描述所有的文件和目录。以根目录为起点,所有的文件和目录都从这里开始。
也许你听过一句话,在Linux系统中,一切皆为文件。这包括了普通文件、目录、设备文件、链接等等,并且不同类型的文件具有不同的属性和用途。相信在读到这里,你对这句话有了一个更深入的理解。
可以简单的总结,Linux将硬件底层封装为文件,并允许进程通过文件指针来进行调用和访问。
在Linux中,硬件设备通常由设备文件来表示,这些设备文件位于/dev目录下。每个硬件设备都有一个相应的设备文件,例如硬盘设备可以表示为/dev/sda,串口可以表示为/dev/ttyS0等。
同时,每个进程都有一个文件描述符表,它是一个索引到文件的整数数组。文件描述符是进程用来访问文件的句柄。通常,标准输入、标准输出和标准错误分别对应文件描述符0、1和2。
进程可以通过系统调用来操作文件。例如,open系统调用用于打开一个文件,read和write用于读取和写入文件数据,close用于关闭文件。进程通过这些系统调用来请求操作文件或设备。
当进程打开一个文件时,操作系统维护一个文件指针(或文件偏移量),它指示文件中下一个读取或写入操作的位置。文件指针可以通过系统调用来移动,如lseek。
借用一张linux的概念图:

可以这么认为,f#系列的函数,都是对系统调用的封装,方便二次开发。

这么一说,对Linux一切皆文件是不是又清楚了一些呢?

把话题转回来,在Linux中,每个文件都有一个唯一的路径,用于描述其在文件系统中的位置。路径可以是绝对路径(从根目录开始的完整路径)或相对路径(相对于当前工作目录的路径)。每个文件和目录都有权限设置,这确定了谁可以对其进行读、写和执行操作。此外,每个文件都有一个所有者和所属的用户组
Linux支持多种文件系统类型,包括ext4、XFS、btrfs等。每个文件系统类型都具有不同的特性和性能。
Linux系统通过系统调用提供了一组API,用于管理文件和目录。这些系统调用包括打开、读取、写入、关闭、创建和删除文件,以及更改文件属性等。
以下几点作为了解:
链接和挂载: Linux支持硬链接和符号链接,允许多个文件名引用同一个文件。此外,Linux还支持文件系统挂载,使不同的文件系统可以被组合到同一个目录结构中。
特殊文件: Linux系统还包括特殊文件,如设备文件(用于与硬件设备通信)、套接字文件(用于进程间通信)和管道文件(用于进程间数据传输)。
文件系统维护: Linux系统中有一系列工具用于文件系统维护,如fsck用于文件系统检查和修复,du用于查看磁盘使用情况,df用于查看磁盘空间等。

文件系统

linux的文件都存储在磁盘上,那么谈文件系统就不能跳过磁盘。

文件系统将文件数据分成小块(通常是扇区或块),这些块以逻辑方式组织在磁盘上,这样操作系统就可以进行有效地读取和写入。
当在Linux上进行创建、编辑或删除文件的操作时,实际上是在磁盘上进行操作。文件数据被写入到磁盘的数据块中,文件的元数据(如inode)被更新以反映文件的更改。

来了解下磁盘的物理结构。

一块磁盘通常由多个盘面(Platters)组成,每个盘面分为多个磁道(Tracks),而每个磁道又分为多个扇区(Sectors)。

扇区是磁盘上最小的可寻址存储单元,通常为512字节或4KB。
操作系统和文件系统使用扇区来读取和写入数据。
磁头则是用于读写数据的磁盘表面上的读/写头。磁头的数量取决于磁盘驱动器的设计。

不知道大家小时候有没有用过播放英语听力的磁带,一圈圈的绕起来,和磁盘的结构很像。类比一下,磁盘也是如此,你可以想象磁盘上的磁道是一整条磁带,而数据就按照特定的顺序排列在上面。可以这么讲,磁盘的逻辑结构是线性的。而文件数据也线性地分布在上面。

文件系统是一种在磁盘上创建的组织数据的结构,这样使得数据存储、访问和管理更加方便。磁盘则是实际的存储媒介,而文件系统为数据在磁盘上的组织和操作提供了接口和规则。

以下是文件系统的组成图示以及每个组成部分的主要用途:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+-------------------------------------------+
| File System |
+-------------------------------------------+
| Superblock |
| (File System Metadata) |
|-------------------------------------------|
| Group Descriptor Table (GDT) |
| (Information about block groups) |
|-------------------------------------------|
| Block Bitmaps (one per block group) |
| (Tracks used and free data blocks) |
|-------------------------------------------|
| Inode Bitmaps (one per block group) |
| (Tracks used and free inodes) |
|-------------------------------------------|
| Inode Table (one per block group) |
| (Stores metadata for files and directories)|
|-------------------------------------------|
| Data Blocks (File and Directory Contents) |
| (Stores the actual data of files/directories)|
+-------------------------------------------+

引导块(Boot Block)通常位于文件系统之前,是文件系统的前导部分。引导块的主要目的是引导操作系统,从而启动计算机。引导块与文件系统的关系在于它是文件系统之前的一部分,但不直接与文件系统的组件(如超级块、inode 表等)交互。引导块包含有关如何加载操作系统的信息,这个引导加载程序负责启动操作系统,并在需要时加载文件系统。

而Block Group则是文件系统根据分区的大小划分为数个Block Group。而每个Block Group都有着相
同的结构组成。可以联想到国家设立省市来实行分级管理。

Superblock: 超级块包含文件系统的元数据,如文件系统大小、块大小、inode 数量等。它是文件系统的核心信息存储,用于初始化和维护文件系统。

Group Descriptor Table (GDT): 组描述符表包含了关于每个数据块组的信息,如块位图、inode 位图和inode 表的位置。

Block Bitmaps: 块位图跟踪哪些数据块已经被使用,哪些还没有被使用。

Inode Bitmaps: inode 位图用于跟踪哪些inode节点已被使用,哪些还没有被使用。

Inode Table: inode 表存储文件和目录的元数据,如文件权限、拥有者、文件大小等。每个文件和目录在inode表中都有一个对应的inode条目(这点很重要)。

Data Blocks: 数据块存储实际的文件内容和目录结构。它包含文件的实际数据,以及目录中文件和子目录的引用。

综上,文件系统遵循着将文件数据和属性分开存放的思想。数据通通存在data blocks中,而文件属性则单独放在inode表里。二者的联系则借助于GDT。

如何从文件系统角度新建,删除文件?

当需要新建文件时,首先会在Inode Bitmap中寻找一个未使用的索引节点。接着在Inode Table中分配这个索引节点,记录文件的元数据信息,比如文件名、权限等。然后会在Block Bitmap中寻找一些未使用的数据块(block Bitmaps也会随着更新)。最后,将文件的数据写入这些数据块中,完成文件的创建。

当需要删除文件时,首先会释放文件占用的数据块,在Block Bitmap中标记这些数据块为未使用状态。
然后释放文件的索引节点,在Inode Bitmap中标记这个索引节点为未使用状态。最后,在Inode Table中删除文件的元数据信息,完成文件的删除。

软硬链接

当使用ls -l命令时,会看到类似下图的信息:

1
-rw-r--r--. 1 root root 654 "11月 9 13:06" chengzi.c

从左到右,分别是:模式,硬链接数,文件所有者,组,大小,最后修改时间。

上文也提到了,inode是文件系统中的数据结构,包含着文件的属性信息。
硬链接(Hard Links)则是指一个文件可以有多个文件名指向同一个inode。inode作为文件系统中的数据结构,包含了文件的元数据(比如权限、所有者、文件大小等)以及指向实际数据块的指针。
所有硬链接文件都指向同一个inode,因此它们实际上共享相同的数据块。
删除任意一个硬链接并不影响其他硬链接,只有当所有链接都被删除时,inode的引用计数才会变为零,文件系统才会释放相关的数据块。
但硬链接也有自己的缺点,硬链接不能跨越文件系统,只能在同一文件系统内创建。也不能对目录创建硬链接
不能对目录创建硬链接的主要原因是防止文件系统的树形结构出现循环
举个例子:你在目录a下查找文件b,但同时目录a中存在目录c,目录c是指向目录a的硬链接,这会导致什么后果?

1
2
3
4
5
6
/home/user
|-- a
| |-- directory_c (硬链接到 /home/user/a)
其他文件或目录
| |-- file_b

/home/user/b/directory_c 和 /home/user/a 实际上指向同一个目录,这就导致了在进行遍历查找时,可能会陷入循环的问题。

创建硬链接:

1
ln source_file hard_link_name

的文件夹引用计数是2。大家都知道空的文件夹其实并不是空的, 其中含有两个隐藏的文件.和…。这两个文件分别指向了本目录和上层目录,都是硬链接。如果一个目录引用计数是17,那就代表这个目录下还有15个子目录。

与硬链接相对应的就是软链接。
软链接可以类比成windows系统下的快捷方式
软链接是一种特殊类型的文件,它创建一个指向另一个文件或目录的符号链接。软链接类似于快捷方式,它只包含目标文件的路径信息,而不是实际的数据。如果源文件或目录被删除或移动,软链接将失效。

创建软链接:

1
ln -s /path/to/source /path/to/symlink

一些关于文件系统的补充

针对上面的知识点,补充一些不太重要但可以使得理解更顺畅的小tip。

请先记住一句话:任何访问内存的动作,都是在访问内存page数组!

当应用程序请求访问文件时,操作系统会使用文件系统进行读取。文件系统会查找文件的元数据(如inode)以获取文件的物理位置。Linux系统有一个页缓存(Page Cache)层,它将文件系统中的数据缓存在内存中。如果文件的数据已经存在于缓存中,就不需要从磁盘读取,若没有就需要从磁盘中取得数据。
物理内存和磁盘都是以4kb大小一片来存储数据的,出于对效率的考虑,二者之间数据交换也是以4kb为最小单位进行操作。
例如一个内存为4GB的电脑,以4KB为最小片,就可以划分出大约一百万个片(可以理解为有一百万个下标的数组),这就是上面提到的内存page数组。通过一些转换方法,os就可以完成地址和下标的转化,从而找到正确的地址空间来存放数据。
这也遵循着先描述再组织的思想,每一个4KB的page都被描述为struct page,其中存放着page页的必要属性信息。
当一个进程需要打开文件时,正如上面讲到的,会在一个file struct中寻找对应的指针。

1
2
3
4
5
6
7
8
9
10
11
12
+-------------------+         +------------------------+
| | | |
| Process (进程) | | File Struct (file) |
| | | |
| +------------+ | | +----------------+ |
| | *files | | | | File* 0 | |
| | (指针) |----------> | | File* 1 | |
| +------------+ | | | File* 2 | |
| | | +----------------+ |
+-------------------+ +------------------------+

0,1,2分别是这个指针数组的下标,从上往下分别指向stdin,stdout,stderr所代表的三个已打开的文件。

比如我们打开fd为7的文件,你需要明白的是,其属性与文件内容是分开存储的!属性存储在inode struct里,而文件内容以某种形式封装在address_space struct中,Address_Space可以关联到文件的页缓冲区,这是用于缓存文件内容的数据结构。而页缓冲区就是位于物理内存那一百万个片中。