【Android系统启动】 Android Init 进程启动流程源码解析

从源码角度看Android系统init进程启动过程 init进程是Linux系统中用户空间的第一个进程,进程号为1。Kernel启动后,在用户空间启动init进程,并调用/system/core/init.cpp中的main方法执行一些重要的工作。 备注:本文将结合Android8.0的源码看init进程的启动过程以及init进程做了哪些重要工作。 1. init进程启动前系统的启动流程 在引入init进程前,我们需要大致了解系统是如何走到init... 阅读详情

【Android系统启动】 Android Init 进程启动流程源码解析

前言

  1. 文章源码按照AOSP官网源码android14-releaseInit进程代码进行解读;
  2. 如有不当之处,麻烦指出作者进行修正

代码位置

  1. system/core/init/main.cpp
  2. /system/core/rootdir/init.rc
  3. /system/core/init/first_stage_init.cpp

请添加图片描述

源码解析

main.cpp Init进程入口

int main(int argc, char** argv) {
#if __has_feature(address_sanitizer)
    __asan_set_error_report_callback(AsanReportCallback);
#elif __has_feature(hwaddress_sanitizer)
    __hwasan_set_error_report_callback(AsanReportCallback);
#endif
    // Boost prio which will be restored later
    setpriority(PRIO_PROCESS, 0, -20);
    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);
        }

        if (!strcmp(argv[1], "second_stage")) {
            return SecondStageMain(argc, argv);
        }
    }

    return FirstStageMain(argc, argv);
}
  1. 可以从Init进程的main入口函数这里看到,Init进程进入后会将优先级设为最高,然后解析参数,根据参数有5种情况:
    1. 无参数:调用FirstStageMain()方法,为Init进程第一初始化阶段;
    2. “selinux_setup” : 调用SetupSelinux()方法,为Init进程初始化SeLinux的阶段;
    3. “second_stage” : 调用SecondStageMain()方法,为Init进程第二初始化阶段;
    4. “ueventd” : 调用ueventd_main()方法,本篇文章不涉及;
    5. “subcontext” : 调用SubcontextMain()方法,本篇文章不涉及;
  2. 根据上一篇文章Kernel启动流程种可以看到, 当时调用是没有带参数的,因此这里从FirstStageMain开始;
// https://cs.android.com/android/kernel/superproject/+/common-android14-5.15:common/init/main.c
if (!try_to_run_init_process("/sbin/init") ||
	    !try_to_run_init_process("/etc/init") ||
	    !try_to_run_init_process("/bin/init") ||
	    !try_to_run_init_process("/bin/sh"))
		return 0;

static int try_to_run_init_process(const char *init_filename)
{
	int ret;

	ret = run_init_process(init_filename);

	if (ret && ret != -ENOENT) {
		pr_err("Starting init: %s exists but couldn't execute it (error %d)\n",
		       init_filename, ret);
	}

	return ret;
}

first_stage_init.cpp 第一阶段Init

int FirstStageMain(int argc, char** argv) {
    // 如果配置为在发生panic时重启到bootloader,则安装重启信号处理程序
    if (REBOOT_BOOTLOADER_ON_PANIC) {
        InstallRebootSignalHandlers();
    }

    // 记录启动时间
    boot_clock::time_point start_time = boot_clock::now();

    // 存储错误信息的容器
    std::vector<std::pair<std::string, int>> errors;
#define CHECKCALL(x) \
    if ((x) != 0) errors.emplace_back(#x " failed", errno);

    // 清除umask
    umask(0);

    // 清空环境变量
    CHECKCALL(clearenv());
    // 设置默认PATH
    CHECKCALL(setenv("PATH", _PATH_DEFPATH, 1));
    // 在initramdisk中进行基本的文件系统设置
    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));
    CHECKCALL(mount("devpts", "/dev/pts", "devpts", 0, NULL));
#define MAKE_STR(x) __STRING(x)
    CHECKCALL(mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC)));
#undef MAKE_STR
    // 不向非特权进程暴露原始命令行
    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));
    CHECKCALL(mount("sysfs", "/sys", "sysfs", 0, NULL));
    CHECKCALL(mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL));

    // 创建/dev/kmsg设备节点
    CHECKCALL(mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11)));

    // 如果配置为允许任何人写入kmsg,则创建/dev/kmsg_debug设备节点
    if constexpr (WORLD_WRITABLE_KMSG) {
        CHECKCALL(mknod("/dev/kmsg_debug", S_IFCHR | 0622, makedev(1, 11)));
    }

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

    // 创建设备节点
    CHECKCALL(mknod("/dev/ptmx", S_IFCHR | 0666, makedev(5, 2)));
    CHECKCALL(mknod("/dev/null", S_IFCHR | 0666, makedev(1, 3)));

    // 下面的挂载在第一阶段init中完成,以便第一阶段挂载可以挂载/mnt/{vendor,product}/的子目录。
    // 其他挂载,不需要第一阶段挂载,则应该在rc文件中完成。

    // 为由vold管理的设备挂载临时文件系统
    CHECKCALL(mount("tmpfs", "/mnt", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
                    "mode=0755,uid=0,gid=1000"));
    // /mnt/vendor用于挂载特定于供应商的分区,这些分区不能作为供应商分区的一部分,例如,因为它们被以读写方式挂载。
    CHECKCALL(mkdir("/mnt/vendor", 0755));
    // /mnt/product用于挂载特定于产品的分区,这些分区不能作为产品分区的一部分,例如,因为它们被以读写方式挂载。
    CHECKCALL(mkdir("/mnt/product", 0755));

    // /debug_ramdisk用于保留调试ramdisk中的额外文件
    CHECKCALL(mount("tmpfs", "/debug_ramdisk", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
                    "mode=0755,uid=0,gid=0"));

    // /second_stage_resources用于保留从第一阶段到第二阶段init的文件
    CHECKCALL(mount("tmpfs", kSecondStageRes, "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
                    "mode=0755,uid=0,gid=0"))
#undef CHECKCALL

    // 将标准输入/输出/错误重定向到/dev/null
    SetStdioToDevNull(argv);
    // 现在/dev上已经挂载了tmpfs,并且我们有了/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";
    }

    // 记录init第一阶段已启动
    LOG(INFO) << "init first stage started!";

    // 打开旧的根目录
    auto old_root_dir = std::unique_ptr<DIR, decltype(&closedir)>{opendir("/"), closedir};
    if (!old_root_dir) {
        PLOG(ERROR) << "Could not opendir(\"/\"), not freeing ramdisk";
    }

    struct stat old_root_info;
    // 获取旧的根目录信息
    if (stat("/", &old_root_info) != 0) {
        PLOG(ERROR) << "Could not stat(\"/\"), not freeing ramdisk";
        old_root_dir.reset();
    }

    // 确定是否需要在控制台上输出信息
    auto want_console = ALLOW_FIRST_STAGE_CONSOLE ? FirstStageConsole(cmdline, bootconfig) : 0;
    // 确定是否要并行加载模块
    auto want_parallel =
            bootconfig.find("androidboot.load_modules_parallel = \"true\"") != std::string::npos;

    // 加载内核模块
    boot_clock::time_point module_start_time = boot_clock::now();
    int module_count = 0;
    if (!LoadKernelModules(IsRecoveryMode() && !ForceNormalBoot(cmdline, bootconfig), want_console,
                           want_parallel, module_count)) {
        if (want_console != FirstStageConsoleParam::DISABLED) {
            LOG(ERROR) << "Failed to load kernel modules, starting console";
        } else {
            LOG(FATAL) << "Failed to load kernel modules";
        }
    }
    // 记录加载的模块数量和时间
    if (module_count > 0) {
        auto module_elapse_time = std::chrono::duration_cast<std::chrono::milliseconds>(
                boot_clock::now() - module_start_time);
        setenv(kEnvInitModuleDurationMs, std::to_string(module_elapse_time.count()).c_str(), 1);
        LOG(INFO) << "Loaded " << module_count << " kernel modules took "
                  << module_elapse_time.count() << " ms";
    }

    bool created_devices = false;
    // 如果在故障时要求在控制台上输出信息
    if (want_console == FirstStageConsoleParam::CONSOLE_ON_FAILURE) {
        if (!IsRecoveryMode()) {
            // 创建设备节点
            created_devices = DoCreateDevices();
            if (!created_devices) {
                LOG(ERROR) << "Failed to create device nodes early";
            }
        }
        // 启动控制台
        StartConsole(cmdline);
    }

    // 如果存在/bootimage_ramdisk.prop,则将其复制到第二阶段
    if (access(kBootImageRamdiskProp, F_OK) == 0) {
        std::string dest = GetRamdiskPropForSecondStage();
        std::string dir = android::base::Dirname(dest);
        std::error_code ec;
        if (!fs::create_directories(dir, ec) && !!ec) {
            LOG(FATAL) << "Can't mkdir " << dir << ": " << ec.message();
        }
        if (!fs::copy_file(kBootImageRamdiskProp, dest, ec)) {
            LOG(FATAL) << "Can't copy " << kBootImageRamdiskProp << " to " << dest << ": "
                       << ec.message();
        }
        LOG(INFO) << "Copied ramdisk prop to " << dest;
    }

    // 如果存在"/force_debuggable",则设置环境变量INIT_FORCE_DEBUGGABLE以供第二阶段init使用
    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;  // 调用不会抛出异常的重载copy_file()
        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();
        }
        // 设置环境变量INIT_FORCE_DEBUGGABLE
        setenv("INIT_FORCE_DEBUGGABLE", "true", 1);
    }

    // 如果是正常启动,则准备切换根文件系统
    if (ForceNormalBoot(cmdline, bootconfig)) {
        // 创建/first_stage_ramdisk目录
        mkdir("/first_stage_ramdisk", 0755);
        // 准备切换根文件系统
        PrepareSwitchRoot();
        // 必须使用挂载点作为目标调用SwitchRoot(),因此在此处将目标目录绑定到自身。
        if (mount("/first_stage_ramdisk", "/first_stage_ramdisk", nullptr, MS_BIND, nullptr) != 0) {
            PLOG(FATAL) << "Could not bind mount /first_stage_ramdisk to itself";
        }
        // 切换根文件系统
        SwitchRoot("/first_stage_ramdisk");
    }

    // 进行第一阶段挂载,如果未创建设备节点,则传递false
    if (!DoFirstStageMount(!created_devices)) {
        LOG(FATAL) << "Failed to mount required partitions early ...";
    }

    // 获取新的根目录信息
    struct stat new_root_info;
    if (stat("/", &new_root_info) != 0) {
        PLOG(ERROR) << "Could not stat(\"/\"), not freeing ramdisk";
        old_root_dir.reset();
    }

    // 如果旧的根目录存在且新的根目录设备号与旧的不同,则释放ramdisk
    if (old_root_dir && old_root_info.st_dev != new_root_info.st_dev) {
        FreeRamdisk(old_root_dir.get(), old_root_info.st_dev);
    }

    // 在恢复模式下设置初始AVB版本
    SetInitAvbVersionInRecovery();

    // 设置环境变量,记录第一阶段启动时间
    setenv(kEnvFirstStageStartedAt, std::to_string(start_time.time_since_epoch().count()).c_str(),
           1);

    // 设置selinux init的路径和参数
    const char* path = "/system/bin/init";
    const char* args[] = {path, "selinux_setup", nullptr};
    // 打开/dev/kmsg,重定向标准输出和标准错误到其上,并关闭文件描述符
    auto fd = open("/dev/kmsg", O_WRONLY | O_CLOEXEC);
    dup2(fd, STDOUT_FILENO);
    dup2(fd, STDERR_FILENO);
    close(fd);
    // 执行selinux init
    execv(path, const_cast<char**>(args));

    // 如果execv()返回,说明出现了错误,此时记录错误并终止
    PLOG(FATAL) << "execv(\"" << path << "\") failed";

    return 1;
}

上面代码的注释部分是原文翻译,部分是自己加上去。可以看到第一阶段做了如下工作:

  1. InstallRebootSignalHandlers : 实际上监听Linux的Signal,当收到这些Signal时,重启Bootloader,如果REBOOT_BOOTLOADER_ON_PANIC不为true,则不会处理;
  2. 设置基本的环境, 如设置环境变量、挂载基本的文件系统、挂载设备节点、初始化日志等等,并监听是否出错,如果出错则停止;
  3. 加载内核模块:根据配置加载内核模块,可以选择是否并行加载模块,并记录加载模块的数量和时间。
  4. 创建设备节点和启动控制台:根据配置决定是否在控制台上输出信息,如果配置为在故障时输出信息,则创建设备节点并启动控制台。
  5. 复制 ramdisk 属性文件:如果存在 /bootimage_ramdisk.prop 文件,则将其复制到第二阶段用于使用。
  6. 设置调试模式:如果存在 /force_debuggable 文件,则设置环境变量 INIT_FORCE_DEBUGGABLE 为 true,并复制相关属性文件到调试 ramdisk。
  7. 准备切换根文件系统:如果是正常启动,创建 /first_stage_ramdisk 目录并准备切换根文件系统。
  8. 进行第一阶段挂载:进行第一阶段的文件系统挂载。
  9. 释放旧的 ramdisk:如果旧的根目录存在且新的根目录设备号与旧的不同,则释放 ramdisk。
  10. 设置初始 AVB 版本:在恢复模式下设置初始 Android Verified Boot (AVB) 版本。
  11. 设置环境变量:记录第一阶段启动时间。
  12. 执行 SELinux 初始化:设置 SELinux 初始化的路径和参数,并执行 SELinux 初始化。

那么下一步去查看SeLinux的初始化吧。

selinux.cpp SeLinux初始化

// SELinux设置过程围绕着snapuserd进行精心编排。策略必须从动态分区加载,在OTA期间,这些分区无法在没有snapuserd的情况下读取。
// 但是,随着以内核特权运行的snapuserd,加载策略将立即触发审核。
//
// 我们使用五个步骤来解决这个问题:
//  (1) 在snapuserd运行时将策略读入字符串中。
//  (2) 重写快照设备映射表,生成新的dm-user设备并刷新I/O。
//  (3) 终止snapuserd,因为它不再有任何dm-user设备可以附加。
//  (4) 加载sepolicy并在/dev中执行关键的restorecon,小心避免读取/system中的任何内容。
//  (5) 重新启动snapuserd并将其附加到步骤(2)中的dm-user设备上。
//
// 在此序列之后,可以安全地启用强制模式并继续引导。
int SetupSelinux(char** argv) {
    SetStdioToDevNull(argv); // 设置标准输入/输出/错误到/dev/null
    InitKernelLogging(argv); // 初始化内核日志记录

    if (REBOOT_BOOTLOADER_ON_PANIC) { // 如果在发生panic时重新引导引导加载程序
        InstallRebootSignalHandlers(); // 安装重新引导信号处理程序
    }

    boot_clock::time_point start_time = boot_clock::now(); // 记录启动时间点

    MountMissingSystemPartitions(); // 挂载缺失的系统分区

    SelinuxSetupKernelLogging(); // SELinux设置内核日志记录

    LOG(INFO) << "Opening SELinux policy"; // 记录打开SELinux策略

    PrepareApexSepolicy(); // 准备APEX sepolicy

    // 在潜在终止snapuserd之前读取策略
    std::string policy;
    ReadPolicy(&policy);
    CleanupApexSepolicy(); // 清理APEX sepolicy

    auto snapuserd_helper = SnapuserdSelinuxHelper::CreateIfNeeded(); // 创建snapuserd SELinux帮助器
    if (snapuserd_helper) {
        // 终止旧的snapuserd以避免审核消息。在此之后,我们不能从/system(或其他动态分区)读取,直到调用FinishTransition()为止。
        snapuserd_helper->StartTransition(); // 开始转换
    }

    LoadSelinuxPolicy(policy); // 加载SELinux策略

    if (snapuserd_helper) {
        // 在启用强制之前,完成挂起的snapuserd转换。
        snapuserd_helper->FinishTransition(); // 完成转换
        snapuserd_helper = nullptr;
    }

    // 此restorecon在SelinuxSetEnforcement之前有意执行,因为在将SELinux设置为强制模式后,从tmpfs转换文件到*_contexts_file上下文所需的权限不应授予任何进程。
    if (selinux_android_restorecon("/dev/selinux/", SELINUX_ANDROID_RESTORECON_RECURSE) == -1) {
        PLOG(FATAL) << "restorecon failed of /dev/selinux failed"; // 恢复失败,终止
    }

    SelinuxSetEnforcement(); // 设置SELinux执行

    // 我们在内核域中,希望转换到init域。保存SELabel在其xattrs中的文件系统,例如ext4,此处不需要显式的restorecon,但其他文件系统需要。特别是对于A/B设备的恢复镜像等ramdisk。
    if (selinux_android_restorecon("/system/bin/init", 0) == -1) {
        PLOG(FATAL) << "restorecon failed of /system/bin/init failed"; // 恢复失败,终止
    }

    // 设置环境变量,记录SELinux启动时间
    setenv(kEnvSelinuxStartedAt, std::to_string(start_time.time_since_epoch().count()).c_str(), 1);

    const char* path = "/system/bin/init"; // 第二阶段init的路径
    const char* args[] = {path, "second_stage", nullptr}; // 第二阶段init的参数
    execv(path, const_cast<char**>(args)); // 执行第二阶段init

    // 如果execv()返回,则出现错误,记录并终止
    PLOG(FATAL) << "execv(\"" << path << "\") failed";

    return 1;
}

这里做的工作有:

  1. 初始化日志并设置标准输入/输出/错误到 /dev/null;
  2. 如果配置为在发生 panic 时重启到 bootloader,则捕获信号并重启(跟上面第一阶段一样);
  3. 记录启动时间点;
  4. 挂载缺失的系统分区;
  5. 设置内核日志记录和打开 SELinux 策略;
  6. 准备 APEX sepolicy 并读取策略,执行相关转换;
  7. 执行必要的恢复操作,包括设置 SELinux 强制模式;
  8. 执行第二阶段 init。

init.cpp - SecondStageMain 第二阶段Init

int SecondStageMain(int argc, char** argv) {
    if (REBOOT_BOOTLOADER_ON_PANIC) {
        InstallRebootSignalHandlers(); // 如果发生 panic,安装重新引导信号处理程序
    }

    // 等到 signalfd 注册之前,不应启动任何线程。
    // 如果确实需要这些线程,每个线程都应确保阻塞 SIGCHLD 信号。
    // 参见 b/223076262
    boot_clock::time_point start_time = boot_clock::now(); // 记录启动时间点

    trigger_shutdown = [](const std::string& command) { shutdown_state.TriggerShutdown(command); };

    SetStdioToDevNull(argv); // 设置标准输入/输出/错误到 /dev/null
    InitKernelLogging(argv); // 初始化内核日志记录
    LOG(INFO) << "init second stage started!"; // 记录第二阶段初始化开始

    SelinuxSetupKernelLogging(); // 设置 SELinux 内核日志记录

    // 更新 $PATH(如果第二阶段 init 新于第一阶段 init,则设置)
    if (setenv("PATH", _PATH_DEFPATH, 1) != 0) {
        PLOG(FATAL) << "Could not set $PATH to '" << _PATH_DEFPATH << "' in second stage"; // 设置 $PATH 失败,终止
    }

    // 初始化不应因为对其他进程的依赖而崩溃,因此我们忽略 SIGPIPE 并直接在调用点处理 EPIPE。
    // 请注意,将信号设置为 SIG_IGN 是继承的,但自定义信号处理程序不是。
    // 由于我们不希望为子进程忽略 SIGPIPE,因此为信号处理程序设置一个空操作函数。
    {
        struct sigaction action = {.sa_flags = SA_RESTART};
        action.sa_handler = [](int) {};
        sigaction(SIGPIPE, &action, nullptr);
    }

    // 设置 init 及其派生子进程的 oom_adj。
    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(); // 写入 oom_adj 失败,记录错误
    }

    // 设置一个所有进程都可以访问的会话密钥环。
    // 它将保存诸如 FBE 加密密钥等内容。没有进程应该覆盖其会话密钥环。
    keyctl_get_keyring_ID(KEY_SPEC_SESSION_KEYRING, 1);

    // 表示引导正在进行中,以通知后台固件加载程序等。
    close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));

    // 查看是否需要加载调试属性以允许在设备解锁时 adb root。
    const char* force_debuggable_env = getenv("INIT_FORCE_DEBUGGABLE");
    bool load_debug_prop = false;
    if (force_debuggable_env && AvbHandle::IsDeviceUnlocked()) {
        load_debug_prop = "true"s == force_debuggable_env;
    }
    unsetenv("INIT_FORCE_DEBUGGABLE"); // 清除环境变量

    // 当不需要时,卸载调试 ramdisk,以防止属性服务从中读取 .prop 文件。
    if (!load_debug_prop) {
        UmountDebugRamdisk();
    }

    PropertyInit(); // 初始化属性服务

    // 在属性服务读取 .prop 文件后,卸载第二阶段资源。
    UmountSecondStageRes();

    // 当需要时,在属性服务读取 .prop 文件后卸载调试 ramdisk。
    if (load_debug_prop) {
        UmountDebugRamdisk();
    }

    // 挂载第二阶段 init 所需的额外文件系统。
    MountExtraFilesystems();

    // 现在为第二阶段设置 SELinux。
    SelabelInitialize(); // 初始化 SELinux 标签
    SelinuxRestoreContext(); // 恢复 SELinux 上下文

    Epoll epoll;
    if (auto result = epoll.Open(); !result.ok()) {
        PLOG(FATAL) << result.error(); // 打开 epoll 失败,终止
    }

    // 在响应其他待处理函数之前,我们总是先收集子进程。
    // 这是为了防止竞争,其他守护进程在服务退出后看到并要求 init 通过 ctl.start 启动它,而 init 尚未收集它。
    epoll.SetFirstCallback(ReapAnyOutstandingChildren);

    InstallSignalFdHandler(&epoll); // 安装信号描述符处理程序
    InstallInitNotifier(&epoll); // 安装初始化通知程序
    StartPropertyService(&property_fd); // 启动属性服务

    // 记录初始化阶段启动时间,以便 bootstat 记录。
    RecordStageBoottimes(start_time);

    // 设置 libavb 版本以进行框架限定的 OTA 匹配(在 Treble 构建中)。
    if (const char* avb_version = getenv("INIT_AVB_VERSION"); avb_version != nullptr) {
        SetProperty("ro.boot.avb_version", avb_version);
    }
    unsetenv("INIT_AVB_VERSION"); // 清除环境变量

    fs_mgr_vendor_overlay_mount_all(); // 挂载所有 vendor overlay
    export_oem_lock_status(); // 导出 OEM 锁定状态
    MountHandler mount_handler(&epoll); // 挂载处理器
    SetUsbController(); // 设置 USB 控制器
    SetKernelVersion(); // 设置内核版本

    const BuiltinFunctionMap& function_map = GetBuiltinFunctionMap(); // 获取内置函数映射
    Action::set_function_map(&function_map); // 设置动作函数映射

    if (!SetupMountNamespaces()) {
        PLOG(FATAL) << "SetupMountNamespaces failed"; // 设置 mount namespaces 失败,终止
    }

    InitializeSubcontext(); // 初始化子上下文

    ActionManager& am = ActionManager::GetInstance(); // 获取动作管理器实例
    ServiceList& sm = ServiceList::GetInstance(); // 获取服务列表实例

    LoadBootScripts(am, sm); // 加载引导脚本

    // 默认情况下,启用此项并允许 INFO 日志被丢弃,将会增加 0.2s 的 Nexus 9 启动时间,因此默认禁用。
    if (false) DumpState(); // 打印状态信息

    // 在脚本开始运行之前,使 GSI 状态可用。
    auto is_running = android::gsi::IsGsiRunning() ? "1" : "0";
    SetProperty(gsi::kGsiBootedProp, is_running); // 设置 GSI 启动属性
    auto is_installed = android::gsi::IsGsiInstalled() ? "1" : "0";
    SetProperty(gsi::kGsiInstalledProp, is_installed); // 设置 GSI 已安装属性
    if (android::gsi::IsGsiRunning()) {
        std::string dsu_slot;
        if (android::gsi::GetActiveDsu(&dsu_slot)) {
            SetProperty(gsi::kDsuSlotProp, dsu_slot); // 设置 DSU 槽位属性
        }
    }

    // 将内置动作排入队列以启动 cgroups 设置等。
    am.QueueBuiltinAction(SetupCgroupsAction, "SetupCgroups");
    am.QueueBuiltinAction(SetKptrRestrictAction, "SetKptrRestrict");
    am.QueueBuiltinAction(TestPerfEventSelinuxAction, "TestPerfEventSelinux");
    am.QueueBuiltinAction(ConnectEarlyStageSnapuserdAction, "ConnectEarlyStageSnapuserd");
    am.QueueEventTrigger("early-init");

    // 将一个动作排入队列,等待冷启动完成,以便我们知道 ueventd 已设置所有 /dev...
    am.QueueBuiltinAction(wait_for_coldboot_done_action, "wait_for_coldboot_done");
    // ...以便我们可以开始排队需要来自 /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");

    // 触发所有引导动作以启动系统。
    am.QueueEventTrigger("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");

    // 在主循环之前恢复优先级。
    setpriority(PRIO_PROCESS, 0, 0);

    while (true) {
        // 默认情况下,睡眠直到发生事件。
        // 不要将 far_future 转换为 std::chrono::milliseconds,因为这会触发溢出。
        // boot_clock 的单位是 1ns。
        const boot_clock::time_point far_future = boot_clock::time_point::max();
        boot_clock::time_point next_action_time = far_future;

        auto shutdown_command = shutdown_state.CheckShutdown(); // 检查是否有关机命令
        if (shutdown_command) {
            LOG(INFO) << "Got shutdown_command '" << *shutdown_command
                      << "' Calling HandlePowerctlMessage()";
            HandlePowerctlMessage(*shutdown_command); // 处理关机消息
        }

        if (!(prop_waiter_state.MightBeWaiting() || Service::is_exec_service_running())) {
            am.ExecuteOneCommand(); // 执行一个命令
            // 如果还有更多工作要做,立即再次唤醒。
            if (am.HasMoreCommands()) {
                next_action_time = boot_clock::now();
            }
        }
        // 由于上面的代码检查了待处理的动作,因此在下面的 Epoll::Wait() 调用之前,不得有新的动作排队,
        // 而没有调用 WakeMainInitThread()。
        if (!IsShuttingDown()) {
            auto next_process_action_time = HandleProcessActions(); // 处理进程动作

            // 如果有需要重新启动的进程,请及时唤醒。
            if (next_process_action_time) {
                next_action_time = std::min(next_action_time, *next_process_action_time);
            }
        }

        std::optional<std::chrono::milliseconds> epoll_timeout;
        if (next_action_time != far_future) {
            epoll_timeout = std::chrono::ceil<std::chrono::milliseconds>(
                    std::max(next_action_time - boot_clock::now(), 0ns));
        }
        auto epoll_result = epoll.Wait(epoll_timeout); // 等待 epoll 事件
        if (!epoll_result.ok()) {
            LOG(ERROR) << epoll_result.error(); // 记录 epoll 错误
        }
        if (!IsShuttingDown()) {
            HandleControlMessages(); // 处理控制消息
            SetUsbController(); // 设置 USB 控制器
        }
    }

    return 0;
}

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()) {
        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);
    }
}
  1. InstallRebootSignalHandlers();捕获部分标准信号;
  2. SetStdioToDevNull(argv); 重定向stdin、stdout、stderr到 /dev/null(可以理解丢弃部分打印);
  3. InitKernelLogging(argv);、SelinuxSetupKernelLogging();初始化内核和selinux的日志;
  4. 设置Init进程和Init的子进程的OOM_ADJ值, ADJ值用来决定内存回收的优先顺序, ADJ值越大, 越容易被回收, 这里设置的是-1000, 最小的ADJ值;
  5. 卸载第二阶段的资源;
  6. 如果第7步允许adb root, 这个时候再卸载ramdisk
  7. 挂载额外的文件系统;
  8. 在第二阶段启动SELinux
  9. 新建Epoll,并初始化子线程信号的处理函数(TODO);
  10. StartPropertyService(&property_fd);启动property服务;
  11. 记录开机时间 ro.boottime.init ro.boottime.init.first_stage ro.boottime.init.selinux ro.boottime.init.modules
  12. 设置AVB版本;
  13. 设置USB控制器;
  14. 设置Kernel版本;
  15. 获取一些内置的命令(常见的bash命令),然后注入Action中,Action后续会直接执行这些命令;
  16. 设置Linux系统挂载命名空间,挂载命名空间是Linux系统的一个机制,可以隔离出一个具有独立挂载点信息的运行环境,各个命名空间内挂载互不影响;
  17. 初始化上下文,引导boot脚本;这里会从/system/etc/init/hw/init.rc、/system/etc/init等位置查找;
  18. 设置GSI可用并且设置GSI版本;
  19. 将相关动作、触发器等按队列插入ActionManager;
  20. 在主循环之前恢复优先级;
  21. 开启一个死循环,检查shutdown命令、执行actions,如果有命令需要执行,就计算下一个命令的时间,放入epoll,如果没有则会阻塞等待事件发生,以此完成事件型驱动;

启动zygote

init.rc文件中命令执行了start zygote

    start zygote

Q&A

Init进程启动分为几个阶段?每个阶段做了什么事情?

普遍认为2个阶段,但是从代码看也可以分为3个或者4个阶段。 第一阶段->selinux->第二阶段->uventd。第一个阶段,第一个阶段主要做了一些挂载基本文件系统、设置环境变量等的操作,第二阶段则挂在了额外的文件系统,使用epoll实现了一个事件驱动型的功能,加载和执行.rc文件中的命令;

Init如何启动zygote?

在init.rc文件中, on zygote-start的地方, 写了start zygote, start是android内置的命令,在解析rc文件之前就已经将这些方法放到代码中了,命令会执行start方法,将zygote拉起来;

Init如何解析.rc文件?

Init进程在第二阶段启动过程中,会调用LoadBootScripts方法,方法内部有Parser等工具类,将文件内容一一解析,然后会被放到actionmanager中执行。

PS:代码中epoll的使用和原理还不大懂,后续需要填坑…(哈哈,其实别的也很多不懂)

参考

  1. https://cs.android.com/android/platform/superproject/+/android14-release:
  2. 源码详解Android 9.0§ 系统启动流程之init进程(第二阶段)_android 开机第二阶段-优快云博客
深入研究源码:Android10.0系统启动流程(一) 前言 针对系统启动流程的专题研究也进行了差不多一周了,虽然进度缓慢,但也算是有一些阅读源码的心得,以及对源码的一点理解 由于系统启动流程涉及的源码相对较多,所以决定分几篇文章来写,本篇主要是对启动流程进行一个简单的概述,以及谈一下我个人的读源码的方法 正文 启动流程概述 这里说的启动流程是指Android系统的启动流程 让我们来回想一下,我们是如何“启动”一个全新未开封的Android手机的: 按电源键 进入开机动画 经过漫长的等待(大概几分钟?哈哈) 开机动画结束 正式开机,进入设置画面 进入系. 阅读详情

