0%

概述

操作系统中管理分层存储器体系的部分称为存储管理器(memory manager)。它的任务是有效地管理内存,即记录哪些内存是正在使用的,哪些内存是空闲的;在进程需要时为其分配内存,在进程使用完后释放内存。

这里会研究几个不同的存储管理方案。

无存储器抽象

最简单的存储器抽象就是根本没有抽象,每一个程序都直接访问物理内存。

地址空间

概述

要使多个应用程序同时处于内存中并且不互相影响,需要解决两个问题:保护和重定位。不太好的解决方式:

  • 给内存块标记上一个保护键,并且比较执行进程的键和其访问的每个内存字的保护键。
  • 在程序被装载时重定位程序,这是一个缓慢且复杂的解决方法。

一个更好的办法是创造一个新的存储器抽象:地址空间。地址空间为程序创造了一种抽象的内存,它是一个进程可用于寻址内存的一套地址集合。每个进程都有一个自己的地址空间,并且这个地址空间独立于其他进程的地址空间(除了在一些特殊情况下进程需要共享它们的地址空间外)。

比较难的是给每个程序一个自己独有的地址空间,使得一个程序中的地址28所对应的物理地址与另一个程序中的地址28所对应的物理地址不同。有一种比较简单的方法是:基址寄存器与界限寄存器

这个简单的解决办法是使用动态重定位,简单地把每个进程的地址空间映射到物理内存的不同部分。当使用基址寄存器和界限寄存器时,程序装载到内存中连续的空闲位置且装载期间无须重定位,当一个进程运行时,程序的起始物理地址装载到基址寄存器中,程序的长度装载到界限寄存器中。

每次一个进程访问内存,取一条指令,读或写一个数据字,CPU硬件会在把地址发送到内存总线前,自动把基址值加到进程发出的地址值上。同时,它检査程序提供的地址是否等于或大于界限寄存器里的值。如果访问的地址超过了界限,会产生错误并中止访问。

使用基址寄存器和界限寄存器重定位的缺点是,每次访问内存都需要进行加法和比较运算。比较运算可以做得很快,但是加法运算由于进位传递时间的问题,在没有使用特殊电路的情况下会显得很慢。

实际上,所有进程所需的RAM数量总和通常要远远超出存储器能够支持的范围,有两种处理内存超载的通用方法:

  • 交换技术,即把一个进程完整调入内存,使该进程运行一段时间,然后把它存回磁盘。空闲进程主要存储在磁盘上,所以当它们不运行时就不会占用内存
  • 虚拟内存,该策略甚至能使程序在只有一部分被调入内存的情况下运行

交换技术

交换技术的操作如下图所示:

交换技术

开始时内存中只有进程A。之后创建进程B和C或者从磁盘将它们换入内存。图d显示A被交换到磁盘。然后D被调入,B被调出,最后A再次被调入。由于A的位置发生变化,所以在它换入的时候通过软件或者在程序运行期间(多数是这种情况)通过硬件对其地址进行重定位。例如,基址寄存器和界限寄存器就适用于这种情况。

交换在内存中产生了多个空闲区(hole,也称为空洞),通过把所有的进程尽可能向下移动,有可能将这些小的空闲区合成一大块。该技术称为内存紧缩(memory compaction),通常不进行这个操作,因为它要耗费大量的CPU时间。例如,一台有16GB内存的计算机可以每8ns复制8个字节,它紧缩全部内存大约要花费16s。

如果进程的数据段可以增长,例如,很多程序设计语言都允许从堆中动态地分配内存,那么当进程空间试图增长时,就会出现问题。若进程与一个全闲区相邻,那么可把该空闲区分配给进程供其增大。另一方面,若进程相邻的是另一个进程,那么要么把需要增长的进程移到内存中一个足够大的区域中去,要么把一个或多个进程交换出去,以便生成一个足够大的空闲区。若一个进程在内存中不能增长,而且磁盘上的交换区也已满了,那么这个进程只有挂起直到一些空间空闲(或者可以结束该进程)。

如果大部分进程在运行时都要增长,为了减少因内存区域不够而引起的进程交换和移动所产生的开销,一种可用的方法是,当换入或移动进程时为它分配一些额外的内存。然而,当进程被换出到磁盘上时,应该只交换进程实际上使用的内存中的内容,将额外的内存交换出去是一种浪费。

如果进程有两个可增长的段,例如,供变量动态分配和释放的作为堆使用的一个数据段,以及存放普通局部变量与返回地址的一个堆栈段,则可使用另一种安排:进程的堆栈段在进程所占内存的顶端并向下增长,紧接在程序段后面的数据段向上增长。在这两者之间的内存可以供两个段使用。如果用完了,进程或者必须移动到足够大的空闲区中(它可以被交换出内存直到内存中有足够的空间),或者结束该进程。

空闲内存管理

在动态分配内存时,操作系统必须对其进行管理。一般而言,有两种方法跟踪内存使用情况:

  • 位图
  • 空闲区链表

如下图所示:

空闲内存管理

位图

使用位图方法时,内存可能被划分成小到几个字或大到几千字节的分配单元。每个分配单元对应于位图中的一位,0表示空闲,1表示占用(或者相反)。

分配单元的大小是一个重要的设计因素。分配单元越小,位图越大。若进程的大小不是分配单元的整数倍,那么在最后一个分配单元中就会有一定数量的内存被浪费了。

在决定把一个占n个分配单元的进程调入内存时,存储管理器必须捜索位图,在位图中找出有n个连续0的串。査找位图中指定长度的连续0串是耗时的操作(因为在位图中该串可能跨越字的边界),这是位图的缺点。

链表

另一种记录内存使用情况的方法是,维护一个记录已分配内存段和空闲内存段的链表。其中链表中的一个结点或者包含一个进程,或者是两个进程间的一块空闲区。

进程表中表示终止进程的结点中通常含有指向对应于其段链表结点的指针,因此段链表使用双向链表可能要比单向链表更方便。这样的结构更易于找到上一个结点,并检査是否可以合并。

虚拟内存

概述

程序大于内存的问题早在计算时代伊始就产生了,虽然存储器容量增长快速,但是软件大小的增长更快。这一发展的结果是,需要运行的程序往往大到内存无法容纳,而且必然需要系统能够支持多个程序同时运行,它们很容易超出了内存大小。交换技术并不是一个具有吸引力的解决方案,因为一个典型SATA磁盘的峰值传输率髙达每秒好几百兆,这意味着需要好几秒才能换出或换入一个1GB的程序。

虚拟内存的基本思想是:每个程序拥有自己的地址空间,这个空间被分割成多个块,每一块称作一页(page),毎一页有连续的地址范围。这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时,由硬件立刻执行必要的映射。当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的指令。

从某个角度来讲,虚拟内存是对基址寄存器和界限寄存器的一种综合。

分页

大部分虚拟内存系统中都使用一种称为分页(paging)的技术,当程序执行指令MOV REG, 1000时,它把地址为1000的内存单元的内容复制到REG中(或者相反)。地址可以通过索引、基址寄存器、段寄存器或其他方式产生。

由程序产生的这些地址称为虚拟地址(virtual address)**,它们构成了一个虚拟地址空间(virtual address space)。在没有虚拟内存的计算机上,系统直接将虚拟地址送到内存总线上,读写操作使用具有同样地址的物理内存字;而在使用虚拟内存的情况下,虚拟地址不是被直接送到内存总线上,而是被送到内存管理单元(Memory Management Unit,MMU)**, MMU把虚拟地址映射为物理内存地址。

举例如下:

分页

在这个例子中,有一台可以产生16位地址的计算机,地址范围从0到64K-1,且这些地址是虚拟地址。然而,这台计算机只有32KB的物理内存,因此,虽然可以编写64KB的程序,但它们却不能被完全调人内存运行。在磁盘上必须有一个最多64KB的程序核心映像的完整副本,以保证程序片段在需要时能被调入内存。

虚拟地址空间按照固定大小划分成被称为页面(page)**的若干单元。在物理内存中对应的单元称为页框(page frame)**。页面和页框的大小通常是一样的,在本例中是4KB。

当程序试图访问地址0时,将虚拟地址0送到MMU。MMU看到虚拟地址落在页面0(0~4095),根据其映射结果,这一页面对应的是页框2 (8192〜12287),因此MMU把地址变换为8192,并把地址8192送到总线上。内存对MMU无所知,它只看到一个读或写地址8192的请求并执行它。MMU从而有效地把所有从0〜4095的虚拟地址映射到了8192-12287的物理地址。

当程序访问了一个未映射的页面,例如访问地址32780:MMU注意到该页面没有被映射(在图中用叉号表示),于是使CPU陷入到操作系统,这个陷阱称为**缺页中断或缺页错误(page fault)**。操作系统找到一个很少使用的页框且把它的内容写入磁盘(如果它不在磁盘上)。随后把需要访问的页面读到刚才回收的页框中,修改映射关系,然后重新启动引起陷阱的指令。

页表

页表的目的是把虚拟页面映射为页框。作为一种最简单的实现,虚拟地址到物理地址的映射可以槪括如下:虚拟地址被分成虚拟页号(髙位部分)和偏移量(低位部分)两部分。例如,对于16位地址和4KB的页面大小,髙4位可以指定16个虚拟页面中的一页,而低12位接着确定了所选页面中的字节偏移量(0~4095)。

虚拟页号可用作页表的索引,以找到该虚拟页面对应的页表项。由页表项可以找到页框号(如果有的话)。然后把页框号拼接到偏移量的髙位端,以替换掉虚拟页号,形成送往内存的物理地址。

—个典型的页表项:[Data][髙速缓存禁止位][访问位保护位][修改位][“在/不在”位][页框号]

  • 页框号:通过它找到真正的物理地址
  • “在/不在”位:表示该表项对应的虚拟页面在不在内存中,访问该页面是否会引起缺页中断
  • 保护位:指出一个页允许什么类型的访问,是否启用读、写、执行该页面。
  • 修改(modified)位和访问(referenced)位:在写入一页时由硬件自动设置修改位。该位在操作系统重新分配页框时是非常有用的。如果一个页面已经被修改过,则必须把它写回磁盘。如果一个页面没有被修改过,则只简单地把它丢弃就可以了,因为它在磁盘上的副本仍然是有效的。这一位有时也被称为脏位(dirty bit),因为它反映了该页面的状态。
  • 高速缓存禁止位:用于禁止该页面被髙速缓存。对那些映射到设备寄存器而不是常规内存的页面而言,这个特性是非常重要的。假如操作系统正在紧张地循环等待某个I/O设备对它刚发出的命令作出响应,保证硬件是不断地从设备中读取数据而不是访问一个旧的被髙速缓存的副本是非常重要的。

应该注意的是,若某个页面不在内存中,用于保存该页面的磁盘地址不是页表的组成部分。原因很简单,页表只保存把虚拟地址转换为物理地址时硬件所需要的信息。操作系统在处理缺页中断时需要把该页面的磁盘地址等信息保存在操作系统内部的软件表格中。硬件不需要它。

虚拟内存本质上是用来创造一个新的抽象槪念——地址空间,这个槪念是对物理内存的抽象,类似于进程是对物理处理器(CPU)的抽象。虚拟内存的实现,是将虚拟地址空间分解成页,并将每一页映射到物理内存的某个页框或者(暂时)解除映射。

在任何分页系统中,都需要考虑两个主要问题:

  1. 虚拟地址到物理地址的映射必须非常快。
  2. 如果虚拟地址空间很大,页表也会很大。

加速分页过程

转换检测缓冲区

为计算机设置一个小型的硬件设备,将虚拟地址直接映射到物理地址,而不必再访问页表。这种设备称为转换检测缓冲区(Translation Lookaside Buffer, TLB),有时又称为相联存储器(associate memory)或快表。

这种解决方案的建立基干这样一种观察:大多数程序总是对少量的页面进行多次的访问,而不是相反。因此,只有很少的页表项会被反复读取,而其他的页表项很少被访问。

它通常在MMU中,包含少量的表项,每个表项记录了一个页面的相关信息,包括虚拟页号、页面的修改位、保护码(读/写/执行权限)和该页所对应的物理页框。除了虚拟页号(不是必须放在页表中),这些域与页表中的域是一一对应的。另外还有一位用来记录这个表项是否有效(即是否在使用)。

软件TLB管理

在过去,对TLB的管理和TLB的失效处理都完全由MMU硬件来实现,只有在内存中没有找到某个页面时,才会陷入到操作系统中。但是,许多现代的机器几乎所有的页面管理都是在软件中实现的,在这些机器上,TLB表项被操作系统显式地装载。当发生TLB访问失效时,不再是由MMU到页表中査找并取出需要的页表项,而是生成一个TLB失效并将问题交给操作系统解决。系统必须先找到该页面,然后从TLB中删除一个项,接着装载一个新的项,最后再执行先前出错的指令。当然,所有这一切都必须在有限的几条指令中完成,因为TLB失效比缺页中断发生得更加频繁。

如果TLB大到(如64个表项)可以减少失效率时,TLB的软件管理就会变得足 够有效。这种方法的最主要的好处是获得了一个非常简单的MMU,这就在CPU芯片上为髙速缓存以及 其他改善性能的设计腾出了相当大的空间。

大内存页表

多级页表

一个二多级页表如下所示:

多级页表

考虑32位虚拟地址0x00403004,它的虚拟地址对应PT1 = 1,PT2 = 3, Offset = 4。MMU首先用PT1作为索引访问顶级页表得到表项1,它对应的地址范围是4M到8M-1。然后,它用PT2作为索引访问刚刚找到的二级页表并得到表项3,它对应的虚拟地址范围是在它的4M块内的12288〜16383(即绝对地址4206592-4 210687)。这个表项含有虚拟地址0x00403004所在页面的页框号。如果该页面不在内存中,页表项中的“在/不在”位将是0,引发一次缺页中断。如果该页面在内存中,从二级页表中得到的页框号将与偏移量(4)结合形成物理地址。该地址被放到总线上并送到内存中。

倒排页表

当虚拟地址空间比物理内存大得多的时候,倒排页表节省了大量的空间,但是从虚拟地址到物理地址的转换会变得很困难。

页面置换算法

“页面置换”问题在计算机设计的其他领域中也同样会发生。例如高速缓存的更换等。

若每执行几条指令程序就发生一次缺页中断,那么就称这个程序发生了颠簸

最优页面置换算法

该算法是这样工作的:在缺页中断发生时,有些页面在内存中,其中有一个页面(包含紧接着的下一条指令的那个页面)将很快被访问,其他页面则可能要到10、100或1000条指令后才会被访问,每个页面都可以用在该页面首次被访问前所要执行的指令数作为标记。

这个算法唯一的问题就是它是无法实现的。

最近未使用页面置换算法

为使操作系统能够收集有用的统计信息,在大部分具有虚拟内存的计算机中,系统为每一页面设置了两个状态位。当页面被访问(读或写)时设置R位:当页面被写入(即修改)时设置M位。这些位包含在每个页表项中,每次访问内存时更新这些位,因此由硬件来设置它们是必要的。一旦设置某位为1,它就一直保持1直到操作系统将它复位。

当启动一个进程时,它的所有页面的两个位都由操作系统设置成0, R位被定期地(比如在每次时钟中断时)清零,以区别最近没有被访问的页面和被访问的页面。当发生缺页中断时,操作系统检査所有的页面并根据它们当前的R位和M位的值,把它们分为4类:

  • 第0类:没有被访问,没有被修改
  • 第1类:没有被访问,已被修改
  • 第2类:已被访问,没有被修改
  • 第3类:已被访问,已被修改

NRU(Not Recently Used,最近未使用)算法随机地从类编号最小的非空类中挑选一个页面淘汰。这个算法隐含的意思是,在最近一个时钟滴答中(典型的时间是大约20ms)淘汰一个没有被访问的已修改页面要比淘汰一个被频繁使用的“干净”页面好。NRU的主要优点是易于理解和能够有效地被实现,虽然它的性能不是最好的,但是已经够用了。

先进先出页面置换算法

另一种开销较小的页面置换算法是FIFO(First-In First-Out,先进先出)算法。由操作系统维护一个所有当前在内存中的页面的链表,最新进入的页面放在表尾,最早进入的页面放在表头。当发生缺页中断时,淘汰表头的页面并把新调入的页面加到表尾。

第二次机会页面置换算法

FIFO算法可能会把经常使用的页面置换出去,为了避免这一问题,对该算法做一个简单的修改:检査最老页面的R位。如果R位是0,那么这个页面既老又没有被使用,可以立刻置换掉:如果是1,就将R位清0,并把该页面放到链表的尾端,修改它的装入时间使它就像刚装入的一样,然后继续捜索。这一算法称为第二次机会(second chance)算法。

时钟页面置换算法

尽管第二次机会算法是一个比较合理的算法,但它经常要在链表中移动页面,既降低了效率又不是很有必要。一个更好的办法是把所有的页面都保存在一个类似钟面的环形链表中,一个表针指向最老的页面。当发生缺页中断时,算法首先检査表针指向的页面,如果它的R位是0就淘汰该页面,并把新的页面插入这个位置,然后把表针前移一个位置:如果R位是1就清除R位并把表针前移一个位置。重复这个过程直到找到了一个R位为0的页面为止。

最近最少使用页面置换算法

最近最少使用页面置换算法(LRU)在理论上是可以实现的,但代价很髙。为了完全实现LRU,需要在内存中维护一个所有页面的链表,最近最多使用的页面在表头,最近最少使用的页面在表尾。困难的是在每次访问内存时都必须要更新整个链表。在链表中找到一个页面,删除它,然后把它移动到表头是一个非常费时的操作,即使使用硬件实现也一样费时(假设有这样的硬件)。

然而,还是有一些使用特殊硬件实现LRU的方法。首先考虑一个最简单的方法,这个方法要求硬件有一个64位计数器C,它在每条指令执行完后自动加1,每个页表项必须有一个足够容纳这个计数器值的域。在每次访问内存后,将当前的C值保存到被访问页面的页表项中。一旦发生缺页中断,操作系统就检査所有页表项中计数器的值,找到值最小的一个页面,这个页面就是最近最少使用的页面。

最不常用页面置换算法

前面一种LRU算法虽然在理论上是可以实现的,但只有非常少的计算机拥有这种硬件。因此,需要一个能用软件实现的解决方案。一种可能的方案称为NFU(Not Frequently Used,最不常用)算法。该算法将每个页面与一个软件计数器相关联,计数器的初值为0。每次时钟中断时,由操作系统扫描内存中所有的页面,将每个页面的R位(它的值是0或1)加到它的计数器上。这个计数器大体上跟踪了各个页面被访问的频繁程度。发生缺页中断时,则置换计数器值最小的页面。

工作集页面置换算法

在单纯的分页系统里,刚启动进程时,在内存中并没有页面。在CPU试图取第一条指令时就会产生一次缺页中断,使操作系统装入含有第一条指令的页面。其他由访问全局数据和堆栈引起的缺页中断通常会紧接着发生。一段时间以后,进程需要的大部分页面都已经在内存了,进程开始在较少缺页中断的情况下运行。这个策略称为请求调页(demand paging),因为页面是在需要时被调入的,而不是预先装入。

大部分进程都表现出一种局部性访问行为,即在进程运行的任何阶段,它都只访问较少的一部分页面。一个进程当前正在使用的页面的集合称为它的工作集,如果整个工作集都被装入到了内存中,那么进程在运行到下一运行阶段(例如,编译器的下一遍扫描)之前,不会产生很多缺页中断。

所以不少分页系统都会设法跟踪进程的工作集,以确保在让进程运行以前,它的工作集就已在内存中了。该方法称为工作集模型。在进程运行前预先装入其工作集页面也称为预先调页(prepaging)。

为了实现工作集模型,操作系统必须跟踪哪些页面在工作集中。通过这些信息可以直接推导出一个合理的页面置换算法:当发生缺页中断时,淘汰一个不在工作集中的页面。为了实现该算法,就需要一种精确的方法来确定哪些页面在工作集中。根据定义,工作集就是最近A次内存访问所使用过的页面的集合。

一种常见的近似方法就是,不是向后找最近A次的内存访问,而是考虑其执行时间。一个进程从它开始执行到当前所实际使用的CPU时间总数通常称作当前实际运行时间。通过这个近似的方法,进程的工作集可以被称为在过去的实际运行时间中它所访问过的页面的集合。

该算法工作方式如下:假定使用硬件来置R位和M位,同样,假定在每个时钟滴答中,有一个定期的时钟中断会用软件方法来清除R位。每当缺页中断发生时,扫描页表以找出一个合适的页面淘汰之。在处理毎个表项时,都需要检査R位:

  • 如果它是1,就把当前实际时间写进页表项的“上次使用时间”域,以表示缺页中断发生时该页面正在被使用。既然该页面在当前时钟滴答中已经被访问过,那么 很明显它应该出现在工作集中,并且不应该被删除。
  • 如果R是0,那么表示在当前时钟滴答中,该页面还没有被访问过,则它就可以作为候选者被置换。为了知道它是否应该被置换,需要计算它的生存时间(即当前实际运行时间减去上次使用时间)。如果它的生存时间大于t,那么这个页面就不再在工作集中,而用新的页面置换它。扫描会继续进行以更新剩余的表项。

然而,如果R是0同时生存时间小于或等于t,则该页面仍然在工作集中。这样就要把该页面临时保留下来,但是要记录生存时间最长(“上次使用时间”的最小值)的页面。如果扫描完整个页表却没有找到适合被淘汰的页面,也就意味着所有的页面都在工作集中。在这种情况下,如果找到了一个或者多个R = 0的页面,就淘汰生存时间最长的页面。在最坏情况下,在当前时间滴答中,所有的页面都被访问过了(也就是都有R=l),因此就随机选择一个页面淘汰,如果有的话最好选一个干净页面。

工作集时钟页面置换算法

当缺页中断发生后,需要扫描整个页表才能确定被淘汰的页面,因此基本工作集算法是比较费时的。有一种改进的算法,它基于时钟算法,并且使用了工作集信息,称为WSClock(工作集时钟)算法,由于它实现简单,性能较好,所以在实际工作中得到了广泛应用。

工作集时钟页面置换算法

与时钟算法一样,所需的数据结构是一个以页框为元素的循环表,参见图a,最初,该表是空的。当装入第一个页面后,把它加到该表中。随着更多的页面的加入,它们形成一个环。每个表项包含来自基本工作集算法的上次使用时间,以及R位(已标明)和M位(未标明)。

与时钟算法一样,每次缺页中断时,首先检査指针指向的页面。如果R位被置为1,该页面在当前时钟滴答中就被使用过,那么该页面就不适合被淘汰。然后把该页面的R位置为0,指针指向下一个页面,并重复该算法。该事件序列之后的状态参见图b。

如果R位被置为0,如果页面的生存时间大于t并且该页面是干净的,它就不在工作集中,并且在磁盘上有一个有效的副本。申请此页框,并把新页面放在其中,如图d所示。另一方面,如果此页面被修改过,就不能立即申请页框,因为这个页面在磁盘上没有有效的副本。为了避免由于调度写磁盘操作引起的进程切换,指针继续向前走,算法继续对下一个页面进行操作。毕竟,有可能存在一个旧的且干净的页面可以立即使用。

总结

算法 注释
最优算法 不可实现,但可用作基准
NRU(最近未使用)算法 LRU的粗糙版
FIFO(先进先出)算法 可能抛弃重要页面
第二次机会算法 比FIFO有较大的改善
时钟算法 现实
LRU(最近最少使用)算法 很优秀,但很难实现
最不经常使用算法 LRU的相对粗略的近似
老化算法 非常近似LRU的有效算法
工作集算法 实现起来开销很大
工作集时钟算法 好的有效算法

分页系统的设计问题

局部/全局分配策略

三个进程A、B、C构成了可运行进程的集合。假如A发生了缺页中断,页面置换算法在寻找最近最少使用的页面时是只考虑分配给A的页面呢?还是考虑所有在内存中的页面?

负载控制

即使是使用最优页面置换算法并对进程采用理想的全局页框分配,系统也可能会发生颠簸。事实上,一旦所有进程的组合工作集超出了内存容量,就可能发生颠簸。

减少竞争内存的进程数的一个好方法是将一部分进程交换到磁盘,并释放他们所占有的所有页面。即使是使用分页,交换也是需要的,只是现在交换是用来减少对内存潜在的需求,而不是收回它的页面。

页面大小

分离的指令空间和数据空间

共享页面

共享库

可以使用其他的粒度取代单个页面来实现共享。如果一个程序被启动两次,大多数操作系统会自动共享所有的代码页面,而在内存中只保留一份代码页面的副本。代码页面总是只读的,因此这样做不存在任何问题。

依赖于不同的操作系统,每个进程都拥有一份数据页面的私有副本,或者这些数据页面被共享并且被标记为只读。如果任何一个进程对一个数据页面进行修改,系统就会为此进程复制这个数据页面的一个副本,并且这个副本是此进程私有的,也就是说会执行“写时复制”。

现代操作系统中,有很多大型库被众多进程使用,例如,处理浏览文件以便打开文件的对话框的库和多个图形库。把所有的这些库静态地与磁盘上的每一个可执行程序绑定在一起,将会使它们变得更加庞大。

一个更加通用的技术是使用共享库。

内存映射文件

共享库实际上是一种更为通用的机制——内存映射文件(memory-mapped file)的一个特例。这种机制的思想是:进程可以通过发起一个系统调用,将一个文件映射到其虚拟地址空间的一部分。在多数实现中,在映射共享的页面时不会实际读入页面的内容,而是在访问页面时才会被每次一页地读入,磁盘文件则被当作后备存储。当进程退出或显式地解除文件映射时,所有被改动的页面会被写回到磁盘文件中。

内存映射文件提供了一种I/O的可选模型。可以把一个文件当作一个内存中的大字符数组来访问,而不用通过读写操作来访问这个文件。在一些情况下,程序员发现这个模型更加便利。如果两个或两个以上的进程同时映射了同一个文件,它们就可以通过共享内存来通信。一个进程在共享内存上完成了写操作,此刻当另一个进程在映射到这个文件的虚拟地址空间上执行读操作时,它就可以立刻看到上一个进程写操作的结果。因此,这个机制提供了一个进程之间的高带宽通道,而且这种应用很普遍(甚至扩展到用来映射无名的临时文件)。很显然,如果内存映射文件可用,共享库就可以使用这个机制。

清除策略

如果发生缺页中断时系统中有大量的空闲页框,此时分页系统工作在最佳状态。如果每个页框都被占用,而且被修改过的话,再换入一个新页面时,旧页面应首先被写回磁盘。为保证有足够的空闲页框,很多分页系统有一个称为分页守护进程(paging daemon)的后台进程,它在大多数时候睡眠,但定期被唤醒以检査内存的状态。如果空闲页框过少,分页守护进程通过预定的页面置换算法选择页面换出内存。如果这些页面装入内存后被修改过,则将它们写回磁盘。

在任何情况下,页面中原先的内容都被记录下来。当需要使用一个已被淘汰的页面时,如果该页框还没有被覆盖,将其从空闲页框缓冲池中移出即可恢复该页面。保存一定数目的页框供给比使用所有内存并在需要时捜索一个页框有更好的性能。分页守护进程至少保证了所有的空闲页框是“干净”的,所以空闲页框在被分配时不必再急着写回磁盘。

一种实现清除策略的方法就是使用一个双指针时钟。前指针由分页守护进程控制。当它指向一个脏页面时,就把该页面写回磁盘,前指针向前移动。当它指向一个干净页面时,仅仅指针向前移动。后指针用于页面置换,就像在标准时钟算法中一样。现在,由于分页守护进程的工作,后指针命中干净页面的概率会增加。

虚拟内存接口

到现在为止,所有的讨论都假定虚拟内存对进程和程序员来说是透明的,也就是说,它们都可以在一台只有较少物理内存的计算机上看到很大的虚拟地址空间。对于不少系统而言这样做是对的,但对于一些髙级系统而言,程序员可以对内存映射进行控制,并可以通过非常规的方法来增强程序的行为。

