目錄[-]
深入講解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機制的示意圖如下:
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()中的調用關係如下:
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()時,會執行類似下面的句子:
1
2
3
|
get_property_workspace(&fd,
&sz); //
讀取pa_workspace.fd sprintf(tmp,
"%d,%d" ,
dup(fd), sz); add_environment( "ANDROID_PROPERTY_WORKSPACE" ,
tmp); |
【system/core/init/Init.c】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/*
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】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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】
1
2
3
4
5
6
7
8
9
10
11
12
|
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()函數的調用關係圖,如下:
2.1.2.2 創建socket接口
在加載動作完成後,start_property_service ()會創建一個socket接口,並監聽這個接口。
【core/init/Property_service.c】
1
2
3
4
5
6
|
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】
1
2
3
4
5
6
7
|
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】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
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】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
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 ; .
. . . . . } } |
看到了嗎?設置屬性時,一開始就把屬性名和屬性值的長度都限制了。
1
2
|
#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】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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】
1
2
3
4
5
6
7
8
9
10
11
12
13
|
/* *
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】
1
2
3
4
5
6
7
8
9
10
11
12
|
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)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
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:
我們知道,init進程在分析init.rc文件時,會形成一個service鏈表,現在msg_start()就是從這個service鏈表裏去查找相應名稱的service節點的。找到節點後,再調用service_start(svc, args)。
service_start()常常會fork一個子進程,然後爲它設置環境變量(ANDROID_PROPERTY_WORKSPACE):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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() 的代碼如下:
1
2
3
4
5
|
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]所指定的可執行文件,然後還要再寫個屬性值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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()的代碼如下:
1
2
3
4
5
6
7
8
9
|
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】
1
2
3
4
5
6
7
8
9
10
|
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); } } |
1
2
3
4
|
void
service_stop(struct service *svc) { service_stop_or_reset(svc,
SVC_DISABLED); } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
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進程處理屬性設置動作的地方:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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() 一句。該函數的代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
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表的定義如下:
這其實很容易理解,比如要設置“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】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
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】
1
2
3
4
5
|
void
property_changed( const
char
*name, const
char
*value) { if
(property_triggers_enabled) queue_property_triggers(name,
value); } |
【 system/core/init/Init_parser.c 】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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); } } } } |
1
2
3
4
5
6
|
void
action_add_queue_tail(struct action *act) { if
(list_empty(&act->qlist)) { list_add_tail(&action_queue,
&act->qlist); } } |
這幾個就是和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】
1
2
3
4
|
int
__system_property_area_init() { return
map_prop_area_rw(); } |
它映射時需要的是讀寫權限。而對普通進程而言,只有讀權限,當然不可能調用__system_property_area_init()了。其實在System_properties.c文件中,我們還可以找到另一個長得挺像的初始化函數——__system_properties_init():
1
2
3
4
|
int
__system_properties_init() { return
map_prop_area(); } |
它調用的map_prop_area()會把屬性共享內存,以只讀模式映射到用戶進程空間:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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()的代碼如下:
1
2
3
4
5
6
7
8
|
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】
1
2
3
4
5
6
|
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】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
__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】
1
2
3
4
5
6
7
|
__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 “屬性共享內存”裏的數據結構
【bionic/libc/bionic/System_properties.c】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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; |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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文件中,有一段註釋,給出了一個不算太清楚的示意圖,截取如下:
看過這張圖後,各位同學搞清楚了嗎?反正我一開始沒有搞清楚,後來只好研究代碼,現在算是知道一點兒了,詳情如下:
l 一開始的prop_area節點嚴格地說並不屬於字典樹,但是它代表着屬性共享內存塊的起始;
l 緊接着prop_area節點,需要有一個空白的prop_bt節點。這個是必須的噢,在前文說明init進程的main()函數的調用關係圖中,我們表達了這個概念:
這個就是空節點;
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
我們依此順序設置屬性,就會形成下面這樣的樹:
其中天藍色塊表示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屬性,會形成如下字典樹:
“rs”節點之所以在那個位置,是基於strcmp()的計算結果。“rs”字符串比“ro”字符串大,所以進一步和“ro”的right節點(即“sys”節點)比對,“rs”又比“sys”小,所以在“sys”節點的left枝上建立了新節點。
以上是畫成字典樹的樣子,它表示的是一種邏輯關係。而在實際的“屬性共享內存”中,這些節點基本上是緊湊排列的,大體上會形成下面這樣的排列關係:
說到這裏,大家應該已經比較清楚屬性共享內存塊是怎麼組織的吧。有了這種大致思路,再去看相應的代碼,相信大家會輕鬆一點兒。
3.2.2 property_get()
在讀取具體屬性值時,最終會調用到property_get()函數,該函數的調用關係如下:
說白了就是先從字典樹中找到感興趣的prop_info葉子,然後把葉子裏的值讀出來。
4 Java層的封裝
接下來我們再說說屬性機制裏Java層的封裝。這部分比較簡單,因爲它主要只是在簡單包裝C語言層次的函數。
Java層使用的屬性機制被封裝在SystemProperties中:
【frameworks/base/core/java/android/os/SystemProperties.java】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
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屬性機制的大體機理就講解完畢了,希望對大家有點兒幫助。