相关推荐

Vercel部署个人博客

先去注册一个账号,建议注册一个Github账号后,使用Github账号来登录Vercel。vercel 部署静态资源网站极其方便简单,并且有可观的访问速度,最主要的是免费部署。此时项目已经成功搭建完毕了,点击图片即可跳转到 vercel 所提供的二级域名访问。然后可以直接拉取我的仓库,所有已经配置好了,可以直接使用,建议fork成自己的!然后修改dns就行,注意vercel提供的域名可能访问不了,需要自己绑定!如果有自己的域名,还可以在 vercel 中进行设置。进入Vercel官网。

醉里博客202271.xyz 2360

Android源码分析 - init进程

本篇以android-11.0.0_r25作为基础解析PC启动会通过BIOS引导,从0x7c00处找到以0xaa55为结尾的引导程序启动。而Android通常使用在移动设备上,没有PC的BIOS,取而代之的是BootLoader。在CPU上电复位完成后,会从一个固定的地址加载一段程序,即BootLoader,不同的CPU可能这个地址不同。BootLoader是一段引导程序,其中最为常见的为U-boot,它一般会先检测用户是否按下某些特别按键,这些特别按键是uboot在编译时预先被约定好的,用于进入调试模式。

qq_34231329的博客 1236

汽车OBD接口实战:如何用STM32读取CAN总线数据(附完整代码)

