Android Framework-操作系统基础

最近在看《深入理解Android内核设计思想(第2版)》,个人感觉很不错,内容很多,现将书里个人认为比较重要的内容摘录一下,方便后期随时翻看。

计算机体系结构

硬件是软件的基石,所有的软件功能最终都是由硬件来实现的。计算机体系结构作为一门学科,是软件和硬件的抽象体。

1.1 冯·诺依曼结构
在这里插入图片描述

1.2 哈佛结构
哈佛结构(Harvard Architecture)并不是作为冯诺依曼结构的对立面出现的;相反,它们都属于stored-program类型体系。区别就在于前者的指令与数据并不保存在同一个存储器中,即哈佛结构是对冯诺依曼结构的改进与完善。
在这里插入图片描述
由于取指令和数据无法同步进行,冯诺依曼结构的执行速率并不占优势。而采用哈佛结构的计算机由于指令和数据的单独存储,可以在执行操作的同时预读下一条指令,所以在一定程度上可以提高其吞吐量。
哈佛结构的缺点在于构架复杂且需要两个存储器,因而通常会被运用在对速度有特殊需求且成本预算相对较高的场合。目前市面上采用哈佛结构的芯片包括ARM公司的ARM9、ARM11,以及ATMEL公司的AVR系列等。
不论是何种结构,它们所包含的基本元素都是不变的,即:
CPU(中央处理器);
内存储器;
输入设备;
输出设备。
其中输入和输出设备一般会统称为I/O设备(外存储器实际上也归于这一类)。因而,最后可以把计算机结构简化为:
中央处理器;
内存储器;
I/O设备。

什么是操作系统

操作系统的共性:
操作系统对硬件设备本身是有要求的。
同一款操作系统可以安装在不同型号的机器上。
操作系统提供可用的人机交互界面。
操作系统支持用户编写和安装程序。
在这里插入图片描述
操作系统“肩负”两大重任。
1.面向下层
管理硬件。这里的硬件是笼统的概念,它包含了CPU、内存、Flash、各种I/O设备等系统中所有硬件组成元素。
2.面向上层
一方面,操作系统需要为用户提供可用的人机交互界面;另一方面,它还负责为第三方程序的研发提供便捷、可靠、高效的API(Application Programming Interface)。这样上层应用的设计实现就可以不用直接面向硬件,从而大大缩短了应用开发的时间。
操作系统定义:计算机操作系统是负责管理系统硬件,并为上层应用提供稳定编程接口和人机交互界面的软件集合。

进程间通信的经典实现

操作系统中的各个进程通常运行于独立的内存空间中,并且有严格的机制来防止进程间的非法访问。但是,这并不代表进程与进程间不允许互相通信;
广义地讲,进程间通信(Inter-process communication,IPC)是指运行在不同进程(不论是否在同一台机器)中的若干线程间的数据交换,具体如图所示
在这里插入图片描述
从这个定义可以看到:
IPC中参与通信的进程既可运行在同一台机器上,也允许它们存在于各自的设备环境中(RPC)。如果进程是跨机器运行的,则通常由网络连接在一起,这无疑给进程间通信的实现增加了难度。
在这里插入图片描述
实现方式多种多样。原则上,任何跨进程的数据交换都可以称为进程间通信。除了传统意义上的消息传递、管道等,还可以使用一些简单的方法来实现对性能要求不高的进程通信。比如:
文件共享
比如两个进程间约定以磁盘上的某个文件作为信息交互的媒介。在这种情况下,要特别注意不同进程访问共享文件时的同步问题。
操作系统提供的公共信息机制
比如Windows上的注册表对于所有进程来说都是可以访问的,因而在特定情况下也能作为进程间信息交换的平台。
虽然各操作系统所采用的进程间通信机制可以说五花八门,但以下将要讨论的几种却因其高效、稳定等优点而几乎被广泛应用于所有操作系统中。

1.1 共享内存(Shared Memory)

