Init启动流程

Init介绍

Init进程是Android系统中用户空间的第一个进程(pid=1),它是用户进程的鼻祖,负责孵化各种属性服务、守护进程也包括Zygote。Init是由多个源文件共同组成的,这些文件位于/system/core/init。

启动过程

Kernel启动找到Init进程后,进程入口为源码init目录下的main.cpp的main()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/system/core/init/main.cpp
int main(int argc, char** argv) {
...

// init进程创建子进程ueventd,负责设备节点的创建、权限设定等
if (!strcmp(basename(argv[0]), "ueventd")) {
return ueventd_main(argc, argv);
}

if (argc > 1) {
// 初始化日志系统
if (!strcmp(argv[1], "subcontext")) {
android::base::InitLogging(argv, &android::base::KernelLogger);
const BuiltinFunctionMap& function_map = GetBuiltinFunctionMap();

return SubcontextMain(argc, argv, &function_map);
}

if (!strcmp(argv[1], "selinux_setup")) {
return SetupSelinux(argv); // Step2,对Selinux初始化
}

if (!strcmp(argv[1], "second_stage")) {
return SecondStageMain(argc, argv); // Step3,解析init.rc文件、提供服务、创建epoll与处理子进程的终止等
}
}

return FirstStageMain(argc, argv); // Step1,挂载相关文件系统
}

FirstStageMain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
/system/core/init/first_stage_init.cpp
int FirstStageMain(int argc, char** argv) {
// 将各种信号量,如SIGABRT,SIGBUS等的行为设置为SA_RESTART,
// 一旦监听到这些信号,即执行重启系统
if (REBOOT_BOOTLOADER_ON_PANIC) {
// 处理Init进程挂掉的情况,会重启bootloader
InstallRebootSignalHandlers();
}

...

// Clear the umask.
umask(0);

// 设置环境变量地址
CHECKCALL(clearenv());
CHECKCALL(setenv("PATH", _PATH_DEFPATH, 1));
// 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.
// 挂载tmpfs文件系统
CHECKCALL(mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755"));
CHECKCALL(mkdir("/dev/pts", 0755));
CHECKCALL(mkdir("/dev/socket", 0755));
CHECKCALL(mkdir("/dev/dm-user", 0755));
// 挂载devpts文件系统
CHECKCALL(mount("devpts", "/dev/pts", "devpts", 0, NULL));
#define MAKE_STR(x) __STRING(x)
// 挂载proc文件系统
CHECKCALL(mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC)));
#undef MAKE_STR
// 不要将原始命令行公开给非特权进程,root也只有只读权限
CHECKCALL(chmod("/proc/cmdline", 0440));
std::string cmdline;
android::base::ReadFileToString("/proc/cmdline", &cmdline);
// 不要将原始bootconfig公开给非特权进程
chmod("/proc/bootconfig", 0440);
std::string bootconfig;
android::base::ReadFileToString("/proc/bootconfig", &bootconfig);
gid_t groups[] = {AID_READPROC};
CHECKCALL(setgroups(arraysize(groups), groups)); // 设置用户组
// 挂载sysfs文件系统
CHECKCALL(mount("sysfs", "/sys", "sysfs", 0, NULL));
CHECKCALL(mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL));

// 提前创建了kmsg设备节点文件,用于输出log信息
CHECKCALL(mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11)));

if constexpr (WORLD_WRITABLE_KMSG) {
CHECKCALL(mknod("/dev/kmsg_debug", S_IFCHR | 0622, makedev(1, 11)));
}

// 创建Linux伪随机设备
CHECKCALL(mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8)));
CHECKCALL(mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9)));

// log wrapper所必需的,需要在ueventd运行之前被调用
CHECKCALL(mknod("/dev/ptmx", S_IFCHR | 0666, makedev(5, 2)));
CHECKCALL(mknod("/dev/null", S_IFCHR | 0666, makedev(1, 3)));

// 在第一阶段挂tmpfs、mnt/vendor、mount/product分区。
// 其他的分区在第二阶段通过rc文件解析来加载
CHECKCALL(mount("tmpfs", "/mnt", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
"mode=0755,uid=0,gid=1000"));
// 创建可供读写的vendor目录
CHECKCALL(mkdir("/mnt/vendor", 0755));
// /mnt/product is used to mount product-specific partitions that can not be
// part of the product partition, e.g. because they are mounted read-write.
CHECKCALL(mkdir("/mnt/product", 0755));

// /debug_ramdisk is used to preserve additional files from the debug ramdisk
CHECKCALL(mount("tmpfs", "/debug_ramdisk", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
"mode=0755,uid=0,gid=0"));

// /second_stage_resources is used to preserve files from first to second
// stage init
CHECKCALL(mount("tmpfs", kSecondStageRes, "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
"mode=0755,uid=0,gid=0"))

// First stage init stores Mainline sepolicy here.
CHECKCALL(mkdir("/dev/selinux", 0744));
#undef CHECKCALL

// 将内核的stdin/stdout/stderr全部重定向/dev/null,关闭默认控制台输出
SetStdioToDevNull(argv);
// tmpfs已经挂载到/dev上,同时也挂载了/dev/kmsg,可以和外界沟通了
// 初始化日志系统
InitKernelLogging(argv);

//检测上面的操作是否发生了错误
if (!errors.empty()) {
for (const auto& [error_string, error_errno] : errors) {
LOG(ERROR) << error_string << " " << strerror(error_errno);
}
LOG(FATAL) << "Init encountered errors starting first stage, aborting";
}

LOG(INFO) << "init first stage started!";

...

// 如果该文件存在且设备已经解锁,则允许adb root指令(userdebug sepolicy)
if (access("/force_debuggable", F_OK) == 0) {
constexpr const char adb_debug_prop_src[] = "/adb_debug.prop";
constexpr const char userdebug_plat_sepolicy_cil_src[] = "/userdebug_plat_sepolicy.cil";
std::error_code ec; // to invoke the overloaded copy_file() that won't throw.
if (access(adb_debug_prop_src, F_OK) == 0 &&
!fs::copy_file(adb_debug_prop_src, kDebugRamdiskProp, ec)) {
LOG(WARNING) << "Can't copy " << adb_debug_prop_src << " to " << kDebugRamdiskProp
<< ": " << ec.message();
}
if (access(userdebug_plat_sepolicy_cil_src, F_OK) == 0 &&
!fs::copy_file(userdebug_plat_sepolicy_cil_src, kDebugRamdiskSEPolicy, ec)) {
LOG(WARNING) << "Can't copy " << userdebug_plat_sepolicy_cil_src << " to "
<< kDebugRamdiskSEPolicy << ": " << ec.message();
}
// setenv for second-stage init to read above kDebugRamdisk* files.
setenv("INIT_FORCE_DEBUGGABLE", "true", 1);
}

...

// 挂载system、cache、data等系统分区
if (!DoFirstStageMount(!created_devices)) {
LOG(FATAL) << "Failed to mount required partitions early ...";
}

...

// 进入下一步,SetupSelinux
const char* path = "/system/bin/init"; // 找到Init的二进制文件目录
const char* args[] = {path, "selinux_setup", nullptr};
auto fd = open("/dev/kmsg", O_WRONLY | O_CLOEXEC);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
close(fd);
execv(path, const_cast<char**>(args)); // 通过execv来启动Init进程

// execv() only returns if an error happened, in which case we
// panic and never fall through this conditional.
PLOG(FATAL) << "execv(\"" << path << "\") failed";

return 1;
}

主要通过mount挂载对应的文件系统,mkdir创建对应的文件目录,并配置相应的访问权限。

这些文件只是在应用运行的时候存在,一旦应用运行结束就会随着应用一起消失。

挂载的文件系统主要有四类:

  1. tmpfs:一种虚拟内存文件系统,它会将所有的文件存储在虚拟内存中。由于tmpfs是驻留在RAM的,因此它的内容是不持久的。断电后,tmpfs的内容就消失了。
  2. devpts:为伪终端提供了一个标准接口,它的标准挂接点是/dev/pts。只要pty的主复合设备/dev/ptmx被打开,就会在/dev/pts下动态地创建一个新的pty设备文件。
  3. proc:也是一个虚拟文件系统,它可以看作是内核内部数据结构的接口,通过它我们可以获得系统的信息,同时也能够在运行时修改特定的内核参数。
  4. sysfs:与proc文件系统类似,也是一个不占有任何磁盘空间的虚拟文件系统。它通常被挂接在/sys目录下。作用是把系统的设备和总线按层次组织起来,使得它们可以在用户空间读取,用来向用户空间导出内核的数据结构和属性。

在FirstStageMain还会通过InitKernelLogging(argv)来初始化log日志系统。此时Android还没有自己的系统日志,采用kernel的log系统,打开的设备节点/dev/kmsg, 可通过cat /dev/kmsg来获取内核log。

最后会通过execv方法传递对应的path与下一阶段的参数selinux_setup。

SetupSelinux

1
2
3
4
5
6
7
8
9
10
11
/system/core/init/selinux.cpp
int SetupSelinux(char** argv) {

...

const char* path = "/system/bin/init";
const char* args[] = {path, "second_stage", nullptr};
execv(path, const_cast<char**>(args));

return 1;
}

SetupSelinux方法中加载了selinux的策略并启动selinux的强制模式,然后启动了Init进程。Init的二进制文件存放在机器的/system/bin/init,然后通过execv启动Init进程第二阶段。

SecondStageMain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
int SecondStageMain(int argc, char** argv) {
...

SetStdioToDevNull(argv);
// 初始化本阶段内核日志
InitKernelLogging(argv);
LOG(INFO) << "init second stage started!";

...

// 禁止OOM Killer杀死该进程以及它的子进程
if (auto result =
WriteFile("/proc/1/oom_score_adj", StringPrintf("%d", DEFAULT_OOM_SCORE_ADJUST));
!result.ok()) {
LOG(ERROR) << "Unable to write " << DEFAULT_OOM_SCORE_ADJUST
<< " to /proc/1/oom_score_adj: " << result.error();
}

// 设置所有进程都能访问的会话密钥
keyctl_get_keyring_ID(KEY_SPEC_SESSION_KEYRING, 1);

// 创建/dev/.booting文件,一个标记,表示booting进行中
close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));

...

// 初始化属性服务,并从指定文件读取属性
// 使用mmap共享内存,/dev/__properties__/property_info
PropertyInit();

...

// 进行Selinux第二阶段,并恢复一些文件安全上下文
SelinuxSetupKernelLogging();
SelabelInitialize();
SelinuxRestoreContext();

// 初始化epoll,android这里对epoll做了一层封装
Epoll epoll;
if (auto result = epoll.Open(); !result.ok()) {
PLOG(FATAL) << result.error();
}

// 使用epoll对Init子进程的信号进行监听
// epoll中注册signalfd,主要是为了创建handler处理子进程终止信号
InstallSignalFdHandler(&epoll);
InstallInitNotifier(&epoll);
// 开启属性服务,并注册到epoll中
StartPropertyService(&property_fd);

...

// 会执行/system/bin/init subcontext
InitializeSubcontext();

// 为解析init.rc中的action和service做准备
ActionManager& am = ActionManager::GetInstance();
ServiceList& sm = ServiceList::GetInstance();

// 加载系统启动脚本init.rc
LoadBootScripts(am, sm);

...

// cgroups用于控制资源,cpuset相关
am.QueueBuiltinAction(SetupCgroupsAction, "SetupCgroups");
am.QueueBuiltinAction(SetKptrRestrictAction, "SetKptrRestrict");
am.QueueBuiltinAction(TestPerfEventSelinuxAction, "TestPerfEventSelinux");
am.QueueBuiltinAction(ConnectEarlyStageSnapuserdAction, "ConnectEarlyStageSnapuserd");
// 执行rc文件中触发器为on early-init的语句
am.QueueEventTrigger("early-init");

// 等冷插拔设备初始化完成
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(SetMmapRndBitsAction, "SetMmapRndBits");
// 设备组合键的初始化操作
Keychords keychords;
am.QueueBuiltinAction(
[&epoll, &keychords](const BuiltinArguments& args) -> Result<void> {
for (const auto& svc : ServiceList::GetInstance()) {
keychords.Register(svc->keycodes());
}
keychords.Start(&epoll, HandleKeychord);
return {};
},
"KeychordInit");

// 执行rc文件中触发器为on init的语句
am.QueueEventTrigger("init");

// 当设备处于充电模式时,不需要mount文件系统或者启动系统服务。
// 充电模式下,将charger设为执行队列,否则把late-init设为执行队列
std::string bootmode = GetProperty("ro.bootmode", "");
if (bootmode == "charger") {
am.QueueEventTrigger("charger");
} else {
am.QueueEventTrigger("late-init");
}

// 基于属性当前状态,运行所有的属性触发器
am.QueueBuiltinAction(queue_property_triggers_action, "queue_property_triggers");

