Linux内核的I/O机制是指用于处理输入输出操作的一系列技术和算法。这些机制和技术共同构成了Linux内核的I/O机制,提供了丰富而灵活的输入输出功能,满足不同应用场景下的需求。在业务执行过程中,常伴随大量的IO操作,如果IO操作和CPU消耗不能合理安排,将会导致整体业务执行效率低下,用户体验极差。比如手机启动过程,有大量CPU消耗和IO操作。
在Linux中,I/O机制主要包括以下几个方面:
- 文件系统:Linux使用文件系统作为对外提供数据存储和访问的接口。文件系统可以是基于磁盘的,也可以是虚拟的,如procfs、sysfs等。通过文件系统,应用程序可以通过读写文件来进行输入输出操作。
- 文件描述符:在Linux中,每个打开的文件都会分配一个唯一的整数标识符,称为文件描述符(file descriptor)。应用程序可以使用文件描述符进行对文件的读写操作。
- 阻塞I/O和非阻塞I/O:在进行I/O操作时,可以选择阻塞或非阻塞模式。阻塞I/O会使调用进程在完成I/O操作之前被挂起,而非阻塞I/O则会立即返回,在数据未准备好时可能返回一个错误或特殊值。
- 异步I/O:异步I/O是指应用程序发起一个读/写请求后不需要等待其完成就可以继续执行其他任务。当请求完成时,内核会通知应用程序并将数据复制到指定缓冲区中。
- 多路复用:多路复用是一种同时监控多个输入源(例如套接字)是否有数据可读/可写的机制。常见的多路复用技术有select、poll和epoll。
用Bootchart记录android启动过程的CPU/IO消耗如下图:
Systemd readahead:
Systemd readahead-collect.service搜集系统启动过程中的文件访问信息,Systemd
readahead-replay.service在后续启动过程中完成回放,即将IO操作与CPU并行;
提高效率的一个宗旨,把CPU和IO的交替等,变为CPU和IO操作(不需要CPU参与)同时工作,充分利用系统资源,为解决CPU/IO并行问题,Linux提供了很多IO模型。
一、I/O模型
1.1阻塞与非阻塞
(1)阻塞: 一般来说,进程阻塞,等待IO条件满足才返回;
有个例外,阻塞可以被信号打断;
若设置信号标记,act.sa_flags |= SA_RESTART,接收信号,read阻塞不返回,但是信号响应函数还是会调用;相当于系统自动重新进入阻塞。
用signal()函数设置信号,其调用sigaction自动设置SA_RESTART
(2)非阻塞
read/write等IO调用,IO设备没就绪,立即返回,实际工程上用的不多;
1.2多路复用
实际业务中,一般有多个IO请求,每个请求响应都用简单的阻塞模型效率太低,Linux提供了多路复用的的系统调用:
(1) select
select()处理流程
- a.告诉系统,要关注哪些IO请求;
- b.阻塞等待,直到有IO就绪,select返回;
- c.主动查询是哪个IO就绪,然后响应该IO;
- d.重新关注新的IO请求;
当IO请求过多时,这种查询的方式也很浪费资源,因此Linux提供了一个新的系统调用
(2)epoll()
epoll与select的不同:
- a.将注册IO请求和等待事件触发分离开;
- b.返回后,直接告诉哪些IO就绪,不用再主动查询;
当IO数量不多时,可以用select或epoll,但当IO非常多时,比如大型网络应用,响应多个IO请求时,用epoll效率远高于select。
signal io方式,都是read/write阻塞,底层实现,待IO就绪后,内核发送信号,唤醒阻塞;
比如读触摸屏应用,read被阻塞,只有触摸屏被按下,触发中断程序响应,读取触摸屏行为数据后,内核发送信号唤醒APP的等待,APP读到触摸动作信息,做相应业务处理。
目前工程上,处理异步I/O更多用以下方法:
1.3异步IO
(1) C库提供的Glibc-AIO:Glibc-AIO原理,aio_read()立即返回,后台自动创建线程读取io,aio_suspend()查询IO是否完成,完成立即返回,未完成,等待;
(2) 内核提供的Kernel-AIO:一般用来读取硬盘数据,比如数据库读取;
这些异步模型,天然的将IO与CPU消耗等待做并行处理;
1.4Libevent事件触发
功能类似QT/VC的按钮,注册回调函数,当事件触发时,执行回调函数。
libevent是一个跨平台库,封装底层平台调用,提供统一API。Windows/Solaris/linux。
gcc xxx.c -levent
模型对比:
【文章福利】小编推荐自己的Linux内核技术交流群:【 865977150】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!
资料直通车:最新Linux内核源码资料文档+视频资料
内核学习地址:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈
二、EXT文件系统
一切都是文件,Linux通过VFS中间层,支持多种文件系统,对APP统一接口,文件系统的本质是将用户数据和元数据(管理数据的数据),组织成有序的目录结构。
2.1EXT2文件系统总体存储布局
一个磁盘可以划为多个分区,每个分区必须先用格式化工具(某种mkfs)格式化成某种格式的文件系统,然后才能存储文件,格式化的过程会在磁盘上写一些管理存储布局信息。
一个典型的ext格式化文件系统存储布局如下:
启动块(BootBlock):大小1K,是由PC标准规定的,用来存储磁盘分区信息和启动信息,任何文件系统都不能使用启动块。启动块之后才是EXT文件系统的开始;
超级块(Superblock):描述整个分区的文件系统信息,比如块大小,文件系统版本号,上次mount时间等;
超级块在每个块组的开头都有一份拷贝;
块组描述符表(GDT, Group Descriptor Table)
由很多块组描述符(Group Descriptor) 组成,整个分区分成多少个块组就对应多少个GD;
每个GD存储一个块组的描述信息,比如这个块组从哪里开始是inode表,哪里开始是数据块,空闲的inode和数据块还有多少个等。
和超级块类似,GDT在每个块组开头也有一份拷贝;
块位图(Block Bitmap)
用来描述整个块组中那些块空闲,本身占用一个块,每个bit代表本块组的一个块,bit为1表示对应块被占用,0表示空闲;
tips
df命令统计整个磁盘空间非常快,因为只需要查看每个块组的块位图即可;
du命令查看一个较大目录会很慢,因为需要搜索整个目录的所有文件;
inode位图(inode Bitmap)
和块位图类似,本身占用一个块,每个bit表示一个inode是否空闲可用;
inode表(inode Table)
每个文件对应一个inode,用来描述文件类型,权限,大小,创建/修改/访问时间等信息;
一个块组中的所有inode组成了inode表;
Inode表占用多少个块,格式化时就要确定,mke2fs工具默认策略是每8K分配一个inode。
就是说当文件全部是8K时,inode表会充分利用,当文件过大,inode表会浪费,文件过小,inode不够用;
硬链接指向同一个inode;
数据块(Data Block)
a.常规文件:
文件的数据存储在数据块中;
b.目录
该目录下所有文件名和目录名存储在数据块中;
文件名保存在目录的数据块中,ls –l看到的其他信息保存在该文件的inode中;
目录也是文件,是一种特殊类型的文件;
c.符号链接
如果目标路径名较短,直接保存在inode中以便查找,如果过长,分配一个数据块保存。
d.设备文件、FIFO和socket等特殊文件
没有数据块,设备文件的主,次设备号保存在inode中。
2.2实例解析文件系统结构:
用一个文件来模拟一个磁盘;
1.创建一个1M文件,内容全是0
dd if=/dev/zero of=fs count=256 bs=4k
2.对文件fs格式化
格式化后的fs文件大小依然是1M,但内容已经不是全零。
3.用dumpe2fs工具查看这个分区的超级块和块组描述表信息
(base) leon\@pc:\~/nfs/linux\$ dumpe2fs fs
dumpe2fs 1.42.13 (17-May-2015) Filesystem volume name: <none> Last mounted on: <not available> Filesystem UUID: a00715b2-528b-4ca6-8c2b-953389a5ab00 Filesystem magic number: 0xEF53 Filesystem revision #: 1 (dynamic) Filesystem features: ext_attr resize_inode dir_index filetype sparse_super large_file Filesystem flags: signed_directory_hash Default mount options: user_xattr acl Filesystem state: clean Errors behavior: Continue Filesystem OS type: Linux Inode count: 128 Block count: 1024 Reserved block count: 51 Free blocks: 986 Free inodes: 117 First block: 1 Block size: 1024 Fragment size: 1024 Reserved GDT blocks: 3 Blocks per group: 8192 Fragments per group: 8192 Inodes per group: 128 Inode blocks per group: 16 Filesystem created: Fri Aug 21 16:48:02 2020 Last mount time: n/a Last write time: Fri Aug 21 16:48:02 2020 Mount count: 0 Maximum mount count: -1 Last checked: Fri Aug 21 16:48:02 2020 Check interval: 0 (<none>) Reserved blocks uid: 0 (user root) Reserved blocks gid: 0 (group root) First inode: 11 Inode size: 128 Default directory hash: half_md4 Directory Hash Seed: e5c519af-d42e-43b5-bc8d-c67c5a79bcbe Group 0: (Blocks 1-1023) 主 superblock at 1, Group descriptors at 2-2 保留的GDT块位于 3-5 Block bitmap at 6 (+5), Inode bitmap at 7 (+6) Inode表位于 8-23 (+7) 986 free blocks, 117 free inodes, 2 directories 可用块数: 38-1023 可用inode数: 12-128 (base) leon\@pc:\~/nfs/linux\$
块大小1024字节,一共1024个块,第0块是启动块,第一个块开始才是EXT2文件系统,
Group0占据1到1023个块,共1023个块。
超级块在块1,GDT2,预留3-5,
块位图在块6,占用一个块,1024x8=8192bit,足够表示1023个块,只需一个块就够了;
inode bitmap在块7
inode表在8-23,占用16个块,默认每8K对应一个inode,共1M/8K=128个inode。每个inode占用128字节,128x128=16k
4用普通文件制作的文件系统也可以像磁盘分区一样mount到某个目录
$ sudo mount -o loop fs /mnt/
-o loop选项告诉mount这是一个常规文件,不是块设备,mount会把它的数据当作分区格式来解释;
文件系统格式化后,在根目录自动生成三个字目录: ., …, lost+found
Lost+found目录由e2fsck工具使用,如果在检查磁盘时发生错误,就把有错误的块挂在这个目录下。
现在可以在/mnt 读写文件,umount卸载后,确保所有改动都保存在fs文件中了。
5.解读fs二进制文件
od –tx1 –Ax fs
开头行表示省略全零数据。
000000开始的1KB是启动块,由于不是真正的磁盘,这里全零;
000400到0007ff是1KB的超级块,对照dumpe2fs输出信息对比如下:
超级块:
Ext2各字段按小端存储。
块组描述符
整个文件系统1M,每个块1KB,一共1024个块,除了启动块0,其他1-1023全部属于group0.
Block1是超级块,块位图Block6,inode位图Block7,inode表从Block8开始,由于超级块中指出每个块组有128inode,每个inode大小128字节,因此共占用16个块(8-23)从Block24开始就是数据块。
查看块位图,6x1024=0x1800
前37位(ff ff ff ff 1f)已经被占用,空闲块是连续的Block38-1023=986 free blocks
查看inode位图,7x1024=0x1c00
已用11个inode中,前10个inode是被ext2文件系统保留的,其中第二个inode是根目录,第11个inode是lost+found目录。块组描述符也指出该组有两个目录,就是根目录和lost+found目录。
解析根目录的inode,地址Block8*1024+inode2(1*128)=0x2080
st_mode用八进制表示,包含了文件类型和权限,最高位4表示为文件类型目录,755表示权限,size是大小,说明根目录现在只有一个块。Links=3表示根目录有三个硬链接,分别是根目录下的
和lost+found字目录下的,这里的Blockcount是以512字节为一个块统计的,磁盘最小读写单位一个扇区(Sector)512字节,而不是格式化文件系统时指定的块大小。所以Blockcount是磁盘的物理块数量,而不是分区的逻辑块数量。
根据上图Block[0]=24块,在文件系统中24x1024字节=0x6000,从od命令查找0x6000地址
目录数据块由许多不定长记录组成,每条记录描述该目录下的一个文件;
记录1,inode号为2,就是根目录本身,记录长12字节,文件名长度1(“.”),类型2;
记录2,inode号为2,也是根目录本身,记录长12字节,文件名长度(“…”),类型2;
记录3,inode号为11,记录长1000字节,文件名长度(”lost+found”),类型2;
debugfs命令,不需要mount就可以查看这个文件系统的信息
debugfs fs
stat / cd ls 等
将fs挂载,在根目录创建一个hello.txt文件,写入内容”hello fs!”,重新解析根目录
查看块位图
可见前38bit被占用,第38块地址38x1027=0x9800
查看inode位图,7x1024=0x1c00
由图知,用掉了12个inode
查看根目录的数据块内容
debug fs查看t.txt属性 stat t.txt
t.txt文件inode号=12
Inode12的地址=Block8*1024+inode12(11*128)=0x2580
查看t.txt的inode
文件大小10字节=stlen(“hello fs!”),数据块地址0x26x1024 = 0x9800
查看内容
2.3数据块寻址
如果一个文件很大,有多个数据块,这些块可能不是连续存放的,那如何寻址所有块呢?
在上面根目录数据块是通过inode的索引项Blocks[0]找到的,实际上这样的索引项一共有15个,从Blocks[0]到Blocks[14],每个索引项占4字节,前12个索引项都表示块编号,例如上面Blocks[0]保存块24,如果块大小是1KB,这样就可以表示12KB的文件,剩下的三个索引项Blocks[12]~
Blocks[14],如果也这么用,就只能表示最大15KB文件,这远远不够。实际上剩下的这3个索引项都是间接索引,Blocks[12]所指向的间接寻址块(Indirect Block),其中存放类似Blocks[0]这种索引,再由索引项指向数据块。假设块大小是b,那么一级间接寻址加上之前的12个索引项,最大可寻址b/4+12个数据块=1024/4+12=268KB的文件。
同理Blocks[13]作为二级寻址,最大可寻址(b/4)*(b/4)+12=64.26MB,Blocks[14]作为三级寻址,最大可寻址(b/4)*(b/4) *(b/4)+12=16.06GB
可见,这种寻址方式对于访问不超过12数据块的小文件,是非常快的。访问任意数据只需要两次读盘操作,一次读Inode,一次读数据块。
而访问大文件数据最多需要5次读盘操作,inode,一级寻址块、二级寻址块、三级寻址块、数据块。
实际上磁盘中的inode和数据块往往会被内核缓存,读大文件的效率也不会太低。
在EXT4,支持Extents,其描述连续数据块的方式,可以节省元数据空间。