17、并行启动优化:init.rc中并行服务启动,on property触发并行任务,SELinux策略对并行启动的影响。
各位同学,今天我们来聊聊并行启动优化。说实话,Android 系统启动慢,很多时候不是 CPU 不够快,而是我们让 CPU 闲着等别人。你想想看,如果所有服务都排着队一个一个来,那得多浪费时间?
我在做某款旗舰机启动优化时,就遇到过这个问题。当时测出来开机时间比竞品慢了 1.2 秒,查来查去,发现是 init.rc 里一堆服务串行启动了。嗯,这其实是个很常见的坑。
17.1 init.rc 中的并行服务启动
init.rc 是 Android 启动的核心脚本。它定义了各种服务、动作和触发条件。默认情况下,服务是按顺序启动的。但我们可以通过一些技巧让它们并行跑起来。
先看一个典型的串行启动例子:
service servicemanager /system/bin/servicemanager
class core
user system
group system
critical
service surfaceflinger /system/bin/surfaceflinger
class core
user system
group system graphics
critical
service media /system/bin/mediaserver
class main
user media
group media audio camera
这三个服务如果串行启动,servicemanager 跑完才跑 surfaceflinger,surfaceflinger 跑完才跑 media。但实际上,它们之间并没有严格的依赖关系。
怎么改成并行?很简单,用 class 和 trigger 配合:
on boot
class_start core
class_start main
on property:sys.boot_completed=1
class_start late_start
这里 class_start core 会启动所有 class 为 core 的服务。这些服务是并行启动的。同理 class_start main 也是并行启动 main 类的服务。
关键点: 同一 class 内的服务是并行启动的。不同 class 之间可以通过 trigger 控制启动时机。
我个人习惯把服务分成三类:
- core:系统核心服务,必须最先启动,比如 servicemanager、surfaceflinger
- main:主要服务,可以稍晚启动,比如 media、audioserver
- late_start:非关键服务,开机完成后启动,比如一些后台监控服务
这样分的好处是,core 类服务并行启动,main 类服务也并行启动,整体启动时间就压缩了。
17.2 on property 触发并行任务
除了 class 机制,我们还可以用 on property 来触发并行任务。这个机制非常灵活,说白了就是:当某个系统属性变成特定值时,触发一组动作。
举个例子:
on property:sys.boot_completed=1
start logd
start dumpsys
start bugreport
on property:persist.sys.usb.config=mtp
start mtp_server
start ptp_server
当 sys.boot_completed 变成 1 时,logd、dumpsys、bugreport 这三个服务会同时启动。它们之间没有依赖关系,所以并行跑没问题。
我在项目中遇到过一个问题:某个服务启动时需要读取一个配置文件,但这个配置文件是由另一个服务生成的。如果两个服务并行启动,就可能出现文件还没生成就读取的情况。嗯,这时候就需要用 on property 来同步了。
小技巧: 可以用 setprop 在服务启动完成后设置一个属性,然后另一个服务用 on property 等待这个属性。这样既实现了并行,又保证了依赖关系。
看这个例子:
service config_generator /system/bin/config_generator
class main
onrestart setprop sys.config.ready 1
on property:sys.config.ready=1
start config_consumer
config_generator 启动后,会在重启时设置 sys.config.ready=1。但这里有个问题:如果它第一次启动成功,就不会触发 onrestart。所以更好的做法是在服务内部主动设置属性。
实际上,更常见的做法是:
service config_generator /system/bin/config_generator
class main
on property:init.svc.config_generator=running
start config_consumer
当 config_generator 的状态变成 running 时,就启动 config_consumer。这样更可靠。
17.3 SELinux 策略对并行启动的影响
说到并行启动,就不得不提 SELinux。很多同学觉得 SELinux 只是安全模块,跟启动速度没关系。其实不然。SELinux 策略如果写得不好,会严重拖慢并行启动的效率。
为什么会这样?因为并行启动时,多个服务同时访问文件系统、设备节点、共享内存。如果 SELinux 策略没有正确配置,就会出现大量的 AVC 拒绝日志,甚至导致服务启动失败。
我记得有一次,某个服务在并行启动时总是崩溃。查了半天,发现是 SELinux 策略里少了一条 allow 规则。这个服务需要访问 /data/misc 下的一个文件,但策略只允许它访问 /data/system。串行启动时没问题,因为其他服务还没启动,文件还没被占用。但并行启动时,另一个服务先创建了文件,权限就变了。
注意: 并行启动时,SELinux 的 type_transition 规则可能会因为文件创建顺序不同而失效。一定要确保所有可能的路径都有对应的策略。
来看一个实际的 SELinux 策略例子:
# 允许 servicemanager 访问 binder 设备
allow servicemanager binder_device:chr_file rw_file_perms;
# 允许 surfaceflinger 访问 graphics 设备
allow surfaceflinger graphics_device:chr_file rw_file_perms;
# 允许 media 访问 audio 设备
allow media audio_device:chr_file rw_file_perms;
这些规则看起来没问题。但并行启动时,如果 servicemanager 和 surfaceflinger 同时访问 binder 设备,就可能出现竞争。这时候需要确保 binder 驱动本身是线程安全的,同时 SELinux 策略要允许并发访问。
我建议在调试并行启动时,打开 SELinux 的 permissive 模式:
adb shell setenforce 0
然后观察 logcat 中的 AVC 日志:
adb logcat -b events | grep avc
如果发现大量 AVC 拒绝,说明策略需要补充。等所有服务都能正常启动后,再切回 enforcing 模式,把缺失的策略补上。
17.4 并行启动的避坑指南
最后,我总结几个并行启动时容易踩的坑:
- 文件锁冲突:多个服务同时写同一个文件,会导致锁等待。解决办法是让每个服务写自己的文件,或者用临时文件。
- 共享内存竞争:如果两个服务通过共享内存通信,一定要用信号量或互斥锁保护。
- SELinux 策略遗漏:并行启动时,服务访问文件的顺序不确定,可能导致某些路径没有被策略覆盖。
- 属性设置时机:用
on property触发任务时,要确保属性设置不会因为竞争而丢失。
我曾经遇到过一个很隐蔽的问题:某个服务在启动时设置了 sys.service.ready=1,但另一个服务用 on property:sys.service.ready=1 等待。结果因为并行启动,第一个服务还没设置属性,第二个服务就已经检查过了,导致永远等不到。解决办法是用 init.svc.<service>=running 这种更可靠的触发条件。
好了,关于并行启动优化,今天就聊到这里。核心思路就是:能并行的尽量并行,但要注意依赖关系和 SELinux 策略。下一节我们会讲更具体的启动时间测量方法,到时候可以结合今天的内容一起实践。
一句话总结: 并行启动是缩短开机时间的利器,但要用好它,必须理解 init.rc 的 class 机制、on property 触发方式,以及 SELinux 策略对并发访问的影响。