用“芯”探核 基于龙芯的Linux内核探索解析

978-7-115-44492-9
作者: 陈华才
译者:
编辑: 俞彬

图书目录:

详情

这是一本基于龙芯平台,结合Linux-5.4.x 版本的内核源代码来解析Linux 内核的书籍。本书首先介绍了有关龙芯处理器和Linux 内核的基础知识,然后重点讲解了内核启动、异常与中断、内存管理、进程管理、显卡驱动、网卡驱动和电源管理这7 大板块的内容。本书甚少涉及代码的细枝末节,而是重点关注代码实现的主干流程,并且创造性地引入了树形视图和链式视图这两种比流程图更好用的代码解析方法。 本书适合Linux 系统相关的开发人员,特别是基于龙芯处理器做内核开发的技术人员学习参考。

图书摘要

“十三五”国家重点图书出版规划项目

龙芯中科 中国自主产权芯片技术与应用丛书

国家出版基金项目

用“芯”探核:基于龙芯的Linux内核探索解析

陈华才 著

人民邮电出版社

北京

图书在版编目(CIP)数据

用“芯”探核:基于龙芯的Linux内核探索解析 / 陈华才著. --北京:人民邮电出版社,2020.8

(中国自主产权芯片技术与应用丛书)

ISBN 978-7-115-44492-9

Ⅰ.①用… Ⅱ.①陈… Ⅲ.①Linux操作系统 Ⅳ.①TP316.85

中国版本图书馆CIP数据核字(2020)第023209号

◆著   陈华才

责任编辑 俞彬

责任印制 王郁 马振武

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

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

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

北京市艺辉印刷有限公司印刷

◆ 开本:787×1092 1/16

印张:36.75  彩插:4

字数:920千字  2020年8月第1版

印数:1–2500册  2020年8月北京第1次印刷

定价:118.00元

读者服务热线:(010)81055410 印装质量热线:(010)81055316

反盗版热线:(010)81055315

广告经营许可证:京东市监广登字20170147号

内容提要

这是一本基于龙芯平台,结合Linux-5.4.x版本的内核源代码来解析Linux内核的书籍。本书首先介绍了有关龙芯处理器和Linux内核的基础知识,然后重点讲解了内核启动、异常与中断、内存管理、进程管理、显卡驱动、网卡驱动和电源管理这7大板块的内容。本书甚少涉及代码的细枝末节,而是重点关注代码实现的主干流程,并且创造性地引入了树形视图和链式视图这两种比流程图更好用的代码解析方法。

本书适合Linux系统相关的开发人员,特别是基于龙芯处理器做内核开发的技术人员学习参考。

专家推荐

Linux操作系统内核结构复杂,硬件关联性强,开发难度较大。本书作者陈华才博士从龙芯CPU上运行的内核入手,深入浅出地分析了内核各个重要模块,并讲解了大量关联知识。他还分享了在内核代码中融会贯通、化繁为简的经验技巧,降低了开发者入门的门槛。相信本书能为从事国产自主创新网信生态构建的广大科技人员提供重要的、切合实际的帮助。

倪光南/中国工程院院士

Linux内核开发是一件门槛比较高的事情,而国产CPU上的内核开发者还面临一个额外的困难:缺乏合适的参考书籍。这本基于龙芯平台的内核开发书籍的出版,恰逢其时。作者长期从事龙芯内核研究和实践,是官方Linux内核中龙芯支持代码的主要贡献者之一。扎实的理论功底和丰富的实践经验使得他能够在有限的篇幅里覆盖龙芯内核开发实践中必须掌握的内容和常见的难点。强烈推荐!

张福新/中科院计算所研究员、龙芯中科技术有限公司技术总监

此书不是针对Linux内核的全面描述,而是紧密结合龙芯CPU和Linux内核5.4版本来讲述操作系统内核知识,它很好地平衡了内容的广度和深度,并且兼具知识性与实用性,偶尔提到的方法论等,都体现出了作者在Linux内核方面深厚的知识积淀和丰富的实战经验。此书既适合作为学习Linux内核的初级教材,也适合作为掌握龙芯CPU配套Linux内核的参考书,并同样适合想要学习Linux内核知识的其他读者使用。非常高兴能看到这样一本定位清晰、特色鲜明的Linux内核书籍出版!

韩乃平/麒麟软件有限公司执行总裁

经过20多年的发展,Linux操作系统已经成为全球操作系统中最重要的生态之一;龙芯作为国产CPU的优秀代表,Linux操作系统也是其最主要的应用运行环境。随着国内信息产业中龙芯CPU和Linux操作系统的不断广泛应用,在相关体系架构、Linux内核技术方面急需大量的高水平人才。但目前市面上关于Linux内核的书籍并不丰富,涉及龙芯的Linux内核资料更是非常缺乏。

本书由长期从事龙芯平台下Linux内核研发的技术专家,也是Linux内核MIPS架构的重要贡献者亲自撰写,内容覆盖全面,讲解由浅入深。我认为,这是每一位有志于从事Linux内核研发,或者希望参与龙芯生态建设的技术人员都应读的书籍。

刘闻欢/统信软件技术有限公司总经理

首先,本书基于龙芯平台,不仅从系统开发角度展示了国产龙芯,也基于MIPS架构讲解了Linux内核开发。其次,作者来自龙芯产业化基地,工作涉猎芯片、系统和应用,讲解视野非常全面。再者,作者长期活跃于Linux内核社区,是国内少数长期坚持在一线的工程师。所以本书不仅剖析了内核启动、异常处理、进程调度、内存管理等常规知识,也讲解了显卡驱动、网卡驱动、电源管理等新技术。另外,龙芯已开放全系Qemu模拟器,读者可以通过Linux Lab边学边练,为以后实际参与龙芯平台开发打下坚实基础。

吴章金/Linux Lab开源项目作者、泰晓科技创始人、前魅族Linux部门技术总监

序1

我很高兴看到这本基于龙芯的Linux内核书籍面世。随着龙芯生态不断发展壮大,口口相传的作坊模式已经不能满足相关产业对龙芯人才的需求,我们急需更多优秀开发人员把他们的经验凝聚成册,以服务日益庞大的开发者队伍。为此,近年来龙芯公司组织了一系列的书籍编写,逐步覆盖了龙芯体系结构设计、龙芯桌面及服务器使用/管理/应用开发、操作系统开发等内容。这本书将进一步丰富龙芯专业技术书籍的覆盖范围。

本书的作者陈华才是龙芯内核的主力开发人员之一。他十年如一日地投身于龙芯内核开发,解决了不计其数的问题,多次帮助龙芯CPU团队定位隐藏很深的芯片设计疑难问题,为龙芯发展做出了重要贡献。内核是操作系统乃至整个生态的核心支撑部件,它的质量和性能直接影响用户体验。同时,内核开发也是公认难度很高的技术活,以至于在市场上精通Linux内核开发的技术人员极度稀缺。像这样一本由资深开发人员撰写的书尤为难能可贵。

这本书和市面上已有的Linux内核书籍相比,最大的特点是紧密结合实践,贴近产业需求。书中涉及的电脑体系结构以及源代码都已经被实际应用在批量推广的龙芯产品上,读者容易找到相应的开发环境。在解析内核启动流程和各关键模块时,既有必要的原理解释,又重点突出了龙芯的具体实现,方便读者把原理和实际代码联系起来。例如,在解析Cache初始化代码的时候,它介绍了比较特殊的龙芯V-Cache设计,也说明了龙芯处理器用户手册上的相关名词对应内核代码的哪些内容。讲到异常与中断处理的时候,它就会交代清楚龙芯的中断和异常架构,以及和代码对应的中断路由等重要概念。更重要的是,由于作者对内核有很深刻的认识,他总是能够用简洁的语言交代清楚模块之间的关系和一段代码背后的本质内容,而不是让读者陷入太多的细节之中。对龙芯相关产业公司的内核技术人员来说,本书会显得非常贴心,在读者容易疑惑的地方会有详细解释,在简单的地方就不会赘述。

我期待本书能够帮助更多人掌握龙芯内核开发的技术,鼓励更多人加入龙芯生态建设的队伍。

胡伟武/龙芯中科技术有限公司董事长

序2

经过多年的发展,开源软件已经逐渐深入人心并在各个方面大放异彩,尤其是以Linux内核为代表的开源基础软件。如今,Top500超级计算机几乎全部使用Linux,以Redhat、SuSE和Oracle为代表的企业版Linux已经占领了各种大中型服务器;以Ubuntu、Deepin为代表的桌面版Linux逐渐与Windows分庭抗礼;而移动设备更是基于Linux的Android系统的天下。

源于其庞大的体量,中国事实上已经拥有最大规模的Linux用户。然而,谈到对Linux操作系统尤其是Linux内核的贡献,中国却长期以来处于一种缺位的状态。这种缺位不仅体现在源代码开发的直接贡献上,也体现在有关Linux内核原创书籍资料的匮乏上。不过,令人可喜的是一切都在往好的方面发展。自从章文嵩、吴峰光等人带头贡献社区开始,近年来Linux内核里的中国开发者越来越多;而在书籍资料方面,这几年也有张天飞、余华兵等剖析Linux内核的原创新书出现。

龙芯CPU诞生于中科院计算所,是基于MIPS体系结构的一系列国产CPU的总称,具体包括龙芯1号、龙芯2号、龙芯3号三个子系列十多种型号。本书作者陈华才多年来参与龙芯版Linux内核的开发与维护,与上下游CPU厂商、操作系统厂商形成了良好的互动与反馈闭环。在他和他带领的团队的努力下,龙芯CPU得以快速成熟和产品化。不仅如此,他还在坚持与时俱进、紧跟上游的基础上,向内核社区贡献了大量的源代码。

授人以鱼不如授人以渔。信息产业的国产化任重而道远,绝不是少数几个“天才”人物就能解决的问题。因此,提高自身的研发水平固然重要,但更重要的是打通生态链的各个环节,让更多的人参与进来一起建设。陈华才博士在工作之余,倾注了大量的心血将自己的知识和见解提炼出来,完成了这本基于龙芯CPU的内核书籍。更加难能可贵的是,这本书基于最新的5.x内核,让读者不仅能够深刻掌握龙芯的机理,而且能够了解内核社区的最新动态。

《用“芯”探核:基于龙芯的Linux内核探索解析》是第一本基于国产CPU的内核书籍。借此机会,期待我们的计算机国产化事业蒸蒸日上。

金海/CCF(中国计算机学会)副理事长,华中科技大学教授

前言

本书是一本基于龙芯平台,结合源代码来探索和解析Linux内核的书。

为什么写作本书

市面上解析Linux内核的经典书籍已有不少,国内原创的有《Linux内核完全注释》《Linux内核源代码情景分析》《边学边干Linux内核指导》;从国外引进的有《Linux内核设计与实现》(Linux Kernel Development,简称LKD)、《Linux设备驱动程序》(Linux Device Drivers,简称LDD)、《深入理解Linux内核》(Understanding the Linux Kernel,简称ULK)和《深入Linux内核架构》(Professional Linux Kernel Architecture)。其中LKD、LDD、ULK和《Linux内核源代码情景分析》这4本经典书籍曾经被称为Linux内核领域的“四库全书”。那么,为什么还要写作本书呢?

一方面,大多数已有书籍是基于X86或者ARM体系结构的,而本书基于MIPS家族的龙芯处理器平台;另一方面,大多数已有书籍基于2.4版本或者2.6版本的Linux内核,而本书基于新的5.x版本的Linux内核[1]

其实,新书也是有的,尤其难能可贵的是还有原创新书。比如,2017年问世了一本由张天飞(网名“笨叔叔”)编写的《奔跑吧 Linux内核》。目前这本书得到了读者的广泛好评,可以说是第一本(小声地说一句,其实本书开始撰写的时间比它更早)基于4.x版本的内核书籍。2019年又出版了一本由余华兵(我的校友)编写的《Linux内核深度解析》,它也是基于4.x版本的内核。不过,这两本书专注于ARM/ARM64架构,主要篇幅在内存管理、进程管理和中断管理上面,不涉及设备驱动(但《Linux内核深度解析》涉及文件系统);而本书专注于MIPS/龙芯架构,基于目前最新的5.x版本的内核(以5.4版本为主),而且对内核的覆盖也更为全面,除异常/中断处理、内存管理和进程管理之外还涉及设备驱动和电源管理。因此《奔跑吧 Linux内核》《Linux内核深度解析》和本书相比各有千秋。

本书的读者对象

如果你想对Linux内核的总体轮廓有个初步了解,请阅读《Linux内核设计与实现》。

如果你想接触内核源代码,又不想迷失在浩瀚的代码海洋中,请阅读《Linux内核完全注释》,这本书基于早期的0.11版本的Linux内核,虽然代码非常精简,但足以阐述各种操作系统原理。

如果你已经不再满足于“史前时代”的内核版本,想在相对现代的Linux内核上练练手脚,请阅读《边学边干Linux内核指导》。

如果你已经开始真正的内核开发,并且偏重于设备驱动的话,请阅读《Linux设备驱动程序》。

如果以上书籍已经不能满足你,那么恭喜,你已经突破了第一重境界。从现在开始,可以阅读《深入理解Linux内核》了。这本书虽然比较艰深,但却是经典中的经典。在读此书之前,你看山是山,看水是水;在阅读此书的过程中,你将看山不是山,看水不是水;最后,当你历尽千辛万苦将此书学透之后,你将看山还是山,看水还是水。

等等,怎么没有提“四库全书”中的《Linux内核源代码情景分析》一书?个人愚见,这是一本非常详细的工具书。你可以把它当字典用,但如果当成教程直接阅读,很容易迷失自我,“不识庐山真面目,只缘身在此山中”。

如果你需要4.x、5.x等新版本内核的源码解析,那么《奔跑吧 Linux内核》《Linux内核深度解析》和本书都适合你。《奔跑吧 Linux内核》主要基于4.0.0版本的Linux内核;《Linux内核深度解析》主要基于4.12.0版本的Linux内核;而本书绝大多数代码基于5.4.0版本的Linux内核。如果你基于龙芯处理器做内核开发,或者有兴趣将来从事这方面的工作,那么恭喜,本书非常适合你。

一直以来,从事龙芯内核开发工作的“标准教程”是《龙芯处理器用户手册》《MIPS体系结构透视(第2版)》和《深入理解Linux内核(第3版)》。然而,《龙芯处理器用户手册》不涉及Linux内核;《MIPS体系结构透视(第2版)》讲述的是传统的MIPS处理器,离真正的龙芯差距太大;而《深入理解Linux内核(第3版)》所使用的内核版本又过于陈旧。本书试图解决这些问题。

本书使用的Linux内核源代码的Git仓库建立在江苏航天龙梦信息技术有限公司(以下简称航天龙梦)的开发者网站上,读者可自行下载:http://dev.lemote.com:8000/cgit/linux-official.git/。

本书的内容概述

本书基于龙芯处理器和Linux内核。第1章概括性地介绍了龙芯处理器和Linux内核,同时还引入了一种快速而有效的代码阅读方法和一种开发健壮内核的方法。在此基础上,第2章将开始解析Linux内核在龙芯计算机上的启动过程。第3~5章分别介绍操作系统的3大核心功能:异常与中断处理、内存管理和进程管理。Linux是包含设备驱动的一体化内核(或称宏内核,与之相对的是微内核),第6章和第7章将以显卡驱动与网卡驱动为例来进行原理说明。电源管理是操作系统中一个相对独立但又必不可少的功能组件,第8章将专门予以介绍。