允许程序员对内存映射进行控制的一个原因就是为了允许两个或者多个进程共享同一部分内存。如果程序员可以对内存区域进行命名,那么就有可能实现共享内存:通过让一个进程把一片内存区域的名称通知另一个进程,而使得第二个进程可以把这片区域映射到它的虚拟地址空间中去。通过两个进程(或者更多)共享同一部分页面,髙带宽的共享就成为可能——一个进程往共享内存中写内容而另一个从中读出内容。

页面共享也可以用来实现高性能的消息传递系统。

虚拟内存系统的实现问题

与分页相关的工作

操作系统要在下面的四段时间里做与分页相关的工作:进程创建时,进程执行时,缺页中断时和进程终止时。

  • 当在分页系统中创建一个新进程时,操作系统要确定程序和数据在初始时有多大,并为它们创建一个页表。操作系统还要在内存中为页表分配空间并对其进行初始化。当进程被换出时,页表不需要驻留在内存中,但当进程运行时,它必须在内存中。另外,操作系统要在磁盘交换区中分配空间,以便在一个进程换出时在磁盘上有放置此进程的空间。操作系统还要用程序正文和数据对交换区进行初始化,这样当新进程发生缺页中断时,可以调入需要的页面。某些系统直接从磁盘上的可执行文件对程序正文进行分页,以节省磁盘空间和初始化时间。最后,操作系统必须把有关页表和磁盘交换区的信息存储在进程表中。
  • 当调度一个进程执行时,必须为新进程重置MMU,刷新TLB,以清除以前的进程遗留的痕迹。新进程的页表必须成为当前页表,通常可以通过复制该页表或者把一个指向它的指针放进某个硬件寄存器来完成。有时,在进程初始化时可以把进程的部分或者全部页面装入内存中以减少缺页中断的发生,例如,PC(程序计数器)所指的页面肯定是需要的。
  • 当缺页中断发生时,操作系统必须通过读硬件寄存器来确定是哪个虚拟地址造成了缺页中断。通过该信息,它要计算需要哪个页面,并在磁盘上对该页面进行定位。它必须找到合适的页框来存放新页面,必要时还要置换老的页面,然后把所需的页面读入页框。最后,还要回退程序计数器,使程序计数器指向引起缺页中断的指令,并重新执行该指令。
  • 当进程退出的时候,操作系统必须释放进程的页表、页面和页面在硬盘上所占用的空间。如果某些页面是与其他进程共享的,当最后一个使用它们的进程终止的时候,才可以释放内存和磁盘上的页面。

缺页中断处理

  1. 硬件陷入内核,在堆栈中保存程序计数器。大多数机器将当前指令的各种状态信息保存在特殊的CPU寄存器中。
  2. 启动一个汇编代码例程保存通用寄存器和其他易失的信息,以免被操作系统破坏。这个例程将操作系统作为一个函数来调用。
  3. 当操作系统发现一个缺页中断时,尝试发现需要哪个虚拟页面。通常一个硬件寄存器包含了这一信息,如果没有的话,操作系统必须检索程序计数器,取出这条指令,用软件分析这条指令,看看它在缺页中断时正在做什么。
  4. 一旦知道了发生缺页中断的虚拟地址,操作系统检査这个地址是否有效,并检査存取与保护是否一致。如果不一致,向进程发出一个信号或杀掉该进程。如果地址有效且没有保护错误发生,系统则检查是否有空闲页框。如果没有空闲页框,执行页面置换算法寻找一个页面来淘汰。
  5. 如果选择的页框“脏”了,安排该页写回磁盘,并发生一次上下文切换,挂起产生缺页中断的进程,让其他进程运行直至磁盘传输结束。无论如何,该页框被标记为忙,以免因为其他原因而被其他进程占用。
  6. —旦页框“干净”后(无论是立刻还是在写回磁盘后),操作系统査找所需页面在磁盘上的地址,通过磁盘操作将其装入。该页面正在被装入时,产生缺页中断的进程仍然被挂起,并且如果有其他可运行的用户进程,则选择另一个用户进程运行。
  7. 当磁盘中断发生时,表明该页已经被装入,页表已经更新可以反映它的位置,页框也被标记为正常状态。
  8. 恢复发生缺页中断指令以前的状态,程序计数器重新指向这条指令。
  9. 调度引发缺页中断的进程,操作系统返回调用它的汇编语言例程。
  10. 该例程恢复寄存器和其他状态信息,返回到用户空间继续执行,就好像缺页中断没有发生过一样。

指令备份

当程序访问不在内存中的页面时,引起缺页中断的指令会半途停止并引发操作系统的陷阱。在操作系统取出所需的页面后,它需要重新启动引起陷阱的指令。但这并不是一件容易实现的事。

锁定内存中的页面

虚拟内存和I/O通过微妙的方式相互作用着。

设想一个进程刚刚通过系统调用从文件或其他设备中读取数据到其地址空间中的缓冲区。在等待I/O完成时,该进程被挂起,另一个进程被允许运行,而这个进程产生一个缺页中断。如果分页算法是全局算法,包含I/O缓冲区的页面会有很小的机会(但不是没有)被选中换出内存。如果一个I/O设备正处在对该页面进行DMA传输的过程之中,将这个页面移出将会导致部分数据写入它们所属的缓冲区中,而部分数据被写入到最新装入的页面中。

一种解决方法是锁住正在做I/O操作的内存中的页面以保证它不会被移出内存。锁住一个页面通常称为在内存中钉住(pinning)页面。

另一种方法是在内核缓冲区中完成所有的I/O操作,然后再将数据复制到用户页面。

后备存储

在磁盘上分配页面空间的最简单的算法是在磁盘上设置特殊的交换分区,甚至从文件系统划分一块独立的磁盘(以平衡I/O负载)。大多数UNIX是这样处理的。在这个分区里没有普通的文件系统,这样就消除了将文件偏移转换成块地址的开销。取而代之的是,始终使用相应分区的起始块号。

策略和机制的分离

控制系统复杂度的一种重要方法就是把策略从机制中分离出来。通过使大多数存储管理器作为用户级进程运行,就可以把该原则应用到存储管理中。

一个如何分离策略和机制的简单例子可以参见下图:

策略和机制分离

其中存储管理系统被分为三个部分:

  1. 一个底层MMU处理程序
  2. 一个作为内核一部分的缺页中断处理程序
  3. 一个运行在用户空间中的外部页面调度程序

所有关于MMU工作的细节都被封装在MMU处理程序中,该程序的代码是与机器相关的,而且操作系统每应用到一个新平台就要被重写一次。

缺 页中断处理程序是与机器无关的代码,包含大多数分页机制。策略主要由作为用户进程运行的外部页面调度程序所决定。

当一个进程启动时,需要通知外部页面调度程序以便建立进程页面映射,如果需要的话还要在磁盘上分配后备存储。当进程正在运行时,它可能要把新对象映射到它的地址空间,所以还要再一次通知外部页面调度程序。

一旦进程开始运行,就有可能出现缺页中断。缺页中断处理程序找出需要哪个虚拟页面,并发送一条消息给外部页面调度程序告诉它发生了什么问题。外部页面调度程序从磁盘中读入所需的页面,把它复制到自己的地址空间的某一位置。然后告诉缺页中断处理程序该页面的位置。缺页中断处理程序从外部页面调度程序的地址空间中清除该页面的映射,然后请求MMU处理程序把它放到用户地址空间的正确位置,随后就可以重新启动用户进程了。

分段

概述

作业的地址空间被划分为若干个段,每个段定义了一组逻辑信息。例程序段、数据段等。每个段都从0开始编址,并采用一段连续的地址空间。段的长度由相应的逻辑信息组的长度决定,因而各段长度不等。整个作业的地址空间是二维的。

段的长度在运行期间可以动态改变,比如,堆栈段的长度在数据被压入时会增长,在数据被弹出时又会减小。

因为每个段都构成了一个独立的地址空间,所以它们可以独立地增长或减小而不会影响到其他的段。如果一个在某个段中的堆栈需要更多的空间,它就可以立刻得到所需要的空间,因为它的地址空间中没有任何其他东西阻挡它增长。段当然有可能会被装满,但通常情况下段都很大,因此这种情况发生的可能性很小。要在这种分段或二维的存储器中指示一个地址,程序必须提供两部分地址,一个段号和一个段内地址。

纯分段实现

分段和分页的实现本质上是不同的:页面是定长的而段不是。下图演示了使用分段的过程(最后通过内存紧缩实现):

纯分段

分段和分页结合

在段页式存储管理系统中,作业的地址空间首先被分成若干个逻辑分段,每段都有自己的段号,然后再将每段分成若干个大小相等的页。对于主存空间也分成大小相等的页,主存的分配以页为单位。

段页式系统中,作业的地址结构包含三部分的内容:段号,页号,页内位移量

程序员按照分段系统的地址结构将地址分为段号与段内位移量,地址变换机构将段内位移量分解为页号和页内位移量。

分段和分页的区别

  • 段是信息的逻辑单位,它是根据用户的需要划分的,因此段对用户是可见的;页是信息的物理单位,是为了管理主存的方便而划分的,对用户是透明的。
  • 段的大小不固定,有它所完成的功能决定;页大大小固定,由系统决定
  • 段向用户提供二维地址空间;页向用户提供的是一维地址空间
  • 段是信息的逻辑单位,便于存储保护和信息的共享,页的保护和共享受到限制。

内部碎片和外部碎片

  • 内部碎片(internalfragment),通常是指将内存以固定大小的块进行分配,采用这种方案,进程所分配的内存可能比所需的大,多出来的未被使用的内存叫做内部碎片。内部碎片就是已经被分配出去(能明确指出属于哪个进程)却不能被利用的内存空间。
  • 外部碎片(externalfragment),通常是指随着进程移进移出内存,内存的空闲空间被分割成小片段,当所有的总的可用内存之和可以满足分配请求,但是却不连续,就出现了外部碎片问题。外部碎片指的是还没有被分配出去(不属于任何进程),但由于太小了无法分配给申请内存空间的新进程的内存空闲区域。

页式虚拟存储系统存在内部碎片,段式虚拟存储系统存在外部碎片。

进程

进程模型:

  • 进程是对正在运行程序的一个抽象。单个CPU通过轮转方式也可以实现伪并行。在CPU的一个核中,任何时候都只有一个进程在执行。

进程分类:

  • 在系统中有与用户交互的前台进程,也有停留在后台处理诸如邮件,打印等活动的守护进程

进程的地址空间:

  • 在Unix/Linux中,只有一个系统调用可以用来创建新进程:fork。子进程共享父进程的所有内存,它通过写时复制(copy-on-write)的方式共享,它有机会共享父进程的其他资源,比如打开的文件等。一旦两者之一想要修改部分内存,则这块内存首先被复制,以确保发生在私有内存区域,写时的内存是不可以共享的
  • 在Windows中,一开始父进程和子进程之间的地址空间就是不同的。

进程的层次结构:

  • 在UNIX中,进程和它的所有子进程以及其后裔组成一个进程组。进程不能剥夺其子进程的继承权。
  • 在Windows中没有进程层次的概念,所有进程的地位是相同的。唯一类似进程层次的是在创建进程的时候,父进程会得到一个句柄,它可以用来控制子进程。但是它有权将这个句柄传送给某个进程,这样它们就不存在层次结构了。

进程状态:运行态,阻塞态,就绪态。如下图:

进程状态

为了实现进程模型,操作系统维护着一张表格(一个结构数组),即进程表(process table)。每个进程占用一个进程表项。该表项的部分典型字段如下:

进程表部分字段

在了解这些之后,对单个CPU如何进行伪并行可以有更多的理解。当中断发生后,操作系统底层的工作步骤如下:

  1. 中断硬件将程序计数器,程序状态字,寄存器等压入堆栈;
  2. 计算机跳转到中断向量所指示的地址(中断向量:包含中断服务程序的入口地址),硬件从中断向量装入新的程序计数器;
  3. 汇编语言保存寄存器值,对于当前进程而言,通常保存在进程表项中;
  4. 汇编语言设置新的堆栈;
  5. C中断服务例程运行处理某个特定的中断类型剩下的工作(典型地读和缓存输入);
  6. 调度程序决定下一个即将运行的进程;
  7. C过程将控制返回至汇编代码,为当前进程装入寄存器值以及内存映射
  8. 汇编语言启动新的进程运行。

线程

线程模型:

  • 线程给进程模型增加了一项内容,即在同一个进程环境中,允许彼此之间有较大的独立性的多个线程执行;
  • 在同一个进程中并行运行多个线程,是对在同一台计算机上并行运行多个进程的模拟。前者多个线程共享同一个地址空间及其它资源。后者多个进程共享物理内存,磁盘,打印机等资源;
  • 一个线程可以读写甚至清除另一个线程的堆栈,线程之间没有保护;
  • 当多线程进程在单个CPU中运行时,线程轮流运行。

在一个进程中所有线程共享的资源:

  • 地址空间
  • 全局变量
  • 打开文件
  • 子进程
  • 即将发生的定时器
  • 信号与信号处理程序
  • 账户信息

每个线程自己的内容:

  • 程序计数器
  • 寄存器
  • 堆栈
  • 状态

POSIX线程:

  • 为了实现可移植的线程程序,IEEE定义了线程的标准。它定义个线程包为pthread
  • 大部分UNIX及UNIX-like都支持该标准;
  • pthread_create,pthread_exit,pthread_join,pthread_yield等。

实现线程的方式:

  • 在用户空间实现
  • 在内核空间实现
  • 混合实现

如下图:

线程实现方式

用户空间实现线程:

  • 把线程包放在用户空间,内核对其一无所知,从内核的角度,它按照正常的方式管理,即单线程进程;
  • 用户级线程可以在不支持线程的操作系统上实现;
  • 在用户空间管理线程时,每个进程需要有其专用的线程表,跟进程表类似,它记录着每个线程的程序计数器,堆栈指针等。该线程表由运行时系统管理,当一个线程转换到就绪/阻塞状态时,在表中存放重启该线程所需要的信息;
  • 保存线程状态和调度程序都只是本地过程,所以启动它们比进行内核调用效率更髙。另一方面,不需要陷入内核,不需要上下文切换,也不需要对内存髙速缓存进行刷新,因此非常快捷;
  • 允许每个进程有自己定制的调度算法。用户级线程还具有较好的可扩展性,这是因为在内核空间中内核线程需要一些固定表格空间和堆栈空间,如果内核线程的数量非常大,就会出现问题;
  • 问题一:阻塞调用问题。使用线程的一个主要目标是,允许每个线程使用阻塞调用,但是还要避免被阻塞的线程影响其他的线程。有一种可能的替代方案,就是如果某个调用会阻塞,就提前通知。只有在安全的情形下(即不会阻塞)才进行阻塞调用。否则调用就不进行,代之以运行另一个线程。这个处理方法需要重写部分系统调用库,所以效率不髙也不优雅,不过没有其他的可选方案了。
  • 问题二:缺页中断问题。在对缺失的指令进行定位和读入时,相关的进程就被阻塞。如果有一个线程引起页面故障,内核由于不知道有线程的存在,通常会把整个进程阻塞直到磁盘I/O 完成为止;
  • 问题三:如果一个线程开始运行,那么在该进程中的其他线程就不能运行,除非第一个线程自动放弃CPU。在一个单独的进程内部,没有时钟中断,所以不可能用轮转调度的方式调度线程。除非某个线程能够按照自己的意志进入运行时系统,否则调度程序就没有任何机会。

在内核中实现线程:

  • 内核的线程表保存了每个线程的寄存器、状态和其他信息。这些信息和在用户空间中(在运行时系统中)的线程是一样的;
  • 由于在内核中创建或撒销线程的代价比较大,某些系统采取“环保”的处理方式回收其线程。当某个线程被撒销时,就把它标志为不可运行的,但是其内核数据结构没有受到影响。稍后,在必须创建一个新线程时,就重新启动某个旧线程,从而节省了一些开销;
  • 当一个多线程进程创建新的进程时,新进程是拥有与原进程相同数量的线程,还是只有一个线程?在很多情况下,最好的选择取决于进程计划下一步做什么;
  • 在经典模型中,信号是发给进程而不是线程的。当一个信号到达时,应该由哪一个线程处理它?线程可以“注册”它们感兴趣的某些信号,因此当一个信号到达的时候,可把它交给需要它的线程。但是如果多个线程注册了相同的信号,可能会出现问题。

混合实现:

  • 内核级线程和用户级线程彼此多路复用;
  • 内核只识别内核级线程,并对其进行调度。其中一些内核级线程会被多个用户级线程多路复用。

进程间通信

概述

IPC,Inter Process Communication。在进程之间的通信最好使用一种结构良好的方式而不要使用中断。有三个问题:

  1. 一个进程如何把信息传递给另一个进程;
  2. 确保多个进程在关键活动中不会出现交叉,比如说争夺资源;
  3. 顺序正确,比如,如果进程A产生数据而进程B打印数据,那么B在打印之前必须等待,直到A已经产生一些数据。

同样的问题和解决方法也适用于线程。

进程隔离

  • 进程隔离是为了保护操作系统中进程相互不干扰而设计的一组不同硬件和软件的技术
  • 进程的隔离实现,使用了虚拟地址空间
  • 进程A的虚拟地址和进程B的虚拟地址不同,这样就防止了进程A将数据信息写入进程B,也就是说进程之间的数据是不共享的,因此进程之间的交互就必须得通过IPC机制

进程间通信就是在不同进程之间传播或交换信息,那么不同进程之间存在着什么双方都可以访问的介质呢?进程的用户空间是互相独立的,一般而言是不能互相访问的,唯一的例外是共享内存区。但是,系统空间却是“公共场所”,所以内核显然可以提供这样的条件。除此以外,那就是双方都可以访问的外设了。在这个意义上,两个进程当然也可以通过磁盘上的普通文件交换信息,或者通过“注册表”或其它数据库中的某些表项和记录交换信息。广义上这也是进程间通信的手段,但是一般都不把这算作“进程间通信”。因为那些通信手段的效率太低了,而人们对进程间通信的要求是要有一定的实时性。

进程间通信主要包括管道, 系统IPC(Inter-Process Communication,进程间通信)(包括消息队列,信号量,共享存储), SOCKET.

管道(pipe)

  1. 普通管道PIPE, 通常有种限制,一是半双工,只能单向传输;二是只能在父子进程间使用.
  2. 流管道s_pipe: 去除了第一种限制,可以双向传输.
  3. 命名管道:name_pipe, 去除了第二种限制,可以在许多并不相关的进程之间进行通讯.

管道单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在于内存中。

管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据:管道一端的进程顺序地将进程数据写入缓冲区,另一端的进程则顺序地读取数据,该缓冲区可以看做一个循环队列,读和写的位置都是自动增加的,一个数据只能被读一次,读出以后再缓冲区都不复存在了。当缓冲区读空或者写满时,有一定的规则控制相应的读进程或写进程是否进入等待队列,当空的缓冲区有新数据写入或慢的缓冲区有数据读出时,就唤醒等待队列中的进程继续读写。

管道: 优点是所有的UNIX实现都支持, 并且在最后一个访问管道的进程终止后,管道就被完全删除;缺陷是管道只允许单向传输或者用于父子进程之间.并且管道在创建时分配一个page大小的内存,缓存区大小比较有限。

系统IPC: 优点是功能强大,能在毫不相关进程之间进行通讯; 缺陷是关键字KEY_T使用了内核标识,占用了内核资源,而且只能被显式删除,而且不能使用SOCKET的一些机制,例如select,epoll等.

信号量(semophore)

信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

消息队列(message queue)

消息队列,就是一个消息的链表,是一系列保存在内核中消息的列表,由消息队列标识符标识。用户进程可以向消息队列添加消息,也可以向消息队列读取消息。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。

可以把消息看做一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息,对消息队列有读权限的进程可以从消息队列中读取消息。

信号(sinal)

信号是Linux系统中用于进程之间通信或操作的一种机制,信号可以在任何时候发送给某一进程,而无须知道该进程的状态。如果该进程并未处于执行状态,则该信号就由内核保存起来,知道该进程恢复执行并传递给他为止。如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。

信号传递信息少,不适用于信息交换,更适用于进程中断控制,比如非法内存访问,杀死某个进程等。

共享内存(shared memory)

共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。

采用共享内存进行通信的一个主要好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。

套接字(socket)

套接字作为更通用的接口,传输效率低,主要用于不同机器或跨网络的通信。

总结

Linux是使用的虚拟内存寻址方式,有如下特性:

  • 用户空间的虚拟内存地址是映射到物理内存中的
  • 对虚拟内存的读写实际上是对物理内存的读写,这个过程就是内存映射
  • 这个内存映射过程是通过系统调用mmap()来实现的

IPC机制通信流程【共享内存机制除外】

  • 首先,发送方进程通过系统调用将要发送的数据从用户空间copy到内核空间缓存区中
  • 接着,接收方开辟一段内存空间,然后通过系统调用将内核缓存区中的数据copy到接收方的内存缓存区

Linux-IPC

这种方式存在两个问题:

  • 需要做2次数据拷贝操作
  • 接收方进程在接收数据之前,需要事先分配空间来存取数据,但不知道事先要分配多大的空间,这样就可能存在一种在空间上的浪费

针对Android中的Binder通信流程,在数据拷贝上有一个优化:

binder

  • Binder借助了内存映射的方法,在内核空间和接收方用户空间的数据缓存区之间做了一层内存映射,就相当于直接拷贝到了接收方用户空间的数据缓存区,从而减少了一次数据拷贝

同步/互斥方式

概述

  • 互斥:是指散步在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。
  • 同步:是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。
  • 竞争条件(race condition):两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序。

临界区

在某些时候进程可能需要访问共享内存或共享文件,或执行另外一些会导致竞争的操作,把对共享内存进行访问的程序片段称作临界区。临界区应满足这些条件:

  1. 任何两个进程不能同时处于其临界区
  2. 不应对CPU的速度和数量做任何假设
  3. 临界区外运行的进程不得阻塞其他进程
  4. 不得使进程无限期等待进入临界区

忙等待的互斥

当一个进程想进入临界区时,先检査是否允许进入,若不允许,则该进程将原地等待,直到允许为止。

这种方法不仅浪费了CPU时间,而且还可能引起预想不到的结果。考虑一台计算机有两个进程,H优先级较髙,L优先级较低。调度规则规定,只要H处于就绪态它就可以运行。在某一时刻,L处干临界区中,此时H变到就绪态,准备运行(例如,一条I/O操作结束)。现在H开始忙等待,但由于当H就绪时L不会被调度,也就无法离开临界区,所以H将永远忙等待下去。这种情况有时被称作优先级反转问题 (priority inversion problem)。

在单处理器系统中,最简单的方法是使毎个进程在刚刚进入临界区后立即屏蔽所有中断,并在就要离开之前再打开中断。在屏蔽中断之后CPU将不会被切换到其他进程。于是,一旦某个进程屏蔽中断之后,它就可以检査和修改共享内存,而不必担心其他进程介入。

这个方案并不好,因为把屏蔽中断的权力交给用户进程是不明智的。若一个进程屏蔽中断后不再打开中断,整个系统可能会因此终止。而且,如果系统是多处理器,则屏蔽中断仅仅对执行disable指令的那个CPU有效。其他CPU仍将继续运行,并可以访问共享内存。

另一方面,对内核来说,当它在更新变量或列表的几条指令期间将中断屏蔽是很方便的。当就绪进程队列之类的数据状态不一致时发生中断,则将导致竞争条件。所以结论是:屏蔽中断对于操作系统本身而言是一项很有用的技术,但对于用户进程则不是一种合适的通用互斥机制。由于多核芯片的数量越来越多,即使在低端PC上也是如此。因此,通过屏蔽中断来达到互斥的可能性一甚至在内核中一变得日益减少了。

睡眠与唤醒

当一个进程A访问临界区时,其他试图访问临界区的进程在无法进入临界区时将阻塞,而不是忙等待,在前一个进程处理完一个任务后,会主动唤醒其他阻塞的进程。

信号量

信号量

当需要一个计数器来限制可以使用某共享资源的线程数目时,可以使用信号量对象。它保存了对当前访问某一个指定资源的线程的计数值,该计数值是当前还可以使用该资源的线程数目。如果这个计数达到了零,则所有对这个对象所控制的资源的访问尝试都被放入到一个队列中等待,直到超时或计数值不为零为止。

信号量的取值可以为0或者其他正整数,在信号量上只有三种操作可以进行:初始化,P操作和V操作,这三种操作都是原子操作。

P(passeren:通过)操作(递减操作)可以用于阻塞一个进程,V(vrijgeven:释放)操作(增加操作)可以用于解除阻塞一个进程。

信号量机制功能强大,但使用时对信号量的操作分散,而且难以控制,读写和维护都很困难。

1
2
3
4
5
6
7
P(S):
S--;
while S<0 do block;

V(S):
S++;
if S<=0 then wakeup;

信号量(semaphore)的数据结构为一个值和一个指针,指针指向等待该信号量的下一个进程。信号量的值与相应资源的使用情况有关。当它的值大于0时,表示当前可用资源的数量;当它的值小于0时,其绝对值表示等待使用该资源的进程个数。信号量的值仅能由PV操作来改变。

一般来说,信号量S>0时,S表示可用资源的数量。执行一次P操作意味着请求分配一个单位资源,因此S的值减1;当S<0时,表示已经没有可用资源,请求者必须等待别的进程释放该类资源,它才能运行下去。而执行一个V操作意味着释放一个单位资源,因此S的值加1;若S<0,表示有某些进程正在等待该资源,因此要唤醒一个等待状态的进程,使之运行下去。

使用PV操作实现进程互斥时应该注意的是:

  • 每个程序中用户实现互斥的P、V操作必须成对出现,先做P操作,进临界区,后做V操作,出临界区。若有多个分支,要认真检查其成对性。
  • P、V操作应分别紧靠临界区的头尾部,临界区的代码应尽可能短,不能有死循环。
  • 互斥信号量的初值一般为1。
  • 分析进程间的制约关系,确定信号量种类。在保持进程间有正确的同步关系情况下,哪个进程先执行,哪些进程后执行,彼此间通过什么资源(信号量)进行协调,从而明确要设置哪些信号量。
  • 信号量的初值与相应资源的数量有关,也与P、V操作在程序代码中出现的位置有关。
  • 同一信号量的P、V操作要成对出现,但它们分别在不同的进程代码中。

C语言PV操作

  1. #include <semaphore.h>
  2. int sem_init(sem_t *sem,int pshared, unsigned int value)
    • 函数作用:初始化信号量
    • sem:信号量指针
    • Pshared:决定信号量能否在几个进程间共享,一般取0
    • Value:信号量的初始值
  3. 信号的操作
    • P操作:int sem_wait(sem_t *sem);
    • int sem_try_wait(sem_t *sem);
    • V操作:int sempost(sem_t *sem);
    • int sem_getvalue(sem_t *sem);
    • 销毁信号:int sem_destroy(sem_t *sem);

