AndroidT(13) init 进程 -- first stage init 的初始化 (二)

news2024/11/19 14:54:15

1.概览

  第一阶段的 init 工作主要用于读取系统启动阶段需要的配置信息(例如 linux的bootconfig,cmdline等配置信息)、挂载文件系统、安装 kernel 中的模块驱动,最后就是启动第二阶段的 init 来进行 Android 系统相关的组件。第一阶段的 init 被编译为静态的可执行程序,位于 ramdisk 中。在 kernel 启动后该 init 应用程序则会被运行。

2.构建必要的目录及设备

  这个步骤用于环境的初始化,大概分为下面几类
    a)环境变量的设置
    b)必要文件及设备的创建
    c)对文件及设备的访问权限配置,这里只有传统的 DAC(Discretionary Access Control) 还没启动DAC(Mandatory Access Control).对 DAC 和 MAC 有疑问的可以参考之前的博客《Android R(11)HIDL服务的sepolicy(五)》。

3.设置 log 的输出位置

  我们知道 Android 有自己的 log 系统,但此时它还是不可用的。所以需要将 log 导入到 kernel log 中去,方便调试。

//system\core\init\first_stage_init.cpp
FirstStageMain
    ...
    SetStdioToDevNull(argv);
    // Now that tmpfs is mounted on /dev and we have /dev/kmsg, we can actually
    // talk to the outside world...
    InitKernelLogging(argv);

4.kernel 驱动模块加载

4.1 所在位置

  走到 kernel 启动 init 进程这一阶段时,内存中的 ramdisk 内容则是由 vendor_boot ramdik 和 init_boot ramdisk 组成的,下面是官网给出的示意图,这部分工作则是在 bootloader 阶段完成的,在此不做赘述
在这里插入图片描述

  kernel 模块是位于 vendor_boot ramdik 的,所以有必要展示下 vendor_boot 的目录结构

├── bootconfig
├── dtb
├── vendor_ramdisk
|__ vendor-ramdisk-by-name

  vendor_ramdisk 的展开结构如下

├── acct
├── apex
├── config
├── data
├── data_mirror
├── debug_ramdisk
├── default.prop
├── dev
├── first_stage_ramdisk
│   └── fstab.s5e8835
├── init.recovery.s5e8835.rc
├── lib
│   └── modules
│       ├── first_module.ko
│       ├-- ...
│       ├── modules.alias
│       ├── modules.dep
│       ├── modules.load.recovery
│       ├── modules.softdep
│       └── last_module.ko
├── linkerconfig
├── metadata
├── mnt
├── odm
├── odm_dlkm
├── odm_file_contexts
├── odm_property_contexts
├── oem
├── plat_file_contexts
├── plat_property_contexts
├── plat_service_contexts
├── postinstall
├── proc
├── product_file_contexts
├── product_property_contexts
├── product_service_contexts
├── prop.default
├── res
│   └── images
│       ├── ...
│       └── stage_fill.png
├── sdcard
├── second_stage_resources
├── sepolicy
├── storage
├── sys
├── system
│   ├── bin
│   ├── ├── ...
│   │   └── ziptool
│   ├── etc
│   │   ├── cgroups.json
│   │   ├── init
│   │   │   ├── hw
│   │   │   │   └── init.rc
│   │   │   └── servicemanager.recovery.rc
│   │   ├── ld.config.txt
│   │   ├── mke2fs.conf
│   │   ├── recovery.fstab
│   │   ├── security
│   │   │   └── otacerts.zip
│   │   └── ueventd.rc
│   └── lib64
│       ├── ...
│       ├── android.hardware.health-V1-ndk.so
│       ├── hw
│       │   ├── ...
│       │   └── android.hardware.health@2.0-impl-default.so
│       ├── ...
│       └── libz.so
├── system_dlkm
├── system_ext_file_contexts
├── system_ext_property_contexts
├── system_ext_service_contexts
├── tmp
├── vendor
├── vendor_dlkm
├── vendor_file_contexts
├── vendor_property_contexts
└── vendor_service_contexts

4.2 模块目录统计策略

  kernel 中的模块加载则是在这一阶段进行的,下面是对应的代码