如何学习本书

本书采用了循序渐进的写作方法,非常适合按顺序进行阅读。本书的大部分内容用于解析Linux内核源代码,因此读者需要一边看书一边对照阅读代码。

第1章的“基础知识”首先会对龙芯处理器和Linux内核进行概括性的介绍。虽然本书是一本解读源代码的书,但跟别的书籍不一样:我们较多地瞄准代码的主干流程,而较少地涉及细枝末节。因此,在第1章中,我们会给读者介绍一种快速而有效的代码阅读方法,称为“先观其大略,再咬文嚼字”,并且引入了“树形视图”和“链式视图”两种比流程图更有用的代码解析方法。从某种意义上说,学习内核是为了开发内核,因此这一章还会教读者如何开发和维护健壮的内核代码。

操作系统本质上是一个大程序,顺着程序的执行流程一起前进是自然而然的事情。因此,我们认为从启动过程开始研究Linux内核是一个比较好的切入点。在掌握基础知识以后,读者可以通过第2章学习和了解Linux内核在龙芯计算机上从上电开始的整个启动过程,并以此获得一个对龙芯处理器和Linux内核的宏观印象。

异常与中断处理、内存管理和进程管理是操作系统的3大核心功能,我们就在后续的3章中分别予以介绍。这3章遵循“从基础到上层”和“广度优先深度其次”的写作原则,读者既可根据编写顺序来阅读学习,也可根据个人兴趣自行安排阅读顺序。

由于Linux是一体化内核操作系统,因此设备驱动也放在内核层实现。设备驱动所涵盖的范围非常广泛,本书不可能面面俱到,因此只选取了两种常用的典型设备驱动——Radeon显卡与E1000E网卡,来做举例性的原理说明。读者可根据自身需要进行选择性学习。

电源管理在操作系统内核中相对独立,因此与前面的章节没有太大的关联,读者掌握基础知识以后即可根据需要选择性阅读。

对自旋锁、信号量等各种并发与同步原语或者内核发展历史感兴趣的读者,可直接通过附录进行学习。

陈华才

2020年5月

[1] 最经典的《深入理解Linux内核》一共出了3版,其中第3版讲述的是2.6.11版本的Linux内核;2010年出版的《深入Linux内核架构》讲述的是2.6.24版本的Linux内核,这两本书都是基于X86平台。

第01章 基础知识

龙芯处理器是一系列基于MIPS架构,加入了大量扩展和优化的CPU。基于龙芯CPU的计算机产品使用的操作系统主要是Linux。本书用龙芯/Linux平台作为使用龙芯CPU和Linux操作系统的平台统称。本章是读者在阅读本书后续章节之前必须掌握的一些基础知识,包括对龙芯处理器和Linux内核的初步了解,对高效阅读源代码方法的熟练掌握,以及对内核社区常用的补丁文件格式的理解。为了让大家有机会成长为优秀的Linux内核开发人员,本章还介绍了开发/维护一个健壮性良好的内核应当遵循的一些原则与规范。

1.1 龙芯处理器简介

中央处理器(Central Processing Unit,CPU)分为复杂指令集计算机(Complex Instruction Set Computer,CISC)和精简指令集计算机(Reduced Instruction Set Computer,RISC)两大类。CISC具有指令集复杂而庞大、指令字不等长、寻址方式复杂、计算指令操作数可以是内存等特征,典型代表有X86。RISC具有指令集精简而高效、指令字等长、寻址方式简明、计算指令操作数必须是寄存器等特征,典型代表有ARM、MIPS和Power。CISC和RISC各有优劣,在发展过程中也并非井水不犯河水,而是互相吸收对方的优点。X86在内部早已实现RISC化(所谓微指令),而RISC也引入了单指令流多数据流(Single Instruction Multiple Data,SIMD)等功能比较强大但复杂的指令(所谓向量化)。

龙芯CPU属于无互锁流水阶段微型计算机(Microcomputer without Interlocked Pipeline Stage,MIPS)家族,是RISC精简指令集体系结构的一种,产品线包括龙芯1号(小CPU)、龙芯2号(中CPU)和龙芯3号(大CPU)3个系列。龙芯系列处理器由龙芯中科技术有限公司(以下简称龙芯中科)研发,产品以32位和64位单核及多核CPU为主,主要面向网络安全、高端嵌入式、个人电脑、服务器和高性能计算机等应用。

龙芯1号系列为32位处理器,采用GS132(单发射32位)或GS232(双发射32位)处理器核,实现了带有静态分支预测和阻塞Cache的乱序执行流水线,集成各种外围接口,形成面向特定应用的单片解决方案,主要应用于云终端、工业控制、数据采集、手持终端、网络安全、消费电子等领域。2011年推出的龙芯1A和龙芯1B具有接口功能丰富、功耗低、性价比高、应用面广等特点。除了SoC,龙芯1A还可以作为桥片(PCI南桥)使用。2013年和2014年相继推出的龙芯1C和龙芯1D分别针对指纹生物识别和超声波计量领域定制,具有成本低、功耗低、功能丰富、性能突出等特点。2015年研制的龙芯1H则针对石油钻探领域随钻测井应用设计,目标工作温度高达175℃。

龙芯2号系列处理器采用GS264(双发射64位)或GS464(四发射64位)高性能处理器核,实现了带有动态分支预测和非阻塞Cache的超标量乱序执行流水线,同时还使用浮点数据通路复用技术实现了定点的SIMD指令,集成各种外围接口,形成面向嵌入式计算机、工业控制、移动信息终端、汽车电子等应用的64位高性能低功耗SoC芯片。2006年推出的龙芯2E是最早进行产业化的处理器,其主要产品是福珑迷你计算机。2008年推出的龙芯2F经过近几年的产业化推广,目前已经实现规模应用,其产品包括福珑迷你计算机、逸珑上网本计算机以及梦珑一体机等。集成度更高的龙芯2H于2013年推出,可作为独立SoC芯片,也可作为龙芯3号的桥片使用。目标为安全、移动领域的龙芯2K1000于2017年量产。

龙芯3号系列处理器基于可伸缩的多核互联架构设计,在单个芯片上集成多个GS464(四发射64位)、GS464E(增强型四发射64位)或GS464V(带向量扩展的增强型四发射64位)高性能处理器核以及大量的2级Cache,还通过高速存储和I/O接口实现多芯片的互联以组成更大规模的高速缓存一致的非均匀内存访问(Cache-Coherent Non-Uniform Memory Access,CC-NUMA)系统。龙芯3号面向高端嵌入式计算机、桌面计算机、服务器、高性能计算机等应用。2009年底推出四核龙芯3A1000。2011年推出65 nm的八核龙芯3B1000。2012年推出采用32 nm工艺设计的性能更高的八核龙芯3B1500,其最高主频可达1.5 GHz,支持向量运算加速,最高峰值计算能力达到192 GFLOPS(Giga Floating-point Operations Per Second,每秒10亿次浮点运算)。2015年,基于GS464E的新一代龙芯3A2000研制成功,在基本功耗与龙芯3A1000相当的情况下,综合性能提升2~4倍,已于2016年实现量产。工艺升级的新一代处理器是龙芯3A3000,已于2017年推出,性能较龙芯3A2000提高50%。龙芯3A1000、3A2000、3A3000保持引脚兼容,硬件上可以直接替换。全新的微结构升级的GS464V处理器核以及基于GS464V的龙芯3A4000已于2019年研制成功,增加了位向量指令集,在龙芯3A3000的基础上性能再次大幅提高。

MIPS体系结构的发展经历了MIPS I、MIPS II、MIPS III、MIPS IV、MIPS V(没有实现过)、MIPS R1、MIPS R2、MIPS R3、MIPS R5和MIPS R6(注意没有MIPS R4)等许多个版本,这些指令集版本的关系如图1-1所示。

在传统版本发展阶段(MIPS I→MIPS II→MIPS III→MIPS IV),每一代指令集都是前一代的超集。其中,MIPS I和MIPS II只有32位,MIPS III和MIPS IV包括32位和64位。MIPS V虽然也是MIPS IV的超集,但只有规范定义,没有具体的处理器实现。

现代版本发展阶段改变了命名方式,在MIPS IV的基础上定义MIPS R1,其完整版叫MIPS64 R1,32位子集叫MIPS32 R1。后续的MIPS R1→MIPS R2→MIPS R3→MIPS R5的发展历程也是逐渐扩充的,但MIPS R6放弃了兼容,不再是以前版本的超集。

龙芯1号系列的各个型号处理器与MIPS32 R2(MIPS R2的32位版本)兼容;龙芯2E、2F与MIPS III兼容;龙芯2G、2H、3A1000、3B1000、3B1500与MIPS64 R1(MIPS R1的64位版本)兼容;龙芯3A2000、3A3000与MIPS64 R2(MIPS R2的完整版)兼容。注意,以上这些MIPS版本指的是体系结构(指令集ISA),并不是具体的CPU设计。MIPS公司早期的“官方公版处理器”主要有R2000、R3000、R4000、R6000、R8000、R10000、R12000、R16000等系列,而现在的MIPS公司主要是对设计授权,不再自己生产CPU。龙芯处理器在指令集以外的部分的许多设计跟R4000比较相似(如时钟源、Cache、MMU、FPU的设计等),因此在本书的解析中许多代码都是使用和R4000(代码缩写为r4k)相同的版本。

关于MIPS指令集和特权级的具体介绍本书不详细展开,读者如有需要请查阅MIPS官方文档MIPS Architecture For Programmers,该文档一共有3卷。

1.1.1 龙芯3号功能特征

本书的重点是计算机类应用,因此主要关注龙芯3号。目前已经得到大规模应用的龙芯3号处理器包括四核3A1000、八核3B1500、四核3A2000、四核3A3000和四核3A4000共5款。龙芯3号的整体架构基于两级互联实现,以四核处理器为例,其结构如图1-2所示。

龙芯处理器支持通过HT控制器实现多路互联,多路互联意味着一台计算机上有多个处理器芯片(多个物理CPU)。处理器芯片内部集成内存控制器,这就意味着每个物理CPU有自己的“本地内存”,同时又能通过HT控制器互联总线访问其他物理CPU的“远程内存”。这种组织架构就是非均匀内存访问(Non-Uniform Memory Access,NUMA),即每个自带内存控制器的独立单元(传统上就是一个物理CPU)就是一个NUMA节点。如果硬件负责维护节点间的高速缓存一致性,就叫高速缓存一致的非均匀内存访问架构(Cache-Coherent Non-Uniform Memory Access,CCNUMA)。龙芯3号的NUMA是一种CC-NUMA,每个NUMA节点由4个CPU核组成,四核处理器(龙芯3A)包含1个NUMA节点,八核处理器(龙芯3B)包含2个NUMA节点。龙芯3号每个NUMA节点包含4个CPU核,每个CPU核有自己的本地Cache(一级Cache,L1 Cache),节点内的4个CPU核共享二级Cache(分体式共享,分为4个模块但作为整体被CPU核共享)。

NUMA虽然在内存访问上存在不均匀性,但在运行过程中每个处理器的地位是对等的,因此依旧属于对称多处理器(Symmetric Multi-Processor,SMP)。或者说,广义的SMP系统既包括均匀内存访问的对称多处理器(UMA-SMP),也包括非均匀内存访问的对称多处理器(NUMA-SMP)。与SMP系统相对的是单处理器(Uni-Processor,UP)系统。

龙芯3号每个节点的第一级互联采用6×6的交叉开关,用于连接4个CPU核(作为主设备)、4个二级Cache模块(作为从设备)以及2个I/O端口(每个端口使用一个Master主设备和一个Slave从设备)。一级互联开关连接的每个I/O端口连接一个16位的HT控制器,每个16位的HT端口还可以作为两个8位的HT端口使用。HT控制器通过一个DMA控制器和一级交叉开关相连,DMA控制器负责I/O端口的DMA控制并负责片间一致性的维护。龙芯3号的DMA控制器还可以通过配置实现预取和矩阵转置或搬移。

龙芯3号每个节点的第二级互联采用5×4的交叉开关,连接4个二级Cache模块(作为主设备)。从设备一方则包括两个DDR2/3内存控制器、本地的低速或高速I/O控制器(包括PCI、LPC、SPI、UART等)以及芯片内部的配置寄存器模块。

在交叉开关上面,所谓主设备,就是主动发起访问请求的主控方;所谓从设备,就是被动接受访问请求并给出响应的受控方。龙芯3号的两级交叉开关都采用读写分离的数据通道,数据通道宽度为128位,工作在与处理器核相同的频率,用以提供高速的片上数据传输。

由于基于龙芯3号可扩展互联架构,因此在组建NUMA时,四核龙芯3A可以通过HT端口连接构成2芯片八核的SMP结构或者4芯片16核的SMP结构。同样,八核龙芯3B也可以通过HT端口连接构成2芯片16核的SMP结构。

下面根据主要型号的发展顺序简单介绍龙芯3号的功能特征。

(一)龙芯3A1000

龙芯3A1000是一个配置为单节点四核的处理器,采用65 nm工艺制造,最高工作主频为1 GHz,主要技术特征如下。

○ 片内集成4个64位的四发射超标量GS464高性能处理器核。

○ 每个核的私有一级Cache包含64 KB指令Cache和64 KB数据Cache。

○ 片内集成4 MB的分体共享二级Cache(由4个体模块组成,每个体模块容量为1 MB)。

○ 通过目录协议维护多核及I/O DMA访问的Cache一致性。

○ 片内集成2个64位400 MHz的DDR2/3控制器。

○ 片内集成2个16位800 MHz的HyperTransport控制器。

○ 每个16位的HT端口拆分成两个8路的HT端口使用。

○ 片内集成32位100 MHz PCIX/66 MHz PCI控制器。

○ 片内集成1个LPC、2个UART、1个SPI、16路GPIO接口。

(二)龙芯3B1500

龙芯3B1500是一个配置为双节点的八核处理器,采用32 nm工艺制造,最高工作主频为1.2 GHz(低电压版)/1.5 GHz(高电压版),主要技术特征如下。

○ 片内集成8个64位的四发射超标量GS464高性能处理器核。

○ 每个核的私有一级Cache包含64 KB指令Cache、64 KB数据Cache和128 KB牺牲Cache。

○ 每个处理器核频率单独可设。

○ 片内集成8 MB的分体共享二级Cache(由8个体模块组成,每个体模块容量为1 MB)。

○ 通过目录协议维护多核及I/O DMA访问的Cache一致性。

○ 片内集成2个64位667 MHz的DDR2/3控制器,支持DDR3-1333。

○ 片内集成2个16位800 MHz的HyperTransport控制器,最高支持1600 MHz总线。

○ 每个16位的HT端口可以拆分成两个8路的HT端口使用。

○ 片内集成32位33 MHz PCI控制器。

○ 片内集成1个LPC、2个UART、1个SPI、16路GPIO接口。

(三)龙芯3A2000

龙芯3A2000是龙芯3A1000四核处理器的结构升级版本,封装引脚与龙芯3A1000兼容。龙芯3A2000是一个配置为单节点四核的处理器,采用40 nm工艺制造,最高工作主频为1 GHz,主要技术特征如下。

○ 片内集成4个64位的四发射超标量GS464E高性能处理器核。

○ 每个核的私有一级Cache包含64 KB指令Cache、64 KB数据Cache和256 KB牺牲Cache。