共享内存是一种常用的进程间通信机制。由于两个进程可以直接共享访问同一块内存区域,减少了数据的复制操作,因而速度上的优势比较明显。一般情况下,实现内存共享的步骤如图所示。
在这里插入图片描述
Step1. 创建内存共享区
进程1首先通过操作系统提供的API从内存中申请一块共享区域——比如在Linux环境中可以通过shmget函数来实现。生成的共享内存块将与某个特定的key(即shmget的第一个参数)进行绑定。
Step2. 映射内存共享区
成功创建了内存共享区后,我们需要将它映射到进程1的空间中才能进一步操作。在Linux环境下,这一步可以通过shmat来实现。
Step3.访问内存共享区
进程1已经创建了内存共享区,那么进程2如何访问到它呢?没错,就是利用第一步中的key。具体而言,进程2只要通过shmget,并传入同一个key值即可。然后进程2执行shmat,将这块内存映射到它的
空间中。
Step4. 进程间通信
共享内存的各个进程实现了内存映射后,便可以利用该区域进行信息交换。由于内存共享本身并没有同步的机制,所以参与通信的诸进程需要自己协商处理。
Step5. 撤销内存映射区
完成了进程间通信后,各个进程都需要撤销之前的映射操作。在Linux中,这一步可以通过shmdt来实现。
Step6. 删除内存共享区
最后必须删除共享区域,以便回收内存。在Linux环境中,可以通过shctl函数来实现。

1.2 管道(Pipe)

管道也是操作系统中常见的一种进程间通信方式,它适用于所有POSIX系统以及Windows系列产品。
Pipe这个词很形象地描述了通信双方的行为,即进程A与进程B。
1.分立管道的两边,进行数据的传输通信。
2.管道是单向的,意味着一个进程中如果既要“读”也要“写”的话,那么就得建立两根管道。这点很像水管的特性,通常水流只做正向或反向行进。
3.一根管道同时具有“读取”端(read end)和“写入”端(write end)。比如进程A从write end写入数据,那么进程B就可以从read end读取到数据。
4.管道有容量限制。即当pipe满时,写操作(write)将阻塞;反之,读操作(read)也会阻塞。

1.3 UNIX Domain Socket

UNIX Domain Socket(UDS)是专门针对单机内的进程间通信提出来的,有时也被称为IPC Socket。
大家所熟识的Network Socket是以TCP/IP协议栈为基础的,需要分包、重组等一系列操作。而UDS因为是本机内的“安全可靠操作”,实现机制上并不依赖于这些协议。
Android中使用最多的一种IPC机制是Binder,其次就是UDS。相关资料显示,2.2版本以前的Android系统,曾使用Binder作为整个GUI架构中的进程间通信基础。后来因某些原因不得不弃之而用UDS,可见后者还是有一定优势的。
使用UDS进行进程间通信的典型流程如图所示
在这里插入图片描述
UDS的基本流程与传统Socket一致,只是在参数上有所区分。下面提供一个UDS的范例,功能如下:
服务器端监听IPC请求;
客户端发起IPC申请;
双方成功建立起IPC连接;
客户端向服务器端发送数据,证明IPC通信是有效的。

1.4 RPC(Remote Procedure Calls)

RPC涉及的通信双方通常运行于两台不同的机器中。在RPC机制中,开发人员不需要特别关心具体的中间传输过程是如何实现的,这种“透明性”可以较大程度地降低研发难度,如图所示。
在这里插入图片描述

一般而言,一个完整的RPC通信需要以下几个步骤:
1.客户端进程调用Stub接口;
2.Stub根据操作系统的要求进行打包,并执行相应的系统调用;
3.由内核来完成与服务器端的具体交互,它负责将客户端的数据包发给服务器端的内核;
4.服务器端Stub解包并调用与数据包匹配的进程;
5.进程执行操作;
6.服务器以上述步骤的逆向过程将结果返回给客户端。

同步机制的经典实现

既然操作系统支持多个进程(多线程)的并发执行,那么它们之间难免会出现相互制约的情况。比如两个进程(线程)需要共享唯一的硬件设备,或者同一块内存区域;又或者像生产流水线一样,进程
(线程)的工作依赖于另一方对共享资源的执行结果——换句话说,它们存在协同关系。
从定义上来讲,如果多个(包括两个)进程间存在时序关系,需要协同工作以完成一项任务,则称为同步;如果它们并不满足协同的条件,而只是因为共享具有排他性的资源时所产生的关系,则称为互
斥。
操作系统中常见的几种同步机制。

信号量(Semaphore)

