引言
本文借鉴自邓平凡著《深入理解android卷I》第三章:【深入理解init】3.2.4小节:【属性服务】,以及《深入讲解Android Property机制》,仅供记录学习使用,Android版本4.4 kitkak。
Android平台的property service(属性服务)机制,类似于windows中的注册表。通常,系统或应用会将一些属性以键值对的形式存储在注册表中,使得系统重启或者应用重启后,能够根据之前注册表中的设置进行初始化系统或应用。
1、Property初始化
property属性的相关代码可以在/system/core/init目录下找到,首先是在init.c文件的main函数中可以看到几个和property相关的语句(省略了其他无关代码):
// kitkak/system/core/init/init.c
int main(int argc, char **argv)
{
int property_set_fd_init = 0;
bool is_charger = false;
property_init();
is_charger = !strcmp(bootmode, "charger");
INFO("property init\n");
if (!is_charger)
property_load_boot_defaults();
…………
queue_builtin_action(property_service_init_action, "property_service_init");
/* run all property triggers based on current state of the properties */
queue_builtin_action(queue_property_triggers_action, "queue_property_triggers");
for(;;) {
int nr, i, timeout = -1;
execute_one_command();
restart_processes();
if (!property_set_fd_init && get_property_set_fd() > 0) {
ufds[fd_count].fd = get_property_set_fd();
ufds[fd_count].events = POLLIN;
ufds[fd_count].revents = 0;
fd_count++;
property_set_fd_init = 1;
}
…………
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();
}
}
}
return 0;
}
首先来看property_init()函数:
// kitkat/system/core/init/property_service.c
void property_init(void)
{
init_property_area();
}
// kitkat/system/core/init/property_service.c
static int init_property_area(void)
{
if (property_area_inited) //初始化部分:static int property_area_inited = 0;
return -1;
if(__system_property_area_init())
return -1;
if(init_workspace(&pa_workspace, 0))
return -1;
fcntl(pa_workspace.fd, F_SETFD, FD_CLOEXEC);
property_area_inited = 1;
return 0;
}
__system_property_area_init()函数是bionic c库中的函数,其位于:/bionic/libc/bionic/system_properties.c:
// kitkat/bionic/libc/bionic/system_properties.c
int __system_property_area_init()
{
return map_prop_area_rw();
}
// kitkat/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;
…………
prop_area *__system_property_area__ = NULL;
// kitkat\bionic\libc\include\sys\_system_properties.h
#define PROP_FILENAME "/dev/__properties__"
// kitkat\bionic/libc/bionic/system_properties.c
static char property_filename[PATH_MAX] = PROP_FILENAME;
// kitkat\bionic\libc\bionic\system_properties.c
static int map_prop_area_rw()
{
prop_area *pa;
int fd;
int ret;
/* dev is a tmpfs that we can use to carve a shared workspace
* out of, so let's do that...
*/
fd = open(property_filename, O_RDWR | O_CREAT | O_NOFOLLOW | O_CLOEXEC |
O_EXCL, 0444);
if (fd < 0) {
if (errno == EACCES) { //open函数所打开的文件不符合所要求测试的权限
/* for consistency with the case where the process has already
* mapped the page in and segfaults when trying to write to it
*/
abort();
}
return -1;
}
ret = fcntl(fd, F_SETFD, FD_CLOEXEC);
if (ret < 0)
goto out;
if (ftruncate(fd, PA_SIZE) < 0)
goto out;
pa_size = PA_SIZE;
pa_data_size = pa_size - sizeof(prop_area);
compat_mode = false;
pa = mmap(NULL, pa_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(pa == MAP_FAILED)
goto out;
memset(pa, 0, pa_size);
pa->magic = PROP_AREA_MAGIC;
pa->version = PROP_AREA_VERSION;
/* reserve root node */
pa->bytes_used = sizeof(prop_bt);
/* plug into the lib property services */
__system_property_area__ = pa;
close(fd);
return 0;
out:
close(fd);
return -1;
}
__system_property_area_init()函数辗转调用map_prop_area_rw()函数,可以看到其首先通过open函数在/dev目录下打开了一个文件:__properties__,open函数的第三个八进制参数0444表示该文件的权限为:-r--r--r--,第二个参数意义如下:
- O_RDWR:读写打开;
- O_CREAT:若欲打开的文件不存在则自动建立该文件,使用该选项时,需要第三个参数mode,用来指定新文件的访问权限位(对应上面的0444);
- O_EXCL:如果O_CREAT 也被设置,此指令会去检查文件是否存在。文件若不存在则建立该文件,否则将导致打开文件错误。 此外, 若O_CREAT 与O_EXCL 同时设置,并且欲打开的文件为符号链接,则会打开文件失败;
- O_NOFOLLOW:如果参数pathname 所指的文件为一符号连接,则会令打开文件失败;
- O_CLOEXEC:在进程执行exec系统调用时关闭此打开的文件描述符(原子操作)。防止父进程泄露打开的文件给子进程,即便子进程没有相应权限;
函数fcntl(fd, F_SETFD, FD_CLOEXEC)的作用是将文件描述符close-on-exec标志设置为第三个参数的最后一位,而函数ftruncate(fd, PA_SIZE)会将参数fd指定的文件大小改为参数PA_SIZE所指定的大小:
// kitkat\bionic\libc\include\sys\_system_properties.h
#define PA_SIZE (128 * 1024)
接下来便是mmap函数,其主要功能是将文件映射进内存。这里将文件映射进内存,用于后续对__prooerties__文件进行内存共享。该函数原型为:void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);
1、参数start:指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址;
2、参数length:代表将文件中多大的部分映射到内存;
3、参数prot:映射区域的保护方式。可以为以下这种方式的组合:
- PROT_EXEC:映射区域可被执行;
- PROT_READ:映射区域可被读取;
- PROT_WRITE:映射区域可被写入;
- PROT_NONE:映射区域不能存取;
4、参数flags:影响映射区域的各种特性。在调用mmap()时必须要指定MAP_SHARED 或MAP_PRIVATE;
-
MAP_FIXED:如果参数start所指的地址无法成功建立映射时,则放弃映射,不对地址做修正,通常不鼓励用此旗标;
-
MAP_SHARED:对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享;
-
MAP_PRIVATE:对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”(copy on write)对此区域作的任何修改都不会写回原来的文件内容;
-
MAP_ANONYMOUS:建立匿名映射。此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享;
-
MAP_DENYWRITE:只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝;
-
MAP_LOCKED:将映射区域锁定住,这表示该区域不会被置换(swap);
5、参数fd:要映射到内存中的文件描述符。如果使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1。有些系统不支持匿名内存映射,则可以使用fopen打开/dev/zero文件,然后对该文件进行映射,可以同样达到匿名内存映射的效果。
6、参数offset:文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小的整数倍。
7、返回值:若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1),错误原因存于errno 中。
8、错误代码:
-
EBADF:参数fd 不是有效的文件描述词;
-
EACCES:存取权限有误。如果是MAP_PRIVATE 情况下文件必须可读,使用MAP_SHARED则要有PROT_WRITE以及该文件要能写入;
-
EINVAL:参数start、length 或offset有一个不合法;
-
EAGAIN:文件被锁住,或是有太多内存被锁住;
-
ENOMEM:内存不足;
根据代码中的mmap函数的调用情况:pa = mmap(NULL, pa_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);可以看出其将__prooerties__文件的PA_SIZE大小以共享的方式映射进内存,映射区域拥有读写权限,映射内存的起始地址赋值给局部prop_area类型指针pa,后续代码通过对指针pa所指向的实例进行赋值,并将pa指针赋值给全局变__system_property_area__ ,然后关闭文件句柄。这个全局变量__system_property_area__作为指向属性文件所映射的内存空间起始位置的指针,担负着后续其他程序调用获取和设置属性的主要对象的责任,文章后续还能见到。这里的映射过程借用别人的图片来展示一下这个过程:
init_property_area函数中调用的__system_property_area_init()函数功能就此结束,接下来看后续调用的init_workspace函数。
// kitkat/system/core/init/property_service.c
typedef struct {
size_t size;
int fd;
} workspace;
static int init_workspace(workspace *w, size_t size)
{
void *data;
int fd = open(PROP_FILENAME, O_RDONLY | O_NOFOLLOW);
if (fd < 0)
return -1;
w->size = size;
w->fd = fd;
return 0;
}
static workspace pa_workspace;
根据函数调用init_workspace(&pa_workspace, 0)和函数定义可以看出,其以只读的方式再次打开文件"/dev/__properties__",这里把打开该文件的句柄保存在静态结构体实例pa_workspace的成员fd中,因为后续要用到。return之后返回init_property_area函数,其后续将property_area_inited标志置为1,property_init函数结束。至此,函数property_init的调用链如下图:
2、启动Property
// kitkak/system/core/init/init.c
INFO("property init\n");
if (!is_charger)
property_load_boot_defaults();
…………
queue_builtin_action(property_service_init_action, "property_service_init");
在init.c文件的property_init函数之后,会看到上面的语句,这里的charger是关机充电下的系统模式,所以正常非关机充电情况下会执行property_load_boot_defaults()函数,该函数只有一个对/default.prop属性文件的加载语句 load_properties_from_file(PROP_PATH_RAMDISK_DEFAULT),后面会进行解析。而语句queue_builtin_action(property_service_init_action, "property_service_init")的作用就是把property_service_init_action函数处理为一个名为property_service_init的action(这个action同init进程启动过程中,在init.rc文件能看到的各种action相同),并将该action追加到init进程启动过程中的action列表末尾,当顺序执行或者触发名为property_service_init的action时,就会执行property_service_init_action函数,该函数只有一个对start_property_service函数的调用语句。
// kitkat/system/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);
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;
}
start_property_service函数首先按照顺序加载几个默认属性文件,这几个属性文件的路径定义位于kitkat\bionic\libc\include\sys\_system_properties.h:
/*
** Rules:
**
** - there is only one writer, but many readers
** - prop_area.count will never decrease in value
** - once allocated, a prop_info's name will not change
** - once allocated, a prop_info's offset will not change
** - reading a value requires the following steps
** 1. serial = pi->serial
** 2. if SERIAL_DIRTY(serial), wait*, then goto 1
** 3. memcpy(local, pi->value, SERIAL_VALUE_LEN(serial) + 1)
** 4. if pi->serial != serial, goto 2
**
** - writing a value requires the following steps
** 1. pi->serial = pi->serial | 1
** 2. memcpy(pi->value, local_value, value_len)
** 3. pi->serial = (value_len << 24) | ((pi->serial + 1) & 0xffffff)
*/
#define PROP_PATH_RAMDISK_DEFAULT "/default.prop"
#define PROP_PATH_SYSTEM_BUILD "/system/build.prop"
#define PROP_PATH_SYSTEM_DEFAULT "/system/default.prop"
#define PROP_PATH_LOCAL_OVERRIDE "/data/local.prop"
#define PROP_PATH_FACTORY "/factory/factory.prop"
// kitkat/system/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);
}
}
// kitkat/system/core/init/property_service.c
/* reads a file, making sure it is terminated with \n \0 */
void *read_file(const char *fn, unsigned *_sz)
{
char *data;
int sz;
int fd;
struct stat sb;
data = 0;
fd = open(fn, O_RDONLY);
if(fd < 0) return 0;
// for security reasons, disallow world-writable
// or group-writable files
if (fstat(fd, &sb) < 0) {
ERROR("fstat failed for '%s'\n", fn);
goto oops;
}
if ((sb.st_mode & (S_IWGRP | S_IWOTH)) != 0) {
ERROR("skipping insecure file '%s'\n", fn);
goto oops;
}
sz = lseek(fd, 0, SEEK_END);
if(sz < 0) goto oops;
if(lseek(fd, 0, SEEK_SET) != 0) goto oops;
data = (char*) malloc(sz + 2);
if(data == 0) goto oops;
if(read(fd, data, sz) != sz) goto oops;
close(fd);
data[sz] = '\n';
data[sz+1] = 0;
if(_sz) *_sz = sz;
return data;
oops:
close(fd);
if(data != 0) free(data);
return 0;
}
read_file函数中的结构体 stat和fstat参考《struct stat 操作 小结》可以看出,首先以只读方式打开属性文件并返回该文件句柄,因为后面的fstat函数需要一个已打开的文件句柄作为实参,把该文件的状态和属性赋值到stat结构体当中去。
关于lseek函数,函数声明:off_t lseek(int fd, off_t offset, int whence);函数说明:每一个已打开的文件都有一个读写位置,当打开文件时通常其读写位置是指向文件开头,若是以附加的方式打开文件(如O_APPEND),则读写位置会指向文件尾。当read()或write()时,读写位置会随之增加,所以lseek()的作用就是用来控制该文件的读写位置。当调用成功时则返回目前的读写位置,也就是距离文件开头多少个字节,若有错误则返回-1。关于参数whence:
- SEEK_SET:参数offset 即为新的读写位置;
- SEEK_CUR:以目前的读写位置往后增加offset 个位移量;
- SEEK_END:将读写位置指向文件尾后再增加offset 个位移量;
- 当whence 值为SEEK_CUR 或SEEK_END 时, 参数offet 允许负值的出现;
常用方式:
- 欲将读写位置移到文件开头时:lseek(int fildes, 0, SEEK_SET)
- 欲将读写位置移到文件尾时:lseek(int fildes, 0, SEEK_END)
- 想要取得目前文件位置时:lseek(int fildes, 0, SEEK_CUR)
可以看出两次执行lseek主要是为了检测能否正常移动到文件首部和尾部,而移动到尾部的时候返回值sz即为该文件大小。后面malloc申请了sz+2大小的空间,用于后面的read函数把文件内容读取出来,存储到data所指向的内存空间中,并在末尾添加了'\n'和0,返回指针data。
函数load_properties_from_file中,将属性文件内容读取出来以后,便通过load_properties函数将属性文件中每行的key与value提取出来,执行property_set(key, value)将该属性键值设置进属性内存区域。property_set函数代码如下:
// kitkat/system/core/init/property_service.c
int property_set(const char *name, const char *value)
{
prop_info *pi;
int ret;
size_t namelen = strlen(name);
size_t valuelen = strlen(value);
if (!is_legal_property_name(name, namelen)) return -1;
if (valuelen >= PROP_VALUE_MAX) return -1;
pi = (prop_info*) __system_property_find(name);
if(pi != 0) {
/* ro.* properties may NEVER be modified once set */
if(!strncmp(name, "ro.", 3)) return -1;
__system_property_update(pi, value, valuelen);
} else {
ret = __system_property_add(name, namelen, value, valuelen);
if (ret < 0) {
ERROR("Failed to set '%s'='%s'\n", name, value);
return ret;
}
}
/* If name starts with "net." treat as a DNS property. */
if (strncmp("net.", name, strlen("net.")) == 0) {
if (strcmp("net.change", name) == 0) {
return 0;
}
/*
* The 'net.change' property is a special property used track when any
* 'net.*' property name is updated. It is _ONLY_ updated here. Its value
* contains the last updated 'net.*' property.
*/
property_set("net.change", name);
} else if (persistent_properties_loaded &&
strncmp("persist.", name, strlen("persist.")) == 0) {
/*
* Don't write properties to disk until after we have read all default properties
* to prevent them from being overwritten by default values.
*/
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;
}
其首先通过is_legal_property_name()函数,判断传入的属性名称是否合法:
// kitkat/system/core/init/property_service.c
static bool is_legal_property_name(const char* name, size_t namelen)
{
size_t i;
bool previous_was_dot = false;
if (namelen >= PROP_NAME_MAX) return false;
if (namelen < 1) return false;
if (name[0] == '.') return false;
if (name[namelen - 1] == '.') return false;
/* Only allow alphanumeric, plus '.', '-', or '_' */
/* Don't allow ".." to appear in a property name */
for (i = 0; i < namelen; i++) {
if (name[i] == '.') {
if (previous_was_dot == true) return false;
previous_was_dot = true;
continue;
}
previous_was_dot = false;
if (name[i] == '_' || name[i] == '-') continue;
if (name[i] >= 'a' && name[i] <= 'z') continue;
if (name[i] >= 'A' && name[i] <= 'Z') continue;
if (name[i] >= '0' && name[i] <= '9') continue;
return false;
}
return true;
}
可以看出对属性名有着严格的限制,首先关于属性的长度,在kitkat\bionic\libc\include\sys\system_properties.h中有定义:
#define PROP_NAME_MAX 42
#define PROP_VALUE_MAX 92
即属性名的长度不得超过42,不得小于1, 而属性值的长度不得超过92。属性名的第一个字符和最后一个都不得为点(.),也不得出现连续的点(..)。属性名的字符范围限制在下划线(_)、连接符(-)、点(.)、大小写英文字母(A-Z和a-z)、数字0-9当中。
如果属性名合法,便会执行__system_property_find()函数去寻找该属性名是否已经存在,该函数定义在kitkat\bionic\libc\bionic\system_properties.c中,其最终调用该文件中的find_property()函数在属性字典树结构中去查找该属性名是否已经存在,这里调用find_property()的时候就是以前面提到的全局变量__system_property_area__为对象,在其所指向的内存空间中查找。
能在该树形结构中找到的属性,只要不是"ro."开头的属性,就直接调用system_properties.c文件中的__system_property_update()函数对属性值进行更新。对于一些特殊的属性,property_set函数进行了特殊处理。"ro."开头的属性,因为在设定上只能初始话一次,所以不得更改;"net."开头的属性值发生更改后,必须也对"net.change"的属性值发生更改,比如语句property_set("net.hostname", "xxxx")将属性"net.hostname"的值设置为"xxxx",那么必然也会执行property_set("net.change", "net.hostname")将属性"net.change"属性的值改为"net.hostname",代表属性"net.hostname"发生了更改;"persist."开头的属性则会在设备/data/property目录下建立对应的文件,该文件以属性名为文件名,属性值为文件内容。
init进程的配置文件init.rc中存在一些依赖属性值来触发的action,所以property_set函数在更新或添加一个属性值过后,需要通过property_changed()间接调用queue_property_triggers()函数,去查看这个属性的更改会不会触发哪些action,如果会触发,则将这些action添加到action任务列表中,等待顺序执行。至此,属性文件的加载动作结束。
函数start_property_service在加载完属性文件后,便通过语句fd = create_socket(PROP_SERVICE_NAME, SOCK_STREAM, 0666, 0, 0)来创建socket:
// kitkat/system/core/init/util.c
int create_socket(const char *name, int type, mode_t perm, uid_t uid, gid_t gid)
{
struct sockaddr_un addr;
int fd, ret;
char *secon;
fd = socket(PF_UNIX, type, 0);
if (fd < 0) {
ERROR("Failed to open socket '%s': %s\n", name, strerror(errno));
return -1;
}
memset(&addr, 0 , sizeof(addr));
addr.sun_family = AF_UNIX;
snprintf(addr.sun_path, sizeof(addr.sun_path), ANDROID_SOCKET_DIR"/%s",
name);
ret = unlink(addr.sun_path);
if (ret != 0 && errno != ENOENT) {
ERROR("Failed to unlink old socket '%s': %s\n", name, strerror(errno));
goto out_close;
}
secon = NULL;
if (sehandle) {
ret = selabel_lookup(sehandle, &secon, addr.sun_path, S_IFSOCK);
if (ret == 0)
setfscreatecon(secon);
}
ret = bind(fd, (struct sockaddr *) &addr, sizeof (addr));
if (ret) {
ERROR("Failed to bind socket '%s': %s\n", name, strerror(errno));
goto out_unlink;
}
setfscreatecon(NULL);
freecon(secon);
chown(addr.sun_path, uid, gid);
chmod(addr.sun_path, perm);
INFO("Created socket '%s' with mode '%o', user '%d', group '%d'\n",
addr.sun_path, perm, uid, gid);
return fd;
out_unlink:
unlink(addr.sun_path);
out_close:
close(fd);
return -1;
}
首先出现的结构体类型sockaddr_un位于kitkat\bionic\libc\kernel\common\linux\un.h:
/****************************************************************************
****************************************************************************
***
*** This header was automatically generated from a Linux kernel header
*** of the same name, to make information necessary for userspace to
*** call into the kernel available to libc. It contains only constants,
*** structures, and macros generated from the original header, and thus,
*** contains no copyrightable information.
***
*** To edit the content of this header, modify the corresponding
*** source file (e.g. under external/kernel-headers/original/) then
*** run bionic/libc/kernel/tools/update_all.py
***
*** Any manual change here will be lost the next time this script will
*** be run. You've been warned!
***
****************************************************************************
****************************************************************************/
#ifndef _LINUX_UN_H
#define _LINUX_UN_H
#define UNIX_PATH_MAX 108
struct sockaddr_un {
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
sa_family_t sun_family;
char sun_path[UNIX_PATH_MAX];
};
#endif
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
根据注释可以看到这个文件是由脚本文件自动生成的,所以根据注释提示在kitkat\external\kernel-headers\original\linux\un.h可以找到原始定义:
#ifndef _LINUX_UN_H
#define _LINUX_UN_H
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* pathname */
};
#endif /* _LINUX_UN_H */
而samifily_t类型通过层层寻找也可以找到:
// kitkat\bionic\libc\include\sys\un.h
#include <sys/_types.h>
typedef __sa_family_t sa_family_t;
#include <linux/un.h>
// Z:\amlogic-s905L\kitkat\bionic\libc\include\sys\_types.h
typedef __uint16_t __sa_family_t; /* sockaddr address family type */
也就是是说,这个变量代表可以理解为socket的地址类型或协议族类型。这些不同协议类型的取值,系统已经定义了对应常量,可以在kitkat\bionic\libc\include\sys\socket.h中看到,内容比较多,就不罗列了。
回到create_socket函数,可以看到首先通过socket函数创建了进程通信协议类型(PF_UNIX)、双向连续且可信赖的数据流(SOCK_STREAM )即TCP类型的socket。然后对sockaddr_un类型的实例成员addr进行赋值,之后unlink函数根据赋值的addr.sun_path删除旧的socket文件,通过bind函数将创建的socket与指定的IP和端口绑定起来,最后通过chmod()和chown()函数对该socket文件赋予正确的所属用户和权限。其中addr.sun_path赋值为:ANDROID_SOCKET_DIR/PROP_SERVICE_NAME,这两个常量定义如下:
// kitkat\system\core\include\cutils\sockets.h
#define ANDROID_SOCKET_DIR "/dev/socket"
// kitkat\bionic\libc\include\sys\_system_properties.h
#define PROP_SERVICE_NAME "property_service"
至此,start_property_service函数中create_socket函数执行完毕,之后便通过语句listen(fd, 8)将该socket设置为listen模式。并将全局静态变量property_set_fd设置为socket函数返回值,该全局变量初始值为-1,而socket函数只有在出错的情况下才会返回-1,即正常情况下会返回一个大于0的整形socket描述符,全局变量property_set_fd此处的赋值在后面会用到。
start_property_service函数执行结束,代表着init.c的main函数中 queue_builtin_action(property_service_init_action, "property_service_init")这个action执行结束。那么在main函数中继续向下看,可以看到语句queue_builtin_action(queue_property_triggers_action, "queue_property_triggers"),这个也不难理解。因为至此属性文件均已加载完毕,那么init.rc文件中那些将属性值作为触发器的到action,此时就可以检测触发了。至此,第二模块的调用链如下图:
3、持续监听socket
init.c文件中main函数,末尾有一个 是个死循环for(;;),循环内部关于property,会检测property_set_fd的值是否大于0,如果大于0,则将执行handle_property_set_fd()函数。而这个值我们刚刚才说到其已经被赋值,所以来看看handle_property_set_fd()函数吧:
// kitkat/system/core/init/property_service.c
void handle_property_set_fd()
{
prop_msg msg;
int s;
int r;
int res;
struct ucred cr;
struct sockaddr_un addr;
socklen_t addr_size = sizeof(addr);
socklen_t cr_size = sizeof(cr);
char * source_ctx = NULL;
if ((s = accept(property_set_fd, (struct sockaddr *) &addr, &addr_size)) < 0) {
return;
}
/* Check socket options here */
if (getsockopt(s, SOL_SOCKET, SO_PEERCRED, &cr, &cr_size) < 0) {
close(s);
ERROR("Unable to receive socket options\n");
return;
}
r = TEMP_FAILURE_RETRY(recv(s, &msg, sizeof(msg), 0));
if(r != sizeof(prop_msg)) {
ERROR("sys_prop: mis-match msg size received: %d expected: %d errno: %d\n",
r, sizeof(prop_msg), errno);
close(s);
return;
}
switch(msg.cmd) {
case PROP_MSG_SETPROP:
msg.name[PROP_NAME_MAX-1] = 0;
msg.value[PROP_VALUE_MAX-1] = 0;
if (!is_legal_property_name(msg.name, strlen(msg.name))) {
ERROR("sys_prop: illegal property name. Got: \"%s\"\n", msg.name);
close(s);
return;
}
getpeercon(s, &source_ctx);
if(memcmp(msg.name,"ctl.",4) == 0) {
// Keep the old close-socket-early behavior when handling
// ctl.* properties.
close(s);
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);
}
// Note: bionic's property client code assumes that the
// property server will not close the socket until *AFTER*
// the property is written to memory.
close(s);
}
freecon(source_ctx);
break;
default:
close(s);
break;
}
}
之前通过socket()函数创建的服务端socket,在经过bind()和listen()之后,最终在这里通过accept()函数真正接收socket客户端的连线。之后通过getsockopt()函数检测socket相关状态信息,然后使用宏TEMP_FAILURE_RETRY忽略recv()函数中断造成的错误,去接收socket客户端发送来的消息,其只对设置属性的消息(PROP_MSG_SETPROP)进行处理。
如果是"ctl."开头的控制命令属性(包括ctl.start、ctl.stop、ctl.restart),首先需要通过check_control_perms()函数检测有没有对该控制命令的执行权限:
// kitkat/system/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 }
};
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;
}
可以看到system和root用户是拥有该权限的,其他程序通过对比control_perms数组中中的uid和gid,如果权限符合则也拥有执行该控制命令的权限。即会执行handle_control_message()函数去执行具体的命令,鉴于其调用链复杂,所以后面再细说说这个函数。
先看看非控制命令的属性设置,其先通过check_perm()函数检测相关权限,不过这次是和数组property_perms中的uid和gid做对比。比如设置"net."开头的属性,则需要进程的uid为:AID_SYSTEM。如果拥有对该属性的设置权限,则调用前面解析过的property_set()函数直接设置该属性的值。property_perms数组如下:
// kitkat/system/core/init/property_service.c
* White list of permissions for setting property services. */
struct {
const char *prefix;
unsigned int uid;
unsigned int gid;
} property_perms[] = {
{ "net.rmnet0.", AID_RADIO, 0 },
{ "net.gprs.", AID_RADIO, 0 },
{ "net.ppp", AID_RADIO, 0 },
{ "net.qmi", AID_RADIO, 0 },
{ "net.lte", AID_RADIO, 0 },
{ "net.cdma", AID_RADIO, 0 },
{ "ril.", AID_RADIO, 0 },
{ "gsm.", AID_RADIO, 0 },
{ "persist.radio", AID_RADIO, 0 },
{ "net.dns", AID_RADIO, 0 },
{ "sys.usb.config", AID_RADIO, 0 },
{ "net.", AID_SYSTEM, 0 },
{ "dev.", AID_SYSTEM, 0 },
{ "runtime.", AID_SYSTEM, 0 },
{ "hw.", AID_SYSTEM, 0 },
{ "sys.", AID_SYSTEM, 0 },
{ "sys.powerctl", AID_SHELL, 0 },
{ "service.", AID_SYSTEM, 0 },
{ "wlan.", AID_SYSTEM, 0 },
{ "bluetooth.", AID_BLUETOOTH, 0 },
{ "dhcp.", AID_SYSTEM, 0 },
{ "dhcp.", AID_DHCP, 0 },
{ "debug.", AID_SYSTEM, 0 },
{ "debug.", AID_SHELL, 0 },
{ "log.", AID_SHELL, 0 },
{ "service.adb.root", AID_SHELL, 0 },
{ "service.adb.tcp.port", AID_SHELL, 0 },
{ "persist.sys.", AID_SYSTEM, 0 },
{ "persist.service.", AID_SYSTEM, 0 },
{ "persist.security.", AID_SYSTEM, 0 },
{ "persist.service.bdroid.", AID_BLUETOOTH, 0 },
{ "selinux." , AID_SYSTEM, 0 },
{ NULL, 0, 0 }
};
现在我们来看看前面遗留的handle_control_message()函数,其主要是做了程序调度的工作,后面看模块结尾处调用链的图更清晰一些。
用bootanim这个服务为例,设置("ctl.stop", "bootanim")这样的指令,会先判断这个服务是否在运行。如果在运行则会杀死bootanim这个进程,并将"init.svc.bootanim"属性的值改为"stopping";如果这个服务没在运行,则直接将"init.svc.bootanim"属性的值改为"stopped"。
指令("ctl.start", "bootanim")则是直接调用service_start()函数做一些处理(代码有部分省略):
// kitkat/system/core/init/init.c
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);
setsockcreatecon(scon);
for (si = svc->sockets; si; si = si->next) {
int socket_type = (
!strcmp(si->type, "stream") ? SOCK_STREAM :
(!strcmp(si->type, "dgram") ? SOCK_DGRAM : SOCK_SEQPACKET));
int s = create_socket(si->name, socket_type,
si->perm, si->uid, si->gid);
if (s >= 0) {
publish_socket(si->name, s);
}
}
…………
if (!dynamic_args) {
if (execve(svc->args[0], (char**) svc->args, (char**) ENV) < 0) {
ERROR("cannot execve('%s'): %s\n", svc->args[0], strerror(errno));
}
} else {
char *arg_ptrs[INIT_PARSER_MAXARGS+1];
int arg_idx = svc->nargs;
char *tmp = strdup(dynamic_args);
char *next = tmp;
char *bword;
/* Copy the static arguments */
memcpy(arg_ptrs, svc->args, (svc->nargs * sizeof(char *)));
while((bword = strsep(&next, " "))) {
arg_ptrs[arg_idx++] = bword;
if (arg_idx == INIT_PARSER_MAXARGS)
break;
}
arg_ptrs[arg_idx] = '\0';
execve(svc->args[0], (char**) arg_ptrs, (char**) ENV);
}
_exit(127);
}
…………
if (properties_inited())
notify_service_state(svc->name, "running");
}
首先可以看到fork函数,学习《linux c语言 fork() 和 exec 函数的简介和用法》这篇博文可以知道。其以init进程为父进程创建子进程,并通过umask()函数设置进程文件的默认权限掩码,该进程用于后续执行我们要启动的服务。
在该进程中,首先通过properties_inited()函数获取全局静态变量property_area_inited的值,还记得在第一模块的初始化部分,init_property_area()函数就已经将property_area_inited的值置为1。所以程序向下执行get_property_workspace(),这个函数则是获取全局静态变量pa_workspace的成员信息。此刻,时间线再次收束,还记得第一模块初始化部分init_property_area()函数中的语句:init_workspace(&pa_workspace, 0)就已经将"/dev/__properties__"这个文件以只读方式打开,并将文件句柄传给了pa_workspace的fd成员。所以接着执行后面的add_environment()函数将dup()过的文件句柄添加到名为ANDROID_PROPERTY_WORKSPACE的环境变量中去。除了会添加服务自身的环境变量外,还会添加服务中这些socket的环境变量。以init.rc文件中的netd服务为例:
service netd /system/bin/netd
class main
socket netd stream 0660 root system
socket dnsproxyd stream 0660 root inet
socket mdns stream 0660 root system
之后的代码设置了uid和gid等相关安全问题,此时一切准备就绪,程序根据启动服务有没有相关参数,通过execve()函数正式拉起执行目标服务,结束后语句_exit(127)退出子进程,以netd服务为例,将属性"init.svc.netd"的值设置为"running"。
至此,service_start()函数结束,handle_control_message()函数结束,main函数的死循环中一次handle_property_set_fd()函数调用结束。来看看第三模块相关调用链:
4、外部调用
4.1 java层
java层对property属性的调用接口位于:kitkat/frameworks/base/core/java/android/os/SystemProperties.java;其中的主要接口SystemProperties.get()和SystemProperties.set()都是通过jni接口native_get()和native_set()对C/C++层调用。
在kitkat\frameworks\base\core\jni\android_os_SystemProperties.cpp文件中可以看到,这两个接口最终都是指向了kitkat\system\core\libcutils\properties.c中。
这个c文件中的property_get()、prooerty_set()函数相同的接口有三个,根据下面的注释可以看到,这两种是针对其他的调用方式的实现,所以不用管这两种实现。主要来看第一种实现。
/*
* The Linux simulator provides a "system property server" that uses IPC
* to set/get/list properties. The file descriptor is shared by all
* threads in the process, so we use a mutex to ensure that requests
* from multiple threads don't get interleaved.
*/
…………
/* SUPER-cheesy place-holder implementation for Win32 */
// kitkat\system\core\libcutils\properties.c
#include <sys/_system_properties.h>
int property_set(const char *key, const char *value)
{
if (!strcmp("persist.sys.autosleep", key) || !strcmp("persist.sys.autosleeptime", key)) {
__system_property_set("ctl.start", "auto_sleep");
}
return __system_property_set(key, value);
}
int property_get(const char *key, char *value, const char *default_value)
{
int len;
len = __system_property_get(key, value);
if(len > 0) {
return len;
}
if(default_value) {
len = strlen(default_value);
memcpy(value, default_value, len + 1);
}
return len;
}
辗转_system_properties.h和system_properties.h,最终又回到了kitkat\bionic\libc\bionic\system_properties.c中。对!就是在第一模块初始化的时候所出现的那个文件。在这个文件可以找到__system_property_get()和__system_property_set()这两个接口的实现。先来看看__system_property_set()函数:
// kitkat\bionic\libc\include\sys\_system_properties.h
struct prop_msg
{
unsigned cmd;
char name[PROP_NAME_MAX];
char value[PROP_VALUE_MAX];
};
// kitkat\bionic\libc\bionic\system_properties.c
int __system_property_set(const char *key, const char *value)
{
int err;
prop_msg msg;
if(key == 0) return -1;
if(value == 0) value = "";
if(strlen(key) >= PROP_NAME_MAX) return -1;
if(strlen(value) >= PROP_VALUE_MAX) return -1;
memset(&msg, 0, sizeof msg);
msg.cmd = PROP_MSG_SETPROP;
strlcpy(msg.name, key, sizeof msg.name);
strlcpy(msg.value, value, sizeof msg.value);
err = send_prop_msg(&msg);
if(err < 0) {
return err;
}
return 0;
}
其首先创建一个prop_msg类型的结构体实例msg,将需要设置的属性名和属性值赋值给msg的name和value成员,然后将PROP_MSG_SETPROP作为消息命令赋值给cmd成员。还记得第三模块socket服务端监听并接收客户端连接消息的时候,其只对PROP_MSG_SETPROP这个消息做响应。待发送消息准备完毕,通过send_prop_msg()函数发送消息。
// kitkat\bionic\libc\bionic\system_properties.c
static const char property_service_socket[] = "/dev/socket/" PROP_SERVICE_NAME;
static int send_prop_msg(prop_msg *msg)
{
struct pollfd pollfds[1];
struct sockaddr_un addr;
socklen_t alen;
size_t namelen;
int s;
int r;
int result = -1;
s = socket(AF_LOCAL, SOCK_STREAM, 0);
if(s < 0) {
return result;
}
memset(&addr, 0, sizeof(addr));
namelen = strlen(property_service_socket);
strlcpy(addr.sun_path, property_service_socket, sizeof addr.sun_path);
addr.sun_family = AF_LOCAL;
alen = namelen + offsetof(struct sockaddr_un, sun_path) + 1;
if(TEMP_FAILURE_RETRY(connect(s, (struct sockaddr *) &addr, alen)) < 0) {
close(s);
return result;
}
r = TEMP_FAILURE_RETRY(send(s, msg, sizeof(prop_msg), 0));
if(r == sizeof(prop_msg)) {
pollfds[0].fd = s;
pollfds[0].events = 0;
r = TEMP_FAILURE_RETRY(poll(pollfds, 1, 250 /* ms */));
if (r == 1 && (pollfds[0].revents & POLLHUP) != 0) {
result = 0;
} else {
result = 0;
}
}
close(s);
return result;
}
第二模块中创建socket的socket文件路径位于"/dev/socket/property_service",send_prop_msg()函数中首先创建socket,并将其做为客户端通过connect()函数连接之前创建的socket服务端。连接成功之后,通过send()函数将修改属性的PROP_MSG_SETPROP消息发送给服务端。第三模块说到socket服务端持续监听并接受客户端的消息,就是我们这里发送的消息了。
再来看看__system_property_get()函数:
// kitkat\bionic\libc\bionic\system_properties.c
int __system_property_get(const char *name, char *value)
{
const prop_info *pi = __system_property_find(name);
if(pi != 0) {
return __system_property_read(pi, 0, value);
} else {
value[0] = 0;
return 0;
}
}
其先通过__system_property_find()函数辗转调用之前出现过的find_property()函数,在属性字典树中寻找目标属性。如果能找到该属性,则通过__system_property_read()函数读取该属性的值。
这里有一个问题,那就是__system_property_set()函数是通过socket方式请求init进程,去对属性内存空间进行设置。在第一模块初始化的时候,init进程通过调用__system_property_area_init()函数获取到了属性内存文件的读写权限,所以可以访问属性内存空间,但是__system_property_get()函数并未请求init进程,也没有对属性内存空间有读写权限,那它是如何获取到属性值的呢?原来是在该文件里,有个和__system_property_area_init()类似的接口:__system_properties_init(),拿出来对比一下:
// kitkat\bionic\libc\bionic\system_properties.c
#################################
int __system_property_area_init()
{
return map_prop_area_rw();
}
#################################
int __system_properties_init()
{
return map_prop_area();
}
#################################
static int map_prop_area()
{
…………
fd = open(property_filename, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd >= 0) {
/* For old kernels that don't support O_CLOEXEC */
ret = fcntl(fd, F_SETFD, FD_CLOEXEC);
if (ret < 0)
goto cleanup;
}
if ((fd < 0) && (errno == ENOENT)) {
fd = get_fd_from_env();
fromFile = false;
}
…………
prop_area *pa = mmap(NULL, pa_size, PROT_READ, MAP_SHARED, fd, 0);
…………
__system_property_area__ = pa;
cleanup:
if (fromFile) {
close(fd);
}
return result;
}
这里因为只是获取属性值,故以只读方式打开属性内存文件,如果打开失败,通过get_fd_from_env()函数的语句:getenv("ANDROID_PROPERTY_WORKSPACE")从环境变量中获取该属性文件的文件描述符。这个环境变量ANDROID_PROPERTY_WORKSPACE正是第三模块service_start()函数中,语句add_environment("ANDROID_PROPERTY_WORKSPACE", tmp)所添加的。后续mmap()函数映射进内存的时候也是只读方式,并将内存起始位置赋值给__system_property_area__ 。这个全局变量这里又一次出现了,__system_property_get()函数能找到目标属性位置,就是以它为对象,在其所指向的内存空间中查找。
那么问题就剩下__system_properties_init()何时调用了,在文章开头我所引用的文章《深入讲解Android Property机制》中,已经有详细说明,我还不太理解,就不细说了。大概意思就是在main函数运行前,加载C运行期库初始化运行环境的时候就已经调用了。
4.2、C/C++层
以开机动画的程序为例,其就是直接调用kitkat\system\core\libcutils\properties.c的property_get()和property_set()接口,而这个文件我们刚刚已经看过了,所以不再赘述。
// kitkak/frameworks/base/cmds/bootanimation/BootAnimation.cpp
#include <cutils/properties.h>
4.3、shell层
通常我们通过adb连接到设备后,可以通过setprop设置某个属性值,通过getprop获取某个属性值,或者所有属性值。那么这里的原理是什么呢?首先可以在system/bin目录下看到,setprop和getprop这两个可执行文件,且都是链接到toolbox这个文件。那么我们来找一下toolbox的源代码位置吧。
root@xxx:/ # ls -l /system/bin/setprop
lrwxr-xr-x root shell 2021-03-19 15:54 setprop -> toolbox
root@xxx:/ # ls -l /system/bin/getprop
lrwxr-xr-x root shell 2021-03-19 15:54 getprop -> toolbox
在kitkak/system/core/toolbox目录下可以找到这两个程序的源文件getprop.c和setprop.c:
// kitkak/system/core/toolbox/setprop.c
#include <stdio.h>
#include <cutils/properties.h>
int setprop_main(int argc, char *argv[])
{
if(argc != 3) {
fprintf(stderr,"usage: setprop <key> <value>\n");
return 1;
}
if(property_set(argv[1], argv[2])){
fprintf(stderr,"could not set property\n");
return 1;
}
return 0;
}
显而易见,和java、c/c++层所调用的接口相同,都是指向了kitkat\system\core\libcutils\properties.c。
这一模块的调用关系也用图总结一下:
5、总结
先来看看总体的调用图吧,如果觉得字太小可以去画图网站看原图,可以缩放观看。因为追踪的是调用链细节,所以看起来有一代点复杂,但这能让你看到程序运行的每一处细节。
如果觉得太深入代码而忽略了整体的概要框架理解起来更清晰,那么我们借用《Android Property属性的实现细节》,这篇文章的图片来简要概括一下。
首先,开机启动后property service服务从几个prop属性文件中把属性数据读取出来,映射存储到一块共享内存中,这块共享内存只提供读权限给用户进程。只有property service服务对属性数据拥有写权限,且只有init进程中调用了property service中写属性的接口,而init进程提供socket方式接收set属性的消息。所以用户进程想要set属性,就可以通过socket方式发送set属性的消息给init进程,init进程调用set属性的接口,完成对属性的写入操作。
6、补充
博文到这里就结束了,首先很感谢各位前辈总结、分享的博文和技巧,由于自身太菜,这篇博文断断续续写了两周。创作不易,欢迎分享,但请不要随意转载呀,有需要的话可以私我。文章有错误的地方欢迎指正,我会在这里进行补充。