文件系统的IO

VFS虚拟文件系统

虚拟文件系统(也称为虚拟文件系统开关)是内核中的软件层,它为用户空间程序提供文件系统接口。它还在内核中提供了一个抽象,允许不同的文件系统实现共存
在 Linux 文件系统中,用户空间、系统调用、虚拟机文件系统、缓存、文件系统以及存储之间的关系如下图:
Linux 支持的文件系统也不少,根据存储位置的不同,可以把文件系统分为三类:
  • 磁盘的文件系统,它是直接把数据存储在磁盘中,比如 Ext 2/3/4、XFS 等都是这类文件系统。
  • 网络的文件系统,用来访问其他计算机主机数据的文件系统,比如 NFS、SMB 等等。
  • 内存的文件系统,这类文件系统的数据不是存储在硬盘的,而是占用内存空间,我们经常用到的 /proc 和 /sys 文件系统都属于这一类,读写这类文件,实际上是读写内核中相关的数据数据。
 
 
文件系统首先要先挂载到某个目录才可以正常使用,比如 Linux 系统在启动时,会把文件系统挂载到根目录。
notion image

挂载

在 Linux 看来,任何硬件设备也都是文件,它们各有自己的一套文件系统(文件目录结构)。
因此产生的问题是,当在 Linux 系统中使用这些硬件设备时,只有将Linux本身的文件目录与硬件设备的文件目录合二为一,硬件设备才能为我们所用。合二为一的过程称为“挂载”。
如果不挂载,通过Linux系统中的图形界面系统可以查看找到硬件设备,但命令行方式无法找到。
挂载,指的就是将设备文件中的顶级目录连接到 Linux 根目录下的某一目录(最好是空目录),访问此目录就等同于访问设备文件。
纠正一个误区,并不是根目录下任何一个目录都可以作为挂载点,由于挂载操作会使得原有目录中文件被隐藏,因此根目录以及系统原有目录都不要作为挂载点,会造成系统异常甚至崩溃,挂载点最好是新建的空目录。
举个例子,我们想通过命令行访问某个 U 盘中的数据, 如下所示为 U 盘文件目录结构和 Linux 系统中的文件目录结构
                                                          U 盘和 Linux 系统文件目录结构
U 盘和 Linux 系统文件目录结构
上图 中可以看到,目前 U 盘和 Linux 系统文件分属两个文件系统,还无法使用命令行找到 U 盘文件,需要将两个文件系统进行挂载。
接下来,我们在根目录下新建一个目录 /sdb-u,通过挂载命令将 U 盘文件系统挂载到此目录,挂载效果如下图 所示
notion image
可以看到,U 盘文件系统已经成为 Linux 文件系统目录的一部分,此时访问 /sdb-u/ 就等同于访问 U 盘。
Linux 根目录下的 /dev/ 目录文件负责所有的硬件设备文件,事实上,当 U 盘插入 Linux 后,系统也确实会给 U 盘分配一个目录文件(比如 sdb1),就位于 /dev/ 目录下(/dev/sdb1),但无法通过 /dev/sdb1/ 直接访问 U 盘数据,访问此目录只会提供给你此设备的一些基本信息(比如容量)。
总之,Linux 系统使用任何硬件设备,都必须将设备文件与已有目录文件进行挂载

自定义镜像文件挂载到VFS目录

df命令查看当前Linux 系统上的文件系统磁盘使用情况统计。
[root@01ec49fa52d2 dev]# df -h Filesystem Size Used Avail Use% Mounted on overlay 251G 2.5G 236G 2% / tmpfs 64M 0 64M 0% /dev tmpfs 13G 0 13G 0% /sys/fs/cgroup shm 64M 0 64M 0% /dev/shm /dev/sdd 251G 2.5G 236G 2% /etc/hosts tmpfs 13G 0 13G 0% /proc/acpi tmpfs 13G 0 13G 0% /sys/firmware
在系统home文件下创建a.txt文件,并写入一定数据
使用dd命令创建磁盘镜像文件
dd if=/home/a.txt of=~/disk02.img bs=1048576 count=100
使用losetup将磁盘镜像文件虚拟成块设备
losetup /dev/loop0 ~/disk02.img mke2fs /dev/loop0 #格式化成ext2文件 mkdir /mnt/ooxx
mount -t ext2 /dev/loop0 /mnt/ooxx #将loop0设备挂载到ooxx目录 cd /mnt/ooxx mkdir bin lib64
                                                     挂载流程
挂载流程
查看挂载后的文件系统磁盘使用情况
Filesystem Size Used Avail Use% Mounted on overlay 251G 2.5G 236G 2% / tmpfs 64M 0 64M 0% /dev tmpfs 13G 0 13G 0% /sys/fs/cgroup shm 64M 0 64M 0% /dev/shm /dev/sdd 251G 2.5G 236G 2% /etc/hosts tmpfs 13G 0 13G 0% /proc/acpi tmpfs 13G 0 13G 0% /sys/firmware /dev/loop0 0.5G 0.2G 0.3G 40% /mnt/ooxx #多了一块loop0设备 映射到/mnt/ooxx
把bash 命令拷贝一份到ooxx的bin目录下
mkdir bin lib64 #在ooxx上新建 whereis bash ldd /bin/bash #分析动态链接库 cp /bin/bash bin cp /lib64/{libtinfo.so.5,libdl.so.2,libc.so.6,ld-linux-x86-64.so.2} lib64 #拷贝根目录文件到ooxx下的lib64目录 chroot ./ 改变根目录到ooxx echo "aaa" > /abc.txt 写aaa到ooxx的a.txt文件 exit cat abc.txt
umount卸载 /mnt/ooxx 再进入 ooxx看不到a.txt了,但其实是去镜像文件了
 
