Android P之init进程启动源码分析指南之二



前言

  在上一篇章Android P之init进程启动源码分析指南之一中我们讲解了init启动的第一阶段工作,在第一阶段中init主要做了如下几方面的工作:

  • ueventd/watchdogd跳转判断以及其它初始化
  • 创建并挂载相关的文件系统
  • 初始化内核Log系统
  • 文件系统挂载
  • SELinux Init初始化
  • 第一阶段收尾和第二阶段准备工作

在本篇章中我们将要讲解init的第二阶段相关内容。

注意:本文演示的代码是Android P高通msm8953平台源码。涉及的源码如下:

system/core/init/init.cpp
system/core/libkeyutils/include/keyutils.h
system/core/libkeyutils/keyutils.cpp
bionic/libc/bionic/system_property_api.cpp
bionic/libc/include/sys/_system_properties.h
bionic/libc/system_properties/system_properties.cpp
system/core/init/util.cpp
external/selinux/libselinux/src/android/android_platform.c
system/core/init/selinux.cpp
system/core/init/init/sigchld_handler.h
system/core/init/init/sigchld_handler.cpp
system/core/init/init/init.cpp


一. Init启动第二阶段(用户态)

  通过前面的篇章我们了解到init启动的第一阶段还处于内核态,而进入init第二阶段则进入了用户态,nit进程的第二阶段仍然从main函数开始入手(继续分析main函数剩余源码)。好吗,让我们开干,read the fucking code!其内核打印流程日志大致如下:

14,1287,13153937,-;init: init second stage started!
14,1288,13212668,-;init: Using Android DT directory /proc/device-tree/firmware/android/
14,1289,13221989,-;selinux: SELinux: Loaded file_contexts\x0a
14,1290,13222060,-;init: Running restorecon...
11,1291,13254676,-;init: waitid failed: No child processes
12,1292,13272393,-;init: Couldn't load property file '/odm/default.prop': open() failed: No such file or directory: No such file or directory
14,1293,13280671,-;init: Created socket '/dev/socket/property_service', mode 666, user 0, group 0
14,1294,13286239,-;init: Forked subcontext for 'u:r:vendor_init:s0' with pid 409
14,1295,13293752,-;init: Forked subcontext for 'u:r:vendor_init:s0' with pid 410
14,1296,13298801,-;init: Parsing file /init.rc...
14,1297,13448446,-;ueventd: ueventd started!
14,1298,13455793,-;selinux: SELinux: Loaded file_contexts\x0a

1.1 创建进程会话密钥

int main(int argc, char** argv) {......// ueventd 和 watchdog 跳转的判断以及其它初始化的设置bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr);//跳过这个阶段if (is_first_stage) {......}// At this point we're in the second stage of init.InitKernelLogging(argv);//初始化内核log系统,具体参见篇章一的介绍LOG(INFO) << "init second stage started!";// Set up a session keyring that all processes will have access to. It// will hold things like FBE encryption keys. No process should override// its session keyring.keyctl_get_keyring_ID(KEY_SPEC_SESSION_KEYRING, 1);//初始化进程会话密钥// Indicate that booting is in progress to background fw loaders, etc.close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));//创建 /dev/.booting 文件,就是个标记,表示booting进行中......
}

  由于我么在init启动的第一阶段设置了INIT_SECOND_STAGE 值,所以会跳过is_first_stage直接进入第二阶段,InitKernelLogging的功能在第一阶段介绍了就不介绍了,这里我们简单分析一下keyctl_get_keyring_ID函数的作用,通过它的注释作用大概如下:

设置所有进程都可以访问的会话密钥环。 它将保留FBE加密密钥之类的内容。 不应覆盖任何过程其会话密钥环。

1.1.1 keyctl_get_keyring_ID

  本代码源码路径如下system/core/libkeyutils/keyutils.cpp,其基本逻辑如下:

// Deliberately not exposed. Callers should use the typed APIs instead.
static long keyctl(int cmd, ...) {va_list va; //va_start,va_arg,va_end是配合使用的,用于将可变参数从堆栈中读取出来va_start(va, cmd);//va_start是获取第一个参数地址unsigned long arg2 = va_arg(va, unsigned long);//va_arg 遍历参数unsigned long arg3 = va_arg(va, unsigned long);unsigned long arg4 = va_arg(va, unsigned long);unsigned long arg5 = va_arg(va, unsigned long);va_end(va);//val_end va_end 恢复堆栈return syscall(__NR_keyctl, cmd, arg2, arg3, arg4, arg5);//syscall内核调用
}key_serial_t keyctl_get_keyring_ID(key_serial_t id, int create) {return keyctl(KEYCTL_GET_KEYRING_ID, id, create);
}//将参数带入得到如下代码逻辑
keyctl(KEYCTL_GET_KEYRING_ID, KEY_SPEC_SESSION_KEYRING, 1);

  这里使用到的是内核提供给用户空间使用的 密钥保留服务 (key retention service),它的主要意图是在 Linux 内核中缓存身份验证数据。远程文件系统和其他内核服务可以使用这个服务来管理密码学、身份验证标记、跨域用户映射和其他安全问题。它还使 Linux 内核能够快速访问所需的密钥,并可以用来将密钥操作(比如添加、更新和删除)委托给用户空间。关于这块的介绍具体参考博客Linux 密钥保留服务入门,里面有比较详细的介绍。

  让我们将参数带入,会得到下面的代码逻辑,其中各个参数逻辑如下:

  • KEYCTL_GET_KEYRING_ID:表示通过第二个参数的类型获取当前进程的密钥信息
  • KEY_SPEC_SESSION_KEYRING:表示获取当前进程的 SESSION_KEYRING
  • 1:表示如果获取不到就新建一个会话密钥环

可以看到keyctl最后是通过syscall 与系统内核的通讯密钥管理工具交互的,这个大家了解一下即可。


1.2 初始化属性服务以及加载各种属性域

  Android property 系统其实可以理解为键值对:属性名字和属性值。大部分 property是记录在某些文件中的, init 进程启动的时候,会加载这些文件,完成 property 系统初始化工作。其代码逻辑如下:

    property_init();//初始化属性域// 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();//读取设备树(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);

1.2.1 property_init 初始化属性域

  该函数主要作用是初始化属性域,在Android平台中,为了让运行中的所有进程共享系统运行时所需要的各种设置值,系统开辟了属性存储区域,并提供了访问该区域的API。初始化这个过程有点多,不是本章讨论的重点,大家可以参见下面的关系调用图分析,这篇博客Android O属性服务源码详解写得不错,各位可以看看。
在这里插入图片描述

1.2.2 process_kernel_dt

  该函数定义在system/core/init/init.cpp中,其主要作用是读取设备树(DT)上的属性设置信息,在Android P系统中device-tree 的目录为/proc/device-tree/firmware/android/compatible,如下所示:

E800:/proc/device-tree/firmware/android # ls
compatible fstab name vbmeta
E800:/proc/device-tree/firmware/android #

然后按照一定规则查找系统属性,然后通过 property_set 设置系统属性。

static void process_kernel_dt() {// 判断 /proc/device-tree/firmware/android/compatible 文件中的值是否为 android,firmwareif (!is_android_dt_value_expected("compatible", "android,firmware")) {return;}//kAndroidDtDir的值为/proc/device-tree/firmware/android,其中get_android_dt_dir的源码在system/core/init/util.cpp里面std::unique_ptr<DIR, int (*)(DIR*)> dir(opendir(get_android_dt_dir().c_str()), closedir);if (!dir) return;std::string dt_file;struct dirent *dp;while ((dp = readdir(dir.get())) != NULL) {//遍历dir中的文件if (dp->d_type != DT_REG || !strcmp(dp->d_name, "compatible") || !strcmp(dp->d_name, "name")) {//跳过compatible和name文件continue;}std::string file_name = get_android_dt_dir() + dp->d_name;// 读取文件内容,将 , 替换为 . // 然后设置属性以 <ro.boot.文件名 file_value> 格式存储android::base::ReadFileToString(file_name, &dt_file);std::replace(dt_file.begin(), dt_file.end(), ',', '.');property_set("ro.boot."s + dp->d_name, dt_file);}
}