○ 片内集成4 MB的分体共享二级Cache(由4个体模块组成,每个体模块容量为1 MB)。

○ 通过目录协议维护多核及I/O DMA访问的Cache一致性。

○ 片内集成2个64位带ECC、667 MHz的DDR2/3控制器。

○ 片内集成2个16位1.6 GHz的HyperTransport控制器。[1]

○ 每个16位的HT端口拆分成两个8路的HT端口使用。

○ 片内集成32位33 MHz PCI控制器。

○ 片内集成1个LPC、2个UART、1个SPI、16路GPIO接口。

相比龙芯3A1000,其主要改进如下。

○ 处理器核结构全面升级,引入双TLB设计(VTLB+FTLB)和SFB(Store Fill Buffer)。

○ 内存控制器HT控制器结构、频率全面升级。

○ 内部互连结构和外部扩展互连结构均全面升级。

○ 支持SPI启动功能。

○ 支持全芯片软件频率配置。

○ 全芯片的性能优化提升,在主频不变的情况下性能提高2~4倍。

(四)龙芯3A3000

龙芯3A3000是龙芯3A2000四核处理器的工艺升级版本,封装引脚与龙芯3A1000、3A2000兼容。龙芯3A3000是一个配置为单节点四核的处理器,采用28 nm工艺制造,最高工作主频为1.5 GHz,主要技术特征如下。

○ 片内集成4个64位的四发射超标量GS464E高性能处理器核。

○ 每个核的私有一级Cache包含64 KB指令Cache、64 KB数据Cache和256 KB牺牲Cache。

○ 片内集成8 MB的分体共享二级Cache(由4个体模块组成,每个体模块容量为2 MB)。

○ 通过目录协议维护多核及I/O DMA访问的Cache一致性。

○ 片内集成2个64位带ECC、667 MHz的DDR2/3控制器。

○ 片内集成2个16位1.6 GHz的HyperTransport控制器[2]

○ 每个16位的HT端口拆分成两个8路的HT端口使用。

○ 片内集成32位33 MHz PCI控制器。

○ 片内集成1个LPC、2个UART、1个SPI、16路GPIO接口。

相比龙芯3A2000,其主要改进如下。

○ 工艺升级,主频提升至1.5 GHz。

○ 二级Cache容量由4 MB增加到8 MB。

○ 全芯片的性能优化,性能提升50%左右。

(五)龙芯3A4000

龙芯3A4000是一款四核龙芯处理器,采用28 nm工艺制造,稳定工作主频为1.8~2.0 GHz,主要技术特征如下。

○ 片内集成4个64位的四发射超标量GS464V高性能处理器核,支持向量运算扩展指令。

○ 每个核的私有一级Cache包含64 KB指令Cache、64 KB数据Cache和256 KB牺牲Cache。

○ 片内集成8 MB的分体共享二级Cache(由4个体模块组成,每个体模块容量为2 MB)。

○ 通过目录协议维护多核及I/O DMA访问的Cache一致性。

○ 片内集成2个64位带ECC、800 MHz的DDR3/4控制器。

○ 片内集成2个16位1.6 GHz的HyperTransport控制器。

○ 每个16位的HT端口拆分成两个8路的HT端口使用。

○ 片内集成2个I2C、1个UART、1个SPI、16路GPIO接口。

龙芯3A4000的顶层结构设计在龙芯3A2000和3A3000的基础上进行了较大幅度的优化,其主要改进如下。

○ 继续采用双TLB设计,VTLB保持64项,FTLB从1024项增加到2048项。

○ 调整了片上互联结构,简化了地址路由,I/O模块间互联采用RING结构。

○ 优化了HT控制器的带宽利用率与跨片延时。

○ 优化了内存控制器结构,增加了DDR4内存的支持,并支持内存槽连接加速卡。

○ 规范了配置寄存器空间与访问方式,引入了CSR配置寄存器访问机制。

○ 优化了中断控制器结构,支持向量中断分发机制。

○ 增加了8路互联支持。

○ 全芯片的性能优化提升,在主频不变的情况下性能提高约50%。

关于龙芯3号系列处理器的更多细节可参阅各型号的《龙芯处理器用户手册》,每种型号均有上下两册。

注意:

龙芯3B1500的处理器核带有向量扩展指令集,所以早期也曾称为GS464V,但该扩展指令集存在较多的缺陷,实际中并未使用起来。因此,现在一般也将龙芯3B1500的处理器核称为GS464(同龙芯3A1000),而将带有新的向量扩展指令集的龙芯3A4000的处理器核称为GS464V。

1.1.2 龙芯3号处理器核

在逻辑上,一个龙芯3号处理器核包括主处理器、协处理器0、协处理器1、协处理器2、一级Cache、SFB和TLB等多个组成部分。

主处理器:实现整数运算、逻辑运算、流程控制等功能的部件,包括32个通用寄存器(General Purpose Registers,GPR)以及Hi/Lo两个辅助寄存器。

协处理器0(CP0):名称是系统控制协处理器,负责一些跟特权级以及内存管理单元(Memory Management Unit,MMU)有关的功能,至少包括32个CP0寄存器。

协处理器1(CP1):名称是浮点运算协处理器(FPU),负责单精度/双精度浮点运算,包括32个浮点运算寄存器(FPR)和若干个浮点控制与状态寄存器(FCSR)。

协处理器2(CP2):名称是多媒体指令协处理器,负责多媒体指令(MMI指令)的运算,共享FPU的运算寄存器。

一级Cache(L1 Cache):包括64 KB指令Cache(I-Cache)、64 KB数据Cache(D-Cache),从龙芯3B1500开始还包括256 KB牺牲Cache(V-Cache)。注意,二级Cache不属于处理器核的组成部分,而是被多个处理器核共享的。I-Cache和D-Cache采用VIPT组织方式,4路组相联;V-Cache采用PIPT组织方式,16路组相联。

SFB:全称Store Fill Buffer,是从龙芯3A2000开始引入的功能部件,可以大幅优化访存性能。SFB位于寄存器和一级Cache之间,在功能上可以把SFB理解为零级Cache(L0 Cache),但是只有数据访问会经过SFB,取指令直接访问一级Cache。

TLB:全称Translation Lookaside Buffer,即快速翻译查找表,是为了加速页表访问而引入的一种高速缓存(专属于页表的Cache)。龙芯3号的主TLB不分指令和数据,因此统称为JTLB(另有软件透明的uTLB,分为指令ITLB和数据DTLB,整个uTLB是JTLB的子集,类似于一级Cache和二级Cache的关系)。龙芯3A1000~3B1500每个核有64项JTLB(页大小可变因此也叫VTLB);龙芯3A2000、3A3000除64项VTLB以外还有1024项页大小固定的FTLB;龙芯3A4000除64项VTLB以外还有2048项页大小固定的FTLB。VTLB和FTLB同属于JTLB,前者采用全相联方式,后者采用8路组相联方式。

注意:

根据MIPS处理器规范,一个处理器核总共可以有4个协处理器,但是龙芯3号只设计了3个,因此没有CP3。

MIPS处理器核的字节序格式既可以使用大尾端(Big-Endian)也可以使用小尾端(LittleEndian),但龙芯只支持小尾端格式。

下面简单介绍龙芯3号处理器核的一些内部特征细节,主要包括通用寄存器、CP0寄存器和指令集。

(一)通用寄存器

龙芯处理器核有32个通用寄存器(GPR),在64位模式下,GPR字长均为64位;在32位兼容模式下,GPR只有低32位可用。所谓通用寄存器,就是可以用作任意用途,不过0号GPR是一个特殊的通用寄存器,其值永远为0。虽然在硬件设计上GPR的用途没有特意规定(GPR0除外),但是在软件使用上遵循一定的约定,这种约定就是应用二进制接口(Application Binary Interface,ABI)。MIPS处理器有3种常用的ABI:O32、N32和N64(O代表Old,旧的;N代表New,新的),其主要特征如下。

O32:只能用32位操作数指令和32位GPR,C语言数据类型的char、short、int、long和指针分别为8位、16位、32位、32位和32位。

N32:可以用64位操作数指令和64位GPR,C语言数据类型的char、short、int、long和指针分别为8位、16位、32位、32位和32位。

N64:可以用64位操作数指令和64位GPR,C语言数据类型的char、short、int、long和指针分别为8位、16位、32位、64位和64位。

除了上述数据格式的差别,3种ABI的另一个重要区别是32个通用寄存器的使用约定不一样,具体如表1-1所示。在内核代码中寄存器编号采用$n的形式来表示第n个通用寄存器,即GPRn;寄存器名称也叫助记符,用于表征其功能,在内核代码里面通常使用小写字母,但在文档资料介绍中通常使用大写字母。

GPR0:ZERO,它的值永远为0,之所以要设置这个寄存器,是因为RISC使用定长指令(MIPS的标准指令字长度为32位),如果没有零值寄存器,就无法把32位/64位的零操作数编码到指令中。

GPR1:AT,是保留给汇编器使用的,用于合成宏指令。宏指令是那些处理器实际上不提供,而由汇编器利用多条指令合成的伪指令,比如加载任意立即数的li指令。

GPR31:RA,通常用来保存函数返回的地址。

V系列寄存器:用于保存函数的返回值。

A系列寄存器:用于传递函数的参数。N32/N64与O32相比有更多的A系列寄存器,因此更有利于使用寄存器而不是堆栈来传递参数。

S系列寄存器:函数调用时需要保存的寄存器。

T系列寄存器:可以随意使用的临时寄存器。更确切地说,S寄存器由被调用者负责保存/恢复,T寄存器由调用者负责保存/恢复(如果需要的话)。N32/N64与O32相比T系列寄存器更少,因为它们被用于A系列寄存器了。

K系列寄存器:保留给内核在异常处理时使用的,应用程序不应当使用。

GPR28:GP,是Global Pointer(全局指针)的缩写。由于存在大量的进程间共享,应用程序和动态链接库一般被设计成位置无关代码(Position Independent Code,PIC)。因此,全局变量(更一般地说是全局符号,包括全局函数和全局变量)往往不能通过一个确定的地址直接访问。为了解决这个问题,需要在每个链接单元中引入一个GOT(Global Offset Table),然后通过GP寄存器指向GOT表来间接访问这些全局变量。相比之下,内核通常链接在固定的地址(地址有关代码),因此不需要GOT表。新版的Linux内核也支持重定位,但一台电脑上只运行一个内核,没有共享问题,所以重定位内核可以使用地址修正法而不需要GOT表。内核的模块使用了位置无关代码,但内核模块同样没有共享问题;所以在加载模块的时候地址即可唯一确定,因而即便是跨模块的全局符号访问也可以通过EXPORT_SYMBOL()等方法来解决。总而言之,内核里对全局函数和全局变量的访问不需要使用GP,因此在内核中GP通常用来指向当前进程的thread_info地址,这样可以优化对当前进程本地数据结构的访问性能。

GPR29:SP,是Stack Pointer(栈指针)的缩写,主要用于访问局部变量(以及栈里面的其他数据)。

GPR30:FP,是Frame Pointer(帧指针)的缩写,用来辅助访问局部变量。FP不是必需的,在不需要FP的代码里面,可以当S8使用。

为了理解SP和FP,我们简单介绍一下龙芯3号(MIPS通用)的栈结构[3],如图1-3所示。

在大多数体系结构的设计中,堆是从低地址往高地址扩展,而栈是从高地址往低地址扩展。龙芯平台的栈设计也是从高地址往低地址扩展,其栈的起始点称为栈底(最高地址),在使用过程中动态浮动的结束点称为栈顶(最低地址)。除了保存局部变量以外,栈的最大作用是保存函数调用过程中的寄存器状态。因此,在函数逐级调用时,栈里面的内容是一段一段的,每一段称为一个栈帧。从高地址往低地址看,首先是父函数(当前函数的调用者)的栈帧,然后是当前函数的栈帧,再然后是子函数的栈帧,依此类推。每一个栈帧的内容是类似的:首先是函数返回地址,然后是需要保存的寄存器,再然后是当前函数用到的局部变量,最后是调用的子函数参数(最下层的叶子函数不需要参数空间)。在大多数情况下,函数调用时子函数的第一步就是调整栈指针SP(从父函数栈帧的最低地址调整到子函数栈帧的最低地址),然后在整个子函数活动时间内SP保持不变,因此引用局部变量时有一个固定不变的基地址。但是C语言运行时库里面可能会提供alloca()等函数用于在栈里面分配内存空间,这会导致活动期间SP发生变化。在这种情况下,为了方便引用局部变量就会引入FP;FP指向当前栈帧的最高地址并且在函数活动期间不发生变化。注意:MIPS的ABI约定优先使用寄存器传递参数,寄存器不够用时才会使用栈传递。但即便如此,栈帧结构里面依旧会给每个参数预留空间,栈帧里面这些不会被使用的参数空间称为影子空间(不管哪种ABI,前4个参数一定会通过A0~A3寄存器传递,所以栈帧参数空间里面前4个一定是影子空间)。

(二)CP0寄存器

系统控制协处理器(CP0)拥有至少32个寄存器。之所以说“至少”,是因为某些编号寄存器包含扩展的子寄存器。龙芯3A2000(以及更新的处理器)比龙芯3A1000、3B1000、3B1500拥有更多的扩展,其概述如表1-2所示。

对于这些寄存器的具体功能,本章不一一展开,后续章节在必要的时候会专门讲解。

(三)指令集

MIPS作为一种RISC指令集,相对来说是比较精简的,指令名(助记符)也很有规律。下面分类介绍常用指令。

访存指令:LB、LBU、LH、LHU、LW、LWU、LD、SB、SH、SW、SD、LUI、LI、DLI、LA、DLA。

访存指令分为加载指令和存储指令两类,前者是将内存内容读到寄存器,后者是将寄存器内容写入内存。指令名的含义基本上遵循“操作类型—操作位宽—后缀”的规律。操作类型是L(Load,即加载)或S(Store,即存储);操作位宽为B、H、W或D,分别代表Byte(字节,8位)、Half-Word(半字,16位)、Word(字,32位)、Double-Word(双字,64位);后缀U表示加载的数是无符号整数,会对高位进行零扩展而不是符号扩展。例如,LHU表示加载一个16位无符号整数到寄存器,对高位部分进行零扩展。

但是,LUI、LI/DLI、LA/DLA不符合上述规律。LUI表示加载高半字立即数,即加载一个无符号16位立即数并左移16位。LI/DLI是宏指令,作用是加载一个任意32位/64位立即数到寄存器。LA/DLA也是宏指令,作用是加载一个符号(变量名或函数名)的32位/64位地址到寄存器。

计算指令:ADDI、ADDIU、DADDI、DADDIU、ADD、ADDU、DADD、DADDU、SUB、SUBU、DSUB、DSUBU、MULT、DMULT、MULTU、DMULTU、DIV、DDIV、DIVU、DDIVU、MFHI、MTHI、MFLO、MTLO。

计算指令指整数的加减乘除四则运算,指令名的含义基本上遵循“操作位宽一计算类型一后缀”的规律。操作位宽无D的表示32位操作数,有D的表示64位操作数;计算类型分加(ADD)、减(SUB)、乘(MULT)、除(DIV)4种;无后缀的表示两个源操作数都来自寄存器,后缀为I的表示一个源操作数来自寄存器而另一个源操作数是立即数,后缀为U的表示无符号操作数(实际含义是溢出时不产生异常),后缀IU则是I后缀和U后缀的组合。例如,DADDIU表示64位加法指令,一个加数来自寄存器而另一个加数为立即数,运算溢出时不产生异常。

两个标准字长的操作数做乘法往往会产生双倍字长的结果,比如32位操作数乘32位操作数的结果可能是64位操作数。因此,MIPS在32个通用寄存器之外专门设置了Hi寄存器和Lo寄存器,分别用来保存乘法运算结果的高位字和低位字。在进行除法运算时,Lo寄存器保存商,Hi寄存器保存余数。MFHI/MTHI/MFLO/MTLO用于在通用寄存器和Hi/Lo两个辅助寄存器之间传递数据,MF指的是Move From,MT指的是Move To。顾名思义,MFHI就是将Hi寄存器中的数据传递到通用寄存器。

逻辑指令:AND、OR、XOR、NOR、ANDI、ORI、XORI。

4种基本运算:AND是逻辑与,OR是逻辑或,XOR是逻辑异或,NOR是逻辑或非。无后缀I的指令表示两个源操作数都来自寄存器,有后缀I的表示一个源操作数来自寄存器而另一个源操作数是立即数。

移位指令:SLL、SRL、SRA、ROTR、SLLV、SRLV、SRAV、ROTRV、DSLL、DSRL、DSRA、DROTR、DSLLV、DSRLV、DSRAV、DROTRV。

4种32位基本操作:SLL是逻辑左移(低位补充零),SRL是逻辑右移(高位补充零),SRA是算术右移(高位补充符号位),ROTR是循环右移(高位补充从低位移出的部分)。其他的可以以此类推,带D前缀的指令是64位操作数,无V后缀的是固定移位(移位的位数由立即数给出),有V后缀的是可变移位(移位的位数由寄存器给出)。

跳转指令:J、JR、JAL、JALR、B、BAL、BEQ、BEQAL、BNE、BNEAL、BLTZ、BLTZAL、BGTZ、BGTZAL、BLEZ、BLEZAL、BGEZ、BGEZAL。

前缀为J的指令表示绝对跳转(无条件跳转),跳转目标地址是相对PC所在地址段的256 MB边界的偏移;前缀为B的是相对跳转(有条件跳转,也叫分支指令),跳转目标地址是相对PC的偏移。J类跳转指令里面:无后缀R表示目标地址为立即数,有后缀R表示跳转目标为寄存器的值;无后缀AL表示普通跳转,有后缀AL表示链接跳转(自动保存返回地址到RA寄存器,用于函数调用)。B类分支指令里面:EQ表示跳转条件为相等(Equal),NE表示跳转条件为不相等(Not Equal),LT表示跳转条件为小于(Less Than),GT表示跳转条件为大于(Greater Than),LE表示跳转条件为小于或等于(Less Than or Equal),GE表示跳转条件为大于或等于(Greater Than or Equal),AL后缀表示链接跳转。B类分支指令里面有一部分是宏指令。例如,BGEZAL表示若源操作数大于或等于零,就执行相对链接跳转。

协处理器指令:MFC0、MTC0、DMFC0、DMTC0、MFC1、MTC1、DMFC1、DMTC1、MFC2、MTC2、DMFC2、DMTC2。

协处理器指令用于在通用寄存器和协处理器寄存器之间传送数据。MF表示Move From,MT表示Move To;无前缀D的指令表示操作32位协处理器寄存器,有前缀D的表示操作64位协处理器寄存器;后缀C0表示协处理器0,C1表示协处理器1,C2表示协处理器2。例如,DMFC0表示从协处理器0的64位寄存器传递一个数据到通用寄存器。

MMU相关指令:CACHE(高速缓存维护)、TLBP(TLB查询)、TLBR(读TLB项)、TLBWI(写TLB指定项)、TLBWR(写TLB随机项)。

特殊指令:SYNC(内存屏障)、SYSCALL(系统调用)、ERET(异常返回)、BREAK(断点)、EI(开中断)、DI(关中断)、NOP(空操作)、WAIT(暂停等待)。

浮点运算指令和龙芯扩展指令在内核中极少使用,此处不予介绍。有关寄存器和指令集的更多信息请参阅MIPS架构文档、龙芯处理器手册以及《MIPS体系结构透视》[1]

1.1.3 龙芯电脑基本结构

本书讲述的是基于龙芯平台的Linux内核,操作系统管理的不仅仅是处理器,而是整个计算机。那么,现在我们来了解一下龙芯电脑的基本结构。

传统的处理器仅仅指的是CPU核,而现代的处理器通常包括更多的功能。从处理器结构图(如图1-2所示)可以看出,除4个CPU核以外,龙芯处理器还包括本地私有一级Cache、共享二级Cache、两个内存控制器和两个HT控制器等重要结构。其中内存控制器用于连接内存,HT0控制器用于多个处理器芯片互联以构建CC-NUMA系统,HT1控制器用于连接芯片组进而挂接各种外围设备。

龙芯3号处理器可以跟多种芯片组搭配,比如RS690(北桥)+SB600(南桥)、RS780(北桥)+SB700(南桥)、RS880(北桥)+SB800(南桥)、SR5690(北桥)+SP5100(南桥)。以上芯片组都是由美国AMD公司出品,互相之间基本保持兼容,故以RS780为代表将它们统称为RS780机型。除了使用AMD芯片组之外,龙芯电脑还可以使用南北桥合一(合在一起称为桥片)的LS2H或LS7A,这两种芯片组都是龙芯中科自产。

在RS780机型中,龙芯的外围总线总体上是基于PCI-Express(以下简称PCI-E或PCIe)的,PCIe的根节点位于北桥芯片,下面可以挂接PCIe显卡(包括北桥内部的集成显卡)、PCIe声卡、PCIe网卡等高速设备。南桥芯片内部包含各种低速控制器,如SATA控制器、USB控制器、PCI控制器等,这些低速控制器本身是PCIe设备,其下挂接的则是各种相应的低速设备。使用RS780机型的龙芯电脑基本结构如图1-4所示。

LS2H是第一款可以用于龙芯3号处理器的国产桥片,内含CPU核,因此也可以当SoC用(即龙芯2H)。LS7A则可以视为LS2H的升级改进版本,是第一款专门针对龙芯3号处理器设计的国产桥片(去掉了CPU核),在功能和性能上有了全面的提升,也是现在使用的主流型号。LS2H/LS7A机型与RS780机型相比没有太大的区别,仅仅是南桥芯片和北桥芯片集成在一起而已。使用LS2H/LS7A芯片组的龙芯电脑基本结构如图1-5所示。

从图1-4和图1-5的对比可以看出,除了将南北桥芯片集成在一起之外,LS2H/LS7A机型允许一些低速设备控制器不通过PCIe根,而是直接与HT控制器的内部总线相连接。比如在LS7A桥片中,RTC和I2C设备直接使用内部总线连接,而LPC、SPI等控制器则通过PCIe连接。在Linux内核中,直接通过内部总线连接的设备叫平台设备(platform device),因此内部总线也叫平台总线(platform bus)。也就是说,在LS2H/LS7A机型中,外围设备有PCIe总线设备(简称PCIe设备)和平台总线设备(简称平台设备)两大类。

根据PCIe总线规范,PCIe设备运行时是可探测的,因此同一套软件可以不做任何变化地应用于不同外设配置的机器上。然而,平台设备运行时却不可以探测,传统上这类设备只能在内核里面静态声明,因而影响可移植性。为了解决这个问题,现在比较常用的方法是设备树(DeviceTree)。设备树是对一台机器上所有平台设备信息的描述(实际上也可以描述其他类型的设备),集成在BIOS中并以启动参数的方式传递给Linux内核。如果BIOS没有传递设备树信息,则内核可以使用默认的设备树描述。在龙芯平台上,LS2H、LS7A和RS780这3种机型的默认设备树描述文件分别是arch/mips/boot/dts/loongson/loongson3_ls2h.dts、arch/ mips/boot/dts/loongson/loongson3_ls7a.dts和arch/mips/boot/dts/loongson/loongson3_rs780.dts。

从龙芯电脑结构框架图(如图1-4和图1-5所示)中还可以看出,4个CPU核的一级Cache与2个HT控制器一样,都是二级Cache的主设备。龙芯处理器在硬件上可以通过目录协议[4]来维护CPU核间Cache一致性,以及CPU核与外设DMA之间的Cache一致性。这种不需要软件处理一致性问题的设计,大大方便了Linux内核的开发者。然而,为了满足一些特殊场合的要求,龙芯平台的Linux内核既支持一致性DMA(由硬件维护CPU核与DMA之间的一致性),也支持非一致性DMA(由软件维护CPU核与DMA之间的一致性)。注意:这里涉及的一致性是指空间一致性(coherency)而不是时序一致性(consistancy)。空间一致性指多个Cache副本之间的一致性,由龙芯CPU硬件负责维护;而时序一致性指多个处理器之间访存操作的顺序问题,通常需要软件和硬件协同解决(参考本书附录A.1内存屏障一节)。

1.2 Linux内核简介

Linux是操作系统大家族中的一名成员。从20世纪90年代末开始,Linux变得越来越流行,并跻身于有名的商用UNIX操作系统之列。这些UNIX系列包括AT&T公司(现在由SCO公司拥有)开发的SVR4(System V Release 4),加利福尼亚大学伯克利分校发布的BSD,DEC公司(现在属于惠普公司)的Digital UNIX,IBM公司的AIX,惠普公司的HP-UX,Sun公司的Solaris,以及苹果公司的Mac OS X等。

1991年,Linus Torvalds开发出最初的Linux,这个操作系统适用于基于Intel 80386微处理器的IBM PC兼容机。经过多年的发展,Linux已经可以在许多其他平台上运行,包括Alpha、Itanium(IA64)、MIPS、ARM、SPARC、MC680x0、PowerPC以及zSeries。

Linux最吸引人的一个优点就在于它是一个自由的操作系统:它的源代码基于GNU公共许可证(GNU Public License,GPL),是开放的,任何人都可以获得源代码并研究它;只要得到源代码,就可以深入探究这个成功而又现代的操作系统。Linux提倡自由、开源、共享,人人为我,我为人人。在GPL的号召下,全世界的Linux开发者组成了一个虚拟的开源社区。这是一种非常优秀的组织结构,尽管开发者分布在世界各地,但是可以通过源代码和互联网进行高效的无障碍交流。大家既从开源社区获取资源,也把自己的贡献回馈给开源社区。

从技术角度来说,Linux只是操作系统内核,而不是一个完全的类UNIX操作系统,这是因为它不包含全部的UNIX应用程序,诸如文件系统实用程序、命令解释器、窗口系统、图形化桌面、系统管理员命令、文本编辑程序、编译开发程序等。以上这些应用程序大部分都可在GNU公共许可证下免费获得,因此包含Linux内核、基础运行环境(运行时库如GLibc)、编译环境(如GCC)、外壳程序(Shell,即命令解释器)和图形操作界面(GUI)的完整操作系统套件被称为GNU/Linux。尽管如此,在大多数情况下,仍用Linux来指代完整的GNU/Linux。

Linux内核遵循IEEE POSIX标准(POSIX的全称是Portable Operating System Interface of UNIX,表示可移植操作系统接口)。它包括现代UNIX操作系统的全部特点,如虚拟存储、虚拟文件系统、内核线程、轻量级进程、UNIX信号量、SVR4进程间通信、支持内核抢占、对称多处理器系统等。

Linux内核是一体化内核(或称宏内核)操作系统,宏内核的设计风格是“凡是可以在内核里实现的都在内核里实现”。因此,除了异常/中断处理、内存管理和进程管理3大基本功能以外,文件系统、设备驱动和网络协议也放在内核层实现。宏内核的优点是内核内部的各种互操作都可以通过函数调用实现,因此性能较好;而缺点是体积较大且理论上健壮性不太好(因为内部耦合性太高)。与宏内核相对的是微内核,常见的实现是GNU Hurd,其设计风格是“凡是可以不在内核里实现的都不在内核里实现”,因此很多功能子系统被设计成了一种服务(进程)。微内核的优点是体积较小且理论上更健壮(因为内核本身的功能较少,不容易出错);而缺点是操作系统的大量互操作都依赖于进程间通信(Inter-Process Communication,IPC),因此性能较差。微内核虽然把一些内核的核心功能剥离到了服务进程中,但重要的服务崩溃后实际上跟内核崩溃类似,因为整个系统也同样处于一个基本不可用的状态。Linux虽然是宏内核,但是也吸收了一些微内核的优点,比如从1.0版本开始就可以通过模块化(将一些非核心的功能设计成可以运行时动态加载/卸载的内核模块)来减少内核核心部分的体积。

1.2.1 Linux内核发展简史

Linux内核从最初发布的0.01版本到本书所使用的5.4.x版本,经历了“史前时代”“奇偶时代”“快速演进时代”和“极速演进时代”4个阶段,如图1-6所示。

(一)史前时代(0.01~1.0)

版本更迭过程为0.01→0.02→0.10→0.11→0.12→0.95→0.96→0.97.x→0.98.x →0.99.x→1.0.0,其中重要的版本如下。

○ 0.01: 第一个版本。

○ 0.02: 第一个公开发布的版本。

○ 0.11: 《Linux内核完全注释》使用的版本。

(二)奇偶时代(1.0.0~2.6.10)

这个时期的版本号用“a.b.c”表示,其中a为主版本号,b为次版本号,c为修订号。版本号变更的原则是,发生重大改变时升级主版本号,发生非重大改变时升级次版本号;次版本号为奇数表示开发版,次版本号为偶数表示稳定版;稳定版和开发版在修订号上各自升级演进,开发版达到稳定状态时,发布下一个稳定版。比如1.0.x在尽量不引入新功能的前提下不断升级;同时1.1.x在不断开发新功能的状态下不断升级,当1.1.x开发到足够稳定时,转变成1.2.x成为稳定版;同时新的开发版1.3.x诞生……

稳定版包括1.0.x、1.2.x、2.0.x、2.2.x、2.4.x、2.6.x;开发版包括1.1.x、1.3.x、2.1.x、2.3.x、2.5.x,其中重要的版本如下。

○ 1.0.0:第一个正式版本,支持模块化,支持网络。

○ 1.2.0:开始支持非X86架构。

○ 2.0.0:开始支持对称多处理(SMP)。

○ 2.2.0:开始被各种发行版大规模应用。

○ 2.4.5:开始有“中国制造”的代码(如LVS等)。

○ 2.4.18:《深入理解Linux内核(第2版)》使用的版本。

○ 2.6.0:开始声名响彻天下。完全可抢占,O(1)调度器,SYSFS,X86_64支持,NUMA支持,NPTL支持……

(三)快速演进时代(2.6.11~2.6.39)

从2.6.11开始,Linux内核界发生了两件大事:第一件大事是抛弃了BitKeeper,转而开始用Git管理源代码;第二件大事是抛弃了奇偶版本法,转而使用“a.b.c.d”表示版本号,其中a为主版本号,b为次版本号,c为主修订号,d为次修订号。主修订号c的升级既包括新特性引入,也包括缺陷修订(Bugfix),次修订号d的升级只包括缺陷修订。

这个阶段开发速度加快,版本号即便c段相邻,差别也很大。在奇偶时代,2.4.5和2.4.6的差异不是很大;而在快速演进时代,2.6.36和2.6.37的差别会非常大,甚至与2.4.x和2.5.x之间的差异相当。

