Linux系统架构和应用技巧.pdf
http://www.100md.com
2020年4月17日
![]() |
| 第1页 |
![]() |
| 第8页 |
![]() |
| 第18页 |
![]() |
| 第28页 |
![]() |
| 第31页 |
![]() |
| 第134页 |
参见附件(5421KB,409页)。
Linux系统架构和应用技巧是中井悦司写的关于linux的书籍,主要讲述了Linux内部结构,虚拟化基础设施环境的构建,内核源代码的阅读,RHEL6新功能综述等等内容。

Linux系统架构和应用技巧内容
本书内容涉及Linux内部结构、虚拟化基础设施环境的构建、内核源代码的阅读以及RHEL6新功能综述。通过搭建虚拟化基础设施,给读者提供了方便实用的Linux系统的学习和实践环境;同时,设计了10个可操作的脚本实验,尽可能覆盖Linux操作系统的关键应用技术,包括进程监控、远程登录、文本处理等。其中的技巧根植于作者的多年经验,具有极强的现场感和可操作性。
书籍作者资料
中井悦司
就职于著名的Linux发行商Red Hat,主要致力于推动Linux/OSS在企业系统中的应用。从使用Linux/OSS的企业应用开发,到10000余台Linux服务器的运维、私有云的设计和构建,通过各种各样的项目掌握了丰富的Linux编程经验,并积极地将自己的经验传授给年轻的程序员们。著有《Linux系统网络管理技术》等。
目录
第1章你必须知道的!Linux内部结构
1.1Linux的三大基础:磁盘、进程、内存
1.2磁盘和文件
1.2.1磁盘的3D参数
1.2.2新旧分区表
1.2.3文件系统和I/O子系统
1.3控制进程就等于控制Linux
1.3.1fork和exec分别是进程的分身和变身
1.3.2作业控制中的各项任务处理
第2章别说缺少机器!虚拟化基础设施环境的构建
2.1基础设施工程师的成长来自于日常积累
2.2LinuxKVM虚拟网络
2.2.1虚拟网络的构建和虚拟机的配置
2.2.2DNS服务器的搭建
2.2.3邮件服务器的建立
2.3HA集群环境在虚拟机上的实现
2.3.1对HAAdd-on的理解
2.3.2Linux主机的准备以及虚拟机的构建
2.3.3HAAdd-On的导入和配置
2.3.4HA集群设计及运用的准备
第3章10轮决胜!在自编的脚本中灵活使用命令
3.1简单有效的Shell脚本
3.2Shell脚本的基本规则
3.2.1Shell脚本的操作确认
3.2.2引号的使用方法
3.2.3条件判断的写法
3.2.4数组和位置参数的使用方法
3.2.5命令置换和数值演算
3.3用Shell脚本一决胜负
3.3.1[第1轮对决]在跳板服务器上学到的秘籍~基本模式+异常处理
3.3.2[第2轮对决]在分布式She11上学到的秘籍~基本模式+管道
3.3.3[第3轮对决]在进程监视中学到的秘籍~状态迁移处理
3.3.4[第4轮对决]秘籍外传~由make命令进行简单的批处理
3.3.5[第5轮对决]从模拟快照(snapshot)学到的秘籍~用管道操作日志
3.3.6[第6轮对决]在云备份中学到的秘籍~在思考实验中组合处理流程
3.4Per1脚本的对决
3.4.1[第7轮对决]自己编写Per1的乐趣
3.4.2[第8轮对决]通过Tweet体验Per1带来的便利
3.4.3[第9轮对决]用进程监控掌握fork
3.4.4[第1e轮对决]终极秘籍Per1与管道的结合
第4章最后的堡垒!内核源代码的阅读
4.1阅读源代码
4.2内核源代码的走读方法
4.2.1Linux内核的构建步骤
4.2.2内核源代码探索入门
4.2.3读懂结构体和指针
4.3探索核心子系统
4.3.1进程管理子系统
4.3.2内存管理子系统
4.4内核源代码的分析实例
4.4.1Linux内核的系统时间
4.4.2闰秒发生的瞬间
4.4.3进一步探索的指南
第5章先行一步!RHEL6新功能综述
5.1支持商品化硬件的“操作系统进化”
5.1.1ext4文件系统的采用
5.1.2NetworkManager服务的引入
5.1.3用dracut创建初始RAM磁盘
5.1.4通过anacron实现定期任务执行
5.2对服务器启动处理进行变革的Upstart
5.2.1Upstart的概要
5.2.2Upstart任务的创建示例
5.3用ControlGroups控制资源分配
5.3.1ControlGroup的概要
5.3.2各子系统的主要参数
5.3.3cg命令群的管理
5.3.4虚拟机的cgroups操作
5.4通过LXC体验容器型虚拟化技术
5.4.1容器型虚拟化技术的概要
5.4.2容器中Web服务器的启动
5.4.3其他的容器设置
Linux系统架构和应用技巧截图


