Android Rild 概述


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, &param);
param.sched_priority=newprio;
pthread_attr_setschedparam(&attr, &param);
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)。

#i nclude <unistd.h>
int pipe(int filedes[2]); //創建管道

pipe()說明:
返回值:0成功,-1出錯。
如果調用成功,則進程此時由了兩個額外的打開文件描述符,filedes[0]中的值是管道的讀取端,而filedes[1]是管道的寫入端。

#include<unistd.h>
#include<sys/types.h>
#include<errno.h>
#include<stdio.h>
#include<stdlib.h>

int main(){
    int pipe_fd[2];
    pid_t pid;
    char buf_r[100];
    char *p_wbuf;
    int r_num;

    memset(buf_r,0,sizeof(buf_r));

    //創建管道
    if(pipe(pipe_fd)<0){
        printf("pipe create error\n");
        return -1;
    }

    if((pid=fork())==0){//表示在子進程中
        printf("\n");
        //關閉管道寫描述符,進行管道讀操作
        close(pipe_fd[1]);
        sleep(2);
        //管道描述符中讀取
        if((r_num=read(pipe_fd[0],buf_r,100))>0){
            printf("%d numbers read from the pipe is %s\n",r_num,buf_r);
        }
        close(pipe_fd[0]);
        exit(0);
    }
    else if(pid>0){//表示在父進程中,父進程寫
    //關閉管道讀描述符,進行管道寫操作
        close(pipe_fd[0]);
        if(write(pipe_fd[1],"Hello",5)!=-1)
            printf("parent write1 success!\n");
        if(write(pipe_fd[1],"Pipe",5)!=1)
            printf("parent write2 success!\n");
        close(pipe_fd[1]);
        sleep(3);
        waitpid(pid,NULL,0);
        exit(0);
    }
}
   
管道讀寫注意事項:
1.必須在系統調用fork()中調用pipe(),否則子進程將不會繼承文件描述符;
2.當使用半雙工管道時,任何關聯的進程都必須共享一個相關的祖先進程。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM