目录
案例二:PRODUCT_PRODUCT_PROPERTIES :=和+=的区别
init进程在启动的第二阶段,需要做的最重要事情之一就是提供属性的机制。在整个android领域的native层和fw层,以及app层,都可以通过属性来存储一些比较重要的键值对,init模块源码/system/core/init/property_service.cpp 专门提供了这样的服务,从命名来看可以叫他服务,其实她并不是我们理解的服务。
这里先附上此篇总结的init进程属性服务的流程图:
一、系统属性初始化
init在启动阶段,初始化property,其源码如下:
总结如下三个步骤:
- 创建/dev/__properties__目录,并进行初始化,创建一些相关文件
- 接受来自kernel的启动命令和启动参数相关配置,包括kernel额外的一些参数配置
- 加载默认配置的一些系统属性
1、系统属性的本质
//system/core/init/property_service.cpp
void PropertyInit() {
....
mkdir("/dev/__properties__", S_IRWXU | S_IXGRP | S_IXOTH);
CreateSerializedPropertyInfo();
if (__system_property_area_init()) {
LOG(FATAL) << "Failed to initialize property area";
}
if (!property_info_area.LoadDefaultPath()) {
LOG(FATAL) << "Failed to load serialized property info file";
}....
}
void CreateSerializedPropertyInfo() {
auto property_infos = std::vector<PropertyInfoEntry>();
if (access("/system/etc/selinux/plat_property_contexts", R_OK) != -1) {
if (!LoadPropertyInfoFromFile("/system/etc/selinux/plat_property_contexts",
&property_infos)) {
return;
}
// Don't check for failure here, since we don't always have all of these partitions.
// E.g. In case of recovery, the vendor partition will not have mounted and we
// still need the system / platform properties to function.
if (access("/dev/selinux/apex_property_contexts", R_OK) != -1) {
LoadPropertyInfoFromFile("/dev/selinux/apex_property_contexts", &property_infos);
}
if (access("/system_ext/etc/selinux/system_ext_property_contexts", R_OK) != -1) {
LoadPropertyInfoFromFile("/system_ext/etc/selinux/system_ext_property_contexts",
&property_infos);
}
if (access("/vendor/etc/selinux/vendor_property_contexts", R_OK) != -1) {
LoadPropertyInfoFromFile("/vendor/etc/selinux/vendor_property_contexts",
&property_infos);
}
if (access("/product/etc/selinux/product_property_contexts", R_OK) != -1) {
LoadPropertyInfoFromFile("/product/etc/selinux/product_property_contexts",
&property_infos);
}
if (access("/odm/etc/selinux/odm_property_contexts", R_OK) != -1) {
LoadPropertyInfoFromFile("/odm/etc/selinux/odm_property_contexts", &property_infos);
}
} else {
if (!LoadPropertyInfoFromFile("/plat_property_contexts", &property_infos)) {
return;
}
LoadPropertyInfoFromFile("/system_ext_property_contexts", &property_infos);
LoadPropertyInfoFromFile("/vendor_property_contexts", &property_infos);
LoadPropertyInfoFromFile("/product_property_contexts", &property_infos);
LoadPropertyInfoFromFile("/odm_property_contexts", &property_infos);
LoadPropertyInfoFromFile("/dev/selinux/apex_property_contexts", &property_infos);
}
auto serialized_contexts = std::string();
auto error = std::string();
if (!BuildTrie(property_infos, "u:object_r:default_prop:s0", "string", &serialized_contexts,
&error)) {
LOG(ERROR) << "Unable to serialize property contexts: " << error;
return;
}
constexpr static const char kPropertyInfosPath[] = "/dev/__properties__/property_info";
if (!WriteStringToFile(serialized_contexts, kPropertyInfosPath, 0444, 0, 0, false)) {
PLOG(ERROR) << "Unable to write serialized property infos to file";
}
selinux_android_restorecon(kPropertyInfosPath, 0);
}
如上代码逻辑首先创建了/dev/__properties__目录,然后在CreateSerializedPropertyInfo函数中创建了/dev/__properties__/property_info目录和里面看起来跟selinux scontext相关的东西。我找了一台remount的机器,进入此目录如下:
紧接着执行__system_property_area_init用来初始化属性空间,其实就是引用上文创建的这个目录,从这个文件里面函数的定义来,除了初始化之后还有get和set等定义,即个人猜测我们设置某条属性和获取某条属性就是通过此接口来实现,其本质就是通过文件的方式来保存。
最后property_info_area.LoadDefaultPath来序列号此文件里面的内容到内存中,其核心逻辑使用了mmap映射的方式:
2、接收来自kernel的参数
1)kernel参数如何转换成属性?
流程1:ProcessKernelDt
获取kernel设备树/proc/device-tree目录里面的一些信息,并设置ro.boot.相关属性
Android 设备树目录(Device Tree Directory)主要用于存储设备树(Device Tree)相关的信息。设备树是一种描述硬件设备信息和配置的数据结构,用于在启动时传递硬件信息给内核和操作系统。Android 设备树目录的主要作用包括:
- 描述硬件信息:设备树用于描述系统中的硬件设备,包括处理器、内存、外设等的配置信息,以便系统能够正确地识别和与这些硬件设备进行交互。
- 硬件配置:设备树包含了硬件设备的配置信息,如中断控制器、时钟频率、设备地址等,这些信息对于系统的正常运行至关重要。
- 与内核交互:设备树在系统启动时被加载到内存中,内核通过解析设备树来获取硬件信息并配置相应的驱动程序,从而实现对硬件设备的管理和控制。
- 跨平台兼容性:设备树的使用可以使得相同的内核可以在不同硬件平台上运行,只需相应修改设备树即可适配不同硬件配置,提高了系统的可移植性和兼容性。
流程2:ProcessKernelCmdline
解析kernel传递过来的cmdline参数(路径/proc/cmdline),并设置ro.boot.相关的属性。如下逻辑
//system/system/core/init/util.cpp
void ImportKernelCmdline(const std::function<void(const std::string&, const std::string&)>& 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) {
fn(pieces[0], pieces[1]);
}
}
}
//system/system/core/init/property_service.cpp
constexpr auto ANDROIDBOOT_PREFIX = "androidboot."sv;
static void ProcessKernelCmdline() {
//解析kernel的cmdline,读取/proc/cmdline文件内容
ImportKernelCmdline([&](const std::string& key, const std::string& value) {
//解析cmdline内容,如果是以androidboot开头的参数,就进入判断设置属性
if (StartsWith(key, ANDROIDBOOT_PREFIX)) {
//设置属性的时候将其拼接变成ro.boot.
InitPropertySet("ro.boot." + key.substr(ANDROIDBOOT_PREFIX.size()), value);
}
});
}
cmdline为bootloader传递给kernel的参数,cat cmdline和获取ro.boot属性如下,即我们平时的这类属性都是从cmdline中获取的
流程3:ProcessBootconfig
解析kernel传递过来的config配置(路径/proc/bootconfig),并设置ro.boot.相关的属性。如下逻辑
//system/system/core/init/util.cpp
void ImportBootconfig(const std::function<void(const std::string&, const std::string&)>& fn) {
std::string bootconfig;
android::base::ReadFileToString("/proc/bootconfig", &bootconfig);
for (const auto& entry : android::base::Split(bootconfig, "\n")) {
std::vector<std::string> pieces = android::base::Split(entry, "=");
if (pieces.size() == 2) {
// get rid of the extra space between a list of values and remove the quotes.
std::string value = android::base::StringReplace(pieces[1], "\", \"", ",", true);
value.erase(std::remove(value.begin(), value.end(), '"'), value.end());
fn(android::base::Trim(pieces[0]), android::base::Trim(value));
}
}
}
//system/system/core/init/property_service.cpp
constexpr auto ANDROIDBOOT_PREFIX = "androidboot."sv;
static void ProcessBootconfig() {
ImportBootconfig([&](const std::string& key, const std::string& value) {
if (StartsWith(key, ANDROIDBOOT_PREFIX)) {
InitPropertySet("ro.boot." + key.substr(ANDROIDBOOT_PREFIX.size()), value);
}
});
}
config同上面一致,在/proc/bootconfig文件里面,但是我在这台机器上面并没有获取出来:
流程4:ExportKernelBootProps
设置其他ro.boot.等值,这里的逻辑像是先去GetProperty了一遍,如果没有值就设置默认值
2)如何自定义kernel参数?
kernel的参数传递,在不同平台不一致,这里分别介绍一下MTK和高通是如何实现的。
1)MTK平台
MTK平台的定制在mt_boot.c文件,大概逻辑如下:
/aosp?vendor/mediatek/proprietary/bootable/bootloader/lk/app/mt_boot/mt_boot.c
//流程1:入口函数
APP_START(mt_boot)
.init = mt_boot_init,
APP_END
//流程2:执行mt_boot_init
void mt_boot_init(const struct app_descriptor *app){
//.....
boot_linux_from_storage();
//.....
}
//流程3:执行boot_linux_from_storage
int boot_linux_from_storage(void){
//.....
boot_linux((void *)kernel_target_addr,(unsigned *)tags_target_addr,board_machtype(),(void *)ramdisk_target_addr,ramdisk_real_sz);
return 0;
}
//流程4:执行boot_linux
void boot_linux(void *kernel, unsigned *tags, unsigned machtype, void *ramdisk, unsigned ramdisk_sz){
#ifdef DEVICE_TREE_SUPPORT
boot_linux_fdt((void *)kernel, (unsigned *)tags, machtype, (void *)ramdisk, ramdisk_sz);
panic("%s Fail to enter EL1\n", __func__);
#endif
}
//流程5:执行boot_linux_fdt
int boot_linux_fdt(void *kernel, unsigned *tags, unsigned machtype, void *ramdisk, unsigned ramdisk_sz){
//....
get_reboot_reason(boot_reason);
//....
}
其中有待研究的一些kernel参数如下:
2)高通平台
高通平台的定制在UpdateCmdLine.c文件,大概逻辑如下:
3)原生平台
google提供了一套原生的方式来添加自定义kernel cmd键值对,使用编译宏控BOARD_KERNEL_CMDLINE,如下代码通常在device目录下配置:
此宏控为什么生效?是因为编译脚本又如下配置:
/build/make/core/board_config.mk
INTERNAL_KERNEL_CMDLINE := $(BOARD_KERNEL_CMDLINE)
ifneq (,$(BOARD_BOOTCONFIG))
INTERNAL_KERNEL_CMDLINE += bootconfig
INTERNAL_BOOTCONFIG := $(BOARD_BOOTCONFIG)
endif
3、初始化所有系统属性
4、PropertyInit函数没有执行完?
虽然流程如上,但是在实际开发调试过程中,我发现PropertyInit函数的日志只打印了一点点,如下调试:
当前还不是很清楚为什么,init进程在这里突然嘎然而止了呢?针对此种现象在第二章会重点说明一下。
二、系统属性服务的启动
这里介绍一下系统属性是如何被设置,并且某个属性被设置的时候,init触发器机制是如何监听的?
其实它是依赖于init进程的一个后台任务,因为它平时并不运行,但是通过了linux socket机制可以对齐进行唤醒。我们先来看看StartPropertyService(同样被init进程在启动的第二阶段),字面意思就是开启系统属性服务,其实并不是真正意义上的服务,源码如下:
//system/system/core/init/property_service.cpp
void StartPropertyService(int* epoll_socket) {
InitPropertySet("ro.property_service.version", "2");
LOG(INFO) << "----SHEN: property_service StartPropertyService start";
int sockets[2];
//流程1:创建一对 双向通信的 Unix 域套接字(AF_UNIX),用于 init 进程与属性服务间的 IPC。SOCK_SEQPACKET 保证消息顺序,SOCK_CLOEXEC 防止子进程继承
if (socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, sockets) != 0) {
PLOG(FATAL) << "Failed to socketpair() between property_service and init";
}
//双向套接字类似于TCP,例如向sockets[0]写数据,sockets[1]可以直接读取到,实现一个全双工
//sockets[0] 赋值给函数参数epoll_socket,这个指针其实是init进程传递进来,那么init进程可以拿到这个socket[0],来直接合socket[1]进行通信
//sockets[1] 赋值给init_socket,用于接收 init 进程的消
*epoll_socket = from_init_socket = sockets[0];
init_socket = sockets[1];
StartSendingMessages();
//流程2:创建属性服务主Socket,路径为/dev/socket/property_service
if (auto result = CreateSocket(PROP_SERVICE_NAME, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, /*passcred=*/false, /*should_listen=*/false, 0666, /*uid=*/0, /*gid=*/0, /*socketcon=*/{});
result.ok()) {
property_set_fd = *result;
} else {
LOG(FATAL) << "start_property_service socket creation failed: " << result.error();
}
LOG(INFO) << "----SHEN: property_service StartPropertyService listen property_set_fd";
//流程3:监听property_set_fd,最大连接队列为8
listen(property_set_fd, 8);
LOG(INFO) << "----SHEN: property_service StartPropertyService property_service_thread";
//流程4:创建独立线程运行 PropertyServiceThread,处理属性设置请求(如 property_set() 调用)
auto new_thread = std::thread{PropertyServiceThread}; //此行代码会创建并启动线程,线程呗启动后立即执行PropertyServiceThread函数
property_service_thread.swap(new_thread);//交换线程的管理权给property_service_thread控制
auto async_persist_writes = android::base::GetBoolProperty("ro.property_service.async_persist_writes", false);
if (async_persist_writes) {
persist_write_thread = std::make_unique<PersistWriteThread>();
}
LOG(INFO) << "----SHEN: property_service StartPropertyService end";
}
1、属性服务与init之间的通信
在StartPropertyService函数中,主要几个核心流程如下:
- 创建了一对双向通信的Unix域套接字(socketpair):用于和init主进程进行通信
- 创建系统服务套接字(Socket):用于监听属性存储文件/dev/socket/property_service的数据变化
- 监听系统服务套接字(listen):监听系统服务套接字并设置最大连接队列8
- 创建系统服务线程(PropertyServiceThread):注册监听器并进入loop轮询
1)双向套接字sockets[0]
socketpair是Linux/UNIX系统中用于创建一对相互连接的匿名套接字的系统调用,主要服务于本地进程间通信(IPC)。通常使用场景:父进程与子进程之间的通信;多线程之间的通信;替代复杂的C/S通信模式。有如下几个特点:
- 写入sv[0]的数据只能从sv[1]读取,反之亦然
- 若未写入数据时读取会阻塞
- 需显式关闭未使用的描述符以避免资源泄漏
sockets[0]被赋值给epoll_socket,他又是StartPropertyService函数传递进来的参数,这个函数在init主函数中被调用,即sockets[0]被init进程持有:
根据双向socket可以知道,init进程持有之后,拿来做什么呢?
即SendLoadPersistentPropertiesMessage函数来给这个sockets[0]发送消息,从消息的名字set_load_persistent_properties来看用来加载persistent属性的动作。即这个时候sockets[1]就能够监听得到这个消息,后续会讲解他在系统服务线程PropertyServiceThread中处理这个消息。
这里继续讨论一下,init进程什么时候会调用SendLoadPersistentPropertiesMessage函数来发送消息呢?
即在post-fs-data的事件触发的时候,通知init进程去进行load_persist_props动作,去加载系统属性。
2)主套接字property_set_fd
接下来在看看上面这段代码,createsocket用来创建套接字,这里专门是为了属性服务进行创建的,经过研读此函数和PROP_SERVICE_NAME来看,其实就是在/dev/socket目录下面创建了一个名为property_service文件的特殊文件:
2、PropertyServiceThread线程轮询监听
并且后续还创建了一个PropertyServiceThread线程来对其进行监听和轮询:
//system/core/init/property_service.cpp
static void PropertyServiceThread() {
Epoll epoll;
//创建并打开epoll:epoll是linux中用来同时管理监听上千个套接字,只要套接字中有事件触发,就会回调后面指定的函数指针
if (auto result = epoll.Open(); !result.ok()) {
LOG(FATAL) << result.error();
}
LOG(INFO) << "----SHEN: property_service PropertyServiceThread epoll.handle_property_set_fd";
//注册函数handle_property_set_fd监听文件描述符property_set_fd
if (auto result = epoll.RegisterHandler(property_set_fd, handle_property_set_fd);
!result.ok()) {
LOG(FATAL) << result.error();
}
//注册函数HandleInitSocket监听init_socket
if (auto result = epoll.RegisterHandler(init_socket, HandleInitSocket); !result.ok()) {
LOG(FATAL) << result.error();
}
//进入轮询:从epoll中读取事件,epoll.Wait(std::nullopt)阻塞读取
while (true) {
auto epoll_result = epoll.Wait(std::nullopt);
LOG(INFO) << "----SHEN: property_service PropertyServiceThread loop";
if (!epoll_result.ok()) {
LOG(ERROR) << epoll_result.error();
LOG(INFO) << "----SHEN: property_service PropertyServiceThread loop " << epoll_result.error();
}
}
}
这里使用了linux的epoll机制。epoll
是Linux内核提供的一种高效I/O多路复用机制,专门用于管理大量文件描述符(包括socket)。它可以同时监听数千甚至数万个socket,是现代高性能网络服务器的核心技术(如Nginx、Redis等)
从上面的逻辑来看,epoll同时监听了两个套接字:
- property_set_fd:系统属性服务主套接字,在handle_property_set_fd函数中响应
- sockets[1]:init_socket套接字就是前面提到的双向套接字的另一端,在HandleInitSocket函数中响应
1)双向套接字sockets[1]的响应:HandleInitSocket
本章第一小节提到,sockets[0]和sockets[1]是一堆双向套接字,在init主线程在post-fs-data阶段的时候会触发load_persist_props函数向sockets[0]发送消息,sockets[1]就会收到消息,并且在如下函数中响应,用来加载系统属性的一些初始值:
如上代码,即init进程拿到sockets[0]之后,在post-fs-data阶段,通过如上方式去触发,对系统属性的一些初始化。这种方式感觉怪怪的,为什么不直接在PropertyInit中执行?难道是因为系统属性服务相关依赖文件系统的挂载?
2)主套接字property_set_fd的响应:handle_property_set_fd
再来看看property_set_fd套接字的监听函数:根据如下的大概逻辑可以了解到,直接通过accept从套接字中读取数据,并讲结果转换层cmd命令,通常是设置系统属性。
3、系统属性的设置与监听
handle_property_set_fd里面对日志进行了设置和处理:
1)PropertySet属性的设置
属性的设置流程大概如上,主要还是通过__system_property_find和__system_property_update和__system_property_add这些调用来实现对/dev/__properties__文件目录的一些控制
值得注意的就是persist这类属性是写入到内存里面的,参考
2)NotifyPropertyChange属性的监听
4、property_service日志断层问题?
如上日志,init进程中间存在5秒的断档,这5秒期间应该有如下流程:
- PropertyInit里面的ProcessKernelDt/ProcessKernelCmdline/ProcessBootconfig/ExportKernelBootProps/PropertyLoadBootDefaults的日志
- StartPropertyService里面的HandleInitSocket日志
但是最后有奇迹般的出现StartPropertyService里面loop循环,最初以为是进程挂掉了,后面综合init整个进程的角度来看,我更加比较倾向中间这5秒无法输出日志?具体原因有待确定
三、总结与相关案例
1、流程图
如上流程图,我这里给出我的一些观后感:
- PropertyInit函数被调用的时机太早,虽然文件系统已经准备好,但是只能访问dev目录,像data目录都还没有挂载,所以这个函数并不是从/data/property/persistent_properties中进行加载,而是从kernel参数以及启动参数去初始化属性
-
StartPropertyService函数主要目的是去启动属性服务,主要目的就是创建套接字,创建监听器,最后必须要进入轮询,为后续属性动态监听和响应init.rc来作准备
-
post-fs-data事件触发的时候才去调用LoadPersistenProperties函数去加载/data/property/persistent_properties中的属性。为什么要选择在这个时候去加载?因为这个时候才挂载好data目录
2、案例之ro属性实现允许被修改
在init进程中进行ro属性设置,如果此属性已经被设置过或者被初始化过,那么将会报如下异常,错误码为0xb,PROP_ERROR_READ_ONLY_PROPERTY被定义为0x0b。
但是我们可以更正这里的逻辑,让我们能够setprop已经存在的ro属性,参考案例如下:
3、案例之PRODUCT_PRODUCT_PROPERTIES :=和+=的区别
接着如上案例,因为ro.setupwizard.rotation_locked为ro属性,因此只能被赋值一次且无法修改,那么考虑使用PRODUCT_PRODUCT_PROPERTIES来进行初始化,在实验中发现如下两种有趣的现象:
- 写法一:先进行+=对ro属性进行初始化,然后通过:=对ro属性进行重新设置,最后的结果是true,即:=没有对ro属性进行一个覆盖?第二行的:=居然指向失败?
#先初始化ro属性
PRODUCT_PRODUCT_PROPERTIES += ro.setupwizard.rotation_locked=true
#重新设置ro属性,设置失败
PRODUCT_PRODUCT_PROPERTIES := ro.setupwizard.rotation_locked=false
#结论一:如果PRODUCT_PRODUCT_PROPERTIES对某ro属性定义了两次,并没有对属性的所有定义进行整合?即后面定义的值无法覆盖前文定义的值?那么init初始化属性的时候对其进行了两次设置?
#结论二:init初始化属性的时候在第二次设置的时候,针对ro属性直接设置失败,即ro属性只能被初始化一次
- 写法二:先进行:=ro属性进行设置,然后通过+=对ro属性进行设置,最后结果是false
#先初始化ro属性
PRODUCT_PRODUCT_PROPERTIES := ro.setupwizard.rotation_locked=false
#重新设置ro属性,设置失败
PRODUCT_PRODUCT_PROPERTIES += ro.setupwizard.rotation_locked=true
#结论一:因为已经存在值,所以+=直接无效