二十三、Linux性能优化实战学习笔记-Linux 文件系统是怎么工作的?

简介: Linux 文件系统为每个文件 都 分配两个数据结构,索引节点(indexnode)和目录项(directory entry)。它们主要用来记录文件的元信息和目录结构。

一、文件系统-索引节点和目录项

Linux 文件系统为每个文件 都 分配两个数据结构,索引节点(indexnode)和目录项(directory entry)。它们主要用来记录文件的元信息和目录结构。

20200819150731620.png

1、索引节点

简称为 inode,用来记录文件的元数据,比如 inode 编号、文件大小、访问权限、修改日期、数据的位置等。索引节点和文件一 一对应,它跟文件内容一样,都会被持久化存储到磁盘中。所以记住,索引节点同样占用磁盘空间。

2、目录项

简称为 dentry,用来记录文件的名字、索引节点指针以及与其他目录项的关联关系。多个关联的目录项,就构成了文件系统的目录结构。不过,不同于索引节点,目录项是由内核维护的一个内存数据结构,所以通常也被叫做目录项缓存


阅读上图需要注意的几点:

1、目录项本身就是一个内存缓存,而索引节点则是存储在磁盘中的数据,为了协调慢速磁盘与快速 CPU 的性能差异,文

件内容会缓存到页缓存 Cache 中

2、索引节点也会缓存到内存,加速文件的访问

3、磁盘在执行文件系统格式化时,会被分成三个存储区域,超级块、索引节点区和数据块区。其中

  • 超级块,存储整个文件系统的状态。
  • 索引节点区,用来存储索引节点。
  • 数据块区,则用来存储文件数据。


具体的数据结构见:

github.com

二、文件系统-虚拟文件系统

VFS

VFS 是一个内核抽象层,能够隐藏具体文件系统的实现细节,从而给用户态进程提供一套统一的 API 接口。VFS 使用了一种通用文件系统的设计,具体的文件系统只要实现了 VFS 的设计接口,就能够注册到 VFS 中,从而使内核可以读写这种文件系统。 这很像面向对象设计中的抽象类与子类之间的关系,抽象类负责对外接口的设计,子类负责具体的实现。其实,VFS本身就是用 c 语言实现的一套面向对象的接口。


通用文件模型


VFS 通用文件模型中包含以下四种元数据结构:

  1. 超级块对象(superblock object),用于存放已经注册的文件系统的信息。比如ext2,ext3等这些基础的磁盘文件系统,还有用于读写socket的socket文件系统,以及当前的用于读写cgroups配置信息的 cgroups 文件系统等。
  2. 索引节点对象(inode object),用于存放具体文件的信息。对于一般的磁盘文件系统而言,inode 节点中一般会存放文件在硬盘中的存储块等信息;对于socket文件系统,inode会存放socket的相关属性,而对于cgroups这样的特殊文件系统,inode会存放与 cgroup 节点相关的属性信息。这里面比较重要的一个部分是一个叫做 inode_operations 的结构体,这个结构体定义了在具体文件系统中创建文件,删除文件等的具体实现。
  3. 文件对象(file object),一个文件对象表示进程内打开的一个文件,文件对象是存放在进程的文件描述符表里面的。同样这个文件中比较重要的部分是一个叫 file_operations 的结构体,这个结构体描述了具体的文件系统的读写实现。当进程在某一个文件描述符上调用读写操作时,实际调用的是 file_operations 中定义的方法。 对于普通的磁盘文件系统,file_operations 中定义的就是普通的块设备读写操作;对于socket文件系统,file_operations 中定义的就是 socket 对应的 send/recv 等操作;而对于cgroups这样的特殊文件系统,file_operations 中定义的就是操作 cgroup 结构体等具体的实现。
  4. 目录项对象(dentry object),在每个文件系统中,内核在查找某一个路径中的文件时,会为内核路径上的每一个分量都生成一个目录项对象,通过目录项对象能够找到对应的 inode 对象,目录项对象一般会被缓存,从而提高内核查找速度。


Linux 文件系统的四大基本要素

  • 目录项
  • 索引节点
  • 逻辑块
  • 超级块


为了支持各种不同的文件系统,Linux 内核在用户进程和文件系统的中间,又引入了一个抽象层,也就是虚拟文件系统 VFS(Virtual File System)


