浅析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的消息循环的。但是,通过分析我们发现,实际上它是一个通用的消息循环模型,也可以说是一种设计模式。既然是设计模式,就应该反复推敲它,直至为我所用。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值