转载

【每日一博】深入讲解 Android Property 机制

深入讲解 Android Property 机制

1       概述

Android 系统(本文以 Android 4.4 为准)的属性( Property )机制有点儿类似 Windows 系统的注册表,其中的每个属性被组织成简单的键值对( key/value )供外界使用。

我们可以通过在 adb shell 里敲入 getprop 命令来获取当前系统的所有属性内容,而且,我们还可以敲入类似“ getprop 属性名”的命令来获取特定属性的值。另外,设置属性值的方法也很简单,只需敲入“ setprop 属性名 新值”命令即可。

可是问题在于我们不想只认识到这个层次,我们希望了解更多一些 Property 机制的运作机理,而这才是本文关心的重点。

      说白了, Property

机制的运作机理可以汇总成以下几句话:

1)  

系统一启动就会从若干属性脚本文件中加载属性内容;

2)   系统中的所有属性( key/value

)会存入同一块共享内存中;

3)  

系统中的各个进程会将这块共享内存映射到自己的内存空间,这样就可以直接读取属性内容了;

4)   系统中只有一个实体可以设置、修改属性值,它就是属性服务( Property Service

);

5)   不同进程只可以通过 socket

方式,向属性服务发出修改属性值的请求,而不能直接修改属性值;

6)   共享内存中的键值内容会以一种字典树的形式进行组织。

Property 机制的示意图如下:

【每日一博】深入讲解 Android Property 机制

2       Property Service

2.1   init 进程里的 Property Service

Property Service 实体其实是在 init 进程里启动的。我们知道, init Linux 系统中用户空间的第一个进程。它负责创建系统中最关键的几个子进程,比如 zygote 等等。在本节中,我们主要关心 init 进程是如何启动 Property Service 的。

我们查看 core/init/Init.c 文件,可以看到 init 进程的 main() 函数,它里面和 property

相关的关键动作有:

1 )间接调用 __system_property_area_init() :打开属性共享内存,并记入 __system_property_area

变量;

2 )间接调用 init_workspace()

:只读打开属性共享内存,并记入环境变量;

3 )根据 init.rc ,异步激发 property_service_init_action()

,该函数中会:

l  

加载若干属性文本文件,将具体属性、属性值记入属性共享内存;

l   创建并监听 socket

4 )根据 init.rc ,异步激发 queue_property_triggers_action() ,将刚刚加载的属性对应的激发动作,推入 action 列表。

main() 中的调用关系如下:

【每日一博】深入讲解 Android Property 机制

2.1.1    初始化属性共享内存

我们可以看到,在 init 进程的 main() 函数里,辗转打开了一个内存文件“ /dev/__properties__ ”,并把它设定为 128KB 大小,接着调用 mmap() 将这块内存映射到 init 进程空间了。这个内存的首地址被记录在 __system_property_area__ 全局变量里,以后每添加或修改一个属性,都会基于这个 __system_property_area__ 变量来计算位置。

       初始化属性内存块时,为什么要两次 open 那个 /dev/__properties__ 文件呢?我想原因是这样的:第一次 open 的句柄,最终是给属性服务自己用的,所以需要有读写权限;而第二次 open 的句柄,会被记入 pa_workspace.fd ,并在合适时机添加进环境变量,供其他进程使用,因此只能具有读取权限。

第一次 open 时,执行的代码如下:

fd = open (property_filename, O_RDWR | O_CREAT | O_NOFOLLOW | O_CLOEXEC | O_EXCL , 0444);

传给 open() 的参数标识里指明了 O_RDWR ,表示用“读写方式”打开文件。另外 O_NOFOLLOW 标识主要是为了防止我们打开“符号链接”,不过我们知道, __properties__ 文件并不是符号链接,所以当然可以成功 open O_CLOEXEC 标识是为了保证一种独占性,也就是说当 init 进程打开这个文件时,此时就算其他进程也 open 这个文件,也会在调用 exec 执行新程序时自动关闭该文件句柄。 O_EXCL 标识和 O_CREATE 标识配合起来,表示如果文件不存在,则创建之,而如果文件已经存在,那么 open 就会失败。第一次 open 动作后,会给 __system_property_area__ 赋值,然后程序会立即 close

刚打开的句柄。

第二次 open 动作发生在接下来的 init_workspace() 函数里。此时会再一次打开 __properties__ 文件,这次却是以只读模式打开的:

int fd = open ( PROP_FILENAME , O_RDONLY | O_NOFOLLOW );

打开的句柄记录在 pa_workspace.fd 处,以后每当 init 进程执行 socket 命令,并调用 service_start()

时,会执行类似下面的句子:

get_property_workspace(&fd, &sz);   // 读取pa_workspace.fd  sprintf(tmp, "%d,%d", dup(fd), sz);  add_environment("ANDROID_PROPERTY_WORKSPACE", tmp);
说白了就是把 pa_workspace.fd 的句柄记入一个名叫“ ANDROID_PROPERTY_WORKSPACE