信号量与PV原语操作是由Dijkstra发明的,也是使用最为广泛的互斥方法之一。它包括以下几个元素:
Semaphore S(信号量);
Operation P(来自荷兰语proberen,意为test),有时也表达为wait();
Operation V(来自荷兰语verhogen,意为increment),有时也表达为signal()。
Semaphore S用于指示共享资源的可用数量。P原语可以减小S计数,V则增加它的计数。由此可知当某个进程想进入共享区时,首先要执行P操作;同理,想退出共享区时执行V操作。PV原语都属于原子操
作(Atomic Operations),意味着它们的执行过程是不允许被中断的。
在这里插入图片描述
P操作的执行过程:
信号量S自减1;
如果此时S仍然≥0,说明共享资源此时是允许访问的,因而调用者将直接返回,然后开始操作共享资源;
否则的话就要等待别人主动释放资源,这种情况下调用者会被加入等待队列中,直到后续被唤醒;
当某人释放了共享资源后,处于等待队列中的相关(取决于具体情况)对象会被唤醒,此时该对象就具备了资源的访问权。
V操作的执行过程:
信号量S自增1;
此时如果S>0,说明当前没有希望访问资源的等待者,所以直接返回;
否则V操作要唤醒等待队列中的相关对象,对应P操作中的最后一步。

Mutex

Mutex是Mutual Exclusion的缩写,其释义为互斥体。那么,它和Semaphore有什么区别和联系呢?
根据计算机领域的普遍观点,如果资源允许多个对象同时访问,称为Counting Semaphores;而对于只允许取值0或1(即locked/unlocked)的Semaphore,则叫作Binary Semaphore。后者可以认为与Mutex具有相同的性质。换句话说,Mutex通常是对某一排他资源的共享控制——要么这个资源被占用(locked),要么就是可以访问的(unlocked)。在很多操作系统中,Binary Semaphore和Mutex没有本质差异,前者是特定的Semaphore机制,而后者相较于Semaphore在实现上则更为简单。

管程(Monitor)

管程实际上是对Semaphore机制的延伸和改善,是一种控制更为简单的同步手段。
采用Semaphore机制的程序易读性相对较差,对于信号量的管理也分散在各个参与对象中,因而有可能由此引发一系列问题,如死锁、进程饿死等。为了使资源的互斥访问更利于维护,科学家们提出了管
程的概念。如下:
管程(Monitor)是可以被多个进程/线程安全访问的对象(object)或模块(module)。
管程中的方法都是受mutual exclusion保护的,意味着在同一时刻只允许有一个访问者使用它们。另外,管程还具备如下属性:
安全性;
互斥性;
共享性。

Linux Futex

Futex(Fast Userspace muTEXes)是由Hubertus Franke等人发明的一种同步机制,首次出现在Linux 2.5.7版本,并在2.6.x中成为内核主基线的一部分。它的核心优势已经体现在其名称中了,即“Fast”。Futex的“快”主要体现于它在应用程序空间中就可以应对大多数的同步场景(只有当需要仲裁时才会进入内核空间),这样一来节省了不少系统调用和上下文切换的时间。
Futex在Android中的一个重要应用场景是ART虚拟机。如果Android版本中开启了ART_USE_FUTEXES宏,那么ART虚拟中的同步机制就会以Futex为基石来实现。
对于不存在竞争的情况下,采用futex机制在用户态就可以完成锁的获取,而不需要通过系统调用进入内核态,从而提高了效率。

同步范例

