IO学习笔记01

75 阅读17分钟

0、前言

该篇文章为IO的前置知识学习

1、Linux操作系统

1.1、冯诺依曼计算机体系结构

无论是以前的计算机还是现在计算机都是采用计算机之父冯诺依曼结构,依据冯诺依曼计算机结构,计算机的组成分为五部分:

  • 输入设备:输入数据
  • 控制器:控制程序执行
  • 运算器:数据加工处理
  • 存储器:记忆程序和数据
  • 输出设备:输出结果

1.2、Linux操作系统

结合上面冯诺依曼体系结构的总结,在当代计算机系统中

  • 输入设备:键盘、鼠标等
  • 运算器和控制器:CPU
  • 存储器:依照功能划分为内存和磁盘两类
  • 输出设备:显示屏、打印机等

关于Linux操作系统,用相对笼统的理解方式,操作系统包括:

  • 内核(kernel):kernel是一个运行在Linux内存中特殊的程序,用于管理整个Linux操作系统
  • 其他程序(app):运行在kernel以外的内存任意空间上

依照上述分析我们可以将内存划分为两块:内核空间应用程序空间

01-内核空间和用户空间

关于Linux的基本思想:

  • 一切都是文件:对kernel而言,指令、软硬件设备、进程都可以归结为文件
  • 每个文件都有确定的用途:用文件描述指令、软硬件、进程等用途

1.3、Kernel

下面我们对kernel进行部分拆解

1.3.1、VFS与文件系统的关系

关于VFS全称Virtual Filesystem Switch,翻译为虚拟文件系统转换,更多人愿意称其为虚拟文件系统,这里需要注意的是,VFS不是真实存在盘上的,而是存在内存,其用途就是文件系统和Linux 内核的接口,VFS以统一数据结构管理各种逻辑文件系统

  • 统一数据结构:对我们用户而言就是相当于构建了一颗文件目录树,也有人将VFS称之为文件目录树(具体可以用tree指令看一下)

02-tree

  • 各种逻辑文件系统:就是指真实存在的文件,如磁盘sda1、sda2等与虚拟目录的映射关系(具体可以借助df -h 指令和fdisk -l 指令查看),下面演示下df -h,读者若有兴趣可自行研究

03-df-h

到这里读者大概可以了解VFS与本地文件系统的关系,我们总结下:

  • VFS就是一块虚拟的内存映射,用于将复杂的文件系统抽象到了VFS中
  • 向上对用于提供标准的文件操作接口
  • 向下对文件系统提供标准的接入接口,便于其他文件系统移至到Linux
1.3.2、VFS如何描述文件

上文我们知道了VFS与文件系统的关系,在Linux的基本思想中一切皆文件,既:对文件的描述也可以理解为就是这个文件我们可以做何种操作或者该文件处在哪种状态

  • 对于应用程序空间而言,是使用文件描述符(fd)来表示一个文件,该fd是内核为其分配的一个整数
  • 对于内核空间而言,使用的是一个inode来表示一个文件,这个inode可以对应着应用程序一个或者多个文件描述符

至此,我们对VFS了解到这个程度即可

2、Linux文件

2.1、文件属性

2.1.1、文件类型
drwxrwxr-x 2 linzx linzx  6 Jul 26 15:17 logs
lrwxrwxrwx 1 linzx linzx 10 Jul 26 15:20 source_soft.txt -> source.txt
-rw-rw-r-- 1 linzx linzx  5 Jul 26 15:19 source.txt

上面有三个类型的文件,以source为例,这里我们首先要关注的是开头的这部分"-rw-rw-r--",这部分是文件的属性,我们首先观察首位符号

  • -:普通文件
-rw-rw-r-- 1 linzx linzx  5 Jul 26 15:19 source.txt
  • d:文件夹
mkdir logs
# 创建文件夹
drwxrwxr-x 2 linzx linzx  6 Jul 26 15:17 logs
  • l:软硬链接
ln -s source.txt source_soft.txt -> source.txt
# 软链接
lrwxrwxrwx 1 linzx linzx 10 Jul 26 15:20 source_soft.txt -> source.txt 
  • b:块设备文件,磁盘文件就属于块设备