1.2.3 process_kernel_dt

  该函数牵涉的代码定义在system/core/init/init.cpp和system/core/init/util.cpp里面,该函数解析 kernel 的 cmdline 文件提取以 “androidboot.” 字符串打头的字符串,通过 property_set 设置该系统属性,其中qemu 用于区分启动 android 的设备是否为模拟器,如果是模拟器的话,需要设置的系统属性以 “ro.kernel.” 打头,我们运行的是真机所以这个可以忽略。其中重点需要注意的是Android P的 kernel 的 cmdline 启动参数文件为 /proc/cmdline 中。

static void process_kernel_cmdline() {// The first pass does the common stuff, and finds if we are in qemu.// The second pass is only necessary for qemu to export all kernel params// as properties.import_kernel_cmdline(false, import_kernel_nv);if (qemu[0]) import_kernel_cmdline(true, import_kernel_nv);
}void import_kernel_cmdline(bool in_qemu,const std::function<void(const std::string&, const std::string&, bool)>& fn) {std::string cmdline;android::base::ReadFileToString("/proc/cmdline", &cmdline);for (const auto& entry : android::base::Split(android::base::Trim(cmdline), " ")) {std::vector<std::string> pieces = android::base::Split(entry, "=");if(pieces.size() > 2){for(std::vector<std::string>::size_type i = 2; i < pieces.size(); i++){pieces[1] += "=" + pieces[i];}}if (pieces.size() >= 2) {fn(pieces[0], pieces[1], in_qemu);}}
}static void import_kernel_nv(const std::string& key, const std::string& value, bool for_emulator) {if (key.empty()) return;if (for_emulator) {// In the emulator, export any kernel option with the "ro.kernel." prefix.property_set(android::base::StringPrintf("ro.kernel.%s", key.c_str()).c_str(), value.c_str());return;}if (key == "qemu") {strlcpy(qemu, value.c_str(), sizeof(qemu));} else if (android::base::StartsWith(key, "androidboot.")) {property_set(android::base::StringPrintf("ro.boot.%s", key.c_str() + 12).c_str(),value.c_str());}
#if 1else if (android::base::StartsWith(key, "mmc_erase_content")){property_set("ro.mmc_erase_content", value.c_str());
#endif} else if (android::base::StartsWith(key, "paxdroid.sn")) {property_set("ro.fac.sn", value.c_str());} else if (android::base::StartsWith(key, "paxdroid.exsn")) {property_set("ro.fac.exsn", value.c_str());} else if (android::base::StartsWith(key, "paxdroid.aboot_version")){property_set("ro.fac.aboot_version", value.c_str());} else if (android::base::StartsWith(key, "paxdroid.aboot_git")){property_set("ro.fac.aboot_version_git", value.c_str()); }else if (android::base::StartsWith(key, "paxdroid.configfile")) {int len;unsigned char *str;unsigned char *val_str;len = value.size();str = (unsigned char *)malloc(len+1);val_str = (unsigned char *)malloc(len+1);if (str == NULL || NULL == val_str) {/* badly */}strcpy((char* __restrict)val_str,(const char *)value.c_str());//ERROR("ganyx: len:%d, str:%s\n", len, str);//LOG(INFO)<<"ganyx1: len:"<<len;len = base64decode((const unsigned char *)val_str, len, str, len);str[len] = 0;if (strncmp((const char *)(str+len-16), "SIGNED_VER:", (size_t)(sizeof("SIGNED_VER:")-1)) ==0) {/* remove signed data */*(str+len-284) = 0;}free(val_str);parse_config_file((const char *)str);free(str);}    
}

其中cat /proc/cmdline的内容如下所示,当然不同机型和平台可能有差异,仅供参考作用。

core_ctl_disable_cpumask=0-7 
kpti=0 
console=ttyMSM0,115200,n8 
androidboot.console=ttyMSM0 
androidboot.hardware=qcom 
msm_rtb.filter=0x237 
ehci-hcd.park=3 
lpm_levels.sleep_disabled=1 
androidboot.bootdevice=7824900.sdhci 
earlycon=msm_serial_dm,0x78af000 
firmware_class.path=/vendor/firmware_mnt/image 
androidboot.usbconfigfs=true 
loop.max_part=7 
buildvariant=userdebug 
androidboot.emmc=true 
androidboot.pmi_mode=nopmi 
androidboot.verifiedbootstate=orange 
androidboot.keymaster=1 
root=PARTUUID=174c6ac6-7f74-f102-84b5-d04c631a160a 
androidboot.vbmeta.device=PARTUUID=23e2bc13-82ef-4123-566d-4486fac5eba5 
androidboot.vbmeta.avb_version=1.0 
androidboot.vbmeta.device_state=unlocked 
androidboot.vbmeta.hash_alg=sha256 
androidboot.vbmeta.size=5184 
androidboot.vbmeta.digest=c6514d102da4a393636dd2e9f9cb6c2faf358a629532d5bb02b5efae9b06947b 
androidboot.veritymode=disabled 
androidboot.serialno=42264871 
androidboot.baseband=msm 
mdss_mdp.panel=1:dsi:0:qcom,mdss_dsi_pax_1920x1080_video:1:qcom,mdss_dsi_pax_1920x1080_video_1:cfg:dual_dsi skip_initramfs rootwait ro 
init=/init 
androidboot.dtbo_idx=2

1.2.4 export_kernel_boot_props

  该函数定义在system/core/init/init.cpp,该函数的主要作用是额外设置一些属性,在章节1.2.2处理dt属性和1.2.3章节处理kernek cmd 系统属性的过程中引入了一些以 “ro.boot.” 为前缀的系统属性值,这里如果结构体prop_map中src_prop 对应的属性值不为空,则额外增加设置到 dst_prop 属性中。

static void export_kernel_boot_props() {struct {const char *src_prop;const char *dst_prop;const char *default_value;} prop_map[] = {{ "ro.boot.serialno",   "ro.serialno",   "", },{ "ro.boot.mode",       "ro.bootmode",   "unknown", },{ "ro.boot.baseband",   "ro.baseband",   "unknown", },{ "ro.boot.bootloader", "ro.bootloader", "unknown", },{ "ro.boot.hardware",   "ro.hardware",   "unknown", },{ "ro.boot.revision",   "ro.revision",   "0", },};for (size_t i = 0; i < arraysize(prop_map); i++) {std::string value = GetProperty(prop_map[i].src_prop, "");property_set(prop_map[i].dst_prop, (!value.empty()) ? value : prop_map[i].default_value);}
}

1.2.5 设置其它零碎属性

  分析至此,属性加载设置基本完毕了,然后就是一些零碎的属性设置了,这个没有什么好说的了。

    // 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);// Set memcg property based on kernel cmdline argumentbool memcg_enabled = android::base::GetBoolProperty("ro.boot.memcg",false);if (memcg_enabled) {// root memory control cgroupmkdir("/dev/memcg", 0700);chown("/dev/memcg",AID_ROOT,AID_SYSTEM);mount("none", "/dev/memcg", "cgroup", 0, "memory");// app mem cgroups, used by activity manager, lmkd and zygotemkdir("/dev/memcg/apps/",0755);chown("/dev/memcg/apps/",AID_SYSTEM,AID_SYSTEM);mkdir("/dev/memcg/system",0550);chown("/dev/memcg/system",AID_SYSTEM,AID_SYSTEM);}

1.3 清空环境变量

    unsetenv("INIT_SECOND_STAGE");unsetenv("INIT_STARTED_AT");                 // 清除掉之前使用过的环境变量unsetenv("INIT_SELINUX_TOOK");unsetenv("INIT_AVB_VERSION");

1.4 进行SELinux第二阶段工作并恢复安全上下文

    // Now set up SELinux for second stage.SelinuxSetupKernelLogging();//初始化内核log系统,具体参见篇章一的介绍SelabelInitialize();//电二阶段初始化SELinuxSelinuxRestoreContext();//恢复安全上下文

1.4.1 SelabelInitialize

  这里牵涉到的代码路径为external/selinux/libselinux/src/android/android_platform.c和system/core/init/selinux.cpp,其代码逻辑如下:


// selinux_android_file_context_handle() takes on the order of 10+ms to run, so we want to cache
// its value.  selinux_android_restorecon() also needs an sehandle for file context look up.  It
// will create and store its own copy, but selinux_android_set_sehandle() can be used to provide
// one, thus eliminating an extra call to selinux_android_file_context_handle().
void SelabelInitialize() {sehandle = selinux_android_file_context_handle();//创建context的处理函数selinux_android_set_sehandle(sehandle);//将刚刚新建的处理赋值给fc_sehandle
}static const struct selinux_opt seopts_file_plat[] = {{ SELABEL_OPT_PATH, "/system/etc/selinux/plat_file_contexts" },{ SELABEL_OPT_PATH, "/plat_file_contexts" }
}; static const struct selinux_opt seopts_file_odm[] = { { SELABEL_OPT_PATH, "/odm/etc/selinux/odm_file_contexts" },{ SELABEL_OPT_PATH, "/odm_file_contexts" }
}; 
struct selabel_handle* selinux_android_file_context_handle(void)
{struct selinux_opt seopts_file[MAX_FILE_CONTEXT_SIZE];int size = 0; unsigned int i;for (i = 0; i < ARRAY_SIZE(seopts_file_plat); i++) {if (access(seopts_file_plat[i].value, R_OK) != -1) {seopts_file[size++] = seopts_file_plat[i];break;}    }    for (i = 0; i < ARRAY_SIZE(seopts_file_vendor); i++) {if (access(seopts_file_vendor[i].value, R_OK) != -1) {seopts_file[size++] = seopts_file_vendor[i];break;}    }    for (i = 0; i < ARRAY_SIZE(seopts_file_odm); i++) {if (access(seopts_file_odm[i].value, R_OK) != -1) {seopts_file[size++] = seopts_file_odm[i];break;}    }    return selinux_android_file_context(seopts_file, size);
}void selinux_android_set_sehandle(const struct selabel_handle *hndl)
{   fc_sehandle = (struct selabel_handle *) hndl;
}

  在init启动启动第一阶段内核态我们嗲用SelinuxInitialize函数初始化了SELinux,而在这一阶段主要调用selinux_android_file_context_handle初始化context_handle。
  根据Android SELinux开发入门指南博客中的描述,init 进程给一些文件或者系统属性进行安全上下文检查时会使用 libselinux 的 API,查询 file_contexts & property_contexts 文件中的安全上下文。

  以系统属性为例,当其他进程通过 socket 与系统属性通信时请求访问某一项系统属性的值时,属性服务系统会通过 libselinux 提供的 selabel_lookup 函数到 property_contexts 文件中查找要访问属性的安全上下文,有了该进程的安全上下文和要访问属性的安全上下文之后,属性系统就能决定是否允许一个进程访问它所指定的服务了。

  所以需要先得到这些文件的 handler,以便可以用来查询系统文件和系统属性的安全上下文。

1.4.2 SelinuxRestoreContext

  这个函数主要用户恢复按照SELinux指定的文件的安全上下文,这个理解起来也非常的简单因为这些文件是SELinux安全机制初始化之前创建的,所以需要重新恢复安全性,以保证指定的SELinux规则能生效。

// The files and directories that were created before initial sepolicy load or
// files on ramdisk need to have their security context restored to the proper
// value. This must happen before /dev is populated by ueventd.
void SelinuxRestoreContext() {LOG(INFO) << "Running restorecon...";selinux_android_restorecon("/dev", 0);selinux_android_restorecon("/dev/kmsg", 0);if constexpr (WORLD_WRITABLE_KMSG) {selinux_android_restorecon("/dev/kmsg_debug", 0);}selinux_android_restorecon("/dev/socket", 0);selinux_android_restorecon("/dev/random", 0);selinux_android_restorecon("/dev/urandom", 0);selinux_android_restorecon("/dev/__properties__", 0);selinux_android_restorecon("/plat_file_contexts", 0);selinux_android_restorecon("/nonplat_file_contexts", 0);selinux_android_restorecon("/vendor_file_contexts", 0);selinux_android_restorecon("/plat_property_contexts", 0);selinux_android_restorecon("/nonplat_property_contexts", 0);selinux_android_restorecon("/vendor_property_contexts", 0);selinux_android_restorecon("/plat_seapp_contexts", 0);selinux_android_restorecon("/nonplat_seapp_contexts", 0);selinux_android_restorecon("/vendor_seapp_contexts", 0);selinux_android_restorecon("/plat_service_contexts", 0);selinux_android_restorecon("/nonplat_service_contexts", 0);selinux_android_restorecon("/vendor_service_contexts", 0);selinux_android_restorecon("/plat_hwservice_contexts", 0);selinux_android_restorecon("/nonplat_hwservice_contexts", 0);selinux_android_restorecon("/vendor_hwservice_contexts", 0);selinux_android_restorecon("/sepolicy", 0);selinux_android_restorecon("/vndservice_contexts", 0);selinux_android_restorecon("/dev/block", SELINUX_ANDROID_RESTORECON_RECURSE);selinux_android_restorecon("/dev/device-mapper", 0);selinux_android_restorecon("/sbin/mke2fs_static", 0);selinux_android_restorecon("/sbin/e2fsdroid_static", 0);selinux_android_restorecon("/sbin/mkfs.f2fs", 0);selinux_android_restorecon("/sbin/sload.f2fs", 0);
}

1.5 创建epoll句柄并初始化子进程终止信号处理函数

    epoll_fd = epoll_create1(EPOLL_CLOEXEC);//创建epoll实例,返回epoll描述符if (epoll_fd == -1) {PLOG(FATAL) << "epoll_create1 failed";}sigchld_handler_init();//初始化sigchld_hander处理函数if (!IsRebootCapable()) {// If init does not have the CAP_SYS_BOOT capability, it is running in a container.// In that case, receiving SIGTERM will cause the system to shut down.InstallSigtermHandler();}

  init是一个守护进程,Android的设计者为了防止init的进程成为僵尸进程(zombie process),需要init在子进程结束时获取该子进程的结束码,进而通过该结束码将程序表中的子进程移除,防止成为僵尸进程的子进程占用程序表的空间(程序表的空间达到上限时,系统就不能再启动新的进程了,会引起严重的系统问题)。

  在这里我们有必要简单介绍一下Linux进程的状态,我们知道Linux是一个多用户,多任务的系统,可以同时运行多个用户的多个程序,就必然会产生很多的进程,而每个进程会有不同的状态,其状态主要分为如下几种:

  • Linux进程状态:R (TASK_RUNNING),可执行状态&运行状态(在run_queue队列里的状态)

  • Linux进程状态:S (TASK_INTERRUPTIBLE),可中断的睡眠状态, 可处理signal

  • Linux进程状态:D (TASK_UNINTERRUPTIBLE),不可中断的睡眠状态, 可处理signal, 有延迟

  • Linux进程状态:T (TASK_STOPPED or TASK_TRACED),暂停状态或跟踪状态, 不可处理signal, 因为根本没有时间片运行代码

  • Linux进程状态:Z (TASK_DEAD - EXIT_ZOMBIE),退出状态,进程成为僵尸进程。不可被kill, 即不响应任务信号, 无法用SIGKILL杀死

  对于上面的进程状态我们可以通过ps -A在运行的相关Android终端进行查看如下,可以看到大部分进程都处于S状态。

USER           PID  PPID     VSZ    RSS WCHAN            ADDR S NAME
root             1     0   32248   3472 SyS_epoll_wait      0 S init
root             2     0       0      0 kthreadd            0 S [kthreadd]
root             3     2       0      0 smpboot_thread_fn   0 S [ksoftirqd/0]
root             5     2       0      0 worker_thread       0 S [kworker/0:0H]
root             7     2       0      0 rcu_gp_kthread      0 S [rcu_preempt]
root             8     2       0      0 rcu_gp_kthread      0 S [rcu_sched]
root             9     2       0      0 rcu_gp_kthread      0 S [rcu_bh]
root            10     2       0      0 rcu_nocb_kthread    0 S [rcuop/0]
root            11     2       0      0 rcu_nocb_kthread    0 S [rcuos/0]
root            12     2       0      0 rcu_nocb_kthread    0 S [rcuob/0]

  最后我们必须要知道的一点在Linux当中,父进程是通过捕捉 SIGCHLD 信号来得知子进程运行结束的情况,而子进程在进程终止时会发出该信号。关于这个可以延伸一下Android的tombstones,网易的云捕等相关的Native crash捕获工具基本逻辑也是如此。

1.5.1 epoll_create1

  在正式开始讲解该函数之前我们需要了解IO多路复用概念和以及技术发展史。
IO多路复用概念
  I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。有过Linux下网络编程经验的童靴应该知道,IO多路复用是随着网络编程需求的发展而进行者更新迭代和进步的,
  正是由于有了IO多路复用,当你的某个socket可读或者可写的时候。它能够给你一个通知。这样当配合非堵塞的socket使用时,仅仅有当系统通知我哪个描写叙述符可读了,我才去运行read操作。能够保证每次read都能读到有效数据而不做纯返回-1和EAGAIN的无用功。写操作相似。操作系统的这个功能通过select/poll/epoll之类的系统调用来实现。这些函数都能够同一时候监视多个描写叙述符的读写就绪状况,这样多个描写叙述符的I/O操作都能在一个线程内并发交替地顺序完毕,这就叫I/O多路复用,这里的“复用”指的是复用同一个线程。

IO多路技术发展史
(1) select

NAMEselect, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexingSYNOPSIS/* According to POSIX.1-2001, POSIX.1-2008 */#include <sys/select.h>/* According to earlier standards */#include <sys/time.h>#include <sys/types.h>#include <unistd.h>int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);void FD_CLR(int fd, fd_set *set);int  FD_ISSET(int fd, fd_set *set);void FD_SET(int fd, fd_set *set);void FD_ZERO(fd_set *set);

  历史的车轮滚滚向前,IO多路复用技术也是如此,在网络socket编程中很长一段时间使用的是select多路复用,虽然该技术能达到多路复用的功能,但是存在如下缺点:

  • 编写难度大
  • 同时处理的文件描述符是有上限的
  • 每次需要重新设定fd集合
  • 性能会随用户的增多而效率降低

(2) epoll

NAMEpoll, ppoll - wait for some event on a file descriptorSYNOPSIS#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);

  poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

(3) epoll

  终极改进版本,epoll是Linux多路服用IO接口select/poll的加强版,e对应的英文单词就是enhancement,中文翻译为增强,加强,提高,充实的意思。所以epoll模型会显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的 select 实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。
  epoll机制一般使用epoll_create(int size)函数创建epoll句柄,size用来告诉内核这个句柄可监听的fd的数目。注意这个参数不同于select()中的第一个参数,在select中需给出最大监听数加1的值。
  此外,当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,能够看到创建出的fd,因此在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。上述代码使用的epoll_create1(EPOLL_CLOEXEC)来创建epoll句柄,该标志位表示生成的epoll fd具有“执行后关闭”特性。上述只是对IO多路复用的讲解,这个还是内容比较多的,大家如果有兴趣可以查阅相关的博客自行进行相关的学习。

NAMEepoll_create, epoll_create1 - open an epoll file descriptorSYNOPSIS#include <sys/epoll.h>int epoll_create(int size);int epoll_create1(int flags);DESCRIPTIONepoll_create()  creates  an  epoll(7)  instance.  Since Linux 2.6.8, the size argument is ignored, but must be greater than zero; seeNOTES below.epoll_create() returns a file descriptor referring to the new epoll instance.  This file descriptor is used for  all  the  subsequentcalls  to  the  epoll  interface.   When no longer required, the file descriptor returned by epoll_create() should be closed by usingclose(2).  When all file descriptors referring to an epoll instance have been closed, the kernel destroys the instance  and  releasesthe associated resources for reuse.epoll_create1()If  flags  is 0, then, other than the fact that the obsolete size argument is dropped, epoll_create1() is the same as epoll_create().The following value can be included in flags to obtain different behavior:EPOLL_CLOEXECSet the close-on-exec (FD_CLOEXEC) flag on the new file descriptor.  See the description of the O_CLOEXEC flag in open(2)  forreasons why this may be useful.

  有了上述的这番讲解,我想对于这里的epoll_create1就很容理解了,就是通过内核创建一个文件描述符,用于后续的IO多路复用。

1.5.2 sigchld_handler_init

//该函数定义在system/core/init/init/sigchld_handler.cpp
static void SIGCHLD_handler(int) {if (TEMP_FAILURE_RETRY(write(signal_write_fd, "1", 1)) == -1) {PLOG(ERROR) << "write(signal_write_fd) failed";}
}void sigchld_handler_init() {// Create a signalling mechanism for SIGCHLD.int s[2];//利用sockerpair创建一对已经连接的socket文件描述符,分别作为信号的读/写端if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, s) == -1) {PLOG(FATAL) << "socketpair failed in sigchld_handler_init";}signal_write_fd = s[0];signal_read_fd = s[1];// Write to signal_write_fd if we catch SIGCHLD.struct sigaction act;memset(&act, 0, sizeof(act));//设置信号处理器对应的执行函数为SIGCHLD_handler//用于处理init子进程的SIGCHLD消息act.sa_handler = SIGCHLD_handler;act.sa_flags = SA_NOCLDSTOP;//调用信号处理函数sigaction,将监听的信号及对应的信号处理器注册到内核中sigaction(SIGCHLD, &act, 0);//用于终止出现问题的子进程ReapAnyOutstandingChildren();//注册信号处理函数handle_signalregister_epoll_handler(signal_read_fd, handle_signal);
}

Linux 信号机制
  在正式开始分析代码前,有些知识点我们必须了解,信号机制是Linux进程间通信的一种重要方式,Linux 信号一方面用于正常的进程间通信和同步,如任务控制(SIGINT, SIGTSTP,SIGKILL, SIGCONT,…);另一方面,它还负责监控系统异常及中断。 当应用程序运行异常时, Linux 内核将产生错误信号并通知当前进程。 当前进程在接收到相关信号后,可以有三种不同的处理方式。

  • 忽略该信号。
  • .捕捉该信号并执行对应的信号处理函数(signal handler),这里采用的就是这种方法
  • 执行该信号的缺省操作(如 SIGTERM, 其缺省操作是终止进程)。
       #include <signal.h>int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);Feature Test Macro Requirements for glibc (see feature_test_macros(7)):sigaction(): _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCEsiginfo_t: _POSIX_C_SOURCE >= 199309LDESCRIPTIONThe  sigaction()  system call is used to change the action taken by a process on receipt of a specific signal.  (See signal(7) for an overview ofsignals.)signum specifies the signal and can be any valid signal except SIGKILL and SIGSTOP.If act is non-NULL, the new action for signal signum is installed from act.  If oldact is non-NULL, the previous action is saved in oldact.The sigaction structure is defined as something like:struct sigaction {void     (*sa_handler)(int);void     (*sa_sigaction)(int, siginfo_t *, void *);sigset_t   sa_mask;int        sa_flags;void     (*sa_restorer)(void);};

  上述就是Linux信号机制牵涉到的函数和机构体,我们将上述相关知识代入到代码中来分析一把,

  • 首先,利用sockerpair创建一对已经连接的socket文件描述符,分别作为信号的读/写端,这样当一端写入时,另一端就能被通知到,socketpair 两端既可以写也可以读,这里只是单向的让 s[0] 写,s[1] 读。
  • 接着创建sigaction 结构体,这里注意到sigaction结构体的sa_flags为SA_NOCLDSTOP。由于系统默认在子进程暂停时也会发送信号SIGCHLD,init需要忽略子进程在暂停时发出的SIGCHLD信号,因此将act.sa_flags 置为SA_NOCLDSTOP,该标志位表示仅当进程终止时才接受SIGCHLD信号。
  • 接着调用sigaction(SIGCHLD, &act, 0)是信号绑定关系,也就是当监听到SIGCHLD信号时,由 act 这个 sigaction 结构体进行处理,最终交由SIGCHLD_handler函数处理。

  上述文字描述还是显得很单薄,空洞不是,那必须拿出必杀技了,有图才有真相不是,让我们通过示意图对整个逻辑把控一番,这样会更加得清晰明了,如果还是不明了,那只能说臣妾做不到了。
在这里插入图片描述
  让我们对上面的示意图捋一捋,顺一顺,总结总结:

  • init进程收到收到子进程 SIGCHLD 信号然后通过 sigaction 函数将信号处理过程转移到 sigaction 结构体

  • sigaction 成员变量 sa_flags 另外指定所关心的具体信号为 SA_NOCLDSTOP,也就是子进程终止信号成员变量 sa_handler 表明当子进程终止时,通过 SIGCHLD_handler 函数处理

  • SIGCHLD_handler 信号处理函数中通过 s[0] 文件描述符写了一个"1",由于 socketpair 的特性,s[1] 能接读到该字符串(后序讲解)

  • 通过 register_epoll_handler 将 s[1] 注册到 epoll 内核事件表中,handle_signal 是 s[1] 有数据到来时的处理函数(后续讲解)

1.5.3 SIGCHLD_handler

//该函数定义在system/core/init/signal_handler.cpp路径
static void SIGCHLD_handler(int) {if (TEMP_FAILURE_RETRY(write(signal_write_fd, "1", 1)) == -1) {PLOG(ERROR) << "write(signal_write_fd) failed";}
}

  通过前面的代码分析我们可知,当init子进程终止产生SIGCHLD信号时,SIGCHLD_handler将对socketpair对中signal_write_fd执行写操作。由于socketpair的绑定关系,这将触发信号对应的signal_read_fd收到数据。

1.5.4 register_epoll_handler

  通过前面的代码分析我么可以知道,在初始化信号监听函数的最后sigchld_handler_init函数调用了register_epoll_handler,该函数定义在system/core/init/init.cpp中,注意这里传入的两个参数分别为signal_read_fd和handle_signal。

void register_epoll_handler(int fd, void (*fn)()) {epoll_event ev;ev.events = EPOLLIN;ev.data.ptr = reinterpret_cast<void*>(fn);//epoll_fd增加一个监听对象fd,fd上有数据到来时,调用fn处理即调用handle_signalif (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) {PLOG(ERROR) << "epoll_ctl failed";}
}

1.5.5 handle_signal

static void handle_signal() {// Clear outstanding requests.char buf[32];read(signal_read_fd, buf, sizeof(buf));ReapAnyOutstandingChildren();
}

  相关代码定义在system/core/init/signal_handler.cpp,其逻辑非常简单就是读取(清空)signal_read_fd的数据,然后调用ReapAnyOutstandingChildren函数进行相关处理。

1.5.6 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.//waitpid会暂时停止目前进程的执行,直到有信号来到或子进程结束//这里waitid的标记为WNOHANG,即非阻塞,返回为正值就说明有进程挂掉了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)); });std::string name;std::string wait_string;Service* service = nullptr;if (PropertyChildReap(pid)) {name = "Async property child";//忽略} else if (SubcontextChildReap(pid)) {name = "Subcontext";//忽略} else {//利用FindService找到pid对应的服务//FindService主要是通过轮询init.rc解析生成的services_列表,找到pid与参数一致的service service = ServiceList::GetInstance().FindService(pid, &Service::pid);if (service) {//输出找到服务信息name = StringPrintf("Service '%s' (pid %d)", service->name().c_str(), pid);if (service->flags() & SVC_EXEC) {auto exec_duration = boot_clock::now() - service->time_started();auto exec_duration_ms =std::chrono::duration_cast<std::chrono::milliseconds>(exec_duration).count();wait_string = StringPrintf(" waiting took %f seconds", exec_duration_ms / 1000.0f);}} else {name = StringPrintf("Untracked pid %d", pid);}}// 根据svc的类型,决定后续的处理方式if (siginfo.si_code == CLD_EXITED) {LOG(INFO) << name << " exited with status " << siginfo.si_status << wait_string;} else {LOG(INFO) << name << " received signal " << siginfo.si_status << wait_string;}if (!service) return true;//没有找到返回//清除子进程相关资源service->Reap(siginfo);if (service->flags() & SVC_TEMPORARY) {//移除临时服务ServiceList::GetInstance().RemoveService(*service);}return true;
}

  这里我们以一个实例举例,我们有一个进程paxservice是由init进程启动的,然后我们执行kill -9 将其kill掉,然后在内核里面就能看到对应的如上代码生成的日志信息了。

<14>[  669.181500] init: Service 'paxservice' (pid 712) received signal 9
<14>[  669.181558] init: Sending signal 9 to service 'paxservice' (pid 712) process group...

1.5.7 Reap

void Service::Reap(const siginfo_t& siginfo) {//清除未携带SVC_ONESHOT 或 携带了SVC_RESTART标志的srvc的进程组if (!(flags_ & SVC_ONESHOT) || (flags_ & SVC_RESTART)) {KillProcessGroup(SIGKILL);}// Remove any descriptor resources we may have created.//清楚该service中创建出的任意描述符std::for_each(descriptors_.begin(), descriptors_.end(),std::bind(&DescriptorInfo::Clean, std::placeholders::_1));for (const auto& f : reap_callbacks_) {f(siginfo);}if (flags_ & SVC_EXEC) UnSetExec();//清理工作完毕后,后面决定是否重启机器或重启服务//TEMPORARY服务不参与这种判断if (flags_ & SVC_TEMPORARY) return;pid_ = 0;flags_ &= (~SVC_RUNNING);start_order_ = 0;// Oneshot processes go into the disabled state on exit,// except when manually restarted.// 对于携带了SVC_ONESHOT并且未携带SVC_RESTART的service,将这类服务的标志置为SVC_DISABLED,不再自启动if ((flags_ & SVC_ONESHOT) && !(flags_ & SVC_RESTART)) {flags_ |= SVC_DISABLED;}// Disabled and reset processes do not get restarted automatically.if (flags_ & (SVC_DISABLED | SVC_RESET))  {NotifyStateChange("stopped");return;}// If we crash > 4 times in 4 minutes, reboot into recovery.// 未携带SVC_RESTART的关键服务,在规定的间隔内,crash字数过多时,会导致整机重启进入recovery;boot_clock::time_point now = boot_clock::now();if ((flags_ & SVC_CRITICAL) && !(flags_ & SVC_RESTART)) {if (now < time_crashed_ + 4min) {if (++crash_count_ > 4) {LOG(FATAL) << "critical process '" << name_ << "' exited 4 times in 4 minutes";}} else {time_crashed_ = now;crash_count_ = 1;}}// 将待重启service的标志位置为SVC_RESTARTING(init进程将根据该标志位,重启服务)flags_ &= (~SVC_RESTART);flags_ |= SVC_RESTARTING;// Execute all onrestart commands for this service.//重启在init.rc文件中带有onrestart选项的服务onrestart_.ExecuteAllCommands();NotifyStateChange("restarting");return;
}

  该代码定义在system/core/init/service.cpp中,该函数的主要作用就是清除问题进程相关的资源,然后根据进程对应的类型,决定是否重启机器或重启进程。这里还是以上个例子举例可以看到paxservice被杀后,会清除进程信息并重启。