在演进如此迅速的时代,如果继续采用奇偶版本法会有什么问题?首先,2.7版本开发持续时间会很长,不到2.8版本发布时,2.7版本加入的新特性无法得到利用。其次,2.7版本新特性同样很难后向移植(backport)到2.6版本,因为代码差异太大。

这个阶段的重要版本如下。

○ 2.6.11.0:《深入理解Linux内核(第3版)》使用的版本。

○ 2.6.20.0:开始支持KVM虚拟化技术。

○ 2.6.23.0:开始支持龙芯2E,引入CFS调度器,缺省使用SLUB内存分配器。

○ 2.6.24.0:i386和X86_64合并成X86架构。

○ 2.6.33.0:开始支持龙芯2F,在MIPS系列处理器上支持内核压缩。

○ 2.6.38.0:引入AutoGroup机制,大幅提升桌面应用体验,引入透明巨页(THP)。

(四)极速演进时代(3.0~5.x)

在快速演进阶段,内核版本号的a.b一直保持为2.6没变,完全可以合二为一。与此同时,参与Linux内核开发的个人与单位越来越多,Linux内核发展开始进入极速演进时代。在这个阶段,版本号回归“a.b.c”表示法,其中a为主版本号,b为次版本号,c为修订号。在含义上,新的a相当于之前(快速演进时代)的a.b,新的b相当于之前的c,新的c相当于之前的d。次版本号b的升级既包括新特性引入,也包括缺陷修订,修订号c的升级只包括缺陷修订。关于每个版本的Linux内核都引入了什么新功能,可以参考本书附录B或者准官方的内核发行概述。

这个阶段的重要版本如下。

○ 3.6.0:开始支持龙芯1号。

○ 3.8.0:引入调度实体负载跟踪机制(PELT),MIPS系列处理器开始支持透明巨页。

○ 3.10.0:Radeon系列显卡开始支持高清视频解码(UVD)。

○ 3.13.0:NUMA调度性能大幅度改进。

○ 3.14.0:MIPS系列处理器开始支持FP64/O32。

○ 3.15.0:开始支持龙芯3A1000,开始支持MIPS向量扩展(MSA)。

○ 3.16.0:开始引入快速排队读写锁(qrwlock)。

○ 3.17.0:开始支持龙芯3B1500,开始支持MIPS硬件页表遍历器(HTW)。

○ 3.18.0:开始支持用GCC5编译内核。

○ 4.0.0:开始支持在线补丁(LivePatching)和内核地址净化器(KASan)。

○ 4.2.0:开始引入快速排队自旋锁(qspinlock),代码量达到2000万行。

○ 4.5.0:MIPS开始支持IEEE754-2008标准,引入CGroup_V2。

○ 4.7.0:开始支持龙芯3A2000,MIPS开始支持可变长ASID、48位虚拟地址空间、可重定位内核和内核地址空间布局随机化(KASLR),CPUFreq增加schedutil策略。

○ 4.8.0:开始支持龙芯1C,完善支持软件MIPS KVM,内存页回收从基于管理区重构为基于NUMA节点。

○ 4.9.0:MIPS开始引入通用内核,引入TCP拥塞控制算法BBR。

○ 4.12.0:MIPS支持48位虚拟地址空间和硬件虚拟化(KVM/VZ),在线补丁使用每进程一致性模型(原来是全局一致性模型)。

○ 4.13.0:开始支持龙芯3A3000,MIPS开始支持自旋锁/读写锁。

○ 4.15.0:开始支持RISC-V,X86引入KPTI(对付Meltdown漏洞)和Retpoline(对付Spectre漏洞)。

○ 5.0.0:调度器引入EAS(节能感知)特征,块设备层全面切换到多队列模型(blk-mq),AMDGPU显卡驱动支持FreeSync,全面支持零拷贝网络。

1.2.2 Linux内核的开发模式

目前,Linux内核开发处于极速演进时代。在代码仓库管理上,有主线仓库(Mainline)、稳定仓库(Stable)、未来仓库(Linux-next)和子系统仓库(Subsystem,如Linux-mips)4大类,其关系如图1-7所示。

绝大多数开发者贡献的代码首先要接受子系统仓库管理员(Maintainer)的审核,才能进入某个特定的子系统仓库;在进入子系统仓库以后,未来仓库会进行二次审核;二次审核通过以后,将进入主线仓库(偶尔也有跳过未来仓库,从子系统仓库直接进入主线仓库的情况)。可以说,代码进入子系统仓库才仅仅处于Alpha状态;进入未来仓库才算达到Beta状态;如果进入了主线仓库,就相当于达到RC状态或者Final状态,算是被官方采纳了。通过这种多层次的严格审核,Linux内核的代码质量得到了极大的保障。

下面分别介绍这4类代码仓库。

(一)主线仓库

主线仓库是最重要的仓库,其升级规则是在次版本号上面升级演进,两个正式版之间会发布若干个候选版(RC版),如3.0→3.1-rc1→3.1-rcN→3.1→3.2-rc1→……

某一个正式版和下一个候选版之间的时期叫作合并窗口期,比如3.0和3.1-rc1之间就是3.1的合并窗口期。只有在合并窗口期才允许增加新特性,其他阶段只允许缺陷修订。也就是说,如果开发者想让某个新特性进入3.1内核,那么必须保证在3.1-rc1之前进入,否则就只能等待3.2的合并窗口期了。主线仓库的管理员及对应的仓库地址如下。

管理员:Linus Torvalds

Git仓库地址:git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git

(二)稳定仓库

稳定仓库基于主线仓库的正式版产生,在修订号上面升级演进,如3.0.x分支和3.1.x分支在稳定仓库中的版本演进关系分别为3.0→3.0.1→3.0.2→3.0.3→3.0.N→……和3.1→3.1.1→3.1.2→3.1.3→3.1.N→……

稳定仓库的代码变更全都是缺陷修订,不引入新的特征,其管理员及对应的仓库地址如下。

管理员:Greg Kroah-Hartman等

Git仓库地址:git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git

(三)未来仓库

未来仓库的前身为Andrew Morton维护的Linux-mm。代码变更在进入下一版主线仓库之前先到达这里,如果说主线仓库在功能上类似于奇偶时代的偶数版本(稳定版)的话,那么未来仓库在功能上就类似于奇偶时代的奇数版本(开发版)。未来仓库的版本命名规则是日期,如Next20151212。未来仓库会不定期合并主线仓库的代码,将其作为新一轮添加特性的基础(Base)代码。未来仓库的管理员及对应的仓库地址如下。

管理员:Stephen Rothwell

Git仓库地址:git://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git

(四)子系统仓库

子系统仓库为数众多,一般按体系结构(arch)、驱动类型(drivers)进行分类。龙芯内核开发者比较关心的子系统仓库主要是以下两个。

○ MIPS子系统:管理员为Ralf Baechle

Git仓库地址:git://git.kernel.org/pub/scm/linux/kernel/git/ralf/linux.git

○ GPU子系统:管理员为David Airlie

Git仓库地址:git://people.freedesktop.org/~airlied/linux

和未来仓库一样,子系统仓库会不定期合并主线仓库的代码,将其作为新一轮添加特性的基础代码。

内核根目录的MAINTAINERS文件会列出所有的现任管理员及其相关信息,比如MIPS架构下面龙芯相关的3个子架构(即龙芯1号、龙芯2号和龙芯3号)的管理员信息如下。

MIPS/LOONGSON1 ARCHITECTURE
M:     Keguang Zhang <keguang.zhang@gmail.com>
L:     linux-mips@vger.kernel.org
S:     Maintained
F:     arch/mips/loongson32/
F:     arch/mips/include/asm/mach-loongson32/
F:     drivers/*/*loongson1*
F:     drivers/*/*/*loongson1*
MIPS/LOONGSON2 ARCHITECTURE
M:     Jiaxun Yang <jiaxun.yang@flygoat.com>
L:     linux-mips@vger.kernel.org
S:     Maintained
F:     arch/mips/loongson64/fuloong-2e/
F:     arch/mips/loongson64/lemote-2f/
F:     arch/mips/include/asm/mach-loongson64/
F:     drivers/*/*loongson2*
F:     drivers/*/*/*loongson2*
MIPS/LOONGSON3 ARCHITECTURE
M:     Huacai Chen <chenhc@lemote.com>
L:     linux-mips@vger.kernel.org
S:     Maintained
F:     arch/mips/loongson64/
F:     arch/mips/include/asm/mach-loongson64/
F:     drivers/platform/mips/cpu_hwmon.c
F:     drivers/*/*loongson3*
F:     drivers/*/*/*loongson3*

MAINTAINERS文件中包含一个个子架构的条目。每个条目的开头第一行是关于子架构的描述,比如,龙芯1号的MIPS/LOONGSON1 ARCHITECTURE、龙芯2号的MIPS/LOONGSON2 ARCHITECTURE和龙芯3号的MIPS/LOONGSON3 ARCHITECTURE。M开头的行是管理员的姓名和电子邮箱,龙芯1号的管理员是张科广,龙芯2号的管理员是杨嘉勋,这两位都是开源社区的龙芯爱好者,而龙芯3号的管理员就是笔者(陈华才)。L开头的行是该子架构用于开发交流的邮件列表;S开头的行是该子架构的维护状态;F开头的行是该子架构涉及的主要源代码文件的路径。

1.2.3 关于长期维护稳定版本

所谓的长期维护稳定(Long Term Stable,LTS)版本,实际上是一种特殊的稳定(Stable)版本。Stable版本的缺陷修订实际上是主线版本中缺陷修订的后向移植。普通的Stable版本的维护时间为3个月左右,因此当主线仓库中下一版的正式版发布,上一版的Stable分支就不再继续升级(End Of Life,EOL)。而LTS版本的维护时间为2年左右,也可能更长。同时维护的LTS版本为5个左右,当一个新的LTS版本被选中时,一般最老的LTS版本就不再继续升级(EOL)。

Linux内核官方选择长期维护稳定版本的依据大致有几点:具有里程碑意义(如3.0),过去一段时间内引入的新特性的集大成者,或者被Redhat、Debian等著名发行版采用的版本。曾经被选为长期维护稳定版本的内核有2.6.16.x、2.6.27.x、2.6.32.x、2.6.33.x、2.6.34.x、2.6.35.x、3.0.x、3.2.x、3.4.x、3.10.x、3.12.x、3.14.x、3.16.x、3.18.x、4.1.x、4.4.x、4.9.x、4.14.x、4.19.x和5.4.x。

1.2.4 龙芯的内核版本选型

上一小节所提到的长期维护稳定版本是指Linux内核官方的版本选型。通常Linux操作系统发行商和CPU生产商也会提供自己主导开发的长期维护版本,厂商的选型与官方的选型可能相同也可能不同。龙芯的Linux内核开发主要由龙芯中科与航天龙梦两家单位主导,而龙芯相关的内核代码是随着时间的推移逐步融入Linux官方的。即便是官方的5.4.x版本,也并未全部采纳龙芯相关的内核代码,因此官方内核尚不能完美支持龙芯。为了满足各种不同的需求,龙芯中科与航天龙梦提供了多个完全支持龙芯的长期维护稳定版本,包括2.6.32.x、2.6.36.x、3.4.x、3.5.x、3.6.x、3.8.x、3.10.x、3.16.x、4.1.x、4.4.x、4.9.x、4.14.x、4.19.x和5.4.x。自3.10.x以来的所有官方LTS版本几乎都在此列。

龙芯选择长期维护稳定版本时有两种主要思路:一种是基于官方Linux内核(也称Vanilla Kernel,香草内核),另一种是基于RHEL(RedHat Enterprise Linux,即红帽企业版Linux,应用最广泛的Linux发行版之一)。经过多年的开发实践,现在笔者和龙芯开源社区都倾向于选择官方Linux内核,因为官方内核是“内部完全自洽”的,而红帽企业版Linux是“内部部分自洽”的。

那么如何理解“内部完全自洽”和“内部部分自洽”呢?众所周知,Linux内核源代码大致包括体系结构部分(arch),公共部分(common,包括scheduler、mm、fs等)和设备驱动(driver)。而arch细分又包括X86、ARM、MIPS、PowerPC等众多架构。官方内核的各种arch、各种driver以及public的任意组合都是能正常工作的,称为“内部完全自洽”;而红帽企业版Linux主要为X86定制优化,从官方内核引入某一版本(如3.10.0)的基础代码之后,会大规模引入后向移植,因此红帽企业版Linux对于X86来说是自洽的,但对非X86的MIPS等架构来说是严重不自洽的。

我们可以通过内核版本(KernelVersion)、后向移植(Backport)与KernelLevel等几个概念来更清楚地描述这个自洽问题。

官方内核有清晰的内核版本演进策略。比如目前使用a.b.c版本号结构,a和b是大版本演进,一边加入新功能,一边修复缺陷;c是稳定性演进,只修复缺陷。例如,4.2.0版会在4.1.0版的基础上增加新功能,同时修复缺陷;而4.1.1版只是在4.1.0版的基础上修复缺陷。因此,每当一个大版本发布(a或者b的升级),就会产生新的分支进入稳定通道(只升级c),4.1.x和4.2.x是不同的稳定分支。其中一部分稳定分支会作为官方的LTS分支,如前面提到的4.1.x、4.4.x、4.9.x、4.14.x、4.19.x及5.4.x。其他如4.2.x、4.3.x只是普通的稳定分支,而官方主线分支本书写作时已经升级到了5.5-rc1。

官方内核和红帽企业版Linux都会做后向移植,但官方内核的后向移植是全范围的修复缺陷,而红帽企业版Linux的后向移植是选择性地加入新功能和修复缺陷。这正是“内部完全自洽”与“内部部分自洽”问题的根源:红帽企业版Linux对X86和driver有大量的后向移植(尤其是有大量的新功能 ),对MIPS几乎没有后向移植(连修复缺陷都没有);导致红帽企业版Linux在X86架构上自洽,而在MIPS架构上不自洽。

我们以官方内核作为标杆,将任意内核源代码中某个子系统的版本状态定义成KernelLevel。那么上述问题用KernelLevel量化一下就更容易理解了。

○ 官方3.10.0内核:ArchLevel=X86Level=MIPSLevel=3.10,CommonLevel=3.10,DriverLevel=3.10,PatchLevel=0。

○ 官方3.10.108内核:ArchLevel=X86Level=MIPSLevel=3.10,CommonLevel=3.10,DriverLevel=3.10,PatchLevel=108。

○ 官方4.14.0内核:ArchLevel=X86Level=MIPSLevel=4.14,CommonLevel=4.14,DriverLevel=4.14,PatchLevel=0。

○ 官方4.14.120内核:ArchLevel=X86Level=MIPSLevel=4.14,CommonLevel=4.14,DriverLevel=4.14,PatchLevel=120。

○ 红帽3.10.0内核:ArchLevel=Undefined(其中X86Level≈4.0,MIPSLevel= 3.10),CommonLevel≈3.18,DriverLevel≈4.2,PatchLevel=Undefined(X86相关部分与X86Level基本一致)。

当内核中几个主要部分的KernelLevel完全相同时,即ArchLevel=CommonLevel=DriverLevel,我们称为内部完全自洽。当ArchLevel中各个架构的KernelLevel互不一致,但其中某个架构的KernelLevel与CommonLevel或DriverLevel保持一致或基本一致时,我们称为针对某架构的内部部分自洽。PatchLevel越高,代表在该分支里面越稳定(因为缺陷修订越全面)。

因此我们可以得出以下几个结论。

1. 所有版本的官方内核均内部完全自洽。

2. 同一个稳定分支的官方内核,版本号第三位越高越稳定。

3. 红帽3.10.0内核对X86自洽并且基本相当于官方4.0版本(特指RHEL7.2的标配内核,因为随着RHEL的升级,红帽企业版Linux的KernelLevel还会继续发生改变),对MIPS完全不自洽(甚至都无法顺利编译)。

龙芯长期维护的Linux内核应当如何选型的答案是显而易见的:官方内核。选择官方内核的优点是更好的内部完全自洽,也就更加稳定可靠,适配更容易;其缺点是相比于同一个标称版本的红帽企业版Linux,驱动比较旧,支持的设备比较少。然而,选择官方内核是利大于弊的,并且我们可以采用以下几种同时维护多个内核版本的方法。

1. 龙芯在确实需要3.10.x内核的时候,可以选择基于最稳定版本的官方内核3.10.108。

2. 龙芯在需要支持各种新功能和新设备时,可以选择基于新稳定版本的官方内核,如4.14.x或者4.19.x。

3. 龙芯由于特殊情况需要3.10.x内核但又想支持个别新设备时,可以选择基于最稳定版本的官方内核3.10.108,通过仔细查看官方内核的Git历史记录然后针对特定设备做后向移植(万万不可采取从新版本内核中批量复制目录的方法)。

4. 龙芯如果既想要大规模支持各种新功能和新设备,又想维持3.10.x主版本号不变的印象时,可以直接选择新版本的官方内核,如4.14.x或者4.19.x,然后通过修改Makefile伪装成3.10.x(就跟RHEL标称的3.10.0内核一样,只是给用户制造了一个3.10.0的假象,其内容根本不是3.10.0)。

方法1和方法2是自然而然的,是大多数情况下的正确选择;而方法3和方法4是特殊情况下的特殊选择。方法3对内核开发人员的技能要求比较高,但是这种移植的工作量是值得的,开发人员可以更深刻地掌握设备驱动;即便移植得不够完美,其影响也是“有界”的,而选择红帽企业版Linux所导致内部不自洽的隐患则是“无界”的。方法4看似不合理其实很合理,因为该方法的本质跟红帽企业版Linux一样是“旧瓶装新酒”,但这新酒是内部完全自洽的新酒,是“红帽思路”的最佳替代方案。

为什么方法3要强调万万不可从新版本内核中批量复制目录呢?因为这不是正规的内核开发方法,而是导致内部不自洽的罪魁祸首。如果在龙芯上采用红帽企业版Linux,则会导致更大范围的内部不自洽。不自洽的隐患很大,隐藏的问题也未必会马上体现出来;而一旦出现问题时,要指出“何处不自洽”并不容易(因为内核源代码规模庞大,问题通常隐藏得比较深)。除非是内核领域拥有极高造诣的开发人员,否则是难以快速定位的。这就好比:除非是领域内的顶尖高手,否则贸然使用转基因技术是很容易出问题的,出现问题后要一个普通人立即指出转基因技术导致了什么问题也是不现实的。

注意:

红帽是一家伟大的公司,红帽的内核也是基于官方内核开发和优化的,在X86平台上运行良好。本书没有任何批评红帽企业版Linux的意思,只是分析了为什么红帽的内核不适宜作为龙芯的基础内核。相反,红帽开发和维护Linux内核的方法和成功经验是非常值得龙芯学习的。

1.3 如何高效阅读代码

阅读软件源代码是每个开发者的必由之路,尤其是内核开发者。因为内核开发在很大程度上并不是重新发明“轮子”,而是深入理解并尽量复用现有的内核设计框架,然后参照相似的功能模块去添加或改写某项需要的功能。在对内核整体框架以及某些子系统融会贯通以后,我们才有可能站在巨人的肩膀上去改进框架本身,实现自主创新。如果过分强调不必要的“自主创新”,可能会让内核的可维护性变差,最终结果反而得不偿失。以作者的个人经验,阅读代码与编写代码的时间大概是6 : 4。

如何高效阅读代码是一个因人而异并且因地制宜的问题。本书在这里给出一种阅读代码的方法,虽然不一定适合每一个人和每一种情况,但是极具参考价值。并且,本书绝大部分代码解析都根据本节所述的方法进行了适当的精简,以方便读者阅读和理解。

自由软件的开发与商业软件相比,有一个很大的不同就是文档相对缺乏。但同时有一种说法叫作“代码就是最好的文档”——只要你愿意,没有什么是学不会的。那么,像Linux内核这样动辄上千万行代码的浩大工程,该如何读懂,又从哪里读起呢?

本书建议用“广度优先”的方式阅读代码,具体来说,当我们阅读一个函数的时候,先看看这个函数整体上一步一步在完成什么工作,等基本掌握要领以后,再逐个去了解其调用的子函数;而不建议使用“深度优先”方法,一上来就把第一个子函数以及子函数的子函数弄清楚。代码好比一棵树,“广度优先”就是说要先找到主干,然后搞清楚主干上有几根树枝,再去某条感兴趣的树枝上寻找有意义的叶子;而“深度优先”则是随便碰到一根树枝,就赶紧深入进去把所有的叶子给找出来。“广度优先”会让你有一种“会当凌绝顶,一览众山小”的自信;而“深度优先”会让你有一种“不识庐山真面目,只缘身在此山中”的迷茫。

本书基于“广度优先”的大原则,提供一种“三部曲”的具体实现方法:找准入口点,理清主脉络,顾名思义看功能。另外,在内核的开发过程中除了原始的源代码还有可能会碰到代码补丁,因此本章在“三部曲”之后专门使用一个小节来介绍补丁文件。

下面我们先进一步细述“三部曲”。

1.3.1 找准入口点

“三部曲”的第一步是找准入口点。本书是一本讲述Linux内核的书,而Linux内核基本上由C语言和汇编语言组成。在C语言里面,应用程序的入口点是main()函数,其实内核也是比较类似的。以相对独立的内核模块为例,在绝大多数情况下,如果模块的名字是modname,那么模块的入口函数就是modname_init()。万一不是,可以在相关文件目录里面搜索module_init,因为模块入口一般会用module_init(mod_entry)或者类似的方式进行声明(这里mod_entry指的是模块的入口函数名字)。

那么内核本身的入口又在哪里呢?凭直觉,凭经验,它总会跟start、init、entry之类的词汇有些关联。初始入口(第一入口)是体系结构相关的,在汇编语言代码里面;而通用入口(第二入口)是体系结构无关的,在C语言里面实际上就是init/main.c里面的start_kernel()。顺便说一下,包括龙芯在内的所有MIPS处理器,初始入口都是arch/mips/ kernel/head.S里面的kernel_entry。CPU执行的第一条内核指令就在初始入口kernel_entry,而kernel_entry之后的代码在完成与体系结构高度相关的初始化以后,会跳转到通用入口start_kernel()执行。

1.3.2 理清主脉络

“三部曲”的第二步是理清主脉络。这一步的原则是“去粗取精”,去掉没用的,留下有用的。眼不见为净,影响阅读的内容直接删除。当然了,我们并不是建议在原始代码库中直接删除,而是建议一边看代码一边做笔记,先把函数复制到笔记里面,然后再根据需要逐步删除,直到留下主脉络。

那么什么叫“有用的”,什么叫“没用的”?既然已经进入了内核源代码库,当然每一句都是有用的,这里所说的有用或没用,仅仅指的是对理解主脉络有利还是不利。而“有利”或者“不利”又是分为多个层次的。

1.代码vs.注释

注释是非常有用的,可以帮助我们理解代码。但是注释在很大程度上是用于了解细节的,而在对主脉络的了解上面,其用处不大。所以,首先考虑去掉注释,包括“//…”和“/*…*/”格式的直接注释,以及“#if 0 … #endif”格式的条件汇编。