//system\core\init\first_stage_init.cpp
FirstStageMain
    //code 1
    if (!LoadKernelModules(IsRecoveryMode() && !ForceNormalBoot(cmdline, bootconfig), want_console, want_parallel, module_count))
    //code 2
    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";
    }

  code 1 中,LoadKernelModules 只支持对目录 /lib/modules 深度为 1 的模块的加载

//system\core\init\first_stage_init.cpp
#define MODULE_BASE_DIR "/lib/modules"
bool LoadKernelModules(bool recovery, bool want_console, bool want_parallel, int& modules_loaded)
    std::unique_ptr<DIR, decltype(&closedir)> base_dir(opendir(MODULE_BASE_DIR), closedir);
    std::vector<std::string> module_dirs;
    while ((entry = readdir(base_dir.get()))) {
        if (entry->d_type != DT_DIR) {
            continue;
        }
        sscanf(entry->d_name, "%d.%d", &dir_major, &dir_minor)
        module_dirs.emplace_back(entry->d_name);
    }

  可以看到,它只统计 /lib/modules 目录下的第一级目录,并记录在 module_dirs vector 中,不会递归的查找。

4.3 模块的加载策略

  先看下整体的代码逻辑

//system\core\init\first_stage_init.cpp
#define MODULE_BASE_DIR "/lib/modules"
FirstStageMain
    for (const auto& module_dir : module_dirs) {
        //code 1
        std::string dir_path = MODULE_BASE_DIR "/";
        dir_path.append(module_dir);
        //code 2
        Modprobe m({dir_path}, GetModuleLoadList(recovery, dir_path));
        //code 3
        bool retval = m.LoadListedModules(!want_console);
        //code 4
        modules_loaded = m.GetModuleCount();
    }

  可见整体的代码逻辑还是相当清晰的,遍历 module_dirs 中所有的目录,对每一个目录调用 Modprobe 工具类来加载。
  下面先看看工具类 Modprobe,它是独立的一个库 libmodprobe

//system\core\libmodprobe\Android.bp
cc_library_static {
    name: "libmodprobe",
    vendor_available: true,
    ramdisk_available: true,
    recovery_available: true,
    srcs: [
        "libmodprobe.cpp",
        "libmodprobe_ext.cpp",
    ],
    shared_libs: [
        "libbase",
    ],
    export_include_dirs: ["include/"],
}

  code 2 中 GetModuleLoadList 的定义如下

std::string GetModuleLoadList(bool recovery, const std::string& dir_path) {
    auto module_load_file = "modules.load";
    if (recovery) {
        struct stat fileStat;
        std::string recovery_load_path = dir_path + "/modules.load.recovery";
        if (!stat(recovery_load_path.c_str(), &fileStat)) {
            module_load_file = "modules.load.recovery";
        }
    }
    return module_load_file;
}

  如果是 recovery 模式那么就使用 modules.load.recovery 反之则是modules.load。所以对于正常启动此处会返回 modules.load。下面来看看它的构造过程

Modprobe m({dir_path}/*base_paths*/, "modules.load"/*load_file*/);
Modprobe::Modprobe(const std::vector<std::string>& base_paths, const std::string load_file, bool use_blocklist)
    for (const auto& base_path : base_paths) {
        //code 2-1
        auto alias_callback = std::bind(&Modprobe::ParseAliasCallback, this, _1);
        ParseCfg(base_path + "/modules.alias", alias_callback);
        ...
        //code 2-2
        auto dep_callback = std::bind(&Modprobe::ParseDepCallback, this, base_path, _1);
        ParseCfg(base_path + "/modules.dep", dep_callback);
        ...
        //code 2-3
        auto softdep_callback = std::bind(&Modprobe::ParseSoftdepCallback, this, _1);
        ParseCfg(base_path + "/modules.softdep", softdep_callback);
        ...
        //code 2-4
        auto load_callback = std::bind(&Modprobe::ParseLoadCallback, this, _1);
        ParseCfg(base_path + "/" + load_file, load_callback);
        ...
        //code 2-5
        auto options_callback = std::bind(&Modprobe::ParseOptionsCallback, this, _1);
        ParseCfg(base_path + "/modules.options", options_callback);
        ...
        //code 2-6
        auto blocklist_callback = std::bind(&Modprobe::ParseBlocklistCallback, this, _1);
        ParseCfg(base_path + "/modules.blocklist", blocklist_callback);
    }

  Modprobe 构造方法中仅仅只是将各个文件的内容进行解析并存入对于的变量中去,供后续使用。下面则列出他们最终被存入的变量
  code 2-1

//\system\core\libmodprobe\libmodprobe.cpp
ParseAliasCallback
    this->module_aliases_.emplace_back(alias, module_name);

  code 2-2

ParseDepCallback
    deps.push_back(prefix + *arg);
    this->module_deps_[canonical_name] = deps;

  code 2-3

ParseSoftdepCallback
    while (it != args.end()) {
        if (state == "pre:") {
            this->module_pre_softdep_.emplace_back(module, token);
        } else {
            this->module_post_softdep_.emplace_back(module, token);
        }
    }

  code 2-4

ParseLoadCallback
    this->module_load_.emplace_back(canonical_name);

  code 2-5

ParseOptionsCallback
    auto [unused, inserted] = this->module_options_.emplace(canonical_name, options);

  code 2-6

ParseBlocklistCallback
    this->module_blocklist_.emplace(canonical_name);

  code 3 中,就会根据上面初始化的值进行 kernel 模块的加载了,其实现如下

//system\core\libmodprobe\libmodprobe.cpp
bool retval = m.LoadListedModules(!want_console);
    auto ret = true;
    for (const auto& module : module_load_) {
        if (!LoadWithAliases(module, true)) {
            if (IsBlocklisted(module)) continue;
            ret = false;
            if (strict) break;
        }
    }
    return ret;

  LoadWithAliases 里面涉及的模块则是来自于文件 /lib/modules/modules.alias。

//system\core\libmodprobe\libmodprobe.cpp
Modprobe::LoadWithAliases(const std::string& module_name, ...)
    std::set<std::string> modules_to_load = {canonical_name};
    for (const auto& [alias, aliased_module] : module_aliases_) {
        ...
        modules_to_load.emplace(aliased_module);
    }

    for (const auto& module : modules_to_load) {
        if (InsmodWithDeps(module, parameters))
            module_loaded = true;
    }

  将事先准备好的 module_aliases_ 内容过滤下再调用 InsmodWithDeps 进行安装。

//system\core\libmodprobe\libmodprobe.cpp
InsmodWithDeps
    auto dependencies = GetDependencies(module_name);
    for (auto dep = dependencies.rbegin(); dep != dependencies.rend() - 1; ++dep)
        LoadWithAliases(*dep, true)
    // load target module itself with args
    Insmod(dependencies[0], parameters)

  从上面代码也可以看出来,在安装模块前,则会先安装他们的依赖。最后再安装 当前 kernel 模块。

4.4 加载时间的统计

  加载时间在优化系统启动时间时还是很重要的,kernle 模块的加载可以通过搜索 如下 log 查看

kernel modules took

  它的实现如下

    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";
    }

  可以看到,此时间还被记录到环境变量中去了 kEnvInitModuleDurationMs

//system\core\init\first_stage_init.h
static constexpr char kEnvInitModuleDurationMs[] = "INIT_MODULE_DURATION_MS";

5.属性文件的处理

//system\core\init\second_stage_resources.h
constexpr const char kSecondStageRes[] = "/second_stage_resources";
constexpr const char kBootImageRamdiskProp[] = "/system/etc/ramdisk/build.prop";
//system\core\init\first_stage_init.cpp
FirstStageMain
    if (access(kBootImageRamdiskProp, F_OK) == 0) {
        //system\core\init\second_stage_resources.h
        std::string dest = GetRamdiskPropForSecondStage();
            //return /second_stage_resources/system/etc/ramdisk/build.prop
            return std::string(kSecondStageRes) + kBootImageRamdiskProp;
        std::string dir = android::base::Dirname(dest);
        fs::copy_file(kBootImageRamdiskProp, dest, ec)
    }

  对于 ramdisk 中的属性文件,最终也会反应到 Android 的属性服务中去的。但是 ramdisk 在 Android 启动后会被卸载掉,所以这里需要做下拷贝操作。
  它的路径如下

/second_stage_resources/system/etc/ramdisk/build.prop

6.是否允许root

//system\core\init\second_stage_resources.h
constexpr const char kDebugRamdiskProp[] = "/debug_ramdisk/adb_debug.prop";
//system\core\init\first_stage_init.cpp
FirstStageMain
    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";
        if (access(adb_debug_prop_src, F_OK) == 0 &&
            !fs::copy_file(adb_debug_prop_src, kDebugRamdiskProp, ec))
        if (access(userdebug_plat_sepolicy_cil_src, F_OK) == 0 &&
            !fs::copy_file(userdebug_plat_sepolicy_cil_src, kDebugRamdiskSEPolicy, ec))
        // setenv for second-stage init to read above kDebugRamdisk* files.
        setenv("INIT_FORCE_DEBUGGABLE", "true", 1);
    }

  所以只要 ramdisk 中存在 /force_debuggable 就代表着要开启调试模式,这也意味着adb是可用的,并且是支持 root 的。

7.切换根目录

FirstStageMain
    if (ForceNormalBoot(cmdline, bootconfig)) {
        mkdir("/first_stage_ramdisk", 0755);
        PrepareSwitchRoot();
        // SwitchRoot() must be called with a mount point as the target, so we bind mount the
        // target directory to itself here.
        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");
    }

  代码很简单,提出来是为了提示接下去根目录就变化了。

8.根据配置挂在第一阶段的文件系统

FirstStageMain
    //system\core\init\first_stage_mount.cpp
    DoFirstStageMount(!created_devices)
        //code 1
        auto fsm = FirstStageMount::Create();
        //code 2
        (*fsm)->DoCreateDevices()
        //code 3
        return (*fsm)->DoFirstStageMount();

  可以看到流程很清晰,下面来看看他们的具体实现。

8.1 code 1

//system\core\init\first_stage_mount.cpp
auto fsm = FirstStageMount::Create();
    auto fstab = ReadFirstStageFstab()
        //Fstab fstab;
        if (!ReadFstabFromDt(&fstab)) {
            if (ReadDefaultFstab(&fstab)) {
                fstab.erase(std::remove_if(fstab.begin(), fstab.end(),
                                        [](const auto& entry) {
                                            return !entry.fs_mgr_flags.first_stage_mount;
                                        }),
                            fstab.end());
            }
        }
        return fstab;
    return std::make_unique<FirstStageMountVBootV2>(std::move(*fstab));

  可以看出,挂在表如果在 Deivce tree 中获取到了,那么就不再调用 ReadDefaultFstab,两者是互斥的。我们使用 ReadDefaultFstab 来获取,所以下面来看看他的实现

//system\core\fs_mgr\fs_mgr_fstab.cpp
ReadDefaultFstab(Fstab* fstab)
    ReadFstabFromDt(fstab, false /* verbose */);
    default_fstab_path = GetFstabPath();
    Fstab default_fstab;
    ReadFstabFromFile(default_fstab_path, &default_fstab)
    for (auto&& entry : default_fstab) {
        fstab->emplace_back(std::move(entry));
    }
    return true;

  我们依然忽略掉从 Device tree 中获取的部分,看下查找 fs 文件的顺序

//system\core\fs_mgr\fs_mgr_fstab.cpp
GetFstabPath
    for (const char* prop : {"fstab_suffix", "hardware", "hardware.platform"}) {
        std::string suffix;
        //a
        if (!fs_mgr_get_boot_config(prop/*key*/, &suffix/*out_val*/)) continue;
        //b
        for (const char* prefix : {// late-boot/post-boot locations
                                   "/odm/etc/fstab.", "/vendor/etc/fstab.",
                                   // early boot locations
                                   "/system/etc/fstab.", "/first_stage_ramdisk/system/etc/fstab.",
                                   "/fstab.", "/first_stage_ramdisk/fstab."}) {
            //c
            std::string fstab_path = prefix + suffix;
            if (access(fstab_path.c_str(), F_OK) == 0) {
                return fstab_path;
            }
        }
    }

  从上面的代码 c 处可以看到,路径由前缀和后缀构成。
  前缀的实现代码如下

//system\core\fs_mgr\fs_mgr_boot_config.cpp
fs_mgr_get_boot_config(const std::string& key, std::string* out_val)
    // next, check if we have "ro.boot" property already
    *out_val = android::base::GetProperty("ro.boot." + key, "");
    if (!out_val->empty()) {
        return true;
    }
    // next, check if we have the property in bootconfig
    if (fs_mgr_get_boot_config_from_bootconfig_source(key, out_val)) {
        return true;
    }

    // finally, fallback to kernel cmdline, properties may not be ready yet
    if (fs_mgr_get_boot_config_from_kernel_cmdline(key, out_val)) {
        return true;
    }

  可以看出,在每一个key中获取后缀的顺序如下并且任何一个获取到后就立即返回,以该值为准,属性值->bootconfig->kernel cmdline。
  一般来说,到 ro.boot.hardware 属性的获取就可以确定了,如下