文件系统的基本数据单位是文件,它的目的是对磁盘上的文件进行组织管理,那组织的方式不同,就会形成不同的文件系统。
Linux 最经典的一句话是:「一切皆文件」,不仅普通的文件和目录,就连块设备、管道、socket 等,也都是统一交给文件系统管理的。
Linux 文件系统会为每个文件分配两个数据结构:索引节点(index node/inode)和目录项(directory entry /dentry),它们主要用来记录文件的元信息和目录层次结构。
  • 索引节点inode,也就是 ,用来记录文件的元信息,比如 inode 编号、文件大小、访问权限、创建时间、修改时间、数据在磁盘的位置等等。索引节点是文件的唯一标识,它们之间一 一对应,也同样都会被存储在硬盘中,所以索引节点同样占用磁盘空间
    •  
  • 目录项,也就是dentry ,用来记录文件的名字、索引节点指针以及与其他目录项的层级关联关系。多个目录项关联起来,就会形成目录结构,但它与索引节点不同的是,目录项是由内核维护的一个数据结构,不存放于磁盘,而是缓存在内存
    •  
由于索引节点唯一标识一个文件,而目录项记录着文件的名,所以目录项和索引节点的关系是多对一,也就是说,一个文件可以有多个目录项。比如,硬链接的实现就是多个目录项中的索引节点指向同一个文件。
注意,目录也是文件,也是用索引节点唯一标识,和普通文件不同的是,普通文件在磁盘里面保存的是文件数据,而目录文件在磁盘里面保存子目录或文件。
notion image
 
目录项和目录的区别?
 
虽然名字很相近,但是它们不是一个东西,目录是个文件,持久化存储在磁盘,而目录项是内核一个数据结构,缓存在内存。如果查询目录频繁从磁盘读,效率会很低,所以内核会把已经读过的目录用目录项这个数据结构缓存在内存,下次再次读到相同的目录时,只需从内存读就可以,大大提高了文件系统的效率。
注意,目录项这个数据结构不只是表示目录,也是可以表示文件的。
 
磁盘读写的最小单位是扇区,扇区的大小只有 512B 大小,很明显,如果每次读写都以这么小为单位,那这读写的效率会非常低。
所以,文件系统把多个扇区组成了一个逻辑块,每次读写的最小单位就是逻辑块(数据块),Linux 中的逻辑块大小为 4KB,也就是一次性读写 8 个扇区,这将大大提高了磁盘的读写的效率。
索引节点是存储在硬盘上的数据,那么为了加速文件的访问,通常会把索引节点加载到内存中。
另外,磁盘进行格式化的时候,会被分成三个存储区域,分别是超级块、索引节点区和数据块区。 - 超级块,用来存储文件系统的详细信息,比如块个数、块大小、空闲块等等。 - 索引节点区,用来存储索引节点; - 数据块区,用来存储文件或目录数据;
我们不可能把超级块和索引节点区全部加载到内存,这样内存肯定撑不住,所以只有当需要使用的时候,才将其加载进内存,它们加载进内存的时机是不同的:
  • 超级块:当文件系统挂载时进入内存;
  • 索引节点区:当文件被访问时进入内存;

文件的使用

我们从用户角度来看文件的话,就是我们要怎么使用文件?首先,我们得通过系统调用(system call)来打开一个文件。
notion image
fd= open(name, flag); # 打开文件 ... write(fd,...); # 写数据 ... close(fd); # 关闭文件
上面简单的代码是读取一个文件的过程: - 首先用 open 系统调用打开文件,open 的参数中包含文件的路径名和文件名。 - 使用 write 写数据,其中 write 使用 open 所返回的文件描述符,并不使用文件名作为参数。 - 使用完文件后,要用 close 系统调用关闭文件,避免资源的泄露。
我们打开了一个文件后,操作系统会跟踪进程打开的所有文件,所谓的跟踪呢,就是操作系统为每个进程维护一个打开文件表,文件表里的每一项代表「文件描述符」,所以说文件描述符是打开文件的标识。(相同描述符可以同时存在多个进程中,例如输入输出的文件描述符)
操作系统在打开文件表中维护着打开文件的状态和信息:
notion image
  • 文件指针:系统跟踪上次读写位置作为当前文件位置指针,这种指针对打开文件的某个进程来说是唯一的
  • 文件打开计数器:文件关闭时,操作系统必须重用其打开文件表条目,否则表内空间不够用。因为多个进程可能打开同一个文件,所以系统在删除打开文件条目之前,必须等待最后一个进程关闭文件,该计数器跟踪打开和关闭的数量,当该计数为 0 时,系统关闭文件,删除该条目;
  • 文件磁盘位置:绝大多数文件操作都要求系统修改文件数据,该信息保存在内存中,以免每个操作都从磁盘中读取;
  • 访问权限:每个进程打开文件都需要有一个访问模式(创建、只读、读写、添加等),该信息保存在进程的打开文件表中,以便操作系统能允许或拒绝之后的 I/O 请求;
在用户视角里,文件就是一个持久化的数据结构,但操作系统并不会关心你想存在磁盘上的任何的数据结构,操作系统的视角是如何把文件数据和磁盘块对应起来。
所以,用户和操作系统对文件的读写操作是有差异的,用户习惯以字节的方式读写文件,而操作系统则是以数据块来读写文件,那屏蔽掉这种差异的工作就是文件系统了。
我们来分别看一下,读文件和写文件的过程:
  • 当用户进程从文件读取 1 个字节大小的数据时,文件系统则需要获取字节所在的数据块,再返回数据块对应的用户进程所需的数据部分。
  • 当用户进程把 1 个字节大小的数据写进文件时,文件系统则找到需要写入数据的数据块的位置,然后修改数据块中对应的部分,最后再把数据块写回磁盘。
所以说,文件系统的基本操作单位是数据块

文件描述符

 
Linux 系统中,把一切都看做是文件,当进程打开现有文件或创建新文件时,内核向进程返回一个文件描述符,文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用来指向被打开的文件,所有执行I/O操作的系统调用都会通过文件描述符

查看文件描述符

lsof(list open files)是一个列出当前系统打开文件的工具
notion image
lsof输出各列信息的意义如下: COMMAND:进程的名称 PID:进程标识符 USER:进程所有者
FD:文件描述符,应用程序通过文件描述符识别该文件。如cwd、txt等。
(1)cwd:表示current work dirctory,即:应用程序的当前工作目录,这是该应用程序启动的目录,除非它本身对这个目录进行更改
(2)txt :该类型的文件是程序代码,如应用程序二进制文件本身或共享库,如上列表中显示的 /sbin/init 程序
(3)lnn:library references (AIX);
(4)er:FD information error (see NAME column);
(5)jld:jail directory (FreeBSD);
(6)ltx:shared library text (code and data);
(7)mxx :hex memory-mapped type number xx.
(8)m86:DOS Merge mapped file;
(9)mem:memory-mapped file;
(10)mmap:memory-mapped device;
(11)pd:parent directory;
(12)rtd:root directory;
(13)tr:kernel trace file (OpenBSD);
(14)v86 VP/ix mapped file;
(15)0:表示标准输出
(16)1:表示标准输入
(17)2:表示标准错误
一般在标准输出、标准错误、标准输入后还跟着文件状态模式:r、w、u等
(1)u:表示该文件被打开并处于读取/写入模式
(2)r:表示该文件被打开并处于只读模式
(3)w:表示该文件被打开并处于
(4)空格:表示该文件的状态模式为unknow,且没有锁定
(5)-:表示该文件的状态模式为unknow,且被锁定
同时在文件状态模式后面,还跟着相关的锁
(1)N:for a Solaris NFS lock of unknown type;
(2)r:for read lock on part of the file;
(3)R:for a read lock on the entire file;
(4)w:for a write lock on part of the file;(文件的部分写锁)
(5)W:for a write lock on the entire file;(整个文件的写锁)
(6)u:for a read and write lock of any length;
(7)U:for a lock of unknown type;
(8)x:for an SCO OpenServer Xenix lock on part of the file;
(9)X:for an SCO OpenServer Xenix lock on the entire file;
(10)space:if there is no lock.
TYPE:文件类型,如DIR、REG等,常见的文件类型
(1)DIR:表示目录
(2)CHR:表示字符类型
(3)BLK:块设备类型
(4)UNIX: UNIX 域套接字
(5)FIFO:先进先出 (FIFO) 队列
(6)IPv4:网际协议 (IP) 套接字
TYPE:文件类型,如DIR、REG等 DEVICE:指定磁盘的名称 SIZE:文件的大小 NODE:索引节点(文件在磁盘上的标识) NAME:打开文件的确切名称
查看bash进程打开了哪些文件lsof -p $$
$$ 当前bash的pid 等同 $BASHPID

Linux中重定向机制

符号前面都是被重定向的文件描述符
 
重定向符号
  • >               输出重定向到一个文件或设备 覆盖原来的文件
  • >!              输出重定向到一个文件或设备 强制覆盖原来的文件
  • >>             输出重定向到一个文件或设备 追加原来的文件
  • <               输入重定向到一个程序
标准错误重定向符号
2> 将一个标准错误输出重定向到一个文件或设备 覆盖原来的文件 b-shell 2>> 将一个标准错误输出重定向到一个文件或设备 追加到原来的文件 2>&1 将一个标准错误输出重定向到标准输出 注释:1 可能就是代表 标准输出 >& 将一个标准错误输出重定向到一个文件或设备 覆盖原来的文件 c-shell |& 将一个标准错误 管道 输送 到另一个命令作为输入
在 bash 命令执行的过程中,主要有三种输出入的状况,分别是:
1. 标准输入;代码为 0 ;或称为 stdin ;使用的方式为 <
2. 标准输出:代码为 1 ;或称为 stdout;使用的方式为 1>
3. 错误输出:代码为 2 ;或称为 stderr;使用的方式为 2>
stdin(0):标准输入,这个概念有点不太容易理解比如:1.使用<从文件中读取内容,2.当前命令将内容通过管道传输给下一个命令而下一个命令,而实际内容是传输给了stdin所以下一个命令也是从stdin中读取内容。
stdout(1):标准输出;这是默认选项。使用方法:1>等价于>  或者 1>>等价于>>,;如果想使用其它文件描述符,必须将文件描述符放在操作符之前。
stderr(2):标准错误,使用方法2>或者2>>,标准错误可以将错误信息插入到文件而不在终端显示  。
<:从文件中读取内容。
>:将内容插入到文件,每次插入前都会清空文件内容。
>>:将内容插入到文件, 将内容追加到现有文件的末尾。

自定义文件描述符

vi ooxx.txt #创建ooxx文件 exec 8< ooxx.txt # 创建文件描述符8读取ooxx文件
cd /proc/$$/fd 查看bash打开的文件描述符, 可以看到下面 8已经作为一个文件描述符被bash打开
[root@01ec49fa52d2 fd]# ll total 0 lrwx------ 1 root root 64 Jun 24 23:03 0 -> /dev/pts/1 lrwx------ 1 root root 64 Jun 24 23:03 1 -> /dev/pts/1 lrwx------ 1 root root 64 Jun 24 23:03 2 -> /dev/pts/1 lrwx------ 1 root root 64 Jun 24 23:33 255 -> /dev/pts/1 lr-x------ 1 root root 64 Jun 24 23:03 8 -> /root/ooxx.txt
lsof -p $$ 查看下bash打开的文件描述符
[root@01ec49fa52d2 fd]# lsof -p $$ COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME bash 15 root cwd DIR 0,141 0 1967 /proc/15/fd bash 15 root rtd DIR 0,139 4096 2592 / bash 15 root txt REG 0,139 964536 33462 /usr/bin/bash bash 15 root mem REG 8,48 33462 /usr/bin/bash (path dev=0,139) bash 15 root mem REG 8,48 35490 /usr/lib64/libnss_files-2.17.so (path dev=0,139) bash 15 root mem REG 8,48 35347 /usr/lib64/libc-2.17.so (path dev=0,139) bash 15 root mem REG 8,48 35376 /usr/lib64/libdl-2.17.so (path dev=0,139) bash 15 root mem REG 8,48 35597 /usr/lib64/libtinfo.so.5.9 (path dev=0,139) bash 15 root mem REG 8,48 35323 /usr/lib64/ld-2.17.so (path dev=0,139) bash 15 root 0u CHR 136,1 0t0 4 /dev/pts/1 bash 15 root 1u CHR 136,1 0t0 4 /dev/pts/1 bash 15 root 2u CHR 136,1 0t0 4 /dev/pts/1 bash 15 root 8r REG 0,139 0 2607 /root/ooxx.txt #8r 表示读取
lsof -op展示偏移量
[root@01ec49fa52d2 fd]# lsof -op $$ COMMAND PID USER FD TYPE DEVICE OFFSET NODE NAME bash 15 root cwd DIR 0,141 1967 /proc/15/fd bash 15 root rtd DIR 0,139 2592 / bash 15 root txt REG 0,139 33462 /usr/bin/bash bash 15 root mem REG 8,48 33462 /usr/bin/bash (path dev=0,139) bash 15 root mem REG 8,48 35490 /usr/lib64/libnss_files-2.17.so (path dev=0,139) bash 15 root mem REG 8,48 35347 /usr/lib64/libc-2.17.so (path dev=0,139) bash 15 root mem REG 8,48 35376 /usr/lib64/libdl-2.17.so (path dev=0,139) bash 15 root mem REG 8,48 35597 /usr/lib64/libtinfo.so.5.9 (path dev=0,139) bash 15 root mem REG 8,48 35323 /usr/lib64/ld-2.17.so (path dev=0,139) bash 15 root 0u CHR 136,1 0t0 4 /dev/pts/1 bash 15 root 1u CHR 136,1 0t0 4 /dev/pts/1 bash 15 root 2u CHR 136,1 0t0 4 /dev/pts/1 bash 15 root 8r REG 0,139 0t0 2607 /root/ooxx.txt bash 15 root 255u CHR 136,1 0t0 4 /dev/pts/1 # node列为inode号
查看ooxx的inode号
[root@01ec49fa52d2 fd]# stat ~/ooxx.txt File: '/root/ooxx.txt' Size: 0 Blocks: 0 IO Block: 4096 regular empty file Device: 8bh/139d Inode: 2607 Links: 1 Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root) Access: 2021-06-24 23:30:46.250000000 +0000 Modify: 2021-06-24 23:30:46.250000000 +0000 Change: 2021-06-24 23:30:46.250000000 +0000 Birth: -
关于 /proc 目录
在GUN/Linux操作系统中,/proc是一个位于内存中的伪文件系统(in-memory pseudo-file system)。该目录下保存的不是真正的文件和目录,而是一些“运行时”信息,如系统内存、进程、磁盘io、设备挂载信息和硬件配置信息等。proc目录是一个控制中心,用户可以通过更改其中某些文件来改变内核的运行状态。proc目录也是内核提供给我们的查询中心,我们可以通过这些文件查看有关系统硬件及当前正在运行进程的信息。在Linux系统中,许多工具的数据来源正是proc目录中的内容。例如,lsmod命令就是cat /proc/modules命令的别名,lspci命令是cat /proc/pci命令的别名。
proc目录下所有文件
proc目录下所有文件
数字代表进程,进程被映射为proc的目录
进入:/proc/$$/fd 目录下存放了bash进程所有打开的fd
 

测试socket文件描述符

echo $$ #打印当前bash的进程id 15 cd /proc/15/fd # 进程在Linux中也是一个文件,访问bash进程的文件描述符 exec 8<> /dev/tcp/www.baidu.com/80 # 新增文件描述符打开和百度的链接socket ll
查看当前文件描述符会发现一个标红的socket文件
notion image
lsof -op $$ 产看所有打开的操作符
COMMAND PID USER FD TYPE DEVICE OFFSET NODE NAME bash 15 root cwd DIR 0,141 1967 /proc/15/fd bash 15 root rtd DIR 0,139 2592 / bash 15 root txt REG 0,139 33462 /usr/bin/bash bash 15 root mem REG 8,48 33462 /usr/bin/bash (path dev=0,139) bash 15 root mem REG 8,48 35547 /usr/lib64/libresolv-2.17.so (path dev=0,139) bash 15 root mem REG 8,48 35488 /usr/lib64/libnss_dns-2.17.so (path dev=0,139) bash 15 root mem REG 8,48 35490 /usr/lib64/libnss_files-2.17.so (path dev=0,139) bash 15 root mem REG 8,48 35347 /usr/lib64/libc-2.17.so (path dev=0,139) bash 15 root mem REG 8,48 35376 /usr/lib64/libdl-2.17.so (path dev=0,139) bash 15 root mem REG 8,48 35597 /usr/lib64/libtinfo.so.5.9 (path dev=0,139) bash 15 root mem REG 8,48 35323 /usr/lib64/ld-2.17.so (path dev=0,139) bash 15 root 0u CHR 136,1 0t0 4 /dev/pts/1 bash 15 root 1u CHR 136,1 0t0 4 /dev/pts/1 bash 15 root 2u CHR 136,1 0t0 4 /dev/pts/1 bash 15 root 8u IPv4 34855 0t0 TCP 01ec49fa52d2:51134->36.152.44.96:http (CLOSE_WAIT) bash 15 root 255u CHR 136,1 0t0 4 /dev/pts/1

