# 文件
- Linux 系统中,一切对象都是文件。例如数据、目录、设备、套接字,都以文件的形式展示,供用户读写。
- 目录(directory)也是一种文件,用于包含任意个其它文件。
- 一个目录文件,会占用 4KB 的磁盘存储空间。
- 一个目录文件与目录下的各个文件,在逻辑上是层级关系,但在磁盘中它们是分别存储的文件。
- 提到 "文件" 这个概念时,有时指包括目录在内的文件,有时指除目录以外的文件。
# 文件命名规则
- 区分大小写,不能包含正斜杆 / 。
- 建议只包含
[0-9A-Za-z._-]
这些常用字符,尽量避免使用特殊字符,以免需要转义,或者与某些软件不兼容。
- 建议只包含
- 名字以 . 开头的文件是隐藏文件。
- 每个目录下都有 . 和 .. 两个隐藏文件,指向当前目录和上一级目录。
# 文件路径
- 文件路径有两种写法:
- 绝对路径(absolute path)
- :从根目录 / 开始的路径。例如
/proc/tty
。 - 每个文件有且仅有一个绝对路径。
- :从根目录 / 开始的路径。例如
- 相对路径(relative path)
- :相对于当前目录的路径,从 . 或 .. 开始。
- 例如:以
/proc/tty
为起点的相对路径../sys
,对应的绝对路径为/proc/sys
。 - 每个文件可以有任意个相对路径。
- 绝对路径(absolute path)
- Linux 系统的文件路径以正斜杠
/
作为分隔符,而反斜杠\
通常用于转义字符。- 文件路径中多余的 / 会被忽略。例如,以下几个路径指向同一个目录:
/proc/tty /proc/tty/ /proc//tty
- 文件路径中多余的 / 会被忽略。例如,以下几个路径指向同一个目录:
# 文件类型
# 常用符号
文件类型 | 英文名 | 符号 |
---|---|---|
普通文件 | regular file | - |
目录文件 | directory | d |
软链接文件 | symbolic link | l |
套接字文件 | socket | s |
块设备文件 | block special file | b |
字符设备文件 | character special file | c |
# 链接文件
Linux 系统中有两种链接文件(link file):
- 硬链接(hard link)
- :与目标文件的 inode 编号相同,因此文件内容、大小、文件类型等元数据都相同。
- 如果文件 B 是文件 A 的硬链接,则两者指向磁盘中存储的同一个文件。删除文件 A 不会影响到文件 B ,但是修改文件 A 会同时修改文件 B ,反之亦然。
- 不支持给目录创建硬链接,避免出现循环的目录树。
- 不支持跨磁盘分区创建硬链接,否则会报错
Invalid cross-device link
,因为每个磁盘分区拥有独立的 inode table 。
- 软链接(soft link)
- :又称为符号链接(symbolic link),是一个 2 Bytes 大小的文件,存储目标文件的绝对路径。
- 如果文件 B 是文件 A 的软链接,则两者指向存储中存储的不同文件。删除文件 A 之后,文件 B 就指向一个无效的路径。
- 可以给目录创建软链接,可以跨磁盘分区创建软链接。
# 稀疏文件
- 稀疏文件(Sparse file):文件中包含一些空数据块。
- 每个空数据块,称为一个空洞(hole),由一串连续的空字节组成。
- 文件系统将稀疏文件写入磁盘时,通常会跳过空洞,只记录非空数据、空洞的位置和大小,从而节省磁盘空间。
- 文件系统从磁盘读取稀疏文件时,会自动将空洞填充到文件的非空数据中。
- 因此,用户查看稀疏文件的内容、文件体积时,跟正常文件看起来一样,但它实际占用的磁盘空间小于文件体积。
例:
- 虚拟机的磁盘在宿主机上通常保存为稀疏文件的形式,可能文件体积有 100G ,但实际只包含 1G 的非空数据。
- 执行
ls -l
命令查看的是文件体积(即包含的数据大小), 执行du
命令查看的是文件实际占用的磁盘空间。- 即使一个文件只包含 1 Byte 数据,存储时也要占用 1 个 block 。如下:
[root@CentOS ~]# echo 1 > f1 [root@CentOS ~]# ls -lh f1 -rw-rw-r--. 1 root root 2 Nov 28 16:41 f1 [root@CentOS ~]# du -h f1 4.0K f1
- 即使一个文件体积很大,但占用的磁盘空间可能很小。如下:
[root@CentOS ~]# truncate f1 -s 10M [root@CentOS ~]# ls -lh f1 -rw-rw-r--. 1 root root 10M Nov 28 16:45 f1 [root@CentOS ~]# du -sh f1 4.0K f1 [root@CentOS ~]# wc -c f1 10485760 f1
- 即使一个文件只包含 1 Byte 数据,存储时也要占用 1 个 block 。如下:
- 假设执行
ping localhost >> stdout.log
:- 用 >> 重定向输出时,会以 O_APPEND 模式打开文件。因此当文件长度被截断时,会自动将写入时的偏移量设置到文件末尾,避免产生稀疏文件。
- 因此,保存日志文件时,建议使用 >> ,而不是 > ,便于轮换日志文件。
# 文件描述符
- 每个进程在启动时,会创建一个文件表(file table),用于记录该进程打开的所有文件的信息,比如文件的 inode 编号、打开模式、当前偏移量。
- 每个进程的文件表相互独立,保存在 Linux 内核的内存空间中。
- 每个文件在文件表中的索引序号,称为文件描述符(file descriptor ,fd)。
- 文件描述符是从 0 开始递增的非负整数,优先分配当前可用的最小值。
- 例如:
- 当进程打开文件 /tmp/f1 时,会在文件表中增加一条记录,并给该文件分配一个文件描述符 3 。
- 当进程打开文件之后、进行读取时,如果将文件 /tmp/f1 重命名为 /tmp/f2 ,并不会影响进程的读取操作。因为进程是根据 inode 从磁盘读取数据的。
- 当进程关闭文件时,会从文件表中删除该条记录,回收其文件描述符。
- 每个进程刚启动时,默认会先打开 stdin、stdout、stderr 三个文件:
/dev/stdin
:标准输入,文件描述符为 0 。用户通过键盘在终端输入的字符串,通常会写入 stdin 文件,程序可通过 stdin 读取该字符串。/dev/stdout
:标准输出,文件描述符为 1 。程序通过 print() 函数打印的字符串,通常会写入 stdout 文件,然后显示在终端。/dev/stderr
:标准错误,文件描述符为 2 。程序的报错信息,通常会写入 stderr 文件,然后显示在终端。- 因此,进程打开的其它文件,是使用从 3 开始的文件描述符。
- 每个进程使用的文件描述符都会记录在
/proc/<PID>/fd/
目录下。如下:[root@CentOS ~]# ls -lh /dev/std* /dev/stderr: symbolic link to `/proc/self/fd/2' # 标准输入、输出文件都是指向 /proc/self/fd/ 目录的符号链接 /dev/stdin: symbolic link to `/proc/self/fd/0' /dev/stdout: symbolic link to `/proc/self/fd/1'
[root@CentOS ~]# file /proc/self/fd/* # /proc/self/fd/ 目录下的文件描述符是指向终端设备的符号链接 /proc/self/fd/0: symbolic link to `/dev/pts/2' /proc/self/fd/1: symbolic link to `/dev/pts/2' /proc/self/fd/2: symbolic link to `/dev/pts/2'
# 相关 API
# 打开文件
#include <fcntl.h>
// #include <sys/types.h> // 定义了 size_t、time_t、pid_t 等数据类型
int open(const char *pathname, int flags); // 打开文件
int open(const char *pathname, int flags, mode_t mode); // 先创建文件,再打开它
// pathname :文件路径
// flags :打开模式,是一些二进制标志位
// mode_t :权限模式,比如 S_IRWXU 为 00700 ,S_IRUSR 为 00400
// 函数执行成功时,返回打开的文件描述符
// 函数执行失败时,返回 -1
- 文件的打开模式 flags 可取值:以上三种是基本的打开模式,不能同时使用,但可以与以下模式通过或运算符 | 组合使用:
O_RDONLY # 只读 O_WRONLY # 只写 O_RDWR # 可读可写
O_APPEND # 追加模式,每次调用 write() 时,自动将偏移量移动到文件末尾(这样便于多个进程同时追加数据到文件中) O_CREAT # 如果要打开的文件不存在,则自动创建它 O_EXCL # 与 O_CREAT 一起使用时,如果文件存在,或者为符号链接,则打开失败 O_DIRECTORY # 文件必须为目录文件,否则打开失败 O_NOFOLLOW # 文件不能为符号链接,否则打开失败 O_NONBLOCK # 读写文件时,不等获取数据就返回,不会阻塞线程 O_NDELAY # 等价于 O_NONBLOCK O_SYNC # 同步模式,每次调用 write() 时,会阻塞线程直到数据实际写入磁盘 O_TRUNC # 如果文件存在,且采用可写模式,则将文件长度截断为 0
# 读写文件
#include <unistd.h>
int close(int fd);
// 关闭文件描述符
// 函数执行成功时,返回 0
// 函数执行成功时,返回 -1
ssize_t read(int fd, void *buf, size_t count);
// 从文件中读取数据
// fd :文件描述符
// buf :指向一块内存空间的指针,用作读缓冲。内核会从文件读取数据,写入 buf
// cout :想要读取的字节数
// 函数执行成功时,会将读取的数据写入 buf ,并返回实际读取的字节数
// 函数执行失败时,返回 -1
// 如果返回值为 0 ,则说明没有读取到数据,例如当前偏移量位于文件末尾时
ssize_t write(int fd, const void *buf, size_t count);
// 写入数据到文件中
// buf :指向一块内存空间的指针,用作写缓冲。内核会从 buf 读取数据,写入文件
// 函数执行成功时,返回实际写入的字节数
// 函数执行失败时,返回 -1
off_t lseek(int fd, off_t offset, int whence);
// 将当前文件偏移量设置为从 whence 开始的 offset 位置处
// offset :任意整数,可以为负
// whence :一个参照位置,可取值:
// SEEK_SET :文件首部
// SEEK_END :文件尾部
// SEEK_CUR :当前文件偏移量
- 当前文件偏移量(current file offset ,cfo):一个非负整数,表示当前读、写的是文件中第几个字节。
- cfo 保存在 Linux 内核的内存空间中。
- 打开一个文件时,cfo 默认为 0 。
- 每次读、写文件 n 个字节,默认会自动将 cfo 的值加 n ,直到加到文件末尾。因此 cfo 默认不会超过文件长度。
- 如果调用 lseek() 主动设置 cfo ,超过文件长度,并调用 write() 写入数据,则会自动将 cfo 位置之前的空间用空字节填充,导致文件变成稀疏文件。
# 增删文件
#include <unistd.h>
// int creat(const char *pathname, mode_t mode); // 创建文件。该函数已弃用,可用 open() 创建文件并打开
int unlink(const char *pathname); // 取消软链接、硬链接,即从文件系统删除某个路径的文件
- unlink() 的原理:
- 如果 path 是硬链接、软链接文件,则直接删除该链接。
- 如果 path 是硬链接,且指向的文件的 inode 引用计数为 1 (即只有这一个硬链接),则考虑是否删除磁盘中存储的 inode 文件:
- 如果当前没有进程打开该文件,则删除该文件,释放 inode 。
- 如果当前有进程打开该文件,则先删除 path 链接,等进程关闭文件时才自动删除 inode 文件。
- 如果 path 是 socket 或 device 类型的文件,则从文件系统删除它,但已打开该文件的进程依然可以访问它。
- unlink() 一次只能删除一个文件,且不能删除目录。
- rm 命令就是调用 unlink() 来删除文件,可递归删除目录。
- 删除文件有略微耗时,通常低于 1 秒。文件越大、文件数越多,耗时越久,可能阻塞终端几分钟。
- 例:删除一个正被进程打开的文件
[root@CentOS ~]# cat /dev/urandom | head -c 2G > f1 [root@CentOS ~]# tail -f f1 & [root@CentOS ~]# rm -f f1 [root@CentOS ~]# lsof -u root | grep deleted tail 11526 root 3r REG 253,1 2147483648 393694 /root/f1 (deleted)
- 此时执行 ls 命令,不会显示 f1 文件。执行
du -sh .
命令,会发现当前目录的体积减少了。但是执行df -h
命令,会发现磁盘的已用空间没有减少。需要重启进程,释放文件描述符。 - 假设一个进程产生了体积很大的日志文件,又不能重启进程,则可以通过
echo > f1
、truncate f1 -s 10M
等命令减小文件体积。
- 此时执行 ls 命令,不会显示 f1 文件。执行
# sendfile
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
// 从一个文件描述符 out_fd 拷贝数据到另一个文件描述符 in_fd ,从 offset 偏移量开始拷贝,最多拷贝 count 个字节
- sendfile() 是 Linux 提供的一种零拷贝技术。
- read() 读取文件的流程:
- DMA 从磁盘拷贝文件数据到 Page Cache 。拷贝完之后,发送中断通知 CPU ,发生 CPU 上下文切换。
- CPU 从 Page Cache 拷贝数据到进程物理内存。
- write() 写入文件的流程相反。
- 用 sendfile() 拷贝文件的流程:
- DMA 从磁盘拷贝数据到 Page Cache 。
- DMA 从 Page Cache 拷贝数据到目标文件。
- 可见,比起用 read() + write() 拷贝文件,用 sendfile() 避免了 CPU Copy ,节约了 CPU 时间,因此称为零拷贝,耗时更少。
- 例如 Nginx 服务器发送文件时,可以将磁盘文件用 sendfile() 拷贝写入 Socket 文件,提高发送文件的速度。
- 除了 sendfile() ,用 mmap() 也可实现零拷贝。
- read() 读取文件的流程: