【进阶】从 linux 到 android,进程的方方面面

596 查看

最近在阅读《Linux内核设计与实现》,这里做一下linux中进程相关的知识点整理,以及android中进程的浅析。

下面1,2小节整理自《Linux内核设计与实现》 第三章《进程管理》和第四章《进程调度》。第3节整理android中进程的知识点。

1 Linux中的进程管理

以下内容整理自:《Linux内核设计与实现》 第三章《进程管理》

1.1进程和线程

进程是资源分配的最小单位。
线程是操作系统调度执行的最小单位。

进程和线程是程序运行时状态,是动态变化的,进程和线程的管理操作(比如,创建,销毁等)都是由内核来实现的。Linux中的进程于Windows相比是很轻量级的,而且不严格区分进程和线程,线程是一种特殊的进程。

进程提供2种虚拟机制:虚拟处理器和虚拟内存
每个进程有独立的虚拟处理器和虚拟内存,每个线程有独立的虚拟处理器,同一个进程内的线程有可能会共享虚拟内存。

内核把进程的列表存放在任务队列(task list)中(双向循环链表),链表的每一项类型为task_struct,我们称之为进程描述符(process descriptor)。进程的信息主要保存在task_struct中(位于 include/linux/sched.h)

 1833901-b93eccc35f31487b

通过task_struct和thread_info存放和表示进程。

 1833901-6ccaad23134ab9fb

 

进程标识PID(process identification value)和线程标识TID(thread identification value)对于同一个进程或线程来说都是相等的。
Linux中可以用ps命令查看所有进程的信息:
ps -eo pid,tid,ppid,comm

1.2 进程的生命周期

进程的各个状态之间的转化构成了进程的整个生命周期。

 1833901-0a02648834412e5f

 

进程有五种进程状态:
除了图片上面的三种还有,_TASK_TRACED_TASK_STOPPED

1.3 进程的创建

Linux中创建进程分2步:fork()和exec()。

1 fork(): 通过拷贝当前进程创建一个子进程 (实际上最终是通过clone( ) )
2 exec(): 读取可执行文件,将其载入到地址空间中运行 (是一个系统调用族)

创建的流程:

1 调用dup_task_struct()为新进程分配内核栈,task_struct等,其中的内容与父进程相同。
2 check新进程(进程数目是否超出上限等)
3 清理新进程的信息(比如PID置0等),使之与父进程区别开。
4 新进程状态置为 TASK_UNINTERRUPTIBLE
5 更新task_struct的flags成员。
6 调用alloc_pid()为新进程分配一个有效的PID
7 根据clone()的参数标志,拷贝或共享相应的信息
8 做一些扫尾工作并返回新进程指针
9 创建进程的fork()函数实际上最终是调用clone()函数。

创建线程和进程的步骤一样,只是最终传给clone()函数的参数不同。
比如,通过一个普通的fork来创建进程,相当于:clone(SIGCHLD, 0)
创建一个和父进程共享地址空间,文件系统资源,文件描述符和信号处理程序的进程,即一个线程:clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)

Linux通过Clone()系统调用实现fork()

在内核中创建的内核线程与普通的进程之间还有个主要区别在于:内核线程没有独立的地址空间,它们只能在内核空间运行。这与之前提到的Linux内核是个单内核有关。

 

1.4 进程的终止

发生在进程调用exit()系统调用时
和创建进程一样,终结一个进程同样有很多步骤:

子进程上的操作,靠do_exit()完成->定义于(kernel/exit.c)

1 设置task_struct中的标识成员设置为PF_EXITING
2 调用del_timer_sync()删除内核定时器, 确保没有定时器在排队和运行
3 调用exit_mm()释放进程占用的mm_struct
4 调用sem__exit(),使进程离开等待IPC信号的队列
5 调用exit_files()和exit_fs(),释放进程占用的文件描述符和文件系统资源
6 把task_struct的exit_code设置为进程的返回值
7 调用exit_notify()向父进程发送信号,并把自己的状态设为EXIT_ZOMBIE
8 切换到新进程继续执行

子进程进入EXIT_ZOMBIE之后,虽然永远不会被调度,关联的资源也释放 掉了,但是它本身占用的内存还没有释放,比如创建时分配的内核栈,task_struct结构等。这些由父进程来释放。父进程上的操作(release_task)
父进程受到子进程发送的exit_notify()信号后,将该子进程的进程描述符和所有进程独享的资源全部删除。