VFS 定义了一组所有文件系统都支持的数据结构和标准接口,用户进程和内核中的其他子系统,只需要跟 VFS 提供的统一接口进行交互就可以了,而不需要再关心底层各种文件系统的实现细节。

1、系统调用、VFS、缓存、文件系统以及块存储之间的关系

20200819151624967.png

Linux 支持各种各样的文件系统,如 Ext4、XFS、NFS 等等。按照存储位置的不同,这些文件系统可以分为三类。


  1. 第一类是基于磁盘的文件系统,也就是把数据直接存储在计算机本地挂载的磁盘中。常见的 Ext4、XFS、OverlayFS 等,都是这类文件系统。
  2. 第二类是基于内存的文件系统,也就是我们常说的虚拟文件系统。这类文件系统,不需要任何磁盘分配存储空间,但会占用内存。我们经常用到的 /proc 文件系统,其实就是一种最常见的虚拟文件系统。此外,/sys 文件系统也属于这一类,主要向用户空间导出层次化的内核对象。
  3. 第三类是网络文件系统,也就是用来访问其他计算机数据的文件系统,比如 NFS、SMB、cifs等
servers]# df -TH
Filesystem           Type   Size  Used Avail Use% Mounted on
/dev/mapper/rootvg-rootlv
                     ext4   5.2G  4.2G  747M  85% /
tmpfs                tmpfs   68G     0   68G   0% /dev/shm
/dev/sda2            ext4   500M   35M  440M   8% /boot
/dev/sda1            vfat   525M  279k  524M   1% /boot/efi
/dev/mapper/rootvg-varlv
                     ext4    85G   13G   68G  16% /var
/dev/mapper/rootvg-gpmasterlv
                     xfs    430G   79G  352G  19% /gpmaster
//192.168.10.144/nas_ocxp
                     cifs   4.2T  3.2T  1.1T  76% /dfs/A
192.168..10.147:/NAS_ACF
                     nfs    805T  591T  215T  74% /dfs/B
192.168..10.148:/NAS_OC nfs    794T  588T  206T  75% /dfs/C

这些文件系统,要先挂载到 VFS 目录树中的某个子目录(称为挂载点),然后才能访问其中的文件

拿第一类,也就是基于磁盘的文件系统为例,在安装系统时,要先挂载一个根目录(/),在根目录下再把其他文件系统(比如其他的磁盘分区、/proc 文件系统、/sys文件系统、NFS 等)挂载进来。

20200819153919580.png

三、文件系统 I/O

1、系统调用

把文件系统挂载到挂载点后,你就能通过挂载点,再去访问它管理的文件了。VFS 提供了一组标准的文件访问接口。这些接口以系统调用的方式,提供给应用程序使用。


cat 命令的系统调用


strace cat /dev/null

scripts]# strace cat /dev/null 
execve("/bin/cat", ["cat", "/dev/null"], [/* 34 vars */]) = 0
brk(0)                                  = 0x1960000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f568c57c000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
stat("/usr/local/greenplum-cc-web/./lib", {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0
open("tls/x86_64/libc.so.6", O_RDONLY)  = -1 ENOENT (No such file or directory)
open("tls/libc.so.6", O_RDONLY)         = -1 ENOENT (No such file or directory)
open("x86_64/libc.so.6", O_RDONLY)      = -1 ENOENT (No such file or directory)
open("libc.so.6", O_RDONLY)             = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY)      = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=103856, ...}) = 0
mmap(NULL, 103856, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f568c562000
close(3)                                = 0
open("/lib64/libc.so.6", O_RDONLY)      = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0000\356A\3777\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1930416, ...}) = 0
mmap(0x37ff400000, 3750184, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x37ff400000
mprotect(0x37ff58a000, 2097152, PROT_NONE) = 0
mmap(0x37ff78a000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x18a000) = 0x37ff78a000
mmap(0x37ff790000, 14632, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x37ff790000
close(3)                                = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f568c561000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f568c560000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f568c55f000
arch_prctl(ARCH_SET_FS, 0x7f568c560700) = 0
mprotect(0x37ff78a000, 16384, PROT_READ) = 0
mprotect(0x37ff21f000, 8192, PROT_READ) = 0
munmap(0x7f568c562000, 103856)          = 0
brk(0)                                  = 0x1960000
brk(0x1981000)                          = 0x1981000
open("/usr/lib/locale/locale-archive", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=99164480, ...}) = 0
mmap(NULL, 99164480, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f56866cc000
close(3)                                = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0
open("/dev/null", O_RDONLY)             = 3
fstat(3, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 3), ...}) = 0
read(3, "", 32768)                      = 0
close(3)                                = 0
close(1)                                = 0
close(2)                                = 0
exit_group(0)                           = ?

