现在的位置: 首页 > 综合 > 正文

Android情景分析之属性服务

2018年04月01日 ⁄ 综合 ⁄ 共 10596字 ⁄ 字号 评论关闭
文章目录

我们都知道,在Windows平台上有一个注册表管理器,注册表的内容采用key-value键值对的形式来记录用户、软件的一些使用信息。即使系统或者软件重启,它还是能够根据之前在注册表中的记录,进行相应的初始化工作。

那么在Android平台上,也有类似的机制,称之为属性服务(property service)。应用程序可以通过这个属性机制,查询或者设置相应的属性。我们可以使用getprop命令来查看当前系统中都有哪些属性。比如我的红米手机,如图1所示(图中只显示了部分属性)。

图1

创建属性内容存储文件

属性服务的初始化工作主要是在init进程的main函数中完成。代码路径system\core\init\init.c。init的main函数中首先调用property_init()函数创建存储文件,代码如下:

void property_init(void)
{
    init_property_area();
}

static int init_property_area(void)
{
	// 判断是否已经初始化过了
    if (property_area_inited)
        return -1;
	
	// 打开/dev/__properties__设备,然后mmap一块大小为(128 * 1024)的内存出来
    if(__system_property_area_init())
        return -1;

	/* 
                pa_workspace是个全局变量,结构如下所示
		typedef struct {
    		size_t size;	共享内存的大小
   		 	int fd;		共享内存的文件描述符
                 } workspace;
                 init_workspace函数打开设备/dev/__properties__,将文件描述符赋值给fd,size清零。
        */
    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函数具体做了什么事情,__system_property_area_init->map_prop_area_rw:

static int map_prop_area_rw()
{
    prop_area *pa;
    int fd;
    int ret;

    /* 
		#define PROP_FILENAME "/dev/__properties__"
	 	static char property_filename[PATH_MAX] = PROP_FILENAME;
		
	 	这里就是打开/dev/__properties__文件,感情property area是用共享文件来实现的
	 	指定了O_CREAT标志,如果文件不存在则创建之
     */
    fd = open(property_filename, O_RDWR | O_CREAT | O_NOFOLLOW | O_CLOEXEC |
            O_EXCL, 0444);
    if (fd < 0) {
        if (errno == EACCES) {
            /* 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;
	/*
		#define PA_SIZE         (128 * 1024)
		改变文件大小为PA_SZIE
	*/
    if (ftruncate(fd, PA_SIZE) < 0)
        goto out;

	/*
		并不是整个pa都用来记录属性内容的,那么其前面sizeof(prop_area)的大小是用来记录这个pa的一些基本信息,例如,特征值、版本号等等。紧接着prop_area结构后面的data内存才是真正用来记录属性信息的。
		struct prop_area {
    			unsigned bytes_used;
    			unsigned volatile serial;
    			unsigned magic;
    			unsigned version;
    			unsigned reserved[28];
    			char data[0];
		}; 
	*/
    pa_size = PA_SIZE;
    pa_data_size = pa_size - sizeof(prop_area);
    compat_mode = false;
	
	/*
		在init进程中将/dev/__properties__文件映射成一块可读可写的内存
		注意,我们在后面会看到,在其他客户端进程都是以只读的形式访问pa的,如果客户端要修改或者新增属性,那么需要进程间通信,最后都是由init进程来完成的
	*/
    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 
		最后将pa复制给全局变量__system_property_area__
	*/
    __system_property_area__ = pa;

    close(fd);
    return 0;

out:
    close(fd);
    return -1;
}

上面的内容比较简单,不过最后的一个赋值语句__system_property_area__ = pa;比较特殊。__system_property_area__变量是在bioniclibc库中定义的一个全局变量。这里为什么要赋值给它呢?

         原来,虽然属性区域是由init进程在初始化的时候创建的,但是Android希望其他进程也能读取这块内存里面的东西,为了做到这一点,它便做了如下两项工作:

1.      属性区域通过文件形式实现进程间共享数据。

2.      如何让其他进程也去读取这个文件呢?Android利用了gcc的constructor属性,这个属性指明了一个__libc_prenit函数,当bionic libc库被加载时,将自动调用这个__libc_prenit,这个函数内部就将完成共享文件到本地进程的映射工作。

客户端进程映射属性文件

直接看代码吧,bionic\libc\bionic\libc_init_dynamic.cpp

// constructor属性指示加载器加载该库后,首先调用__libc_preinit函数。这一点和windows
// 上动态库的DllMain函数类似
__attribute__((constructor)) static void __libc_preinit() {
	…
	__libc_init_common(*args);
	…
}

void __libc_init_common(KernelArgumentBlock& args) {
 …
  __system_properties_init(); // 初始化话客户端属性区域.
}

__system_properties_init –> map_prop_area
static int map_prop_area()
{
    bool fromFile = true;
    int result = -1;
    int fd;
int ret;

	// 打开已经存在的文件/dev/__properties__
    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;
    }

    if (fd < 0) {
        return -1;
    }
	
	// 读取并检查文件属性
    struct stat fd_stat;
    if (fstat(fd, &fd_stat) < 0) {
        goto cleanup;
    }

    if ((fd_stat.st_uid != 0)
            || (fd_stat.st_gid != 0)
            || ((fd_stat.st_mode & (S_IWGRP | S_IWOTH)) != 0)
            || (fd_stat.st_size < sizeof(prop_area)) ) {
        goto cleanup;
    }
	
	// 客户端取得共享文件的大小
    pa_size = fd_stat.st_size;
pa_data_size = pa_size - sizeof(prop_area);

	// mmap将文件映射为内存,此处为PROT_READ形式,客户端进程对属性文件是只读// 的,而不能对其进行设置
    prop_area *pa = mmap(NULL, pa_size, PROT_READ, MAP_SHARED, fd, 0);

    if (pa == MAP_FAILED) {
        goto cleanup;
    }

	// 检查属性区域的特征
    if((pa->magic != PROP_AREA_MAGIC) || (pa->version != PROP_AREA_VERSION &&
                pa->version != PROP_AREA_VERSION_COMPAT)) {
        munmap(pa, pa_size);
        goto cleanup;
    }

    if (pa->version == PROP_AREA_VERSION_COMPAT) {
        compat_mode = true;
    }

    result = 0;
	
	// 赋值到本地全局变量
    __system_property_area__ = pa;

cleanup:
    if (fromFile) {
        close(fd);
    }

    return result;
}

加载属性文件并启动属性服务

接着看init进程的初始化代码

// 如果启动模式不是charger,那么就加载/default.prop属性文件
if (!is_charger)
        property_load_boot_defaults();

最后调用queue_builtin_action(property_service_init_action,"property_service_init");完成属性服务器的启动过程。

property_service_init_action-> start_property_service:

void start_property_service(void)
{
	int fd;

	/*
		#define PROP_PATH_SYSTEM_BUILD     "/system/build.prop"
		#define PROP_PATH_SYSTEM_DEFAULT   "/system/default.prop"
	*/
    	load_properties_from_file(PROP_PATH_SYSTEM_BUILD);
	load_properties_from_file(PROP_PATH_SYSTEM_DEFAULT);

	// 如果是调试模式,就加载
	// #define PROP_PATH_LOCAL_OVERRIDE   "/data/local.prop"
	load_override_properties();

	/* 
		Read persistent properties after all default values have been loaded. 
		有一些属性是需要保存到永久介质上的,这些属性文件则由下面这个函数加载,这些文件存储在/data/property目录下,并且这些文件的文件名必须以persist.开头。
	*/
    	load_persistent_properties();

	// 创建一个socket,用于ipc通信
 	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;
}

init进程处理设置属性请求

设置属性的请求都是在init进程的main函数的一个for循环中进行处理的。下面我们继续看代码:

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();
           }
}

