欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

android 8.1 属性服务源码详解

程序员文章站 2022-05-08 16:09:44
...

一、 概述

我们知道,在 windows 平台中有一个叫注册表的东西,注册表可以存储一些类似 key/value 的键值对。一般来说,系统或某些应用程序会把自己的一些属性存储在注册表职工,即便系统重启或应用程序重启,它还能根据之前在注册表中设置的属性,进行相应的初始化工作。

Android 平台提供了一个类似的机制,称之为属性服务(property service),应用程序可以通过这个属性机制,查询或设置属性。

adb shell 控制行命令下,通过 getprop 查询属性,通过 setprop 设置属性。

android:/ $ getprop
[UserVolumeLabel]: [RockChips]
[camera2.portability.force_api]: [1]
[dalvik.vm.appimageformat]: [lz4]
[dalvik.vm.dex2oat-Xms]: [64m]
[dalvik.vm.dex2oat-Xmx]: [512m]

android:/ $ setprop vold.has_adoptable 0

属性在整个系统中全局可见,每个进程都可以 get/set 属性,在编译的过程中,android 的各种系统参数会被汇总到 build.prop 文件中,系统开机之后会读取配置文件并构建共享缓存区。

本章主要分析在 Android 系统源码中属性服务是如何构建和使用的。

源码分析将从属性服务的三个方向对整个属性服务系统进行分析:
1. property_init —— 属性服务初始化
2. property_set —— 属性设置流程
3. start_property_service —— 开启属性服务

二、property_init

2.1、属性服务基本原理说明

Android 的属性服务是利用 mmap 实现共享内存实现的。

首先通过 mmap 映射出一段专门用于存储属性值的共享内存地址空间(为:/dev/properties/ 路径),所有的应用程序都可以访问这段内存空间,这样就实现了进程间的信息共享。

应用程序访问共享内存包含读写操作,android 中为了实现各个进程对共享进程写操作的同步,它规定所有对属性变量的写操作请求都会通过 socket 通讯发送至 init 进程属性后台服务,有 init 进程统一处理所有进程的属性变量写请求。

2.2、初始化属性服务共享内存

android 8.1 的属性服务共享内存的分类更为精确,不同前缀的属性被分在了不同(目录)的共享内存中。

上面的操作是通过解析属性类别文件 (property_contexts)进行分类:init 进程解析属性类别文件,然后将解析到的前缀(key),类别名称(value)分别存放到 prefixes 链表 和 contexts 链表中。

这样做的好处是将庞杂的属性根据前缀进行分类,存储到不同的共享内存中,查找和修改都更加高效,类似于 map 的做法。下面是 property_context 的一些类别选项。

bluetooth.              u:object_r:bluetooth_prop:s0
config.                 u:object_r:config_prop:s0

上面的句子表示以 bluetooth. 打头的属性将被存储到 /dev/properties/u:object_r:bluetooth_prop:s0 这一块共享内存中。

了解了属性服务的基本存储规则之后,再来分析属性服务的初始化代码。

代码路径:source/bionic/libc/bionic/system_properties.cpp

void property_init() {
    if (__system_property_area_init()) {
        LOG(ERROR) << "Failed to initialize property area";
        exit(1);
    }
}

int __system_property_area_init() {
  // ① 
  free_and_unmap_contexts();
  // ② 
  mkdir(property_filename, S_IRWXU | S_IXGRP | S_IXOTH);
  // ③ 
  if (!initialize_properties()) {
    return -1;
  }
  bool open_failed = false;
  bool fsetxattr_failed = false;
  list_foreach(contexts, [&fsetxattr_failed, &open_failed](context_node* l) {
    if (!l->open(true, &fsetxattr_failed)) {
      open_failed = true;
    }
  });
  if (open_failed || !map_system_property_area(true, &fsetxattr_failed)) {
    free_and_unmap_contexts();
    return -1;
  }
  initialized = true;
  return fsetxattr_failed ? -2 : 0;
}
  1. free_and_unmap_contexts 函数清空属性类别相关的两个链表, prefixes 和 contexts 链表。
  2. 随后创建了 property_filename 目录,这个目录用来存储属性共享内存相关文件,路径名称为:/dev/properties,当然,在这个路径下根据不同的属性类别创建了不同的属性存储区域。