首先调用 open() ,打开一个文件;然后调用 read() ,读取文件的内容;最后再调用 write() ,把文件内容输出到控制台的标准输出中。

int open(const char *pathname, int flags, mode_t mode);
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

2、I/O 分类

文件读写方式的各种差异,导致 I/O 的分类多种多样。最常见的有


  • 缓冲与非缓冲 I/O
  • 直接与非直接 I/O
  • 阻塞与非阻塞 I/O
  • 同步与异步 I/O

第一种,根据是否利用标准库缓存,可以把文件 I/O 分为缓冲 I/O 与非缓冲 I/O。


缓冲 I/O:是指利用标准库缓存来加速文件的访问,而标准库内部再通过系统调度访问文件

非缓冲 I/O:是指直接通过系统调用来访问文件,不再经过标准库缓存


这里所说的“缓冲”,是指标准库内部实现的缓存。比方说,你可能见到过,很多程序遇到换行时才真正输出,而换行前的内容,其实就是被标准库暂时缓存了起来

无论缓冲 I/O 还是非缓冲 I/O,它们最终还是要经过系统调用来访问文件。我们知道,系统调用后,还会通过页缓存,来减少磁盘的 I/O 操作。

第二种,根据是否利用操作系统的页缓存,可以把文件 I/O 分为直接 I/O 与非直接 I/O


直接 I/O,是指跳过操作系统的页缓存,直接跟文件系统交互来访问文件。

非直接 I/O ,文件读写时,先要经过系统的页缓存,然后再由内核或额外的系统调用,真正写入磁盘。


想要实现直接 I/O,需要你在系统调用中,指定 O_DIRECT 标志。如果没有设置过,默认的是非直接 I/O。


直接I/O的应用场景 :在数据库等场景中,你还会看到,跳过文件系统读写磁盘的情况,也就是我们通常所说的裸 I/O


第三种,根据应用程序是否阻塞自身运行,可以把文件 I/O 分为阻塞 I/O 和非阻塞 I/O:


所谓阻塞 I/O,是指应用程序执行 I/O 操作后,如果没有获得响应,就会阻塞当前线程,自然就不能执行其他任务。

所谓非阻塞 I/O,是指应用程序执行 I/O 操作后,不会阻塞当前的线程,可以继续执行其他的任务,随后再通过轮询或者事件通知的形式,获取调用的结果

例如:访问管道或者网络套接字时,设置 O_NONBLOCK 标志,就表示用非阻塞方式访问;而如果不做任何设置,默认的就是阻塞访问。


第四种,根据是否等待响应结果,可以把文件 I/O 分为同步和异步 I/O:


同步 I/O,是指应用程序执行 I/O 操作后,要一直等到整个 I/O 完成后,才能获得I/O 响应。


异步 I/O,是指应用程序执行 I/O 操作后,不用等待完成和完成后的响应,而是继续执行就可以。等到这次 I/O 完成后,响应会用事件通知的方式,告诉应用程序。


举个例子,在操作文件时,如果你设置了 O_SYNC 或者 O_DSYNC 标志,就代表同步I/O。如果设置了 O_DSYNC,就要等文件数据写入磁盘后,才能返回;而 O_SYNC,则是在 O_DSYNC 基础上,要求文件元数据也要写入磁盘后,才能返回


这类似于文件系统的日志设计

20200819161013144.png

另外一个例子 :在访问管道或者网络套接字时,设置了 O_ASYNC 选项后,相应的 I/O 就是异步I/O。这样,内核会再通过 SIGIO 或者 SIGPOLL,来通知进程文件是否可读写。

四、实战

1、查看inode占用情况

