系列文章目录
学懂IO必备的操作系统知识(一)
学懂IO必备的操作系统知识(二)
学懂IO必备的TCP、socket知识(三)
很多非计算机专业的学生学习java编程时,一上来就接触各种IO:BIO、NIO、AIO,同步io、阻塞io,可能直接傻掉了,what‘s the hell!
接下来我们就从操作系统层面一点点的解开IO的面纱,让你一睹真容,其实它就是个普普通通的“人”。
计算机主要组成:运算器、控制器、存储器、输入设备和输出设备。
IO指的是input、output,数据的输入输出,一般涉及到磁盘的io、网络的io(网卡的io)。
一、磁盘io
我们先讲磁盘的io,一般来说程序是没有办法直接访问操作磁盘的,直接io(不经过内核缓冲区,直接操作磁盘数据)除外。其他形式的磁盘io,都需要经过内核kernel,内核就是操作系统的程序,只是它的权利很大而已。
内核
kernel 包含了虚拟文件系统、fd表、of表、inode表、pagecache 等。
操作系统
每种操作系统都是有自己的虚拟文件系统的。Windows对应的是各种盘下的文件夹、文件等;linux的是通过虚拟目录树组成的,在linux的世界中一切皆文件,所有的文件操作,都是通过fd定位资源和状态的,不管读写文件还是进行网络通信。
linux中涉及的文件类型如下:
ll 命令 第一列:第一个字符代表类型,第二列:硬链接的数量
-:普通文件(可执行、图片、文件)
d:目录
b:块设备–可以前后随意读取,硬盘
c:字符设备–不可以前后随意读取,键盘
s:socket
p:pipeline (管道)
l:link 链接(弱连接、强链接)
[eventpoll]:
… …
rw前的-,就是文件类型
理解具体情况,需要了解由内核维护的3个数据结构:
- 进程级文件描述符表(file descriptor table)
- 系统级打开文件表(open file table)
- 文件系统i-node表(i-node table)
这3个数据结构之间的关系如下图所示:
文件描述符表
系统为每个进程维护一份文件描述符表,该表的每一个条目都记录了记录了打开文件描述体(open file table)指针,通过of找到访问的方式、文件偏移量、i-node对象指针等。表中每个条目为一个fd-文件描述符,是供程序读写使用。
该表的每一个条目都记录了单个文件描述符的相关信息,包括:
- 控制标志(flags),目前内核仅定义了一个,即close-on-exec
- 打开文件描述体指针
fd其实就是一个数字,linux默认是有0,1、2的,再增加就是从3开始
- 0:标准输入(system.in())
- 1:标准输出(system.out(),输出到屏幕)
- 2:标准错误输出(system.error())
exec 8 < demo.txt,在当前进程创建一个fd,可以在/proc/$$/fd 下看到8, 两个 $代码当前进程号,可以替换为其他的id号
read a 0<& 8 :d 读取8中的内容到a,echo a可以看到8里面的内容。
exec 8 <> /dev/tcp/www.baidu.com/80 :这样就跟百度取的一个连接,通过8这个fd进行读写,如图:
打开文件列表
内核对所有打开的文件维护一个系统级别的打开文件描述表(open file description table)。表中的条目称为打开文件描述体(open file description),存储了与一个打开的文件相关的全部信息,包括:
- 文件偏移量(file offset),调用read()和write()更新,调用lseek()直接修改
- 访问模式(status flags),由open()调用设置,例如:只读、只写或读写等
- i-node对象指针(v-node ptr),指向一个inode元素,从而关联物理文件
i-node表
在linux系统中,文件使用inode号来描述,inode存储了文件的很多元信息。每个文件系统会为存储于其上的所有文件(包括目录)维护一个i-node表
单个i-node包含以下信息:
- 文件类型(file type),可以是常规文件、目录、套接字或FIFO
- 文件的字节数
- 文件拥有者的User ID
- 文件的Group ID
- 文件的读、写、执行权限
- 文件的时间戳,共有三个:ctime指inode上一次变动的时间,mtime指文件内容上一次变动的时间,atime指文件上一次打开的时间。
链接数,即有多少文件名指向这个inode - 文件数据block的位置
i-node存储在磁盘设备上,内核在内存中维护了一个副本,这里的i-node表为后者。副本除了原有信息,还包括:引用计数(从打开文件描述体)、所在设备号以及一些临时属性,
页缓存pagecache
pagecache是内核实现的,以4K为缓存单位,在64位系统上为8k,这里会有脏页的产生。pagecache是为了提高程序与磁盘的交互,减少用户态到内核态的切换而产生的,只要增加了一层缓存,就会影响到数据的一致性,当新建一个缓存页或者修改了缓存页时,还没有刷盘,这个时候它就是脏页(与磁盘数据不一致),如果计算机突然断电会导致,脏页中的数据丢失。
多个程序操作一个文件时,只会在pagecache加载一次,共享缓冲页。
pagecache存在内存淘汰策略,淘汰的都是正常的落盘的页。
刷屏策略:内核通过占比阈值、时间阈值刷盘。
通过/etc/sysctl.conf 调整 pagecache相关的参数。具体每项意思这里就不赘述了
vm.dirty_background_ratio = 10
vm.dirty_background_bytes = 0
vm.dirty_ratio = 20
vm.dirty_bytes = 0
vm.dirty_writeback_centisecs = 500
vm.dirty_expire_centisecs = 3000
pcstate -p $$:查看当前进程的pagecache (怎么添加pcstate后续会加)
如何用代码查看pagecache ,确实是刷盘的?
首先改写/etc/sysctl.conf 配置项,再编写一个java程序,往一个文件demo.txt 死循环一直往里面写数据,写的过程中可以通过pcstate 查看pagecache的具体情况,这个时候可以通过正常关闭虚拟机、直接关掉虚拟机来测试pagecache,正常关闭pagecache是会刷盘的,直接关掉的pagecache脏页会丢失未刷盘。重启虚拟机就可以看到demo.txt 大小的变化。
硬盘分区
根据自己需要自定义分区,下面是个例子3个分区:
1:200M (bios启动这个分区kernel在里面,boot 也挂载这个,会覆盖下面的boot挂载)
2:swap:2G
3:/、root、boot、etc…
重定向
重定向不是命令,是机制
$$ 当前base的进程id,== $BASEPID
/proc 内核映射的文件
/proc/pid/fd/ == lsof -p pid 查看指定进程打开的fd
ls ./ 1> ls.out :ls 输出的内容,重定向到ls.out中
cat 0< ls.out 1>cat.out :cat 的输入从ls.out获取,输出到cat.out (0<等于<;1>等于>)
read a:读取键盘输入,回车结束,
**echo $a:**打印上面键盘输入的值。
read a 0< cat.out :指定read的输入流为cat.out ,但是read换行结束,因此a 只有cat.out的第一行。
ls ./ /dd 1> std.out 2>error.out :ls的标准输出到std.out中,标准错误输出到error.out 中
ls ./ /dd 1> std.out 2>&1 :ls的标准输出、标准错误都指向到了std.out
进程间的变量是隔离的,除非使用export导出
{ echo “ad”; echo “123”; }:多条指令的代码块,注意空格和分号
管道
|管道左侧的输出作为右侧的输入
head all.log : 读取头的十行,head -2 all.log 读取头的前两行
tail all.log : 读取尾的十行,tail-2 all.log 读取尾的两行
head -10 all.log |tail -1 :只获取第10行
如果命令含有了管道|,管道左右会启动子进程执行命令;
ps:$$ 优先级 > 管道优先级 > $BASEPID 优先级 ,影响进程间变量的读取
int 0x80 :int cpu 指令,0x80是16进制数字128,数字对应的是终端描述符
涉及命令
ifconfig:informationConfig 查看IP地址
df -h:disk file 磁盘空间占用情况
mount:挂载系统外的文件。 /dev/hda1 /boot :将 /dev/hda1 挂在 /boot 之下。可以增加其他配置项
umount:卸除目前挂在Linux目录中的文件系统。
stat:以文字的格式来显示inode的内容。查看 testfile 文件的inode内容内容,命令: stat testfile
ln:ln demo.txt target.txt ,demo.txt映射到target.txt,是硬链接,两个文件的inode号是一样的;ln -s demo.txt target.txt ,demo.txt映射到target.txt,是弱链接两个文件的inode号是不一样的;
whereis bash:查看bash路径
echo $$ :可以打印出当前进程号
chroot:把根目录换成指定的目的目录。例:chroot /mnt/ls
lsof:(list open files)是一个列出当前系统打开文件fd的工具,默认是所有进程的,lsof -p 8341 指定进程
pstree:查看进程的父子关系
ps -ef | grep java :ps=process根据关键字筛选进程
command > /dev/null :/dev/null 是一个特殊的文件,写入到它的内容都会被丢弃
二、 整体流程图
读取数据流程图1:
程序有缓冲区、内核有缓冲区、硬盘有缓冲区。
执行程序编译后的字节码文件时,会把字节码对应成cpu的命令,cpu根据指令保护现场切换到内核态,调取kernel的方法,cpu再通过内核的调度从硬盘读取数据(中间通过fd找到打开文件记录,再到inode 记录)),每次的数据读取既可以都通过cpu的寄存器来传递,也可以通过DMA 就可以读写磁盘,直接从磁盘缓存区写入到内存pagecache,不必一直依赖cpu。
写数据:程序缓冲区>kernel pagecache >硬盘缓冲区 >硬盘
读数据:硬盘>硬盘缓冲区 >kernel pagecache >程序缓冲区
读取数据流程图2:
在图一的过程中还涉及图2的一个细节流程,如果kernel pagecache没有想要的数据,做缺页处理,app1进程保护现场挂起,cpu交给dma去处理从磁盘往kernel写数据的事儿,等数据准备完毕了,dma发起一个中断,告诉cpu数据准备完毕,你可以继续干活了,cpu就可以恢复现场,程序继续运行。
后续更精彩… …