2.程序流程vs.变量声明

去掉注释以后,纯粹的代码会变得清爽很多。但是如果代码依旧十分复杂,影响阅读,那么就可以删除一些变量声明和简单的初始化赋值语句,留下重要的程序流程。

3.功能语句vs.调试语句

如果到了这里主脉络依旧不是十分清晰,那么可以开始考虑去掉各种调试和打印语句,比如printf()、printk()、debug()。

4.正常流程vs.异常流程

异常处理是程序健壮性必不可少的部分,但是如果异常处理本身的代码过于复杂,那它势必会影响可读性。因此在必要的时候,为了方便阅读,可以去掉返回值检查,如try-catch中的catch子句等。

5.常见路径vs.罕见路径

通常情况下,代码精简到这一步就已经比较容易理解了。但如果有必要,可以对switchcase、if-else等结构进行处理,只保留最常见的一种情况。

下面举一个实际的例子,代码来源是GXemul。它是一个模拟MIPS(包括龙芯)机器的模拟器软件,代码入口为main()。相比于Linux内核的庞大复杂,GXemul是一个设计精巧的应用程序,非常适合用来举例说明。

int main(int argc, char *argv[])
{
    /*  Setting constants:  */
    const int constant_yes = 1;
    const int constant_true = 1;
    const int constant_no = 0;
    const int constant_false = 0;
    struct emul **emuls;
    char **diskimages = NULL;
    int n_diskimages = 0;
    int n_emuls;
    int i;
    progname = argv[0];
    /*  Initialize all emulator subsystems:  */
    console_init();
    cpu_init();
    device_init();
    machine_init();
    timer_init();
    useremul_init();
    emuls = malloc(sizeof(struct emul *));
    if(emuls == NULL){
        fprintf(stderr, "out of memory\n");
        exit(1);
    }
    /*  Allocate space for a simple emul setup:  */
    n_emuls = 1;
    emuls[0] = emul_new(NULL);
    if(emuls[0] == NULL){
        fprintf(stderr, "out of memory\n");
        exit(1);
    }
    get_cmd_args(argc, argv, emuls[0], &diskimages, &n_diskimages);
    if(!skip_srandom_call){
        struct timeval tv;
        gettimeofday(&tv, NULL);
        srandom(tv.tv_sec ^ getpid()^ tv.tv_usec);
    }
    /*  Print startup message:  */
    debug("GXemul");
    debug("    Copyright(C)2003-2006  Anders Gavare\n");
    debug("Read the source code and/or documentation for other Copyright messages.\n\n");
    if(emuls[0]->machines[0]->machine_type == MACHINE_NONE){
            n_emuls --;
        } else {
            for(i=0; i<n_diskimages; i++)diskimage_add(emuls[0]->machines[0], diskimages[i]);
        }
        /*  Simple initialization, from command line arguments:  */
        if(n_emuls > 0){
            /*  Make sure that there are no configuration files as well:  */
            for(i=1; i<argc; i++)
                if(argv[i][0] == '@'){
                    fprintf(stderr, "You can either start one "
                        "emulation with one machine directly from "
                        "the command\nline, or start one or more "
                        "emulations using configuration files."
                        " Not both.\n");
                    exit(1);
                }
            /*  Initialize one emul:  */
            emul_simple_init(emuls[0]);
        }
        /*  Initialize emulations from config files:  */
        for(i=1; i<argc; i++){
            if(argv[i][0] == '@'){
                char tmpstr[50];
                char *s = argv[i] + 1;
                if(strlen(s)== 0 && i+1 < argc && argv[i+1][0] != '@'){
                    i++;
                    s = argv[i];
                }
                n_emuls ++;
                emuls = realloc(emuls, sizeof(struct emul *)* n_emuls);
                if(emuls == NULL){
                    fprintf(stderr, "out of memory\n");
                    exit(1);
                }
                /*  Always allow slave xterms when using multiple emulations:  */
                console_allow_slaves(1);
                /*  Destroy the temporary emuls[0], since it will be overwritten:  */
                if(n_emuls == 1){
                    emul_destroy(emuls[0]);
                }
                emuls[n_emuls - 1] = emul_create_from_configfile(s);
                snprintf(tmpstr, sizeof(tmpstr), "emul[%i]", n_emuls-1);
            }
        }
        if(n_emuls == 0){
            fprintf(stderr, "No emulations defined. Maybe you forgot to "
                "use -E xx and/or -e yy, to specify\nthe machine type."
                " For example:\n\n    %s -e 3max -d disk.img\n\n"
                "to boot an emulated DECstation 5000/200 with a disk "
                "image.\n", progname);
            exit(1);
    }
    device_set_exit_on_error(0);
    console_warn_if_slaves_are_needed(1);
    /*  Run all emulations:  */
    emul_run(emuls, n_emuls);
    /*
     *  Deinitialize everything:
     */
    console_deinit();
    for(i=0; i<n_emuls; i++)emul_destroy(emuls[i]);
    return 0;
}

这是一个长达106行代码的函数,根据精简原则,我们首先去掉其中的注释(在上述代码中使用粗体标注的部分)。去掉注释后还剩下面92行代码。

int main(int argc, char *argv[])
{
     const int constant_yes = 1;
     const int constant_true = 1;
     const int constant_no = 0;
     const int constant_false = 0;
     struct emul **emuls;
     char **diskimages = NULL;
     int n_diskimages = 0;
     int n_emuls;
     int i;
     progname = argv[0];
     console_init();
     cpu_init();
     device_init();
     machine_init();
     timer_init();
     useremul_init();
     emuls = malloc(sizeof(struct emul *));
     if(emuls == NULL){
          fprintf(stderr, "out of memory\n");
          exit(1);
     }
     n_emuls = 1;
     emuls[0] = emul_new(NULL);
     if(emuls[0] == NULL){
          fprintf(stderr, "out of memory\n");
          exit(1);
     }
     get_cmd_args(argc, argv, emuls[0], &diskimages, &n_diskimages);
     if(!skip_srandom_call){
           struct timeval tv;
           gettimeofday(&tv, NULL);
           srandom(tv.tv_sec ^ getpid()^ tv.tv_usec);
     }
     debug("GXemul");
     debug("    Copyright(C)2003-2006  Anders Gavare\n");
     debug("Read the source code and/or documentation for other Copyright messages.\n\n");
     if(emuls[0]->machines[0]->machine_type == MACHINE_NONE){
          n_emuls --;
     } else {
          for(i=0; i<n_diskimages; i++)diskimage_add(emuls[0]->machines[0], diskimages[i]);
     }
     if(n_emuls > 0){
          for(i=1; i<argc; i++)
              if(argv[i][0] == '@'){
                    fprintf(stderr, "You can either start one "
                         "emulation with one machine directly from "
                         "the command\nline, or start one or more "
                         "emulations using configuration files."
                         " Not both.\n");
                    exit(1);
              }
          emul_simple_init(emuls[0]);
     }
     for(i=1; i<argc; i++){
          if(argv[i][0] == '@'){
               char tmpstr[50];
               char *s = argv[i] + 1;
               if(strlen(s)== 0 && i+1 < argc && argv[i+1][0] != '@'){
                    i++;
                    s = argv[i];
               }
               n_emuls ++;
               emuls = realloc(emuls, sizeof(struct emul *)* n_emuls);
               if(emuls == NULL){
                    fprintf(stderr, "out of memory\n");
                    exit(1);
               }
               console_allow_slaves(1);
               if(n_emuls == 1){
                    emul_destroy(emuls[0]);
               }
               emuls[n_emuls - 1] = emul_create_from_configfile(s);
               snprintf(tmpstr, sizeof(tmpstr), "emul[%i]", n_emuls-1);
         }
     }
     if(n_emuls == 0){
          fprintf(stderr, "No emulations defined. Maybe you forgot to "
               "use -E xx and/or -e yy, to specify\nthe machine type."
               " For example:\n\n    %s -e 3max -d disk.img\n\n"
               "to boot an emulated DECstation 5000/200 with a disk "
               "image.\n", progname);
          exit(1);
     }
     device_set_exit_on_error(0);
     console_warn_if_slaves_are_needed(1);
     emul_run(emuls, n_emuls);
     console_deinit();
     for(i=0; i<n_emuls; i++)emul_destroy(emuls[i]);
     return 0;
}