实例一

  1. 一个生产者,一个消费者,公用一个缓冲区。定义两个同步信号量:

    • empty——表示缓冲区是否为空,初值为1。

    • full——表示缓冲区中是否为满,初值为0。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      // 生产者进程:
      while(true){
      生产一个产品;
      P(empty);
      产品送往Buffer;
      V(full);
      }

      // 消费者进程:
      while(true){
      P(full);
      从Buffer取出一个产品;
      V(empty);
      消费该产品;
      }
  2. 一个生产者,一个消费者,公用n个环形缓冲区。定义两个同步信号量:

    • empty——表示缓冲区是否为空,初值为n。

    • full——表示缓冲区中是否为满,初值为0。

      设缓冲区的编号为1~n-1,定义两个指针in和out,分别是生产者进程和消费者进程使用的指针,指向下一个可用的缓冲区。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      // 生产者进程
      while(TRUE){
      生产一个产品;
      P(empty);
      产品送往buffer(in);
      in=(in+1)mod n;
      V(full);
      }

      // 消费者进程
      while(TRUE){
      P(full);
      从buffer(out)中取出产品;
      out=(out+1)mod n;
      V(empty);
      消费该产品;
      }
  3. 一组生产者,一组消费者,公用n个环形缓冲区

    在这个问题中,不仅生产者与消费者之间要同步,而且各个生产者之间、各个消费者之间还必须互斥地访问缓冲区。
    定义四个信号量:

    • empty——表示缓冲区是否为空,初值为n。

    • full——表示缓冲区中是否为满,初值为0。

    • mutex1——生产者之间的互斥信号量,初值为1。

    • mutex2——消费者之间的互斥信号量,初值为1。

      设缓冲区的编号为1~n-1,定义两个指针in和out,分别是生产者进程和消费者进程使用的指针,指向下一个可用的缓冲区。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      // 生产者进程
      while(TRUE){
      生产一个产品;
      P(empty);
      P(mutex1);
      产品送往buffer(in);
      in=(in+1)mod n;
      V(mutex1);
      V(full);
      }

      // 消费者进程
      while(TRUE){
      P(full)
      P(mutex2);
      从buffer(out)中取出产品;
      out=(out+1)mod n;
      V(mutex2);
      V(empty);
      消费该产品;
      }

实例二

题:桌上有一空盘,允许存放一只水果。爸爸可向盘中放苹果,也可向盘中放桔子,儿子专等吃盘中的桔子,女儿专等吃盘中的苹果。规定当盘空时一次只能放一只水果供吃者取用,请用P、V原语实现爸爸、儿子、女儿三个并发进程的同步。

分析:在本题中,爸爸、儿子、女儿共用一个盘子,盘中一次只能放一个水果。当盘子为空时,爸爸可将一个水果放入果盘中。若放入果盘中的是桔子,则允许儿子吃,女儿必须等待;若放入果盘中的是苹果,则允许女儿吃,儿子必须等待。本题实际上是生产者-消费者问题的一种变形。这里,生产者放入缓冲区的产品有两类,消费者也有两类,每类消费者只消费其中固定的一类产品。

