Android藍牙源碼分析——GKI定時器

GKI定時器初始化在gki_ulinux.c中的GKI_init中,如下:

void GKI_init(void)
{
    ......

    struct sigevent sigevent;
    memset(&sigevent, 0, sizeof(sigevent));
    sigevent.sigev_notify = SIGEV_THREAD;
    sigevent.sigev_notify_function = (void (*)(union sigval))bt_alarm_cb;
    sigevent.sigev_value.sival_ptr = NULL;
    if (timer_create(CLOCK_REALTIME, &sigevent, &posix_timer) == -1) {
        ALOGE("%s unable to create POSIX timer: %s", __func__, strerror(errno));
        timer_created = false;
    } else {
        timer_created = true;
    }
}

這裏調用系統API創建了一個定時器,但是此時定時器還沒開始計時。這裏指定了SIGEV_THREAD表示當定時器到時內核就會創建一個線程執行回調,這裏是bt_alarm_cb。

我們可以設想一下,如果我們自己設計一個定時器模塊該如何做?首先會有一堆定時任務,我們可以創建很多個timer來分別定時,也可以只創建一個timer來管理所有定時任務。只不過需要將這些任務從近到遠排好序,先給timer設置爲最近的一個,最近的到時了再設置爲下一個最近的,這樣一直到所有定時任務都處理完爲止。但是GKI的定時結構更復雜一些,因爲GKI可以支持多個task,每個task都有多個計時任務隊列,每個任務隊列都有所不同,比如有的隊列計時精度較低,以秒爲單位,而有的精度要求較高,以毫秒爲單位。這種情況下,GKI採取的方式是輪詢,即每個任務隊列都有一個輪詢週期,在一個輪詢結束時去檢查隊列中有沒有超時的任務,有就依次回調這些任務的超時處理。然後再開始下一波輪詢。

我們接下來從三個方面來分析GKI:

  • 如何啓動定時器
  • 如何添加定時任務的
  • 定時器到時如何處理

先來看看定時器的啓動,在gki_time.c中的GKI_start_timer中,如下:

void GKI_start_timer (UINT8 tnum, INT32 ticks, BOOLEAN is_continuous)
{
    INT32   reload;
    INT32   orig_ticks;
    UINT8   task_id = GKI_get_taskid();

    if (ticks <= 0)
        ticks = 1;

    orig_ticks = ticks;     /* save the ticks in case adjustment is necessary */

    /* If continuous timer, set reload, else set it to 0 */
    if (is_continuous)
        reload = ticks;
    else
        reload = 0;

    GKI_disable();

    /* Add the time since the last task timer update.
    ** Note that this works when no timers are active since
    ** both OSNumOrigTicks and OSTicksTilExp are 0.
    */
    if (INT32_MAX - (gki_cb.com.OSNumOrigTicks - gki_cb.com.OSTicksTilExp) > ticks)
    {
        ticks += gki_cb.com.OSNumOrigTicks - gki_cb.com.OSTicksTilExp;
    }
    else
        ticks = INT32_MAX;

    switch (tnum)
    {
        case TIMER_0:
            gki_cb.com.OSTaskTmr0R[task_id] = reload;
            gki_cb.com.OSTaskTmr0 [task_id] = ticks;
            break;
    }

    /* Only update the timeout value if it is less than any other newly started timers */
    gki_adjust_timer_count (orig_ticks);

    GKI_enable();
}

GKI中的task不止一個,我們已知的有btif task和btu task,這兩個task跑在不同的線程。每個task都有自己的定時任務隊列,即便如此,他們
都共用GKI的全局定時器。這裏傳入的參數tnum表示timer的編號,類似於GKI中的mailbox。GKI中每個task可以有多個mailbox,發送消息可以指定發送到哪個mailbox,每個mailbox下都有一個消息隊列。這裏timer也類似,每個task可以有多個定時任務隊列,這裏指定要啓動哪個隊列的計時。傳入的參數ticks表示計時時長,is_continuous表示是一次性的還是連續的,所謂連續就是說任務到時再繼續重新計時,也就是輪詢。

要注意的是每時每刻都有可能有新的定時任務進來,那指定一個基準就非常重要了,這個基準就是當前正在進行的定時任務的起點。所以這裏要對ticks進行調整,傳入的ticks是從當前開始算起的,所以要調整爲以當前正在進行的定時任務的起點爲基準,而當前定時任務已經進行了多久呢?結果是OSNumOrigTicks - OSTicksTilExp,所以要給ticks加上這個差,接下來給這個計時輪詢設置到OSTaskTmr中,將來啓動計時會用到。

接下來再看gki_adjust_timer_count,這個函數的意思是如果當前新到的這個定時任務比系統當前的這個任務更早到時的話,需要調整一下。如下:

void gki_adjust_timer_count (INT32 ticks)
{
    if (ticks > 0)
    {
        /* See if the new timer expires before the current first expiration */
        if (gki_cb.com.OSNumOrigTicks == 0 || (ticks < gki_cb.com.OSTicksTilExp && gki_cb.com.OSTicksTilExp > 0))
        {
            gki_cb.com.OSNumOrigTicks = (gki_cb.com.OSNumOrigTicks - gki_cb.com.OSTicksTilExp) + ticks;
            gki_cb.com.OSTicksTilExp = ticks;
            alarm_service_reschedule();
        }
    }

    return;
}

這裏調整完了還得再調alarm_service_reschedule重新設置一下定時器,這個函數裏除了設置定時器之外,還會考慮要不要馬上持有wake lock。如果離計時的deadline還早的話,如果持有wake lock就先釋放掉。如果離deadline在3s內,如果沒有持有wake lock就馬上申請。

接下來再來看如何添加定時任務,以gatt_start_rsp_timer爲例,這個函數用於等待請求的回調:

void gatt_start_rsp_timer(UINT16 clcb_idx)
{
    tGATT_CLCB *p_clcb = &gatt_cb.clcb[clcb_idx];
    UINT32 timeout = GATT_WAIT_FOR_RSP_TOUT;
    p_clcb->rsp_timer_ent.param  = (TIMER_PARAM_TYPE)p_clcb;
    if (p_clcb->operation == GATTC_OPTYPE_DISCOVERY &&
        p_clcb->op_subtype == GATT_DISC_SRVC_ALL)
    {
        timeout = GATT_WAIT_FOR_DISC_RSP_TOUT;
    }
    btu_start_timer (&p_clcb->rsp_timer_ent, BTU_TTYPE_ATT_WAIT_FOR_RSP,
                     timeout);
}

這裏主要是調用了btu_start_timer,傳入了計時器超時,計時任務的timer entry,這個entry要掛到btu task計時任務隊列下的,此外還帶上了計時的類型。我們看btu_start_timer的實現,如下:

void btu_start_timer (TIMER_LIST_ENT *p_tle, UINT16 type, UINT32 timeout)
{
    BT_HDR *p_msg;
    GKI_disable();
    /* if timer list is currently empty, start periodic GKI timer */
    if (btu_cb.timer_queue.p_first == NULL)
    {
        /* if timer starts on other than BTU task */
        if (GKI_get_taskid() != BTU_TASK)
        {
            /* post event to start timer in BTU task */
            if ((p_msg = (BT_HDR *)GKI_getbuf(BT_HDR_SIZE)) != NULL)
            {
                p_msg->event = BT_EVT_TO_START_TIMER;
                GKI_send_msg (BTU_TASK, TASK_MBOX_0, p_msg);
            }
        }
        else
        {
            /* Start free running 1 second timer for list management */
            GKI_start_timer (TIMER_0, GKI_SECS_TO_TICKS (1), TRUE);
        }
    }

    GKI_remove_from_timer_list (&btu_cb.timer_queue, p_tle);

    p_tle->event = type;
    p_tle->ticks = timeout;
    p_tle->ticks_initial = timeout;

    GKI_add_to_timer_list (&btu_cb.timer_queue, p_tle);
    GKI_enable();
}

如果btu task的計時任務隊列爲空,表示當前btu task的計時輪詢已經停掉了,所以要啓動當前btu task的計時輪詢,這裏的輪詢週期是1s。

好了,我們最後來看計時任務到時會如何處理。文章開頭時我們提到定時器到時會執行bt_alarm_cb回調,我們看這個函數:

static void bt_alarm_cb(void *data)
{
    UINT32 ticks_taken = 0;

    alarm_service.timer_last_expired_us = now_us();
    if (alarm_service.timer_last_expired_us > alarm_service.timer_started_us)
    {
        ticks_taken = GKI_MS_TO_TICKS((alarm_service.timer_last_expired_us
                                       - alarm_service.timer_started_us) / 1000);
    }

    GKI_timer_update(ticks_taken > alarm_service.ticks_scheduled
                   ? ticks_taken : alarm_service.ticks_scheduled);
}

這裏首先要算出alarm service經過的時間,然後用這個時間去更新計時器,我們看GKI_timer_update,如下:

void GKI_timer_update (INT32 ticks_since_last_update)
{
    UINT8   task_id;
    long    next_expiration;        /* Holds the next soonest expiration time after this update */

    gki_cb.com.OSTicksTilExp -= ticks_since_last_update;

    ......

    if (gki_cb.com.OSTicksTilExp > 0)
    {
        ......
        return;
    }

    next_expiration = GKI_NO_NEW_TMRS_STARTED;

    gki_cb.com.OSNumOrigTicks -= gki_cb.com.OSTicksTilExp;

    GKI_disable();

    for (task_id = 0; task_id < GKI_MAX_TASKS; task_id++)
    {
        ......

         /* If any timer is running, decrement */
        if (gki_cb.com.OSTaskTmr0[task_id] > 0)
        {
            gki_cb.com.OSTaskTmr0[task_id] -= gki_cb.com.OSNumOrigTicks;

            if (gki_cb.com.OSTaskTmr0[task_id] <= 0)
            {
                /* Reload timer and set Timer 0 Expired event mask */
                gki_cb.com.OSTaskTmr0[task_id] = gki_cb.com.OSTaskTmr0R[task_id];
                GKI_send_event (task_id, TIMER_0_EVT_MASK);
            }
        }

        /* Check to see if this timer is the next one to expire */
        if (gki_cb.com.OSTaskTmr0[task_id] > 0 && gki_cb.com.OSTaskTmr0[task_id] < next_expiration)
            next_expiration = gki_cb.com.OSTaskTmr0[task_id];
    }

    /* Set the next timer experation value if there is one to start */
    if (next_expiration < GKI_NO_NEW_TMRS_STARTED)
    {
        gki_cb.com.OSTicksTilExp = gki_cb.com.OSNumOrigTicks = next_expiration;
    }
    else
    {
        gki_cb.com.OSTicksTilExp = gki_cb.com.OSNumOrigTicks = 0;
    }

    // Set alarm service for next alarm.
    alarm_service_reschedule();

    GKI_enable();

    return;
}

函數比較長,首先更新OSTicksTilExp,這個表示當前任務還有多久到時,既然時間已經又過了那麼久,所以這裏應該減掉這個時間差。如果OSTicksTilExp仍然大於0,表示任務還未到時,則直接返回。否則先更新一下OSNumOrigTicks,因爲OSTicksTilExp爲負表示任務已經超時一段時間了,由於OSNumOrigTicks表示任務執行時長,所以應該算上這一段超過的時間。

更新完GKI的定時器參數後,該更新各task的定時器參數了,包括OSWaitTmr和OSTaskTmr。不過OSWaitTmr我找來找去沒發現有什麼用處,我們這裏只看OSTaskTmr。這裏OSTaskTmr是在哪裏設置的呢?在啓動task的計時輪詢的時候,就是GKI_start_timer中。我們在設置這個值的時候,基準是當時正在計時任務的起始點,所以現在這個任務既然已經到時了,那麼OSTaskTmr也應該減掉一個時間差,即爲OSNumOrigTicks。

如果OSTaskTmr小於0,表示這個計時輪詢一輪已經結束了,該啓動下一輪了。這裏調用GKI_send_event發送了一個event出去,我們看這個事件處理邏輯,如下:

if (event & TIMER_0_EVT_MASK) {
    GKI_update_timer_list (&btu_cb.timer_queue, 1);

    while (!GKI_timer_queue_is_empty(&btu_cb.timer_queue)) {
        TIMER_LIST_ENT *p_tle = GKI_timer_getfirst(&btu_cb.timer_queue);
        if (p_tle->ticks != 0)
            break;

        GKI_remove_from_timer_list(&btu_cb.timer_queue, p_tle);

        switch (p_tle->event) {
            ......

            case BTU_TTYPE_ATT_WAIT_FOR_RSP:
                gatt_rsp_timeout(p_tle);
                break;

            ......
        }
    }

    /* if timer list is empty stop periodic GKI timer */
    if (btu_cb.timer_queue.p_first == NULL)
    {
        GKI_stop_timer(TIMER_0);
    }
}

這個GKI_update_timer_list (&btu_cb.timer_queue, 1);我暫時沒看懂,理論上不應該傳1的,這個定時輪詢週期是1s,應該是100個ticks纔對。更新計時任務隊列中每個任務的ticks,都減掉這個時間差,如果小於0就表示任務到時了,下面就可以依次處理了。在while循環中依次取隊列頭,如果ticks爲0表示隊列中該任務已經到時了,就把該任務從隊列中刪除,然後根據該任務的event來做不同處理。

回到GKI_timer_update,如果OSTaskTmr大於0,表示這個隊列的輪詢還沒到,當前到時的可能是別的隊列的輪詢,所以暫時不做處理。這個next_expiration表示所有隊列中最先超時的,然後GKI全局的OSTicksTilExp和OSNumOrigTicks都設爲該值。表示從當前開始,下一個任務總時長是OSNumOrigTicks,且當前距離該任務超時還剩OSTicksTilExp。然後再調alarm_service_reschedule函數重新設置timer。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章