1、概述
今天主要讲下RIL相关模块内容,RIL分为RILJ和RILD两部分,RILJ属于Java层,RILD属于C层。那先看下RILD所处于的位置,以及与其相关的模块,如下图1.1使用红色框标注的内容。RILD处于android系统HAL(HARDWARE ABSTRACTION LAYER)这一层。是telephony framework(RILJ属于这一部分,用来与RILD建立通道)与modem(基带芯片)沟通的桥梁。最上层就是通话和短信等APK。
图1.1
2、RIL Architecture
图1.2 RIL Architecture
如上图所示RIL 整体架构分为三层,最上层为RIL-Java 适配层,中间层即为RILD, 下层为Modem. RIL-Java 通过socket与RILD 层对接,RILD层与Modem通过串口对接。
3、RILD 简介
Radio Interface Layer Demon,简称RIL,是手机上Modem(基带芯片,WCDMA,CDMA2000等就是指基带芯片的不同技术)与android系统通讯的桥梁,RIL的角色非常重要,RIL被设计成能够可靠的高效的传输数据一个模块
图1.3
Android RILD可以分成2个模块,一个部分RIL Demon(RILD),用于通过socket和framework通讯以及处理整个RIL的event;另一部分是手机厂商自己实现的部分,在这里暂时称之为vendor RIL。 之所以这样设计是因为不同的厂商使用的Modem不一样,而RIL又和Modem紧密联系,所以Android有把和Modem联系紧密的部分和公共部分剥离开,让不同的厂商可以自己实现vendor RIL以适应厂商自己的Modem。所以又可以细化如下图所示:
图1.4
但就目前来说,不同的芯片厂商都是自己实现了RILD,但基本的架构是沿用了Google rild。
4、RILD File Structure
Android系统的ril代码在hardware/ril 目录如下图所示:
图1.5 RIL File
rild: 包括rild.c 以及radiooptions.c, 建立守护进程。可利用radiooptions.c 进行自动或者手动调试, 它的radiooptiongs通过启动参数获取, 利用socket与rild通信,可供调试时配置Modem参数。
libril: 主要包括ril.cpp, ril_event.cpp 等,该包生成动态共享库libril.so, 与rild结合相当紧密。组成部分为ril.cpp,ril_event.cpp。libril.so驻留在 rild这一守护进程中,主要完成同上层通信的工作(启动事件循环机制,以及开启同上层通信的socket),接受rilJ request并传递给reference-ril.so, 同时把来自reference -ril.so的response回传给调用进程。
reference-ril: 该文件中内容将生成动态库reference-ril.so, 包括的文件主要有atchannel.c,at_tok.c,misc.c等。reference-ril so 和 rild 结合比较松散,通过dlopen去打开库,这也是因为reference-ril.so主要负责跟Modem硬件通信,这样的设计可以方便适配更多不同的Modem。它转换来自libril.so的请求为AT命令,同时监控Modem的反馈信息,并传递回libril.so。在初始化时, rild通过符号RIL_Init获取一组函数指针并以此与之建立联系。atchannels负责处理提交的at commnd, 并通过串口发送给modem, 以及使用reader-loop循环监控串口modem上报的信息。
4.1 RIL Key File Description
rild.c
rild.c 为RILD 的入口,负责初始化事件循环(libril.so),启动关联modem的串口监听(librefrence_ril.so),注册socket监听。
init event loop. 使用ril.cpp 中的RIL_startEventLoop 方法。ril_event_init完成后,通过ril_event_set来配置一新ril_event,并通过ril_event_add加入队列之中(实际通常用rilEventAddWakeup来添加),add会把队列里所有ril_event的fd,放入一个fd集合readFds中。这样 ril_event_loop能通过一个多路复用I/O的机制(select)来等待这些fd,如果任何一个fd有数据写入,则进入分析流程processTimeouts(),processReadReadies(&rfds, n),fire Pending()。并且我们可以看到,在进入ril_event_loop之前,已经挂入了一s_wakeupfd_event,通过pipe的机制实现,这个event的目的是可以在一些情况下,能内部唤醒ril_event_loop的多路复用阻塞,比如一些带timeout的命令timeout到期的时候。
start serial port. 通过动态加载动态库的方式执行reference-ril.c(reference-ril.so)中的RIL_Init.
RIL_Init首先通过参数获取硬件接口的设备文件或模拟硬件接口的socket. 接下来便新开一个线程继续初始化, 即mainLoop。
mainLoop的主要任务是建立起与硬件的通信,然后通过read方法阻塞等待硬件的主动上报或响应。在注册一些基础回调(timeout,readerclose)后,mainLoop首先打开硬件设备文件,建立起与硬件的通信,s_device_path和s_port是前面获取的设备路径参数,将其打开(两者可以同时打开并拥有各自的reader,这里也很容易添加双卡双待等支持)。
接下来通过at_open函数建立起这一设备文件上的reader等待循环,这也是通过新建一个线程完成, ret = pthread_create(&s_tid_reader, &attr, readerLoop, &attr),入口点readerLoop。readerLoop 负责监听modem 发送过来的讯息。
Register socket. RIL_register(funcs);
由RIL_Init的返回RIL_RadioFunctions结构的指针。
typedef struct {
int version; /* set to RIL_VERSION */
RIL_RequestFunc onRequest;
RIL_RadioStateRequest onStateRequest;
RIL_Supports supports;
RIL_Cancel onCancel;
RIL_GetVersion getVersion;
} RIL_RadioFunctions;
其中最重要的是onRequest域,上层来的请求都由这个函数进行映射后转换成对应的AT命令发给硬件。rild通过RIL_register注册这一指针。
RIL_register中要完成的另外一个任务,就是打开前面提到的跟上层通信的socket接口(s_fdListen是主接口,s_fdDebug供调试时使用)。
然后将这两个socket接口使用任务一中实现的机制进行注册(仅列出s_fdListen)ril_event_set (&s_listen_event, s_fdListen, false, listenCallback, NULL);
rilEventAddWakeup (&s_listen_event);
这样将两个socket加到任务一中建立起来多路复用I/O的检查句柄集合中,一旦有上层来的(调试)请求,event机制便能响应处理了。
reference-ril .c & atchannel.c
hardware/ril/reference-ril 包中的两个主体文件,同时也是Android 实现电话功能的两个主体文件,这个库必须实现的是一个名称为RIL_Init的函数,这个函数执行的结果是返回一个RIL_RadioFunctions结构体的指针,指针指向函数指针。即整个库的入口,这个库在执行的过程中需要创建一个线程来执行实际的功能。在执行的过程中,这个库将打开一个/dev/ttySXXX的终端,然后利用这个终端控制硬件执行。
在rild.c 中使用RIL_Init 获取的RIL_RadioFunctions, 实际为reference_ril.c 中的s_callbacks。
关键的入口方法有:
static void onRequest (int request, void *data, size_t datalen, RIL_Token t); reference-ril 的入口函数。
关键的出口方法有:
static void onUnsolicited (const char *s, const char *sms_pdu); 执行URC类型的响应。
static void handleFinalResponse(const char *line); 执行非URC类型的响应,激活s_commandcond 信号,使send_command_xxxx方法返回,从而导致onRequest返回。
libril.so动态库
libril.so库的目录是:hardware/ril/libril
其中主要的文件为ril.cpp.
RIL_startEventLoop(void);
void RIL_setcallbacks (const RIL_RadioFunctions *callbacks);
RIL_register (const RIL_RadioFunctions *callbacks);
RIL_onRequestComplete(RIL_Token t, RIL_Errno e, void *response, size_t responselen);
void RIL_onUnsolicitedResponse(int unsolResponse, void *data, size_t datalen);
RIL_requestTimedCallback (RIL_TimedCallback callback, void *param, const struct timeval *relativeTime);
这些函数也是被rild守护进程调用的,不同的vendor可以通过自己的方式实现这几个接口,这样可以保证RIL可以在不同系统的移植。其中 RIL_register()函数把外部的RIL_RadioFunctions结构体注册到这个库之中,在恰当的时候调用相应的函数。在Android 电话功能执行的过程中,这个库处理了一些将请求转换成字符串的功能。
RIL_startEventLoop 将启动整个的Event-loop。
RIL_onRequestComplete 当reference-ril执行onRequest完毕后,将执行,把对应的结果返回给上层。
RIL_onUnsolicitedResponse 当reference-ril 执行URC 类型的响应完毕后,将对应的结果返回给上层。
RIL_requestTimedCallback 注入一个新的timer-event到event-loop中。
5、RILD init Flow
RILD初始化过程主要是创建3个主要的线程,readerLoop, mainLoop, EventLoop.如下图所示:
图1.6
1、 在RILD主线程中首先在函数RIL_startEventLoop()启动eventLoop线程然后wait,在eventLoop中初始化主要是初始化ril_event链表timer_list和pending_list,以及ril_event指针数据watch_table,然后添加wakeup event事件到watch_table.
2、 创建mainLoop,在mainLoop中主要是创建readerLoop,然后注册初始化回调函数initializeCallback(),这个函数会被作为一个timeout event添加到timer_list中,当对应的timer到达时进行回调,做一系列的初始化动作,主要向modem下大量的AT Command,初始化SIM卡以及modem。
3、 readerLoop主要是用来循环读取来自modem的message,然后处理判断进行分发。
4、 在主线程后半部分回去向watch_table添加两个EVENT,一个是和RILJ连接的socket fd event,一个用来在命令行(即adb)进行调试的socket连接fd event。
以上各thread在后面都将详细介绍。
6、Event-Loop
Rild管理的真正精髓在ril.cpp,ril_event.cpp中
Event对象定义如下:
struct ril_event {
struct ril_event *next;
struct ril_event *prev;
int fd; //文件描述符,要添加到fdset集合,select函数用到
int index; //watch_table数组的索引,用于清空该数字对应的元素
bool persist; //指示该event在触发后是否需要移除watch_table
struct timeval timeout; //计时器触发时间,微妙级
ril_event_cb func; //事件触发时的回调函数
void *param; //以上回调函数的参数
};
通过next和prev组合成Event-Loop.
fd: 事件相关设备句柄。例如对于串口数据事件,fd就是相关串口的设备句柄。
index: 为watch_table中的索引,当不在watch_table 中时为-1。
persist: 是否在watch_table中保持,如果是保持的,则不从watch_table中删除.
func: 事件回调处理函数.
param: 回调时处理函数的参数.
为了统一管理事件处理,Android使用了三个队列:watch_table(非严格意义上的list),timer_list, pending_list,并使用了一个设备句柄池readFDS。 readFDS:是Linux的fd_set,readFDS保存了Rild中所有的设备文件句柄,以便利用select函数(参考Reference A)统一的完成事件的侦听以及多路复用。watch_list:监测事件队列。需要监测的事件都放入到该队列中。
watch_list:监测事件队列,需要检测的事件都放入到该队列中。
timer_list:timer事件队列,在timeout被回调的事件放入该队列中
pending_list:待处理事件队列,事件已经触发,在此排队等待回调函数被调用。
static fd_set readFds;
static int nfds = 0;
// MAX_FD_EVENTS = 8
static struct ril_event * watch_table[MAX_FD_EVENTS];
static struct ril_event timer_list;
static struct ril_event pending_list;
一个事件一般都要先初始化:
ril_event_init();
然后生成管道(文件,socket等文件句柄都有可能)后,事件,管道的读取端,回调函数以及它的参数,传给ril_event_set 函数初始化事件。
void ril_event_set(struct ril_event * ev, int fd, bool persist, ril_event_cb func, void * param);
然后通过:
void ril_event_add(struct ril_event * ev) 添加到监测队列(watch_table)。
在实际中一般使用:
rilEventAddWakeup (&s_wakeupfd_event);来添加。
add会把队列里所有ril_event的fd,放入一个fd集合readFds中,这样在event-loop中就可以使用select 函数来实现I/O多路复用。Select 函数描述见附录A:
另外我们可以看到, 在进入ril_event_loop之前, 已经挂入了一s_wakeupfd_event, 通过pipe的机制实现的,这个event的目的是可以在一些情况下,能内部唤醒ril_event_loop的多路复用阻塞,比如一些带timeout的命令timeout到期的时候。
Event-loop 的整体流程如下:
图1.7 Event-Loop
ril_event_loop 利用select等待在readFDS(fd_set)上,当select设备有数据时,ril_event_loop会从select返回,在 watch_list中相应的Event放置到pend_list,如果Event是持久性的则不从watch_list中删除。然后processTimeouts遍历Timer_list,如果有超时的event,从timer_list中删除然后添加到pengding_list中, 接着 ril_event_loop遍历pengding_list处理Event事件,发起事件回调函数。
几个重要的Event:
s_listen_event: (s_fdListen,listenCallback); 非持久型
listenCallback处理函数,
接收客户端连接:s_fdCommand=accepte(..)
添加s_commands_event()
重新建立s_listen_event,等待下一次连接
s_command_event: (s_fdCommand,ProcessCommandsCallback)
从fdCommand Socket连接中读取StreamRecord
使用ProcessCommandBufer处理数据
s_debug_event: (s_fdDebug, debugCallback)
调试专用
s_wakeupfd_event: 无名管道,用于队列主动唤醒(前面提到的队列刷新,就用它来实现,请参考使用它的相关地方)
s_listen_event在大的功能上处理客户端连接(Ril-JAVA层发起的connect),并建立s_commands_event去处理Socket连接发来的Ril命令。ProcessCommandBufer实际上包含了Ril指令的下行过程。
7、mainLoop
mainLoop 相对功能比较简单1、create readerLoop,添加了timer event到timer_list。是在RIL_Init()函数中创建的,在RIL_Init()中首先打开tty设备,然后早at_open函数中create了mainLoop,最后返回了s_callbacks(一个回调函数指针数组RILJ下发的request都会回调该数字的onRequest回调函数)。mainLoop初始化流程如下图所示:
图1.8
8、readerLoop
readerLoop主要是读取来自modem的数据,包括URC(modem主动上报的message)和request对应的response,本人一直秉承着有图有真相的理念,下面依然是show出在readerLoop流程图。如下图所示:
图1.9
9、RIL Flow
从上往下 Request
RIL-Java 把RILRequest传递给RILSender, RILSender 通过Socket 发送RILRequest(Parcel) 给RILD.在Event-loop 中的select 监听到请求链接的信号,激活s_listen_event. 植入pending_list中。
s_listen_list 启动listencallback 监听到Socket请求,接下来,s_fdCommand = accept(s_fdListen, (sockaddr *) &peeraddr, &socklen),获取传入的socket描述符,也就是上层的java RIL传入的连接.然 后,通过record_stream_new建立起一个record_stream, 将其与s_fdCommand绑定, 我们来关注command event的回调, processCommandsCallback函数, 从前面的event机制分析, 一旦s_fdCommand上有数据, 此回调函数就会被调用. (略过onNewCommandConnect的分析)
processCommandsCallback通过record_stream_get_next阻塞读取s_fdCommand上发来的 数据, 直到收到一完整的request(request包的完整性由record_stream的机制保证), 然后将其送达processCommandBuffer.
进入processCommandBuffer 后,就正式进入了命令的解析部分. 每个命令将以RequestInfo的形式存在.
typedef struct RequestInfo {
int32_t token; //this is not RIL_Token
CommandInfo *pCI;
struct RequestInfo *p_next;
char cancelled;
char local; // responses to local commands do not go back to command process
} RequestInfo;
这里的pRI就是一个RequestInfo结构指针, 从socket过来的数据流, 前面提到是Parcel处理过的序列化字节流, 这里会通过反序列化的方法提取出来. 最前面的是request号, 以及token域(request的递增序列号). 我们更关注这个request号, 前面提到, 上层和rild之间, 这个号是统一的. 它的定义是一个包含ril_commands.h的枚举。
在ril.cpp中
static CommandInfo s_commands[] = {
#include "ril_commands.h"
};
这个s_commands 数组和RIL-Java 中的RILConstant.java 中定义的常量匹配。
pRI直接访问这个数组, 来获取自己的pCI.
这是一个CommandInfo结构:
typedef struct {
int requestNumber;
void (*dispatchFunction) (Parcel &p, struct RequestInfo *pRI);
int(*responseFunction) (Parcel &p, void *response, size_t responselen);
} CommandInfo;
基本解析到这里就完成了, 接下来, pRI被挂入pending的request队列, 执行具体的pCI->dispatchFunction, 进行详细解析.
在dispatchXXX 方法中,将对parcel 进行解析,获取相应的从RIL-Java传来的逻辑和业务数据,然后调用s_callbacks 的onRequest方法。request请求在这里转入底层的libreference-ril处理,handler是reference-ril.c中的onRequest.
在onRequest中将根据request 的不同类型调用不同的方法去生成AT Command, 然后at_send_command, 然后at_send_command_full(注此方法不唯一,可能使用其他的方法如: at_send_command_singleline, at_send_command_sms, at_send_command_multiline等,这是根据at返回值,以及发命令流程的类型来区别的.), 然后at_send_command_full_nolock, 然后writeline. 把数据送入modem.
(注,at_send_command 是同步的,必须等待其返回)。
根据response s_type(命令的具体响应)及handleFinalResponse(标准响应)命令的类型(s_type)在send command的时候设置,有NO_RESULT,NUMERIC,SINGLELINE,MULTILINE几种,供不同的AT使用。比 如AT+CSQ是singleline, 返回at+csq=xx,xx,再加一行OK,比如一些设置命令,就是no_result, 只有一行OK或ERROR。这几个类型的解析都很相仿,通过一定的判断(比较AT头标记等),如果是对应的响应,就通过 addIntermediate挂到一个临时结果sp_response->p_intermediates队列里。如果不是对应响应,那它其实应该是穿插其中的自动上报,用onUnsolicite来处理。
具体响应,只起一个获取响应信息到临时结果,等待具体分析的作用。无论有无具体响应,最终都得以标准响应handleFinalResponse来完成,也就是接受到OK,ERROR等标准response来结束,这是大多数AT命令的规范。handleFinalResponse 会设置s_commandcond这一object,也就是at_send_command_full_nolock等待的对象。到这里,响应的完整信息 已经完全获得,send command可以进一步处理返回的信息了(临时结果,以及标准返回的成功或失败,都在sp_response中)。
进一步通过at_tok.c 分析response. 然后调用RIL_onRequestComplete 返回。
RIL_onRequestComplete:RIL_onRequestComplete和RIL_onUnsolicitedResponse很相仿,功能也一致。通 过Parcel来传递回上层,同样是先写入RESPONSE_SOLICITED(区别于 RESPONSE_UNSOLICITED),pRI->token(上层传下的request号),错误码(send command的错误,不是AT响应)。如果有AT响应,通过访问pRI->pCI->responseFunction来完成具体 response的解析,并写入Parcel。
然后通过同样的途径:sendResponse-->sendResponseRaw-->blockingWrite-->write to s_fdCommand完成最终的响应传递。
从下往上 Response
Modem 通过串口向RIL发送数据。
Read-Loop 监听到,通过readLine读取数据。在这里首先对sms进行检测,isSMSUnsolicited, 因为短信的AT处理通常比较麻烦,无论收发都单独列出。这里是因为要即时处理这条短信消息(两行,标志+pdu),而不能拆开处理。除开sms的特例,所有的line都要经过processLine.
在processLine中测试是否为URC 数据,如网络状态,短信,来电等要主动上报,另外一种即响应应答,流程如下:
processLine
|----no cmd--->handleUnsolicited //主动上报
|----isFinalResponseSuccess--->handleFinalResponse //成功,标准响应
|----isFinalResponseError--->handleFinalResponse //失败,标准响应
|----get '>'--->send sms pdu //收到>符号,发送sms数据再继续等待响应
|----switch s_type--->具体响应 //命令有具体的响应信息需要对应分析
对于handFinalResponse 那么就只要返回line, 然后激活s_commandcond让onRequest处理对应的返回即可。下面分析URC类型。
主动上报即调用handleUnsolicited. 它将调用reference-ril.c 中的onUnsolicited 方法。
onUnsolicite(主动上报响应)
static void onUnsolicited (const char *s, const char *sms_pdu);
短信的AT设计真是很麻烦,以致这个函数的第二个参数完全就是为它准备的,若是短信的URC是要单独特殊处理的。response 的主要的解析过程,由at_tok.c中的函数完成,其实就是字符串按块解析,具体的解析方式由每条命令或上报信息自行决定。这里不再详述,onUnsolicited只解析出头部(一般是+XXXX的形式),然后按类型决定下一步操作,操作为 RIL_onUnsolicitedResponse和RIL_requestTimedCallback两种。
a)RIL_onUnsolicitedResponse: 将unsolicited的信息直接返回给上层。通过Parcel传递,将 RESPONSE_UNSOLICITED,unsolResponse(request号)写入Parcel先,然后通过 s_unsolResponses数组,查找到对应的responseFunction完成进一步的的解析,存入Parcel中。最终通过 sendResponse将其传递回原进程。流程:sendResponse-->sendResponseRaw-->blockingWrite-->write to s_fdCommand(前面建立起来的和上层框架的socket连接)这些步骤之后有一些唤醒系统等其他操作。不再详述。
b)RIL_requestTimedCallback:
通过event机制实现的timer机制,回调对应的内部处理函数。通过internalRequestTimedCallback将回调添加到event循环,最终完成callback上挂的函数的回调。比如pollSIMState,onPDPContextListChanged等回调,不用返回上层,内部处理就可以。
通过socket 将URL提交给RIL-Java.
整体的RIL-Flow 如下图:
图1.10 RIL-Process Flow
11、RIL-Java
RIL-Java在本质上就是一个RIL代理,起到一个转发的作用,是Android Java概念空间中的电话系统的起点。在RIL-D中,已经创建了Socket 等待RIL-Java链接,一旦连接成功,RIL-JAVA就可通过RILSender发起一个请求,并通过RILReceiver等待应答,并将结构发送到目标处理对象。在RIL-Java中,这个请求称为RILRequest。
CommandInterface
CommandInterface 是一个顶层的接口,有一个直接子类BaseCommand, 然后RIL 继承了BaseCommand, 在CommandInterface 中定义了一系列的关于Telephony 的通用方法。这些都是电话的基本功能的描述,是对Modem AT指令的提炼抽象。对应相关的规范文档包括:
这些文档在欧洲电信联盟官网可以找到
3GPP TS24.008 chart5
V.25ter AT Commands
3GPP 07.07 AT Comamnds-General commands
3GPP 07.07 AT Comamnds-Call Control commans
3GPP 07.07 AT Comamnds-Network Service related commands
3GPP 07.07 AT Comamnds-MT control and status command
3GPP 07.07 AT Comamnds-GPRS Commands
3GPP 07.07 Mobile Termination Errors
3GPP 07.05 SMS AT Commands
RILRequest
RILRequest(后面简称RR)为RIL 把上层的请求转化为Parcel 提供了统一的机制。
RILRequest的应用属性包括:
int mSerial;
int mRequest;
Message mResult;
Parcel mp;
RILRequest mNext;
mSerial 即序列号,该序列号意义重大,它贯通RIL-JAVA 和整个 RIL层,在RIL层称为: token. 作为RR的唯一识别号。在Response返回时需要与RR对接时,通过它来识别。它由sNextSerial来统一分发,保证其唯一性。
mRequest: 即具体的请求类型。该定义也贯通RIL-JAVA 和整个RIL 层,在RIL-Java层定义在RILConstants.java 中。
mResult: 即从RIL层返回的数据。一般由parcel 分析而来。
mp: 将上层具体的请求信息,转化为parcel 供RILSender通过socket传输到RIL层。
mNext: 池中的下一个RR.
RR采用了比较诡异的Pool 技术。并没有采用传统的数组定义的模式,而是采用了隐含列表的形式通过mNext来实现。Pool 中最多数量为MAX_POOL_SIZE = 4; 通过引用sPool来指示当前可用的RILRequest. 对外通过obtain方法来统一提供RILRequest. 通过release来释放RR(当多于或者等于4个时,直接释放掉,少于4个时回收重用)。
RILSender
RILSender 负责把RR 通过socket发送到RIL层。在RIL中启动了一个专门的发送线程HandlerThread mSenderThread; 而RILSender作为handler, 来具体操作这个mSenderThread, 即接收message和响应处理。
RIL-Java 的请求发送一般包括三部:
(1): 生成RILRequest,此时将生成m_Serial(请求的Token)并将请求号,数据,及其Result Message 对象填入到RILRequest中
(2):上层函数调用Command Interface将请求消息发送到Sender的架构。
(3): Sender接收到EVENT_SEND消息后,将请求发送到RILD的架构。
RILReceiver
RILReceiver(后面简称Receiver)实现了Runnable接口,是RIL中的一个线程。
Receiver负责监听链接RIL的socket(socket 名称为: SOCKET_NAME_RIL: rild). 并解析数据流为parcel, 然后提交processResponse处理。 该方法会查看parcel中的类型(parcel中的开头4个字节,即一个int), 查看是否是URC类型,如果是URC 类型,则调用processUnsolicited方法处理,如果不是,则调用processSolicited方法处理。processUnsolicited则继续读取parcel中的4个字节(即一个int)来判断response 的类型,然后调用RIL相应的Registrant的notifyRegistrant通知上层。processSolicited首先从池中找到对应的RR(从未响应RR队列:mRequestsList中找出,并移除), 然后解析parcel成message, 赋予RR的mResult字段,然后调用rr.mResult.sendToTarget() 通知上层。过程如下:
图1.11RIL-Java Response
12、异步应答架构
对于异步应答来讲,命令的发起者发送后,并不等待应答就返回,应答的回应是异步的,处理结果通过消息的方式返回。
在RIL-Java 中采用异步应当架构,RR发送后直接返回上层,而下层传递上来的信息以message的形式,通过Handler接收来处理。
异步应答需要注意3个问题:
(1). 如何进行请求RR 和 响应Response的匹配。
(2). 维持等待响应的请求序列。
(3). 如何管理响应超时。
对于第一个问题,通过RR 中的mRequest 来标识信息类型,通过mp 来封装具体的请求信息,通过mResult来封装返回的结果。而最关键的是通过一个token 来表示RR的序列号。在RIL-Java中即为mSerial.
对于第二个问题, 在RIL-Java 中利用一个mRequestsList维持等待请求序列。当RR通过RILSender发送后,添加到mRequestList中,当响应返回后,通过findAndRemoveRequestFromList 从mRequestList中抽取。
对于第三个问题, 在RIL 中的send_command_xxxx 设定等待的时间实现,这里不再说明。
下面给出一个完整的Request-Response 流程。
发送步骤:
第一步:生成RILRequest,此时将生成m_Serial(请求的Token)并将请求号,数据,及其Result Message 对象填入到RILRequest中
第二步:使用send将RILRequest打包到EVENT_SEND消息中发送到到RIL Sender Handler,
第三步:RilSender 接收到EVENT_SEND消息,将RILRequest通过套接口发送到RILD,同时将RILRequest保存在mRequest中以便应答消息的返回。
接收步骤:
第一步:分析接收到的Parcel,根据类型不同进行处理。
第二步:根据数据中的Token(mSerail),反查mRequest,找到对应的请求信息。
第三步:将是数据转换成结果数据。
第四步:将结果放在RequestMessage中发回到请求的发起者。
AT Command Parse
Dial Example.
拨打电话的界面响应以及上层的处理已经在Call UI Response.docx以及Phone App Core Process.docx 中详细描述。这里我们只从RIL-Java 开始,描述其在RIL Layer 的过程。
(1). GsmCallTracker 收到上层的dial 请求,调用cm.dial(…) 方法, cm 即RIL。
(2). RIL 获取一个RR, 在RR的mp 中首先写入了RR 的请求类型,为RIL_REQUEST_DIAL,然后写入了RR的mSerial。接着写入其他的信息如电话号码,模式等。
(3). 使用send(rr)方法将rr 封装成message发送给RILSender. Message的类型是EVENT_SEND。
(4). RILSender收到message后,通过socket(socket name: SOCKET_NAME_RIL) 把rr.mp这个parcel 发送到RIL layer. 然后它将返回。
(5). RIL layer 的Event-Loop 多路复用函数select, 将检测到socket(name: SOCKET_NAME_RIL) 有请求,于是processReadReadies 将把对应的s_listen_event 加入到pending_list中,随后firePending() 被调用, s_listen_event被激活,调用listenCallback监听Socket请求, 接着s_fdCommand = accept(s_fdListen, (sockaddr *) &peeraddr, &socklen),获取传入的socket描述符,也就是上层的java RIL传入的连接.然 后,通过record_stream_new建立起一个record_stream, 将其与s_fdCommand绑定, 生成s_commands_event,其回调函数为processCommandsCallback并注入watch_table中。前面的event机制分析, 一旦s_fdCommand上有数据, 此回调函数就会被调用.
(6). processCommandsCallback通过record_stream_get_next阻塞读取s_fdCommand上发来的 数据, 直到收到一完整的request(request包的完整性由record_stream的机制保证), 然后将其送达processCommandBuffer.
(7). 进入processCommandBuffer 后,就正式进入了命令的解析部分. 根据Parcel传入的请求类型: RIL_REQUEST_DIAL, 在s_commands中找到对应的s_command 为:{RIL_REQUEST_DIAL, dispatchDial, responseVoid}, 从parcel 中获取token, 即为RR中的mSerial. 封装为RequestInfo, 并调用RequestInfo中的s_command 的dispatchDial方法。
(8). 在dispatchDial中,进一步对parcel进行解析,获取原来rr中的全部写入数据。然后调用s_callbacks 的onRequest方法。request请求在这里转入底层的libreference-ril处理,handler是reference-ril.c中的onRequest.
(9). 在onRequest 中,根据request 的类型RIL_REQUEST_DIAL调用requestDial(data, datalen, t)函数。在此函数中,将把request解析为 at command.
(10). 通过at_send_command方法发送at command. 实际即调用at_send_command_full方法。继续调用at_send_command_full_nolock。 此时将等待modem的返回,即通过s_commandcond这个变量来wait。
(11). Modem 通过串口向RIL 发送数据,被Read-Loop监听到。通过readLine读取数据,然后调用processLine.
(12). handFinalResponse 那么就只要返回line, 然后激活s_commandcond让onRequest处理对应的返回, 接着到RIL_onRequestComplete。
(13). RIL_onRequestComplete,通过Parcel来传递回上层, 先写入RESPONSE_SOLICITED标识,然后写入pRI->token(上层传下的request号),然后调用responseVoid来详细的解析parcel.
(14). 然后通过:sendResponse-->sendResponseRaw-->blockingWrite-->write to s_fdCommand完成最终的响应传递。
(15). RIL-JAVA 中的RILReceiver 收到从RIL层收到的资讯后,将数据解析为Parcel. 并调用processResponse进一步处理。
(16). processResponse 读取开头的int, 判断是RESPONSE_SOLICITED,继续调用processSolicited (p);
(17). processSolicited 继续读取token(mSerial), 调用findAndRemoveRequestFromList 获得原始的RR. 然后根据RR 的请求类型: RIL_REQUEST_DIAL, 调用responseVoid方法解析完Parcel. 然后调用rr.mResult.sendToTarget();
(18). mResult 的target实际就是GsmCallTracker.在对用的handleMessage做对应的处理。
13、Reference
这部分时对RIL设计到的一些相关知识点的简单补充。
13.1、Select Function.
int select(int n,fd_set * readfds,fd_set * writefds,fd_set * exceptfds,struct timeval * timeout);
函数说明
select()用来等待文件描述词状态的改变。参数n代表最大的文件描述词加1,参数readfds、writefds 和exceptfds 称为描述词组,是用来回传该描述词的读,写或例外的状况。底下的宏提供了处理这三种描述词组的方式:
FD_CLR(inr fd,fd_set* set);用来清除描述词组set中相关fd 的位
FD_ISSET(int fd,fd_set *set);用来测试描述词组set中相关fd 的位是否为真
FD_SET(int fd,fd_set*set);用来设置描述词组set中相关fd的位
FD_ZERO(fd_set *set); 用来清除描述词组set的全部位
参数
timeout为结构timeval,用来设置select()的等待时间,其结构定义如下
struct timeval
{
time_t tv_sec;
time_t tv_usec;
};
返回值
如果参数timeout设为NULL则表示select()没有timeout。
错误代码:
执行成功则返回文件描述词状态已改变的个数,如果返回0代表在描述词状态改变前已超过timeout时间,当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds,exceptfds和timeout的值变成不可预测。
EBADF 文件描述词为无效的或该文件已关闭
EINTR 此调用被信号所中断
EINVAL 参数n 为负值。
ENOMEM 核心内存不足
RIL_RadioFunctions.
typedef struct {
int version; /* set to RIL_VERSION */
RIL_RequestFunc onRequest;
RIL_RadioStateRequest onStateRequest;
RIL_Supports supports;
RIL_Cancel onCancel;
RIL_GetVersion getVersion;
} RIL_RadioFunctions;
s_callbacks
static const RIL_RadioFunctions s_callbacks = {
RIL_VERSION,
onRequest, //key method, implements reference-ril at command.
currentState,
onSupports,
onCancel,
getVersion
};
ril_event
struct ril_event {
struct ril_event *next;
struct ril_event *prev;
int fd;
int index;
bool persist;
struct timeval timeout;
ril_event_cb func;
void *param;
};
RequestInfo
typedef struct RequestInfo {
int32_t token; //this is not RIL_Token
CommandInfo *pCI;
struct RequestInfo *p_next;
char cancelled;
char local; // responses to local commands do not go back to command process
} RequestInfo;
ATResponse
typedef struct {
int success; /* true if final response indicates
success (eg "OK") */
char *finalResponse; /* eg OK, ERROR */
ATLine *p_intermediates; /* any intermediate responses */
} ATResponse;
13.2、Parcel 基本机制
首先看如下简单的代码:
Bundle b = new Bundle();
// b.putString("Test", "Test....");
Parcel other = Parcel.obtain();
other.writeBundle(b);
Bundle bb = Bundle.CREATOR.createFromParcel(other);
就是把Bundle写入parcel,然后重新读取出来,读取的时候就出现了异常: bad magic number.
通过阅读JNI源码,Parcel实际上将读入到数据存入了缓冲区,缓冲区大小可变,自己可根据实际情况用setDataCapacity设定,提高效率。
重要的是游标:DataPosition(以字节为单位), 用来描述当前指向的缓冲区中的读(写)位置索引。写入数据,默认采用顺序写入的方式,游标自动根据写入的长度偏移;在自己实现读取时,读取时 必须 先设置游标位置,
并且读入和写入的顺序保持一致。
回到此代码 我们只要在读取前面加入other.setDataPosition(0); 即可。
在android中维持着一个Parcel Pool, 每次使用时,我们可以从池中申请: Parcel p = Parcel.obtain(); 使用完后通过p.recycle() 归还给pool.
Parcel主要使用在IPC通信中,一般和IBinder一起使用。实现的思想即通过内存拷贝的形式:
......
byte[] bytes = parcel.marshall(); //得到parcel中的数据
Parcel other = Parcel.obtain();
other.unmarshall(bytes, 0, bytes.length); //拷贝数据
other.setDataPosition(0); //必须设置,不然此时游标处于缓冲区的尾部。无法读取数据。
......
一般我们在IPC应用中,在其他Process中得到的parcel已经经过了上面的步骤,可放心使用。
Registrant & RegistrantList
相关代码:
frameworks\base\core\java\android\os\Registrant.java
frameworks\base\core\java\android\os\RegistrantList.java
这两个类是在Telephony Framework使用最多,也可以这么理解这两个类算是整个Telephony Framework的架构核心。Registrant & RegistrantList使用handler机制实现了观察者模式,每个模块都可注册自己感兴趣的EVENT到其他模块,当EVENT事件发生然后进行回调。
Registrant的总体思想是:一个对象中开辟一个空间用于存放Message,当调用register方法时将Message存放进去,当其调用notify方法时将所有Message取出并发送到MessageQueue中等待处理。
13.3、Linux 多线程编程概述
此reference 引用于: http://fanqiang.chinaunix.net/a4/b8/20010811/0905001105.html
1 引言
线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者。传统的 Unix也支持线程的概念,但是在一个进程(process)中只允许有一个线程,这样多线程就意味着多进程。现在,多线程技术已经被许多操作系统所支 持,包括Windows/NT,当然,也包括Linux。
为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?我们首先必须回答这些问题。
使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址 空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地 址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时 间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。
使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费 时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的 共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写 多线程程序时最需要注意的地方。
除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:
1) 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
2) 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
3) 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
下面我们先来尝试编写一个简单的多线程程序。
2 简单的多线程编程
Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需 要使用库libpthread.a。顺便说一下,Linux下pthread的实现是通过系统调用clone()来实现的。clone()是Linux所 特有的系统调用,它的使用方式类似fork,关于clone()的详细情况,有兴趣的读者可以去查看有关文档说明。下面我们展示一个最简单的多线程程序 example1.c。
/* example.c*/
#include <stdio.h>
#include <pthread.h>
void thread(void)
{
int i;
for(i=0;i<3;i++)
printf("This is a pthread.\n");
}
int main(void)
{
pthread_t id;
int i,ret;
ret=pthread_create(&id,NULL,(void *) thread,NULL);
if(ret!=0){
printf ("Create pthread error!\n");
exit (1);
}
for(i=0;i<3;i++)
printf("This is the main process.\n");
pthread_join(id,NULL);
return (0);
}
我们编译此程序:
gcc example1.c -lpthread -o example1
运行example1,我们得到如下结果:
This is the main process.
This is a pthread.
This is the main process.
This is the main process.
This is a pthread.
This is a pthread.
再次运行,我们可能得到如下结果:
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.
前后两次结果不一样,这是两个线程争夺CPU资源的结果。上面的示例中,我们使用到了两个函数, pthread_create和pthread_join,并声明了一个pthread_t型的变量。
pthread_t在头文件/usr/include/bits/pthreadtypes.h中定义:
typedef unsigned long int pthread_t;
它是一个线程的标识符。函数pthread_create用来创建一个线程,它的原型为:
extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr,
void *(*__start_routine) (void *), void *__arg));
第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是运行函数的参数。这里,我们的 函数thread不需要参数,所以最后一个参数设为空指针。第二个参数我们也设为空指针,这样将生成默认属性的线程。对线程属性的设定和修改我们将在下一 节阐述。当创建线程成功时,函数返回0,若不为0则说明创建线程失败,常见的错误返回代码为EAGAIN和EINVAL。前者表示系统限制创建新的线程, 例如线程数目过多了;后者表示第二个参数代表的线程属性值非法。创建线程成功后,新创建的线程则运行参数三和参数四确定的函数,原来的线程则继续运行下一 行代码。
函数pthread_join用来等待一个线程的结束。函数原型为:
extern int pthread_join __P ((pthread_t __th, void **__thread_return));
第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。这个函数是一个线程阻塞的函数,调用它的函数 将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。一个线程的结束有两种途径,一种是象我们上面的例子一样,函数结束了,调用它 的线程也就结束了;另一种方式是通过函数pthread_exit来实现。它的函数原型为:
extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));
唯一的参数是函数的返回代码,只要pthread_join中的第二个参数thread_return不是NULL,这个值将被传递给 thread_return。最后要说明的是,一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用pthread_join的线 程则返回错误代码ESRCH。
在这一节里,我们编写了一个最简单的线程,并掌握了最常用的三个函数pthread_create,pthread_join和pthread_exit。下面,我们来了解线程的一些常用属性以及如何设置这些属性。
3 修改线程的属性
在上一节的例子里,我们用pthread_create函数创建了一个线程,在这个线程中,我们使用了默认参数,即将该函数的第二个参数设为NULL。的确,对大多数程序来说,使用默认属性就够了,但我们还是有必要来了解一下线程的有关属性。
属性结构为pthread_attr_t,它同样在头文件/usr/include/pthread.h中定义,喜欢追根问底的人可以自己去查看。属 性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调 用。属性对象主要包括是否绑定、是否分离、堆栈地址、堆栈大小、优先级。默认的属性为非绑定、非分离、缺省1M的堆栈、与父进程同样级别的优先级。
关于线程的绑定,牵涉到另外一个概念:轻进程(LWP:Light Weight Process)。轻进程可以理解为内核线程,它位于用户层和系统层之间。系统对线程资源的分配、对线程的控制是通过轻进程来实现的,一个轻进程可以控制 一个或多个线程。默认状况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定的。绑定状况下,则顾名思义,即某个线程 固定的"绑"在一个轻进程之上。被绑定的线程具有较高的响应速度,这是因为CPU时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它总有一个 轻进程可用。通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求。
设置线程绑定状态的函数为pthread_attr_setscope,它有两个参数,第一个是指向属性结构的指针,第二个是绑定类型,它有两个取 值:PTHREAD_SCOPE_SYSTEM(绑定的)和PTHREAD_SCOPE_PROCESS(非绑定的)。下面的代码即创建了一个绑定的线 程。
#include <pthread.h>
pthread_attr_t attr;
pthread_t tid;
/*初始化属性值,均设为默认值*/
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
pthread_create(&tid, &attr, (void *) my_function, NULL);
线程的分离状态决定一个线程以什么样的方式来终止自己。在上面的例子中,我们采用了线程的默认属性,即为非分离状态,这种情况下,原有的线程等待创建的 线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线 程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。程序员应该根据自己的需要,选择适当的分离状态。设置线程分离状态的函数为 pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第二个参数可选为PTHREAD_CREATE_DETACHED(分离线程)和 PTHREAD _CREATE_JOINABLE(非分离线程)。这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在 pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的 线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用 pthread_cond_timewait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是 在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。
另外一个可能常用的属性是线程的优先级,它存放在结构sched_param中。用函数pthread_attr_getschedparam和函数 pthread_attr_setschedparam进行存放,一般说来,我们总是先取优先级,对取得的值修改后再存放回去。下面即是一段简单的例子。
#include <pthread.h>
#include <sched.h>
pthread_attr_t attr;
pthread_t tid;
sched_param param;
int newprio=20;
pthread_attr_init(&attr);
pthread_attr_getschedparam(&attr, ¶m);
param.sched_priority=newprio;
pthread_attr_setschedparam(&attr, ¶m);
pthread_create(&tid, &attr, (void *)myfunction, myarg);
user-level线程与kernel-level线程
实现线程有两个方式:uesr-level与kernel-level。
User-level线程是由
4 线程的数据处理
和进程相比,线程的最大优点之一是数据的共享性,各个进程共享父进程处沿袭的数据段,可以方便的获得、修改数据。但这也给多线程编程带来了许多问题。 我们必须当心有多个不同的进程访问相同的变量。许多函数是不可重入的,即同时不能运行一个函数的多个拷贝(除非使用不同的数据段)。在函数中声明的静态变 量常常带来问题,函数的返回值也会有问题。因为如果返回的是函数内部静态声明的空间的地址,则在一个线程调用该函数得到地址后使用该地址指向的数据时,别 的线程可能调用此函数并修改了这一段数据。在进程中共享的变量必须用关键字volatile来定义,这是为了防止编译器在优化时(如gcc中使用-OX参 数)改变它们的使用方式。为了保护变量,我们必须使用信号量、互斥等方法来保证我们对变量的正确使用。下面,我们就逐步介绍处理线程数据时的有关知识。
4.1 线程数据
在单线程的程序里,有两种基本的数据:全局变量和局部变量。但在多线程程序里,还有第三种数据类型:线程数据(TSD: Thread-Specific Data)。它和全局变量很象,在线程内部,各个函数可以象使用全局变量一样调用它,但它对线程外部的其它线程是不可见的。这种数据的必要性是显而易见 的。例如我们常见的变量errno,它返回标准的出错信息。它显然不能是一个局部变量,几乎每个函数都应该可以调用它;但它又不能是一个全局变量,否则在 A线程里输出的很可能是B线程的出错信息。要实现诸如此类的变量,我们就必须使用线程数据。我们为每个线程数据创建一个键,它和这个键相关联,在各个线程 里,都使用这个键来指代线程数据,但在不同的线程里,这个键代表的数据是不同的,在同一个线程里,它代表同样的数据内容。
和线程数据相关的函数主要有4个:创建一个键;为一个键指定线程数据;从一个键读取线程数据;删除键。
创建键的函数原型为:
extern int pthread_key_create __P ((pthread_key_t *__key,
void (*__destr_function) (void *)));
第一个参数为指向一个键值的指针,第二个参数指明了一个destructor函数,如果这个参数不为空,那么当每个线程结束时,系统将调用这个函数来 释放绑定在这个键上的内存块。这个函数常和函数pthread_once ((pthread_once_t*once_control, void (*initroutine) (void)))一起使用,为了让这个键只被创建一次。函数pthread_once声明一个初始化函数,第一次调用pthread_once时它执行这 个函数,以后的调用将被它忽略。
在下面的例子中,我们创建一个键,并将它和某个数据相关联。我们要定义一个函数createWindow,这个函数定义一个图形窗口(数据类型为Fl_Window *,这是图形界面开发工具FLTK中的数据类型)。由于各个线程都会调用这个函数,所以我们使用线程数据。
/* 声明一个键*/
pthread_key_t myWinKey;
/* 函数 createWindow */
void createWindow ( void ) {
Fl_Window * win;
static pthread_once_t once= PTHREAD_ONCE_INIT;
/* 调用函数createMyKey,创建键*/
pthread_once ( & once, createMyKey) ;
/*win指向一个新建立的窗口*/
win=new Fl_Window( 0, 0, 100, 100, "MyWindow");
/* 对此窗口作一些可能的设置工作,如大小、位置、名称等*/
setWindow(win);
/* 将窗口指针值绑定在键myWinKey上*/
pthread_setpecific ( myWinKey, win);
}
/* 函数 createMyKey,创建一个键,并指定了destructor */
void createMyKey ( void ) {
pthread_keycreate(&myWinKey, freeWinKey);
}
/* 函数 freeWinKey,释放空间*/
void freeWinKey ( Fl_Window * win){
delete win;
}
这样,在不同的线程中调用函数createMyWin,都可以得到在线程内部均可见的窗口变量,这个变量通过函数 pthread_getspecific得到。在上面的例子中,我们已经使用了函数pthread_setspecific来将线程数据和一个键绑定在一 起。这两个函数的原型如下:
extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer));
extern void *pthread_getspecific __P ((pthread_key_t __key));
这两个函数的参数意义和使用方法是显而易见的。要注意的是,用pthread_setspecific为一个键指定新的线程数据时,必须自己释放原有 的线程数据以回收空间。这个过程函数pthread_key_delete用来删除一个键,这个键占用的内存将被释放,但同样要注意的是,它只释放键占用 的内存,并不释放该键关联的线程数据所占用的内存资源,而且它也不会触发函数pthread_key_create中定义的destructor函数。线 程数据的释放必须在释放键之前完成。
4.2 互斥锁
互斥锁用来保证一段时间内只有一个线程在执行一段代码。必要性显而易见:假设各个线程向同一个文件顺序写入数据,最后得到的结果一定是灾难性的。
我们先看下面一段代码。这是一个读/写程序,它们公用一个缓冲区,并且我们假定一个缓冲区只能保存一条信息。即缓冲区只有两个状态:有信息或没有信息。
void reader_function ( void );
void writer_function ( void );
char buffer;
int buffer_has_item=0;
pthread_mutex_t mutex;
struct timespec delay;
void main ( void ){
pthread_t reader;
/* 定义延迟时间*/
delay.tv_sec = 2;
delay.tv_nec = 0;
/* 用默认属性初始化一个互斥锁对象*/
pthread_mutex_init (&mutex,NULL);
pthread_create(&reader, pthread_attr_default, (void *)&reader_function), NULL);
writer_function( );
}
void writer_function (void){
while(1){
/* 锁定互斥锁*/
pthread_mutex_lock (&mutex);
if (buffer_has_item==0){
buffer=make_new_item( );
buffer_has_item=1;
}
/* 打开互斥锁*/
pthread_mutex_unlock(&mutex);
pthread_delay_np(&delay);
}
}
void reader_function(void){
while(1){
pthread_mutex_lock(&mutex);
if(buffer_has_item==1){
consume_item(buffer);
buffer_has_item=0;
}
pthread_mutex_unlock(&mutex);
pthread_delay_np(&delay);
}
}
这里声明了互斥锁变量mutex,结构pthread_mutex_t为不公开的数据类型,其中包含一个系统分配的属性对象。函数 pthread_mutex_init用来生成一个互斥锁。NULL参数表明使用默认属性。如果需要声明特定属性的互斥锁,须调用函数 pthread_mutexattr_init。函数pthread_mutexattr_setpshared和函数 pthread_mutexattr_settype用来设置互斥锁属性。前一个函数设置属性pshared,它有两个取 值,PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED。前者用来不同进程中的线程同步,后者用于同步本进 程的不同线程。在上面的例子中,我们使用的是默认属性PTHREAD_PROCESS_ PRIVATE。后者用来设置互斥锁类型,可选的类型有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、 PTHREAD_MUTEX_RECURSIVE和PTHREAD _MUTEX_DEFAULT。它们分别定义了不同的上所、解锁机制,一般情况下,选用最后一个默认属性。
pthread_mutex_lock声明开始用互斥锁上锁,此后的代码直至调用pthread_mutex_unlock为止,均被上锁,即同一时 间只能被一个线程调用执行。当一个线程执行到pthread_mutex_lock处时,如果该锁此时被另一个线程使用,那此线程被阻塞,即程序将等待到 另一个线程释放此互斥锁。在上面的例子中,我们使用了pthread_delay_np函数,让线程睡眠一段时间,就是为了防止一个线程始终占据此函数。
上面的例子非常简单,就不再介绍了,需要提出的是在使用互斥锁的过程中很有可能会出现死锁:两个线程试图同时占用两个资源,并按不同的次序锁定相应的 互斥锁,例如两个线程都需要锁定互斥锁1和互斥锁2,a线程先锁定互斥锁1,b线程先锁定互斥锁2,这时就出现了死锁。此时我们可以使用函数 pthread_mutex_trylock,它是函数pthread_mutex_lock的非阻塞版本,当它发现死锁不可避免时,它会返回相应的信 息,程序员可以针对死锁做出相应的处理。另外不同的互斥锁类型对死锁的处理不一样,但最主要的还是要程序员自己在程序设计注意这一点。
4.3 条件变量
前一节中我们讲述了如何使用互斥锁来实现线程间数据的共享和通信,互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定。而 条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足 时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。 这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线承间的同步。
条件变量的结构为pthread_cond_t,函数pthread_cond_init()被用来初始化一个条件变量。它的原型为:
extern int pthread_cond_init __P ((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr));
其中cond是一个指向结构pthread_cond_t的指针,cond_attr是一个指向结构pthread_condattr_t的指针。结 构pthread_condattr_t是条件变量的属性结构,和互斥锁一样我们可以用它来设置条件变量是进程内可用还是进程间可用,默认值是 PTHREAD_ PROCESS_PRIVATE,即此条件变量被同一进程内的各个线程使用。注意初始化条件变量只有未被使用时才能重新初始化或被释放。释放一个条件变量 的函数为pthread_cond_ destroy(pthread_cond_t cond)。
函数pthread_cond_wait()使线程阻塞在一个条件变量上。它的函数原型为:
extern int pthread_cond_wait __P ((pthread_cond_t *__cond,
pthread_mutex_t *__mutex));
线程解开mutex指向的锁并被条件变量cond阻塞。线程可以被函数pthread_cond_signal和函数 pthread_cond_broadcast唤醒,但是要注意的是,条件变量只是起阻塞和唤醒线程的作用,具体的判断条件还需用户给出,例如一个变量是 否为0等等,这一点我们从后面的例子中可以看到。线程被唤醒后,它将重新检查判断条件是否满足,如果还不满足,一般说来线程应该仍阻塞在这里,被等待被下 一次唤醒。这个过程一般用while语句实现。
另一个用来阻塞线程的函数是pthread_cond_timedwait(),它的原型为:
extern int pthread_cond_timedwait __P ((pthread_cond_t *__cond,
pthread_mutex_t *__mutex, __const struct timespec *__abstime));
它比函数pthread_cond_wait()多了一个时间参数,经历abstime段时间后,即使条件变量不满足,阻塞也被解除。
函数pthread_cond_signal()的原型为:
extern int pthread_cond_signal __P ((pthread_cond_t *__cond));
它用来释放被阻塞在条件变量cond上的一个线程。多个线程阻塞在此条件变量上时,哪一个线程被唤醒是由线程的调度策略所决定的。要注意的是,必须用 保护条件变量的互斥锁来保护这个函数,否则条件满足信号又可能在测试条件和调用pthread_cond_wait函数之间被发出,从而造成无限制的等 待。下面是使用函数pthread_cond_wait()和函数pthread_cond_signal()的一个简单的例子。
pthread_mutex_t count_lock;
pthread_cond_t count_nonzero;
unsigned count;
decrement_count () {
pthread_mutex_lock (&count_lock);
while(count==0)
pthread_cond_wait( &count_nonzero, &count_lock);
count=count -1;
pthread_mutex_unlock (&count_lock);
}
increment_count(){
pthread_mutex_lock(&count_lock);
if(count==0)
pthread_cond_signal(&count_nonzero);
count=count+1;
pthread_mutex_unlock(&count_lock);
}
count值为0时,decrement 函数在pthread_cond_wait处被阻塞,并打开互斥锁count_lock。此时,当调用到函数increment_count 时,pthread_cond_signal()函数改变条件变量,告知decrement_count()停止阻塞。读者可以试着让两个线程分别运行这 两个函数,看看会出现什么样的结果。
函数pthread_cond_broadcast(pthread_cond_t *cond)用来唤醒所有被阻塞在条件变量cond上的线程。这些线程被唤醒后将再次竞争相应的互斥锁,所以必须小心使用这个函数。
4.4 信号量
信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。当公共资源增加时,调用函数sem_post()增加 信号量。只有当信号量值大于0时,才能使用公共资源,使用后,函数sem_wait()减少信号量。函数sem_trywait()和函数 pthread_ mutex_trylock()起同样的作用,它是函数sem_wait()的非阻塞版本。下面我们逐个介绍和信号量有关的一些函数,它们都在头文件 /usr/include/semaphore.h中定义。
信号量的数据类型为结构sem_t,它本质上是一个长整型的数。函数sem_init()用来初始化一个信号量。它的原型为:
extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));
sem为指向信号量结构的一个指针;pshared不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享;value给出了信号量的初始值。
函数sem_post( sem_t *sem )用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不在阻塞,选择机制同样是由线程的调度策略决定的。
函数sem_wait( sem_t *sem )被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。函数sem_trywait ( sem_t *sem )是函数sem_wait()的非阻塞版本,它直接将信号量sem的值减一。
函数sem_destroy(sem_t *sem)用来释放信号量sem。
下面我们来看一个使用信号量的例子。在这个例子中,一共有4个线程,其中两个线程负责从文件读取数据到公共的缓冲区,另两个线程从缓冲区读取数据作不同的处理(加和乘运算)。
/* File sem.c */
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#define MAXSTACK 100
int stack[MAXSTACK][2];
int size=0;
sem_t sem;
/* 从文件1.dat读取数据,每读一次,信号量加一*/
void ReadData1(void){
FILE *fp=fopen("1.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
sem_post(&sem);
++size;
}
fclose(fp);
}
/*从文件2.dat读取数据*/
void ReadData2(void){
FILE *fp=fopen("2.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
sem_post(&sem);
++size;
}
fclose(fp);
}
/*阻塞等待缓冲区有数据,读取数据后,释放空间,继续等待*/
void HandleData1(void){
while(1){
sem_wait(&sem);
printf("Plus:%d+%d=%d\n",stack[size][0],stack[size][1],
stack[size][0]+stack[size][1]);
--size;
}
}
void HandleData2(void){
while(1){
sem_wait(&sem);
printf("Multiply:%d*%d=%d\n",stack[size][0],stack[size][1],
stack[size][0]*stack[size][1]);
--size;
}
}
int main(void){
pthread_t t1,t2,t3,t4;
sem_init(&sem,0,0);
pthread_create(&t1,NULL,(void *)HandleData1,NULL);
pthread_create(&t2,NULL,(void *)HandleData2,NULL);
pthread_create(&t3,NULL,(void *)ReadData1,NULL);
pthread_create(&t4,NULL,(void *)ReadData2,NULL);
/* 防止程序过早退出,让它在此无限期等待*/
pthread_join(t1,NULL);
}
在Linux下,我们用命令gcc -lpthread sem.c -o sem生成可执行文件sem。 我们事先编辑好数据文件1.dat和2.dat,假设它们的内容分别为1 2 3 4 5 6 7 8 9 10和 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 ,我们运行sem,得到如下的结果:
Multiply:-1*-2=2
Plus:-1+-2=-3
Multiply:9*10=90
Plus:-9+-10=-19
Multiply:-7*-8=56
Plus:-5+-6=-11
Multiply:-3*-4=12
Plus:9+10=19
Plus:7+8=15
Plus:5+6=11
从中我们可以看出各个线程间的竞争关系。而数值并未按我们原先的顺序显示出来这是由于size这个数值被各个线程任意修改的缘故。这也往往是多线程编程要注意的问题。
5 小结
多线程编程是一个很有意思也很有用的技术,使用多线程技术的网络蚂蚁是目前最常用的下载工具之一,使用多线程技术的grep比单线程的grep要快上几倍,类似的例子还有很多。希望大家能用多线程技术写出高效实用的好程序来。
管道pipe
管道:当从一个进程连接数据流到另一个进程时,使用术语管道(pipe)。 |