理解了以上几种同步机制的原理后,我们再来分析一个范例,即经典的生产者与消费者问题。Android系统源码中有多个地方都应用了这个经典模型,如音频子系统里AudioTrack和AudioFlinger间的数据交互。
()“The producer-consumer problem”)描述如下:
两个进程共享一块大小为N的缓冲区——其中一个进程负责往里填充数据(生产者),而另一个进程则负责往里读取数据(消费者)。
问题的核心有两点:
当缓冲区满时,禁止生产者继续添加数据,直到消费者读取了部分数据后;
当缓冲区空时,消费者应等待对方继续生产后再执行操作。
在这里插入图片描述
如果使用信号量来解决这个问题,需要用到3个Semaphore。功能分别如下:
S_emptyCount:用于生产者获取可用的缓冲区空间大小,初始值为N。
S_fillCount:用于消费者获取可用的数据大小,初始值为0。
S_mutex:用于操作缓冲区,初始值为1。
对于生产者来说,执行步骤如下:
循环开始;
Produce_item;
P(S_emptyCount);
P(S_mutex);
Put_item_to_buffer;
V(S_mutex);
V(S_fillCount);
继续循环。
对于消费者来说,执行步骤如下:
循环开始;
P(S_fillCount);
P(S_mutex);
Read_item_from_buffer;
V(S_mutex);
V(S_emptyCount);
Consume;
继续循环。
一开始,S_emptyCount的值为N,S_fillCount的值为0,所以消费者在P(S_fillCount)后处于等待状态。而生产者首先生产一件产品,然后获取S_emptyCount——因为它为N,处于可访问状态,所以产
品可以被放入buffer中。之后生产者通过V(S_fillCount)来增加可用产品的计数,并唤醒正于这个Semaphore上等待的消费者。于是消费者开始读取数据,并利用V(S_emptyCount)来表明buffer又空出了一个位置。
生产者和消费者就是如此循环反复来完成整个工作,这样我们就通过Semaphore机制保证了生产者和消费者的有序执行。

Android中的同步机制

Android封装的同步类包括:
Mutex
头文件是frameworks/native/include/utils/Mutex.h。
Android中的Mutex只是对pthread提供的API的简单再封装,所以函数声明和实现体都放在同一个头文件中,这样做也方便了调用者的操作。
另外,Mutex中还包含一个AutoLock的嵌套类,它是利用变量生命周期特点而设计的一个辅助类。
Condition
头文件是frameworks/native/include/utils/Condition.h
Condition是“条件变量”在Android系统中的实现类,它是依赖Mutex来完成的。
Barrier
头文件是frameworks/native/services/surfaceflinger/Barrier.h
Barrier是同时基于Mutex和Condition实现的一个模型。

进程间同步——Mutex