从上面的步骤可以看出,必须要确保每个子进程都有父进程,如果父进程在子进程结束之前就已经结束了会怎么样呢?
子进程在调用exit_notify()时已经考虑到了这点。如果子进程的父进程已经退出了,那么子进程在退出时,exit_notify()函数会先调用forget_original_parent(),然后再调用find_new_reaper()来寻找新的父进程。find_new_reaper()函数先在当前线程组中找一个线程作为父亲,如果找不到,就让init做父进程。(init进程是在linux启动时就一直存在的)


2 Linux中的进程调度

以下内容整理自:《Linux内核设计与实现》 第四章《进程调度》

2.1 什么是调度?

现在的操作系统都是多任务的,为了能让更多的任务能同时在系统上更好的运行,需要一个管理程序来管理计算机上同时运行的各个任务(也就是进程)。这个管理程序就是调度程序,它的功能说起来很简单:

1决定哪些进程运行,哪些进程等待
2 决定每个进程运行多长时间

此外,为了获得更好的用户体验,运行中的进程还可以立即被其他更紧急的进程打断。
总之,调度是一个平衡的过程。一方面,它要保证各个运行的进程能够最大限度的使用CPU(即尽量少的切换进程,进程切换过多,CPU的时间会浪费在切换上);另一方面,保证各个进程能公平的使用CPU(即防止一个进程长时间独占CPU的情况)。

2.2 调度实现的原理

前面说过,调度功能就是决定哪个进程运行以及进程运行多长时间
决定哪个进程运行以及运行多长时间都和进程的优先级有关。为了确定一个进程到底能持续运行多长时间,调度中还引入了时间片的概念。

关于进程的优先级

1 进程的优先级有2种度量方法,一种是nice值,一种是实时优先级。
2 nice值的范围是-20~+19,值越大优先级越低,也就是说nice值为-20的进程优先级最大。
3 实时优先级的范围是0~99,与nice值的定义相反,实时优先级是值越大优先级越高。
4 实时进程都是一些对响应时间要求比较高的进程,因此系统中有实时优先级高的进程处于运行队列的话,它们会抢占一般的进程的运行时间。
5 实时优先级高于nice值。
6 一个进程不可能有2个优先级。

关于时间片

有了优先级,可以决定谁先运行了。但是对于调度程序来说,并不是运行一次就结束了,还必须知道间隔多久进行下次调度。
于是就有了时间片的概念。时间片是一个数值,表示一个进程被抢占前能持续运行的时间。
也可以认为是进程在下次调度发生前运行的时间(除非进程主动放弃CPU,或者有实时进程来抢占CPU)。
时间片的大小设置并不简单,设大了,系统响应变慢(调度周期长);设小了,进程频繁切换带来的处理器消耗。默认的时间片一般是10ms

调度实现原理
下面举个直观的例子来说明:

假设系统中只有3个进程ProcessA(NI=+10),ProcessB(NI=0),ProcessC(NI=-10),NI表示进程的nice值,时间片=10ms
1) 调度前,把进程优先级按一定的权重映射成时间片(这里假设优先级高一级相当于多5msCPU时间)。
假设ProcessA分配了一个时间片10ms,那么ProcessB的优先级比ProcessA高10(nice值越小优先级越高),ProcessB应该分配105+10=60ms,以此类推,ProcessC分配205+10=110ms
2) 开始调度时,优先调度分配CPU时间多的进程。由于ProcessA(10ms),ProcessB(60ms),ProcessC(110ms)。显然先调度ProcessC
3) 10ms(一个时间片)后,再次调度时,ProcessA(10ms),ProcessB(60ms),ProcessC(100ms)。ProcessC刚运行了10ms,所以变成100ms。此时仍然先调度ProcessC
4) 再调度4次后(4个时间片),ProcessA(10ms),ProcessB(60ms),ProcessC(60ms)。此时ProcessB和ProcessC的CPU时间一样,这时得看ProcessB和ProcessC谁在CPU运行队列的前面,假设ProcessB在前面,则调度ProcessB
5) 10ms(一个时间片)后,ProcessA(10ms),ProcessB(50ms),ProcessC(60ms)。再次调度ProcessC
6) ProcessB和ProcessC交替运行,直至ProcessA(10ms),ProcessB(10ms),ProcessC(10ms)。
这时得看ProcessA,ProcessB,ProcessC谁在CPU运行队列的前面就先调度谁。这里假设调度ProcessA
7) 10ms(一个时间片)后,ProcessA(时间片用完后退出),ProcessB(10ms),ProcessC(10ms)。
8) 再过2个时间片,ProcessB和ProcessC也运行完退出。

这个例子很简单,主要是为了说明调度的原理,实际的调度算法虽然不会这么简单,但是基本的实现原理也是类似的:

1)确定每个进程能占用多少CPU时间(这里确定CPU时间的算法有很多,根据不同的需求会不一样)
2)占用CPU时间多的先运行
3)运行完后,扣除运行进程的CPU时间,再回到 1)

 

2.3 Linux上调度实现的方法

Linux上的调度算法是不断发展的,在2.6.23内核以后,采用了“完全公平调度算法”,简称CFS。
CFS算法在分配每个进程的CPU时间时,不是分配给它们一个绝对的CPU时间,而是根据进程的优先级分配给它们一个占用CPU时间的百分比。

2.4 调度相关的系统调用

调度相关的系统调用主要有2类:

1) 与调度策略和进程优先级相关 (就是上面的提到的各种参数,优先级,时间片等等) – 下表中的前8个

2) 与处理器相关 – 下表中的最后3个

系统调用 描述
nice() 设置进程的nice值
sched_setscheduler() 设置进程的调度策略,即设置进程采取何种调度算法
sched_getscheduler() 获取进程的调度算法
sched_setparam() 设置进程的实时优先级
sched_getparam() 获取进程的实时优先级
sched_get_priority_max() 获取实时优先级的最大值,由于用户权限的问题
sched_get_priority_min() 获取实时优先级的最小值,理由与上面类似
sched_rr_get_interval() 获取进程的时间片
sched_setaffinity() 设置进程的处理亲和力,其实就是保存在task_struct中的cpu_allowed这个掩码标志。该掩码的每一位对应一个系统中可用的处理器,默认所有位都被设置,即该进程可以再系统中所有处理器上执行。用户可以通过此函数设置不同的掩码,使得进程只能在系统中某一个或某几个处理器上运行。
sched_getaffinity() 获取进程的处理亲和力
sched_yield() 暂时让出处理器

3 android中的进程基础

3.1 进程

默认情况下,Android为每个应用程序创建一个单独的进程,所有组件运行在该进程中,这个默认进程的名字通常与该应用程序的包名相同。比如

那么该程序默认的进程名为com.lt.mytest设置该属性可以使得本应用程序与其它应用程序共享相同的进程。

注意: 标签不支持android:process属性

但是,如果我们想要控制让某个特定的组件属于某个进程,我们可以在manifest文件中进行配置。在每种组件元素(activity、service、receiver、provider)的manifest条目中,都支持一个android:process的属性,通过这个属性,我们可以指定某个组件运行的进程。我们可以通过设置这个属性,让每个组件运行在它自己的进程中,也可以只让某些组件共享一个进程。我们要可以通过设置android:process属性,让不同应用程序中的组件运行在相同的进程中,这些应用程序共享相同的Linux用户ID,拥有相同的证书。

元素也有一个android:process属性,可以设置一个应用于全部组件的默认值。 当可用内存数量低,而一些与用户即时交互的进程又需要内存时,Android随时可能会终止某个进程。运行在被终止的进程中的组件会因此被销毁,但是,当再次需要这些组件工作时,就会再启动一个进程。

在决定要终止哪个进程时,Android系统会权衡它们对于用户的重要性。例如,相较于运行可见activities的进程,终止一个运行不可见activities的进程会更加合理。是否终止一个进程,依赖于运行在这个进程中的组件的状态。

3.2进程生命周期

Android系统会尽可能让一个应用程序进程运行更长的时间,但是它也需要移除旧的进程,为那些新创建的进程或者相比起来更加重要的进程释放内存空间。要决定哪个进程保留,哪个进程终止,系统会将每个进程放置到“importance hierarchy”中,“importance hierarchy”是基于运行在进程中的组件以及这些组件的状态的。拥有最低重要性的进程会首先被干掉,然后就是那些次低重要性的进程,依次类推。在“importance hierarchy”中,共有五个等级。下面的列表中,按照重要性列出了五种不同类型的进程:

1、 前台进程(Foreground process)
2、 可见进程(Visible process)
3、 服务进程(Service process)
4、 后台进程(Background process)
5、 空进程(Empty process)

元素 Android:process属性定义了运行Activity所在进程的名称。通常,一个应用程序的所有组件运行在应用程序创建的默认的进程。它具有与应用程序包相同的名称。元素的 android:process属性可以为所有组件设置不同的默认进程名称。但是,每个组件都可以覆盖默认设置,让应用程序跨多个进程。

如果分配给此属性的名称以一个冒号(‘:’)开头,发将创建一个新的属于应用程序的私有的进程,在这一进程中运行。如果进程的名称由小写字母开始,活动将在该名称的全局进程中运行,只要它有这样做的权限。这样做将使在不同的应用程序中的组件共享一个进程,减少资源的使用。

