Android4.4 property机制学习

引言

本文借鉴自邓平凡著《深入理解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 允许负值的出现;

常用方式:

  1. 欲将读写位置移到文件开头时:lseek(int fildes, 0, SEEK_SET)
  2. 欲将读写位置移到文件尾时:lseek(int fildes, 0, SEEK_END)
  3. 想要取得目前文件位置时: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、补充

博文到这里就结束了,首先很感谢各位前辈总结、分享的博文和技巧,由于自身太菜,这篇博文断断续续写了两周。创作不易,欢迎分享,但请不要随意转载呀,有需要的话可以私我。文章有错误的地方欢迎指正,我会在这里进行补充。

猜你喜欢

转载自blog.csdn.net/GDUYT_gduyt/article/details/112188833