class Mutex {
    
    
public:
 enum {
    
    
 PRIVATE = 0,//只限同一进程间的同步
 SHARED = 1//支持跨进程间的同步
 };

这说明Mutex既可以处理进程内同步的情况,也能完美解决进程间同步的问题。
如果在Mutex构造时指定它的type是SHARED的话,说明它是适用于跨进程共享的。此时Mutex将进一步调用pthread_mutexattr_setpshared接口来为这个互斥量设置PTHREAD_PROCESS_SHARED属性。
与Semaphore不同的是,Mutex只有两种状态,即0和1。所以这个类只提供了3个重要的接口函数:

status_t lock(); //获取资源锁
 void unlock(); //释放资源锁
 status_t tryLock(); /*不论成功与否都会及时返回,而不是等待*/

当调用者希望访问临界资源时,它必须先通过lock()来获得资源锁。如果此时资源可用,这个函数将马上返回;否则,会进入阻塞等待,直到有人释放了资源锁并唤醒它。释放资源锁调用的是unlock(),同时正在等待使用这个锁的其他对象就会被唤醒,然后继续执行它的任务。另外,Mutex还特别提供了一个tryLock()来满足程序的多样化需求。这个函数仅会“试探性”地查询资源锁是否可用——如果答案是肯定的就获取它,然后成功返回(返回值为0),从这一点看它和lock()的表现是一样的;但在资源暂不可用的情况下,它并不会进入等待,而同样是立即返回,只是返回值不为0。
这三个函数的实现很简单,具体源码如下:

inline status_t Mutex::lock() {
    
    
 return -pthread_mutex_lock(&mMutex);//变量mMutex的类型是
pthread_mutex_t
}
inline void Mutex::unlock() {
    
    
 pthread_mutex_unlock(&mMutex);
}
inline status_t Mutex::tryLock() {
    
    
 return -pthread_mutex_trylock(&mMutex);
}

Mutex实际上只是基于pthread接口的再封装。

条件判断——Condition

Condition的字面意思是“条件”。换句话说,它的核心思想是判断“条件是否已经满足”——满足的话马上返回,继续执行未完成的动作;否则就进入休眠等待,直到条件满足时有人唤醒它。
这种情况用Mutex能实现吗?理论上讲,的确是可以的。举一个例子,假设两个线程A和B共享一个全局变量vari,且它们的行为如下。
Thread A:不断去修改vari,每次改变后的值未知。
Thread B:当vari为0时,它需要做某些动作。
显而易见,A和B都想访问vari这个共享资源,属于Mutex的问题领域。但需要商榷的细节是:线程A的“企图”仅仅是获得vari的访问权;而线程B则“醉翁之意不在酒”,其真正等待的条件是“vari等于0”。
那么如果用Mutex去完成的话,线程B就只能通过不断地读取vari来判断条件是否满足,有点类似于下面的伪代码

while(1)//死循环,直到条件满足时退出
{
    
    
 acquire_mutex_lock(vari);//获取vari的Mutex锁
 if(0 == vari) //条件满足
 {
    
    
 release_mutex_lock(vari);//释放锁
 break;
 }
 else
 {
    
    
 release_mutex_lock(vari);//释放锁
 sleep();//休眠一段时间
 } 
}

对于线程B而言,什么时候达到条件(vari==0)是未知的,这点和其他只是使用vari的线程(比如线程A)有很大不同,因而采用轮询的方式显然极大地浪费了CPU时间
再举一个生活中的例子,以加深大家的理解。比如有一个公共厕所,假设同时只能供一个人使用。现在把希望使用这一资源的人分为两类:其一当然是正常使用厕所的用户(类似于线程A);其二就是更换厕纸的工作人员(类似于线程B)。如果我们使用Mutex来解决这一资源同步共享问题,会发生什么情
况呢?
首先对于用户来说并不会产生太大的影响,他们仍然正常排队、使用、归还。
但对于工作人员来说就有点麻烦了。在Mutex机制下,工作人员也需要和其他人一样正常排队。只有等排到他时,他才能进去看下厕纸是否用完——用完就更换,否则就什么也不做直接退出,然后继续排队等待,如此循环往复。假设排一次队需要5分钟,而工作人员进去后需要更换厕纸的概率只有1/10。那么可想而知,这位工作人员的效率是相当低的,因为他的时间都浪费在等待“厕纸为空”上了。
所以,我们需要寻找另一种模型来解决这一特殊的同步场景。可行的方法之一是工作人员不需要排队,而是由其他人通知他厕所缺纸的事件。这样做既减少了排队人员的数量,提高了资源的使用率,同时也改善了工作人员的办事效率,可谓一举两得。

class Condition {
    
    
public:
 enum {
    
     //和Mutex一样,它支持跨进程共享
 PRIVATE = 0,
 SHARED = 1
 };
 …
 status_t wait(Mutex& mutex); //在某个条件上等待
 status_t waitRelative(Mutex& mutex, nsecs_t reltime); /*也是在某个条
件上等待,增加了超
 时退出功能*/
 void signal(); //条件满足时通知相应等待者
 void broadcast(); //条件满足时通知所有等待者
private:
#if defined(HAVE_PTHREADS)
 pthread_cond_t mCond;
#else
 void* mState;
#endif
};

“栅栏、障碍”——Barrier

Condition表示“条件”,而Barrier表示“栅栏、障碍”。后者是对前者的一个应用,即Barrier是填充了“具体条件”的Condition,这给我们理解Condition提供了一个很好的范例:

class Barrier
{
    
    
public:
 inline Barrier() : state(CLOSED) {
    
     }
 inline ~Barrier() {
    
     }
 void open() {
    
    
 Mutex::Autolock _l(lock);
 state = OPENED;
 cv.broadcast();
 }
 void close() {
    
    
 Mutex::Autolock _l(lock);
 state = CLOSED;
 }
 void wait() const {
    
    
 Mutex::Autolock _l(lock);
 while (state == CLOSED) {
    
    
 cv.wait(lock);
 }
 }
private:
 enum {
    