// Restore prio before main loop
setpriority(PRIO_PROCESS, 0, 0);
while (true) {
// 进入死循环状态
auto epoll_timeout = std::optional<std::chrono::milliseconds>{kDiagnosticTimeout};

auto shutdown_command = shutdown_state.CheckShutdown();
if (shutdown_command) {
LOG(INFO) << "Got shutdown_command '" << *shutdown_command
<< "' Calling HandlePowerctlMessage()";
HandlePowerctlMessage(*shutdown_command);
shutdown_state.set_do_shutdown(false);
}

if (!(prop_waiter_state.MightBeWaiting() || Service::is_exec_service_running())) {
am.ExecuteOneCommand();
}
if (!IsShuttingDown()) {
// 重启死掉的子进程
auto next_process_action_time = HandleProcessActions();

// If there's a process that needs restarting, wake up in time for that.
if (next_process_action_time) {
epoll_timeout = std::chrono::ceil<std::chrono::milliseconds>(
*next_process_action_time - boot_clock::now());
if (*epoll_timeout < 0ms) epoll_timeout = 0ms;
}
}

if (!(prop_waiter_state.MightBeWaiting() || Service::is_exec_service_running())) {
// If there's more work to do, wake up again immediately.
if (am.HasMoreCommands()) epoll_timeout = 0ms;
}

// 循环等待事件发生
auto pending_functions = epoll.Wait(epoll_timeout);
if (!pending_functions.ok()) {
LOG(ERROR) << pending_functions.error();
} else if (!pending_functions->empty()) {
...
}
}

return 0;
}

LoadBootScripts()会加载init.rc配置文件,之后加载/{system, vendor, odm}/etc/init/下(Android设备中的目录)的所有rc配置文件。

LoadBootScripts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/system/core/init/init.cpp
static void LoadBootScripts(ActionManager& action_manager, ServiceList& service_list) {
// 初始化ServiceParse、ActionParser、ImportParser三个解析器
Parser parser = CreateParser(action_manager, service_list);

std::string bootscript = GetProperty("ro.boot.init_rc", "");
if (bootscript.empty()) {
// 解析init.rc文件
parser.ParseConfig("/system/etc/init/hw/init.rc");
if (!parser.ParseConfig("/system/etc/init")) {
late_import_paths.emplace_back("/system/etc/init");
}
// late_import is available only in Q and earlier release. As we don't
// have system_ext in those versions, skip late_import for system_ext.
parser.ParseConfig("/system_ext/etc/init");
if (!parser.ParseConfig("/vendor/etc/init")) {
late_import_paths.emplace_back("/vendor/etc/init");
}
if (!parser.ParseConfig("/odm/etc/init")) {
late_import_paths.emplace_back("/odm/etc/init");
}
if (!parser.ParseConfig("/product/etc/init")) {
late_import_paths.emplace_back("/product/etc/init");
}
} else {
parser.ParseConfig(bootscript);
}
}

Parser CreateParser(ActionManager& action_manager, ServiceList& service_list) {
Parser parser;

// 加载解析Service语句的解析器
parser.AddSectionParser("service", std::make_unique<ServiceParser>(
&service_list, GetSubcontext(), std::nullopt));
// 加载解析on语句的解析器
parser.AddSectionParser("on", std::make_unique<ActionParser>(&action_manager, GetSubcontext()));
// 加载解析import语句的解析器
parser.AddSectionParser("import", std::make_unique<ImportParser>(&parser));

return parser;
}

init.rc

init.rc有两个,分别位于:

/system/core/rootdir/init.rc,正常启动

/bootable/recovery/etc/init.rc,刷机

import
1
2
3
4
5
6
import /init.environ.rc		// 导入全局环境变量
import /system/etc/init/hw/init.usb.rc // adb服务、USB相关内容的定义
import /init.${ro.hardware}.rc // 硬件相关的初始化,一般是厂商定制
import /vendor/etc/init/hw/init.${ro.hardware}.rc
import /system/etc/init/hw/init.usb.configfs.rc
import /system/etc/init/hw/init.${ro.zygote}.rc // 定义Zygote服务

init.rc中会根据系统的不同属性来引入不同的zygote脚本。

  • init.zygote32.rc:zygote进程对应的执行程序是app_process(纯32bit模式)
  • init.zygote64.rc:zygote进程对应的执行程序是app_process(纯64bit模式)
  • init.zygote64_32.rc:启动两个zygote进程(zygote和zygote_secondary),对应的执行程序分别是app_process64(主模式),app_process32
on early-init
1
2
3
4
5
on early-init
...
start ueventd
exec_start apexd-bootstrap
...

