精通Linux内核:智能设备开发核心技术
上QQ阅读APP看书,第一时间看更新

第1章 基于Linux内核的操作系统

Linux内核第一版发布于1991年,如今最新版本已经到了5.x。最初它仅仅像是一只五脏俱全的小麻雀,发展到现在浩瀚如海,代码量也已经超过了千万行;最初基于Intel x86的PC,如今囊括了x86和ARM等主流平台在内的几十个平台;现在已经有CentOS、Debian、Fedora、openSUSE、Ubuntu、Red Hat Enterprise Linux和Android等基于Linux的操作系统。Android的崛起,更是将Linux带到了大众面前,如今不仅仅影响着数以万计的程序员,连人民大众的生活也与它息息相关。

1.1 处理器、平台和操作系统

处理器,也就是我们常说的中央处理器(Central Processing Unit, CPU),它是一台计算机的运算中心和控制中心,简单地说,它负责解释执行指令,并处理数据。

但是仅仅一个处理器是无法运行的,必须有一套与处理器配合的硬件,它们与处理器一起构成一个平台。不同的处理器要求的硬件是不同的,比如x86处理器,一般配套的有南桥、北桥芯片和存储BIOS的芯片等。

操作系统(Operating System, OS)是管理计算机硬件和软件资源的程序,是运行在平台上最基本的软件,其他软件以它为基础。

处理器运行操作系统用于管理应用程序的执行,应用程序通过操作系统使用平台的硬件,达到预期的运行效果,满足用户的需求。

内核是操作系统最基本的部分,也是操作系统与硬件关系最密切的一部分,控制系统对硬件的访问。同样,Linux内核是它的操作系统的基础,包含了内存管理、文件系统、进程管理和设备驱动等核心模块。一个基于Linux内核的操作系统,一般应该包含以下几个部分。

(1)BootLoader:比如GRUB和SYSLINUX,它们负责将内核加载进内存,在系统上电或者BIOS初始化完成后执行加载。

(2)init程序:负责启动系统的服务和操作系统的核心程序。

(3)必要的软件库:比如加载elf文件的ld-linux.so、支持C程序的库,再比如GNU C Library(简称glibc)、Android的Bionic。

(4)必要的命令和工具:比如shell命令和GNU coreutils等。coreutils是GNU下的一个软件包,提供ls等常用的命令。

1.2 以安卓为例剖析操作系统

Android最初是由Andy Rubin开发的,后被谷歌收购,如今已经风靡全球。它基于Linux内核,主要用于移动设备领域,起初应用于智能手机和平板电脑,随后扩展到智能电视、车载系统和可穿戴设备。

Android的成功不仅仅源于操作系统本身,还包括一系列配套的工具、文档和全球数以万计的开发者,成百上千万的应用,如今已经形成了一个完整且庞大的生态。

从事Android的开发者主要分为以下三个领域。

(1)应用开发者:利用系统的接口(SDK)开发运行在移动设备上的应用,主要使用Java、Kotlin和C++等高级编程语言。

(2)Framework和类库开发者:负责构建某个模块的架构,定义、维护接口,主要使用Java、C++和C语言。

(3)驱动和内核开发者:包括设备驱动开发和平台验证等工作,主要使用C和C++语言,多任职于设备公司和器件厂商。

1.2.1 安卓的整体架构

与开发者的划分对应,Android操作系统的架构也是层次分明的,如图1-1所示。

图1-1 Android操作系统架构图

显然,从应用层开始,到Linux内核,都是上一层依赖于下一层。

Linux内核是整个架构的基础,它负责管理内存、电源、文件系统和进程等核心模块,提供系统的设备驱动。

硬件抽象层可以保护芯片厂商的知识产权,避开Linux的GPL协议。由于内核是开源的,部分芯片厂商保密算法的代码,可以以二进制文件的形式,提供给手机等设备制造公司集成在硬件抽象层。

它的另一个主要作用是屏蔽硬件的差异。Linux内核与设备驱动的联系紧密,可以根据系统的要求将数据和控制标准化。比如加速度传感器,它们的驱动报告值的单位由芯片和驱动共同决定,不同的传感器可能有差异,但Android系统要求的单位是统一的(比如m/s2),否则应用将不得不处理硬件差异,硬件抽象层负责将得到的数据转化为Android要求的数值。