上面start_property_service函数中创建了一个用于通信的socket,最后赋值给全局变量property_set_fd,此处就是判断收到的信息是否是是属性服务器的socket。

int get_property_set_fd()
{
    return property_set_fd;
}

如果是属性服务器的通信socket,那么就调用handle_property_set_fd函数来处理消息,并设置相应的属性。

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;
	
	// 接收TCP连接
    if ((s = accept(property_set_fd, (struct sockaddr *) &addr, &addr_size)) < 0) {
        return;
    }

	/* 
		Check socket options here 
		取出socket的可选内容,可能包括客户端进程的权限等属性
	*/
    if (getsockopt(s, SOL_SOCKET, SO_PEERCRED, &cr, &cr_size) < 0) {
        close(s);
        ERROR("Unable to receive socket options\n");
        return;
    }
	
	// 接收socket的主体数据
    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);
		
	/*
		如果是ctl开头的消息,则认为是控制消息,控制消息用来执行一些命令,例如用adb shell登录后,输入setprop ctl.start bootanim就可以查看开机动画了,如果要关闭就输入setprop.stop bootanim就可以了。
	*/
        if(memcmp(msg.name,"ctl.",4) == 0) {
            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 {
		/*
			不是ctl开头的属性,则首先检查其权限。例如,设置net.开头的属性需要AID_SYSTEM权限,log.开头的属性需要AID_SHELL属性等。
		*/
            if (check_perms(msg.name, cr.uid, cr.gid, source_ctx)) {
		// 最后通过property_set函数设置客户端需要设置的属性
                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;
    }
}

当客户端的权限满足要求时,init进程就调用property_set进行相关处理,这个函数逻辑比较简单,代码如下所示:

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.开头的属性表明是只读的,直接返回*/
        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) {
	// 如果属性名是以persist.开头的,表明该属性是永久的,那么需要把属性写
	// 到相应文件中去
        write_persistent_property(name, value);
    } else if (strcmp("selinux.reload_policy", name) == 0 &&
               strcmp("1", value) == 0) {
		// 与selinux相关
        selinux_reload_policy();
	}

	/*
		init.rc中有如下这句话:
		on property:persist.service.adb.enable=1
			start adbd
		待persist.service.adb.enable属性设置为1后,就执行start adbd这个command,这就是通过property_changed函数来完成的。
	*/
    property_changed(name, value);
    return 0;
}

property_changed函数其主要实现如下,property_changed-> queue_property_triggers:

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);
            }
        }
    }
}

客户端设置属性

客户端通过property_set发送设置属性的请求,代码如下所示:property_set -> __system_property_set

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

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));

// #define PROP_SERVICE_NAME "property_service"
// static const char property_service_socket[] = "/dev/socket/" PROP_SERVICE_NAME;
    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;
	
	// 会连接上init进程里面的socket
    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;
}

抱歉!评论已关闭.