1.4 简析Linux内核
由于Android内核和Linux内核有密切的联系,因此在学习Android底层驱动开发之前必须先了解Linux内核的基本知识。本节将简要介绍Linux内核的基本应用知识,为读者学习本书后面的高级知识打下基础。
1.4.1 内核的体系结构
图1-1所示是一个完整操作系统最基本的视图,由此可见内核的作用就是将应用程序和硬件分离开来。
图1-1 操作系统的基本视图
内核的主要任务是负责与计算机硬件进行交互,实现对硬件的编程控制和接口操作,调度对硬件资源的访问。除此之外,内核为用户应用程序提供一个高级的执行环境和访问硬件的虚拟接口。提供硬件的兼容性是内核的设计目标之一,几乎所有的硬件都可以得到Linux的支持,只要不是为其他操作系统所定制的。
与硬件兼容性相关的是可移植性,即在不同的硬件平台上运行Linux的能力。内核从最初只支持标准IBM兼容机上的Intel X86架构到现在可以支持Alpha、ARM、MIPS、PowerPC等几乎所有硬件平台,如此广泛的平台支持之所以能够成功,部分原因在于内核清晰地划分为了体系相关部分和体系无关部分。
如图1-2所示是Linux操作系统的基本视图。由此可见,Linux内核分为如下两部分。
- 体系相关部分:这部分内核为体系结构和硬件所特有。
- 体系无关部分:这部分内核是可移植的。体系无关部分通常会定义与体系相关部分的接口,这样,内核向新的体系结构移植的过程就变成确认这些接口的特性并将它们加以实现的过程。
用户应用程序和内核之间的联系是通过它和内核的中间层——标准C库来实现的。标准C库函数是建立在内核提供的系统调用基础之上的。通过标准C库及内核体系无关部分与体系相关部分的接口,用户应用程序和部分内核都成为可移植的。根据上面的描述,给出Linux操作系统的标准视图,如图1-3所示。
图1-2 Linux操作系统的基本视图
图1-3 Linux系统的标准视图
图1-3中标准视图主要构成模块的具体说明如下所示。
(1)系统调用接口
为了与用户应用程序进行交互,内核提供了一组系统调用接口,通过这组接口,应用程序可以访问系统硬件和各种操作系统资源。系统调用接口层在用户应用程序和内核之间添加了一个中间层,形象地说,它扮演了一个函数调用多路复用和多路分解器的角色。
(2)进程管理
进程管理负责创建和销毁进程,并处理它们之间的互相联系(进程间通信),同时负责安排调度它们去分享CPU。
进程管理部分实现了一个进程世界的抽象,这个进程世界类似于我们人类世界,只不过我们人类世界里的个体是人,而在进程世界里则是一个一个的进程;人与人之间通过书信、手机、网络等进行交互,而各个进程之间则通过不同方式的进程间通信进行交互;我们所有人都在分享同一个地球,而所有进程都在分享一个或多个CPU。
(3)内存管理
在进程世界里,内存是重要的资源之一,就好比我们的土地一样。因此,管理内存的策略与方式是决定系统性能的一个关键因素。内核的内存管理部分根据不同的需要,提供了包括malloc/free在内的许多简单或者复杂的接口,并为每个进程都提供了一个虚拟的地址空间,基本上实现了虚拟内存对进程的按需分配。
(4)虚拟文件系统
虚拟文件系统为用户空间提供了文件系统接口,同时又为各个具体的文件系统提供了通用的接口抽象。在VFS上面是对诸如open、close、read和write等函数的一个通用API抽象,在VFS下面则是具体的文件系统,它们定义了上层函数的实现方式。
通过虚拟文件系统,我们可以利用标准的Linux文件系统调用对不同介质上的不同文件系统进行操作。VFS是内核在各种具体的文件系统上建立的一个抽象层,它提供了一个通用的文件系统模型,而该模型囊括了我们所能想到的所有文件系统的行为。
(5)网络功能
网络子系统处理数据包的收集、标识、分发,以及路由和地址的解析等所有网络有关的操作。socket层是网络子系统的标准API,它为各种网络协议提供了一个用户接口。
(6)设备驱动程序
操作系统的目的是为用户提供一种方便访问硬件的途径,因此,几乎每一个系统操作最终都会映射到物理的硬件设备上。除了CPU、内存等有限的几个对象外,所有设备的访问控制操作都要由相关的代码来完成,这些代码就是所谓的设备驱动程序。
(7)代码
这里的代码需要依赖体系结构,因为部分内核代码是体系相关的,在“linux/arch”子目录中定义了内核源代码中依赖体系结构的部分,其中包含了对应各种特定体系结构的子目录。比如,对于一个典型的桌面系统来说,使用的是i386目录。
每个特定体系结构对应的子目录又包含了很多下级子目录,分别关注内核中的一个特定方面,比如引导、内核、内存管理等。
1.4.2 和Android密切相关的Linux内核知识
我们知道,Android是在Linux 2.6的内核基础之上运行的,提供的核心系统服务包括安全、内存管理、进程管理、网络组和驱动模型等内容。在接下来的内容中,将简要讲解上述核心系统服务的基本知识。
1.安全
关于Linux安全的知识主要涉及用户权限问题和目录权限问题。
(1)Linux系统中的用户和权限
Linux系统中的每个文件和目录都有访问权限,用它来确定谁可以通过何种方式对文件和目录进行访问和操作。Linux系统中规定了文件有如下三种不同类型的用户。
- user:文件拥有者。
- group:同组用户,和网上邻居中的group组类似。
- others:可以访问系统的其他用户。
(2)文件及目录权限的功能
Linux系统还规定了访问上述三种文件或目录的方式:读(r)、写(w)、可执行或查找(x)。
- 读权限(r):表示只允许指定用户读取相应文件的内容,禁止对它做任何的更改操作,如目录读权限表示可以列出存储在该目录下的文件,即读目录内容。
- 写权限(w):表示允许指定用户打开并修改文件,如目录写表示允许你从目录中删除或创建新的文件或目录。
- 执行权限(x):表示允许将该文件作为一个程序执行。文件被创建时,文件所有者自动拥有对该文件的读、写和可执行权限,以便于对文件的阅读和修改。
Linux系统在创建文件时会自动把该文件的读写权限分配给其属主,使用户能够显示和修改该文件。也可以将这些权限改变为其他的组合形式。一个文件若有执行权限,则允许它作为一个程序被执行。
2.内存管理
内存管理是计算机编程最为基本的领域之一。虽然在很多脚本语言中,作为开发人员来说无须担心内存是如何管理的,但是这并不能降低内存管理的重要性。对实际编程来说,理解内存管理器的能力与局限性至关重要。在大部分系统语言中必须进行内存管理,例如C和C++。
其实在运行整个系统时,系统有多少内存,我们就有多少内存。我们无须花费心思和精力去弄清楚它到底有多少内存,因为每一台机器的内存数量都是相同的。所以,如果内存需要非常固定,那么只需要选择一个内存范围并使用它即可。
但是即使是在这样一个简单的计算机中也会有问题,尤其是当我们不知道程序的每个部分将需要多少内存时。如果空间有限,而内存需求是变化的,那么需要用一些方法来满足下面的需求。
- 确定是否有足够的内存来处理数据。
- 从可用的内存中获取一部分内存。
- 向可用内存池(pool)中返回部分内存,以使其可以由程序的其他部分或者其他程序使用。
实现上述需求的程序库称为分配程序(allocator),因为它们负责分配和回收内存。程序的动态性越强,内存管理就越重要,你的内存分配程序的选择也就更重要。我们要了解可用于内存管理的不同方法,它们的好处与不足,以及它们最适用的情形。
3.进程管理
在Linux操作系统中包括如下三种不同类型的进程。
- 交互进程:由一个Shell启动的进程。交互进程既可以在前台运行,也可以在后台运行。
- 批处理进程:批处理进程和终端没有联系,是一个进程序列。
- 守护进程:系统守护进程是Linux系统启动时启动的进程,在后台运行。
Linux管理进程的最好方法就是使用命令行下的系统命令。Linux下面的进程涉及的命令有ps、kill、pgrep等。
(1)父进程和子进程
父进程和子进程的关系是管理和被管理的关系,当父进程终止时,子进程也随之被终止。但如果子进程被终止,父进程并不一定终止。比如当httpd服务器运行时,我们可以杀掉其子进程,父进程并不会因为子进程的终止而终止。在进程管理中,当我们发现占用资源过多或存在无法控制的进程时,应该杀死它,以保护系统的稳定安全运行。
(2)进程命令
在Linux中,通过命令来管理和操作进程。常用的命令可以分为如下几类。
①监视进程命令
·ps(process status命令)用于显示瞬间进程的动态,其使用方式如下所示。
ps [options] [--help]
ps的参数非常多,常用参数的具体说明如下所示。
◦-a:显示所有终端机下执行的进程,除了阶段作业领导者之外。
◦-w:采用宽阔的格式显示进程状况。
◦-au:显示较详细的进程状况。
◦-aux:显示所有包含其他用户的进程。
·pstree命令:功能是以树状图显示运行的程序。pstree指令用ASCII字符显示树状结构,清楚地表达程序间的相互关系。如果不指定程序识别码或用户名称,则会把系统启动时的第一个程序视为基层,并显示之后的所有程序。若指定用户名称,便会以隶属该用户的第一个程序当做基层,然后显示该用户的所有程序。其使用方式如下所示。
pstree [-a] [-c] [-h|-Hpid] [-l] [-n] [-p] [-u] [-G|-U] [pid|user] pstree -V
常用参数的具体说明如下所示。
◦-a:显示每个程序的完整指令,包含路径、参数或常驻服务的标识。
◦-c:如果有重复的进程名,则分开列出(预设值会在前面加上*)。
·top命令:Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器。其使用方式如下所示。
top [-] [d delay] [q] [c] [S] [s] [i] [n] [b]
常用参数的具体说明如下所示。
◦d:指定每两次屏幕信息刷新之间的时间间隔,可以使用s交互命令来改变。
◦q:该选项将使top没有任何延迟地进行刷新。如果调用程序有超级用户权限,那么top将以尽可能高的优先级运行。
◦c:显示整个命令行,而不只是显示命令名。
◦S:切换到累计模式。
◦s:安全模式,避免潜在的危机。
◦i:不显示任何闲置(idle)或无用(zombie)的进程。
◦n:更新的次数,完成后将会退出top。
②控制进程命令
向Linux系统的内核发送一个系统操作信号和某个程序的进程标识号,系统内核就可以操作进程标识号指定的进程。例如在top命令中会看到系统运行的许多进程,有时就需要使用kill中止某些进程来增加系统资源。在安装和登录命令中使用多个虚拟控制台的作用是当一个程序出错造成系统死锁时可以切换到其他虚拟控制台工作关闭这个程序。此时使用的命令就是kill,因为kill是大多数Shell内部命令可以直接调用的。
在Linux系统中,使用kill命令来控制进程。kill可以删除执行中的程序或工作,可以将指定的信息送至程序,预设的信息为SIGTERM(15),可将指定程序中止。如果仍然无法中止该程序,可以使用SIGKILL(9)信息尝试强制删除程序。程序或工作的编号可利用ps指令或jobs指令查看。
使用kill命令的方式有如下两种。
kill [-s<信息名称或编号>][程序] kill [-l<信息编号>]
各个参数的具体说明如下所示。
- -l<信息编号>:如果不加<信息编号>选项,则-l参数会列出全部的信息名称。
- -s<信息名称或编号>:指定要送出的信息。
注意:进程是Linux系统中一个非常重要的概念。Linux是一个多任务的操作系统,系统中经常同时运行着多个进程。我们不关心这些进程究竟是如何分配的,或者内核是如何管理分配时间片的,所要关心的是如何去控制这些进程,让它们能够很好地为用户服务。
③进程优先级设定(nice命令)
在当前程序运行优先级基础之上调整指定值得到新的程序运行优先级,用新的程序运行优先级运行命令行“command[arguments...]”。优先级的范围为-20~19共40个等级,其中数值越小优先级越高,数值越大优先级越低,即-20的优先级最高,19的优先级最低。若调整后的程序运行优先级高于-20,则以优先级-20来运行命令行;若调整后的程序运行优先级低于19,则以优先级19来运行命令行。若nice命令未指定优先级的调整值,则以默认值10来调整程序运行优先级,即在当前程序运行优先级基础之上增加10。若不带任何参数运行命令nice,则显示出当前的程序运行优先级。
使用nice命令的方式如下所示。
nice [-n adjustment] [-adjustment] [--adjustment=adjustment] [--help] [--version] [command [arg...]
各个参数的具体说明如下所示。
- -n adjustment,-adjustment,--adjustment=adjustment:都将该原有优先级增加adjustment。
- --help:显示帮助信息。
- --version:显示版本信息。
4.设备驱动程序
既然是一本讲解内核和驱动开发的书籍,就不可避免地要讲解设备驱动程序的知识。设备驱动程序用于与系统连接的输入/输出设备通信,如硬盘、软驱、各种接口、声卡等。按照经典的UNIX箴言“万物皆文件”(everything is a file)这一说法,对外设的访问可利用“/dev”目录下的设备文件来完成,程序对设备的处理完全类似于常规的文件。设备驱动程序的任务在于支持应用程序通过设备文件与设备通信。
通常可以将外设分为以下两类。
- 字符设备:提供连续的数据流,应用程序可以顺序地读取,通常不支持随机存取。相反,此类设备支持按字节/字符来读写数据。例如,调制解调器是典型的字符设备。
- 块设备:应用程序可以随机访问设备数据,程序可自行确定读取数据的位置。硬盘是典型的块设备,应用程序可以寻址磁盘上的任何位置,并由此读取数据。此外,数据的读写只能以块(通常是512B)的倍数进行。与字符设备不同,块设备并不支持基于字符的寻址。
编写块设备的驱动程序比字符设备要复杂得多,因为内核为提高系统性能广泛地使用了缓存机制。
5.网络
网卡也可以通过设备驱动程序控制,但在内核中属于特殊状况,因为网卡不能利用设备文件访问。原因在于在网络通信期间,数据打包到了各种协议层中。在接收到数据时,内核必须针对各协议层的处理,对数据进行拆包与分析,然后才能将有效数据传递给应用程序。在发送数据时,内核必须首先根据各个协议层的要求打包数据,然后才能发送。
为支持通过文件接口处理网络连接(按照应用程序的观点),Linux使用了源于BSD的套接字抽象。套接字可以看做应用程序、文件接口、内核的网络实现之间的代理。