删除注释以后函数仍然很长,所以我们开始第二次精简,去掉变量声明和简单的赋值语句(在上述代码中使用粗体标注的部分)。去掉变量声明和简单的赋值语句后还剩下面78行代码。

int main(int argc, char *argv[])
{
     console_init();
     cpu_init();
     device_init();
     machine_init();
     timer_init();
     useremul_init();
     emuls = malloc(sizeof(struct emul *));
     if(emuls == NULL){
         fprintf(stderr, "out of memory\n");
         exit(1);
     }
     emuls[0] = emul_new(NULL);
     if(emuls[0] == NULL){
         fprintf(stderr, "out of memory\n");
         exit(1);
     }
     get_cmd_args(argc, argv, emuls[0], &diskimages, &n_diskimages);
     if(!skip_srandom_call){
          gettimeofday(&tv, NULL);
          srandom(tv.tv_sec ^ getpid()^ tv.tv_usec);
     }
     debug("GXemul");
     debug("    Copyright(C)2003-2006  Anders Gavare\n");
     debug("Read the source code and/or documentation for other Copyright messages.\n\n");
     if(emuls[0]->machines[0]->machine_type == MACHINE_NONE){
         n_emuls --;
     } else {
          for(i=0; i<n_diskimages; i++)diskimage_add(emuls[0]->machines[0], diskimages[i]);
     }
     if(n_emuls > 0){
         for(i=1; i<argc; i++)
             if(argv[i][0] == '@'){
                  fprintf(stderr, "You can either start one "
                      "emulation with one machine directly from "
                      "the command\nline, or start one or more "
                      "emulations using configuration files."
                      " Not both.\n");
                  exit(1);
             }
         emul_simple_init(emuls[0]);
     }
     for(i=1; i<argc; i++){
         if(argv[i][0] == '@'){
              if(strlen(s)== 0 && i+1 < argc && argv[i+1][0] != '@'){
                   i++;
                   s = argv[i];
              }
              n_emuls ++;
              emuls = realloc(emuls, sizeof(struct emul *)* n_emuls);
              if(emuls == NULL){
                  fprintf(stderr, "out of memory\n");
                  exit(1);
              }
              console_allow_slaves(1);
              if(n_emuls == 1){
                  emul_destroy(emuls[0]);
              }
              emuls[n_emuls - 1] = emul_create_from_configfile(s);
              snprintf(tmpstr, sizeof(tmpstr), "emul[%i]", n_emuls-1);
         }
     }
     if(n_emuls == 0){
         fprintf(stderr, "No emulations defined. Maybe you forgot to "
             "use -E xx and/or -e yy, to specify\nthe machine type."
             " For example:\n\n    %s -e 3max -d disk.img\n\n"
             "to boot an emulated DECstation 5000/200 with a disk "
             "image.\n", progname);
         exit(1);
     }
     device_set_exit_on_error(0);
     console_warn_if_slaves_are_needed(1);
     emul_run(emuls, n_emuls);
     console_deinit();
     for(i=0; i<n_emuls; i++)emul_destroy(emuls[i]);
     return 0;
}

现在函数看起来比较清爽了,但是仍然不够,因此我们进行第三次精简,去掉各种调试和打印语句(在上述代码中使用粗体标注的部分)。去掉各种调试和打印语句后还剩下面52行代码。

int main(int argc, char *argv[])
{
     console_init();
     cpu_init();
     device_init();
     machine_init();
     timer_init();
     useremul_init();
     emuls = malloc(sizeof(struct emul *));
     emuls[0] = emul_new(NULL);
     get_cmd_args(argc, argv, emuls[0], &diskimages, &n_diskimages);
     if(!skip_srandom_call){
          gettimeofday(&tv, NULL);
          srandom(tv.tv_sec ^ getpid()^ tv.tv_usec);
     }
     if(emuls[0]->machines[0]->machine_type == MACHINE_NONE){
         n_emuls --;
     } else {
          for(i=0; i<n_diskimages; i++)diskimage_add(emuls[0]->machines[0], diskimages[i]);
     }
     if(n_emuls > 0){
         for(i=1; i<argc; i++)
             if(argv[i][0] == '@'){
                  exit(1);
             }
         emul_simple_init(emuls[0]);
     }
     for(i=1; i<argc; i++){
         if(argv[i][0] == '@'){
             if(strlen(s)== 0 && i+1 < argc && argv[i+1][0] != '@'){
                  i++;
                  s = argv[i];
             }
             n_emuls ++;
             emuls = realloc(emuls, sizeof(struct emul *)* n_emuls);
             console_allow_slaves(1);
             if(n_emuls == 1){
                 emul_destroy(emuls[0]);
             }
             emuls[n_emuls - 1] = emul_create_from_configfile(s);
         }
     }
     if(n_emuls == 0){
         exit(1);
     }
     device_set_exit_on_error(0);
     console_warn_if_slaves_are_needed(1);
     emul_run(emuls, n_emuls);
     console_deinit();
     for(i=0; i<n_emuls; i++)emul_destroy(emuls[i]);
     return 0;
}

一般来说,超过一屏的函数或多或少都会影响可读性,因此我们需要进行第四次精简,去掉各种异常处理语句(在上述代码中使用粗体标注的部分)。去掉各种异常处理语句后还剩下面43行代码。

int main(int argc, char *argv[])
{
     console_init();
     cpu_init();
     device_init();
     machine_init();
     timer_init();
     useremul_init();
     emuls = malloc(sizeof(struct emul *));
     emuls[0] = emul_new(NULL);
     get_cmd_args(argc, argv, emuls[0], &diskimages, &n_diskimages);
     if(!skip_srandom_call){
          gettimeofday(&tv, NULL);
          srandom(tv.tv_sec ^ getpid()^ tv.tv_usec);
     }
     if(emuls[0]->machines[0]->machine_type == MACHINE_NONE){
         n_emuls --;
     } else {
         for(i=0; i<n_diskimages; i++)diskimage_add(emuls[0]->machines[0], diskimages[i]);
     }
     if(n_emuls > 0){
         emul_simple_init(emuls[0]);
     }
     for(i=1; i<argc; i++){
          if(argv[i][0] == '@'){
              if(strlen(s)== 0 && i+1 < argc && argv[i+1][0] != '@'){
                  i++;
                  s = argv[i];
              }
              n_emuls ++;
              emuls = realloc(emuls, sizeof(struct emul *)* n_emuls);
              console_allow_slaves(1);
              if(n_emuls == 1){
                  emul_destroy(emuls[0]);
              }
              emuls[n_emuls - 1] = emul_create_from_configfile(s);
         }
     }
     emul_run(emuls, n_emuls);
     console_deinit();
     for(i=0; i<n_emuls; i++)emul_destroy(emuls[i]);
     return 0;
}

对于一个熟练的开发者来说,该函数的逻辑精简到这个状态已经比较清晰了(可以到此为止)。但读者如果是初次接触的话,现在的代码还是相对有点复杂。让我们来进行第五次精简,去掉那些不常用的、罕见的代码路径(在上述代码中使用粗体标注的部分)。去掉不常用的、罕见的代码路径后还剩下面18行代码。

int main(int argc, char *argv[])
{
     console_init();
     cpu_init();
     device_init();
     machine_init();
     timer_init();
     useremul_init();
     emuls = malloc(sizeof(struct emul *));
     emuls[0] = emul_new(NULL);
     get_cmd_args(argc, argv, emuls[0], &diskimages, &n_diskimages);
     for(i=0; i<n_diskimages; i++)diskimage_add(emuls[0]->machines[0], diskimages[i]);
     if(n_emuls > 0)emul_simple_init(emuls[0]);
     emul_run(emuls, n_emuls);
     console_deinit();
     for(i=0; i<n_emuls; i++)emul_destroy(emuls[i]);
     return 0;
}

这就是最终剩下的主脉络,非常清晰明了!那么,这个函数到底在干什么呢?让我们开始“三部曲”的第三步。

1.3.3 顾名思义看功能

前面我们提到,代码就像一棵树,因此本书选用一种树形视图来表示函数。依旧以GXemul为例,前面经过五次精简的main()函数,稍作处理后可以按下面的方法表示。

main()
  |-- console_init();
  |-- cpu_init();
  |-- device_init();
  |-- machine_init();
  |-- timer_init();
  |-- useremul_init();
  |-- emuls[0] = emul_new(NULL);
  |-- get_cmd_args(argc, argv, emuls[0], &diskimages, &n_diskimages);
  |-- for(i=0; i<n_diskimages; i++)diskimage_add(emuls[0]->machines[0], diskimages[i]);
  |-- emul_simple_init(emuls[0]);
  |-- emul_run(emuls, n_emuls);
  |      |-- console_init_main(emuls[0]);
  |      |-- for(j=0; j<e->n_machines; j++)cpu_run_init(e->machines[j]);
  |      |-- timer_start();
  |      |-- for(j=0; j<e->n_machines; j++)machine_run(e->machines[j]);
  |      |-- timer_stop();
  |      |-- for(j=0; j<e->n_machines; j++)cpu_run_deinit(e->machines[j]);
  |      \-- console_deinit_main();  
  |-- console_deinit();
  \-- emul_destroy(emuls[i]);

其中main()函数为根节点,五次精简后的每一行代码为根节点的下级节点,进一步展开感兴趣的下级节点(如树形视图中的emul_run()函数),可以得到更下一级的叶子节点。树形视图中的每行代码,我们甚至不需要看其实现细节,光靠“顾名思义”就能大概知道其功能了。比如,console_init()是控制台初始化,cpu_init()是处理器初始化,device_init()是设备初始化,machine_init()是机器架构初始化,timer_init()是时钟初始化,useremul_init()是用户模拟器初始化,diskimage_add()是添加磁盘设备,emul_simple_init()是模拟机器初始化,而emul_run()显然是核心中的核心,即模拟器运行的主事件循环(通过进一步展开的下级节点也证实了这一点)。

树形视图是一种自顶向下鸟瞰全局的宏观视图,但有时候我们特别希望有一种方法能够清晰地表述某个很深的函数调用,因此作为补充,本书还会采用一种叫作链式视图的表示法。比如本书第6章在讲解Radeon显卡驱动中将提到模式设置函数drm_crtc_helper_set_mode(),在首次模式设置中从驱动入口函数开始一路向下的调用链展示如下。

radeon_init()→ radeon_driver_load_kms()→ radeon_modeset_init()→ radeon_fbdev_
init()→ drm_fb_helper_initial_config()→ drm_fb_helper_single_fb_probe()→ register_
framebuffer()→ do_register_framebuffer()→ fb_notifier_call_chain(FB_EVENT_FB_
REGISTERED)→ fbcon_event_notify(FB_EVENT_FB_REGISTERED)→ fbcon_fb_registered()→ do_
fbcon_takeover()→ do_take_over_console()→ do_bind_con_driver()→ visual_
init()→ fbcon_init()→ drm_fb_helper_set_par()→ restore_fbdev_mode()→ drm_mode_set_
config_internal()→ crtc->funcs->set_config()→ radeon_crtc_set_config()→ drm_crtc_
helper_set_mode()

这是一条很长的调用链,非常具有代表性。

在很多解析源代码的书籍中,都会使用流程图来描述代码逻辑。然而,流程图虽然直观,但是描述能力有限(尤其是缺乏源代码树形视图的层次化表达能力),往往很难精确描述一个函数的执行过程。而一个精心画出来的精确的流程图,往往又会因为复杂性而失去了直观的特点。并且,单靠流程图并不能完全理解源代码,我们还需要将源代码与流程图两相对照。因此本书将会尽量用精简版的源代码(即树形视图和链式视图)来代替流程图,这样可以让读者快速理解多级函数的复杂调用关系,同时不需要在源代码和流程图之间反复切换。以笔者和同事的经验,一个在编程语言方面有一定积累的开发人员,在大多数情况下理解树形视图和链式视图会比理解流程图更加容易。

1.3.4 理解补丁文件

阅读软件源代码,尤其是阅读Linux内核源代码时,不可避免地要接触补丁文件,那么什么是补丁文件呢?其实补丁文件就是一个变更集,它描述了源代码从旧版本到新版本之间的差异变化,或者更一般地说,描述了源代码从一个状态到另一个状态的差异(不一定是从旧版本到新版本)。如果用数学方法来表达,就是

源代码差异 = 源代码状态B - 源代码状态A (1-1)

也可以反过来表达,即

源代码状态A + 源代码差异 = 源代码状态B (1-2)

举个具体的例子,假设当前目录下有linux-4.4.1和linux-4.4.2两个子目录,分别是Linux-4.4.1版本和Linux-4.4.2版本的源代码顶级目录。那么可以用diff命令来执行公式(1-1),导出一个变更集(即源代码差异)到补丁文件kernel.patch。

diff -Naurp linux-4.4.1 linux-4.4.2 > kernel.patch

接下来可以先进入linux-4.4.1目录,用patch命令来执行公式(1-2),通过应用补丁文件kernel.patch将Linux-4.4.1版本的源代码状态变成跟Linux-4.4.2版本一致。

patch -p1 < kernel.patch

上面是对补丁文件的正向应用,使源代码从状态A变成状态B。实际上,补丁文件还可以反向应用,使源代码从状态B变成状态A。比如,先进入linux-4.4.2目录,然后通过反向应用补丁文件kernel.patch将Linux-4.4.2版本的源代码状态变成跟Linux-4.4.1版本一致。

patch -Rp1 < kernel.patch

利用两个目录来保存两个版本的内核源代码,使用diff和patch命令来操作补丁文件的做法是一种非常原始的方法。通常在内核开发中我们推荐使用Git做版本管理工具。Git可以记录源代码变化的版本历史,可以回滚到任意一个历史状态,也可以导出两个版本之间的变更集(即源代码差异)。图1-8是一个Git历史记录的示例(用git log命令查看)。

图中用节点和线条来描述历史演进关系,每个节点代表一个完整的源代码状态(即某一个版本的完整源代码)。在Git的术语里面一个版本节点称为一个commit(提交),用一个40位十六进制数的散列值来表达。Git里面有分支的概念,历史记录是允许分支合并的,也就是说可以有多条历史线同时演进(如图1-7所示,在Git源代码仓库中用git log --graph命令可以查看到类似的结果),本节的示例里面只考虑单条的历史线。

在Git里面我们有更先进的方法导出和应用补丁文件(commit1和commit2分别代表两次commit的散列值)。

导出补丁(公式(1-1)):git diff commit1 commit2 > kernel.patch

应用补丁(公式(1-2)):git apply kernel.patch

这两个git命令导出和应用的补丁称为简单格式补丁,它与diff和patch命令操作的补丁具有相同的格式。但git还可以操作更加强大的正规格式补丁(commit1和commit2分别代表两次commit的散列值)。

导出补丁(公式(1-1)):git format-patch commit1..commit2 -o kernel_patch_dir