early-init中启动了ueventd服务和apex相关服务。

  • ueventd服务

    1
    2
    3
    4
    5
    service ueventd    //ueventd服务的可执行文件的路径为/system/bin/ueventd
    class core //ueventd归属于core class,同样归属于core class的还有adbdconsole等服务
    critical //表明这个Service对设备至关重要,如果Service在四分钟内退出超过4次,则设备将重启进入恢复模式。
    seclabel u:r:ueventd:s0 //selinux相关的配置
    shutdown critical //ueventd服务关闭行为
  • early-init触发时机

    1
    2
    3
    /system/core/init.init.cpp$SecondStageMain

    am.QueueEventTrigger("early-init");
on init
1
2
3
4
5
6
on init
...
start logd // 用于保存Android运行期间的日志
...
start servicemanager // Android系统服务管理者,负责查询和注册服务
...
on late-init
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
on late-init
# 启动vold服务(管理和控制Android平台外部存储设备,包括SD插拔、挂载、卸载、格式化等)
trigger early-fs
trigger factory-fs
trigger fs
trigger post-fs
trigger late-fs

# 挂载/data,启动apexd服务
trigger post-fs-data

# Should be before netd, but after apex, properties and logging is available.
trigger load_bpf_programs

# 启动zygote服务,在启动zygote服务之前会先启动netd服务(专门负责网络管理和控制的后台守护进程)
trigger zygote-start

# 移除/dev/.booting文件
trigger firmware_mounts_complete

trigger early-boot
trigger boot
trigger mmi
  • zygote

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /system/core/rootdir/init.zygote64.rc

    service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server
    class main
    priority -20
    user root
    group root readproc reserved_disk
    socket zygote stream 660 root system
    socket usap_pool_primary stream 660 root system
    onrestart exec_background - system system -- /system/bin/vdc volume abort_fuse
    onrestart write /sys/power/state on
    onrestart restart audioserver
    onrestart restart cameraserver
    onrestart restart media
    onrestart restart media.tuner
    onrestart restart netd
    onrestart restart wificond
    task_profiles ProcessCapacityHigh
    critical window=${zygote.critical_window.minute:-off} target=zygote-fatal

    该文件通过service语句来创建zygote进程,该进程的代码位于/system/bin/app_process目录下。当相关的触发器被触发后,便会启动zygote进程。

总结

  • Init入口中包含5个分支:

    • ueventd:实际上就是Init程序的软链接,在init.rc的early-init阶段启动
    • selinux_setup:FitstStageMain中启动
    • subcontext:SecondStageMain中启动
    • second_stage:在selinux_setup启动完之后执行
    • first_stage:默认首先执行
  • Init进程第一阶段做的主要工作是挂载分区,创建设备节点和一些关键目录,初始化日志输出系统,启用SELinux安全策略。

    Init进程第二阶段的主要工作是初始化属性系统,解析SELinux的匹配规则,处理子进程终止信号,启动系统属性服务。

    Init进程第三阶段主要是解析init.rc来启动其他进程,进入死循环,进行子进程实时监控。

  • 在SecondStage,首先加载init.rc配置,然后再依次在Android设备中查找以下三种配置,并加载:

    • /system/etc/init/
    • /vendor/etc/init/
    • /odm/etc/init/
  • Android根文件系统的镜像中不存在/dev目录,该目录是Init进程启动后动态创建的。为此,Init进程创建子进程ueventd,并将创建设备节点文件的工作托付给ueventd。

    ueventd通过两种方式创建设备节点文件:

    • 冷插拔(Cold Plug)

      以预先定义的设备信息为基础,当ueventd启动后,统一创建设备节点文件。这一类设备节点文件也被称为静态节点文件。

    • 热插拔(Hot Plug)

      在系统运行中,当有设备插入USB端口时,ueventd就会接收到这一事件,为插入的设备动态创建设备节点文件。这一类设备节点文件也被称为动态节点文件。

  • signal:每个进程在处理其他进程发送的signal信号时都需要先注册,当进程的运行状态改变或终止时会产生某种signal信号,Init进程是所有用户空间进程的父进程,当其子进程终止时产生signal信号,以便父进程进行处理,主要是为了防止子进程成为僵尸进程。

    僵尸进程:父进程使用fork创建子进程,子进程终止后,如果父进程不知道子进程已经终止的话,这时子进程虽然已经退出,但是在系统进程表中还为它保留了一些信息(如进程号、运行时间、退出状态等),这个子进程就是僵尸进程。系统进程表是一项有限的资源,如果它被僵尸进程耗尽的话,系统可能会无法创建新的进程。


Init启动流程
https://citrus-maxima.github.io/2024/03/10/Init启动流程/
作者
柚子树
发布于
2024年3月10日
许可协议