ll /dev/sda*
brw-rw---- 1 root disk 8, 0 May 18 11:41 /dev/sda
brw-rw---- 1 root disk 8, 1 May 18 11:41 /dev/sda1
brw-rw---- 1 root disk 8, 2 May 18 11:41 /dev/sda2
  • c:字符设备,按照字符流的方式被有序访问,键盘就是属于字符设备
ll /dev/tty*
crw-rw-rw- 1 root tty       5,   0 May 18 11:41 tty
crw--w---- 1 root tty       4,   0 May 18 11:41 tty0
  • s:socket套接字文件
  • p:pipeline管道文件
2.1.2、文件权限

我们继续回到source.txt文件

-rw-rw-r-- 1 linzx linzx  5 Jul 26 15:19 source.txt

我们看到这块属性 rw-rw-r-- ,这块属性分别描述了文件的权限信息,

#rw-	rw-		r--		三个一组,分别表示ugo权限
  • u:user属主(或称为owner),表示文件的所属用户所拥有的权限
  • g:group属组,表示与文件属主同组的其他用户对此文件所拥有的权限
  • o:other其他人,表示当前系统中其他用户对此文件所拥有的权限
  • a:all所有,除上述所述三个级别外,a是上述三个级别的总和

下面我们看下三种权限:

  • r:可读
  • w:可写
  • x:可执行
  • 以上权限分别也可以用二进制表示
    • r--:二进制表示为100,数字表示为4
    • -w-:二进制表示为010,数字表示为2
    • --x:二进制表示为001,数字表示为1
    • rwx:二进制表示为111,数字表示为7
# 对source.txt 所有用户赋予可执行权限
# 这里的  + 表示为授予,类似的也有 
#        - 表示为取消授权
#        = 表示为重新定义权限
# 执行 chmod a+x source.txt
-rwxrwxr-x 1 linzx linzx  5 Jul 26 15:19 source.txt

# 对source.txt 所有用户赋予所有权限
# 这里的777就表示了各用户组的权限信息
chmod 777 source.txt
-rwxrwxrwx 1 linzx linzx  5 Jul 26 15:19 source.txt
2.1.2、其他属性

回到source.txt

-rw-rw-r-- 1 linzx linzx  5 Jul 26 15:19 source.txt

我们把剩下的属性解释下

  • 1:表示Inode链接数为1(Inode后续解释)
  • 第一个linzx:表示所属用户名,可用chown修改
  • 第二个linzx:表示所属用户组名,可用chgrp可以修改
  • 5:表示文件的大小
  • Jul 26 15:19:表示文件的修改时间,第一次创建则为创建时间
  • source.txt:表示文件名

2.2、Inode

2.2.1、文件存储以及Inode

来模拟一个场景假设给你一个10M大小文件,文件是由多个大小为512字节数据拼接起来的,要你设计如何合理的读取与更新文件的更为合适?

相信很多有经验的开发会给出统一的答复,因为一次读取512字节太麻烦了,按批次读取,每次读取一“块”数据文件(包含多个512字节数据),这样既不会损坏数据,又可以提高读取与更新效率。

没错!Linux的文件存储也是这样设计的,其中

  • 磁盘的最小存储单位为扇区(sector),大小为512字节
  • 多个扇区组成一个“块”(block),常见大小为4KB,既8个扇区的大小
  • 在文件系统中文件存储是按“块”进行存储,既有存储的地方,同时我们文件系统也需要记录文件包含多少“块”,以及创建日期、所属用户等元数据进行存储,这个存储元数据的的区域就是Inode,每个文件都必须包含一个Inode

05-Inode01

2.2.2、Inode的内容

我们可以通过stat指令查看Inode信息

# 执行 stat source.txt
  File: ‘source.txt’
  Size: 5               Blocks: 8          IO Block: 4096   regular file
Device: fd04h/64772d    Inode: 4194404     Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1001/   linzx)   Gid: ( 1001/   linzx)
Access: 2021-07-26 15:19:27.803793344 +0800
Modify: 2021-07-26 15:19:27.803793344 +0800
Change: 2021-07-26 15:29:16.998793344 +0800
Birth: -

# 下方为翻译信息
  文件: ‘source.txt’
  大小: 5               块: 8          IO 块: 4096   普通文件