应用补丁(公式(1-2)):git am kernel_patch_dir/*.patch

在这两个命令中,如果commit1和commit2相邻,就会导出一个补丁;如果不相邻,就会导出一系列补丁。这些补丁保存在kernel_patch_dir目录中,按版本从早到晚(从旧到新)的顺序,以0001-xxx-yyy.patch、0002-xxx-yyy.patch的格式逐个命名。正规格式补丁导出以后可以直接以电子邮件的形式发送出去,而应用正规格式补丁的同时会自动提交到代码库。一个正规格式的补丁文件包括4大部分内容:头部信息、描述信息、正文区和脚注区。图1-9是一个正规格式补丁文件的具体示例。

图1-9中补丁文件的头部信息指的是前4行,包含了作为电子邮件的commit编号、发送人、补丁日期和邮件标题(邮件标题也是commit标题)。描述信息指的是图中第一个空白行以后、第一个分割线之前的部分,包括补丁内容描述(补丁内容描述也是commit描述)和作者签名(以Signed-off-by开头的两行,如有必要还可以加上审查者签名Reviewed-by、确认者签名Acked-by、报告者签名Reported-by、测试者签名Tested-by等)。接下来从第一个分割线开始到最后一个分割线之前的部分都是正文区,这是最重要的一部分,即补丁的主体部分(简单格式补丁只有正文区部分)。最后的脚注区就是Git的版本号标识。

接下来重点关注补丁文件的正文区。一个补丁文件可以涉及多个源代码文件,涉及的每个源代码文件可以包含多处变更。因此补丁正文区的内容包括三大部分:总体概述(如修改了哪些源文件、增加了多少行、删除了多少行等),文件路径描述(以diff开头的连续4行,其中以“---”开头的行表示旧版本中的源文件路径,以“+++”开头的行表示新版本中的源文件路径),若干个变更区段(以“@@”开始的若干行)。变更区段是补丁内容的最小单位,图1-9所示的补丁文件仅涉及一个源代码文件的一处变更,也就只有一个变更区段。

变更区段的内容有4种行:定位行、上下文行、删除行和增加行。定位行就是以“@@”开头的行,其中的4个数字分别是变更区段在旧版本源文件中的起始行号、总行数以及在新版本源文件中的起始行号、总行数。起始行号允许一定的误差,因此需要配合上下文(区段的前3行与后3行,以及区段中其他以空格开头的行)进一步确定区段的位置;总行数则不允许有任何误差,否则会被认为是一个非法补丁。区段中以“-”开头的行是删除行,代表旧版本源文件里面有而新版本源文件里面没有的行;以“+”开头的行是增加行,代表旧版本源文件里面没有而新版本源文件里面有的行。

那么图1-9中的补丁文件究竟包含了什么信息呢?现在我们可以知道了。

它来自Git代码库中一次散列值为dee809bfaa4caedb56cbc4d842ecf85acbbdb3e1的commit,该commit标题是“drm/amdgpu: Set a suitable dev_info.gart_page_size”,源代码补丁的作者是Rui Wang <wangr@lemote.com>和Huacai Chen <chenhc@lemote.com>,其中后者也是邮件的发送人。这个补丁修改了源代码文件drivers/gpu/drm/amd/amdgpu/ amdgpu_kms.c,修改的位置在旧版源文件和新版源文件的第717行左右,该区段在变更前和变更后的代码行数均为7行。这次变更删除了一行代码。

dev_info.gart_page_size = AMDGPU_GPU_PAGE_SIZE;

同时又在原来的位置增加了一行代码(实质上就是修改了一行代码)。

dev_info.gart_page_size = max((int)PAGE_SIZE, AMDGPU_GPU_PAGE_SIZE);

在实际的内核开发过程中,代码补丁往往比这个例子要复杂很多,但是原理是相同的。理解了补丁文件的原理,在阅读源代码及其变更历史的时候就会如虎添翼。例如,我们已经知道Linux-3.15版本的内核加入了龙芯3A的支持,但是如果直接查看Linux-3.15版本的完整源代码,会发现相关的代码延伸非常广泛,遍及多个子系统、几十个目录、上百个文件。面对这种情况,想要在一个早期的内核版本上移植相同的功能,简直无从下手。那么,如何才能“干净利落”又“完整无缺”地分离出那些跟龙芯3号有关的一项项功能呢?答案就是查看Git记录,导出系列补丁,然后按顺序逐个分析解读。在理解这一系列补丁的基础上,如果需要在一个早期的内核版本(如Linux-3.12版本)上添加龙芯3号的支持,这并不是一件非常困难的事情。

关于diff、patch、git等命令的更多用法可参阅相应的使用指南。

1.4 如何开发健壮内核

内核是整个操作系统的基础。从某种意义上来说,内核最重要的是稳定性,这甚至比功能和性能更重要。如果说功能和性能是一棵树的枝叶,那么稳定性就是一棵树的根基。而内核稳定性的根本来源就是内核代码的健壮性。

如何开发并维护一个健壮的Linux内核?首先我们必须选择一个良好的基础版本,然后在基础版本上增加龙芯特有的扩展特性。这一点已经在1.2.4节中详细介绍过,多数情况下我们采取的原则是以Linux官方的长期维护稳定版本为基础。除此之外还需要注意如下几点:① 采用规范的代码风格;② 合理地生成补丁系列;③ 谨慎地对待自主创新。

1.4.1 内核代码风格

规范的代码风格具有良好的可读性和可维护性。因此,一方面我们不仅要编写代码,而且要编写漂亮的代码;另一方面我们不仅要解决问题,而且要优雅地解决问题。

在整个代码库里面,应当使用统一的代码风格。内核的代码风格规范可直接查阅自带文档Documentation/CodingStyle及其中文版Documentation/zh_CN/CodingStyle。原文太长,本书仅摘录重点部分。

1.命名

变量、函数等均采用下划线命名法,不采用驼峰式命名法和匈牙利命名法。例如,CopyFromUser()是错误的,而copy_from_user()是正确的。

2.缩进

○ 一律采用制表符(即Tab键)缩进,不使用空格缩进。

○ 几乎所有{}包括的代码块都要缩进,只有switch-case不需要缩进。

3.行长

为了保证代码能在最老的终端上完整显示,通常一行最多允许80个字符。超过80个字符应当在合适的地方换行,续行时必须缩进。但在注释里面,如果超过80个字符能带来更好的可读性,则可放宽要求。

4.括号与空格

○ 用于整个函数的{},左括号独占一行。

○ 用于结构体、循环体、switch-case、if-else的{},左括号不另起新行。

○ 不另起新行的左括号之前必须有空格。

○ 所有的(),在左括号之前和右括号之后都必须有空格。

○ 双目操作符的两边,分隔符(即 , 和 ; 两个符号)的后边都必须有空格。

5.注释格式

○ 单行注释使用/* xxxxxxxx */,不使用// xxxxxxx。

○ 多行注释使用如下格式:

   /* xxxxxxxx
    * yyyyyyyy
    * zzzzzzzzz
    */

6.举例说明

比如,下面的例子是严重不符合内核代码风格的。

int TestFunction()
{
      int i,x; 
      for(i=0;i<5;i++){  
            switch(x) //x is the condition
            {
                 case 0:
                       do_something;
                 case 1:
                       do_something;
                 default:
                       do_something;
            }
            do_something;
     }
     return 0;
}

这个例子里面至少有8处错误,可以说本节前面提到的每一条规则都有违反,而规范的代码风格应该如下。

int test_function()
{
      int i, x;
      for(i=0; i<5; i++){
           switch(x){  /* x is the condition */
           case 0:
                do_something;
           case 1:
                do_something;
           default:
                do_something;
           }
           do_something;
      }
      return 0;
} 

1.4.2 合理生成补丁

这里假定内核开发者都使用Git作为版本管理工具,Git可以通过提交历史记录生成补丁系列,因此合理生成补丁也意味着开发者需要合理使用Git。我们必须牢记以下原则。

○ Git记录必须全程有机可回溯。

○ 补丁质量与代码质量同样重要。

○ 日志信息与代码质量同样重要。

○ 每次提交必须是一个完整的功能单位。

通常在内核中引入一项完整的功能会涉及多个文件甚至多个子系统(这里不考虑单个的微小功能)。在分解补丁系列时也是有学问的,比如把所有的修改放到一次提交(即一个补丁)里面是不合理的,应当将一个个子功能分解进行多次提交(即生成一个补丁系列)。

错误做法:按物理单位(文件)分解。比如一项新特性的引入需要增加A、B两个子功能,涉及x、y、z共3个文件,这里将所有的代码变更分解成x.patch、y.patch和z.patch。

正确做法:按逻辑单位(功能)分解。比如一项新特性的引入需要增加A、B两个子功能,涉及x、y、z共3个文件,这里将所有的代码变更分解成A.patch和B.patch(A.patch、B.patch都有可能同时涉及多个文件)。如果多个子功能存在依赖,那么必须依照依赖关系的顺序来提交。比如B依赖A时,应当先提交A,后提交B。

如何检验一个补丁系列是否符合上面的原则呢?这个问题可以精益求精,但是至少应该达到以下两项标准。

1. 按照顺序以增量方式逐个应用补丁系列中的补丁时,每应用一个补丁(每提交一次),都必须保证内核能够顺利构建、顺利运行。

2. 如果补丁系列涉及增加Kconfig中的配置项,通常需要在配置项所涉及的代码功能已经全部加入以后再增加配置项本身。

举个例子,对龙芯3A1000的支持是在Linux-3.15版本时融合到官方内核的,当时生成的补丁系列分解如下(仅列出标题,详细内容请查阅内核的Git记录)。

○ MIPS: Loongson: Add basic Loongson-3 definition。

○ MIPS: Loongson: Add basic Loongson-3 CPU support。

○ MIPS: Loongson 3: Add Lemote-3A machtypes definition。

○ MIPS: Loongson: Add UEFI-like firmware interface(LEFI)support。

○ MIPS: Loongson 3: Add HT-linked PCI support。

○ MIPS: Loongson 3: Add IRQ init and dispatch support。

○ MIPS: Loongson 3: Add serial port support。

○ MIPS: Loongson: Add swiotlb to support All-Memory DMA。

○ MIPS: Loongson: Add Loongson-3 Kconfig options。

○ MIPS: Loongson 3: Add Loongson-3 SMP support。

○ MIPS: Loongson 3: Add CPU hotplug support。

○ MIPS: Loongson: Add a Loongson-3 default config file。

在1.3.4小节中我们已经理解了补丁文件的内容,现在我们知道了补丁系列的分解非常重要。补丁系列的合理分解主要是提高补丁文件正文区的质量,而补丁文件中的提交标题和提交日志其实也同样重要。

提交标题通常应当遵循“子系统: 主题描述”的格式,如本节例子中的第一次补丁提交标题,子系统是“MIPS: Loongson:”,而主题描述是“Add basic Loongson-3 definition”。主题描述应当是一个概括性的单句,可以在一行中完整显示。提交日志应当如实描述补丁正文所做的事情,重点描述“为什么”和“怎么做”,而不是“做什么”,因为“做什么”在良好的补丁正文中是不言自明的。

1.4.3 谨慎对待创新

自主创新是个好东西,在某种意义上创新本身就是能力的体现,因此大家都喜欢创新。然而,作为一个Linux内核开发者,我建议大家谨慎对待自主创新,也请大家牢记以下几点。

○ 保持自由开放,人人为我,我为人人。

○ 集众人之智,采众家之长,消化吸收再创新。

○ 站在巨人的肩膀上,站得更高才能看得更远。

“自由、开放、共享,人人为我,我为人人”是Linux的哲学。经过多年的发展,内核现有的功能,尤其一些基础设施是非常完善的。当你撸起袖子准备大干一番的时候,请千万千万抑制住内心的冲动,并保持一个开发者所应有的谦虚。Linux内核使用非常广泛,通常大家都会面对一些同类的问题,换句话说就是你面临一个全新问题的概率非常小。所以,在很大程度上你不需要重新发明“轮子”,而是需要深入理解并尽量复用现有的内核设计框架,然后参照相似的功能模块去增加新功能或者扩充已有的功能(所谓“少即是多”)。“磨刀不误砍柴工”,动手之前最好先去了解一下你想做的事情是否已经有类似的、成熟的解决方案。这就是开源社区正常的运转机制:集众人之智,采众家之长,消化吸收再创新。

站得更高才能看得更远,在对内核整体框架以及某些子系统融会贯通以后,你自然有机会站在巨人的肩膀上去改进框架本身或者解决全新的问题,实现真正的自主创新。如果过分强调不必要的“自主创新”,执着于重新发明“轮子”,必定会让自己的贡献难以融入官方内核;最终的结局可能是自己开发的内核可维护性越来越差,反而得不偿失。

笔者作为龙芯3号的管理员,从事Linux内核开发多年。最主要的贡献其实是消化吸收龙芯中科开发者所提供的原始代码,通过“取其精华、去其糟粕”的方式整理重构使之符合Linux内核的代码规范,然后在保证健壮性的基础上将龙芯的特色功能以补丁系列的方式逐步融入官方内核。内核中由我主导的“自主创新”部分其实并不多,主要有如下几点。

○ 龙芯3号的动态变频(CPUFreq)功能。

○ 龙芯3号的CPU热插拔功能与自动调核功能。

○ 龙芯3号的待机(STR)与休眠(STD)功能。

○ 龙芯3号“固件—内核接口规范”中的设备树(Device-Tree)功能。

○ 龙芯3号的KEXEC快速启动功能(包括基于KEXEC的KDUMP功能)。

“天行健,君子以自强不息;地势坤,君子以厚德载物”。我们提倡小心谨慎以避免为了创新而创新,我们更提倡加强知识积累并在融会贯通的基础上自主创新。在知识积累的过程中,我们强烈建议大家参与开源社区的互动。通过向开源社区提交内核补丁,可以得到与社区“大神”直接交流的机会,而这个过程能够有效地提升我们的技能,帮助我们开发出更加健壮的内核代码。

向开源社区提交补丁与驱动程序的具体方法可查阅内核自带文档Documentation/SubmittingPatches,Documentation/SubmittingDrivers和Documentation/SubmitChecklist。这些文档也提供了中文版,即Documentation/zh_CN/SubmittingPatches与Documentation/zh_CN/SubmittingDrivers。

[1] 仅高配版支持通过HTO进行芯片间互联,高配版亦称龙芯3B2000。

[2] 仅高配版支持通过HTO进行芯片间互联,高配版亦称龙芯3B3000。

[3] 在操作系统里面,堆(Heap)和栈(Stack)有着严格的区分,但在有些文献里面把栈称为堆栈。

[4] 硬件维护一致性的协议主要有两大类:侦听协议和目录协议。其基本方法都是本地Cache(如一级Cache)与共享Cache(如二级Cache)之间通过消息传递来维护每个Cache行的状态机。侦听协议要求每个CPU核都随时侦听总线上的消息,通信代价较大。目录协议则让每个CPU核负责一个Cache目录,每个Cache目录对应整个Cache里面的一个子集;CPU核是Cache目录的宿主,本地Cache传递消息的时候直接发往宿主而不是广播到总线。龙芯使用的目录协议中,Cache行的状态分三种:INV(无效,表示没有缓存任何数据)、SHD(共享,表示多个本地Cache里面都有相同的数据副本,只能读不能写)和EXC(独占,表示有一个本地Cache要写数据,因此其他本地Cache中的数据副本都要无效化)。

相关图书

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

相关文章

相关课程