淺析Android的RILD服務進程的消息循環

        Android中,RILD是RIL(Radio Interface Layer) Deamon的簡稱。簡單的說它下面承接GSM/GPRS Modem(電話通信模塊),上面接電話應用相關的Java庫(telephony internal)。telephony internal通過socket將請求發送給RILD的消息循環,消息循環則將請求轉發給底層通信模塊(直接調用底層的庫)來實現對通信模塊功能的調用。反之,當通信模塊有類似於來電的消息時,也會通過RILD的回調,將信息包裝成消息,發送到RILD的消息循環中去處理,最後再通過socket回送給telephony internal,以便通知上層。整體結構參見如下:


  • 概述 

        消息循環(ril_event_loop()函數內)位於單獨的線程中,用來處理三種消息,分別對應三個不同的隊列,面對的是三種不同的需求:
        1.定時列表(timer_list): 此隊列中的消息主要用於處理一些延時的操作。比如:從飛信模式切換到通信模式(實際上就是打開通信模塊)時,若SIM卡未準備好,那麼需要延續一段時間再檢查是否準備好,此時就要將消息扔至此隊列。
        2.偵聽列表(watch_list): 此隊列中的消息一是作爲socket的服務端,用來偵聽客戶端的請求;另一個是作爲本進程的其它線程(如:檢查通信模塊來電消息的線程)傳遞過來的消息。
        3.掛起列表(pending_list): 之所以叫掛起,實際上指的是在處理上面兩中類型的消息時,並不真正的處理消息體,而是將符合條件的消息丟到本隊列中。由於消息附帶處理函數,所以在處理本隊列的消息時,直接觸發即可。
        消息循環所要做的事就是先等待某個時間間隔(定時消息要求的)或者是客戶端(socket或通訊模塊的消息)的請求,然後再按次序處理上面三個對列中的消息。

        下面還是通過源代碼來分析消息循環的處理過程。

  • ril_event

        ril_event指代的是消息,它的數據結構如下:

    struct ril_event *next; // 下一個消息
    struct ril_event *prev; // 前一個消息
    int fd; // 開始我以爲這裏有可能是socket描述符或pipe描述符,但實際上它還被用作消息的回調函數的參數?
    int index; // 偵聽列表(watch_list)中的索引,偵聽列表採用的數組而不是鏈表
    bool persist; // 是否常駐偵聽列表(watch_list)中。若常駐,則當被轉到pending_list中時,不刪除。
    struct timeval timeout; // 超時,這個爲一個時間點。在處理定時消息時,用於和當前的時間比較,以確定是否處理該消息。
    ril_event_cb func; // 回調。此消息對應的處理函數。
    void *param; // 回調參數。
        三種消息列表的定義如下:

static struct ril_event * watch_table[MAX_FD_EVENTS]; // 偵聽列表
static struct ril_event timer_list; // 定時列表
static struct ril_event pending_list; // 掛起列表

  • ril_event_init()

        初始化過程還是要從rild的main入口開始分析起,位於文件hardware/ril/rild/rild.c。簡單期間,只介紹本文相關的部分。main()函數會調用函數RIL_register(),然後會調用RIL_startEventLoop()。RIL_startEventLoop()中會創建線程eventLoop,這個實際上就是rild的消息循環所在的線程。

    ret = pthread_create(&s_tid_dispatch, &attr, eventLoop, NULL);

        eventLoop()首先會進行初始化:

    ril_event_init();

        然後,會通過pipe()函數創建管道。pipe()函數會創建一對文件描述符,一個用於讀,另一個用於寫。都被記錄在全局變量中。它實際上是用來發送定時消息的,後面會介紹。

    ret = pipe(filedes);
    // ......
    s_fdWakeupRead = filedes[0]; // 消息循環中偵聽
    s_fdWakeupWrite = filedes[1]; // 用於通知消息循環定義消息已發送

        最後一步就是進入消息循環:

    // Only returns on error
    ril_event_loop();

  • ril_event_loop()

        ril_event_loop()中,每次循環都主要做三件事:
        1.初始化。主要是獲取要等待的描述符,以及獲取超時信息,以便能夠及時處理定時消息。
        2.等待。等待socket客戶端有新的消息,或一定的時間間隔,之後處理定時消息。
        3.依次處理三種類型的消息。
        下面分別敘述之。先是獲取要等待的描述符,這裏爲什麼要這麼做呢?我的理解是在添加新的消息時,有可能會修改全局的描述符列表。也就是說,全局的描述符列表有可能在本次循環的處理過程中發生變化。

    // make local copy of read fd_set
    memcpy(&rfds, &readFds, sizeof(fd_set));

        接下來,就是計算select()函數的超時參數。select的最後一個參數代表超時。若爲NULL,則select死等;若爲0,則select立即返回;若大於0,則限制了select最長的等待時間。

    if (-1 == calcNextTimeout(&tv)) { // 獲取超時參數,若失敗則返回-1。
        // no pending timers; block indefinitely
        dlog("~~~~ no timers; blocking indefinitely ~~~~");
        ptv = NULL;
    } else { // 獲取成功,則使用該值。
        dlog("~~~~ blocking for %ds + %dus ~~~~", (int)tv.tv_sec, (int)tv.tv_usec);
        ptv = &tv;
    }

        calcNextTimeout()函數先是判斷超時消息的列表是否爲空,爲空則返回-1;

    // Sorted list, so calc based on first node
    if (tev == &timer_list) {
        // no pending timers
        return -1;
    }

        然後,獲取到處理第一個定時消息的時間間隔,這裏只取第一個的原因是,定時消息列表是排序的,這一點後面會介紹。這裏雖然分兩種情況,實際上是在計算處理第一個消息的時間點和當前時間的間隔,注意不可能爲負值。

    struct ril_event * tev = timer_list.next;
    // ......
    if (timercmp(&tev->timeout, &now, >)) { // 處理消息的時間點還沒到
        timersub(&tev->timeout, &now, tv);
    } else { // 處理消息的時間點已經過了
        // timer already expired.
        tv->tv_sec = tv->tv_usec = 0; // 最小值爲0,此時會導致select()直接退出。
    }

        接下來就是等待了。根據上面的分析,等待的描述符只有兩種:socket描述符和用於發送定時消息的pipe描述符。

    n = select(nfds, &rfds, NULL, NULL, ptv);

        最後,就是依次處理三種類型的消息:

    // Check for timeouts
    processTimeouts(); // 處理定時消息
    // Check for read-ready
    processReadReadies(&rfds, n); // 處理偵聽消息
    // Fire away
    firePending(); // 處理掛起消息

        processTimeouts()是處理定時消息。它實際上是查找所有當前時間點以前的消息(因爲是排序的,所以只要查找前面幾個即可)。然後,將這些消息仍到掛起列表(pending_list)中,這裏可以看到pending的意思是“暫時不處理,後面再說”,很貼切。

    struct ril_event * tev = timer_list.next;
    // ......
    while ((tev != &timer_list) && (timercmp(&now, &tev->timeout, >))) { // 查找所有當前時間點以前的消息
        // Timer expired
        dlog("~~~~ firing timer ~~~~");
        next = tev->next;
        removeFromList(tev); // 從定時消息隊列中移除。
        addToList(tev, &pending_list); // 添加至掛起列表(pending_list)中
        tev = next;
    }

        接下來處理偵聽列表(watch_list)。 processReadReadies(&rfds, n)的主要工作是從偵聽列表中,取出描述符在等待的列表中的消息,移動至掛起列表(pending_list)中。這裏要注意,如果ril_event的persist設置了,則不會從偵聽列表中移除該消息。

    for (int i = 0; (i < MAX_FD_EVENTS) && (n > 0); i++) {
        struct ril_event * rev = watch_table[i];
        if (rev != NULL && FD_ISSET(rev->fd, rfds)) { // 此處判斷描述符是否在當前循環的等待列表中。
            addToList(rev, &pending_list); // 添加至掛起列表(pending_list)中
            if (rev->persist == false) { // 會判斷ril_event的persist參數,以決定是否從watch_list移除該消息。
                removeWatch(rev, i);
            }
            n--;
        }
    }

        最後一步就是處理掛起消息列表firePending()。這一步是依次調用掛起列表(pending_list)中消息的回調函數,此處就不詳述了。

    struct ril_event * ev = pending_list.next;
    while (ev != &pending_list) {
        struct ril_event * next = ev->next;
        removeFromList(ev);
        ev->func(ev->fd, 0, ev->param); // 注意此處的參數包含了消息中的描述符。
        ev = next;
    }

  • internalRequestTimedCallback()

        internalRequestTimedCallback()用於發送定時消息。它包含了三個參數:回調函數、參數和時間間隔。主要部分包含了下面的三個函數調用:

    ril_event_set(&(p_info->event), -1, false, userTimerCallback, p_info); // 設置消息參數
    ril_timer_add(&(p_info->event), &myRelativeTime); // 將定時消息添加到列表中,myRelativeTime爲多久後觸發消息
    triggerEvLoop(); // 通知消息循環

        設置消息(ril_event_set)就是將參數設置到消息體中。添加消息(ril_timer_add)則先設置消息的處理時間爲時間未來的某個時間點。

    struct timeval now;
    getNow(&now);
    timeradd(&now, tv, &ev->timeout);

        然後,根據時間點找到插入位置。可以看到,插入時已經按時間排序了。

    // keep list sorted
    while (timercmp(&list->timeout, &ev->timeout, < )
            && (list != &timer_list)) {
        list = list->next;
    }

        最後,添加到列表中。

    addToList(ev, list);

        通知消息循環(triggerEvLoop)的實現也很簡單,就是向上面提到的管道的寫端寫入數據。這裏同樣解釋了一個問題:爲什麼採用管道(pipe)來通知定時消息已送至消息隊列?這是爲了和socket描述符保持一致,以便採用一致的方式來處理。

   ret = write (s_fdWakeupWrite, " ", 1);

  • 總結

        雖然本文是介紹RILD的消息循環的。但是,通過分析我們發現,實際上它是一個通用的消息循環模型,也可以說是一種設計模式。既然是設計模式,就應該反覆推敲它,直至爲我所用。

發佈了86 篇原創文章 · 獲贊 10 · 訪問量 43萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章