df -i /dev/sda3
scripts]# lsblk
NAME                                  MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
sda                                     8:0    0  5.5T  0 disk 
├─sda1                                  8:1    0  500M  0 part /boot/efi
├─sda2                                  8:2    0  500M  0 part /boot
└─sda3                                  8:3    0  5.5T  0 part 
  ├─root_vg-swaplv (dm-0)             253:0    0   10G  0 lvm  [SWAP]
  ├─root_vg-rootlv (dm-1)             253:1    0   10G  0 lvm  /
  ├─root_vg-homelv (dm-2)             253:2    0   20G  0 lvm  /home
  ├─root_vg-varlv (dm-3)              253:3    0   30G  0 lvm  /var
  ├─root_vg-locallv (dm-4)            253:4    0   20G  0 lvm  /usr/local
  ├─root_vg-tmplv (dm-5)              253:5    0   20G  0 lvm  /tmp
  ├─root_vg-optlv (dm-6)              253:6    0  100G  0 lvm  /opt
  ├─root_vg-usrlv (dm-7)              253:7    0   80G  0 lvm  /usr
  ├─root_vg-oralv (dm-8)              253:8    0  100G  0 lvm  /oracle
  ├─root_vg-pptinst (dm-9)            253:9    0   20G  0 lvm  /ktappt1
  ├─root_vg-hisinst (dm-10)           253:10   0   20G  0 lvm  /ktahis1
  ├─root_vg-uasinst (dm-11)           253:11   0   20G  0 lvm  /ktauas1
  ├─root_vg-brminst (dm-12)           253:12   0   20G  0 lvm  /ktabrm1
  ├─root_vg-redislv (dm-13)           253:13   0  350G  0 lvm  /redis
  ├─root_vg-data1 (dm-14)             253:14   0   40G  0 lvm  
  ├─root_vg-root_vg--gp6lv (dm-15)    253:15   0  100G  0 lvm  /gpmaster
  └─root_vg-root_vg--gpdatalv (dm-16) 253:16   0  100G  0 lvm  /data
 scripts]# df -i /dev/sda3
Filesystem       Inodes IUsed    IFree IUse% Mounted on
-              33065188  1365 33063823    1% /dev

索引节点的容量,(也就是 Inode 个数)是在格式化磁盘时设定好的,一般由格式化工具自动生成。当你发现索引节点空间不足,但磁盘空间充足时,很可能就是过多小文件导致的。


所以,一般来说,删除这些小文件,或者把它们移动到索引节点充足的其他磁盘中,就可以解决这个问题


查看inode的大小

~]# cat /etc/mke2fs.conf
[defaults]
        base_features = sparse_super,filetype,resize_inode,dir_index,ext_attr
        enable_periodic_fsck = 1
        blocksize = 4096
        inode_size = 256
        inode_ratio = 16384

参考:  深入理解文件系统_MyySophia的博客-CSDN博客 4.5.1

2、缓存

free 输出的 Cache,是页缓存和可回收 Slab 缓存的和,你可以从/proc/meminfo ,直接得到它们的大小,这是宏观上的缓存情况

scripts]# cat /proc/meminfo | grep -E "SReclaimable|Cached"
Cached:         47490632 kB
SwapCached:        32628 kB
SReclaimable:   11174060 kB

SReclaimable:   11174060 kB

从微观上来看 文件系统中的目录项和索引节点的缓存

 scripts]# cat /proc/slabinfo | grep -E '^#|dentry|inode'
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
xfs_inode         167327 167560   1024    4    1 : tunables   54   27    8 : slabdata  41890  41890      0
fuse_inode             0      0    768    5    1 : tunables   54   27    8 : slabdata      0      0      0
nfs_inode_cache        5     12   1048    3    1 : tunables   24   12    8 : slabdata      4      4      0
rpc_inode_cache       20     28    832    4    1 : tunables   54   27    8 : slabdata      7      7      0
fat_inode_cache        4      6    672    6    1 : tunables   54   27    8 : slabdata      1      1      0
ext4_inode_cache  235633 235720   1000    4    1 : tunables   54   27    8 : slabdata  58930  58930      0
mqueue_inode_cache      1      4    896    4    1 : tunables   54   27    8 : slabdata      1      1      0
isofs_inode_cache      0      0    640    6    1 : tunables   54   27    8 : slabdata      0      0      0
hugetlbfs_inode_cache      1      6    608    6    1 : tunables   54   27    8 : slabdata      1      1      0
inotify_inode_mark_entry    196    416    120   32    1 : tunables  120   60    8 : slabdata     13     13      0
sock_inode_cache    1306   1765    704    5    1 : tunables   54   27    8 : slabdata    353    353      4
shmem_inode_cache   1408   1510    784    5    1 : tunables   54   27    8 : slabdata    302    302      0
proc_inode_cache    7713   8046    656    6    1 : tunables   54   27    8 : slabdata   1341   1341      3
inode_cache        11097  11250    592    6    1 : tunables   54   27    8 : slabdata   1873   1875      0
dentry            46186194 46186560    192   20    1 : tunables  120   60    8 : slabdata 2309325 2309328    146
selinux_inode_security  10513  11236     72   53    1 : tunables  120   60    8 : slabdata    212    212      0