<14>[  669.181500] init: Service 'paxservice' (pid 712) received signal 9
<14>[  669.181558] init: Sending signal 9 to service 'paxservice' (pid 712) process group...
<14>[  669.182047] libprocessgroup: Successfully killed process cgroup uid 0 pid 712 in 0ms
<14>[  669.187762] init: starting service 'paxservice'...
//服务配置选项
service paxservice /system/bin/paxserviceclass mainuser rootgroup root

1.5.8 ExecuteCommand

  该代码定义在system/core/init/Action.cpp中的ExecuteAllCommands函数中:

void Action::ExecuteAllCommands() const {for (const auto& c : commands_) {ExecuteCommand(c);}
}void Action::ExecuteCommand(const Command& command) const {android::base::Timer t;//进程重启时,将执行对应的函数auto result = command.InvokeFunc(subcontext_);//打印logauto duration = t.duration();// There are many legacy paths in rootdir/init.rc that will virtually never exist on a new// device, such as '/sys/class/leds/jogball-backlight/brightness'.  As of this writing, there// are 198 such failures on bullhead.  Instead of spamming the log reporting them, we do not// report such failures unless we're running at the DEBUG log level.bool report_failure = !result.has_value();if (report_failure && android::base::GetMinimumLogSeverity() > android::base::DEBUG &&result.error_errno() == ENOENT) {report_failure = false;}// Any action longer than 50ms will be warned to user as slow operationif (report_failure || duration > 50ms ||android::base::GetMinimumLogSeverity() <= android::base::DEBUG) {std::string trigger_name = BuildTriggersString();std::string cmd_str = command.BuildCommandString();LOG(INFO) << "Command '" << cmd_str << "' action=" << trigger_name << " (" << filename_<< ":" << command.line() << ") took " << duration.count() << "ms and "<< (result ? "succeeded" : "failed: " + result.error_string());}
}

  最后让我们来总结一下整个流程:

  • 调用epoll_create1创建epoll_fd句柄
  • 调用sigchld_handler_init函数,该函数的本质是本质就是监听子进程死亡的信息,然后进行对应的清理工作,并根据死亡进程的类型,决定是否需要重启进程或机器。

1.6 设置其它一些系统属性

    property_load_boot_defaults();export_oem_lock_status();set_usb_controller();

  property_load_boot_defaults,export_oem_lock_status,set_usb_controller这三个函数都是加载和设置一些系统属性。

void property_load_boot_defaults() {if (!load_properties_from_file("/system/etc/prop.default", NULL)) {// Try recovery pathif (!load_properties_from_file("/prop.default", NULL)) {// Try legacy pathload_properties_from_file("/default.prop", NULL);}}load_properties_from_file("/product/build.prop", NULL);load_properties_from_file("/odm/default.prop", NULL);load_properties_from_file("/vendor/default.prop", NULL);update_sys_usb_config();
}//定义在system/core//init/property_service.cpp
static void export_oem_lock_status() {if (!android::base::GetBoolProperty("ro.oem_unlock_supported", false)) {return;}std::string value = GetProperty("ro.boot.verifiedbootstate", "");if (!value.empty()) {property_set("ro.boot.flash.locked", value == "orange" ? "0" : "1");}
}static void set_usb_controller() {std::unique_ptr<DIR, decltype(&closedir)>dir(opendir("/sys/class/udc"), closedir);if (!dir) return;dirent* dp;while ((dp = readdir(dir.get())) != nullptr) {if (dp->d_name[0] == '.') continue;property_set("sys.usb.controller", dp->d_name);break;}
}

1.7 开启系统属性服务

void start_property_service() {selinux_callback cb;cb.func_audit = SelinuxAuditCallback;selinux_set_callback(SELINUX_CB_AUDIT, cb);property_set("ro.property_service.version", "2");//创建一个非阻塞socketproperty_set_fd = CreateSocket(PROP_SERVICE_NAME, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK,false, 0666, 0, 0, nullptr);if (property_set_fd == -1) {PLOG(FATAL) << "start_property_service socket creation failed";}//调用listen函数监听property_set_fd,于是该socket变成了一个serverlisten(property_set_fd, 8);//监听server socket是是否有数据到来register_epoll_handler(property_set_fd, handle_property_set_fd);
}

  该代码定义在system/core/init/property_service.cpp中,通过前面的代码分析我们知道在init进程在共享内存区域中,创建并初始化属性域,然后通过property_set 可以轻松设置系统属性了,那这里为什么还要大费周章的启动一个属性服务呢?当然做这些都是有原因的,Android出于安全以及权限方面考虑,不是任何猫猫狗狗和随意的进程都可以肆意的修改任何的系统属性。Android为了达到这一目的,将属性的设置统一交由init进程管理,其他进程不能直接修改属性,而只能通知 init 进程来修改,而在这过程中,init 进程可以进行权限检测控制,决定是否允许修改。
  那么start_property_service怎么做到上述的要求呢,主要通过如下几个步骤就OK了(这个的逻辑和sigchld_handler_init有点像):

  • 首先创建一个 socket 并返回文件描述符,然后设置最大并发数为 8,其他进程可以通过这个 socket 通知 init 进程修改系统属性
  • 最后注册 epoll 事件,也就是当监听到 property_set_fd 改变时调用 handle_property_set_fd

  在正式开始源码细节分析前,先奉上整体流程图,以供大家心里有个整体概括:
在这里插入图片描述

1.7.1 handle_property_set_fd

  这个函数的作用主要是调用accept4处理property_set_fd的scoket描述符中的数据信息,然后从 socket 中读取操作信息,根据不同的操作类型,调用 HandlePropertySet做具体的操作:

static void handle_property_set_fd() {static constexpr uint32_t kDefaultSocketTimeout = 2000; /* ms *///等待客户端连接int s = accept4(property_set_fd, nullptr, nullptr, SOCK_CLOEXEC);if (s == -1) {return;}ucred cr;socklen_t cr_size = sizeof(cr);if (getsockopt(s, SOL_SOCKET, SO_PEERCRED, &cr, &cr_size) < 0) {//获取连接到此socket的进程的凭据close(s);PLOG(ERROR) << "sys_prop: unable to get SO_PEERCRED";return;}SocketConnection socket(s, cr);uint32_t timeout_ms = kDefaultSocketTimeout;uint32_t cmd = 0;if (!socket.RecvUint32(&cmd, &timeout_ms)) {// 读取 socket 中的操作类型信息PLOG(ERROR) << "sys_prop: error while reading command from the socket";socket.SendUint32(PROP_ERROR_READ_CMD);return;}switch (cmd) {// 根据操作类型信息,执行对应处理,两者区别一个是以 char 形式读取,一个以 String 形式读取case PROP_MSG_SETPROP: {char prop_name[PROP_NAME_MAX];char prop_value[PROP_VALUE_MAX];if (!socket.RecvChars(prop_name, PROP_NAME_MAX, &timeout_ms) ||!socket.RecvChars(prop_value, PROP_VALUE_MAX, &timeout_ms)) {PLOG(ERROR) << "sys_prop(PROP_MSG_SETPROP): error while reading name/value from the socket";return;}prop_name[PROP_NAME_MAX-1] = 0;prop_value[PROP_VALUE_MAX-1] = 0;const auto& cr = socket.cred();std::string error;uint32_t result =HandlePropertySet(prop_name, prop_value, socket.source_context(), cr, &error);if (result != PROP_SUCCESS) {LOG(ERROR) << "Unable to set property '" << prop_name << "' to '" << prop_value<< "' from uid:" << cr.uid << " gid:" << cr.gid << " pid:" << cr.pid << ": "<< error;}break;}case PROP_MSG_SETPROP2: {std::string name;std::string value;if (!socket.RecvString(&name, &timeout_ms) ||!socket.RecvString(&value, &timeout_ms)) {PLOG(ERROR) << "sys_prop(PROP_MSG_SETPROP2): error while reading name/value from the socket";socket.SendUint32(PROP_ERROR_READ_DATA);return;}const auto& cr = socket.cred();std::string error;uint32_t result = HandlePropertySet(name, value, socket.source_context(), cr, &error);if (result != PROP_SUCCESS) {LOG(ERROR) << "Unable to set property '" << name << "' to '" << value<< "' from uid:" << cr.uid << " gid:" << cr.gid << " pid:" << cr.pid << ": "<< error;}socket.SendUint32(result);break;}default:LOG(ERROR) << "sys_prop: invalid command " << cmd;socket.SendUint32(PROP_ERROR_INVALID_CMD);break;}
}

1.7.1 HandlePropertySet

  这个是最终的调用处理函数,set property msg 分为两类处理,msg name 以“ctl.”为起始的 msg 通过HandleControlMessage处理,主要是启动、停止、重启服务。修改其它 prop时会调用 property_get,然后通过 bionic 的__system_property_set 函数来实现,而这个函数会通过 socket 与 init 的 property service 取得联系。但是不管是前者还是后者,都要进行 SELinux 安全性检查,只有该进程有操作权限才能执行相应操作。

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)) {//检查可以的合法性*error = "Illegal property name";return PROP_ERROR_INVALID_NAME;}if (StartsWith(name, "ctl.")) {//如果一ctl开头,就执行Service的一些控制操作if (!CheckControlPropertyPerms(name, value, source_context, cr)) {//SELinux安全检查,有权限才进行操作*error = StringPrintf("Invalid permissions to perform '%s' on '%s'", name.c_str() + 4,value.c_str());return PROP_ERROR_HANDLE_CONTROL_MESSAGE;}HandleControlMessage(name.c_str() + 4, value, cr.pid);return PROP_SUCCESS;}const char* target_context = nullptr;const char* type = nullptr;property_info_area->GetPropertyInfo(name.c_str(), &target_context, &type);if (!CheckMacPerms(name, target_context, source_context.c_str(), cr)) {//检查SElinux规则*error = "SELinux permission check failed";return PROP_ERROR_PERMISSION_DENIED;}if (type == nullptr || !CheckType(type, value)) {*error = StringPrintf("Property type check failed, value doesn't match expected type '%s'",(type ?: "(null)"));return PROP_ERROR_INVALID_VALUE;}// sys.powerctl is a special property that is used to make the device reboot.  We want to log// any process that sets this property to be able to accurately blame the cause of a shutdown.if (name == "sys.powerctl") {std::string cmdline_path = StringPrintf("proc/%d/cmdline", cr.pid);std::string process_cmdline;std::string process_log_string;if (ReadFileToString(cmdline_path, &process_cmdline)) {// Since cmdline is null deliminated, .c_str() conveniently gives us just the process// path.process_log_string = StringPrintf(" (%s)", process_cmdline.c_str());}LOG(INFO) << "Received sys.powerctl='" << value << "' from pid: " << cr.pid<< process_log_string;}if (name == "selinux.restorecon_recursive") {return PropertySetAsync(name, value, RestoreconRecursiveAsync, error);}return PropertySet(name, value, error);//其它属性调用PropertySet
}

  这段代码比较简单,整体的大概流程如下:

  • 通过IsLegalPropertyName检测要设置的是否是合法的属性名
  • 如果是以 “ctl.” 打头的属性名表明是控制命令(譬如ctl.start,ctl.stop,ctl.restart),如果能经过权限检测则调用HandleControlMessage进行处理
  • 其它种情况调用PropertySet处理