flg1080:/ # getprop ro.boot.hardware
staf1080

  那么此时的后缀就为 staf1080。
  后缀确定后,也就很好确定获取文件系统挂在顺序了
    1) /odm/etc/fstab.staf1080
    2) /vendor/etc/fstab.staf1080
    3) /system/etc/fstab.staf1080
    4) /first_stage_ramdisk/system/etc/fstab.staf1080
    5) /fstab.staf1080
    6) /first_stage_ramdisk/fstab.staf1080
  注意了,上面的路径是按先后顺序只取一例,不做糅合的。
  最后 Create 返回的是 FirstStageMountVBootV2 数据结构,看看它的类图
在这里插入图片描述

  再看看它的构造

return std::make_unique<FirstStageMountVBootV2>(std::move(*fstab));
    //system\core\init\first_stage_mount.cpp
    FirstStageMountVBootV2((Fstab fstab)
        ...
        for (const auto& entry : fstab_) {
            if (!entry.vbmeta_partition.empty()) {
                vbmeta_partitions_.emplace_back(entry.vbmeta_partition);
            }
        }
        ...

  构造方法中都是 vbmeta 的内容,这部分属于安全的内容,在此则不做赘述。

8.2 code 2

//system\core\init\first_stage_mount.cpp
(*fsm)->DoCreateDevices()
||
FirstStageMount::DoCreateDevices()
    InitDevices()
        GetSuperDeviceName(&devices);
        GetDmVerityDevices(&devices)
        InitRequiredDevices(std::move(devices))
            //system\core\init\block_dev_initializer.cpp
            return block_dev_init_.InitDevices(std::move(devices));
    CreateLogicalPartitions

  其中 InitDevices 做的也很简单,从 fstab 中获取到各个分区的设备信息,然后 polling 等待 uevnet信息,如果是对应的block设备事件则调用它的处理方法进行处理

//system\core\init\devices.cpp
HandleDevice
    if (action == "add" || (action == "change" && StartsWith(devpath, "/dev/block/dm-"))){
        ...
    }
    if (action == "remove") {
        ...
    }

  CreateLogicalPartitions最终调用 android::fs_mgr::CreateLogicalPartitions 进行处理

//system\core\fs_mgr\fs_mgr_dm_linear.cpp
CreateLogicalPartitions(const std::string& block_device)
    return CreateLogicalPartitions(*metadata.get(), block_device);
        ...

8.3 code 3

  最后开始工具 fs配置文件进行文件系统挂载了

return (*fsm)->DoFirstStageMount();
    //system\core\init\first_stage_mount.cpp
    return MountPartitions()
        TrySwitchSystemAsRoot
        for (auto current = fstab_.begin(); current != fstab_.end();) {
            MountPartition(current, false /* erase_same_mounts */, &end)
        }

        for (const auto& entry : fstab_) {
            if (entry.fs_type == "overlay") {
                fs_mgr_mount_overlayfs_fstab_entry(entry);
            }
        }

        // If we don't see /system or / in the fstab, then we need to create an root entry for
        // overlayfs.
        if (!GetEntryForMountPoint(&fstab_, "/system") && !GetEntryForMountPoint(&fstab_, "/")) {
            FstabEntry root_entry;
            if (GetRootEntry(&root_entry)) {
                fstab_.emplace_back(std::move(root_entry));
            }
        }

        MapScratchPartitionIfNeeded(&fstab_, init_devices);
        fs_mgr_overlayfs_mount_all(&fstab_);

9.启动下一阶段的 init

  在完成这一次的使命后,first init 也到了该退出的时候了。

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

  execv的关键点则是会使用 path 指向的可执行程序替换当前进程已在执行的程序,用人话讲就是 first init 被 /system/bin/init 替换了,下面就会执行 /system/bin/init 中的 main 入口方法了。
  所以上面代码等价于在 shell 中执行如下命令,并且它的 pid 保持不变及为 1

/system/bin/init selinux_setup

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/633457.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《微服务实战》 第三十章 分布式事务框架seata TCC模式

前言 本章节介绍分布式事务框架seata TCC模式&#xff0c;上一章节介绍seata以及集成到Springboot、微服务框架里。 1、TCC模式 一个分布式的全局事务&#xff0c;整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的&#xff0c;分支事务要满足 两阶段提交 的模型要…

如何利用ChatGPT写毕业论文

如何利用ChatGPT写毕业论文 ChatGPT是什么&#xff1f;利用ChatGPT写毕业论文的步骤1.准备数据2.训练模型3.生成论文4.检查论文 总结地址 ChatGPT是什么&#xff1f; ChatGPT是一个基于GPT-2模型的开源聊天机器人&#xff0c;它可以回答用户的问题&#xff0c;进行闲聊和提供各…

或许是一个新的算法方向?

动动发财的小手&#xff0c;点个赞吧&#xff01; 今日谷歌 DeepMind 使用深度强化学习发现更快的排序算法&#xff0c;相关论文[1]成果已经发表在Nature上。 据报道&#xff1a;该算法可以提速 70&#xff05;&#xff0c;相比之下&#xff0c;快了3倍之多。 摘要 排序或散列等…

230611-通过Doxygen实现项目代码的文档自动化生成(Mac+Win通用)

背景介绍 目前主流的Python项目的文档管理多通过Sphinx实现&#xff1b;当前Sphinx尚未有针对C#等代码的插件&#xff1b;若想对C#的项目代码进行Sphinx的管理&#xff0c;可通过Doxygen导出为xml文件&#xff0c;进行二次转换&#xff1b;有关Doxygen的介绍及使用&#xff0c…

Java使用Opencv进行大图找小图并使用其找图功能进行bilibili视频下载案例

Java使用Opencv进行大图找小图并使用其找图功能进行bilibili视频下载案例 一、Opencv大图找小图说明二、Opencv的window安装1.下载windows下的安装包2.安装3.Java中Opencv加载测试 三、Java中通过Opencv进行模板匹配大图找小图四、进行多图查找五&#xff1a;案例下载bilibili视…

碳排放预测模型 | Python实现基于机器学习回归分析的碳排放预测模型——随机森林、决策树、KNN 和多层感知器 (MLP) 预测分析

文章目录 效果一览文章概述研究内容环境准备源码设计KNNRandom ForestDecision TreeMLPModel Evaluation学习总结参考资料效果一览

【Android开发基础】随机点名系统(关于读取xml资源文件)

文章目录 一、引言二、设计1、读取xml2、下拉框Spinner3、随机算法 三、实施1、子元素随机&#xff08;单位&#xff1a;班级&#xff09;2、父元素随机&#xff08;单位&#xff1a;专业&#xff09;3、指定人数随机4、指定人数混合排序 四、附件 一、引言 描述&#xff1a;这…

【手撕MyBatis源码】动态SQL全流程解析

文章目录 动态SQL概述ifchoose(when、otherwise)trim&#xff08;where、set&#xff09;foreach OGNL表达式BoundSql动态SQL主流程分析SqlNodeDynamicContext源码解析StaticTextSqlNodeTextSqlNodeIfSqlNodeChooseSqlNodeForEachSqlNode 动态脚本结构动态脚本执行 SqlSourceSt…

Spring Cloud - Eureka原理、注册、搭建、应用(全过程详解)

目录 一、Eureka 注册原理 1.1、为什么要使用 Eureka 1.2、Eureka 的工作流程及原理 1.3、eureka 的作用 二、具体实现 2.1、搭建注册中心 2.2、服务注册和部署 2.2.1、user-service 服务注册 2.2.2、服务部署 2.2.3、order-service 服务注册 2.2.4、验证服务 2.3、…

java SSM 药品集中管理系统myeclipse开发mysql数据库springMVC模式java编程计算机网页设计

一、源码特点 java SSM 药品集中管理系统是一套完善的web设计系统&#xff08;系统采用SSM框架进行设计开发&#xff0c;springspringMVCmybatis&#xff09;&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代 码和数据库&#xff0c;系统主要采…

[神经网络]迁移学习-微调

一、概述 一般的有监督迁移学习分为以下三种&#xff1a; ①将训练好的模型作为特征抽取模块&#xff08;例如以resnet进行特征提取&#xff09; ②在一个相关的任务中训练后直接后直接使用(例如gpt) ③在训练好的模型基础上进行微调 此外还有无监督学习的方式 zero-shot&#…

【集群】LVS+Keepalived群集

文章目录 前言一、Keepalived的概念1. Keepalived 概述2. Keepalived 作用3. Keepalived 实现原理剖析3.1 Keepalived 工作原理3.1 VRRP协议&#xff08;虚拟路由冗余协议&#xff09; 4. Keepalived 主要模块及其作用4.1 健康检查方式&#xff08;学名&#xff1a;探针&#x…

【架构基础】正交设计四原则

数学中的正交&#xff0c;是指相互垂直的两个向量&#xff0c;简单来讲就是平面上的两个垂直线段&#xff0c;其中一个线段变长或减短或者转圈圈&#xff0c;另外一根是不变的也不影响它们的垂直度的。表现为空间的独立性&#xff0c;在软件中我们可以理解为两个只有交叉点而互…

springboot0+java+vuie个人家庭财务理财系统

。本文介绍了个人理财系统的开发全过程。通过分析个人理财系统管理的不足&#xff0c;创建了一个计算机管理个人理财系统的方案。文章介绍了个人理财系统的系统分析部分&#xff0c;包括可行性分析等&#xff0c;系统设计部分主要介绍了系统功能设计和数据库设计。 本个人理财系…

【数据湖架构】在 Azure Data Lake Storage (ADLS)二代上构建数据湖

介绍 一开始&#xff0c;规划数据湖似乎是一项艰巨的任务——决定如何最好地构建数据湖、选择哪种文件格式、是拥有多个数据湖还是只有一个数据湖、如何保护和管理数据湖。并非所有这些都需要在第一天回答&#xff0c;有些可能通过反复试验来确定。构建数据湖没有明确的指南&am…

【C++】一文带你吃透C++多态

&#x1f34e; 博客主页&#xff1a;&#x1f319;披星戴月的贾维斯 &#x1f34e; 欢迎关注&#xff1a;&#x1f44d;点赞&#x1f343;收藏&#x1f525;留言 &#x1f347;系列专栏&#xff1a;&#x1f319; C/C专栏 &#x1f319;那些看似波澜不惊的日复一日&#xff0c;…

【链表part02】| 24.两两交换链表中的节点、19.删除链表的倒数第N个节点、02.07.链表相交、142.环形链表

目录 ✿LeetCode24.两两交换链表中的节点❀ ✿LeetCode19.删除链表的倒数第N个节点❀ ✿LeetCode面试题 02.07. 链表相交❀ ✿LeetCode142.环形链表||❀ ✿LeetCode24.两两交换链表中的节点❀ 链接&#xff1a;24.两两交换链表中的节点 给你一个链表&#xff0c;两两交换其…

轻骑逐单于,大雪满弓刀:华为分布式存储的一骑绝尘

唐代诗人卢纶&#xff0c;有一首脍炙人口的《和张仆射塞下曲》&#xff0c;“月黑雁飞高&#xff0c;单于夜遁逃。欲将轻骑逐&#xff0c;大雪满弓刀。”诗中的慷慨激昂&#xff0c;热血炙烈&#xff0c;千年来让无数国人心魂激荡。 时代变迁&#xff0c;岁月迁移&#xff0c;今…

LeetCode面向运气之Javascript—第20题-有效的括号-95.97%

LeetCode第20题-有效的括号 题目要求 给定一个只包括 ‘(’&#xff0c;‘)’&#xff0c;‘{’&#xff0c;‘}’&#xff0c;‘[’&#xff0c;‘]’ 的字符串 s &#xff0c;判断字符串是否有效。 有效字符串需满足&#xff1a; 左括号必须用相同类型的右括号闭合。左括号…

卑微小测试的一天----自动生成正交法测试用例

前言 工作过程中&#xff0c;我们接触到需求后第一要务是 熟悉需求并且输出测试用例&#xff0c;针对接口测试的入参测试&#xff0c;需要校验大量入参的组合场景&#xff0c;这时我们通常采用正交法来设计测试用例&#xff0c;在减少测试用例的数量时&#xff0c;同时保障测试…