目录
[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. 扩展问题)
[(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
cpppathname表示要打开或创建的目标文件路径。
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用于指定创建文件时的默认权限。
例如:
cppopen("myfile", O_WRONLY | O_CREAT, 0644);表示创建出的文件权限为:
bashrw-r--r--需要注意,最终权限还会受到 umask 的影响。
12.4 open 使用哪个版本?
如果只打开已经存在的文件,可以使用两个参数版本:
cppopen("myfile", O_RDONLY);如果目标文件不存在,需要创建文件,则应使用三个参数版本:
cppopen("myfile", O_WRONLY | O_CREAT, 0644);
13. 系统调用和库函数的关系
前面使用的:
cppfopen fclose fread fwrite属于 C 标准库函数,也就是库函数,来自 libc。
而:
cppopen 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. 文件描述符的本质
当进程打开文件时,操作系统会在内核中创建对应的数据结构,用来描述这个被打开的文件。
大致过程如下:
- 操作系统在内存中创建 file 结构体,表示一个已经打开的文件对象。
- 每个进程都有一个指针 files。
- files 指向一张表,通常称为 files_struct。
- files_struct 中最重要的部分是一个指针数组。
- 数组中的每个元素都指向一个已经打开的文件对象。
- 文件描述符 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 所指向的文件。
例如:
cppdup2(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。
也就是说:
cppFILE *是 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 权限,就无法触发这一"索引查找"过程,自然无法进入目录。
核心逻辑:
- 目录 inode 的核心作用:目录本质是特殊文件,其 inode 存储的并非文件内容,而是一份"文件名-inode 号"的映射表(类似书本的"章节名-页码"对应表)。
- x 权限的关键角色:当你执行 cd 进入目录时,系统需要读取这份映射表才能定位目录内的内容(比如 . 和 .. 这两个特殊项),而 x 权限是读取这份映射表的"准入许可"。
(6)问题来了,又如何找到目录的 inode 呢?
向上递归,找到根目录,找到它的 inode
通过 inode 查看它的数据,再向下递归,
就能找到目标目录的 inode。
在 Linux 中,根目录 / 的 inode 号在大多数文件系统(如 ext4)中固定为 2,这是文件系统初始化时的约定,可直接使用,无需通过文件名查找。若要验证,可执行 ls -di /,输出通常为 2 /,其中 2 即为根目录 inode 号。原理上,系统通过文件系统超级块直接获取根目录 inode 号,而非通过"文件名→inode"映射查找,因为根目录是路径起点,没有更高层级目录可用于检索其 inode。所以,知道文件名是 / 并不能"推导"出 inode 号,而是依赖文件系统内部的固定配置。
系统找文件必须带路径,这样很慢,所以 Linux 系统会将常用的路径缓存起来(dentry 缓存)
感谢阅读,本文如有错漏之处,烦请斧正。