文件描述符偏移量

在 ooxx.txt中写入
aaaa 123
Linux read命令用于从标准输入读取数值。
read 内部命令被用来从标准输入读取单行数据。这个命令可以用来读取键盘输入,当使用重定向的时候,可以读取文件中的一行数据。
read a 0<& 8 #重定向 输入fd到自定义fd 8,读取/root/ooxx.txt 的数据到,读到第一个换行符停止 read a 0<& 8 echo $a aaaa
此时查看文件描述符偏移量
[root@9dd2f3e1447b ~]# lsof -op $$ COMMAND PID USER FD TYPE DEVICE OFFSET NODE NAME bash 202 root cwd DIR 0,142 9774 /root bash 202 root rtd DIR 0,142 2590 / bash 202 root txt REG 0,142 33462 /usr/bin/bash bash 202 root mem REG 8,48 33462 /usr/bin/bash (path dev=0,142) bash 202 root mem REG 8,48 34191 /usr/lib/locale/locale-archive (path dev=0,142) bash 202 root mem REG 8,48 35490 /usr/lib64/libnss_files-2.17.so (path dev=0,142) bash 202 root mem REG 8,48 35347 /usr/lib64/libc-2.17.so (path dev=0,142) bash 202 root mem REG 8,48 35376 /usr/lib64/libdl-2.17.so (path dev=0,142) bash 202 root mem REG 8,48 35597 /usr/lib64/libtinfo.so.5.9 (path dev=0,142) bash 202 root mem REG 8,48 35323 /usr/lib64/ld-2.17.so (path dev=0,142) bash 202 root mem REG 8,48 35288 /usr/lib64/gconv/gconv-modules.cache (path dev=0,142) bash 202 root 0u CHR 136,1 0t0 4 /dev/pts/1 bash 202 root 1u CHR 136,1 0t0 4 /dev/pts/1 bash 202 root 2u CHR 136,1 0t0 4 /dev/pts/1 bash 202 root 8r REG 0,142 0t5 293 /root/ooxx.txt #因为读取了5位(aaaa+换行)所以偏移了5位 bash 202 root 255u CHR 136,1 0t0 4 /dev/pts/1
打开另一个会话的窗口定义文件描述符6,发现偏移量为0,由此说明每个进程都会维护各自的文件描述符,互不影响
[root@9dd2f3e1447b ~]# lsof -op $$ COMMAND PID USER FD TYPE DEVICE OFFSET NODE NAME bash 181 root cwd DIR 0,142 9774 /root bash 181 root rtd DIR 0,142 2590 / bash 181 root txt REG 0,142 33462 /usr/bin/bash bash 181 root mem REG 8,48 33462 /usr/bin/bash (path dev=0,142) bash 181 root mem REG 8,48 34191 /usr/lib/locale/locale-archive (path dev=0,142) bash 181 root mem REG 8,48 35490 /usr/lib64/libnss_files-2.17.so (path dev=0,142) bash 181 root mem REG 8,48 35347 /usr/lib64/libc-2.17.so (path dev=0,142) bash 181 root mem REG 8,48 35376 /usr/lib64/libdl-2.17.so (path dev=0,142) bash 181 root mem REG 8,48 35597 /usr/lib64/libtinfo.so.5.9 (path dev=0,142) bash 181 root mem REG 8,48 35323 /usr/lib64/ld-2.17.so (path dev=0,142) bash 181 root mem REG 8,48 35288 /usr/lib64/gconv/gconv-modules.cache (path dev=0,142) bash 181 root 0u CHR 136,0 0t0 3 /dev/pts/0 bash 181 root 1u CHR 136,0 0t0 3 /dev/pts/0 bash 181 root 2u CHR 136,0 0t0 3 /dev/pts/0 bash 181 root 6r REG 0,142 0t0 293 /root/ooxx.txt # 文件描述未偏移 bash 181 root 255u CHR 136,0 0t0 3 /dev/pts/0
 

测试pipeline类型文件描述符