解:在本题中,应设置三个信号量S、So、Sa,信号量S表示盘子是否为空,其初值为l;信号量So表示盘中是否有桔子,其初值为0;信号量Sa表示盘中是否有苹果,其初值为0。同步描述如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int S=1;
int Sa=0;
int So=0;
main() {
cobegin
father(); /*父亲进程*/
son(); /*儿子进程*/
daughter(); /*女儿进程*/
coend


father() {
while(1) {
P(S);
将水果放入盘中;
if(放入的是桔子)V(So);
else V(Sa);
}
}
son() {
while(1) {
P(So);
从盘中取出桔子;
V(S);
吃桔子;

}

daughter() {
while(1) {
P(Sa);
从盘中取出苹果;
V(S);
吃苹果;


互斥量

如果不需要信号量的计数能力,有时可以使用信号量的一个简化版本,称为互斥量(mutex)。互斥量仅仅适用于管理共享资源或一小段代码。由于互斥量在实现时既容易又有效,这使得互斥量在实现用户空间线程包时非常有用。

互斥量是一个可以处于两态之一的变量:解锁和加锁。互斥量使用两个过程。当一个线程(或进程)需要访问临界区时,它调用mutex_lock,如果该互斥量当前是解锁的(即临界区可用),此调用成功,调用线程可以自由进入该临界区。另一方面,如果该互斥量已经加锁,调用线程被阻塞,直到在临界区中的线程完成并调用mutex_unlock,如果多个线程被阻塞在该互斥量上,将随机选择一个线程并允许它获得锁。

管程

使用信号量时要非常小心。一处很小的错误将导致很大的麻烦。为了更易于编写正确的程序,出现了一种髙级同步原语,称为管程(monitor)。

其基本思想是将共享变量和对它们的操作集中在一个模块中,操作系统或并发程序就由这样的模块构成。这样模块之间联系清晰,便于维护和修改,易于保证正确性。

管程有一个很重要的特性,即任一时刻管程中只能有一个活跃进程,这一特性使管程能有效地完成互斥。管程是编程语言的组成部分,编译器知道它们的特殊性,因此可以采用与其他过程调用不同的方法来处理对管程的调用。典型的处理方法是,当一个进程调用管程过程时,该过程中的前几条指令将检査在管程中是否有其他的活跃进程。如果有,调用进程将被挂起,直到另一个进程离开管程将其唤醒。如果没有活跃进程在使用管程,则该调用进程可以进入。

进入管程时的互斥由编译器负责,但通常的做法是用一个互斥量或二元信号量。因为是由编译器而非程序员来安排互斥,所以出错的可能性要小得多。在任一时刻,写管程的人无须关心编译器是如何实现互斥的。他只需知道将所有的临界区转换成管程过程即可,决不会有两个进程同时执行临界区中的代码。

进程/线程调度

概述

当计算机系统是多道程序设计系统时,通常就会有多个进程或线程同时竞争CPU。只要有两个或更多的进程处于就绪状态,这种情形就会发生。如果只有一个CPU可用,那么就必须选择下一个要运行的进程。在操作系统中,完成选择工作的这一部分称为调度程序(scheduler),该程序使用的算法称为调度算法(scheduling algorithm)。

尽管有一些不同,但许多适用于进程调度的处理方法也同样适用于线程调度。当内核管理线程的时候,调度经常是按线程级别的,与线程所属的进程基本或根本没有关联。

  • 计算密集型进程:具有较长时间的C PU集中使用和较小频度的I/O等待
  • I/O密集型进程:具有较短时间的C PU集中使用和频繁的I/O等待

在不同的系统中,调度程序的优化是不同的:

  • 批处理系统
  • 交互式系统
  • 实时系统

调度算法的目标:

  1. 所有系统
    • 公平:给每个进程公平的CPU份额
    • 策略强制执行:保证规定的策略被执行
    • 平衡:保持系统的所有部分都忙碌
  2. 批处理系统
    • 吞吐量:毎小时最大作业数
    • 周转时间:从提交到终止间的最小时间
    • CPU利用率:保持CPU始终忙碌
  3. 交互式系统
    • 响应时间:快速响应请求
    • 均衡性:满足用户的期望
  4. 实时系统
    • 满足截止时间:避免丢失数据
    • 可预测性:在多媒体系统中避免品质降低

批处理系统中的调度

先来先服务

先来先服务(FCFS)调度算法是一种最简单的调度算法,该算法既可用于作业调度,也可用于进程调度。当在作业调度中采用该算法时,每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业,将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列。在进程调度中采用FCFS算法时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配处理机,使之投入运行。该进程一直运行到完成或发生某事件而阻塞后才放弃处理机。

最短作业(进程)优先

短作业(进程)优先调度算法,是指对短作业或短进程优先调度的算法。它们可以分别用于作业调度和进程调度。短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。而短进程优先(SPF)调度算法则是从就绪队列中选出一个估计运行时间最短的进程,将处理机分配给它,使它立即执行并一直执行到完成,或发生某事件而被阻塞放弃处理机时再重新调度。

最短剩余时间优先

最短作业优先的抢占式版本是最短剩余时间优先(shortest remaining time next)算法。使用这个算 法,调度程序总是选择剩余运行时间最短的那个进程运行。

交互式系统中的调度

时间片轮转法

在早期的时间片轮转法中,系统将所有的就绪进程按先来先服务的原则排成一个队列,每次调度时,把CPU分配给队首进程,并令其执行一个时间片。时间片的大小从几ms到几百ms。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。这样就可以保证就绪队列中的所有进程在一给定的时间内均能获得一时间片的处理机执行时间。换言之,系统能在给定的时间内响应所有用户的请求。

优先级调度

  • 非抢占式优先权算法
  • 抢占式优先权算法

多级反馈队列

前面介绍的各种用作进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程,而且如果并未指明进程的长度,则短进程优先和基于进程长度的抢占式调度算法都将无法使用。而多级反馈队列调度算法则不必事先知道各种进程所需的执行时间,而且还可以满足各种类型进程的需要,因而它是目前被公认的一种较好的进程调度算法。在采用多级反馈队列调度算法的系统中,调度算法的实施过程如下所述:

  1. 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。
  2. 当一个新进程进入内存后,首先将它放入第一队列的末尾,按FCFS原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按FCFS原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个长作业(进程)从第一队列依次降到第n队列后,在第n 队列便采取按时间片轮转的方式运行。
  3. 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第1~(i-1)队列均空时,才会调度第i队列中的进程运行。如果处理机正在第i队列中为某进程服务时,又有新进程进入优先权较高的队列(第1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第i队列的末尾,把处理机分配给新到的高优先权进程。

最短进程优先

对于批处理系统而言,由于最短作业优先常常伴随着最短响应时间,所以如果能够把它用于交互进程,那将是非常好的。

实时系统

实时系统是一种时间起着主导作用的系统。典型地,一种或多种外部物理设备发给计算机一个服务请求,而计算机必须在一个确定的时间范围内恰当地做出反应。例如,在CD播放器中的计算机获得从驱动器而来的位流,然后必须在非常短的时间间隔内将位流转换为音乐。如果计算时间过长,那么音乐就会听起来有异常。

这种系统要求调度程序是高度可预测和有规律的。

策略/机制分离

当一个进程有多个子进程并在其控制下运行时,主进程完全可能掌握哪一个子进程最重要(或最紧迫)而哪一个最不重要。但是,以上讨论的调度算法中没有一个算法从用户进程接收有关的调度决策信息,这就导致了调度程序很少能够做出最优的选择。

解决问题的方法是将调度机制(scheduling mechanism)与调度策略(scheduling policy)分离。也就是将调度算法以某种形式参数化,而参数可以由用户进程填写。在这里,调度机制位于内核,而调度策略则由用户进程决定。策略与机制分离是一种关键性思路。

线程调度

当若干进程都有多个线程时,就存在两个层次的并行:进程和线程。在这样的系统中调度处理有本质差别,这取决于所支持的是用户级线程还是内核级线程(或两者都支持)。

用户级进程

由于内核并不知道有线程存在,所以内核还是和以前一样地操作,选取一个进程,假设为A,并给予A以时间片控制。A中的线程调度程序决定哪个线程运行,假设为A1。由于多道线程并不存在时钟中断,所以这个线程可以按其意愿任意运行多长时间。如果该线程用完了进程的全部时间片,内核就会选择另一个进程运行。

在进程A终于又一次运行时,线程A1会接着运行。该线程会继续耗费A进程的所有时间,直到它完成工作。不过,该线程的这种不合群的行为不会影响到其他的进程。其他进程会得到调度程序所分配的合适份额,不会考虑进程A内部所发生的事。

内核级线程

内核选择一个特定的线程运行。它不用考虑该线程属于哪个进程,不过如果有必要的话,它可以这样做。对被选择的线程赋予一个时间片,而且如果超过了时间片,就会强制挂起该线程。

用户级线程和内核级线程之间的差别在于性能。用户级线程的线程切换需要少量的机器指令,而内核级线程需要完整的上下文切换,修改内存映像,使髙速缓存失效,这导致了若干数量级的延迟。另一方面,在使用内核级线程时,一旦线程阻塞在I/O上就不需要像在用户级线程中那样将整个进程挂起。

从进程A的一个线程切换到进程B的一个线程,其代价高于运行进程A的第2个线程(因为必须修改内存映像,清除内存髙速缓存的内容),内核对此是了解的,并可运用这些信息做出决定。例如,给定两个在其他方面同等重要的线程,其中一个线程与刚好阻塞的线程属于同一个进程,而另一个线程属于其他的进程,那么应该倾向前者。

另一个重要因素是用户级线程可以使用专为应用程序定制的线程调度程序。一般而言,应用定制的线程调度程序能够比内核更好地满足应用的需要。

典型IPC问题

哲学家就餐问题

如下图所示:

哲学家就餐问题

五个哲学家围坐在一张圆桌周围,每个哲学家面前都有一盘通心粉,由于通心粉很滑,所以需要两把叉子才能夹住,相邻两个盘子之间放有一把叉子。哲学家的生活中有两种交替活动时段:即吃饭和思考。

当一个哲学家觉得饿了时,他就试图分两次去取其左边和右边的叉子,每次拿一把,但不分次序。如果成功地得到了两把叉子,就开始吃饭,吃完后放下叉子继续思考。关键问题是:能为每一个哲学家写一段描述其行为的程序,且决不会死锁吗?

哲学家就餐问题对于互斥访问有限资源的竞争问题(如I/O设备)一类的建模过程十分有用。

读者-写者问题

读者-写者问题为数据库访问建立了一个模型。例如,设想一个飞机订票系统,其中有许多竞争的进程试图读写其中的数据。多个进程同时读数据库是可以接受的,但如果一个进程正在更新(写)数据库,则所有的其他进程都不能访问该数据库,即使读操作也不行。如何对读者和写者进行编程?

死锁

产生原因

  1. 系统资源的竞争
    • 通常系统中拥有的不可抢占资源,其数量不足以满足多个进程运行的需要,使得进程在运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可抢占资源的竞争才可能产生死锁,对可抢占资源的竞争是不会引起死锁的。
  2. 进程推进顺序非法
    • 进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都会因为所需资源被占用而阻塞。
    • 信号量使用不当也会造成死锁。进程间彼此相互等待对方发来的消息,结果也会使得这些进程间无法继续向前推进。例如,进程A等待进程B发的消息,进程B又在等待进程A发的消息,可以看出进程A和B不是因为竞争同一资源,而是在等待对方的资源导致死锁。
  3. 死锁产生的必要条件,产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。
    • 互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
    • 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
    • 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
    • 循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中Pi等待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有,如图2-15所示。

死锁产生条件

鸵鸟算法

最简单的解决方法是鸵鸟算法:把头埋到沙子里,假装根本没有问题发生。

死锁检测和死锁恢复

系统并不试图阻止死锁的产生,而是允许死锁发生,当检测到死锁发生后,采取措施进行恢复。

死锁检测

每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。

当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)。

当然,死锁一般要比两个线程互相持有对方的锁这种情况要复杂的多。线程A等待线程B,线程B等待线程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,然后又找到了线程D,发现线程D请求的锁被线程A自己持有着。这是它就知道发生了死锁。

下面是一幅关于四个线程(A,B,C和D)之间锁占有和请求的关系图。像这样的数据结构就可以被用来检测死锁。

死锁恢复

  • 利用抢占恢复:在某些情况下,可能会临时将某个资源从它的当前所有者那里转移给另一个进程。许多情况下,尤 其是对运行在大型主机上的批处理操作系统来说,需要人工进行干预。
  • 利用回滚恢复,等待一段随机的时间后重试。
  • 设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。

加锁顺序

当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
Thread 1:
lock A
lock B

Thread 2:
wait for A
lock C (when A locked)

Thread 3:
wait for A
wait for B
wait for C

如果一个线程(比如线程3)需要一些锁,那么它必须按照确定的顺序获取锁。它只有获得了从顺序上排在前面的锁之后,才能获取后面的锁。

例如,线程2和线程3只有在获取了锁A之后才能尝试获取锁C。因为线程1已经拥有了锁A,所以线程2和3需要一直等到锁A被释放。然后在它们尝试对B或C加锁之前,必须成功地对A加了锁。

按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要事先知道所有可能会用到的锁,并对这些锁做适当的排序,但总有些时候是无法预知的。

加锁时限

另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行.加锁超时后可以先继续运行干点其它事情,再回头来重复之前加锁的逻辑.

以下是一个例子,展示了两个线程以不同的顺序尝试获取相同的两个锁,在发生超时后回退并重试的场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
Thread 1 locks A
Thread 2 locks B

Thread 1 attempts to lock B but is blocked
Thread 2 attempts to lock A but is blocked

Thread 1's lock attempt on B times out
Thread 1 backs up and releases A as well
Thread 1 waits randomly (e.g. 257 millis) before retrying.

Thread 2's lock attempt on A times out
Thread 2 backs up and releases B as well
Thread 2 waits randomly (e.g. 43 millis) before retrying.

在上面的例子中,线程2比线程1早200毫秒进行重试加锁,因此它可以先成功地获取到两个锁。这时,线程1尝试获取锁A并且处于等待状态。当线程2结束时,线程1也可以顺利的获得这两个锁(除非线程2或者其它线程在线程1成功获得两个锁之前又获得其中的一些锁)。

需要注意的是,由于存在锁的超时,所以我们不能认为这种场景就一定是出现了死锁。也可能是因为获得了锁的线程(导致其它线程超时)需要很长的时间去完成它的任务。

此外,如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。如果只有两个线程,并且重试的超时时间设定为0到500毫秒之间,这种现象可能不会发生,但是如果是10个或20个线程情况就不同了。因为这些线程等待相等的重试时间的概率就高的多(或者非常接近以至于会出现问题)。(超时和重试机制是为了避免在同一时间出现的竞争,但是当线程很多时,其中两个或多个线程的超时时间一样或者接近的可能性就会很大,因此就算出现竞争而导致超时后,由于超时时间一样,它们又会同时开始重试,导致新一轮的竞争,带来了新的问题。)

什么是操作系统

概述

现代计算机系统是一个复杂的系统,为了管理这些设备,为用户程序提供一个更清晰,更简便的计算机模型,计算机安装了一层软件:操作系统

用户与操作系统交互的程序有两种(用户接口程序并不属于操作系统的一部分):

  • Shell:基于文本;
  • GUI(Graphical User Interface):基于图形。

在计算机系统中操作系统所处的位置如下图:

操作系统位置

  • 内核态的操作系统拥有对所有硬件的完全访问权,可以执行机器能够运行的任何指令;
  • 用户态只使用了机器指令的一个子集。

用户接口程序(Shell,GUI)允许用户运行其他程序。

扩展机器

抽象是管理复杂性的一个关键。以SATA硬盘为例,在硬件层面其接口及其复杂,而且经过不停地修改,因此提供了一个叫做硬盘驱动的软件来与硬盘交互,这类软件提供了读写硬盘的接口,而不需要深入细节。即使在这个层面,对大多数应用来说还是太底层了,因此,所有的操作系统都提供了使用硬盘的另一层抽象:文件

资源管理

从这个角度看,操作系统的任务是在相互竞争的程序之间有序地控制对硬件设备资源的分配。它记录着哪个程序在使用什么资源,对资源请求进行分配,评估使用代价,并且为不同的程序和用户调解互相冲突的资源请求。

计算机硬件

CPU

CPU是“Center Process Unit”缩写,CPU与电路进行电信号交换,而且对于电信号,只能识别ON和OFF两种状态。

如果把电信号的开(ON)/关(OFF)与数字0和1对应起来,就能将二进制数转换为电信号,同时电信号也可以转换回二进制数。因为我们可以把十进制数转换成二进制数,也能把二进制数还原成十进制数,所以人们发明了普通的计算机。

当给每个文字编码,将之与二进制数字对应起来,就可以把文字也转换成电信号,让CPU来处理文字。依此类推,人们接着又找到了将图像、音乐等等转换成电信号的方法,使CPU的应用范围越来越广。不过CPU还是一如既往,只能处理电信号。

CPU能处理的并不仅仅只有数据,还可以用电信号向CPU发出指令。我们所编写的程序最终都要转换成所谓的机器语言,这些机器语言就是以电信号的形式发送给CPU的。这些机器语言不过就是一连串的指令代码,实际上也就是一串0和1的组合而已。

CPU从内存中取出指令并执行,在每个CPU的周期中,计算机首先从内存中取出指令,解码以确定其类型和操作数,然后执行,然后取指,解码并执行下一条指令。

每种CPU都有一套专用的可执行的指令集。由于用来访问内存以得到指令的或数据的时间要比执行指令花费的时间长得多,因此CPU都有用于保存关键变量的临时数据的通用寄存器。

此外还有一些专用寄存器:

  • 程序计数器:指向下一条指令的地址;
  • 堆栈指针:指向内存中当前栈的顶端;
  • 程序状态字(Program Status Word,PSW):包含条件码位,CPU优先级,模式(用户态/内核态),以及其他的控制位。

存储器

分类 典型访问时间 典型容量
寄存器 1ns <1KB
高速缓存 2ns 4MB
主存 10ns 1-16GB
磁盘 10ms 1-nT

磁盘

I/O设备

总线

计算机启动

主引导扇区

  • 主引导扇区是硬盘0号柱面,0号磁道的第一个扇区,大小为512字节。
  • 主引导记录(MBR),占用主引导扇区的前446字节,紧随其后的64字节是磁盘分区表DPT,最后还剩两个字节则恒为55AA,表示结束符号(Magic Number)。

MBR,全称为Master Boot Record,即硬盘的主引导记录。MBR,共446字节,一般在操作系统安装时写入,但它并不属于操作系统。MBR就是一段引导程序,用于检测磁盘的分区合法性和加载操作系统,它的重要作用就是识别活动分区,并引导操作系统。它不依赖任何操作系统,而且启动代码也是可以改变的,从而能够实现多系统引导。

分区表DPT,共64字节,记录了硬盘有多少分区以及分区的各种属性。由于一个分区的信息要占用16字节,所以分区表只能定义4个分区,这就是为什么我们说硬盘一般最多只能分为4个主分区。

主分区、扩展分区、逻辑分区

主分区是由主引导扇区中64字节的分区表所定义的,最多只能有4个。但为了满足更多分区的需求,便产生了扩展分区。形式上,如果拥有扩展分区,就必须牺牲一个主分区,而且最多有一个扩展分区,也就是说:主分区+扩展分区<=4 && 扩展分区<=1。因此扩展分区也可以看成一种特殊的主分区。

但扩展分区并不可以直接使用,扩展分区又必须以逻辑分区的形式出现,可以这样认为:扩展分区包含着若干逻辑分区,而且至少包含一个。

扩展分区中的逻辑分区是以链式存在的。即每一个逻辑分区都记录着下一个逻辑分区的位置信息,依次串联。事实上每一个逻辑分区都有一个和主引导扇区类似的引导扇区,引导扇区里有类似的分区表。该分区表记录了该分区的信息和一个指针,指向下一个逻辑分区的引导扇区。

因此,逻辑分区是借鉴了主分区的方法,相当于在一个主分区下面建立了若干级“主分区”。一个可以预测的现象是:一旦某一个逻辑分区损害,跟在它后面的所有逻辑分区都将丢失,而前面的逻辑分区去可以保留。这也是链式结果的特点。

固件接口标准

BIOS

IBM推出的业界标准的固件接口,存储于主板的ROM/EEPROM/flash中,提供的功能包括:

  • 开机自检
  • 加载引导程序(MBR中的,通常是bootloader的第一级)
  • 向OS提供抽象的硬件接口

PS:CMOS是PC上的另一个重要的存储器,用于保存BIOS的设置结果,CMOS是RAM。

在每台计算机上有一块“双亲板”,其上有一个称为BIOS(Basic Input Output System)的程序。

在计算机启动时,BIOS开始运行。它首先检查所安装的RAM数量,键盘和其他设备是否已安装且正常响应。接着它开始扫描PCIe和PCI总线并找出连在上面的所有设备。

然后BIOS通过存储在CMOS存储器中的设备清单决定启动设备。用户可以在系统刚刚启动的时候进入一个BIOS配置程序,对设备清单进行修改。

然后操作系统询问BIOS,以获取配置信息。对于每种设备,系统检查对应的设备驱动程序是否存在。如果没有,系统要求用户插入含有该设备驱动程序的CD-ROM(由设备供应商提供)或者从网络上下载驱动程序。一旦拥有全部的设备驱动程序,操作系统将它们调入内核,然后初始化有关表格,创建需要的任何背景进程,并在每个终端上启动登录程序或者GUI。

EFI/UEFI

Unified Extensible Firmware Interface,架设在系统固件之上的软件接口,用于替代BIOS接口,EFI是UEFI的前称。

一般认为,UEFI由以下几个部分组成:

  • Pre-EFI初始化模块
  • EFI驱动程序执行环境(DXE)
  • EFI驱动程序
  • 兼容性支持模块(CSM)
  • EFI高层应用
  • GUID磁盘分区表(GPT)

通常初始化模块和DXE被集成在一个ROM中;EFI驱动程序一般在设备的ROM中,或者ESP中;EFI高层应用一般在ESP中。CSM用于给不具备UEFI引导能力的操作系统提供类似于传统BIOS的系统服务。

启动方式

Legacy mode

即通过MBR/BIOS进行引导的传统模式,流程如下:

  1. BIOS加电自检(Power On Self Test – POST)。
  2. 读取主引导记录(MBR)。BIOS根据CMOS中的设置依次检查启动设备:将相应启动设备的第一个扇区(也就是MBR扇区)读入内存。
  3. 检查MBR的结束标志位是否等于55AA,若不等于则转去尝试其他启动设备,如果没有启动设备满足要求则显示”NO ROM BASIC”然后死机。
  4. 当检测到有启动设备满足要求后,BIOS将控制权交给相应启动设备的MBR。
  5. 根据MBR中的引导代码启动引导程序。

UEFI mode

UEFI启动不依赖于Boot Sector(比如MBR),大致流程如下:

  1. Pre-EFI初始化模块运行,自检
  2. 加载DXE(EFI驱动程序执行环境),枚举并加载EFI驱动程序(设备ROM或ESP中)
  3. 找到ESP中的引导程序,通过其引导操作系统。

CSM mode

UEFI中的兼容性支持模块(Compatible Support Module)提供了引导UEFI固件的PC上的传统磁盘(MBR格式)的方法。

分区表

MBR分区表

指的是512字节的Master Boot Record(主引导记录)中的分区表,由于大小限制,其中只能存有最多四个分区的描述(亦即4个主分区)。

EBR分区表

位于Extended Boot Record(扩展引导纪录)中的分区表,该分区表所描述的分区即扩展分区。每个EBR仅表示了一个扩展分区,该扩展分区紧接在它的EBR后。EBR中的四个分区描述符中的第一个表示其描述的分你去,第二个描述符则表示下一个扩展分区(如果是最后一个则为空),也就是说,各个EBR串接成了一个EBR链表。

GPT分区表

GUID Partition Table,是EFI标准的一部分,用于替代MBR分区表,相较起来有分区更大,数量更多(没有4个主分区的限制)等优势,GPT格式的硬盘结构如下,可以看到首部MBR的位置有个保护MBR(用于防止不识别GPT的硬盘工具错误识别并破坏硬盘中的数据),这个MBR中只有一个类型为0xEE的分区。

Bootloader

Bootloader即引导程序,用于启动操作系统或者其它引导程序(比如Grub启动Windows Bootmgr)

Grub

GNU的开源引导程序,可以用于引导Linux等操作系统,或者用于链式引导其它引导程序(比如Windows Boot Manager),分为三个部分,分别称为步骤1、1.5、2,看名字就可以知道,步骤1.5是可有可没有的,这三个步骤对应的文件分别是:

  • Boot.img:步骤1对应的文件,446个字节大小,步骤1可以引导步骤1.5也可以引导步骤2。MBR分区格式的磁盘中,放在MBR里(446也是为了符合MBR的启动代码区大小);GPT分区格式的磁盘中,放在Protective MBR中。
  • Core.img:步骤1.5对应的文件,32256字节大小。MBR分区格式的磁盘中,放在紧邻MBR的若干扇区中;GPT分区格式的磁盘中,则放在34号扇区开始的位置(第一个分区所处的位置),而对应的GPT分区表中的第一个分区的entry被置空。通常其中包含文件系统驱动以便load步骤2的文件。
  • /boot/grub:步骤2对应的文件目录,放在系统分区或者单独的Boot分区中

Windows Boot Manager

是从Windows Vista开始引进的新一代开机管理程序,用以取代NTLDR。

当电脑运行完开机自检后,传统型BIOS会根据引导扇区查找开机硬盘中标记”引导”分区下的BOOTMGR文件;若是UEFI则是Bootmgfw.efi文件和Bootmgr.efi文件,接着管理程序会读取开机配置数据库(BCD, Boot Configuration Database)下的引导数据,接着根据其中的数据加载与默认或用户所选择的操作系统。

操作系统大观

  • 大型机操作系统
  • 服务器操作系统
  • 多处理器操作系统
  • 个人计算机操作系统
  • 掌上计算机操作系统
  • 嵌入式操作系统
  • 传感器节点操作系统
  • 实时操作系统
  • 智能卡操作系统

操作系统结构

单体系统

整个操作系统在内核态以单一程序的方式运行,整个操作系统以过程集合的方式编写,首先编译单个过程,然后链接成一个大型可执行二进制程序。使用这种技术,系统中的每个过程可以自由调用其他过程,但无数个不受限制地彼此调用的过程显得过于笨拙,而且任何一个过程的崩溃都会连累整个接口。

许多操作系统支持可装载的扩展,在UNIX中称为共享库(Shared Library,so),在Windows中称为动态链接库(Dynamic Link Library,dll)

在单体系统中也可能有一些结构存在。

层次系统

将单体系统中的结构进一步通用化,就变成一个层次式的系统。THE操作系统结构如下:

THE操作系统结构

微内核

微内核的设计思想:为了实现高可靠性,将操作系统划分为小的,定义良好的模块,只有其中一个模块——微内核,运行在内核态,其余的模块作为普通用户进程运行。

由于把每个设备驱动和文件系统分别作为普通用户进程,当这些模块发生错误时不会造成整个系统的崩溃。

当然,微内核由于增加了许多系统调用,因此性能上可能会有所下降。

客户端-服务器模式

这个模式可以看成是微内核的变体。

虚拟机

分层系统可延伸为虚拟机概念,虚拟机的基本思想是单个计算机的硬件抽象为几个不同的执行部件从而使得仿佛每个独立的执行环境都在自己的计算机运行一样。通过cpu调度和虚拟内存技术,操作系统能带来一种幻觉,即进程认为有自己的处理器和自己的(虚拟)内存。

  • 非虚拟机:进程→内核-硬件
  • 虚拟机:进程→内核→虚拟机实现→硬件

创建虚拟机的原因:最根本的是,在并行运行几个不同的执行环境(即不同的操作系统)能共享相同的硬件。

虚拟机方法的主要困难在于磁盘系统。解决方法是提供虚拟磁盘。

底层机器有两种模式:用户模式和内核模式。

虚拟机软件可以运行在内核模式,因为它就是操作系统,虚拟机本身只能运行在用户模式,但他必须有虚拟用户模式和虚拟内核模式。这两种模式都运行在物理用户模式。

虚拟模式的转换可按下述方法实现。例如,当以一个虚拟用户模式而在虚拟机上运行的程序执行系统调用时,他会在真正机器上引起一个到虚拟机监控器的转换。当虚拟机监控器获得控制,他能改变虚拟机的寄存器内容和程序计数器以模拟系统调用的效果。接着他能重新启动虚拟机,注意他现在是在虚拟机内核模式下执行。

实例:VMWare,Java VM

操作系统和内核

区别

  • 操作系统:管理计算机硬件与软件资源的系统软件,同时也是计算机系统的内核与基石。操作系统需要处理如管理与配置内存、决定系统资源供需的优先次序、控制输入与输出设备、操作网络与管理文件系统等基本事务。操作系统也提供一个让用户与系统交互的操作界面。对我们来说,操作系统最直观的感受就是桌面系统,以及上层的应用程序,而后面的资源处理等等就是操作系统背后的黑盒。
  • 内核:一个提供硬件抽象层、磁盘及文件系统控制、多任务等功能的系统软件。内核是操作系统最基本的部分。它是为众多应用程序提供对计算机硬件的安全访问的一部分软件,这种访问是有限的,并且内核决定一个程序在什么时候对某部分硬件操作多长时间。直接对硬件操作是非常复杂的,所以内核通常提供一种硬件抽象的方法来完成这些操作。硬件抽象隐藏了复杂性,为应用软件和硬件提供了一套简洁,统一的接口,使程序设计更为简单。简单来说,内核是一个操作系统的核心。它负责管理系统的进程、内存、设备驱动程序、文件和网络系统等等,决定着系统的性能和稳定性。是连接应用程序和硬件的桥梁。内核就是操作系统背后黑盒的核心。

它们的关系如下图:

操作系统和内核

内核的分类

宏内核(单内核)

内核管理着操作系统的内存,文件,IO,网络等等,每个功能可以看做一个模块,在宏内核中,这些模块都是集成在一起的,运行在内核进程中,模块之间的交互直接通过方法调用。如下图:

宏内核

微内核

在微内核中,内核只提供最核心的功能,比如任务调度,内存管理等等,其他模块被移出内核,运行在不同的进程中,这样即使某一个模块出现问题,只要重启这个模块的进程即可,不会影响到其他模块,稳定性大大增加。甚至可以在系统运行过程中替换现有模块的实现。而且由于模块独立的性质,可以做到模块的按需加载。但是模块间的相互调用需要通过进程间通信,通信效率相对较低。如下图:

微内核

混合内核

宏内核和微内核各有千秋,也各有缺点,所以混合内核就是集中了两者的特点,让微内核中的一些核心模块运行在内核中,从而使内核效率更高一些。

外内核

外内核是把硬件暴露给应用程序,应用程序可以直接访问硬件,外内核对系统提供保护。目前还在研究阶段。

宏内核/微内核对比

- 宏内核 微内核
通信效率 高(函数调用) 低(进程间通信)
稳定性 低(模块集成在一起) 高(模块间互不影响)
扩展性 低(模块集成在一起) 高(模块间互不影响)
代码量 多(需要实现所有模块) 少(只需要实现核心功能)

主流操作系统内核

宏内核:

  • Linux
  • Windows 9X 系列
  • MacOS 8.6 版本之前

微内核:

  • Fuchsia
  • 鸿蒙
  • Minix

混合内核:

  • Windows XP
  • Windows 7
  • Mac OS X
  • XNU

外内核:

  • Nemesis

用户态和内核态

特权级

熟悉Unix/Linux系统的人都知道,fork的工作实际上是以系统调用的方式完成相应功能的,具体的工作是由sys_fork负责实施。其实无论是不是Unix或者Linux,对于任何操作系统来说,创建一个新的进程都是属于内核功能,因为它要做很多底层的工作,消耗系统的物理资源,比如分配物理内存,从父进程拷贝相关信息,拷贝设置页目录页表等等,这些显然不能随便让哪个程序就能去做,于是就自然引出特权级别的概念,显然,最关键性的权力必须由高特权级的程序来执行,这样才可以做到集中管理,减少有限资源的访问和使用冲突。

特权级显然是非常有效的管理和控制程序执行的手段,因此在硬件上对特权级做了很多支持,就Intel x86架构的CPU来说一共有0~3四个特权级,0级最高,3级最低,硬件上在执行每条指令时都会对指令所具有的特权级做相应的检查。硬件已经提供了一套特权级使用的相关机制,软件自然就是好好利用的问题,这属于操作系统要做的事情,对于 Unix/Linux来说,只使用了0级特权级和3级特权级。也就是说在Unix/Linux系统中,一条工作在0级特权级的指令具有了CPU能提供的最高权力,而一条工作在3级特权级的指令具有CPU提供的最低或者说最基本权力。

用户态和内核态

现在我们从特权级的调度来理解用户态和内核态就比较好理解了,当程序运行在3级特权级上时,就可以称之为运行在用户态,因为这是最低特权级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态;反之,当程序运行在0级特权级上时,就可以称之为运行在内核态。

虽然用户态下和内核态下工作的程序有很多差别,但最重要的差别就在于特权级的不同,即权力的不同。运行在用户态下的程序不能直接访问操作系统内核数据结构和程序,比如上面例子中的testfork()就不能直接调用 sys_fork(),因为前者是工作在用户态,属于用户态程序,而sys_fork()是工作在内核态,属于内核态程序。

当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态,比如testfork()最初运行在用户态进程下,当它调用fork()最终触发 sys_fork()的执行时,就切换到了内核态。

同步/异步和阻塞/非阻塞

  • 同步、异步:涉及到IO通知机制。所谓同步,就是发起调用后,被调用者处理消息,必须等处理完才直接返回结果,没处理完之前是不返回的,调用者主动等待结果;所谓异步,就是发起调用后,被调用者直接返回,但是并没有返回结果,等处理完消息后,通过状态、通知或者回调函数来通知调用者,调用者被动接收结果。
  • 阻塞、非阻塞:涉及到CPU线程调度。所谓阻塞,就是调用结果返回之前,该执行线程会被挂起,不释放CPU执行权,线程不能做其它事情,只能等待,只有等到调用结果返回了,才能接着往下执行;所谓非阻塞,就是在没有获取调用结果时,不是一直等待,线程可以往下执行,如果是同步的,通过轮询的方式检查有没有调用结果返回,如果是异步的,会通知回调。
  • 同步阻塞:老张在厨房用普通水壶烧水,一直在厨房等着(阻塞),盯到水烧开(同步);
  • 异步阻塞:老张在厨房用响水壶烧水,一直在厨房中等着(阻塞),直到水壶发出响声(异步),老张知道水烧开了;
  • 同步非阻塞:老张在厨房用普通水壶烧水,在烧水过程中,就到客厅去看电视(非阻塞),然后时不时去厨房看看水烧开了没(轮询检查同步结果);
  • 异步非阻塞:老张在厨房用响水壶烧水,在烧水过程中,就到客厅去看电视(非阻塞),当水壶发出响声(异步),老张就知道水烧开了。

POSIX

POSIX,Portable Operating System Interface(可移植操作系统接口)。是UNIX系统的一个设计标准,很多类UNIX系统也在支持兼容这个标准,如Linux,遵循这个标准的好处是软件可以跨平台。

从Unix之后出现了许多独立开发的与Unix类似但又不完全兼容的OS,通称Unix-like OS。局面非常混乱,为了提高兼容性和应用程序的可移植性,标准化Unix-like OS,提出了大家都应该遵守的POSIX标准。后来,Unix这个名字成为了商标,只有花钱进行POSIX标准兼容性测试并通过了的OS,才能称为Unix,其余的OS,最多称为Unix-like OS。

Windows从WinNT开始就有兼容POSIX的考虑,因为当年在要求严格的领域,Unix地位比Windows高。

一般情况下,应用程序通过应用编程接口(API)而不是直接通过系统调用来编程,因为应用程序使用的这种编程接口实际上并不需要和内核提供的系统调用对应。一个API定义了一组应用程序使用的编程接口,它们可以实现成一个系统调用,也可以通过调用多个系统调用来实现,也可以完全不使用任何系统调用。实际上,API可以在各种不同的操作系统上实现,给应用程序提供完全相同的接口,而它们本身在这些系统上的实现却可能迥异。

完成同一功能,不同内核提供的系统调用(也就是一个函数)是不同的,例如创建进程,linux下是fork函数,windows下是creatprocess函数。为了解决跨平台的问题,linux和windows等都要实现基本的posix标准,linux把fork函数以及windows把creatprocess函数封装成同一个函数,都声明在unistd.h里。这样,程序员编写普通应用时候,只用包含unistd.h,然后调用这个封装后的函数,程序就在源代码级别可移植了。

概述

Glide是一个快速高效的Android图片加载库,提供了易用的API,高性能、可扩展的图片解码管道(decode pipeline),以及自动的资源池技术。

Glide 支持拉取,解码和展示视频快照,图片,和GIF动画,开发者可以将 Glide 网络请求从 HttpUrlConnection 替换成如 Google Volley 和 Square OkHttp 这样的工具库。Glide 提供了多种图片格式的缓存,可以感知Activity/Fragment的生命周期。能够复用和主动回收Bitmap,以及带有高效的缓存策略,减小了内存的开销。

本文基于 Glide 4.11.0 版本,参考网上一些解析,结合源码对 Glide 的机制做一个分析。

阅读全文 »

概述

app_process是Android原生的一个可执行程序,位于/system/bin目录下,zygote进程便是由这个执行文件启动的。

基本用法

1
2
3
4
5
6
7
8
9
10
Usage: app_process [java-options] cmd-dir start-class-name [options]

java-options - 传递给 JVM 的参数
cmd-dir - 命令执行路径
start-class-name - 程序入口, main() 方法所在的类名
options - 可以是下面这些
--zygote 启动 zygote 进程用的
--start-system-server 启动系统服务(也是启动 zygote 进程的时候用的)
--application 启动应用程序
--nice-name= 启动之后的进程名称

app_process没有帮助程序,但是可以从源代码进行分析:

  • 传入 –zygote 会启动 com.android.internal.os.ZygoteInit,否则启动 com.android.internal.os.RuntimeInit。
  • –start-system-server 只在启动 zygote 时有效。
  • 在非 zygote 模式中,有无 –application 的选项的区别只是是否将 stdout 和 stderr 重定向到 AndroidPrintStream。
  • 只有在非 zygote 的情况下,–nice-name= 选项有效。

与 Java 相似, Android 支持在环境变量 CLASSPATH 中指定类搜索路径 (CLASSPATH),此外还可以在虚拟机参数中指定 -Djava.class.path= 。但是, Android 使用 ART 环境运行 Java ,传统的 Java 字节码文件(.class) 是不能直接运行的,app_process 支持在 CLASSPATH 中指定 dex 或 apk 文件。

1
2
3
4
5
6
7
8
9
10
11
# 使用 dex
$ export CLASSPATH=/sdcard/app.dex
$ app_process /system/bin com.hearing.Main
# 或者
$ app_process -Djava.class.path=/sdcard/app.dex /system/bin com.hearing.Main

# 使用 apk
$ export CLASSPATH=/sdcard/app.apk
$ app_process /system/bin com.hearing.Main
# 或者
$ app_process -Djava.class.path=/sdcard/app.apk /system/bin com.hearing.Main

实例-C/S静默卸载

通过adb的方式由app_process开启一个Server,客户端通过Socket与之连接并调用需要系统权限的功能。(需要手动adb启动Server,实际可行性不大,并不能实现自动化启动)(如果在代码里自动调用app_process启动Server,该Server进程并不能获得系统权限)

Main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Main {
public static void main(String[] args) {
// 利用looper让线程循环
Looper.prepareMainLooper();
System.out.println("*****************hack server starting****************");
// 开一个子线程启动服务
new Thread(new Runnable() {
@Override
public void run() {
new SocketService(new SocketService.SocketListener() {
@Override
public String onMessage(String msg) {
// 接收客户端传过来的消息
return resolveMsg(msg);
}
});
}
}).start();
Looper.loop();
}

private static String resolveMsg(String msg) {
// 执行客户端传过来的消息并返回执行结果
ShellUtil.ExecResult execResult = ShellUtil.execute("pm uninstall " + msg);
return execResult.getMessage();
}
}

Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class SocketService {
private final int PORT = 10500;
private SocketListener listener;

public SocketService(SocketListener listener) {
this.listener = listener;
try {
// 利用ServerSocket类启动服务,然后指定一个端口
ServerSocket serverSocket = new ServerSocket(PORT);
System.out.println("server running " + PORT + " port");
ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(10);
// 新建一个线程池用来并发处理客户端的消息
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 5000, TimeUnit.MILLISECONDS, queue);
while (true) {
Socket socket = serverSocket.accept();
// 接收到新消息
executor.execute(new MsgProcess(socket));
}
} catch (Exception e) {
System.out.println("SocketServer create Exception:" + e);
}
}

class MsgProcess implements Runnable {
Socket socket;

public MsgProcess(Socket s) {
socket = s;
}

public void run() {
try {
// 通过流读取内容
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = bufferedReader.readLine();
System.out.println("server receive: " + line);
PrintWriter printWriter = new PrintWriter(socket.getOutputStream());
String repeat = listener.onMessage(line);
System.out.println("server send: " + repeat);
// 服务端返回给客户端的消息
printWriter.print(repeat);
printWriter.flush();
printWriter.close();
bufferedReader.close();
socket.close();
} catch (IOException e) {
System.out.println("socket connection error:" + e.toString());
}
}
}

public interface SocketListener {
// 通话消息回调
String onMessage(String text);
}
}

Client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class SocketClient {
private final String TAG = "HackRoot SocketClient";
private final int PORT = 10500;
private SocketListener listener;
private PrintWriter printWriter;

public SocketClient(final String cmd, SocketListener listener) {
this.listener = listener;
new Thread(new Runnable() {
@Override
public void run() {
Socket socket = new Socket();
try {
// 与hackserver建立连接
socket.connect(new InetSocketAddress("127.0.0.1", PORT), 3000);
socket.setSoTimeout(3000);
printWriter = new PrintWriter(socket.getOutputStream(), true);
Log.d(TAG, "client send: " + cmd);
// 发送指令
printWriter.println(cmd);
printWriter.flush();
// 读取服务端返回
readServerData(socket);
} catch (IOException e) {
Log.d(TAG, "client send fail: " + e.getMessage());
e.printStackTrace();
}
}
}).start();
}

private void readServerData(final Socket socket) {
try {
InputStreamReader ipsReader = new InputStreamReader(socket.getInputStream());
BufferedReader bfReader = new BufferedReader(ipsReader);
String line = null;
while ((line = bfReader.readLine()) != null) {
Log.d(TAG, "client receive: " + line);
listener.onMessage(line);
}
// 释放资源
ipsReader.close();
bfReader.close();
printWriter.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}

interface SocketListener {
void onMessage(String msg);
}
}

ShellUtil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
public class ShellUtil {
private static final String COMMAND_LINE_END = "\n";
private static final String COMMAND_EXIT = "exit\n";

public static void runShell(String command) {
Process process = null;
BufferedReader bufferedReader = null;
StringBuilder mShellCommandSB = new StringBuilder();
System.out.println("runShell: " + command);
mShellCommandSB.delete(0, mShellCommandSB.length());
String[] cmd = new String[]{"/system/bin/sh", "-c", command};
try {
process = Runtime.getRuntime().exec(cmd);
bufferedReader = new BufferedReader(new InputStreamReader(
process.getInputStream()));
String line;

while ((line = bufferedReader.readLine()) != null) {
mShellCommandSB.append(line);
}
System.out.println("runShell result: " + mShellCommandSB.toString());
process.waitFor();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException e) {
// TODO: handle exception
}
}

if (process != null) {
process.destroy();
}
}
}

public static ExecResult execute(String command) {
return execute(new String[]{command});
}

public static ExecResult execute(String[] commands) {
if (commands == null || commands.length == 0) {
return new ExecResult(false, "empty command");
}
int result = -1;
Process process = null;
DataOutputStream dataOutputStream = null;
BufferedReader sucResult = null, errResult = null;
StringBuilder sucMsg = null, errMsg = null;

try {
// 获取shell级别的process
process = Runtime.getRuntime().exec("sh");
dataOutputStream = new DataOutputStream(process.getOutputStream());
for (String command : commands) {
if (command == null) continue;
System.out.println("execute command: " + command);
// 执行指令
dataOutputStream.write(command.getBytes());
dataOutputStream.writeBytes(COMMAND_LINE_END);
// 刷新
dataOutputStream.flush();
}
dataOutputStream.writeBytes(COMMAND_EXIT);
dataOutputStream.flush();
result = process.waitFor();
sucMsg = new StringBuilder();
errMsg = new StringBuilder();
sucResult = new BufferedReader(new InputStreamReader(process.getInputStream()));
errResult = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String s;
while ((s = sucResult.readLine()) != null) {
sucMsg.append(s);
}
while ((s = errResult.readLine()) != null) {
errMsg.append(s);
}

} catch (IOException | InterruptedException e) {
e.printStackTrace();
} finally {
try {
// 关闭资源,防止内存泄漏
assert dataOutputStream != null;
dataOutputStream.close();
assert sucResult != null;
sucResult.close();
assert errResult != null;
errResult.close();
} catch (IOException e) {
e.printStackTrace();
}
process.destroy();
}
ExecResult execResult;
if (result == 0) {
execResult = new ExecResult(true, sucMsg.toString());
} else {
execResult = new ExecResult(false, errMsg.toString());
}
// 返回执行结果
return execResult;
}

public static class ExecResult {
private boolean success;
private String message;

public ExecResult(boolean success, String message) {
this.success = success;
this.message = message;
}

public boolean getSuccess() {
return this.success;
}

public String getMessage() {
return this.message;
}
}
}

客户端

  • 通过app_process部署Server;
  • 客户端通过调用SocketClient中的方法,向Server端发送想要执行的命令即可。

概述

通过将传统的操作系统安全控制机制扩展到以下用途,Android 致力于成为最安全、最实用的移动平台操作系统:

  • 保护应用和用户数据
  • 保护系统资源(包括网络)
  • 将应用同系统、其他应用和用户隔离开来

为了实现这些目标,Android 提供了以下关键安全功能:

  • 通过 Linux 内核在操作系统级别提供的强大安全功能
  • 针对所有应用的强制性应用沙盒
  • 安全的进程间通信
  • 应用签名
  • 应用定义的权限和用户授予的权限

Linux安全

Linux在历经无数攻击以及成千上万的开发者的不断研究和修复之后,其安全性不言而喻,作为移动计算环境的基础,Linux 内核为 Android 提供了一些关键的安全功能,其中包括:

  • 基于用户的权限模式
  • 进程隔离
  • 用于实现安全 IPC 的可扩展机制
  • 能够移除内核中不必要的和可能不安全的部分

作为一种多用户操作系统,Linux 内核的一个基本安全目标是将用户资源彼此隔离开来。Linux 的安全理念是防范用户资源之间相互侵扰。因此,Linux 可以:

  • 防止用户 A 读取用户 B 的文件
  • 确保用户 A 不会占用用户 B 的内存
  • 确保用户 A 不会占用用户 B 的 CPU 资源
  • 确保用户 A 不会占用用户 B 的设备(例如,电话、GPS、蓝牙)

应用沙盒

Android 平台利用基于用户的 Linux 保护机制来识别和隔离应用资源,可将不同的应用分离开,并保护应用和系统免受恶意应用的攻击。为此,Android 会为每个 Android 应用分配一个独一无二的用户 ID (UID),并在自己的进程中运行。

Android 会使用此 UID 设置一个内核级应用沙盒。内核会在进程级别利用标准的 Linux 机制(例如,分配给应用的用户 ID 和组 ID)实现应用和系统之间的安全防护。 默认情况下,应用不能彼此交互,而且对操作系统的访问权限会受到限制。例如,如果应用 A(一个单独的应用)尝试执行恶意操作,例如在没有权限的情况下读取应用 B 的数据或拨打电话,操作系统会阻止此类行为,因为应用 A 没有适当的用户权限。

由于应用沙盒位于内核层面,因此该安全模型的保护范围扩展到了原生代码和操作系统应用。位于更高层面的所有软件(例如,操作系统库、应用框架、应用运行时环境和所有应用)都会在应用沙盒中运行。

进程间通信

Android进程及消息机制

应用签名

Android签名

SELinux

概述

SELinux:Security-Enhanced Linux,安全增强型 Linux,是一个 Linux 内核模块,也是 Linux 的一个安全子系统。SELinux 主要由美国国家安全局开发,2.6 及以上版本的 Linux 内核都已经集成了 SELinux 模块。

SELinux 主要作用就是最大限度地减小系统中服务进程可访问的资源(最小权限原则)。

DAC

DAC(Discretionary Access Control,自主式访问控制)。

在没有使用 SELinux 的操作系统中,决定一个资源是否能被访问的因素是:依据程序的拥有者与文件资源的 rwx 权限来决定有无存取的能力,只要访问这个资源的用户符合以上的条件就可以被访问。

存在的问题:

  • root 用户不受任何管制,系统上任何资源都可以无限制地访问。
  • 如果不小心将某个目录的权限配置为 777 ,由于对任何人的权限会变成 rwx ,因此该目录的文件就会被任何人所任意存取执行。

MAC

MAC(Mandatory Access Control,委任式访问控制)。

在使用了 SELinux 的操作系统中,决定一个资源是否能被访问的因素除了上述因素之外,还需要判断每一类进程是否拥有对某一类资源的访问权限。这样一来,即使进程是以 root 身份运行的,也需要判断这个进程的类型以及允许访问的资源类型才能决定是否允许访问某个资源。进程的活动空间也可以被压缩到最小。即使是以 root 身份运行的服务进程,一般也只能访问到它所需要的资源。即使程序出了漏洞,影响范围也只有在其允许访问的资源范围内,安全性大大增加。

如此一来,我们针对控制的『主体』变成了『程序』而不是『使用者』了。此外,这个主体程序也不能任意使用系统文件资源,因为每个文件资源也有针对该主体程序配置可取用的权限,如此一来,控制项目就细的多了,但整个系统程序那么多、文件那么多,一项一项控制非常麻烦,所以 SELinux 也提供一些默认的策略 (Policy) ,并在该策略内提供多个rule,让你可以选择是否激活该控制rule。

在委任式存取控制的配置下,我们的程序能够活动的空间就变小了。举例来说,默认情况下,httpd 仅能在 /var/www/ 这个目录底下存取文件,如果 httpd 这个程序想要到其他目录去存取数据时,除了rule配置要开放外,目标目录也得要配置成 httpd 可读取的模式 (type) 才行。所以,即使不小心 httpd 被 cracker 取得了控制权,他也无权浏览 /etc/shadow 等重要的配置档。

运行模式

  • 主体 (Subject):SELinux 主要管理的就是程序,即process;
  • 目标 (Object):主体程序能否存取的『目标资源』一般就是文件系统;
  • 政策 (Policy):由於程序与文件数量庞大,因此 SELinux 会依据某些服务来制订基本的存取安全性政策。这些政策内还会有详细的守则 (rule) 来指定不同的服务开放某些资源的存取与否。
  • 安全性上下文 (security context):主体能不能存取目标除了政策指定之外,主体与目标的安全性上下文必须一致才能够顺利存取。这个安全性上下文 (security context) 有点类似文件系统的 rwx 啦,安全性上下文如果配置错误,你的某些服务(主体程序)就无法存取文件系统(目标资源),就会一直出现『权限不符』的错误信息了。

在目前的 CentOS 5.x 里面仅有提供两个主要的政策,分别是:

  1. targeted:针对网络服务限制较多,针对本机限制较少,是默认的政策;
  2. strict:完整的 SELinux 限制,限制方面较为严格。

建议使用默认的 targeted 政策即可。

SELinux组件

由上图我们可以发现,主体程序必须要通过 SELinux 政策内的守则放行后,就可以与目标资源进行安全性本文的比对,若比对失败则无法存取目标,若比对成功则可以开始存取目标。问题是,最终能否存取目标还是与文件系统的 rwx 权限配置有关,如此一来,加入了 SELinux 之后,出现权限不符的情况时,我们就得要一步一步的分析可能的问题了。

sharedUserId

Android给每个APK进程分配一个单独的空间,manifest中的userid就是对应一个分配的Linux用户ID,并且为它创建一个沙箱,以防止影响其他应用程序(或者其他应用程序影响它)。用户ID在应用程序安装到设备中时被分配,并且在这个设备中保持它的永久性。

通常,不同的APK会具有不同的userId,因此运行时属于不同的进程中,而不同进程中的资源是不共享的,在保障了程序运行的稳定。然后在有些时候,我们自己开发了多个APK并且需要他们之间互相共享资源,那么就需要通过设置shareUserId来实现这一目的。

通过Shared User id,拥有同一个User id的多个APK可以配置成运行在同一个进程中。所以默认就是可以互相访问任意数据。也可以配置成运行成不同的进程,同时可以访问其他APK的数据目录下的数据库和文件。就像访问本程序的数据一样。

用法也很简单:

在需要共享资源的项目的每个AndroidMainfest.xml中添加shareuserId的标签。android:sharedUserId=”com.example”,id名自由设置,但必须保证每个项目都使用了相同的sharedUserId。一个mainfest只能有一个Shareuserid标签。

Termux

概述

Termux是一个Android下一个高级的终端模拟器,开源且不需要root,支持apt管理软件包,拥有自己的apt仓库源,可以十分方便地安装软件包,可以实现支持Python,PHP,Ruby,Go,Nodejs,MySQL等功能环境。

仓库地址

一切皆文件

linux/unix下的哲学核心思想是一切皆文件。它指的是,对所有文件(目录、字符设备、块设备、套接字、打印机、进程、线程、管道等)操作,读写都可用fopen()/fclose()/fwrite()/fread()等函数进行处理,屏蔽了硬件的区别,所有设备都抽象成文件,提供统一的接口给用户,虽然类型各不相同,但是对其提供的却是同一套操作界面,更进一步,对文件的操作也可以跨文件系统执行。

操作一个已经打开的文件:使用文件描述符(file descriptor),简称fd,它是一个对应某个已经打开的文件的索引(非负整数)。

文件描述符

文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。

Linux 系统中,把一切都看做是文件,当进程打开现有文件或创建新文件时,内核向进程返回一个文件描述符,文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用来指向被打开的文件,所有执行I/O操作的系统调用都会通过文件描述符。

Termux工作方式

apt&dpkg

在Linux平台下使用源代码进行软件编译可以具有定制化的设置,但对于Linux distribution的发行商来说,毕竟不是每个人都会进行源代码编译的,这个问题将会严重的影响linux平台上软件的发行与推广。

为了解决上述的问题,厂商先在他们的系统上面编译好了用户所需要的软件,然后将这个编译好并可执行的软件直接发布给用户安装。不同的 Linux 发行版使用不同的打包系统,一般而言,大多数发行版分别属于两大包管理技术阵营:Debian 的”.deb”,和 Red Hat的”.rpm”。

dpkg是Debian的一个底层包管理工具,主要用于对已下载到本地和已安装的软件包进行管理。

虽然我们在使用dpkg时,已经解决掉了软件安装过程中的大量问题,但是当依赖关系不满足时,仍然需要手动解决,而apt这个工具解决了这样的问题,linux distribution 先将软件放置到对应的服务器中,然后分析软件的依赖关系,并且记录下来,然后当客户端有安装软件需求时,通过清单列表与本地的dpkg以存在的软件数据相比较,就能从网络端获取所有需要的具有依赖属性的软件了。

Termux在它自己维护的apt源中有它编译好的deb软件包,这些deb针对的是 applicationId为 com.termux 的应用。

Termux提供了什么

  • 在Android Linux内核的基础上,提供了一些更为丰富的命令以及它们的依赖,比如说 bash(Android系统自带的shell是位于/system/bin下的sh,sh支持的功能比较有限),apt(可以说是对Android sh最为强大的扩展,通过apt可以安装编译好的各种软件,包括Python)。
  • 通过在app的进程中fork子进程,通过文件操作开启一个终端,然后通过返回的文件描述符对终端进行操作,首先执行login(termux)或者bash(termux)或者sh(/system/bin/sh)命令,得到一个可执行shell操作的环境。后续才可继续进行安装python等操作。
  • 为了支持这些扩展的命令,需要配置一些环境变量(包括Path等)。

修改包名

为什么需要修改依赖的包名?

  • termux-app 的 默认applicationId是 com.termux,如果需要修改applicationId的话,则需要重新编译所有的deb包以及提供一个apt源,并且重新生成bootstraps.zip

Python官网的deb解包后目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
└─data
├─usr
│ ├─bin
│ ├─lib
│ │ └─valgrind
│ └─share
│ ├─apps
│ │ └─konsole
│ ├─doc
│ │ └─python
│ ├─lintian
│ │ └─overrides
│ ├─man
│ │ └─man1
│ └─pixmaps

termux 编译后的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
├─data
│ └─data
│ └─$pkg
│ └─files
│ └─usr
│ ├─bin
│ ├─include
│ │ └─python2.7
│ ├─lib
│ │ ├─pkgconfig
│ │ └─python2.7
│ └─share
│ ├─doc
│ │ └─python2
│ └─man
│ └─man1
└─_

修改方法:

源码解读

流程图:

Termux流程图

setupIfNeeded

bootstraps.zip目录结构:

1
2
3
4
5
6
7
8
9
10
11
bootstrap-aarch64
├── SYMLINKS.txt
├── bin
├── etc
├── include
├── lib
├── libexec
├── repo.asc
├── share
├── tmp
└── var
1
2
3
public static final String FILES_PATH = "/data/data/$pkg/files";
public static final String PREFIX_PATH = FILES_PATH + "/usr";
public static final String HOME_PATH = FILES_PATH + "/home";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
public static void setupIfNeeded(final Activity activity, final Runnable whenDone) {
// Termux can only be run as the primary user (device owner) since only that
// account has the expected file system paths. Verify that:
UserManager um = (UserManager) activity.getSystemService(Context.USER_SERVICE);
boolean isPrimaryUser = um.getSerialNumberForUser(android.os.Process.myUserHandle()) == 0;
if (!isPrimaryUser) {
Termux.mInstance.setInstalled(false);
Termux.mInstance.getTermuxHandle().initFail();
return;
}

final File PREFIX_FILE = new File(Termux.PREFIX_PATH);
if (PREFIX_FILE.isDirectory()) {
whenDone.run();
return;
}

new Thread() {
@Override
public void run() {
try {
final String STAGING_PREFIX_PATH = Termux.FILES_PATH + "/usr-staging";
final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);

if (STAGING_PREFIX_FILE.exists()) {
deleteFolder(STAGING_PREFIX_FILE);
}

final byte[] buffer = new byte[8096];
final List<Pair<String, String>> symlinks = new ArrayList<>(50);

final URL zipUrl = determineZipUrl();
try (ZipInputStream zipInput = new ZipInputStream(zipUrl.openStream())) {
ZipEntry zipEntry;
while ((zipEntry = zipInput.getNextEntry()) != null) {
if (zipEntry.getName().equals("SYMLINKS.txt")) {
BufferedReader symlinksReader = new BufferedReader(new InputStreamReader(zipInput));
String line;
while ((line = symlinksReader.readLine()) != null) {
String[] parts = line.split("←");
if (parts.length != 2)
throw new RuntimeException("Malformed symlink line: " + line);
String oldPath = parts[0];
String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
symlinks.add(Pair.create(oldPath, newPath));

ensureDirectoryExists(new File(newPath).getParentFile());
}
} else {
String zipEntryName = zipEntry.getName();
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
boolean isDirectory = zipEntry.isDirectory();

ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());

if (!isDirectory) {
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
int readBytes;
while ((readBytes = zipInput.read(buffer)) != -1)
outStream.write(buffer, 0, readBytes);
}
if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") || zipEntryName.startsWith("lib/apt/methods")) {
//noinspection OctalInteger
Os.chmod(targetFile.getAbsolutePath(), 0700);
}
}
}
}
}

if (symlinks.isEmpty())
throw new RuntimeException("No SYMLINKS.txt encountered");
for (Pair<String, String> symlink : symlinks) {
Os.symlink(symlink.first, symlink.second);
}

if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) {
throw new RuntimeException("Unable to rename staging folder");
}
activity.runOnUiThread(whenDone);
} catch (final Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e);
Termux.mInstance.setInstalled(false);
Termux.mInstance.getTermuxHandle().initFail();
}
}
}.start();
}