android:/dev/__properties__ # ls
properties_serial                    u:object_r:ffs_prop:s0                 u:object_r:pan_result_prop:s0
u:object_r:audio_prop:s0             u:object_r:fingerprint_prop:s0         u:object_r:persist_debug_prop:s0
u:object_r:bluetooth_prop:s0         u:object_r:firstboot_prop:s0           u:object_r:persistent_properties_ready_prop:s0
u:object_r:boottime_prop:s0          u:object_r:graphic_prop:s0             u:object_r:powerctl_prop:s0
  1. initialize_properties 函数解析属性类别文件,根据前缀的不同,创建不同的属性共享内存区域,主要是通过 initialize_properties_from_file 函数解析系统的属性类别文件,然后填充 prefixes 和 contexts 链表。

initialize_properties_from_file

static bool initialize_properties_from_file(const char* filename) {
  FILE* file = fopen(filename, "re");
  if (!file) {
    return false;
  }

  char* buffer = nullptr;
  size_t line_len;
  char* prop_prefix = nullptr;
  char* context = nullptr;

  // 以行为单位读取,将结果保存到 buffer 中
  while (getline(&buffer, &line_len, file) > 0) {
    // 将 buffer 的数据,按空格作为区分,key 赋值给 prop_prefix,value 赋值给 context
    int items = read_spec_entries(buffer, 2, &prop_prefix, &context);
    if (items <= 0) { // 没有读取到,比如 # 这种是注释
      continue;
    }
    if (items == 1) { // 只读取到 key,释放 key 的内存
      free(prop_prefix);
      continue;
    }

    // 以 ctl. 开头属性类别忽略掉,因为这个不属于属性,主要用于 IPC 机制
    if (!strncmp(prop_prefix, "ctl.", 4)) {
      free(prop_prefix);
      free(context);
      continue;
    }

    /*
     * C++中[ arg1,arg2,... ](T param, T param1,... ){ commond} 这个是lambda表达式,也可以看作一个函数指针
     * []中是引用外部参数
     *()中是参数定义,这个跟普通方法的()一样
     * {}中是方法体
     */
     // list_find 函数的作用是遍历 contexts 链表,如果发现 context 值已经存在
     // 就将对应的链表结构 context_node 返回
    auto old_context =
        list_find(contexts, [context](context_node* l) { return !strcmp(l->context(), context); });
    if (old_context) {
      // context 已经存在,按 prefix 的长度顺序更新 prefixes 链表
      list_add_after_len(&prefixes, prop_prefix, old_context);
    } else {
      // context 是新属性类别,将 context 值加到 contexts 链表中
      list_add(&contexts, context, nullptr);
      list_add_after_len(&prefixes, prop_prefix, contexts);
    }
    free(prop_prefix);
    free(context);
  }

  free(buffer);
  fclose(file);

  return true;
}

理解上面的这段代码,首先需要熟悉链表结构。prefixes 链表的类型为 prefix_node,contexts 链表的类型为 context_node。

struct prefix_node {
  prefix_node(struct prefix_node* next, const char* prefix, context_node* context)
      : prefix(strdup(prefix)), prefix_len(strlen(prefix)), context(context), next(next) {
  }
  ~prefix_node() {
    free(prefix);
  }
  char* prefix;
  const size_t prefix_len;
  context_node* context;
  struct prefix_node* next;
};

class context_node {
 public:
  context_node(context_node* next, const char* context, prop_area* pa)
      : next(next), context_(strdup(context)), pa_(pa), no_access_(false) {
    lock_.init(false);
  }
  ~context_node() {
    unmap();
    free(context_);
  }
  bool open(bool access_rw, bool* fsetxattr_failed);
  bool check_access_and_open();
  void reset_access();

  const char* context() const {
    return context_;
  }
  prop_area* pa() {
    return pa_;
  }

  context_node* next;

 private:
  bool check_access();
  void unmap();

  Lock lock_;
  char* context_;
  prop_area* pa_;
  bool no_access_;
};

prefix_node 有三个比较重要的成员变量:
1. prefix —— 用来存储属性类别项的 key
2. context —— 用来存储相关联的 context_node
3. next —— 链表下一个节点

context_node 也有几个比较重要的成员变量:
1. context —— 用来存储属性类别项的 value
2. _pa —— prop_area 类型,包含共享属性内存区域的详细信息(地址、当前数据、偏移量等),后文详细说明
3. next —— 链表下一个节点