父子进程变量不可见,除非+export表示成环境变量
[root@9dd2f3e1447b home]# echo $$ 父进程bash 282 [root@9dd2f3e1447b home]# x=100 [root@9dd2f3e1447b home]# echo $x 100 [root@9dd2f3e1447b home]# /bin/bash [root@9dd2f3e1447b home]# echo $$ 子进程bash 294 [root@9dd2f3e1447b home]# echo $x [root@9dd2f3e1447b home]#
代码块
管道是开启两个子进程,将一个子进程的输入当作另外一个子进程的输出
[root@9dd2f3e1447b home]# echo $$ 294 [root@9dd2f3e1447b home]# a=1 [root@9dd2f3e1447b home]# echo $a 1 [root@9dd2f3e1447b home]# { a=9; echo "asasa"; } | cat asasa [root@9dd2f3e1447b home]# echo $a 1
$$解释的优先级比管道高
[root@9dd2f3e1447b home]# echo $$ 294 [root@9dd2f3e1447b home]# echo $$ | cat 294 [root@9dd2f3e1447b home]# echo $BASHPID | cat 311 [root@9dd2f3e1447b home]#
让管道左边阻塞住,并获取左边子进程id
[root@9dd2f3e1447b home]# { echo $BASHPID; read x; } | { cat ; read y; } 314
打开另一个会话窗口
[root@9dd2f3e1447b ~]# ps -ef | grep 314 找到父进程为294 root 314 294 0 08:21 pts/0 00:00:00 /bin/bash root 321 202 0 08:25 pts/1 00:00:00 grep --color=auto 314 [root@9dd2f3e1447b ~]# ps -ef | grep 294 查看父进程查到管道的另一个子进程 root 294 282 0 08:03 pts/0 00:00:00 /bin/bash root 314 294 0 08:21 pts/0 00:00:00 /bin/bash #管道左边进程 root 315 294 0 08:21 pts/0 00:00:00 /bin/bash #管道右边进程 root 323 202 0 08:26 pts/1 00:00:00 grep --color=auto 294
查看314,315两进程的文件描述符
[root@9dd2f3e1447b ~]# cd /proc/314/fd [root@9dd2f3e1447b fd]# ll total 0 lrwx------ 1 root root 64 Jun 25 08:22 0 -> /dev/pts/0 l-wx------ 1 root root 64 Jun 25 08:22 1 -> pipe:[119345] #输出到管道(左边) lrwx------ 1 root root 64 Jun 25 08:22 2 -> /dev/pts/0 lrwx------ 1 root root 64 Jun 25 08:22 255 -> /dev/pts/0 lr-x------ 1 root root 64 Jun 25 08:22 6 -> /root/ooxx.txt [root@9dd2f3e1447b fd]# cd /proc/315/fd [root@9dd2f3e1447b fd]# ll total 0 lr-x------ 1 root root 64 Jun 25 08:27 0 -> pipe:[119345] #输入管道(右边) lrwx------ 1 root root 64 Jun 25 08:27 1 -> /dev/pts/0 lrwx------ 1 root root 64 Jun 25 08:25 2 -> /dev/pts/0 lrwx------ 1 root root 64 Jun 25 08:27 255 -> /dev/pts/0 lr-x------ 1 root root 64 Jun 25 08:27 6 -> /root/ooxx.txt
[root@9dd2f3e1447b fd]# lsof -op 314 COMMAND PID USER FD TYPE DEVICE OFFSET NODE NAME bash 314 root cwd DIR 0,142 288 /home bash 314 root rtd DIR 0,142 2590 / bash 314 root txt REG 0,142 33462 /usr/bin/bash bash 314 root mem REG 8,48 33462 /usr/bin/bash (path dev=0,142) bash 314 root mem REG 8,48 35490 /usr/lib64/libnss_files-2.17.so (path dev=0,142) bash 314 root mem REG 8,48 34191 /usr/lib/locale/locale-archive (path dev=0,142) bash 314 root mem REG 8,48 35347 /usr/lib64/libc-2.17.so (path dev=0,142) bash 314 root mem REG 8,48 35376 /usr/lib64/libdl-2.17.so (path dev=0,142) bash 314 root mem REG 8,48 35597 /usr/lib64/libtinfo.so.5.9 (path dev=0,142) bash 314 root mem REG 8,48 35323 /usr/lib64/ld-2.17.so (path dev=0,142) bash 314 root mem REG 8,48 35288 /usr/lib64/gconv/gconv-modules.cache (path dev=0,142) bash 314 root 0u CHR 136,0 0t0 3 /dev/pts/0 bash 314 root 1w FIFO 0,11 0t0 119345 pipe #写管道 (左边) bash 314 root 2u CHR 136,0 0t0 3 /dev/pts/0 bash 314 root 6r REG 0,142 0t0 293 /root/ooxx.txt bash 314 root 255u CHR 136,0 0t0 3 /dev/pts/0
[root@9dd2f3e1447b fd]# lsof -op 315 COMMAND PID USER FD TYPE DEVICE OFFSET NODE NAME bash 315 root cwd DIR 0,142 288 /home bash 315 root rtd DIR 0,142 2590 / bash 315 root txt REG 0,142 33462 /usr/bin/bash bash 315 root mem REG 8,48 33462 /usr/bin/bash (path dev=0,142) bash 315 root mem REG 8,48 35490 /usr/lib64/libnss_files-2.17.so (path dev=0,142) bash 315 root mem REG 8,48 34191 /usr/lib/locale/locale-archive (path dev=0,142) bash 315 root mem REG 8,48 35347 /usr/lib64/libc-2.17.so (path dev=0,142) bash 315 root mem REG 8,48 35376 /usr/lib64/libdl-2.17.so (path dev=0,142) bash 315 root mem REG 8,48 35597 /usr/lib64/libtinfo.so.5.9 (path dev=0,142) bash 315 root mem REG 8,48 35323 /usr/lib64/ld-2.17.so (path dev=0,142) bash 315 root mem REG 8,48 35288 /usr/lib64/gconv/gconv-modules.cache (path dev=0,142) bash 315 root 0r FIFO 0,11 0t0 119345 pipe #读管道 bash 315 root 1u CHR 136,0 0t0 3 /dev/pts/0 bash 315 root 2u CHR 136,0 0t0 3 /dev/pts/0 bash 315 root 6r REG 0,142 0t0 293 /root/ooxx.txt bash 315 root 255u CHR 136,0 0t0 3 /dev/pts/0
 

软链接和硬链接