createSession

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private TerminalSession createSession() {
new File(HOME_PATH).mkdirs();

String[] env = BackgroundJob.buildEnvironment(false, HOME_PATH);
String executablePath = null;
for (String shellBinary : new String[]{"login", "bash", "zsh"}) {
File shellFile = new File(PREFIX_PATH + "/bin/" + shellBinary);
if (shellFile.canExecute()) {
executablePath = shellFile.getAbsolutePath();
break;
}
}

if (executablePath == null) {
XLLog.d(TermuxDebug.TAG, "fall back to system shell");
executablePath = "/system/bin/sh";
}

String[] processArgs = BackgroundJob.setupProcessArgs(executablePath, null);
executablePath = processArgs[0];
int lastSlashIndex = executablePath.lastIndexOf('/');
String processName = "-" +
(lastSlashIndex == -1 ? executablePath : executablePath.substring(lastSlashIndex + 1));

String[] args = new String[processArgs.length];
args[0] = processName;
if (processArgs.length > 1)
System.arraycopy(processArgs, 1, args, 1, processArgs.length - 1);

TerminalSession session = new TerminalSession(executablePath, HOME_PATH, args, env);
session.initializeEmulator(Integer.MAX_VALUE, 120);

return session;
}

参数如下:

1
2
3
4
5
6
mShellPath: /data/data/$pkg/files/usr/bin/login
mCwd: /data/data/$pkg/files/home
mArgs: [-login]
mEnv: [TERM=xterm-256color, HOME=/data/data/$pkg/files/home, PREFIX=/data/data/$pkg/files/usr, BOOTCLASSPATH/system/framework/com.qualcomm.qti.camera.jar:/system/framework/QPerformance.jar:/system/framework/core-oj.jar:/system/framework/core-libart.jar:/system/framework/conscrypt.jar:/system/framework/okhttp.jar:/system/framework/bouncycastle.jar:/system/framework/apache-xml.jar:/system/framework/legacy-test.jar:/system/framework/ext.jar:/system/framework/framework.jar:/system/framework/telephony-common.jar:/system/framework/voip-common.jar:/system/framework/ims-common.jar:/system/framework/org.apache.http.legacy.boot.jar:/system/framework/android.hidl.base-V1.0-java.jar:/system/framework/android.hidl.manager-V1.0-java.jar:/system/framework/tcmiface.jar:/system/framework/WfdCommon.jar:/system/framework/oem-services.jar:/system/framework/qcom.fmradio.jar:/system/framework/telephony-ext.jar:/system/app/miui/miui.apk:/system/app/miuisystem/miuisystem.apk, ANDROID_ROOT=/system, ANDROID_DATA=/data, EXTERNAL_STORAGE=/sdcard, LD_LIBRARY_PATH=/data/data/$pkg/files/usr/lib, LANG=en_US.UTF-8, PATH=/data/data/$pkg/files/usr/bin:/data/data/$pkg/files/usr/bin/applets, PWD=/data/data/$pkg/files/home, TMPDIR=/data/data/$pkg/files/usr/tmp]
mShellPid: 15381
mTerminalFileDescriptor: 54

操作terminal

1
2
3
4
5
6
7
8
9
10
/**
* A queue written to from a separate thread when the process outputs, and read by main thread to process by
* terminal emulator.
*/
private final ByteQueue mProcessToTerminalIOQueue = new ByteQueue(4096);
/**
* A queue written to from the main thread due to user interaction, and read by another thread which forwards by
* writing to the {@link #mTerminalFileDescriptor}.
*/
private final ByteQueue mTerminalToProcessIOQueue = new ByteQueue(4096);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public void initializeEmulator(int columns, int rows) {

int[] processId = new int[1];
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
mShellPid = processId[0];

final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor);

new Thread("TermSessionInputReader[pid=" + mShellPid + "]") {
@Override
public void run() {
try (InputStream termIn = new FileInputStream(terminalFileDescriptorWrapped)) {
final byte[] buffer = new byte[4096];
while (true) {
int read = termIn.read(buffer);
if (read == -1) return;
if (!mProcessToTerminalIOQueue.write(buffer, 0, read)) return;
mMainThreadHandler.sendEmptyMessage(MSG_NEW_INPUT);
}
} catch (Exception e) {
// Ignore, just shutting down.
}
}
}.start();

new Thread("TermSessionOutputWriter[pid=" + mShellPid + "]") {
@Override
public void run() {
final byte[] buffer = new byte[4096];
try (FileOutputStream termOut = new FileOutputStream(terminalFileDescriptorWrapped)) {
while (true) {
int bytesToWrite = mTerminalToProcessIOQueue.read(buffer, true);
if (bytesToWrite == -1) return;
termOut.write(buffer, 0, bytesToWrite);
}
} catch (IOException e) {
// Ignore.
}
}
}.start();

new Thread("TermSessionWaiter[pid=" + mShellPid + "]") {
@Override
public void run() {
int processExitCode = JNI.waitFor(mShellPid);
mMainThreadHandler.sendMessage(mMainThreadHandler.obtainMessage(MSG_PROCESS_EXITED, processExitCode));
}
}.start();

}

createSubprocess

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
static int create_subprocess(JNIEnv* env,
char const* cmd,
char const* cwd,
char* const argv[],
char** envp,
int* pProcessId,
jint rows,
jint columns)
{
int ptm = open("/dev/ptmx", O_RDWR | O_CLOEXEC);
if (ptm < 0) return throw_runtime_exception(env, "Cannot open /dev/ptmx");

#ifdef LACKS_PTSNAME_R
char* devname;
#else
char devname[64];
#endif
if (grantpt(ptm) || unlockpt(ptm) ||
#ifdef LACKS_PTSNAME_R
(devname = ptsname(ptm)) == NULL
#else
ptsname_r(ptm, devname, sizeof(devname))
#endif
) {
return throw_runtime_exception(env, "Cannot grantpt()/unlockpt()/ptsname_r() on /dev/ptmx");
}

// Enable UTF-8 mode and disable flow control to prevent Ctrl+S from locking up the display.
struct termios tios;
tcgetattr(ptm, &tios);
tios.c_iflag |= IUTF8;
tios.c_iflag &= ~(IXON | IXOFF);
tcsetattr(ptm, TCSANOW, &tios);

/** Set initial winsize. */
struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) columns };
ioctl(ptm, TIOCSWINSZ, &sz);

pid_t pid = fork();
if (pid < 0) {
return throw_runtime_exception(env, "Fork failed");
} else if (pid > 0) {
*pProcessId = (int) pid;
return ptm;
} else {
// Clear signals which the Android java process may have blocked:
sigset_t signals_to_unblock;
sigfillset(&signals_to_unblock);
sigprocmask(SIG_UNBLOCK, &signals_to_unblock, 0);

close(ptm);
setsid();

int pts = open(devname, O_RDWR);
if (pts < 0) exit(-1);

dup2(pts, 0);
dup2(pts, 1);
dup2(pts, 2);

DIR* self_dir = opendir("/proc/self/fd");
if (self_dir != NULL) {
int self_dir_fd = dirfd(self_dir);
struct dirent* entry;
while ((entry = readdir(self_dir)) != NULL) {
int fd = atoi(entry->d_name);
if(fd > 2 && fd != self_dir_fd) close(fd);
}
closedir(self_dir);
}

clearenv();
if (envp) for (; *envp; ++envp) putenv(*envp);

if (chdir(cwd) != 0) {
char* error_message;
// No need to free asprintf()-allocated memory since doing execvp() or exit() below.
if (asprintf(&error_message, "chdir(\"%s\")", cwd) == -1) error_message = "chdir()";
perror(error_message);
fflush(stderr);
}
execvp(cmd, argv);
// Show terminal output about failing exec() call:
char* error_message;
if (asprintf(&error_message, "exec(\"%s\")", cmd) == -1) error_message = "exec()";
perror(error_message);
_exit(1);
}
}

一些问题和思考

  • 最开始的版本使用的bootstrap.zip中打包进了包括bash以及apt在内的许多命令及依赖,大小有16M左右,它提供了一个比较完整的shell功能,后续可以通过apt去下载python然后获取youtube-dl,一个流程下载需要26M左右的流量
  • 由于deb包以及bootstrap.zip都是我们自己编译后放在自己的服务器上的,因此在安装python时可以按照dpkg本地安装的方式,python.deb的获取通过http的方式从服务器下载,在编译bootstrap.zip时只添加dpkg以及其依赖,其余的apt和bash都不提供,使用Android系统内部的shell客户端————sh,虽然sh功能有限,使用不方便,但是可以满足基本的一些需求。按照这种方式,打包的bootstrap.zip只包括dpkg,大小为3M,然后加上下载的python以及youtube-dl,总共13M左右(而且免去了apt update等所需的流量)。
  • 由于dpkg按照python.deb的方式需要手动解决依赖包的问题,需要手动下载许多的deb依赖包,因此最后直接在打包bootstrap.zip的时候,去掉dpkg,直接加上python的依赖,将其打包进bootstrap.zip,在客户端下载配置完bootstrap.zip后,即可直接使用python的pip下载youtube-dl,操作上方便了许多,并且避免了不同Android系统可能存在的兼容性问题。

Android App Bundle

概述

Android App Bundle是Google推出的Apk动态打包,动态组件化的技术,与Instant App不同,AAB是借助Split Apk完成动态加载,使用AAB动态下发方式,可以大幅度减少应用体积。只须在 Android Studio 中构建一个应用 (app bundle),就可以将应用所需的全部内容 (适用于所有设备) 都涵盖在内:所有语言、所有设备屏幕大小、所有硬件架构。它本身并不支持动态化,只是动态化的一个载体文件,真正实现逻辑并不是它。

Split APK

Split APK是Google为解决65536上限,以及APK安装包越来越大等问题,在Android L中引入的机制。它可以将一个庞大的APK,按屏幕密度,ABI等形式拆分成多个独立的APK,在应用程序更新时,不必下载整个APK,只需单独下载某个模块即可安装更新。Split APK将原来一个APK中多个模块共享同一份资源的模型分离成多个APK使用各自的资源,并且可以继承Base APK中的资源,多个APK有相同的data,cache目录,多个dex文件,相同的进程,在Settings.apk中只显示一个APK,并且使用相同的包名。

Building App Bundles

在gradle中配置Split的维度:

1
2
3
4
5
6
7
8
9
10
11
12
13
android {
bundle {
language {
enableSplit = true
}
density {
enableSplit = true
}
abi {
enableSplit = true
}
}
}

然后通过Android Studio/Gradle/Bundletool生成.aab文件。

Test Android App Bundle

可以通过两种方法测试aab:

  1. 在本地使用 bundletool 命令行工具
  2. 将 bundle上传到 Google Play Console,然后使用新的内部测试轨道

bundletool

Github 仓库 中下载 bundletool 工具。

从 app bundle 生成一组 APK:

1
java -jar bundletool-all-0.3.3.jar build-apks --bundle=[aab file] --output=[output file name].apks

解压.apks

解压 .apks 文件:unzip app.apks -d tmp,输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
Archive:  app.apks
extracting: tmp/base-hdpi.apk
extracting: tmp/base-mdpi.apk
extracting: tmp/base-ldpi.apk
extracting: tmp/base-xhdpi.apk
extracting: tmp/base-xxxhdpi.apk
extracting: tmp/base-xxhdpi.apk
extracting: tmp/base-tvdpi.apk
extracting: tmp/base-ca.apk
extracting: tmp/base-da.apk
extracting: tmp/base-fa.apk
extracting: tmp/base-ja.apk
extracting: tmp/base-ka.apk
extracting: tmp/base-pa.apk
extracting: tmp/base-ta.apk
extracting: tmp/base-nb.apk
extracting: tmp/base-be.apk
extracting: tmp/base-de.apk
extracting: tmp/base-ne.apk
extracting: tmp/base-te.apk
extracting: tmp/base-af.apk
extracting: tmp/base-bg.apk
extracting: tmp/base-th.apk
extracting: tmp/base-fi.apk
extracting: tmp/base-si.apk
extracting: tmp/base-hi.apk
extracting: tmp/base-vi.apk
extracting: tmp/base-kk.apk
extracting: tmp/base-mk.apk
extracting: tmp/base-sk.apk
extracting: tmp/base-uk.apk
extracting: tmp/base-el.apk
extracting: tmp/base-master.apk
extracting: tmp/base-gl.apk
extracting: tmp/base-nl.apk
extracting: tmp/base-ml.apk
extracting: tmp/base-pl.apk
extracting: tmp/base-sl.apk
extracting: tmp/base-tl.apk
extracting: tmp/base-am.apk
extracting: tmp/base-km.apk
extracting: tmp/base-bn.apk
extracting: tmp/base-in.apk
extracting: tmp/base-kn.apk
extracting: tmp/base-mn.apk
extracting: tmp/base-ko.apk
extracting: tmp/base-lo.apk
extracting: tmp/base-ro.apk
extracting: tmp/base-sq.apk
extracting: tmp/base-ar.apk
extracting: tmp/base-fr.apk
extracting: tmp/base-hr.apk
extracting: tmp/base-mr.apk
extracting: tmp/base-or.apk
extracting: tmp/base-sr.apk
extracting: tmp/base-tr.apk
extracting: tmp/base-ur.apk
extracting: tmp/base-as.apk
extracting: tmp/base-bs.apk
extracting: tmp/base-cs.apk
extracting: tmp/base-es.apk
extracting: tmp/base-is.apk
extracting: tmp/base-ms.apk
extracting: tmp/base-et.apk
extracting: tmp/base-it.apk
extracting: tmp/base-lt.apk
extracting: tmp/base-pt.apk
extracting: tmp/base-gu.apk
extracting: tmp/base-eu.apk
extracting: tmp/base-hu.apk
extracting: tmp/base-ru.apk
extracting: tmp/base-lv.apk
extracting: tmp/base-zu.apk
extracting: tmp/base-sv.apk
extracting: tmp/base-iw.apk
extracting: tmp/base-sw.apk
extracting: tmp/base-hy.apk
extracting: tmp/base-ky.apk
extracting: tmp/base-my.apk
extracting: tmp/base-az.apk
extracting: tmp/base-uz.apk
extracting: tmp/base-en.apk
extracting: tmp/base-zh.apk
extracting: tmp/base-armeabi_v7a.apk
extracting: tmp/base-arm64_v8a.apk
extracting: tmp/standalone-arm64_v8a_mdpi.apk
extracting: tmp/standalone-arm64_v8a_ldpi.apk
extracting: tmp/standalone-arm64_v8a_hdpi.apk
extracting: tmp/standalone-arm64_v8a_xhdpi.apk
extracting: tmp/standalone-armeabi_v7a_ldpi.apk
extracting: tmp/standalone-arm64_v8a_xxxhdpi.apk
extracting: tmp/standalone-arm64_v8a_xxhdpi.apk
extracting: tmp/standalone-arm64_v8a_tvdpi.apk
extracting: tmp/standalone-armeabi_v7a_mdpi.apk
extracting: tmp/standalone-armeabi_v7a_xhdpi.apk
extracting: tmp/standalone-armeabi_v7a_xxhdpi.apk
extracting: tmp/standalone-armeabi_v7a_hdpi.apk
extracting: tmp/standalone-armeabi_v7a_xxxhdpi.apk
extracting: tmp/standalone-armeabi_v7a_tvdpi.apk
inflating: tmp/toc.pb

可以使用如下命令安装多个 split apks:

1
adb install-multiple -r 1.apk 2.apk

Google Play Console

使用内部测试方式。

Dynamic Delivery

  • 基本 APK
  • 配置 APK
  • 动态功能 APK

一些问题

  • 从非GP的渠道下载base.apk:采用Google官方的方法,即提示用户跳转GP安装最新版
  • 从Apk更新到aab:测试之后发现可以正常更新
  • 分享apk:Instant Apps/茄子快传分享Split Apk时可以组装成一个完整的apk
  • Google Play签名计划会使用我们自己的签名文件去签名(而不是我们手动签名)(v1,v2)

附录

概述

注:本文基于Android 9.0源码,为了文章的简洁性,引用源码的地方可能有所删减。

本文主要分析Android系统在启动之后系统服务之间的关系以及各个关键进程的启动逻辑,Android系统简要启动流程如下(图片来源网络,总结得非常好):

android-boot

系统启动

当电源按下时引导芯片代码会从预定义的地方(固化在ROM)开始执行,加载引导程序BootLoader到RAM,然后执行。

引导程序BootLoader

它是Android操作系统开始运行前的一个小程序,主要将操作系统OS拉起来并进行。

Linux内核启动

当内核启动时,会设置缓存、加载驱动等。此外,还启动了Kernel的swapper进程(pid = 0)和kthreadd进程(pid = 2):

  • swapper进程:又称为idle进程,系统初始化过程Kernel由无到有开创的第一个进程, 用于初始化进程管理、内存管理,加载Binder Driver、Display、Camera Driver等相关工作。
  • kthreadd进程:Linux系统的内核进程,是所有内核进程的鼻祖,会创建内核工作线程kworkder,软中断线程ksoftirqd,thermal等内核守护进程。

当内核完成系统设置时,它首先在系统中寻找init.rc文件,并启动init进程。init进程是一个由内核启动的第一个用户级进程,它的进程号是1,父进程id号是0。init进程是所有用户空间的鼻祖, 它会启动servicemanager(binder服务管家,其功能为查询和注册服务), Zygote进程(Java进程的鼻祖). Zygote进程会创建 system_server进程以及各种app进程。

init进程

init.cpp

init进程的入口函数是system/core/init/init.cpp的main函数,它的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
int main(int argc, char** argv) {
add_environment("PATH", _PATH_DEFPATH);

// 第一阶段:内核态
// 第二阶段:用户态
bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr);

if (is_first_stage) {
// Clear the umask,与Linux系统权限有关
umask(0);

// Get the basic filesystem setup we need put together in the initramdisk
// on / and then we'll let the rc file figure out the rest.
mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755");
mkdir("/dev/pts", 0755);
// ...

InitKernelLogging(argv);

LOG(INFO) << "init first stage started!";

if (!DoFirstStageMount()) {
LOG(ERROR) << "Failed to mount required partitions early ...";
panic();
}

// Set up SELinux(Security-Enhanced Linux), loading the SELinux policy.
selinux_initialize(true);

// We're in the kernel domain, so re-exec init to transition to the init domain now
// that the SELinux policy has been loaded.
// 按selinux policy要求,重新设置init文件属性
if (restorecon("/init") == -1) {
// 失败的话会reboot
PLOG(ERROR) << "restorecon failed";
security_failure();
}

setenv("INIT_SECOND_STAGE", "true", 1);
setenv("INIT_STARTED_AT", StringPrintf("%" PRIu64, start_ms).c_str(), 1);

// 再次调用init的main函数,启动用户态的init进程
char* path = argv[0];
char* args[] = { path, nullptr };
execv(path, args);

// execv() only returns if an error happened, in which case we
// panic and never fall through this conditional.
PLOG(ERROR) << "execv(\"" << path << "\") failed";
security_failure();
}

// 第二阶段:用户态
InitKernelLogging(argv);
LOG(INFO) << "init second stage started!";

property_init(); // 初始化属性服务

// 执行内核命令
process_kernel_dt();
process_kernel_cmdline();
export_kernel_boot_props();

// 设置属性
property_set("ro.boottime.init", getenv("INIT_STARTED_AT"));
property_set("ro.boottime.init.selinux", getenv("INIT_SELINUX_TOOK"));

// 重置之前使用过的一些环境变量
unsetenv("INIT_SECOND_STAGE");
unsetenv("INIT_STARTED_AT");
unsetenv("INIT_SELINUX_TOOK");
unsetenv("INIT_AVB_VERSION");

// Now set up SELinux for second stage.
selinux_initialize(false);
selinux_restore_context();

signal_handler_init();

// 开启属性服务
start_property_service();

// 解析init.rc
// ...

while (true) {
// 默认会休眠直到有事件唤醒
int epoll_timeout_ms = -1;

if (!(waiting_for_prop || ServiceManager::GetInstance().IsWaitingForExec())) {
am.ExecuteOneCommand();
}
// 重启一些挂掉的进程,例如Zygote
if (!(waiting_for_prop || ServiceManager::GetInstance().IsWaitingForExec())) {
restart_processes();
// If there's a process that needs restarting, wake up in time for that.
if (process_needs_restart_at != 0) {
epoll_timeout_ms = (process_needs_restart_at - time(nullptr)) * 1000;
if (epoll_timeout_ms < 0) epoll_timeout_ms = 0;
}

// If there's more work to do, wake up again immediately.
if (am.HasMoreCommands()) epoll_timeout_ms = 0;
}

epoll_event ev;
int nr = TEMP_FAILURE_RETRY(epoll_wait(epoll_fd, &ev, 1, epoll_timeout_ms));
if (nr == -1) {
PLOG(ERROR) << "epoll_wait failed";
} else if (nr == 1) {
((void (*)()) ev.data.ptr)();
}
}

return 0;
}