”的环境变量去。

system/core/init/Init.c

/* add_environment - add "key=value" to the current environment */ int add_environment(const char *key, const char *val) {  int n;  for (n = 0; n < 31; n++) {   if (!ENV[n]) {    size_t len = strlen(key) + strlen(val) + 2;    char *entry = malloc(len);    snprintf(entry, len, "%s=%s", key, val);    ENV[n] = entry;    return 0;   }  }  return 1; } 

这个环境变量在日后有可能被其他进程拿来用,从而将属性内存区映射到自己的内存空间去,这个后文会细说。

         接下来, main() 函数在设置好属性内存块之后,会调用 queue_builtin_action() 函数向内部的 action_list 列表添加 action 节点。关于这部分的详情,可参考其他讲述 Android 启动机制的文档,这里不再赘述。我们只需知道,后续,系统会在合适时机回调“由 queue_builtin_action() 的参数”所指定的 property_service_init_action() 函数就可以了。

2.1.2    初始化属性服务

property_service_init_action() 函数只是在简单调用 start_property_service() 而已,后者的代码如下:

core/init/Property_service.c

void start_property_service(void) {  int fd;  load_properties_from_file(PROP_PATH_SYSTEM_BUILD);  load_properties_from_file(PROP_PATH_SYSTEM_DEFAULT);  /* Read vendor-specific property runtime overrides. */  vendor_load_properties();  load_override_properties();  /* Read persistent properties after all default values have been loaded. */  load_persistent_properties();  fd = create_socket(PROP_SERVICE_NAME, SOCK_STREAM, 0666, 0, 0);  if(fd < 0) return;  fcntl(fd, F_SETFD, FD_CLOEXEC);  fcntl(fd, F_SETFL, O_NONBLOCK);  listen(fd, 8);  property_set_fd = fd; } 

其主要动作无非是加载若干属性文件,然后创建并监听一个 socket 接口。

2.1.2.1   加载属性文本文件

start_property_service() 函数首先会调用 load_properties_from_file() 函数,尝试加载一些属性脚本文件,并将其中的内容写入属性内存块里。从代码里可以看到,主要加载的文件有:

l   /system/build.prop

l   /system/default.prop (该文件不一定存在)

l   /data/local.prop

l   /data/property

目录里的若干脚本

load_properties_from_file() 函数的代码如下:

core/init/Property_service.c

static void load_properties_from_file(const char *fn) {  char *data;  unsigned sz;  data = read_file(fn, &sz);  if(data != 0) {   load_properties(data);   free(data);  } } 

其中调用的 read_file() 函数很简单,只是把文件内容的所有字节读入一个 buffer ,并在内容最后添加两个字节: ’/n’ 0

         接着调用的 load_properties() 函数,会逐行分析传来的 buffer ,解析出行内的 key value 部分,并调用 property_set() ,将 key value 设置进系统的属性共享内存去。

我们绘制出 property_service_init_action() 函数的调用关系图,如下:

【每日一博】深入讲解 Android Property 机制

2.1.2.2   创建 socket 接口

在加载动作完成后, start_property_service () 会创建一个 socket 接口,并监听这个接口。

core/init/Property_service.c

fd = create_socket(PROP_SERVICE_NAME, SOCK_STREAM, 0666, 0, 0); if(fd < 0) return; fcntl(fd, F_SETFD, FD_CLOEXEC); fcntl(fd, F_SETFL, O_NONBLOCK); listen(fd, 8); property_set_fd = fd;

这个 socket 是专门用来监听其他进程发来的“修改”属性值的命令的,它被设置成“非阻塞”( O_NONBLOCK )的 socket

2.1.3    初始化属性后的触发动作

既然在上一小节的 property_service_init_action() 动作中,系统已经把必要的属性都加载好了,那么现在就可以遍历刚生成的 action_list ,看看哪个刚加载好的属性可以进一步触发连锁动作。这就是 init 进程里为什么有两次和属性相关的 queue_builtin_action() 的原因。

system/core/init/Init.c

static int queue_property_triggers_action(int nargs, char **args) {     queue_all_property_triggers();     /* enable property triggers */     property_triggers_enabled = 1;     return 0; }

system/core/init/Init_parser.c

void queue_all_property_triggers() {  struct listnode *node;  struct action *act;  list_for_each(node, &action_list) {   act = node_to_item(node, struct action, alist);   if (!strncmp(act->name, "property:", strlen("property:"))) {    /* parse property name and value       syntax is property:<name>=<value> */    const char* name = act->name + strlen("property:");    const char* equals = strchr(name, '=');    if (equals) {     char prop_name[PROP_NAME_MAX + 1];     char value[PROP_VALUE_MAX];     int length = equals - name;     if (length > PROP_NAME_MAX) {      ERROR("property name too long in trigger %s", act->name);     } else {      memcpy(prop_name, name, length);      prop_name[length] = 0;      /* does the property exist, and match the trigger value? */      property_get(prop_name, value);      if (!strcmp(equals + 1, value) ||!strcmp(equals + 1, "*")) {       action_add_queue_tail(act);      }     }    }   }  } } 

这段代码是说,当获取的属性名和属性值,与当初 init.rc 里记录的某 action 的激发条件匹配时,就把该 action 插入执行队列的尾部( action_add_queue_tail(act) )。

2.2   init 进程循环监听 socket

         现在再回过头看 init 进程,其 main() 函数的最后,我们可以看到一个 for(;;) 循环,不断监听外界发来的命令,包括设置属性的命令。

system/core/init/Init.c

for(;;) {  . . . . . .  . . . . . .  nr = poll(ufds, fd_count, timeout);  if (nr <= 0)   continue;  for (i = 0; i < fd_count; i++) {   if (ufds[i].revents == POLLIN) {    if (ufds[i].fd == get_property_set_fd())     handle_property_set_fd();    else if (ufds[i].fd == get_keychord_fd())     handle_keychord();    else if (ufds[i].fd == get_signal_fd())     handle_signal();   }  } } 

2.2.1    处理“ ctl. ”命令

         当从 socket 收到“设置属性”的命令后,会调用上面的 handle_property_set_fd() 函数,代码截选如下:

core/init/Property_service.c

void handle_property_set_fd() {  prop_msg msg;  . . . . . .  if ((s = accept(property_set_fd, (struct sockaddr *) &addr, &addr_size)) < 0) {   return;  }  . . . . . .   switch(msg.cmd) {  case PROP_MSG_SETPROP:   msg.name[PROP_NAME_MAX-1] = 0;   msg.value[PROP_VALUE_MAX-1] = 0;   . . . . . .   if(memcmp(msg.name,"ctl.",4) == 0) {    . . . . . .    if (check_control_perms(msg.value, cr.uid, cr.gid, source_ctx)) {     handle_control_message((char*) msg.name + 4, (char*) msg.value);    } else {     ERROR("sys_prop: Unable to %s service ctl [%s] uid:%d gid:%d pid:%d/n",       msg.name + 4, msg.value, cr.uid, cr.gid, cr.pid);    }   } else {    if (check_perms(msg.name, cr.uid, cr.gid, source_ctx)) {     property_set((char*) msg.name, (char*) msg.value);    } else {     ERROR("sys_prop: permission denied uid:%d  name:%s/n",        cr.uid, msg.name);    }    . . . . . .    close(s);   }   . . . . . .   break;  . . . . . .  } } 

         看到了吗?设置属性时,一开始就把属性名和属性值的长度都限制了。

#define PROP_NAME_MAX   32 #define PROP_VALUE_MAX  92

也就是说,有意义的部分的最大字节数分别为 31 字节和 91 字节,最后一个字节先被强制设为 0 了。

2.2.1.1   check_control_perms()

         对于普通属性而言,主要是调用 property_set() 来设置属性值,但是有一类特殊属性是以“ ctl. ”开头的,它们本质上是一些控制命令,比如启动某个系统服务。这种控制命令需调用 handle_control_message() 来处理。

当然,并不是随便谁都可以发出这种控制命令的,也就是说,不是谁都可以成功设置以“ ctl. ”开头的特殊属性。 handle_property_set_fd() 会先调用 check_control_perms() 来检查发起方是否具有相应的权限。

core/init/Property_service.c

static int check_control_perms(const char *name, unsigned int uid, unsigned int gid, char *sctx) {  int i;  if (uid == AID_SYSTEM || uid == AID_ROOT)    return check_control_mac_perms(name, sctx);  /* Search the ACL */  for (i = 0; control_perms[i].service; i++) {   if (strcmp(control_perms[i].service, name) == 0) {    if ((uid && control_perms[i].uid == uid) ||     (gid && control_perms[i].gid == gid)) {     return check_control_mac_perms(name, sctx);    }   }  }  return 0; } 

可以看到,如果设置方的 uid AID_SYSTEM 或者 AID_ROOT ,那么一般都是具有权限的。而如果 uid 是其他值,那么就得查 control_perms 表了,这个表的定义如下:

core/init/Property_service.c

/*  * White list of UID that are allowed to start/stop services.  * Currently there are no user apps that require.  */ struct {  const char *service;  unsigned int uid;  unsigned int gid; } control_perms[] = {  { "dumpstate",AID_SHELL, AID_LOG },  { "ril-daemon",AID_RADIO, AID_RADIO },   {NULL, 0, 0 } }; 

uid AID_SHELL 的进程可以启动、停止 dumpstate 服务, uid AID_RADIO 的进程可以启动、停止 ril-daemon 服务。

2.2.1.2   handle_control_message()

         在通过权限检查之后,就可以调用 handle_control_message() 来处理控制命令了:

system/core/init/Init.c

void handle_control_message(const char *msg, const char *arg) {  if (!strcmp(msg,"start")) {   msg_start(arg);  } else if (!strcmp(msg,"stop")) {   msg_stop(arg);  } else if (!strcmp(msg,"restart")) {   msg_restart(arg);  } else {   ERROR("unknown control msg '%s'/n", msg);  } } 

假设从 socket 发来的命令是“ ctl.start ”,那么就会走到 msg_start(arg)

static void msg_start(const char *name) {  struct service *svc = NULL;  char *tmp = NULL;  char *args = NULL;  if (!strchr(name, ':'))   svc = service_find_by_name(name);  else {   tmp = strdup(name);   if (tmp) {    args = strchr(tmp, ':');    *args = '/0';    args++;    svc = service_find_by_name(tmp);   }  }  if (svc) {   service_start(svc, args);  } else {   ERROR("no such service '%s'/n", name);  }  if (tmp)   free(tmp); } 

这里启动的 service 基本上都是在 init.rc 里说明的系统 service 。比如 netd

【每日一博】深入讲解 Android Property 机制

我们知道, init 进程在分析 init.rc 文件时,会形成一个 service 链表,现在 msg_start() 就是从这个 service 链表里去查找相应名称的 service 节点的。找到节点后,再调用 service_start(svc, args)

service_start() 常常会 fork 一个子进程,然后为它设置环境变量( ANDROID_PROPERTY_WORKSPACE ):

void service_start(struct service *svc, const char *dynamic_args) {  . . . . . .  . . . . . .  pid = fork();  if (pid == 0) {   struct socketinfo *si;   struct svcenvinfo *ei;   char tmp[32];   int fd, sz;   umask(077);   if (properties_inited()) {    get_property_workspace(&fd, &sz);    sprintf(tmp, "%d,%d", dup(fd), sz);    add_environment("ANDROID_PROPERTY_WORKSPACE", tmp);   }   for (ei = svc->envvars; ei; ei = ei->next)    add_environment(ei->name, ei->value);  . . . . . . 

get_property_workspace() 的代码如下:

void get_property_workspace(int *fd, int *sz) {     *fd = pa_workspace.fd;     *sz = pa_workspace.size; }

大家还记得前文阐述 init_workspace() 时,把打开的句柄记入 pa_workspace.fd 的句子吧,现在就是在用这个句柄。

一切准备好后, service_start() 会调用 execve() ,执行 svc->args[0] 所指定的可执行文件,然后还要再写个属性值:

void service_start(struct service *svc, const char *dynamic_args) {  . . . . . .  . . . . . .  execve(svc->args[0], (char**) arg_ptrs, (char**) ENV);  . . . . . .  . . . . . .  svc->time_started = gettime();  svc->pid = pid;  svc->flags |= SVC_RUNNING;  if (properties_inited())   notify_service_state(svc->name, "running"); } 

其中的 notify_service_state() 的代码如下:

void notify_service_state(const char *name, const char *state) {  char pname[PROP_NAME_MAX];  int len = strlen(name);  if ((len + 10) > PROP_NAME_MAX)   return;  snprintf(pname, sizeof(pname), "init.svc.%s", name);  property_set(pname, state); } 

一般情况下,这种在 init.rc 里记录的系统 service 的名字都不会超过 22 个字节,加上“ init.svc. ”前缀也不会超过 31 个字节,所以每次启动 service ,都会修改相应的属性。比如 netd 服务,一旦它被启动,就会将 init.svc.netd 属性的值设为“ running ”。

以上是 handle_control_message() 处理“ ctl.start ”命令时的情况,相应地还有处理“ ctl.stop ”命令的情况,此时会调用到 msg_stop()

system/core/init/Init.c

static void msg_stop(const char *name) {     struct service *svc = service_find_by_name(name);      if (svc) {         service_stop(svc);     } else {         ERROR("no such service '%s'/n", name);     } }
void service_stop(struct service *svc) {     service_stop_or_reset(svc, SVC_DISABLED); }
static void service_stop_or_reset(struct service *svc, int how) {  /* The service is still SVC_RUNNING until its process exits, but if it has   * already exited it shoudn't attempt a restart yet. */  svc->flags &= (~SVC_RESTARTING);  if ((how != SVC_DISABLED) && (how != SVC_RESET) && (how != SVC_RESTART)) {   /* Hrm, an illegal flag.  Default to SVC_DISABLED */   how = SVC_DISABLED;  }   /* if the service has not yet started, prevent    * it from auto-starting with its class    */  if (how == SVC_RESET) {   svc->flags |= (svc->flags & SVC_RC_DISABLED) ? SVC_DISABLED : SVC_RESET;  } else {   svc->flags |= how;  }  if (svc->pid) {   NOTICE("service '%s' is being killed/n", svc->name);   kill(-svc->pid, SIGKILL);   notify_service_state(svc->name, "stopping");  } else {   notify_service_state(svc->name, "stopped");  } } 

可以看到,停止一个 service 时,主要是调用 kill( ) 来杀死服务子进程,并将 init.svc.xxx 属性值设为 stopping

OK ,终于把 init 进程里,处理“ ctl. ”命令的部分讲完了,下面我们接着看 init 进程处理普通属性的部分。

2.2.2    处理属性设置命令

我们还是先回到前文 init 进程处理属性设置动作的地方:

void handle_property_set_fd() {    . . . . . .    if(memcmp(msg.name,"ctl.",4) == 0) {   . . . . . .    } else {   if (check_perms(msg.name, cr.uid, cr.gid, source_ctx)) {       property_set((char*) msg.name, (char*) msg.value);   } else {       ERROR("sys_prop: permission denied uid:%d  name:%s/n",        cr.uid, msg.name);   }   . . . . . .   close(s);    }    . . . . . .    break;     . . . . . .     } } 

2.2.2.1   check_perms()

要设置普通属性,也是要具有一定权限哩。请看上面的 check_perms()

一句。该函数的代码如下:

static int check_perms(const char *name, unsigned int uid, unsigned int gid, char *sctx) {  int i;  unsigned int app_id;  if(!strncmp(name, "ro.", 3))   name +=3;  if (uid == 0)   return check_mac_perms(name, sctx);  app_id = multiuser_get_app_id(uid);  if (app_id == AID_BLUETOOTH) {   uid = app_id;  }  for (i = 0; property_perms[i].prefix; i++) {   if (strncmp(property_perms[i].prefix, name,      strlen(property_perms[i].prefix)) == 0) {    if ((uid && property_perms[i].uid == uid) ||     (gid && property_perms[i].gid == gid)) {     return check_mac_perms(name, sctx);    }   }  }  return 0; } 

主要也是在查表, property_perms 表的定义如下:

【每日一博】深入讲解 Android Property 机制

       这其实很容易理解,比如要设置“ sys. ”打头的系统属性,进程的 uid 就必须是 AID_SYSTEM ,否则阿猫阿狗都能设置系统属性,岂不糟糕。

2.2.2.2   property_set()

权限检查通过之后,就可以真正设置属性了。在前文“概述”一节中,我们已经说过,只有 Property Service (即 init 进程)可以写入属性值,而普通进程最多只能通过 socket Property Service 发出设置新属性值的请求,最终还得靠 Property Service 来写。那么我们就来看看 Property Service 里具体是怎么写的。

         总体说来, property_set() 会做如下工作:

1)   判断待设置的属性名是否合法;

2)   尽力从“属性共享内存”中找到匹配的 prop_info 节点,如果能找到,就调用 __system_property_update() ,当然如果属性是以“ ro. ”打头的,说明这是个只读属性,此时不会 update 的;如果找不到,则调用 __system_property_add() 添加属性节点。

3)   update add 动作之后,还需要做一些善后处理。比如,如果改动的是“ net. ”开头的属性,那么需要重新设置一下 net.change 属性,属性值为刚刚设置的属性名字。

4)   如果要设置 persist 属性的话,只有在系统将所有的默认 persist 属性都加载完毕后,才能设置成功。 persist 属性应该是那种会存入可持久化文件的属性,这样,系统在下次启动后,可以将该属性的初始值设置为系统上次关闭时的值。

5)   如果将“ selinux.reload_policy ”属性设为“ 1 ”了,那么会进一步调用 selinux_reload_policy() 。这个意味着要重新加载 SEAndroid 策略。

6)   最后还需调用 property_changed() 函数,其内部会执行 init.rc 中指定的那些和 property 同名的 action

core/init/Property_service.c

int property_set(const char *name, const char *value) {  . . . . . .  . . . . . .  pi = (prop_info*) __system_property_find(name);  if(pi != 0) {   if(!strncmp(name, "ro.", 3)) return -1;   __system_property_update(pi, value, valuelen);  } else {   ret = __system_property_add(name, namelen, value, valuelen);   . . . . . .  }  if (strncmp("net.", name, strlen("net.")) == 0)  {   if (strcmp("net.change", name) == 0) {    return 0;   }   property_set("net.change", name);  } else if (persistent_properties_loaded &&    strncmp("persist.", name, strlen("persist.")) == 0) {   write_persistent_property(name, value);  } else if (strcmp("selinux.reload_policy", name) == 0 &&       strcmp("1", value) == 0) {   selinux_reload_policy();  }  property_changed(name, value);  return 0; }  

         一开始当然要先找到“希望设置的目标属性”在共享内存里对应的 prop_info 节点啦,后续关于 __system_property_update() __system_property_add() 的操作,主要都是在操作该 prop_info 节点,代码比较简单。 prop_info 的详细内容我们会在下文阐述,这里先跳过。

         如果可以找到 prop_info 节点,就尽量将这个属性的值更新一下,除非是遇到“ ro. ”属性,这种属性是只读的,当然不能 set 。如果找不到 prop_info 节点,此时会为这个新属性创建若干字典树节点,包括最终的 prop_info 叶子。

         属性写入完毕后,还要调用 property_changed() ,做一些善后处理:

system/core/init/Init.c

void property_changed(const char *name, const char *value) {     if (property_triggers_enabled)         queue_property_triggers(name, value); }

system/core/init/Init_parser.c

void queue_property_triggers(const char *name, const char *value) {  struct listnode *node;  struct action *act;  list_for_each(node, &action_list) {   act = node_to_item(node, struct action, alist);   if (!strncmp(act->name, "property:", strlen("property:"))) {    const char *test = act->name + strlen("property:");    int name_length = strlen(name);    if (!strncmp(name, test, name_length) &&      test[name_length] == '=' &&      (!strcmp(test + name_length + 1, value) ||       !strcmp(test + name_length + 1, "*"))) {     action_add_queue_tail(act);    }   }  } } 
void action_add_queue_tail(struct action *act) {     if (list_empty(&act->qlist)) {         list_add_tail(&action_queue, &act->qlist);     } }

从代码可以看出,当某个属性修改之后, Property Service 会遍历一遍 action_list 列表,找到其中匹配的 action 节点,并将之添加进 action_queue 队列。之所以会有 if (list_empty(&act->qlist)) 判断,是为了防止重复添加。下面是 init.rc 脚本中的一个片段:

system/core/rootdir/init.rc

【每日一博】深入讲解 Android Property 机制

这几个就是和 property 相关的 action ,其他相关的 action 还有不少,我们就不列了。我们以第一个 action 为例来说明。如果我们修改了 vold.decrypt 属性的值,那么 queue_property_triggers() 搜索 action_list 时,就能找到一个名为“ property:vold.decrypt=trigger_reset_main ”的 action 节点,此时的逻辑无非是比较“冒号后的名字”、“赋值号后的值”,是否分别和 queue_property_triggers() name value 参数匹配,如果匹配,就把这个 action 节点添加进 action_queue 队列里。

3       客户进程访问属性的机制

3.1   映射“属性共享内存”的时机

现在有一个问题必须先提出来,那就是“属性共享内存”是在什么时刻映射进用户进程空间的?总不会平白无故地就可以成功调用 property_get() 吧。其实,为了让大家方便地调用 property_get() ,属性机制的设计者的确是用了一点儿小技巧,下面我们就来看看细节。

3.1.1    静态加载时的初始化

在前文介绍 Init 进程初始化属性共享内存时,调用了一个叫做 __system_property_area_init() 的函数:

bionic/libc/bionic/System_properties.c

int __system_property_area_init() {     return map_prop_area_rw(); }

它映射时需要的是读写权限。而对普通进程而言,只有读权限,当然不可能调用 __system_property_area_init() 了。其实在 System_properties.c 文件中,我们还可以找到另一个长得挺像的初始化函数—— __system_properties_init()

int __system_properties_init() {     return map_prop_area(); }

它调用的 map_prop_area() 会把属性共享内存,以只读模式映射到用户进程空间:

static int map_prop_area() {  fd = open(property_filename, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);  . . . . . .  if ((fd < 0) && (errno == ENOENT)) {   fd = get_fd_from_env();     fromFile = false;  }  . . . . . .  pa_size = fd_stat.st_size;  pa_data_size = pa_size - sizeof(prop_area);  prop_area *pa = mmap(NULL, pa_size, PROT_READ, MAP_SHARED, fd, 0);  . . . . . .  result = 0;  __system_property_area__ = pa;  . . . . . .  return result; } 

其中调用的 get_fd_from_env() 的代码如下:

static int get_fd_from_env(void) {     char *env = getenv("ANDROID_PROPERTY_WORKSPACE");     if (!env) {         return -1;     }     return atoi(env); }

哇,终于看到读取“ ANDROID_PROPERTY_WORKSPACE ”环境变量的地方啦。不过呢,它的重要性似乎并没有我们一开始想的那么大。在 map_prop_area() 函数里分明写着,只有在 open() 属性文件不成功的情况下,才会尝试从环境变量中读取文件句柄,而一般都会 open 成功的。不管文件句柄 fd 是怎么得到的吧,反正能映射成空间地址就行。映射后的空间地址,仍然会记录在 __system_property_area__ 全局变量中。

         现在我们只需找到调用 __system_properties_init() 的源头就可以了。经过查找,我们发现 __libc_init_common() 会调用它,代码如下:

bionic/libc/bionic/Libc_init_common.cpp