有时候我们希望给某个文件取个别名,那么在 Linux 中可以通过硬链接(Hard Link) 和软链接(Symbolic Link) 的方式来实现,它们都是比较特殊的文件,但是实现方式也是不相同的。
硬链接是多个目录项中的「索引节点」指向一个文件,也就是指向同一个 inode,但是 inode 是不可能跨越文件系统的,每个文件系统都有各自的 inode 数据结构和列表,所以硬链接是不可用于跨文件系统的。由于多个目录项都是指向一个 inode,那么只有删除文件的所有硬链接以及源文件时,系统才会彻底删除该文件。
软链接相当于重新创建一个文件,这个文件有独立的 inode,但是这个文件的内容是另外一个文件的路径,所以访问软链接的时候,实际上相当于访问到了另外一个文件,所以软链接是可以跨文件系统的,甚至目标文件被删除了,链接文件还是在的,只不过指向的文件找不到了而已。
notion image
notion image

DMA

DMA:Direct memory access,是一种访问内存的模式.
没有DMA之前的上古时代,很多设备都是低速的,比如PS/2的键鼠、PATA的光驱等。此时CPU访问它们的方式都是PIO。简单说,就是CPU去轮询(polling)这个设备(其实是设备控制器,CPU是不能直接访问设备的),检查是否有新的数据,如果有,就把它读入到内存中。
这个轮训的方式,至今在单片机的数据采集中还经常用到。这样做有个问题:低效,CPU大把的时间都用来检查了。那么有没有别的办法呢?有,中断。CPU通知设备控制器,我很忙,有很多事要去做,不能一直看你,这样吧,你接收到数据就告诉我好了。单片机的中断机制也是这样,UART的FIFO满了,产生一个中断告诉CPU,CPU进入Interrupt Service Route函数,把UART FIFO中的数据读走。
这样也有一个问题:当数据多了(比如磁盘中的一个大文件),中断就会很频繁。也变得低效了。怎么办?
于是就有了DMA控制器,它可以理解成是CPU的“协处理器”,也是CPU的小弟,CPU告诉它,我要读取磁盘中的一个小电影,但是我还有其他的事情(比如说打一局Dota),你来负责电影的读取吧。
notion image
就这样,DMA控制器替CPU接管了访问磁盘的总线控制权(所有的外设都是挂在总线上的),根据CPU发过来的数据大小和设备ID,来实现对设备的读写。这样就比PIO高级了。
这种DMA叫做Third-party DMA,或者Standard DMA,或者System DMA。总之呢,就是用一个总线上所有设备共享的DMA控制器来管DMA这件事。这类DMA也是很慢的,在磁盘这类高速设备出现之后就基本被First party DMA取代了。
First-party DMA又称为Bus Mastering,就是所有的外设直接可以申请总线的控制权,基本没有CPU什么事了。后面我们在学习用Xilinx FPGA实现一个PCIe的时候,它的IP core就是Bus mastering模式的(XAPP1052)。

PageCache

因为硬盘和内存的读写性能差距巨大,Linux默认情况是以异步方式读写文件的。比如调用系统函数open()打开或者创建文件时缺省情况下是带有O_ASYNC flag的。Linux借助于内核的page cache来实现这种异步操作 引用《Understanding the Linux Kernel, 3rd Edition》中关于page cache的定义:
The page cache is the main disk cache used by the Linux kernel. In most cases, the kernel refers to the page cache when reading from or writing to disk. New pages are added to the page cache to satisfy User Mode processes’s read requests. If the page is not already in the cache, a new entry is added to the cache and filled with the data read from the disk. If there is enough free memory, the page is kept in the cache for an indefinite period of time and can then be reused by other processes without accessing the disk. Similarly, before writing a page of data to a block device, the kernel verifies whether the corresponding page is already included in the cache; if not, a new entry is added to the cache and filled with the data to be written on disk. The I/O data transfer does not start immediately: the disk update is delayed for a few seconds, thus giving a chance to the processes to further modify the data to be written (in other words, the kernel implements deferred write operations).
也就是说,我们平常向硬盘写文件时,默认异步情况下,并不是直接把文件内容写入到硬盘中才返回的,而是成功拷贝到内核的page cache后就直接返回,所以大多数情况下,硬盘写操作不会是性能瓶颈。写入到内核page cache的pages成为dirty pages,稍后会由内核线程pdflush真正写入到硬盘上。
从硬盘读取文件时,同样不是直接把硬盘上文件内容读取到用户态内存,而是先拷贝到内核的page cache,然后再“拷贝”到用户态内存,这样用户就可以访问该文件。因为涉及到硬盘操作,所以第一次读取一个文件时,不会有性能提升;不过,如果一个文件已经存在page cache中,再次读取该文件时就可以直接从page cache中命中读取不涉及硬盘操作,这时性能就会有很大提高。
notion image
下面用dd比较下异步(缺省模式)和同步写硬盘的速度差别:
$ dd if=/dev/urandom of=async.txt bs=64M count=16 iflag=fullblock 16+0 records in 16+0 records out 1073741824 bytes (1.1 GB, 1.0 GiB) copied, 7.618 s, 141 MB/s $ dd if=/dev/urandom of=sync.txt bs=64M count=16 iflag=fullblock oflag=sync 16+0 records in 16+0 records out 1073741824 bytes (1.1 GB, 1.0 GiB) copied, 13.2175 s, 81.2 MB/s
 

pagecache数据丢失问题

比如存在这样的场景:一批数据已经成功写入到page cache,这时程序突然crash,但是在page cache里的数据还没来得及被pdflush写回到硬盘,这批数据会丢失吗?答案是,要看具体情况:
  1. 如果OS没有crash或者重启的话,仅仅是写数据的程序crash,那么已经成功写入到page cache中的dirty pages是会被pdflush在合适的时机被写回到硬盘,不会丢失数据;
  1. 如果OS也crash或者重启的话,因为page cache存放在内存中,一旦断电就丢失了,那么就会丢失数据。至于这种情况下,会丢失多少数据,主要看系统重启前有多少dirty pages被写入到硬盘,已经成功写回硬盘的就不会丢失;没来得急写回硬盘的数据就彻底丢失了。这也是异步写硬盘的一个潜在风险。同步写硬盘时就不存在这种丢数据的风险。同步写操作返回成功时,能保证数据一定被保存在硬盘上了。
 