设备: fd04h/64772d    Inode: 4194404     链接数: 1
权限: (0664/-rw-rw-r--)  Uid: ( 1001/   linzx)   Gid: ( 1001/   linzx)
最近访问: 2021-07-26 15:19:27.803793344 +0800
最近修改: 2021-07-26 15:19:27.803793344 +0800
Inode最近改动: 2021-07-26 15:29:16.998793344 +0800
创建时间: -

以上显示的信息,除了文件名,其它的都会存在Inode的内容中

2.2.3、Inode的存储

Inode其实也是会消耗磁盘空间,硬盘格式化时,操作系统自动将硬盘分为:

  • 数据区:存放数据文件
  • Inode元数据区(Inode table):存在Inode元数据

Inode默认大小为128字节或256字节,在格式化时,就会给定Inode的总数,一般是每1KB或2KB设置一个Inode,关于Inode的总数和使用数量我们可以借助指令 df -i 来查看

df -i
Filesystem                   Inodes IUsed    IFree IUse% Mounted on
/dev/mapper/rootvg-lv_root  5496448 67910  5428538    2% /
devtmpfs                     997660   382   997278    1% /dev
tmpfs                       1000333     1  1000332    1% /dev/shm
tmpfs                       1000333   503   999830    1% /run
tmpfs                       1000333    16  1000317    1% /sys/fs/cgroup
/dev/sda1                    256000   329   255671    1% /boot
/dev/mapper/rootvg-lv_home  2621440    17  2621423    1% /home
/dev/mapper/rootvg-lv_opt  26214400 39239 26175161    1% /opt
/dev/mapper/rootvg-lv_log   2621440   179  2621261    1% /var/log
tmpfs                       1000333     1  1000332    1% /run/user/0

关于Inode table占用磁盘大小,我们这边给出一个例子:给定磁盘大小1G,Inode大小为128字节,每1KB就设置一个Inode,最终Inode table的大小就会达到128MB,占整块硬盘的12.8%,可见Inode元数据的存储其实也是很占用磁盘

【关于这一点的延伸可以拓展到HDFS中小文件对NameNode的影响,这里笔者就不做过多讲解,有兴趣读者可自行研究】

2.2.4、Inode与软硬链接

一般情况下,对于内核空间而言,一个Inode会与文件名进行绑定,一个Inode对应一个磁盘块文件,下面我们先介绍下来两种链接形式

  • 硬链接:将多个文件名指向一个Inode,注:这里是文件名
# ln 源文件 目标文件
# 以source.txt为例
-rw-rw-r-- 1 linzx linzx  5 Jul 26 15:19 source.txt

# 为source.txt创建一个硬链接,名字为source_d.txt
ln source.txt source_d.txt

# 这里可以看到source.txt Inode的链接数从1变为2
ll 
-rw-rw-r-- 2 linzx linzx  5 Jul 26 15:19 source_d.txt
-rw-rw-r-- 2 linzx linzx  5 Jul 26 15:19 source.txt

# 我们在用ll -i查看具体的Inode号,我们可以看到source.txt和source_d.txt的Inode号是一样的
ll -i
4194404 -rw-rw-r-- 2 linzx linzx  5 Jul 26 15:19 source_d.txt
4194404 -rw-rw-r-- 2 linzx linzx  5 Jul 26 15:19 source.txt
# 既我们对source_d.txt文件的修改会影响都source.txt读取的内容
# 但是我们删除source.txt也不会对source_d.txt有影响
rm -f source.txt
ll
-rw-rw-r-- 1 linzx linzx  7 Jul 27 11:18 source_d.txt

通过上面的演示,读者知道Inode为什么会不记录文件名这个属性,因为文件名不能作为判断磁盘文件块是否存在的标准。同时我们也来了解硬链接就是有种类似Windows的指定别名

提到磁盘文件块,这里笔者又要增加一个知识点,在Linux中怎样才能真正删除磁盘文件块?

其实很简单,在Linux系统中,会为每一个磁盘文件块维护一个引用计数,只要有引用指向这个磁盘文件块,文件就不会被删除,而这个引用就是我们熟知的Inode,对于删除磁盘文件块我们可以通过删除Inode来完成

  • 软链接:相当于对文件创建个快捷方式
