奔跑吧Linux内核入门篇(第2版)

978-7-115-55560-1
作者: 笨叔陈悦
译者:
编辑: 谢晓芳

图书目录:

详情

本书基于Linux 5.0和ARM64处理器循序渐进地讲述Linux内核的理论与实验。本书共16章,主要介绍Linux系统基础知识、Linux内核基础知识、ARM64架构基础知识、内核编译和调试、内核模块、简单的字符设备驱动、系统调用、进程管理、内存管理、同步管理、中断管理、调试和性能优化、开源社区、文件系统、虚拟化与云计算等方面的内容,并通过一个综合能力训练来引导读者动手实现一个小的操作系统。 本书适合Linux开发人员、嵌入式开发人员以及对Linux感兴趣的程序员阅读,也适合作为高等院校计算机相关专业的教材。

图书摘要

版权信息

书名:奔跑吧Linux内核入门篇(第2版)

ISBN:978-7-115-55560-1

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。


著    笨 叔  陈 悦

责任编辑 谢晓芳

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


本书基于Linux 5.0和ARM64处理器循序渐进地讲述Linux内核的理论与实验。本书共16章,主要介绍Linux系统基础知识、Linux内核基础知识、ARM64架构基础知识、内核编译和调试、内核模块、简单的字符设备驱动、系统调用、进程管理、内存管理、同步管理、中断管理、调试和性能优化、开源社区、文件系统、虚拟化与云计算等方面的内容,并通过一个综合能力训练来引导读者动手实现一个小的操作系统。

本书适合Linux开发人员、嵌入式开发人员以及对Linux感兴趣的程序员阅读,也适合作为高等院校计算机相关专业的教材。


分析与运行Linux内核是培养读者系统软件设计能力的有效方法。然而,Linux内核的机制复杂、算法精妙、代码量庞大,因此初学者难以快速入门,并深入理解和灵活应用。本书结合作者多年的项目实践经验,剖析了源代码,是Linux内核方面的一本经典入门图书。

——吴国伟, 大连理工大学

本书第1版得到了读者的一致好评。本书第2版新增了很多内容,尤其是操作系统方面的热门内容——文件系统和虚拟化。我印象最深刻的是利用树莓派实现一个小的操作系统。通过这样的综合实验,读者会对Linux内核有更深的理解。理论加动手实践是学习Linux内核的最佳途径之一。

——陈莉君,西安邮电大学

本书图文并茂,结合实验,把作者一手的知识与经验毫无保留地呈现给了读者。本书有助于读者逐步成为Linux内核领域的高级开发人员。

——夏耐,南京大学

相对于第1版,本书第2版增加了ARM64架构和树莓派硬件平台方面的内容,并且介绍了如何设计一个有价值的小操作系统(BenOS)。通过本书,读者可以学会如何在真实的硬件平台上运行自己搭建的操作系统,真正体验动手设计操作系统的乐趣。

——常瑞,浙江大学

本书是剖析Linux内核的经典图书。本书包含大量的实验,非常适合作为计算机相关专业的教材。对于Linux开发人员来说,本书是不可多得的工具书。

——陈全,上海交通大学

本书兼顾理论与实践,通过实验使读者轻松开启Linux内核之门,为他们日后成为优秀的开源程序员奠定了基础。

——淮晋阳,红帽中国培训部

本书不仅介绍了Linux内核方面的技术,还针对移动互联、大数据等不同场景,剖析了微内核、宏内核的特点。另外,本书还讲解了信息化技术领域中基于ARM架构的内核技术。书中的实验一定会为读者带来别样的阅读体验。

——贺唯佳,中国电子科技集团普华基础软件股份有限公司基础软件促进中心


Linux操作系统自诞生以来,得到了国内外开源爱好者与产业界的持续关注和投入。近年来,Linux操作系统在云计算、服务器、桌面、终端、嵌入式系统等领域得到了广泛的应用,越来越多的行业开始利用Linux操作系统作为信息技术的基础平台或者利用Linux操作系统进行产品开发。

作为Linux操作系统的核心,Linux内核以开放、自由、协作、高质量等特点吸引了众多顶尖科技公司的参与,并有数以千计的开发者为Linux内核贡献了高质量的代码。在学习和研究操作系统的过程中,Linux内核为“操作系统”课程提供了一个不可或缺的案例,国内外众多大学的“操作系统”课程以Linux内核作为研究平台。随着基础软硬件技术的快速发展,Linux内核代码将更加庞大和复杂,试图深入理解并掌握它是一件非常不容易的事情。

结合优麒麟系统的特性以及操作实践,本书深入浅出地介绍了Linux内核的若干常用模块。本书结构合理、内容丰富,可作为Linux相关爱好者、开发者的参考用书,也可作为大学“操作系统”课程的辅助教材。

廖湘科

中国工程院院士


张天飞和陈悦老师的力作《奔跑吧Linux内核入门篇(第2版)》终于出版了。这是与“操作系统”课程配套的一本非常优秀的实验教材。

本书介绍了操作系统的基本概念、设计原理和实现技术,重点讲述了Linux内核入门知识,旨在培养读者动手做实验的技能。本书具有结构合理、重点突出、内容丰富、逻辑清晰的特点。本书主要包含Linux内核模块、设备驱动、系统调用、进程管理、内存管理、中断机制、同步机制、文件系统,以及Linux虚拟化和云计算等内容。

学习和理解操作系统最好的方法是原理与实验并重。本书有助于读者提升操作系统实验技能。本书具有以下特色。

本书将Linux系统方面的基础原理与实验相互融合,有助于读者深入理解Linux系统的原理和精髓,掌握核心技术和方法,提高分析问题与解决问题的能力。本书特色突出、内容新颖,能充分满足大学计算机专业的本科教学需要。

综上所述,这是操作系统方面一本非常优秀的实验教材。本书既适合高校计算机专业的学生阅读,也可供Linux爱好者、相关从业人员参考。

费翔林

南京大学计算机科学与技术系


本书是《奔跑吧Linux内核入门篇》的第2版。2019年,第1版出版后得到了广大Linux爱好者、从业人员的喜爱,也有不少高校使用第1版作为“操作系统”课程的实验教辅材料。

自从2019年Linux社区宣布了Linux 5.0的全新版本之后,Linux社区迈向了全新的发展。2019年5月,红帽公司宣布了RHEL8正式发布,采用Linux 4.18内核。2020年4月,Canonical公司发布了全新的Ubuntu Linux 20.04版本,并且提供长达5年的支持,这个版本采用了最新的Linux 5.4内核。从本书第1版采用的Linux 4.0内核到目前的Linux 5.4内核,其间经历了20多个版本,加入了很多新特性并且很多内核的设计与实现已经发生了巨大变化。为了帮助读者适应Linux社区最新的变化,有必要基于较新的Linux内核和Linux发行版来修订第1版。

本书由笨叔和陈悦编写。陈悦第一时间在“操作系统”课程中采用本书第1版作为实验教材。这取得了非常好的效果。两位作者基于宝贵的教学经验,结合Linux的新发展、“操作系统”课程的教学要求,对第1版做了比较大的修订,新增了很多实验。

第2版基于Linux 5.0内核对第1版的内容做了全面的修订和更新。

最近几年国产芯片发展迅猛,国内很多公司在探索使用ARM64架构来构建自己的硬件生态,包括手机芯片、服务器芯片等,例如华为鲲鹏服务器芯片。第2版基于ARM64处理器架构介绍Linux内核的入门与实践。另外,第2版新增了第3章。

第2版新增了4章内容,包括第3章、第14~16章。

第2版新增了不少实验,通过20多个实验逐步实现一个有一定使用价值的小操作系统,从而达到能力训练的目的。

不少读者已经购买了树莓派,第2版以树莓派作为硬件开发平台,读者可以在树莓派上做实验。

除了上述新特性,第2版还保持了第1版的几大特性。

Linux内核庞大而复杂,任何一本厚厚的Linux内核书都可能会让人看得昏昏欲睡。因此,对于初学者来说,Linux内核的入门需要循序渐进,一步一个脚印。初学者可以从如何编译Linux内核开始入门,学习如何调试Linux内核,动手编写简单的内核模块,逐步深入Linux内核的核心模块。

为了降低读者的学习难度,本书不会分析Linux内核的源代码,要深入理解Linux内核源代码的实现,可以参考《奔跑吧Linux内核》一书。

对于初学者,理解操作系统最好的办法之一就是动手实验。因此,本书在每章中都设置了几个经典的实验,读者可以在学习基础知识后通过实验来加深理解。

除了介绍Linux内核的基本理论之外,本书还介绍了当前Linux社区中新的开发工具和社区运作方式,比如如何使用Vim 8阅读Linux内核代码,如何使用git工具进行社区开发,如何参与社区开发等。

在学习Linux内核时,大多数人希望使用功能全面且好用的图形化界面来单步调试内核。本书会介绍一种单步调试内核的方法——基于Eclipse + QEMU + GDB。另外,本书提供首个采用“-O0”编译和调试Linux内核的实验,可以解决调试时出现的光标乱跳和<optimized out>等问题。本书也会介绍实际工程中很实用的内核调试技巧,例如ftrace、systemtap、内存检测、死锁检测、动态输出技术等,这些都可以在QEMU + ARM64实验平台上验证。

Linux内核涉及的内容包罗万象,但本书重点讲述Linux内核的入门和实践。

本书共有16章。

第1章首先介绍什么是Linux系统以及常用的Linux发行版,然后介绍宏内核和微内核之间的区别,以及如何学习Linux内核等内容。该章还包括如何安装Linux系统、如何编译Linux内核等实验。

第2章介绍GCC工具、Linux内核常用的C语言技巧、Linux内核常用的数据结构、Vim工具以及git工具等内容。

第3章主要介绍ARM64架构以及实验平台树莓派的相关知识。

第4章主要讲述内核的配置和编译技巧,实验包括使用QEMU虚拟机来编译和调试ARM的Linux内核。

第5章从一个简单的内核模块入手,讲述Linux内核模块的编写方法,实验围绕Linux内核模块展开。

第6章从如何编写简单的字符设备入手,介绍字符设备驱动的编写。

第7章主要讲述系统调用的基本概念。

第8章讨论进程概述、进程的创建和终止、进程调度以及多核调度等内容。

第9章介绍从硬件角度看内存管理、从软件角度看内存管理、物理内存管理、虚拟内存管理、缺页异常、内存短缺等内容,以及多个与内存管理相关的实验。

第 10 章讲述原子操作、内存屏障、自旋锁机制、信号量、读写锁、RCU、等待队列等内容。

第11章介绍Linux内核中断管理机制、软中断、tasklet机制、工作队列机制等内容。

第12章讨论printk()输出函数、动态输出、proc、debugfs、ftrace、分析Oops错误、perf性能分析工具、内存检测,以及使用kdump工具解决死机问题等内容,并介绍调试和性能优化方面的18个实验。

第13章讲述开源社区、如何参与开源社区、如何提交补丁、如何在Gitee中创建和管理开源项目等内容。

第14章介绍文件系统方面的知识,包括文件系统的基础知识、虚拟文件系统层、文件系统的一致性、一次写磁盘的全过程、文件系统实验等内容。

第 15 章介绍虚拟化与云计算方面的入门知识,包括 CPU 虚拟化、内存虚拟化、I/O 虚拟化、Docker、Kubernetes等方面的知识。

第16章通过20多个实验来引导读者实现一个小操作系统,并介绍开放性实验。读者可以根据实际情况来选做部分或者全部实验。

由于作者知识水平有限,书中难免存在纰漏,敬请各位读者批评指正。关于本书的任何问题请发送邮件到runninglinuxkernel@126.com。欢迎用手机扫描下方的二维码,到“奔跑吧Linux内核”微信公众号中参与交流。

感谢国防科技大学优麒麟社区为本书实验提供了优麒麟Linux发行版,感谢优麒麟社区的余杰老师认真阅读了全书稿件,并提出了很多修改意见和建议。北京麦克泰软件公司的何小庆老师为本书的实验提供了大量支持。另外,有不少同学帮忙审阅了第2版的部分或者全部稿件,在此特别感谢他们,他们分别是胡梦龙、冯少合、李亚东、汪洋、蔡琛、胡茂留。

感谢国防科技大学的廖湘科院士在百忙之中对本书编写和出版工作的关注,并为本书作序。廖院士是高性能计算机和操作系统领域的科学巨匠,感激他在繁重的工作之余仍常常关心开源软件的发展以及年轻一代程序员的成长。

最后感谢家人对我们的支持和鼓励,虽然周末时间我们都在忙于写作本书,但他们总是给予我们无限的温暖。

笨 叔

陈 悦


为了帮助读者更好地完成本书的实验,我们对实验环境和实验平台做了一些约定。

1.实验环境

本书推荐的实验环境如下。

读者在安装完优麒麟Linux 20.04系统后可以通过如下命令来安装本书需要的软件包。

$ sudo apt update -y
$ sudo apt install net-tools libncurses5-dev libssl-dev build-essential openssl qemu-system-arm libncurses5-dev gcc-aarch64-linux-gnu git bison flex bc vim universal-ctags cscope cmake python3-dev gdb-multiarch openjdk-13-jre trace-cmd kernelshark bpfcc-tools cppcheck docker docker.io

我们基于VMware镜像搭建了全套开发环境,读者可以通过作者的微信公众号来获取下载地址。使用本书配套的VMware镜像可以减少配置开发环境带来的麻烦。

2.实验平台

本书的所有实验都可以在如下两个实验平台上完成。

1)QEMU + ARM64实验平台

本书主要基于ARM64架构以及Linux 5.0内核来讲解。本书基于QEMU + ARM64实验平台,它有如下新特性。

要下载本书配套的QEMU+ARM64实验平台的仓库,可以访问https://benshushu.coding.net/ public/runninglinuxkernel_5.0/runninglinuxkernel_5.0/git/files或者https://github.com/figozhang/ runninglinuxkernel_5.0。

其中,rlk_5.0/kmodues/rlk_basic目录里包含了本书大部分的实验代码,仅供读者参考,希望读者自行完成所有的实验。

2)树莓派实验平台

有不少读者可能购买了树莓派,因此可以利用树莓派来做本书的实验。树莓派3B以及树莓派4B都支持ARM64处理器。实验中使用的设备如下。

3.关于实验和配套资料

本书为了节省篇幅,大部分实验只列出了实验目的和实验要求,希望读者能独立完成实验。另外,本书配套的实验指导手册会尽可能给出详细的实验步骤和讲解。

本书会提供如下免费的配套资料。

读者可以通过作者的微信公众号“奔跑吧Linux社区”获取下载地址。

[1] 优麒麟Linux 20.04内置的QEMU 4.2还不支持树莓派4B。若要在QEMU中模拟树莓派4B,那么还需要打上一系列补丁,然后重新编译QEMU。本书配套的实验平台VMware镜像会提供支持树莓派4B的QEMU程序。


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线投稿(直接访问www.epubit.com/ contribute即可)。

如果您所在学校、培训机构或企业想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接通过邮件发送给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近几十年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、人工智能、测试、前端、网络技术等。

异步社区

微信服务号


Linux系统已经被广泛应用在人们的日常用品中,如手机、智能家居、汽车电子、可穿戴设备等,只不过很多人并不知道自己使用的电子产品里面运行的是Linux系统。我们来看一下Linux基金会在2017年发布的一组数据。

全球100万个顶级域名中超过90%在使用Linux系统;全球大部分的股票交易市场是基于Linux系统来部署的,包括纽交所、纳斯达克等;全球知名的淘宝、亚马逊、易趣、沃尔玛等电子商务平台都在使用Linux系统。

这足以证明Linux系统是个人计算机(PC)操作系统之外的绝对霸主。参与Linux内核开发的开发人员和公司也是最多、最活跃的,截至2017年,有超过1600名开发人员和200家公司参与Linux内核的开发。

因此,了解和学习Linux内核显得非常迫切。

Linux系统诞生于1991年10月5日,它的产生和开源运动有着密切的关系。

1983年,Richard Stallman发起GNU(GUN’s Not UNIX)计划,他是美国自由软件的精神领袖,也是GNU计划和自由软件基金会的创立者。到了1991年,根据该计划已经完成了Emacs和GCC编译器等工具,但是唯独没有完成操作系统和内核。GNU在1990年发布了一个名为Hurb的内核开发计划,不过开发过程不顺利,后来逐步被Linux内核替代。

