深入講解Android Property機制

lidki 9年前發布 | 109K 次閱讀 Android Android開發 移動開發

 

深入講解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進程里啟動的。我們知道,initLinux系統中用戶空間的第一個進程。它負責創建系統中最關鍵的幾個子進程,比如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__文件并不是符號鏈接,所以當然可以成功openO_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;

}</pre>

這個環境變量在日后有可能被其他進程拿來用,從而將屬性內存區映射到自己的內存空間去,這個后文會細說。


         接下來,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;

}</pre>

其主要動作無非是加載若干屬性文件,然后創建并監聽一個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);
}

}</pre>

其中調用的read_file()函數很簡單,只是把文件內容的所有字節讀入一個buffer,并在內容最后添加兩個字節:’\n’0

        接著調用的load_properties()函數,會逐行分析傳來的buffer,解析出行內的keyvalue部分,并調用property_set(),將keyvalue設置進系統的屬性共享內存去。

        我們繪制出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);
                }
            }
        }
    }
}

}</pre>

這段代碼是說,當獲取的屬性名和屬性值,與當初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();
    }
}

}</pre>

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</pre>

<span style="font-family:宋體;">也就是說,有意義的部分的最大字節數分別為</span><span>31</span><span style="font-family:宋體;">字節和</span><span>91</span><span style="font-family:宋體;">字節,最后一個字節先被強制設為</span><span>0</span><span style="font-family:宋體;">了。</span> </p>


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;

}</pre>

可以看到,如果設置方的uidAID_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 } };</pre>

    uidAID_SHELL的進程可以啟動、停止dumpstate服務,uidAID_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);
    

    } }</pre>

    假設從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);
    

    }</pre>

    這里啟動的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);
    

    . . . . . .</pre>

    其中 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");
    

    }</pre>

    其中的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); }</pre>

    一般情況下,這種在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);
    

    } }</pre>

     

    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"); } }</pre>

       可以看到,停止一個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; . . . . . . } }</pre>

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

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

      深入講解Android Property機制

            這其實很容易理解,比如要設置“sys.”打頭的系統屬性,進程的uid就必須是AID_SYSTEM,否則阿貓阿狗都能設置系統屬性,豈不糟糕。


      2.2.2.2  property_set()

      權限檢查通過之后,就可以真正設置屬性了。在前文“概述”一節中,我們已經說過,只有Property Service(即init進程)可以寫入屬性值,而普通進程最多只能通過socketProperty Service發出設置新屬性值的請求,最終還得靠Property Service來寫。那么我們就來看看Property Service里具體是怎么寫的。

               總體說來,property_set()會做如下工作:

      1)  判斷待設置的屬性名是否合法;
      2)  盡力從“屬性共享內存”中找到匹配的prop_info節點,如果能找到,就調用__system_property_update(),當然如果屬性是以“ro.”打頭的,說明這是個只讀屬性,此時不會update的;如果找不到,則調用__system_property_add()添加屬性節點。
      3)  updateadd動作之后,還需要做一些善后處理。比如,如果改動的是“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; } </pre>

               一開始當然要先找到“希望設置的目標屬性”在共享內存里對應的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);
       }
      

      } } }</pre>

      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()namevalue參數匹配,如果匹配,就把這個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; }</pre>

      其中調用的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)); }</pre>

              當一個用戶進程被調用起來時,內核會先調用到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;</pre>

 

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;</pre>

現在的問題是,這棵樹是如何組織其枝葉的?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_btleftright域。這么說來,以不同順序添加屬性,其實會導致最終得到的字典樹在形態上發生些許變化。

         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);
}
. . . . . .
. . . . . .</pre> <p>
</p>

         我們就以上面的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; }</pre>

最終調用的還是property_get()函數。

5      尾聲

至此,有關Android屬性機制的大體機理就講解完畢了,希望對大家有點兒幫助。

來自:http://my.oschina.net/youranhongcha/blog/389640

 本文由用戶 lidki 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!