可以看到,initialize_properties_from_file 实际上就是解析属性类别文件,将所有的属性类别信息存储到 prefixs 和 contexts 链表中,后续属性共享内存地址的初始化将围绕这两个链表结构进行。

申请各个属性类别对应的共享内存空间

前面说到,android 8.1 代码中将属性进行了更加详细的分类,根据属性contexts前缀的不同,将属性信息保存在不同的目录下,下面的代码根据已经初始化完成的 prefixs & contexts 链表 map 出共享内存空间。

bool open_failed = false;
bool fsetxattr_failed = false;
list_foreach(contexts, [&fsetxattr_failed, &open_failed](context_node* l) {
if (!l->open(true, &fsetxattr_failed)) {
  open_failed = true;
}
});

if (open_failed || !map_system_property_area(true, &fsetxattr_failed)) {
    free_and_unmap_contexts();
    return -1;
}
initialized = true;

遍历 contexts 链表,对链表中的每个 context 都执行 open 操作。可以猜测出, open 函数就是主要的操作函数,用来申请共享内存空间,下面来看看这个函数。

bool context_node::open(bool access_rw, bool* fsetxattr_failed) {
  lock_.lock();
  if (pa_) {
    lock_.unlock();
    return true;
  }

  char filename[PROP_FILENAME_MAX];
  int len = async_safe_format_buffer(filename, sizeof(filename), "%s/%s", property_filename,
                                     context_);
  if (len < 0 || len > PROP_FILENAME_MAX) {
    lock_.unlock();
    return false;
  }

  if (access_rw) {
    pa_ = map_prop_area_rw(filename, context_, fsetxattr_failed);
  } else {
    pa_ = map_prop_area(filename);
  }
  lock_.unlock();
  return pa_;
}

首先构造共享地址路径 filename,由 property_filename 和 context_ 拼接而成(/dev/properties/u:object_r:audio_prop:s0),然后调用 map_prop_area_rw 函数 map 出可读写的共享内存空间,空间首地址被赋值给 _pa 成员变量。

static prop_area* map_prop_area_rw(const char* filename, const char* context,
                                   bool* fsetxattr_failed) {
  // 打开共享内存目录,如果不存在则创建
  const int fd = open(filename, O_RDWR | O_CREAT | O_NOFOLLOW | O_CLOEXEC | O_EXCL, 0444);

  if (fd < 0) {
    if (errno == EACCES) {
      /* for consistency with the case where the process has already
       * mapped the page in and segfaults when trying to write to it
       */
      abort();
    }
    return nullptr;
  }

  ...

  pa_size = PA_SIZE;
  pa_data_size = pa_size - sizeof(prop_area);

  // 调用 mmap 函数映射内存地址
  void* const memory_area = mmap(nullptr, pa_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
  if (memory_area == MAP_FAILED) {
    close(fd);
    return nullptr;
  }

  // 共享内存空间首地址赋值给 _pa
  prop_area* pa = new (memory_area) prop_area(PROP_AREA_MAGIC, PROP_AREA_VERSION);

  close(fd);
  return pa;
}

至此,property_init 函数调用结束,该函数初始化了 prefixs 和 contexts 两个,并创建了相应属性的内存映射共享地址。

三、property_set

property_set 用于设置属性,将 key/value 键值对存储在共享内存中。

代码路径:source/system/core/init/property_service.cpp

static uint32_t PropertySetImpl(const std::string& name, const std::string& value) {
    size_t valuelen = value.size();

    // 检查属性名的合法性
    if (!is_legal_property_name(name)) {
        LOG(ERROR) << "property_set(\"" << name << "\", \"" << value << "\") failed: bad name";
        return PROP_ERROR_INVALID_NAME;
    }

    if (valuelen >= PROP_VALUE_MAX) {
        LOG(ERROR) << "property_set(\"" << name << "\", \"" << value << "\") failed: "
                   << "value too long";
        return PROP_ERROR_INVALID_VALUE;
    }

    // 根据属性名找到对应的 prop_info 信息
    prop_info* pi = (prop_info*) __system_property_find(name.c_str());
    if (pi != nullptr) {
        // ro.* properties are actually "write-once".
        if (android::base::StartsWith(name, "ro.")) {
            LOG(ERROR) << "property_set(\"" << name << "\", \"" << value << "\") failed: "
                       << "property already set";
            return PROP_ERROR_READ_ONLY_PROPERTY;
        }

        __system_property_update(pi, value.c_str(), valuelen);
    } else {
        int rc = __system_property_add(name.c_str(), name.size(), value.c_str(), valuelen);
        if (rc < 0) {
            LOG(ERROR) << "property_set(\"" << name << "\", \"" << value << "\") failed: "
                       << "__system_property_add failed";
            return PROP_ERROR_SET_FAILED;
        }
    }

    // 如果属性名以 persist 打头,表明是永久存储的键值对,不会随重启重置
    // 所以需要存储到不一样的路径 /data/property/
    if (persistent_properties_loaded && android::base::StartsWith(name, "persist.")) {
        write_persistent_property(name.c_str(), value.c_str());
    }

    // 通知属性发生变化,如果 rc 文件中有属性变化相关的 action,则执行
    property_changed(name, value);
    return PROP_SUCCESS;
}

这个函数中有几个比较重要的函数,首先根据属性名找到对应的 prop_info —— __system_property_find,如果 pi 不为空,表明属性名原先已经存在,则调用 __system_property_update 函数进行更新,否则调用 __system_property_add 添加属性节点。下面分别看下这几个函数。

3.1、__system_property_find

理解这个函数首先我们需要理解属性在共享内存中是如何组织存储的。

在 Android 的源码中,有 3 个跟属性组织存储相关的结构体:prop_info & prop_area & prop_bt,通俗来说,属性共享内存中的内容被组织成一棵字典树,内存块中的第一个节点是特殊的共享内存区域总述节点,类型为 prop_area,包含这块共享内存区域的首地址、使用情况等信息。

字典树之后,自然紧跟着 “树枝” 和 “树叶”,”树枝” 以 prob_bt 表示,”树叶” 以 prop_info 表示,我们在读取和设置属性值时,最终都是在操作 “树叶” 节点。

共享内存中的数据结构 & 存储原理

class prop_area {
  uint32_t bytes_used_; // 数据偏移量
  atomic_uint_least32_t serial_; // 该字段用于属性读写的互斥操作
  uint32_t magic_;
  uint32_t version_;
  uint32_t reserved_[28];
  char data_[0];
}

struct prop_info {
  atomic_uint_least32_t serial; // 该字段用于属性读写的互斥操作
  // we need to keep this buffer around because the property
  // value can be modified whereas name is constant.
  char value[PROP_VALUE_MAX];   // 属性值
  char name[0];     // 属性名
 }

前面说到,属性存储组织结构为一棵字典树,prop_bt 就是这棵字典树的 “树枝”,负责整棵树的组织工作。

struct prop_bt {
  uint32_t namelen;
  atomic_uint_least32_t prop;

  atomic_uint_least32_t left;
  atomic_uint_least32_t right;

  atomic_uint_least32_t children;

  char name[0];
}

在 system_property.cpp 中有一段这样的注释描述字典树的组织结构:

/*
 * Properties are stored in a hybrid trie/binary tree structure.
 * Each property's name is delimited at '.' characters, and the tokens are put
 * into a trie structure.  Siblings at each level of the trie are stored in a
 * binary tree.  For instance, "ro.secure"="1" could be stored as follows:
 *
 * +-----+   children    +----+   children    +--------+
 * |     |-------------->| ro |-------------->| secure |
 * +-----+               +----+               +--------+
 *                       /    \                /   |
 *                 left /      \ right   left /    |  prop   +===========+
 *                     v        v            v     +-------->| ro.secure |
 *                  +-----+   +-----+     +-----+            +-----------+
 *                  | net |   | sys |     | com |            |     1     |
 *                  +-----+   +-----+     +-----+            +===========+
 */

就这张图说明几点:
1. prop_area 包含共享内存区域的详细信息,无论是树枝 prop_bt 还是树叶 prop_info 都是基于 prop_area ,所以如果要找属性名对应的 prop_info 或者 prop_bt 前提都需要先找到属性名对应的 prop_area 共享内存信息。
2. 图刚开始的空白结构代表 prop_area 起始地址位置处一个预置的 prob_bt 节点,在 prop_area 的构造函数中已经表明,可以理解为父节点。

bytes_used_ = sizeof(prop_bt);
  1. 属性名以 “.” 符号为分割符,比如 ro.secure 属性会被分割成 “ro” 和 “secure” 两部分,并且每个部分都使用一个 prop_bt 节点表达。值得注意的是:这个 ‘.’ 关系被表示为父子关系,所以 “ro” 节点的 children 域指向 “secure”,但是请注意的是,一个 prop_bt 节点只会有一个 children 域,如果它还有其他孩子,那么这些孩子将和第一个节点(比如 secure 节点)组成一棵二叉树。

这一点非常重要,图中 ro 的 left 指向 net,不是代表 ro.net 打头的属性,而是 net 打头的属性。

  1. 当一个属性对应的 “树枝” prop_bt 构建好之后,会另外创建一个 prop_info 节点,专门表示这个属性,这个节点就是 “字典树叶”

prop_bt 节点的 name 域只记录
“树枝” 的名字,比如 “ro”、”secure” 等等,而 prop_info 节点的 name 域记录的则是属性的全名,比如 “ro.secure”

下面用一个例子说明属性存储的字典树是如何组织的:
我们手头有3个属性,分别为:

ro.secure
ro.boot.netd
net.bt.name

按照顺序进行属性设置,形成的字典数如下所示:
android 8.1 属性服务源码详解

其中,每一块椭圆形代表一个 prop_bt 节点,每个 prop_bt 节点都包含 children、left、right 域,用来组织字典树。属性最后完成时,会生成一个 prop_info 节点用来概述属性内容,该 prop_info name 域为属性的全名。

代码分析

通过上面属性内存数据结构及存储原理的分析,看代码就会简单一些。

const prop_info* __system_property_find(const char* name) {
  if (!__system_property_area__) {
    return nullptr;
  }

  prop_area* pa = get_prop_area_for_name(name);
  if (!pa) {
    async_safe_format_log(ANDROID_LOG_ERROR, "libc", "Access denied finding property \"%s\"", name);
    return nullptr;
  }

  return pa->find(name);
}

可以看到,get_prop_area_for_name 函数找到 name 对应 prop_area,前面在初始化属性共享内存的时候,对 contexts 链表中每一个类别的拼接地址(如/dev/properties/u:object_r:audio_prop:s0) 进行了内存映射,将首地址赋给了 prop_area 类型的 _pa 对象。这里就是找到遍历 contexts 链表找到对应的 _pa 对象。

prop_area 类型的 _pa 是属性内存存储组织的总述节点,有了 _pa 根据前面的阐述的原理,一步一步就能获取封装 prop_info 结构,这里就不做代码记录了。

const prop_info* prop_area::find(const char* name) {
  return find_property(root_node(), name, strlen(name), nullptr, 0, false);
}

const prop_info* prop_area::find_property(prop_bt* const trie, const char* name, uint32_t namelen,
                                          const char* value, uint32_t valuelen,
                                          bool alloc_if_needed) {
  if (!trie) return nullptr;

  const char* remaining_name = name;
  prop_bt* current = trie;
  while (true) {
    const char* sep = strchr(remaining_name, '.');
    const bool want_subtree = (sep != nullptr);
    const uint32_t substr_size = (want_subtree) ? sep - remaining_name : strlen(remaining_name);

    if (!substr_size) {
      return nullptr;
    }

    prop_bt* root = nullptr;
    uint_least32_t children_offset = atomic_load_explicit(&current->children, memory_order_relaxed);
    if (children_offset != 0) {
      root = to_prop_bt(&current->children);
    } else if (alloc_if_needed) {
      uint_least32_t new_offset;
      root = new_prop_bt(remaining_name, substr_size, &new_offset);
      if (root) {
        atomic_store_explicit(&current->children, new_offset, memory_order_release);
      }
    }

    if (!root) {
      return nullptr;
    }

    current = find_prop_bt(root, remaining_name, substr_size, alloc_if_needed);
    if (!current) {
      return nullptr;
    }

    if (!want_subtree) break;

    remaining_name = sep + 1;
  }

3.2、 __system_property_update

通过获取到的 prop_info 树叶信息(包含地址),即可更新属性值,下面具体分析一下。

prop_info 结构体成员结构如下:

struct prop_info {
  atomic_uint_least32_t serial; // 该字段用于属性读写的互斥操作
  // we need to keep this buffer around because the property
  // value can be modified whereas name is constant.
  char value[PROP_VALUE_MAX];   // 属性值
  char name[0];     // 属性名
 }

系统运行后,所有的进程都可以通过 socket 发送请求更新属性的值,所以属性读写操作需要一些手段去实现互斥操作。 这里有一个比较重要的成员 serial,就是防止多进程操作同一属性值得情况设计的。

int __system_property_update(prop_info* pi, const char* value, unsigned int len) {
  if (len >= PROP_VALUE_MAX) {
    return -1;
  }

  prop_area* pa = __system_property_area__;

  if (!pa) {
    return -1;
  }

  uint32_t serial = atomic_load_explicit(&pi->serial, memory_order_relaxed);
  serial |= 1;
  atomic_store_explicit(&pi->serial, serial, memory_order_relaxed);
  // The memcpy call here also races.  Again pretend it
  // used memory_order_relaxed atomics, and use the analogous
  // counterintuitive fence.
  atomic_thread_fence(memory_order_release);
  strlcpy(pi->value, value, len + 1);

  atomic_store_explicit(&pi->serial, (len << 24) | ((serial + 1) & 0xffffff), memory_order_release);
  __futex_wake(&pi->serial, INT32_MAX);

  atomic_store_explicit(pa->serial(), atomic_load_explicit(pa->serial(), memory_order_relaxed) + 1,
                        memory_order_release);
  __futex_wake(pa->serial(), INT32_MAX);

  return 0;
}

可以看到,属性值的更新其实非常简单,调用 strlcpy(pi->value, value, len + 1) 函数直接更新 prop_info value 值即可。

__futex_wake 以及 serial 值用于控制更新操作的并发进行。

3.3、 __system_property_add

如果未找到对应的 prop_info,也就是要设置的属性需要添加到属性共享内存中。

int __system_property_add(const char* name, unsigned int namelen, const char* value,
                          unsigned int valuelen) {
  if (valuelen >= PROP_VALUE_MAX) {
    return -1;
  }

  if (namelen < 1) {
    return -1;
  }

  if (!__system_property_area__) {
    return -1;
  }

  // 通过属性名找到对应的 prop_area
  prop_area* pa = get_prop_area_for_name(name);

  if (!pa) {
    async_safe_format_log(ANDROID_LOG_ERROR, "libc", "Access denied adding property \"%s\"", name);
    return -1;
  }

  bool ret = pa->add(name, namelen, value, valuelen);
  if (!ret) {
    return -1;
  }

  // 读写操作并发控制
  atomic_store_explicit(
      __system_property_area__->serial(),
      atomic_load_explicit(__system_property_area__->serial(), memory_order_relaxed) + 1,
      memory_order_release);
  __futex_wake(__system_property_area__->serial(), INT32_MAX);
  return 0;
}

首先进行一些属性合法性判断,get_prop_area_for_name 函数获取属性类别对应的 prop_area 信息,前面已经说过,prop_area 是属性共享内存地址空间操作的基础。

最后会调用 prop_area-> add 函数将属性添加到共享内存地址空间中。

bool prop_area::add(const char* name, unsigned int namelen, const char* value,
                    unsigned int valuelen) {
  return find_property(root_node(), name, namelen, value, valuelen, true);
}

与前面 __system_property_update 分析中阐述的原理一样,遍历属性名,以 “.” 为分割,依次查找对应的 prop_bt 节点,如果不存在则创建,并更新 children、left、right 域,最后在 prop_area 共享地址空间出分配出内存地址,创建 prop_info 节点存储属性的键值。

3.4、通知属性变化

property_changed(name, value);

该章节内容涉及 rc 文件的解析 & 处理,init 进程在处理 rc 文件时,如果文件中包含某属性变化时触发类型的 action section 时(如下),会将除第一行外的命令行封装成一个 action 动作,添加到全局 _action 链表中。

on property:vold.decrypt=trigger_shutdown_framework
    class_reset late_start
    class_reset main

当 set_property 设置属性时,扫尾工作中调用 property_changed 函数触发该动作,执行 action 包含的命令动作。

点击链接查看更详细说明:init 进程启动

四、start_property_service

void start_property_service() {
    property_set("ro.property_service.version", "2");

    property_set_fd = CreateSocket(PROP_SERVICE_NAME, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK,
                                   false, 0666, 0, 0, nullptr, sehandle);
    if (property_set_fd == -1) {
        PLOG(ERROR) << "start_property_service socket creation failed";
        exit(1);
    }

    listen(property_set_fd, 8);

    register_epoll_handler(property_set_fd, handle_property_set_fd);
}

之前的分析中 property_set 可以轻松设置系统属性,那为什么这里还要启动一个属性服务呢?

这里其实涉及到一些权限的问题,不是所有进程都可以随意修改任何的系统属性,
Android 将属性的设置统一交由 init 进程管理,其他进程不能直接修改属性,而只能通知 init 进程来修改,而在这过程中,init 进程可以进行权限检测控制,决定是否允许修改。

首先创建一个 socket 并返回文件描述符,然后设置最大并发数为 8,其他进程可以通过这个 socket 通知 init 进程修改系统属性。

最后注册 epoll 事件,也就是当监听到 property_set_fd 改变时调用 handle_property_set_fd 函数进行请求处理。

4.1、handle_property_set_fd

这个函数主要作用是建立 socket 连接,然后从 socket 中读取操作信息,根据不同的操作类型,调用 handle_property_set 做具体的操作

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

    struct ucred cr;
    socklen_t cr_size = sizeof(cr); // 获取操作进程凭证
    if (getsockopt(s, SOL_SOCKET, SO_PEERCRED, &cr, &cr_size) < 0) {
        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;
    }

    // 根据操作类型信息,执行对应处理,两者区别一个是以 char 形式读取,一个以 String 形式读取
    switch (cmd) {
    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;

        handle_property_set(socket, prop_value, prop_value, true);
        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;
        }

        handle_property_set(socket, name, value, false);
        break;
      }

    default:
        LOG(ERROR) << "sys_prop: invalid command " << cmd;
        socket.SendUint32(PROP_ERROR_INVALID_CMD);
        break;
    }
}

4.2、handle_property_set

这就是最终的处理函数,以 “ctl.” 打头的 key 就做一些 Service 的 Start、Stop、Restart 操作,其它的属性则调用 property_set 进行属性设置,
不管是前者还是后者,都要进行 SELinux 安全性检查,只有该进程有操作权限才能执行相应操作

static void handle_property_set(SocketConnection& socket,
                                const std::string& name,
                                const std::string& value,
                                bool legacy_protocol) {
  const char* cmd_name = legacy_protocol ? "PROP_MSG_SETPROP" : "PROP_MSG_SETPROP2";
  if (!is_legal_property_name(name)) {
    LOG(ERROR) << "sys_prop(" << cmd_name << "): illegal property name \"" << name << "\"";
    socket.SendUint32(PROP_ERROR_INVALID_NAME);
    return;
  }


  struct ucred cr = socket.cred();
  char* source_ctx = nullptr;
  getpeercon(socket.socket(), &source_ctx);

  if (android::base::StartsWith(name, "ctl.")) {
    if (check_control_mac_perms(value.c_str(), source_ctx, &cr)) {
      handle_control_message(name.c_str() + 4, value.c_str());
      if (!legacy_protocol) {
        socket.SendUint32(PROP_SUCCESS);
      }
    } else {
      LOG(ERROR) << "sys_prop(" << cmd_name << "): Unable to " << (name.c_str() + 4)
                 << " service ctl [" << value << "]"
                 << " uid:" << cr.uid
                 << " gid:" << cr.gid
                 << " pid:" << cr.pid;
      if (!legacy_protocol) {
        socket.SendUint32(PROP_ERROR_HANDLE_CONTROL_MESSAGE);
      }
    }
  } else {
    if (check_mac_perms(name, source_ctx, &cr)) {
      uint32_t result = property_set(name, value);
      if (!legacy_protocol) {
        socket.SendUint32(result);
      }
    } else {
      LOG(ERROR) << "sys_prop(" << cmd_name << "): permission denied uid:" << cr.uid << " name:" << name;
      if (!legacy_protocol) {
        socket.SendUint32(PROP_ERROR_PERMISSION_DENIED);
      }
    }
  }

  freecon(source_ctx);
}

操作流程示意:
1. 通过 is_legal_property_name 检测是否是合法的属性名
2. 通过 getpeercon 函数获取进程的安全上下文以及进程凭证,用于后面的 selinux 权限检测
3. 如果以 “ctl.” 打头的属性名表明是控制命令,使用 handle_control_message 进行处理,否则使用 property_set 进行常规属性设置。
4. 无论是控制命令还是常规属性设置操作之前,都要进行权限检测,根据前面 getpeercon 获取的进程安全上下文分别调用 check_control_mac_perms 和 check_mac_perms 判断进程有没有对应的权限进行操作。

相关标签: 属性服务