1991年,Linus Torvalds在一台386计算机上学习Minix操作系统,并动手实现了一个新的操作系统,然后在comp.os.minix新闻组上发布了第一个版本的Linux内核。

1993年,有大约100名程序员参与了Linux内核代码的编写,Linux 0.99的代码已经有大约10万行。

1994年,采用GPL(General Public License)协议的Linux 1.0正式发布。GPL协议最初由Richard Stallman撰写,是一个广泛使用的开源软件许可协议。

1995年,Bob Young创办了Red Hat公司,以GNU/Linux为核心,把当时大部分的开源软件打包成发行版,这就是Red Hat Linux发行版。

1996年,Linux 2.0发布,该版本可以支持多种处理器,如alpha、mips、powerpc等,内核代码量大约是40万行。

1999年,Linux 2.2发布,它支持ARM处理器。第一家国产Linux发行版——蓝点Linux系统诞生,它是第一个支持在帧缓冲上进行汉化的Linux中文版本。

2001年,Linux 2.4发布,支持对称多处理器和很多外设驱动。同年,毛德操老师出版了《Linux 2.4内核源代码情景分析》,该书推动了国人对Linux内核的研究热潮,书中对Linux内核理解的深度和广度至今无人能及。

2003年,Linux 2.6发布。与Linux 2.4相比,该版本增加了性能优化方面的很多新特性,使Linux成为真正意义上的现代操作系统。

2008年,谷歌正式发布Android 1.0,Android系统基于Linux内核来构建。在之后的十几年里,Android系统占据了手机系统的霸主地位。

2011年,Linux 3.0发布。在长达8年的Linux 2.6开发期间,众多IT巨头持续为Linux内核贡献了很多新特性和新的外设驱动。同年,全球最大的Linux发行版厂商Red Hat宣布营收达到10亿美元。

2015年,Linux 4.0发布。

2019年3月,Linux 5.0发布。

2019年11月,Linux 5.4发布。

到现在为止,国内外的科技巨头都已投入Linux内核的开发中,其中包括微软、华为、阿里巴巴等。

Linux最早的应用就是个人计算机操作系统,也是就我们常说的Linux发行版。从1995年的Red Hat Linux发行版到现在,Linux经历的发行版多如牛毛,可是现在最流行的发行版仅有几个,比如RHEL、Debian、SuSE、Ubuntu和CentOS等。国内出现过多个国产的Linux发行版,比如蓝点Linux、红旗Linux和优麒麟Linux等。

Red Hat Linux不是第一个制作Linux发行版的厂商,但它是在商业和技术上做得最好的Linux厂商。从Red Hat 9.0版本发布之后,Red Hat公司不再发行个人计算机的桌面Linux发行版,而是转向利润更高、发展前景更好的服务器版本的开发上,也就是后来的Red Hat Enterprise Linux(Red Hat企业版Linux,RHEL)。原来的Red Hat Linux个人发行版和Fedora社区合并,成为Fedora Linux发行版。

到目前为止,Red Hat系列Linux系统有3个版本可供选择。

1.Fedora Core

Fedora Core发行版是Red Hat公司的新技术测试平台,很多新的技术首先会应用到Fedora Core中,经过性能测试才会加入Red Hat的RHEL版本中。Fedora Core面向桌面应用,所以Fedora Core会提供最新的软件包。Fedora大约每6个月会发布一个新版本。Fedora Core由Fedora Project社区开发,并得到Red Hat公司的赞助,所以它是以社区的方式来运作的。

2.RHEL

RHEL是面向服务器应用的Linux发行版,注重性能、稳定性和服务器端软件的支持。

2018年4月,Red Hat公司发布的RHEL 7.5操作系统提升了性能,增强了安全性。

3.CentOS Linux

CentOS的全称为Community Enterprise Operating System,它根据RHEL的源代码重新编译而成。因为RHEL是商业产品,所以CentOS把Red Hat的所有商标信息都改成了CentOS的。除此之外,CentOS和RHEL的另一个不同之处是CentOS不包含封闭源代码的软件。因此,CentOS可以免费使用,并由社区主导。RHEL在发行时会发布源代码,所以第三方公司或者社区可以使用RHEL发布的源代码进行重新编译,以形成一个可使用的二进制版本。因为Linux的源代码基于GPL v2,所以从获取RHEL的源代码到编译成新的二进制都是合法的。国内外的确有不少公司是这么做的,比如甲骨文的Unbreakable Linux。

2014年,Red Hat公司收购了CentOS社区,但CentOS依然是免费的。CentOS并不向用户提供商业支持,所以如果用户在使用CentOS时遇到问题,只能自行解决。

Debian由Ian Murdock在1993年创建,是一个致力于创建自由操作系统的合作组织。因为Debian项目以Linux内核为主,所以Debian一般指的是Debian GNU/Linux。Debian能风靡全球的主要原因在于其特有的apt-get/dpkg软件包管理工具,该工具被誉为所有Linux软件包管理工具中最强大、最好用的一个。

目前有很多Linux发行版基于Debian,如最流行的Ubuntu Linux。

Ubuntu的中文音译是“乌班图”,它是以Dabian为基础打造的以桌面应用为主的Linux发行版。Ubuntu注重提高桌面的可用性以及安装的易用性等方面,因此经过这几年的发展,Ubuntu已经成为最受欢迎的桌面Linux发行版之一。

SuSE Linux是来自德国的著名Linux发行版,在Linux业界享有很高的声誉。SuSE公司在Linux内核社区的贡献仅次于Red Hat公司,培养了一大批Linux内核方面的专家。SuSE Linux在欧洲Linux市场中占有将近80%的份额,但是在中国占有的市场份额并不大。

优麒麟(Ubuntu Kylin)Linux诞生于2013年,是由中国国防科技大学联合Ubuntu、CSIP开发的开源桌面Linux发行版,是Ubuntu的官方衍生版。该项目以国际社区合作方式进行开发,并遵守GPL协议,在Debian、Ubuntu、Mate、LUPA 等国际社区及众多国内外社区爱好者广泛参与的同时,持续向Linux Kernel、OpenStack、Debian/Ubuntu等开源项目贡献力量。从发布至今,优麒麟Linux在全球已经有2800多万次的下载量,优麒麟Linux 20.04的桌面如图1.1所示。

图1.1 优麒麟Linux 20.04的桌面

如图1.2所示,优麒麟自研的UKUI轻量级桌面环境是按照Windows用户的使用习惯进行设计开发的,它开创性地将Windows标志性的“开始”菜单、任务栏引入Linux操作系统中,降低了Windows用户迁移到Linux平台的时间成本。优麒麟Linux还秉承“友好易用,简单轻松”的设计理念,对文件管理器、控制面板等桌面重要组件进行全新开发,同时配备一系列网络、天气、侧边栏等实用插件,为用户日常学习和工作带来更便利的体验,具有稳定、高效、易用的特点。

图1.2 UKUI桌面环境架构

同时,优麒麟Linux默认安装的麒麟软件中心、麒麟助手、麒麟影音、WPS办公软件、搜狗输入法等软件让普通用户更易上手。针对ARM平台的安卓原生兼容技术,优麒麟Linux可以把安卓系统中强大的生态软件无缝移植到Linux系统中。基于优麒麟Linux的银河麒麟企业发行版支持x86和ARM64架构,在中国的市场上占有率遥遥领先。

读者可以从Linux内核的官方网站上下载最新的版本,比如编写本书时最新的稳定内核版本是Linux 5.6.6,如图1.3所示,不过本书以Linux 5.4内核为蓝本。Linux内核的版本号分成3部分,第1个数字表示主版本号,第2个数字表示次版本号,第3个数字表示修正版本号。

Linux 5.0内核的目录结构如图1.4所示。

图1.3 从Linux内核的官方网站上下载最新的版本

图1.4 Linux 5.4内核的目录结构

其中重要的目录介绍如下。

操作系统属于软件的范畴,负责管理系统的硬件资源,同时为应用程序的开发和执行提供配套环境。操作系统必须具备如下两大功能。

除此之外,操作系统还需要具备如下一些特性。

操作系统内核的设计在历史上存在两大阵营。一个是宏内核,另一个是微内核。宏内核是指所有的内核代码都被编译成二进制文件,所有的内核代码都运行在一个大的内核地址空间里,内核代码可以直接访问和调用,效率高并且性能好,如图1.5所示。而微内核是指把操作系统分成多个独立的功能模块,每个功能模块之间的访问需要通过消息来完成,因此效率没有那么高。比如,当时Linus学习的Minix就是微内核的典范。现代的一些操作系统(比如Windows)就采用微内核的方式,内核保留操作系统最基本的功能,比如进程调度、内存管理通信等,其他的功能从内核移出,放到用户态中实现,并以C/S(客户端/服务器)模型为应用程序提供服务,如图1.6所示。

图1.5 宏内核架构

图1.6 微内核架构

Linus Torvalds在设计Linux内核之初并没有使用当时学术界流行的微内核架构,而采用实现方式比较简单的宏内核架构,一方面是因为Linux内核在当时是业余作品,另一方面是因为Linus Torvalds更喜欢宏内核的设计。宏内核架构的优点是设计简洁且性能比较好,而微内核架构的优势很明显,比如稳定性和实时性等。微内核架构最大的问题就是高度模块化带来的交互的冗余和效率的损耗。把所有的理论设计放到现实的工程实践中是一种折中的艺术。Linux内核在20多年的发展历程中,形成了自己的工程理论,并且不断融入了微内核的精华,如模块化设计、抢占式内核、动态加载内核模块等。

Linux内核支持动态加载内核模块。为了借鉴微内核的一些优点,Linux内核在很早就提出了内核模块化的设计。Linux内核中很多核心的实现或者设备驱动的实现都可以编译成一个个单独的模块。模块是被编译成的目标文件,并且可以在运行时的内核中动态加载和卸载。和微内核实现的模块化不一样,它们不是作为独立模块执行的,而是和静态编译的内核函数一样,运行在内核态中。模块的引入给Linux内核带来了不少的优点,其中最大的优点就是很多内核的功能和设备驱动可以编译成动态加载和卸载的模块,并且驱动开发者在编写内核模块时必须遵守定义好的接口来访问内核核心,这使得开发内核模块变得容易很多。另一个优点是,很多内核模块(比如文件系统等)可以设计成和平台无关的。相比微内核的模块,第三个优点就是继承了宏内核的性能优势。

Linux内核从1991年至2020年已有近29年的发展过程,从原来不到1万行代码发展成现在已经超过 2 000 万行代码。对于如此庞大的项目,我们在学习的过程中首先需要了解Linux内核的整体概貌,再深入学习每个核心子模块。

Linux内核概貌如图1.7所示,典型的Linux系统可以分成3部分。

图1.7 Linux内核概貌

我们重点关注内核空间中的一些主要部件。

1.系统调用层

Linux内核把系统分成两个空间——用户空间和内核空间。CPU既可以运行在用户空间,也可以运行在内核空间。一些架构的实现还有多种执行模式,如x86架构有ring0 ~ ring3这4种不同的执行模式。但是,Linux内核只使用了ring0与ring3两种模式来实现内核态和用户态。

Linux内核为内核态和用户态之间的切换设置了软件抽象层,叫作系统调用(system call)层,其实每个处理器的架构设计中都提供了一些特殊的指令来实现内核态和用户态之间的切换。Linux内核充分利用了这种硬件提供的机制来实现系统调用层。

系统调用层最大的目的是让用户进程看不到真实的硬件信息,比如当用户需要读取一个文件的内容时,编写用户进程的程序员不需要知道这个文件具体存放在磁盘的哪个扇区里,只需要调用open()、read()或mmap()等函数即可。

用户进程大部分时间运行在用户态,当需要向内核请求服务时,它会调用系统提供的接口进入内核态,比如上述例子中的open()函数。当内核完成open()函数的调用之后,就会返回用户态。

2.arch抽象层

Linux内核支持多种架构,比如现在最流行的x86和ARM,也包括MIPS、powerpc等。Linux内核最初的设计只支持x86架构,后来不断扩展,到现在已经支持几十种架构。为Linux内核添加新的架构不是一件很难的事情,比如在Linux 4.15内核里新增对RISC-V架构的支持。Linux内核为不同架构的实现做了很好的抽象和隔离,也提供了统一的接口来实现。比如,在内存管理方面,Linux内核把和架构相关的代码都存放在arch/xx/mm目录里,把和架构不相关的代码都存放在mm目录里,从而实现完美的分层。

3.进程管理

进程是现代操作系统中非常重要的概念,包括上下文切换(context switch)以及进程调度(schedule)。每个进程在运行时都感觉完全占有了全部的硬件资源,但是进程不会长时间占有硬件资源。操作系统利用进程调度器让多个进程并发执行。Linux内核并没有严格区分进程和线程,而经常使用task_struct数据结构来描述。在Linux内核中,调度器的发展经历了好几代,从很早的O(n)调度器到Linux 2.6内核中的O(1)调度器,再到现在的完全公平调度器(Complete Fair Scheduler,CFS)算法。目前比较热门的话题是关于性能和功耗的优化,比如ARM阵营提出了大小核架构,至今在Linux内核实现中还没有体现。因此,诸如绿色节能调度器(Energy Awareness Scheduler,EAS)这样的调度算法是研究热点。

进程管理还包括进程的创建和销毁、线程组管理、内核线程管理、队列等待等内容。

4.内存管理

内存管理是Linux内核中最复杂的模块,涉及物理内存的管理和虚拟内存的管理。在一些小型的嵌入式RTOS中,内存管理不涉及虚拟内存的管理,比较简单和简洁。但是作为通用的操作系统内核,Linux内核的虚拟内存管理非常重要。虚拟内存有很多优点,比如多个进程可以并发执行,进程请求的内存可以比物理内存大,多个进程可以共享函数库等,因此虚拟内存的管理变得越来越复杂。在Linux内核中,关于虚拟内存的模块有反向映射、页面回收、内核同页合并(Kernel Same page Merging,KSM)、mmap、缺页中断、共享内存、进程虚拟地址空间管理等。

物理内存的管理也比较复杂。页面分配器(page allocator)是核心部件,它需要考虑当系统内存紧张时,如何回收页面和继续分配物理内存。其他比较重要的模块有交换分区管理、页面回收和OOM(Out Of Memory )Killer等。

5.中断管理

中断管理包含处理器的异常(exception)处理和中断(interrupt)处理。异常通常是指处理器在执行指令时如果检测到反常条件,就必须暂停下来处理这些特殊的情况,如常见的缺页异常(page fault)。而中断异常一般是指外设通过中断信号线路来请求处理器,处理器会暂停当前正在做的事情来处理外设的请求。Linux内核在中断管理方面有上半部和下半部之分。上半部是在关闭中断的情况下执行的,因此处理时间要求短、平、快;而下半部是在开启中断的情况下执行的,很多对执行时间要求不高的操作可以放到下半部来执行。Linux内核为下半部提供了多种机制,如软中断、tasklet和工作队列等。

6.设备管理

设备管理对于任何操作系统来说都是重中之重。Linux内核之所以这么流行,就是因为Linux系统支持的外设是所有开源操作系统中最多的。当很多大公司发布新的芯片时,第一个要支持的操作系统是Linux系统,也就是尽可能要在Linux内核社区里推送。

Linux内核的设备管理是一个很广泛的概念,包含的内容很多,如ACPI、设备树、设备模型kobject、设备总线(如PCI总线)、字符设备驱动、块设备驱动、网络设备驱动等。

7.文件系统

优秀的操作系统必须包含优秀的文件系统,但是文件系统有不同的应用场合,如基于闪存的文件系统F2FS、基于磁盘存储的文件系统ext4和XFS等。为了支持各种各样的文件系统,Linux抽象出名为虚拟文件系统(Virtual File System,VFS)层的软件层,这样Linux内核就可以很方便地集成多种文件系统。

总之,Linux内核是一个庞大的工程,处处体现了抽象和分层的思想,Linux内核是值得我们深入学习的。