需要说明的是,从应用层到硬件抽象层都属于用户空间,硬件抽象层并不属于内核,它与内核虽然逻辑上联系紧密,但也不能直接调用内核的函数,一般调用类库提供的接口实现,比如Bionic。图1-1所示的Android操作系统的架构图描述的只是逻辑关系,并不是函数调用关系,从函数调用关系角度看,libc(Bionic的一部分)与内核的关系更加紧密。

Framework和类库是Android的核心,也是Android与其他以Linux内核为基础的操作系统最大的区别。它规定并实现系统的标准、接口,构建Android的框架,比如数据存储、显示、服务和应用程序管理等。

应用层与最终用户关系距离最近,利用Framework的接口,接收设备驱动的数据,根据用户的指令,将控制传递至设备驱动。

以上是Android系统整体架构,针对某一个模块,比如Sensor(传感器),系统运行时的架构如图1-2所示。

图1-2 Sensor架构图

图中箭头表示控制和数据传递关系,控制由APP向下传递,数据则由下向上。

图中涉及三个进程,APP1、APP2和Service,APP和Service之间传递控制和数据都需要通过进程通信实现,Service将控制传递至HAL,最终到设备驱动,HAL得到驱动产生的数据,报告至Service,由Service分发给APP1和APP2。

从函数调用角度来看,Framework、Service和HAL都可能调用library,最终进入内核,Framework可能需要通过内核打开、关闭文件或与其他进程通信等,但它并不会直接访问设备驱动。仅保留Service和HAL的唯一访问设备驱动的通路,有助于设备的单一控制。如果APP进程直接控制驱动,会造成混乱。

需要说明的是,并不是每一个模块都需要HAL,比如Touch(触摸屏),Touch不需要APP控制,屏幕亮起即工作,关屏则关闭,Touch驱动的数据单位就是点坐标,也不需要转化,所以它不需要HAL。

不仅仅是Sensor,任何一个模块,只要它与某个硬件有关,哪怕只是申请内存,都绕不开内核。下面从操作系统的几个核心功能看看Android与内核的关系。

进程管理:进程这个概念本身是由内核实现的,还包括进程调度、进程通信等。

Android利用Pthread等实现了它的进程、线程,是对内核系统调用的封装。另外,Android还引入Binder通信,Binder也不是仅仅依靠用户空间就可以完成的,它也是需要驱动的,目前Linux内核的代码已经包含了Binder的驱动代码。

内存管理:内存、内存映射和内存分配、回收等内核都已实现。

Android也实现了特有的ION内存管理机制,可以使用它在用户空间申请大段连续的物理内存。与Binder一样,ION的驱动代码也已经存在于最新的内核中了。

文件系统:内核实现了文件系统的框架和常用的文件系统。

文件系统不仅关乎数据存储,进程、内存和设备等都离不开它。对Linux而言,几乎是“一切皆文件”,Android延续了这种理念。

用户界面:Android开发了一套控件(View)供应用开发人员使用,背后有一套完整的显示架构支持它们,包括Framework中的Window Manager和Library中的Surface Manager等。除此之外,Android还开发了虚拟机(Dalvik),运行Java开发的应用程序。

设备驱动:设备驱动由内核提供,需要随系统启动而运行的设备驱动,一般在内核启动过程中加载,某些在特定情况下才会使用的设备驱动(比如Wifi),在设备使用时被加载,前者一般被编译进内核,后者以ko文件的形式存在。

如果把整个Android比作一个房子,内核就是地基,加上Android架构,就成了一个毛坯房,再加上应用开发者开发的应用才是一个可以拎包入住的房子。如此看来,一部新手机算是精装交付了。

1.2.2 Linux内核的核心作用

Linux内核与操作系统是底层基础和上层建筑的关系,内核提供了基本功能和概念,操作系统在此基础上根据自身的定位和特色实现自身的架构,提供接口和工具来支持应用开发,由应用体现其价值。

开机后,内核由bootloader加载进入内存执行,它是创建系统的第一个进程,初始化时钟、内存、中断等核心功能,然后执行init程序。init程序是基于Linux内核的操作系统,在用户空间的起点,它启动核心服务,挂载文件系统,更改文件权限,由后续的服务一步步初始化整个操作系统。

