9.3.2 RIL_startEventLoop分析
第一个关键点是RIL_startEventLoop函数,这个函数实际上是由libRil.so实现的,它的代码在Ril.cpp中,代码如下所示:
[—>Ril.cpp]
extern"C"void RIL_startEventLoop(void){
int ret;
pthread_attr_t attr;
s_started=0;
pthread_mutex_lock(&s_startupMutex);
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
//创建工作线程eventLoop。
ret=pthread_create(&s_tid_dispatch,&attr,eventLoop,NULL);
/*
工作线程eventLoop运行后会设置s_started为1,并触发s_startupCond。
这几个语句的目的是保证在RIL_startEventLoop返回前,工作线程一定是已经创建并运行了。
*/
while(s_started==0){
pthread_cond_wait(&s_startupCond,&s_startupMutex);
}
pthread_mutex_unlock(&s_startupMutex);
if(ret<0){
return;
}
}
从上面的代码可知,RIL_startEventLoop会等待工作线程创建并运行成功。这个线程为什么会如此重要呢?下面就来了解一下工作线程eventLoop。
1.工作线程eventLoop
工作线程eventLoop的代码如下所示:
[—>Ril.cpp]
static voideventLoop(voidparam){
int ret;
int filedes[2];
//①初始化请求队列。
ril_event_init();
//下面这几个操作告诉RIL_startEventLoop函数本线程已经创建并成功运行了。
pthread_mutex_lock(&s_startupMutex);
s_started=1;
pthread_cond_broadcast(&s_startupCond);
pthread_mutex_unlock(&s_startupMutex);
//创建匿名管道。
ret=pipe(filedes);
……
s_fdWakeupRead=filedes[0];
s_fdWakeupWrite=filedes[1];
//设置管道读端口的属性为非阻塞。
fcntl(s_fdWakeupRead,F_SETFL,O_NONBLOCK);
//②下面这两句话将匿名管道的读写端口加入到event队列中。
ril_event_set(&s_wakeupfd_event,s_fdWakeupRead,true,
processWakeupCallback,NULL);
rilEventAddWakeup(&s_wakeupfd_event);
//③进入事件等待循环中,等待外界触发事件并做对应的处理。
ril_event_loop();
return NULL;
}
工作线程的工作并不复杂,主要有三个关键点。
(1)ril_event_init分析
工作线程,顾名思义就是用来干活的。要让它干活,是否得有一些具体的任务呢?它是如何管理这些任务的呢?对这两问题的回答是:
工作线程使用了一个叫ril_event的结构体来描述一个任务,并且它将多个任务按时间顺序组织起来,保存在任务队列中。这个时间顺序是指该任务的执行时间,由外界设定,可以是未来的某时间。
ril_event_init函数就是用来初始化相关队列和管理结构的,代码如下所示:
注意 在代码中,“任务”也称为“事件”,如没有特殊说明必要,这两者以后不再做区分。
[—>Ril.cpp]
void ril_event_init()
{
MUTEX_INIT();//初始化一个mutex对象listMutex。
FD_ZERO(&readFds);//初始化readFds,看来Ril会使用select来做多路IO复用。
//下面的timer_list和pending_list分别是两个队列。
init_list(&timer_list);//初始化timer_list,任务插入的时候按时间排序。
init_list(&pending_list);//初始化pendling_list,保存每次需要执行的任务。
/*
watch_table(监控表)定义如下:
static struct ril_event*watch_table[MAX_FD_EVENTS];
其中MAX_FD_EVENTS的值为8。监控表主要用来保存那些FD已经加入到readFDs中的任务。
*/
memset(watch_table,0,sizeof(watch_table));
}
此ril_event_init函数没什么新鲜的内容。任务在代码中的对等物Ril_event结构的代码如下所示:
[—>Ril_event.h]
struct ril_event{
struct ril_event*next;
struct ril_event*prev;//next和prev将ril_event组织成了一个双向链表。
int fd;//该任务对应的文件描述符,以后简称FD。
int index;//这个任务在监控表中的索引。
/*
是否永久保存在监控表中,一个任务处理完毕后将根据这个persist参数来判断。是否需要从监控表中移除。
*/
bool persist;
struct timeval timeout;//该任务的执行时间。
ril_event_cb func;//任务函数。
void*param;//传给任务函数的参数。
};
ril_event_init刚初始化完任务队列,下面就有地方添加任务了。
(2)任务加入队列
下面这两行代码初始化一个FD为s_wakeupfd_event的任务,并将其加入到监控表中:
/*
s_wakeupfd_event定义为一个静态的ril_event,ril_event_set函数将初始化它的FD为管道的读端,任务函数ril_event_cb对应为processWakeupCallback,并设置persist为true。
*/
ril_event_set(&s_wakeupfd_event,s_fdWakeupRead,true,
processWakeupCallback,NULL);
//来看这个函数:
rilEventAddWakeup(&s_wakeupfd_event);
rilEventAddWakeup比较有意思,来看这个函数,代码如下所示:
[—>Ril.cpp]
static void rilEventAddWakeup(struct ril_event*ev){
ril_event_add(ev);//ev指向一项任务
triggerEvLoop();
}
//直接看ril_event_add函数和triggerEvLoop函数。
void ril_event_add(struct ril_event*ev)
{
……
MUTEX_ACQUIRE();//锁保护
for(int i=0;i<MAX_FD_EVENTS;i++){
//从监控表中找到第一个空闲的索引,然后把这个任务加到监控表中,
//index表示这个任务在监控中的索引。
if(watch_table[i]==NULL){
watch_table[i]=ev;
ev->index=i;
……
//将任务的FD加入到readFds中,这是select使用的标准方法。
FD_SET(ev->fd,&readFds);
if(ev->fd>=nfds)nfds=ev->fd+1;
……
break;
}
}
MUTEX_RELEASE();
……
}
//再来看triggerEvLoop函数,这个就更简单了:
static void triggerEvLoop(){
int ret;
/*
s_tid_dispatch是工作线程eventLoop的线程ID,pthread_self返回调用线程的线程ID。
由于这里调用triggerEvLoop的就是eventLoop自己,所以不会走if分支,但是可以看看里面的内容。
*/
if(!pthread_equal(pthread_self(),s_tid_dispatch)){
do{
//s_fdWakeupWrite为匿名管道的写端口,看来触发eventLoop工作的条件就是
//往这个端口写一点数据了。
ret=write(s_fdWakeupWrite,"",1);
}while(ret<0&&errno==EINTR);
}
}
一般的线程间通信使用同步对象来触发,而rild是通过往匿名管道写数据来触发工作线程工作的。
(3)ril_event_loop分析
来看最后一个关键函数ril_event_loop,其代码如下所示:
[—>Ril.cpp]
void ril_event_loop()
{
int n;
fd_set rfds;
struct timeval tv;
struct timeval*ptv;
for(;){
memcpy(&rfds,&readFds,sizeof(fd_set));
/*
根据timer_list来计算select函数的等待时间,timer_list已经按任务的执行时间排好序了。
*/
if(-1==calcNextTimeout(&tv)){
ptv=NULL;
}else{
ptv=&tv;
}
……;
//调用select进行多路IO复用。
n=select(nfds,&rfds,NULL,NULL,ptv);
……
//将timer_list中那些执行时间已到的任务移到pending_list队列。
processTimeouts();
//从监控表中转移那些有数据要读的任务到pending_list队列,如果任务的persisit不为
//true,则同时从监控表中移除这些任务。
processReadReadies(&rfds,n);
//遍历pending_list,执行任务的任务函数。
firePending();
}
}
根据对ril_event_Loop函数的分析可知,Rild支持两种类型的任务:
定时任务。它的执行由执行时间决定,和监控表没有关系,在Ril.cpp中由ril_timer_add函数添加。
非定时任务,也叫Wakeup Event。这些任务的FD将加入到select的读集合(readFDs)中,并且在监控表中存放了对应的任务信息。它们触发的条件是这些FD可读。对于管道和Socket来说,FD可读意味着接收缓冲区中有数据,这时调用recv不会因为没有数据而阻塞。
注意 对于处于listen端的socket来说,FD可读表示有客户端连接上了,此时需要调用accept接受连接。
2.关于RIL_startEventLoop的总结
总结一下RIL_startEventLoop的工作。从代码中看,这个函数将启动一个比较重要的工作线程eventLoop,该线程主要用来完成一些任务处理,而目前还没有给它添加任务。