本文详细介绍了如何利用STM32微控制器通过汽车OBD接口读取CAN总线数据的完整实战方案。内容涵盖硬件连接(如OBD引脚定义与TJA1050收发器接线)、STM32的CAN外设初始化与滤波器配置,并深入解析OBD-II诊断协议,演示了如何请求和解析发动机转速、车速等关键数据。文末提供了构建稳定数据读取系统的代码框架与错误处理建议,为嵌入式开发者和汽车电子爱好者提供了从零到一的实践指南。

9q8w7e6r5的博客 297

Android源码学习---init

Android源码之init源码分析,init解析zygote,Android属性服务

m0_56366502的博客 1092

【目标检测】yoloV5算法详解

一、与yoloV4相比,yoloV5的改进 输入端:在模型训练阶段,使用了Mosaic数据增强、自适应锚框计算、自适应图片缩放 基准网络:使用了FOCUS结构和CSP结构 Neck网络:在Backbone和最后的Head输出层之间插入FPN_PAN结构 Head输出层:训练时的损失函数GIOU_Loss,预测筛选框的DIOU_nns 二、yoloV5结构框架 CBL:CBL模块是由Conv+BN+Leaky_relu激活函数组成 Res unit:借鉴ResNet中的残差结构,用来构建深层网络,CBM是

qq_40373651的博客 2万+

DAC8568 Controller

DAC8568 ControllerDAC8568 简介串行接口三级目录 DAC8568 简介 PIN Description Name Description LDAC‾\overline{LDAC}LDAC load DACs SYNC‾\overline{SYNC}SYNC 低电平有效,电平触发控制, 该输入为输入数据的帧同步信号. 当SYNC为低,使能DAC内输入移位寄存器,并在随后的所有时钟沿采样数据。 在第32个时钟之后,DAC更新输出。 若不到32个时钟SYNC被置高(即

u010170039的博客 5130

Android P (9.0) 之Init进程源码分析

概述 众所周知,init进程是Android系统的第一个用户进程,Android启动流程大致如下: 今天我们来分析一下用户空间的第一个进程init进程。 init进程主要提供以下几个功能: 挂载文件系统、生成部分设备节点、创建目录 属性服务 处理子进程终止 分析和运行init.rc(等等rc文件) init进程代码路径为 system/core/init/init.cpp。今天我们主要是...

wangzaieee的博客 6146

基于学术研究风云卫星数据下载教程

目录 一、相关网址 1.登录注册网址:国家卫星气象中心 2.风云数据服务客户端下载网址 3.风云卫星遥感数据服务网址 二、下载数据步骤 1.首先在注册登录网址,注册一个账户 2.待账户审核后,登录账户即可搜索并下载数据 三、相关工具下载 1.登录风云卫星遥感数据服务网址 2.NCSA HDFView软件 3.风云卫星数据(L1)处理软件包(FENGYUN Toolkit v1.1) 4.静止卫星云图分区图显示软件(SECView) 5.风云二号静止气象卫星数据及产品处理..

珞瑜的博客 1万+

android13 init启动流程分析

该函数主要是对系统环境设置以及创建一些必要的目录和文件,并挂载一些文件系统。之后就下执行SetupSelinux函数。首先执行FirstStageMain函数,该函数位于system/core/init/first_stage_init.cpp。该函数位于system/core/init/init.cpp。这里会设置一些selinux的规则。也就是一些安全策略。从system/core/init/main.cpp分析其代码如下。这个函数主要是服务启动以及属性设置,同时解析init.rc文件等等。

jiong_1的博客 427

优秀的 Verilog/FPGA开源项目介绍(六)- MIPI

MIPI的协议我就不详细介绍了(可以查看<下面的二维码>),这里和开源方案比较接近的即使FPGA实现MIPI物理层(DPHY)中电气属性相关的几种方案:MIPI DPHY RX...

OpenFPGA的博客 4457

MOS管基础知识及应用电路Multisim仿真

本文系统介绍了MOS管的基础知识及其在电子电路中的典型应用。首先详细解析了MOS管的基本结构、分类和工作原理,包括NMOS、PMOS及CMOS的特性差异。重点阐述了MOS管的三种放大组态电路(共源、共漏、共栅)及其小信号模型分析。随后通过Multisim仿真验证了MOS管在防反接电路、开关电路、放大电路、逻辑门电路、电平转换电路和开关电源(BUCK、BOOST、BUCK-BOOST)等应用中的实际表现。文章还深入探讨了自给偏压和分压式偏置电路的设计要点。全文通过理论分析与仿真实验相结合的方式,全面展示了MO

XU157303764的博客 1483

Android中的init进程源码分析

init进程启动时main方法的具体执行流程详细注释

h397318057的博客 392

【医学图像处理】X-ray成像之X射线的产生

【医学图像处理】X-ray成像之X射线的产生 概述 电流流过钨丝,并将其加热。 随着加热,增加的能量使电子能够通过热电子发射从灯丝中释放出来。 电子被吸引到带正电的阳极,并以由管电位(电压)确定的最大能量撞击钨靶。 当电子轰击目标时,它们通过轫致辐射和特有的相互作用将能量转换为热量(99%)和X射线光子(1%)。 X射线光子以一束能量(X射线光谱)从光束中释放出来,构成X射线图像形成的基础。 设备 阴极 灯丝 用细的(0.2毫米)钨丝制成,因为钨: 原子序数高(A 184,Z 74) 是良好的热

ZhangPY的专栏 2357

从源码解析-Android系统启动流程概述 init进程zygote进程SystemServer进程启动流程

Android系统启动流程 启动流程 Loader Kernel Native Framework Application init进程 启动 rc文件规则 Actions Commands Service Options init.rc文件 init进程的功能 Zygote进程 main AndroidRuntime::start AndroidRuntime::startVm AndroidRuntime::sta

美丽心情 1210

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

Android 12系统源码_系统启动(一)init进程

结合Android12系统源码梳理系统启动过程中的init进程相关的源码

AfinalStone的专栏 1693

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

Android9.0(Pie)1号进程init的启动流程学习

1、引言 众嗦粥汁,无论是电脑还是手机,亦或是其他终端设备,其从上电到完全启动都有一套复杂的流程。本来是打算使用相对简单一些的android4.4版本代码来学习这一整个流程,但是由于工作生产环境从android4.4的kitkak变为了9.0的pie,所以直接用9.0的代码来学习,学习代码和熟悉9.0环境两不误,哦耶! 拿android系统的设备来说,由于其底层是linux内核,其从开机上电后,会先后经历加载boot、启动kernel等过程。(当然这个过程...

菜鸟博客 1508

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

android系统启动分析,Android系统启动流程源码分析

用Source Insight来追踪Android系统启动流程的源码,并将我认为重要的源码进行分析1、当系统引导程序启动Linux内核,内核会记载各种数据结构和驱动程序,加载完毕之后,Android系统开始启动并加载第一个用户级别的进程:Init.c(system\core\init)//Init.c中的main()方法int main(int argc, char **argv){//解析执行i...

weixin_42361608的博客 245

深入Android S(12.0)探索Framework之SystemServer进程的启动详解

SystemServer 作为 Zygote 进程 fork 出的第一个进程,其进程名为:system_server。其承载着整个 Framework 的核心服务,如创建并启动 ActivityManagerService、PackageManagerService、WindowManagerService、InputManagerService 等 90 多个核心系统服务。接下来一起深入学习一下 SystemServer 进程的创建与启动流程。

neuHenry 1251

Android启动过程分析

针对Android程序员,我这边给大家整理了一些资料,包括不限于高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!Android前沿技术大纲全套体系化高级架构视频Android高级架构资料、源码、笔记、视频。

2401_84149496的博客 322

【Android FrameWork】第五天:init加载RC文件

本文基于Android 12+源码,深入分析Init进程加载RC文件的机制。Init进程作为用户空间首个进程,通过解析RC文件(初始化脚本)控制系统启动流程。文章详细阐述了两个关键阶段:1)初始化解析环境(日志系统、文件系统挂载、解析器配置);2)RC文件解析过程(主文件解析、Import指令处理、Service/Action指令解析)。源码分析表明,Init通过Parser类实现指令映射,ServiceParser和ActionParser分别处理服务定义和动作指令,最终将配置存入全局管理类。该机制为理解