Linux内核采用C语言编写,因此熟悉C语言是学习Linux内核的基础。读者可以重温C语言方面的课程,然后阅读一些经典的C语言著作,如《C专家编程》《C陷阱与缺陷》《C和指针》等。

刚刚接触Linux内核的读者可以尝试在自己的计算机上安装Linux发行版,如优麒麟Linux 20.04,并尝试使用Linux作为操作系统。另外,建议读者熟悉一些常用的命令,熟悉如何使用Vim和git等工具,尝试编译和更换优麒麟Linux内核的代码。

然后,可以在Linux机器上做一些编程和调试练习,如使用QEMU + GDB + Eclipse单步调试内核、熟悉GDB的使用等。

接下来,选择一个简单的字符设备驱动,如触摸屏驱动等,编写并调试设备驱动。

在对Linux驱动有了深刻的理解之后,就可以研究Linux内核的一些核心API的实现,如malloc()和中断线程化等。

学习Linux内核的过程是枯燥的,但是Linux内核的魅力只有在深入后你才能体会到。Linux内核是由全球顶尖的程序员编写的,每看一行代码,就好像在与全球顶尖的程序员交流和过招,这种体验是你在大学课堂上和其他项目中无法得到的。

因此,对于Linux系统爱好者来说,不要停留在仅会安装Linux系统和配置服务的层面,还要深入学习Linux内核。

1.实验目的

通过本实验熟悉Linux系统的安装过程。首先,需要在虚拟机中安装20.04版本的优麒麟Linux系统。掌握了安装方法之后,读者可以在真实的物理机器上安装Linux系统。

2.实验详解

实验步骤如下。

(1)从优麒麟官方网站上下载优麒麟Linux 20.04的安装程序。

(2)从VMware官方网站上下载VMware Workstation 15 Player。这个工具对于个人用户是免费的,对于商业用户是收费的,如图1.8所示。读者也可以使用另外一个免费的虚拟机工具——VirtualBox。

图1.8 免费安装VMware Workstation 15 Player

(3)打开VMware Player。在软件的主界面中选择Create a New Virtual Machine。

(4)在New Virtual Machine Wizard界面中,选中Installer disc image file(iso)单选按钮,单击Browse按钮,选择刚才下载的安装程序,如图1.9所示。然后,单击Next按钮。

图1.9 选择下载的安装程序

(5)在弹出的界面中输入即将要安装的Linux系统的用户名和密码,如图1.10所示。

图1.10 输入用户名和密码

(6)设置虚拟机的磁盘空间,尽可能设置得大一点。虚拟机的磁盘空间是动态分配的,比如这里设置了200GB,但并不会马上在主机上分配200GB的磁盘空间,如图1.11所示。

图1.11 设置磁盘空间

(7)可以在Customize Hardware选项里重新对一些硬件进行配置,比如把内存设置得大一点。完成VMware Player的设置之后,就会马上进入虚拟机。

(8)在虚拟机中会自动执行安装程序,如图1.12所示。安装完成之后,会自动重启并显示新安装系统的登录界面,如图1.13和图1.14所示。

图1.12 配置硬件

图1.13 VMware Workstation 15 Player登录界面(1)

图1.14 VMware Workstation 15 Player登录界面(2)

1.实验目的

(1)学会如何给Linux系统更换最新版本的Linux内核。

(2)学习如何编译和安装Linux内核。

2.实验详解

在编译Linux内核之前,需要通过命令安装相关软件包。

sudo apt-get install libncurses5-dev libssl-dev build-essential openssl

从Linux内核的官方网站上下载最新的版本,比如写作本书时最新并且稳定的内核版本是Linux 5.6.6。

可以通过如下命令进行解压。

#tar -Jxf linux-5.6.6.tar.xz

解压完之后,可以通过make menuconfig进行内核的配置,如图1.15所示。

除了手动配置Linux内核的选项之外,还可以直接复制Ubuntu Linux系统中自带的配置文件。例如,Ubuntu Linux机器上的内核版本是5.4.0-26-generic,因而内核配置文件为config-5.4.0-26-generic。

#cd linux-5.5.6
#cp /boot/config-5.4.0-26-generic .config

图1.15 配置内核

下面开始编译内核,其中-jn中的“n”表示使用多少个CPU核心来并行编译内核。

#make –jn

为了查看系统中有多少个CPU核心,可以执行如下命令。

#cat /proc/cpuinfo

…

processor       : 7
vendor_id       : GenuineIntel
cpu family      : 6
model            : 60
model name      : Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz
stepping        : 3

processor这一项等于7,说明系统中有8个CPU核心,因为是从0开始计数的,所以刚才的make -jn命令就可以写成make -j8了。

编译内核是一个漫长的过程,可能需要几十分钟时间,这取决于计算机的运算速度和配置的内核选项。

通过make编译完之后,下一步需要编译和安装内核模块。

#sudo make modules_install

最后一步就是把编译好的内核镜像安装到优麒麟Linux系统中。

#sudo make install

完成之后就可以重启计算机,登录最新的系统了。

1.实验目的

通过本实验学习如何编译ARM64版本的内核映像,并且在QEMU虚拟机中运行。

2.实验详解

市面上有不少基于ARM64架构的开发板,比如树莓派,读者可以采用类似于树莓派的开发板进行学习。除了硬件开发板之外,我们还可以使用QEMU虚拟机这个产业界流行的模拟器来模拟ARM64处理器。使用QEMU虚拟机有两个好处:一是不需要额外购买硬件,只需要一台安装了Linux发行版的计算机即可;二是QEMU虚拟机支持单步调试内核的功能。

为了不购买开发板就能在个人计算机上学习和调试Linux系统,我们使用QEMU虚拟机来打造ARM64的实验平台,使用Ubuntu Linux的根文件系统打造实用的文件系统。

这个实验平台具有如下特点。

在Linux主机的另外一个超级终端输入killall qemu-system-aarch64,即可关闭QEMU虚拟机。也可以按Ctrl+A组合键,然后按X键来关闭QEMU虚拟机。

1)安装工具

首先,在Linux主机中安装相关工具。

$ sudo apt-get install apt-get install qemu-system-arm libncurses5-dev gcc-aarch64-linux-gnu build-essential git bison flex libssl-dev

然后,在Linux 主机系统中默认安装ARM64 GCC编译器的9.3版本。

$ aarch64-linux-gnu-gcc -v
Using built-in specs.
COLLECT_GCC=aarch64-linux-gnu-gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc-cross/aarch64-linux-gnu/9/lto-wrapper
Target: aarch64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 9.3.0-8ubuntu1' --with-bugurl=file:///usr/share/doc/gcc-9/README.Bugs --enable-languages=c,ada,c++,go,d,fortran,objc,obj-c++,gm2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-9 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-libquadmath --disable-libquadmath-support --enable-plugin --enable-default-pie --with-system-zlib --without-target-system-zlib --enable-libpth-m2 --enable-multiarch --enable-fix-cortex-a53-843419 --disable-werror --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=aarch64-linux-gnu --program-prefix=aarch64-linux-gnu- --includedir=/usr/aarch64-linux-gnu/include
Thread model: posix
gcc version 9.3.0 (Ubuntu 9.3.0-8ubuntu1)

最后,检查QEMU虚拟机的版本是否为4.2.0。

$ qemu-system-aarch64 --version
QEMU emulator version 4.2.0 (Debian 1:4.2-3ubuntu3)
Copyright (c) 2003-2019 Fabrice Bellard and the QEMU Project developers

2)下载仓库

下载runninglinuxkernel_5.0的git仓库并切换到runninglinuxkernel_5.0分支。

$ git clone https://git.com/figozhang/runninglinuxkernel_5.0.git

3)编译内核以及创建文件系统

runninglinuxkernel_5.0 目录中有一个 rootfs_arm64.tar.xz 文件,这个文件采用 Ubuntu Linux 20.04系统的根文件系统制作而成。但是,这个根文件系统还只是半成品,我们还需要根据编译好的内核来安装内核映像和内核模块,整个过程比较复杂。

整个过程比较烦琐,我们可以创建一个脚本来简化上述过程。

注意,该脚本会使用dd命令生成一个4GB大小的映像文件,因此主机系统需要保证至少10GB的空余磁盘空间。读者如果需要生成更大的根文件系统映像,那么可以修改run_rlk_arm64.sh脚本文件。

首先,编译内核。

$ cd runninglinuxkernel_5.0
$ ./run_rlk_arm64.sh build_kernel

执行上述脚本需要几十分钟时间,具体依赖于主机的计算能力。

然后,编译根文件系统。

$ cd runninglinuxkernel_5.0
$ sudo ./run_rlk_arm64.sh build_rootfs

读者需要注意,编译根文件系统需要管理员权限,而编译内核则不需要。执行完上述命令后,将会生成名为rootfs_arm64.ext4的根文件系统。

4)运行刚才编译好的ARM64版本的Linux系统

要运行run_rlk_arm64.sh脚本,输入run参数即可。

$./run_rlk_arm64.sh run

或者

$ qemu-system-aarch64 -m 1024 -cpu cortex-a57 -smp 4 -M virt -bios QEMU_EFI.fd -nographic -kernel arch/arm64/boot/Image -append "noinintrd root=/dev/vda rootfstype=ext4 rw crashkernel=256M" -drive if=none,file=rootfs_arm64.ext4,id=hd0 -device virtio-blk-device,drive=hd0 --fsdev local,id=kmod_dev,path=./kmodules,security_model=none -device virtio-9p-device,fsdev=kmod_dev,mount_tag=kmod_mount

运行结果如下。

rlk@ runninglinuxkernel_5.0 $ ./run_rlk_arm64.sh run
[    0.000000] Booting Linux on physical CPU 0x0000000000 [0x411fd070]
[    0.000000] Linux version 5.4.0+ (rlk@ubuntu) (gcc version 9.3.0 (Ubuntu 9.3.0-8ubuntu1)) #5 SMP Sat Mar 28 22:05:46 PDT 2020
[    0.000000] Machine model: linux,dummy-virt
[    0.000000] efi: Getting EFI parameters from FDT:
[    0.000000] efi: UEFI not found.
[    0.000000] crashkernel reserved: 0x0000000070000000 - 0x0000000080000000 (256 MB)
[    0.000000] cma: Reserved 64 MiB at 0x000000006c000000
[    0.000000] NUMA: No NUMA configuration found
[    0.000000] NUMA: Faking a node at [mem 0x0000000040000000-0x000000007fffffff]
[    0.000000] NUMA: NODE_DATA [mem 0x6bdf0f00-0x6bdf1fff]
[    0.000000] Zone ranges:
[    0.000000]   Normal   [mem 0x0000000040000000-0x000000007fffffff]
[    0.000000] Movable zone start for each node
[    0.000000] Early memory node ranges
[    0.000000]   node   0: [mem 0x0000000040000000-0x000000007fffffff]
[    0.000000] Initmem setup node 0 [mem 0x0000000040000000-0x000000007fffffff]
[    0.000000] On node 0 totalpages: 262144
[    0.000000]   Normal zone: 4096 pages used for memmap
[    0.000000]   Normal zone: 0 pages reserved
[    0.000000]   Normal zone: 262144 pages, LIFO batch:63
[    0.000000] Kernel command line: noinintrd sched_debug root=/dev/vda rootfstype=ext4 rw crashkernel=256M loglevel=8
[    0.000000] Dentry cache hash table entries: 131072 (order: 8, 1048576 bytes, linear)
[    0.000000] Inode-cache hash table entries: 65536 (order: 7, 524288 bytes, linear)
[    0.000000] mem auto-init: stack:off, heap alloc:off, heap free:off
[    0.000000] Memory: 685128K/1048576K available (8444K kernel code, 1018K rwdata, 2944K rodata, 1152K init, 505K bss, 297912K reserved, 65536K cma-reserved)
[    1.807706] Freeing unused kernel memory: 1152K
[    1.810096] Run /sbin/init as init process
[    2.124322] random: fast init done
[    2.269567] systemd[1]: systemd 245.2-1ubuntu2 running in system mode.
Ubuntu Focal Fossa (development branch) ubuntu ttyAMA0
rlk login:

登录系统时使用的用户名和密码如下。

5)在线安装软件包

QEMU虚拟机可以通过VirtIO-Net技术来生成虚拟的网卡,并通过网络桥接技术和主机进行网络共享。下面使用ifconfig命令检查网络配置。

root@ubuntu:~# ifconfig
enp0s1: flags=4163  mtu 1500
        inet 10.0.2.15  netmask 255.255.255.0  broadcast 10.0.2.255
        inet6 fec0::ce16:adb:3e70:3e71  prefixlen 64  scopeid 0x40
        inet6 fe80::c86e:28c4:625b:2767  prefixlen 64  scopeid 0x20
        ether 52:54:00:12:34:56  txqueuelen 1000  (Ethernet)
        RX packets 23217  bytes 33246898 (31.7 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 4740  bytes 267860 (261.5 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 2  bytes 78 (78.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 2  bytes 78 (78.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

可以看到,这里生成了名为enp0s1的网卡设备,分配的IP地址为10.0.2.15。

可通过apt update命令更新Debian系统的软件仓库。

root@ubuntu:~# apt update

如果更新失败,有可能是因为系统时间比较旧了,可以使用date命令来设置日期。

root@ubuntu:~# date -s 2020-03-29 #假设最新日期是2020年3月29日
Sun Mar 29 00:00:00 UTC 2020

可使用apt install命令来安装软件包。比如,可以在线安装gcc。

root@ubuntu:~# apt install gcc

6)在主机和QEMU虚拟机之间共享文件

主机和QEMU虚拟机可以通过NET_9P技术进行文件共享,这需要QEMU虚拟机和主机的Linux内核都使能NET_9P的内核模块。本实验平台已经支持主机和QEMU虚拟机的共享文件,可以通过如下简单方法来测试。

复制一个文件到runninglinuxkernel_5.0/kmodules目录中。

$ cp test.c  runninglinuxkernel_5.0/kmodules

启动QEMU虚拟机之后,首先检查一下/mnt目录中是否有test.c文件。

root@ubuntu:/# cd /mnt
root@ubuntu:/mnt # ls
README     test.c

我们在后续的实验中会经常利用这个特性,比如把编译好的内核模块或者内核模块源代码放入QEMU虚拟机。

7)在主机上交叉编译内核模块

在本书中,读者常常需要编译内核模块,然后放入QEMU虚拟机中。这里提供两种编译内核模块的方法:一种方法是在主机上进行交叉编译,然后共享到QEMU虚拟机中;另一种方法是在QEMU虚拟机中进行本地编译。

读者可以自行编写简单的内核模块,详见第4章中的内容。我们在这里简单介绍在主机上交叉编译内核模块的方法。

$ cd hello_world  #进入内核模块代码目录
$ export ARCH=arm64
$ export CROSS_COMPILE=aarch64-linux-gnu-

编译内核模块。

$ make

把内核模块文件test.ko复制到runninglinuxkernel_5.0/kmodules目录中。

$cp test.ko  runninglinuxkernel_5.0/kmodules

在QEMU虚拟机的mnt目录中可以看到test.ko模块,加载该内核模块。

$ insmod test.ko

8)在QEMU虚拟机中本地编译内核模块

在QEMU虚拟机中安装必要的软件包。

root@ubuntu: # apt install build-essential

在QEMU虚拟机中编译内核模块时需要指定QEMU虚拟机的本地内核路径,例如BASEINCLUDE变量指定了本地内核路径。“/lib/modules/$(shell uname -r)/build”是链接文件,用来指向具体的内核源代码路径,通常指向已经编译过的内核路径。

BASEINCLUDE ?= /lib/modules/$(shell uname -r)/build

编译内核模块,下面以最简单的hello_world内核模块程序为例。

root@ubuntu:/mnt/hello_world# make
make -C /lib/modules/5.4.0+/build M=/mnt/hello_world modules;
make[1]: Entering directory '/usr/src/linux'
  CC [M]  /mnt/hello_world/test-1.o
  LD [M]  /mnt/hello_world/test.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /mnt/hello_world/test.mod.o
  LD [M]  /mnt/hello_world /test.ko
make[1]: Leaving directory '/usr/src/linux'
root@ubuntu: /mnt/hello_world#

加载内核模块。

root@ubuntu:/mnt/hello_world# insmod test.ko

9)更新根文件系统

