Showing Posts From
Linux
- 25 Oct, 2024
对Linux的UBI以及UBIFS文件系统的学习总结
以两篇笔记的篇幅整理一下在Linux系统中UBI以及UBIFS文件系统的概念,结构,应用中的流程以及使用中应注意的问题。 UBI/UBIFS的概念及其在Linux文件系统中的层次架构 UBI:unsorted block images。 UBIFS UBI是建立在Linux MTD闪存文件系统结构上的一个抽象层,其对于闪存的读写访问仍然要依赖于MTD层,但是又不是一个独立的可以直接给应用层使用的文件系统。UBI为了方便应用层文件系统对于闪存的高效使用,把闪存介质在使用中普遍存在的坏块管理、磨损平衡等方面的问题处理好,并通过逻辑存储块和物理存储块的映射,向其更上层通过逻辑存储块提供一个理想的存储空间管理接口。 而UBIFS则是一个基于UBI的文件系统,能够直接在应用层调用进行文件的创建和读写操作。UBIFS是基于UBI抽象层之上,也就是说UBIFS虽然是一种文件系统,但是并不像JFFS2、YAFFS这类文件系统一样直接工作在MTD块设备上,而是工作在UBI层所创建的一个卷(volume)上。 因此,UBI和UBIFS是相关但是完全不同的两个概念,UBI只是在传统的Linux针对闪存读写的MTD结构上的一个虚拟层,封装了部分闪存读写管理相关的通用问题解决方案,但是并不是一个可以直接提供给应用层访问的文件系统;而UBIFS则是在UBI的基础上,所实现的一个可以由用户空间访问的文件系统,UBIFS必须建立在UBI的基础之上。 因此,Linux上的MTD、各种不同类型的文件系统以及UBI/UBIFS之间的关系如下图所示: !image.png - MTD层提供了对底层闪存硬件的读写操作的通用接口。 - 在MTD层之上运行的UBI则提供了针对闪存器件尤其是NAND Flash操作的磨损平衡、坏块管理等处理,通过逻辑块和物理块映射的操作,为其更上层的文件系统层提供一个理想的存储访问介面。 - UBIFS则是基于UBI的一种文件系统,本身没有磨损平衡、坏块管理等方面的功能,因此不适合直接放在MTD层次上使用。UBIFS就是一个可以在用户空间访问的文件系统。 此外,UBI层上可以支持多个逻辑卷,也就是说一个UBI空间可以支持多个逻辑分区,每个逻辑分区都是一个独立的UBIFS文件系统。在UBI上的逻辑卷甚至可以支持动态创建删除以及动态调整各个逻辑卷大小的功能,这些都得益于UBI给上层文件系统提供了一个虚拟逻辑块组成的理想存储介面。 !image.png UBI Header 如上所述,UBI是基于MTD层所提供的底层闪存硬件标准化读写接口来实现的,那么在UBI层次上看到的就是MTD所提供的物理闪存块和闪存页。为了能够方便UBI层锁定的功能的实现,UBI在进行格式化的时候会向每个闪存块写入UBI Header来标记每个块的状态信息,例如这个Header中记录了这个块被擦除的次数,基于此了解到这个块的磨损状态,从而在后续执行磨损平衡的过程中始终保证所有的闪存块都得到大致相同的擦写次数,延长闪存的使用寿命。 在每个闪存Block头部写入的UBI Header主要是两个Header结构:EC Header(主要包含各个块的擦写次数)和VID Header(主要包含这个闪存块与UBI上逻辑卷之间的关系),每个Header结构都是64字节长度,每个Header占一个Page,这样的话在UBI格式化之后,每个闪存块的前两个Page分别用于保存EC Header和VID Header,实际数据部分从第三个闪存页开始,每个闪存块整体可用于存储实际数据的空间会比闪存块本身小两个page的大小。 !1729905954108.png UBI的运行状态表 除了UBI在Flash上的每个erase block的前两个存储页里面写入EC Header和VID Header信息以外,UBI的运行还要依赖于三张表: - Volume Table,卷表,其中记录着在这个UBI空间上创建的所有卷的名字、类型、id、卷空间的大小、卷校验值等信息。Volume Table保存在Flash空间中,UBI驱动初始化的时候从Flash读出来。因为这个信息对于UBI的卷管理非常关键,为了应对在读写这个Volume Table的时候发生掉电的情况,在Flash上实际上保存着两份Volume Table,其中一份作为备份。这个Volume Table仅仅被UBI用于进行卷管理,在用户空间是看不到的。 - EC Table,也就是各个erase block的擦除计数表,记录了每个erase block的擦除计数,主要用于UBI执行Flash的磨损平衡算法。如前所示,因为每个Erase Block都会在第一个page写入自己的擦除次数信息,因此这个EC table实际上是UBI启动初始化的过程中,对所有的flash erase block进行扫描,读取每个block的擦除次数,统计在这个RAM中运行的EC table中,此后的磨损平衡算法依赖于这个EC Table选择合适的erase block写入新数据。 - Erase Block Associate Table,逻辑内存块和物理内存块的映射关系表。类似于EC Table,这个表同样是基于Flash各个Block中记录的UBI VID Header信息解析出来的,运行在RAM中。 总结起来,在UBI的正常运行所需要的三张表中,Volume Table是记录在Flash中的,初始化的时候从Flash中读出即可;而EC Table和Erase Block Associate Table这两张表都是在UBI驱动初始化时从Flash的所有Erase block中读取EC Header和VID Header信息,通过对这些信息解析出来的。 UBI工具:Ubiformat/Ubiattach/Ubimkvol 正是因为包含在UBI中的所有闪存存储块都需要提前写入EC Header和VID Header(VID Header是在这个存储块映射到UBIFS的逻辑存储块时写入信息),所以UBI在使用之前先必须调用ubiformat进行格式化。也正因为EC Header等信息的存在,不能使用nandwrite这类工具直接写入UBI中包含的内存卡,而应该是时使用UBI操作的专门工具才行,否则会把EC Header信息擦去。 !image.png 对MTD分区使用ubiformat命令进行以上格式化以后,接下来就可以使用ubiattach命令把这个MTD分区绑定到UBI系统中,为应用层下一步挂载文件系统提供UBI卷的准备: !image.png 经过以上的ubiattach绑定操作(实际上就是告诉Kenerl,这个MTD2分区是一个UBI镜像,按照UBI镜像)后,Kernel会在/dev下创建与这个UBI Image对应的设备文件/dev/ubi0: !image.png 接下来,因为一个UBI image上可以创建多个UBI Volume,而每个UBI Volume上可以运行一个UBIFS的文件系统。所以可以进一步在UBI Image对应的/dev/ubi0设备文件上使用ubimkvol命令创建多个UBI Volume,并且把每个UBI Volume按照UBIFS文件系统挂载起来使用: !image.png 关于UBIFS文件系统的内容在另外一篇笔记中整理。 如果需要把UBI Image从系统中解绑掉,则应该首先umount已经挂载到kernel上的UBIFS文件系统,然后调用ubidetach命令执行解绑动作:ubidetach -p /dev/mtd2。 参考资料 - https://v.flomoapp.com/mine/?memo_id=NjY1ODgxMw - 文件系统(十):一文看懂 UBI 文件系统 - liwen01 - 博客园 - 《固态存储:原理、架构与数据安全》 --- 在前一篇文章中整理了UBI部分相关的一些技术细节,下一部分主要总结在UBI基础上所运行的UBIFS文件系统的相关内容。 UBIFS介绍 UBIFS:Unsorted Block Images File System。UBIFS是一种被设计用于运行在UBI之上的文件系统,因为UBIFS本身并不具备坏块处理、Flash Block的磨损平衡等方面的特性,所以UBIFS一定是要运行在创建于MTD分区之上的UBI层的。对于UBIFS而言,看到的Flash空间实际上就是已经通过UBI处理好坏块标记、磨损平衡等问题,并通过虚拟逻辑块与物理擦除块之间映射出来的理想Flash空间。 UBIFS的诞生,主要是为了解决之前一代的Flash文件系统,如JFFS2、YAFFS等在NAND Flash存储容量日益增大导致的文件系统加载速度慢、效率低等方面的问题。 - 例如JFFS2文件系统在启动过程在mount的时候,会读出这个JFFS2分区所在flash的所有内容,尝试构建出来完整的文件系统,这个过程与flash的大小线性相关,flash越大所需要的mount时间越长,因为JFFS2不适合于那些占用空间比较大的Flash分区。令外JFFS2文件系统也没有write back的机制,每次对JFFS2文件系统的写入都会直接写入Flash,这样就会导致Flash IO访问的效率比较低,而具备这个write back机制的UBIFS文件系统,在应用层写入UBIFS文件系统的操作会先写入内存,等到一定触发条件后才会一次性的写入flash,这样的IO访问效率会高很多,当然也会存在Flash突然掉电或者重启数据不完整的情况,这样就需要在进行关键数据写入时通过sync、fsync等操作强制把buffer的数据写入Flash。 UBIFS文件系统的优势: - UBIFS借助于UBI层处于与MTD器件之间的操作,UBIFS文件系统的整个目录结构保存在flash中,这样在mount的时候就不需要把所有数据全部读出来重新建立文件系统的目录结构,因此mount操作的时间比较短而且不会随着文件系统的大小发生变化。 - 但是需要注意,在UBIFS文件系统mount执行要先在MTD block上执行ubiattach的动作来对ubi image进行初始化,这个过程中会把每个bloack前两个page中的EC Header以及VID Header读出来,因此ubiattach所需要的时间是与这个ubi image所占的flash空间大小线性相关的。 - UBIFS由write back方面的支持,可有效提升Flash IO操作的效率。 - UBIFS支持日志Journal结构,可有效减少数据损失的风险并提高写入效率。 - UBIFS可以支持对文件系统内容进行压缩处理,节省文件系统所需要的存储空间。 UBIFS的使用 对于UBIFS文件系统的使用而言,主要有两种方式: - 使用mkfs.ubifs基于当前已有的目录结构及其内容制作UBIFS文件系统镜像;使用ubinize命令把这个UBIFS文件系统镜像打包到一个UBI镜像中;然后使用ubiformat命令(不能用nandwrite命令写ubi image到flash中)把这个UBI镜像烧写到指定的MTD分区中。此后就可以把这个UBI镜像上运行的UBIFS文件系统mount到Kernel开始读写操作了。 - 使用ubiformat命令把指定的MTD分区格式化为一个UBI image对应的分区;使用ubiattach命令把这个UBI image挂载到kernel中;在这个已挂载UBI镜像分区上使用mkubivol命令创建一个UBI volume;然后使用mount命令把这个新创建的UBI Volume挂载到系统的某个挂载点上。此后就可以在应用层像使用普通文件系统一样在这个文件系统中进行读写操作了。 一般而言,对于量产产品的开发而言,基本上都是提前把文件系统在Host主机上做好打包成烧写镜像写入flash,因此UBIFS文件系统的使用流程主要会采用第一种方式。 1. mkfs.ubifs制作UBIFS文件系统的镜像 使用mkfs.ubifs制作UBIFS文件系统镜像的前提,是要先准备好一个目录其中包含有文件系统的具体内容,以及一些flash和UBI操作的参数。 mkfs.ubifs命令的使用参数如下: - -r :使用-r参数传递要打包UBIFS文件系统镜像的本地目录。 - -m :读写flash的最小IO大小,实际上就是flash一个读写page的大小。具体要查所使用的flash的数据手册。 - -e :UBI层所释放出来的逻辑虚拟块的大小。需要注意逻辑块和物理块的大小是不一样的,因为每个物理块在UBI层中前两个page都会写入header信息,所以逻辑块的大小就是物理块的大小减去两个page size。 - -c :即这个UBIFS文件系统需要占用的最大空间所对应的erase block的数量,可以使用这个UBIFS文件系统的大小除以Flash物理块的大小得到。 - -o :输出的UBIFS文件系统镜像文件名称 2. ubinize创建UBI镜像 下一步要使用ubinize工具,基于第一步生成的UBIFS文件系统的镜像文件,配合其配置文件,进一步生成UBI镜像文件。最终要烧写到flash中的镜像文件不是UBIFS的镜像文件,而应该是UBI镜像文件。 需要在UBI镜像生成的配置文件中指定这个UBI镜像中所包含的UBIFS文件系统镜大小、名称、ID、UBIFS镜像文件等信息: - mode:固定为ubi - image:指定UBIFS文件系统镜像文件的名称 - vol_id:这个文件系统在UBI镜像上的卷ID - vol_name:这个文件系统在UBI镜像上的卷名称 - vol_size:这个文件系统的大小。整个配置文件中可以有一个卷设置为autoresize。 - vol_type:静态只读还是动态可修改。 UBI镜像的配置文件实际上是一个ini文件,下面是一个例子: 以上配置文件中包含了两个UBIFS镜像对应的UBI卷的配置信息,分别是configuration-data-volume和rootfs-volume,每个卷的下方指定了这个文件系统的大小、镜像文件名称、卷ID、卷名称等信息。后续ubinize命令的执行就是依赖于对这个文件进行解析进行的。 以上配置文件准备好以后,接下来就是调用ubinize命令来生成UBI镜像文件: - -p :flash物理块的大小,从flash的数据手册上可以查到。 - -m :跟mkfs.ubifs命令的-m参数一样,实际上是flash的page size。可以从flash的数据手册上查到。 - -s :如果flash支持sub page的话,sub-page size就是sub page的大小,否则就是page size。 - -o :指定生成的UBI镜像文件的名称。 - :ubinize命令执行所需要的ini配置文件。 3. ubiformat烧写UBI image到MTD分区中 下一步就是使用ubiformat命令把以上生成的UBI image烧写到MTD分区之中。 - -f :使用-f指定要写入flash的ubi image文件。 - -s :如果flash支持sub page的话,就是sub page size,否则就是page size。 - -O vid_header_offset:VID Header在每一个block的offset,一般是一个page size。 4. UBIFS的使用 经过以上三步操作以后,自定义的UBIFS就已经成功烧写到flash中了,下一步就是对UBIFS的使用:先调用ubiattach把UBI镜像分区注册到kernel中,然后再mount UBIFS文件系统到系统中。 ubiattach: - -m :指定UBI分区在MTD上的索引 - -s :子页大小。 - -O vid_header_offset:VID Header在每个block中的偏移位置。 mount UBIFS文件系统: mount的这一步比较简单,UBIFS在mount时指定的volume descriptor可以支持两者方式,卷索引和卷名称: - ubi_ (e.g. ubi0_4) - ubi: (e.g. ubi0:rootfs) UBIFS文件系统的存储结构 如上所述,UBIFS是工作在UBI镜像的分区卷上的,因此对于UBIFS而言,它所看到的存储空间是由UBI所维护的一个理想的存储空间,这个存储空间以一系列的逻辑擦除块LEB构成,由UBI层来处理逻辑擦除块与Flash上的物理擦除块PEB之间的对应关系。因此UBIFS的数据存储结构,总是从逻辑擦除块空间的第一个块LEB0开始。 UBIFS中的节点概念 UBIFS文件系统中使用节点(Node)这个概念来描述这个文件系统的内容在Flash上存储所对应的不同类型的数据结构。节点是构成UBI文件系统的基本操作元素,可以说保存在UBIFS文件系统中的内容就是一个个不同类型的节点。 总体来说,UBIFS中的节点主要分为以下几种类型: - 索引节点(Index Node):对应于文件系统中所保存的文件和目录的元数据和属性信息,例如文件大小、权限、时间戳、所有者信息以及指向文件/目录内容数据块的指针。在UBIFS中,索引节点类似于Linux Kernel中的inode的概念。 - 数据节点(Data Node):数据节点中保存了文件系统中文件内容所对应的实际数据部分,每个文件的数据节点所在的位置通过其索引节点所包含的指针来访问和引用。 - 目录项节点(Directory Entry Node):用于保存目录中所包含文件/目录名以及对应的索引节点的编号,因此目录项节点用于在文件系统中组织和管理目录中的文件和子目录。 - Master节点(Master Node):保存在UBIFS文件系统地Master分区中,其中主要包含了文件系统的全局性信息,如最高的index node编号、提交编号、根索引节点的位置等。 - 垃圾收集节点(Gabage Collection Node):当UBIFS文件系统中的某个文件数据被修改,新的数据会被写入其他的LEB,之前的LEB中的数据因为过期应被擦除,但是UBIFS的实际操作中,这些过期的LEB并不会被立即擦除,而是标记为过期LEB,并且记录在垃圾收集节点中,后续在存储空间比较紧张的情况下,由垃圾回收机制基于垃圾收集节点中保存的内容进行统一地擦除、回收和重用。 - 孤儿节点(Orphan Node):当UBIFS文件系统中的一个文件被删除以后,它的索引节点编号(Index Node Number)会被保存在孤儿节点中。所有的Orphan节点保存在UBIFS文件系统结构中的Orphan分区中,如果发生了非正常卸载(例如擦除操作到一半的情况下卸载文件系统)的情况,下次mount这个文件系统的时候,就会去扫描孤儿分区中的孤儿节点,把其中保存的待删除文件的inode删除掉,保证文件系统操作的统一性。 - 日志节点(Journal Node):UBIFS作为一种日志型的文件系统,所有对其中文件内容的修改操作首先会写入日志节点,最后再作为原子操作提交到对主文件系统内容的修改。所以日志节点及其UBIFS存储结构中的日志分区就是用来保存这些修改操作的记录。 UBIFS存储结构 整体的存储结构上,UBIFS把其所在UBI分区卷分为6个区域: - Super Block分区,固定占用LEB0,对应于Linux Kernel中的struct ubifs_sb_node结构体,其中保存了这个UBIFS文件系统固定不变的参数,例如Flash擦除块的大小、读写页的大小、整个文件系统占用的LEB的总数量、文件系统的属性等信息。 - Master分区,固定占用LEB1和LEB2,用于保存每个索引节点的根节点,两个LEB的内容完全一样。相互备份确保在异常掉电的时候能够快速的恢复出来。Master分区对应于kernel中的struct ubifs_mst_node结构体。Master分区中所包含的重点信息有: - 提交号,commit number,每次向flash器件的修改提交都会更新这个commit number,由此来保证文件系统修改的一致性。 - 根索引节点的位置,即这个UBIFS文件系统根目录对应的索引节点在UBI镜像上所在的位置,包括其所在的LEB编号(root_lnum)和偏移量(root_offs),文件系统加载的时候可以通过这个信息找到根目录对应的索引节点,接入找到它下面包含的子目录和文件。 - Total Free、Total Dirty、Total Used、Total Dead、Total Dark,记录了文件系统中各种状态空间的总量,包括空闲空间、脏空间、已使用空间、已死亡空间和暗空间 - Journal分区从LEB3开始。Journal分区用于保存日志节点的内容,也就是保存所有对文件和目录内容的修改操作,以方便在异常情况下进行原子回滚操作。Journal分区的大小可以在mkfs.ubifs命令中指定,而其占据的LEB的数量在Super Block中的log_lebs参数定义。 - LEB Properties分区,也就是LEB属性区,用于保存这个UBIFS文件系统所包含的所有的LEB的占用情况 - Orphan分区用于保存所有的Orphan节点,其作用如上所述,用于在Orphan节点中缓存所有待删除文件的inode number,方便在发生删除操作进行的过程中出现异常情况后,操作系统下次mount文件系统的时候删除这些文件以保证文件系统结构的一致性。 - Main分区就用于保存文件系统中所有的实际内容,例如这个文件系统所包含的所有目录和文件的索引节点、目录项节点以及数据节点。 参考资料: - How to Use UBIFS | Timesys LinuxLink - 文件系统(十):一文看懂 UBI 文件系统 - liwen01 - 博客园
- 16 Oct, 2024
Linux的OOM错误及其处理思路
对于OOM错误的简介和总结 OOM,Out Of Memory,顾名思义,也就是Linux系统在运行中因为物理内存耗尽所出现的问题。 在Linux系统的应用程序运行过程中,应用程序向Kernel不断申请内存,Kernel的内存管理子系统,会找到物理内存中的空闲页面标记为被占用状态,并且在该应用程序的页表中建立这个物理内存页面与应用程序虚拟空间所申请的虚拟内存页面之间的关联关系。当应用程序需要使用的内存过多,Linux的内存管理子系统已经找不到空闲的物理内存页来满足应用程序的内存申请要求时,就会发生这个系统运行内存不足的问题(OOM,Out Of Memory)。 对于系统运行内存不足的问题,一般有几种处理方式: - 利用SWAP分区。在系统的硬盘中存在一个独立的SWAP分区,运行内存不足且kernel已经无法再释放内存的情况下,把暂时不用的内存页交换到这个硬盘分区上,以腾出内存用作它用。后续如果要用到这个缓存的内存页,再从SWAP分区上交换回来。这就是我们在安装Linux系统时需要单独划分一个SWAP分区的原因所在。 - Linux Kernel内部有一个OOM Killer的机制,开启后在运行内存不足的情况下,通过一定的算法找到最不重要、耗费内存最多的进程,直接kil掉以确保系统运行的基本稳定性。 - 对于嵌入式系统等专用功能的系统,以上的SWAP分区和OOM killer都不适用,标准的处理方式往往就是直接系统崩溃报出OOM的错误。 SWAP分区 对于包含SWAP分区的PC以及服务器端的系统而言,当系统的物理内存不足的时候,Linux Kernel还会尝试把一部分不常用的内存页缓存到SWAP分区中,如果后续应用程序的运行要用到这个内存页的话,再从SWAP分区重新读入到内存中,这就是内存页面SWAP交换的过程。当然因为涉及到物理内存页与硬盘之间的数据拷贝,硬盘的读写是很慢的,所以一旦应用中运行中涉及到内存页的交换,应用程序的执行速度是很慢的。 但是对于嵌入式Linux系统而言,绝大多数的情况下是不支持SWAP分区的。因此在嵌入式系统中一旦出现OOM的错误,标准的处理流程就是进程Crash以及系统崩溃,所以OOM对于嵌入式Linux系统的开发而言是非常严重的错误,需要审慎处理。 - 实际上绝大多数的Linux嵌入式系统都有一定的实时性要求,如果嵌入式系统也支持SWAP分区的话,内存数据交换过程中在物理内存页和硬盘/Flash等介质之间来回拷贝数据,执行速度很难满足这个实时性要求。而且嵌入式Linux系统一般都是专用系统,其上执行的应用程序是非常固定的,所以只需要针对这些专用的应用程序的执行,评估其运行所需要的内存需求,选择合适的内存大小即可。 - 嵌入式Linux系统上不适用SWAP分区的另外一个原因则是,嵌入式系统上一般使用Flash作为存储介质,Flash的存储块的大小一般远远大于内存页的大小(4KB),而标准的SWAP分区是以内存页为单位进行交换的,这样就注定SWAP分区在Flash上的使用不会有理想的性能。 - 当然,要在嵌入式系统上开启SWAP也不是没有可能,不过嵌入式系统上所使用的SWAP是一种基于RAM的压缩文件系统zram。大致的逻辑是,提前分配一片内存作为压缩内存SWAP系统zram,在系统运行内存不足的情况下,找到使用率比较低的内存页,压缩后放入这个zram文件系统中,后续如果要用到这个内存页的话,再从zram中解压缩后释放出来。zram中保存的内存页经过一定的压缩算法处理,在内存的使用上更节约,通过这种方式达到内存交换的目的。 OOM Killer OOM Killer是Linux Kernel的一个特性,其主要的作用,就是在发生系统内部不足的情况下,Kernel通过一定的算法找到最应该kill的应用程序,把这个应该程序kill掉,然后释放出来内存,避免系统的崩溃。 因此OOM Killer的触发条件就是:在当前应用程序运行的情况下,系统已经用光了所有的物理内存;系统已经无法再释放内存来保证系统运行的稳定性。 在kernel的运行中,当OOM Killer特性开启的情况下,kernel会根据多个维度的统计数据(当前系统中的进程数量,各个进程所占用的内存空间大小,进程本身的重要性,剩余内存的多少,用户设置的oom_score_adj参数等)计算出来每个进程的oom_score。每个进程都有一个oom_score,可以通过cat /proc//oom_score命令来查看,各个进程的oom_score的分数介于0-1000之间,这个分数越高,在遇到内存不足的情况下,OOM killer会首先选择最高oom_score的进程,kill掉这个进程以后把内存释放出来。所以对于服务器和PC上运行的Linux系统而言,就是利用这个机制在系统内存负载居高不下已经影响到系统运行本身稳定性的情况下,强制kill进程来释放内存。 当然对于以上的OOM killer机制,用户也可以认为介入,设置进程的重要性,来避免OOM killer运行的时候误杀重要的进程,这就是通过oom_score_adj参数来进行调节: - 同样的,每个进程的/proc//目录下也都有一个oom_score_adj文件可以设置这个参数,如果给这个文件写入一个正值就相当于是提升这个进程被kill掉的可能性,例如echo 100 > /proc//oom_score_adj命令相当于提高这个进程的oom_score,在内存不足的情况下这个进程就更可能会被kill掉;而echo -100 > /proc//oom_score_adj命令则相当于降低这个进程的oom_score,在内存不足的情况下这个进程就更不太可能会被kill掉。 - 如果要让进程在任何时候都不会被OOM killer杀掉:echo -1000 > /proc//oom_score_adj - 而如果要选择一个优先级最低的进程,让OOM发生的时候首先被kill掉来释放内存,可以:echo 1000 > /proc//oom_score_adj 可以利用以下脚本来实时的监控系统中各个进程的oom_score和oom_score_adj: 对于OOM问题在嵌入式系统中的理解 如上所述,一般而言,对于嵌入式Linux系统而言,都会根据其应用程序的执行,提前在系统设计与评估阶段仔细的评估该应用所需要的内存大小,然后选择与之对应的系统硬件配置,所以只要系统设计阶段不出现特别大的问题,一般而言,系统硬件所提供的内存容量是足够应用程序运行所使用的。但是如果在应用程序的实现中,出现一些bug导致对内存的异常使用(最典型的就是内存泄漏,申请内存后没有及时释放,导致累计效应),在应用程序的运行中对于内存的消耗不断增加,直到系统中所有的内存耗尽从而导致OOM的错误,这一点是在嵌入式Linux系统开发中所需要面对的重点问题。 关于SWAP分区及其类似方案在嵌入式Linux系统中的应用,以上已有详细总结,SWAP分区本身并不适合嵌入式系统所使用的Flash存储介质,而zram同样会导致交换过程带来的低效率操作,难以满足嵌入式系统的实时性处理要求。 OOM Killer的设计用于在系统内存不足,存在崩溃风险的情况下,从当前系统选择消耗内存较多的非关键进程,kill掉以后来尝试恢复内存的占用状况保证系统运行的基本稳定性。这一点对于嵌入式系统而言同样不适用,因为嵌入式系统一般都是长期运行的专用应用,在系统启动后运行起来的进程都是关键进程,kill掉以后就没有办法提供基本的功能特性,这个时候即使系统本身还没有崩溃,整个产品的实用性已经大打折扣了。所以具体的实现上,不太可能依赖于OOM killer来选择进程kill掉保证系统运行的稳定性。 因此,对于嵌入式系统所面对的OOM问题来讲,关键仍然在于两点: - 系统设计和方案平台选型针对产品需求的合理性,需要确保硬件规格在满足产品需求的前提下,在内存的使用上有一定的裕量来确保系统运行的稳定性,而不是依赖于一些不可控的手段。 - 应用程序设计上一定要尽最大可能避免内存泄漏等因素导致的软件bug,对内存占用的持续积累,最终造成物理内存不足的OOM问题。当然极端情况下,也有部分产品在设计上在产品运行一段时间后,自动重启一次来尝试避免长期运行情况下出现的内存方面的问题。 参考文档: - Linux OOM (Out-of-memory) Killer. let me first explain what is OOM | by Raza | Medium
- 12 Oct, 2024
对于Signal 11 SIGSEGV错误的简单总结
SIGSEGV问题的简介以及原因总结 Signal 11,也就是SIGSEGV(Signal Segmentation Violation),是Unix/Linux系统在应用程序运行中,遇到内存访问错误的情况下,向应用程序所发出的错误信号。一般是应用程序代码在运行中尝试访问和读写非法内存地址时,由系统内核向应用程序发出这个信号。这类错误一般发生在可以通过指针等访问底层内存地址的C/C++语言代码中,发生后可能会导致数据污染、系统崩溃或者其他不可预计的行为,应该是嵌入式C/C++应用开发过程中最普遍,但是也最严重的问题。 导致SIGSEGV的原因一般是: - 引用空指针或者野指针。如果指针在引用前,既没有进行正确的初始化和设置,也没有做必要的检查,直接引用这些指针就会导致程序代码对内存的访问处于失控的状态,报出SIGSEGV的错误。 - Buffer溢出。无论是在堆空间、栈空间还是全局变量空间的Buffer或者队列,当访问这些buffer超出其地址范围时,会导致内存buffer溢出的问题。 - 内存访问权限问题。各个应用程序运行过程中所分配的内存区域,以及其对应的虚拟内存页,都因其类型的不同有着不同的访问权限,最典型的就是代码段内存空间是只读的,如果向只读类型的内容空间尝试写入时,也会发生SIGSEGV的错误。 - 栈溢出。如果在子函数的内部栈空间上申请大量的内存,子函数在执行的过程中对于其栈空间内存的使用超过其限制后,发生栈溢出的问题,也会导致SIGSEGV的问题。 从Linux Kernel的角度看待SIGSEGV信号 对于Linux系统中运行的应用程序而言,每个进程都对应于一个独立的虚拟地址空间,其所占用的内存按照类型来区分,包含有多个段,例如代码段、数据段、堆空间、栈空间等。各个段又按照4KB为单位,分为很多个虚拟内存页,每个虚拟内存页都有自己的地址和权限设置。这些虚拟地址页再通过MMU以及内核中维护的各个进程的页表来与物理内存页一一对应。每个应用程序进程在kernel中都有自己的页表,保存着自己的虚拟内存空间与实际占用的物理内存页之间的对应关系。 那么对于Linux Kernel而言,什么时候会向一个进程发出SIGSEGV信号呢? 当应用程序对某个虚拟内存地址发出读写访问的请求时,这个请求会转到内核空间,此时: - Kernel会查询这个进程对应的页表,寻找与请求虚拟地址对应的页表及其权限,如果请求的虚拟地址页包含在页表中,并且访问权限匹配正确,则kernel会按照应用程序的请求访问指定的内存地址。如果权限不一致,kernel就会向应用程序发出一个SIGSEGV信号,表示这个内存访问的权限出错。 - 如果Kernel在页表中找不到与应用程序指定虚拟地址的虚拟地址页,则会发生page fault的错误。针对page fault会存在三种情况,分别由page fault handler进行处理: - Major Page Fault,也称为硬缺页错误(hard page fault),要访问的虚拟内存地址在物理内存中没有对应的页帧,这个时候需要从硬盘等设备中载入(例如读写硬盘上的文件到内存,或者把硬盘上保存的cache交换到内存中),然后再由MMU建立物理内存页与虚拟内存页之间的映射关系。因为要从硬盘上读入文件,这个处理过程当然就比较慢,一般是异步操作。 - Minor Page Fault,也称为软缺页错误(soft page fault),是指要访问的内存页不在当前进程对应的虚拟地址空间中,但是在物理内存中,这个时候就只需要MMU在当前进程的虚拟地址空间与物理地址空间建立关联关系即可,无需从硬盘读取内容,这个过程的处理一般是多个进程之间共享一部分物理内存空间(例如共享动态库)。因此这个过程的响应速度会比较快,一般是同步操作。 - 无论是Major Page Fault还是Minor Page Fault都属于正常操作,kernel不会像应用程序上报异常的SIGSEGV信号。 - page fault还有一种就完全是无效的缺页错误了。最典型的就是用户进程访问的内存地址越界所导致的非法内存访问、对空地址的引用,此时Kernel就会向应用程序上报一个SIGSEGV的信号,默认行为是终止应用程序运行。 调试SIGSEGV问题的思路 1. 编译开启所有的warning配置 首先一点,就是应该在编译应用程序的时候,养成开启所有的warning message,并解决所有导致warning message的应用程序代码问题。因为编译器在编译的过程中,如果产生了warning message,极大概率这个警告消息背后隐藏着一定的bug,即使这个bug短期内没有发现明显的测试问题,在某些应用逻辑的运行下,就有概率会导致出现一些几率性的问题,非常难以定位和解决。所以养成习惯去自动开启所有的warning message,并且提前解决掉这些问题,可以防患于未然,提前把可能导致问题的bug解决掉。 例如当使用gcc进行编译的时候,可以通过-Wall以及-Wextra等编译选项开启所有的warning message,这些错误在实践中一定都相对比较好解决: !image.png 2. 静态扫描分析 在开发流程中导入静态分析工具的思路与以上在编译过程中开启warning配置的思路一致,都是尝试在软件代码运行之前,想办法通过扫描和编译静态代码的过程中提前侦测可能的问题,并提前解决之。只不过,代码的静态扫描和分析工具完全是对静态源代码本身进行扫描,是在编译流程之前完成的工作;而编译过程中开启warning配置,则是利用编译工具在编译代码的过程中同步进行的代码扫描和分析,两者大同小异。 目前无论是开源还是商业的代码静态扫描分析工具,均已经非常成熟。例如针对嵌入式C、C++应用代码的扫描,开源的cppcheck,商业软件的Coverity、Fortify等工具在实践中都得到了非常普遍的应用,确实是可以很高效的提升团队开发的代码质量,提前解决掉导致测试问题的代码缺陷。 因此,在开发实践中,从开发质量和效率的提升角度考虑,一定要积极的把静态分析工具的扫描及其扫码问题的解决流程导入到代码提交的开发流程中。 3. 使用Valgrind进行动态内存分析 不同于cppcheck和Coverity这类静态代码扫描和分析工具,Valgrind是一种动态分析工具,也就意味着,Valgrind是在应用程序的运行中对其内存的使用情况进行监控和分析的:Valgrind会跟待分析的应用程序一起运行,在后台监控待分析应用程序的内存使用情况,更准确的给出内存出错的位置和原因。 当然,Valgrind的作用不仅仅是对内存的使用情况进行动态跟踪和分析,还可以对程序进行各种性能分析、代码覆盖测试、堆栈分析及CPU的Cache命中率、丢失率分析等,是一个非常强大的开源工具。 利用Valgrind的memcheck可以实现对应用程序运行中的内存覆盖、内存泄漏、内存越界检测等方面的检测。 Valgrind在嵌入式系统调试中的使用步骤大致如下: - 首先需要编译Valgrind。 - 使用Valgrind加上memcheck选项启动应用程序的运行:valgrind —tool=memcheck a.out 但是,对于valgrind在嵌入式系统中的应用而言,存在比较大的实用性的问题,尤其是考虑到大部分的嵌入式系统在某些环节的处理上都有实时或者一定时序的严格要求。这是因为Valgrind的运行要实时的监控应用程序的内存使用情况,会导致应用程序的运行速度比不开启valgrind慢20-30倍,这一点在绝大多数嵌入式应用程序的调试中是不可接受的。 所以,valgrind更大的用于之地还是在PC、服务器这类资源比较充裕的环境中,如果要使用valgrind对嵌入式应用程序的内存进行分析,比较现实的做法是对软件的架构做更合理的设计和实现,可以让部分软件模块运行在PC中,然后在PC中利用valgrind来针对这些软件模块做独立的单元测试或者模块级别的测试。 4. 借助于coredump 当Linux的coredump功能开启的时候,应用程序运行中出现各种异常或者严重bug导致程序退出的时候,Linux系统会把该程序运行时的内存、寄存器状态、堆栈指针、内存管理信息、各种函数的堆栈调用信息保存到一个core文件中。对这个core文件进行分析和跟踪就可以大致定位出来导致内存问题的具体原因。 首先需要开启core dump功能:ulimit -c unlimited。 然后运行自己的应用程序,当应用程序的内存错误导致程序退出的时候,就会在当前目录下自动产生一个core文件。然后再用gdb来分析这个core文件,在gdb的交互式环境中,通过bt命令查看程序退出之前的函数调用栈的信息,就可以大致的定位出来导致这个内存错误的具体位置和原因。 5. 检查Kernel Log定位问题 当发生段错误的时候,内核同步会在其kernel log buffer里面写一条日志,通过对这个日志进行分析也可以得到一些有用的信息,基于这些信息可以初步定位出来发生段错误的原因和大致的参考位置。kernel的运行log一般保存在/var/log/syslog里面。 - 当然要kernel的运行log要自动记录再/var/log/syslog这个文件里面,需要在后台运行一个rsyslog的服务,才能看到并利用这个文件对问题进行分析处理。 下面是一个发生段错误以后在kernel log文件中看到的信息: !image.png - at :表示应用层代码在发生段错误时所尝试访问的虚拟内存地址。 - ip :错误发生时,应用层程序代码在内存中执行的位置。 - sp :错误发生时的应用程序堆栈指针。 - error :错误发生时应用程序正在执行的内存操作类型。主要的类型包括以下: - 4:表示程序正在尝试读尚未分配的内存地址。 - 5:表示程序正在尝试读write-only的内存地址。 - 6:表示程序正在尝试写尚未分配的内存地址。 - 7:表示程序正在尝试写没有写权限的内存地址。 参考文档 - What is Signal 11 SIGSEGV Error? | phoenixNAP KB
- 21 Jun, 2024
BIOS/UEFI/MBR/GPT/GRUB/EFI等概念的总结
BIOS与UEFI BIOS:Basic Input Output System。 UEFI:Unified Extensible Firmware Interface。 简单总结起来,无论是BIOS还是UEFI,本质上就是计算机的bootloader,而且这个bootloader还不是放在硬盘上的,而是固化在计算机主板的CMOS或者EEPROM存储器上:这是当然的道理,否则使用一块新的硬盘或者对之前的硬盘格式化以后,bootloader就不存在了,计算机也就没法正常启动了,因此BIOS和UEFI这两个固件一定是保存在主板上的其他非易失性存储器中,这样的话在硬盘上无论如何折腾,至少计算机是能够基本自检和正常启动的。 - 从这一点上,BIOS/UEFI就类似于各种嵌入式处理器上的Rom Loader,而不是Uboot。Rom loader是固化在嵌入式处理器芯片内部的,芯片上电以后首先执行的就是Rom Loader的程序,Rom Loader负责把烧写在Flash中的Uboot加载到RAM中运行。 BIOS和UEFI实际上就是计算机上电以后执行的第一个程序,其主要的功能不外乎: - CPU本身的初始化中断向量的设置; - 对各种硬件外设进行的初始化; - 系统自检,跟据自检结果输出提示信息以及决定是否继续启动系统; - 加载并操作系统镜像或者第二阶段的Bootloader。 实际上,BIOS是自从PC诞生以后就成为默认的Bootloader,直到2010年以后逐渐被UEFI所替代。不过因为BIOS的使用时间很长,BIOS这个名字也已经深入人心,所以很多时候我们仍然使用BIOS来表示计算机的bootloader。 UEFI最终能够替代BIOS无非是因为BIOS的设计随着时代的发展已经跟不上潮流: - BIOS最高只能支持2TB的硬盘,而UEFI理论最高则能够支持到ZB级别。 - UEFI的启动速度更快,原因是BIOS必须运行在CPU的16位模式下,而UEFI可以工作在CPU的32位或者64位工作模式下,执行速度更快。 - BIOS最高只有1MB的程序运行空间,所以BIOS的功能就没法做的非常复杂和强大。例如BIOS无法支持UEFI所能够支持的GUI图形界面和鼠标操作。 - UEFI能够支持安全启动和网络启动,这两点都是BIOS所不具备的。 正因为以上原因,BIOS在2010年后开始逐渐被UEFI所替代,但是UEFI的功能与BIOS实际上是大同小异的。 另外一点值得注意的是,UEFI的设计上提供了对BIOS的兼容性,所以UEFI包含一个legacy的模式,如果设置UEFI使用Legacy模式的话,那么其执行流程就跟BIOS没有区别了。 !Untitled.png MBR与GPT MBR:Master Boot Record,硬盘主引导记录分区表 GPT:GUID Partition Table,全局唯一标识分区表 MBR和GPT的概念与硬盘的分区结构息息相关。无论是UEFI还是BIOS在启动的过程中,最终总是需要初始化硬盘以后,读取硬盘的分区结构,了解到硬盘是如何分区的,各个分区的大小和位置,操作系统启动分区在哪个地方。那么这样就要求硬盘需要按照BIOS或者UEFI的要求,把硬盘的分区信息按照BIOS/UEFI能够解析的结构放在固定的位置,这样BIOS/UEFI在启动的过程中才能顺利的找到硬盘分区信息,解析后加载操作系统正确的执行。 BIOS和UEFI这两种类型的bootloader,都分别定义了自己的硬盘分区信息结构,其中BIOS定义的硬盘分区结构就是MBR,而UEFI的规范中则定义了GPT结构。 MBR:与BIOS配合使用 MBR分区信息模式,会在硬盘的开头存放一个固定大小的MBR分区,这个分区就是硬盘主引导记录,其中定义了整个硬盘的分区结构和引导分区的位置。不过MBR分区规范中最高只能支持2TB大小的硬盘(因为BIOS只支持MBR分区模式,所以这就是为什么BIOS最大只能支持2TB硬盘的原因),并且最多只支持4个主分区或者3个主分区+1个扩展分区,如果需要更多的分区,可以在扩展分区中再创建逻辑分区。 在使用MBR方式进行硬盘分区管理的情况下,会使用硬盘0号扇区保存MBR信息。整个0号扇区总共512字节,分为引导程序、磁盘签名、硬盘分区表、结束标志四个部分。 !Untitled.png - 引导程序,占MBR分区的前面440个字节,负责加载并启动操作系统。 - 磁盘签名,4个字节,Windows格式化硬盘后写入的标签信息。 - 分区表,64个字节,保存了这个硬盘的分区信息。 - 结束标志,MBR分区的最后两个字节,固定位0x55,0xAA。 对于BIOS启动模式而已,BIOS程序负责加载硬盘0号扇区上的MBR记录,然后把CPU的控制权交给MBR引导代码,在MBR的引导代码中对分区表信息进行读取和解析,然后从引导分区读取操作系统镜像,并启动操作系统的运行。 GPT:与UEFI配合使用 GPT分区模式则由UEFI的规范所定义的硬盘分区结构。不同于BIOS所使用的MBR分区结构,GPT分区结构不限制分区的数量和分区大小,只不过Windows系统限制最大只能支持128个GPT分区。 相比于MBR分区结构只使用0号扇区存储整个分区信息,GPT的分区结构要更复杂一些: !Untitled.png - 保护MBR。GPT分区结构的开头第一个扇区是保护性MBR,用于跟传统的MBR分区结构兼容。 - GPT头。其中包含了签名信息,GPT头结构的大小,备份GPT头所在的逻辑块地址,GPT分区表所在的逻辑块地址和大小,分区的数量,每个分区表条目的大小,CRC校验值等信息。UEFI对GPT头结构信息读取和解析后即可找到GPT分区表所在的位置并解析分区表的信息。 - GPT分区表。这部分是GPT分区结构的核心部分,定义整个硬盘中的GPT分区的分布情况,各个分区的类型/GUID,分区的物理地址和大小,以及属性。 - 用户分区。这部分就是各个分区所占据的实际存储空间,分区的位置与大小在分区表中定义。 - 备份GPT分区表和GPT头。这部分实际上是GPT头和GPT分区表内容的备份,以防止GPT头和分区表损坏后系统无非恢复。这一点是比MBR更安全的地方,MBR是没有备份机制的,所以MBR的第一扇区主引导记录一旦被破环就无法恢复正常启动了。 值得注意的是,为了与UEFI配合起来使用,在GPT分区结构的用户分区中,必须包含一个实际上是FAT文件系统的ESP分区(EFI System Partition),这个分区中保存了真正用于加载和引导操作系统的二级bootloader文件。UEFI在启动运行的过程中,会读取和解析硬盘的GPT头以及GPT分区表,从而可以正确的识别这个ESP分区。UEFI通过分区表中的GUID来判断某个分区是否是ESP分区,ESP分区的GUID固定是:C12A7328-F81F-11D2-BA4B-00A0C93EC93B。当识别到ESP分区后,UEFI按照FAT类型文件系统进行加载,并读取其中的二级bootloader文件,例如\BOOT\BOOTX64.EFI,后续操作系统的引导过程由这个二级bootloader文件来负责。 Grub 如果独立安装Linux系统,或者在Windows系统的基础上再进一步安装Linux系统,并且实现双系统启动的话,在安装Linux系统的最后进行Grub的安装就是不可避免的了。 那么Grub到底在MBR和GPT的基础上做了什么事情,从而能够实现双系统甚至多系统的启动? BIOS+MBR结构 如前所述,在BIOS+MBR结构下,BIOS在启动运行的过程中,会把硬盘0号扇区的前446字节所包含的bootloader文件加载到内存中开始执行。所以这个446字节的bootloader就负责加载操作系统的镜像,在安装Windows系统以后,0号扇区的前446字节会被写入ntldr,由ntldr来引导和启动Windows系统;而如果安装的是Linux,使用Grub2作为bootloader的话,那么在安装Linux系统的最后阶段,则会把Grub2的stage 1 binary写入0号扇区的前446字节。 当然这个446字节的空间太小了,肯定不可能把Grub的所有功能全部包含进来,因此实际上Grub的启动分成了两个阶段: - Stage1:写入0号扇区的446字节是第一阶段,其作用就是用来找到和加载真正的Grub bootloader主程序,也就是位于操作系统启动分区的Grub2第二阶段的程序。而且受限于446字节的大小,这个阶段的stage1 binary是没法包含文件系统功能的,所以Grub2第二阶段的位置是直接写死在MBR的446字节程序中的。Grub2 Stage1的镜像对应于boot.img,安装时写入硬盘0号扇区. - Stage2:被加载Stage1加载后,解析/boot/grub2/grub.cfg配置文件,跟据该配置文件的定义,显示多系统的启动选择界面,或者直接加载Linux kernel和文件系统,然后就由Kernel来启动后续的过程。Grub2 Stage2的镜像对应于core.img,位置为/boot/grub2/i386-pc目录下。 !Untitled.png UEFI+GPT结构 如以上总结GPT分区结构部分的内容,UEFI firmware启动以后,会对GPT的分区结构进行解析,跟据专门的GUID找到一个特殊的ESP分区,这个分区类型实际上是一个FAT32文件系统,默认的系统启动器就是这个分区下的/efi/boot/bootx64.efi文件(32位的话是bootia32.efi)。 那么在UEFI+GPT分区结构的情况下,在安装Grub2到硬盘上的时候,实际上会在ESP分区下创建一个以操作系统名称命名的子目录(例如下面的ubuntu),在其中放入Grub2对应的efi文件和配置文件。同时会把Grub2的efi文件拷贝到/EFI/Boot目录下,并且重命名位bootx64.efi。这样下次UEFI启动的时候,找到ESP分区就从其Boot目录下找到bootx64.efi文件并运行以加载操作系统镜像或者提供多系统选择的界面,由用户选择需要启动的系统。 - 如上所述,实际上Grub2在ESP分区中的安装以及启动运行比以上更复杂一些。Grub2在ESP分区的/EFI/ubuntu下拷贝了多个efi文件,并且真正替换bootx64.efi文件的时其中的shimx64.efi。所以在安装Grub2以后,实际上UEFI最终首先会找到EFI/Boot下的bootx64.efi文件(实际上也就是shimx64.efi)启动运行,然后再链式加载运行EFI/ubuntu下面的grubx64.efi。以上过程涉及到Secure Boot的签名运算和安全校验的机制,启动过程中先运行由Microsoft签名shimx64.efi,再由shimx64.efi加载运行真正的grub镜像grubx64.efi文件。 参考资料 - BIOS与UEFI介绍与使用-腾讯云开发者社区-腾讯云 (tencent.com) - 详解MBR分区结构以及GPT分区结构-CSDN博客 - BIOS 与 UEFI 引导流程 - LARRY1024 - 博客园 (cnblogs.com) - Linux中GRUBX64.EFI和SHIMX64.EFI有什么区别?_ITPUB博客
- 07 Jul, 2020
Linux的磁盘加密技术LUKS
Linux系统的磁盘加密技术LUKS简介 - LUKS:Linux Unified Key Setup。 - LUKS是Linux系统为硬盘分区进行加密操作提供的一个标准,能够通用于各种Linux系统发布版本中。 - 使用LUKS技术对磁盘分区进行加密后,必须要先输入密码对其进行解密后才能把这个加密分区挂载到文件系统中进行访问,没有密码的情况下无法挂载,也就限制了在没有访问密码的情况下,即使把硬盘拿下来也是没有办法访问硬件中原有的数据,这样就保证了硬盘数据的安全性。 - 在Linux系统中使用LUKS技术对磁盘分区进行加密需要提前安装cryptsetup软件包。 - 使用cryptsetup对分区进行了加密后,这个分区就不再允许直接挂载。LUKS也是一种基于device mapper 机制的加密方案。如果要使用这个分区,必须对这个分区做一个映射,映射到/dev/mapper这个目录里去,我们只能挂载这个映射才能使用。然而做映射的时候是需要输入解密密码的。 - 使用LUKS和cryptsetup工具对磁盘分区进行加密的特点: - 加密后不能直接进行挂载,必须先使用cryptsetup命令+输入访问密码把这个加密分区的设备文件映射到device mapper下,然后才能把device mapper的映射文件挂载到文件系统中访问; - 加密后硬盘丢失也不用担心数据被盗,没有访问密码的情况下,无法正常的挂载到新的系统中; - 加密后必须先输入密码进行device mapper映射才能挂载成功并正常使用。 - 可以使用cryptsetup工具对一个磁盘分区设置多达8个访问密钥,并且可以在后期使用命令灵活的管理增加以及删除访问密钥; cryptsetup的基本使用 cryptsetup命令的常用参数包括: - luksFormat:对磁盘分区设置密码 - cryptsetup luksFormat [device file path] - 执行中需要设置一个访问密码,作为以后访问这个磁盘分区的密钥; - luksOpen:建立加密分区设备文件到/dev/mapper下的映射文件 - cryptsetup luksOpen [device file path] [mapper file name] - 执行中需要输入磁盘分区的密钥; - 执行成功后会在/dev/mapper下创建一个以[mapper file name]命名的设备映射文件; - status:查看映射设备文件的状态 - cryptsetup status [mapper file name] - 查看加密分区的挂载状态以及元信息; - luksClose:关闭之前在/dev/mapper下的映射文件 - cryptsetup luksClose [mapper file name] - 执行前需要先把这个分区umount掉,执行 luksClose命令完成后会删除在device mapper下创建的映射文件; - luksAddKey:为磁盘分区增加一个访问密钥 - cryptsetup luksAddKey [device file path] - 最多可以为一个磁盘分区设置8个密钥; - 除了通过键盘输入字符串密钥以外,也可以指定一个磁盘上已经存在的文件为加密磁盘分区的访问密钥:sudo cryptsetup luksAddKey [device file path] [any other file path] - luksReoveKey:从磁盘分区已有的访问密钥列表中删除指定的访问密钥,此后就不能再用这个密钥访问文件系统了 - cryptsetup luksReoveKey [device file path] - 执行时输入需要删除的密钥 在VMware虚拟机上进行分区加密及自动挂载实验 - VMware版本:VMware Workstation 15 Player。 - 操作系统:Debian 10.0。 虚拟Linux系统中原有一块虚拟硬盘40GB,对应于/dev/sda,内部包含两个分区/dev/sda1和/sda2; 在VMware的当前虚拟机系统中通过选项界面再增加一块SCSI硬盘,大小设置为10GB: !Untitled.png 新建后启动虚拟机,可以在/dev下看到一个新的设备文件sdb,下一步计划在这个硬盘上创建LUKS加密分区。 1.在Debian上安装cryptsetup sudo apt-get install cryptsetup 2.使用fdisk工具对新磁盘进行分区 直接基于新创建的sdb硬盘创建一个主分区,对应于设备文件/dev/sdb1; !Untitled.png 3.使用cryptsetup工具对新建立的sdb1分区进行加密; !Untitled.png 4.在/dev/mapper下创建sdb1的映射磁盘分区 - crtpt_disk是随便取的,执行cryptsetup luksOpen命令成功后,就会在/dev/mapper下创建与之对应的磁盘分区映射文件,实际上是/dev/dm-0的映射文件; - 通过以上命令创建的这个/dev/mapper/crypt_disk文件在系统重启后会丢失; - _此后就可以像使用/dev/sda1一样的方式来使用/dev/mapper/crypt_disk了;_ 5.对加密分区进行格式化 此处使用ext4分区格式对/dev/mapper/crtpt_disk进行格式化: 6.挂载并开始使用加密分区 - 注意,对于这类加密分区而言,不能直接挂载其分区文件/dev/sdb1,而是要挂载前面通过cryptsetup luksOpen命令创建的映射文件/dev/mapper/crypt_disk; - 此后就可以在/mnt中使用这个分区进行数据的读写操作了; 7.开机自动加载并自动输入密码 - 按照以上流程设置后,开机过程中会自动创建/dev/sdb1的device mapper映射文件crypt_disk,在加载过程中自动输入密码,并自动挂载到挂载点/mnt下,此后就可以在/mnt下正常使用加密分区了; 参考资料: - 1-14 文件系统的特性与磁盘加密技术 - Linux下的磁盘加密LUKS - linux使用luks加密硬盘或u盘
- 03 Jun, 2020
Linux IPC机制之消息队列
消息队列基础 - Linux系统中的消息队列机制是SystemV IPC提供的三大进程间IPC机制之一:消息队列,信号量,共享内存; - Linux内核中会为每个IPC结构(包括消息队列、信号量和共享内存等)都使用一个非负整数的标识符来加以引用,而无论何时创建或者使用一个已经存在于内核中的IPC结构,都需要指定一个关键字(key_t),使用这个关键字对不同的IPC结构进行区分; - 消息队列中每一个消息都需要遵守系统的:消息最大长度限制MSGMAX,每个消息队列的总的字节数限制MSGMNB,系统上消息队列的总数限制MSGMNI。 - 可以进入/proc/sys/kernel/查看具体的限制数据; - 在Linux中可以使用ipcs命令来查看系统内核中存在的消息队列的状态以及统计数据; 消息队列的工作原理 从本质上讲,消息队列实际上就是位于内核空间的链表。应用空间定义和声明的一个消息队列实际上会对应内核空间的一组(多个)链表,用户层收发的每一条消息都对应于链表中的一个节点,具有相同消息类型的消息节点按照写入的顺序通过指针链接在一起,因此每一个消息类型都对应有一个链表。 应用层对于消息队列的发送和接收从本质上来讲,都是基于其中的某个链表,而非整个消息队列。 内核中消息队列的数据结构的原理如下图所示: !Untitled.png - 消息类型的ID用一个整数来表示,而且这个ID必须大于0,每个消息类型都有一个属于自己的链表; - 应用层发出和接收消息必须要指定一个消息类型ID,这样内核才能知道把这个消息放入哪个链表,或者从哪个链表上取出数据; - 消息类型ID为0的节点用于记录整个消息队列中所有消息加入消息队列的顺序,即上图用红色线条标注的链条; - 在应用中如果我们需要按照顺序读取整个消息队列中的所有消息,即要求消息队列中无论消息类型的ID是多少都返回的时候,就可以在读取的时候指定读取的消息类型ID为0,这样就会把整个消息队列的所有消息全部按照顺序返回; 消息队列的主要API ftok函数 - 用于基于参数生成一个消息队列的key,其原型为: - key_t ftok(const char *pathname, int proj_id); - 参数: - pathname:存在于文件系统的某个文件或者目录的路径名称,可以随意设置,但是这个路径名称对应的文件/目录必须存在; - proj_id:根据自己的约定随意设置,Unix系统中取值范围为0-255; - 功能:ftok函数根据参数传递的路径名,提取出该路径下文件inode在系统内的信息,根据这些文件信息与参数传递的project ID合成一个独一无二的消息队列key; msgget函数 - 用于创建/访问一个消息队列,其原型为: - int msgget(key_t key, int msgflag); - 参数: - key:消息队列的key,一般使用ftok()产生; - msgflag:有两个选项,IPC_CREAT和IPC_EXCL; - 单独使用IPC_CREAT:消息队列不存在的话就创建消息队列,存在的话就打开返回; - 使用IPC_CREAT | IPC_EXCL:消息队列不存在的话就创建消息队列,存在的话就出错返回; - 返回值: - 如果执行成功就返回一个非负整数,表示这个消息队列的ID,后续收发消息对需要引用这个消息队列ID;如果执行失败则返回-1; msgctl函数 - 用于控制和设置消息队列,其原型为: - int msgctl(int msqid, int cmd, struct msqid_ds *buf); - 参数: - msqid:由以上msgget函数执行返回的消息队列的ID; - cmd:有三个选项,IPC_STAT,IPC_SET,IPC_RMID; - IPC_STAT:获取消息队列的内部属性信息数据(uid,gid,字节数,消息数等),赋值给参数传递的msqid_ds结构中; - IPC_SET:使用参数传递的设置msqid_ds结构数据设置消息队列中可以设置的属性信息;(需要足够的权限,并且只能设置可设置的部分) - IPC_RMID:删除消息队列; - 返回值: - 执行成功返回0,失败返回-1; msgsnd函数: - 向消息队列发送一条消息,其原型为: - int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); - 参数: - msqid:由以上msgget函数执行返回的消息队列的ID; - msgp:要发送的消息内容; - 消息内容的前sizeof(long int)个字节一定要是消息类型的ID,否则内核收到这个消息以后不知道应该放到哪个链表上进行处理; - msgsz:要发送的消息长度; - 这个消息长度应该减去消息类型ID的长度,即sizeof(long int); - msgflag: - 默认为0,表示消息队列满的时候阻塞等待; - 设置为IPC_NOWAIT,当消息队列满的时候直接返回错误; - 返回值: - 执行成功返回0,失败返回-1; - 注意事项: - 发出消息的大小必须要小于系统规定的上限,即MSGMAX参数; - 消息结构体必须要以一个long int长整型开始,内核根据消息的这个值来判断应该放入哪个链表,而消息的接收者也根据这个值来确定要接收的消息的类型; msgrcv函数 - 用于从消息队列中接收消息,其原型为: - ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); - 参数: - msqid:由以上msgget函数执行返回的消息队列的ID; - msgp:接收到的消息头指针; - msgsz:接收到的消息的长度; - msgtyp:指定要接收的消息的消息类型; - msgtyp = 0:表示获取整个消息队列中的第一条消息,实际上反复的读取msgtyp = 0的消息,会把整个消息队列的消息按照顺序读取出来,不依赖于具体的消息类型; - msgtyp > 0:表示获取类型为 msgtyp 的第一条消息,除非指定了 msgflg 为MSG_EXCEPT,这表示获取除了 msgtyp 类型以外的第一条消息。 - 可以结合上面的消息队列链表结构来对以上内容进行理解。 - msgflag: - 默认为0,表示消息队列中没有指定消息类型的消息的时候,阻塞等待; - 设置为IPC_NOWAIT,当消息队列中没有指定消息类型的消息时直接返回错误; - 返回值: - 执行成功则返回实际接收到接收缓冲区里的字符个数,失败返回-1; 示例代码 这里设计一个比较典型的消息队列通信的案例:可以支持向同一个消息队列的不同消息类型链表发送不同消息内容的消息,并能够正确的接收和解析; - 创建一个消息队列,send进程向该详细队列中写入消息,recv进程从消息队列中读取消息; - send进程一次执行向消息队列的不同消息类型发送消息; - 相当于是向一个消息队列的多个链表分别发送消息; - send进程一次执行向消息队列的同一消息类型发送不同的消息内容; - 相当于是向一个消息队列的同一个消息链表写入类型不同的消息数据; msg.h: send.c: recv.c: 通过send进程发出消息后,读取消息类型为1的所有消息: 通过send进程发出消息后,读取整个消息队列中的所有消息(无论消息类型是多少): 对于Linux应用层的多模块应用程序中使用消息队列的一些思考 - 多模块应用程序一般从架构上可以分为一个主模块和多个子模块,应用启动过程中首先启动主模块,主模块根据配置或者启动参数决定启动各个子模块; - 主模块在退出时也需要负责对所有的消息队列做删除操作,避免下次启动造成状态混乱; - 每个模块都有自己的独立的消息队列,所有的消息队列在主模块启动时创建,各个子模块启动后,把主模块和自己的消息队列通过msgget挂接进来; - 所有的子模块只会与主模块之间通过消息队列直接通信,不会给其他模块直接发送消息,消息通过主模块来进行总体的管理和中转,以降低子模块之间的耦合; - 因此对于每个子模块而言,只需要挂接自己的消息队列和主模块的消息队列;而对于主模块,因为有可能与所有的子模块通信,因此需要创建/挂接自己和所有子模块的消息队列; - 所有的模块(包括主模块)都有一个专门用于接收和处理自己消息队列中信息的消息处理线程,在该线程中通过while循环+msgrcv阻塞接收的方式等待接收和处理发送给自己的消息,此处msgrcv函数应该使用msgtype=0的接收参数,这样就可以不加区分的接收和处理整个消息队列各个链表中的所有消息,收到消息后,再针对不同的消息类型做不同的处理逻辑; - 所有的模块(包括主模块)在需要向其他模块发出消息时只需要调用msgsnd发送消息即可,在消息类型参数msgtype可以指定不同的消息类型id,来对不同的消息内容进行区分; - 如以上例子代码所示,不同消息类型可以包含不同的消息数据结构,但总的来讲,因为消息队列本身有总的字节数的限制,因此如果有比较大的数据,不建议使用消息队列直接传输,进程之间可以配合共享内存来对大块的数据结构进行传输,线程之间则可以通过全局数据结构来进行消息数据的传递; 参考资料 - 【Linux】进程间通信之消息队列 - Linux系统编程—消息队列
- 01 Jun, 2020
在Debian下使用CppCheck对C代码进行静态扫描
cppcheck的官网: http://cppcheck.net/。 - cppcheck是一个C/C++静态检查工具,可以帮助我们检测出代码可能存在(潜在)的问题; - cppcheck的执行完全基于C/C++源码的文本文件进行词法、语法分析,帮助开发人员找出代码中常见的隐患所在,从而规范化软件开发流程,提高开发质量; - cppcheck不是编译器,替代不了gcc,cppcheck的执行环境中也不需要安装gcc这类编译工具; - cppcheck对于软件开发的生态非常完善,可以以插件的方式整合到Visual Studio、Eclipse、Jenkins、SVN、Git等编译器、版本管理工具以及持续部署的工具链中; - 目前市场上的主流静态分析工具除了cppcheck之外,还有clang、pclint、coverity、tscancode等,主要属性对比如下: !Untitled.png cppcheck在debian/linux上的安装 sudo apt-get install cppcheck; 使用cppcheck对单个c源文件进行扫描 以cppcheck官网上提供的例子进行测试: 对以上单个C源码文件执行cppcheck扫描(使能所有的告警输出信息): 分别对应的问题是: - warning:buf越界; - style:buf的定义范围可以更小,例如放在x==1000的判断条件之内; - style:仍然是buf定义范围问题,如果程序没有在x==1000条件下执行,则buf根本就不会用到; - information:无法找到头文件; - 可以使用--suppress=missingIncludeSystem选项忽略掉包含头文件的告警信息; - 也可以使用-I选项指定头文件所在的目录路径; 使用cppcheck对整个c源文件目录进行扫描 以海思Hi3521DV100 SDK中mpp的sample源文件目录为例进行扫描: - 可以看到,使用cppcheck对一个源码目录进行静态分析和扫描的过程与对单个C文件进行扫描类似,cppcheck会逐个递归的进入各个子目录,对其中包含的C源码文件进行扫描并输出扫描结果,开发人员只需要按照以上给出的结果对问题进行修改即可; cppcheck的常用执行选项 - cppcheck输出的消息提示类型使用--enable选项进行设置: - --enable=all:启用所有类型的消息提示; - --enable=warning:启用警告消息; - --enable=performance:启用性能消息; - --enable=information:启用信息消息; - --enable=style:启用可移植性和样式信息; - --enable=warning,performance:同时启用警告和性能消息; - 保存cppcheck的分析结果到一个文本文件中,使用重定向命令即可 - cppcheck sample --enable=all 2> err.txt; - 多线程检查:选项 -j 用于指定需要使用的线程数,对于大型软件的静态扫描尤其有用 - cppcheck -j 4 sample --enable=all; - 使用--template选项格式化输出信息 - 以gcc兼容的格式输出扫描信息: - cppcheck sample/ --enable=all --template=gcc; - 以Visual Studio兼容的格式输出扫描信息: - cppcheck sample/ --enable=all --template=vs; - 此外也可以使用自定义的方式来格式化输出扫描信息: - cppcheck --template="{file},{line},{severity},{id},{message}" sample/ --enable=all; - 以xml的形式输出扫描信息 - cppcheck --xml-version=2 sample/ --enable=all 2> err.xml; - xml文件中的每一个error节点就是一条扫描输出信息; 参考资料 - 【代码质量】C++代码质量扫描主流工具深度比较 - Cppcheck 用法(上篇)