与其它应用程序共享的一个Linux User Id的名字。

默认情况下,Android为每个应用程序分配一个唯一的User Id。然而,如果有多个应用程序都将该属性设置为一个相同的值,那么它们将共享相同的Id。如果这些应用程序再被设置成运行在一个相同的进程,它们便可以彼此访问对方的数据。

3.3 android中的多进程使用需要注意的问题

原文请参考官方文档链接,下面内容由博主进行翻译。

如果你的app有需要的话,将你的app中使用app会在降低内存消耗。但是大多数的app都是不需要多进程的,因为如果方法不当的话,反而会增加内存消耗。(一个可以使用多进程的app情况是,比如你需要在后台和前台都需要做大量的工作并且需要分别管理)
比如说音乐播放器,我们需要在后台利用service进行长时间的音乐播放。假如我们将整个app都放在一个进程中的话,那么即使我们在操作其他app,后台音乐播放的时候,关于activity UI界面的许多内存分配以及控制音乐播放的service都会被保存。这种情况,我们就可以使用两个进程:一个专门针对UI界面,一个专门针对后台音乐service的播放。
这篇文章会对你有帮助,Android 后台任务型App多进程架构演化
所以针对service,我们就可以指定android:process为一个字符串(可以为任意名字)

当然这里使用冒号(‘:’) 开头,这在上面的文章中也提到了,这保证了当前进程是app私有的。

注意点:
1 如果你需要将你的app划分为多进程,那么只能让一个进程负责UI处理,其他进程应当避免UI处理,否则你的内存会急速上升,一旦UI绘制之后,想降低内存消耗也会是一个难题。
2 当在android中使用多进程的时候,应当保持代码的精简。应为对于共同的实现操作现在会在不同的进程里造成多余的系统开销。假如你使用enums(虽然你不应该使用enums),那么内存会需要在不同的进程里创建和初始化这些变量。关于adapters的任何抽象以及临时变量都会造成重复的开销。
3 关于多进程的另外一个关注点就是其中存在的一些共同的依赖关系,比如说你的app有一个content provider 运行在默认的进程中(包含UI的进程),那么后台进程使用content provider ,那么content provider也会需要你的内存中有UI进程。这时候,如果你的目标是后台进程独立于繁重的前台进程,那么它肯定也就不能使用UI进程中content provider 或者service那些了。

 

3.4 关于我们从最近任务列表中清除app的问题

 1833901-a2bbaa6305fcedf0

 

通过3.3, 我们也会联想到平常使用音乐软件(比如音乐),当我们选择退出应用的时候,音乐都会在后台播放,当时当我们从任务列表中清除音乐软件的时候,音乐就会停止了,那么当我们从任务列表中清除app,到底发生了什么?直接看看stackexchange这个回答吧 what-actually-happens-when-you-swipe-an-app-out-of-the-recent-apps-list

简单来说,这和多次按返回键退出应用一样,系统会杀掉后台进程,但优势也不是这样。
从最近任务中移除一个条目会移除这个app存在的后台进程。但是它并不会直接结束service,当他们在任务列表中被清除的时候,其实他们自己有相应的api(onTaskRemoved被调用)处理service是否应当被结束。也就是说,你使用的e-mail接收的app即使你在任务列表中把它清除了,它的service也会接收e-mail信息。
当然如果你想要完全停止一个app,你可以通过设置->应用管理 ->进入应用信息页面,点击强制退出。强制退出会让该app的所有进程被杀掉,所有的service停止,所有的通知被移除,所有的提醒被关闭等。该app除了被再次调用的情况下,不会再被启动。
也就是说,是由app来决定在任务列表清楚的时候,后台进程是否被杀掉。

这也就解释了为什么我们在最近任务列表中清除了支付宝,但是支付宝却还在我们的后台运行进程里面了。如果我们直接在应用信息界面强行停止了,这时候,支付宝就完全退出了。

 1833901-738c907eb0ffee0a

 

3.5 android进程保活

关于 Android 进程保活,你所需要知道的一切
Android App 不死之路
Android 后台任务型App多进程架构演化

4 参考文章

http://developer.android.com/guide/topics/manifest/activity-element.html

https://developer.android.com/training/articles/memory.html#MultipleProcesses

http://android.stackexchange.com/questions/19987/what-actually-happens-when-you-swipe-an-app-out-of-the-recent-apps-list#_=_

《Linux内核设计与实现》读书笔记(三)- Linux的进程

《Linux内核设计与实现》读书笔记(四)- 进程的调度