如果读者修改了runninglinuxkernel_5.0内核的配置文件,比如arch/arm64/config/rlk_defconfig文件,那么需要重新编译内核以及更新根文件系统。

$ ./run_rlk_arm64.sh build_kernel         # 重新编译内核
$ sudo ./run_rlk_arm64.sh update_rootfs  # 更新根文件系统

1.实验目的

通过本实验学习如何创建基于Ubuntu发行版的根文件系统。

2.实验要求

Ubuntu系统提供的debootstrap工具可以帮助我们快速创建指定架构的根文件系统。本实验要求使用debootstrap工具来创建基于Ubuntu Linux 20.04系统的根文件系统,并且要求能够在QEMU + ARM实验平台上正确挂载和引导系统。

1.实验目的

通过本实验搭建新的处理器实验平台。

2.实验要求

最近,RISC-V开源指令集很火,国内外很多大公司已加入RISC-V阵营。国内很多公司已经开始研制基于RISC-V的芯片了。但是,基于RISC-V的开发板很难买到,而且价格昂贵,给学习者带来巨大的困难。本实验利用QEMU虚拟机来创建和运行RISC-V架构的Debian Linux系统。

3.实验步骤

(1)使用Linux 5.0内核编译RISC-V系统。

(2)参考实验1-4创建基于Debian Linux系统的根文件系统。

(3)在QEMU虚拟机中运行基于RISC-V的Linux系统。


Linux内核是一个复杂的开源项目,主要采用的语言是C语言和汇编语言。因此,深入理解Linux内核的必要条件是熟悉C语言。Linux内核是由全球顶尖的程序员编写的,其中采用了众多精妙的C语言编写技巧,是非常值得学习的典范。

另外,Linux内核采用GCC编译器来编译,了解和熟悉GCC以及GDB的使用也很有必要。

Linux内核代码已经达到2 000万行,庞大的代码量会让读者在阅读和理解代码方面感到力不从心。那么,在Linux中有没有一款合适的可用来阅读和编写代码的工具呢?本章将介绍如何使用Vim这个编辑工具来阅读Linux内核代码。

由Linux内核创始人Linus开发的git工具已经在全球范围内被广泛应用,因此读者必须了解和熟悉git的使用。

GNU编译器套件(GNU Compiler Collection,GCC)在1987年发布了第一个C语言版本,GCC是使用GPL许可证发行的自由软件,也是GNU计划的关键部分。GCC现在是GNU Linux操作系统的默认编译器,同时也被很多自由软件采用。在后续的发展过程中,GCC扩展支持了很多编程语言,如C++、Java、Go等语言。另外,GCC还支持多种不同的硬件平台,如x86、ARM等架构。

GCC的编译流程主要分为4个步骤。

如图2.1所示,可使用C语言编写test程序的源代码文件test.c。首先,进入GCC的预编译器(cpp)进行预处理,对头文件、宏等进行展开,生成test.i文件。然后,进入GCC的编译器,GCC可以支持多种编程语言,这里调用C语言版的编译器(ccl)。编译完之后,生成汇编程序,输出test.s文件。在汇编阶段,GCC调用汇编器(as)进行汇编,生成可重定位的目标程序。最后一步是链接,GCC调用链接器,把所有目标文件和C语言库链接成可执行的二进制文件。

图2.1 GCC编译流程

由此可见,C语言代码需要经历两次编译和一次链接过程才能生成可执行的程序。

GCC具有良好的可扩展性,除了可以编译x86架构的二进制程序外,还可以支持很多其他架构的处理器,如ARM、MIPS、RISC-V等。这里涉及两个概念:一个是本地编译,另一个是交叉编译。

交叉工具链的命名规则一般如下。

[arch] [-os] [-(gnu)eabi]

许多Linux发行版提供了编译好的用于ARM64 GCC的工具链,如Ubuntu Linux 20.04提供如下和ARM相关的编译器。

GCC编译的一般格式如下。

gcc [选项]  源文件  [选项] 目标文件

GCC的常用选项如表2.1所示。

表2.1 GCC的常用选项

选  项

功 能 描 述

-o

生成目标文件,可以是.i、.s以及.o文件

-E

只运行C预编译器

-c

通知GCC取消链接,只编译生成目标文件,但不做最后的链接

-Wall

生成所有警告信息

-w

不生成任何警告信息

-I

指定头文件的目录路径

-L

指定库文件的目录路径

-static

链接成静态库

-g

包含调试信息

-v

输出编译过程中的命令行和编译器版本等信息

-Werror

把所有警告信息转换成错误信息,并在警告发生时终止编译

-O0

关闭所有优化选项

-O或-O1

最基本的优化等级

-O2

-O1的进阶等级,也是推荐使用的优化等级,编译器会尝试提高代码性能,而不会占用大量存储空间和花费大量编译时间

-O3

最高优化等级,会延长编译时间

相信读者在阅读本章之前已经学习过C语言了,但是想精通C语言还需要下一番苦功夫。Linux内核是基于C语言编写的,熟练掌握C语言是深入学习Linux内核的基本要求。

GCC的C编译器除了支持ANSI C标准之外,还对C语言进行了很多的扩充。这些扩充为代码优化、目标代码布局以及安全检查等提供了很强的支持,因此支持GNU扩展的C语言称为GNU C语言。Linux内核采用GCC编译器,所以Linux内核的代码自然使用了GCC的很多新的扩充特性。本节将介绍GCC C语言一些扩充的新特性,希望读者在学习Linux内核时特别留意。

1.语句表达式

在GNU C语言中,括号里的复合语句可以看作表达式,称为语句表达式。在语句表达式里,可以使用循环、跳转和局部变量等。这个特性通常用在宏定义中,可以让宏定义变得更安全,如比较两个值的大小。

#define max(a,b) ((a) > (b) ? (a) : (b))

上述代码会导致安全问题,a和b有可能会计算两次,比如,向a传入i++,向b传入j++。在GNU C语言中,如果知道a和b的类型,可以像下面这样写这个宏。

#define maxint(a,b) \
  ({int _a = (a), _b = (b); _a > _b ? _a : _b; })

如果不知道a和b的类型,还可以使用typeof宏。

<include/linux/kernel.h>

#define min(x, y) ({                \
    typeof(x) _min1 = (x);            \
    typeof(y) _min2 = (y);            \
    (void) (&_min1 == &_min2);        \
    _min1 < _min2 ? _min1 : _min2; })

typeof也是GNU C语言的一种扩充用法,可以用来构造新的类型,通常和语句表达式一起使用。

下面是一些例子。

typeof (*x) y;
typeof (*x) z[4];
typeof (typeof (char *)[4]) m;

第一句声明y是x指针指向的类型。第二句声明z是数组,其中数组的类型是x指针指向的类型。第三句声明m是指针数组,这和char *m[4]声明的效果是一样的。

2.变长数组

GNU C语言允许使用变长数组,这在定义数据结构时非常有用。

<mm/percpu.c>

struct pcpu_chunk {
    struct list_head    list;        
    unsigned long populated[];    /* 变长数组 */
};

以上数据结构中的最后一个元素被定义为变长数组,这种数组不占用结构体空间。这样,我们就可以根据对象大小动态地分配结构体的大小。

struct line {
  int length;
  char contents[0];
};

struct line *thisline = malloc(sizeof(struct line) + this_length);
thisline->length = this_length;

如上所示,line数据结构中定义了变量length和变长数组contents[0],line数据结构的大小只包含int类型的大小,不包含contents的大小,也就是sizeof (struct line) = sizeof (int)。创建结构体对象时,可根据实际需要指定这个变长数组的长度,并分配相应的空间。上述示例代码分配了this_length字节的内存,并且可以通过contents[index]来访问第index个地址的数据。

3.case的范围

GNU C语言支持指定case的范围为标签,例如:

case low ... high:
case 'A' ... 'Z':

这里指定case的范围为low~high、'A'~'Z'。下面是Linux内核中的示例代码。

<arch/x86/platform/uv/tlb_uv.c>

static int local_atoi(const char *name)
{
    int val = 0;

    for (;; name++) {
        switch (*name) {
        case '0' ... '9':
            val = 10*val+(*name-'0');
            break;
        default:
            return val;
        }
    }
}

另外,还可以用整型数表示范围,但是这里需要注意“...”的两边有空格,否则编译会出错。

<drivers/usb/gadget/udc/at91_udc.c>

static int at91sam9261_udc_init(struct at91_udc *udc)
{

    for (i = 0; i < NUM_ENDPOINTS; i++) {
        ep = &udc->ep[i];

        switch (i) {
        case 0:
            ep->maxpacket = 8;
            break;
        case 1 ... 3:
            ep->maxpacket = 64;
            break;
        case 4 ... 5:
            ep->maxpacket = 256;
            break;
        }
    }

}

4.标号元素

标准C语言要求数组或结构体在初始化时必须以固定顺序出现。但GNU C语言可以通过指定索引或结构体成员名来初始化,不必按照原来的固定顺序进行初始化。

结构体成员的初始化在Linux内核中经常使用,如在设备驱动中初始化file_operations数据结构。下面是Linux内核中的一个例子。

<drivers/char/mem.c>

static const struct file_operations zero_fops = {
    .llseek               = zero_lseek,
    .read                = new_sync_read,
    .write                = write_zero,
    .read_iter        = read_iter_zero,
    .aio_write        = aio_write_zero,
    .mmap                = mmap_zero,
};

在上述代码中,zero_fops的成员llseek被初始化为zero_lseek函数,read成员被初始化为new_sync_read函数,以此类推。当file_operations数据结构的定义发生变化时,这种初始化方法依然能保证已知元素的正确性,未初始化的成员的值为0或NULL。

5.可变参数宏

在GNU C语言中,宏可以接受可变数目的参数,这主要运用在输出函数中。

<include/linux/printk.h>

#define pr_debug(fmt, ...) \
    dynamic_pr_debug(fmt, ##__VA_ARGS__)

“...”代表可以变化的参数表,“__VA_ARGS__”是编译器保留字段,在进行预处理时把参数传递给宏。当调用宏时,实际参数就被传递给dynamic_pr_debug函数。

6.函数属性

GNU C语言允许声明函数属性(function attribute)、变量属性(variable attribute)和类型属性(type attribute),以便编译器进行特定方面的优化和更仔细的代码检查。以上属性的语法格式如下。

__attribute__ ((attribute-list))

GNU C语言里定义的函数属性有很多,如noreturn、format以及const等。此外,还可以定义一些和处理器架构相关的函数属性,如ARM架构中可以定义interrupt、isr等属性,有兴趣的读者可以阅读GCC的相关文档。

下面是Linux内核中使用format函数属性的一个例子。

<drivers/staging/lustru/include/linux/libcfs/>

int libcfs_debug_msg(struct libcfs_debug_msg_data *msgdata,
                const char *format1, ...)
    __attribute__ ((format (printf, 2, 3)));

libcfs_debug_msg()函数里声明了format函数属性,用于告诉编译器按照printf的参数表中的格式规则对函数参数进行检查。数字2表示第2个参数为格式化字符串,数字3表示参数“...”里的第1个参数在函数参数总数中排第几。

noreturn函数属性用于通知编译器函数从不返回值,这让编译器屏蔽了不必要的警告信息。比如die函数,该函数没有返回值。

void __attribute__((noreturn)) die(void);

const函数属性让编译器只调用函数一次,以后再调用时只需要返回第一次的结果即可,从而提高效率。

static inline u32 __attribute_const__ read_cpuid_cachetype(void)
{
    return read_cpuid(CTR_EL0);
}

Linux还有一些其他的函数属性,它们定义在compiler-gcc.h文件中。

#define __pure                      __attribute__((pure))
#define __aligned(x)                __attribute__((aligned(x)))
#define __printf(a, b)              __attribute__((format(printf, a, b)))
#define __scanf(a, b)               __attribute__((format(scanf, a, b)))
#define  noinline                   __attribute__((noinline))
#define __attribute_const__         __attribute__((__const__))
#define __maybe_unused              __attribute__((unused))
#define __always_unused             __attribute__((unused))

7.变量属性和类型属性

变量属性可以对变量或结构体成员进行属性设置。对于类型属性,常见的有alignment、packed和sections等。

alignment类型属性规定变量或结构体成员的最小对齐格式,以字节为单位。

struct qib_user_info {
    __u32 spu_userversion;
    __u64 spu_base_info;
} __aligned(8);

在上面这个例子中,编译器以8字节对齐的方式来分配数据结构qib_user_info。

packed类型属性可以使变量或结构体成员使用最小的对齐方式,对变量以字节对齐,对域以位对齐。

struct test
{
    char a;
    int x[2] __attribute__ ((packed));
};

x成员使用了packed类型属性,并且存储在变量a的后面,所以结构体test一共占用9字节。

8.内建函数

GNU C语言提供了一系列内建函数以进行优化,这些内建函数以“_builtin_”作为前缀。下面介绍Linux内核中常用的一些内建函数。

#define __swab16(x)                \
    (__builtin_constant_p((__u16)(x)) ?    \
    ___constant_swab16(x) :            \
    __fswab16(x))
#define LIKELY(x) __builtin_expect(!!(x), 1)     //x很可能为真
#define UNLIKELY(x) __builtin_expect(!!(x), 0)     //x很可能为假
<include/linux/prefetch.h>
#define prefetch(x) __builtin_prefetch(x)

#define prefetchw(x) __builtin_prefetch(x,1)

下面是使用prefetch()函数进行优化的一个例子。

<mm/page_alloc.c>

void __init __free_pages_bootmem(struct page *page, unsigned int order)
{
    unsigned int nr_pages = 1 << order;
    struct page *p = page;
    unsigned int loop;

    prefetchw(p);
    for (loop = 0; loop < (nr_pages - 1); loop++, p++) {
        prefetchw(p + 1);
        __ClearPageReserved(p);
        set_page_count(p, 0);
    }
…
}

在处理page数据结构之前,可通过prefetchw()预取到缓存中,从而提升性能。

9.asmlinkage

在标准C语言中,函数的形参在实际传入参数时会涉及参数存放问题。对于x86架构,函数参数和局部变量被一起分配到函数的栈(stack)中。

<arch/x86/include/asm/linkage.h>

#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))

__attribute__((regparm(0)))用于告诉编译器不需要通过任何寄存器来传递参数,只通过栈来传递。

对于ARM64来说,函数参数的传递有一套过程调用标准(Procedure Call Standard,PCS)。ARM64中的x0~x7寄存器存放传入参数,当参数超过8个时,多余的参数被存放在函数的栈中。所以,ARM64平台没有定义asmlinkage。

<include/linux/linkage.h>

#define asmlinkage CPP_ASMLINKAGE
#define asmlinkage CPP_ASMLINKAGE

10.UL

在Linux内核代码中,我们经常会看到一些数字的定义中使用了UL后缀。数字常量会被隐式定义为int类型,将两个int类型数据相加的结果可能会发生溢出,因此使用UL强制把int类型的数据转换为unsigned long类型,这是为了保证运算过程不会因为int的位数不同而导致溢出。

1:表示有符号整型数字1
1UL:表示无符号长整型数字1

Linux内核代码中广泛使用了数据结构和算法。本节介绍链表和红黑树。

Linux内核代码大量使用了链表这种数据结构。链表是为了解决数组不能动态扩展这个缺陷而产生的一种数据结构。链表中包含的元素可以动态创建并插入和删除。链表中的每个元素都是离散存放的,因此不需要占用连续的内存。链表通常由若干节点组成,每个节点的结构都是一样的,由有效数据区和指针区两部分组成。有效数据区用来存储有效数据信息,而指针区用来指向链表的前继节点或后继节点。因此,链表就是利用指针将各个节点串联起来的一种存储结构。

1.单向链表

单向链表的指针区只包含一个指向下一个元素的指针,因此会形成单一方向的链表,如以下代码所示。

struct list {
    int data;               /*有效数据*/
    struct list *next;     /*指向下一个元素的指针*/
};

如图2.2所示,单向链表具有单向移动性,也就是只能访问当前节点的后继节点,而无法访问当前节点的前继节点,因此在实际项目中运用得比较少。

图2.2 单向链表示意图

2.双向链表

如图2.3所示,双向链表和单向链表的区别在于指针区包含了两个指针,一个指向前继节点,另一个指向后继节点,如以下代码所示。

struct list {
    int data;               /*有效数据*/
    struct list *next;     /*指向下一个元素的指针*/
    struct list *prev;     /*指向上一个元素的指针*/
};

图2.3 双向链表示意图

3.Linux内核中链表的实现

单向链表和双向链表在实际使用中有一些局限性,如数据区必须存放固定数据,而实际需求是多种多样的。这种方法无法构建一套通用的链表,因为每个不同的数据区需要一套链表。为此,Linux内核把所有链表操作的共同部分提取出来,把不同的部分留给编程人员自行处理。Linux内核实现了一套纯链表的封装,链表节点只有指针区而没有数据区,还封装了各种操作函数,如创建节点函数、插入节点函数、删除节点函数、遍历节点函数等。

Linux内核中的链表可使用list_head数据结构来描述。

<include/linux/types.h>

struct list_head {
    struct list_head *next, *prev;
};

list_head数据结构不包含链表节点的数据区,而是通常嵌入其他数据结构,如page数据结构中就嵌入了lru链表节点,做法通常是把page数据结构挂入LRU链表。

<include/linux/mm_types.h>

struct page {
    ...
    struct list_head lru;
    ...
}

链表头的初始化有两种方法。一种是静态初始化,另一种是动态初始化。把next和prev指针都初始化并指向自身,这样便能够初始化一个带头节点的空链表。

<include/linux/list.h>

/*静态初始化*/
#define LIST_HEAD_INIT(name) { &(name), &(name) }

#define LIST_HEAD(name) \
    struct list_head name = LIST_HEAD_INIT(name)

/*动态初始化*/
static inline void INIT_LIST_HEAD(struct list_head *list)
{
    list->next = list;
    list->prev = list;
}

可添加节点到链表中,Linux内核为此提供了几个接口函数,如list_add()用于把节点添加到表头,list_add_tail()则用于把节点添加到表尾。

<include/linux/list.h>

void list_add(struct list_head *new, struct list_head *head)
list_add_tail(struct list_head *new, struct list_head *head)

以下是用于遍历节点的接口函数。

#define list_for_each(pos, head) \
    for (pos = (head)->next; pos != (head); pos = pos->next)

list_for_each()宏只遍历节点的当前位置,那么如何获取节点本身的数据结构呢?这里还需要使用list_entry()宏。

#define list_entry(ptr, type, member) \
    container_of(ptr, type, member)

container_of()宏定义在kernel.h头文件中。

#define container_of(ptr, type, member) ({            \
    const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
    (type *)( (char *)__mptr - offsetof(type,member) );})

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