总结

  随着Android版本越高,init的工作量也是越来越大了,分析起来不得不使出吃奶的力气了,在init进程的第二阶段主要工作是主要工作是初始化属性系统,解析SELinux的匹配规则,处理子进程终止信号,启动系统属性服务等等,可以说每一项都很关键,如果说第一阶段是为属性系统,SELinux做准备,那么第二阶段就是真正去把这些落实的,由于牵涉内容太多了只求将主体讲透彻,细节就需要各位童靴自行把控了。并且这篇文章也是耗时最久的,因为东西太多了。



写在最后

  Android P之init进程启动源码分析指南之二的告一段落了,不容易啊分析起来,在接下来的篇章我们将继续讲解init解析init.rc阶段相关工作。如果对给位有帮助欢迎点赞一个,如果写得有问题也欢迎多多指正。未完待续,下个篇章再见。

查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. 【心善渊&Selenium基础】— 7、Selenium中使用XPath定位

    文章目录1、Selenium使用XPath的语法2、Selenium中使用XPath查找元素(1)XPath通过`id`,`name`,`class`属性定位(2)XPath通过标签中的其他属性定位(3)XPath层级定位(4)XPath索引定位(5)XPath逻辑定位(6)XPath模糊匹配定位(7)XPath其他定位方式@1.contains@2.sta…...

    2024/4/23 16:09:25
  2. 值得学习!阿里P8架构师“墙裂”推荐:Java程序员必读的架构书籍

    程序员的一生其实基本上都在学习,那提到学习,我第一时间想到的就是读书了。为啥推荐大家读书呢,书籍的作者都是几年甚至几十年的经验,最后总结为一本书,那就算里面有错误的点,我想你花几十块,只用几个礼拜甚至几天就可以拜读人家十几年的经验,我觉得怎么算都是血赚?下…...

    2024/4/23 16:09:24
  3. Unity寻路导航NavMesh

    寻路导航NavMesh Unity中对于寻路算法进行了封装,支持提交将地图进行导航烘焙,降低了实时计算的消耗 菜单Window–>Navigation,打开导航面板 操作选择不移动的游戏对象,勾选Navigation Static如果是不连接的游戏对象,勾选 Generate OffMeshLinks如果需要设置区域,可以在属性…...

    2024/4/23 16:09:24
  4. 我的STM32主控红外遥控双轮平衡小车

    我的STM32主控红外遥控双轮平衡小车(2015-05-28 17:38:27)[编辑][删除]http://blog.sina.com.cn/s/blog_c0e651900102voe6.html我的红外遥控双轮平衡小车程序初步完整,STM32F103ZET6主控,整个LCD显示,MPU6050数据读取,角度计算,卡尔曼滤波,PID算法,PWM输出,红外遥控,都…...

    2024/4/23 16:09:22
  5. 为了背单词,我花了两天写了一款背单词小程序

    前言“要是考试不考英语就好了”哎,提起英语,都是伤心事。有时候严重怀疑自己不是一块学习英语的料。单词背了忘,忘了背,背了又忘。考试之前看啥单词都会,一上考场:这单词啥意思?前两个月又开始了痛苦的英语学习。起步:背单词。开始还是按照以前的方法,每天规定背多少…...

    2024/4/23 16:09:21
  6. 计算机图形学十二:Whitted风格光线追踪及其加速方法

    Whitted风格光线追踪及其加速方法摘要1 Whitted-Style 光线追踪1.1 原理详解 摘要 本篇内容主要分为两部分,第一部分介绍最基础的Whitted-style光线追踪的原理,该方法是业界后来许许多多的光线追踪方法变体的基础框架,相对重要。第二部分则会具体介绍一些加速光线追踪的方法…...

    2024/4/23 16:09:23
  7. 深入浅出 超详细 从 线程锁 到 redis 实现分布式锁(篇节 1)

    在 使用 redis 实现分布式锁 之前 我们需要先了解以下几点 什么是分布式锁 要介绍 什么是分布式锁,那首先要提到 与之对应的 的两个锁:线程锁 和 进程锁 1.线程锁 主要 用来 给方法、代码块加锁。当某个方法或者代码块使用锁时,那么在同一时刻至多仅有一个线程可以执行该段代…...

    2024/4/23 16:09:19
  8. 为什么要报考消防工程师?消防工程师证书有什么用呢?

    为什么要报考消防工程师?消防工程师证书有什么用呢?随着行业的发展以及市场需求量的增加,消防工程师证书已经成为行业中的香饽饽,虽然很多人听说过消防工程师证书,但是对于这个证书并不了解,那么消防工程师到底有什么用呢?为什么要考消防工程师呢?从单位来讲:可以申请…...

    2024/4/23 16:09:18
  9. 有哪些开源的 Python 库让你相见恨晚?

    Arrow我们知道 Python 已经内置了好几个处理时间相关的库,但是对于时间以及时区间的转换并不清晰,操作起来略繁琐,而 Arrow 可以弥补这个问题,它提供了更友好的方法,方便我们对时间,日期,格式化等操作。很多人学习python,不知道从何学起。 很多人学习python,掌握了基本…...

    2024/4/23 16:09:24
  10. html中长英文换行问题

    1、当一个英文比较单词比较长时,html中默认不自动换行解决方案:max-width:100%;overflow-wrap:break-word;CSS 属性 overflow-wrap 是用来说明当一个不能被分开的字符串太长而不能填充其包裹盒时,为防止其溢出,浏览器是否允许这样的单词中断换行。normal表示在正常的单词结…...

    2024/4/23 16:09:19
  11. 全面掌握Spring Boot与Kubernetes云原生微服务架构实践——为何采用微服务架构

    QQ 1274510382 Wechat JNZ_aming 商业联盟 QQ群538250800 技术搞事 QQ群599020441 解决方案 QQ群152889761 加入我们 QQ群649347320 共享学习 QQ群674240731 纪年科技aming 网络安全 ,深度学习,嵌入式,机器强化,生物智能,生命科学。...

    2024/4/23 16:09:22
  12. 02 uboot分析之源码

    uboot要做的工作关看门狗 初始化时钟 初始化SDRAM 将程序从nand flash拷贝到SDRAM 设置栈第一阶段源码分析 第一步,跳转到reset .globl _start _start: b resetreset做了些啥,注释说设置cpu为SVC32模式 reset:/** set the cpu to SVC32 mode*/mrs r0,cpsrbic r0,r0,#0x…...

    2024/4/20 1:38:01
  13. Altium Deigner安装包

    安装包 **链接:https://pan.baidu.com/s/1Sg9kl1OgTdcQbh8q2CEspQ ** 提取码:yc2m AD设计指南 链接:https://pan.baidu.com/s/18V2J9z-rWbAw4AGDO_MCIw 提取码:dyba AD元件库 链接:https://pan.baidu.com/s/1ksZZ4W2ZFCgtynw5W9Kyew 提取码:j4e9...

    2024/4/19 5:07:52
  14. MATLAB学习笔记(12)图形

    本文为学习笔记,加油!!!二维图形 plot(x, y) % 对向量x绘制向量y。以x为横坐标,y为纵坐标,按照坐标(xj,yj)的有序排列绘制曲线。 plot(y) % 以j为横坐标,yj为纵坐标,绘制(j, yj)的有序集合的图形。plot(A) % 绘制矩阵A的列对它下标的图形。对于mn的矩阵A, 有n个含有m个…...

    2024/4/19 17:25:40
  15. Nginx面试专题

    1、请解释一下什么是 Nginx? Nginx 是一个 web 服务器和反向代理服务器,用于 HTTP、HTTPS、SMTP、POP3和 IMAP 协议。 2 、请列举 x Nginx 的一些特性。 Nginx 服务器的特性包括: 反向代理/L7 负载均衡器 嵌入式 Perl 解释器 动态二进制升级 可用于重新编写 URL,具有非常…...

    2024/4/16 23:34:14
  16. CentOS7 安装Chrome

    使用命令下载rpm安装包:1wget https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm下载包名为google-chrome-stable_current_x86_64.rpm 使用root权限进行安装1rpm -ivh google-chrome-stable_current_x86_64.rpm安装中报错:1234/usr/bin/lsb_releas…...

    2024/4/16 23:33:56
  17. 练手小项目,爬取3DM图片

    博客原文:https://weweweha.com 1. 概述 ​ 爬取3DM指定网页的游戏壁纸,并且通过多线程来加速爬取图片的速度。 2.使用库 ​ request库用来1解析指定网页,re库用来搜索指定网页中的图片地址,threading多线程模块用来加速图片爬取。 3.网页分析 ​ [外链图片转存失败,源站…...

    2024/4/19 14:32:09
  18. XFCall java学习笔记(一)

    对象new 出来的对象都是保存在堆内存中的不是使用 new 创建变量,而是使用一个“自动”变量。 这个变量直接存储"值",并置于栈内存中,因此更加高效。基本类型有自己对应的包装类型,如果你希望在堆内存里表示基本类型的数据,就需要用到它们的包装类或者使用 自动装…...

    2024/4/18 1:42:01
  19. 这样清理运行内存,你的iphone就不会卡了

    一般来说,苹果系统因为自己内存管理机制,不会卡顿。但有的时候当手机长时间使用,打开过很多程序后,手机可能会出现卡慢的情况,这时候我们就需要一些操作彻底关闭这些程序,放出运行内存。这样的话手机有足够空闲的运行内存了,就会快很多。时间原因:最新改动小白有网查看最…...

    2024/4/16 23:34:56
  20. spring事务的几种传播机制,及声明了事务注解的方法运行的所需环境

    1. spring中几种事务的传播机制 PROPAGATION_REQUIRED 含义支持当前事务,如果不存在 就新建一个可满足的外围运行环境 外围方法可不添加任何注解,亦可添加任何注解, 内面的方法终会运行于事务之中 (没有江山,自己打江山;有江山,直接继承江山)PROPAGATION_SUPPORTS 含义 …...

    2024/4/23 16:09:14

