深入Android系统(六)第一个用户进程-Init进程
十一假期有点堕落,无限火力有点上瘾,谨戒、谨戒Init进程是Linux 内核启动后创建的第一个用户进程,地位非常重要。Init进程在初始化过程中会启动很多重要的守护进程,因此,了解Init进程的启动过程有助于我们更好的理解Android系统。在介绍Init进程前,我们先简单介绍下Android的启动过程。从系统角度看,Android的启动过程可分为3个大的阶段:bootloader引导装载和启动L
十一假期有点堕落,无限火力有点上瘾,谨戒、谨戒
Init进程
是Linux 内核
启动后创建的第一个用户进程
,地位非常重要。
Init进程
在初始化过程中会启动很多重要的守护进程,因此,了解Init进程
的启动过程有助于我们更好的理解Android
系统。
在介绍Init进程
前,我们先简单介绍下Android
的启动过程。从系统角度看,Android
的启动过程可分为3个大的阶段:
bootloader引导
装载和启动Linux内核
启动Android系统
,可分为- 启动
Init进程
- 启动
Zygote
- 启动
SystemService
- 启动
SystemServer
- 启动
Home
- 等等…
- 启动
我们看下启动过程图:
下面简单介绍下启动过程:
Bootloader
引导
当按下电源键开机时,最先运行的就是
Bootloader
Bootloader
的主要作用是初始化基本的硬件设备(如 CPU、内存、Flash等)并且建立内存空间映射,为装载Linux内核
准备好合适的运行环境。- 一旦
Linux内核
装载完毕,Bootloader
将会从内存中清除掉 - 如果在
Bootloader
运行期间,按下预定义的的组合键,可以进入系统的更新模块。Android
的下载更新可以选择进入Fastboot模式
或者Recovery模式
:Fastboot
是Android
设计的一套通过USB
来更新Android
分区映像的协议,方便开发人员快速更新指定分区。Recovery
是Android
特有的升级系统。利用Recovery
模式可以进行恢复出厂设置,或者执行OTA、补丁和固件升级。进入Recovery
模式实际上是启动了一个文本模式的Linux
- 装载和启动
Linux
内核
Android 的
boot.img
存放的就是Linux
内核和一个根文件系统
Bootloader
会把boot.img
映像装载进内存- 然后
Linux
内核会执行整个系统的初始化 - 然后装载
根文件系统
- 最后启动
Init
进程
- 启动
Init
进程
Linux
内核加载完毕后,会首先启动Init
进程,Init
进程是系统的第一个进程
Init
进程启动过程中,会解析Linux
的配置脚本init.rc
文件。根据init.rc
文件的内容,Init
进程会:- 装载
Android
的文件系统 - 创建系统目录
- 初始化属性系统
- 启动
Android
系统重要的守护进程,像USB守护进程
、adb守护进程
、vold守护进程
、rild守护进程
等
- 装载
- 最后,
Init
进程也会作为守护进程来执行修改属性请求,重启崩溃的进程等操作
- 启动
ServiceManager
ServiceManager
由Init
进程启动。在Binder
章节已经讲过,它的主要作用是管理Binder服务
,负责Binder服务
的注册与查找
- 启动
Zygote
进程
Init
进程初始化结束时,会启动Zygote
进程。Zygote
进程负责fork
出应用进程,是所有应用进程的父进程
Zygote
进程初始化时会创建Android 虚拟机
、预装载系统的资源文件和Java
类- 所有从
Zygote
进程fork
出的用户进程都将继承和共享这些预加载的资源,不用浪费时间重新加载,加快的应用程序的启动过程 - 启动结束后,
Zygote
进程也将变为守护进程,负责响应启动APK
的请求
- 启动
SystemServer
SystemServer
是Zygote
进程fork
出的第一个进程,也是整个Android
系统的核心进程
SystemServer
中运行着Android
系统大部分的Binder服务
SystemServer
首先启动本地服务SensorManager
- 接着启动包括
ActivityManagerService
、WindowsManagerService
、PackageManagerService
在内的所有Java服务
- 启动
MediaServer
MediaServer
由Init
进程启动。它包含了一些多媒体相关的本地Binder服务
,包括:CameraService
、AudioFlingerService
、MediaPlayerService
、AudioPolicyService
- 启动
Launcher
SystemServer
加载完所有的Java服务
后,最后会调用ActivityManagerService
的SystemReady()
方法- 在
SystemReady()
方法中,会发出Intent<android.intent,category.HOME>
- 凡是响应这个
Intent
的apk
都会运行起来,一般Launcher
应用才回去响应这个Intent
Init
进程的初始化过程
Init
进程的源码目录在system/core/init
下。程序的入口函数main()
位于文件init.c
中
main()
函数的流程
书中使用的是
Android 5.0
源码,相比Android 9.0
这部分已经有很多改动,不过大的方向是一致的,只能对比着学习了。。。
main()
函数比较长,整个Init
进程的启动流程都在这个函数中。由于涉及的点比较多,这里我们先了解整体流程,细节后面补充,一点一点来哈
Init
进程的main()
函数的结构是这样的:
int main(int argc, char** argv) {
//启动参数判断部分
if (is_first_stage) {
//初始化第一阶段部分
}
//初始化第二阶段部分
while (true) {
//一个无限循环部分
}
启动程序参数判断
进入main()
函数后,首先检查启动程序的文件名
函数源码:
if (!strcmp(basename(argv[0]), "ueventd")) {
return ueventd_main(argc, argv);
}
if (!strcmp(basename(argv[0]), "watchdogd")) {
return watchdogd_main(argc, argv);
}
- 如果文件名是
ueventd
,执行ueventd
守护进程的主函数ueventd_main
- 如果文件名是
watchdogd
,执行watchdogd
守护进程的主函数watchdogd_main
- 都不是,则继续执行
才开始是不是就已经有些奇怪了,Init
进程中还包含了另外两个守护进程的启动,这主要是因为这几个守护进程的代码重合度高,开发人员干脆都放在一起了。
我们看一下Android.mk
中的片段:
# Create symlinks.
LOCAL_POST_INSTALL_CMD := $(hide) mkdir -p $(TARGET_ROOT_OUT)/sbin; \
ln -sf ../init $(TARGET_ROOT_OUT)/sbin/ueventd; \
ln -sf ../init $(TARGET_ROOT_OUT)/sbin/watchdogd
- 在编译时,
Android
生成了两个指向init
文件的符号链接ueventd
和watchdogd
- 这样,启动时如果执行的是这两个符号链接,
main()
函数就可以根据名称判断到底启动哪一个
初始化的第一阶段
设置文件属性掩码
函数源码:
// Clear the umask.
umask(0);
默认情况下一个进程创建出的文件合文件夹的属性是022
,使用umask(0)
意味着进程创建的属性是0777
mount
相应的文件系统
函数源码:
// 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);
mkdir("/dev/socket", 0755);
mount("devpts", "/dev/pts", "devpts", 0, NULL);
#define MAKE_STR(x) __STRING(x)
mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC));
//......
mount("sysfs", "/sys", "sysfs", 0, NULL);
mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL);
//......
// Mount staging areas for devices managed by vold
// See storage config details at http://source.android.com/devices/storage/
mount("tmpfs", "/mnt", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
"mode=0755,uid=0,gid=1000");
// /mnt/vendor is used to mount vendor-specific partitions that can not be
// part of the vendor partition, e.g. because they are mounted read-write.
mkdir("/mnt/vendor", 0755);
InitKernelLogging(argv);
创建一些基本的目录,包括/dev
、/proc
、/sys
等,同时把一些文件系统,如tmpfs
、devpt
、proc
、sysfs
等mount
到相应的目录
tmpfs
是一种基于内存的文件系统,mount
后就可以使用。tmpfs
文件系统下的文件都放在内存中,访问速度快,但是掉电丢失。因此适合存放一些临时性的文件tmpfs
文件系统的大小是动态变化的,刚开始占用空间很小,随着文件的增多会随之变大Android
将tmpfs
文件系统mount
到/dev
目录。/dev
目录用来存放系统创造的设备节点
devpts
是虚拟终端文件系统,通常mount
到/dev/pts
目录下proc
也是一种基于内存的虚拟文件系统,它可以看作是内核内部数据结构的接口- 通过它可以获得系统的信息
- 同时能够在运行时修改特定的内核参数
sysfs
文件系统和proc
文件系统类似,它是在Linux 2.6
内核引入的,作用是把系统设备和总线按层次组织起来,使得他们可以在用户空间存取
初始化kernel
的Log
系统
通过InitKernelLogging()
函数进行初始化,由于此时Android
的log系统
还没有启动,所以Init
只能使用kernel
的log系统
// Now that tmpfs is mounted on /dev and we have /dev/kmsg, we can actually
// talk to the outside world...
InitKernelLogging(argv);
初始化SELinux
// Set up SELinux, loading the SELinux policy.
SelinuxSetupKernelLogging();
SelinuxInitialize();
SELinux
是在Android 4.3
加入的安全内核,后面详细介绍
初始化的第二阶段
创建.booting
空文件
在/dev
目录下创建一个空文件.booting
表示初始化正在进行
// At this point we're in the second stage of init.
InitKernelLogging(argv);
LOG(INFO) << "init second stage started!";
//......
// Indicate that booting is in progress to background fw loaders, etc.
close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));
大家留心注释,我们已经处于初始化的第二阶段了
is_booting()
函数会依靠空文件.booting
来判断是否进程处于初始化中- 初始化结束后这个文件将被删除
初始化Android
的属性系统
property_init();
property_init()
函数主要作用是创建一个共享区域来储存属性值,后面会详细介绍
解析kernel
参数并进行相关设置
// If arguments are passed both on the command line and in DT,
// properties set in DT always have priority over the command-line ones.
process_kernel_dt();
process_kernel_cmdline();
// Propagate the kernel variables to internal variables
// used by init as well as the current required properties.
export_kernel_boot_props();
// Make the time that init started available for bootstat to log.
property_set("ro.boottime.init", getenv("INIT_STARTED_AT"));
property_set("ro.boottime.init.selinux", getenv("INIT_SELINUX_TOOK"));
// Set libavb version for Framework-only OTA match in Treble build.
const char* avb_version = getenv("INIT_AVB_VERSION");
if (avb_version) property_set("ro.boot.avb_version", avb_version);
这部分进行的是属性的设置,我们看下几个重点方法:
process_kernel_dt()
函数:读取设备树(DT
)上的属性设置信息,查找系统属性,然后通过property_set
设置系统属性process_kernel_cmdline()
函数:解析kernel
的cmdline
文件提取以androidboot.
字符串打头的字符串,通过property_set
设置该系统属性export_kernel_boot_props()
函数:额外设置一些属性,这个函数中定义了一个集合,集合中定义的属性都会从kernel
中读取并记录下来
进行第二阶段的SELinux
设置
进行第二阶段的SELinux
设置并恢复一些文件安全上下文
// Now set up SELinux for second stage.
SelinuxSetupKernelLogging();
SelabelInitialize();
SelinuxRestoreContext();
初始化子进程终止信号处理函数
epoll_fd = epoll_create1(EPOLL_CLOEXEC);
if (epoll_fd == -1) {
PLOG(FATAL) << "epoll_create1 failed";
}
sigchld_handler_init();
在
linux
当中,父进程是通过捕捉SIGCHLD
信号来得知子进程运行结束的情况,SIGCHLD
信号会在子进程终止的时候发出。
- 为了防止
init
的子进程成为僵尸进程
(zombie process),init
在子进程结束时获取子进程的结束码 - 通过结束码将程序表中的子进程移除,防止成为僵尸进程的子进程占用程序表的空间
- 程序表的空间达到上限时,系统就不能再启动新的进程了,这样会引起严重的系统问题
设置系统属性并开启属性服务
property_load_boot_defaults();
export_oem_lock_status();
start_property_service();
set_usb_controller();
property_load_boot_defaults()
、export_oem_lock_status()
、set_usb_controller()
这三个函数都是调用、设置一些系统属性start_property_service()
:开启系统属性服务
加载init.rc
文件
init.rc
是一个可配置的初始化文件,在Android
中被用作程序的启动脚本,它是run commands
运行命令的缩写
通常第三方定制厂商可以配置额外的初始化配置:
init.%PRODUCT%.rc
。在init
的初始化过程中会解析该配置文件,完成定制化的配置过程。
init.rc
文件的规则和具体解析逻辑后面详解,先看下它在main
函数中的相关流程。
函数代码:
const BuiltinFunctionMap function_map;
Action::set_function_map(&function_map);
subcontexts = InitializeSubcontexts();
// 创建 action 相关对象
ActionManager& am = ActionManager::GetInstance();
// 创建 service 相关对象
ServiceList& sm = ServiceList::GetInstance();
// 加载并解析init.rc文件到对应的对象中
LoadBootScripts(am, sm);
解析完成后,会执行
am.QueueEventTrigger("early-init");
// Queue an action that waits for coldboot done so we know ueventd has set up all of /dev...
// 等待冷插拔设备初始化完成
am.QueueBuiltinAction(wait_for_coldboot_done_action, "wait_for_coldboot_done");
// ... so that we can start queuing up actions that require stuff from /dev.
am.QueueBuiltinAction(MixHwrngIntoLinuxRngAction, "MixHwrngIntoLinuxRng");
am.QueueBuiltinAction(SetMmapRndBitsAction, "SetMmapRndBits");
am.QueueBuiltinAction(SetKptrRestrictAction, "SetKptrRestrict");
// 初始化组合键监听模块
am.QueueBuiltinAction(keychord_init_action, "keychord_init");
// 在屏幕上显示 Android 字样的Logo
am.QueueBuiltinAction(console_init_action, "console_init");
// Trigger all the boot actions to get us started.
am.QueueEventTrigger("init");
// Starting the BoringSSL self test, for NIAP certification compliance.
am.QueueBuiltinAction(StartBoringSslSelfTest, "StartBoringSslSelfTest");
// Repeat mix_hwrng_into_linux_rng in case /dev/hw_random or /dev/random
// wasn't ready immediately after wait_for_coldboot_done
am.QueueBuiltinAction(MixHwrngIntoLinuxRngAction, "MixHwrngIntoLinuxRng");
// Don't mount filesystems or start core system services in charger mode.
std::string bootmode = GetProperty("ro.bootmode", "");
if (bootmode == "charger") {
am.QueueEventTrigger("charger");
} else {
am.QueueEventTrigger("late-init");
}
// Run all property triggers based on current state of the properties.
am.QueueBuiltinAction(queue_property_triggers_action, "queue_property_triggers");
am.QueueEventTrigger
函数即表明到达了某个所需的某个时间条件- 如
am.QueueEventTrigger("early-init")
表明early-init
条件触发,对应的动作可以开始执行 - 需要注意的是这个函数只是将时间点(如:
early-init
)填充进event_queue_
运行队列 - 后面的
while(true)
循环才会真正的去按顺序取出,并触发相应的操作
- 如
到这里,
init.rc
相关的action
和service
已经解析完成- 对应的列表也已经准备就绪
- 对应的
Trigger
也已经添加完成
接下来就是执行阶段了:
while (true) {
// 执行命令列表中的 Action
if (!(waiting_for_prop || Service::is_exec_service_running())) {
am.ExecuteOneCommand();
}
if (!(waiting_for_prop || Service::is_exec_service_running())) {
if (!shutting_down) {
// 启动服务列表中的服务
auto next_process_restart_time = RestartProcesses();
//......
}
//......
}
// 监听子进程的死亡通知信号
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)();
}
}
到这里,main
函数的整体流程就分析完了。分析过程中我们舍弃了很多细节,接下来就是填补细节的时候了。
启动Service
进程
在main
函数的while()
循环中会调用RestartProcesses()
来启动服务列表中的服务进程,我们看下函数源码:
static std::optional<boot_clock::time_point> RestartProcesses() {
//......
//循环检查每个服务
for (const auto& s : ServiceList::GetInstance()) {
// 判断标志位是否为 SVC_RESTARTING
if (!(s->flags() & SVC_RESTARTING)) continue;
// ......省略时间相关的判断
// 启动服务进程
auto result = s->Start();
}
//......
}
RestartProcesses()
会检查每个服务:凡是带有SVC_RESTARTING
标志的,才会执行服务的启动s->Start();
其实重点在s->Start();
方法,我们具体来看下(删减版):
Result<Success> Service::Start() {
//......省略部分
// 清空service相关标记
// 判断service状态,如果已运行直接返回
// 判断service二进制文件是否存在
// 初始化console、scon(安全上下文)等
//......省略部分
pid_t pid = -1;
if (namespace_flags_) {// 当service定义了namespace时会赋值为CLONE_NEWPID|CLONE_NEWNS
pid = clone(nullptr, nullptr, namespace_flags_ | SIGCHLD, nullptr);
} else {
pid = fork();
}
if (pid == 0) {// 子进程创建成功
//......省略部分
// setenv、writepid、重定向标准IO
//......省略部分
// As requested, set our gid, supplemental gids, uid, context, and
// priority. Aborts on failure.
SetProcessAttributes();
if (!ExpandArgsAndExecv(args_)) {// 解析参数并启动service
PLOG(ERROR) << "cannot execve('" << args_[0] << "')";
}
// 不太懂这里退出的目的是干啥
_exit(127);
}
if (pid < 0) {
// 子进程创建失败
pid_ = 0;
return ErrnoError() << "Failed to fork";
}
//......省略部分
// 执行service其他参数的设置,如oom_score_adj、创建并设置ProcessGroup相关的参数
//......省略部分
NotifyStateChange("running");
return Success();
}
Service
进程的启动流程还有很多细节,这部分只是简单介绍下流程,涉及的scon
、PID
、SID
、PGID
等东西还很多。
偷懒啦!能力、时间有限,先往下学习,待用到时再来啃吧
解析启动脚本init.rc
Init
进程启动时最重要的工作就是解析并执行启动文件init.rc
。官方说明文档下载链接
init.rc
文件格式
init.rc
文件是以section
为单位组织的
-
section
分为两大类:action
:以关键字on
开始,表示一堆命令的集合service
:以关键字service
开始,表示启动某个进程的方式和参数
-
section
以关键字on
或service
开始,直到下一个on
或service
结束 -
section
中的注释以#
开始
打个样儿:
import /init.usb.rc
import /init.${ro.hardware}.rc
on early-init
mkdir /dev/memcg/system 0550 system system
start ueventd
on init
symlink /system/bin /bin
symlink /system/etc /etc
on nonencrypted
class_start main
class_start late_start
on property:sys.boot_from_charger_mode=1
class_stop charger
trigger late-init
service ueventd /sbin/ueventd
class core
critical
seclabel u:r:ueventd:s0
shutdown critical
service flash_recovery /system/bin/install-recovery.sh
class main
oneshot
service ueventd /sbin/ueventd
class core
critical
seclabel u:r:ueventd:s0
shutdown critical
无论是action
还是service
,并不是按照文件中的书写顺序执行的,执行与否以及何时执行要由Init
进程在运行时决定。
对于init.rc
的action
:
- 关键字
on
后面跟的字符串称为trigger
,如上面的early-init
、init
等。 trigger
后面是命令列表,命令列表中的每一行就是一条命令。
对于init.rc
的service
:
- 关键字
service
后面是服务名称
,可以使用start
加服务名称
来启动一个服务。如start ueventd
服务名称
后面是进程的可执行文件的路径和启动参数service
下面的行称为option
,每个option
占一行- 例如:
class main
中的class
表示服务所属的类别,可以通过class_start
来启动一组服务,像class_start main
- 例如:
想要了解更多,可以参考源码中的README
文档,路径是system/core/init/README.md
init.rc
的关键字
这部分是对
system/core/init/README.md
文档的整理,挑重点记录哈
Android
的rc
脚本包含了4中类型的声明:Action
、Commands
、Services
、Options
- 所有的指令都以行为单位,各种符号则由空格隔开。
c语言
风格的反斜杠\
可用于在符号间插入空格- 双引号
""
可用于防止字符串被空格分割成多个记号 - 行末的反斜杠
\
可用于折行 - 注释以
#
开头 Action
和Services
用来申明一个分组- 所有的
Commands
和Options
都属于最近声明的分组 - 位于第一个分组之前的
Commands
或Options
将被忽略
- 所有的
Actions
Actions
是一组Commands
的集合。每个Action
都有一个trigger
用来决定何时执行。当触发条件与Action
的trigger
匹配一致时,此Action
会被加入到执行队列的尾部
每个Action
都会依次从队列中取出,此Action
的每个Command
都将依次执行。
Actions
格式如下:
on < trigger >
< command >
< command >
< command >
Services
通过Services
定义的程序,会在Init中启动,如果退出了会被重启。
Services
的格式如下:
service <name> <pathname> [ <argument> ]*
<option>
<option>
...
Options
Options
是Services
的修订项。它们决定了一个Service
何时以及如何运行。
critical
:表示这是一个关键的Service
,如果Service
4分钟内重新启动超过4次,系统将自动重启并进入recovery
模式console [<console>]
:表示该服务需要一个控制台- 第二个参数用来指定特定控制台名称,默认为
/dev/console
- 指定时需省略掉
/dev/
部分,如/dev/tty0
需写成console tty0
- 第二个参数用来指定特定控制台名称,默认为
disabled
:表示服务不会通过class_start
启动。它必须以命令start service_name
的方式指定来启动setenv <name> <value>
:在Service
启动时将环境变量name
设置为value
socket <name> <type> <perm> [ <user> [ <group> [ <seclabel> ] ] ]
:创建一个名为/dev/socket/<name>
的套接字,并把文件描述符传递给要启动的进程type
的值必须是dgram
、stream
、seqpacket
user
和group
默认为0seclabel
是这个socket
的SElinux
安全上下文,默认为当前service
的上下文
user <username>
:在执行此服务之前切换用户名,默认的是root。如果进程没有相应的权限,则不能使用该命令oneshot
:Service
退出后不再重启class <name>
:给Service
指定一个名字。所有同名字的服务可以同时启动和停止。如果不通过class
显示指定,默认为default
onrestart
:当Service
重启时,执行一条命令
还有很多哈,就不一一介绍了,像shutdown <shutdown_behavior>
这种,参照官方说明就好啦
Triggers
trigger
本质上是一个字符串,能够匹配某种包含该字符串的事件。trigger
又被细分为事件触发器(event trigger)
和属性触发器(property trigger)
-
事件触发器
可由trigger
命令或初始化过程中通过QueueEventTrigger()
触发- 通常是一些事先定义的简单字符串,例如:
boot
,late-init
等
- 通常是一些事先定义的简单字符串,例如:
-
属性触发器
是当指定属性的变量值变成指定值时触发- 其格式为
property:<name>=*
- 其格式为
请注意,一个Action
可以有多个属性触发器
,但是最多有一个事件触发器
。看下官方的例子:
on boot && property:a=b
上面的Action
只有在boot
事件发生时,并且属性a
和数值b
相等的情况下才会被触发。
而对于下面的Action
on property:a=b && property:c=d
存在三种触发情况:
- 在启动时,如果
属性a
的值等于b
并且属性c
的值等于d
- 在
属性c
的值已经是d
的情况下,属性a
的值被更新为b
- 在
属性a
的值已经是b
的情况下,属性c
的值被更新为d
对于事件触发器
,大体包括:
类型 | 说明 |
---|---|
boot | init.rc被装载后触发 |
device-added-<path> | 指定设备被添加时触发 |
device-removed-<path> | 指定设备被移除时触发 |
service-exited-<name> | 在特定服务退出时触发 |
early-init | 初始化之前触发 |
late-init | 初始化之后触发 |
init | 初始化时触发 |
Commands
Command
是用于Action
的命令列表或者Service
的Option<onrestart>
中,在源码中是这样的:
static const Map builtin_functions = {
{"chmod", {2, 2, {true, do_chmod}}},
{"chown", {2, 3, {true, do_chown}}},
{"class_start", {1, 1, {false, do_class_start}}},
{"class_stop", {1, 1, {false, do_class_stop}}},
......
};
看几个常用的吧
bootchart [start|stop]
:开启或关闭进程启动时间记录工具
//init.rc file
mkdir /data/bootchart 0755 shell shell
bootchart start
- 在
Init进程
中会启动bootchart
,默认不会执行时间采集 - 当我们需要采集启动时间时,需创建一个
/data/bootchart/enabled
文件
chmod <octal-mode> <path>
:更改文件权限
chmode 0755 /metadata/keystone
chown <owner> <group> <path>
:更改文件的所有者和组
chown system system /metadata/keystone
mkdir <path> [mode] [owner] [group]
:创建指定目录
mkdir /data/bootchart 0755 shell shell
trigger <event>
:触发某个事件(Action)
,用于将该事件
排在某个事件
之后
on late-init
trigger early-fs
trigger fs
trigger post-fs
trigger late-fs
trigger post-fs-data
trigger zygote-start
trigger load_persist_props_action
trigger early-boot
trigger boot
class_start <serviceclass>
:启动所有指定服务class
下的未运行服务
class_start main
class_start late_start
class_stop <serviceclass>
:停止所有指定服务class
下的已运行服务
class_stop charger
exec [ <seclabel> [ <user> [ <group>\* ] ] ] -- <command> [ <argument>\* ]
:通过给定的参数fork
和启动一个命令。
- 具体的命令在
--
后开始 - 参数包括
seclable
(默认的话使用-
)、user
、group
exec
为阻塞式,在当前命令完成前,不会运行其它命令,此时Init
进程暂停执行。
exec - system system -- /system/bin/tzdatacheck /system/usr/share/zoneinfo /data/misc/zoneinfo
还有很多指令就不一一介绍了,参考官方文档和源码就好啦
init
脚本的解析
上面我们知道了,在init.cpp
的main
函数中通过LoadBootScripts()
来加载rc
脚本,我们简单看下解析流程(注释比较详细啦)
/**
* 7.0后,init.rc进行了拆分,每个服务都有自己的rc文件
* 他们基本上都被加载到/system/etc/init,/vendor/etc/init, /odm/etc/init等目录
* 等init.rc解析完成后,会来解析这些目录中的rc文件,用来执行相关的动作
* ===============================================================================
* 对于自定义的服务来说,我们只需要通过 LOCAL_INIT_RC 指定自己的rc文件即可
* 编译时会根据分区标签将rc文件拷贝到指定的partition/etc/init目录
*/
static void LoadBootScripts(ActionManager& action_manager, ServiceList& service_list) {
// 创建解析器
Parser parser = CreateParser(action_manager, service_list);
std::string bootscript = GetProperty("ro.boot.init_rc", "");
if (bootscript.empty()) {
// 如果没有特殊配置ro.boot.init_rc,先解析 init.rc 文件
parser.ParseConfig("/init.rc");
if (!parser.ParseConfig("/system/etc/init")) {
late_import_paths.emplace_back("/system/etc/init");
}
if (!parser.ParseConfig("/product/etc/init")) {
late_import_paths.emplace_back("/product/etc/init");
}
if (!parser.ParseConfig("/odm/etc/init")) {
late_import_paths.emplace_back("/odm/etc/init");
}
if (!parser.ParseConfig("/vendor/etc/init")) {
late_import_paths.emplace_back("/vendor/etc/init");
}
} else {
// 直接解析 ro.boot.init_rc 中的数据
parser.ParseConfig(bootscript);
}
}
/**
* 创建解析器,目前只有三种section:service、on、import
* 与之对应的,函数中也出现了三种解析器:ServiceParser、ActionParser、ImportParser
*/
Parser CreateParser(ActionManager& action_manager, ServiceList& service_list) {
Parser parser;
parser.AddSectionParser("service", std::make_unique<ServiceParser>(&service_list, subcontexts));
parser.AddSectionParser("on", std::make_unique<ActionParser>(&action_manager, subcontexts));
parser.AddSectionParser("import", std::make_unique<ImportParser>(&parser));
return parser;
}
init
中启动的守护进程
在init.rc
中定义了很多守护进程,9我们来看下相关内容:
# adb 守护进程放在了这里
import /init.usb.rc
# service adbd /system/bin/adbd --root_seclabel=u:r:su:s0
# class core
# ......
on boot
# Start standard binderized HAL daemons
class_start hal
# 启动所有class core的服务
# 像adb、console等
class_start core
on eraly-init
start ueventd
on post-fs
# Load properties from
# /system/build.prop,
# /odm/build.prop,
# /vendor/build.prop and
# /factory/factory.prop
load_system_props
# start essential services
# 这几个 service 的.rc文件都在对应项目中,通过LOCAL_INIT_RC来指定
start logd
# 启动三个Binder服务管理相关的service
# servicemanager用于框架/应用进程之间的 IPC,使用 AIDL 接口
start servicemanager
# hwservicemanager用于框架/供应商进程之间的 IPC,使用 HIDL 接口
# 也可用于供应商进程之间的 IPC,使用 HIDL 接口
start hwservicemanager
# vndservicemanager用于供应商/供应商进程之间的 IPC,使用 AIDL 接口
start vndservicemanager
on post-fs-data
# 启动 vold(Volume守护进程),负责系统扩展储存的自动挂载
# 这个进程后面详解
start vold
# 负责响应 uevent 事件,创建对应的设备节点
service ueventd /sbin/ueventd
class core
......
# 包含常用的shell命令,如ls、cd等
service console /system/bin/sh
class core
......
# Zygote 进程在这里导入,现在支持32位和64位
import /init.${ro.zygote}.rc
# zygote中会启动一些相关service,像media、netd、wificond等
# service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
# onrestart restart audioserver
# onrestart restart cameraserver
# onrestart restart media
# onrestart restart netd
# onrestart restart wificond
# 启动 Zygote 进程,触发这个action的位置在 on late-init 中
on zygote-start && property:ro.crypto.state=unencrypted
start netd
start zygote
start zygote_secondary
启动流程和需要启动的service
通过init.rc
基本上就可以完成定制。
感觉Init
进程通过解析*.rc
的方式大大简化了开发,真的是6啊。设计这一套AIL
的人是真滴猛。。。。。。。
Init
进程对信号的处理
Init
进程是系统的一号进程,系统中的其他进程都是Init
进程的后代.
按照Linux
的设计,Init
进程需要在这些后代死亡
时负责清理
它们,以防止它们变成僵尸进程
。
僵尸进程
简介
关于
僵尸进程
可以参考Wiki百科-僵尸进程哈
在类UNIX
系统中,僵尸进程
是指完成执行(通过exit
系统调用,或运行时发生致命错误或收到终止信号所致),但在操作系统的进程表
中仍然存在其进程控制块,处于终止状态
的进程。
这发生于子进程
需要保留表项以允许其父进程
读取子进程的退出状态:一旦退出态
通过wait系统调用
读取,僵尸进程
条目就从进程表中删除,称之为回收(reaped)
。
正常情况下,进程直接被其父进程wait
并由系统回收。
僵尸进程的避免
父进程
通过wait
和waitpid
等函数等待子进程
结束,这会导致父进程
挂起- 如果
父进程
很忙,那么可以用signal
函数为SIGCHLD
安装handler
,因为子进程
结束后,父进程
会收到该信号,可以在handler
中调用wait
回收 - 如果
父进程
不关心子进程
什么时候结束,那么可以用signal(SIGCHLD,SIG_IGN)
通知内核,自己对子进程
的结束不感兴趣,那么子进程
结束后,内核会回收,并不再给父进程
发送信号 - 还有一些技巧,就是
fork
两次,父进程
先fork
一个子进程
,然后继续工作,子进程fork
一个孙进程
后退出,那么孙进程
被init
接管,孙进程
结束后,Init
会回收。不过子进程
的回收 还要自己做
我们下面来看下Init
进程怎么处理的
初始化SIGCHLD
信号
Init
进程的main
函数中,在初始化第二阶段,有这么一个方法sigchld_handler_init()
:
// file : init.cpp
int main(int argc, char** argv) {
......
sigchld_handler_init();
......
}
// file : sigchld_handler.cpp
void sigchld_handler_init() {
// Create a signalling mechanism for SIGCHLD.
// 创建一个socketpair,往一个socket中写,就可以从另外一个套接字中读取到数据
int s[2];
socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, s);
signal_write_fd = s[0];
signal_read_fd = s[1];
// Write to signal_write_fd if we catch SIGCHLD.
// 信号初始化相关参数设置
// 设置SIGCHLD_handler信号处理函数
// 设置SA_NOCLDSTOP标志
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = SIGCHLD_handler;
act.sa_flags = SA_NOCLDSTOP;
// 注册SIGCHLD信号
sigaction(SIGCHLD, &act, 0);
ReapAnyOutstandingChildren();
// 注册signal_read_fd到epoll_fd
register_epoll_handler(signal_read_fd, handle_signal);
}
// file : sigchld_handler.cpp
/**
* SIGCHLD_handler的作用是当init进程接收到SIGCHLD信号时,往signal_write_fd中写入数据
* 这个时候套接字对中的另外一个signal_read_fd就可读了。
*/
static void SIGCHLD_handler(int) {
if (TEMP_FAILURE_RETRY(write(signal_write_fd, "1", 1)) == -1) {
PLOG(ERROR) << "write(signal_write_fd) failed";
}
}
// file : sigchld_handler.cpp
/**
* register_epoll_handler函数主要的作用是注册属性socket文件描述符到轮询描述符epoll_fd
*/
void register_epoll_handler(int fd, void (*fn)()) {
epoll_event ev;
ev.events = EPOLLIN;
ev.data.ptr = reinterpret_cast<void*>(fn);
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) {
PLOG(ERROR) << "epoll_ctl failed";
}
}
信号的初始化是通过系统调用sigaction()
来完成的
- 参数
act
中:sa_handler
用来指定信号的处理函数sa_flags
用来指定触发标志,SA_NOCLDSTOP
标志意味着当子进程终止时才接收SIGCHLD
信号
在Linux
系统中,信号又称为软中断
,信号的到来会中断进程正在处理的工作,因此在信号处理函数中不要去调用一些不可重入的函数。而且Linux
不会对信号排队,不管是在信号的处理期间再来多少个信号,当前的信号处理函数执行完后,内核只会再发送一个信号给进程,因此,为了不丢失信号,我们的信号处理函数执行得越快越好
而对于SIGCHLD
信号,父进程需要执行等待操作,这样的话时间就比较长了,因此需要有办法解决这个矛盾
- 上面的代码中创建了一对本地
socket
用于进程间通信 - 当信号到来时,
SIGCHLD_handler
处理函数只要向socket
中的signal_write_fd
写入数据就可以 - 这样,信号的处理就转变到了
socket
的处理上了
此时,我们需要监听signal_read_fd
,并提供一个回调函数,这就是register_epoll_handler()
函数的作用
- 函数中的
EPOLLIN
表示当文件描述符可读时才会触发 *fn
就是触发后的回调函数指针
,赋值给了ev.data.ptr
,请留意下这个指针变量,后面会用到- 提供的回调函数就是
handle_signal()
响应子进程的死亡事件
Init
进程启动完毕后,会监听创建的socket
,如果有数据到来,主线程会唤醒并调用处理函数handle_signal()
static void handle_signal() {
// Clear outstanding requests.
// 清空 signal_read_fd 中的数据
char buf[32];
read(signal_read_fd, buf, sizeof(buf));
ReapAnyOutstandingChildren();
}
void ReapAnyOutstandingChildren() {
while (ReapOneProcess()) {
}
}
static bool ReapOneProcess() {
siginfo_t siginfo = {};
// This returns a zombie pid or informs us that there are no zombies left to be reaped.
// It does NOT reap the pid; that is done below.
// 查询是否存在僵尸进程
if (TEMP_FAILURE_RETRY(waitid(P_ALL, 0, &siginfo, WEXITED | WNOHANG | WNOWAIT)) != 0) {
PLOG(ERROR) << "waitid failed";
return false;
}
// 没有僵尸进程直接返回
auto pid = siginfo.si_pid;
if (pid == 0) return false;
// At this point we know we have a zombie pid, so we use this scopeguard to reap the pid
// whenever the function returns from this point forward.
// We do NOT want to reap the zombie earlier as in Service::Reap(), we kill(-pid, ...) and we
// want the pid to remain valid throughout that (and potentially future) usages.
// 等待子进程终止
auto reaper = make_scope_guard([pid] { TEMP_FAILURE_RETRY(waitpid(pid, nullptr, WNOHANG)); });
......
if (!service) return true;
// 进行退出的其他操作
service->Reap(siginfo);
......
}
当接收到子进程的SIGCHLD
信号后,会找出该进程对应的Service
对象,然后调用Reap
函数,我们看下函数内容:
void Service::Reap(const siginfo_t& siginfo) {
// 如果 不是oneshot 或者 是重启的子进程
// 杀掉整个进程组,思考了下,先杀掉为重启做准备吧
// 这样当重启的时候,就不会因为子进程已经存在而导致错误了
if (!(flags_ & SVC_ONESHOT) || (flags_ & SVC_RESTART)) {
KillProcessGroup(SIGKILL);
}
// 做一些当前进程的清理工作
......
// Oneshot processes go into the disabled state on exit,
// except when manually restarted.
// 如果 是oneshot 或者 不是重启的子进程,设置为SVC_DISABLED
if ((flags_ & SVC_ONESHOT) && !(flags_ & SVC_RESTART)) {
flags_ |= SVC_DISABLED;
}
// Disabled and reset processes do not get restarted automatically.
// 如果是SVC_DISABLED或者SVC_RESET
// 设置进程状态为stopped,然后返回
if (flags_ & (SVC_DISABLED | SVC_RESET)) {
NotifyStateChange("stopped");
return;
}
// If we crash > 4 times in 4 minutes, reboot into recovery.
// 省略 crash 次数检测
......
// 省略一些进程状态的设置,都是和重启相关的
// Execute all onrestart commands for this service.
// 执行onrestart的指令
onrestart_.ExecuteAllCommands();
// 设置状态为重启中
NotifyStateChange("restarting");
return;
}
在Reap()
函数中
- 会根据对应进程
Service
对象的flags_
标志位来判断该进程能不能重启 - 如果需要重启,就给
flags_
标志位添加SVC_RESTARTING
标志位
到这里,我们清楚了handle_signal()
函数的内部流程,那么它又是从哪里被调用的呢?
我们再回到init.cpp
的main()
方法中看看:
while (true) {
......
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)();
}
}
请注意ev.data.ptr
,还记得register_epoll_handler()
函数不
void register_epoll_handler(int fd, void (*fn)()) {
......
ev.data.ptr = reinterpret_cast<void*>(fn);
......
}
当epoll_wait
有数据接收到时,就会执行((void (*)()) ev.data.ptr)();
,也就是我们的回调函数handle_signal()
了
咳咳咳,到这里就把
Init
进程对子进程死亡通知的逻辑给梳理完了,源码一直在变,好在核心的逻辑没有变化,且看且珍惜吧,哈哈哈~
属性系统
简介
属性
在Android
系统中大量使用,用来保存系统设置或在进程间传递一些简单的信息
- 每个
属性
由属性名
和属性值
组成 属性名
通常一长串以.
分割的字符串,这些名称的前缀有特定含义,不能随便改动属性值
只能是字符串
Java
层可以通过如下方法来获取和设置属性:
//class android.os.SystemProperties
@SystemApi
public static String get(String key);
@SystemApi
public static String get(String key, String def);
@hide
public static void set(String key, String val);
native
层可以使用:
android::base::GetProperty(key, "");
android::base::SetProperty(key, val);
对于系统中的每个进程来说:
读取属性值
对任何进程都是没有限制的,直接由本进程从共享区域中读取修改属性值
则必须通过Init进程
完成,同时Init进程
还需要检查发起请求的进程是否具有相应的权限
属性值修改成功后,Init
进程会检查init.rc
文件中是否已经定义了和该属性值匹配的trigger
。如果有定义,则执行trigger
下的命令。如:
on property:ro.debuggable=1
start console
这个trigger
的含义是:当属性ro.debuggable
被设置为1
,则执行命令start console
,启动console
Android
系统级应用和底层模块非常依赖属性系统
,常常依靠属性值
来决定它们的行为。
Android
的系统设置程序中,很多功能的打开和关闭都是通过某个特定的系统属性值
来控制。这也意味着随便改变属性值
将会严重影响系统的运行,因此,对于属性值
的修改必须要有特定的权限。对于权限的设定,现在统一由SELinux
来控制。
属性服务的启动流程
我们先看下属性服务启动的整体流程:
int main(int argc, char** argv) {
......
// 属性服务初始化
property_init();
......
// 启动属性服务
start_property_service();
......
}
在init.cpp
的main()
函数中,通过property_init();
来对属性服务进行初始化
void property_init() {
// 创建一个文件夹,权限711,只有owner才可以设置
mkdir("/dev/__properties__", S_IRWXU | S_IXGRP | S_IXOTH);
// 读取一些属性文件,将属性值存储在一个集合中
CreateSerializedPropertyInfo();
if (__system_property_area_init()) {// 创建属性共享内存空间(这个函数是libc库的部分)
LOG(FATAL) << "Failed to initialize property area";
}
if (!property_info_area.LoadDefaultPath()) {// 加载默认路径上的属性到共享区域
LOG(FATAL) << "Failed to load serialized property info file";
}
}
然后通过start_property_service()
函数启动服务:
void start_property_service() {
// 省略SELinux相关操作
......
property_set("ro.property_service.version", "2");
// 创建prop service对应的socket,并返回socket fd
property_set_fd = CreateSocket(PROP_SERVICE_NAME, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, false, 0666, 0, 0, nullptr);
// 省略创建失败的异常判断
......
// 设置最大连接数量为8
listen(property_set_fd, 8);
// 注册epolls事件监听property_set_fd
// 当监听到数据变化时,调用handle_property_set_fd函数
register_epoll_handler(property_set_fd, handle_property_set_fd);
}
socket
描述符property_set_fd
被创建后,用epoll
来监听property_set_fd
- 当
property_set_fd
有数据到来时,init进程
将调用handle_property_set_fd()
函数来进行处理
我们再来看下handle_property_set_fd()
函数
static void handle_property_set_fd() {
static constexpr uint32_t kDefaultSocketTimeout = 2000; /* ms */
// 省略一些异常判断
......
uint32_t cmd = 0;
// 省略cmd读取操作和一些异常判断
......
switch (cmd) {
case PROP_MSG_SETPROP: {
char prop_name[PROP_NAME_MAX];
char prop_value[PROP_VALUE_MAX];
// 省略字符数据的读取组装操作
......
uint32_t result =
HandlePropertySet(prop_name, prop_value, socket.source_context(), cr, &error);
// 省略异常情况处理
......
break;
}
case PROP_MSG_SETPROP2: {
std::string name;
std::string value;
// 省略字符串数据的读取操作
......
uint32_t result = HandlePropertySet(name, value, socket.source_context(), cr, &error);
// 省略异常情况处理
......
break;
}
default:
LOG(ERROR) << "sys_prop: invalid command " << cmd;
socket.SendUint32(PROP_ERROR_INVALID_CMD);
break;
}
}
Init进程
在接收到设置属性的cmd
后,会执行处理函数HandlePropertySet()
uint32_t HandlePropertySet(const std::string& name, const std::string& value,
const std::string& source_context,
const ucred& cr, std::string* error) {
// 判断要设置的属性名称是否合法
// 相当于命名规则检查
if (!IsLegalPropertyName(name)) {
// 不合法直接返回
return PROP_ERROR_INVALID_NAME;
}
// 如果是ctl开头,说明是控制类属性
if (StartsWith(name, "ctl.")) {
// 检查是否具有对应的控制权限
......
// 权限通过后执行对应的控制指令
// 其实控制指令就简单的几个start/stop等,大家可以在深入阅读下这个函数
HandleControlMessage(name.c_str() + 4, value, cr.pid);
return PROP_SUCCESS;
}
const char* target_context = nullptr;
const char* type = nullptr;
// 获取要设置的属性的上下文和数据类型
// 后面会对target_context和type进行比较判断
property_info_area->GetPropertyInfo(name.c_str(), &target_context, &type);
// 检查是否具有当前属性的set权限
if (!CheckMacPerms(name, target_context, source_context.c_str(), cr)) {
// 没有直接返回
return PROP_ERROR_PERMISSION_DENIED;
}
// 对属性的类型和要写入数据的类型进行判断
// 大家看看CheckType函数就明白了,其实只有一个string类型。。。。。
if (type == nullptr || !CheckType(type, value)) {
// 不合法,直接返回
return PROP_ERROR_INVALID_VALUE;
}
// 如果是sys.powerctl属性,需要做一些特殊处理
if (name == "sys.powerctl") {
// 增加一些额外打印
......
}
if (name == "selinux.restorecon_recursive") {
// 特殊属性,特殊处理
return PropertySetAsync(name, value, RestoreconRecursiveAsync, error);
}
return PropertySet(name, value, error);
}
除了一些特殊的属性外,真正设置属性的函数是PropertySet
static uint32_t PropertySet(const std::string& name, const std::string& value, std::string* error) {
size_t valuelen = value.size();
// 判断属性名是否合法
if (!IsLegalPropertyName(name)) {
*error = "Illegal property name";
return PROP_ERROR_INVALID_NAME;
}
// 判断写入的数据长度是否合法
// 判断属性是否为只读属性(ro)
if (valuelen >= PROP_VALUE_MAX && !StartsWith(name, "ro.")) {
*error = "Property value too long";
return PROP_ERROR_INVALID_VALUE;
}
// 判断要写入数据的编码格式
if (mbstowcs(nullptr, value.data(), 0) == static_cast<std::size_t>(-1)) {
*error = "Value is not a UTF8 encoded string";
return PROP_ERROR_INVALID_VALUE;
}
// 根据属性名获取系统中存放的属性对象
prop_info* pi = (prop_info*) __system_property_find(name.c_str());
if (pi != nullptr) {
// ro.* properties are actually "write-once".
// ro开头的属性只允许写入一次
if (StartsWith(name, "ro.")) {
*error = "Read-only property was already set";
return PROP_ERROR_READ_ONLY_PROPERTY;
}
// 如果已经存在,并且不是只读属性,执行属性更新函数
__system_property_update(pi, value.c_str(), valuelen);
} else {
// 如果系统中不存在属性,执行添加属性添加函数
int rc = __system_property_add(name.c_str(), name.size(), value.c_str(), valuelen);
if (rc < 0) {
*error = "__system_property_add failed";
return PROP_ERROR_SET_FAILED;
}
}
// Don't write properties to disk until after we have read all default
// properties to prevent them from being overwritten by default values.
// 如果是持久化的属性,进行持久化处理
if (persistent_properties_loaded && StartsWith(name, "persist.")) {
WritePersistentProperty(name, value);
}
// 将属性的变更添加到Action队列中
property_changed(name, value);
return PROP_SUCCESS;
}
Init
进程epoll
属性的socket
,等待和处理属性请求。
- 如果有请求到来,则调用
handle_property_set_fd
来处理这个请求 - 在
handle_property_set_fd
函数里,首先检查请求者的uid/gid
看看是否有权限,如果有权限则调property_service.cpp
中的PropertySet
函数。
在PropertySet
函数中
- 它先查找就没有这个属性,如果找到,更改属性。如果找不到,则添加新属性。
- 更改时还会判断是不是
ro
属性,如果是,则不能更改。 - 如果是
persist
的话还会写到/data/property/<name>
中。
最后它会调property_changed
函数,把事件挂到队列里
- 如果有人注册这个属性的话(比如
init.rc
中on property:ro.kernel.qemu=1
),最终会触发它
ueventd
和watchdogd
简介
ueventd
进程
守护进程
ueventd
的主要作用是接收ueventd
来创建和删除设备中dev
目录下的设备节点
ueventd
进程和Init
进程并不是一个进程,但是它们的二进制文件是相同的,只不过启动时参数不一样导致程序的执行流程不一样。
在init.rc
文件中
on early-init
start ueventd
## Daemon processes to be run by init.
##
service ueventd /sbin/ueventd
class core
critical
seclabel u:r:ueventd:s0
shutdown critical
这样Init
进程在执行action eraly-init
是就会启动ueventd
进程。
watchdogd
进程
watchdogd
和ueventd
类型,都是独立于Init
的进程,但是代码和Init
进程在一起。watchdogd
是用来配合硬件看门狗
的。
当一个硬件系统开启了watchdog
功能,那么运行在这个硬件系统之上的软件必须在规定的时间间隔内向watchdog
发送一个信号。这个行为简称为喂狗(feed dog)
,以免watchdog
记时超时引发系统重起。
现在的系统中很少有看到watchdogd
进程了,不过这种模式还是很不错、值得借鉴的
更多推荐
所有评论(0)