操作系统里的 process的引入为何种植牙的不适用用于所有程序?

先帮朋友发个JD,感兴趣的朋友可以看看,刚刚上市的爱奇艺开发职位,还是热乎的~

爱奇艺APP基础架构工程师

去监控应用的卡顿。在收集到线上数据以后,发现一个比较怪异的现象,大量的卡顿的情况下,当前执行线程(主线程)的执行时间其实并不长,主线程只执行了几毫秒,但是却卡顿1s甚至更长的时间。很明显这个时候是由于主线程没有抢占到CPU导致,为了搞清楚为什么主线程没有抢到CPU,我把 Android 线程调度仔细撸了一遍。

进程是资源管理的最小单位,线程是程序执行的最小单位。在操作系统设计上,从进程演化出线程,最主要的目的就是更好的支持SMP以及减小(进程/线程)上下文切换开销。

无论按照怎样的分法,一个进程至少需要一个线程作为它的指令执行体,进程管理着资源(比如cpu、内存、文件等等),而将线程分配到某个cpu上执行。一个进程当然可以拥有多个线程,此时,如果进程运行在SMP机器上,它就可以同时使用多个cpu来执行各个线程,达到最大程度的并行,以提高效率;同时,即使是在单cpu的机器上,采用多线程模型来设计程序,正如当年采用多进程模型代替单进程模型一样,使设计更简洁、功能更完备,程序的执行效率也更高,例如采用多个线程响应多个输入,而此时多线程模型所实现的功能实际上也可以用多进程模型来实现,而与后者相比,线程的上下文切换开销就比进程要小多了,从语义上来说,同时响应多个输入这样的功能,实际上就是共享了除cpu以外的所有资源的。

针对线程模型的两大意义,分别开发出了核心级线程和用户级线程两种线程模型,分类的标准主要是线程的调度者在核内还是在核外。前者更利于并发使用多处理器的资源,而后者则更多考虑的是上下文切换开销。

需要理解 Linux 进程与 Android 线程的关系,需要先解释清楚 Linux 中内核线程、用户线程的关系,在 内核线程、轻量级进程、用户线程的区别和联系 中有比较清晰的阐述。可以总结为几点:

内核线程只运行在内核态,不受用户态上下文的拖累。

用户线程是完全建立在用户空间的线程库,用户线程的创建、调度、同步和销毁全由库函数在用户空间完成,不需要内核的帮助。因此这种线程是极其低消耗和高效的。

轻量级进程(LWP)是建立在内核之上并由内核支持的用户线程,它是内核线程的高度抽象,每一个轻量级进程都与一个特定的内核线程关联。内核线程只能由内核管理并像普通进程一样被调度。

LinuxThreads 是用户空间的线程库,所采用的是线程-进程 1对1 模型(即一个用户线程对应一个轻量级进程,而一个轻量级进程对应一个特定 的内核线程),将线程的调度等同于进程的调度,调度交由内核完成,而线程的创建、同步、销毁由核外线程库完成(LinuxThtreads已绑定到 GLIBC中发行)。

此外,Linux 内核不存在真正意义上的线程。Linux 将所有的执行实体都称之为任务(task),每一个任务在 Linux 上都类似于一个单线程的进程,具有内存空间、执行实体、文件资源等。但是,Linux 下不同任务之间可以选择公用内存空间,因而在实际意义上,共享同一个内存空间的多个任务构成了一个进程,而这些任务就成为这个进程里面的线程。

比如在 Android 上我们通过 adb shell进入手机后,可以通过 ps 命令查看某个应用下的所有线程,先通过 ps | grep $包名找到对应进程的进程号,然后执行 ps -t -p -P 6493

同时我们可以,执行 ls /proc/6493/tasks 查看该进程下的所有 tasks,他们之间有完整的对应关系:

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

决定哪些进程运行,哪些进程等待

决定每个进程运行多长时间

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

进程提供了两种优先级,一种是普通的进程优先级,第二个是实时优先级。前者适用 SCHED_NORMAL 调度策略,后者可选 SCHED_FIFOSCHED_RR 调度策略。任何时候,实时进程的优先级都高于普通进程,实时进程只会被更高级的实时进程抢占,同级实时进程之间是按照 FIFO(一次机会做完)或者 RR(多次轮转)规则调度的。