这里,首先把0地址转换为type结构体的指针。然后获取type结构体中member成员的指针,也就是获取member在type结构体中的偏移量。最后用指针ptr减去offset,从而得到type结构体的真实地址。

下面是遍历链表的一个例子。

<drivers/block/osdblk.c>

static ssize_t class_osdblk_list(struct class *c,
                struct class_attribute *attr,
                char *data)
{
    int n = 0;
    struct list_head *tmp;

    list_for_each(tmp, &osdblkdev_list) {
        struct osdblk_device *osdev;

        osdev = list_entry(tmp, struct osdblk_device, node);

        n += sprintf(data+n, "%d %d %llu %llu %s\n",
            osdev->id,
            osdev->major,
            osdev->obj.partition,
            osdev->obj.id,
            osdev->osd_path);
    }
    return n;
}

红黑树(red black tree)被广泛应用在内核的内存管理和进程调度中,用于将排序的元素组织到树中。红黑树还被广泛应用于计算机科学的各个领域,在速度和实现复杂度之间取得了很好的平衡。

红黑树是具有以下特征的二叉树。

红黑树的一个优点是,所有重要的操作(例如插入、删除、搜索)都可以在O(log2n)的时间内完成,n为树中元素的数目。经典的算法教科书都会讲解红黑树的实现,这里只是列出Linux内核中使用红黑树的一个例子,供读者在进行驱动和内核编程的过程中参考。这个例子可以在Linux内核代码的Documentation/Rbtree.txt文件中找到。

#include <linux/init.h>
#include <linux/list.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/mm.h>
#include <linux/rbtree.h>

MODULE_AUTHOR("figo.zhang");
MODULE_DESCRIPTION(" ");
MODULE_LICENSE("GPL");

  struct mytype { 
     struct rb_node node;
     int key; 
};

/*红黑树的根节点*/
 struct rb_root mytree = RB_ROOT;
/*根据key查找节点*/
struct mytype *my_search(struct rb_root *root, int new)
  {
     struct rb_node *node = root->rb_node;

     while (node) {
          struct mytype *data = container_of(node, struct mytype, node);

          if (data->key > new)
               node = node->rb_left;
          else if (data->key < new)
               node = node->rb_right;
          else
               return data;
     }
     return NULL;
  }

/*把一个元素插入红黑树中*/
  int my_insert(struct rb_root *root, struct mytype *data)
  {
     struct rb_node **new = &(root->rb_node), *parent=NULL;

     /*寻找可以添加新节点的地方*/
     while (*new) {
          struct mytype *this = container_of(*new, struct mytype, node);

          parent = *new;
          if (this->key > data->key)
               new = &((*new)->rb_left);
          else if (this->key < data->key) {
               new = &((*new)->rb_right);
          } else
               return -1;
     }

     /*添加一个新节点*/
     rb_link_node(&data->node, parent, new);
     rb_insert_color(&data->node, root);

     return 0;
  }

static int __init my_init(void)
{
     int i;
     struct mytype *data;
     struct rb_node *node;

     /*插入元素*/
     for (i =0; i < 20; i+=2) {
          data = kmalloc(sizeof(struct mytype), GFP_KERNEL);
          data->key = i;
          my_insert(&mytree, data);
     }

     /*遍历红黑树,输出所有节点的key值*/
      for (node = rb_first(&mytree); node; node = rb_next(node)) 
          printk("key=%d\n", rb_entry(node, struct mytype, node)->key);

     return 0;
}

static void __exit my_exit(void)
{
     struct mytype *data;
     struct rb_node *node;
     for (node = rb_first(&mytree); node; node = rb_next(node)) {
          data = rb_entry(node, struct mytype, node);
          if (data) {
                rb_erase(&data->node, &mytree);
                kfree(data);
          }
     }
}
module_init(my_init);
module_exit(my_exit);

mytree是红黑树的根节点,my_insert()用于把一个元素插入红黑树中,my_search()根据key来查找节点。Linux内核大量使用了红黑树,如虚拟地址空间(Virtual Memory Area,VMA)的管理。

生产者-消费者模型是计算机编程中最常见的一种模型。生产者产生数据,而消费者消耗数据。比如网络设备,硬件设备接收网络包,然后应用程序读取网络包。环形缓冲区是实现生产者-消费者模型的经典算法。环形缓冲区通常有读指针和写指针。读指针指向环形缓冲区中可读的数据,写指针指向环形缓冲区中可写的数据。通过移动读指针和写指针实现缓冲区数据的读取和写入。

在Linux内核中,KFIFO是采用无锁环形缓冲区的典型代表。FIFO的全称是“First In First Out”,是一种先进先出的数据结构,并采用环形缓冲区的方法来实现,同时提供了无边界的字节流服务。采用环形缓冲区的好处是,当一个数据元素被消耗之后,其余数据元素不需要移动存储位置,从而减少复制操作,提高效率。

1.创建KFIFO

在使用KFIFO之前需要进行初始化,这里有静态初始化和动态初始化两种方式。

<include/linux/kfifo.h>

int kfifo_alloc(fifo, size, gfp_mask)

以上函数创建并分配一个大小为size的KFIFO环形缓冲区。参数fifo指向缓冲区的kfifo数据结构,参数size指定缓冲区中元素的数量,参数gfp_mask表示分配给KFIFO元素使用的分配掩码。

静态分配可以使用下面的宏。

#define DEFINE_KFIFO(fifo, type, size)
#define INIT_KFIFO(fifo)

2.入列

为了把数据写入KFIFO环形缓冲区,可以使用kfifo_in()函数接口。

int kfifo_in(fifo, buf, n)

以上函数把buf指针指向的n个数据元素复制到KFIFO环形缓冲区中。参数fifo指向的是KFIFO环形缓冲区,参数buf指向数据要复制到的缓冲区,参数n指定要复制多少个数据元素。

3.出列

为了从KFIFO环形缓冲区中列出或摘取数据,可以使用kfifo_out()函数接口。

#define    kfifo_out(fifo, buf, n)

以上函数从fifo指向的环形缓冲区中复制n个数据元素到buf指向的环形缓冲区中。如果KFIFO环形缓冲区的数据元素小于n个,那么复制出去的数据元素也小于n个。

4.获取缓冲区大小

KFIFO提供了几个接口函数来查询环形缓冲区的状态。

#define kfifo_size(fifo)
#define kfifo_len(fifo)
#define    kfifo_is_empty(fifo)
#define    kfifo_is_full(fifo)

kfifo_size()用来获取环形缓冲区的大小,也就是最多可以容纳多少个数据元素。kfifo_len()用来获取当前环形缓冲区中有多少个有效数据元素。kfifo_is_empty()判断环形缓冲区是否为空。kfifo_is_full()判断环形缓冲区是否已满。

5.与用户空间中的数据交互

KFIFO还封装了两个函数,用于与用户空间中的数据交互。

#define    kfifo_from_user(fifo, from, len, copied)
#define    kfifo_to_user(fifo, to, len, copied)

kfifo_from_user()会把from指向的用户空间中的len个数据元素复制到KFIFO中,最后一个参数copied表示成功复制了几个数据元素。kfifo_to_user()则相反,用于把KFIFO中的数据元素复制到用户空间中。这两个宏结合了copy_to_user()、copy_from_user()以及KFIFO的工作机制,给驱动开发者提供了方便。在第6章,虚拟FIFO设备的驱动程序会采用这两个接口函数来实现。

Linux内核代码很庞大,而且数据结构错综复杂,只使用文本工具来浏览代码会让人抓狂和崩溃。很多读者使用Windows中收费的代码浏览软件Source Insight来阅读内核源代码,但是使用Vim工具一样可以打造出相比Source Insight更强大的功能。

Vim是类似于Vi的、功能强大并且可以高度定制的文件编辑器,它在Vi的基础上改进并增加了很多特性。由于Vim的设计理念和Windows的Source Insight等编辑器很不一样,因此刚接触Vim的读者会或多或少感到不适应,但了解了Vim的设计思路之后就会慢慢喜欢上Vim。Vim的设计理念是整个文本编辑器都用键盘来操作,而不需要使用鼠标。键盘上的几乎每个键都有固定的用法,用户可以在普通模式下完成大部分编辑工作。

Vim是Linux开源系统中最著名的代码编辑器之一,在国内外拥有众多的使用者,并且拥有众多的插件。在20世纪80年代,Bram Moolenaar从开源的Vi工具开发了Vim的1.0版本。Vim是Vi Improved的意思。1994年发布的Vim 3.0版本加入了多视窗编辑模式,1994年发布的Vim 4.0版本加入了图形用户界面(GUI),2006年发布的Vim 7.0版本加入了拼写检查、上下文补全、标签页编辑等功能。经过长达10年的更新迭代之后,开发团队终于在2016年发布了跨时代的Vim 8.0版本。

Vim 8.0版本拥有以下新特性,这让Vim编辑器变得更好用、更强大。

Vim 8最重要的新特性就是支持异步I/O。老版本的Vim在调用外部的插件程序时,如编译、更新tags索引库、检查错误等,只能等待外部程序结束了才能返回Vim主程序。对异步I/O的支持可以让外部的插件程序在后台运行,不影响Vim主程序的代码编辑和浏览等,从而提升了Vim的用户体验。

Ubuntu Linux 20.04系统默认安装了Vim 8.1版本。

Vim编辑器有3种工作模式,分别是命令模式(command mode)、输入模式(insert mode)和底行模式(last line mode)。

在Linux终端输入Vim可以打开Vim编辑器,自动载入所要编辑的文件,比如“vim mm/memory.c”表示打开Vim编辑器时自动打开memory.c文件。

要退出Vim编辑器,可以在底行模式下输入“:q”,这时不保存文件并且离开,输入“:wq”表示存档并且离开。

在Vim的实际使用过程中,3种模式的切换是最常用的操作。通常熟悉Vim的读者都会尽可能避免处于插入模式,因为插入模式的功能有限。Vim的强大之处在于它的命令模式。所以越熟悉Vim,就会在插入模式上花费越少的时间。

1.从命令模式和底行模式转为插入模式

从命令模式和底行模式转为插入模式是最常见的操作,因此使用频率最高的一个命令就是“i”,它表示从光标所在位置开始插入字符。另外一个使用频率比较高的命令是“o”,它表示在光标所在的行新增一行,并进入插入模式。常见的插入命令如表2.2所示。

表2.2 常见的插入命令

功  能

命  令

描  述

使用频率

插入字符

i

进入插入模式,并从光标所在处输入字符

常用

I

进入插入模式,并从光标所在行的第一个非空格符处开始输入

不常用

a

进入插入模式,并在光标所在的下一个字符处开始输入

不常用

A

进入插入模式,并从光标所在行的最后一个字符处开始输入

不常用

新增一行

o

进入插入模式,并从光标所在行的下一行新增一行

常用

O

进入插入模式,并从光标所在行的上一行新增一行

不常用

在输入上述插入命令之后,在Vim编辑器的左下角会出现INSERT字样,表示已经进入插入模式。

2.从插入模式转为命令模式或底行模式

按Esc键可以退出插入模式,进入命令模式。

3.从命令模式转为底行模式

在命令模式下输入“:”便会进入底行模式。

Vim编辑器已放弃使用键盘上的方向键,而使用h、j、k、l命令来实现左、下、上、右方向键的功能,这样就不用频繁地在方向键和字母键之间来回移动,从而节省时间。另外,在h、j、k、l命令的前面可以添加数字,比如9j表示向下移动9行。

常见的光标移动命令如表2.3所示。

表2.3 常见的光标移动命令

命  令

描  述

w

正向移动到下一个单词的开头

b

反向移动到下一个单词的开头

f{char}

正向移动到下一个{char}字符所在之处

Ctrl + f

屏幕向下移动一页,相当于Page Down键

Ctrl + b

屏幕向上移动一页,相当于Page Up键

Ctrl + d

屏幕向下移动半页

Ctrl + u

屏幕向上移动半页

+

光标移动到非空格符的下一行

-

光标移动到非空格符的上一行

0

移动到光标所在行的最前面的字符

$

移动到光标所在行的最后面的字符

H

移动到屏幕最上方那一行的第一个字符

L

移动到屏幕最下方那一行的第一个字符

G

移动到文件的最后一行

nG

n为数字,表示移动到文件的第n

gg

移动文件的第一行

nEnter

n为数字,光标向下移动n

常见的删除、复制和粘贴命令如表2.4所示。

表2.4 常见的删除、复制和粘贴命令

命  令

描  述

x

删除光标所在的字符(相当于Del键)

X

删除光标所在的前一个字符(相当于Backspace键)

dd

删除光标所在的行

ndd

删除光标所在行的向下n

yy

复制光标所在的那一行

nyy

n为数字,复制光标所在的向下n

p

把已经复制的数据粘贴到光标的下一行

u

撤销前一个命令

在进行大段文本的复制时,我们可以输入命令“v”以进入可视选择模式。

常见的查找和替换命令如表2.5所示。

表2.5 常见的查找和替换命令

命  令

描  述

/<要查找的字符>

向下查找

?<要查找的字符>

向上查找

:{作用范围}s/{目标}/{替换}/{替换标志}

比如:%s/figo/ben/g会在全局范围(%)查找figo并替换为ben,所有出现的地方都会被替换(g)

和文件相关的操作都需要在底行模式下进行,也就是在命令模式下输入“:”。常见的文件相关命令如表2.6所示。

表2.6 常见的文件相关命令

命  令

描  述

:q

退出Vim

:q!

强制退出Vim,修改过的文件不会被保存

:w

保存修改过的文件