第一阶段:

  • 判断及增加环境变量
  • 创建文件系统目录并挂载相关的文件系统
  • 重定向输入输出/内核Log系统
  • 挂载一些分区设备
  • 完成SELinux相关工作
  • is_first_stage 收尾

第二阶段:

  • 初始化属性域,清空环境变量
  • 完成SELinux相关工作
  • 启动属性服务
  • 解析init.rc配置文件,执行各个阶段的动作,创建Zygote的工作就是在其中的某个阶段完成的。
  • init进入一个无限循环,并且等待一些事情的发生。

下面给出 init.rc 部分内容(Mi Max3–MIUI 10–Android 9):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
on early-init
on init
on property:sys.boot_from_charger_mode=1
on load_persist_props_action
on firmware_mounts_complete
on late-init
on post-fs // mount file system
start logd
mount rootfs rootfs / ro remount
mount rootfs rootfs / shared rec
mount none /mnt/runtime/default /storage slave bind rec
// ...
// ...
on post-fs-data // mount /data/
// 启动 logd
start logd
// 启动 vold, 用于管理Android外部存储介质的后台进程,包括SD卡的插拔等
start vold
// ...
// ...
on boot
// ...
class_start core

启动service

在 init.zygote64_32.rc 文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
class main
priority -20
user root
group root readproc reserved_disk
socket zygote stream 660 root system
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart audioserver
onrestart restart cameraserver
onrestart restart media
onrestart restart netd
onrestart restart wificond
writepid /dev/cpuset/foreground/tasks

service zygote_secondary /system/bin/app_process32 -Xzygote /system/bin --zygote --socket-name=zygote_secondary --enable-lazy-preload
class main
priority -20
user root
group root readproc reserved_disk
socket zygote_secondary stream 660 root system
onrestart restart zygote
writepid /dev/cpuset/foreground/tasks

通过 init_parser.cpp 完成整个 service 解析工作,此处就不详细展开讲解析过程。

zygote所对应的可执行文件是/system/bin/app_process,通过调用pid = fork()创建子进程,通过execve(svc->args[0], (char**)svc->args, (char**) ENV),进入app_main.cpp的main()函数。故zygote是通过fork和execv共同创建的。

流程如下:

zygote启动

当init子进程退出时,会产生 SIGCHLD 信号,并发送给init进程,通过socket套接字传递数据,调用到 wait_for_one_process() 方法,根据是否是oneshot,来决定是重启子进程,还是放弃启动。surfaceflinger, servicemanager, zygote 以及 system_server 进程被杀都会触发Zygote重启。

属性服务

我们知道,Windows平台上有一个叫注册表的东西。注册表可以存储一些类似key/value的键值对。一般而言,系统或某些应用程序会把自己的一些属性存储在注册表中,即使下次系统重启或应用程序重启,它还能够根据之前在注册表中设置的属性,进行相应的初始化工作。

Android平台也提供了一个类型机制,可称之为属性服务(property service)。应用程序可通过这个属性机制,查询或设置属性。当某个进程A修改属性值后,init进程会检查访问权限,当权限满足要求后,则更改相应的属性值,属性值一旦改变则会触发相应的触发器(即rc文件中的on开头的语句),

属性服务初始化:

1
2
3
4
5
6
void property_init(void)
{
init_property_area();//初始化属性存储区域
//加载default.prop文件
load_properties_from_file(PROP_PATH_RAMDISK_DEFAULT);
}

在 properyty_init 函数中,先调用 init_property_area 函数,创建一块用于存储属性的存储区域(共享内存),然后加载 default.prop 文件中的内容。

访问方法:

  • Native: property_get/property_set
  • Java: SystemProperties
  • Shell: adb shell getprop

zygote进程

概述

Zygote是由init进程通过解析init.zygote.rc文件而创建的,zygote所对应的可执行程序app_process,所对应的源文件是app_main.cpp,进程名为zygote。

当Zygote进程启动后, 便会执行到frameworks/base/cmds/app_process/app_main.cpp文件的main()方法。整个调用流程:

1
2
3
4
5
6
7
8
9
app_main.main
AndroidRuntime.start
AndroidRuntime.startVm
AndroidRuntime.startReg
ZygoteInit.main (首次进入Java世界)
registerServerSocketFromEnv
preload
forkSystemServer
runSelectLoop

Zygote进程创建Java虚拟机,并注册JNI方法,真正成为Java进程的母体,用于孵化Java进程。在创建完system_server进程后,zygote功成身退,调用runSelectLoop()随时待命,当接收到创建新进程请求时立即唤醒并执行相应工作。Zygote进程共做了如下几件事:

  1. 创建AppRuntime并调用其start方法,启动Zygote进程。
  2. 创建DVM(ART)并为DVM注册JNI。
  3. 通过JNI调用ZygoteInit的main函数进入Zygote的Java框架层。
  4. 通过registerServerSocketFromEnv函数创建服务端Socket,并通过runSelectLoop函数等待ActivityManagerService的请求来创建新的应用程序进程。
  5. 启动SystemServer进程。

app_main.cpp

Zygote本身是一个Native的应用程序,和驱动、内核等均无关系。Zygote是由init进程根据init.rc文件中的配置项而创建的。zygote最初的名字叫“app_process”,这个名字是在Android.mk文件中被指定的,但app_process在运行过程中,通过Linux下的pctrl系统调用将自己的名字换成了“zygote”,所以我们通过ps命令看到的进程名是“zygote”。

zygote的原型app_process所对应的源文件是framework/base/cmds/app_process/app_main.cpp,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
int main(int argc, const char* const argv[])
{
AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));

// --zygote : Start in zygote mode
// --start-system-server : Start the system server.
// --application : Start in application (stand alone, non zygote) mode.
// --nice-name : The nice name for this process.
bool zygote = false;
bool startSystemServer = false;
bool application = false;
String8 niceName;
String8 className;

while (i < argc) {
const char* arg = argv[i++];
if (strcmp(arg, "--zygote") == 0) {
zygote = true;
niceName = ZYGOTE_NICE_NAME; // zygote64 or zygote
} else if (strcmp(arg, "--start-system-server") == 0) {
startSystemServer = true;
} else if (strcmp(arg, "--application") == 0) {
application = true;
} else if (strncmp(arg, "--nice-name=", 12) == 0) {
niceName.setTo(arg + 12);
} else if (strncmp(arg, "--", 2) != 0) {
className.setTo(arg);
break;
} else {
--i;
break;
}
}

Vector<String8> args;
if (!className.isEmpty()) {
// We're not in zygote mode
// 需要传递给RuntimeInit的唯一参数是application参数,剩余的args传递给启动类main
args.add(application ? String8("application") : String8("tool"));
runtime.setClassNameAndArgs(className, argc - i, argv + i);
} else {
// We're in zygote mode.
maybeCreateDalvikCache();

if (startSystemServer) {
args.add(String8("start-system-server"));
}

char prop[PROP_VALUE_MAX];
if (property_get(ABI_LIST_PROPERTY, prop, NULL) == 0) {
LOG_ALWAYS_FATAL("app_process: Unable to determine ABI list from property %s.",
ABI_LIST_PROPERTY);
return 11;
}

String8 abiFlag("--abi-list=");
abiFlag.append(prop);
args.add(abiFlag);

// In zygote mode, pass all remaining arguments to the zygote
// main() method.
for (; i < argc; ++i) {
args.add(String8(argv[i]));
}
}

if (!niceName.isEmpty()) {
runtime.setArgv0(niceName.string(), true /* setProcName */);
}

if (zygote) {
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
} else if (className) {
runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
} else {
fprintf(stderr, "Error: no class name or --zygote supplied.\n");
app_usage();
LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
}
}

AppRuntime类的声明和实现均在app_main.cpp中,它是从AndroidRuntime类派生出来的,上述start函数使用的是基类AndroidRuntime的start。

AndroidRuntime.start

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// frameworks/base/core/jni/AndroidRuntime.cpp
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
const char* rootDir = getenv("ANDROID_ROOT");
if (rootDir == NULL) {
rootDir = "/system";
if (!hasDir("/system")) {
LOG_FATAL("No root directory specified, and /android does not exist.");
return;
}
setenv("ANDROID_ROOT", rootDir, 1);
}

/* start the virtual machine */
JniInvocation jni_invocation;
jni_invocation.Init(NULL);
JNIEnv* env;
if (startVm(&mJavaVM, &env, zygote) != 0) {
return;
}
onVmCreated(env);

// 因为后续Java世界用到的一些函数是采用native方式来实现的,所以必须提前注册这些函数
if (startReg(env) < 0) {
ALOGE("Unable to register all android natives\n");
return;
}

// 将参数封装到strArray里

/*
* Start VM. This thread becomes the main thread of the VM, and will
* not return until the VM exits.
*/
char* slashClassName = toSlashClassName(className != NULL ? className : "");
jclass startClass = env->FindClass(slashClassName);
if (startClass == NULL) {
ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);
} else {
// 找到ZygoteInit类的static main函数的jMethodId
jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
"([Ljava/lang/String;)V");
if (startMeth == NULL) {
ALOGE("JavaVM unable to find main() in '%s'\n", className);
} else {
// 调用ZygoteInit.main函数后,Zygote便进入了Java世界
env->CallStaticVoidMethod(startClass, startMeth, strArray);
}
}
free(slashClassName);
}

ZygoteInit.main

CallStaticVoidMethod最终将调用com.android.internal.os.ZygoteInit的main函数,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
public static void main(String argv[]) {
ZygoteServer zygoteServer = new ZygoteServer();
try {
boolean startSystemServer = false;
String socketName = "zygote";
String abiList = null;
boolean enableLazyPreload = false;
for (int i = 1; i < argv.length; i++) {
if ("start-system-server".equals(argv[i])) {
startSystemServer = true;
} else if ("--enable-lazy-preload".equals(argv[i])) {
enableLazyPreload = true;
} else if (argv[i].startsWith(ABI_LIST_ARG)) {
abiList = argv[i].substring(ABI_LIST_ARG.length());
} else if (argv[i].startsWith(SOCKET_NAME_ARG)) {
socketName = argv[i].substring(SOCKET_NAME_ARG.length());
} else {
throw new RuntimeException("Unknown command line argument: " + argv[i]);
}
}
// 创建一个Zygote的Socket接口,用来和AMS等通信
zygoteServer.registerServerSocketFromEnv(socketName);
if (!enableLazyPreload) {
// 预加载一些类和资源,这是导致Android系统启动慢的原因之一
// 应用程序都从Zygote孵化出来,应用程序都会继承Zygote的所有内容,如果在Zygote启动的时候加载这些类和资源,
// 这些孵化的应用程序就继承Zygote的类和资源,这样启动引用程序的时候就不需要加载类和资源了,启动的速度就会快很多。
preload(bootTimingsTraceLog);
}

// Do an initial gc to clean up after startup
gcAndFinalize();

if (startSystemServer) {
Runnable r = forkSystemServer(abiList, socketName, zygoteServer);
// 子进程(system_server)
if (r != null) {
// 调用com.android.server.SystemServer.main方法
r.run();
return;
}
}

Log.i(TAG, "Accepting command socket connections");

// The select loop returns early in the child process after a fork and
// loops forever in the zygote.
caller = zygoteServer.runSelectLoop(abiList);
} catch (Throwable ex) {
Log.e(TAG, "System zygote died with exception", ex);
throw ex;
} finally {
zygoteServer.closeServerSocket();
}

// We're in the child process and have exited the select loop. Proceed to execute the command.
if (caller != null) {
caller.run();
}
}

上面有三个重要的调用:

  • registerServerSocketFromEnv用来创建一个Zygote的Socket接口,用来和AMS等通信
  • forkSystemServer用来fork创建system_server进程;
  • runSelectLoop用来处理客户端连接与请求,包括AMS请求创建app进程。

system_server进程

概述

SyetemServer在启动时做了如下工作:

  1. 启动Binder线程池,这样就可以与其他进程进行通信。
  2. 创建SystemServiceManager用于对系统的服务进行创建、启动和生命周期管理。
  3. 启动各种系统服务。AMS,PMS,以及WMS等都是运行在system_server这个进程中的线程。

ZygoteInit.forkSystemServer

在上节中,ZygoteInit.main方法首次进入Java世界,然后调用了forkSystemServer方法创建system_server进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
private static Runnable forkSystemServer(String abiList, String socketName,
ZygoteServer zygoteServer) {
/* Hardcoded command line to start the system server */
String args[] = {
"--setuid=1000",
"--setgid=1000",
"--setgroups=1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1018,1021,1023,1024,1032,1065,3001,3002,3003,3006,3007,3009,3010",
"--capabilities=" + capabilities + "," + capabilities,
"--nice-name=system_server",
"--runtime-args",
"--target-sdk-version=" + VMRuntime.SDK_VERSION_CUR_DEVELOPMENT,
"com.android.server.SystemServer",
};
ZygoteConnection.Arguments parsedArgs = null;

int pid;

try {
parsedArgs = new ZygoteConnection.Arguments(args);
ZygoteConnection.applyDebuggerSystemProperty(parsedArgs);
ZygoteConnection.applyInvokeWithSystemProperty(parsedArgs);

boolean profileSystemServer = SystemProperties.getBoolean(
"dalvik.vm.profilesystemserver", false);
if (profileSystemServer) {
parsedArgs.runtimeFlags |= Zygote.PROFILE_SYSTEM_SERVER;
}

/* Request to fork the system server process */
pid = Zygote.forkSystemServer(
parsedArgs.uid, parsedArgs.gid,
parsedArgs.gids,
parsedArgs.runtimeFlags,
null,
parsedArgs.permittedCapabilities,
parsedArgs.effectiveCapabilities);
} catch (IllegalArgumentException ex) {
throw new RuntimeException(ex);
}

/* For child process */
if (pid == 0) {
if (hasSecondZygote(abiList)) {
waitForSecondaryZygote(socketName);
}

zygoteServer.closeServerSocket();
return handleSystemServerProcess(parsedArgs);
}

return null;
}

在这里设置了system_server进程的uid,gid和groups,nice-name等,有两个重要的调用:

  • Zygote.forkSystemServer()函数用来fork一个新的进程,如果pid==0,表示已经进入SystemServer子进程,于是先关闭“Zygote”socket,因为系统服务进程system_server也继承了Socket,不用所以close它;
  • 调用了handleSystemServerProcess()方法,返回Runnable对象到ZygoteInit.main并调用,其实是调用到了com.android.server.SystemServer.main方法。

Zygote.forkSystemServer

首先看看Zygote.forkSystemServer方法,它调用的是nativeForkSystemServer方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
static jint com_android_internal_os_Zygote_nativeForkSystemServer(
JNIEnv* env, jclass, uid_t uid, gid_t gid, jintArray gids,
jint runtime_flags, jobjectArray rlimits, jlong permittedCapabilities,
jlong effectiveCapabilities) {
pid_t pid = ForkAndSpecializeCommon(env, uid, gid, gids,
runtime_flags, rlimits,
permittedCapabilities, effectiveCapabilities,
MOUNT_EXTERNAL_DEFAULT, NULL, NULL, true, NULL,
NULL, NULL, NULL);
if (pid > 0) {
}
return pid;
}

static pid_t ForkAndSpecializeCommon(/* ... */) {
SetSignalHandlers();
pid_t pid = fork(); // fork子进程
UnsetChldSignalHandler();
}

static void SetSignalHandlers() {
struct sigaction sig_chld = {};
sig_chld.sa_handler = SigChldHandler;

if (sigaction(SIGCHLD, &sig_chld, NULL) < 0) {
ALOGW("Error setting SIGCHLD handler: %s", strerror(errno));
}

struct sigaction sig_hup = {};
sig_hup.sa_handler = SIG_IGN;
if (sigaction(SIGHUP, &sig_hup, NULL) < 0) {
ALOGW("Error setting SIGHUP handler: %s", strerror(errno));
}
}

// Sets the SIGCHLD handler back to default behavior in zygote children.
static void UnsetChldSignalHandler() {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = SIG_DFL;

if (sigaction(SIGCHLD, &sa, NULL) < 0) {
ALOGW("Error unsetting SIGCHLD handler: %s", strerror(errno));
}
}

static void SigChldHandler(int /*signal_number*/) {
pid_t pid;
int status;

while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
if (pid == gSystemServerPid) {
ALOGE("Exit zygote because system server (%d) has terminated", pid);
kill(getpid(), SIGKILL);
}
}
}

在ForkAndSpecializeCommon中有个逻辑是如果SystemServer进程停止工作,那么首先通过getpid()来获取Zygote进程的pid,然后调用kill函数杀死它,即SystemServer停止工作之后,Zygote进程自杀,然后在Init进程的main()函数中有一个死循环,如果它的子进程Zygote停止工作,就会去重启子进程。

handleSystemServerProcess

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private static Runnable handleSystemServerProcess(ZygoteConnection.Arguments parsedArgs) {
ClassLoader cl = createPathClassLoader(systemServerClasspath, parsedArgs.targetSdkVersion);
Thread.currentThread().setContextClassLoader(cl);
return ZygoteInit.zygoteInit(parsedArgs.targetSdkVersion, parsedArgs.remainingArgs, cl);
}

public static final Runnable zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) {
// 执行Binder驱动程序初始化的相关工作,它调用之后system_server进程就可以进行Binder进程通信
ZygoteInit.nativeZygoteInit();
return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader);
}

// frameworks/base/core/java/com/android/internal/os/RuntimeInit.java
protected static Runnable applicationInit(int targetSdkVersion, String[] argv,
ClassLoader classLoader) {
final Arguments args = new Arguments(argv);
return findStaticMain(args.startClass, args.startArgs, classLoader);
}

// 通过反射获得com.android.server.SystemServer的main方法
protected static Runnable findStaticMain(String className, String[] argv,
ClassLoader classLoader) {
Class<?> cl = Class.forName(className, true, classLoader);
Method m = cl.getMethod("main", new Class[] { String[].class });
return new MethodAndArgsCaller(m, argv);
}

static class MethodAndArgsCaller implements Runnable {
/** method to call */
private final Method mMethod;

/** argument array */
private final String[] mArgs;

public MethodAndArgsCaller(Method method, String[] args) {
mMethod = method;
mArgs = args;
}

public void run() {
mMethod.invoke(null, new Object[] { mArgs });
}
}

到这里可以知道ZygoteInit.main方法中的Runnable r = forkSystemServer(abiList, socketName, zygoteServer);返回的r即是MethodAndArgsCaller,在子进程中调用r.run方法便是调用了com.android.server.SystemServer.main方法。

SystemServer#main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// SystemServer.java
public final class SystemServer {
...
public static void main(String[] args) {
//先初始化SystemServer对象,再调用对象的run()方法
new SystemServer().run();
}
}

private void run() {
Looper.prepareMainLooper();// 准备主线程looper

//加载android_servers.so库,该库包含的源码在frameworks/base/services/目录下
System.loadLibrary("android_servers");

createSystemContext(); //初始化系统上下文

//创建系统服务管理
mSystemServiceManager = new SystemServiceManager(mSystemContext);
LocalServices.addService(SystemServiceManager.class, mSystemServiceManager);

//启动各种系统服务
try {
startBootstrapServices(); // 启动引导服务
startCoreServices(); // 启动核心服务
startOtherServices(); // 启动其他服务
} catch (Throwable ex) {
throw ex;
}

//一直循环执行
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}

调用关系:

1
2
3
4
5
6
7
8
SystemServer.main
SystemServer.run
Looper.prepareMainLooper();
createSystemContext
startBootstrapServices();
startCoreServices();
startOtherServices();
Looper.loop();

LocalServices通过用静态Map变量sLocalServiceObjects,来保存以服务类名为key,以具体服务对象为value的Map结构。

首先看看启动引导服务的方法startBootstrapServices:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
private void startBootstrapServices() {
// 阻塞等待与installd建立socket通道
Installer installer = mSystemServiceManager.startService(Installer.class);

// In some cases after launching an app we need to access device identifiers,
// therefore register the device identifier policy before the activity manager.
mSystemServiceManager.startService(DeviceIdentifiersPolicyService.class);

mActivityManagerService = mSystemServiceManager.startService(
ActivityManagerService.Lifecycle.class).getService();
mActivityManagerService.setSystemServiceManager(mSystemServiceManager);
mActivityManagerService.setInstaller(installer);

mPowerManagerService = mSystemServiceManager.startService(PowerManagerService.class);
mActivityManagerService.initPowerManagement();

mSystemServiceManager.startService(RecoverySystemService.class);

// Now that we have the bare essentials of the OS up and running, take
// note that we just booted, which might send out a rescue party if
// we're stuck in a runtime restart loop.
RescueParty.noteBoot(mSystemContext);

mSystemServiceManager.startService(LightsService.class);

// Package manager isn't started yet; need to use SysProp not hardware feature
if (SystemProperties.getBoolean("config.enable_sidekick_graphics", false)) {
mSystemServiceManager.startService(WEAR_SIDEKICK_SERVICE_CLASS);
}

mDisplayManagerService = mSystemServiceManager.startService(DisplayManagerService.class);

// We need the default display before we can initialize the package manager.
mSystemServiceManager.startBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);

// 当设备正在加密时,仅运行core apps
String cryptState = SystemProperties.get("vold.decrypt");
if (ENCRYPTING_STATE.equals(cryptState)) {
Slog.w(TAG, "Detected encryption in progress - only parsing core apps");
mOnlyCore = true;
} else if (ENCRYPTED_STATE.equals(cryptState)) {
Slog.w(TAG, "Device encrypted - only parsing core apps");
mOnlyCore = true;
}

// Start the package manager.
mPackageManagerService = PackageManagerService.main(mSystemContext, installer,
mFactoryTestMode != FactoryTest.FACTORY_TEST_OFF, mOnlyCore);
mFirstBoot = mPackageManagerService.isFirstBoot();
mPackageManager = mSystemContext.getPackageManager();

mSystemServiceManager.startService(UserManagerService.LifeCycle.class);

AttributeCache.init(mSystemContext);

// Set up the Application instance for the system process and get started.
mActivityManagerService.setSystemProcess();

mDisplayManagerService.setupSchedulerPolicies();
mSystemServiceManager.startService(new OverlayManagerService(mSystemContext, installer));

// 启动传感器服务
startSensorService();

该方法所创建的服务:ActivityManagerService, PowerManagerService, LightsService, DisplayManagerService, PackageManagerService, UserManagerService, sensor服务等。

然后是启动核心服务startCoreServices:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void startCoreServices() {
// 启动服务BatteryService,用于统计电池电量,需要LightService.
mSystemServiceManager.startService(BatteryService.class);

// 启动服务UsageStatsService,用于统计应用使用情况
mSystemServiceManager.startService(UsageStatsService.class);
mActivityManagerService.setUsageStatsManager(
LocalServices.getService(UsageStatsManagerInternal.class));

if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_WEBVIEW)) {
// 启动服务WebViewUpdateService
mWebViewUpdateService = mSystemServiceManager.startService(WebViewUpdateService.class);
}

BinderCallsStatsService.start();
}

启动服务BatteryService,UsageStatsService,WebViewUpdateService等。

启动其它服务的startOtherServices方法比较长,主要是启动一系列的服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void startOtherServices() {
// ...
ServiceManager.addService("sec_key_att_app_id_provider", new KeyAttestationApplicationIdProviderService(context));
mSystemServiceManager.startService(KeyChainSystemService.class);
ServiceManager.addService("scheduling_policy", new SchedulingPolicyService());
mSystemServiceManager.startService(TelecomLoaderService.class);
telephonyRegistry = new TelephonyRegistry(context);
ServiceManager.addService("telephony.registry", telephonyRegistry);
mEntropyMixer = new EntropyMixer(context);

mContentResolver = context.getContentResolver();

mSystemServiceManager.startService(ACCOUNT_SERVICE_CLASS);
mSystemServiceManager.startService(CONTENT_SERVICE_CLASS);

// 准备好window, power, package, display服务
wm.systemReady();
mPowerManagerService.systemReady(...);
mPackageManagerService.systemReady();
mDisplayManagerService.systemReady(...);
mActivityManagerService.systemReady(...);
// ...
}

到此, System_server主线程的启动工作总算完成, 进入Looper.loop()状态,等待其他线程通过handler发送消息到主线再处理。

与system_server进程的Application相关的解析可见:Android-Application

app进程

AMS发送请求

ActivityManagerService也是由SystemServer创建的。假设通过startActivity来启动一个新的Activity,而这个Activity附属于一个还未启动的进程,则会启动一个新的进程,调用startActivity的进程通过Binder调用到ActivityManagerService中的方法,然后经过层层调用(具体可见Android-Activity启动原理),会调用到Process.start()方法,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static final ProcessStartResult start(final String processClass,
final String niceName,
int uid, int gid, int[] gids,
int runtimeFlags, int mountExternal,
int targetSdkVersion,
String seInfo,
String abi,
String instructionSet,
String appDataDir,
String invokeWith,
String[] zygoteArgs) {
return zygoteProcess.start(processClass, niceName, uid, gid, gids,
runtimeFlags, mountExternal, targetSdkVersion, seInfo,
abi, instructionSet, appDataDir, invokeWith, zygoteArgs);
}

这里的参数processClass为“android.app.ActivityThread”,它是传进去的第一个参数,也就是程序初始化进程时要加载的主文件Java类。当应用进程启动之后,会把这个类加载到进程,调用它的main()方法作为应用程序进程的入口。Process类的start()直接调用了ZygoteProcess类的start()方法,该start()方法调用了ZygoteProcess类的startViaZygote()方法,下面看看该方法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
private Process.ProcessStartResult startViaZygote(final String processClass, final String niceName,
final int uid, final int gid, final int[] gids, int runtimeFlags, int mountExternal,
int targetSdkVersion, String seInfo, String abi, String instructionSet,String appDataDir,
String invokeWith, String[] extraArgs) throws ZygoteStartFailedEx {
ArrayList<String> argsForZygote = new ArrayList<String>();
argsForZygote.add("--runtime-args");
argsForZygote.add("--setuid=" + uid);
argsForZygote.add("--setgid=" + gid);
// ......
synchronized(mLock) {
return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), argsForZygote);
}
}

首先给它设置值,包括uid、gid等。接着调用openZygoteSocketIfNeeded()方法来连接“zygote”Socket,链接Socket成功之后,就会调用zygoteSendArgsAndGetResult()方法来进一步处理。

先来看看openZygoteSocketIfNeeded()方法:

1
2
3
4
5
6
private ZygoteState openZygoteSocketIfNeeded(String abi) throws ZygoteStartFailedEx {
if (primaryZygoteState == null || primaryZygoteState.isClosed()) {
try {
primaryZygoteState = ZygoteState.connect(mSocket);
}
}

方法中的mSocket的值是“zygote”,通过connect()方法去连接“zygote”Socket。接着看看zygoteSendArgsAndGetResult()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static Process.ProcessStartResult zygoteSendArgsAndGetResult(
ZygoteState zygoteState, ArrayList<String> args)
throws ZygoteStartFailedEx {
try {
final BufferedWriter writer = zygoteState.writer;
final DataInputStream inputStream = zygoteState.inputStream;
writer.write(Integer.toString(args.size()));
writer.newLine();
for (int i = 0; i < sz; i++) {
String arg = args.get(i);
writer.write(arg);
writer.newLine();
}
writer.flush();
}

通过Socket写入流writer把前面传过来的那些参数写进去,Socket即ZygoteServer类的runSelectLoop()方法监听。写入这些数据之后,ZygoteServer类的runSelectLoop()方法就能被监听到。

响应请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
void setForkChild() {
mIsForkChild = true;
}

Runnable runSelectLoop(String abiList) {
ArrayList<FileDescriptor> fds = new ArrayList<FileDescriptor>();
ArrayList<ZygoteConnection> peers = new ArrayList<ZygoteConnection>();

fds.add(mServerSocket.getFileDescriptor());
peers.add(null);

while (true) {
StructPollfd[] pollFds = new StructPollfd[fds.size()];
for (int i = 0; i < pollFds.length; ++i) {
pollFds[i] = new StructPollfd();
pollFds[i].fd = fds.get(i);
pollFds[i].events = (short) POLLIN;
}
try {
Os.poll(pollFds, -1);
} catch (ErrnoException ex) {
throw new RuntimeException("poll failed", ex);
}
for (int i = pollFds.length - 1; i >= 0; --i) {
if ((pollFds[i].revents & POLLIN) == 0) {
continue;
}

if (i == 0) {
ZygoteConnection newPeer = acceptCommandPeer(abiList);
peers.add(newPeer);
fds.add(newPeer.getFileDesciptor());
} else {
try {
ZygoteConnection connection = peers.get(i);
final Runnable command = connection.processOneCommand(this);
// 通过mIsForkChild变量控制父进程接着死循环/子进程返回command
if (mIsForkChild) {
// We're in the child. We should always have a command to run at this
// stage if processOneCommand hasn't called "exec".
if (command == null) {
throw new IllegalStateException("command == null");
}

return command;
} else {
// We're in the server - we should never have any commands to run.
if (command != null) {
throw new IllegalStateException("command != null");
}

if (connection.isClosedByPeer()) {
connection.closeSocket();
peers.remove(i);
fds.remove(i);
}
}
} catch (Exception e) {
if (!mIsForkChild) {
ZygoteConnection conn = peers.remove(i);
conn.closeSocket();
fds.remove(i);
} else {

throw e;
}
} finally {
mIsForkChild = false;
}
}
}
}
}

进入ZygoteConnection类的processOneCommand()方法后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Runnable processOneCommand(ZygoteServer zygoteServer) {
pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, parsedArgs.gids,
parsedArgs.runtimeFlags, rlimits, parsedArgs.mountExternal, parsedArgs.seInfo,
parsedArgs.niceName, fdsToClose, fdsToIgnore, parsedArgs.instructionSet,
parsedArgs.appDataDir);
try {
if (pid == 0) {
// in child
zygoteServer.setForkChild();
zygoteServer.closeServerSocket();
IoUtils.closeQuietly(serverPipeFd);
serverPipeFd = null;
return handleChildProc(parsedArgs, descriptors, childPipeFd);
}
}
}
  • 此处通过Zygote.forkAndSpecialize()来fork新的应用进程,而启动systemserver进程是通过Zygote.forkSystemServer()来fork SystemServer进程。
  • 此处通过handleChildProc()方法处理,而之前是用handleSystemServerProcess()来处理。

通过fork新的应用程序进程之后,返回pid等于0就表示进入子进程,于是调用handleChildProc()方法进一步处理:

1
2
3
4
5
private Runnable handleChildProc(Arguments parsedArgs, FileDescriptor[] descriptors,FileDescriptor pipeFd) {
// ZygoteInit.zygoteInit中会创建Binder线程池,
// 也就是说每个进程无论是否包含任何activity等组件,一定至少会包含一个Binder线程
return ZygoteInit.zygoteInit(parsedArgs.targetSdkVersion, parsedArgs.remainingArgs,null /* classLoader */);
}

到此处,后面便和上面一样的了,唯一不同的是,SystemServer进程启动之后进入的是主类SystemServer.java的main()函数,而这里应用程序启动起来后进入的是主类是ActivityThread.java的main()函数,进入ActivityThread.main()方法后,会创建Application实例,具体可见:Android-Application

接下来就只剩下forkAndSpecialize方法的解析了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static int forkAndSpecialize(int uid, int gid, int[] gids, int runtimeFlags,
int[][] rlimits, int mountExternal, String seInfo, String niceName, int[] fdsToClose,
int[] fdsToIgnore, boolean startChildZygote, String instructionSet, String appDataDir) {
// 停止Zygote的4个Daemon子线程的运行,等待并确保Zygote是单线程(用于提升fork效率),
// 并等待这些线程的停止,初始化gc堆的工作
VM_HOOKS.preFork();
// Resets nice priority for zygote process.
resetNicePriority();
// 调用fork()创建新进程,设置新进程的主线程id,重置gc性能数据,设置信号处理函数
int pid = nativeForkAndSpecialize(
uid, gid, gids, runtimeFlags, rlimits, mountExternal, seInfo, niceName, fdsToClose,
fdsToIgnore, startChildZygote, instructionSet, appDataDir);
// 在fork新进程后,启动Zygote的4个Daemon线程,java堆整理等。
VM_HOOKS.postForkCommon();
return pid;
}

nativeForkAndSpecialize()方法是一个Native方法,最终调用的是frameworks/base/core/jni/com_android_internal_os_Zygote.cpp#com_android_internal_os_Zygote_nativeForkAndSpecialize()方法,该方法又调用的是ForkAndSpecializeCommon方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
static const char kZygoteClassName[] = "com/android/internal/os/Zygote";

int register_com_android_internal_os_Zygote(JNIEnv* env) {
gZygoteClass = MakeGlobalRefOrDie(env, FindClassOrDie(env, kZygoteClassName));
gCallPostForkChildHooks = GetStaticMethodIDOrDie(env, gZygoteClass, "callPostForkChildHooks", "(IZZLjava/lang/String;)V");
return RegisterMethodsOrDie(env, "com/android/internal/os/Zygote", gMethods, NELEM(gMethods));
}

static void SetSignalHandlers() {
struct sigaction sig_chld = {};
sig_chld.sa_handler = SigChldHandler;
}

// Sets the SIGCHLD handler back to default behavior in zygote children.
static void UnsetChldSignalHandler() {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = SIG_DFL;
}

static void SigChldHandler(int /*signal_number*/) {
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
// If the just-crashed process is the system_server, bring down zygote
// so that it is restarted by init and system server will be restarted
// from there.
if (pid == gSystemServerPid) {
ALOGE("Exit zygote because system server (%d) has terminated", pid);
kill(getpid(), SIGKILL);
}
}
}

static pid_t ForkAndSpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray javaGids, jint runtime_flags,
jobjectArray javaRlimits, jlong permittedCapabilities, jlong effectiveCapabilities,
jint mount_external, jstring java_se_info, jstring java_se_name,
bool is_system_server, jintArray fdsToClose, jintArray fdsToIgnore,
bool is_child_zygote, jstring instructionSet, jstring dataDir) {
// 设置子进程的signal信号处理函数
SetSignalHandlers();

pid_t pid = fork();

if (pid == 0) {
PreApplicationInit();

// 清理所有必须立即关闭的描述符
if (!DetachDescriptors(env, fdsToClose, &error_msg)) {
fail_fn(error_msg);
}
// Re-open all remaining open file descriptors so that they aren't shared
// with the zygote across a fork.
if (!gOpenFdTable->ReopenOrDetach(&error_msg)) {
fail_fn(error_msg);
}
MountEmulatedStorage(uid, mount_external, use_native_bridge, &error_msg);

// If this zygote isn't root, it won't be able to create a process group,
// since the directory is owned by root.
if (!is_system_server && getuid() == 0) {
int rc = createProcessGroup(uid, getpid());
}
// selinux上下文
rc = selinux_android_setcontext(uid, is_system_server, se_info_c_str, se_name_c_str);

if (se_name_c_str == NULL && is_system_server) {
se_name_c_str = "system_server";
}
if (se_name_c_str != NULL) {
SetThreadName(se_name_c_str);
}

// Unset the SIGCHLD handler, but keep ignoring SIGHUP (rationale in SetSignalHandlers).
UnsetChldSignalHandler();
// 等价于调用zygote.callPostForkChildHooks()
env->CallStaticVoidMethod(gZygoteClass, gCallPostForkChildHooks, runtime_flags,
is_system_server, is_child_zygote, instructionSet);
}
return pid;
}

接着进入zygote.callPostForkChildHooks()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
private static void callPostForkChildHooks(int runtimeFlags, boolean isSystemServer,
boolean isZygote, String instructionSet) {
// 调用ZygoteHooks.postForkChild()
VM_HOOKS.postForkChild(runtimeFlags, isSystemServer, isZygote, instructionSet);
}

// ZygoteHooks.java
public void postForkChild(int runtimeFlags, boolean isSystemServer, boolean isZygote,
String instructionSet) {
nativePostForkChild(token, runtimeFlags, isSystemServer, isZygote, instructionSet);
// 设置了新进程Random随机数种子为当前系统时间,也就是在进程创建的那一刻就决定了未来随机数的情况,也就是伪随机。
Math.setRandomSeedInternal(System.currentTimeMillis());
}

nativePostForkChild通过JNI最终调用调用如下方法:art/runtime/native/dalvik_system_ZygoteHooks.cc#ZygoteHooks_nativePostForkChild():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void ZygoteHooks_nativePostForkChild(JNIEnv* env, jclass, jlong token, jint debug_flags, jstring instruction_set) {
// 此处token记录着当前线程
Thread* thread = reinterpret_cast<Thread*>(token);
// 设置新进程的主线程id
thread->InitAfterFork();
if (instruction_set != nullptr && !is_system_server) {
ScopedUtfChars isa_string(env, instruction_set);
InstructionSet isa = GetInstructionSetFromString(isa_string.c_str());
Runtime::NativeBridgeAction action = Runtime::NativeBridgeAction::kUnload;
if (isa != InstructionSet::kNone && isa != kRuntimeISA) {
action = Runtime::NativeBridgeAction::kInitialize;
}
Runtime::Current()->InitNonZygoteOrPostFork(env, is_system_server, action, isa_string.c_str());
} else {
Runtime::Current()->InitNonZygoteOrPostFork(env, is_system_server,
Runtime::NativeBridgeAction::kUnload, nullptr, profile_system_server);
}
}

接着调用了art/runtime/runtime.cc#InitNonZygoteOrPostFork方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void Runtime::InitNonZygoteOrPostFork(JNIEnv* env, bool is_system_server,
NativeBridgeAction action, const char* isa, bool profile_system_server) {
is_zygote_ = false;

if (is_native_bridge_loaded_) {
switch (action) {
case NativeBridgeAction::kUnload:
UnloadNativeBridge(); // 卸载用于跨平台的桥连库
is_native_bridge_loaded_ = false;
break;

case NativeBridgeAction::kInitialize:
InitializeNativeBridge(env, isa); // 初始化用于跨平台的桥连库
break;
}
}

// 创建Java堆处理的线程池
heap_->CreateThreadPool();
// 重置gc性能数据,以保证进程在创建之前的GCs不会计算到当前app上。
heap_->ResetGcPerformanceInfo();

if (!safe_mode_ && (jit_options_->UseJitCompilation() || jit_options_->GetSaveProfilingInfo()) && jit_ == nullptr) {
// 创建JIT
CreateJit();
}
// 设置信号处理函数
StartSignalCatcher();
}

总结

init进程(pid=1)是Linux系统中用户空间的第一个进程,主要工作如下:

  • 初始化环境变量及挂载文件;
  • 解析并执行init.rc文件;
  • 处理子进程的退出(signal方式);
  • 创建一块共享的内存空间用于属性服务器,并启动属性服务进程;
  • 进入无限循环状态,执行如下流程:
    • 检查是否需要重启的进程,若有则将其重新启动;
    • 进入等待状态,直到系统属性变化事件(property_set改变属性值),或者收到子进程的信号SIGCHLD,或者keychord 键盘输入事件等,则会退出等待状态,执行相应的回调函数。

可见init进程在开机之后的核心工作就是响应property变化事件和回收僵尸进程以及重启进程。

  • 当某个进程改变了一个系统属性值时,系统会通过socket向init进程发送一个property变化的事件通知,那么init进程会触发相关回调。
  • 回收僵尸进程,在Linux内核中,如父进程不等待子进程的结束直接退出,会导致子进程在结束后变成僵尸进程,占用系统资源。为此,init进程专门安装了SIGCHLD信号接收器,当某些子进程退出时发现其父进程已经退出,则会向init进程发送SIGCHLD信号,init进程回收僵尸子进程。

Zygote启动过程的调用流程:

  • 解析init.zygote.rc中的参数,创建AppRuntime并调用AppRuntime.start()方法;
  • 调用AndroidRuntime的startVM()方法创建虚拟机,再调用startReg()注册JNI函数;
  • 通过JNI方式调用ZygoteInit.main(),第一次进入Java世界;
  • registerServerSocketFromEnv()方法建立socket通道,zygote作为通信的服务端,用于响应客户端请求;
  • preload()预加载通用类、相关资源,共享库等,用于提高app启动效率;
  • 接下来再通过startSystemServer(),fork出system_server进程,也是上层framework的运行载体;
  • zygote调用runSelectLoop()方法等待,当接收到创建新进程请求时唤醒并执行相应工作。

zygote预加载了一些资源,通过它来 fork system_server 进程,则 system_server 可以直接使用这些加载好的资源如 JNI 函数,常用的方法,共享库,资源等。此外,选择使用 zygote 进程来 fork 应用程序而不是使用 system_server 来 fork 是因为 system_server 有自己的工作要处理,其内有许多线程如 AMS, WMS, PKMS 等,这种多线程的父进程 fork 子进程可能会导致死锁。

在 Linux 系统中 fork 子进程会复制父进程用户空间的数据到子进程(copy-on-write),然后复制当前线程到子进程,而父进程中的其它线程在子进程中都”蒸发”了。如果在fork之前,一个线程持有了某个锁,然后另外一个线程调用了fork创建子进程,在子进程中持有那个锁的线程却”蒸发”了,从子进程的角度来看,这个锁被“永久”的上锁了,因为它的持有者“蒸发”了。如果子进程中的任何一个线程对这个已经被持有的锁进行lock操作,就会发生死锁。

将整个流程总结为下图:

init-zygote总结

概述

反射机制就是允许编程人员在程序运行时来改变程序的结构或者变量的类型。通过这个特性,我们可以在运行时得知某个类的所有成员,包括其属性和方法,同时也能够调用这些方法。请注意反射机制的特殊之处就在于可以使用编译期间完全未知的类,也就是通过反射机制可以加载一个在运行时才得知名字的类,从而取得其内部的成员函数并调用。

  1. 动态语言:

    一般认为在程序运行时,允许改变程序结构或变量类型,这种语言称为动态语言。从这个观点看,Perl,Python,Ruby是动态语言,C++,Java,C#不是动态语言。尽管这样,JAVA有着一个非常突出的动态相关机制:反射(Reflection)。运用反射我们可以于运行时加载、探知、使用编译期间完全未知的classes。换句话说,Java程序可以加载在运行时才得知名称的class,获悉其完整构造方法,并生成其对象实体、或对其属性设值、或唤起其成员方法。

  2. 反射:

    要让Java程序能够运行,就得让Java类被Java虚拟机加载。Java类如果不被Java虚拟机加载就不能正常运行。正常情况下,我们运行的所有的程序在编译期时候就已经把那个类被加载了。Java的反射机制是在编译时并不确定是哪个类被加载了,而是在程序运行的时候才加载。使用的是在编译期并不知道的类。这样的编译特点就是java反射。

  3. 反射的作用:

    如果有AB两个程序员合作,A在写程序的时需要使用B所写的类,但B并没完成他所写的类。那么A的代码是不能通过编译的。此时,利用Java反射的机制,就可以让A在没有得到B所写的类的时候,来使自身的代码通过编译。

  4. 反射的实质:

    反射就是把Java类中的各种存在给解析成相应的Java类。要正确使用Java反射机制就得使用Class(C大写) 这个类。它是Java反射机制的起源。当一个类被加载以后,Java虚拟机就会自动产生一个Class对象。通过这个Class对象我们就能获得加载到虚拟机当中这个Class对象对应的方法、成员以及构造方法的声明和定义等信息。

  5. 反射机制的优点与缺点:

    • 静态编译:在编译时确定类型,绑定对象,即通过。

    • 动态编译:运行时确定类型,绑定对象。动态编译最大限度发挥了java的灵活性,体现了多态的应用,降低类之间的藕合性。

      一句话,反射机制的优点就是可以实现动态创建对象和编译,体现出很大的灵活性,特别是在J2EE的开发中它的灵活性就表现的十分明显。比如,一个大型的软件,不可能一次就把把它设计的很完美,当这个程序编译后,发布了,当发现需要更新某些功能时,我们不可能要用户把以前的卸载,再重新安装新的版本,假如这样的话,这个软件肯定是没有多少人用的。采用静态的话,需要把整个程序重新编译一次才可以实现功能的更新,而采用反射机制的话,它就可以不用安装,只需要在运行时才动态的创建和编译,就可以实现该功能。它的缺点是对性能有影响。使用反射基本上是一种解释操作,我们可以告诉JVM,我们希望做什么并且它满足我们的要求。这类操作总是慢于只直接执行相同的操作。

Class

在面向对象的世界里,万事万物皆对象。在java语言中,静态的成员,普通数据类型不是对象。

类是对象,类是java.lang.Class类的实例对象。这个对象的表示方式有三种:

  • 第一种表示方式:

    1
    Class c1 = Foo.class;//任何一个类都有一个隐含的静态成员变量class
  • 第二种表示方式:

    1
    Class c2 = foo1.getClass();//已知该类的对象,通过getClass方法得到这个实例类的class(类类型)
  • 第三种表达方式

    1
    Class c3 = Class.forName("imooc.reflect.Foo");

    可以通过类类型创建该类的类对象

    1
    2
    Class c1 = Foo.class;//c1就是类类型
    Foo foo=(Foo)c1.newInstance();//需要有无参数的构造方法。

静态动态加载类

编译时刻加载类是静态加载类,运行时刻是动态加载类。

new创建对象是静态加载类,在编译时刻就需要加载所有可能用到的类。如果创建了一个可以使用的C1对象和不可使用的C2对象,即使C1可用,由于C2不可以而编译不过使得C1不可用。用动态加载类可以解决该问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public interface OfficeAble {
void start();
}

public class Word implements OfficeAble {
public void start() {
System.out.println("Word...start...");
}
}