# ln -s 源文件 目标文件
# 以source.txt为例
-rw-rw-r-- 1 linzx linzx  7 Jul 27 11:23 source.txt

# 为source.txt创建一个软链接,source_soft.txt
ln -s source.txt source_soft.txt
ll
lrwxrwxrwx 1 linzx linzx 10 Jul 27 15:18 source_soft.txt -> source.txt
-rw-rw-r-- 1 linzx linzx  7 Jul 27 11:23 source.txt

# 我们在用ll -i查看具体的Inode号,我们可以看到source.txt和source_soft.txt的Inode号是不一样
# source_soft.txt的文件类型为l
ll -i
4194404 lrwxrwxrwx 1 linzx linzx 10 Jul 27 15:18 source_soft.txt -> source.txt
4194405 -rw-rw-r-- 1 linzx linzx  7 Jul 27 11:23 source.txt

# 删除source.txt文件,再cat source_soft.txt
rm -f source.txt
cat source_soft.txt
cat: source_soft.txt: No such file or directory

通过上面的演示,我们可以知道source_soft.txt链接文件的Inode和源文件source.txt不同,且当源文件source.txt被删除后,source_soft.txt也一并变成不可用,这就是软硬链接的最大区别

2.3、父子进程以及管道

2.3.1、父子进程

显示当前进程ID

  • $BASHPID:显示当前进程ID
  • $$:显示当前进程ID(与BASHPID最大的区别就是,优先级比BASHPID最大的区别就是,优先级比BASHPID高,后面演示)
[root@node01 linzx]# echo $$
50329
[root@node01 linzx]# echo $BASHPID
50329
[root@node01 linzx]# ps -ef|grep bash
root     50329 50315  0 10:11 pts/0    00:00:00 -bash
root     51301 50329  0 10:17 pts/0    00:00:00 grep --color=auto bash

开启子进程

  • bash:在当前进程开启一个新的子进程
[root@node01 linzx]# echo $$
50329
[root@node01 linzx]# bash
[root@node01 linzx]# echo $$
51829
[root@node01 linzx]# ps -ef|grep bash | grep -v grep
root     50329 50315  0 10:11 pts/0    00:00:00 -bash
root     51829 50329  0 10:20 pts/0    00:00:00 bash

从上面的演示可以看到,在执行完bash指令后,进程ID为51829,我们再通ps指令查看下关于bash进程信息,可以看到,进程ID为51829的父进程ID是50329,也就是我们执行bash的父进程ID

父子进程变量传递问题

我们都知道在Linux系统中各个进程之间都存在进程隔离

  • export:将变量设置为环境变量,以达到共享效果
# 实验一
[root@node01 linzx]# echo $$
50329
[root@node01 linzx]# x=10
[root@node01 linzx]# echo $x
10
[root@node01 linzx]# bash
[root@node01 linzx]# echo $$
55004
[root@node01 linzx]# echo $x
		# 这里取不到数据为空

通过上面演示可以看到,父进程50329设置了变量x值为10,子进程55004获取不到变量x的值

# 实验二
# 进程一
[root@node01 linzx]# echo $$
50329
[root@node01 linzx]# export x=10
[root@node01 linzx]# echo $x
10

# 开启另一个shell窗口,进程二
[root@node01 linzx]# echo $$
54482
[root@node01 linzx]# echo $x
		# 这里取不到数据为空

通过上面演示可以看到,进程50329设置了环境变量x值为10,进程54482获取不到环境变量x的值,因为进程50329与进程54482是两个不同的进程,所处在的环境变量不一样,所以就算进程50329设置了环境变量x,进程54482还是无法获取

# 实验三
[root@node01 linzx]# echo $$
50329
[root@node01 linzx]# export x=10
[root@node01 linzx]# echo $x
10
[root@node01 linzx]# bash
[root@node01 linzx]# echo $$
55467
[root@node01 linzx]# echo $x
10

通过上面演示可以看到,进程50329设置了环境变量x值为10,子进程55467可以获取到环境变量x的值,这点可以与上面两个实验一同进行对比:

  1. 父进程通过export为环境变量x进行赋值
  2. 父进程创建了子进程,既子进程继承了父进程的环境变量,所以子进程可以获取父进程的赋值
2.3.2、管道