:w!

强制保存修改过的文件

:wq

保存文件后退出Vim

:wq!

强制保存文件后退出Vim

2005年,Linus Torvalds因为不满足于当时任何可用的开源版本控制系统,于是亲手开发了一个全新的版本控制软件——git。git发展到今天,已经成为全世界最流行的代码版本管理软件之一,微软公司的开发工具也支持git。

早年,Linus Torvalds选择使用商业版本的代码控制系统BitKeeper来管理Linux内核代码。BitKeeper是由BitMover公司开发的,授权Linux社区免费使用。到了2005年,Linux社区中有人试图破解BitKeeper协议时被BitMover公司发现,因此BitMover公司收回了BitKeeper的使用授权,于是Linus Torvalds花了两周时间,用C语言写了一个分布式版本控制系统,git就这样诞生了。

在学习git这个工具之前,读者有必要了解一下集中式版本控制系统和分布式版本控制系统。

集中式版本控制系统把版本库集中存放在中央服务器里,当我们需要编辑代码时,需要首先从中央服务器中获取最新的版本,然后编写或修改代码。修改和测试完代码之后,需要把修改的东西推送到中央服务器。集中式版本控制系统需要每次都连接中央服务器,如果有很多人协同工作,网络带宽将是瓶颈。

和集中式版本控制系统相比,分布式版本控制系统没有中央服务器的概念,每个人的计算机就是一个完整的版本库,这样工作中就不需要联网,和网络带宽无关。分布式版本便于多人协同工作,比如A修改了文件1,B也修改了文件1,那么A和B只需要把各自的修改推送给对方,就可以相互看到对方修改的内容了。

使用git进行开源工作的流程一般如下。

(1)复制项目的git仓库到本地工作目录。

(2)在本地工作目录里添加或修改文件。

(3)在提交修改之前检查补丁格式等。

(4)提交修改。

(5)生成补丁并发给评审,等待评审意见。

(6)评审发送修改意见,再次修改并提交。

(7)直到评审同意补丁并且合并到主干分支。

下面介绍一下git常用的命令。

在Ubuntu Linux中可使用apt-get工具来安装git。

$ sudo apt-get install git

在使用git之前需要配置用户信息,如用户名和邮箱信息。

$ git config --global user.name "xxx"
$ git config --global user.email xxx@xxx.com

可以设置git默认使用的文本编辑器,一般使用Vi或Vim。当然,也可以设置为Emacs。

$ git config --global core.editor emacs

要检查已有的配置信息,可以使用 git config --list 命令。

$ git config –list

1.下载git仓库

版本库又名仓库,英文是repository,可以简单理解成目录。git仓库中的所有文件都由git来管理,每个文件的修改、删除都可以被git跟踪,并且可以追踪提交的历史和详细信息,还可以还原到历史中的某个提交,以便做回归测试。

git clone命令可以从现有的git仓库中下载代码到本地,功能类似于svn工具的checkout。如果需要参与开源项目或者查看开源项目的代码,就需要使用git clone将项目的代码下载到本地,并进行浏览或修改。

我们以Linux内核官方的git仓库为例,通过下面的命令可以把Linux内核官方的git仓库下载到本地。

$ git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git

执行完上述命令之后,会在本地当前目录中创建名为linux的子目录,其中包含的.git目录用来保存git仓库的版本记录。

Linux内核官方的git仓库以Linus Torvalds创建的git仓库为准。每隔两三个月,Linus就会在自己的git仓库中发布新的Linux内核版本,读者可以到网页版本上浏览。

2.查看提交的历史

通过git clone命令下载代码仓库到本地之后,就可以通过git log命令来查看提交(commit)的历史。

 $ git log

commit d081107867b85cc7454b9d4f5aea47f65bcf06d1
Author: Michael S. Tsirkin <mst@redhat.com>
Date:   Fri Apr 13 15:35:23 2018 -0700

    mm/gup.c: document return value

    __get_user_pages_fast handles errors differently from
    get_user_pages_fast: the former always returns the number of pages
    pinned, the later might return a negative error code.

    Link: http://lkml.kernel.org/r/1522962072-182137-6-git-send-email- mst@redhat. com
    Signed-off-by: Michael S. Tsirkin <mst@redhat.com>
    Reviewed-by: Andrew Morton <akpm@linux-foundation.org>
    Cc: Kirill A. Shutemov <kirill.shutemov@linux.intel.com>
    Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
    Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>

上面的git log命令显示了一条git提交的相关信息,包含的内容如下。

可以使用--oneline选项来查看简洁版的信息。

$ git log --oneline 
d081107 mm/gup.c: document return value
c61611f get_user_pages_fast(): return -EFAULT on access_ok failure
09e35a4 mm/gup_benchmark: handle gup failures
60bb83b resource: fix integer overflow at reallocation
16e205c Merge tag 'drm-fixes-for-v4.17-rc1' of git://people.freedesktop.org/~airlied/linux

如果只想查找指定用户提交的日志,可以使用命令git log --author。例如,要找Linux内核源码中 Linus所做的提交,可以使用如下命令。

$ git log --author=Linus --oneline 

16e205c Merge tag 'drm-fixes-for-v4.17-rc1' of git://people.freedesktop.org/~airlied/linux
affb028 Merge tag 'trace-v4.17-2' of git://git.kernel.org/pub/scm/linux/kernel/git/rostedt/linux-trace
0c314a9 Merge tag 'pci-v4.17-changes-2' of git://git.kernel.org/pub/scm/linux/kernel/git/helgaas/pci
681857e Merge branch 'parisc-4.17-2' of git://git.kernel.org/pub/scm/linux/kernel/git/deller/parisc-linux

git log命令的参数“--patch-with-stat”用于显示提交代码的差异、增改文件以及行数等信息。

$ git log --patch-with-stat 

commit d081107867b85cc7454b9d4f5aea47f65bcf06d1
Author: Michael S. Tsirkin <mst@redhat.com>
Date:   Fri Apr 13 15:35:23 2018 -0700

    mm/gup.c: document return value

    __get_user_pages_fast handles errors differently from
    get_user_pages_fast: the former always returns the number of pages
    pinned, the later might return a negative error code.

    Signed-off-by: Michael S. Tsirkin <mst@redhat.com>
    Reviewed-by: Andrew Morton <akpm@linux-foundation.org>
    Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
---
 arch/mips/mm/gup.c  | 2 ++
 arch/s390/mm/gup.c  | 2 ++
 arch/sh/mm/gup.c    | 2 ++
 arch/sparc/mm/gup.c | 4 ++++
 mm/gup.c            | 4 +++-
 mm/util.c           | 6 ++++--
 6 files changed, 17 insertions(+), 3 deletions(-)

diff --git a/arch/mips/mm/gup.c b/arch/mips/mm/gup.c
index 1e4658e..5a4875ca 100644
--- a/arch/mips/mm/gup.c
+++ b/arch/mips/mm/gup.c
…

要对某个提交的内容进行查看,可以使用git show命令。在git show命令的后面需要添加某个提交的commit id,可以是缩减版本的commit id,如下所示。

$ git show d0811078

3.修改和提交

使用git进行提交的流程如下。

(1)修改、增加或删除一个或多个文件。

(2)使用git diff查看当前修改。

(3)使用git status查看当前工作目录的状态。

(4)使用git add把修改、增加或删除的文件添加到本地版本库。

(5)使用git commit命令生成提交。

git diff命令可以显示保存在缓存中或未保存在缓存中的改动,常用的选项如下。

git add命令可以把修改的文件添加到缓存中。

git rm命令可以删除本地仓库中的某个文件。不建议直接使用rm命令。同样,当需要移动文件或目录时,可以使用git mv命令。

git status命令用来查看当前本地仓库的状态,既显示工作目录和缓存区的状态,也显示被缓存的修改文件以及还没有被git跟踪到的文件或目录。

git commit命令用来将更改记录提交到本地仓库。提交时通常需要编写一条简短的日志信息,以告诉其他人为什么要做修改。为git commit命令添加“-s”会在提交中自动添加“Signed-off-by:”签名。如果需要对提交的内容做修改,可以使用git commit --amend命令。

分支(branch)意味着可以从开发主线中分离出分支,然后在不影响主线的同时继续开发工作。分支管理在实际项目开发中非常有用,比如,为了开发某个功能A,预计需要一个月时间才能完成编码和测试工作。假设在完成编码工作时把补丁提交到主干,没经过测试的代码可能会影响项目中的其他模块,因此通常的做法是在本地创建一个属于自己的分支,然后把补丁提交到这个分支,等完成最后的测试验证工作之后,再把补丁合并到主干。

1.创建分支

在管理分支之前,需要先使用git branch命令查看当前git仓库里有哪些分支。

$ git branch 
*master

比如Linux内核官方的git仓库中只有一个分支,名为“master”(主分支),该分支也是当前分支。当创建新的git仓库时,默认情况下git会创建master分支。

下面使用git branch命令创建一个新的分支,名为linux-benshushu。

$ git branch linux-benshushu
$ git branch 
 linux-benshushu
* master

“*”表示当前分支,我们虽然创建了一个名为linux-benshushu的分支,但是当前分支还是master分支。

2.切换分支

下面使用git checkout branchname命令来切换分支。

$ git checkout linux-benshushu 
Switched to branch 'linux-benshushu'
$ git branch 
* linux-benshushu
  master

另外,可以使用git checkout -b branchname命令合并上述两个步骤,也就是创建新的分支并立即切换到该分支。

3.删除分支

如果想删除分支,可以使用git branch -d branchname命令。

$ git branch -d linux-benshushu 
error: Cannot delete the branch 'linux-benshushu' which you are currently on.

上面显示不能删除当前分支,所以需要切换到其他分支才能删除linux-benshushu分支。

$ git checkout master
Switched to branch 'master'

$ git branch -d linux-benshushu 
Deleted branch linux-benshushu (was d081107).

4.合并分支

git merge命令用来合并指定分支到当前分支,比如对于linux-benshushu分支,我们可通过下面的命令把该分支合并到主分支。

$ git checkout master
$ git branch 
  linux-benshushu
* master

$ git merge linux-benshushu 
Updating 60cc43f..6e82d42
Fast-forward
 Makefile | 1 +
 1 file changed, 1 insertion(+)

5.推送分支

推送分支就是把本地创建的新分支中的提交推送到远程仓库。在推送过程中,需要指定本地分支,这样才能把本地分支中的提交推送到远程仓库里对应的远程分支。推送分支的命令格式如下。

git push <远程主机名> <本地分支名>:<远程分支名>

通过以下命令,可查看有哪些远程分支。

$ git branch –a
 linux-benshushu
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/master

远程分支以remotes开头,可以看到远程分支只有一个,也就是origin仓库的主分支。通过下面的命令可以把本地的主分支中的改动推送到远程仓库中的主分支。本地分支名和远程分支名同名,因此可以忽略远程分支名。

$ git push origin master

当本地分支名和远程分支名不相同时,需要明确指出远程分支名。如下命令可把本地的主分支推送到远程的dev分支。

$ git push origin master:dev

1.实验目的

(1)熟悉GCC的编译过程,学会使用ARM GCC交叉工具链编译应用程序并在QEMU虚拟机中运行。

(2)学会写简单的Makefile。

2.实验详解

本实验通过一个简单的C语言程序演示GCC的编译过程。源文件test.c中的代码如下。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define PAGE_SIZE 4096
#define MAX_SIZE 100*PAGE_SIZE

int main()
{
    char *buf = (char *)malloc(MAX_SIZE);

    memset(buf, 0, MAX_SIZE);

    printf("buffer address=0x%p\n", buf);

    free(buf);
           return 0;
}

1)预处理

GCC的“-E”选项可以让编译器在预处理阶段就结束,选项“-o”可以指定输出的文件格式。

$ aarch64-linux-gnu-gcc -E test.c -o test.i

在预处理阶段会把C标准库的头文件中的代码包含到这段程序中。test.i文件的内容如下所示。

extern void *malloc (size_t __size) __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__malloc__)) ;

…

int main()
{
 char *buf = (char *)malloc(100*4096);

 memset(buf, 0, 100*4096);

 printf("buffer address=0x%p\n", buf);

 free(buf);
        return 0;
}

2)编译

编译阶段的任务主要是对预处理好的test.i文件进行编译,并生成汇编代码。GCC首先检查代码是否有语法错误等,然后把代码编译成汇编代码。我们这里使用“-S”选项来编译。

$ aarch64-linux-gnu-gcc -S test.i -o test.s

编译阶段生成的汇编代码如下。

    .arch armv8-a
    .file    "test.c"
    .text
    .section    .rodata
    .align    3
.LC0:
    .string    "buffer address=0x%p\n"
    .text
    .align    2
    .global    main
    .type    main, %function
main:
.LFB6:
    .cfi_startproc
    stp    x29, x30, [sp, -32]!
    .cfi_def_cfa_offset 32
    .cfi_offset 29, -32
    .cfi_offset 30, -24
    mov    x29, sp
    mov    x0, 16384
    movk    x0, 0x6, lsl 16
    bl    malloc
    str    x0, [sp, 24]
    mov    x2, 16384
    movk    x2, 0x6, lsl 16
    mov    w1, 0
    ldr    x0, [sp, 24]
    bl    memset
    ldr    x1, [sp, 24]
    adrp    x0, .LC0
    add    x0, x0, :lo12:.LC0
    bl    printf
    ldr    x0, [sp, 24]
    bl    free
    mov    w0, 0
    ldp    x29, x30, [sp], 32
    .cfi_restore 30
    .cfi_restore 29
    .cfi_def_cfa_offset 0
    ret
    .cfi_endproc
.LFE6:
    .size    main, .-main
    .ident    "GCC: (Ubuntu 9.3.0-10ubuntu1) 9.3.0"
    .section    .note.GNU-stack,"",@progbits

3)汇编

汇编阶段的任务是将汇编文件转换成二进制文件,利用“-c”选项就可以生成二进制文件。

$ aarch64-linux-gnu-gcc -c test.s -o test.o

4)链接

链接阶段的任务是对编译好的二进制文件进行链接,这里会默认链接C语言标准库(libc)。代码里调用的malloc()、memset()以及printf()等函数都由C语言标准库提供,链接过程会把程序的目标文件和所需的库文件链接起来,最终生成可执行文件。

Linux内核中的库文件分成两大类。一类是动态链接库(通常以.so结尾),另一类是静态链接库(通常以.a结尾)。默认情况下,GCC在链接时优先使用动态链接库,只有当动态链接库不存在时才使用静态链接库。下面使用“--static”来让test程序静态链接C语言标准库,原因是交叉工具链使用的libc目录中的动态库和QEMU中使用的库可能不一样。如果使用动态链接,可能导致运行时错误。

$ aarch64-linux-gnu-gcc test.o -o test --static

以ARM64 GCC交叉工具链为例,C语言标准库的动态库地址为/usr/arm-linux-gnueabi/lib,最终的库文件是libc-2.23.so文件。

$ ls -l /usr/aarch64-linux-gnu/lib/libc.so.6 
lrwxrwxrwx 1 root root 12 Apr  3 03:11 /usr/aarch64-linux-gnu/lib/libc.so.6 -> libc-2.31.so

C语言标准库的静态库地址如下:

$ ls -l /usr/aarch64-linux-gnu/lib/libc.a 
-rw-r--r-- 1 root root 4576436 Apr  3 03:11 /usr/aarch64-linux-gnu/lib/libc.a

5)在QEMU虚拟机中运行

把test程序放入runninglinuxkernel_5.0/kmodules目录,启动QEMU虚拟机并运行test程序。

$ ./run_rlk_arm64.sh run  #启动QEMU + ARM64平台

# cd /mnt
# ./test 
buffer address= 0xffff92bad010

6)编写如下简单的Makefile文件来编译test程序。

cc = aarch64-linux-gnu-gcc
prom = test
obj = test.o
CFLAGS = -static

$(prom): $(obj)
    $(cc) -o $(prom) $(obj) $(CFLAGS)

%.o: %.c 
    $(cc) -c $< -o $@

clean:
    rm -rf $(obj) $(prom)

1.实验目的

(1)学会和研究Linux内核提供的链表机制。

