一、文件路径
在 Linux 中,文件路径是用字符串表示的,由多个目录名和文件名组成,目录名和文件名之间用斜杠(/)分隔。Linux 中的文件路径可以分为两种类型:绝对路径和相对路径。
绝对路径是从根目录(/)开始的完整路径,它可以唯一地标识一个文件或目录。例如,/home/user/file.txt 是一个绝对路径,它表示根目录下的 home 目录下的 user 目录下的 file.txt 文件。在绝对路径中,每个目录名和文件名都是从根目录开始计算的,因此它们的位置是固定的,不受当前工作目录的影响。
相对路径是相对于当前工作目录的路径,它不能唯一地标识一个文件或目录,而是根据当前工作目录的不同而变化。例如,如果当前工作目录是 /home/user,那么 file.txt 是一个相对路径,它表示当前工作目录下的 file.txt 文件。在相对路径中,每个目录名和文件名都是相对于当前工作目录计算的,因此它们的位置是可变的,受当前工作目录的影响。
在 Linux 中,可以使用一些命令来操作文件路径,例如 cd(change directory) 命令可以改变当前工作目录,pwd 命令可以显示当前工作目录的路径,ls 命令可以列出指定目录下的文件和子目录等。此外,Linux 还提供了一些 C 语言的系统调用函数,例如 chdir 函数可以改变当前工作目录,getcwd 函数可以获取当前工作目录的路径,access 函数可以判断文件是否存在或是否有指定的权限等。
二、文件目录项
2.1 简介
文件系统的目录项(directory entry)是一个数据结构,用于建立文件名和文件的 inode 号之间的映射关系。每个目录都是一个文件,它包含了多个目录项,每个目录项包含了一个文件名和一个 inode 号,以及一些其他的元数据信息,如文件类型、权限、所有者、所属组、大小、创建时间、修改时间等。
目录项的结构如下:
struct dirent
{
__ino_t d_ino; /* inode 号 */
__off_t d_off; /* 文件在目录中的偏移量 */
unsigned short int d_reclen; /* 目录项长度 */
unsigned char d_type; /* 文件类型 */
char d_name[256]; /* 文件名 We must not include limits.h! */
};
其中,d_ino 字段表示文件的 inode 号,d_off 字段表示文件在目录中的偏移量,d_reclen 字段表示目录项的长度,d_type 字段表示文件的类型,d_name 字段表示文件名。在 Linux 中,文件的类型可以是以下几种:
DT_REG:普通文件
DT_DIR:目录文件
DT_FIFO:命名管道
DT_SOCK:套接字文件
DT_CHR:字符设备文件
DT_BLK:块设备文件
DT_LNK:符号链接文件
/* File types for `d_type'. */
enum
{
DT_UNKNOWN = 0,
# define DT_UNKNOWN DT_UNKNOWN
DT_FIFO = 1,
# define DT_FIFO DT_FIFO
DT_CHR = 2,
# define DT_CHR DT_CHR
DT_DIR = 4,
# define DT_DIR DT_DIR
DT_BLK = 6,
# define DT_BLK DT_BLK
DT_REG = 8,
# define DT_REG DT_REG
DT_LNK = 10,
# define DT_LNK DT_LNK
DT_SOCK = 12,
# define DT_SOCK DT_SOCK
DT_WHT = 14
# define DT_WHT DT_WHT
};
目录项的索引是通过目录文件内部的哈希表(hash table)或 B+ 树实现的。当用户打开一个目录时,系统会读取目录文件中的目录项,并将它们缓存在内存中。当用户访问某个文件时,系统会根据文件名查找对应的目录项,获取该文件的 inode 号,然后根据 inode 号读取文件的内容。
需要注意的是,一个文件可以有多个硬链接,即多个不同的目录项指向同一个 inode 号。这意味着,文件的实际路径可能不止一个(一个inode号可以对应多个文件路径,但一个文件路径只有一个inode号),用户可以通过任意一个路径来访问该文件。在 Linux 中,通过 stat 系列函数可以获取文件的硬链接数量。
2.2 例子
下面是一个使用 struct dirent 结构体的示例程序,可以列出指定目录下的所有文件和子目录的名称和 inode 号:
#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
int main(int argc, char **argv) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <directory>\n", argv[0]);
exit(1);
}
char *dirpath = argv[1];
DIR *dirp;
struct dirent *direntp;
if ((dirp = opendir(dirpath)) == NULL) {
perror("opendir error");
exit(1);
}
while ((direntp = readdir(dirp)) != NULL) {
printf("%s (inode=%lu)\n", direntp->d_name, direntp->d_ino);
}
closedir(dirp);
return 0;
}
在这个示例程序中,我们通过命令行参数传递了要列出文件和子目录的目录路径。在主函数中,我们首先调用 opendir 函数打开目录,如果返回值为 NULL,说明打开目录失败,我们将打印出错信息并退出程序。如果 opendir 函数返回值不为 NULL,说明打开目录成功,我们将通过 readdir 函数读取目录中的目录项,并打印出文件名和 inode 号。最后,我们调用 closedir 函数关闭目录。
需要注意的是,direntp->d_name 只包含文件名,不包括路径。如果要获取文件的完整路径,可以将文件名和路径拼接起来。另外,当读取完所有的目录项后,readdir 函数会返回 NULL,表示目录读取结束。
其中:
/* This is the data type of directory stream objects.
The actual structure is opaque to users. */
typedef struct __dirstream DIR;
在 Linux 中,DIR 数据结构是一个指向目录流的指针,是由 opendir 函数返回的,用于表示一个打开的目录。目录流是一个抽象的概念,指的是与目录相关联的文件描述符和目录缓存,它可以用于读取目录中的目录项。
DIR 数据结构的定义如下:
typedef struct {
int fd; /* 目录文件的文件描述符 */
struct dirent *dirent; /* 当前读取的目录项 */
} DIR;
其中,fd 字段是目录文件的文件描述符,dirent 字段是一个指向当前读取的目录项的指针。
在 Linux 中,目录流的底层实现是通过文件描述符和目录缓存来实现的。当用户调用 opendir 函数打开一个目录时,系统会创建一个文件描述符,然后将目录文件映射到该文件描述符上,并创建一个目录缓存,用于存储读取的目录项。每次调用 readdir 函数,系统会从目录缓存中读取一个目录项,并将 direntp 指针指向该目录项。当读取完所有的目录项后,系统会关闭文件描述符,并释放目录缓存。
需要注意的是,DIR 数据结构是不透明的,用户不能直接访问其内部结构,而是通过 opendir 函数打开目录,并通过 readdir 函数读取目录项。在使用完 DIR 数据结构后,用户应该调用 closedir 函数关闭目录,以释放资源。
三、文件inode号
3.1 简介
在 Linux 中,每个文件和目录都有一个唯一的 inode 号(inode number),用于标识该文件或目录的实体。inode 号是一个整数值,通常是一个非负整数,具体取值范围取决于文件系统的实现。
每个inode号都会对应一个i节点(inode结构体),i节点是文件系统中用于表示文件和目录的数据结构。每个文件和目录都有一个对应的i节点,它包含了关于文件或目录的元数据信息,如文件类型、权限、所有者、所属组、大小、创建时间、修改时间等,以及指向文件数据块的指针。
数据块则保存了文件的数据。每个文件有且仅有一个i节点,但可以有0、1或多个数据块。i节点最重要的作用作为寻址文件数据的出发点,因此i节点中需要保存包含文件数据的数据块编号。
如下图所示:
inode编号可以让文件系统(比如ext4)用来快速搜索磁盘上inode表中的inode节点。
比如:
一个文件inode编号 = 13021,假设每个块组包含4096个节点,文件inode编号13021的文件在磁盘上的地址 =
13021 = 4096 * 3 + 733
索引节点在第三个块组,其磁盘地址存放在相应 inode table索引节点表的第733个表项。
inode节点占用硬盘空间,所以硬盘格式化的时候,操作系统自动将硬盘分成两个区域。一个是数据区,存放文件数据;另一个是inode表(inode table),存放inode所包含的信息。
每个inode节点的大小,一般是128字节或256字节。inode节点的总数,在格式化时就给定,一般是每1KB或每2KB就设置一个inode。假定在一块1GB的硬盘中,每个inode节点的大小为128字节,每1KB就设置一个inode,那么inode table的大小就会达到128MB,占整块硬盘的12.8%。
比如ext4文件系统的inode结构体:
// linux-5.4.18/fs/ext4/ext4.h
/*
* Structure of an inode on the disk
*/
struct ext4_inode {
__le16 i_mode; /* File mode */
__le16 i_uid; /* Low 16 bits of Owner Uid */
__le32 i_size_lo; /* Size in bytes */
__le32 i_atime; /* Access time */
__le32 i_ctime; /* Inode Change time */
__le32 i_mtime; /* Modification time */
__le32 i_dtime; /* Deletion Time */
__le16 i_gid; /* Low 16 bits of Group Id */
__le16 i_links_count; /* Links count */
__le32 i_blocks_lo; /* Blocks count */
__le32 i_flags; /* File flags */
......
__le32 i_block[EXT4_N_BLOCKS];/* Pointers to blocks */
其中i_block 数组存储了指向文件数据块的指针,用于定位和访问文件的实际数据。
inode也能够将数据存储在inode本身i_block 数组中。这叫做Inlining。这种存储方法具有节省空间的优点,因为不需要数据块。它还通过避免更多的磁盘访问来获取数据,从而增加了查找时间。
像ext4这样的一些文件系统有一个名为inline_data的选项。启用后,它允许操作系统以这种方式存储数据。由于大小限制,内联只适用于非常小的文件。如果大小不超过60字节,Ext2和更高版本通常会以这种方式存储软链接信息。
内嵌存储(Inlining)是一种特定文件系统(如ext4)提供的功能,它允许将小量的数据直接存储在inode本身,而无需额外的数据块。这种存储方式具有以下特点:
(1)节省空间:通过在inode内部直接存储数据,文件系统避免了分配额外的数据块的需要。这可以节省空间,特别是对于数据量较小的文件。它消除了为这些文件分配和管理单独的数据块的开销。
(2)减少磁盘访问:使用内嵌存储,数据可以直接从inode中访问,而无需进行额外的磁盘读取。这可以提高小文件的查找速度,因为数据直接在inode结构中可用。
(3)大小限制:内嵌存储有一定的大小限制,因为inode具有固定的大小。可以内嵌存储的数据量取决于具体的文件系统及其配置。例如,在ext4中,inline_data选项允许将小文件内嵌存储,通常限制在60字节以内。如果文件超过大小限制,则会回退到传统的使用数据块的方式。
(4)使用场景:内嵌存储通常用于存储小文件,例如配置文件、小型脚本或符号链接信息。这些文件通常符合内嵌存储的大小限制,并从其节省空间和提高性能的优势中受益。
每个文件系统都有一个 inode 表(inode table),用于存储该文件系统中所有文件和目录的 inode节点。当用户创建一个新文件或目录时,系统会分配一个新的 inode 号,并将文件或目录的元数据信息写入 inode结构体中, inode结构体保存在inode表中。当用户访问一个文件或目录时,系统会根据文件名或目录名查找对应的 inode 号,然后根据 inode 号,从inode 表获取到对应的inode结构体,根据inode结构体的数据块地址来找到文件数据所在的block,读取文件或目录的内容。
inode 号可以用于实现硬链接(hard link)。硬链接是指通过多个目录项来共享一个 inode 号的文件,这样多个不同的路径可以指向同一个文件。当用户创建一个硬链接时,系统会创建一个新的目录项,并将 inode 号复制到该目录项中,这样该目录项就可以指向原文件的内容。在 Linux 中,使用 ln 命令可以创建硬链接。
需要注意的是,不同的文件系统的 inode 号是独立的,即同一个 inode 号在不同的文件系统中可能会指向不同的文件或目录。此外,inode 号是文件系统内部使用的标识符,用户不能直接访问或修改 inode 号。
3.2 文件名查找inode号
一个文件只对应一个inode号。
ls查看文件名的inode号:
ls -i filename
//Trace all system calls which take a file name as an argument.
strace -e trace=file ls -i filename
在 Linux 中,可以使用 stat 系列函数来获取文件的 inode 号。stat 函数用于获取文件的元数据信息,例如文件类型、权限、所有者、所属组、大小、创建时间、修改时间等,其中包括 inode 号。
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
int main(int argc, char **argv) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <file>\n", argv[0]);
exit(1);
}
char *filepath = argv[1];
struct stat filestat;
if (stat(filepath, &filestat) < 0) {
perror("stat error");
exit(1);
}
printf("Inode number of %s is %lu\n", filepath, filestat.st_ino);
return 0;
}
stat 函数的第二个参数是一个指向 struct stat 结构体的指针,用于存储获取到的文件元数据信息。
在 Linux 中,struct stat 结构体用于存储文件的元数据信息,包括文件类型、权限、所有者、所属组、大小、创建时间、修改时间、访问时间等。该结构体定义在 sys/stat.h 头文件中,其定义如下:
struct stat {
dev_t st_dev; /* 文件所在的设备编号 */
ino_t st_ino; /* 文件的 inode 号 */
mode_t st_mode; /* 文件的类型和权限 */
nlink_t st_nlink; /* 文件的硬链接数 */
uid_t st_uid; /* 文件所有者的用户 ID */
gid_t st_gid; /* 文件所有者的组 ID */
dev_t st_rdev; /* 如果文件是设备文件,则为其设备编号 */
off_t st_size; /* 文件的大小,以字节为单位 */
blksize_t st_blksize; /* 文件系统的块大小 */
blkcnt_t st_blocks; /* 文件所占用的块数 */
time_t st_atime; /* 文件的最后访问时间 */
time_t st_mtime; /* 文件的最后修改时间 */
time_t st_ctime; /* 文件的最后状态改变时间 */
};
struct stat 结构体中的各个字段含义如下:
st_dev:文件所在的设备编号;
st_ino:文件的 inode 号;
st_mode:文件的类型和权限;
st_nlink:文件的硬链接数;
st_uid:文件所有者的用户 ID;
st_gid:文件所有者的组 ID;
st_rdev:如果文件是设备文件,则为其设备编号;
st_size:文件的大小,以字节为单位;
st_blksize:文件系统的块大小;
st_blocks:文件所占用的块数;
st_atime:文件的最后访问时间;
st_mtime:文件的最后修改时间;
st_ctime:文件的最后状态改变时间。
在实际编程中,我们可以通过调用 stat 或 lstat 函数来获取一个文件的元数据信息,并将其存储在 struct stat 结构体中。需要注意的是,stat 函数和 lstat 函数的区别在于,当文件为符号链接时,stat 函数返回链接目标文件的元数据信息,而 lstat 函数返回链接文件本身的元数据信息。
3.3 inode号查找文件
一个inode号可以对应多个文件。
在 Linux 中,可以使用 inode 号来查找文件。下面介绍两种基本的方法。
(1)使用 find 命令
find 命令可以用于在指定目录下查找满足条件的文件或目录,其中可以使用 -inum 选项指定 inode 号来查找文件。例如,下面的命令可以在 /home/user 目录下查找 inode 号为 123 的文件:
find /home/user -inum 123
(2)使用 debugfs 工具
debugfs 是一个用于调试文件系统的工具,它可以让用户直接访问文件系统的数据结构,包括 inode 表。使用 debugfs 工具可以通过 inode 号查找文件,并且可以查看文件的元数据信息。下面是一个使用 debugfs 工具查找文件的示例:
debugfs /dev/sda1 # 进入 debugfs 工具
debugfs: inode <123> # 查找 inode 号为 123 的文件
debugfs: ls -l # 查看文件的元数据信息
在上面的示例中,我们首先使用 debugfs 命令进入 debugfs 工具,然后使用 inode 命令查找 inode 号为 123 的文件。最后,我们使用 ls -l 命令查看该文件的元数据信息。
需要注意的是,使用 debugfs 工具需要具有管理员权限,同时也需要谨慎操作,避免对文件系统造成损坏。
总结
在 Linux 中,文件路径、目录项和 inode 号之间的关系可以用下图来表示:
文件路径 --> 目录项 --> 文件名 --> inode 号
因此,文件路径和inode号之间的关系是通过目录项建立的。
目录项是指文件系统中的一个目录条目,包含了文件名和文件的 inode 号。在 Linux 中,每个目录中都包含了若干个目录项,其中每个目录项对应着文件系统中的一个文件或目录。因此,我们可以通过目录项来查找文件或目录的 inode 号,根据inode号可以获取到inode节点,从而获取文件或目录的元数据信息,根据元数据信息的指向文件数据块的指针寻址到对应的文件数据块,文件数据块保存着实际的文件内容。
目录:
目录项1 文件名1 -- >inode号1
目录项2 文件名2 -- >inode号2
目录项3 文件名3 -- >inode号3
目录项4 文件名4 -- >inode号4
目录项5 文件名5 -- >inode号5
......
然后根据inode号来获取文件的内容:
inode号 --> inode节点 --> 文件数据块指针寻找 --> 文件数据块1
--> 文件数据块2
--> 文件数据块n
根据 inode 号可以找到磁盘上对应块组中inode表中inode结构体。
一个文件会有多个文件数据块。
具体来说,当我们在 Linux 中打开一个文件时,系统会先通过文件路径找到该文件所在的目录,然后根据文件名在目录中查找对应的目录项,最后获取目录项中存储的 inode 号。通过 inode 号,系统可以查找文件的元数据信息,并访问文件的内容。因此,文件路径、目录项和 inode 号是 Linux 文件系统中的三个重要概念,它们共同构成了文件系统的基本结构。
文件名和inode号之间的关系是通过目录项(directory entry)来建立的。目录项是一个数据结构,它包含了文件名和文件的inode号之间的映射关系。每个目录都是一个文件,它包含了多个目录项,每个目录项包含了一个文件名和一个inode号。当用户在文件系统中打开一个文件时,系统会根据文件名查找对应的目录项,获取该文件的inode号,根据inode号可以获取到inode节点,然后根据inode节点获取文件的元数据信息中的文件数据块地址,根据文件数据块地址寻址到实际的文件数据块,从文件数据块读取文件的内容。
如果用户移动或重命名文件,系统会更新目录项中的文件名,但不会改变文件的inode号,因为inode号是唯一标识文件的标识符。因此,即使文件名发生了改变,只要inode号没有改变,用户仍然可以通过修改后的路径访问该文件。
移动或重命名都是调用的 rename系列的系统调用。
一个文件可以有多个目录项,也称为硬链接。在 Linux 和其他类 Unix 操作系统中,硬链接是指指向与原始文件相同的 inode 的目录项。这意味着可以从多个目录路径访问该文件,并且对文件的修改将反映在所有硬链接中。
硬链接只能针对文件而不是目录创建,并且必须在同一文件系统中创建。此外,只要该文件仍有任何硬链接存在,删除任何硬链接都不会删除该文件本身,因为该文件仍然可以通过其余硬链接访问。
在文件系统重新挂载后,文件的路径可能会发生变化,这取决于重新挂载时挂载点的位置是否改变。挂载点是指将一个文件系统挂载到另一个文件系统中的目录。当一个文件系统被挂载到某个目录下时,该目录就成为了挂载点,文件系统中的文件和目录就可以通过该挂载点访问。
如果重新挂载时挂载点的位置没有改变,那么文件的路径不会发生变化,它们仍然可以通过原来的路径访问。但是,如果重新挂载时挂载点的位置改变了,那么文件的路径就会发生变化,它们可能无法通过原来的路径访问,需要根据新的挂载点来访问。在这种情况下,由于文件的 inode 号不会改变,因此仍然可以通过 inode 号来访问文件。
对于inode号,一个文件的路径修改后以及文件系统重新挂载后,该文件的inode都不会变。
但是一台机器会有多个文件系统,因此文件的inode可能重复。