管道是一种最基本的通信机制,常用于进程间的通信,其实际就是借助了

  • 内核中的缓冲区
  • 两个文件描述符,一个读,一个写,进行读写(R/W)
  • 数据从写端流向读端,只能是单向
  • | 是管道的界定符

下面我们通过几个实验来演示下管道的用法:

配合指令使用

# 实验一
# 进程一(写) | 进程二(读) PS:这个还要考虑优先级问题
[root@node01 linzx]# ll
total 4
drwxrwxr-x 2 linzx linzx  6 Jul 28 10:59 logs
lrwxrwxrwx 1 linzx linzx 10 Jul 27 15:18 source_soft.txt -> source.txt
-rw-rw-r-- 1 linzx linzx  0 Jul 27 15:26 source.txt
-rw-rw-r-- 1 linzx linzx  2 Jul 26 16:18 test.txt
# 将ll指令的结果进行过滤,找到含有source关键字的数据
[root@node01 linzx]# ll | grep source
lrwxrwxrwx 1 linzx linzx 10 Jul 27 15:18 source_soft.txt -> source.txt
-rw-rw-r-- 1 linzx linzx  0 Jul 27 15:26 source.txt

通过上面演示可以看到,以 | 为分界,ll指令的输出,作为grep指令的输入,进行过滤

# 常见的用法还有
# 查看进程ID为 50329的信息
ps -ef|grep 1000
# 查看端口号为 8080的信息
netstat -natp|grep 8080

配合代码块使用

# 实验二
# 代码块由 {} 括起来的指令,用“;”分隔
[root@node01 linzx]# { echo "hello"; echo "linux"; }
hello
linux
[root@node01 linzx]# { echo "hello"; echo "linux"; } | grep hello
hello

# 关于变量问题
[root@node01 linzx]# x=10
[root@node01 linzx]# echo $x
10
# 这段脚本执行,x的值会是11还是10?
[root@node01 linzx]# { x=11; echo "linux"; } | cat
linux
[root@node01 linzx]# echo $x
10

通过上面演示可以看到,代码块中将变量x的值修改为11,但是却没有生效,其实是因为:管道就是以 | 为界限符,将 | 左边的代码放到一个子进程去执行,右边的代码放到另一个子进程去执行,既然是在子进程去修改变量x,那就不会影响到父进程的变量值

优先级问题

# 实验三 
[root@pms-manger linzx]# echo $$
28960
[root@pms-manger linzx]# echo $BASHPID
28960
[root@pms-manger linzx]# echo $BASHPID | cat
38570
[root@pms-manger linzx]# echo $$ | cat
28960

通过上面演示可以看到,同样都是获取当前进程号,BASHPID和$$的效果差距,因为这里还涉及到优先级问题,**BASHPID的优先级低于管道,所以bash在解析执行中,会优先创建管道**,再通过子进程去执行echo BASHPID,相反的$$优先级比管道高,所以echo $$ 执行先于管道,这也正是上文所提及到的**$$的优先级比BASHPID高**的原因

验证管道就是以 | 为界限符,左边的代码放到一个子进程去执行,右边的代码放到另一个子进程去执行

# 利用read指令阻塞
[root@pms-manger linzx]# x=0
[root@pms-manger linzx]# echo $x
0
[root@pms-manger linzx]# read x
# 这里会阻塞等待用户输入,只要输入回车,会一直阻塞下去
10
[root@pms-manger linzx]# echo $x
10

# 实验四
# 窗口一
[root@node01 linzx]# echo $BASHPID
28960
[root@node01 linzx]# { echo $BASHPID; read x; } | { cat; read y; echo $BASHPID; }
42972 #左边子进程ID
# 这里进行阻塞等待输入,打开第二个shell窗口查看进程ID28960信息
hello
42973 #右边子进程ID

通过上面演示可以看到,在窗口一执行指令进行阻塞等待后,打开新的shell窗口二我们可以看到,主进程28960下真的创建了两个子进程,进程ID分别是42972、42973,这也证实了管道就是以 | 为界限符,左边的代码放到一个子进程去执行,右边的代码放到另一个子进程去执行