     OPENED, CLOSED };
 mutable Mutex lock;
 mutable Condition cv;
 volatile int state;
};

Barrier总共提供了3个接口函数,即wait(),open()和close()。。既然说它是Condition的实例,那么“条件”是什么呢?稍微观察一下就会发现,是其中的变量state==OPENED;另一个状态当然就是CLOSED——这有点类似于汽车栅栏的开启和关闭。在汽车通过前,它必须要先确认栅栏是开启的,于是调用wait()。如果条件不满足,那么汽车就只能停下来等待。这个函数首先获取一个Mutex锁,然后才是调用Condition对象cv,为什么呢?我们知道Mutex是用于保证共享资源的互斥使用的,这说明wait()中接下来的操作涉及了对某一互斥资源的访问,即state这个变量。可以想象一下,假如没有一把对state访问的锁,那么当wait与open/close同时去操作它时,有没有可能引起问题呢?
假设有如下步骤
Step 1. 线程A通过wait()取得state值,发现是CLOSED。
Step 2. 线程B通过open()取得state值,将其改为OPENED。
Step 3. open()唤醒正在等待的线程。因为此时线程A还没有进入睡眠,所以实际上没有线程需要唤醒。
Step4. 另外,线程A因为state == CLOSED,所以进入等待,但这时候的栅栏实际上已经开启了,这将导致wait()调用者所在线程得不到唤醒。
这样就很清楚了,对于state的访问必须有一个互斥锁的保护。
接下来,我们分析Condition::wait()的实现:

inline status_t Condition::wait(Mutex& mutex) {
    
    
 return -pthread_cond_wait(&mCond, &mutex.mMutex);
}

和Mutex一样,直接调用了pthread提供的API方法。
pthread_cond_wait的逻辑语义如下。
Step1. 释放锁mutex。
Step2. 进入休眠等待。
Step3. 唤醒后再获取mutex锁。
也就是经历了先释放,再获取锁的过程,为什么这么设计呢?
由于wait即将进入休眠等待,假如此时它不先释放Mutex锁,那么open()/close()又如何能访问“条件变量”state呢?这无疑会使程序陷入互相等待的死锁状态。所以它需要先行释放锁,再进入睡眠。之后因为open()操作完毕会释放锁,也就让wait()有机会再次获得这一Mutex锁。

加解锁的自动化操作——Autolock

在Mutex类的内部还有一个Autolock嵌套类,从字面上看它应该是为了实
现加、解锁的自动化操作,那么如何实现呢?
其实很简单,看看这个类的构造和析构函数大家就明白了:

class Autolock {
    
    
 public:
 inline Autolock(Mutex& mutex) : mLock(mutex) {
    
     mLock.lock(); }
 inline Autolock(Mutex* mutex) : mLock(*mutex) {
    
     mLock.lock(); }
 inline ~Autolock() {
    
     mLock.unlock(); }
 private:
 Mutex& mLock;
 };

当Autolock构造时,会主动调用内部成员变量mLock的lock()方法来获取一个锁。而析构时的情况正好相反,调用它的unlock()方法释放锁

读写锁——ReaderWriterMutex

重点分析一下Art虚拟机中的一种特殊mutex,即ReaderWriterMutex。从字面意思上来理解,它代表的是“读写锁”。与普通的mutex相比,它主要提供了如下一些差异接口:

void ExclusiveLock(Thread* self) ACQUIRE();
void ExclusiveUnlock(Thread* self) RELEASE();
bool ExclusiveLockWithTimeout(Thread* self, int64_t ms, int32_t ns)
 EXCLUSIVE_TRYLOCK_FUNCTION(true);
void SharedLock(Thread* self) ACQUIRE_SHARED() ALWAYS_INLINE;
void SharedUnlock(Thread* self) RELEASE_SHARED() ALWAYS_INLINE;

其中Exclusive和Shared分别代表Write和Read权限,这也很好地诠释了这种锁的特点是允许有多个对象共享Read锁,但同时却只能有唯一一个对象拥有Write锁。ReaderWriterMutex可以有以下3种状态。
Free:还没有被任何对象所持有的情况。
Exclusive:当前被唯一一个对象持有的情况。
Shared:被多个对象持有的情况。
各个状态间所允许的操作及操作结果如表所示。
在这里插入图片描述

操作系统内存管理基础

内存管理(Memory Management)旨在为系统中的所有Task提供稳定可靠的内存分配、释放与保护机制。
内存需要理解几个核心:
虚拟内存;
内存分配与回收;
内存保护。

虚拟内存(Virtual Memory)

虚拟内存的出现为大体积程序的运行提供了可能。它的基本思想是。
将外存储器的部分空间作为内存的扩展,如从硬盘划分出4GB大小。
当内存资源不足时,系统将按照一定算法自动挑选优先级低的数据块,并把它们存储到硬盘中。
后续如果需要用到硬盘中的这些数据块,系统将产生“缺页”指令,然后把它们交换回内存中。
这些操作都是由操作系统内核自动完成的,对上层应用“完全透明”。

内存分配与回收

对应用程序而言,内存的分配和回收是它们最关心的。换句话说,这是程序开发者与操作系统内存管理模块间的直接交互点
在这里插入图片描述
既然是操作系统的重要组成部分,内存管理模块也同样遵循操作系统的定义,即为“上层建筑”控制和使用硬件(内存储器)提供有效的接口方法。Linux Kernel所面对的核心问题包括但不限于:
1.保证硬件无关性
每个硬件平台的物理内存型号、大小甚至架构(比如不同的体系结构)等都可能是不一样的。这种差异绝不能体现在应用程序上,操作系统应尽可能实现向上的“透明”。
2.动态分配内存和回收
需要考虑的问题很多,如如何为内存划分不同的使用区域;分配的粒度问题,即分配的最小单位;如何管理和区别已经使用和未使用的内存;如何回收和再利用等。
3.内存碎片
和磁盘管理一样,内存也一样会有碎片的问题。举个简单的例子,如图所示。
在这里插入图片描述
在这个例子中,假设初始状态有6块未使用的内存单元。随着程序不断地申请使用,前面5块已经成功分配。此时若某程序释放了第2块单元,那么最后将会有两块不连续的未使用内存单元存在——碎片形

在Android系统中,内存的分配与回收分为两个方向
Native层
本地层的程序基本上是由C/C++编写的,与开发人员直接相关的内存函数包括malloc/free,new/delete等。
Java层
大多数Android的应用程序都由Java语言编写。Java相对于C在内存管理上做了很多努力,可以帮助开发人员在一定程度上摆脱内存的各种困扰。但是,Java本身并不是万能的,需要研发者在程序设计过
程中保持良好的内存使用规范,并对Android提供的内存管理机制有比较深入的理解。

进程间通信——mmap

上层应用在使用Binder驱动前,就必须通过mmap()来为其正常工作提供环境。
正如其名所示(Memory Map),mmap可以将某个设备或者文件映射到应用进程的内存空间中,这样访问这块内存就相当于对设备/文件进行读写,而不需要再通过read(),write()了。由此可见,理论上mmap也可以用于进程间通信,即通过映射同一块物理内存来共享内存。这种方式因为减少了数据复制的次数,在一定程度上能提高进程间通信的效率
在这里插入图片描述

写时拷贝技术(Copy on Write)

COW(Copy on Write)是Linux中一个非常关键的技术。它的基本思想用一句话来概括,就是多个对象在起始时共享某些资源(如代码段、数据段),直到某个对象需要修改该资源时才拥有自己的一份拷贝。
当我们调用fork()函数生成一个子进程时,内核并没有马上为这个“另立门户”的孩子分配自己的物理内存,而是仍然共享父进程的
固有资源。这样一来“分家”过程就非常快了——理论上只需要注册一个“门户”就可以了。而如果新进程对现有资源“不是很满意”,
希望自己去做些修改,那么此时才需要为它提供自己的“施展空间”。特别是如果子进程在fork()之后很快地调用了exec(概率很
大),从而载入与父进程迥异的映像,那么COW的存在显然可以较大程度地避免不必要的资源操作,从而提升了运行速度。

Android中的Low Memory Killer

嵌入式设备的一个普遍特点是内存容量相对有限。当运行的程序超过一定数量,或者涉及复杂的计算时,很可能出现内存不足,进而导致系统卡顿的现象。Android系统也不例外,它同样面临着设备物理内存短缺的困境。另外,细心的开发者应该已经注意到了,对于已经启动过一次的Android程序,再一次启动所花的时间明显减少了。原因就在于Android系统并不马上清理那些已经“淡出视野”的程序(比如调用Activity.finish退出UI界面)。换句话说,它们在一定的时间里仍然驻留在内存中(虽然用户已经感觉不到它们的存在)。这样做的好处是明显的,即下一次启动不需要再为程序重新创建一个进程;坏处也同样存在,那就是加大了内存OOM(Out Of Memory)的概率。
那么,应该如何掌握平衡点呢?
熟悉Linux的开发人员应该知道,底层内核有自己的内存监控机制,即OOMKiller。一旦发现系统的可用内存达到临界值,这个OOM的管理者就会自动跳出来“收拾残局”。根据策略的不同,OOM的处理手段略有差异。不过它的核心思想始终是:
按照优先级顺序,从低到高逐步杀掉进程,回收内存。
优先级的设定策略一方面要考虑对系统的损害程度(例如系统的核心进程,优先级通常较高),另一方面也希望尽可能多地释放无用内存。根据经验,一个合理的策略至少要综合以下几个因素:
进程消耗的内存;
进程占用的CPU时间;
oom_adj(OOM权重)。

Android匿名共享内存(Anonymous SharedMemory)

Anonymous Shared Memory(简称Ashmem)是Android特有的内存共享机制,它可以将指定的物理内存分别映射到各个进程自己的虚拟地址空间中,从而便捷地实现进程间的内存共享。

JNI

JNI(Java NativeInterface)是一种允许运行于JVM的Java程序去调用(反向亦然)本地代码(通常JNI面向的本地代码是用C、C++以及汇编语言编写的)的编程框架。本地代码通常与硬件或者操作系统有关联,因而会在一定程度上破坏Java本身的可移植性。不过有时这种方法是必需的,如Android系统中就采用了大量JNI手段去调用本地层的实现库。
通常有以下3种情况需要用到JNI
1.应用程序需要一些平台相关的feature的支持,而Java无法满足。
2.兼容以前的用其他语言书写的代码库。使用JNI技术可以让Java层的代码访问到这些旧库,实现一定程度的代码复用。
3.应用程序的某些关键操作对运行速度要求较高。这部分代码可以用底层语言如汇编来编写,再通过JNI向Java层提供访问接口。
Java函数的本地实现
创建一个可供Java代码调用的本地函数的步骤如下:
1.将需要本地实现的Java方法加上native声明;
2.使用javac命令编译Java类;
3.使用javah生成.h头文件;
4.在本地代码中实现native方法;
5.编译上述的本地方法,生成动态链接库;
6.在Java类中加载这一动态链接库;
7.Java代码中的其他地方可以正常调用这一native方法。

Java中的反射机制

Java中创建一个Class类最常见的方式如下:
FileOutputStream fout = new FileOutputStream(fd);
这种情况意味着我们在编译期便可以确定Class类型。此时编译器可以对new关键字做很多优化工作,所以这种场景下的运行效率通常是最好的,是开发人员的首选方式。
而对于那些无法在编译阶段就得到确定的Class类的情况,我们希望有一种技术可以在程序运行过程中去动态地创建一个对象——这就是反射机制。
反射机制能够赋予程序检查和修正运行时行为的能力,下面我们就以Class类的动态创建为例,简单分析一下其内部的实现原

Class<?> clazz = null;
 try {
    
    
 clazz =
Class.forName("android.media.MediaMetadataRetriever");
 instance = clazz.newInstance();
 Method method = clazz.getMethod("setDataSource",
String.class);
 method.invoke(instance, filePath);

Class.forName通过native函数classForName调用到本地层,在Android N版本中对应的具体函数如下:

/*art/runtime/native/java_lang_Class.cc*/
static jclass Class_classForName(JNIEnv* env, jclass, jstring javaName, jboolean initialize, jobject javaLoader) {
    
    
 ScopedFastNativeObjectAccess soa(env);
 ScopedUtfChars name(env, javaName);Handle<mirror::ClassLoader>
class_loader(hs.NewHandle(soa.Decode<mirror::ClassLoader *>
(javaLoader)));
 ClassLinker* class_linker = Runtime::Current()-
>GetClassLinker();
 Handle<mirror::Class> c(
 hs.NewHandle(class_linker->FindClass(soa.Self(),
descriptor.c_str(), 
class_loader)));return soa.AddLocalReference<jclass>(c.Get());
}

可见,forName最终是通过ClassLinker::FindClass来找到目标类对象的。反射机制提供的其他接口也是类似的——只有经过虚拟机的统一管理,才有可能既为程序提供灵活多样的动态能力,同时又保证了程序的正常运行。

猜你喜欢

转载自blog.csdn.net/jifashihan/article/details/129274469