u012739527的博客 1850

Android中init与受精卵Zygote分析,以及添加自己开机启动的系统服务

最近在做云游戏的项目,由于需要购买硬件,系统提供Android源码需要自己编译。所以特地学习一下记录一下Android的启动(我的是AOSP7.1然后+厂商系统补丁) 先上图: 目录 一. 启动流程硬件开机上电后,系统硬件内会有一个小程序就是bootloader 二.bootloader会引导内核启动加载 三.内核就会去挂载 文件以及初始化设备。最重要的是启动Init进程(这个就是重点相关的) 1.init.rc 脚本是在 init.cpp 入口方法 main 中开始解析的 2 紧接...

陈柱的博客 780

Android 系统框架

init 进程启动过程Android系统启动流程Zygote启动流程及源码分析 APP启动流程init进程是Android用户空间第一个进程,主要做以下3件事情:Android 的AMS是什么 Android AMS源码解析Activity创建到View的显示过程PMS(PackageManagerService) 原理PMS处理APK的复制

朱建伟的专栏 525

深入了解Android卷 学习笔记 03

第三章 深入理解 init 3.1 概述

Chen_Yumin的博客 915

如何起步Framework框架?看完这些豁然开朗!

安卓的系统架构了解吗?有没有过Framework的开发经验?由此可见,Framwork在Android开发中占着举足轻重的地位。服务端、客户端、Linux驱动。它提供了软件开发的框架,使得开发过程更具简便性和稳定性。

m0_56144365的博客 672

Android学习之路(15) AMS与PMS详解

我们从系统启动 init 进程开始分析到 Zygote 进程创建、SystemServer 进程创建,大致的将整体流程进行了梳理。init 进程是系统内核启动后第二个启动的进程,该进程在用户空间。init 进程启动后会解析 init.rc 执行脚本启动 Zygote 进程和 ServiceManager 进程。Zygote 因为是由 init 进程启动的,所以它也是在用户空间。Zygote 的业务处理横跨 native 层和 java 层。startVm() 创建虚拟机。

qq_32907491的博客 3363
上一篇: 【Android经验技巧】 Android全部系统属性含义(长期更新中...)
下一篇: 【Android开发技巧】如何通过进程找到所在目录?
LokiCode
博客等级 码龄3年 839粉丝 18原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LokiCode

谢谢您买的奶茶

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值