# 窗口二
# 利用ps指令查看进程ID28960信息
[root@node01 linzx]# ps -ef|grep 28960
root     28960 28946  0 14:50 pts/0    00:00:00 -bash
root     42972 28960  0 16:19 pts/0    00:00:00 -bash
root     42973 28960  0 16:19 pts/0    00:00:00 -bash
root     43005 42408  0 16:19 pts/1    00:00:00 grep --color=auto 28960

# 查看子进程ID42972的文件描述符
[root@pms-manger fd]# cd /proc/42972/fd
[root@pms-manger fd]# ll
total 0
lrwx------ 1 root root 64 Jul 28 16:21 0 -> /dev/pts/0
l-wx------ 1 root root 64 Jul 28 16:21 1 -> pipe:[27993292]
lrwx------ 1 root root 64 Jul 28 16:19 2 -> /dev/pts/0
lrwx------ 1 root root 64 Jul 28 16:21 255 -> /dev/pts/0

管道的存在

通过窗口二我们可以看到, 1 ->pipe:[27993292],有这种奇怪的写法,这里其实是文件描述符的标识(关于文件描述符后续文章进行解释),1标识标准输入,->标识其指向,1 ->pipe:[27993292]就是指该进程的标准输入依赖管道pipe:[27993292]的输入。这里可以证明了管道真实存在

2.4、文件描述符(file descriptor)

通过前面文章我们知道,对于应用程序空间而言,是使用文件描述符(fd)来表示一个文件,下面我们来看下什么这个fd到底有什么

nohup java -jar test.jar > /dev/null 2>&1 &

相信用过Linux启动java程序的人对上面的命令很熟悉,这里我们重点要讲解下这个

# 含义就是将标准错误2 重定向到标准输出1
# 用大白话来说就是,错误信息和标准信息同时输入到一个文件里
2>&1  

回到文件描述符,在Linux系统中,每一个进程都会存在三个最基本的文件描述符

0:标准输入
1:标准输出
2:错误输出

下面我们借助下,lsof 指令来验证上面三个文件描述符的存在

# lsof -p 进程id,查看哪些文件被该进程打开
[root@linzx ~]# lsof -p $$
COMMAND   PID USER   FD   TYPE DEVICE  SIZE/OFF     NODE NAME
bash    37796 root  cwd    DIR  253,0       195 16797794 /root
bash    37796 root  rtd    DIR  253,0      4096       96 /
bash    37796 root  txt    REG  253,0    960472 16799261 /usr/bin/bash
bash    37796 root  mem    REG  253,0 106070960  8400091 /usr/lib/locale/locale-archive
bash    37796 root  mem    REG  253,0     62184 25197441 /usr/lib64/libnss_files-2.17.so
bash    37796 root  mem    REG  253,0   2127336 25197423 /usr/lib64/libc-2.17.so
bash    37796 root  mem    REG  253,0     19776 25197429 /usr/lib64/libdl-2.17.so
bash    37796 root  mem    REG  253,0    174520 25197779 /usr/lib64/libtinfo.so.5.9
bash    37796 root  mem    REG  253,0    164264 25197416 /usr/lib64/ld-2.17.so
bash    37796 root  mem    REG  253,0     26254 25197752 /usr/lib64/gconv/gconv-modules.cache
bash    37796 root    0u   CHR  136,0       0t0        3 /dev/pts/0
bash    37796 root    1u   CHR  136,0       0t0        3 /dev/pts/0
bash    37796 root    2u   CHR  136,0       0t0        3 /dev/pts/0
bash    37796 root  255u   CHR  136,0       0t0        3 /dev/pts/0

我们简单介绍下上面列的信息:

  • COMMAND:命令的类型
  • PID:进程号
  • USER:用户
  • FD:file descriptor文件描述符
  • TYPE:文件类型
  • DEVICE:设备号
  • SIZE/OFF:文件大小/偏移量
  • NODE:Inode
  • NAME:文件名

我们重点关注下第四列FD:

# cwd:当前工作目录
# rtd:根据目录路径
# text:当前进程的解释程序文本
# mem:当前进程占用的内存
# 下面三行就是我们所说的三个最基本的文件描述符
bash    37796 root    0u   CHR  136,0       0t0        3 /dev/pts/0
bash    37796 root    1u   CHR  136,0       0t0        3 /dev/pts/0
bash    37796 root    2u   CHR  136,0       0t0        3 /dev/pts/0