那么如何避免因为系统重启或者机器突然断电,导致数据丢失问题呢?可以借助于WAL(Write-Ahead Log)技术。
WAL技术在数据库系统中比较常见,在数据库中一般又称之为redo log,Linux 文件系统ext3/ext4称之为journaling。WAL作用是:写数据库或者文件系统前,先把相关的metadata和文件内容写入到WAL日志中,然后才真正写数据库或者文件系统。WAL日志是append模式,所以,对WAL日志的操作要比对数据库或者文件系统的操作轻量级得多。如果对WAL日志采用同步写模式,那么WAL日志写成功,即使写数据库或者文件系统失败,可以用WAL日志来恢复数据库或者文件系统里的文件。
 

自定义系统脏页配置项

sysctl -a | grep dirty 查看Linux内核和脏页相关参数
vm.dirty_background_bytes = 0 #控制脏页内存数量,超过dirty_background_bytes时,内核的flush线程开始回写脏页 vm.dirty_background_ratio = 10 # 控制脏页占可用内存(空闲+可回收)的百分比,达到dirty_background_ratio时,内核的flush线程开始回写脏页。默认值: 10 vm.dirty_bytes = 0 #控制脏页内存数量,达到dirty_bytes时,执行磁盘写操作的进程开始回写脏页 vm.dirty_ratio = 20 # 控制脏页所占可用内存百分比,达到dirty_ratio时,执行磁盘写操作的进程自己开始回写脏数据。默认值:20 #需要注意的是,这两对参数都只能指定其中一个,先设置先生效,另一个会被清零。
vm.dirty_writeback_centisecs = 500 # 控制周期回写进程的唤醒时间,默认值为500,单位是厘秒,实际内核中是*10使用,即5s,也就是每隔5秒唤醒脏页回写进程,降低这个值可以把尖峰的写操作削平成多次写操作。 vm.dirtytime_expire_seconds = 43200 # 默认值12 * 60 * 60,即12小时。一个inode在12小时内未存在任何更新,则很可能其时间戳在过去的12小时内存在过变更,比如lazy方式更新。因此系统默认以12小时为周期的强制查看inode是否存在时间戳变更,避免文件时间戳更新过于迟滞 vm.dirty_expire_centisecs = 3000 # 控制dirty inode实际回写的等待时间,默认值是3000,即30s,只有当超过这个值后,内核回写进程才会将dirty数据回写到磁盘
可通过vi /etc/sysctl.conf 可修改脏页配置,例如
vm.dirty_background_ratio = 90 vm.dirty_ratio = 90
安装pcstat (基于go语言的内核分析工具) 使用手册 https://github.com/tobert/pcstat
notion image
name ;进程名称 size :占用总内存 pages:总页数 cached 缓存的页数 percent :缓存页占总页数比例

验证读取文件会走pagecache

/root/develop/test/ 新增一个OSFileIO 的Java文件代码如下
import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.RandomAccessFile; public class OSFileIO { static byte[] data = "123456789\n".getBytes(); static String path = "/root/develop/test/out.txt"; public static void main(String[] args) throws Exception { switch ( args[0]) { case "0" : testBasicFileIO(); break; case "1": testBufferedFileIO(); break; default: } } //最基本的file写 public static void testBasicFileIO() throws Exception { File file = new File(path); FileOutputStream out = new FileOutputStream(file); while(true){ Thread.sleep(10); out.write(data); } } //测试buffer文件IO // jvm 8kB syscall write(8KBbyte[]) public static void testBufferedFileIO() throws Exception { File file = new File(path); BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file)); while(true){ Thread.sleep(10); out.write(data); } } }
/root/develop/test/ 下编写shell脚本,编译并按指定参数允许java文件 ,并且用strace追踪Java文件执行过程中对系统的调用
rm -fr *out* /usr/local/java/jdk-16/bin/javac OSFileIO.java strace -ff -o out /usr/local/java/jdk-16/bin/java OSFileIO $1
test目录下执行脚本 ./mysh 0 (0 代表走 最基本的file写的逻辑) ,同时开启另外一个shell窗口监控ll -h && pcstat out.txt生成的out.txt缓存命中情况
notion image
发现out.txt百分百缓存

验证pagecache丢失数据

强制关机正在允许的虚拟机,然后重新开机,发现out.txt大小为0 ,之前写的数据全部丢失
notion image

验证pagecache淘汰策略

free -h查看可用
[root@97b2ba191ee7 test]# free -h total used free shared buff/cache available Mem: 24G 1.0G 22G 402M 1.3G 22G Swap: 7.0G 0B 7.0G
vi /etc/sysctl.conf 脏页刷新磁盘策略配置, 并立即生效/sbin/sysctl -p
执行sh脚本发现写到22G的时候,缓存率大幅度降低,达到脏页阈值后开始刷进磁盘
notion image

多进程pagechache的淘汰策略

 
停止代码的运行,将out.txt重新命名,mv out.txt test.txt ,查询此时缓存值和改名前一样因为innode值一样,对应的文件描述符一样
重新运行脚本观察 旧文件 test.txt 和 新文件 out.txt的缓存率情况
notion image
可以看到正在进行写操作的新文件缓存率 一直为100,而没有任何操作的旧文件缓存率已经降到了25,并且一直在下降.
说明
  • 多个进程是共享pagecache的
  • 最近未被使用的pagecache会被淘汰掉,写入磁盘,腾出cache空间给新的执行写操作的文件(底层淘汰策略其实是LRU算法)
notion image