1. 程式人生 > >Android情景分析之屬性服務

Android情景分析之屬性服務

我們都知道,在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;
}