public static void main(String[] args) {
try {
//动态加载类,在运行时加载
Class c = Class.forName(args[0]);
//通过类类型,创建该对象
OfficeAble oa = (OfficeAble) c.newInstance();
oa.start();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}

获取类的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//首先获取类的类类型
Class cl = object.getClass();
//获取类的名称/简称
System.out.println("名称:" + cl.getName() + " 简称:" + cl.getSimpleName());
/**
* Method类,方法对象,一个成员方法就是一个Method对象
* getMethods获取的是所有public方法,包括从父类继承而来的
* getDeclaredMethods获取的是所有该类自己声明的方法,不问访问权限
*
*/
Method[] methods = cl.getMethods();//cl.getDeclaredMethods()
for (int i = 0; i < methods.length; i++){
//得到方法的返回值类型的类类型
Class returnType = methods[i].getReturnType();
System.out.print(returnType.getName() + " ");
//得到方法的名称
System.out.print(methods[i].getName() + "(");
//获取参数列表的类型的类类型
Class[] params = methods[i].getParameterTypes();
for (Class c: params){
System.out.print(c.getName() + ",");
}
System.out.println(")");
}

获取类的属性

在使用反射获取或者修改一个变量的值时,编译器不会进行自动装/拆箱。因此我们无法给一个 Integer 类型的变量赋整型值,必须给它赋一个 Integer 对象才可以。

1
2
3
4
5
6
7
8
9
10
11
/**
* getFields获取的是所有public属性,包括从父类继承而来的
* getDeclaredFields获取的是所有该类自己定义的属性,不问访问权限
*/
Field[] fields = cl.getDeclaredFields();
for (int i = 0; i < fields.length; i++){
//得到成员变量的类型的类类型
Class fieldType = fields[i].getType();
System.out.println(fieldType.getName());
System.out.println(fields[i].getName());
}

对于 final 修饰的字段:

构造方法赋值:

  • 构造方法赋值在JVM编译期不会优化,运行时决定字段的值,修改后通过反射和其他方式访问到的都是新值。

直接赋值:

  • 基本类型、String类型:JVM编译期会优化成常量,导致修改后的值通过反射可以访问到新值,其他方式访问到的仍是旧值。
  • 对象类型:JVM编译期不会优化,运行时决定字段的值,修改后通过反射和其他方式访问到的都是新值。

间接赋值:

  • 间接赋值在JVM编译期不会优化,运行时决定字段的值,修改后通过反射和其他方式访问到的都是新值。

小结

如果final字段值是运行时赋值的,则修改后无论通过何种方式访问获得的都是新值;基本类型、String类型直接赋值时由于JVM编译优化,编译时期用到字段的地方会直接被字段值替换,导致通过反射修改字段值后用到字段的地方仍是原值,但通过反射访问获取到的是新值(给人的错觉是没修改成功)。

获取类的构造方法

1
2
3
4
5
/**
* getConstructors获取的是所有public的构造函数,包括从父类继承而来的
* getDeclaredConstructors获取的是所有该类自己定义的构造函数,不问访问权限
*/
Constructor[] constructor = cl.getDeclaredConstructors();

方法反射

使用method.invok();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
A a = new A();
Class c = a.getClass();
try {
Method method = c.getMethod("print", int.class, int.class);
//方法的反射操作是指用method对象进行操作
//方法如果没有返回值则返回null, 有返回值则返回返回值
method.invoke(a, new Object[]{20, 30});
//method.invoke(a, 20, 30);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}

属性反射

1
2
3
4
5
6
7
8
9
10
Field[] fields = c.getDeclaredFields();

for (Field f : fields) {
f.setAccessible(true);
try {
f.set(object, args...);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}

泛型的本质

编译之后集合的泛型是去泛型化的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ArrayList list = new ArrayList();
ArrayList<String> list1 = new ArrayList<String>();

Class c1 = list.getClass();
Class c2 = list1.getClass();
System.out.println(c1 == c2);//true

/**反射都是编译之后的操作(字节码)
* 返回true说明编译之后集合的泛型是去泛型化的
* Java中集合的泛型,是防止错误输入的,只在编译阶段有效
* 验证:通过方法的反射来给list1加入int类型的数据,绕过编译
*/
try {
Method method = c2.getMethod("add", Object.class);
method.invoke(list1, 20);
method.invoke(list1, "string");
System.out.println(list1);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}

MethodHandle

概述

java7增加了对动态类型语言的支持,使java也可以像C语言那样将方法作为参数传递,其实现在lava.lang.invoke包中。MethodHandle作用类似于反射中的Method类,但它比Method类要更加灵活和轻量级。通过MethodHandle进行方法调用一般需要以下几步:

  1. 创建MethodType对象,指定方法的签名;
  2. 在MethodHandles.Lookup中查找类型为MethodType的MethodHandle;
  3. 传入方法参数并调用MethodHandle.invoke或者MethodHandle.invokeExact方法。

MethodType

创建方法:

1.通过MethodType.methodType及其重载方法,需要指定返回值类型以及0到多个参数:

1
2
public static MethodType methodType(Class<?> rtype, Class<?>[] ptypes);
public static MethodType methodType(Class<?> rtype, List<Class<?>> ptypes);

2.MethodType.genericMethodType,需要指定参数的个数,类型都为Object:

1
2
public static MethodType genericMethodType(int objectArgCount, boolean finalArray);
public static MethodType genericMethodType(int objectArgCount)

3.fromMethodDescriptorString,通过方法描述来创建:

1
public static MethodType fromMethodDescriptorString(String descriptor, ClassLoader loader);

MethodHandles.Lookup

MethodHandles.Lookup相当于MethodHandle工厂类,通过findxxx方法可以得到相应的MethodHandle,还可以配合反射API创建MethodHandle,对应的方法有unreflect、unreflectSpecial等。

invoke

在得到MethodHandle后就可以进行方法调用了,有三种调用形式:

  1. invokeExact: 调用此方法与直接调用底层方法一样,需要做到参数类型精确匹配;
  2. invoke: 参数类型松散匹配,通过asType自动适配;
  3. invokeWithArguments: 直接通过方法参数来调用。其实现是先通过genericMethodType方法得到MethodType,再通过MethodHandle的asType转换后得到一个新的MethodHandle,最后通过新MethodHandle的invokeExact方法来完成调用。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Main {
private static String hello(String s) {
return "s = " + s;
}

public static void main(String[] args) {
MethodHandle methodHandle = getMethodType();

try {
String result = (String) methodHandle.invokeExact("hearing");
System.out.println(result);
} catch (Throwable throwable) {
throwable.printStackTrace();
}

try {
// String s = (String) methodHandle.bindTo(...).invokeWithArguments("hearing-2");
String s = (String) methodHandle.invokeWithArguments("hearing-2");
System.out.println(s);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}

private static MethodHandle getMethodType() {
MethodType methodType = MethodType.methodType(String.class, String.class);
MethodHandle methodHandle = null;
try {
methodHandle = MethodHandles.lookup().findStatic(Main.class, "hello", methodType);
} catch (NoSuchMethodException | IllegalAccessException e) {
e.printStackTrace();
}

return methodHandle;
}
}

基础知识

基本方法

  • thread.join: 让调用线程等待自己结束后在执行。
  • Thread.yield: 礼让,自己先暂停,然后与其他线程共同竞争资源,静态方法(Thread.yield)。

线程中断机制

线程中断是给目标线程发送一个中断信号,开发者可以监听线程的中断状态选择是否停止执行线程逻辑。对于阻塞线程的方法如 sleep, wait 等会在中断状态后抛出 InterruptedException 异常。

  • thread.interrupt: 中断目标线程,给目标线程发一个中断信号,线程被打上中断标记。
  • Thread.interrupted: 判断目标线程是否被中断,会清除中断标记。
  • thread.isInterrupted: 判断目标线程是否被中断,不会清除中断标记。

CAS

CAS (Compare-And-Swap) 是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。CAS 即我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可

ABA问题

出现原因:

  1. 线程1读取出指定内存地址的数据A,加载到寄存器,此时读取出来的原值不仅将作为要被计算的值A,还会作为比较值A。
  2. 此时线程1的cpu被线程2抢占了,线程2也从同样的内存地址中读取了同样的数据A,线程2还比线程1先执行完,线程2产生了新数据B,并且遵守了CAS原理把新数据B存入该内存地址(这个时候内存的值由A被该为B)。
  3. 线程2执行完之后,线程1又没抢过其它线程,此时cpu被线程3抢占,之后步骤和第2步一样,线程3从同样的内存地址中读取了数据B,线程3比线程1先执行完,线程3产生了新数据A(不意味着此A就是彼A,已经被替换了),并且遵守了CAS原理把新数据A存入该内存地址(这个时候内存的值由B又变为A)。
  4. 此时线程1执行完了,要遵守CAS原理存入数据,然后比较值A是原来的A,而执行内存地址中的A是被替换过的了,但原A的值与内存中的A值是相等的,根据CAS,线程1会把新的执行结果存入该内存地址。

解决办法:加版本号,多一步比较版本号。

锁的类型

锁从宏观上分类,分为悲观锁与乐观锁,这是一种思想。Java中重量级锁是悲观锁的一种,自旋锁、轻量级锁与偏向锁属于乐观锁。

乐观锁

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。乐观锁直到修改完成准备提交所做的的修改时才会将数据锁住。完成更改后释放。

java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

悲观锁

悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。Synchronized 是悲观锁,无论哪个线程持有共享变量的锁,都采用独占的方式来访问这些变量,独占锁是一种悲观锁。

可重入锁

如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,当一个线程执行到某个synchronized方法时,在其内调用了另一个synchronized方法,此时线程不必重新去申请锁,而是可以直接执行方法该方法。

可中断锁

可以响应中断的锁。在Java中,synchronized不是可中断锁,而Lock是可中断锁。

(非)公平锁

公平锁即尽量以请求锁的顺序来获取锁。同时有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程会获得该锁,这种就是公平锁。

在Java中,synchronized是非公平锁,而 ReentrantLock 和 ReentrantReadWriteLock 默认情况下是非公平锁,但是可以设置为公平锁。

共享锁和独占(排他)锁

  • 共享锁:允许多个线程同时获取锁,并发访问共享资源,如:ReadWriteLock的读锁。
  • 独占锁:每次只能有一个线程能持有锁,它是悲观的加锁策略。ReentrantLock就是以独占方式实现的互斥锁。

读写锁

读写锁将对一个资源的访问分成了一个读锁和一个写锁。它允许一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行。如ReadWriteLock就是读写锁。

Java中的锁

在 Java 对象的数据结构中有一部分称为 MarkWord, 在其内有锁状态相关的标志位。Java 锁的状态总共有四种,级别由低到高依次为:无锁、偏向锁、轻量级锁、重量级锁,锁状态只能升级,不能降级。

自旋

自旋就是线程在不满足某种条件的情况下,一直循环做某个动作(空代码)。线程一旦进入阻塞(Block),再被唤醒的代价比较高。所以常见的做法是先自旋一段时间,还没拿到锁就进入阻塞。

如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

偏向锁

初次执行到同步代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是 偏向于第一个获得它的线程 的锁。执行完同步代码块后,线程并不会主动释放偏向锁,当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。偏向锁的撤销需要等待全局安全点,即在某个时间点上没有字节码正在执行时,它会先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到无锁或轻量级锁的状态。

如果线程竞争激烈,那么应该禁用偏向锁。

轻量级锁(自旋锁)

轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程访问偏向锁的时候,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。

在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他等待的线程就只能在空耗CPU,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么就会一直用轻量级锁,允许短时间的忙等现象。这是一种折中的方法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。

轻量级锁的获取主要有两种情况:

  • 当关闭偏向锁功能时;
  • 由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。

重量级锁

轻量级锁的忙等是有限度的,如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。

重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资源。

锁优化

减小锁持有时间

不需要同步执行的代码,能不放在同步快里面执行就不要放在同步快内,可以让锁尽快释放;

减小锁粒度

它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争。

比如说 ConcurrentHashMap 的实现,它把整个 HashMap 拆成了若干个小的 segment, 当有线程操作里面的数据时实际上操作的是被拆分后的某个小的 segment, 从而使 ConcurrentHashMap 允许多个线程同时进入,因此增加了并行度,达到了锁优化的目的。

增大锁粒度(锁粗化)

通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但在某些情况下需要增大锁粒度。比如说某个循环内的操作需要加锁,此时应该把锁放到循环外面,否则每次进出循环都需要获取与释放锁。

使用CAS

如果需要同步的操作执行速度非常快,并且线程竞争不激烈,这时候使用 CAS 效率会更高,因为线程阻塞唤醒会有比较大的性能消耗,此时使用 CAS 操作(原子类)会是非常高效的选择。

Java内存模型

参考 Java内存模型与线程

volatile

  • 可见性:线程可见性。
  • 有序性:Java遵循as-if-serial语义,即单线程执行程序时,即使发生重排序,程序的执行结果不能被改变。而 volatile 会禁止指令重排序,因此可以避免其他线程访问到一个未初始化的对象(分配内存,初始化对象,引用赋值给变量)。
  • 不能保证原子性。

原子类

原子类的原理是 CAS 操作,在 Java 中有许多原子类: AtomicBoolean, AtomicInteger, AtomicIntegerArray, AtomicIntegerFieldUpdater, AtomicLong, AtomicLongArray, AtomicLongFieldUpdater, AtomicMarkableReference, AtomicReference, AtomicReferenceArray, AtomicReferenceFieldUpdater, AtomicStampedReference。

synchronized

实现原理

在 JDK 1.6 之前 synchronized 是一个重量级锁,效率比较低下,在JDK 1.6 后 Jvm 为了提高锁的获取与释放效率对 synchronized 进行了优化,引入了偏向锁和轻量级锁。synchronized 是可重入锁,这样可以避免死锁。

在 HotSpot JVM 实现中,锁有个专门的名字:对象监视器(Object Monitor)。synchronized 的同步在软件层面依赖于JVM,看一段代码:

1
2
3
4
5
6
7
public class Main {
public void test() {
synchronized (this) {
System.out.println("Test");
}
}
}

反编译后,其字节码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Main {
// ...
public void test();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String Test
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any

monitorenter: 每个对象都是一个监视器锁(monitor),当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权:

  • 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
  • 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
  • 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;

monitorexit: 执行monitorexit的线程必须是锁对象所对应的monitor对象的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。monitorexit指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁;

可以看出synchronized的实现原理:synchronized的语义底层是通过一个monitor的对象来完成,其实 wait/notify 等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,这会导致在用户态和内核态之间来回切换,对性能有较大影响。

而对于 synchronized 方法,编译后会在方法上增加一个标识,当调用该方法时,如果检测到该标识的存在,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象,本质上与上面 synchronized 代码块是一样的。

原子性,可见性,有序性

  • 原子性: synchronized 通过 monitorenter 和 monitorexit 指令来保证原子性。
  • 可见性: synchronized 规定线程在加锁时清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。
  • 有序性: synchronized 无法禁止指令重排序,所以不能保证有序性。不过还有一种说法是由于 synchronized 修饰的代码,同一时间只能被同一线程访问,那么也就是单线程执行的,从as-if-serial语义而言,即单线程执行程序时,即使发生重排序,程序的执行结果不能被改变,所以可以保证其有序性。

wait&notify

wait 和 notify 不在同一个线程中进行,且必须在 synchronized 语句中,JAVA 的 Object 类型都带有一个内存锁,在有线程获取该内存锁后,其它线程无法访问该内存,从而实现 JAVA 中的同步、互斥操作。notify 调用后并不会马上释放对象锁,而是在相应的 synchronized 语句块执行结束,自动释放锁后,JVM会在 wait 的线程中选取一个线程,赋予其对象锁,唤醒线程,继续执行。

AQS

结构概述

Java中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于AbstractQueuedSynchronizer(简称为AQS)实现的。AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。

AQS框架

当有自定义同步器接入时,只需重写第一层所需要的部分方法即可,不需要关注底层具体的实现流程。当自定义同步器进行加锁或者解锁操作时,先经过第一层的API进入AQS内部方法,然后经过第二层进行锁的获取,接着对于获取锁失败的流程,进入第三层和第四层的等待队列处理,而这些处理方式均依赖于第五层的基础数据提供层。

AQS核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是虚拟双向队列(FIFO)实现的,将暂时获取不到锁的线程加入到队列中,队列头节点不与任何线程关联,它是一个虚节点。

线程两种锁的模式:

  • SHARED: 表示线程以共享的模式等待锁
  • EXCLUSIVE: 表示线程正在以独占的方式等待锁

节点中waitStatus参数有下面几个枚举值:

  • 0: 当一个Node被初始化的时候的默认值
  • CANCELLED =1: 表示线程获取锁的请求已经取消了
  • SIGNAL =-1: 表示线程已经处于唤醒状态
  • CONDITION =-2: 表示节点在等待队列中,节点线程等待唤醒
  • PROPAGATE =-3: 当前线程处在SHARED情况下,该字段才会使用

AQS使用一个Volatile的int类型的成员变量state来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,通过CAS完成对State值的修改。可以通过修改State字段表示的同步状态来实现多线程的独占模式和共享模式(加锁过程):

AQS加锁过程

对于我们自定义的同步工具,需要自定义获取同步状态和释放状态的方式,也就是AQS架构图中的第一层:API层。接下来看看 AQS 中的重要方法。

acquire

1
2
3
4
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

首先通过 tryAcquire 方法尝试去获取锁,如果获取锁成功则直接返回,该方法需要在子类中实现。如果 tryAcquire 方法获取锁失败返回 false 后则会调用 addWaiter 方法通过当前线程和锁模式新建一个节点 newNode,并将其加入等待队列尾部。然后调用 acquireQueued 方法将 newNode 之前的处于 CANCELLED 状态的节点出队列。

addWaiter:线程入队列

addWaiter 方法会通过当前线程和锁模式新建一个节点,并将其加入等待队列尾部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private Node addWaiter(Node mode) {
// 设置关联模式为 独占 或 共享
Node node = new Node(mode);

for (;;) {
Node oldTail = tail;
if (oldTail != null) {
// 尾节点不为 null 说明已经初始化过了
U.putObject(node, Node.PREV, oldTail);
// 设置新节点为尾节点
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return node;
}
} else { // 初始化
initializeSyncQueue();
}
}
}

// 初始化 head and tail 节点
private final void initializeSyncQueue() {
Node h;
if (U.compareAndSwapObject(this, HEAD, null, (h = new Node())))
tail = h;
}

注意头节点是通过空构造函数初始化的,其 waitStatus 默认为 0, 不关联线程,是一个虚节点。

acquireQueued:线程出队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
final boolean acquireQueued(final Node node, int arg) {
try {
// 标记线程是否被中断过
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor(); // 获取前驱节点
// 如果前驱节点是head节点,则其可以尝试获取锁,因为头节点是虚节点
if (p == head && tryAcquire(arg)) {
setHead(node); // 获取成功,将当前节点设置为head节点
p.next = null; // help GC
return interrupted;
}
// 判断是否可以阻塞,若可以则阻塞
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true; // 线程被中断
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}

setHead方法是把当前节点置为虚节点,但并没有修改waitStatus,因为它是一直需要用的数据:

1
2
3
4
5
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}

这里有两个原因可能导致走到第二个 if 语句:

  • 前驱节点为头节点且当前没有获取到锁(可能是锁被抢占了)
  • 前驱节点不为头结点

此时要判断当前线程是否要被阻塞,如需要则阻塞线程,防止无限循环(自旋)浪费资源。流程如下:

acquireQueued流程1

看一下 shouldParkAfterFailedAcquire 和 parkAndCheckInterrupt 方法的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // 获取前驱节点的状态
if (ws == Node.SIGNAL) // 说明前驱结点处于唤醒状态
return true;
if (ws > 0) { // waitStatus>0是取消状态
do { // 循环向前查找取消节点,把取消节点从队列中剔除
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else { // 将前驱节点的状态设置为SIGNAL
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
return false;
}

// 挂起当前线程,返回线程中断状态
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}

线程入队后能够挂起的前提是:其前驱节点的状态为SIGNAL,SIGNAL的含义是 前驱节点获取锁并且出队后要将自己唤醒,如果前驱结点的状态不是SIGNAL,那么自己就不能安心挂起,需要去找个安心的挂起点,下次被唤醒后再通过死循环重新尝试获取锁。流程如下:

acquireQueued流程2

cancelAcquire:CANCELLED节点生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void cancelAcquire(Node node) {
if (node == null) return;
node.thread = null; // 设置该节点不关联任何线程,即虚节点
// 通过前驱节点,跳过取消状态的node
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;

Node predNext = pred.next; // 获取过滤后的前驱节点的后继节点
node.waitStatus = Node.CANCELLED; // 把当前node的状态设置为CANCELLED

// 如果当前节点是尾节点,则将从后往前的第一个非取消状态的节点设置为尾节点,即移除自身
if (node == tail && compareAndSetTail(node, pred)) {
// 更新成功则将tail的后继节点设置为null
pred.compareAndSetNext(predNext, null);
} else {
int ws;
// 如果当前节点不是head的后继节点
// && 1:判断当前节点前驱节点的是否为SIGNAL,2:如果不是,则把前驱节点设置为SINGAL看是否成功
// && 前节点的线程不为null
if (pred != head
&& ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL)))
&& pred.thread != null) {
Node next = node.next;
// 将当前node从链表中移除
if (next != null && next.waitStatus <= 0) pred.compareAndSetNext(predNext, next);
} else { // 如果当前节点是head的后继节点,或者上述条件不满足,那就唤醒当前节点的后继节点
unparkSuccessor(node);
}

node.next = node; // help GC
}
}

该方法的流程如下:获取当前节点的前驱节点,如果前驱节点的状态是CANCELLED,那就一直往前遍历,找到第一个不为CANCELLED的节点,将找到的Pred节点和当前Node关联,将当前Node设置为CANCELLED。根据当前节点的位置,考虑以下三种情况:

  • 当前节点是尾节点:

cancelAcquire1

  • 当前节点是Head的后继节点:

cancelAcquire2

  • 当前节点不是Head的后继节点,也不是尾节点:

cancelAcquire3

release:释放锁

1
2
3
4
5
6
7
8
9
10
public final boolean release(int arg) {
if (tryRelease(arg)) {
// 自定义tryRelease如果返回true,说明该锁没有被任何线程持有
Node h = head;
// 头结点不为空并且头结点的waitStatus不是初始化节点情况,则唤醒其后继结点
if (h != null && h.waitStatus != 0) unparkSuccessor(h);
return true;
}
return false;
}

tryRelease 需要在子类中重写,看一下 unparkSuccessor 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 唤醒后继结点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0) node.compareAndSetWaitStatus(ws, 0);

// 如果下个节点是null或者下个节点被cancelled则从后往前找第一个非Cancelled的节点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
if (s != null) LockSupport.unpark(s.thread);
}

之所以要从后往前找,是因为在 addWaiter 方法中节点入队并不是原子操作,如果在 oldTail.next = node; 执行前就调用 unparkSuccessor 方法,则无法从前往后找了。另外在产生 CANCELLED 状态节点的时候,先断开的是Next指针,Prev指针并未断开,因此也是必须要从后往前遍历才能够遍历完全部的Node。

acquireInterruptibly

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted()) throw new InterruptedException();
if (!tryAcquire(arg)) doAcquireInterruptibly(arg);
}

private void doAcquireInterruptibly(int arg) throws InterruptedException {
// 增加节点
final Node node = addWaiter(Node.EXCLUSIVE);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
// 类似 acquireQueued 方法处理流程
setHead(node);
p.next = null; // help GC
return;
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
// 抛出异常
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}

当线程调用 parkAndCheckInterrupt 方法挂起后,下次被唤醒时,会返回是否中断的 bool 值,如果外部调用了 interrupt 方法,则返回 true,于是走抛出异常的逻辑,外部捕获后可以退出阻塞状态(如Lock.lockInterruptibly方法的使用)。

hasQueuedPredecessors

hasQueuedPredecessors 是公平锁加锁时判断等待队列中是否存在有效节点的方法。如果返回False,说明当前线程可以争取共享资源;如果返回True,说明队列中存在有效节点,当前线程必须加入到等待队列中。

1
2
3
4
5
6
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}

双向链表中,第一个节点为虚节点,并不存储任何信息。真正第一个有数据的节点,是从第二个节点开始的。当h != t时:如果(s = h.next) == null,则表示等待队列正在有线程进行初始化,但只是进行到了Tail指向Head,没有将Head指向Tail(参考新增节点的 addWaiter 方法流程,因为节点入队列并不是原子操作),此时队列中有元素,需要返回True。如果(s = h.next) != null,说明此时队列中至少有一个有效节点。如果此时s.thread == Thread.currentThread()则说明等待队列的第一个有效节点中的线程与当前线程相同,那么当前线程是可以获取资源的;如果s.thread != Thread.currentThread(),说明等待队列的第一个有效节点线程与当前线程不同,当前线程必须加入进等待队列。

Lock

相关方法

Lock 是一个接口,其实现有 ReentrantLock, ReadLock 和 WriteLock 等,看一下相关方法:

lock()

获取锁,如果锁已被其他线程获取,则进行等待。

tryLock()

尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

它有一个重载方法可以指定等待时长,当拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

lockInterruptibly()

当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就是说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

unlock()

释放锁。

AQS

Lock 是通过 AQS 实现锁功能的,其实现的方法如下:

  • isHeldExclusively(): 该线程是否正在独占资源,只有用到Condition才需要去实现它。
  • tryAcquire(int arg): 独占方式。arg为获取锁的次数,尝试获取资源。
  • tryRelease(int arg): 独占方式。arg为释放锁的次数,尝试释放资源。
  • tryAcquireShared(int arg): 共享方式。arg为获取锁的次数,尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int arg): 共享方式。arg为释放锁的次数,尝试释放资源,如果释放后允许唤醒后续等待结点返回True,否则返回False。

一般来说,自定义同步器要么是独占方式,要么是共享方式,只需实现tryAcquire-tryRelease, tryAcquireShared-tryReleaseShared中的一种即可。AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。ReentrantLock是独占锁,所以实现了tryAcquire-tryRelease

以非公平锁为例看一下非公平锁与AQS之间方法的关联之处:

AQS与非公平锁

ReentrantLock

ReentrantLock 实现了 Lock 接口,并提供了与 synchronized 相同的互斥性和内存可见性,但相较于 synchronized 提供了更高的处理锁的灵活性。看一下其 lock 和 unlock 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ReentrantLock implements Lock, java.io.Serializable {

public ReentrantLock() {
// 默认是非公平锁
sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

public void lock() {
sync.lock();
}

public void unlock() {
sync.release(1);
}
}

lock

NonfairSync 和 FairSync 都继承自 Sync, 而 Sync 继承自 AbstractQueuedSynchronizer(AQS):

1
2
3
4
5
6
7
8
9
10
11
12
// NonfairSync
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

// FairSync
final void lock() {
acquire(1);
}

对于 NonfairSync 来说首先通过 CAS 操作,判断 state 是否是 0(当前锁未被占用),如果是 0 则把它置为 1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操作只能保证一个线程操作成功,剩下的只能排队。

非公平即体现在这里,如果占用锁的线程刚释放锁,state 置为 0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就“插队”了。

非公平锁

acquire 方法的实现在 AQS 中,在上面已经看过了,它会调用重写的 tryAcquire 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// NonfairSync
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 没有线程占用锁
if (compareAndSetState(0, acquires)) {
// 占用锁成功,设置独占线程为当前线程
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
// 当前线程已经占用该锁,则将 state 更新为新的重入次数
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

非公平锁会通过 CAS 操作判断当前有没有线程占用锁,没有则直接占用成功,并修改 state。否则判断当前占用锁的线程是不是本线程,若是则叠加 state 表示重入锁的次数并返回 true。否则返回 false 标识占用锁失败,进行 AQS 的下一步操作。

公平锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// FairSync
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

相比于非公平锁,它多调用了 hasQueuedPredecessors 方法来判断等待队列中是否存在有效节点,存在则说明已经有线程在等待获取锁了,公平起见,此时不能把锁给调用者。

unlock

unlock 调用的是 AQS.release 方法。若释放锁成功,则唤醒头结点的下个节点关联的线程。子类需实现 tryRelease 方法进行真正的释放工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Sync
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
// 当前线程不是独占线程则抛异常
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // state 为 0 则表示释放成功
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}

State 字段在 ReentrantLock 中的变化过程:

  • State 初始化的时候为 0,表示没有任何线程持有锁;
  • 当有线程持有该锁时,值就会在原来的基础上+1,同一个线程多次获得锁则会多次+1,这里就是可重入的概念;
  • 解锁时对这个字段-1,一直到 0 则锁释放成功。

ReadWriteLock

ReadWriteLock 是一个接口,有一个实现类是 ReentrantReadWriteLock:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;

// 默认是非公平锁
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}

public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
}

先看写锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c); // 返回锁的可重入剩余次数
if (c != 0) {
if (w == 0 || current != getExclusiveOwnerThread())
// 锁可重入剩余次数为0或不是同一个线程
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
// 请求的重入次数超出剩余数
throw new Error("Maximum lock count exceeded");
setState(c + acquires);
return true;
}
if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}

上面的 writerShouldBlock 方法是一个抽象方法,在公平锁中返回 hasQueuedPredecessors() 方法的返回值;在非公平锁中返回 false。

再看读锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c); // 共享锁数量
// readerShouldBlock 根据是否公平返回不一样
if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}

await&signal

单个 Lock 可能与多个 Condition 对象关联。在 Condition 对象中,与 wait、notify 和 notifyAll 方法对应的分别是await、signal 和 signalAll。
Condition 实例实质上被绑定到一个锁上,要为特定 Lock 实例获得Condition 实例,请使用其 newCondition() 方法。

调用signal()方法后一定要释放当前占用的锁,这样被唤醒的线程才能有获得锁的机会,才能继续执行。

与synchronized比较

类别 synchronized Lock
存在层次 Java的关键字,在jvm层面上实现 一个接口
锁的获取 阻塞等待 分情况而定,Lock有多个锁获取的方式,可以尝试获得锁,线程可以不用一直等待
锁的释放 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 在finally中必须释放锁,不然容易造成线程死锁
中断响应 只能等待锁的释放,不能响应中断 可以用interrupt来中断等待
公平性 非公平锁 公平和非公平都可
性能 竞争资源不激烈,两者的性能差不多 竞争资源非常激烈时Lock的性能要远远优于synchronized

CountDownLatch

CountDownLatch的计数器只能使用一次,它一般用于某个线程等待若干个其他线程执行完任务之后,它才执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void test() throws Exception {
CountDownLatch latch = new CountDownLatch(2);
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread 1 start...");
sleep(3000);
System.out.println("Thread 1 end...");
latch.countDown();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread 2 start...");
sleep(5000);
System.out.println("Thread 2 end...");
latch.countDown();
}
}).start();
System.out.println("Wait for...");
latch.await();
System.out.println("Finish...");
}

CyclicBarrier

CyclicBarrier 的字面意思是回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以使用reset方法重置。当调用await()方法之后,线程就处于barrier了。

CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void test() throws Exception {
int count = 4;
final CyclicBarrier barrier = new CyclicBarrier(count, new Runnable() {
@Override
public void run() {
System.out.println("All thread is finished...");
}
});
for (int i = 0; i < count; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " start...");
sleep(3000);
System.out.println(Thread.currentThread().getName() + " end...");
try {
barrier.await();
} catch (Exception e) {
}
}
}, "Thread " + i).start();
}
}

Semaphore

Semaphore翻译成字面意思为信号量,Semaphore可以控制同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void test() throws Exception {
final Semaphore semaphore = new Semaphore(4);
for (int i = 0; i < 8; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+" begin to acquire...");
sleep(2000);
System.out.println(Thread.currentThread().getName()+" begin to release...");
semaphore.release();
} catch (Exception e) {
}
}
}, "Thread " + i).start();
}
}

Executors

接口与参数

ThreadPoolExecutor->AbstractExecutorService->ExecutorService->Executor.

1
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)

相关重要参数:

  • corePoolSize:核心池的大小,在创建了线程池后默认情况下线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法;
  • maximumPoolSize:线程池最大线程数,它表示在线程池中最多能创建多少个线程;
  • keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止;默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,除非设置了allowCoreThreadTimeOut(boolean)方法。
  • unit:参数keepAliveTime的时间单位;
  • workQueue:一个阻塞队列,用来存储等待执行的任务,如果队列满了则新建非核心线程执行任务;
  • RejectedExecutionHandler: 拒绝策略,是指将任务添加到线程池中时,线程池拒绝该任务所采取的相应策略。抛异常或丢弃任务等。

阻塞队列可选值:

  • ArrayBlockingQueue: 可以限定队列的长度;如果总线程数到了maximumPoolSize且队列满了则发生错误;
  • LinkedBlockingQueue: 队列长度没有限制,即所有超过核心线程数的任务都将被添加到队列中;
  • SynchronousQueue: SynchronousQueue是一个没有数据缓冲的BlockingQueue(队列只能存储一个元素),生产者线程对其的插入操作put必须等待消费者的移除操作take,反过来也一样,消费者移除数据操作必须等待生产者的插入。

线程池工作流程如下:

线程池工作流程

实现优先级

可比较的 Runnable 加上优先级阻塞队列 PriorityBlockingQueue.

1
2
public class PrioritizedRunnable implements Runnable, Comparable<PrioritizedRunnable> {}
ThreadPoolExecutor executor = new ThreadPoolExecutor(..., new PriorityBlockingQueue<Runnable>(), ...);

submit&execute

  • execute() 参数 Runnable ;submit() 参数 (Runnable) 或 (Runnable 和 结果 T) 或 (Callable)
  • execute() 没有返回值;而 submit() 有返回值
  • submit() 的返回值 Future 调用 get 方法时,可以捕获处理异常;execute()只有通过ThreadFactory主动设置线程的异常处理类才能感知到提交的线程中的异常信息。
1
2
3
public interface ThreadFactory {
Thread newThread(Runnable r);
}

对于 execute 方法可以给 newThread 中创建的 Thread 对象设置UncaughtExceptionHandler。而 submit 不能这样,因为它没有抛出异常而是把保存起来在 get 时候才处理。

内置线程池

SingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

1
2
3
public static ExecutorService newSingleThreadExecutor(){
return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

FixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

1
2
3
public static ExecutorService newFixedThreadPool(int nThreads){
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

CachedThreadPool

SynchronousQueue是一个只有1个元素的队列,入队的任务需要一直等待直到队列中的元素被移出。核心线程数是0,意味着所有任务会先入队列;最大线程数是Integer.MAX_VALUE,可以认为线程数量是没有限制的。KeepAlive时间被设置成60秒,意味着在没有任务的时候线程等待60秒以后退出。CachedThreadPool适用于执行很多的短期异步任务的小程序。

1
2
3
public static ExecutorService newCachedThreadPool(){
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}

ScheduledThreadPool

创建一个定长线程池,支持定时及周期性任务执行.

1
2
3
4
5
6
7
8
9
10
11
12
13
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
// ...
}
}, 3, TimeUnit.SECONDS);

线程池状态

1
2
3
4
5
6
7
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static int ctlOf(int rs, int wc) { return rs | wc; }
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;

状态转换如下图:

源码解读

首先看看 execute 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void execute(Runnable command) {
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) { // 小于核心线程数
if (addWorker(command, true)) // 创建并添加Worker,内部有Thread参数
return;
c = ctl.get();
}
// 核心线程满了则尝试往阻塞队列添加任务
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (!isRunning(recheck) && remove(command))
reject(command); // 线程池不在RUNNING状态则拒绝
else if (workerCountOf(recheck) == 0)
// 当一个worker都没有则创建一个非核心线程
addWorker(null, false);
}
// 否则创建一个非核心线程,失败则走拒绝策略
else if (!addWorker(command, false)) reject(command);
}

然后看看 Worker 的执行逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public void run() {
runWorker(this);
}

final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// 当存在firstTask或队列中有task则执行
while (task != null || (task = getTask()) != null) {
w.lock();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (XX x) {
// rethrow
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}

线程执行时,调用 runWorker 方法,这里面会调用 getTask 获取任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?

for (;;) {
int c = ctl.get();
int rs = runStateOf(c);

int wc = workerCountOf(c);
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

try {
// 从任务队列里取任务, timeout 为 keepAliveTime。
// 即线程空闲时间达到keepAliveTime则返回空任务,线程没任务执行就退出了
// 而核心线程默认则不受该参数影响,没任务时会一直阻塞
Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
if (r != null) return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}

关闭线程池

  • void shutdown(): 线程池队列中的任务会执行,无法提交新的任务,该方法不会等待执行的任务执行完成,可以使用 awaitTermination 实现这个目的。
  • List<Runnable> shutdownNow(): 试图关闭正在执行的任务(任务需要处理中断),不会执行已经提交到队列但还没执行的任务,返回等待执行的任务列表,同时此方法不会等待那些正在执行的任务执行完,等待执行的任务会从线程池队列移除。

关闭线程池主要利用的是线程的中断机制,看看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
advanceRunState(SHUTDOWN); // 置为SHUTDOWN
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}

public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
advanceRunState(STOP); // 置为STOP
interruptWorkers();
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}

看下 interruptIdleWorkers 和 interruptWorkers 的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private void interruptIdleWorkers() {
interruptIdleWorkers(false);
}

private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread;
// 尝试获取Worker中的锁,线程池的线程在执行task时也会获取这把锁
// 因此如果任务正在执行,则会拿锁失败
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}

private void interruptWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers)
w.interruptIfStarted(); // 直接置为中断状态
} finally {
mainLock.unlock();
}
}

看看 getTask 中对中断的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private Runnable getTask() {
boolean timedOut = false;
for (;;) {
// SHUTDOWN后状态且(队列为空或处于STOP状态)则返回null
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}

小结

Executors各个方法的弊端,因此可以直接使用 ThreadPoolExecutor 构造方法创建线程池:

  • newFixedThreadPool 和 newSingleThreadExecutor: 主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
  • newCachedThreadPool 和 newScheduledThreadPool: 主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。