最新文章

  1. 【补充】python中的dir函数

    dir() 是一个 Python 内置函数&#xff0c;它用于列出对象的所有属性和方法。 当没有参数传递给 dir() 时&#xff0c;它返回当前作用域中所有可用的名称。 以下是 dir() 函数的用法示例&#xff1a; # 列出当前作用域中的所有名称 print(dir()) # [__annotations__, __buil…...

    2024/5/5 6:03:41
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. 瑞_23种设计模式_迭代器模式

    文章目录 1 迭代器模式&#xff08;Iterator Pattern&#xff09;★★★1.1 介绍1.2 概述1.3 迭代器模式的结构1.4 中介者模式的优缺点1.5 中介者模式的使用场景 2 案例一2.1 需求2.2 代码实现 3 案例二3.1 需求3.2 代码实现 4 JDK源码解析 &#x1f64a; 前言&#xff1a;本文…...

    2024/5/3 5:54:19
  4. CTK插件框架学习-事件监听(04)

    CTK插件框架学习-插件注册调用(03)https://mp.csdn.net/mp_blog/creation/editor/136989802 一、主要流程 发送者注册消息事件接收者订阅消息事件接收者相应消息事件 事件监听比插件接口调用耦合性更弱&#xff0c;事件由框架维护&#xff0c;不需要指定发送方和接收方 二、…...

    2024/5/1 10:29:55
  5. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/5/4 23:54:56
  6. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/5/4 23:54:56
  7. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/5/4 23:54:56
  8. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/5/4 23:55:17
  9. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/5/4 23:54:56
  10. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/5/4 23:55:05
  11. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/5/4 23:54:56
  12. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/5/4 23:55:16
  13. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/5/4 23:54:56
  14. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/5/4 18:20:48
  15. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/5/4 23:54:56
  16. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/5/4 23:55:17
  17. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/5/4 23:55:06
  18. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/5/4 23:54:56
  19. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/5/4 23:55:06
  20. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/5/4 2:59:34
  21. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/5/4 23:55:16
  22. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/5/4 23:54:58
  23. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/5/4 23:55:01
  24. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/5/4 23:54:56
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57