普通进程和实时进程分别用 nice 值和实时优先级(RTPRI)来度量优先级。

Linux 中,使用 nice 值来设定一个普通进程的优先级,系统任务调度器根据 nice 值合理安排调度。

nice 的值越大,进程的优先级就越低,获得 CPU 调用的机会越少,nice值越小,进程的优先级则越高,获得 CPU 调用的机会越多。

一个 nice 值为 -20 的进程优先级最高,nice 值为 19 的进程优先级最低。

父进程 fork 出来的子进程 nice 值与父进程相同。父进程 renice,子进程 nice 值不会随之改变。

实时优先级的范围是 0~99。

与 nice 值的定义相反,实时优先级是值越大优先级越高。

实时进程都是一些对响应时间要求比较高的进程,因此系统中有实时优先级高的进程处于运行队列的话,它们会抢占一般的进程的运行时间。

Linux 进程优先级与 nice 值及实时进程优先级的关系:

通过 ps -p 可以看到这几个值之间的对应关系:

除此之外,在执行阶段,调度程序通过增加或减少进程静态优先级的值,来达到奖励IO消耗型或惩罚cpu消耗型的进程,调整后的进程称为动态优先级。与之对应的我们前面提到的优先级的值被称为静态优先级。

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

举个例子说明调度原理的实现1。

这个例子很简单,主要是为了说明调度的原理,实际的调度算法虽然不会这么简单,

Linux上的调度算法是不断发展的,在2.6.23内核以后,采用了“完全公平调度算法”,简称CFS。

CFS算法的初衷就是让所有进程同时运行在一个CPU上,例如两个进程都需要运行10ms的时间,则CFS算法下,连个进程同时运行在CPU上,且时间为20ms,而不是每个进程分别运行10ms。但是这只是一种理想的运行方式,CFS为了近似这种运行算法,就提出了虚拟运行时间(vruntime)的概念。vruntime记录了一个可执行进程到当前时刻为止执行的总时间(需要以进程总数n进行归一化,并且根据进程的优先级进行加权)。根据vruntime的定义可以知道,vruntime越大,说明该进程运行的越久,所以被调度的可能性就越小。所以我们的调度算法就是每次选择 NICE_0_LOAD 是个定值,及系统默认的进程的权值;se.weight是当前进程的权重(优先级越高,权重越大);
delta 是当前进程运行的时间;我们可以得出这么个关系:vruntime 与delta 成正比,即当前运行时间越长 vruntime 增长越快
vruntime 与 se.weight 成反比,即权重越大 vunruntime 增长越慢。简单来说,一个进程的优先级越高,而且该进程运行的时间越少,则该进程的 vruntime 就越小,该进程被调度的可能性就越高。

的运行时间是有当前系统中所有可调度进程的优先级的比重来确定的,假如现在进程中有三个可调度进程A、B、C,它们的优先级分别为5,10,15,则它们的时间片分别为5/30,10/30,15/30。而不是由自己的时间片计算得来的,这样的话,优先级为1,2的两个进程与优先级为50,100的两个进程分的时间片是相同的。简单来说,CFS采用的所有进程优先级的比重来计算每个进程的时间片的,是相对的而不是绝对的。

Cgroups是control groups的缩写,是Linux内核提供的一种可以限制、记录、隔离进程组(process groups)所使用的物理资源(如:cpu,memory,IO等等)的机制。最初由google的工程师提出,后来被整合进Linux内核。也是目前轻量级虚拟化技术 lxc (linux container)的基础之一。

Cgroups最初的目标是为资源管理提供的一个统一的框架,既整合现有的cpuset等子系统,也为未来开发新的子系统提供接口。现在的cgroups适用于多种应用场景,从单个进程的资源控制,到实现操作系统层次的虚拟化(OS Level Virtualization)。Cgroups提供了以下功能:

限制进程组可以使用的资源数量(Resource limiting )。比如:memory子系统可以为进程组设定一个memory使用上限,一旦进程组使用的内存达到限额再申请内存,就会出发OOM(out of memory)。

进程组的优先级控制(Prioritization )。比如:可以使用cpu子系统为某个进程组分配特定cpu share。

记录进程组使用的资源数量(Accounting )。比如:可以使用cpuacct子系统记录某个进程组使用的cpu时间

进程组隔离(Isolation)。比如:使用ns子系统可以使不同的进程组使用不同的namespace,以达到隔离的目的,不同的进程组有各自的进程、网络、文件系统挂载空间。

进程组控制(Control)。比如:使用freezer子系统可以将进程组挂起和恢复。

任务(task)。在 cgroups 中,任务就是系统的一个进程。

控制族群(control group)。控制族群就是一组按照某种标准划分的进程。Cgroups 中的资源控制都是以控制族群为单位实现。一个进程可以加入到某个控制族群,也从一个进程组迁移到另一个控制族群。一个进程组的进程可以使用 cgroups 以控制族群为单位分配的资源,同时受到 cgroups 以控制族群为单位设定的限制。

层级(hierarchy)。控制族群可以组织成 hierarchical 的形式,既一颗控制族群树。控制族群树上的子节点控制族群是父节点控制族群的孩子,继承父控制族群的特定的属性。

子系统(subsytem)。一个子系统就是一个资源控制器,比如 cpu 子系统就是控制 cpu 时间分配的一个控制器。子系统必须附加(attach)到一个层级上才能起作用,一个子系统附加到某个层级以后,这个层级上的所有控制族群都受到这个子系统的控制。

Android 中在从设置进程优先级到最后映射到不同 cgroups 下的过程,有兴趣的可以参考 Android中关于cpu/cpuset/schedtune的应用 这篇文章。我们这里以 cpu 子系统为例介绍一下再 CPU 子系统下是如何控制不同 cgroup 对 CPU 资源的访问。

/,对应到 Android 的前台进程组。

在 cgroup 下定义了一些参数,来控制不同的 cgroup 在使用 cpu 资源时的配置:

cpu.shares:保存了整数值,用来设置 cgroup 分组任务获得 CPU 时间的相对值。

cpu.rt_period_us:主要是用来设置 cgroup 中的任务可以最长获得 CPU 资源的时间,单位为微秒。

通过下面的数据我们可以看到,前台进程组和后台进程组的 cpu.share 值相比接近于 20:1,也就是说前台进程组中的应用可以利用 95% 的 CPU,而处于后台进程组中的应用则只能获得 5% 的 CPU 利用率。

即单个逻辑CPU下每一秒内可以获得0.8秒的执行时间。

即单个逻辑CPU下每一秒内可以获得0.7秒的执行时间。

PS: 最长的获取CPU资源时间取决于逻辑CPU的数量。比如 设置为200000(0.2秒), 设置为1000000(1秒)。在单个逻辑CPU上的获得时间为每秒为0.2秒。 2个逻辑CPU,获得的时间则是0.4秒。

函数添加到对应的进程组的,调用这两个函数的传递的 SchedPolicy 定义在 sched_policy.h 中,定义不同的调度策略:

set_cpuset_policy 也有类似的逻辑,这里就不重复列举了,有兴趣的可以去看看源码。

在初始化方法中,可以看到对应不同的进程组和映射到不同的 cgroups 层级架构:

可以看到两组定义之间明确的对应关系:

至于这里的对应关系是怎么传递对接上的,会在后面进行解释。

Android 开发者应该都知道在系统中进程重要性的划分:

相信大家都很清楚,这里就不做过多的介绍了,不过对于进程重要性是通过哪些操作发生变更的,以及和我们前面讲的 Linux 进程分组又是怎么关联和映射上的,是下面要讲述的重点。

对于每一个运行中的进程,Linux 内核都通过 proc 文件系统暴露 /proc/[pid]/oom_score_adj 这样一个文件来允许其他程序修改指定进程的优先级,这个文件允许的值的范围是:-1000 ~ +1001之间。值越小,表示进程越重要。当内存非常紧张时,系统便会遍历所有进程,以确定哪个进程需要被杀死以回收内存,此时便会读取 这个文件的值。

PS:在Linux 2.6.36之前的版本中,Linux 提供调整优先级的文件是 /proc/[pid]/oom_adj 。这个文件允许的值的范围是-17 ~ +15之间。数值越小表示进程越重要。 这个文件在新版的 Linux 中已经废弃。但你仍然可以使用这个文件,当你修改这个文件的时候,内核会直接进行换算,将结果反映到 这个文件上。
Android早期版本的实现中也是依赖 oom_adj 这个文件。但是在新版本中,已经切换到使用 这个文件。

为了便于管理,ProcessList.java中预定义了 的可能取值,这里的预定义值也是对应用进程的一种分类。

Lowmemorykiller 根据当前可用内存情况来进行进程释放,总设计了6个级别,即上表中“解释列”加粗的行,即 Lowmemorykiller 的杀进程的6档,如下:

系统内存从很宽裕到不足,Lowmemorykiller 也会相应地从 (第1档)开始杀进程,如果内存还不足,那么会杀 (第2档),不断深入,直到满足内存阈值条件。

在 ProcessRecord 中,记录了和进程状态相关的属性:

对应到底层进程分组,除了上面提到的 定义的不同线程组的定义,同时还为 Activity manager 定义了一套类似的调度分组,和之前的线程分组定义也存在对应关系:

在 ProcessRecord 中,也记录了和调度组相关的属性:

我们知道影响 Android 应用进程优先级变化的是根据 Android
应用组件的生命周期变化相关。Android进程调度之adj算法 里面罗列了所有会触发进程状态发生变化的事件,主要包括:

方法负责计算进程的优先级,总计约700行,执行流程比较清晰,步骤如下,由于代码有点多这里就不贴了,想仔细研究的可以比着系统源码看:

空进程中没有任何组件,因此主线程也为null(ProcessRecord.thread描述了应用进程的主线程)。

系统进程或者Persistent进程会通过设置maxAdj来保持其较高的优先级,对于这类进程不用按照普通进程的算法进行计算,直接按照maxAdj的值设置即可,curSchedGroup 设置为THREAD_GROUP_DEFAULT 进程调度组。

PS:Instrumentation 应用是辅助测试用的,正常运行的系统中不用考虑这种应用。

遍历进程中的所有Activity,找出其中优先级最高的设置为进程的优先级。

特殊类型的进程包括:重量级进程,桌面进程,前一个应用进程,正在执行备份的进程。

重量级进程是指那些通过Manifest指明不能保存状态的应用进程;

“前一个应用”是指:在启动新的Activity时,如果新启动的Activity是属于一个新的进程的,那么当前即将被stop的Activity所在的进程便会成为“前一个应用”进程;

备份进程,进程是否正在进行备份。

的状态下遍历所有的Service,并且还需要遍历每一个Service的所有连接。然后根据连接的关系确认客户端进程的优先级来确定当前进程的优先级。

这里详细记录了在 bindService 过程中,传递的不同的 FLAG 对于 Service 进程和 Client 进程关联计算 adj 级别。由于涉及的分支判断较多,如果想要仔细研究,最好对着代码一一查看。这里只介绍整个过程中涉及到进程调度组发生的变化:

来分别设置当前进程的调度策略。

关于整计算过程,可以参考 Android进程调度之adj算法 里面的总结,不过根据不同的系统版本,会有稍许差异:

当不存在显示的 ui,且 service 上次活动时间距离现在超过30分钟,则只将当前进程的 adj 值赋予给 client 进程

当绑定的是前台进程的情况

的条件下进行两次循环遍,其中涉及到进程调度组发生变更的情况:

完整的 adj 的计算过程,依然请参考 Android进程调度之adj算法 或者源码:

Android 线程优先级的变化分为两种,一种是根据上面计算的进程优先级的变化,给 Android 线程带来的变化,另一种是开发者可以在代码中手动改变线程的优先级。

手动设置 Java 线程优先级

我们都知道,在利用 Thread 创建线程或者用 ThreadPoolExecutor 创建线程的时候,我们可以为当前设置的线程设置优先级 setPriority。这个优先级并不是我们之前讲到的 Nice 值,Java 的优先级分为 10 个等级,取值从 1 到 10,根据取值的大小,优先级越来越高,一般 Android 线程默认启动设置的优先级为

虽然 Java 的优先级和 Nice 值不一样,但是它们之间同样存在一定的对应关系,当我们在 Java 层设置优先级的时候,同样会导致 Linux 对应轻量级进程的 Nice 值的变化,它们的对应关系,我们可以在 thread_android.cc 中找到它们之间的对应关系:

可以看到它们的对应关系:

不过需要特别说明的一点是,当我们通过 Process 进行线程优先级设置的以后,并不会改变 Thread 对象里面优先级的值,这从某种角度上来说,是系统的一个 bug。

THREAD_PRIORITY_BACKGROUND 的优先级,所以为了保证主线程能够拥有较为优先的执行级别,建议在创建异步线程的过程中注意对优先级的控制。

除了开发者手动为线程设置的优先级意外,根据我们上面对 Android 进程变化的分析,可以知道,在程序运行过程中,随着应用状态的变化,Android 进程的调度策略会发生变化,接下来我们继续分析进程调度策略的变化如果改变进程的优先级(也就是主线程的优先级)和其他线程的优先级的。

在前面计算完进程的优先级后,会通过 方法将对应的优先级、adj、进程状态等值应用到进程上,我们注重关注其中关于进程优先级设置的部分。整个执行的过程可以大概总结为:

其中调度组和进程组的映射关系:

通过设置进程组,改变了进程所在 cgroup,

通过设置调度策略实现主线程在实时优先级和普通优先级的切换,

通过设置优先级改变进程 nice 值,同时在底层会改变进程所在的 cgroup。

由于这段代码不是很长,我们也可以看看代码。

到这里我们已经清晰的了解到进程在应用状态变化后,都发生了哪些优先级的变化,接下来还有一个疑团,就是其他线程的优先级的变化,根据观察我们发现,除了主线程的优先级会发生变化,其他子线程在创建以后,除非开发者手动修改其优先级,否则子线程的优先级并不会发生变化。但是在应用状态发生变化的时候,子线程其所在的进程组合主线程(也就是应用进程)是保持一致的,这是由于我们在设置进程组的时候,会遍历当前进程下所有的

在 Android 应用状态发生变化以后,会导致进程的 、procStateschedGroup 等进程状态的重新计算和设置,从而改变进程的优先级和调度策略,帮助系统进行更合理资源分配和资源回收。

Android 中的线程对应到 Linux 的内核中的轻量级进程,所以 Linux 为其分配资源适用 Linux 进程调度策略。其中主线程等同于应用进程的优先级,一般由 Android 系统根据应用状态的变化自行调控,不建议开发者手动设置,不过我们为应用中各子线程设置的优先级,将直接影响到主线程在抢占各种系统资源尤其是 CPU 资源时候的优先级,所以为了保证主线程执行的顺畅,我们应尽量控制子线程的优先级。

经过一些调试以后,我们发现应用在启动 1-2s 以后,主线程的优先级就从 TOP_APP_PRIORITY_BOOST(-10) 降为 THREAD_PRIORITY_BACKGROUND(10) 后台进程的优先级,这直接导致主线程在大多数情况下的优先级是低于其他线程的,从而在抢占 CPU 资源时处于劣势。根据之前对于 Android 线程调度分析,可以排除是系统降低的可能,同时我们对比了其他应用,发现所有其他应用当处于前台的时候,主线程的优先级都是 ,这进一步加强了对于业务代码误操作导致主线程降低的推断,最后我们通过对 Process.setThreadPriority(priority) 调用的排查,发现的确有一个地方不小心为主线程设置了 的优先级。

可以预测,如果不是这次对于卡顿栈的分析,我们不能确定我们还有多久才能发现这个已经存在很久的 bug,我们依然会这样一个小小的失误而承担巨大的成本,因为在后台线程本身就很多主线程的优先级得不到保障的情况下,应用的卡顿是不可避免的,而且可能做再多其他方面的优化,也于事无补,性能检测和监控的价值就在这里,虽然不能马上让应用有质的飞跃,但一点一滴的优化,我们的应用会变得越来越流畅。

Linux 线程实现机制分析

内核线程、轻量级进程、用户线程的区别和联系

Android 多线程系统概述及与Linux系统的关系

内核线程与用户线程的一点小总结 《程序员的自我修养》

深入了解Android系统-进程优先级

cgroups介绍及安装配置使用详解

Android系统中的进程管理:进程的优先级

特别声明:本文为网易自媒体平台“网易号”作者上传并发布,仅代表该作者观点。网易仅提供信息发布平台。

}

我要回帖

更多关于 种植牙的不适用 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信