(2)编写一个应用程序,利用Linux内核提供的链表机制创建一个链表,把100个数字添加到这个链表中,循环该链表以输出所有成员的值。

2.实验详解

Linux内核链表提供的接口函数定义在include/linux/list.h文件中。本实验把这些接口函数移植到用户空间中,并使用它们完成链表操作。

1.实验目的

(1)学习和研究Linux内核提供的红黑树机制。

(2)编写一个应用程序,利用Linux内核提供的红黑树机制创建一棵红黑树,把10 000个随机数添加到这棵红黑树中。

(3)实现一个查找函数,快速在这棵红黑树中查找相应的数字。

2.实验详解

Linux内核提供的红黑树机制实现在lib/rbtree.c和include/linux/rbtree.h文件中。本实验要求把Linux内核实现的红黑树机制移植到用户空间中,并且实现10 000个随机数的插入和查找功能。

1.实验目的

熟悉Vim工具的基本操作。

2.实验详解

Vim操作需要一定的练习才能达到熟练的程度,读者可以使用Ubuntu Linux 20.04系统中的Vim程序进行代码的编辑练习。

1.实验目的

通过配置把Vim打造成一个能和Source Insight相媲美的IDE编辑工具。

2.实验详解

Vim工具可以支持很多个性化的特性,并使用插件来完成浏览和编辑代码的功能。使用过Source Insight的读者也许会对如下功能赞叹有加。

这些功能在Vim里都可以实现,而且比Source Insight高效和好用。本实验将带领读者着手打造一个属于自己的IDE编辑工具。

在打造之前先安装git工具。

$ sudo apt-get install git vim

1)插件管理工具Vundle

Vim支持很多插件,在早期,需要到每个插件网站上下载并复制到home主目录的.vim子目录中才能使用。现在,Vim社区有多个插件管理工具,其中Vundle就很出色,它可以在.vimrc中跟踪、管理和自动更新插件等。

安装Vundle需要使用git工具,可通过如下命令来下载Vundle工具。

$ git clone https://github.com/VundleVim/Vundle.vim.git ~/.vim/bundle/Vundle.vim

接下来,需要在home主目录下的.vimrc配置文件中配置Vundle。

<在.vimrc配置文件中添加如下配置>

" Vundle manage
set nocompatible              " be iMproved, required
filetype off                  " required

" set the runtime path to include Vundle and initialize
set rtp+=~/.vim/bundle/Vundle.vim
call vundle#begin()

" let Vundle manage Vundle, required
Plugin 'VundleVim/Vundle.vim'

" All of your Plugins must be added before the following line
call vundle#end()            " required
filetype plugin indent on    " required

只需要在.vimrc配置文件中添加“Plugin xxx”,即可安装名为“xxx”的插件。

接下来在线安装插件。启动Vim,然后运行命令“:PluginInstall”,就会从网络上下载插件并安装。

2)ctags工具

ctags的英文全称为generate tag files for source code。ctags工具用于扫描指定的源文件,找出其中包含的语法元素,并把找到的相关内容记录下来,这样在浏览和查找代码时就可以利用这些记录实现查找和跳转功能。ctags工具已经被集成到各大Linux发行版中。在Ubuntu Linux中可使用如下命令安装ctags工具。

$ sudo apt-get install universal-ctags

在使用ctags工具之前需要手动生成索引文件。

$ ctags –R .            //递归扫描源代码的根目录和所有子目录中的文件并生成索引文件

上述命令会在当前目录下生成一个tags文件。启动Vim之后需要加载这个tags文件,可以通过如下命令实现这个加载操作。

:set tags=tags

ctags工具中常用的快捷键如表2.7所示。

表2.7 ctags工具中常用的快捷键

快 捷 键

用  法

Ctrl + ]

跳转到光标处的函数或变量的定义位置

Ctrl + T

返回到跳转之前的地方

3)cscope工具

刚才介绍的ctags工具可以跳转到标签定义的地方,但是如果想查找函数在哪里被调用过或者标签在哪些地方出现过,ctags工具就无能为力了。cscope工具可以实现上述功能,这也是Source Insight的强大功能之一。

Cscope工具最早由贝尔实验室开发,后来由SCO公司以BSD协议公开发布。在Ubuntu Linux发行版中可以使用如下命令安装cscope工具。

$ sudo apt-get install cscope

在使用cscope工具之前需要为源代码生成索引库,可以使用如下命令来实现。

$ cscope -Rbq

上述命令会生成3个文件——cscope.cout、cscope.in.out和cscope.po.out。其中cscope.out是基本的索引,后面两个文件是使用“-q”选项生成的,用于加快cscope索引的速度。

在Vim中使用cscope工具非常简单,可首先调用“cscope add”命令添加cscope数据库,然后调用“cscope find”命令进行查找。Vim支持cscope的8种查询功能。

为了方便使用,我们可以在.vimrc配置文件中添加如下快捷键。

"-----------------------------------------------------------
" cscope:建立数据库:cscope -Rbq;  F5键查找C语言符号;F6键查找指定的字符串;  
F7键查找哪些函数调用了本函数
"-----------------------------------------------------------
if has("cscope")
  set csprg=/usr/bin/cscope
  set csto=1
  set cst
  set nocsverb
  " add any database in current directory
  if filereadable("cscope.out")
      cs add cscope.out
  endif
  set csverb
endif

:set cscopequickfix=s-,c-,d-,i-,t-,e-

"nmap <C-_>s :cs find s <C-R>=expand("<cword>")<CR><CR>
nmap <silent> <F5> :cs find s <C-R>=expand("<cword>")<CR><CR> 
nmap <silent> <F6> :cs find t <C-R>=expand("<cword>")<CR><CR>
nmap <silent> <F7> :cs find c <C-R>=expand("<cword>")<CR><CR>

上述定义的快捷键如下。

4)Tagbar插件

Tagbar插件可以用源代码文件生成大纲,包括类、方法、变量以及函数名等,可以选中并快速跳转到目标位置。

为了安装Tagbar插件,可在.vimrc文件中添加如下内容。

Plugin 'majutsushi/tagbar' " Tag bar"

然后重启Vim,输入并运行命令“:PluginInstall”以完成安装。

为了配置Tagbar插件,可在.vimrc文件中添加如下内容。

" Tagbar
let g:tagbar_width=25
autocmd BufReadPost *.cpp,*.c,*.h,*.cc,*.cxx call tagbar#autoopen()

上述配置实现了在打开常见的源代码文件时自动打开Tagbar插件。

5)文件浏览插件NerdTree

NerdTree插件可以显示树状目录。

为了安装NerdTree插件,可在.vimrc文件中添加如下内容。

Plugin 'scrooloose/nerdtree'

然后重启Vim,输入并运行命令“:PluginInstall”以完成安装。

下面配置NerdTree插件:

" NetRedTree
autocmd StdinReadPre * let s:std_in=1
autocmd VimEnter * if argc() == 0 && !exists("s:std_in") | NERDTree | endif
let NERDTreeWinSize=15
let NERDTreeShowLineNumbers=1
let NERDTreeAutoCenter=1
let NERDTreeShowBookmarks=1

6)动态语法检测工具

动态语法检测工具可以在编写代码的过程中检测出语法错误,不用等到编译或运行,这个工具对代码编写者非常有用。本实验安装的是称为ALE(Asynchronization Lint Engine)的一款实时代码检测工具。ALE工具在发现错误的地方会实时提醒,在Vim的侧边会标注哪一行有错误,将光标移动到这一行时会显示错误的原因。ALE工具支持多种语言的代码分析器,比如C语言可以支持gcc、clang等。

为了安装ALE工具,可在.vimrc文件中添加如下内容。

Plugin 'w0rp/ale'

然后重启Vim,输入并运行命令“:PluginInstall”以完成安装。在这个过程中需要从网络上下载代码。

插件安装完之后,做一些简单的配置,在.vimrc文件中添加如下配置。

let g:ale_sign_column_always = 1
let g:ale_sign_error = '✗'
let g:ale_sign_warning = 'w'
let g:ale_statusline_format = ['✗ %d', '⚡ %d', '✔ OK']
let g:ale_echo_msg_format = '[%linter%] %code: %%s'
let g:ale_lint_on_text_changed = 'normal'
let g:ale_lint_on_insert_leave = 1
let g:ale_c_gcc_options = '-Wall -O2 -std=c99'
let g:ale_cpp_gcc_options = '-Wall -O2 -std=c++14'
let g:ale_c_cppcheck_options = ''
let g:ale_cpp_cppcheck_options = ''

使用ALE工具编写一个简单的C程序,如图2.4所示。

图2.4 使用ALE工具编写的C程序

Vim的左边会显示错误或警告,其中“w”表示警告,“x”表示错误。如图2.4所示,第3行出现了警告,这是因为gcc编译器发现变量i虽然定义了但没有使用。

7)自动补全插件YouCompleteMe

代码补全功能在Vim的发展历程中是一项比较弱的功能,因此一直被使用Source Insight的人诟病。早些年出现的自动补全插件(如AutoComplPop、Omnicppcomplete、Neocomplcache等)在效率上低得惊人,特别是在把整个Linux内核代码添加到项目中时,要使用这些代码补全功能,每次都要等待一两分钟的时间,简直让人抓狂。

YouCompleteMe是最近几年才出现的新插件,该插件利用clang来为C/C++代码提供代码提示和补全功能。借助clang的强大功能,YouCompleteMe的补全效率和准确性极高,可以和Source Insight一比高下。因此,Linux开发人员在为Vim配备了YouCompleteMe插件之后,完全可以抛弃Source Insight。

在安装YouCompleteMe插件之前,需要保证Vim的版本必须高于7.4.1578,并且支持Python 2或Python 3。Ubuntu Linux 20.04版本中的Vim满足以上要求,使用其他发行版的读者可以用如下命令进行检查。

$ vim –version

为了安装YouCompleteMe插件,可在.vimrc文件中添加如下内容。

Plugin 'Valloric/YouCompleteMe'

然后重启Vim,输入并运行命令“:PluginInstall”以完成安装。在这个过程中由于要从网络上下载代码,因此需要等待一段时间。

插件安装完之后,需要重新编译,所以在编译之前需要保证已经安装如下软件包。

$ sudo apt-get install build-essential cmake python3-dev

检查系统中的Python版本是否为Python 3。

rlk@ubuntu:~$ python
Python 3.8.2 (default, Mar 13 2020, 10:14:16) 
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

若默认安装的不是Python 3,可以通过update-alternatives--install命令来设置。

$ sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 1

$ sudo update-alternatives --install /usr/bin/python python /usr/bin/python2 2

再使用update-alternatives--config python命令来选择。

rlk@ubuntu:~$ sudo update-alternatives --config python
There are 2 choices for the alternative python (providing /usr/bin/python).

  Selection    Path              Priority   Status
------------------------------------------------------------
* 0            /usr/bin/python2   2         auto mode
  1            /usr/bin/python2   2         manual mode
  2            /usr/bin/python3   1         manual mode

Press <enter> to keep the current choice[*], or type selection number:

接下来对YouCompleteMe插件代码进行编译。

$ cd ~/.vim/bundle/YouCompleteMe
$ python3 install.py --clang-completer

--clang-completer表示对C/C++提供支持。

编译完之后,还需要做一些配置工作,把~/.vim/bundle/YouCompleteMe/third_party/ ycmd/examples/.ycm_extra_conf.py文件复制到~/.vim目录中。

$ cp 
~/.vim/bundle/YouCompleteMe/third_party/ycmd/examples/.ycm_extra_conf.py  
~/.vim

在.vimrc配置文件中还需要添加如下配置。

let g:ycm_server_python_interpreter='/usr/bin/python'
let g:ycm_global_ycm_extra_conf='~/.vim/.ycm_extra_conf.py'

这样就完成了YouCompleteMe插件的安装和配置。

下面做一下简单测试。首先启动Vim,输入“#include <stdio>”以检查是否会出现补全提示,如图2.5所示。

图2.5 代码补全测试

8)自动索引

旧版本的Vim是不支持异步模式的,因此每次写一部分代码都需要手动运行ctags命令来生成索引,这是Vim的一大痛点。这个问题在Vim 8之后得到了改善。下面推荐一个可以异步生成tags索引的插件,这个插件名为vim-gutentags。

安装vim-gutentags插件的命令如下。

Plugin 'ludovicchabant/vim-gutentags'

重启Vim,输入命令“:PluginInstall”以完成安装,在这个过程中需要从网络上下载代码。

对插件进行一些简单的配置,将以下内容添加到.vimrc配置文件中。

" 搜索项目目录的标志,碰到这些文件/目录名就停止向上一级目录递归
let g:gutentags_project_root = ['.root', '.svn', '.git', '.hg', '.project']

" 配置 ctags 的参数
let g:gutentags_ctags_extra_args = ['--fields=+niazS', '--extra=+q']
let g:gutentags_ctags_extra_args += ['--c++-kinds=+px']
let g:gutentags_ctags_extra_args += ['--c-kinds=+px']

当我们修改一个文件时,vim-gutentags会在后台默默帮助我们更新tags数据索引库。

9).vimrc中的其他一些配置

.vimrc中还有一些其他常用的配置,如显示行号等。

set nu!             " 显示行号

syntax enable
syntax on
colorscheme desert

:set autowrite   " 自动保存

10)使用Vim来阅读Linux内核源代码

我们已经把Vim打造成一个足以媲美Source Insight的IDE工具了。下面介绍如何阅读Linux内核源代码。

下载Linux内核官方的源代码或者本书提供的源代码。

git clone https://e.coding.net/benshushu/runninglinuxkernel_5.0.git

Linux内核已经支持使用ctags和cscope来生成索引文件,而且会根据编译的config文件选择需要扫描的文件。下面使用make 命令来生成ctags和cscope。

$ export ARCH=arm64
$ export CROSS_COMPILE=aarch64-linux-gnueabi-
$ make rlk_defconfig
$ make tags cscope TAGS  //生成tags,cscope, TAGS等索引文件

启动Vim,通过“:e mm/memory.c”命令打开memory.c源文件,然后在do_anonymous_page()函数的第2563行中输入“vma->”,Vim中将自动出现struct vm_area_struct数据结构的成员供你选择,而且速度快得惊人,如图2.6所示。

图2.6 在Linux内核代码中尝试代码补全

另外,我们在do_anonymous_page()函数的第2605行中的page_add_new_anon_rmap()位置按F7键,就能很快查找到Linux内核中所有调用page_add_new_anon_rmap()函数的地方,如图2.7所示。

图2.7 查找哪些函数调用了page_add_new_anon_rmap()

1.实验目的

学会如何快速创建一个git本地仓库,并将它运用到实际工作中。

2.实验详解

我们通常在实际项目中会使用一台独立的机器作为git服务器,然后在git服务器中建立一个远程仓库,这样项目中所有的人都可以通过局域网来访问这台git服务器。当然,我们在本实验中可以使用同一台机器来模拟git服务器。

1)git服务器端的操作

首先需要在git服务器端建立一个目录,然后初始化这个git仓库。假设我们是在“/opt/git/”目录下进行创建。

$ cd /opt/git/
$ mkdir test.git
$ cd test.git/
$ git --bare init
Initialized empty Git repository in /opt/git/test.git/

我们通过git --bare init命令创建了一个空的远程仓库。

2)客户端的操作

打开另外一个终端,然后在本地工作目录中编辑代码,比如在home目录中。

$ cd /home/ben/
$ mkdir test

编辑test.c文件,添加用于简单地输出“hello world”的语句。

$ vim test.c

初始化本地的git仓库。

$ git init 
Initialized empty Git repository in /home/figo/work/test/.git/

查看当前工作区的状态。

$ git status 
On branch master

Initial commit

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    test.c

nothing added to commit but untracked files present (use "git add" to track)

可以看到工作区里有test.c文件,通过git add命令添加test.c文件到缓存区中。

$ git add test.c

用git commit提交新的修改记录。

$ git commit –s

test: add init code for xxx project

Signed-off-by: Ben Shushu <runninglinuxkernel@126.com>

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
#
# Initial commit
#
# Changes to be committed:
#       new file:   test.c
#

上述代码中添加了对这个修改记录的描述,保存之后将自动生成另一个新的修改记录。

