所有的计算机程序都需要存储和检索信息。长期存储信息有三个基本要求:
- 能够存储大量信息。
- 存储必须持久化。
- 多个进程可以并发访问这些信息。
这些任务一般由磁盘来进行。虽然固态硬盘在近年逐渐流行,但传统磁盘依然是存储大量数据的首选。本文只针对磁盘,不对固态硬盘进行讨论。
使用磁盘来存储数据,必须要解决三个基本问题:
- 检索问题(路径问题),即如何快速找到信息。
- 权限问题,例如防止一个用户读取其他用户的信息。
- 存储问题,例如如何知道磁盘的哪些区域是空闲的。
在用户角度,使用磁盘的基本单元即是一个个文件。磁盘上的文件是受操作系统管理的,从总体上看,操作系统处理磁盘上的文件的部分称为文件系统(file system)。本文会先从磁盘本身的角度讨论其基本的物理结构和存储原理,再从操作系统的角度讨论其对文件的组织和管理,即文件系统。
硬件->磁盘
物理结构
磁盘是现代电子计算机中唯一的机械设备,大体来看,其主要由 5 个部分组成:
- 盘面 是实际存储数据的部分。盘面的表面是具有磁性的,以 N/S 级来表示一个基本位,在计算机中,这些基本位被解释为 0/1,磁盘以此实现数据的存储。一个最基本的磁盘会包含至少一个盘面,一些容量较大的磁盘会包含多个盘面。盘面越多、盘面的带磁基本单位分布越密集,磁盘的容量越大。磁盘的正反两面均可被使用。
- 主轴 下面的马达会驱动盘面进行高速旋转,以支持到达盘面各个部分。
- 磁头和磁头臂 磁头是对盘面进行读写的部件,磁头会放电以改变盘面某些部分的磁性,以此来完成写入操作;或者检测盘面当前的磁性状态,以此来完成读操作。磁头臂带动磁头进行移动。
- 磁头停靠点 磁盘不工作时,磁头会停靠在此处。
磁头在运动时,不与盘面直接接触,而是距离盘面有极小的空隙。为了防止磁盘工作时收到空气中微粒的干扰,磁盘的生产和使用环境都是密封、无尘的。
存储构成与逻辑抽象
在谈论磁盘的存储时,只关注盘面本身的逻辑布局。在逻辑上,认为一个盘面包含两个存储构成:
- 磁道(柱面) 以磁盘圆心为中心,将盘面划分为数个同心圆,每两个同心圆之间形成的环形区域被称为磁道(track)。当具有多个盘面时,这些盘面是同时旋转的,所有盘面上同一相对位置的磁道被称为柱面(cylinder)。
- 扇区 扇区(sector)是访问磁盘的最基本单元,每个扇区的能存储的数据容量都是相同的,一般为 512Byte 或 4KB。各个磁道的同心圆的半径不同,可以控制带磁存储单元的密度以控制扇区容量相同。在磁盘上读写数据,首先要定位到一个扇区。
承上,为了找到一个扇区,首先要确定扇区在哪个盘面,即使用哪个磁头,然后确定扇区所在的磁道,最后定位到扇区,这种寻址方式被称为 CHS 寻址。
在逻辑上,将上述磁盘的环形磁道进行展开并拼接,可以将磁盘看做一个线性的结构:
在上图中,最终将磁盘抽象成了一个以扇区为基本单位的数组,这个数组以扇区的序号为下标,这个下标被称作逻辑扇区地址或者逻辑块地址(LBA)。操作系统可以将物理上的 CHS 地址与逻辑上的 LBA 地址进行相互转化。
扇区的寻址,终归是要磁盘进行机械运动实际找到这个位置,所以机械运动是影响磁盘效率的最主要因素。机械运动越少,磁盘效率越高;机械运动越多,磁盘效率越低。在软件设计层面,开发者要有意识地将相关数据存放在一起,以减少磁盘的机械运动,提高磁盘效率。
除了上述的硬件外,磁盘还具有几个重要的寄存器,比如控制寄存器用来控制 IO 的方向,地址寄存器存储目标数据所在的 LBA 地址等。除此之外还有数据寄存器和状态寄存器,这里不再讨论。
软件->文件系统
现行 Linux 系统有很多主流的文件系统,例如 ext2、ext3、ext4、xfs等。这里不再讨论各个文件系统之间的区别及优劣,而是直接以 ext2 阐述操作系统对磁盘的组织。
对磁盘的分区
磁盘的容量是庞大的,仅仅是现代的个人电脑上搭载的硬盘的容量也有的超过了 1TB。为了有效地对磁盘做管理,操作系统的策略为:分治,具体做法是对磁盘进行分区,只要分别管理好各个磁盘分区,便能管理好整个磁盘。最常见的磁盘分区操作即是在 windows 机器中的、用户对磁盘的手动分区(假设只搭载了一块磁盘)。对 Linux 机器的磁盘分区的具体操作并非本文的话题,这里不进行讨论。
windows 中的磁盘分区:
在每个分区中的最开始都包含了一个 boot block,这个块用来存放引导程序和数据,所以又被称为引导块。分区的其他部分由数个 block group 组成,只要管理好各个 block group,就能管理好整个分区。block group 又可再被细分。
block group结构
block group是讨论文件系统的关键,对文件系统的讨论,本质是对 block group 的讨论。如上图所示,在 ext2 中,block group 被划分为以下几个部分:
- data blocks 文件的属性信息也属于文件的一部分。在 Linux 中,文件的属性信息和数据内容是分开存放的,data blocks 即是存放文件内容的地方。data blocks 中包含了很多小的 data block 数据块,每个 data block 都有其编号。一个 data block 的大小一般为 4KB,是文件内容存储的最小单位,当一个文件的大小大于 4KB 时,会占据多个 data block。
- block bitmap 在一个分区中,会有很多 data block,blcok bitmap 用来标识哪些数据块已经被使用,哪些数据块未被使用。block bitmap 本身是一个位图结构,删除文件时,不需要修改数据块内容,只需要修改 block bitmap 中对应位置的状态即可。
- inode table 文件除了文件名之外的所有属性信息被存放在一个被称为 inode 的结构中,每个 inode 都有一个唯一的编号,被称为 inode 编号。在操作系统中,一个 inode 编号就代表了一个文件,操作系统只认识 inode。一个 inode 只在当前分区内有效,不能跨分区。inode table 是存放 inode 的地方。在命令行层面,使用
ls -i
命令或者stat
命令查看文件的 inode 编号。
[@shr Fri Dec 01 20:07:09 12.1_signal_test]$ stat test.cpp
File: ‘test.cpp’
Size: 2287 Blocks: 8 IO Block: 4096 regular file
Device: fd01h/64769d Inode: 1704012 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1001/ shr) Gid: ( 1001/ shr)
Access: 2023-12-01 10:47:34.720573248 +0800
Modify: 2023-12-01 10:47:34.663571756 +0800
Change: 2023-12-01 10:47:34.663571756 +0800
Birth: -
[@shr Fri Dec 01 20:07:13 12.1_signal_test]$ ll -i test.cpp
1704012 -rw-rw-r-- 1 shr shr 2287 Dec 1 10:47 test.cpp
[@shr Fri Dec 01 20:07:16 12.1_signal_test]$
一个有效的 inode 会记录当前文件的数据块的存放位置,即文件所占的各个数据区块的索引,如下图所示,这种数据存取方法被称为索引式文件系统(indexed allocation)
当要读取文件数据时,只要拿到其 inode,即可找到所有数据存放区块的位置进行读取。
- inode bitmap 与 block bitmap 的作用一样,inode bitmap 用来标识哪些 inode 编号已经被使用(有效),哪些 inode 编号未被使用(无效)。inode bitmap 本身是一个位图结构。
- group descriptor table 是紧跟 super block 之后的一个区块,记录了当前 block group 的整体使用情况和状态,以及某些区块的起始地址。
- super block 是记录整个分区(文件系统)相关信息的地方,没有 super block 就没有这个文件系统。记录的信息主要有:
- 数据块与 inode 的总量与使用情况
- 数据块与 inode 的大小
- 文件系统的最近挂载时间、最近一次写入数据的时间等
使用dumpe2fs
命令查看 ext2/ext3/ext4 文件系统的详细信息,使用-h
选项只列出 super block 的部分:
[@shr Tue Dec 05 23:16:43 ~]$ sudo dumpe2fs -h /dev/vda1
dumpe2fs 1.42.9 (28-Dec-2013)
Filesystem volume name: <none>
Last mounted on: / #上一次被挂载的位置
Filesystem UUID: 4b499d76-769a-40a0-93dc-4a31a59add28
Filesystem magic number: 0xEF53
Filesystem revision #: 1 (dynamic)
Filesystem features: has_journal ext_attr resize_inode dir_index filetype needs_recovery extent 64bit flex_bg sparse_super large_file huge_file uninit_bg dir_nlink extra_isize
Filesystem flags: signed_directory_hash
Default mount options: user_xattr acl
Filesystem state: clean
Errors behavior: Continue
Filesystem OS type: Linux
Inode count: 2621440 #inode总数
Block count: 10485499 #数据区块总数
Reserved block count: 440360
Free blocks: 9580404 #空闲数据区块的数量
Free inodes: 2538435 #空闲 inode 数量
First block: 0
Block size: 4096 #每个数据块的大小
Fragment size: 4096
Group descriptor size: 64 #组描述符的大小
Reserved GDT blocks: 1019
Blocks per group: 32768 #每个 group 中数据块的总量
Fragments per group: 32768
Inodes per group: 8192 #每个 group 中 inode 的总量
Inode blocks per group: 512 #每个 group 中 inode block 的数量
Flex block group size: 16
Filesystem created: Thu Mar 7 14:38:36 2019 #文件系统的创建时间
Last mount time: Fri Dec 16 13:37:48 2022 #上一次被挂载的时间
Last write time: Fri Dec 16 13:37:47 2022
Mount count: 48
Maximum mount count: -1
Last checked: Thu Mar 7 14:38:36 2019
Check interval: 0 (<none>)
Lifetime writes: 199 GB
Reserved blocks uid: 0 (user root)
Reserved blocks gid: 0 (group root)
First inode: 11
Inode size: 256
Required extra isize: 28
Desired extra isize: 28
Journal inode: 8
First orphan inode: 26243
#…………(省略)…………
由上述信息,现在可以从系统角度理解对文件进行操作的过程:
- 创建文件时,系统会首先找到一个空闲的 inode 并将其分配给新文件,将新文件的属性信息放到 inode 中,然后根据新文件的大小为其找到数个空闲的 data blcok,将数据放到 data block 中,inode 将 data block 的编号进行保存以便后续查找。
- 删除文件时,系统会将其 inode 的状态置为空闲,并将其占据的所有 data block 的状态置为空闲。
- 查找文件时,系统会根据文件的 inode 编号找到对应的 inode,并可以根据 inode 中的信息找到文件的数据存储位置。系统只认识 inode。
- 修改文件时,系统会找到文件的 inode,将新的内容信息写入到文件对应的 data block,将新的属性信息写入到 inode。在这个过程中,如果文件增大,系统可以为文件分配新的 data block。
格式化
在一个磁盘分区被使用之前,各种属性信息都要被提前规划和写入,这即是磁盘的格式化。inode 的大小和数量,data block 的大小和数量等信息都是在格式化时被确定的,且格式化后不能被修改。
格式化会将分区的所有属性信息刷新,区块都会被标记为空闲状态,在用户角度,整个分区会体现为被清空。
对目录文件的理解
上文一直在强调,文件系统识别一个文件的方式是识别其 inode,而站在用户角度,用户只关心文件名,在操作文件时也是以文件名标识一个文件的,所以系统必须可以由文件名得到其对应的文件 inode,这个工作是由文件所在的目录文件进行的。
目录文件也是文件,有自己的属性信息和数据块。目录文件的数据块存储了 “文件名 - 文件 inode” 的映射,当用户提供文件名时,系统就可以根据对应文件的目录文件中存储的映射关系,找到对应文件的 inode,进而找到文件的数据块及各种信息。
#目录文件的详细信息
[@shr Wed Dec 06 15:27:42 ~]$ stat code/
File: ‘code/’
Size: 4096 Blocks: 8 IO Block: 4096 directory
Device: fd01h/64769d Inode: 657216 Links: 8
Access: (0775/drwxrwxr-x) Uid: ( 1001/ shr) Gid: ( 1001/ shr)
Access: 2023-12-06 15:27:37.599832599 +0800
Modify: 2023-11-19 20:29:27.894072234 +0800
Change: 2023-11-19 20:29:27.894072234 +0800
Birth: -
现在的问题是,找到一个文件的 inode,就需要找到其所在的目录文件,而其所在的目录文件也是一个文件,要找到这个文件对应的 inode,就要找到其所在的目录文件……如此而行,直到找到根目录时,才可以得知根目录的下级文件的 inode,进而向下回溯,即:寻找一个文件 inode 的过程其实是一个递归的过程,先递进到根目录,再回溯到目标文件所在的目录文件,并找到目标文件的 inode。
事实上,环境变量 PWD 会存储当前所在的文件路径,当在当前路径进行操作文件时,系统可以直接从根目录向下拿到文件的 inode,无需进行递归操作。同时,一些缓存的存在也会加速文件寻找的过程。
文件系统的运行
Linux内存管理概述
因为文件系统的运行与内存管理有很大的关系,所以先对内存管理进行简单的概述。
从操作系统视角来看,物理内存被划分为了一个个小区域,这个区域被称为页框,认为页框是针对内存管理的概念;从磁盘来看,每个数据块被划分为了一个个被称为页帧的小区域,认为页帧是针对磁盘数据块的概念。每个页框与页帧的大小相同,一般为 4KB,每个磁盘中的页帧被加载到内存中时,都可以恰好填入一个页框中。数据加载是以页框和页帧为基本单位的,由局部性原理,这样做可以减少 IO 次数,提高效率。
作为软硬件的管理者,操作系统可以直接看到物理内存和物理地址,为了方便对物理内存进行管理,操作系统会现将上述的页框单位抽象为一个数据结构 struct page,并将这些数据结构以 struct page mem_array[] 进行组织。由此,对内存的管理,即转化成了对 mem_array[] 数组的管理,访问一块物理内存,只需要拿到对应的 page 结构体,就能找到物理页框。
/*
linux内核中的struct page
linux-3.10.1\linux-3.10.1\include\linux\mm_types.h
*/
struct page {
/* First double word block */
unsigned long flags; /* Atomic flags, some possibly
* updated asynchronously */
struct address_space *mapping; /* If low bit clear, points to
* inode address_space, or NULL.
* If page mapped as anonymous
* memory, low bit is set, and
* it points to anon_vma object:
* see PAGE_MAPPING_ANON below.
*/
/* Second double word */
struct {
union {
/*……此处省略……*/
};
union {
#if defined(CONFIG_HAVE_CMPXCHG_DOUBLE) && \
defined(CONFIG_HAVE_ALIGNED_STRUCT_PAGE)
/* Used for cmpxchg_double in slub */
unsigned long counters;
#else
/*……此处省略……*/
unsigned counters;
#endif
struct {
union {
/*……此处省略……*/
atomic_t _mapcount;
struct { /* SLUB */
/*……此处省略……*/
};
int units; /* SLOB */
};
atomic_t _count; /* Usage count, see below. */
};
};
};
/* Third double word block */
union {
struct list_head lru; /* Pageout list, eg. active_list
* protected by zone->lru_lock !
*/
struct { /* slub per cpu partial pages */
struct page *next; /* Next partial slab */
#ifdef CONFIG_64BIT
int pages; /* Nr of partial slabs left */
int pobjects; /* Approximate # of objects */
#else
short int pages;
short int pobjects;
#endif
};
struct list_head list; /* slobs list of pages */
struct slab *slab_page; /* slab fields */
};
/* Remainder is not double word aligned */
union {
unsigned long private;
/*……此处省略……*/
#if USE_SPLIT_PTLOCKS
spinlock_t ptl;
#endif
struct kmem_cache *slab_cache; /* SL[AU]B: Pointer to slab */
struct page *first_page; /* Compound tail pages */
};
/*……此处省略……*/
#if defined(WANT_PAGE_VIRTUAL)
void *virtual; /* Kernel virtual address (NULL if
not kmapped, ie. highmem) */
#endif /* WANT_PAGE_VIRTUAL */
#ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS
unsigned long debug_flags; /* Use atomic bitops on this */
#endif
#ifdef CONFIG_KMEMCHECK
void *shadow;
#endif
#ifdef LAST_NID_NOT_IN_PAGE_FLAGS
int _last_nid;
#endif
}
#ifdef CONFIG_HAVE_ALIGNED_STRUCT_PAGE
__aligned(2 * sizeof(unsigned long))
#endif
;
Linux文件系统的运行
上文说道,文件被加载后,其一部分属性信息会被放在 struct file 中,但也仅仅是一部分。当文件被加载时,内存中会有一个对应的 struct inode 被构建,struct inode 会拿取文件对应的 inode 块中的信息,其中存储了文件的所有属性信息。
/*
Linux内核中的 struct file, 其中保存了 struct inode 的指针
linux-3.10.1\linux-3.10.1\include\linux\fs.h
*/
struct file {
/*……此处省略……*/
struct path f_path;
#define f_dentry f_path.dentry
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
/*……此处省略……*/
};
/*
Linux内核中的 struct inode, 其中保存了文件的所有属性信息
*/
struct inode {
umode_t i_mode;
unsigned short i_opflags;
kuid_t i_uid;
kgid_t i_gid;
unsigned int i_flags;
/*……此处省略……*/
const struct inode_operations *i_op;
struct super_block *i_sb;
struct address_space *i_mapping;
#ifdef CONFIG_SECURITY
void *i_security;
#endif
/* Stat data, not accessed from path walking */
unsigned long i_ino;
/*
* Filesystems may only read i_nlink directly. They shall use the
* following functions for modification:
*
* (set|clear|inc|drop)_nlink
* inode_(inc|dec)_link_count
*/
union {
const unsigned int i_nlink;
unsigned int __i_nlink;
};
dev_t i_rdev;
loff_t i_size;
struct timespec i_atime;
struct timespec i_mtime;
struct timespec i_ctime;
spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */
unsigned short i_bytes;
unsigned int i_blkbits;
blkcnt_t i_blocks;
#ifdef __NEED_I_SIZE_ORDERED
seqcount_t i_size_seqcount;
#endif
/* Misc */
unsigned long i_state;
struct mutex i_mutex;
/*……此处省略……*/
u64 i_version;
atomic_t i_count;
atomic_t i_dio_count;
atomic_t i_writecount;
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
struct file_lock *i_flock;
struct address_space i_data;
/*……此处省略……*/
};
在内存中,被加载的文件的所有属性信息被放在 struct inode 中,内容被放在缓冲区中。缓冲区通过一棵字典树将数据块与实际的缓冲区内存进行关联,字典树可以根据数据块相对于文件的偏移量,定位到对应的 struct page。
为了解决磁盘写入速度与内存速度的巨大差异带来的效率问题,Linux 使用异步处理的方式进行磁盘数据与内存数据的同步:当系统加载一个文件后,如果该文件没有被修改过,则在内存区段的文件数据会被标记为干净的(clean),如果被修改过了,则该内存区段的文件数据会被标记为脏的(dirty),系统会不定时地将脏数据写回到磁盘,以保持磁盘与内存数据的一致性。正常关机时,关机命令会主动调用 sync
将数据写回到磁盘;如若不正常关机,由于数据未被写入到磁盘,会花很多时间进行磁盘校验,也可能导致文件系统的损坏。
文件链接
软链接
软链接又称符号链接(symbolic link),是一种类似 Windows 的快捷方式功能的文件。在 Linux 中,使用ln -s [linked file name] [link file name]
创建软链接。
[@shr Mon Dec 25 18:57:14 doc]$ ll
total 0
-rw-rw-r-- 1 shr shr 0 Dec 25 18:50 hello
[@shr Mon Dec 25 18:57:15 doc]$
[@shr Mon Dec 25 18:57:15 doc]$ ln -s hello hi
[@shr Mon Dec 25 18:57:26 doc]$ ll -i
total 0
1574642 -rw-rw-r-- 1 shr shr 0 Dec 25 18:50 hello
1574643 lrwxrwxrwx 1 shr shr 5 Dec 25 18:57 hi -> hello #软链接有自己的 inode
[@shr Mon Dec 25 18:57:29 doc]$
如上,软链接有自己的 inode,所以属于一个独立的文件。软链接的数据块存储的是所链接文件的路径,因此软链接可以跨文件系统。可以对一个不存在的文件进行软链接,直到对应的文件被创建,才能打开这个软链接。
软链接的作用相当于快捷方式,当对软链接文件进行读写时,系统会首先拿到软链接中存储的对应文件的路径,然后根据这个路径对被链接的文件进行读写。
硬链接
硬链接(hard link)是与软链接完全不同的一种链接方式,使用ln [linked file name] [link file name]
创建硬链接。
[@shr Mon Dec 25 18:59:20 doc]$ ll
total 4
-rw-rw-r-- 1 shr shr 6 Dec 25 18:59 hello
lrwxrwxrwx 1 shr shr 5 Dec 25 18:57 hi -> hello
[@shr Mon Dec 25 19:02:25 doc]$ ln hello hihi
[@shr Mon Dec 25 19:02:47 doc]$ ll -i
total 8
1574642 -rw-rw-r-- 2 shr shr 6 Dec 25 18:59 hello
1574643 lrwxrwxrwx 1 shr shr 5 Dec 25 18:57 hi -> hello
1574642 -rw-rw-r-- 2 shr shr 6 Dec 25 18:59 hihi #与hello的inode相同
[@shr Mon Dec 25 19:03:33 doc]$
如上,硬链接没有自己的 inode,因此不是一个独立的文件。承上,目录文件的数据块中存储了 文件名-inode 的映射,建立硬链接,其实是在目录文件中新增了一个 文件名-已存在的文件的 inode 的映射。每个 inode 的内部都有一个引用计数,这是因为一个 inode 可以被多个文件名映射,这个引用计数即为硬链接数。
硬链接是与 inode 强关联的,所以硬链接不能跨文件系统。此外,所有用户都不能对目录文件进行硬链接,这是为了避免回路问题,造成文件查找时的无限递归。
在 linux 中,每个目录文件内都有一个 .
目录文件和一个 ..
目录文件分别代表当前目录和上级目录,这两个文件其实是分别对当前目录文件和上级目录文件的硬链接。上文说道,用户不能对目录文件进行硬链接,但是操作系统可以,并且可以控制不进入 .
和 ..
文件进行搜索,这是安全的。
承上,当建立一个空的目录文件 /tmp/doc/ 时,基本上会有三个东西:
/tmp/doc/
/tmp/doc/.
/tmp/doc/..
其中 /tmp/doc/ 与 /tmp/doc/. 是一样的,所以一个空的目录文件的硬链接数为 2。
硬链接最大的好处就是安全。假设对一个文件进行了 10 次硬链接,如果不小心将任何一个文件删除,这个文件的 inode 与数据块仍然存在,此时可以通过另一个文件名读取到正确的文件数据。此外,无论通过哪个文件名进行编辑文件,最终的结果都会写入到 inode 与对应的区块中,因此均能对数据进行修改。