【Linux】基础 IO(一)—— 文件操作及文件系统

目录

[1. 本节重点](#1. 本节重点)

[2. 回顾 C 语言文件 IO](#2. 回顾 C 语言文件 IO)

[3. C 文件接口示例:写文件](#3. C 文件接口示例:写文件)

[4. C 文件接口示例:读文件](#4. C 文件接口示例:读文件)

[5. 输出信息到显示器的方法](#5. 输出信息到显示器的方法)

[6. stdin、stdout、stderr](#6. stdin、stdout、stderr)

[7. fopen 打开文件的方式](#7. fopen 打开文件的方式)

[8. 其他 C 文件接口](#8. 其他 C 文件接口)

[9. 系统文件 IO](#9. 系统文件 IO)

[10. 系统调用示例:写文件](#10. 系统调用示例:写文件)

[11. 系统调用示例:读文件](#11. 系统调用示例:读文件)

[12. open 接口介绍](#12. open 接口介绍)

[12.1 参数 pathname](#12.1 参数 pathname)

[12.2 参数 flags](#12.2 参数 flags)

[12.3 参数 mode](#12.3 参数 mode)

[12.4 open 使用哪个版本?](#12.4 open 使用哪个版本?)

[13. 系统调用和库函数的关系](#13. 系统调用和库函数的关系)

[14. open 的返回值:文件描述符 fd](#14. open 的返回值:文件描述符 fd)

[15. Linux 默认打开的三个文件描述符](#15. Linux 默认打开的三个文件描述符)

[16. 文件描述符的本质](#16. 文件描述符的本质)

[17. 文件描述符的分配规则](#17. 文件描述符的分配规则)

[18. 重定向](#18. 重定向)

[18.1 常见重定向符号](#18.1 常见重定向符号)

[19. dup2 系统调用](#19. dup2 系统调用)

[19.1 dup2 示例](#19.1 dup2 示例)

[20. 为什么 printf 也会被重定向?](#20. 为什么 printf 也会被重定向?)

[21. FILE 和 fd 的关系](#21. FILE 和 fd 的关系)

[22. C 库缓冲区与系统调用的区别](#22. C 库缓冲区与系统调用的区别)

[22.1 为什么会这样?](#22.1 为什么会这样?)

[22.2 缓冲区是谁提供的?](#22.2 缓冲区是谁提供的?)

[23. FILE 结构体中确实封装了 fd](#23. FILE 结构体中确实封装了 fd)

[24. 理解文件系统](#24. 理解文件系统)

[24.1 stat 命令](#24.1 stat 命令)

[25. inode 是什么?](#25. inode 是什么?)

[25.1 每一个分区:](#25.1 每一个分区:)

[25.2 每一个block组:](#25.2 每一个block组:)

[26. 一些常见问题](#26. 一些常见问题)

[1. 基础问题](#1. 基础问题)

[(1) 新建一个文件,系统要做什么?](#(1) 新建一个文件,系统要做什么?)

[(2) 删除一个文件,系统要做什么?](#(2) 删除一个文件,系统要做什么?)

[(3) 查看一个文件,系统要做什么?](#(3) 查看一个文件,系统要做什么?)

[(4) 修改一个文件,系统要做什么?](#(4) 修改一个文件,系统要做什么?)

[2. 扩展问题](#2. 扩展问题)

(1)如何理解目录?

(2)为什么同一个目录下不能有同名文件?

[(3)为什么目录下,没有 w 权限,无法创建文件?](#(3)为什么目录下,没有 w 权限,无法创建文件?)

[(4)为什么目录下,没有 r 权限,无法查看文件?](#(4)为什么目录下,没有 r 权限,无法查看文件?)

[(5)为什么目录下,没有 x 权限,无法进入该目录?](#(5)为什么目录下,没有 x 权限,无法进入该目录?)

[(6)问题来了,又如何找到目录的 inode 呢?](#(6)问题来了,又如何找到目录的 inode 呢?)


1. 本节重点

这一部分主要围绕 Linux 基础 IO 展开,核心内容包括:

  • C 语言文件 IO 相关操作
  • 认识文件相关系统调用接口
  • 认识文件描述符 fd
  • 理解输入、输出、错误输出以及重定向
  • 对比 fd 和 FILE
  • 理解系统调用和库函数之间的关系
  • 理解文件系统中的 inode 概念

2. 回顾 C 语言文件 IO

在 C 语言中,我们通常通过标准库函数操作文件,例如:

  • fopen
  • fclose
  • fread
  • fwrite
  • fprintf
  • printf
  • fseek
  • ftell
  • rewind

这些函数都属于 C 标准库提供的文件操作接口。


3. C 文件接口示例:写文件

下面是一个使用 C 标准库接口写文件的例子。

cpp 复制代码
#include <stdio.h>
#include <string.h>
int main()
{
    FILE *fp = fopen("myfile", "w");
    if(!fp)
    {
        printf("fopen error!\n");
    }
    const char *msg = "hello!\n";
    int count = 5;
    while(count--)
    {
        fwrite(msg, strlen(msg), 1, fp);
    }
    fclose(fp);
    return 0;
}

代码说明

cpp 复制代码
FILE *fp = fopen("myfile", "w");

表示以写方式打开文件 myfile。

如果文件不存在,会创建该文件;如果文件已经存在,会先清空文件内容。

cpp 复制代码
fwrite(msg, strlen(msg), 1, fp);

表示向文件中写入字符串 hello!\n。

这里循环写入 5 次,因此文件中最终会有 5 行:

cpp 复制代码
hello!
hello!
hello!
hello!
hello!

4. C 文件接口示例:读文件

下面是使用 C 标准库接口读取文件的例子。

cpp 复制代码
#include <stdio.h>
#include <string.h>
int main()
{
    FILE *fp = fopen("myfile", "r");
    if(!fp)
    {
        printf("fopen error!\n");
    }
    char buf[1024];
    const char *msg = "hello bit!\n";
    while(1)
    {
        // 注意返回值和参数,此处有坑,仔细查看 man 手册关于该函数的说明
        ssize_t s = fread(buf, 1, strlen(msg), fp);
        if(s > 0)
        {
            buf[s] = 0;
            printf("%s", buf);
        }
        if(feof(fp))
        {
            break;
        }
    }
    fclose(fp);
    return 0;
}

代码说明

cpp 复制代码
fread(buf, 1, strlen(msg), fp);

表示每次尝试从文件中读取 strlen (msg) 个字节。

这里需要特别注意:

cpp 复制代码
buf[s] = 0;

这一步很重要,因为 fread 读取的是原始字节,并不会自动在字符串末尾添加 \0。

如果不手动补 \0,后续用 printf ("% s", buf) 打印时可能会越界读取,导致乱码或未定义行为。


5. 输出信息到显示器的方法

在 C 语言中,向显示器输出内容有多种方式,例如:

cpp 复制代码
#include <stdio.h>
#include <string.h>
int main()
{
    const char *msg = "hello fwrite\n";
    fwrite(msg, strlen(msg), 1, stdout);
    printf("hello printf\n");
    fprintf(stdout, "hello fprintf\n");
    return 0;
}

这里展示了三种输出方式:

cpp 复制代码
fwrite(msg, strlen(msg), 1, stdout);

向标准输出流 stdout 写入数据。

cpp 复制代码
printf("hello printf\n");

默认向标准输出输出内容。

cpp 复制代码
fprintf(stdout, "hello fprintf\n");

显式指定向 stdout 输出内容。


6. stdin、stdout、stderr

C 程序默认会打开三个输入输出流:

名称 含义 默认设备
stdin 标准输入 键盘
stdout 标准输出 显示器
stderr 标准错误 显示器

这三个流的类型都是:

cpp 复制代码
FILE *

而 fopen 的返回值类型也是 FILE *,所以它们本质上都是文件流指针。


7. fopen 打开文件的方式

常见的文件打开方式如下:

7.1 r

复制代码
r      Open text file for reading.
       The stream is positioned at the beginning of the file.

含义:

  • 以只读方式打开文本文件
  • 文件必须存在
  • 文件位置位于文件开头

7.2 r+

复制代码
r+     Open for reading and writing.
       The stream is positioned at the beginning of the file.

含义:

  • 以读写方式打开文件
  • 文件必须存在
  • 文件位置位于文件开头

7.3 w

复制代码
w      Truncate file to zero length or create text file for writing.
       The stream is positioned at the beginning of the file.

含义:

  • 以只写方式打开文件
  • 如果文件不存在,则创建文件
  • 如果文件存在,则将文件截断为 0,即清空文件内容
  • 文件位置位于文件开头

7.4 w+

复制代码
w+     Open for reading and writing.
       The file is created if it does not exist, otherwise it is truncated.
       The stream is positioned at the beginning of the file.

含义:

  • 以读写方式打开文件
  • 如果文件不存在,则创建文件
  • 如果文件存在,则清空文件
  • 文件位置位于文件开头

7.5 a

复制代码
a      Open for appending.
       The file is created if it does not exist.
       The stream is positioned at the end of the file.

含义:

  • 以追加写方式打开文件
  • 如果文件不存在,则创建文件
  • 写入位置位于文件末尾

7.6 a+

复制代码
a+     Open for reading and appending.
       The file is created if it does not exist.
       The initial file position for reading is at the beginning of the file,
       but output is always appended to the end of the file.

含义:

  • 以读和追加写方式打开文件
  • 如果文件不存在,则创建文件
  • 读的位置默认在文件开头
  • 写入内容永远追加到文件末尾

8. 其他 C 文件接口

除了上面提到的函数,还有:

  • fseek
  • ftell
  • rewind

这些函数可以用来控制文件读写位置,感兴趣的可以自行了解。


9. 系统文件 IO

操作文件除了使用 C 标准库接口,还可以使用操作系统提供的系统调用接口。

C 标准库接口包括:

cpp 复制代码
fopen
fclose
fread
fwrite

系统调用接口包括:

复制代码
open
close
read
write
lseek

系统调用更接近操作系统底层。


10. 系统调用示例:写文件

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
    umask(0);
    int fd = open("myfile", O_WRONLY | O_CREAT, 0644);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    int count = 5;
    const char *msg = "hello bit!\n";
    int len = strlen(msg);
    while(count--)
    {
        write(fd, msg, len);
        // fd: 文件描述符
        // msg: 缓冲区首地址
        // len: 本次期望写入多少个字节
        // 返回值: 实际写入多少个字节
    }
    close(fd);
    return 0;
}

代码说明

cpp 复制代码
umask(0);

用于取消进程默认权限掩码的影响,使创建文件时权限更直观。

cpp 复制代码
open("myfile", O_WRONLY | O_CREAT, 0644);

表示:

  • 以只写方式打开文件
  • 如果文件不存在,则创建文件
  • 新文件默认权限为 0644
cpp 复制代码
write(fd, msg, len);

向文件描述符 fd 对应的文件写入数据。

cpp 复制代码
close(fd);

关闭文件。


11. 系统调用示例:读文件

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
    int fd = open("myfile", O_RDONLY);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    const char *msg = "hello bit!\n";
    char buf[1024];
    while(1)
    {
        ssize_t s = read(fd, buf, strlen(msg));
        if(s > 0)
        {
            buf[s] = '\0';
            printf("%s", buf);
        }
        else
        {
            break;
        }
    }
    close(fd);
    return 0;
}

注意

如果将 buf 当字符串打印,最好加上上:

cpp 复制代码
buf[s] = '\0';

因为 read 不会自动添加字符串结束符。


12. open 接口介绍

使用 man open 可以查看 open 的函数原型:

cpp 复制代码
#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);

12.1 参数 pathname

cpp 复制代码
pathname

表示要打开或创建的目标文件路径。

12.2 参数 flags

flags 表示打开文件的方式,可以由多个选项通过按位或 | 组合。

常见选项包括:

复制代码
O_RDONLY

只读打开。

复制代码
O_WRONLY

只写打开。

复制代码
O_RDWR

读写打开。

注意:

O_RDONLY、O_WRONLY、O_RDWR 这三个常量必须指定一个,并且只能指定一个。

其他常见选项:

复制代码
O_CREAT

如果文件不存在,则创建文件。使用该选项时,一般需要第三个参数 mode 指明新文件权限。

复制代码
O_APPEND

以追加方式写入文件。

复制代码
O_TRUNC

打开文件时清空文件内容。

12.3 参数 mode

复制代码
mode_t mode

用于指定创建文件时的默认权限。

例如:

cpp 复制代码
open("myfile", O_WRONLY | O_CREAT, 0644);

表示创建出的文件权限为:

bash 复制代码
rw-r--r--

需要注意,最终权限还会受到 umask 的影响。

12.4 open 使用哪个版本?

如果只打开已经存在的文件,可以使用两个参数版本:

cpp 复制代码
open("myfile", O_RDONLY);

如果目标文件不存在,需要创建文件,则应使用三个参数版本:

cpp 复制代码
open("myfile", O_WRONLY | O_CREAT, 0644);

13. 系统调用和库函数的关系

前面使用的:

cpp 复制代码
fopen
fclose
fread
fwrite

属于 C 标准库函数,也就是库函数,来自 libc。

而:

cpp 复制代码
open
close
read
write
lseek

属于操作系统提供的接口,叫做系统调用接口。

可以认为:

C 标准库中的 f 系列文件操作函数,本质上是对系统调用接口的封装,目的是让开发者使用起来更方便。

例如:

fopen底层可能会封装:open

fwrite底层可能会封装:write


14. open 的返回值:文件描述符 fd

open 的返回值如下:

返回值 含义
成功 返回新打开文件的文件描述符
失败 返回 -1

文件描述符,也就是 fd,本质上是一个小整数。


15. Linux 默认打开的三个文件描述符

Linux 进程默认会打开 3 个文件描述符:

文件描述符 名称 对应 C 流 默认设备
0 标准输入 stdin 键盘
1 标准输出 stdout 显示器
2 标准错误 stderr 显示器

因此,可以用系统调用实现输入输出:

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main()
{
    char buf[1024];
    ssize_t s = read(0, buf, sizeof(buf));
    if(s > 0)
    {
        buf[s] = 0;
        write(1, buf, strlen(buf));
        write(2, buf, strlen(buf));
    }
    return 0;
}

这段代码表示:

  • 从标准输入 0 读取数据
  • 向标准输出 1 输出一份
  • 向标准错误 2 输出一份

16. 文件描述符的本质

当进程打开文件时,操作系统会在内核中创建对应的数据结构,用来描述这个被打开的文件。

大致过程如下:

  1. 操作系统在内存中创建 file 结构体,表示一个已经打开的文件对象。
  2. 每个进程都有一个指针 files。
  3. files 指向一张表,通常称为 files_struct。
  4. files_struct 中最重要的部分是一个指针数组。
  5. 数组中的每个元素都指向一个已经打开的文件对象。
  6. 文件描述符 fd 本质上就是这个数组的下标。

因此:

只要拿着文件描述符,就可以在进程的文件描述符表中找到对应的文件对象。


17. 文件描述符的分配规则

示例代码:

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
    int fd = open("myfile", O_RDONLY);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd);
    close(fd);
    return 0;
}

正常情况下运行结果通常是:

复制代码
fd: 3

为什么是 3?

因为:

  • 0 已经被标准输入占用
  • 1 已经被标准输出占用
  • 2 已经被标准错误占用

所以新打开的文件使用最小的未使用下标,也就是 3。

如果关闭 0:

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
    close(0);
    // close(2);
    int fd = open("myfile", O_RDONLY);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd);
    close(fd);
    return 0;
}

此时新文件描述符可能是:

复制代码
fd: 0

如果关闭 2,则可能得到:

复制代码
fd: 2

因此,文件描述符分配规则是:

在 files_struct 数组中,找到当前没有被使用的最小下标,作为新的文件描述符。


18. 重定向

如果关闭 1,也就是关闭标准输出,再打开文件,会发生什么?

示例:

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
    close(1);
    int fd = open("myfile", O_WRONLY | O_CREAT, 00644);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd);
    fflush(stdout);
    close(fd);
    exit(0);
}

此时,本来应该输出到显示器上的内容,会被输出到文件 myfile 中。

原因是:

  • 关闭了文件描述符 1
  • open 会选择最小的可用文件描述符
  • 所以新打开的文件获得 fd = 1
  • printf 默认向 stdout 输出
  • stdout 底层使用的正是文件描述符 1
  • 于是输出内容被写入文件

这就是输出重定向。

18.1 常见重定向符号

符号 含义
> 输出重定向,覆盖写
>> 输出重定向,追加写
< 输入重定向

19. dup2 系统调用

重定向的本质可以使用 dup2 来解释。

函数原型:

cpp 复制代码
#include <unistd.h>
int dup2(int oldfd, int newfd);

含义:

让 newfd 指向 oldfd 所指向的文件。

例如:

cpp 复制代码
dup2(fd, 1);

表示让标准输出 1 指向 fd 对应的文件。

19.1 dup2 示例

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() 
{
    int fd = open("./log", O_CREAT | O_RDWR, 0664);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    close(1);
    dup2(fd, 1);
    for (;;)
    {
        char buf[1024] = {0};
        ssize_t read_size = read(0, buf, sizeof(buf) - 1);
        if (read_size < 0) 
        {
            perror("read");
            break;
        }
        printf("%s", buf);
        fflush(stdout);
    }
    return 0;
}

这段代码会:

  • 打开或创建 log 文件
  • 将标准输出重定向到 log
  • 从标准输入读取内容
  • 原本输出到显示器的内容,现在写入 log

20. 为什么 printf 也会被重定向?

printf 是 C 标准库中的 IO 函数,一般向 stdout 输出。

但是 stdout 底层访问文件时,最终找的还是文件描述符 1。

当 fd = 1 指向的内容已经从显示器变成文件时:

cpp 复制代码
printf
fprintf
fwrite

这些输出到 stdout 的函数,最终都会写入文件。

所以:

输出重定向的本质是改变文件描述符表中特定下标所指向的文件对象。


21. FILE 和 fd 的关系

因为 IO 库函数最终会调用系统调用接口,而系统调用访问文件依赖文件描述符 fd,所以 C 标准库中的 FILE 结构体内部必然封装了 fd。

也就是说:

cpp 复制代码
FILE *

是 C 标准库层面的文件对象。

复制代码
int fd

是系统调用层面的文件描述符。

二者关系可以理解为:

cpp 复制代码
FILE*  ----封装----> fd ----操作系统----> 文件

22. C 库缓冲区与系统调用的区别

示例代码:

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
    const char *msg0 = "hello printf\n";
    const char *msg1 = "hello fwrite\n";
    const char *msg2 = "hello write\n";
    printf("%s", msg0);
    fwrite(msg1, strlen(msg1), 1, stdout);
    write(1, msg2, strlen(msg2));
    fork();
    return 0;
}

直接运行时,可能输出:

cpp 复制代码
hello printf
hello fwrite
hello write

但是如果执行输出重定向:

复制代码
./hello > file

可能会发现:

复制代码
hello write
hello printf
hello fwrite
hello printf
hello fwrite

也就是:

  • printf 输出了 2 次
  • fwrite 输出了 2 次
  • write 只输出了 1 次

22.1 为什么会这样?

原因和缓冲区以及 fork 有关。

一般来说:

  • C 库函数写入显示器时,通常是行缓冲
  • C 库函数写入普通文件时,通常是全缓冲
  • printf、fwrite 是库函数,自带用户级缓冲区
  • write 是系统调用,没有 C 库提供的用户级缓冲区

当重定向到普通文件时:

复制代码
printf
fwrite

写入的数据可能还留在 C 标准库缓冲区中,没有立即刷新。

执行 fork () 时:

  • 父子进程共享相同的逻辑数据
  • 发生写时拷贝
  • 子进程也拥有一份缓冲区数据
  • 父子进程退出时都会刷新缓冲区

因此 printf 和 fwrite 的内容被写入两次

而:

复制代码
write(1, msg2, strlen(msg2));

是系统调用,直接写入内核,不经过 C 库用户级缓冲区,所以只写一次。

22.2 缓冲区是谁提供的?

printf、fwrite 的缓冲区由 C 标准库提供,是用户级缓冲区。

而操作系统为了提升整机性能,也会提供内核级缓冲区,但这里讨论的是 C 库提供的用户级缓冲区。


23. FILE 结构体中确实封装了 fd

在 /usr/include/stdio.h 中可以看到:

cpp 复制代码
typedef struct _IO_FILE FILE;

在 /usr/include/libio.h 中可以看到类似结构:

cpp 复制代码
struct _IO_FILE {
    int _flags;
    // 缓冲区相关
    char* _IO_read_ptr;
    char* _IO_read_end;
    char* _IO_read_base;
    char* _IO_write_base;
    char* _IO_write_ptr;
    char* _IO_write_end;
    char* _IO_buf_base;
    char* _IO_buf_end;
    char *_IO_save_base;
    char *_IO_backup_base;
    char *_IO_save_end;
    struct _IO_marker *_markers;
    struct _IO_FILE *_chain;
    int _fileno; // 封装的文件描述符
    int _flags2;
    _IO_off_t _old_offset;
    unsigned short _cur_column;
    signed char _vtable_offset;
    char _shortbuf[1];
    _IO_lock_t *_lock;
};

其中:

cpp 复制代码
int _fileno;

就是 FILE 结构体中封装的文件描述符。


24. 理解文件系统

使用:

复制代码
ls -l

可以看到文件的元数据。

示例:

cpp 复制代码
[root@localhost linux]# ls -l
总用量 12
-rwxr-xr-x. 1 root root 7438 9月 13 14:56 a.out
-rw-r--r--. 1 root root  654 9月 13 14:56 test.c

每行通常包含以下信息:

  • 模式,也就是文件类型和权限
  • 硬链接数
  • 文件所有者
  • 所属组
  • 文件大小
  • 最后修改时间
  • 文件名

24.1 stat 命令

除了 ls -l,还可以使用 stat 查看更详细的文件信息。

示例:

cpp 复制代码
[root@localhost linux]# stat test.c
  File: "test.c"
  Size: 654         Blocks: 8          IO Block: 4096   普通文件
Device: 802h/2050d  Inode: 263715      Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2017-09-13 14:56:57.059012947 +0800
Modify: 2017-09-13 14:56:40.067012944 +0800
Change: 2017-09-13 14:56:40.069012948 +0800

其中重要信息包括:

  • 文件大小
  • 占用块数
  • IO 块大小
  • 设备号
  • inode 编号
  • 硬链接数
  • 权限
  • 用户 ID
  • 组 ID
  • 访问时间
  • 修改时间
  • 属性变化时间

25. inode 是什么?

为了理解 inode,需要先了解文件系统。

以 Linux ext2 文件系统为例,磁盘是典型的块设备,硬盘分区会被划分为一个个 block。

一个 block 的大小由格式化时决定,并且之后不能随意更改。

25.1 每一个分区:

1. Boot Block

启动块,也就是 Boot Block,大小是固定的,主要与系统启动相关。

2. Block Group

文件系统会根据分区大小划分成多个 Block Group。

每个 Block Group 都有相同的结构组成。

文档中用 "政府管理各区" 的例子理解:

  • 整个文件系统像一个国家
  • 每个 Block Group 像一个行政区
  • 每个行政区内部有类似的管理结构

25.2 每一个block组:

1. Super Block

超级块,也就是 Super Block,存放文件系统本身的结构信息。

主要记录:

  • block 总量
  • inode 总量
  • 未使用 block 数量
  • 未使用 inode 数量
  • 一个 block 的大小
  • 一个 inode 的大小
  • 最近一次挂载时间
  • 最近一次写入数据时间
  • 最近一次检查磁盘时间
  • 其他文件系统相关信息

如果 Super Block 被破坏,可以认为整个文件系统结构就被破坏了。


2. GDT

GDT 是:Group Descriptor Table,即块组描述符表,用来描述块组属性信息。

描述整个组的整体使用情况


3. Block Bitmap

块位图,记录 Data Block 中哪些数据块已经被占用,哪些数据块没有被占用。


4. inode Bitmap

inode 位图,每个 bit 表示一个 inode 是否空闲可用。


5. inode Table

inode table 本质上是一个 inode 的数组/表格。

每个 inode 是一个数据结构,里面包含了文件的元数据(属性)和指向数据块的指针。

存放的属于举例:

  • 文件大小
  • 文件所有者
  • 文件最近修改时间
  • 权限
  • 数据块位置等

Inode表与数据块关系: 当访问文件时,文件系统会使用其 inode 编号 在 inode 表中定位该 inode,inode随后提供存储文件内容的磁盘上数据块地址


6. Data Block

数据区,用来存放文件真正的内容,通常以块的形式呈现,块的大小常见为4kb。


26. 一些常见问题

1. 基础问题

(1) 新建一个文件,系统要做什么?

先查 GDT 中 inode 使用情况,看分配到哪个组的 inode,之后查看该组的 inode Bitmap,寻一个最近且空闲的 inode 编号,将该编号的状态由0置1,由该编号找到对应的 inode,将文件的属性信息填入该 inode,至此该文件就算新建成功。之后若要对文件进行写入,可由该文件的 inode 找到对应分组,先确认写入数据的大小,以确定需要多少个块,再遍历 Block Bitmap 找到对应数量空闲的块号,将块号填到 inode 的对应数组中,之后直接跳转过去,将要写入的数据写入对应的块中。

(2) 删除一个文件,系统要做什么?

根据 inode 编号找到其在 inode Bitmap 中的状态,为 1 则去 inode Table 中读取该文件的属性,获得该文件的所有块号。

之后在 Block Bitmap 中将获得的所有块号的比特位置 0。

接着在 inode Bitmap 中将该文件的 inode 置 0。

删除等于块内容允许被覆盖(所以有重要文件被删时,不要继续操作,以免该文件被覆盖)

(3) 查看一个文件,系统要做什么?

先得到文件的 inode,再查询 inode Bitmap,查询该 inode 是否有效,确认有效后,再由该 inode 查看该文件属性,若要查询文件内容,由 inode 中对应数组中的块号找到 Data blocks 中该文件所有的块,把所有块的内容按顺序拼接好,然后将需要的属性和数据载入内存即可。

(4) 修改一个文件,系统要做什么?

在查找的基础上,直接修改即可。

2. 扩展问题

前面 4 个问题都需要解决同一个问题:

如何找到一个文件的 inode 编号?通过目录找

使用者从来没关心过 inode,用户使用的是文件名。

inode 表示文件的所有属性,而文件名,并不属于 inode 内的属性!

(1)如何理解目录?

目录也是文件,也有自己的 inode,也有自己的属性和内容

即目录也有数据块

其中存放的是此目录下文件的文件名和对应文件的 inode 的映射关系

(2)为什么同一个目录下不能有同名文件?

因为目录下存的是文件名和该文件对应 inode 的映射关系,是一种 <key, value> 结构,若出现同名文件,即出现相同 key,映射关系就乱了。

(3)为什么目录下,没有 w 权限,无法创建文件?

如果没有写权限,即便该文件创建出来,该文件与 inode 的映射关系也无法写入这个目录文件对应的数据块里。

(4)为什么目录下,没有 r 权限,无法查看文件?

没有 r 权限,则无法查看目录下的文件名与 inode 的映射关系,无法拿到文件的 inode,自然无法查看文件。

(5)为什么目录下,没有 x 权限,无法进入该目录?

目录的 x 权限本质是允许系统通过目录 inode 访问其内部的文件/子目录 inode 映射关系,没有 x 权限,就无法触发这一"索引查找"过程,自然无法进入目录。

核心逻辑:

  1. 目录 inode 的核心作用:目录本质是特殊文件,其 inode 存储的并非文件内容,而是一份"文件名-inode 号"的映射表(类似书本的"章节名-页码"对应表)。
  1. x 权限的关键角色:当你执行 cd 进入目录时,系统需要读取这份映射表才能定位目录内的内容(比如 . 和 .. 这两个特殊项),而 x 权限是读取这份映射表的"准入许可"。
(6)问题来了,又如何找到目录的 inode 呢?

向上递归,找到根目录,找到它的 inode

通过 inode 查看它的数据,再向下递归,

就能找到目标目录的 inode。
在 Linux 中,根目录 / 的 inode 号在大多数文件系统(如 ext4)中固定为 2,这是文件系统初始化时的约定,可直接使用,无需通过文件名查找。若要验证,可执行 ls -di /,输出通常为 2 /,其中 2 即为根目录 inode 号。

原理上,系统通过文件系统超级块直接获取根目录 inode 号,而非通过"文件名→inode"映射查找,因为根目录是路径起点,没有更高层级目录可用于检索其 inode。所以,知道文件名是 / 并不能"推导"出 inode 号,而是依赖文件系统内部的固定配置。
系统找文件必须带路径,这样很慢,所以 Linux 系统会将常用的路径缓存起来(dentry 缓存)


感谢阅读,本文如有错漏之处,烦请斧正。

相关推荐
utf8mb4安全女神2 小时前
shell中的判断语法
linux·运维·服务器
mifengxing3 小时前
操作系统(五)
linux·运维·服务器·操作系统·王道考研
ALINX技术博客3 小时前
【黑金云课堂】FPGA技术教程Linux开发:NVMe/Qt/OpenCV人脸检测
linux·qt·fpga开发
kebidaixu4 小时前
OK3568 RTC 驱动适配与 Linux 系统时间管理总结
linux
戴西软件4 小时前
戴西CAxWorks.AICrash:AI+法规驱动的行人保护自动化分析
linux·运维·网络·人工智能·安全·自动化
CingSyuan4 小时前
Linux服务器数据盘初始化与盘符漂移解决方案:标准分区、LVM逻辑卷、XFS格式化、fstab配置与UUID持久化挂载实战
linux·运维·服务器
jingling5554 小时前
从零到一:用 Aholo Viewer 在浏览器里渲染 3D 高斯泼溅小熊
linux·前端·ubuntu·3d
张青贤5 小时前
centos7内核kernel升级
linux·centos·内核·kernel
Kingairy5 小时前
vi(vim)常用命令汇总
linux·编辑器·vim