书名:Linux系统架构和应用技巧
作者:[日]中井悦司
译者:李睿 马世龙
ISBN:978-7-115-37257-4
本书由北京图灵文化发展有限公司发行数字版。版权所有,侵权必究。
您购买的图灵电子书仅供您个人使用,未经授权,不得以任何方式复制和传播
本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维
权措施,并可能追究法律责任。
图灵社区会员 ptpress(libowen@ptpress.com.cn) 专享 尊重版权
版权声明
推荐序一 1
推荐序二
译者序
序
谢辞
本书的阅读对象
本书的阅读方法
各章概要
第 1 章 你必须知道的!Linux内部结构
1.1 Linux的三大基础:磁盘、进程、内存
1.2 磁盘和文件
1.2.1 磁盘的 3D 参数
1.2.2 新旧分区表
1.2.3 文件系统和 IO 子系统
1.3 控制进程就等于控制Linux
1.3.1 fork 和 exec 分别是进程的分身和变身
1.3.2 作业控制中的各项任务处理
第 2 章 别说缺少机器!虚拟化基础设施环境的构建
3.3 用Shell脚本一决胜负
3.2.5 命令置换和数值演算
3.2.4 数组和位置参数的使用方法
3.2.3 条件判断的写法
3.2.2 引号的使用方法
3.2.1 Shell 脚本的操作确认
3.2 Shell脚本的基本规则
3.1 简单有效的Shell脚本
第 3 章 10轮决胜!在自编的脚本中灵活使用命令
2.3.4 HA 集群设计及运用的准备
2.3.3 HA Add-On 的导入和配置
2.3.2 Linux 主机的准备以及虚拟机的构建
2.3.1 对 HA Add-on 的理解
2.3 HA集群环境在虚拟机上的实现
2.2.3 邮件服务器的建立
2.2.2 DNS 服务器的搭建
2.2.1 虚拟网络的构建和虚拟机的配置
2.2 Linux KVM虚拟网络
2.1 基础设施工程师的成长来自于日常积累3.3.1 [ 第 1 轮对决 ] 在跳板服务器上学到的秘籍 ~ 基本模式 + 异常处
理
3.3.2 [ 第 2 轮对决 ] 在分布式 Shell 上学到的秘籍 ~ 基本模式 + 管
道
3.3.3 [ 第 3 轮对决 ] 在进程监视中学到的秘籍 ~ 状态迁移处理
3.3.4 [ 第 4 轮对决 ] 秘籍外传 ~ 由 make 命令进行简单的批处理
3.3.5 [ 第 5 轮对决 ] 从模拟快照(snapshot)学到的秘籍 ~ 用管道操
作日志
3.3.6 [ 第 6 轮对决 ] 在云备份中学到的秘籍 ~ 在思考实验中组合处理流
程
3.4 Perl脚本的对决
3.4.1 [ 第 7 轮对决 ] 自己编写 Perl 的乐趣
3.4.2 [ 第 8 轮对决 ] 通过 Tweet 体验 Perl 带来的便利
3.4.3 [ 第 9 轮对决 ] 用进程监控掌握 fork
3.4.4 [ 第 10 轮对决 ] 终极秘籍 Perl 与管道的结合
第 4 章 最后的堡垒!内核源代码的阅读
4.1 阅读源代码
4.2 内核源代码的走读方法
4.2.1 Linux 内核的构建步骤
4.2.2 内核源代码探索入门
4.2.3 读懂结构体和指针
5.3.2 各子系统的主要参数
5.3.1 Control Group 的概要
5.3 用Control Groups控制资源分配
5.2.2 Upstart 任务的创建示例
5.2.1 Upstart 的概要
5.2 对服务器启动处理进行变革的Upstart
5.1.4 通过 anacron 实现定期任务执行
5.1.3 用 dracut 创建初始 RAM 磁盘
5.1.2 NetworkManager 服务的引入
5.1.1 ext4 文件系统的采用
5.1 支持商品化硬件的“操作系统进化”
第 5 章 先行一步!RHEL6新功能综述
4.4.3 进一步探索的指南
4.4.2 闰秒发生的瞬间
4.4.1 Linux 内核的系统时间
4.4 内核源代码的分析实例
4.3.2 内存管理子系统
4.3.1 进程管理子系统
4.3 探索核心子系统5.3.3 cg 命令群的管理
5.3.4 虚拟机的 cgroups 操作
5.4 通过LXC体验容器型虚拟化技术
5.4.1 容器型虚拟化技术的概要
5.4.2 容器中 Web 服务器的启动
5.4.3 其他的容器设置
结语
参考文献
版权所有,侵权必究。
家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。
本书中文简体字版由 Gijyutsu-Hyoron Co., Ltd. 授权人民邮电出版社独
Tuttle-Mori Agency, Inc., Tokyo
arrangement with Gijyutsu-Hyoron Co., Ltd., Tokyo in care of
This Simplified Chinese language edition published by Ltd., Tokyo
Original Japanese edition published by Gijyutsu-Hyoron Co.,All rights reserved.
Copyright ? 2012 Etsuji Nakai
PURO NO TAME NO LINUX SYSTEM:10NEN KIKU GIJUTSU by Etsuji Nakai
版权声明推荐序一
推荐序排名不分先后,此处按姓氏音序排名。——编者注
Linux 操作系统近年来发展迅猛,在服务器和手机上都获得了巨大的成功。随
着互联网和云计算的发展,可以预期 Linux 将会占据越来越重要的地位。然而,由于历史原因和条件限制,中国的很多大学一直使用 Windows 环境作为主要的教
学和工作平台,很多计算机专业学生和从业人员没有 Linux 的经验,这不但限制
了他们的职业选择,也对我国 IT 产业的发展造成了相当程度的制约。
能否为有一定计算机基础知识的人提供一本参考书,使得他们能够尽快掌握
Linux 的基本知识,并培养深入学习和运用 Linux 的能力呢?《Linux 系统架构
和应用技巧》就是一本理想的读物。
首先,这本书非常实用、容易上手。通过目标驱动的方式,所讲述的内容包含
了细致的操作说明,可以对着书一步步在计算机上操作,边操作边学习。
其次,本书也绝不是简单的操作指南,包含了相当多的深度内容。例如,在第
2 章就介绍了一个分布式服务系统的构建,甚至包括了高可用系统的构建。第 4 章
更是介绍了如何阅读 Linux 内核源代码。这些深度内容为读者进一步深入探索和
应用 Linux 打下了很好的基础。
第三,与动辄数百页的大部头相比,本书相当轻薄,不会令读者望而生畏。
总之,我认为本书在实用性和深度上取得了很好的平衡,选材精炼,是一本很
好的 Linux 入门读物,适合计算机相关专业的学生和从业人员自学,也适合部分
大学的相关专业作为教材使用。
清华大学计算机系副系主任 陈文光
1
1 推荐序二
20 世纪 90 年代初,Linux 刚刚诞生不久,一部分有梦想的留学生怀揣着一
打打的软盘,不远万里将 Linux 发行版的副本带回国,Linux 因此得以在中国生
根萌芽。那时侯,国内 Linux 资源极其匮乏,使用者也是寥若晨星。1997 年,中
国软件行业协会自由软件研究应用发展分会在北京成立,差不多同一时期,国内最
具人气的水木清华 BBS 开辟了 Linux 版,随后提出建立中文 Linux 计划。这之
后,各种诸如中国开源软件推进联盟之类的民间组织如雨后春笋般相继发展起来
了。
进入 21 世纪以来,国内 IT 产业对 Linux 的重视程度不断提升,开源运动
在国内得到业界广泛支持,一批以 Linux 为核心的系统软件供应商逐渐浮出水
面,并为电力、航天、交通等重点行业提供了主要的技术支撑。与此同时,各类基
于 Linux 的应用,在经历了市场的筛选后,被认可的成熟应用逐步形成规模生
产。基于优化的 Linux 系统等开源软件进行自主研发的技术也呈现出全面替代大
型成熟商业软件产品的趋势。
2009 年是我国的 3G 元年,自此我国正式进入移动互联网时代,这无疑给
Linux 提供了新的发展起点,带来了更大的发展空间。当前,大多数智能移动终端
所采用的操作系统都是基于 Linux 平台开发的。例如,Google 推出的 Android
操作系统、中国联通发布的智能手机操作系统平台“沃 Phone”,以及阿里巴巴集
团旗下阿里云计算有限公司开发的移动操作系统阿里云 OS 等。可以说,Linux 正
以不可阻挡的强劲势头迈步向前。
迄今为止,Linux 在国内已有了十几年的积累,专业人才不论在质量还有数量
上都得到了很大的提升,这一点从 Linux 内核社区做出的贡献就能看出,55 个国
家的 8849 人贡献了 29 万个补丁,其中 16280 个补丁的贡献者来自中国,位列
贡献表排名榜第四位。尽管如此,我国 Linux 的人才缺口依然很大,还远远不能
够满足 Linux 迅速发展的需要。因此,成为一名 Linux 技术人才,无论是在职业
发展方面还是在薪酬待遇上,空间都极为广阔。
这本书不仅是原著者智慧和经验的分享,也融入了翻译者的心血,我很高兴看到这本书能顺利翻译和出版。它的实践性很强,深入浅出,是一本很好的 Linux
参考书。对广大立志成为一名 Linux 工程师的读者来说,这会是一本很好的学习
用书。
北京航空航天大学计算机学院院长 吕卫锋
译者序
与这本书结缘很偶然,2011 年,译者所在的北京航空航天大学软件开发国家重
点实验室承担了国家科技支撑计划项目“基于 U- 系列标准的电子政务应用解决方
案”中的一个课题,内容涉及将基于 Windows 的应用软件迁移至 Linux 内核的国
产操作系统。为此,课题组全体师生对 Windows 和 Linux 操作系统进行了深入的
学习和实践,并在此过程中产生了将实践成果整理成书的想法。一次偶然的机会,译者在浏览图灵技术论坛时与《Linux 系统架构和应用技巧》这本书不期而遇,该
书的理念和我们当时的想法不谋而合。
本书内容丰富,结构清晰,深入浅出,具有较强的实践性,适合具有一定基础
的 Linux 开发人员和爱好者学习。译者希望该书中文版的面世,能够进一步推动
国内 Linux 的学习和实践。
本书有如下特点:
1. 通过搭建虚拟化基础设施,给读者提供了方便实用的 Linux 系统的学习和
实践环境;
2. 设计了 10 个可操作的脚本实验,以尽可能覆盖 Linux 操作系统的关键应
用技术,包括进程监控、远程登录、文本处理等;
3. 提出了 Linux 内核源代码的走读方法,在阅读源代码前,先了解内核的整
体结构,然后进一步了解各组成部分,最后深入到内核源代码,并通过创建简单原
始内核的示例,加深对内核源代码的理解。
致谢:
感谢毕业后一直任职于日本企业的北京航空航天大学计算机学院 99 级师兄王
新宇先生,以及在日本开会期间相识的就读于关西大学研究生院综合信息学研究科
的杜逆索先生,二位一直不厌其烦地为本书的翻译工作提供帮助!
感谢本书的文字编辑,一字一句地校对书中的每一个细节,尽可能地降低了译文的不准确性,也纠正了译文中很多疏忽的地方!
感谢人民邮电出版社图灵公司很好地推动了该书的引进、翻译和出版工作!
译者虽力求忠实准确地传达原著者的思想,但由于时间关系及水平所限,译文
中难免存在疏漏或不当之处,恳请广大读者批评指正。
李睿 马世龙
于北京航空航天大学
序
本书主要面向企业中的 Linux 系统工程师或 Linux 初学者。虽然迄今为止笔
者已出版了两本以 Linux 系统架构为主题的书籍,但是本书的风格略有不同。之
前的两本书,虽有幸得到了“内容充实易懂”之类的评价,但是为了兼顾系统性和
连贯性,不得已略去了现场的实践内容以及紧急状况的应对措施这两部分内容。而
本书则避免了之前教科书式的写法,尽量用笔者自己的口吻,向初中级 Linux 工
程师传授各种 Linux 系统架构和运用的技巧。
过去的工程师通常向同行前辈们学习系统架构和运用技能。然而如今受到社会
形势的变化,乃至企业培训方针变化的影响,很多时候往往来不及向前辈们请教,而是一上来就要独自负责 Linux 服务器的构建和运维工作。如此一来,那些有前
途的年轻工程师们,不仅没有机会体验这份工作带来的喜悦,反而为了适应工作而
每天都疲惫不堪,着实令人可惜。笔者希望看到更多的工程师,能在每天的工作中
体验到感动和喜悦的同时,支撑起 Linux 这一当代企业系统的支柱。在书里公开
这样的想法固然有些难为情,说不定还会被熟人嘲笑,但笔者还是坦率地写了出
来。我们所说的“十年不过时的技术”也许稍嫌夸张 ,但是笔者相信,书中的内
容一定会是长期支持诸位读者们的工程师生涯的“基础中的基础”。
原书名为「プロのための Linux システム·10 年効く技術」,直译为《专业开发者的 Linux 系统·十年
不过时的技术》。——译者注
在阅读本书的过程中,请各位初出茅庐的工程师们时不时地想象一下前辈们指
点江山的得意身影。要知道工程师前辈们也是人,有时也可能会说些没有根据的
话。然而 Linux 的精妙之处就在于,一旦有不确定的地方,立即就能亲自进行确
认。与昂贵的商用 Unix 服务器不同,只要你愿意,随时都可以用家用 PC 建立一
个测试环境。当今的 IT 系统新技术正以前所未有的速度发展着,这种能够亲自动
手进行确认的行动力,对工程师来说是比知识和经验更加宝贵的财富。虽然本书记
载的只是笔者自身的知识和经验,但若每一位读者都能发挥自身的行动力,将其价
值成倍扩大的话,对笔者而言将是无上的喜悦。
中井悦司
1
1 谢辞
谨在此向对本书的执笔和出版提供过帮助的各位表示感谢。
本书的构思来自于技术评论社的池本公平先生的建议。笔者之所以深感把技术
发展趋势的根本,即“工程师的基础知识”传达给大众的重要性,全仰赖池本先生
的大力推动,在此表示感谢。另外,向试读本书的中山健次郎先生也表示感谢。
本书的主要内容取材于笔者自身和 Linux、开源相关的工作经验。在这期间,笔者有幸遇到了诸多对 Linux、开源充满喜爱和探究精神的伙伴们,在此也向他们
表示感谢。
最后要感谢我的妻子真理,面对只顾埋头执笔而忽略家庭的丈夫依然笑颜以
对,还有将满 3 岁的活泼的爱女步実,整日嘟囔着“爸爸真喜欢工作啊”,再一次
对你们母女表示最真挚的感谢。
感谢大家一直以来的支持,今后也请多多指教!
本书的阅读对象
本书涉及与业务系统相关的 Linux 服务器的构建与运用,适合想要进一步深
入了解 Linux 相关技术,或者想要提高 Linux 操作技能的工程师读者。
从 Linux 的内部结构,到脚本编写技术或是内核源码的阅读方法,本书所传
授的技术是在一般教科书中看不到的。希望各位工程师读者能以本书为起点,找到
进一步提升技术的方法。
本书的阅读方法
本书的各章节内容都是相互独立的,既可以从第 1 章开始按部就班地阅读,也
可以从吸引你的那一章开始阅读。
至于每一章的预备知识及难易度则略有不同。若有感觉难以理解的部分,请结
合参考文献里介绍的同类书籍进行配套阅读。从不同的角度来审视同一个问题,是
在学习技术的过程中打破瓶颈的最佳方式。
第 2 章、第 3 章、第 5 章的主要配置文件和脚本,可以从技术评论社的网
站上下载。下载链接如下:
http:image.gihyo.co.jpassetsfilesbook2012978-4-7741-
5143-4downloadLinuxKuradashi_20120517.tgz
为方便起见,本书中介绍的具体的命令和设置顺序,是基于 Red Hat
Enterprise Linux 6.2 的。但是,其适用的技术领域并不依赖于特定的 Linux
发行版。还请读者朋友们将本书作为扩大知识面、迈向新阶段的一个出发点吧。
各章概要
第 1 章 你必须知道的! Linux 内部结构
磁盘管理、过程管理和内存管理是适当运用 Linux 服务器以及进行问题判断
的基础。针对这些内容,本章将从 Linux 内部构造的角度出发,传授有助于实践
的知识。从磁盘的几何结构,到 UEFI 和 GPI、4KB 扇区磁盘等新的机制,再到
IO 调度程序的基础、fork 和 exec 的差异、内核内部的内存分类等,希望读者
们能对这些知识有更深一步的理解。
第 2 章 别说缺少机器!虚拟化基础设施环境的构建
要处理 IT 基础设施,最基本的莫过于对整个系统机制(体系结构)的理解,因此亲手构建典型配置的系统,彻底理解整体架构与各个服务器功能之间的关系是
很有必要的。为了有效地学习以上技能,最好营造一个专门的操作环境。本章围
绕“邮件发送系统”和“HA 集群环境”,介绍了与 Linux KVM 的虚拟机环境相结
合的操作环境的使用方法。
第 3 章 10 轮决胜!在自编的脚本中灵活使用命令
本章主要介绍 Shell 脚本和 Perl 脚本的创建,它们是提高服务器管理工作
的效率所必需的。Shell 脚本部分并不是单纯地介绍基本规则和示例,而是通过列
举能立即学以致用的现场实例,传授实用的 Shell 脚本的编写技能。至于 Perl
脚本,本章则介绍了如何灵活运用文本分析以及 fork 的使用等 Perl 独有的功能
和数据结构,以期创建出更高级的 Perl 脚本。
第 4 章 最后的堡垒!内核源代码的阅读
要对 Linux 的内部结构有更深层的理解,有时也需要阅读 Linux 的内核源代
码,但要整体地把握这么庞大的源代码也并非易事。第 4 章进行了思维转换,主张
用欣赏文学作品般的方式阅读和把握源代码。本章先介绍实际运行的内核的编译顺
序,然后再逐次进行源代码的阅读。除了修改源代码外,还介绍了一个创建原始内
核的简单示例。第 5 章 先行一步! RHEL6 新功能综述
随着支持 Linux 的服务器环境的演化,Linux 也发生了各种各样的变化。本
章详细介绍了作为 Linux 服务器的管理者所应了解的知识,例如 Red Hat
Enterprise Linux 6(RHEL6)的更新,乃至 Upstart、Control Groups、LXC(Linux 容器)等新功能,这些都是与 Linux 相关技术的未来趋势息息相关的
基础知识。
第 1 章 你必须知道的!Linux内部结
构
1.1 Linux的三大基础:磁盘、进程、内存
Linux 工程师的工作有时需要登录陌生的 Linux 服务器才能进行。在这种时
候,起初笔者会用到 df、ps 和 free 这三个命令,倒并非出于刻意,只是本能地
去执行这些指令。
例如,当你接到一个紧急事件的请求响应,而事先又不知道该服务器的配置信
息时,就得首先用这三个命令检查一下这台服务器的状态,并对当前资源的使用情
况进行确认。
此外,通过 w 命令来确认是否有用户在同一时间在同一台服务器上工作,也是非常重要的。
首先,df 命令主要用来检查文件系统上的可用空间。虽然服务器在运行过程中
通常会对文件系统的使用率进行监测,但在进行设备维护而暂时停止监测时,就很
容易导致意外情况的发生。
比如曾发生过这样的严重状况:当被告知“某个命令无法顺利执行”时,试着
输入 df 指令,竟发现此时根文件系统的利用率已高达 100%。因此,在登录服务
器进行工作时,还是养成时常用 df 命令来检查文件系统使用率的习惯吧。
df 命令也可用于确认磁盘分区的构成以及数据的分配状况,还可以确认用于保
存数据的文件系统有多少千兆字节,或是否有 NFS 安装区域等。
了解应用系统的使用数据是如何被组织和存储的,是全面了解服务器运行的一
个关键。经典的软件开发著作《人月神话》[1] 中写道:“光看流程图不看数据表
只是徒劳,但看了数据表,流程图便不看也罢。”IT,顾名思义,是指信息技术,其核心自然是信息(数据)的处理,因此数据可以说是所有操作的基本了。
此外,服务器是利用进程来处理数据的,而 ps 命令则可以被用来确认当前服
1
1务器上运行进程的状态。
如果平常我们能接触到各种 ps 命令的输出,那么只需看看进程的名称,就可
以大致了解这台服务器的用途和设置。有时会发现一些意料之外的进程正在运行,而经过仔细确认,可能就会在不经意间找到问题的原因。
最后的 free 命令适用于确认存储的使用状况。通过 ps 命令所得到的进程的
信息,再结合磁盘、高速缓存和内存的使用情况,能很好地掌握服务器的运行状
态,了解这是一个“重载”到什么程度的服务器。
Technical Notes
[1] 《人月神话》弗雷德里克·布鲁克斯(著),汪颖(译),清华大学出
版社,2007
所谓的“重载”“轻载”,是一种含糊的说法,实际上是结合过去 CPU 的使用
率和磁盘 IO 的频率来进行综合判断的。然而,CPU 或 IO 操作与内存的使用情
况是密切相关的。如同医生可以从患者的外部情况推断病情一样,通过内存的使用
情况,就能推断出包括 CPU 和 IO 在内的整个服务器的运行状态。
即便在 CPU 的使用率和磁盘 IO 的频率的历史数据十分翔实的情况下,也应
时刻质疑“莫非是 CPU 使用率太高了?”“也许是 IO 负载太高了?”,以这样
的态度去观察数据,方能分析出最接近真实状况的结果。
虽然这个引言稍嫌冗长,但通过以上例子,可以总结出三个应了解的 Linux
的基本分支,即用于保存数据的磁盘、实际处理数据的进程,以及存储服务器的各
种运行信息的内存。
针对以上三点,本章将从 Linux 内部结构的角度出发,介绍有助于实践应用
的知识。1.2 磁盘和文件
1.2.1 磁盘的 3D 参数
Linux 这一类操作系统的任务是隐藏物理硬件信息,即对用户和上层应用屏蔽
底层硬件的差异,并提供统一的操作方法。
但是,对于磁盘装置,还是需要让人知道它的物理构造的。这一点似乎常常被
误导。例如,在笔者的 Red Hat Enterprise Linux 6(RHEL6)测试机上运行
fdisk 指令,就能得到如下结果。
fdisk -l devsda
磁盘 devsda: 500.1 GB, 500107862016 字节
磁头 255, 扇区 63, 柱面 60801
Units = 柱面数 of 16065 512 = 8225280 字节
扇区大小 (逻辑 物理): 512 字节 512 字节
IO size (minimumoptimal): 512 bytes 512 bytes
磁盘标识符: 0x8c403069
设备 启动 始点 终点 块 Id 系统
devsda1 1 64 512000 83 Linux
第1分区没有在柱面边界结束。
devsda2 64 51057 409600000 83 Linux
devsda3 51057 51318 2097152 82 Linux 交换 Solaris
devsda4 51318 60802 76176407+ 83 Linux
屏幕上出现了警告信息:“第 1 分区没有在柱面边界结束”。于是在 Web 上
检索“磁盘的柱面”,即得到图 1.1 这样的说明图。图 1.1 典型的硬盘的说明图
结合之前输出结果的第 2 行信息“磁头 255,扇区 63,柱面 60801”,此图
该如何解释呢?那就是,这台测试机器的硬盘“有 255 个磁头数,每个磁道有 63
个扇区,一个磁盘可以分割出 60801 个磁道”。
当然,物理上拥有 255 个磁头的磁盘驱动器是不存在的。磁盘装置的柱面数
(cylinder)、磁头(head)、扇区(sector)信息,三者统称为 CHS 或“3D
参数”,然而实际上,fdisk 命令表示的 3D 参数信息和磁盘装置的实际构造并没
有什么直接的关系。
要弄清楚这是为什么,就需要理解硬盘的两种不同类型(CHS 方式和 LBA 方
式)的存取方法(也经常被说成“寻址模式”)以及它们之间的差异。
正如图 1.1 中所示,要特别规定数据读取和写入的扇区(物理磁盘存取的最小
单位)所处的位置,只需指定以下三个数值即可:柱面数(从最外缘开始计算第几
条磁道)、磁头数(磁盘表面的读 写头的个数)以及扇区数(磁道内等分弧段的
个数)。
使用时间超过 10 年的旧式磁盘中,磁盘装置的这三个值(CHS 值)指明了读
写数据的位置,这就是 CHS 方式。但实际上,Linux 设备驱动程序会计算 CHS
值,以进行磁盘装置与数据之间的读写,因此用户是不需要知道它们的具体数值
的。在磁盘仍然使用 CHS 方式的时代,唯一一次需要用户在操作时知道 CHS 值
的,是在创建磁盘分区的时候。根据当时的 MS-DOS 方式,磁盘分区必须以扇区为
单位进行操作。
由于柱面是从磁盘的外缘开始按顺序进行编号的,因而磁盘柱面的分割与编号
大致如图 1.2 所示。过去还曾有人异想天开,想在不同的物理磁盘(磁盘面)上采
用不同的分割方式,但遗憾的是,这样的分区方式是无法实现的。
图 1.2 典型的分区的分割方法
虽然 Linux 没有必要遵循 MS-DOS 方式,但在当时,通过将操作系统引入不
同的分区,可以实现 MS-DOS 和 Linux 的多重引导或是 Windows 和 Linux 的多重引导。因此,和 MS-DOS 或 Windows 遵循共同的方式是有实际意义的。
如此一来,和 MS-DOS 中的 fdisk.exe 一样,Linux 中的 fdisk 命令在做
磁盘分区时,也得用柱面数来指定分区的开始位置和结束位置了。
然而,这种有着悠久历史的旧式硬盘早已被时代淘汰,沦为“计算机历史博物
馆”中的陈列品了(这种说法可能有些夸张,但至少在笔者的家中是难觅其踪
了)。
现在的硬盘普遍采用 LBA(Logic Block Addressing,逻辑块寻址)方式进
行数据存取。这种方式的机制极其简单,即硬盘内所有的扇区均从 0 开始进行编号
(扇区编号),通过扇区数来指定扇区的位置。
扇区号与物理扇区位置之间的对应,是由内置的硬盘控制器来计算的。通常来
说,扇区号越小,其对应的物理扇区就越位于磁盘的外侧。
与此相结合,分区的开始位置和结束位置同样也是由扇区号来指定的。之前的
fdisk 命令的输出中,是按照传统的 CHS 方式,使用柱面号来表示各分区的起点
和终点的,但实际上这并不是真实的信息。真正的分区信息,需要在 fdisk 命令
之后加上选项 -u 来获取。
fdisk -lu devsda
磁盘 devsda: 500.1 GB, 500107862016 字节
磁头 255, 扇区 63, 柱面 60801, 合计 976773168 扇区
Units = 扇区数 of 1 512 = 512 字节
扇区大小 (逻辑 物理): 512 字节 512 字节
IO size (minimumoptimal): 512 bytes 512 bytes
磁盘标识符: 0x8c403069
设备 启动 始点 终点 块 Id 系统
devsda1 2048 1026047 512000 83 Linux
第1分区没有在柱面边界结束。
devsda2 1026048 820226047 409600000 83 Linux
devsda3 820226048 824420351 2097152 82 Linux 交换 Solaris
devsda4 824420352 976773166 76176407+ 83 Linux如图所示,各个分区的起点和终点是由扇区号来表示的。这些扇区号所划分出
的范围就是实际的分区。图中第一个分区是从 2048 号扇区开始的,由此可知
0~2047 扇区是保留扇区,它们不被用作分区。
磁盘的开始部分为主引导扇区 MBR,GRUB stage1.5 就保存在 MBR 之后的空
间中。这里的情形是,第 0 号扇区为主引导扇区,stage 1.5 则存储在主引导扇
区后直至第 2047 号扇区之间的空间中 。
第 1 分区的起始位置,以前的标准是第 63 扇区,最近变更为第 2048 扇区。而 stage1.5 并没有那
么大,因此在之前的起始位置保存 stage1.5 也没有问题。
绕了这么大一个圈,下面就开始解释上文所提到的警告信息吧。
由于现在的硬盘是以 LBA 方式存取的,因此分区的开始和结束位置皆通过扇区
号来指定。虽然这样做一点问题都没有,但是 fdisk 命令为了支持旧式 CHS 方式
的磁盘,仍然以 CHS 方式来表示磁盘信息。
这时,对于 LBA 方式的磁盘,需要转换成其对应的 3D(磁头数、扇区数、柱
面数)参数。当分区的结束位置不能用 3D 参数中合适的柱面结束位置来对应表示
时,就会出现诸如“第 1 分区没有在柱面边界结束”这样的警告信息。
总之,这种警告信息的出现,意味着“操作系统正以柱面为单位对磁盘进行分
区,可能会导致一些问题”。不过对于在 Linux 下使用的磁盘,这种顾虑是多余
的。
现在,在 fdisk 命令之后加上选项 -u,便可以通过指定扇区号来指定分区。
因此,今后还是养成在 fdisk 命令后附加选项 -u 的习惯吧 。
在 Red Hat Enterprise Linux 的高级开发版本 Fedora 所包含的 fdisk 命令中,默认的操作是以
扇区为单位的。当需要进行旧式的以柱面为单位的操作时,需要指定选项 -u=cylinders。
1.2.2 新旧分区表
前一节介绍的表示分区开始和结束位置的信息,它们究竟会被写入到磁盘的哪
里呢?这是一个常常被问到的问题,答案毫无疑问是“分区表”。
2
2
3
3准确地说,分区表就是存放在第 0 号扇区 MBR 的 446~509 字节的部分。在
MBR 的 0~445 字节中,存放的是所谓的引导加载程序,即服务器启动时,用于引
导 BIOS 的加电自检以及 GRUB stage1 的加载。
由于一个扇区的大小是 512 字节,这里就会有 510~511(从 0 字节开始,到
511 字节结束)两个字节的剩余,于是按惯例这里的数值就记录为 0xAA55。若磁
盘此处的值不为 0xAA55,则判断该磁盘的 MBR 已损坏。
由于分区表的大小只有 64 个字节,因此大部分信息不能被写入其中。在每个
分区表中,只记录着“用 CHS 方式描述的分区开始位置和结束位置”以及“用
LBA 方式(扇区号)描述的分区开始位置以及包含的扇区数”这类有代表性的信息
(结束位置不用扇区号来记录,而是通过开始位置与扇区数相加计算得到)。
之所以要通过两种方式记录分区开始和结束位置的信息,是有其历史原因的。
LBA 方式的磁盘实际上是不使用 CHS 方式记录分区信息的。
图 1.3 是通过 hexdump 命令输出的 MBR 第 446 字节开始往后 66 字节的
内容(分区表加最后两个字节)。通过设置详细的选项,输出了以十进制形式表示
的 LBA 方式的分区信息。
图 1.3 分区表的转储输出
方框里的 4 行数据,分别是 4 个分区的信息。将它与之前带选项 -u 的
fdisk 命令的输出相比较,可以确定采用 LBA 方式记录的扇区号的信息与之前的信息是一致的。
CHS 方式描述的数值在此不做详细分析,但从分区的开始位置和结束位置出现
了若干个相同的数值可以看出,这是段没有意义的信息。由于 CHS 方式描述的信息
实际上并不会被投入使用,因此问题不大,但还是应当注意避免混淆。
顺带一提,最后两个字节正是前面介绍的 0xAA55,而把图 1.3 的最后看成
0x55AA 的读者,还请自行学习一下“小端”(little endian)的知识。
如此看来,使用 LBA 方式是最为简单便捷的了。不过,近来这种方式也出现了
它的局限性。那么是什么呢?
如图 1.3 所示,虽然采用十进制比较难懂,但能看出表示开始位置的扇区号和
全体扇区数的数字总共是 4 个字节,因此可以表示的范围仅为
0x00000000~0xFFFFFFFF。换句话说,它无法支持扇区数超过 0xFFFFFFFF 的大
容量磁盘。
可以想象出这个容量是多大吗?我们知道一个扇区是 512 个字节,用十六进制
的计算器进行计算,答案应该是 2TB。
也许有人要问,如果没有十六进制的计算器该怎么办?即便没有这样的计算
器,也可以在 Linux 上通过使用 bc 指令来计算,如下所示。ibase=16 即指
定“输入值为十六进制”。另外这里提醒大家一下,0xFF 表示的是十进制数
256。
echo ibase=16;FFFFFFFFFF2 | bc
2190433320450
因此,MBR 中的分区表是有限度的,对于容量大于 2TB 的硬盘,是无法为之
创建分区的。
当使用外部存储装置 LUN(逻辑磁盘)作为数据存储区域时,则无需对 LUN
进行分区,只需将其格式化后挂载到文件系统中,或使用 LVM(逻辑卷管理)方法
将其作为逻辑卷进行管理等。这样,容量大于 2TB 的 LUN 就也能够使用了。不过,近来的服务器磁盘正逐步趋向大容量化,随着容量大于 2TB 的本地磁盘
的普及,找到容量大于 2TB 的磁盘的分区方法也指日可待。
GPT(GUID Partition Table,GUID 分区表)正是为了解决这个问题应运而
生的。现在,如果需要从使用 GPT 的硬盘中启动操作系统,就需要服务器和操作系
统都能支持 UEFI。操作系统中,目前的 Red Hat Enterprise Linux
6(RHEL6)是能支持 UEFI 的。
此外,虽然与分区表并不直接相关,但在最近的大容量磁盘中,有的已经以
4KB 作为一个扇区的大小了。
接下来就对 UEFI 和 GPT,以及 4KB 扇区的磁盘做一个详细的介绍,内容或
许略微复杂,请大家认真学习。
UEFI和GPT
UEFI 是以后将要取代 BIOS 的一个方案。
我们都知道,当服务器接上电源,系统 BIOS 就开始启动。BIOS 的启动
只允许使用 1M 的内存空间。因此系统 BIOS 的设置界面不是图形界面,而是
非常简单的基于文本形式的界面。而且服务器上搭载的各种设备的设置不是通
过 BIOS 的设置界面来操作的,而是需要通过 Ctrl+A 等按键来单独操作设置
界面。这些都是因为 BIOS 所能使用的内存空间有限。
UEFI 打破了 BIOS 的这些限制,在功能方面进行了多种扩展,于是支持
UEFI 的服务器在启动时,就可以直接通过 UEFI 的设置界面来调用各种设
备,有的服务器甚至已经有了图形化的设置界面。
此外,调用引导加载程序的方法也发生了改变。以前都需要像 GRUB
stage1、stage1.5、stage2 这样分阶段启动引导加载程序,而改进后,在以
GPT 方式创建的“EFI 系统分区”中,引导加载程序存储就可以直接被调用
了。
最后介绍一下 GPT。过去的分区表在第 0 扇区 MBR 里,GPT 则被写入第
1 扇区至第 33 扇区中,成为一种新型的分区表(图 1.4)。表 1.1 中总结了这两种分区方式的主要区别。
图 1.4 GPT 的构造
表 1.1 以前的分区和 GPT 的比较以前的分区 GPT
分区表的位置 MBR MBR之后(磁盘末尾亦有副本)
最大磁盘容量 2TB 8ZB(无限)
最大分区数 15(使用SCSI磁盘时) 1284
分区卷标 分区ID(表示用途的ID) GUID(表示用途的ID+唯一的ID)
制作分区的工具 fdisk parted
4在设计 GPT 时,通过改变 GPT 头的设置,也可以创建出超过 128
个分区。但一般情况下,128 个仍是最大限度。
为了降低分区表损坏的风险,GPT 在硬盘的最后保存了一份同样内容的分
区表副本。GPT 的头部,则记录了可以用作分区的扇区范围。
每个分区的信息都记录在“分区表”中。一个分区表是 128 字节,一个扇
区(512 字节)可以记录 4 个分区的信息。每个分区的开始扇区和结束扇区
都分别用 8 个字节来记录,因此即便是容量大于 2TB 的硬盘的扇区数,处理
起来也是绰绰有余的。
每个分区中都记录着一个特定的 GUID 标签。尤其是用来存储引导装载程
序的分区,会附上“EFI 系统分区”(ESP)的标签。
如果是 RHEL6,bootefi 下挂载的文件系统就是 ESP 类型的分区。这
里保存了类似于 GRUB stage2(grub.efi)的启动过程。支持 UEFI 的服务
器,会根据 GUID 定位 ESP,启动其中的 grub.efi ,因此就不再需要 GRUB
stage1 和 stage1.5 了。ESP 采用的是 VFAT 格式。
当创建一个 GPT 格式的分区时,应使用 parted 命令。表 1.2 描述了
parted 命令的主要内部命令,它们的具体使用方法可以在网上查到,此处不
再赘述。
表 1.2 parted 命令的主要内部命令
命令 说明
check 对文件系统进行检查
5cp 复制分区
help
显示对相关命令的说明。“help命令名称>”可以显示每一条命令的详细信息命令
名称>
mkfs 创建文件系统
mklabel 指定表示分区表类型的磁盘标签。在使用GPT时即指定“gpt”
mkpart 创建分区
mkpartfs 进行分区和文件系统的创建
move 移动分区
print 显示当前的分区表或磁盘标签的状态
quit 结束parted命令
resize 改变分区的大fi
rm 删除分区
select 指定要处理的设备(例如:devsda)
set 设置包括引导标志在内的各种标志
由于 RHEL6 也支持 GPT,因此在容量大于 2TB 的硬盘上进行安装时,会
自动采用 GPT 方式进行分区。
Technical Notes
[2] 使用 RHEL6 Rescue 模式的备份指南(非 LVM 环境 NFS 环
境 uEFI 模式版)http:www-
06.ibm.comjplinuxtechdocattachments003bc366_rhel6-
rescuee383a2e383bce38389e38292e4bdbfe794a8e38197e_12.pdf
4KB扇区的磁盘
以前的传统硬盘,一个扇区固定为 512 字节。而对于大容量硬盘,则通过
增加扇区的大小,来减小访问扇区产生的消耗。
另外,硬盘内部记录了每个扇区错误校验所需要的信息。通过增加扇区的
大小,可以降低这些附加信息所占的百分比,使记录数据的空间得到更加有效
的利用。
6但是,由于访问硬盘的服务器硬件或操作系统(设备驱动)都是按照之前
512 字节的扇区大小设计的,因此不能一味单纯地增加硬盘扇区的大小。
为了实现两者的兼容,就产生了通过硬盘中的控制器来从逻辑上模拟 512
字节扇区的运作方式。如图 1.5 所示,从服务器的角度来看,扇区的大小仍然
是 512 字节,但实际的数据读取和写入是在 4KB 扇区上进行的。
最近有不少新面世的硬盘均采用了这种处理方式。
图 1.5 4KB 扇区的磁盘结构
乍一看,这种转换方式会产生一定的开销。在读取 4KB 扇区中的逻辑扇区
数据时,即便仅仅 512 个字节的读取和写入,也会需要对整个 4KB 扇区进行
操作。但是实际上,若是大数据的读取和写入是从 4KB 扇区的起始位置开始
的,则基本上并不会产生额外的开销。
要有效地实现这样的效果,不仅需要将分区的开始位置与 4KB 扇区的起始
边界对齐,还需要将文件系统的块大小调整为 4KB(4096 字节)(关于文件
系统的块大小,将在下一节中进行详细介绍)。在笔者之前介绍的测试机的例子中,第一个分区 devsda1 是从第 2048
扇区开始的。如果将其转换成图 1.5 的逻辑区段数,就恰好和第 256 个 4KB
扇区的起始边界对齐。因为分区中包含的扇区数也是 8 的倍数,因此分区的结
束位置也就正好是 4KB 扇区的结束边界。
devsda2 和 devsda3 也是如此,这些分区是在 RHEL6 的安装界面
中设置的。可见安装程序很严谨地考虑到了这些问题。
当然,这些都是仅在使用 4KB 扇区的硬盘时才需要注意的事项,但将来一
定会有越来越多的硬盘采用 4KB 扇区。因此,在设置分区的开始位置以及大小
时,将逻辑扇区数设置为 8 的倍数是比较好的 ,这种方法被称为“分区对
齐”。
在用 parted 命令创建 GPT 分区时,默认情况下会指定分区的开始位
置、结束位置和容量(例如 MB)。此时,parted 命令会自动对齐所创建的分
区。如果不放心的话,可以通过“unit s”命令来指定扇区,对分区的开始和
结束位置进行确认。
恢复系统备份后,有时候需要通过 UEFI 的设置界面重新设置启动对象的启动加载文件。点击 [2] 的
链接可以看到使用 IBM System x 时的操作顺序。
此处为日文资料。——译者注
4KB 扇区的磁盘中有一个被称为“对齐偏移”的功能,在该功能有效的情况下,需要将分区的起始位置
设为“8 的倍数 +7”。由于这是 Linux 中不需要的功能,因此在能够通过硬盘的跳线开关等进行更改的情
况下,建议禁用该功能。详情请参考 [3]。
1.2.3 文件系统和 IO 子系统
文件系统的块大小
我们先来重新思考一下文件系统的数据访问。说到文件系统,大家可能会
想到 ext3、ext4 等,但在本节,笔者打算从更为宏观的角度来介绍文件系
统。
首先,Linux 中,有将各种不同的文件系统统一起来的 VFS(Virtual
File System,虚拟文件系统)层,还有通过设备驱动将数据读取或写入物理
7
5
6
7磁盘的块层,它们一起组成了图 1.6 中所示的 IO 子系统。Linux 的文件系
统只是 VFS 层中的一部分。
图 1.6 IO 子系统的结构
Technical Notes
[3] Linux Kernel Watch:超过 2TB!ATA 磁盘的 4KB 扇区问题
是什么?
http:www.atmarkit.co.jpflinuxrensaiwatch2010watch03a.html
前文提到的“文件系统的块大小”,是块层中的设备驱动程序将数据读取
或写入物理磁盘的最小单位。从物理磁盘的结构上看,是以 512 字节的扇区单
位来读取和写入数据的,但很多时候采用较大的单位来读取和写入数据可以更
有效地进行数据交换,而指定这种单位的就是块大小。
在 Linux 中,文件系统的块大小有 1024 字节、2048 字节和 4096 字节这几种选项。默认的块大小被记录在配置文件 etcmke2fs.conf 中,也
可以通过 mke2fs 命令的 -b 参数来明确指定块大小。若要对已经创建好的文
件系统所设置的块大小进行确认,则应使用 tune2fs 命令 。
我们在上一节中提到,在 4KB 扇区磁盘的情况下,应使分区的开始位置与
4KB 扇区的起始边界相对齐,并将文件系统的块大小也设置为 4KB(4096 字
节)。图 1.7 给出了这样做的原因。
图 1.7 中,上图表示了满足这种条件的情况,设备驱动程序对物理磁盘的
访问实际上只对应了一个 4KB 扇区,没有产生无效的数据读写。
图 1.7 4KB 扇区和块大小的映射
下图则表示了分区的开始位置与 4KB 扇区的起始边界没有对齐的情况。打
个比方,假使设备驱动程序写入了一个块。由于物理磁盘不能只对 4KB 扇区的
9一部分进行重写,因此需要先读取出两个扇区的数据,按要求对其中的部分数
据进行重写,然后再将处理后的数据重新写入这两个扇区。这显然会造成额外
的开销。
链接 [4] 里比较了分区的开始位置与 4KB 扇区的起始边界对齐和不对齐
的情况下分别对磁盘访问性能造成的影响。据分析,分区开始位置发生偏离
时,磁盘的数据写入性能会出现显著的下降。
顺便说一下,类似的情形在常见的 512 字节扇区的磁盘中也会发生,当分
区的大小不为块大小的整数倍时,最终会有一个块超出分区。这种情况下,最
终的这个块就不会在文件系统中使用。图 1.8 即为块大小为 2048 字节的一
个例子,这时分区剩下的最后两个扇区就不会被使用。
Technical Notes
[4] 4KB 扇区磁盘上的 Linux :实际建议
http:www.ibm.comdeveloperworkscnlinuxl-4kb-sector-
disks
图 1.8 最后的块超出分区的情况
还是题外话,在 fdisk 命令的输出中,有时块数的值后面可以看到附加的 + 记号。比如“1.2.1 磁盘的 3D 参数”一例中,devsda4 的块数(分
区中包含的扇区数)后就有 + 记号。当块数为奇数值时,便会附加上这个符
号。
这是有其历史原因的。过去的 Linux 文件系统的块大小规定为 1024 字
节,因此当分区中的扇区数为奇数值时,就会剩下最后的扇区不被使用。+ 记
号就是为了表示“该分区最后一个扇区不会被使用”而特意附上的,但现在这
也许是多余的了。
IO子系统的概貌
我们来回顾一下图 1.6 中介绍的 IO 子系统的整体结构,分别从写入数
据和读取数据两种情况进行考虑。
首先来看看写入数据的情形。当数据被写入文件系统的文件中时,其内容
会被暂时录入到磁盘高速缓存中(①)。这时发出写入数据命令的应用程序
(用户进程)会将此步骤视为数据已录入成功,从而进行下一步操作。
然而此时就会有些数据只是被写入到了磁盘高速缓存中,而没有被写入到
物理磁盘中去。这种数据称为“脏数据”。当磁盘高速缓存上的脏数据积累到
一定程度时,文件系统便会向 IO 调度发出请求,将这些脏数据写入物理磁盘
(②)。
这个写入请求将被添加到 IO 调度内部的“请求队列”中。最后,IO
调度器会响应请求队列中的请求,利用设备驱动程序将数据写入物理磁盘中
(③④)。
大家可能听说过多种类型的 IO 调度器,例如 cfq、deadline 等。它
们之间的差别在于使用的算法不同,即按何种顺序处理请求队列中的请求才最
高效。关于 IO 调度器,之后会做更加详细的说明。
此外,表 1.3 中列出的内核参数,能够用于调整脏数据写入的频率。不过
除非有特别的原因,否则是不需要更改默认值的。
表 1.3 与数据的写出频率相关的内核参数内核参数 说明
vm.dirty_background_radio
vm.dirty_radio
试图将缓存中脏数据的百分比保持
在“dirty_background_radio(%)”之下。尤其是当脏
数据的百分比超过“dirty_radio(%)”时,将立即增加
写出频率
vm.dirty_background_bytes
vm.dirty_bytes
以字节为单位操作与上面相同的指定。优先于上面的指
定,设置为0时则保持上面的指定
vm.dirty_writeback_centisecs
vm.dirty_expire_centisecs
每“dirty_writeback_centisecs(单位为1100
秒)”检查磁盘缓存,每超过
1“dirty_expire_centisecs(单位为1100秒)”,就持续写入新的数据
至于数据的读取,则是按以下流程进行的。假设应用程序(用户进程)要
从文件系统的文件中读出数据。如果目标数据已经存在于磁盘的高速缓存中,文件系统便将这些数据返回给进程,从而完成处理(①)。
反之,如果目标数据不在磁盘的高速缓存中,那么 IO 调度的进程会暂停
执行并进入等待状态,之后该文件系统会向 IO 调度器发出读取数据的请求
(②)。
最后,IO 调度器响应请求,通过设备驱动程序把数据从物理磁盘读入磁
盘高速缓存中(③④),解除 IO 调度进程的等待状态,再接着处理磁盘高速
缓存中的数据(①)。
磁盘高速缓存和文件系统属于 VFS 层,IO 调度和设备驱动属于块层,二者相互配合来完成文件中数据的读取和写入。
之所以将它们分为两个不同的层,是基于 Linux 内核中“模块化结
构”的概念。这样就可以将不同类型的文件系统和不同类型的物理磁盘随意组
合起来使用。
再举个极端的例子,即便是没有物理磁盘的文件系统也能实现数据的处
理。例如,ramfs 是利用内存来提供 RAM 磁盘功能的文件系统,它就完全没
有图 1.6 的②(数据传输请求)这个步骤。
那么写入的数据究竟到哪里去了呢?实际上它们都保留在磁盘缓存中。若是一般的文件系统,脏数据一旦被写入物理磁盘,就不再是脏数据,因此需要
从磁盘高速缓存中删除。
但 ramfs 则不同,磁盘高速缓存中的数据永远是脏数据,永远不会被删
除。ramfs 被描述成“基于内存的 RAM 磁盘”,其实准确来说,应该是“基
于磁盘高速缓存的 RAM 磁盘”。
在 Linux 中,还有一个和 ramfs 具备相同功能的 tmpfs。tmpfs 同样
是将数据保存在磁盘高速缓存中,但有别于 ramfs 的是,它的内容还可以成
为“换出”的对象。换句话说,当物理内存不足时,存储在 tmpfs 的文件会
被写入物理磁盘的交换空间中,这样文件使用的内存就可以被释放。关于
tmpfs 这种允许换出的机制,在本章 1.4.1 节中也会所介绍。
看到这里,可能有人会产生这样的疑问:“ramfs 和 tmpfs,到底选用哪
个好呢?”要是只作一般用途,例如应用程序中所使用的数据的临时存储,建
议还是采用 tmpfs。
这是因为,tmpfs 有设置内存使用上限的功能,而 ramfs 则没有这样的
功能。如果无限制地往 ramfs 上保存文件,就会持续消耗内存,而且内存部
分也不能被换出,最终就会因为内存不足而启动 OOM Killer。
顺带一提,ramfs 的原作者正是 Linus 先生,在源码 ramfsinode.c
中还能看到 Linus 先生留下的注释。
NOTE! This filesystem is probably most useful
not as a real filesystem, but as an example of
how virtual filesystems can be written.
以上注释都表明,ramfs 只是作为一个最简单的文件系统的示例而创建出
来的,它并不适合实际使用。
不过,在 Linux 内核中,有一个地方是使用 ramfs 的。在 Linux 的引
导过程中,内核启动后,初始 RAM 磁盘内容 会被装载到 RAM 磁盘空间
中。这里的 RAM 磁盘空间就需要用到 ramfs。
10理解IO调度器
我们继续回顾图 1.6,看看 IO 调度器是如何处理文件系统的数据传输
请求的。磁盘高速缓存和物理磁盘之间进行数据传输时,磁盘高速缓存中数据
的存储方式和物理磁盘中数据的存储方式是不一致的。
首先,在磁盘高速缓存上,一个文件中连续的数据基本上会被写入到连续
的存储空间中。然而,同样的数据在物理磁盘上就不一定会被放置在连续的存
储空间中了,这种情况被称为“磁盘碎片”。
如图 1.9 所示,磁盘高速缓存中连续的数据被分成了两个部分,分别被写
入了物理磁盘上两个连续的扇区内。在本图的例子中,文件的开始和结束部分
的内容被设置在了物理磁盘上相邻的位置。此外,Linux 的内存以 4KB 的页
为管理单位,磁盘高速缓存中 1 个页能写入 8 个扇区的数据。
图 1.9 磁盘高速缓存和物理磁盘的对应示例
正如图例所示,文件系统的作用就是决定如何将文件的数据分配在磁盘
上,而图 1.9 中的对应关系正是记录在元数据区中的。
因此,图 1.6 中文件系统向 IO 调度器发出数据传输请求时,文件系统
就会对物理磁盘上的连续数据进行分割处理。如此一来,IO 调度器就能够有
效地读取物理磁盘上的连续数据。说得夸张点,即便在考虑性能问题时,对 IO 调度器的理解也是非常重要
的,因此这里会对 IO 调度器做尽可能全面的介绍。
首先,文件系统会将数据分配到物理磁盘上的连续空间(连续扇区)中,在内核中整合创建出一个 bio 对象,再把这个 bio 对象传递给 IO 调度
器。从图 1.9 中可以看出,有两个 bio 对象要传递给 IO 调度器。
IO 调度器接收到来自文件系统的 bio 对象,会将多个 bio 对象合并为
一个请求对象,再将它加入到请求队列中。(图 1.10)
图 1.10 IO 调度器的结构
请求对象的合并方法因 IO 调度器而异,一般来说,如果 bio 对象指定
的连续扇区同时也是请求队列中已有的请求对象所指定的连续扇区,那么就会
将这两个对象合并。如果没有一致的请求对象,则创建一个只包含该 bio 对象
的新的请求对象。
最后,物理磁盘空间上的数据才会在请求对象所指定的区域进行读取或写
入。连续扇区上数据的读取和写入就是这样一并进行的。
此外,这些请求对象的实际处理顺序也是因 IO 调度器而异的。
例如 cfq(complete fair queuing)调度器,其进程的 bio 对象会被
加入到不同的子队列中,以便尽量公平处理每个进程的请求。从图 1.10 中可以了解子队列的机制。IO 调度器接收的 bio 对象,首
先会被作为请求对象加入到子队列中。子队列中的请求对象增加到一定程度
后,便会按照实际传输处理的顺序从子队列转移到调度队列中,进行实际的数
据处理。
现在大家应该都明白 IO 调度器的机制了吧。接下来对具体的 IO 调度
器之间的区别进行介绍。Linux 中主要使用的是以下四种 IO 调度器,它们
的特征分别如下:
noop(no optimization)调度器:只有一个子队列,按照 bio
对象被接收时的顺序处理请求。
cfq(complete fair queuing) 调度器:bio 对象被接收后,获取发送该 bio 对象的进程 ID,根据散列函数,将进程 ID 映射到
64 个子队列的某个队列中,再处理各个子队列中一定量的请求。这
样,每个进程的 IO 请求都会被公平执行。
deadline 调度器:分别用不同的队列来维护读请求和写请求。
将接收的 bio 对象根据其对应的扇区位置插入到请求队列中最合适的
位置(位置的排列以尽量减少磁盘头的移动为原则)。但是,当新的
bio 对象被不断插入到请求队列的前方时,队列后面的请求恐怕就会
长时间得不到处理,因此需要优先处理一定时间内(读请求为 0.5s,写请求为 5s)没有得到处理的请求。
anticipatory 调度器:在 deadline 调度器的基础上,追加
了“预测”下一个 bio 对象的功能。例如,当收到进程 A 发出的读
请求后紧接着又收到了来自进程 B 的请求时,就可以预测进程 A 可
能很快会发出另一个读请求,这种情况下就要在调度进程 B 的请求之
前延迟一段时间(0.7 秒左右),以等待来自进程 A 的下一个读请
求。
这些用来处理请求的不同 IO 调度方法也被称为“电梯算法”。因为要在
最大限度减少磁盘头移动的情况下,读取和写入尽可能多的数据,这就好比大
厦里的电梯,通过最少的运动承载尽可能多的人。
11最早在 Linux 中使用的电梯算法是由 Linus 先生设计的,因此过去也曾
被称为“Linus 电梯”。
RHEL6 中默认的 IO 调度器是 cfq。如果需要对单个硬盘所采用的 IO
调度器进行修改,可以通过 echo 命令在 sys 文件系统的特殊文件
sysblock<磁盘名>queuescheduler 中写入 IO 调度器的名称
(noop、anticipatory、deadline 或 cfq)。
下面就是一个变更 IO 调度器的例子,将 devsda 当前使用的 cfq
调度器更改为了 deadline 调度器。
cat sysblocksdaqueuescheduler
noop anticipatory deadline [cfq]
echo deadline > sysblocksdaqueuescheduler
cat sysblocksdaqueuescheduler
noop anticipatory [deadline] cfq
如果只是确认当前的 IO 调度器,也可以使用 lsblk 命令。如下
SCHED 部分表示的就是当前所采用的 IO 调度器。
lsblk -t
NAME ALIGNMENT MIN-IO OPT-IO PHY-SEC LOG-SEC ROTA SCHED RQ-SIZE
sda 0 512 0 512 512 1 cfq 128
├─sda1 0 512 0 512 512 1 cfq 128
├─sda2 0 512 0 512 512 1 cfq 128
├─sda3 0 512 0 512 512 1 cfq 128
└─sda4 0 512 0 512 512 1 cfq 128
如果需要修改系统默认的 IO 调度器,则在 GRUB 的配置文件
bootgrubgrub.conf 的内核启动项中设置“elevator=deadline”。
最后需要注意的一点是,对于被 devsda 等指定的设备,IO 调度器可
以将其想象成图 1.1 中扇区排列在物理磁盘的磁道上那样的构造,实现访问的
最优化。但是在使用外部磁盘装置的情况下,RAID 阵列的逻辑驱动器(LUN)会被
识别为 devsda 等设备。这种时候,存储装置的控制器也能实现磁盘访问的
最优化,就不需要 Linux 的 IO 调度器去进行过于复杂的优化工作了。
仅就一般而言,对于在数据库服务器或服务器虚拟化环境这样使用高性能
外部存储设备的复杂环境中的数据访问,有时侯 deadline 调度器要比 cfq
调度器效率更高。
顺便说一下,虽然 cfq 调度器的解释是“对每个进程实行公平的 IO 处
理”,但是为了公平的处理,有时候也需要刻意营造一些不公平。
大家或许并不熟悉,其实通过 ionice 命令可以对 cfq 调度器中的每一
个进程设置优先级。优先级有三类,分别是 Real time、Best effort 和
Idle。
和系统上的其他进程相比较,Real time 级别的进程的 IO 是最为优先
被处理的。而 Idle 级别进程的 IO 正相反,只有在系统上所有进程的 IO
都处理完了的情况下才处理它。换句话说,除了这类进程之外没有别的进程的
IO 需要被处理,即系统处于空闲状态时,才会对它进行处理。Best effort
则是默认的优先级,基于 cfq 调度器的正常逻辑,公平地进行 IO 处理。
对于优先级为 Real time 和 Best effort 的进程,还能在同一级别中
依次指定数值为 0~7 的优先级参数,数值越小,优先级越高。
因此,对于那些由于特殊原因不允许延迟的进程,就可以将其优先级设置
为 Real time 级别。
例如,RHEL6 标准的 HA 集群系统(High Availability Add-On)中,有一种被称为“仲裁磁盘”(quorum disk)的机制,它通过将数据定期写入
共享磁盘来确认服务器是否运行正常。若写入出现延迟,有时会误报服务器发
生故障,因此就可以考虑将该进程(qdisk)的优先级设置为 Real time 级
别。
但是也要注意,如果某进程有大量的 IO 处理,若还将其优先级设置为
Real time 级别的话,其他进程的处理就会完全停滞,从而引发其他问题。关于 ionice 命令的使用,可以参考手册页。要想确认 ionice 的运行效
果,则推荐试试下面这个命令。
ionice -c 1 dd if=devzero of=tmptmp0 bs=1M count=500 oflag=direct ionice -c
3 dd if=devzero of=tmptmp1 bs=1M count=500 oflag=direct wait
[1] 18764
[2] 18765
500+0 records in
500+0 records out
524288000 bytes (524 MB) copied, 4.59358 s, 114 MBs
[1]- 结束 ionice -c 1 dd if=devzero of=tmptmp0 bs=1M count=500
oflag=direct
500+0 records in
500+0 records out
524288000 bytes (524 MB) copied, 9.3951 s, 55.8 MBs
[2]+ 结束 ionice -c 3 dd if=devzero of=tmptmp1 bs=1M count=500
oflag=direct
这个例子中有两个同时执行的 dd 命令,这里 dd 命令用于导出 500M 大
小的文件,它们的优先级分别被设置为 Real time 和 Idle。Idle 优先级的
IO 处理完全被推迟,在 Real time 优先级的 dd 命令结束后,Idle 优先
级的 dd 命令才开始执行。这样一来,Idle 优先级的执行时间正好是 Real
time 优先级的两倍(图 1.11)。
图 1.11 Real time 级别和 Idle 级别
dd 命令中的指定选项 oflag=direct,表示直接写入物理磁盘而不使用磁
盘高速缓存,常常被用来测量物理磁盘的性能。至于最后 wait 命令的含义,我们将在下一节中进行说明。
8此处为日文资料。——译者注
RHEL6 的 mke2fs 命令,以及 tune2fs 命令对应于 ext4 文件系统。mkfs.ext4 命令,等同
于“mke2fs -t ext4”。
Linux 初始 RAM 磁盘 initrd 是系统引导过程中挂载的一个临时根文件系统,它与 RAM 磁盘空间是
两个概念。——译者注
默认为 26=64。——译者注
8
9
10
111.3 控制进程就等于控制Linux
众所周知,Linux 上的工作都是通过进程来执行的。若把系统的各种行为拟人
化,进程的一生便是时而稳定,时而“死去”又“活来”,可谓十分曲折。有的进
程甚至每天都被严密地监视着。
在本节,我们将再次探讨进程管理机制的基本知识,看看这种机制是如何支撑
和左右这些进程的命运的。尤其在理解了进程的 fork 之后,脚本的创建技术将得
到质的提高。使用 fork 来编写脚本的方法,在本书 3.4.3 节中亦有介绍。
1.3.1 fork 和 exec 分别是进程的分身和变身
登录系统控制台时,需在“login:”提示符处输入用户名进行登录。这
里“login:”提示符表示的是接收用户名的输入,执行该步骤的是 mingetty 进
程。登录成功后,命令提示符下显示 bash 启动。确切地说,是在 bash 进程启动
时,即显示相应的命令提示符。然后,在命令提示符下运行 ls 命令启动 ls 进
程,可以显示当前目录下的文件名。
大家可能觉得这些都是小儿科,实际上,这一连串进程动作的发生隐藏了一个
至关重要的玄机。我们都知道,在 Linux 上创建进程有 fork 和 exec 两种方
法,大家能准确地说出这两者的区别吗?
干脆来做一个小小的实验吧。首先,在运行级别 3 下启动一个 Linux 服务
器,之后 SSH 远程登录,查看 mingetty 进程的执行情况。
ps -ef | grep mingett[y]
root 1692 1 0 19:21 tty1 00:00:00 sbinmingetty devtty1
root 1694 1 0 19:21 tty2 00:00:00 sbinmingetty devtty2
root 1696 1 0 19:21 tty3 00:00:00 sbinmingetty devtty3
root 1700 1 0 19:21 tty4 00:00:00 sbinmingetty devtty4
root 1702 1 0 19:21 tty5 00:00:00 sbinmingetty devtty5
root 1704 1 0 19:21 tty6 00:00:00 sbinmingetty devtty6
grep 命令的参数中,通过将它最后一个字母 y 写成 [y] 的方法,可以确保
12grep 进程名的结果中不包含有与进程名不符的信息 。这里找出了 6 个
mingetty 进程。这六个虚拟控制台,可以通过 Ctrl+Alt+F1~F6 分别切换到它们
对应的“login:”提示符下。
思考一下,为什么 grep 命令中指定的检索字符串就可以顺利地使用正则表达式呢?
在控制台中按下 Ctrl+Alt+F1,出现“login: 提示符”,输入用户名,然后
输入后回车,出现输入密码的提示。这里请先不要输入密码。
这里再次在 SSH 远程登录的终端,通过 ps 命令查看 mingetty 进程。
ps -ef | grep mingett[y]
root 1694 1 0 19:21 tty2 00:00:00 sbinmingetty devtty2
root 1696 1 0 19:21 tty3 00:00:00 sbinmingetty devtty3
root 1700 1 0 19:21 tty4 00:00:00 sbinmingetty devtty4
root 1702 1 0 19:21 tty5 00:00:00 sbinmingetty devtty5
root 1704 1 0 19:21 tty6 00:00:00 sbinmingetty devtty6
mingetty 进程的数量减少了一个,ID 为 1692 的 mingetty 进程似乎消失
了。难道这个进程接收一个用户名就会消失吗?
并非如此。这一次,用 ps 命令检索进程 ID。
ps -ef | grep 169[2]
root 1692 1 0 19:21 tty1 00:00:00 binlogin --
显示的是 login 进程。知道为什么会这样吗?事实上,mingetty 进程在
exec 系统调用的作用下,已经转变为 login 进程了(图 1.12 左图)。
12
12图 1.12 exec 和 fork 中进程的变化
Linux 的进程,除了进程主程序代码外还有其他各种各样的信息,比如用于识
别进程的进程 ID 就是其中之一。除此之外,还有用于和其他进程或文件交换数据
的管道、文件描述符等。
exec 的作用是舍弃进程原本携带的信息,在进程执行时用新的程序代码替代调
用进程的内容。mingetty 进程的工作则是接收登录用户名,但之后对密码的验证
处理工作则移交给 login 进程继续完成。
图 1.13 是 mingetty 进程中运行 exec 的部分源代码。每行开头的编号表
示源代码中实际的行号。454 行的函数 execl,通过 exec 系统调用切换到变
量 loginprog 所指定的程序中去(这个例子中是 binlogin) 。这之后执行
的便是 binlogin 进程,因此通常 455 行之后的代码便不会再运行了。只有在
exec 系统调用出现失败的情况下,才会运行 455 行之后的代码。
调用 exec 系统调用的 C 语言函数有 execl、execlp、execle、execv、execvp 等。execl 和
execv,由于命令的参数的传递方法的不同,即便最后都附加 p,在 PATH 环境变量中进行命令检索时也仍有
区别。
mingetty.c
454 execl (loginprog, loginprog, autologin? -f : --, logname, NULL);
455 error (%s: can't exec %s: %s, tty, loginprog, strerror (errno));
456 sleep (5);
13
13457 exit (EXIT_FAILURE);
图 1.13 mingetty 中运行 exec 的部分源代码
之后,exec 开始执行 login 进程,在接收密码输入完成用户认证后,便启动
用户的 bash 进程。
之前出于实验需要中断了密码的输入,实际上,只有输入密码才算完成了登录
操作。在输完密码的情况下,我们再次检索同一个进程的 ID。
ps -ef | grep 169[2]
root 1692 1 0 19:21 ? 00:00:00 login -- root
root 1818 1692 0 19:29 tty1 00:00:00 -bash
login 进程仍然保留了与之前相同的进程 ID(1692)。另外,此次检索结果
中还包含了 bash 进程。bash 进程本身的进程 ID(1818)与检索的进程 ID 并
不匹配,但是它的父进程的进程 ID(1692)与 grep 检索中的进程 ID 互相匹配
了(ps –ef 命令的输出中依次包含用户 ID、进程 ID 以及父进程的进程 ID)。
这说明 bash 进程是作为 login 进程的子进程开始启动的。
通常我们说“fork 一个进程”,指的是通过父进程创建一个子进程。但现在的
情况是,bash 进程并不是从 login 进程中直接 fork 出来的。从图 1.12 的右
图可以看出,fork 生成的子进程是一个与正在运行的进程完全相同的副本。
login 进程通过 fork 生成一个自身的副本后,又在子进程中通过 exec 启
动 bash,这一技术简称为“fork-exec”。图 1.14 描述了它的整个流程。图 1.14 从虚拟控制台登录时伴随的进程的变化
如果诸位仍觉得对 fork-exec 一头雾水,那么就从 login 的源代码中看看
fork-exec 实际的执行过程吧。Linux 和开源最伟大的地方就是无论什么都能亲自
探寻。关于如何学习源代码,在本书的第 4 章中也有所介绍,这里只是暂时先在图
1.15 中给出答案。以 标记的注释,是笔者自己补充的。
login.c
1183 child_pid = fork; 子进程的进程 ID 赋值给父进程的 child_pid
子进程的 child_pid 中代入 0
1184 if (child_pid < 0) {
fork 失败时的错误处理
(部分源代码省略)
1189 exit(0);
1190 }
1191
1192 if (child_pid) {
父进程从这里开始执行
这个例子中,等子进程结束后,自己也就结束了
(部分源代码省略)
1201 wait as long as any child is there
1202 while(wait(NULL) == -1 errno == EINTR)
1203 ;
(部分源代码省略)
1206 exit(0);
1207 }
1208
子进程从这里开始执行
1209 child
1210
(部分源代码省略)
子进程通过 exec 变换为 bash
1280 execvp(childArgv[0], childArgv + 1);
图 1.15 login 中进行 fork-exec 的源代码
首先,在 1183 行代码中直接执行了 fork。这里的进程一分为二,且都是从
1184 行开始继续执行的。当然这样并没有意义,因为这两个进程是完全相同的,所
以之后子进程需要通过 exec 变换为 bash 进程。
这里用到一个小技巧,即把 fork 的返回值赋给变量 child_pid。fork 执行
失败时,父进程返回负值。fork 执行成功时,父进程以及通过 fork 创建的子进
程返回的值却不相同。父进程的 child_pid 值为子进程的进程 ID,而子进程的
child_pid 值为 0。
因此,父进程在 1192 行的 if 语句上条件成立,并执行 if 语句中的代码。
这个例子中,父进程会一直等待子进程结束,直到子进程退出时,父进程才结束。子进程跳过 1192 行的 if 语句,从 1209 行处开始执行。这个例子中显示的是在
1280 行调用 exec,进程变换为 bash 继续执行。
fork 也可以采用 Perl 等脚本语言来实现。如果你不擅长 C 语言编程,那就
请务必牢记这里所介绍的 fork 的处理技术。
最后来看看进程结束的过程吧。首先,父进程 login 通过执行图 1.15 中
1202 行的 wait 函数,一直休眠直到子进程结束。如下例所示,通过 ps 命令确
认进程的状态,可看到附加的表示休眠状态的 S 符号。
ps aux | grep logi[n]
root 1692 0.0 0.2 77004 2532 ? Ss 19:21 0:00 login -- root
这时在已登录的虚拟控制台上显示的 bash 命令提示符处,输入 exit 进行用
户注销。此时,通过 exit 系统调用,bash 进程被终止,同时发送 CHLD 信号
给父进程 login。
接收到 CHLD 信号的父进程 login 会退出 wait 函数,同时结束进程。wait
是一个函数,它让父进程在接收到子进程 CHLD 信号之前一直保持休眠状态。
而另一方面,子进程向父进程发送 CHLD 信号,直到父进程接收为止,子进程
都处于“僵尸进程”的状态。如果父进程设置为忽略 CHLD 信号,子进程就会一直
保持僵尸状态。
在通过 ps aux 来查看进程状态时,僵尸状态会被标记为Z,信息末尾会显示
。关于 CHLD 信号等这一类的进程信号,请参见相关书籍。
图 1.16 表示的是 bash 进程在被 exit 终止后的一系列流程。从图中可以
看到,login 进程在结束之际向某处发送了 CHLD 信号,那么谁会接收到这个信号
呢?图 1.16 从虚拟控制台退出时伴随的进程的变化
从之前 ps 命令的输出中可以看到,login 进程的父进程是进程 ID 为 1 的
init 进程。因此,init 进程将会接收 CHLD 信号,这时 login 进程就可以从僵
尸状态中解放出来。
另一方面,接收到 CHLD 信号后,init 进程会检测到 login 进程(或是原
始的 mingetty 进程)已经终止了,于是会再度启动新的 mingetty 进程,这样
便可以在虚拟控制台上再次登录了。启动 mingetty 进程需要用到之前介绍的
fork-exec,因此 mingetty 进程相当于 init 进程的子进程。
此外,一直到 RHEL5,init 进程都是根据配置文件 etcinittab 来重新启
动 mingetty 进程的。但如今发展到 RHEL6,init 进程的启动机制已替换为被称
作“Upstart”的新机制,启动时采用的是 etcinit 下的配置文件。
关于 Upstart,5.2 节会有详细的介绍。
最后介绍一下与 fork-exec 机制相关的进程监控的问题。
用 fork-exec 创建新的进程时,最初 fork 的瞬间,子进程的进程名和父进
程的进程名是相同的。之后才通过 exec 变更为新的过程名。因此,在进行进程监视,对特定进程名的进程数量进行检查的时候,在 fork-exec 执行的那一瞬间,需注意进程数量可能大量增加。
如果设定为“不允许存在同样命名的进程”的话,fork-exe 和监视间隔的时
间会出现微妙的一致,从而误检测到异常的进程数。这种现象并不少见,因此为了
避免判断时带来麻烦,请务必注意到 fork-exec 的存在。
1.3.2 作业控制中的各项任务处理
前文介绍了进程启动的基本原理。下面再来介绍一些实用的技术吧。
首先,在 shell 命令提示符下运行命令,通常在命令执行完毕后,命令提示
符会再度出现,这与 login 进程启动 bash 进程的流程(图 1.14)是一样的。
bash 进程首先响应命令,通过 fork-exec 创建了一个新的进程,之后便会一
直处于休眠状态直到子进程结束。接着 bash 进程接收到 CHLD 信号,确认子进程
已终止,再次显示命令提示符,提示接收下一个命令。
在命令的末尾加上“”符号,可以让命令在后台运行。这种情况和上文一样,bash 进程会通过 fork-exec 创建新的进程,不同的是,此时 bash 进程不再需
要等待子进程结束,便可以直接显示命令提示符,提示接收下一条命令。在执行比
较花时间的命令时,让多条命令并行执行不失为一种便利的方法。
但这里存在一个应用的问题。怎样才能让多条命令并行执行,并且等待所有命
令都执行结束呢?
举个例子,可以考虑通过 shell 脚本实现 SSH 在 10 台服务器上执行远程命
令,并等待所有服务器上的命令执行完毕。图 1.17 是笔者实际使用过的 shell
脚本的例子,其目的是在三台服务器(node01~node03)上启动群集服务
(clstart)[5]。为方便起见,将该脚本命名为 clstart_all.sh。
Technical Notes
[5] High Availability Add-On 设计和运用入门“集群的启动顺
序”http:www.slideshare.netenakairhel6-rhcs-guidepreview-
148758112
此处为日文资料。——译者注
例 1 在每一台服务器上执行
!binsh
ssh node01 usrlocalbinclstart
ssh node02 usrlocalbinclstart
ssh node03 usrlocalbinclstart
例 2 命令执行完之前结束
!binsh
ssh node01 usrlocalbinclstart
ssh node02 usrlocalbinclstart
ssh node03 usrlocalbinclstart
例 3 命令执行完之后退出
!binsh
ssh node01 usrlocalbinclstart
ssh node02 usrlocalbinclstart
ssh node03 usrlocalbinclstart
wait
图 1.17 在多个服务器上并行执行命令的 Shell 脚本
(clstart_all.sh)
如例 1 中所示,node01 运行 clstart 结束后,node02 接着执行,即每一
台服务器按顺序依次执行。这显然是很浪费时间的。
因此,在例 2 中,笔者尝试在后台执行每个命令。这种情况下,三台服务器能
同时正确执行 clstart 命令。但是,在每一台服务器上的 clstart 命令运行结
束前,脚本的运行就已经结束了。
14
14这里就出现了一个问题,即没法在所有服务器的群集服务启动完成后,再接收
下一条输入命令。因为运行于后台的命令还未显示输出信息,命令提示符的显示就
已经提前出现了,从而导致控制台显示出现混乱。
于是,例 3 中作了进一步的改良。虽然只是在最后添加了 wait 命令,但这
是 bash 的一个内部命令,其功能与在图 1.15 的第 1202 行出现的 C 语言的
wait 函数相类似。
wait 命令执行后,clstart_all.sh 便进入休眠状态,等待后台运行中的子
进程全部结束后发来 CHLD 信号通知。所有的子进程结束后,再处理 wait 之后的
下一个命令。
这里 shell 脚本的 wait 命令结束了,因此所有服务器上的 clstart 命令
结束的时候,clstart_all.sh 也就结束了。这种处理方法被称为“异步执行的等
待处理”。
所谓“异步执行”,指的是各自处理后台进程。另外还有一个“等待处理”,指的是父进程在等待所有子进程都报告执行完毕后,再进行下一步处理。这是一种
常用技术,例如在多线程并行编程中就常常被使用到。
此外,在例 2 中,后台 ssh 命令仍然还在执行,其父进程 clstart_all.sh
便先行结束了。这种父进程先于自己结束的子进程便被称为“孤儿进程”。
由于父进程已结束了,进程结束时发出的 CHLD 信号便无法被接收,从而产生
僵尸进程。因此一旦产生孤儿进程,进程 ID 为 1 的 init 进程便会自动成为它
们的父进程。孤儿进程所发出的 CHLD 信号,均由 init 进程接收并确认进程结
束,自然也就不必担心产生僵尸进程了。
以上示例均展示了如何通过 shell 脚本在后台执行子进程。另外,直接在后
台执行命令提示符下达的命令时,应格外注意 shell 输出重定向的最终处理。
若将后台执行命令的输出结果直接显示在屏幕上,接下来输入的命令就会将前
面的输出结果覆盖掉,这是很麻烦的事情。因此可以将在后台执行的命令的屏幕输
出,重定向到文件中。以下列举了三种具有代表性的例子。 command >cmd.log 2>cmd.err ←-标准输出写至文件cmd.log中,标准错误输出写至文件cmd.err中
command >cmd.log 2>1 ←--------------标准输出和标准错误输出都写至cmd.log中
command >devnull 2>1 ←------------丢弃标准输出和标准错误输出
一般来说,在屏幕上输出的 Linux 的进程内容分为两类:“标准输
出”(stdout)和“标准错误输出”(stderr)。
默认情况下两者都会在执行命令的终端上输出内容,但 stdout 和 stderr 被
定义为程序中的两个不同输出来源。进程的正确处理结果被送往标准输出,其他的
错误信息则被送往标准错误输出。
第一个例子中,命令(command)的标准输出写至文件 cmd.log 中,标准错误
输出写至文件 cmd.err 中。标准输出和标准错误输出,分别用文件标识符 1 和 2
来标识,因此正确的写法应该为“1>cmd.log 2>cmd.err”。但是,由于表示标准
输出的“1”是默认值,故在这里可以省略不写。若需要在现有文件之后附加内容,就请用“>>”来代替“>”。
在第二个例子中,标准输出和标准错误输出都写至同一个文件 cmd.log
中。“2>1”表示的是,将标准错误输出和标准输出统一到一个位置。
最后一个例子中,是将输出写入一个特殊文件 devnull 中。devnull 会
将输出内容作丢弃处理,稍微了解一点 Unix 知识的人会称之为“黑洞”。这里需
要注意的是,如果出现拼写错误,例如写成“devnul”,就会在 dev 之下创
建一个输出文件。
需要与 devnull 一起记住的是 devzero。这是一个持续读取字节码
0(NULL 值)的特殊文件。如下所示,通过这种方式可以创建内容为 NULL 的
100M 的空文件。
dd if=devzero of=tmptmp0 bs=1M count=100
为了让 top 命令的执行更有意思,可以让 CPU 持续不断地被使用,下面就是
一个类似的有趣的例子。 cat devzero >devnull
还应该注意的一点是,用 exit 命令(或者是 logout 命令以及 Ctrl+D
等)结束 bash 时的动作。
运行于终端屏幕上的 bash 进程,会在结束的时候向每一个子进程发送 HUP
信号。由于 HUP 是一个令进程终止的信号,因此在这种情况下,运行于后台的命令
也会一并结束。
大家或许都有过如下的困扰。比如在后台运行较为耗时的基准测试程序
(benchmark)时常以失败告终,或者是在 PC 上使用 Putty 连接远端服务器
来运行基准测试程序时,由于运行太过耗费时间,便不得不让 PC 长期开着。
Putty 的正式写法是 PuTTY,它是一个 Telnet、SSH、rlogin、纯 TCP 以及串行接口连接软件。
——译者注
使用 nohup 命令即可解决这个问题,它可以在命令执行时设置成忽略 HUP 信
号。使用方法如下图所示。
nohup command >cmd.log 2>1
这样一来,即便通过 exit 命令结束了 bash 进程,运行于后台的命令
(command)也能无视 HUP 信号继续执行。但是由于身为父进程的 bash 进程结
束了,因而 sbininit 就成为孤儿进程的新的父进程。
除了 nohup 命令以外,screen 命令也很实用。通过使用 screen 命令,能
够在 Putty 的文本终端上创建多个虚拟终端窗口。并且即便关闭了 Putty 的文本
终端,这些虚拟终端仍会继续运行。这种情况下,重启 screen,便能再次连接上
之前的虚拟终端而恢复工作。
这么解释或许难以理解,还是来实际操作试试吧。因为并不一定默认安装
screen 命令,所以如果 screen 命令不存在的话,则导入同名的 RPM 包即可。
yum install screen
15
15在 SSH 连接终端上,运行 screen 命令。
screen
终端窗口顶部的标题显示的是 [screen 0:bash],它表示连接的是第 0 个虚
拟终端。在顶层窗口运行相应的命令,例如输入 Ctrl+A C(先按下 Ctrl+A,再按
下 C),就创建了第一个新的虚拟终端。此时顶层窗口消失,屏幕上重新显示
bash 命令提示符,窗口的标题部分变为 [screen 1:bash]。
若多次输入 Ctrl+A C,就能随意创建新的虚拟终端。之后输入 Ctrl+A“0~9
之间的数字=>”,就能返回到任意指定数字的虚拟终端上。例如,当你想返回第
0 个虚拟终端,输入 Ctrl+A 0,屏幕上就会显示运行中的 top 命令。至于 10
以后的虚拟终端,在 Ctrl+A(单引号)后加上数字即可。还可以用 Ctrl+A N
和 Ctrl+A P 在虚拟终端间进行前后顺序的来回切换。
此外输入 Ctrl+A D,还可以离开创建的虚拟终端,返回 screen 命令执行之
前的屏幕。乍一看似乎 screen 命令已经结束了,但实际上它仍然在后台运行,之
前创建的虚拟终端的会话也仍然存在。
终端关闭后,需要重新建立一个 SSH 连接,运行附加了 -r 选项的 screen
命令。
screen -r
输入 Ctrl+A D 之前的虚拟终端窗口再次出现,这样就可以恢复工作了。
通过这种方式,便可以在 screen 命令的虚拟终端上放心地运行耗时的基准测
试程序。screen 命令也能用于创建自定义的操作环境,具体可以参考手册页。
最后,来介绍一下任务管理的基本命令。
在 bash 命令行的末尾附加 可以让进程运行于后台,此时除了进程 ID外,还会被分配一个只能在 shell 环境中使用的“作业 ID”。
使用 jobs 命令可以查看正在运行中的作业。当 kill 命令向指定进程发送信
号时,可以使用作业 ID 替代进程 ID,例如“%1”即表示指定作业 ID 为 1 的进
程。
至于没有加上 符号的在前台运行的命令,通过 Ctrl+Z 将它暂停之后再运
行 bg 命令,就可以将其切换到后台。如果运行的是 fg 命令,则进程会再度回到
前台运行。
或者指定作业 ID 并执行 fg 命令,也可以将运行于后台的命令切换到前台。
图 1.18 作业的状态变化
笔者有时在 vi 编辑器中进行操作时,会不小心误按 Ctrl+Z。这时编辑器的
界面会消失并返回到命令提示符处。出现这种情况时请不要惊慌,只需运行 fg 命
令,就能安全返回 vi 编辑器的界面了。
1.3.3 快速的数据处理管道
最后就用管道处理的知识来结束进程这个话题吧。虽然管道只是一个基础知识,但其中蕴含着 Linux 从 Unix 中继承的重要概念。
管道机制是由 Doug McIlroy 发明的,他写下了如下这段话:
“程序应该只关注一个目标,并尽可能把它做好。程序应能够互相协同工
作。应该让程序处理文本数据流,因为这是一个通用的接口。”
Douglas McIlroy 是 Unix 系统上管道机制的发明者,也是 Unix 文化的缔造者之一。这三句话是他
归纳的 Unix 哲学,非常经典。原文:Write programs that do one thing and do it well. Write
programs to work together. Write programs to handle text streams, because that is a
universal interface.——译者注
说这段话的前提在于,“用户比起程序员更加清楚应该用电脑干嘛 ”。用户
利用管道的机制,能够自由组合程序员提供的的各种功能,从而实现必要的处理。
与此相反的想法是,“由程序员来决定程序的功能(用户需要的功能)”。基于这一想法开发的应用程
序虽然使用简单,但是存在应用不佳的缺点。由于用户所需要的功能是与时俱进的,因此经常需要进行大规模
的更新。
特别是服务器管理员,经常被要求在现有条件下充分利用可用的工具,快速完
成工作。虽然很多便利的工具在 Web 上就能检索到,但是如果没有时间安装这些工
具,或者是服务器不允许安装多余的工具,这个时候就需要开动脑筋对 Linux 服
务器里的自带工具进行灵活组装了。
管道处理的基础是标准的输入输出数据。之前介绍了程序的两种输出:标准输
出(stdout)和标准错误输出(stderr)。另外,程序的数据输入源是标准输入
(stdin)。默认情况下,标准输入指的是从终端的键盘输入。
例如 cat 命令,通常用于将指定文件名的内容输出到屏幕。如果没有指定文件
名,则把从标准输入中接收的内容一行一行地依次发送到标准输出上(图 1.19 的
上图)。
16
16
17
17图 1.19 标准输入输出和重定向
cat
Hello, ←-------------从键盘输入
Hello, ←-------------在屏幕上输出相同的内容
World! ←-------------从键盘输入
World! ←-------------在屏幕上输出相同的内容
←--------按Ctrl+D结束
把标准输入输出的位置从默认的键盘和屏幕切换到文件,这种处理称为“重定
向”。在之前 stdout 和 stderr 的重定向的介绍中,提到过 > 符号的使用方
法。这里将标准输入的位置变更为文件的话,使用 < 符号即可(图 1.19 的下
图)。
下例是先把从键盘输入的内容保存到文件 greeting.txt 中,再在屏幕上重新
显示。 cat > greeting.txt
Hello, ←-----------从键盘输入
World! ←-----------从键盘输入
←--------按Ctrl+D结束
cat < greeting.txt
Hello,World!
而将这两个进程的标准输出和标准输入直接相结合的,就是管道(图 1.20)。
两个命令之间用“|”号相连。
图 1.20 管道处理
此前,使用 grep 过滤 ps 命令的输出结果便可以写成如下。
ps -ef | grep mingett[y]
当然,也可以通过管道来连接三个或更多的命令。大家以前使用的命令中有多
少使用了管道呢,回想一下是不是很有意思?
笔者回想了一下,发现自己大量使用的是“history|grep~”。在确认过去执行的命令的选项时,常常
会用到这种方法。
history | grep |
在管道中,除 grep 命令外,还常常会用到 cut、tr、sort、uniq、paste、wc、head 等命令。表 1.4 总结了这些命令的使用说明。关于每个命令的更多详细
信息,请参考手册页。
18
18表 1.4 管道中常用的快捷命令
命令 说明
cut 通过分隔符拆分后,显示指定的域
grep 显示与模式相匹配的行
head 显示文件的开始部分
paste
通过指定的分隔符将两个文件的各行进行合并,或者通过指定的分隔符合并一个文
件中的多行
sort 对多行进行排序
tr 替换、删除字符,压缩文字序列
uniq 压缩连续的相同的行
wc 显示字节数、字数、行数
接下来做一个练习题吧。首先,将命令“ps -ef”的输出保存到文件
tmptmp0 中。用 head 命令查看前五行的内容,其结果如下所示。
ps -ef > tmptmp0
head -5 tmptmp0
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 Aug26 ? 00:00:06 sbininit
root 2 0 0 Aug26 ? 00:00:00 [kthreadd]
root 3 2 0 Aug26 ? 00:00:00 [migration0]
root 4 2 0 Aug26 ? 00:00:02 [ksoftirqd0]
举一个现实中的例子,通过 cron 作业将过去的 ps 命令的输出保存在文件中,以便之后可以作各种各样的分析。但在这里,请仅从文件中提取进程的
ID(PID),然后写入另一个文件中。
方法还有很多,请大家既不要检索 Web 也不要参考手册页,用立刻就能想到的
方法来试试看吧。
下面的例子给出了一个比较经典的答案。
cat tmptmp0 | tr -s | cut -d -f 2 | grep -v PID > tmptmp1
cut 命令把 -d 选项指定的文字作为文字分隔符,仅抽取 -f 选项定位的数据
(域)。这个例子中,使用空格作为分隔符。但是,由于连续的空格会被当作多个
分隔符,因此要事先通过 tr 命令将连续空格转换成一个空格。记住 tr 命令和
cut 命令这个组合,用起来十分便捷。
最后,使用 grep 命令删除标题行,将结果写入文件 tmptmp1 中。grep
命令的 -v 选项的意思是,只取出与条件不一致的行。
此外,使用这种方法的时候,要注意行的顶头是否包含了空格。这样的行中,因为顶头的空格也会被当作分隔符,所以域的位置就会后错一位。后面将会介绍如
何删除行顶头处的空格。
tr 命令和 cut 命令的组合也可以用 awk 来替代。
cat tmptmp0 | awk '{print 2}' | grep -v PID > tmptmp2
awk 使用连续的空格作为分隔符,自动分解输入的数据。这里 print2 指的
是打印位于第二个字段的数据。由于行头的空格会被自动忽略,因此不需要像前例
那样事先去除位于行头的空格。
如果想使用空格以外的分隔符,可以通过 -F 选项来指定。下面是一个被保存
在 etcpasswd 文件中的用户名列表的输出例子。
cat etcpasswd | awk -F : '{print 1}'顺便说一下,如果需要输出正在运行的进程的 ID 列表,使用 ps 命令的选项
即可,不需使用管道。
ps -e --no-headers -o pid > tmptmp3
但是,使用这个选项就和之前保存的形式略有不同。之前不论是 tmptmp1
还是 tmptmp2 中进程 ID 的记录都是采用行头左对齐的方式,但是在
tmptmp3 中,进程 ID 则采用右对齐,且行头包含空格。
这里可以稍微多做一些工作,组合使用 head 命令和 tail 命令,去除文件中
的部分内容进行比较。
head -98 tmptmp1 | tail -6
976
992
994
1044
1045
1046
head -98 tmptmp3 | tail -6
976
992
994
1044
1045
1046
如上图所示,tmptmp3 的行头的确包含了连续的空格。接下来就思考一下如
何去除行头空格的方法吧。
对于这种置换处理,比较经典的方法是使用 sed。如果需要将每行中的 AAA
替换成 BBB,一般用“sed sAAABBBg”即可。s 即为用来指定替换的 sed
命令,g 则表示替换行内出现的所有 AAA。如果省略了最后的 g,则只会替换行内
出现的第一个 AAA。tr 命令也可以实现相同的处理,不过和 sed 不同的是,后者允许使用正则表
达式。由于这里我们需要去掉行头的连续空格,因此如下用空字符串来替换即可。
cat tmptmp3 | sed s^ > tmptmp4
在 shell 脚本中常常会用到 awk 和 sed,这是读懂别人所写的 shell 脚本
的必备知识。虽然 Web 上就有铺天盖地的关于这方面的知识,但笔者还是建议大家
多找些 [6] 这一类的书,深入地研究学习一下。此书对 sed 和 awk 中允许使用
的正则表达式也做了详细的解说。
awk 尤其具有很强的通用性,不仅可以进行文本处理,还可以用来代替 perl
等高级语言。当然,用 perl 就可以轻易实现的处理就没必要非得用 awk 了,但
把 awk 作为一种技巧,总能在意想不到的地方起到意外的作用 。
笔者之前还曾把用 Perl 编写的脚本移植到不支持 Perl 的陈旧的 Unix 服务器中,进行从 Perl 到
awk 的移植操作。
在管道处理中,结合使用符号 也是一种便利的技术。这是 bash 的一个
功能,把命令输出插入 内,作为命令的一部分。举个例子,请思考下面这个
命令是用来干什么的。
yes kill | head -(cat tmptmp4 | wc -l)
中,输出的是文件 tmptmp4 中所包含的行数。假设这个结果是 150,那么这个命令等效于下图。
yes kill | head -150
上图表示的是 150 行“kill”的字符串。yes 命令用来无限输出指定的字符
串,这里即表示提取了前 150 行。
有一点需要注意,head 命令并不是在 yes 命令结束之后才执行的。因为如果
是那样的话,yes 命令永远不会结束,管道处理也就永远不会结束了。
19
19实际上,head 命令在 yes 命令开始输出的同时就接收其输入了。head 命令
接收了 150 行的输入后,就会先于 yes 命令结束。
Technical Notes
[6] 《sed 与 awk(第 2 版)》Dale Dougherty、Arnold
Robbins(著),张旭东等(译),机械工业出版社,2003
此时,yes 命令的输出已无法被接收,但如果它试图再次输出的话,就会收到
pipe 信号,然后 yes 命令才会终止。
当然,这只是一个虚构的小例子,下面我们来看一个经常会用到的实例。
mkinitrd -f bootinitramfs-(uname -r).img (uname -r)
这是一个用来指定启动内核的版本号,创建初始 RAM 磁盘文件的命令。uname
–r 的运行结果就是内核的版本号。
介绍完了管道处理,下面我们来作一些练习吧。
① 将用 root 用户以外的用户权限运行的进程 ID 写入文件 tmptmp0
中。
② 在文件 tmptmp0 每行的开头,增加字符串“kill -9”,然后再保存到
文件 tmptmp1 中。
③ tmptmp1 可以作为一个脚本,用来杀死 root 用户以外的进程。不使用
tmptmp0 这样的中间文件,请组合出一个能够执行相同处理的命令。
以上这些小问题,都是为了让大家了解更多不同的命令。当然,每一题的答案
也都不是唯一的。
①的答案
首先提供一个利用了管道处理方法的解答。 ps -ef | grep -E -v ^(root|UID) | awk '{print 2}' > tmptmp0
grep 命令指定了选项 -E,就能使用范围更广的正则表达式,即“扩展正
则表达式”。使用扩展正则表达式来匹配每行开头中的 root 或者是 UID,这
里将包含它们的所有行都删去。UID 位于 ps 命令第 1 行的标题部分。
但如果是查找满足一定条件的进程的进程 ID,则使用专门的 pgrep 命令
更为简单。
pgrep -v -u root > tmptmp0
有关 pgrep 命令中选项的更多信息,请查看手册页。比起使用复杂的管
道,这个例子教会我们,调查一下是否有实现同样功能的命令也很重要。
②的答案
前面的例子介绍了用空字符串替换(也就是删除)行头的连续空格。这里
却恰恰相反,用指定的字符串替换行头的空字符串,即在行头插入指定的字符
串。下面是一个使用 sed 的例子。
cat tmptmp0 | sed s^kill -9 > tmptmp1
另外,使用 awk 也是可行的。
cat tmptmp0 | awk '{print kill -9 0}' > tmptmp1
awk 中,n 表示的是第 n 个域,0 表示的是域分解前的整行字段 。
除此之外,也可以使用之前 yes 命令一例中的方法。
yes kill -9 | head -(cat tmptmp0 | wc -l) | paste -d - tmptmp0 >
tmptmp1
20paste 命令,是用选项 -d 指定的分隔符将两个文件按行合并。在这个例
子中,第一个文件名的地方使用的是 -,这意味着使用的是标准输入而不是文
件。生成与 tmptmp0 同行数的 kill -9,再通过 paste 命令将它们与
tmptmp0 进行合并。
做到这一步或许有些多余,但都是为了让大家记住 paste 命令的使用方
法。之后还会介绍其他用例,请提前参看手册页中关于 paste 命令的 -s 选
项。
③的解答
利用管道完美地结合①和②的答案,就可以避免使用 tmptmp0 作为中
间文件,但这里就不再详述了。实际上,和 pgrep 命令一样,有专门的
pkill 命令用来实现这种功能。
pkill -9 -v -u root
当大量的进程一起停止的时候,就需要将通过 ps 命令显示的进程 ID 逐
条复制和粘贴。但如果使用 pgrep 命令和 pkill 命令,就能简化这些工作。
所以请务必多加练习这些方法。
关于管道处理的介绍就到此结束了。要知道同样的工作可以有一百种方
法。要判断哪种方法最合适是需要经验的,这里介绍“透明
性”(transparency)和“发现性”(discoverability)这两种思路供参
考。
所谓透明性,指的是有经验的人看到代码时,能很快理解该代码是用做什
么处理的。可以说透明性高,就意味着较难发生意料之外的错误(即所谓的
bug)。可以这么想,别人看不懂的复杂代码,其中一定存在你自己也不能理解
的潜在问题。
而发现性,指的是即使不通过努力钻研,也能立刻上手使用代码,没有经
验的人使用起来也能得心应手。高发现性的代码,能够有效地应用于各种场合。
以上观点都是为了避免过度使用复杂选项。毕竟在不能详细检查命令选项
的情况下,可以快速应用的技术是非常重要的。
此外,由于透明性和发现性并非一定同时成立,因此需要根据实际状况来
判断优先考虑哪个。推荐大家从这两者角度,重新看看曾经使用过的命令或者
写过的脚本。
顺便提及,[7] 是一本通俗易懂的经典著作,讲述了设计开发 Unix 的一
群人的思想和经验教训。“只知道 Linux,没有接触过 Unix”的年轻工程师
们,读一读这本书,或许能从一个全新的视角来审视 Linux 呢。
awk 借用 shell 的方法,用 1,2,3... 这样的方式来顺序地表示行中的不同字段,用 0 表示整
个行。——译者注
201.4 至关重要的内存管理
最后让我们进入 Linux 内部结构的关键部分——内存管理吧。因为 Linux
内核中的管理机制对 Linux 的内核性能有很大影响,因此其构造被设计得极为精
妙。
例如,上一节介绍的管道处理是由多个进程共同完成同一项工作,此外,还有
在一个进程内处理多个操作的“多线程”机制。虽然很多人认为“多线程的性能优
于多进程共同工作的机制”,但也不能盲目地这么理解。
在早期的 Unix 系统中,利用之前介绍的 fork 和 fork-exec 启动新进程
时,由于从父进程往子进程复制内存信息需要花费一定的时间,因此启动多个进程
时难免性能表现不佳。但是,现在的 Linux 内核中则不会发生类似的情况。通过
之后将要介绍的“写时复制”(copy-on-write)等机制,反而可以更加高效地创
建进程。
说句题外话,关系数据库(RDB)是一个高性能的应用。开放源代码中有代表性
的 RDB 有 MySQL 和 PostgreSQL,相对于使用多线程的 MySQL,PostgreSQL
采用的就是多进程共同工作的机制。
Technical Notes
[7] 《UNIX 编程艺术》Eric S.Raymond(著),姜宏等(译),电子工
业出版社,2006
至于这样设置的原因,一位 PostgreSQL 的开发者给出了如下回答:“利用多
线程来提高性能,需要复杂的机制,其结果会导致程序中出现 bug 的可能性升高,而分析这些问题也需要花费更多的时间”。这种思考方法可谓与之前介绍的“透明
性”和“发现性”不谋而合。
很多书已对 Linux 的内存管理做过全面介绍,本书的侧重点在于对进程管理
和磁盘管理的相关内容做更进一步的深入讲解,比如用户进程中使用的内存管理
等。1.4.1 物理内存的分配
首先复习一下内存管理的基础——物理地址空间和逻辑地址空间的关系。
安装于服务器中的物理内存,大致可分为 Linux 内核自身使用的区域和用户
进程使用的区域。图 1.21 是 x86 架构(32 位)的分配方式。Linux 内核仅使
用 1MB~896MB 空间大小的低端内存,其他没被使用的空间则被分配给了用户进
程。如图 1.21 中,896MB 以上的高端内存空间被分配给了用户进程,不光如此,低端内存中的空闲区域也被分配给了用户进程使用。
图 1.21 内核内存和用户进程内存的配置(x86 架构)
此外,包括内核本身在内,服务器上运行的程序在访问物理内存时,并不直接
指定物理地址(图 1.21 中的“物理地址空间”所指定的地址),而是指定逻辑地
址(图 1.21 中的“逻辑地址空间”所指定的地址)。首先在内存的内核数据区域
中预先设置逻辑地址和物理地址的对应“页表”,然后 CPU 上搭载的
MMU(Memory Management Unit)硬件会参照该页表,自动实现对映射后物理地址
上的数据的访问。从图 1.21 中可以看出,因为每个进程有不同的逻辑地址空间,所以分别为每
个进程准备了一个页表。MMU 参照的是该 CPU 上运行的用户进程所对应的页表。
此时,内核使用的低端内存区域,会在全部页表中被共同映射到 3GB ~4GB 的
逻辑地址空间上。因此,不管当前运行的进程是什么,Linux 内核本身就能始终使
用 3GB ~4GB 的逻辑地址。
像这样,通过为每个进程提供独立的逻辑地址空间,每个进程的内存访问就各
自独立。进程 A 不能从自己的逻辑地址空间访问到进程 B 的物理内存,这也等于
实现了进程之间的安全保护。
虽然图 1.21 是基于 x86 架构的,但在 x86_64(64 位)架构下,其基本机
制也不会改变。
X86 架构中的逻辑地址空间范围限制在 4GB,其中内核可以使用的地址空间也
限制在 3GB~4GB 之间 。但在 x86_64 架构下,其逻辑地址空间范围不限定于
4GB,因此内核可以自由使用更大范围的内存空间。
用户空间占 3G,内核空间占 1G。——译者注
这样一来,也就不存在低端内存和高端内存之分了。在 x86_64 架构中,所有
内存空间都被认为是可供内核使用的低端内存。
内核管理知识的复习就到此为止。下面来看一看可分别供用户进程和内核使用
的内存有哪些类型。这里,我们把用户进程运行使用的内存以及磁盘高速缓存等称
为“用户内存”,其他用于内核自身运行的内存称为“内核内存”。
参考 proc 文件系统中的特殊文件 procmeminfo,我们来介绍一下用户内
存和内核内存分别有什么样的类型,以及如何查看它们各自的使用量。从
procmeminfo 中可以得到内核使用情况的各种信息,但由于基本是把内核内部的
管理信息原封不动地输出,如果不清楚内核的内部构造,也就无法理解其中的含
义。
于是干脆反其道而行之,通过这个输出来理解与内存管理相关的内核的内部构
造。
21
21用户内存的分类
图 1.22 是在笔者在 RHEL6.2 服务器上的输出。从最开始的 2 行
(MemTotal,以及 MemFree)可以看出,安装了空间为 8GB 的物理内存,当
前的空闲内存为 3GB 左右。其后显示的是各种各样的信息。
之前说的“用户内存”,是通过① ~ ④以及紧接其下的 Unevictable 的
总和来分配的。这个例子中约为 4.3GB。① ~ ④的值可以按表 1.5 进行整
理,至于 Unevictable,我们稍后再作介绍。
表 1.5 用 LRU 列表对用户空间的使用内存进行分类
Active Inactive
匿名内存 3282428KB 588724KB
File-backed内存 219448KB 214596KB
匿名内存和 File-backed 内存的区别在于,物理内存的内容是否与物理
磁盘上的文件相关联。
匿名内存,是用来存储用户进程用作计算的中间数值的,灵活确保程序在
执行时有可用的内存空间。如果熟悉 C 语言的话,说成是“通过 malloc
分配的内存”会更容易理解,其内存中的内容自然与物理磁盘上的文件没有任
何关系。
cat procmeminfo
MemTotal: 8069288 kB
MemFree: 3051032 kB
⑤ Buffers: 149412 kB
⑥ Cached: 308364 kB
SwapCached: 1352 kB
Active: 3501876 kB ←---Active(anon) + Active(file)
Inactive: 803320 kB ←---Inactive(anon) + Inactive(file)
① Active(anon): 3282428 kB
② Inactive(anon): 588724 kB
③ Active(file): 219448 kB
④ Inactive(file): 214596 kB
Unevictable: 0 kB Mlocked: 0 kB
SwapTotal: 2097144 kB
SwapFree: 2093400 kB
Dirty: 52 kB
Writeback: 0 kB
⑧ AnonPages: 3846228 kB
Mapped: 31616 kB
⑦ Shmem: 23732 kB
⑨ Slab: 564328 kB
SReclaimable: 242284 kB
SUnreclaim: 322044 kB
KernelStack: 3248 kB
PageTables: 50484 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 6131788 kB
Committed_AS: 7762476 kB
VmallocTotal: 34359738367 kB
⑩ VmallocUsed: 385112 kB
VmallocChunk: 34359342120 kB……( 以下省略 )……
图 1.22 procmeminfo 中的内存信息
而 File-backed 内存作为磁盘高速缓存的内存空间,其物理内存中的内
容与物理磁盘上的文件是相对应的。例如,将物理磁盘上的文件内容读入到磁
盘高速缓存中使用时,同样的数据也同时存在于物理磁盘上。此外,还有用
于“文件映射”的内存空间,能将物理磁盘上的文件内容与用户进程的逻辑地
址直接关联。
此外还有 Active 和 Inactive 的区别,即该内存上的数据最后一次被
使用的时间。包含有刚被使用过的数据的内存空间被认为是 Active 的,包含
有长时间未被使用过的数据的内存空间则被认为是 Inactive 的。当物理内存
不足,不得不释放正在使用的内存空间时,会首先释放 Inactive 的内存空
间。
匿名内存和 File-backed 内存的内存释放方法也有所不同,这一点稍后再作讨论。
为了将用户内存分为以上 4 类,Linux 内核中有着与表 1.5 相对应的 4
类“LRU(Least Recently Used)列表”,用户内存的各个内存页都会记录
在其中某一个列表上。LRU 在日语中是“最不常用”的意思,前文中提到,包
含长时间未被使用过的数据的内存空间会被优先释放,由此而得名。
“内存页”这个术语可能是第一次出现。Linux 内核将物理内存分割成
4KB 大小的“页”来进行管理,内存的分配、释放等处理都是以页为单位进行
的。
至于 Active 和 Inactive 的分类,则是根据内存的使用情况而灵活变
动的。当对 Inactive 的 LRU 列表记录的内存页进行访问时,该内存页便会
移至 Active 的 LRU 列表中。反之,Active 的 LRU 列表记录的内存页中如
果有长时间不被访问的数据,又会被移至 Inactive 的 LRU 列表中。
接着补充说明一下 Unevictable。前文介绍了“4 种 LRU 列表”,实际
上,还有一种 Unevictable 列表,记录的是“Unevictable”类型的内存
页。
在下一节中会介绍到,使用 LRU 列表对内存页进行分类,是为了更有效地
对内存进行释放处理。然而,原则上内存页中也有不能被释放的部分。这种内
存页,记录到之前介绍的 4 种 LRU 列表中是没有意义的,因此改为记录到
Unevictable 列表中。
这里通过 free 命令查看同一台服务器的内存使用情况,其结果如下所
示。
free
total used free shared buffers cached
Mem: 8069288 5018388 3050900 0 149420 308364
-+ bufferscache: 4560604 3508684
Swap: 2097144 3744 2093400
一般认为,在 free 命令的输出中,buffers 和 cached 的值的总和就是可以使用的磁盘高速缓存的大小。图 1.22 中的⑤⑥即表示了这两个值。
那么,“buffers+cached”的值和表 1.5 中的分类又会有什么样的关系
呢?
如果囫囵吞枣地理解先前的介绍,会觉得因为磁盘高速缓存属于 File-
backed 内存的一种,所以 File-backed 内存的总和即
为“Active(file)+Inactive(file)”所对应的值。
但从实际计算的结果来看,如下所示,buffers+cached 的值要多出
23732KB。
1. buffers+cached = 457776KB
2. Active(file)+Inactive(file) = 434044KB
仔细观察图 1.22 就会发现,它们之间的差与⑦ Shmem 所表示的值刚好
相同,这绝不是巧合。所谓的 Shmem,指的是 tmpfs 所使用的内存。
在这里先回顾一下 tmpfs。如同在 1.2.3 节中提到的,tmpfs 即利用物
理内存来提供 RAM 磁盘功能。安装的 tmpfs 类型的文件系统和普通的文件系
统一样,可以进行读写操作,但是文件的实体并不是物理磁盘,而是保存在服
务器的物理内存上。
在 tmpfs 上保存文件时,文件系统会暂时将它们保存在磁盘高速缓存
上。这便是 Shmem 的本质所在,它属于磁盘高速缓存所对应
的“buffers+cached”一类。但是由于在物理磁盘上并没有与之对应的空间,因此它不属于 File-backed 内存对应的 LRU 列表,而是记录在匿名内存的
LRU 列表上。这就是为什么 File-backed 内存的总和
与“buffers+cached”的值有所出入。
另外,由于匿名内存是换出的对象,因此 tmpfs 也成为了换出对象。使
tmpfs 成为换出对象的机制就是在这里设置的 。
这里将 tmpfs 使用的内存称为 Shmem 是有原因的。在 Linux 中,为了
22实现“共享内存”(shared memory)功能,即多个进程共同使用同一内存中
的内容,需要在内部使用 tmpfs。具体来说,就是通过文件映射功能,把在
tmpfs 中创建(物理内存上)的文件映射至多个进程的逻辑空间中。
出于这些原因,内核内部中 tmpfs 使用的内存便被称
为“Shmem”(shared memory 的缩写)。内核源代码中,tmpfs 的功能也是
通过一个名为 mmshmem.c 的文件来实现的。
它们之间的关系,就如图 1.23 所示。虽然难免存在误差,但大体来说下
面的关系式是成立的 。
用户内存
= Active(file) + Inactive(file) + Active(anon) +
Inactive(anon) + Unevictable
= buffers + cached + AnonPages
最后,介绍一下图 1.23 右上部分所示的 AnonPages。
图 1.23 用户内存的分类
Linux 内核中存在着一个 rmap(reverse mapping)机制,负责管理匿
名内存的每一个内存页,即“该页是被映射到哪个进程的哪个逻辑地址的”这
类信息。这个 rmap 中记录的内存页的总和应与 AnonPages 相对应,也就是
图 1.22 中⑧所表示的数值。
23要让 Shmem 成为换出的对象,就要以匿名内存的形式记录到 LRU 列表
中,但由于其具有文件系统的功能,因此无法记录在 rmap 里。这样一来,Shmem 的值(同时也是磁盘高速缓存和 File-backed 内存之间的差值)也就
成了 AnonPages 和匿名内存之间的差值。
此外,之前介绍的 Unevictable 列表中所包含的页面,在图 1.23 中省
略了。
内核内存的分类
从全部的内存使用量中减去用户内存的使用量,就是内核内存的使用量。
图 1.22 中的① ~ ④加上 Unevictable 的和便是用户内存的使用量。从图
中的注释可知,① ~ ④的和也可以通过“Active+Inactive”来计算。
因此,内核内存的使用量可以按如下方法计算。
内核内存 = MemTotal –(MemFree + Active + Inactive +
Unevictable)
当然,仅凭这个公式也无法得知内核内存的具体细节。遗憾的是,并不是
所有的内核内存信息都会输出到 procmeminfo 中,这里仅针对典型应用来
说明如何确定其使用量。
首先,图 1.22 中的⑨ Slab 是由“Slab 分配器”分配的总量。内存空
间存储着内核所使用的各种数据,但它并不是每次都寻找空闲空间来进行分
配,而是事先汇总数据,在数据类型的基础上再进行分配,提供这一机制的便
是 Slab 分配器。
之前说过,物理内存的分配和释放是以 4KB 大小的页为单位进行的。因
此,在需要 4KB 以下内存的情况下,要将 4KB 的内存页分割好后再分配使
用。事先进行这种分割处理的,也是 Slab 分配器。
Slab 有石板的意思,大概是由于与巨大的岩石被分割为石板的情形相似而
得名的吧。准确地说,内核根据每种数据的类型,创建由多个内存页组成
的“Slab”,再将 Slab 内部划分成一个个相应数据类型的“对象” 。图 241.24 的例子中,对象 A 就有三个 Slab,对象 B 有两个 Slab。
图 1.24 slab 分配器
当内核使用该类型的数据时,就会使用对应的 Slab 内部的对象。当可供
使用的对象不足时,便会用空闲的内存页来创建并添加新的 Slab。相反,当使
用过的对象被从内核中回收,即 Slab 中的所有对象均变作未使用时,根据需
要也可以回收 Slab,使之还原为空闲的内存页。
图 1.22 中,⑨ Slab 的下面有 SReclaimable 和 SUnreclaim 两个
值。SReclaimable 指的是不存在活跃对象、可以回收的 Slab 的容量,SUnreclaim 则表示的是对象在活跃,不能被回收的 Slab 的容量。
从 procslabinfo 的输出中,也能确认每个 Slab 的信息。
cat procslabinfo
slabinfo - version: 2.1
name :
tunables : slabdata
fat_inode_cache 24 72 680 24 4 : tunables 0 0 0 : slabdata 3
3 0
fat_cache 0 306 40 102 1 : tunables 0 0 0 : slabdata 3
3 0
fuse_request 52 104 608 26 4 : tunables 0 0 0 : slabdata 4
4 0……以下省略……因此,例如从保存“fat_inode_cache”的对象的 Slab 可得知,三个
()中的三个()均处于活跃状态。除此之外还
可了解到,一个 Slab 所占用的内存页数()、一个 Slab
所包含的对象数()、总对象数()以及处于活跃
状态的对象个数()等。
提醒读者注意,仔细看看图 1.24,会发现每个 Slab 中对象的放置位置
在一点一点地错开。这是用于提高 CPU 中内存高速缓存的利用效率的技术,被
称为“高速缓存着色”。
CPU 的内存高速缓存被划分成多个存储区域(槽 slot),根据内存的物
理地址来选择使用的槽。此时,如果每个对象的物理地址排列整齐,那么多个
对象便会被分配到相同的槽中,导致无法在同一时间缓存多个同类型的对象。
高速缓存着色通过错开对象的位置来打乱物理地址的排列,这样就可以使用多
个槽。
题外话,若使用 RHEL6 的 RPM 包“hwloc” 中包含的命令,便可显示
服务器上安装的 CPU 的核数以及内存高速缓存的大小。图 1.25 是 hwloc –
ls 命令的运行实例。由此可以看到,通过安装一个双核 CPU,且每个核上都
采用超线程(HT, Hyper-Threading)技术,就能令每个核上都可以同时运行
两个线程。此外还安装了 L1~L3 的 3 级内 存高速缓存。图 1.25 hwloc-ls 命令的执行示例
接下来,着眼于图 1.22 中的⑩ VmallocUsed。 内核使用内存时还存在
一种情况,即并不使用 Slab 分配器中配置的对象,而是直接将空闲页映射到
逻辑地址空间上。内核内部可以用于这一目的的逻辑地址范围,是由⑩上面一
行的 VmallocTotal 所表示的容量范围预先决定的。
虽然比起物理内存的容量,图 1.22 的示例中所示的值大得惊人,但这终
究只是可以使用的逻辑地址范围,并非实际使用的物理内存。实际上可以被用
来分配的内存只有⑩所示的这部分容量。
不过,不要不加思索地认同这个值。在这个范围中,除了服务器上安装的
物理内存,为了与诸如视频卡等外部设备进行数据交换,也分配了安装在这些设备上的内存,这种处理被称为“ioremap”。
至于 VmallocUsed 中包含的内存区域的详情,用
procvmallocinfo 即可确认。
cat procvmallocinfo
0xffffc90000000000-0xffffc90000002000 8192 hpet_enable+0x340x2b0 phys=fed00000
ioremap
0xffffc90000003000-0xffffc90000804000 8392704 alloc_large_system_hash+0x15e0x222
pages=2048 vmalloc vpages N0=2048
0xffffc90000805000-0xffffc9000080a000 20480 alloc_large_system_hash+0x15e0x222
pages=4 vmalloc N0=4
0xffffc9000080b000-0xffffc90000c0c000 4198400 alloc_large_system_hash+0x15e0x222
pages=1024 vmalloc vpages N0=1024
0xffffc90000c0d000-0xffffc90000c10000 12288 alloc_large_system_hash+0x15e0x222
pages=2 vmalloc N0=2
0xffffc90000c11000-0xffffc90000c14000 12288 mem_cgroup_create+0x2a40x4f0 pages=2
vmalloc N0=2
0xffffc90000c16000-0xffffc90000c18000 8192 acpi_os_map_memory+0x170x20
phys=caf7e000 ioremap
0xffffc90000c1a000-0xffffc90000c1c000 8192 acpi_os_map_memory+0x170x20
phys=caf7d000 ioremap……(以下省略)……
每行开头表示的是逻辑地址的范围,第二列的数值表示的是容量(字
节)。然后,末尾所示的“ioremap”是 ioremap 处理的入口。因此,在这个
输出中,将除 ioremap 对应行以外的所有第 2 列数值加起来,便可计算出
VmallocUsed 中除 ioremap 以外的物理内存的使用量。
在这里,“1.33 快速的数据处理管道”中讲到的知识就可以活学活用了。
好好利用管道,来尝试计算一下这个值。方法当然不少,但笔者最先想到的是
如下方法。
cat procvmallocinfo | grep -v ioremap | awk '{print 2}' | paste -s -d + |
bc
用 grep 命令将包含 ioremap 的行排除在外,并通过 awk 命令取出第
252 列的值。使用后面带 -s 选项的 paste 命令,将这些值合并成一个加法表
达式。之前介绍了 paste 命令可以将两个文件的每行进行合并,此时若指定
选项 -s,以及用于指定分隔符的选项 -d,便可以将一个文件中的各行进行合
并。准确地说,当前的内容不是从文件处接收,而是从标准输入处接收的。
来看下面这个运行示例,发生了什么一目了然。
cat procvmallocinfo | grep -v ioremap | awk '{print 2}' | paste -s -d +
8392704+20480+4198400+12288+12288+20480+12288+8192+49152+20480+12288+2101248+8392704+1052672+12288+12288+12288+12288+12288+69632+36864+69632+69632+8192+12288+12288+8192+8192+8192+8192+8192+8192+8192+8192+8192+12288+8192+8192+8192+12288+8192+8192+8192+8192+8192+8192+4198400+8192+8192+8192+12288+8192+8192+8192+8192+8192+8192+8192+8192+8192+8192+12288+8192+8192+8192+8192+8192+8192+8192+8192+8192+2101248+8192+8192+8192+8192+12288+8192+8192+8192+8192+8192+8192+8192+8192+8192+8192+8192+8192+8192+8192+12288+8192+8192+8192+8192+8192+8192+8192+8192+8192+8192+528384+36864+8192+8192+16384+16384+2101248+36864+8192+8192+36864+8192+8192+8192+8192+2101248+12288+12288+12288+12288+12288+12288+12288+12288+12288+12288+12288+12288+12288+12288+12288+12288+12288+8192+8192+12288+2097152+2097152+81920+16384+8192+8192+8192+28672+16384+36864+16384+12288+8192+221184+45056+20480+40960+12288+434176+45056+45056+94208+36864+12288+364544+32768+40960+16384+12288+12288+16384+77824+20480+16384+28672+12288+90112+24576+12288+12288+61440+20480+12288+16384+94208+36864+32768+331776+221184+118784+307200+73728+53248+16384+20480+16384+24576+8192+86016+32768+8192+8192+16384+8192+12288+327680+53248+57344+12288+16384+16384+32768+12288+249856+61440+49152+8192+81920+311296+12288+8192+81920+8192+8192+8192+28672+12288+8192+24576+8192+24576+8192+385024
这个结果是通过 bc 命令计算而来的。若进一步将这个结果除以 1024 转
换为以 KB 为单位,则会出现以下结果。
echo ( (cat procvmallocinfo | grep -v ioremap | awk '{print 2}' | paste -
s -d +) ) 1024 | bc 45452
稍微有些离题了,还是回到内核内存的话题上来吧。图 1.22 中的 ?
KernelStack 和 ? PageTables,可以作为 Slab 和 VmallocUsed 以外的
内核内存来看待。
首先,KernelStack 是内核代码使用的堆栈区域。对使用 C 语言来开发
程序的人来说,这个解释是多余的,但有一点仍要特别注意。由于 Linux 内
核中用户进程在运行过程中要不 ......
作者:[日]中井悦司
译者:李睿 马世龙
ISBN:978-7-115-37257-4
本书由北京图灵文化发展有限公司发行数字版。版权所有,侵权必究。
您购买的图灵电子书仅供您个人使用,未经授权,不得以任何方式复制和传播
本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维
权措施,并可能追究法律责任。
图灵社区会员 ptpress(libowen@ptpress.com.cn) 专享 尊重版权
版权声明
推荐序一 1
推荐序二
译者序
序
谢辞
本书的阅读对象
本书的阅读方法
各章概要
第 1 章 你必须知道的!Linux内部结构
1.1 Linux的三大基础:磁盘、进程、内存
1.2 磁盘和文件
1.2.1 磁盘的 3D 参数
1.2.2 新旧分区表
1.2.3 文件系统和 IO 子系统
1.3 控制进程就等于控制Linux
1.3.1 fork 和 exec 分别是进程的分身和变身
1.3.2 作业控制中的各项任务处理
第 2 章 别说缺少机器!虚拟化基础设施环境的构建
3.3 用Shell脚本一决胜负
3.2.5 命令置换和数值演算
3.2.4 数组和位置参数的使用方法
3.2.3 条件判断的写法
3.2.2 引号的使用方法
3.2.1 Shell 脚本的操作确认
3.2 Shell脚本的基本规则
3.1 简单有效的Shell脚本
第 3 章 10轮决胜!在自编的脚本中灵活使用命令
2.3.4 HA 集群设计及运用的准备
2.3.3 HA Add-On 的导入和配置
2.3.2 Linux 主机的准备以及虚拟机的构建
2.3.1 对 HA Add-on 的理解
2.3 HA集群环境在虚拟机上的实现
2.2.3 邮件服务器的建立
2.2.2 DNS 服务器的搭建
2.2.1 虚拟网络的构建和虚拟机的配置
2.2 Linux KVM虚拟网络
2.1 基础设施工程师的成长来自于日常积累3.3.1 [ 第 1 轮对决 ] 在跳板服务器上学到的秘籍 ~ 基本模式 + 异常处
理
3.3.2 [ 第 2 轮对决 ] 在分布式 Shell 上学到的秘籍 ~ 基本模式 + 管
道
3.3.3 [ 第 3 轮对决 ] 在进程监视中学到的秘籍 ~ 状态迁移处理
3.3.4 [ 第 4 轮对决 ] 秘籍外传 ~ 由 make 命令进行简单的批处理
3.3.5 [ 第 5 轮对决 ] 从模拟快照(snapshot)学到的秘籍 ~ 用管道操
作日志
3.3.6 [ 第 6 轮对决 ] 在云备份中学到的秘籍 ~ 在思考实验中组合处理流
程
3.4 Perl脚本的对决
3.4.1 [ 第 7 轮对决 ] 自己编写 Perl 的乐趣
3.4.2 [ 第 8 轮对决 ] 通过 Tweet 体验 Perl 带来的便利
3.4.3 [ 第 9 轮对决 ] 用进程监控掌握 fork
3.4.4 [ 第 10 轮对决 ] 终极秘籍 Perl 与管道的结合
第 4 章 最后的堡垒!内核源代码的阅读
4.1 阅读源代码
4.2 内核源代码的走读方法
4.2.1 Linux 内核的构建步骤
4.2.2 内核源代码探索入门
4.2.3 读懂结构体和指针
5.3.2 各子系统的主要参数
5.3.1 Control Group 的概要
5.3 用Control Groups控制资源分配
5.2.2 Upstart 任务的创建示例
5.2.1 Upstart 的概要
5.2 对服务器启动处理进行变革的Upstart
5.1.4 通过 anacron 实现定期任务执行
5.1.3 用 dracut 创建初始 RAM 磁盘
5.1.2 NetworkManager 服务的引入
5.1.1 ext4 文件系统的采用
5.1 支持商品化硬件的“操作系统进化”
第 5 章 先行一步!RHEL6新功能综述
4.4.3 进一步探索的指南
4.4.2 闰秒发生的瞬间
4.4.1 Linux 内核的系统时间
4.4 内核源代码的分析实例
4.3.2 内存管理子系统
4.3.1 进程管理子系统
4.3 探索核心子系统5.3.3 cg 命令群的管理
5.3.4 虚拟机的 cgroups 操作
5.4 通过LXC体验容器型虚拟化技术
5.4.1 容器型虚拟化技术的概要
5.4.2 容器中 Web 服务器的启动
5.4.3 其他的容器设置
结语
参考文献
版权所有,侵权必究。
家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。
本书中文简体字版由 Gijyutsu-Hyoron Co., Ltd. 授权人民邮电出版社独
Tuttle-Mori Agency, Inc., Tokyo
arrangement with Gijyutsu-Hyoron Co., Ltd., Tokyo in care of
This Simplified Chinese language edition published by Ltd., Tokyo
Original Japanese edition published by Gijyutsu-Hyoron Co.,All rights reserved.
Copyright ? 2012 Etsuji Nakai
PURO NO TAME NO LINUX SYSTEM:10NEN KIKU GIJUTSU by Etsuji Nakai
版权声明推荐序一
推荐序排名不分先后,此处按姓氏音序排名。——编者注
Linux 操作系统近年来发展迅猛,在服务器和手机上都获得了巨大的成功。随
着互联网和云计算的发展,可以预期 Linux 将会占据越来越重要的地位。然而,由于历史原因和条件限制,中国的很多大学一直使用 Windows 环境作为主要的教
学和工作平台,很多计算机专业学生和从业人员没有 Linux 的经验,这不但限制
了他们的职业选择,也对我国 IT 产业的发展造成了相当程度的制约。
能否为有一定计算机基础知识的人提供一本参考书,使得他们能够尽快掌握
Linux 的基本知识,并培养深入学习和运用 Linux 的能力呢?《Linux 系统架构
和应用技巧》就是一本理想的读物。
首先,这本书非常实用、容易上手。通过目标驱动的方式,所讲述的内容包含
了细致的操作说明,可以对着书一步步在计算机上操作,边操作边学习。
其次,本书也绝不是简单的操作指南,包含了相当多的深度内容。例如,在第
2 章就介绍了一个分布式服务系统的构建,甚至包括了高可用系统的构建。第 4 章
更是介绍了如何阅读 Linux 内核源代码。这些深度内容为读者进一步深入探索和
应用 Linux 打下了很好的基础。
第三,与动辄数百页的大部头相比,本书相当轻薄,不会令读者望而生畏。
总之,我认为本书在实用性和深度上取得了很好的平衡,选材精炼,是一本很
好的 Linux 入门读物,适合计算机相关专业的学生和从业人员自学,也适合部分
大学的相关专业作为教材使用。
清华大学计算机系副系主任 陈文光
1
1 推荐序二
20 世纪 90 年代初,Linux 刚刚诞生不久,一部分有梦想的留学生怀揣着一
打打的软盘,不远万里将 Linux 发行版的副本带回国,Linux 因此得以在中国生
根萌芽。那时侯,国内 Linux 资源极其匮乏,使用者也是寥若晨星。1997 年,中
国软件行业协会自由软件研究应用发展分会在北京成立,差不多同一时期,国内最
具人气的水木清华 BBS 开辟了 Linux 版,随后提出建立中文 Linux 计划。这之
后,各种诸如中国开源软件推进联盟之类的民间组织如雨后春笋般相继发展起来
了。
进入 21 世纪以来,国内 IT 产业对 Linux 的重视程度不断提升,开源运动
在国内得到业界广泛支持,一批以 Linux 为核心的系统软件供应商逐渐浮出水
面,并为电力、航天、交通等重点行业提供了主要的技术支撑。与此同时,各类基
于 Linux 的应用,在经历了市场的筛选后,被认可的成熟应用逐步形成规模生
产。基于优化的 Linux 系统等开源软件进行自主研发的技术也呈现出全面替代大
型成熟商业软件产品的趋势。
2009 年是我国的 3G 元年,自此我国正式进入移动互联网时代,这无疑给
Linux 提供了新的发展起点,带来了更大的发展空间。当前,大多数智能移动终端
所采用的操作系统都是基于 Linux 平台开发的。例如,Google 推出的 Android
操作系统、中国联通发布的智能手机操作系统平台“沃 Phone”,以及阿里巴巴集
团旗下阿里云计算有限公司开发的移动操作系统阿里云 OS 等。可以说,Linux 正
以不可阻挡的强劲势头迈步向前。
迄今为止,Linux 在国内已有了十几年的积累,专业人才不论在质量还有数量
上都得到了很大的提升,这一点从 Linux 内核社区做出的贡献就能看出,55 个国
家的 8849 人贡献了 29 万个补丁,其中 16280 个补丁的贡献者来自中国,位列
贡献表排名榜第四位。尽管如此,我国 Linux 的人才缺口依然很大,还远远不能
够满足 Linux 迅速发展的需要。因此,成为一名 Linux 技术人才,无论是在职业
发展方面还是在薪酬待遇上,空间都极为广阔。
这本书不仅是原著者智慧和经验的分享,也融入了翻译者的心血,我很高兴看到这本书能顺利翻译和出版。它的实践性很强,深入浅出,是一本很好的 Linux
参考书。对广大立志成为一名 Linux 工程师的读者来说,这会是一本很好的学习
用书。
北京航空航天大学计算机学院院长 吕卫锋
译者序
与这本书结缘很偶然,2011 年,译者所在的北京航空航天大学软件开发国家重
点实验室承担了国家科技支撑计划项目“基于 U- 系列标准的电子政务应用解决方
案”中的一个课题,内容涉及将基于 Windows 的应用软件迁移至 Linux 内核的国
产操作系统。为此,课题组全体师生对 Windows 和 Linux 操作系统进行了深入的
学习和实践,并在此过程中产生了将实践成果整理成书的想法。一次偶然的机会,译者在浏览图灵技术论坛时与《Linux 系统架构和应用技巧》这本书不期而遇,该
书的理念和我们当时的想法不谋而合。
本书内容丰富,结构清晰,深入浅出,具有较强的实践性,适合具有一定基础
的 Linux 开发人员和爱好者学习。译者希望该书中文版的面世,能够进一步推动
国内 Linux 的学习和实践。
本书有如下特点:
1. 通过搭建虚拟化基础设施,给读者提供了方便实用的 Linux 系统的学习和
实践环境;
2. 设计了 10 个可操作的脚本实验,以尽可能覆盖 Linux 操作系统的关键应
用技术,包括进程监控、远程登录、文本处理等;
3. 提出了 Linux 内核源代码的走读方法,在阅读源代码前,先了解内核的整
体结构,然后进一步了解各组成部分,最后深入到内核源代码,并通过创建简单原
始内核的示例,加深对内核源代码的理解。
致谢:
感谢毕业后一直任职于日本企业的北京航空航天大学计算机学院 99 级师兄王
新宇先生,以及在日本开会期间相识的就读于关西大学研究生院综合信息学研究科
的杜逆索先生,二位一直不厌其烦地为本书的翻译工作提供帮助!
感谢本书的文字编辑,一字一句地校对书中的每一个细节,尽可能地降低了译文的不准确性,也纠正了译文中很多疏忽的地方!
感谢人民邮电出版社图灵公司很好地推动了该书的引进、翻译和出版工作!
译者虽力求忠实准确地传达原著者的思想,但由于时间关系及水平所限,译文
中难免存在疏漏或不当之处,恳请广大读者批评指正。
李睿 马世龙
于北京航空航天大学
序
本书主要面向企业中的 Linux 系统工程师或 Linux 初学者。虽然迄今为止笔
者已出版了两本以 Linux 系统架构为主题的书籍,但是本书的风格略有不同。之
前的两本书,虽有幸得到了“内容充实易懂”之类的评价,但是为了兼顾系统性和
连贯性,不得已略去了现场的实践内容以及紧急状况的应对措施这两部分内容。而
本书则避免了之前教科书式的写法,尽量用笔者自己的口吻,向初中级 Linux 工
程师传授各种 Linux 系统架构和运用的技巧。
过去的工程师通常向同行前辈们学习系统架构和运用技能。然而如今受到社会
形势的变化,乃至企业培训方针变化的影响,很多时候往往来不及向前辈们请教,而是一上来就要独自负责 Linux 服务器的构建和运维工作。如此一来,那些有前
途的年轻工程师们,不仅没有机会体验这份工作带来的喜悦,反而为了适应工作而
每天都疲惫不堪,着实令人可惜。笔者希望看到更多的工程师,能在每天的工作中
体验到感动和喜悦的同时,支撑起 Linux 这一当代企业系统的支柱。在书里公开
这样的想法固然有些难为情,说不定还会被熟人嘲笑,但笔者还是坦率地写了出
来。我们所说的“十年不过时的技术”也许稍嫌夸张 ,但是笔者相信,书中的内
容一定会是长期支持诸位读者们的工程师生涯的“基础中的基础”。
原书名为「プロのための Linux システム·10 年効く技術」,直译为《专业开发者的 Linux 系统·十年
不过时的技术》。——译者注
在阅读本书的过程中,请各位初出茅庐的工程师们时不时地想象一下前辈们指
点江山的得意身影。要知道工程师前辈们也是人,有时也可能会说些没有根据的
话。然而 Linux 的精妙之处就在于,一旦有不确定的地方,立即就能亲自进行确
认。与昂贵的商用 Unix 服务器不同,只要你愿意,随时都可以用家用 PC 建立一
个测试环境。当今的 IT 系统新技术正以前所未有的速度发展着,这种能够亲自动
手进行确认的行动力,对工程师来说是比知识和经验更加宝贵的财富。虽然本书记
载的只是笔者自身的知识和经验,但若每一位读者都能发挥自身的行动力,将其价
值成倍扩大的话,对笔者而言将是无上的喜悦。
中井悦司
1
1 谢辞
谨在此向对本书的执笔和出版提供过帮助的各位表示感谢。
本书的构思来自于技术评论社的池本公平先生的建议。笔者之所以深感把技术
发展趋势的根本,即“工程师的基础知识”传达给大众的重要性,全仰赖池本先生
的大力推动,在此表示感谢。另外,向试读本书的中山健次郎先生也表示感谢。
本书的主要内容取材于笔者自身和 Linux、开源相关的工作经验。在这期间,笔者有幸遇到了诸多对 Linux、开源充满喜爱和探究精神的伙伴们,在此也向他们
表示感谢。
最后要感谢我的妻子真理,面对只顾埋头执笔而忽略家庭的丈夫依然笑颜以
对,还有将满 3 岁的活泼的爱女步実,整日嘟囔着“爸爸真喜欢工作啊”,再一次
对你们母女表示最真挚的感谢。
感谢大家一直以来的支持,今后也请多多指教!
本书的阅读对象
本书涉及与业务系统相关的 Linux 服务器的构建与运用,适合想要进一步深
入了解 Linux 相关技术,或者想要提高 Linux 操作技能的工程师读者。
从 Linux 的内部结构,到脚本编写技术或是内核源码的阅读方法,本书所传
授的技术是在一般教科书中看不到的。希望各位工程师读者能以本书为起点,找到
进一步提升技术的方法。
本书的阅读方法
本书的各章节内容都是相互独立的,既可以从第 1 章开始按部就班地阅读,也
可以从吸引你的那一章开始阅读。
至于每一章的预备知识及难易度则略有不同。若有感觉难以理解的部分,请结
合参考文献里介绍的同类书籍进行配套阅读。从不同的角度来审视同一个问题,是
在学习技术的过程中打破瓶颈的最佳方式。
第 2 章、第 3 章、第 5 章的主要配置文件和脚本,可以从技术评论社的网
站上下载。下载链接如下:
http:image.gihyo.co.jpassetsfilesbook2012978-4-7741-
5143-4downloadLinuxKuradashi_20120517.tgz
为方便起见,本书中介绍的具体的命令和设置顺序,是基于 Red Hat
Enterprise Linux 6.2 的。但是,其适用的技术领域并不依赖于特定的 Linux
发行版。还请读者朋友们将本书作为扩大知识面、迈向新阶段的一个出发点吧。
各章概要
第 1 章 你必须知道的! Linux 内部结构
磁盘管理、过程管理和内存管理是适当运用 Linux 服务器以及进行问题判断
的基础。针对这些内容,本章将从 Linux 内部构造的角度出发,传授有助于实践
的知识。从磁盘的几何结构,到 UEFI 和 GPI、4KB 扇区磁盘等新的机制,再到
IO 调度程序的基础、fork 和 exec 的差异、内核内部的内存分类等,希望读者
们能对这些知识有更深一步的理解。
第 2 章 别说缺少机器!虚拟化基础设施环境的构建
要处理 IT 基础设施,最基本的莫过于对整个系统机制(体系结构)的理解,因此亲手构建典型配置的系统,彻底理解整体架构与各个服务器功能之间的关系是
很有必要的。为了有效地学习以上技能,最好营造一个专门的操作环境。本章围
绕“邮件发送系统”和“HA 集群环境”,介绍了与 Linux KVM 的虚拟机环境相结
合的操作环境的使用方法。
第 3 章 10 轮决胜!在自编的脚本中灵活使用命令
本章主要介绍 Shell 脚本和 Perl 脚本的创建,它们是提高服务器管理工作
的效率所必需的。Shell 脚本部分并不是单纯地介绍基本规则和示例,而是通过列
举能立即学以致用的现场实例,传授实用的 Shell 脚本的编写技能。至于 Perl
脚本,本章则介绍了如何灵活运用文本分析以及 fork 的使用等 Perl 独有的功能
和数据结构,以期创建出更高级的 Perl 脚本。
第 4 章 最后的堡垒!内核源代码的阅读
要对 Linux 的内部结构有更深层的理解,有时也需要阅读 Linux 的内核源代
码,但要整体地把握这么庞大的源代码也并非易事。第 4 章进行了思维转换,主张
用欣赏文学作品般的方式阅读和把握源代码。本章先介绍实际运行的内核的编译顺
序,然后再逐次进行源代码的阅读。除了修改源代码外,还介绍了一个创建原始内
核的简单示例。第 5 章 先行一步! RHEL6 新功能综述
随着支持 Linux 的服务器环境的演化,Linux 也发生了各种各样的变化。本
章详细介绍了作为 Linux 服务器的管理者所应了解的知识,例如 Red Hat
Enterprise Linux 6(RHEL6)的更新,乃至 Upstart、Control Groups、LXC(Linux 容器)等新功能,这些都是与 Linux 相关技术的未来趋势息息相关的
基础知识。
第 1 章 你必须知道的!Linux内部结
构
1.1 Linux的三大基础:磁盘、进程、内存
Linux 工程师的工作有时需要登录陌生的 Linux 服务器才能进行。在这种时
候,起初笔者会用到 df、ps 和 free 这三个命令,倒并非出于刻意,只是本能地
去执行这些指令。
例如,当你接到一个紧急事件的请求响应,而事先又不知道该服务器的配置信
息时,就得首先用这三个命令检查一下这台服务器的状态,并对当前资源的使用情
况进行确认。
此外,通过 w 命令来确认是否有用户在同一时间在同一台服务器上工作,也是非常重要的。
首先,df 命令主要用来检查文件系统上的可用空间。虽然服务器在运行过程中
通常会对文件系统的使用率进行监测,但在进行设备维护而暂时停止监测时,就很
容易导致意外情况的发生。
比如曾发生过这样的严重状况:当被告知“某个命令无法顺利执行”时,试着
输入 df 指令,竟发现此时根文件系统的利用率已高达 100%。因此,在登录服务
器进行工作时,还是养成时常用 df 命令来检查文件系统使用率的习惯吧。
df 命令也可用于确认磁盘分区的构成以及数据的分配状况,还可以确认用于保
存数据的文件系统有多少千兆字节,或是否有 NFS 安装区域等。
了解应用系统的使用数据是如何被组织和存储的,是全面了解服务器运行的一
个关键。经典的软件开发著作《人月神话》[1] 中写道:“光看流程图不看数据表
只是徒劳,但看了数据表,流程图便不看也罢。”IT,顾名思义,是指信息技术,其核心自然是信息(数据)的处理,因此数据可以说是所有操作的基本了。
此外,服务器是利用进程来处理数据的,而 ps 命令则可以被用来确认当前服
1
1务器上运行进程的状态。
如果平常我们能接触到各种 ps 命令的输出,那么只需看看进程的名称,就可
以大致了解这台服务器的用途和设置。有时会发现一些意料之外的进程正在运行,而经过仔细确认,可能就会在不经意间找到问题的原因。
最后的 free 命令适用于确认存储的使用状况。通过 ps 命令所得到的进程的
信息,再结合磁盘、高速缓存和内存的使用情况,能很好地掌握服务器的运行状
态,了解这是一个“重载”到什么程度的服务器。
Technical Notes
[1] 《人月神话》弗雷德里克·布鲁克斯(著),汪颖(译),清华大学出
版社,2007
所谓的“重载”“轻载”,是一种含糊的说法,实际上是结合过去 CPU 的使用
率和磁盘 IO 的频率来进行综合判断的。然而,CPU 或 IO 操作与内存的使用情
况是密切相关的。如同医生可以从患者的外部情况推断病情一样,通过内存的使用
情况,就能推断出包括 CPU 和 IO 在内的整个服务器的运行状态。
即便在 CPU 的使用率和磁盘 IO 的频率的历史数据十分翔实的情况下,也应
时刻质疑“莫非是 CPU 使用率太高了?”“也许是 IO 负载太高了?”,以这样
的态度去观察数据,方能分析出最接近真实状况的结果。
虽然这个引言稍嫌冗长,但通过以上例子,可以总结出三个应了解的 Linux
的基本分支,即用于保存数据的磁盘、实际处理数据的进程,以及存储服务器的各
种运行信息的内存。
针对以上三点,本章将从 Linux 内部结构的角度出发,介绍有助于实践应用
的知识。1.2 磁盘和文件
1.2.1 磁盘的 3D 参数
Linux 这一类操作系统的任务是隐藏物理硬件信息,即对用户和上层应用屏蔽
底层硬件的差异,并提供统一的操作方法。
但是,对于磁盘装置,还是需要让人知道它的物理构造的。这一点似乎常常被
误导。例如,在笔者的 Red Hat Enterprise Linux 6(RHEL6)测试机上运行
fdisk 指令,就能得到如下结果。
fdisk -l devsda
磁盘 devsda: 500.1 GB, 500107862016 字节
磁头 255, 扇区 63, 柱面 60801
Units = 柱面数 of 16065 512 = 8225280 字节
扇区大小 (逻辑 物理): 512 字节 512 字节
IO size (minimumoptimal): 512 bytes 512 bytes
磁盘标识符: 0x8c403069
设备 启动 始点 终点 块 Id 系统
devsda1 1 64 512000 83 Linux
第1分区没有在柱面边界结束。
devsda2 64 51057 409600000 83 Linux
devsda3 51057 51318 2097152 82 Linux 交换 Solaris
devsda4 51318 60802 76176407+ 83 Linux
屏幕上出现了警告信息:“第 1 分区没有在柱面边界结束”。于是在 Web 上
检索“磁盘的柱面”,即得到图 1.1 这样的说明图。图 1.1 典型的硬盘的说明图
结合之前输出结果的第 2 行信息“磁头 255,扇区 63,柱面 60801”,此图
该如何解释呢?那就是,这台测试机器的硬盘“有 255 个磁头数,每个磁道有 63
个扇区,一个磁盘可以分割出 60801 个磁道”。
当然,物理上拥有 255 个磁头的磁盘驱动器是不存在的。磁盘装置的柱面数
(cylinder)、磁头(head)、扇区(sector)信息,三者统称为 CHS 或“3D
参数”,然而实际上,fdisk 命令表示的 3D 参数信息和磁盘装置的实际构造并没
有什么直接的关系。
要弄清楚这是为什么,就需要理解硬盘的两种不同类型(CHS 方式和 LBA 方
式)的存取方法(也经常被说成“寻址模式”)以及它们之间的差异。
正如图 1.1 中所示,要特别规定数据读取和写入的扇区(物理磁盘存取的最小
单位)所处的位置,只需指定以下三个数值即可:柱面数(从最外缘开始计算第几
条磁道)、磁头数(磁盘表面的读 写头的个数)以及扇区数(磁道内等分弧段的
个数)。
使用时间超过 10 年的旧式磁盘中,磁盘装置的这三个值(CHS 值)指明了读
写数据的位置,这就是 CHS 方式。但实际上,Linux 设备驱动程序会计算 CHS
值,以进行磁盘装置与数据之间的读写,因此用户是不需要知道它们的具体数值
的。在磁盘仍然使用 CHS 方式的时代,唯一一次需要用户在操作时知道 CHS 值
的,是在创建磁盘分区的时候。根据当时的 MS-DOS 方式,磁盘分区必须以扇区为
单位进行操作。
由于柱面是从磁盘的外缘开始按顺序进行编号的,因而磁盘柱面的分割与编号
大致如图 1.2 所示。过去还曾有人异想天开,想在不同的物理磁盘(磁盘面)上采
用不同的分割方式,但遗憾的是,这样的分区方式是无法实现的。
图 1.2 典型的分区的分割方法
虽然 Linux 没有必要遵循 MS-DOS 方式,但在当时,通过将操作系统引入不
同的分区,可以实现 MS-DOS 和 Linux 的多重引导或是 Windows 和 Linux 的多重引导。因此,和 MS-DOS 或 Windows 遵循共同的方式是有实际意义的。
如此一来,和 MS-DOS 中的 fdisk.exe 一样,Linux 中的 fdisk 命令在做
磁盘分区时,也得用柱面数来指定分区的开始位置和结束位置了。
然而,这种有着悠久历史的旧式硬盘早已被时代淘汰,沦为“计算机历史博物
馆”中的陈列品了(这种说法可能有些夸张,但至少在笔者的家中是难觅其踪
了)。
现在的硬盘普遍采用 LBA(Logic Block Addressing,逻辑块寻址)方式进
行数据存取。这种方式的机制极其简单,即硬盘内所有的扇区均从 0 开始进行编号
(扇区编号),通过扇区数来指定扇区的位置。
扇区号与物理扇区位置之间的对应,是由内置的硬盘控制器来计算的。通常来
说,扇区号越小,其对应的物理扇区就越位于磁盘的外侧。
与此相结合,分区的开始位置和结束位置同样也是由扇区号来指定的。之前的
fdisk 命令的输出中,是按照传统的 CHS 方式,使用柱面号来表示各分区的起点
和终点的,但实际上这并不是真实的信息。真正的分区信息,需要在 fdisk 命令
之后加上选项 -u 来获取。
fdisk -lu devsda
磁盘 devsda: 500.1 GB, 500107862016 字节
磁头 255, 扇区 63, 柱面 60801, 合计 976773168 扇区
Units = 扇区数 of 1 512 = 512 字节
扇区大小 (逻辑 物理): 512 字节 512 字节
IO size (minimumoptimal): 512 bytes 512 bytes
磁盘标识符: 0x8c403069
设备 启动 始点 终点 块 Id 系统
devsda1 2048 1026047 512000 83 Linux
第1分区没有在柱面边界结束。
devsda2 1026048 820226047 409600000 83 Linux
devsda3 820226048 824420351 2097152 82 Linux 交换 Solaris
devsda4 824420352 976773166 76176407+ 83 Linux如图所示,各个分区的起点和终点是由扇区号来表示的。这些扇区号所划分出
的范围就是实际的分区。图中第一个分区是从 2048 号扇区开始的,由此可知
0~2047 扇区是保留扇区,它们不被用作分区。
磁盘的开始部分为主引导扇区 MBR,GRUB stage1.5 就保存在 MBR 之后的空
间中。这里的情形是,第 0 号扇区为主引导扇区,stage 1.5 则存储在主引导扇
区后直至第 2047 号扇区之间的空间中 。
第 1 分区的起始位置,以前的标准是第 63 扇区,最近变更为第 2048 扇区。而 stage1.5 并没有那
么大,因此在之前的起始位置保存 stage1.5 也没有问题。
绕了这么大一个圈,下面就开始解释上文所提到的警告信息吧。
由于现在的硬盘是以 LBA 方式存取的,因此分区的开始和结束位置皆通过扇区
号来指定。虽然这样做一点问题都没有,但是 fdisk 命令为了支持旧式 CHS 方式
的磁盘,仍然以 CHS 方式来表示磁盘信息。
这时,对于 LBA 方式的磁盘,需要转换成其对应的 3D(磁头数、扇区数、柱
面数)参数。当分区的结束位置不能用 3D 参数中合适的柱面结束位置来对应表示
时,就会出现诸如“第 1 分区没有在柱面边界结束”这样的警告信息。
总之,这种警告信息的出现,意味着“操作系统正以柱面为单位对磁盘进行分
区,可能会导致一些问题”。不过对于在 Linux 下使用的磁盘,这种顾虑是多余
的。
现在,在 fdisk 命令之后加上选项 -u,便可以通过指定扇区号来指定分区。
因此,今后还是养成在 fdisk 命令后附加选项 -u 的习惯吧 。
在 Red Hat Enterprise Linux 的高级开发版本 Fedora 所包含的 fdisk 命令中,默认的操作是以
扇区为单位的。当需要进行旧式的以柱面为单位的操作时,需要指定选项 -u=cylinders。
1.2.2 新旧分区表
前一节介绍的表示分区开始和结束位置的信息,它们究竟会被写入到磁盘的哪
里呢?这是一个常常被问到的问题,答案毫无疑问是“分区表”。
2
2
3
3准确地说,分区表就是存放在第 0 号扇区 MBR 的 446~509 字节的部分。在
MBR 的 0~445 字节中,存放的是所谓的引导加载程序,即服务器启动时,用于引
导 BIOS 的加电自检以及 GRUB stage1 的加载。
由于一个扇区的大小是 512 字节,这里就会有 510~511(从 0 字节开始,到
511 字节结束)两个字节的剩余,于是按惯例这里的数值就记录为 0xAA55。若磁
盘此处的值不为 0xAA55,则判断该磁盘的 MBR 已损坏。
由于分区表的大小只有 64 个字节,因此大部分信息不能被写入其中。在每个
分区表中,只记录着“用 CHS 方式描述的分区开始位置和结束位置”以及“用
LBA 方式(扇区号)描述的分区开始位置以及包含的扇区数”这类有代表性的信息
(结束位置不用扇区号来记录,而是通过开始位置与扇区数相加计算得到)。
之所以要通过两种方式记录分区开始和结束位置的信息,是有其历史原因的。
LBA 方式的磁盘实际上是不使用 CHS 方式记录分区信息的。
图 1.3 是通过 hexdump 命令输出的 MBR 第 446 字节开始往后 66 字节的
内容(分区表加最后两个字节)。通过设置详细的选项,输出了以十进制形式表示
的 LBA 方式的分区信息。
图 1.3 分区表的转储输出
方框里的 4 行数据,分别是 4 个分区的信息。将它与之前带选项 -u 的
fdisk 命令的输出相比较,可以确定采用 LBA 方式记录的扇区号的信息与之前的信息是一致的。
CHS 方式描述的数值在此不做详细分析,但从分区的开始位置和结束位置出现
了若干个相同的数值可以看出,这是段没有意义的信息。由于 CHS 方式描述的信息
实际上并不会被投入使用,因此问题不大,但还是应当注意避免混淆。
顺带一提,最后两个字节正是前面介绍的 0xAA55,而把图 1.3 的最后看成
0x55AA 的读者,还请自行学习一下“小端”(little endian)的知识。
如此看来,使用 LBA 方式是最为简单便捷的了。不过,近来这种方式也出现了
它的局限性。那么是什么呢?
如图 1.3 所示,虽然采用十进制比较难懂,但能看出表示开始位置的扇区号和
全体扇区数的数字总共是 4 个字节,因此可以表示的范围仅为
0x00000000~0xFFFFFFFF。换句话说,它无法支持扇区数超过 0xFFFFFFFF 的大
容量磁盘。
可以想象出这个容量是多大吗?我们知道一个扇区是 512 个字节,用十六进制
的计算器进行计算,答案应该是 2TB。
也许有人要问,如果没有十六进制的计算器该怎么办?即便没有这样的计算
器,也可以在 Linux 上通过使用 bc 指令来计算,如下所示。ibase=16 即指
定“输入值为十六进制”。另外这里提醒大家一下,0xFF 表示的是十进制数
256。
echo ibase=16;FFFFFFFFFF2 | bc
2190433320450
因此,MBR 中的分区表是有限度的,对于容量大于 2TB 的硬盘,是无法为之
创建分区的。
当使用外部存储装置 LUN(逻辑磁盘)作为数据存储区域时,则无需对 LUN
进行分区,只需将其格式化后挂载到文件系统中,或使用 LVM(逻辑卷管理)方法
将其作为逻辑卷进行管理等。这样,容量大于 2TB 的 LUN 就也能够使用了。不过,近来的服务器磁盘正逐步趋向大容量化,随着容量大于 2TB 的本地磁盘
的普及,找到容量大于 2TB 的磁盘的分区方法也指日可待。
GPT(GUID Partition Table,GUID 分区表)正是为了解决这个问题应运而
生的。现在,如果需要从使用 GPT 的硬盘中启动操作系统,就需要服务器和操作系
统都能支持 UEFI。操作系统中,目前的 Red Hat Enterprise Linux
6(RHEL6)是能支持 UEFI 的。
此外,虽然与分区表并不直接相关,但在最近的大容量磁盘中,有的已经以
4KB 作为一个扇区的大小了。
接下来就对 UEFI 和 GPT,以及 4KB 扇区的磁盘做一个详细的介绍,内容或
许略微复杂,请大家认真学习。
UEFI和GPT
UEFI 是以后将要取代 BIOS 的一个方案。
我们都知道,当服务器接上电源,系统 BIOS 就开始启动。BIOS 的启动
只允许使用 1M 的内存空间。因此系统 BIOS 的设置界面不是图形界面,而是
非常简单的基于文本形式的界面。而且服务器上搭载的各种设备的设置不是通
过 BIOS 的设置界面来操作的,而是需要通过 Ctrl+A 等按键来单独操作设置
界面。这些都是因为 BIOS 所能使用的内存空间有限。
UEFI 打破了 BIOS 的这些限制,在功能方面进行了多种扩展,于是支持
UEFI 的服务器在启动时,就可以直接通过 UEFI 的设置界面来调用各种设
备,有的服务器甚至已经有了图形化的设置界面。
此外,调用引导加载程序的方法也发生了改变。以前都需要像 GRUB
stage1、stage1.5、stage2 这样分阶段启动引导加载程序,而改进后,在以
GPT 方式创建的“EFI 系统分区”中,引导加载程序存储就可以直接被调用
了。
最后介绍一下 GPT。过去的分区表在第 0 扇区 MBR 里,GPT 则被写入第
1 扇区至第 33 扇区中,成为一种新型的分区表(图 1.4)。表 1.1 中总结了这两种分区方式的主要区别。
图 1.4 GPT 的构造
表 1.1 以前的分区和 GPT 的比较以前的分区 GPT
分区表的位置 MBR MBR之后(磁盘末尾亦有副本)
最大磁盘容量 2TB 8ZB(无限)
最大分区数 15(使用SCSI磁盘时) 1284
分区卷标 分区ID(表示用途的ID) GUID(表示用途的ID+唯一的ID)
制作分区的工具 fdisk parted
4在设计 GPT 时,通过改变 GPT 头的设置,也可以创建出超过 128
个分区。但一般情况下,128 个仍是最大限度。
为了降低分区表损坏的风险,GPT 在硬盘的最后保存了一份同样内容的分
区表副本。GPT 的头部,则记录了可以用作分区的扇区范围。
每个分区的信息都记录在“分区表”中。一个分区表是 128 字节,一个扇
区(512 字节)可以记录 4 个分区的信息。每个分区的开始扇区和结束扇区
都分别用 8 个字节来记录,因此即便是容量大于 2TB 的硬盘的扇区数,处理
起来也是绰绰有余的。
每个分区中都记录着一个特定的 GUID 标签。尤其是用来存储引导装载程
序的分区,会附上“EFI 系统分区”(ESP)的标签。
如果是 RHEL6,bootefi 下挂载的文件系统就是 ESP 类型的分区。这
里保存了类似于 GRUB stage2(grub.efi)的启动过程。支持 UEFI 的服务
器,会根据 GUID 定位 ESP,启动其中的 grub.efi ,因此就不再需要 GRUB
stage1 和 stage1.5 了。ESP 采用的是 VFAT 格式。
当创建一个 GPT 格式的分区时,应使用 parted 命令。表 1.2 描述了
parted 命令的主要内部命令,它们的具体使用方法可以在网上查到,此处不
再赘述。
表 1.2 parted 命令的主要内部命令
命令 说明
check 对文件系统进行检查
5cp 复制分区
help
显示对相关命令的说明。“help命令名称>”可以显示每一条命令的详细信息命令
名称>
mkfs 创建文件系统
mklabel 指定表示分区表类型的磁盘标签。在使用GPT时即指定“gpt”
mkpart 创建分区
mkpartfs 进行分区和文件系统的创建
move 移动分区
print 显示当前的分区表或磁盘标签的状态
quit 结束parted命令
resize 改变分区的大fi
rm 删除分区
select 指定要处理的设备(例如:devsda)
set 设置包括引导标志在内的各种标志
由于 RHEL6 也支持 GPT,因此在容量大于 2TB 的硬盘上进行安装时,会
自动采用 GPT 方式进行分区。
Technical Notes
[2] 使用 RHEL6 Rescue 模式的备份指南(非 LVM 环境 NFS 环
境 uEFI 模式版)http:www-
06.ibm.comjplinuxtechdocattachments003bc366_rhel6-
rescuee383a2e383bce38389e38292e4bdbfe794a8e38197e_12.pdf
4KB扇区的磁盘
以前的传统硬盘,一个扇区固定为 512 字节。而对于大容量硬盘,则通过
增加扇区的大小,来减小访问扇区产生的消耗。
另外,硬盘内部记录了每个扇区错误校验所需要的信息。通过增加扇区的
大小,可以降低这些附加信息所占的百分比,使记录数据的空间得到更加有效
的利用。
6但是,由于访问硬盘的服务器硬件或操作系统(设备驱动)都是按照之前
512 字节的扇区大小设计的,因此不能一味单纯地增加硬盘扇区的大小。
为了实现两者的兼容,就产生了通过硬盘中的控制器来从逻辑上模拟 512
字节扇区的运作方式。如图 1.5 所示,从服务器的角度来看,扇区的大小仍然
是 512 字节,但实际的数据读取和写入是在 4KB 扇区上进行的。
最近有不少新面世的硬盘均采用了这种处理方式。
图 1.5 4KB 扇区的磁盘结构
乍一看,这种转换方式会产生一定的开销。在读取 4KB 扇区中的逻辑扇区
数据时,即便仅仅 512 个字节的读取和写入,也会需要对整个 4KB 扇区进行
操作。但是实际上,若是大数据的读取和写入是从 4KB 扇区的起始位置开始
的,则基本上并不会产生额外的开销。
要有效地实现这样的效果,不仅需要将分区的开始位置与 4KB 扇区的起始
边界对齐,还需要将文件系统的块大小调整为 4KB(4096 字节)(关于文件
系统的块大小,将在下一节中进行详细介绍)。在笔者之前介绍的测试机的例子中,第一个分区 devsda1 是从第 2048
扇区开始的。如果将其转换成图 1.5 的逻辑区段数,就恰好和第 256 个 4KB
扇区的起始边界对齐。因为分区中包含的扇区数也是 8 的倍数,因此分区的结
束位置也就正好是 4KB 扇区的结束边界。
devsda2 和 devsda3 也是如此,这些分区是在 RHEL6 的安装界面
中设置的。可见安装程序很严谨地考虑到了这些问题。
当然,这些都是仅在使用 4KB 扇区的硬盘时才需要注意的事项,但将来一
定会有越来越多的硬盘采用 4KB 扇区。因此,在设置分区的开始位置以及大小
时,将逻辑扇区数设置为 8 的倍数是比较好的 ,这种方法被称为“分区对
齐”。
在用 parted 命令创建 GPT 分区时,默认情况下会指定分区的开始位
置、结束位置和容量(例如 MB)。此时,parted 命令会自动对齐所创建的分
区。如果不放心的话,可以通过“unit s”命令来指定扇区,对分区的开始和
结束位置进行确认。
恢复系统备份后,有时候需要通过 UEFI 的设置界面重新设置启动对象的启动加载文件。点击 [2] 的
链接可以看到使用 IBM System x 时的操作顺序。
此处为日文资料。——译者注
4KB 扇区的磁盘中有一个被称为“对齐偏移”的功能,在该功能有效的情况下,需要将分区的起始位置
设为“8 的倍数 +7”。由于这是 Linux 中不需要的功能,因此在能够通过硬盘的跳线开关等进行更改的情
况下,建议禁用该功能。详情请参考 [3]。
1.2.3 文件系统和 IO 子系统
文件系统的块大小
我们先来重新思考一下文件系统的数据访问。说到文件系统,大家可能会
想到 ext3、ext4 等,但在本节,笔者打算从更为宏观的角度来介绍文件系
统。
首先,Linux 中,有将各种不同的文件系统统一起来的 VFS(Virtual
File System,虚拟文件系统)层,还有通过设备驱动将数据读取或写入物理
7
5
6
7磁盘的块层,它们一起组成了图 1.6 中所示的 IO 子系统。Linux 的文件系
统只是 VFS 层中的一部分。
图 1.6 IO 子系统的结构
Technical Notes
[3] Linux Kernel Watch:超过 2TB!ATA 磁盘的 4KB 扇区问题
是什么?
http:www.atmarkit.co.jpflinuxrensaiwatch2010watch03a.html
前文提到的“文件系统的块大小”,是块层中的设备驱动程序将数据读取
或写入物理磁盘的最小单位。从物理磁盘的结构上看,是以 512 字节的扇区单
位来读取和写入数据的,但很多时候采用较大的单位来读取和写入数据可以更
有效地进行数据交换,而指定这种单位的就是块大小。
在 Linux 中,文件系统的块大小有 1024 字节、2048 字节和 4096 字节这几种选项。默认的块大小被记录在配置文件 etcmke2fs.conf 中,也
可以通过 mke2fs 命令的 -b 参数来明确指定块大小。若要对已经创建好的文
件系统所设置的块大小进行确认,则应使用 tune2fs 命令 。
我们在上一节中提到,在 4KB 扇区磁盘的情况下,应使分区的开始位置与
4KB 扇区的起始边界相对齐,并将文件系统的块大小也设置为 4KB(4096 字
节)。图 1.7 给出了这样做的原因。
图 1.7 中,上图表示了满足这种条件的情况,设备驱动程序对物理磁盘的
访问实际上只对应了一个 4KB 扇区,没有产生无效的数据读写。
图 1.7 4KB 扇区和块大小的映射
下图则表示了分区的开始位置与 4KB 扇区的起始边界没有对齐的情况。打
个比方,假使设备驱动程序写入了一个块。由于物理磁盘不能只对 4KB 扇区的
9一部分进行重写,因此需要先读取出两个扇区的数据,按要求对其中的部分数
据进行重写,然后再将处理后的数据重新写入这两个扇区。这显然会造成额外
的开销。
链接 [4] 里比较了分区的开始位置与 4KB 扇区的起始边界对齐和不对齐
的情况下分别对磁盘访问性能造成的影响。据分析,分区开始位置发生偏离
时,磁盘的数据写入性能会出现显著的下降。
顺便说一下,类似的情形在常见的 512 字节扇区的磁盘中也会发生,当分
区的大小不为块大小的整数倍时,最终会有一个块超出分区。这种情况下,最
终的这个块就不会在文件系统中使用。图 1.8 即为块大小为 2048 字节的一
个例子,这时分区剩下的最后两个扇区就不会被使用。
Technical Notes
[4] 4KB 扇区磁盘上的 Linux :实际建议
http:www.ibm.comdeveloperworkscnlinuxl-4kb-sector-
disks
图 1.8 最后的块超出分区的情况
还是题外话,在 fdisk 命令的输出中,有时块数的值后面可以看到附加的 + 记号。比如“1.2.1 磁盘的 3D 参数”一例中,devsda4 的块数(分
区中包含的扇区数)后就有 + 记号。当块数为奇数值时,便会附加上这个符
号。
这是有其历史原因的。过去的 Linux 文件系统的块大小规定为 1024 字
节,因此当分区中的扇区数为奇数值时,就会剩下最后的扇区不被使用。+ 记
号就是为了表示“该分区最后一个扇区不会被使用”而特意附上的,但现在这
也许是多余的了。
IO子系统的概貌
我们来回顾一下图 1.6 中介绍的 IO 子系统的整体结构,分别从写入数
据和读取数据两种情况进行考虑。
首先来看看写入数据的情形。当数据被写入文件系统的文件中时,其内容
会被暂时录入到磁盘高速缓存中(①)。这时发出写入数据命令的应用程序
(用户进程)会将此步骤视为数据已录入成功,从而进行下一步操作。
然而此时就会有些数据只是被写入到了磁盘高速缓存中,而没有被写入到
物理磁盘中去。这种数据称为“脏数据”。当磁盘高速缓存上的脏数据积累到
一定程度时,文件系统便会向 IO 调度发出请求,将这些脏数据写入物理磁盘
(②)。
这个写入请求将被添加到 IO 调度内部的“请求队列”中。最后,IO
调度器会响应请求队列中的请求,利用设备驱动程序将数据写入物理磁盘中
(③④)。
大家可能听说过多种类型的 IO 调度器,例如 cfq、deadline 等。它
们之间的差别在于使用的算法不同,即按何种顺序处理请求队列中的请求才最
高效。关于 IO 调度器,之后会做更加详细的说明。
此外,表 1.3 中列出的内核参数,能够用于调整脏数据写入的频率。不过
除非有特别的原因,否则是不需要更改默认值的。
表 1.3 与数据的写出频率相关的内核参数内核参数 说明
vm.dirty_background_radio
vm.dirty_radio
试图将缓存中脏数据的百分比保持
在“dirty_background_radio(%)”之下。尤其是当脏
数据的百分比超过“dirty_radio(%)”时,将立即增加
写出频率
vm.dirty_background_bytes
vm.dirty_bytes
以字节为单位操作与上面相同的指定。优先于上面的指
定,设置为0时则保持上面的指定
vm.dirty_writeback_centisecs
vm.dirty_expire_centisecs
每“dirty_writeback_centisecs(单位为1100
秒)”检查磁盘缓存,每超过
1“dirty_expire_centisecs(单位为1100秒)”,就持续写入新的数据
至于数据的读取,则是按以下流程进行的。假设应用程序(用户进程)要
从文件系统的文件中读出数据。如果目标数据已经存在于磁盘的高速缓存中,文件系统便将这些数据返回给进程,从而完成处理(①)。
反之,如果目标数据不在磁盘的高速缓存中,那么 IO 调度的进程会暂停
执行并进入等待状态,之后该文件系统会向 IO 调度器发出读取数据的请求
(②)。
最后,IO 调度器响应请求,通过设备驱动程序把数据从物理磁盘读入磁
盘高速缓存中(③④),解除 IO 调度进程的等待状态,再接着处理磁盘高速
缓存中的数据(①)。
磁盘高速缓存和文件系统属于 VFS 层,IO 调度和设备驱动属于块层,二者相互配合来完成文件中数据的读取和写入。
之所以将它们分为两个不同的层,是基于 Linux 内核中“模块化结
构”的概念。这样就可以将不同类型的文件系统和不同类型的物理磁盘随意组
合起来使用。
再举个极端的例子,即便是没有物理磁盘的文件系统也能实现数据的处
理。例如,ramfs 是利用内存来提供 RAM 磁盘功能的文件系统,它就完全没
有图 1.6 的②(数据传输请求)这个步骤。
那么写入的数据究竟到哪里去了呢?实际上它们都保留在磁盘缓存中。若是一般的文件系统,脏数据一旦被写入物理磁盘,就不再是脏数据,因此需要
从磁盘高速缓存中删除。
但 ramfs 则不同,磁盘高速缓存中的数据永远是脏数据,永远不会被删
除。ramfs 被描述成“基于内存的 RAM 磁盘”,其实准确来说,应该是“基
于磁盘高速缓存的 RAM 磁盘”。
在 Linux 中,还有一个和 ramfs 具备相同功能的 tmpfs。tmpfs 同样
是将数据保存在磁盘高速缓存中,但有别于 ramfs 的是,它的内容还可以成
为“换出”的对象。换句话说,当物理内存不足时,存储在 tmpfs 的文件会
被写入物理磁盘的交换空间中,这样文件使用的内存就可以被释放。关于
tmpfs 这种允许换出的机制,在本章 1.4.1 节中也会所介绍。
看到这里,可能有人会产生这样的疑问:“ramfs 和 tmpfs,到底选用哪
个好呢?”要是只作一般用途,例如应用程序中所使用的数据的临时存储,建
议还是采用 tmpfs。
这是因为,tmpfs 有设置内存使用上限的功能,而 ramfs 则没有这样的
功能。如果无限制地往 ramfs 上保存文件,就会持续消耗内存,而且内存部
分也不能被换出,最终就会因为内存不足而启动 OOM Killer。
顺带一提,ramfs 的原作者正是 Linus 先生,在源码 ramfsinode.c
中还能看到 Linus 先生留下的注释。
NOTE! This filesystem is probably most useful
not as a real filesystem, but as an example of
how virtual filesystems can be written.
以上注释都表明,ramfs 只是作为一个最简单的文件系统的示例而创建出
来的,它并不适合实际使用。
不过,在 Linux 内核中,有一个地方是使用 ramfs 的。在 Linux 的引
导过程中,内核启动后,初始 RAM 磁盘内容 会被装载到 RAM 磁盘空间
中。这里的 RAM 磁盘空间就需要用到 ramfs。
10理解IO调度器
我们继续回顾图 1.6,看看 IO 调度器是如何处理文件系统的数据传输
请求的。磁盘高速缓存和物理磁盘之间进行数据传输时,磁盘高速缓存中数据
的存储方式和物理磁盘中数据的存储方式是不一致的。
首先,在磁盘高速缓存上,一个文件中连续的数据基本上会被写入到连续
的存储空间中。然而,同样的数据在物理磁盘上就不一定会被放置在连续的存
储空间中了,这种情况被称为“磁盘碎片”。
如图 1.9 所示,磁盘高速缓存中连续的数据被分成了两个部分,分别被写
入了物理磁盘上两个连续的扇区内。在本图的例子中,文件的开始和结束部分
的内容被设置在了物理磁盘上相邻的位置。此外,Linux 的内存以 4KB 的页
为管理单位,磁盘高速缓存中 1 个页能写入 8 个扇区的数据。
图 1.9 磁盘高速缓存和物理磁盘的对应示例
正如图例所示,文件系统的作用就是决定如何将文件的数据分配在磁盘
上,而图 1.9 中的对应关系正是记录在元数据区中的。
因此,图 1.6 中文件系统向 IO 调度器发出数据传输请求时,文件系统
就会对物理磁盘上的连续数据进行分割处理。如此一来,IO 调度器就能够有
效地读取物理磁盘上的连续数据。说得夸张点,即便在考虑性能问题时,对 IO 调度器的理解也是非常重要
的,因此这里会对 IO 调度器做尽可能全面的介绍。
首先,文件系统会将数据分配到物理磁盘上的连续空间(连续扇区)中,在内核中整合创建出一个 bio 对象,再把这个 bio 对象传递给 IO 调度
器。从图 1.9 中可以看出,有两个 bio 对象要传递给 IO 调度器。
IO 调度器接收到来自文件系统的 bio 对象,会将多个 bio 对象合并为
一个请求对象,再将它加入到请求队列中。(图 1.10)
图 1.10 IO 调度器的结构
请求对象的合并方法因 IO 调度器而异,一般来说,如果 bio 对象指定
的连续扇区同时也是请求队列中已有的请求对象所指定的连续扇区,那么就会
将这两个对象合并。如果没有一致的请求对象,则创建一个只包含该 bio 对象
的新的请求对象。
最后,物理磁盘空间上的数据才会在请求对象所指定的区域进行读取或写
入。连续扇区上数据的读取和写入就是这样一并进行的。
此外,这些请求对象的实际处理顺序也是因 IO 调度器而异的。
例如 cfq(complete fair queuing)调度器,其进程的 bio 对象会被
加入到不同的子队列中,以便尽量公平处理每个进程的请求。从图 1.10 中可以了解子队列的机制。IO 调度器接收的 bio 对象,首
先会被作为请求对象加入到子队列中。子队列中的请求对象增加到一定程度
后,便会按照实际传输处理的顺序从子队列转移到调度队列中,进行实际的数
据处理。
现在大家应该都明白 IO 调度器的机制了吧。接下来对具体的 IO 调度
器之间的区别进行介绍。Linux 中主要使用的是以下四种 IO 调度器,它们
的特征分别如下:
noop(no optimization)调度器:只有一个子队列,按照 bio
对象被接收时的顺序处理请求。
cfq(complete fair queuing) 调度器:bio 对象被接收后,获取发送该 bio 对象的进程 ID,根据散列函数,将进程 ID 映射到
64 个子队列的某个队列中,再处理各个子队列中一定量的请求。这
样,每个进程的 IO 请求都会被公平执行。
deadline 调度器:分别用不同的队列来维护读请求和写请求。
将接收的 bio 对象根据其对应的扇区位置插入到请求队列中最合适的
位置(位置的排列以尽量减少磁盘头的移动为原则)。但是,当新的
bio 对象被不断插入到请求队列的前方时,队列后面的请求恐怕就会
长时间得不到处理,因此需要优先处理一定时间内(读请求为 0.5s,写请求为 5s)没有得到处理的请求。
anticipatory 调度器:在 deadline 调度器的基础上,追加
了“预测”下一个 bio 对象的功能。例如,当收到进程 A 发出的读
请求后紧接着又收到了来自进程 B 的请求时,就可以预测进程 A 可
能很快会发出另一个读请求,这种情况下就要在调度进程 B 的请求之
前延迟一段时间(0.7 秒左右),以等待来自进程 A 的下一个读请
求。
这些用来处理请求的不同 IO 调度方法也被称为“电梯算法”。因为要在
最大限度减少磁盘头移动的情况下,读取和写入尽可能多的数据,这就好比大
厦里的电梯,通过最少的运动承载尽可能多的人。
11最早在 Linux 中使用的电梯算法是由 Linus 先生设计的,因此过去也曾
被称为“Linus 电梯”。
RHEL6 中默认的 IO 调度器是 cfq。如果需要对单个硬盘所采用的 IO
调度器进行修改,可以通过 echo 命令在 sys 文件系统的特殊文件
sysblock<磁盘名>queuescheduler 中写入 IO 调度器的名称
(noop、anticipatory、deadline 或 cfq)。
下面就是一个变更 IO 调度器的例子,将 devsda 当前使用的 cfq
调度器更改为了 deadline 调度器。
cat sysblocksdaqueuescheduler
noop anticipatory deadline [cfq]
echo deadline > sysblocksdaqueuescheduler
cat sysblocksdaqueuescheduler
noop anticipatory [deadline] cfq
如果只是确认当前的 IO 调度器,也可以使用 lsblk 命令。如下
SCHED 部分表示的就是当前所采用的 IO 调度器。
lsblk -t
NAME ALIGNMENT MIN-IO OPT-IO PHY-SEC LOG-SEC ROTA SCHED RQ-SIZE
sda 0 512 0 512 512 1 cfq 128
├─sda1 0 512 0 512 512 1 cfq 128
├─sda2 0 512 0 512 512 1 cfq 128
├─sda3 0 512 0 512 512 1 cfq 128
└─sda4 0 512 0 512 512 1 cfq 128
如果需要修改系统默认的 IO 调度器,则在 GRUB 的配置文件
bootgrubgrub.conf 的内核启动项中设置“elevator=deadline”。
最后需要注意的一点是,对于被 devsda 等指定的设备,IO 调度器可
以将其想象成图 1.1 中扇区排列在物理磁盘的磁道上那样的构造,实现访问的
最优化。但是在使用外部磁盘装置的情况下,RAID 阵列的逻辑驱动器(LUN)会被
识别为 devsda 等设备。这种时候,存储装置的控制器也能实现磁盘访问的
最优化,就不需要 Linux 的 IO 调度器去进行过于复杂的优化工作了。
仅就一般而言,对于在数据库服务器或服务器虚拟化环境这样使用高性能
外部存储设备的复杂环境中的数据访问,有时侯 deadline 调度器要比 cfq
调度器效率更高。
顺便说一下,虽然 cfq 调度器的解释是“对每个进程实行公平的 IO 处
理”,但是为了公平的处理,有时候也需要刻意营造一些不公平。
大家或许并不熟悉,其实通过 ionice 命令可以对 cfq 调度器中的每一
个进程设置优先级。优先级有三类,分别是 Real time、Best effort 和
Idle。
和系统上的其他进程相比较,Real time 级别的进程的 IO 是最为优先
被处理的。而 Idle 级别进程的 IO 正相反,只有在系统上所有进程的 IO
都处理完了的情况下才处理它。换句话说,除了这类进程之外没有别的进程的
IO 需要被处理,即系统处于空闲状态时,才会对它进行处理。Best effort
则是默认的优先级,基于 cfq 调度器的正常逻辑,公平地进行 IO 处理。
对于优先级为 Real time 和 Best effort 的进程,还能在同一级别中
依次指定数值为 0~7 的优先级参数,数值越小,优先级越高。
因此,对于那些由于特殊原因不允许延迟的进程,就可以将其优先级设置
为 Real time 级别。
例如,RHEL6 标准的 HA 集群系统(High Availability Add-On)中,有一种被称为“仲裁磁盘”(quorum disk)的机制,它通过将数据定期写入
共享磁盘来确认服务器是否运行正常。若写入出现延迟,有时会误报服务器发
生故障,因此就可以考虑将该进程(qdisk)的优先级设置为 Real time 级
别。
但是也要注意,如果某进程有大量的 IO 处理,若还将其优先级设置为
Real time 级别的话,其他进程的处理就会完全停滞,从而引发其他问题。关于 ionice 命令的使用,可以参考手册页。要想确认 ionice 的运行效
果,则推荐试试下面这个命令。
ionice -c 1 dd if=devzero of=tmptmp0 bs=1M count=500 oflag=direct ionice -c
3 dd if=devzero of=tmptmp1 bs=1M count=500 oflag=direct wait
[1] 18764
[2] 18765
500+0 records in
500+0 records out
524288000 bytes (524 MB) copied, 4.59358 s, 114 MBs
[1]- 结束 ionice -c 1 dd if=devzero of=tmptmp0 bs=1M count=500
oflag=direct
500+0 records in
500+0 records out
524288000 bytes (524 MB) copied, 9.3951 s, 55.8 MBs
[2]+ 结束 ionice -c 3 dd if=devzero of=tmptmp1 bs=1M count=500
oflag=direct
这个例子中有两个同时执行的 dd 命令,这里 dd 命令用于导出 500M 大
小的文件,它们的优先级分别被设置为 Real time 和 Idle。Idle 优先级的
IO 处理完全被推迟,在 Real time 优先级的 dd 命令结束后,Idle 优先
级的 dd 命令才开始执行。这样一来,Idle 优先级的执行时间正好是 Real
time 优先级的两倍(图 1.11)。
图 1.11 Real time 级别和 Idle 级别
dd 命令中的指定选项 oflag=direct,表示直接写入物理磁盘而不使用磁
盘高速缓存,常常被用来测量物理磁盘的性能。至于最后 wait 命令的含义,我们将在下一节中进行说明。
8此处为日文资料。——译者注
RHEL6 的 mke2fs 命令,以及 tune2fs 命令对应于 ext4 文件系统。mkfs.ext4 命令,等同
于“mke2fs -t ext4”。
Linux 初始 RAM 磁盘 initrd 是系统引导过程中挂载的一个临时根文件系统,它与 RAM 磁盘空间是
两个概念。——译者注
默认为 26=64。——译者注
8
9
10
111.3 控制进程就等于控制Linux
众所周知,Linux 上的工作都是通过进程来执行的。若把系统的各种行为拟人
化,进程的一生便是时而稳定,时而“死去”又“活来”,可谓十分曲折。有的进
程甚至每天都被严密地监视着。
在本节,我们将再次探讨进程管理机制的基本知识,看看这种机制是如何支撑
和左右这些进程的命运的。尤其在理解了进程的 fork 之后,脚本的创建技术将得
到质的提高。使用 fork 来编写脚本的方法,在本书 3.4.3 节中亦有介绍。
1.3.1 fork 和 exec 分别是进程的分身和变身
登录系统控制台时,需在“login:”提示符处输入用户名进行登录。这
里“login:”提示符表示的是接收用户名的输入,执行该步骤的是 mingetty 进
程。登录成功后,命令提示符下显示 bash 启动。确切地说,是在 bash 进程启动
时,即显示相应的命令提示符。然后,在命令提示符下运行 ls 命令启动 ls 进
程,可以显示当前目录下的文件名。
大家可能觉得这些都是小儿科,实际上,这一连串进程动作的发生隐藏了一个
至关重要的玄机。我们都知道,在 Linux 上创建进程有 fork 和 exec 两种方
法,大家能准确地说出这两者的区别吗?
干脆来做一个小小的实验吧。首先,在运行级别 3 下启动一个 Linux 服务
器,之后 SSH 远程登录,查看 mingetty 进程的执行情况。
ps -ef | grep mingett[y]
root 1692 1 0 19:21 tty1 00:00:00 sbinmingetty devtty1
root 1694 1 0 19:21 tty2 00:00:00 sbinmingetty devtty2
root 1696 1 0 19:21 tty3 00:00:00 sbinmingetty devtty3
root 1700 1 0 19:21 tty4 00:00:00 sbinmingetty devtty4
root 1702 1 0 19:21 tty5 00:00:00 sbinmingetty devtty5
root 1704 1 0 19:21 tty6 00:00:00 sbinmingetty devtty6
grep 命令的参数中,通过将它最后一个字母 y 写成 [y] 的方法,可以确保
12grep 进程名的结果中不包含有与进程名不符的信息 。这里找出了 6 个
mingetty 进程。这六个虚拟控制台,可以通过 Ctrl+Alt+F1~F6 分别切换到它们
对应的“login:”提示符下。
思考一下,为什么 grep 命令中指定的检索字符串就可以顺利地使用正则表达式呢?
在控制台中按下 Ctrl+Alt+F1,出现“login: 提示符”,输入用户名,然后
输入后回车,出现输入密码的提示。这里请先不要输入密码。
这里再次在 SSH 远程登录的终端,通过 ps 命令查看 mingetty 进程。
ps -ef | grep mingett[y]
root 1694 1 0 19:21 tty2 00:00:00 sbinmingetty devtty2
root 1696 1 0 19:21 tty3 00:00:00 sbinmingetty devtty3
root 1700 1 0 19:21 tty4 00:00:00 sbinmingetty devtty4
root 1702 1 0 19:21 tty5 00:00:00 sbinmingetty devtty5
root 1704 1 0 19:21 tty6 00:00:00 sbinmingetty devtty6
mingetty 进程的数量减少了一个,ID 为 1692 的 mingetty 进程似乎消失
了。难道这个进程接收一个用户名就会消失吗?
并非如此。这一次,用 ps 命令检索进程 ID。
ps -ef | grep 169[2]
root 1692 1 0 19:21 tty1 00:00:00 binlogin --
显示的是 login 进程。知道为什么会这样吗?事实上,mingetty 进程在
exec 系统调用的作用下,已经转变为 login 进程了(图 1.12 左图)。
12
12图 1.12 exec 和 fork 中进程的变化
Linux 的进程,除了进程主程序代码外还有其他各种各样的信息,比如用于识
别进程的进程 ID 就是其中之一。除此之外,还有用于和其他进程或文件交换数据
的管道、文件描述符等。
exec 的作用是舍弃进程原本携带的信息,在进程执行时用新的程序代码替代调
用进程的内容。mingetty 进程的工作则是接收登录用户名,但之后对密码的验证
处理工作则移交给 login 进程继续完成。
图 1.13 是 mingetty 进程中运行 exec 的部分源代码。每行开头的编号表
示源代码中实际的行号。454 行的函数 execl,通过 exec 系统调用切换到变
量 loginprog 所指定的程序中去(这个例子中是 binlogin) 。这之后执行
的便是 binlogin 进程,因此通常 455 行之后的代码便不会再运行了。只有在
exec 系统调用出现失败的情况下,才会运行 455 行之后的代码。
调用 exec 系统调用的 C 语言函数有 execl、execlp、execle、execv、execvp 等。execl 和
execv,由于命令的参数的传递方法的不同,即便最后都附加 p,在 PATH 环境变量中进行命令检索时也仍有
区别。
mingetty.c
454 execl (loginprog, loginprog, autologin? -f : --, logname, NULL);
455 error (%s: can't exec %s: %s, tty, loginprog, strerror (errno));
456 sleep (5);
13
13457 exit (EXIT_FAILURE);
图 1.13 mingetty 中运行 exec 的部分源代码
之后,exec 开始执行 login 进程,在接收密码输入完成用户认证后,便启动
用户的 bash 进程。
之前出于实验需要中断了密码的输入,实际上,只有输入密码才算完成了登录
操作。在输完密码的情况下,我们再次检索同一个进程的 ID。
ps -ef | grep 169[2]
root 1692 1 0 19:21 ? 00:00:00 login -- root
root 1818 1692 0 19:29 tty1 00:00:00 -bash
login 进程仍然保留了与之前相同的进程 ID(1692)。另外,此次检索结果
中还包含了 bash 进程。bash 进程本身的进程 ID(1818)与检索的进程 ID 并
不匹配,但是它的父进程的进程 ID(1692)与 grep 检索中的进程 ID 互相匹配
了(ps –ef 命令的输出中依次包含用户 ID、进程 ID 以及父进程的进程 ID)。
这说明 bash 进程是作为 login 进程的子进程开始启动的。
通常我们说“fork 一个进程”,指的是通过父进程创建一个子进程。但现在的
情况是,bash 进程并不是从 login 进程中直接 fork 出来的。从图 1.12 的右
图可以看出,fork 生成的子进程是一个与正在运行的进程完全相同的副本。
login 进程通过 fork 生成一个自身的副本后,又在子进程中通过 exec 启
动 bash,这一技术简称为“fork-exec”。图 1.14 描述了它的整个流程。图 1.14 从虚拟控制台登录时伴随的进程的变化
如果诸位仍觉得对 fork-exec 一头雾水,那么就从 login 的源代码中看看
fork-exec 实际的执行过程吧。Linux 和开源最伟大的地方就是无论什么都能亲自
探寻。关于如何学习源代码,在本书的第 4 章中也有所介绍,这里只是暂时先在图
1.15 中给出答案。以 标记的注释,是笔者自己补充的。
login.c
1183 child_pid = fork; 子进程的进程 ID 赋值给父进程的 child_pid
子进程的 child_pid 中代入 0
1184 if (child_pid < 0) {
fork 失败时的错误处理
(部分源代码省略)
1189 exit(0);
1190 }
1191
1192 if (child_pid) {
父进程从这里开始执行
这个例子中,等子进程结束后,自己也就结束了
(部分源代码省略)
1201 wait as long as any child is there
1202 while(wait(NULL) == -1 errno == EINTR)
1203 ;
(部分源代码省略)
1206 exit(0);
1207 }
1208
子进程从这里开始执行
1209 child
1210
(部分源代码省略)
子进程通过 exec 变换为 bash
1280 execvp(childArgv[0], childArgv + 1);
图 1.15 login 中进行 fork-exec 的源代码
首先,在 1183 行代码中直接执行了 fork。这里的进程一分为二,且都是从
1184 行开始继续执行的。当然这样并没有意义,因为这两个进程是完全相同的,所
以之后子进程需要通过 exec 变换为 bash 进程。
这里用到一个小技巧,即把 fork 的返回值赋给变量 child_pid。fork 执行
失败时,父进程返回负值。fork 执行成功时,父进程以及通过 fork 创建的子进
程返回的值却不相同。父进程的 child_pid 值为子进程的进程 ID,而子进程的
child_pid 值为 0。
因此,父进程在 1192 行的 if 语句上条件成立,并执行 if 语句中的代码。
这个例子中,父进程会一直等待子进程结束,直到子进程退出时,父进程才结束。子进程跳过 1192 行的 if 语句,从 1209 行处开始执行。这个例子中显示的是在
1280 行调用 exec,进程变换为 bash 继续执行。
fork 也可以采用 Perl 等脚本语言来实现。如果你不擅长 C 语言编程,那就
请务必牢记这里所介绍的 fork 的处理技术。
最后来看看进程结束的过程吧。首先,父进程 login 通过执行图 1.15 中
1202 行的 wait 函数,一直休眠直到子进程结束。如下例所示,通过 ps 命令确
认进程的状态,可看到附加的表示休眠状态的 S 符号。
ps aux | grep logi[n]
root 1692 0.0 0.2 77004 2532 ? Ss 19:21 0:00 login -- root
这时在已登录的虚拟控制台上显示的 bash 命令提示符处,输入 exit 进行用
户注销。此时,通过 exit 系统调用,bash 进程被终止,同时发送 CHLD 信号
给父进程 login。
接收到 CHLD 信号的父进程 login 会退出 wait 函数,同时结束进程。wait
是一个函数,它让父进程在接收到子进程 CHLD 信号之前一直保持休眠状态。
而另一方面,子进程向父进程发送 CHLD 信号,直到父进程接收为止,子进程
都处于“僵尸进程”的状态。如果父进程设置为忽略 CHLD 信号,子进程就会一直
保持僵尸状态。
在通过 ps aux 来查看进程状态时,僵尸状态会被标记为Z,信息末尾会显示
图 1.16 表示的是 bash 进程在被 exit 终止后的一系列流程。从图中可以
看到,login 进程在结束之际向某处发送了 CHLD 信号,那么谁会接收到这个信号
呢?图 1.16 从虚拟控制台退出时伴随的进程的变化
从之前 ps 命令的输出中可以看到,login 进程的父进程是进程 ID 为 1 的
init 进程。因此,init 进程将会接收 CHLD 信号,这时 login 进程就可以从僵
尸状态中解放出来。
另一方面,接收到 CHLD 信号后,init 进程会检测到 login 进程(或是原
始的 mingetty 进程)已经终止了,于是会再度启动新的 mingetty 进程,这样
便可以在虚拟控制台上再次登录了。启动 mingetty 进程需要用到之前介绍的
fork-exec,因此 mingetty 进程相当于 init 进程的子进程。
此外,一直到 RHEL5,init 进程都是根据配置文件 etcinittab 来重新启
动 mingetty 进程的。但如今发展到 RHEL6,init 进程的启动机制已替换为被称
作“Upstart”的新机制,启动时采用的是 etcinit 下的配置文件。
关于 Upstart,5.2 节会有详细的介绍。
最后介绍一下与 fork-exec 机制相关的进程监控的问题。
用 fork-exec 创建新的进程时,最初 fork 的瞬间,子进程的进程名和父进
程的进程名是相同的。之后才通过 exec 变更为新的过程名。因此,在进行进程监视,对特定进程名的进程数量进行检查的时候,在 fork-exec 执行的那一瞬间,需注意进程数量可能大量增加。
如果设定为“不允许存在同样命名的进程”的话,fork-exe 和监视间隔的时
间会出现微妙的一致,从而误检测到异常的进程数。这种现象并不少见,因此为了
避免判断时带来麻烦,请务必注意到 fork-exec 的存在。
1.3.2 作业控制中的各项任务处理
前文介绍了进程启动的基本原理。下面再来介绍一些实用的技术吧。
首先,在 shell 命令提示符下运行命令,通常在命令执行完毕后,命令提示
符会再度出现,这与 login 进程启动 bash 进程的流程(图 1.14)是一样的。
bash 进程首先响应命令,通过 fork-exec 创建了一个新的进程,之后便会一
直处于休眠状态直到子进程结束。接着 bash 进程接收到 CHLD 信号,确认子进程
已终止,再次显示命令提示符,提示接收下一个命令。
在命令的末尾加上“”符号,可以让命令在后台运行。这种情况和上文一样,bash 进程会通过 fork-exec 创建新的进程,不同的是,此时 bash 进程不再需
要等待子进程结束,便可以直接显示命令提示符,提示接收下一条命令。在执行比
较花时间的命令时,让多条命令并行执行不失为一种便利的方法。
但这里存在一个应用的问题。怎样才能让多条命令并行执行,并且等待所有命
令都执行结束呢?
举个例子,可以考虑通过 shell 脚本实现 SSH 在 10 台服务器上执行远程命
令,并等待所有服务器上的命令执行完毕。图 1.17 是笔者实际使用过的 shell
脚本的例子,其目的是在三台服务器(node01~node03)上启动群集服务
(clstart)[5]。为方便起见,将该脚本命名为 clstart_all.sh。
Technical Notes
[5] High Availability Add-On 设计和运用入门“集群的启动顺
序”http:www.slideshare.netenakairhel6-rhcs-guidepreview-
148758112
此处为日文资料。——译者注
例 1 在每一台服务器上执行
!binsh
ssh node01 usrlocalbinclstart
ssh node02 usrlocalbinclstart
ssh node03 usrlocalbinclstart
例 2 命令执行完之前结束
!binsh
ssh node01 usrlocalbinclstart
ssh node02 usrlocalbinclstart
ssh node03 usrlocalbinclstart
例 3 命令执行完之后退出
!binsh
ssh node01 usrlocalbinclstart
ssh node02 usrlocalbinclstart
ssh node03 usrlocalbinclstart
wait
图 1.17 在多个服务器上并行执行命令的 Shell 脚本
(clstart_all.sh)
如例 1 中所示,node01 运行 clstart 结束后,node02 接着执行,即每一
台服务器按顺序依次执行。这显然是很浪费时间的。
因此,在例 2 中,笔者尝试在后台执行每个命令。这种情况下,三台服务器能
同时正确执行 clstart 命令。但是,在每一台服务器上的 clstart 命令运行结
束前,脚本的运行就已经结束了。
14
14这里就出现了一个问题,即没法在所有服务器的群集服务启动完成后,再接收
下一条输入命令。因为运行于后台的命令还未显示输出信息,命令提示符的显示就
已经提前出现了,从而导致控制台显示出现混乱。
于是,例 3 中作了进一步的改良。虽然只是在最后添加了 wait 命令,但这
是 bash 的一个内部命令,其功能与在图 1.15 的第 1202 行出现的 C 语言的
wait 函数相类似。
wait 命令执行后,clstart_all.sh 便进入休眠状态,等待后台运行中的子
进程全部结束后发来 CHLD 信号通知。所有的子进程结束后,再处理 wait 之后的
下一个命令。
这里 shell 脚本的 wait 命令结束了,因此所有服务器上的 clstart 命令
结束的时候,clstart_all.sh 也就结束了。这种处理方法被称为“异步执行的等
待处理”。
所谓“异步执行”,指的是各自处理后台进程。另外还有一个“等待处理”,指的是父进程在等待所有子进程都报告执行完毕后,再进行下一步处理。这是一种
常用技术,例如在多线程并行编程中就常常被使用到。
此外,在例 2 中,后台 ssh 命令仍然还在执行,其父进程 clstart_all.sh
便先行结束了。这种父进程先于自己结束的子进程便被称为“孤儿进程”。
由于父进程已结束了,进程结束时发出的 CHLD 信号便无法被接收,从而产生
僵尸进程。因此一旦产生孤儿进程,进程 ID 为 1 的 init 进程便会自动成为它
们的父进程。孤儿进程所发出的 CHLD 信号,均由 init 进程接收并确认进程结
束,自然也就不必担心产生僵尸进程了。
以上示例均展示了如何通过 shell 脚本在后台执行子进程。另外,直接在后
台执行命令提示符下达的命令时,应格外注意 shell 输出重定向的最终处理。
若将后台执行命令的输出结果直接显示在屏幕上,接下来输入的命令就会将前
面的输出结果覆盖掉,这是很麻烦的事情。因此可以将在后台执行的命令的屏幕输
出,重定向到文件中。以下列举了三种具有代表性的例子。 command >cmd.log 2>cmd.err ←-标准输出写至文件cmd.log中,标准错误输出写至文件cmd.err中
command >cmd.log 2>1 ←--------------标准输出和标准错误输出都写至cmd.log中
command >devnull 2>1 ←------------丢弃标准输出和标准错误输出
一般来说,在屏幕上输出的 Linux 的进程内容分为两类:“标准输
出”(stdout)和“标准错误输出”(stderr)。
默认情况下两者都会在执行命令的终端上输出内容,但 stdout 和 stderr 被
定义为程序中的两个不同输出来源。进程的正确处理结果被送往标准输出,其他的
错误信息则被送往标准错误输出。
第一个例子中,命令(command)的标准输出写至文件 cmd.log 中,标准错误
输出写至文件 cmd.err 中。标准输出和标准错误输出,分别用文件标识符 1 和 2
来标识,因此正确的写法应该为“1>cmd.log 2>cmd.err”。但是,由于表示标准
输出的“1”是默认值,故在这里可以省略不写。若需要在现有文件之后附加内容,就请用“>>”来代替“>”。
在第二个例子中,标准输出和标准错误输出都写至同一个文件 cmd.log
中。“2>1”表示的是,将标准错误输出和标准输出统一到一个位置。
最后一个例子中,是将输出写入一个特殊文件 devnull 中。devnull 会
将输出内容作丢弃处理,稍微了解一点 Unix 知识的人会称之为“黑洞”。这里需
要注意的是,如果出现拼写错误,例如写成“devnul”,就会在 dev 之下创
建一个输出文件。
需要与 devnull 一起记住的是 devzero。这是一个持续读取字节码
0(NULL 值)的特殊文件。如下所示,通过这种方式可以创建内容为 NULL 的
100M 的空文件。
dd if=devzero of=tmptmp0 bs=1M count=100
为了让 top 命令的执行更有意思,可以让 CPU 持续不断地被使用,下面就是
一个类似的有趣的例子。 cat devzero >devnull
还应该注意的一点是,用 exit 命令(或者是 logout 命令以及 Ctrl+D
等)结束 bash 时的动作。
运行于终端屏幕上的 bash 进程,会在结束的时候向每一个子进程发送 HUP
信号。由于 HUP 是一个令进程终止的信号,因此在这种情况下,运行于后台的命令
也会一并结束。
大家或许都有过如下的困扰。比如在后台运行较为耗时的基准测试程序
(benchmark)时常以失败告终,或者是在 PC 上使用 Putty 连接远端服务器
来运行基准测试程序时,由于运行太过耗费时间,便不得不让 PC 长期开着。
Putty 的正式写法是 PuTTY,它是一个 Telnet、SSH、rlogin、纯 TCP 以及串行接口连接软件。
——译者注
使用 nohup 命令即可解决这个问题,它可以在命令执行时设置成忽略 HUP 信
号。使用方法如下图所示。
nohup command >cmd.log 2>1
这样一来,即便通过 exit 命令结束了 bash 进程,运行于后台的命令
(command)也能无视 HUP 信号继续执行。但是由于身为父进程的 bash 进程结
束了,因而 sbininit 就成为孤儿进程的新的父进程。
除了 nohup 命令以外,screen 命令也很实用。通过使用 screen 命令,能
够在 Putty 的文本终端上创建多个虚拟终端窗口。并且即便关闭了 Putty 的文本
终端,这些虚拟终端仍会继续运行。这种情况下,重启 screen,便能再次连接上
之前的虚拟终端而恢复工作。
这么解释或许难以理解,还是来实际操作试试吧。因为并不一定默认安装
screen 命令,所以如果 screen 命令不存在的话,则导入同名的 RPM 包即可。
yum install screen
15
15在 SSH 连接终端上,运行 screen 命令。
screen
终端窗口顶部的标题显示的是 [screen 0:bash],它表示连接的是第 0 个虚
拟终端。在顶层窗口运行相应的命令,例如输入 Ctrl+A C(先按下 Ctrl+A,再按
下 C),就创建了第一个新的虚拟终端。此时顶层窗口消失,屏幕上重新显示
bash 命令提示符,窗口的标题部分变为 [screen 1:bash]。
若多次输入 Ctrl+A C,就能随意创建新的虚拟终端。之后输入 Ctrl+A“0~9
之间的数字=>”,就能返回到任意指定数字的虚拟终端上。例如,当你想返回第
0 个虚拟终端,输入 Ctrl+A 0,屏幕上就会显示运行中的 top 命令。至于 10
以后的虚拟终端,在 Ctrl+A(单引号)后加上数字即可。还可以用 Ctrl+A N
和 Ctrl+A P 在虚拟终端间进行前后顺序的来回切换。
此外输入 Ctrl+A D,还可以离开创建的虚拟终端,返回 screen 命令执行之
前的屏幕。乍一看似乎 screen 命令已经结束了,但实际上它仍然在后台运行,之
前创建的虚拟终端的会话也仍然存在。
终端关闭后,需要重新建立一个 SSH 连接,运行附加了 -r 选项的 screen
命令。
screen -r
输入 Ctrl+A D 之前的虚拟终端窗口再次出现,这样就可以恢复工作了。
通过这种方式,便可以在 screen 命令的虚拟终端上放心地运行耗时的基准测
试程序。screen 命令也能用于创建自定义的操作环境,具体可以参考手册页。
最后,来介绍一下任务管理的基本命令。
在 bash 命令行的末尾附加 可以让进程运行于后台,此时除了进程 ID外,还会被分配一个只能在 shell 环境中使用的“作业 ID”。
使用 jobs 命令可以查看正在运行中的作业。当 kill 命令向指定进程发送信
号时,可以使用作业 ID 替代进程 ID,例如“%1”即表示指定作业 ID 为 1 的进
程。
至于没有加上 符号的在前台运行的命令,通过 Ctrl+Z 将它暂停之后再运
行 bg 命令,就可以将其切换到后台。如果运行的是 fg 命令,则进程会再度回到
前台运行。
或者指定作业 ID 并执行 fg 命令,也可以将运行于后台的命令切换到前台。
图 1.18 作业的状态变化
笔者有时在 vi 编辑器中进行操作时,会不小心误按 Ctrl+Z。这时编辑器的
界面会消失并返回到命令提示符处。出现这种情况时请不要惊慌,只需运行 fg 命
令,就能安全返回 vi 编辑器的界面了。
1.3.3 快速的数据处理管道
最后就用管道处理的知识来结束进程这个话题吧。虽然管道只是一个基础知识,但其中蕴含着 Linux 从 Unix 中继承的重要概念。
管道机制是由 Doug McIlroy 发明的,他写下了如下这段话:
“程序应该只关注一个目标,并尽可能把它做好。程序应能够互相协同工
作。应该让程序处理文本数据流,因为这是一个通用的接口。”
Douglas McIlroy 是 Unix 系统上管道机制的发明者,也是 Unix 文化的缔造者之一。这三句话是他
归纳的 Unix 哲学,非常经典。原文:Write programs that do one thing and do it well. Write
programs to work together. Write programs to handle text streams, because that is a
universal interface.——译者注
说这段话的前提在于,“用户比起程序员更加清楚应该用电脑干嘛 ”。用户
利用管道的机制,能够自由组合程序员提供的的各种功能,从而实现必要的处理。
与此相反的想法是,“由程序员来决定程序的功能(用户需要的功能)”。基于这一想法开发的应用程
序虽然使用简单,但是存在应用不佳的缺点。由于用户所需要的功能是与时俱进的,因此经常需要进行大规模
的更新。
特别是服务器管理员,经常被要求在现有条件下充分利用可用的工具,快速完
成工作。虽然很多便利的工具在 Web 上就能检索到,但是如果没有时间安装这些工
具,或者是服务器不允许安装多余的工具,这个时候就需要开动脑筋对 Linux 服
务器里的自带工具进行灵活组装了。
管道处理的基础是标准的输入输出数据。之前介绍了程序的两种输出:标准输
出(stdout)和标准错误输出(stderr)。另外,程序的数据输入源是标准输入
(stdin)。默认情况下,标准输入指的是从终端的键盘输入。
例如 cat 命令,通常用于将指定文件名的内容输出到屏幕。如果没有指定文件
名,则把从标准输入中接收的内容一行一行地依次发送到标准输出上(图 1.19 的
上图)。
16
16
17
17图 1.19 标准输入输出和重定向
cat
Hello, ←-------------从键盘输入
Hello, ←-------------在屏幕上输出相同的内容
World! ←-------------从键盘输入
World! ←-------------在屏幕上输出相同的内容
←--------按Ctrl+D结束
把标准输入输出的位置从默认的键盘和屏幕切换到文件,这种处理称为“重定
向”。在之前 stdout 和 stderr 的重定向的介绍中,提到过 > 符号的使用方
法。这里将标准输入的位置变更为文件的话,使用 < 符号即可(图 1.19 的下
图)。
下例是先把从键盘输入的内容保存到文件 greeting.txt 中,再在屏幕上重新
显示。 cat > greeting.txt
Hello, ←-----------从键盘输入
World! ←-----------从键盘输入
←--------按Ctrl+D结束
cat < greeting.txt
Hello,World!
而将这两个进程的标准输出和标准输入直接相结合的,就是管道(图 1.20)。
两个命令之间用“|”号相连。
图 1.20 管道处理
此前,使用 grep 过滤 ps 命令的输出结果便可以写成如下。
ps -ef | grep mingett[y]
当然,也可以通过管道来连接三个或更多的命令。大家以前使用的命令中有多
少使用了管道呢,回想一下是不是很有意思?
笔者回想了一下,发现自己大量使用的是“history|grep~”。在确认过去执行的命令的选项时,常常
会用到这种方法。
history | grep |
在管道中,除 grep 命令外,还常常会用到 cut、tr、sort、uniq、paste、wc、head 等命令。表 1.4 总结了这些命令的使用说明。关于每个命令的更多详细
信息,请参考手册页。
18
18表 1.4 管道中常用的快捷命令
命令 说明
cut 通过分隔符拆分后,显示指定的域
grep 显示与模式相匹配的行
head 显示文件的开始部分
paste
通过指定的分隔符将两个文件的各行进行合并,或者通过指定的分隔符合并一个文
件中的多行
sort 对多行进行排序
tr 替换、删除字符,压缩文字序列
uniq 压缩连续的相同的行
wc 显示字节数、字数、行数
接下来做一个练习题吧。首先,将命令“ps -ef”的输出保存到文件
tmptmp0 中。用 head 命令查看前五行的内容,其结果如下所示。
ps -ef > tmptmp0
head -5 tmptmp0
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 Aug26 ? 00:00:06 sbininit
root 2 0 0 Aug26 ? 00:00:00 [kthreadd]
root 3 2 0 Aug26 ? 00:00:00 [migration0]
root 4 2 0 Aug26 ? 00:00:02 [ksoftirqd0]
举一个现实中的例子,通过 cron 作业将过去的 ps 命令的输出保存在文件中,以便之后可以作各种各样的分析。但在这里,请仅从文件中提取进程的
ID(PID),然后写入另一个文件中。
方法还有很多,请大家既不要检索 Web 也不要参考手册页,用立刻就能想到的
方法来试试看吧。
下面的例子给出了一个比较经典的答案。
cat tmptmp0 | tr -s | cut -d -f 2 | grep -v PID > tmptmp1
cut 命令把 -d 选项指定的文字作为文字分隔符,仅抽取 -f 选项定位的数据
(域)。这个例子中,使用空格作为分隔符。但是,由于连续的空格会被当作多个
分隔符,因此要事先通过 tr 命令将连续空格转换成一个空格。记住 tr 命令和
cut 命令这个组合,用起来十分便捷。
最后,使用 grep 命令删除标题行,将结果写入文件 tmptmp1 中。grep
命令的 -v 选项的意思是,只取出与条件不一致的行。
此外,使用这种方法的时候,要注意行的顶头是否包含了空格。这样的行中,因为顶头的空格也会被当作分隔符,所以域的位置就会后错一位。后面将会介绍如
何删除行顶头处的空格。
tr 命令和 cut 命令的组合也可以用 awk 来替代。
cat tmptmp0 | awk '{print 2}' | grep -v PID > tmptmp2
awk 使用连续的空格作为分隔符,自动分解输入的数据。这里 print2 指的
是打印位于第二个字段的数据。由于行头的空格会被自动忽略,因此不需要像前例
那样事先去除位于行头的空格。
如果想使用空格以外的分隔符,可以通过 -F 选项来指定。下面是一个被保存
在 etcpasswd 文件中的用户名列表的输出例子。
cat etcpasswd | awk -F : '{print 1}'顺便说一下,如果需要输出正在运行的进程的 ID 列表,使用 ps 命令的选项
即可,不需使用管道。
ps -e --no-headers -o pid > tmptmp3
但是,使用这个选项就和之前保存的形式略有不同。之前不论是 tmptmp1
还是 tmptmp2 中进程 ID 的记录都是采用行头左对齐的方式,但是在
tmptmp3 中,进程 ID 则采用右对齐,且行头包含空格。
这里可以稍微多做一些工作,组合使用 head 命令和 tail 命令,去除文件中
的部分内容进行比较。
head -98 tmptmp1 | tail -6
976
992
994
1044
1045
1046
head -98 tmptmp3 | tail -6
976
992
994
1044
1045
1046
如上图所示,tmptmp3 的行头的确包含了连续的空格。接下来就思考一下如
何去除行头空格的方法吧。
对于这种置换处理,比较经典的方法是使用 sed。如果需要将每行中的 AAA
替换成 BBB,一般用“sed sAAABBBg”即可。s 即为用来指定替换的 sed
命令,g 则表示替换行内出现的所有 AAA。如果省略了最后的 g,则只会替换行内
出现的第一个 AAA。tr 命令也可以实现相同的处理,不过和 sed 不同的是,后者允许使用正则表
达式。由于这里我们需要去掉行头的连续空格,因此如下用空字符串来替换即可。
cat tmptmp3 | sed s^ > tmptmp4
在 shell 脚本中常常会用到 awk 和 sed,这是读懂别人所写的 shell 脚本
的必备知识。虽然 Web 上就有铺天盖地的关于这方面的知识,但笔者还是建议大家
多找些 [6] 这一类的书,深入地研究学习一下。此书对 sed 和 awk 中允许使用
的正则表达式也做了详细的解说。
awk 尤其具有很强的通用性,不仅可以进行文本处理,还可以用来代替 perl
等高级语言。当然,用 perl 就可以轻易实现的处理就没必要非得用 awk 了,但
把 awk 作为一种技巧,总能在意想不到的地方起到意外的作用 。
笔者之前还曾把用 Perl 编写的脚本移植到不支持 Perl 的陈旧的 Unix 服务器中,进行从 Perl 到
awk 的移植操作。
在管道处理中,结合使用符号 也是一种便利的技术。这是 bash 的一个
功能,把命令输出插入 内,作为命令的一部分。举个例子,请思考下面这个
命令是用来干什么的。
yes kill | head -(cat tmptmp4 | wc -l)
中,输出的是文件 tmptmp4 中所包含的行数。假设这个结果是 150,那么这个命令等效于下图。
yes kill | head -150
上图表示的是 150 行“kill”的字符串。yes 命令用来无限输出指定的字符
串,这里即表示提取了前 150 行。
有一点需要注意,head 命令并不是在 yes 命令结束之后才执行的。因为如果
是那样的话,yes 命令永远不会结束,管道处理也就永远不会结束了。
19
19实际上,head 命令在 yes 命令开始输出的同时就接收其输入了。head 命令
接收了 150 行的输入后,就会先于 yes 命令结束。
Technical Notes
[6] 《sed 与 awk(第 2 版)》Dale Dougherty、Arnold
Robbins(著),张旭东等(译),机械工业出版社,2003
此时,yes 命令的输出已无法被接收,但如果它试图再次输出的话,就会收到
pipe 信号,然后 yes 命令才会终止。
当然,这只是一个虚构的小例子,下面我们来看一个经常会用到的实例。
mkinitrd -f bootinitramfs-(uname -r).img (uname -r)
这是一个用来指定启动内核的版本号,创建初始 RAM 磁盘文件的命令。uname
–r 的运行结果就是内核的版本号。
介绍完了管道处理,下面我们来作一些练习吧。
① 将用 root 用户以外的用户权限运行的进程 ID 写入文件 tmptmp0
中。
② 在文件 tmptmp0 每行的开头,增加字符串“kill -9”,然后再保存到
文件 tmptmp1 中。
③ tmptmp1 可以作为一个脚本,用来杀死 root 用户以外的进程。不使用
tmptmp0 这样的中间文件,请组合出一个能够执行相同处理的命令。
以上这些小问题,都是为了让大家了解更多不同的命令。当然,每一题的答案
也都不是唯一的。
①的答案
首先提供一个利用了管道处理方法的解答。 ps -ef | grep -E -v ^(root|UID) | awk '{print 2}' > tmptmp0
grep 命令指定了选项 -E,就能使用范围更广的正则表达式,即“扩展正
则表达式”。使用扩展正则表达式来匹配每行开头中的 root 或者是 UID,这
里将包含它们的所有行都删去。UID 位于 ps 命令第 1 行的标题部分。
但如果是查找满足一定条件的进程的进程 ID,则使用专门的 pgrep 命令
更为简单。
pgrep -v -u root > tmptmp0
有关 pgrep 命令中选项的更多信息,请查看手册页。比起使用复杂的管
道,这个例子教会我们,调查一下是否有实现同样功能的命令也很重要。
②的答案
前面的例子介绍了用空字符串替换(也就是删除)行头的连续空格。这里
却恰恰相反,用指定的字符串替换行头的空字符串,即在行头插入指定的字符
串。下面是一个使用 sed 的例子。
cat tmptmp0 | sed s^kill -9 > tmptmp1
另外,使用 awk 也是可行的。
cat tmptmp0 | awk '{print kill -9 0}' > tmptmp1
awk 中,n 表示的是第 n 个域,0 表示的是域分解前的整行字段 。
除此之外,也可以使用之前 yes 命令一例中的方法。
yes kill -9 | head -(cat tmptmp0 | wc -l) | paste -d - tmptmp0 >
tmptmp1
20paste 命令,是用选项 -d 指定的分隔符将两个文件按行合并。在这个例
子中,第一个文件名的地方使用的是 -,这意味着使用的是标准输入而不是文
件。生成与 tmptmp0 同行数的 kill -9,再通过 paste 命令将它们与
tmptmp0 进行合并。
做到这一步或许有些多余,但都是为了让大家记住 paste 命令的使用方
法。之后还会介绍其他用例,请提前参看手册页中关于 paste 命令的 -s 选
项。
③的解答
利用管道完美地结合①和②的答案,就可以避免使用 tmptmp0 作为中
间文件,但这里就不再详述了。实际上,和 pgrep 命令一样,有专门的
pkill 命令用来实现这种功能。
pkill -9 -v -u root
当大量的进程一起停止的时候,就需要将通过 ps 命令显示的进程 ID 逐
条复制和粘贴。但如果使用 pgrep 命令和 pkill 命令,就能简化这些工作。
所以请务必多加练习这些方法。
关于管道处理的介绍就到此结束了。要知道同样的工作可以有一百种方
法。要判断哪种方法最合适是需要经验的,这里介绍“透明
性”(transparency)和“发现性”(discoverability)这两种思路供参
考。
所谓透明性,指的是有经验的人看到代码时,能很快理解该代码是用做什
么处理的。可以说透明性高,就意味着较难发生意料之外的错误(即所谓的
bug)。可以这么想,别人看不懂的复杂代码,其中一定存在你自己也不能理解
的潜在问题。
而发现性,指的是即使不通过努力钻研,也能立刻上手使用代码,没有经
验的人使用起来也能得心应手。高发现性的代码,能够有效地应用于各种场合。
以上观点都是为了避免过度使用复杂选项。毕竟在不能详细检查命令选项
的情况下,可以快速应用的技术是非常重要的。
此外,由于透明性和发现性并非一定同时成立,因此需要根据实际状况来
判断优先考虑哪个。推荐大家从这两者角度,重新看看曾经使用过的命令或者
写过的脚本。
顺便提及,[7] 是一本通俗易懂的经典著作,讲述了设计开发 Unix 的一
群人的思想和经验教训。“只知道 Linux,没有接触过 Unix”的年轻工程师
们,读一读这本书,或许能从一个全新的视角来审视 Linux 呢。
awk 借用 shell 的方法,用 1,2,3... 这样的方式来顺序地表示行中的不同字段,用 0 表示整
个行。——译者注
201.4 至关重要的内存管理
最后让我们进入 Linux 内部结构的关键部分——内存管理吧。因为 Linux
内核中的管理机制对 Linux 的内核性能有很大影响,因此其构造被设计得极为精
妙。
例如,上一节介绍的管道处理是由多个进程共同完成同一项工作,此外,还有
在一个进程内处理多个操作的“多线程”机制。虽然很多人认为“多线程的性能优
于多进程共同工作的机制”,但也不能盲目地这么理解。
在早期的 Unix 系统中,利用之前介绍的 fork 和 fork-exec 启动新进程
时,由于从父进程往子进程复制内存信息需要花费一定的时间,因此启动多个进程
时难免性能表现不佳。但是,现在的 Linux 内核中则不会发生类似的情况。通过
之后将要介绍的“写时复制”(copy-on-write)等机制,反而可以更加高效地创
建进程。
说句题外话,关系数据库(RDB)是一个高性能的应用。开放源代码中有代表性
的 RDB 有 MySQL 和 PostgreSQL,相对于使用多线程的 MySQL,PostgreSQL
采用的就是多进程共同工作的机制。
Technical Notes
[7] 《UNIX 编程艺术》Eric S.Raymond(著),姜宏等(译),电子工
业出版社,2006
至于这样设置的原因,一位 PostgreSQL 的开发者给出了如下回答:“利用多
线程来提高性能,需要复杂的机制,其结果会导致程序中出现 bug 的可能性升高,而分析这些问题也需要花费更多的时间”。这种思考方法可谓与之前介绍的“透明
性”和“发现性”不谋而合。
很多书已对 Linux 的内存管理做过全面介绍,本书的侧重点在于对进程管理
和磁盘管理的相关内容做更进一步的深入讲解,比如用户进程中使用的内存管理
等。1.4.1 物理内存的分配
首先复习一下内存管理的基础——物理地址空间和逻辑地址空间的关系。
安装于服务器中的物理内存,大致可分为 Linux 内核自身使用的区域和用户
进程使用的区域。图 1.21 是 x86 架构(32 位)的分配方式。Linux 内核仅使
用 1MB~896MB 空间大小的低端内存,其他没被使用的空间则被分配给了用户进
程。如图 1.21 中,896MB 以上的高端内存空间被分配给了用户进程,不光如此,低端内存中的空闲区域也被分配给了用户进程使用。
图 1.21 内核内存和用户进程内存的配置(x86 架构)
此外,包括内核本身在内,服务器上运行的程序在访问物理内存时,并不直接
指定物理地址(图 1.21 中的“物理地址空间”所指定的地址),而是指定逻辑地
址(图 1.21 中的“逻辑地址空间”所指定的地址)。首先在内存的内核数据区域
中预先设置逻辑地址和物理地址的对应“页表”,然后 CPU 上搭载的
MMU(Memory Management Unit)硬件会参照该页表,自动实现对映射后物理地址
上的数据的访问。从图 1.21 中可以看出,因为每个进程有不同的逻辑地址空间,所以分别为每
个进程准备了一个页表。MMU 参照的是该 CPU 上运行的用户进程所对应的页表。
此时,内核使用的低端内存区域,会在全部页表中被共同映射到 3GB ~4GB 的
逻辑地址空间上。因此,不管当前运行的进程是什么,Linux 内核本身就能始终使
用 3GB ~4GB 的逻辑地址。
像这样,通过为每个进程提供独立的逻辑地址空间,每个进程的内存访问就各
自独立。进程 A 不能从自己的逻辑地址空间访问到进程 B 的物理内存,这也等于
实现了进程之间的安全保护。
虽然图 1.21 是基于 x86 架构的,但在 x86_64(64 位)架构下,其基本机
制也不会改变。
X86 架构中的逻辑地址空间范围限制在 4GB,其中内核可以使用的地址空间也
限制在 3GB~4GB 之间 。但在 x86_64 架构下,其逻辑地址空间范围不限定于
4GB,因此内核可以自由使用更大范围的内存空间。
用户空间占 3G,内核空间占 1G。——译者注
这样一来,也就不存在低端内存和高端内存之分了。在 x86_64 架构中,所有
内存空间都被认为是可供内核使用的低端内存。
内核管理知识的复习就到此为止。下面来看一看可分别供用户进程和内核使用
的内存有哪些类型。这里,我们把用户进程运行使用的内存以及磁盘高速缓存等称
为“用户内存”,其他用于内核自身运行的内存称为“内核内存”。
参考 proc 文件系统中的特殊文件 procmeminfo,我们来介绍一下用户内
存和内核内存分别有什么样的类型,以及如何查看它们各自的使用量。从
procmeminfo 中可以得到内核使用情况的各种信息,但由于基本是把内核内部的
管理信息原封不动地输出,如果不清楚内核的内部构造,也就无法理解其中的含
义。
于是干脆反其道而行之,通过这个输出来理解与内存管理相关的内核的内部构
造。
21
21用户内存的分类
图 1.22 是在笔者在 RHEL6.2 服务器上的输出。从最开始的 2 行
(MemTotal,以及 MemFree)可以看出,安装了空间为 8GB 的物理内存,当
前的空闲内存为 3GB 左右。其后显示的是各种各样的信息。
之前说的“用户内存”,是通过① ~ ④以及紧接其下的 Unevictable 的
总和来分配的。这个例子中约为 4.3GB。① ~ ④的值可以按表 1.5 进行整
理,至于 Unevictable,我们稍后再作介绍。
表 1.5 用 LRU 列表对用户空间的使用内存进行分类
Active Inactive
匿名内存 3282428KB 588724KB
File-backed内存 219448KB 214596KB
匿名内存和 File-backed 内存的区别在于,物理内存的内容是否与物理
磁盘上的文件相关联。
匿名内存,是用来存储用户进程用作计算的中间数值的,灵活确保程序在
执行时有可用的内存空间。如果熟悉 C 语言的话,说成是“通过 malloc
分配的内存”会更容易理解,其内存中的内容自然与物理磁盘上的文件没有任
何关系。
cat procmeminfo
MemTotal: 8069288 kB
MemFree: 3051032 kB
⑤ Buffers: 149412 kB
⑥ Cached: 308364 kB
SwapCached: 1352 kB
Active: 3501876 kB ←---Active(anon) + Active(file)
Inactive: 803320 kB ←---Inactive(anon) + Inactive(file)
① Active(anon): 3282428 kB
② Inactive(anon): 588724 kB
③ Active(file): 219448 kB
④ Inactive(file): 214596 kB
Unevictable: 0 kB Mlocked: 0 kB
SwapTotal: 2097144 kB
SwapFree: 2093400 kB
Dirty: 52 kB
Writeback: 0 kB
⑧ AnonPages: 3846228 kB
Mapped: 31616 kB
⑦ Shmem: 23732 kB
⑨ Slab: 564328 kB
SReclaimable: 242284 kB
SUnreclaim: 322044 kB
KernelStack: 3248 kB
PageTables: 50484 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 6131788 kB
Committed_AS: 7762476 kB
VmallocTotal: 34359738367 kB
⑩ VmallocUsed: 385112 kB
VmallocChunk: 34359342120 kB……( 以下省略 )……
图 1.22 procmeminfo 中的内存信息
而 File-backed 内存作为磁盘高速缓存的内存空间,其物理内存中的内
容与物理磁盘上的文件是相对应的。例如,将物理磁盘上的文件内容读入到磁
盘高速缓存中使用时,同样的数据也同时存在于物理磁盘上。此外,还有用
于“文件映射”的内存空间,能将物理磁盘上的文件内容与用户进程的逻辑地
址直接关联。
此外还有 Active 和 Inactive 的区别,即该内存上的数据最后一次被
使用的时间。包含有刚被使用过的数据的内存空间被认为是 Active 的,包含
有长时间未被使用过的数据的内存空间则被认为是 Inactive 的。当物理内存
不足,不得不释放正在使用的内存空间时,会首先释放 Inactive 的内存空
间。
匿名内存和 File-backed 内存的内存释放方法也有所不同,这一点稍后再作讨论。
为了将用户内存分为以上 4 类,Linux 内核中有着与表 1.5 相对应的 4
类“LRU(Least Recently Used)列表”,用户内存的各个内存页都会记录
在其中某一个列表上。LRU 在日语中是“最不常用”的意思,前文中提到,包
含长时间未被使用过的数据的内存空间会被优先释放,由此而得名。
“内存页”这个术语可能是第一次出现。Linux 内核将物理内存分割成
4KB 大小的“页”来进行管理,内存的分配、释放等处理都是以页为单位进行
的。
至于 Active 和 Inactive 的分类,则是根据内存的使用情况而灵活变
动的。当对 Inactive 的 LRU 列表记录的内存页进行访问时,该内存页便会
移至 Active 的 LRU 列表中。反之,Active 的 LRU 列表记录的内存页中如
果有长时间不被访问的数据,又会被移至 Inactive 的 LRU 列表中。
接着补充说明一下 Unevictable。前文介绍了“4 种 LRU 列表”,实际
上,还有一种 Unevictable 列表,记录的是“Unevictable”类型的内存
页。
在下一节中会介绍到,使用 LRU 列表对内存页进行分类,是为了更有效地
对内存进行释放处理。然而,原则上内存页中也有不能被释放的部分。这种内
存页,记录到之前介绍的 4 种 LRU 列表中是没有意义的,因此改为记录到
Unevictable 列表中。
这里通过 free 命令查看同一台服务器的内存使用情况,其结果如下所
示。
free
total used free shared buffers cached
Mem: 8069288 5018388 3050900 0 149420 308364
-+ bufferscache: 4560604 3508684
Swap: 2097144 3744 2093400
一般认为,在 free 命令的输出中,buffers 和 cached 的值的总和就是可以使用的磁盘高速缓存的大小。图 1.22 中的⑤⑥即表示了这两个值。
那么,“buffers+cached”的值和表 1.5 中的分类又会有什么样的关系
呢?
如果囫囵吞枣地理解先前的介绍,会觉得因为磁盘高速缓存属于 File-
backed 内存的一种,所以 File-backed 内存的总和即
为“Active(file)+Inactive(file)”所对应的值。
但从实际计算的结果来看,如下所示,buffers+cached 的值要多出
23732KB。
1. buffers+cached = 457776KB
2. Active(file)+Inactive(file) = 434044KB
仔细观察图 1.22 就会发现,它们之间的差与⑦ Shmem 所表示的值刚好
相同,这绝不是巧合。所谓的 Shmem,指的是 tmpfs 所使用的内存。
在这里先回顾一下 tmpfs。如同在 1.2.3 节中提到的,tmpfs 即利用物
理内存来提供 RAM 磁盘功能。安装的 tmpfs 类型的文件系统和普通的文件系
统一样,可以进行读写操作,但是文件的实体并不是物理磁盘,而是保存在服
务器的物理内存上。
在 tmpfs 上保存文件时,文件系统会暂时将它们保存在磁盘高速缓存
上。这便是 Shmem 的本质所在,它属于磁盘高速缓存所对应
的“buffers+cached”一类。但是由于在物理磁盘上并没有与之对应的空间,因此它不属于 File-backed 内存对应的 LRU 列表,而是记录在匿名内存的
LRU 列表上。这就是为什么 File-backed 内存的总和
与“buffers+cached”的值有所出入。
另外,由于匿名内存是换出的对象,因此 tmpfs 也成为了换出对象。使
tmpfs 成为换出对象的机制就是在这里设置的 。
这里将 tmpfs 使用的内存称为 Shmem 是有原因的。在 Linux 中,为了
22实现“共享内存”(shared memory)功能,即多个进程共同使用同一内存中
的内容,需要在内部使用 tmpfs。具体来说,就是通过文件映射功能,把在
tmpfs 中创建(物理内存上)的文件映射至多个进程的逻辑空间中。
出于这些原因,内核内部中 tmpfs 使用的内存便被称
为“Shmem”(shared memory 的缩写)。内核源代码中,tmpfs 的功能也是
通过一个名为 mmshmem.c 的文件来实现的。
它们之间的关系,就如图 1.23 所示。虽然难免存在误差,但大体来说下
面的关系式是成立的 。
用户内存
= Active(file) + Inactive(file) + Active(anon) +
Inactive(anon) + Unevictable
= buffers + cached + AnonPages
最后,介绍一下图 1.23 右上部分所示的 AnonPages。
图 1.23 用户内存的分类
Linux 内核中存在着一个 rmap(reverse mapping)机制,负责管理匿
名内存的每一个内存页,即“该页是被映射到哪个进程的哪个逻辑地址的”这
类信息。这个 rmap 中记录的内存页的总和应与 AnonPages 相对应,也就是
图 1.22 中⑧所表示的数值。
23要让 Shmem 成为换出的对象,就要以匿名内存的形式记录到 LRU 列表
中,但由于其具有文件系统的功能,因此无法记录在 rmap 里。这样一来,Shmem 的值(同时也是磁盘高速缓存和 File-backed 内存之间的差值)也就
成了 AnonPages 和匿名内存之间的差值。
此外,之前介绍的 Unevictable 列表中所包含的页面,在图 1.23 中省
略了。
内核内存的分类
从全部的内存使用量中减去用户内存的使用量,就是内核内存的使用量。
图 1.22 中的① ~ ④加上 Unevictable 的和便是用户内存的使用量。从图
中的注释可知,① ~ ④的和也可以通过“Active+Inactive”来计算。
因此,内核内存的使用量可以按如下方法计算。
内核内存 = MemTotal –(MemFree + Active + Inactive +
Unevictable)
当然,仅凭这个公式也无法得知内核内存的具体细节。遗憾的是,并不是
所有的内核内存信息都会输出到 procmeminfo 中,这里仅针对典型应用来
说明如何确定其使用量。
首先,图 1.22 中的⑨ Slab 是由“Slab 分配器”分配的总量。内存空
间存储着内核所使用的各种数据,但它并不是每次都寻找空闲空间来进行分
配,而是事先汇总数据,在数据类型的基础上再进行分配,提供这一机制的便
是 Slab 分配器。
之前说过,物理内存的分配和释放是以 4KB 大小的页为单位进行的。因
此,在需要 4KB 以下内存的情况下,要将 4KB 的内存页分割好后再分配使
用。事先进行这种分割处理的,也是 Slab 分配器。
Slab 有石板的意思,大概是由于与巨大的岩石被分割为石板的情形相似而
得名的吧。准确地说,内核根据每种数据的类型,创建由多个内存页组成
的“Slab”,再将 Slab 内部划分成一个个相应数据类型的“对象” 。图 241.24 的例子中,对象 A 就有三个 Slab,对象 B 有两个 Slab。
图 1.24 slab 分配器
当内核使用该类型的数据时,就会使用对应的 Slab 内部的对象。当可供
使用的对象不足时,便会用空闲的内存页来创建并添加新的 Slab。相反,当使
用过的对象被从内核中回收,即 Slab 中的所有对象均变作未使用时,根据需
要也可以回收 Slab,使之还原为空闲的内存页。
图 1.22 中,⑨ Slab 的下面有 SReclaimable 和 SUnreclaim 两个
值。SReclaimable 指的是不存在活跃对象、可以回收的 Slab 的容量,SUnreclaim 则表示的是对象在活跃,不能被回收的 Slab 的容量。
从 procslabinfo 的输出中,也能确认每个 Slab 的信息。
cat procslabinfo
slabinfo - version: 2.1
name
tunables
fat_inode_cache 24 72 680 24 4 : tunables 0 0 0 : slabdata 3
3 0
fat_cache 0 306 40 102 1 : tunables 0 0 0 : slabdata 3
3 0
fuse_request 52 104 608 26 4 : tunables 0 0 0 : slabdata 4
4 0……以下省略……因此,例如从保存“fat_inode_cache”的对象的 Slab 可得知,三个
(
可了解到,一个 Slab 所占用的内存页数(
所包含的对象数(
状态的对象个数(
提醒读者注意,仔细看看图 1.24,会发现每个 Slab 中对象的放置位置
在一点一点地错开。这是用于提高 CPU 中内存高速缓存的利用效率的技术,被
称为“高速缓存着色”。
CPU 的内存高速缓存被划分成多个存储区域(槽 slot),根据内存的物
理地址来选择使用的槽。此时,如果每个对象的物理地址排列整齐,那么多个
对象便会被分配到相同的槽中,导致无法在同一时间缓存多个同类型的对象。
高速缓存着色通过错开对象的位置来打乱物理地址的排列,这样就可以使用多
个槽。
题外话,若使用 RHEL6 的 RPM 包“hwloc” 中包含的命令,便可显示
服务器上安装的 CPU 的核数以及内存高速缓存的大小。图 1.25 是 hwloc –
ls 命令的运行实例。由此可以看到,通过安装一个双核 CPU,且每个核上都
采用超线程(HT, Hyper-Threading)技术,就能令每个核上都可以同时运行
两个线程。此外还安装了 L1~L3 的 3 级内 存高速缓存。图 1.25 hwloc-ls 命令的执行示例
接下来,着眼于图 1.22 中的⑩ VmallocUsed。 内核使用内存时还存在
一种情况,即并不使用 Slab 分配器中配置的对象,而是直接将空闲页映射到
逻辑地址空间上。内核内部可以用于这一目的的逻辑地址范围,是由⑩上面一
行的 VmallocTotal 所表示的容量范围预先决定的。
虽然比起物理内存的容量,图 1.22 的示例中所示的值大得惊人,但这终
究只是可以使用的逻辑地址范围,并非实际使用的物理内存。实际上可以被用
来分配的内存只有⑩所示的这部分容量。
不过,不要不加思索地认同这个值。在这个范围中,除了服务器上安装的
物理内存,为了与诸如视频卡等外部设备进行数据交换,也分配了安装在这些设备上的内存,这种处理被称为“ioremap”。
至于 VmallocUsed 中包含的内存区域的详情,用
procvmallocinfo 即可确认。
cat procvmallocinfo
0xffffc90000000000-0xffffc90000002000 8192 hpet_enable+0x340x2b0 phys=fed00000
ioremap
0xffffc90000003000-0xffffc90000804000 8392704 alloc_large_system_hash+0x15e0x222
pages=2048 vmalloc vpages N0=2048
0xffffc90000805000-0xffffc9000080a000 20480 alloc_large_system_hash+0x15e0x222
pages=4 vmalloc N0=4
0xffffc9000080b000-0xffffc90000c0c000 4198400 alloc_large_system_hash+0x15e0x222
pages=1024 vmalloc vpages N0=1024
0xffffc90000c0d000-0xffffc90000c10000 12288 alloc_large_system_hash+0x15e0x222
pages=2 vmalloc N0=2
0xffffc90000c11000-0xffffc90000c14000 12288 mem_cgroup_create+0x2a40x4f0 pages=2
vmalloc N0=2
0xffffc90000c16000-0xffffc90000c18000 8192 acpi_os_map_memory+0x170x20
phys=caf7e000 ioremap
0xffffc90000c1a000-0xffffc90000c1c000 8192 acpi_os_map_memory+0x170x20
phys=caf7d000 ioremap……(以下省略)……
每行开头表示的是逻辑地址的范围,第二列的数值表示的是容量(字
节)。然后,末尾所示的“ioremap”是 ioremap 处理的入口。因此,在这个
输出中,将除 ioremap 对应行以外的所有第 2 列数值加起来,便可计算出
VmallocUsed 中除 ioremap 以外的物理内存的使用量。
在这里,“1.33 快速的数据处理管道”中讲到的知识就可以活学活用了。
好好利用管道,来尝试计算一下这个值。方法当然不少,但笔者最先想到的是
如下方法。
cat procvmallocinfo | grep -v ioremap | awk '{print 2}' | paste -s -d + |
bc
用 grep 命令将包含 ioremap 的行排除在外,并通过 awk 命令取出第
252 列的值。使用后面带 -s 选项的 paste 命令,将这些值合并成一个加法表
达式。之前介绍了 paste 命令可以将两个文件的每行进行合并,此时若指定
选项 -s,以及用于指定分隔符的选项 -d,便可以将一个文件中的各行进行合
并。准确地说,当前的内容不是从文件处接收,而是从标准输入处接收的。
来看下面这个运行示例,发生了什么一目了然。
cat procvmallocinfo | grep -v ioremap | awk '{print 2}' | paste -s -d +
8392704+20480+4198400+12288+12288+20480+12288+8192+49152+20480+12288+2101248+8392704+1052672+12288+12288+12288+12288+12288+69632+36864+69632+69632+8192+12288+12288+8192+8192+8192+8192+8192+8192+8192+8192+8192+12288+8192+8192+8192+12288+8192+8192+8192+8192+8192+8192+4198400+8192+8192+8192+12288+8192+8192+8192+8192+8192+8192+8192+8192+8192+8192+12288+8192+8192+8192+8192+8192+8192+8192+8192+8192+2101248+8192+8192+8192+8192+12288+8192+8192+8192+8192+8192+8192+8192+8192+8192+8192+8192+8192+8192+8192+12288+8192+8192+8192+8192+8192+8192+8192+8192+8192+8192+528384+36864+8192+8192+16384+16384+2101248+36864+8192+8192+36864+8192+8192+8192+8192+2101248+12288+12288+12288+12288+12288+12288+12288+12288+12288+12288+12288+12288+12288+12288+12288+12288+12288+8192+8192+12288+2097152+2097152+81920+16384+8192+8192+8192+28672+16384+36864+16384+12288+8192+221184+45056+20480+40960+12288+434176+45056+45056+94208+36864+12288+364544+32768+40960+16384+12288+12288+16384+77824+20480+16384+28672+12288+90112+24576+12288+12288+61440+20480+12288+16384+94208+36864+32768+331776+221184+118784+307200+73728+53248+16384+20480+16384+24576+8192+86016+32768+8192+8192+16384+8192+12288+327680+53248+57344+12288+16384+16384+32768+12288+249856+61440+49152+8192+81920+311296+12288+8192+81920+8192+8192+8192+28672+12288+8192+24576+8192+24576+8192+385024
这个结果是通过 bc 命令计算而来的。若进一步将这个结果除以 1024 转
换为以 KB 为单位,则会出现以下结果。
echo ( (cat procvmallocinfo | grep -v ioremap | awk '{print 2}' | paste -
s -d +) ) 1024 | bc 45452
稍微有些离题了,还是回到内核内存的话题上来吧。图 1.22 中的 ?
KernelStack 和 ? PageTables,可以作为 Slab 和 VmallocUsed 以外的
内核内存来看待。
首先,KernelStack 是内核代码使用的堆栈区域。对使用 C 语言来开发
程序的人来说,这个解释是多余的,但有一点仍要特别注意。由于 Linux 内
核中用户进程在运行过程中要不 ......
您现在查看是摘要介绍页, 详见PDF附件(5421KB,409页)。