内核实现了内存管理、文件系统、进程管理和网络管理等核心模块,将用户空间封装内核提供的系统调用作为类库,供其他部分使用。

内核并不是最底层的,下面还有硬件层,即内核驱动硬件、系统的数据来源于硬件、最终的结果也要靠硬件去体现,内核是系统与硬件的桥梁。

内存管理、文件系统和进程管理是本书讨论的重点,接下来的章节中读者可以看到它们实现的细节。

1.3 内核整体架构

内核的代码比较复杂,一方面是代码量大,另一方面是模块间交叉,一个问题往往关联多个模块。好在内核维护的过程中,代码的结构一直比较清晰,本节将对内核的整体架构进行介绍。

1.3.1 内核代码的目录结构

内核源代码中一级子目录如下。

Documentation目录:存放说明文档,没有代码。内核中一些复杂或者专业的模块会有帮助文档,涉及它们的背景和总结等,读者困惑的时候可以查看该目录下是否有相关说明。

arch目录:arch是architecture的简称,包含了与体系结构相关的代码,下面的每一个子目录都表示内核支持的一种体系结构,比如x86和ARM等。系统中有些特性的实现与具体的体系结构相关,比如内存页表和进程上下文等,这部分代码基本都会存放在此,其他目录存放的代码多是共性的。

kernel目录:内核的核心部分,包含进程调度、中断处理和时钟等模块的核心代码,它们与体系结构相关的代码存放在arch/xxx/kernel下。

drivers目录:设备驱动代码集中存放于此,体量庞大,随着内核代码更新不断引入新的硬件驱动。

mm目录:mm是memory management的缩写,包含内存管理相关的代码,这部分代码与体系结构无关,与体系结构相关的代码存放在arch/xxx/mm下。

fs目录:fs是file system的缩写,包含文件系统的代码,涉及文件系统架构(VFS)和系统支持的各种文件系统,一个子目录至少对应一种文件系统,比如proc子目录对应proc文件系统。

ipc目录:ipc是inter process communication的缩写,包含了消息队列、共享内存和信号量等进程通信方式的实现。

block目录:包含块设备管理的代码,块设备与字符设备对应。前者支持随机访问,SD卡和硬盘等都是块设备;后者只能顺序访问,键盘和串口等都是字符设备。

lib目录:包含公用的函数库,比如红黑树和字符串操作等。通过上节的分析可以知道,内核处在C语言库(glibc等)的下一层。实际上,glibc是由封装内核的系统调用实现的,所以在内核中编程不能使用它们,应该使用的是内核提供的库(不限于lib目录)。新手可能会有类似“为什么不能使用C标准库的printf在内核中打印调试信息”的问题,这就是原因。

init目录:包含内核初始化相关的代码,其中的main.c定义了内核启动的入口start_kernel函数。

firmware目录:包含运行在芯片内的固件,固件也是一种软件,只不过由芯片执行,而不是CPU。

scripts目录:包含辅助内核配置的脚本,比如运行make menuconfig命令配置内核时,由它们提供支持。

剩下的目录中,net、crypto、certs、security、tools和virt等分别与网络、加密、证书(certificates)、安全、工具和虚拟化等相关。

鉴于篇幅,本书不讨论网络相关的话题,主要涉及从arch到init这些目录。

1.3.2 内核的核心模块及关联

内核中的模块较多,而且关系错综复杂,整体结构如图1-3所示。

图1-3 内核模块关系图

首先是几个相对独立的模块,比如中断(异常)、时钟和内核同步等,它们依赖于硬件和具体的体系结构,对其他模块依赖较小。它们也都属于基础模块,用于为其他模块提供支持。

中断模块不仅在设备驱动中频繁使用,内存管理和进程调度等也需要它的支持,内存管理需要缺页中断,进程调度则需要时钟中断和处理器间中断等。

时钟设备一方面是系统计算时间的基础,另一方面提供时钟中断,与大多数模块都有关联。

内核同步模块贯穿整个内核,如果没有同步机制,错综复杂的执行流访问临界区域就会失去保护,系统瞬间瘫痪。

内存管理、文件管理和进程管理算得上是内核中除了网络外最复杂的三个模块,也是本书讨论的重点。说它们最复杂与drivers目录体量庞大并不矛盾:drivers目录包含了成千上万个设备的驱动,单算一个驱动其实并不复杂。