void __libc_init_common(KernelArgumentBlock& args) {   . . . . . .   . . . . . .   _pthread_internal_add(main_thread);   __system_properties_init(); // Requires 'environ'. }

这个函数可是在 bionic 目录里的,小技巧已经用到 C 库里啦。

         __libc_init_common() 又会被 __libc_init() 调用:

bionic/libc/bionic/Libc_init_static.cpp

__noreturn void __libc_init(void* raw_args,  void (*onexit)(void),  int (*slingshot)(int, char**, char**),  structors_array_t const * const structors) {   KernelArgumentBlock args(raw_args);   __libc_init_tls(args);   __libc_init_common(args);   . . . . . .   . . . . . .   call_array(structors->preinit_array);   call_array(structors->init_array);   . . . . . .   exit(slingshot(args.argc, args.argv, args.envp)); } 

        当一个用户进程被调用起来时,内核会先调用到 C 运行期库( crtbegin )层次来初始化运行期环境,在这个阶段就会调用到 __libc_init() ,而后才会间接调用到 C 程序员熟悉的 main() 函数。可见属性共享内存在执行 main() 函数之前就已经映射好了。

3.1.2    动态加载时的初始化

除了 __libc_init() 中会调用 __libc_init_common() ,还有一处会调用。

bionic/libc/bionic/Libc_init_dynamic.cpp

__attribute__((constructor)) static void __libc_preinit() {   . . . . . .   __libc_init_common(*args);   . . . . . .   pthread_debug_init();   malloc_debug_init(); }

请大家注意函数名那一行起始处的 __attribute__((constructor)) 属性,这是 GCC 的一个特有属性。被这种属性修饰的函数会被放置在特殊的代码段中。这样,当动态链接器一加载 libc.so 时,会尽早执行 __libc_preinit() 函数。这样一来,动态库里也可以放心调用 property_get() 了。

3.2   读取属性值

下面我们来集中精力研究读取属性值的部分。我们在前文留下过一个尾巴,当时对属性共享内存块里的 prop_info 节点,只做了非常简略的提及,现在我们就来细说它。

说白了,属性共享内存中的内容,其实被组织成一棵字典树。内存块的第一个节点是个特殊的总述节点,类型为 prop_area 。紧随其后的就是字典树的“树枝”和“树叶”了,树枝以 prop_bt 表达,树叶以 prop_info 表达。我们读取或设置属性值时,最终都只是在操作“叶子”节点而已。

3.2.1    “属性共享内存”里的数据结构

【每日一博】深入讲解 Android Property 机制

bionic/libc/bionic/System_properties.c

struct prop_area {  unsigned bytes_used;  unsigned volatile serial;  unsigned magic;  unsigned version;  unsigned reserved[28];  char data[0]; }; typedef struct prop_area prop_area; struct prop_info {  unsigned volatile serial;  char value[PROP_VALUE_MAX];  char name[0]; }; typedef struct prop_info prop_info; 
typedef volatile uint32_t prop_off_t; struct prop_bt {  uint8_t namelen;  uint8_t reserved[3];  prop_off_t prop;  prop_off_t left;  prop_off_t right;  prop_off_t children;  char name[0]; }; typedef struct prop_bt prop_bt; 

现在的问题是,这棵树是如何组织其枝叶的? System_properties.c 文件中,有一段注释,给出了一个不算太清楚的示意图,截取如下:

【每日一博】深入讲解 Android Property 机制

看过这张图后,各位同学搞清楚了吗?反正我一开始没有搞清楚,后来只好研究代码,现在算是知道一点儿了,详情如下:

l   一开始的 prop_area 节点严格地说并不属于字典树,但是它代表着属性共享内存块的起始;

l   紧接着 prop_area 节点,需要有一个空白的 prop_bt 节点。这个是必须的噢,在前文说明 init 进程的 main()

函数的调用关系图中,我们表达了这个概念:

【每日一博】深入讲解 Android Property 机制
这个就是空节点;

l   属性名将以‘ . ’符号为分割符,被分割开来。比如 ro.secure 属性名就会被分割成“ ro ”和“ secure ”两部分,而且每个部分用一个 prop_bt 节点表达。

l   属性名中的这种‘ . ’关系被表示为父子关系,所以“ ro ”节点的 children 域,会指向“ secure ”节点。但是请注意,一个节点只有一个 children 域,如果它还有其他孩子,那些孩子将会和第一个子节点(比如 secure 节点)组成一棵二叉树。

l   当一个属性名对应的“字典树枝”都已经形成好后,会另外创建一个 prop_info

节点,专门表示这个属性,该节点就是“字典树叶”。

下面我们画几张图来说明问题。比如我们现在手头有 3 个属性,分别为

ro.abc.def

ro.hhh.def

sys.os.ccc

我们依此顺序设置属性,就会形成下面这样的树:

【每日一博】深入讲解 Android Property 机制

其中天蓝色块表示 prop_area 节点,桔黄色块表示 prop_bt 节点,浅绿色块表示 prop_info 节点。简单地说,父节点的 children 域,只指代其第一个子节点。后续从属于同一父节点的兄弟子节点,会被组织成一棵二叉子树,该二叉子树的根就是父节点的第一个子节点。我们用蓝色箭头来表示二叉子树的关系,在代码中对应 prop_bt left right 域。这么说来,以不同顺序添加属性,其实会导致最终得到的字典树在形态上发生些许变化。

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

         现在我们向上面这棵字典树中再添加一个 rs.ppp.qqq 属性,会形成如下字典树:

【每日一博】深入讲解 Android Property 机制

rs ”节点之所以在那个位置,是基于 strcmp() 的计算结果。“ rs ”字符串比“ ro ”字符串大,所以进一步和“ ro ”的 right 节点(即“ sys ”节点)比对,“ rs ”又比“ sys ”小,所以在“ sys ”节点的 left 枝上建立了新节点。

         以上是画成字典树的样子,它表示的是一种逻辑关系。而在实际的“属性共享内存”中,这些节点基本上是紧凑排列的,大体上会形成下面这样的排列关系:

【每日一博】深入讲解 Android Property 机制

         说到这里,大家应该已经比较清楚属性共享内存块是怎么组织的吧。有了这种大致思路,再去看相应的代码,相信大家会轻松一点儿。

3.2.2    property_get()

在读取具体属性值时,最终会调用到 property_get() 函数,该函数的调用关系如下:

【每日一博】深入讲解 Android Property 机制

说白了就是先从字典树中找到感兴趣的 prop_info 叶子,然后把叶子里的值读出来。

4       Java 层的封装

接下来我们再说说属性机制里 Java 层的封装。这部分比较简单,因为它主要只是在简单包装 C 语言层次的函数。

Java 层使用的属性机制被封装在 SystemProperties 中:

frameworks/base/core/java/android/os/SystemProperties.java

public class SystemProperties {  public static final int PROP_NAME_MAX = 31;  public static final int PROP_VALUE_MAX = 91;  private static final ArrayList<Runnable> sChangeCallbacks = new ArrayList<Runnable>();  private static native String native_get(String key);  private static native String native_get(String key, String def);  private static native int native_get_int(String key, int def);  private static native long native_get_long(String key, long def);  private static native boolean native_get_boolean(String key, boolean def);  private static native void native_set(String key, String def);  private static native void native_add_change_callback(); /**   * Get the value for the given key.   * @return an empty string if the key isn't found   * @throws IllegalArgumentException if the key exceeds 32 characters   */  public static String get(String key) {   if (key.length() > PROP_NAME_MAX) {    throw new IllegalArgumentException("key.length > " + PROP_NAME_MAX);   }   return native_get(key);  }  . . . . . .  . . . . . . 

         我们就以上面的 get() 成员函数为例来说明,它基本上只是在调用 native_get() 函数而已,该函数对应的 C 语言函数可以从下表查到,就是那个 SystemProperties_getS()

frameworks/base/core/jni/android_os_SystemProperties.cpp

static JNINativeMethod method_table[] = {     { "native_get", "(Ljava/lang/String;)Ljava/lang/String;",       (void*) SystemProperties_getS },     { "native_get", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",       (void*) SystemProperties_getSS },     { "native_get_int", "(Ljava/lang/String;I)I",       (void*) SystemProperties_get_int },     { "native_get_long", "(Ljava/lang/String;J)J",       (void*) SystemProperties_get_long },     { "native_get_boolean", "(Ljava/lang/String;Z)Z",       (void*) SystemProperties_get_boolean },     { "native_set", "(Ljava/lang/String;Ljava/lang/String;)V",       (void*) SystemProperties_set },     { "native_add_change_callback", "()V",       (void*) SystemProperties_add_change_callback }, };

frameworks/base/core/jni/android_os_SystemProperties.cpp

static jstring SystemProperties_getS(JNIEnv *env, jobject clazz,            jstring keyJ) {  return SystemProperties_getSS(env, clazz, keyJ, NULL); } static jstring SystemProperties_getSS(JNIEnv *env, jobject clazz,            jstring keyJ, jstring defJ) {  int len;  const char* key;  char buf[PROPERTY_VALUE_MAX];  jstring rvJ = NULL;  if (keyJ == NULL) {   jniThrowNullPointerException(env, "key must not be null.");   goto error;  }  key = env->GetStringUTFChars(keyJ, NULL);  len = property_get(key, buf, "");  if ((len <= 0) && (defJ != NULL)) {   rvJ = defJ;  } else if (len >= 0) {   rvJ = env->NewStringUTF(buf);  } else {   rvJ = env->NewStringUTF("");  }  env->ReleaseStringUTFChars(keyJ, key); error:  return rvJ; } 

最终调用的还是 property_get() 函数。

5       尾声

至此,有关 Android 属性机制的大体机理就讲解完毕了,希望对大家有点儿帮助。

正文到此结束
Loading...