dentry 行表示目录项缓存,inode_cache 行,表示 VFS 索引节点缓存,其余的则是各种文件系统的索引节点缓存


slabtop 查看缓存

20200819161940721.png

20200819164434883.png

--update awk脚本统计

base]# cat /proc/slabinfo  |  awk 'BEGIN{OFS=":"}{if($3*$4/1024/1024 > 100) {print $1,$3*$4/1024/1024 "MB"}}'
xfs_inode:199.343MB
dentry:64980.2MB
buffer_head:2697.92MB
radix_tree_node:316.038MB
kmalloc-1024:105MB
kmalloc-64:3651.45MB

3、导致缓存陡增的操作

在根目录查找文件操作

cripts]# find / -name bigKey.sh

进行之前将缓存清空

scripts]# echo 3 > /proc/sys/vm/drop_caches

观察slabtop dentry 增加很快。

案例: musql 分区表太多导致空间爆炸,使用du 分析时导致dentry上升的问题:

故障分析 | 一条du命令引发的内存不足报警

相关实践学习
CentOS 7迁移Anolis OS 7
龙蜥操作系统Anolis OS的体验。Anolis OS 7生态上和依赖管理上保持跟CentOS 7.x兼容,一键式迁移脚本centos2anolis.py。本文为您介绍如何通过AOMS迁移工具实现CentOS 7.x到Anolis OS 7的迁移。
目录
相关文章
|
3月前
|
Shell Linux
Linux shell编程学习笔记30:打造彩色的选项菜单
Linux shell编程学习笔记30:打造彩色的选项菜单
|
16天前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
57 8
|
21天前
|
存储 Linux 文件存储
Linux文件系统
Linux文件系统 一切皆文件 在Linux中,“一切皆文件”的概念意味着系统中的所有资源,包括硬件设备、目录及进程等,均被视为文件。这种设计简化了操作和管理,具体包括: 普通文件:存储数据的常规文件。 目录文件:包含其他文件和子目录的文件。 进程文件:在/proc目录下代表系统中运行的进程。 设备文件:位于/dev目录,代表硬件设备。 网络字节流套接字文件:用于网络通信的数据流。 链接文件:指向另一个文件的符号链接或硬链接。 管道文件:用于进程间通信的文件。
47 7
|
29天前
|
存储 缓存 监控
|
2月前
|
并行计算 Ubuntu Linux
Ubuntu学习笔记(三):Linux下操作指令大全
Ubuntu学习笔记,介绍了Linux操作系统中常用的命令和操作,如文件管理、系统信息查看、软件安装等。
47 3
|
3月前
|
Shell Linux
Linux shell编程学习笔记82:w命令——一览无余
Linux shell编程学习笔记82:w命令——一览无余
|
3月前
|
监控 Linux Shell
30 个实用的 Linux 命令贴与技巧,提升你的效率(附实战案例)
本文介绍了30个实用的Linux命令及其应用场景,帮助你提升命令行操作效率。涵盖返回目录、重新执行命令、查看磁盘使用情况、查找文件、进程管理、网络状态监控、定时任务设置等功能,适合各水平的Linux用户学习和参考。
|
3月前
|
存储 Linux 索引
Linux 下最主流的文件系统格式——ext
【9月更文挑战第8天】硬盘被划分为若干相同大小的块(Block),默认大小为4K,便于灵活管理文件数据。文件数据分散存放于这些块中,提高了数据添加、删除和插入的便利性。
|
3月前
|
存储 缓存 Linux
Linux文件系统的功能规划
【9月更文挑战第12天】本文通过类比图书馆,形象地解释了文件系统的组织形式和管理方法。首先,文件系统需按块存储文件,并设有索引区方便查找。其次,热点文件应有缓存层提高效率,文件需分类存储以便管理。最后,Linux内核需记录文件使用情况,通过文件描述符区分不同文件,确保文件操作准确无误。