内存管理模块涉及内存寻址、映射、虚拟内存和物理内存空间的管理、缓存和异常等。

文件系统的重要性从“一切皆文件”这句话就可以体现出来,硬件的控制、数据的传递和存储,几乎都与文件有关。

进程管理模块涉及进程的实现、创建、退出和进程通信等,进程本身是管理资源的载体,管理的资源包括内存、文件、I/O设备等,所以它与内存管理和文件系统等模块都有着紧密的关系。本书先介绍内核管理和文件系统,最后介绍进程管理,希望读者能够对进程有更清晰地认识。

至于设备驱动模块,从功能角度来看,每一个设备驱动都是一个小型的系统,电源管理、内存申请、释放、控制、数据,复杂一些的还会涉及进程调度等,它与前面几个模块都有着密切关系。所以对内核函数的熟悉程度,很大程度上决定了一个驱动工程师的效率;对其他模块的理解程度,很大程度上决定了他的高度。

1.4 实例分析

在本书中,每一章结尾都会添加两个小节,其内容有些是与章节内容相关的实例,有些是知识点总结和心得体会,统一命名为“实例分析”。它们以讨论逻辑和方法为目的,多数并不局限于某一领域,但为了方便理解,本书依旧会将在某一个具体领域内讨论它们,在其他领域内的使用也是类似的。

1.4.1 系统响应“点击智能手机触摸屏”的过程

曾几何时,我一直有个疑问:当移动鼠标的时候,屏幕上的光标随之移动,是如何实现的?

智能手机时代来临,这个疑问就变成了:点击或者滑动手机屏幕,屏幕随之改变,这是如何实现的?

这些看似简单的动作,实际上需要硬件和操作系统的多个部分协作完成。

首先是触摸屏芯片,点击触摸屏引起信号变化,芯片监测到信号后,计算出当前触摸的位置(坐标),然后通过中断模块通知CPU“有事报告”。

CPU得到中断后,根据中断信息,调用触摸屏驱动的处理函数。驱动接下来读取触摸的坐标,通过input子系统报告给操作系统。

操作系统经过一系列“辗转腾挪”,将点的坐标信息传递给应用,过程中可以涉及进程通信、IO多路复用等。

应用得到点坐标后,根据点的位置和当前的界面布局做出判断,决定下一步需要进行的操作,比如,点落在空白区域被忽略、落在按钮上触发按钮操作、落在App图标上启动应用等。

这是由下到上的过程,接下来应用还需要根据相应的操作刷新屏幕,假设它希望改变某个图标的颜色,通过函数调用将该请求传递至操作系统,操作系统通过计算得到新屏幕中的各图层,又经过一系列“辗转腾挪”,屏幕的驱动得到了刷新界面的指令,获取数据刷新界面,这是一个由上到下的过程。

1.4.2 智能手机的传感器游戏

智能手机有很多传感器游戏(应用),比如激流快艇、微信摇一摇、统计每天走动步数的App等,它们都是使用手机传感器的数据作为计算的依据。

有了触摸屏的例子后,理解传感器游戏就水到渠成了,它们的原理是一样的,只不过将触摸屏芯片换成了传感器芯片。

但是App需要的传感器是不同的,比如,激流快艇游戏,需要的是陀螺仪的数据;统计步数的App,需要的是加速度传感器的数据。这些传感器的精确度和采样频率对用户体验的影响较大,比如陀螺仪,高端手机必备,低端手机上可能并没有安装,这种情况下的陀螺仪数据是通过其他传感器的数据计算得到的,精确度不足,采样频率也达不到要求,操作会有明显的滞后和偏差。

从流程角度看,传感器与触摸屏大同小异,同样是由芯片开始,经过操作系统的传递,数据到达应用,然后计算并刷新屏幕。不同的地方在于,系统可能会根据策略对数据做一定的处理,比如在低端手机上没有陀螺仪的情况下,根据加速度和磁力传感器的数据计算得到陀螺仪的数据。

换作另外一个桌面操作系统,可能就不需要做类似的处理,因为它并不执着于得到陀螺仪的数据。触摸屏的例子也是如此,如果它不需要支持触控操作,也不需要实现中间的“辗转腾挪”。内核是操作系统的基础,是它们的通用部分,操作系统根据自身的定位决定它需要在此基础上实现的功能,定制它的特有算法和流程。