$ git commit -s
[master (root-commit) ea92c29] test: add init code for xxx project
 1 file changed, 8 insertions(+)
 create mode 100644 test.c

接下来需要把本地的git仓库推送到远程仓库中。

使用git remote add命令添加刚才那个远程仓库的地址。

$ git remote add origin ssh://ben@192.168.0.1:/opt/git/test.git

其中“192.168.0.1”是服务器端的IP地址,“ben”是服务器端的登录名。

最后使用git push命令进行推送。

$ git push origin master
figo@192.168.0.1's password: 
Counting objects: 3, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 320 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To ssh://figo@10.239.76.39:/opt/git/test.git
 * [new branch]      master -> master

3)复制远程仓库

这时我们就可以在局域网内通过git clone复制这个远程仓库到本地了。

$ git clone ssh://ben@192.168.0.1:/opt/git/test.git
Cloning into 'test'...
ben@192.168.0.1's password: 
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (3/3), done.
Checking connectivity... done.
$ cd test/
$ git log
commit ea92c29d88ba9e58960ec13911616f2c2068b3e6
Author: Ben Shushu <runninglinuxkernel@126.com>
Date:   Mon Apr 16 23:13:32 2018 +0800

    test: add init code for xxx project

    Signed-off-by: Ben Shushu <runninglinuxkernel@126.com>

1.实验目的

了解和学会如何解决合并分支时遇到的冲突。

2.实验详解

首先,创建发生分支合并冲突的环境,步骤如下。

(1)创建一个本地分支。

$ git init

(2)在master分支上新建test.c文件。输入简单的“hello world”程序,然后生成一个修改记录。

#include <stdio.h>

int main()
{
       int i;

       printf("hello word\n");

       return 0;
}

(3)基于master分支创建dev分支。

$ git checkout –b dev

(4)在dev分支上做如下改动,并生成另一个修改记录。

diff --git a/test.c b/test.c
index 39ee70f..ed431cc 100644
--- a/test.c
+++ b/test.c
@@ -2,7 +2,10 @@

 int main()
 {
-       int i;
+       int i = 10;
+       char *buf;
+
+       buf = malloc(100);

        printf("hello word\n");

(5)切换到主分支,然后继续修改test.c文件,再次生成一个修改记录。

diff --git a/test.c b/test.c
index 39ee70f..e0ccfb9 100644
--- a/test.c
+++ b/test.c
@@ -3,6 +3,7 @@
 int main()
 {
        int i;
+       int j = 5;

        printf("hello word\n");

(6)这样我们的实验环境就搭建好了。在这个git仓库里有两个分支,一个是master分支,另一个是dev分支,它们同时修改了相同的文件,如图2.8所示。

图2.8 主分支和dev分支

(7)使用如下命令把dev分支上的提交合并到master分支,如果遇到了冲突,请解决。

$ git branch      //先确认当前分支是master分支
$ git merge dev     //把dev分支合并到master分支

下面简单介绍一下如何解决分支合并冲突。当合并分支遇到冲突时会显示如下提示,其中明确告诉了我们是在合并哪个文件时发生了冲突。

$ git merge dev
Auto-merging test.c
CONFLICT (content): Merge conflict in test.c
Automatic merge failed; fix conflicts and then commit the result.

接下来要做的工作就是手动修改冲突了。打开test.c文件,你会看到“<<<<<<<”和“>>>>>>>”符号包括的区域就是发生冲突的地方。至于如何修改冲突,git工具是没有办法做判断的,只能读者自己判断,前提条件是要对代码有深刻的理解。

#include <stdio.h>

int main()
{
<<<<<<< HEAD
        int i;
        int j = 5;
=======
        int i = 10;
        char *buf;

        buf = malloc(100);
>>>>>>> dev

        printf("hello word\n");

        return 0;
}

冲突修改完之后,可以通过git add命令把test.c文件添加到git仓库中。

$git add test.c

然后使用git merge--continue命令继续合并工作,直到合并完成为止。

$ git merge --continue
[master 9ad3b85] Merge branch 'dev'

读者可以重复以上实验步骤,重建一个本地git仓库,使用变基命令合并dev分支到master分支,遇到冲突时请尝试解决。

1.实验目的

通过模拟一个项目的实际操作来演示如何利用git进行Linux内核的开发和管理。该项目的需求如下。

(1)该项目需要基于Linux 4.0内核进行二次开发。

(2)在本地建立一个名为“ben-linux-test”的git仓库,上传的内容要包含Linux 4.0中所有提交的信息。

2.实验详解

首先,参考实验2-6,在本地建立一个空的名为“ben-linux-test”的git仓库。

然后,下载Linux官方的仓库代码。

接下来要做的工作就是在这个本地的git仓库里下载Linux 4.0的官方代码,那么应该怎么做呢?首先我们需要添加Linux官方的git仓库。这里可以使用git remote add命令来添加一个远程仓库的地址,如下所示。

$ git remote add linux https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git

使用git remote -v命令把Linux内核官方的远程仓库添加到本地,并且使用别名linux。

$ git remote -v
linux    https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git (fetch)
linux    https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git (push)
origin    https://github.com/figozhang/ben-linux-test.git (fetch)
origin    https://github.com/figozhang/ben-linux-test.git (push)

使用git fetch命令把新添加的远程仓库下载到本地。

$ git fetch linux
remote: Counting objects: 6000860, done.
remote: Compressing objects: 100% (912432/912432), done.
Rceiving objects:   1% (76970/6000860), 37.25 MiB | 694.00 KiB/s

下载完成后,使用git branch -a 命令查看分支情况。

$ git branch -a
* master
  remotes/linux/master
  remotes/origin/master

可以看到远程仓库有两个。一个是我们刚才在本地创建的仓库(remotes/origin/master),另一个是Linux内核官方的远程仓库(remotes/linux/master)。

为了把官方仓库中含Linux 4.0标签的所有提交添加到本地的master分支,首先需要从remotes/linux/master分支中检查名为linux-4.0的本地分支。

$ git checkout -b linux-4.0 linux/master
Checking out files: 100% (61345/61345), done.
Branch linux-4.0 set up to track remote branch master from linux.
Switched to a new branch 'linux-4.0'

$ git branch -a
* linux-4.0
  master
  remotes/linux/master
  remotes/origin/master

因为项目需要在Linux 4.0中完成,所以把linux-4.0分支重新放到Linux 4.0标签上,这时可以使用git reset命令。

$ git reset v4.0 --hard
Checking out files: 100% (61074/61074), done.
HEAD is now at 39a8804 Linux 4.0

这样本地linux-4.0分支将真正基于Linux 4.0内核,并且包含Linux 4.0中所有提交的信息。

接下来要做的工作就是把本地linux-4.0分支中提交的信息都合并到本地的master分支。

首先,需要切换到本地的master分支。

$ git checkout master

然后,使用git merge命令把本地linux-4.0分支中所有提交的信息都合并到master分支。

$ git merge linux-4.0 --allow-unrelated-histories

以上合并操作会生成名为merge branch的提交消息,如下所示。

merge branch 'linux-4.0'

# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.

最后,本地master分支中提交的信息将变成下面这样。

$ git log --oneline
c67cf17 Merge branch 'linux-4.0'
f85279c first commit
39a8804 Linux 4.0
6a23b45 Merge branch 'for-linus' of git://git.kernel.org/pub/scm/linux/kernel/git/viro/vfs
54d8ccc Merge branch 'fixes' of git://git.kernel.org/pub/scm/linux/kernel/git/evalenti/linux-soc-thermal
56fd85b Merge tag 'asoc-fix-v4.0-rc7' of git://git.kernel.org/pub/scm/linux/kernel/git/broonie/sound
14f0413c ASoC: pcm512x: Remove hardcoding of pll-lock to GPIO4

这样本地master分支就包含了Linux 4.0内核的所有git log信息。最后,只需要把这个master分支推送到远程仓库即可。

$ git push origin master

现在远程仓库的master分支已经包含Linux 4.0内核的所有提交了,在此基础上可以建立属于该项目自己的分支,比如dev-linux-4.0分支、feature_a_v0分支等。

$ git branch -a
  dev-linux-4.0
* feature_a_v0
  master
  remotes/linux/master
  remotes/origin/master

1.实验目的

(1)在Linux 4.0上做开发。为了简化开发,我们假设只需要修改Linux 4.0根目录下面的Makefile,如下所示。

VERSION = 4
PATCHLEVEL = 0
SUBLEVEL = 0
EXTRAVERSION =
NAME = Hurr durr I'ma sheep //修改这里,改成 benshushu

(2)把修改推送到本地仓库。

(3)过了几个月,这个项目需要变基(rebase)到Linux 4.15内核,并且把之前做的工作也变基到Linux 4.15内核,同时更新到本地仓库中。如果变基时遇到冲突,那么需要进行修复。

(4)合并一个分支以及变基到最新的主分支。

(5)在合并分支和变基分支的过程中,修复冲突。

2.实验详解

在实际项目开发过程中,分支的管理是很重要的。以现在这个项目为例,项目开始时,我们会选择一个内核版本进行开发,比如选择Linux 4.0内核。等到项目开发到一定的阶段,比如Beta阶段,需求发生变化。这时需要基于最新的内核进行开发,如基于Linux 4.15内核。因此就要把开发工作变基到Linux 4.15了。这种情形在实际开源项目中是很常见的。

因此,分支管理显得很重要。master分支通常是用来与开源项目同步的,dev分支是我们平常开发用的分支。另外,每个开发人员在本地可以建立属于自己的分支,如feature_a_v0分支,表示开发者在本地创建的用来开发feature a的分支,版本是v0。

$ git branch -a
* dev-linux-4.0
  feature_a_v0
  master
  remotes/linux/master
  remotes/origin/master
remotes/origin/dev-linux-4.0

1)把开发工作推送到dev-linux-4.0分支

下面基于dev-linux-4.0分支进行工作,比如这里要求修改Makefile,然后生成一个修改记录并且将它推送到dev-linux-4.0分支。

首先,修改Makefile。修改后的内容如下。

diff --git a/Makefile b/Makefile
index fbd43bf..2c48222 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,7 @@ VERSION = 4
 PATCHLEVEL = 0
 SUBLEVEL = 0
 EXTRAVERSION =
-NAME = Hurr durr I'ma sheep
+NAME = benshushu

 # *DOCUMENTATION*
 # To see a list of typical targets execute "make help"
@@ -1598,3 +1598,5 @@ FORCE:
 # Declare the contents of the .PHONY variable as phony.  We keep that
 # information in a variable so we can use it in if_changed and friends.
 .PHONY: $(PHONY)
+
+#demo for rebase by benshush //在最后一行添加,为了将来变基制造冲突

然后,生成一个修改记录。

$ git add Makefile
$ git commit –s

    demo: modify Makefile

    modify Makefile for demo

    v1: do it base on linux-4.0

最后,把上述修改推送到远程仓库。

$ git push origin dev-linux-4.0
Counting objects: 3, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 341 bytes | 0 bytes/s, done.
Total 3 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
remote: Checking connectivity: 3, done.
   c67cf17..f35ab68  dev-linux-4.0 -> dev-linux-4.0

2)新建dev-linux-4.15分支

首先,从远程仓库(remotes/linux/master)分支新建一个名为linux-4.15-org的分支。

$ git checkout -b linux-4.15-org linux/master

然后,把linux-4.15-org分支重新放到 v4.15标签上。

$ git reset v4.15 --hard
Checking out files: 100% (21363/21363), done.
HEAD is now at d8a5b80 Linux 4.15

接着,切换到master分支。

$ git checkout master 
Checking out files: 100% (57663/57663), done.
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.

接下来,把linux-4.15-org分支中的所有信息都合并到master分支。

figo@figo:~ben-linux-test$ git merge linux-4.15-org

合并完之后,查看master分支的日志信息,如下所示。

figo@figo ~ben-linux-test$ git log --oneline 
749d619 Merge branch 'linux-4.15-org'
c67cf17 Merge branch 'linux-4.0'
f85279c first commit
d8a5b80 Linux 4.15

最后,把主分支的更新推送到远程仓库,这样远程仓库中的master分支便基于Linux 4.15内核了。

figo@figo:~ben-linux-test$ git push origin master

3)变基到Linux 4.15上

首先,基于dev-linux-4.0分支创建dev-linux-4.15分支。

figo@figo:~ben-linux-test$ git checkout dev-linux-4.0
figo@figo:~ben-linux-test$ git checkout -b dev-linux-4.15

因为我们已经把远程仓库中的master分支更新到Linux 4.15,所以接下来把master分支中的所有信息都变基到dev-linux-4.15分支。在这个过程中可能有冲突发生。

$ git rebase master 
First, rewinding head to replay your work on top of it...
Applying: demo: modify Makefile
Using index info to reconstruct a base tree...
M Makefile
Falling back to patching base and 3-way merge...
Auto-merging Makefile
CONFLICT (content): Merge conflict in Makefile
error: Failed to merge in the changes.
Patch failed at 0001 demo: modify Makefile
The copy of the patch that failed is found in: .git/rebase-apply/patch

When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".

这里显示在合并“demo: modify Makefile”这个补丁时发生了冲突,并且告知我们发生冲突的文件是Makefile。接下来,可以手动修改Makefile文件并处理冲突。

# SPDX-License-Identifier: GPL-2.0
VERSION = 4
PATCHLEVEL = 15
SUBLEVEL = 0
EXTRAVERSION =
<<<<<<< 749d619c8c85ab54387669ea206cddbaf01d0772
NAME = Fearless Coyote
=======
NAME = benshushu
>>>>>>> demo: modify Makefile

手动修改冲突之后,可以通过git diff命令看一下变化。通过git add命令添加修改的文件,然后通过git rebase --continue命令继续做变基处理。当后续遇到冲突时还会停下来,手动修改冲突,并继续通过git add来添加修改后的文件,直到所有冲突被修改完。

$ git add Makefile 
$ git rebase --continue 
Applying: demo: modify Makefile

变基完成之后,我们可通过git log --oneline命令查看dev-linux-4.15分支的状况。

figo@figo:~ben-linux-test$ git log --oneline
344e37a demo: modify Makefile
749d619 Merge branch 'linux-4.15-org'
c67cf17 Merge branch 'linux-4.0'
f85279c first commit
d8a5b80 Linux 4.15

最后,我们把dev-linux-4.15分支推送到远程仓库来完成这个项目。

figo@figo:~ben-linux-test$ git push origin dev-linux-4.15

4)合并和变基分支的区别

本实验使用merge和rebase来合并分支,有些读者可能感到有些迷惑。

$ git merge master
$ git rebase master

上述两个命令都用于将主分支合并到当前分支,结果有什么不同呢?

假设一个git仓库里有一个master分支,还有一个dev分支,如图2.9所示。

图2.9 执行合并分支之前

每个节点的提交顺序如表2.8所示。

表2.8 节点的提交顺序

节  点

提 交 顺 序

A

1号

B

2号

C

3号

D

4号

E

5号

F

6号

G

7号

在执行git merge master命令之后,dev分支变成图2.10所示的结果。

图2.10 执行git merge master合并之后的结果

我们可以看到,在执行git merge master命令之后,dev分支中的提交都是基于时间轴来合并的。

执行git rebase master命令之后,dev分支变成图2.11所示的结果。

图2.11 执行git rebase master合并之后的结果

git rebase命令用来改变一串提交基于的分支,如git rebase master表示dev分支的DFG这3个节点的提交都基于最新的master分支,也就是基于E节点的提交。git rebase命令的常见用途是保持正在开发的分支(如dev分支)相对于另一个分支(如主分支)是最新的。

merge和rebase命令都用来合并分支,那么分别应该在什么时候使用呢?


相关图书

Linux常用命令自学手册
Linux常用命令自学手册
庖丁解牛Linux操作系统分析
庖丁解牛Linux操作系统分析
Linux后端开发工程实践
Linux后端开发工程实践
轻松学Linux:从Manjaro到Arch Linux
轻松学Linux:从Manjaro到Arch Linux
Linux高性能网络详解:从DPDK、RDMA到XDP
Linux高性能网络详解:从DPDK、RDMA到XDP
跟老韩学Linux架构(基础篇)
跟老韩学Linux架构(基础篇)

相关文章

相关课程