[深入理解Android卷一全文-第九章]深入理解Vold和Rild
由於《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知識的傳播不應該因為紙質媒介的問題而中斷,所以我將在CSDN部落格中全文轉發這兩本書的全部內容。
第9章 深入理解Vold和Rild
本章主要內容
· 介紹Vold。
· 介紹Rild。
本章涉及的原始碼檔名稱及位置
下面是本章分析的原始碼檔名及其位置。
· Main.cpp
system/vold/Main.cpp
· NetlinkManager.cpp
system/vold/NetlinkManager.cpp
· NetlinkManager.h
system/vold/NetlinkManager.h
· NetlinkHandler.cpp
system/vold/NetlinkHandler.cpp
· NetlinkListener.cpp
system/core/libsysutils/src/NetlinkListener.cpp
· SocketListener.cpp
system/core/libsysutils/src/SocketListener.cpp
· VolumeManager.cpp
system/vold/VolumeManager.cpp
· DirectVolume.cpp
system/vold/DirectVolume.cpp
· FrameworkListener.cpp
system/core/libsysutils/src/FrameworkListener.cpp
· MountService.java
framework/base/services/java/com/android/server/MountService.java
· Rild.c
hardware/ril/rild/Rild.c
· Ril.cpp
hardware/ril/libril/Ril.cpp
· Ril_event.h
hardware/ril/lilbril/Ril_event.h
· Reference_ril.c
hardware/ril/reference_ril/Reference_ril.c
· Atchannle.c
hardware/ril/reference_ril/Atchannle.c
· Ril.h
hardware/ril/include/telephony/Ril.h
· PhoneApp.java
package/apps/Phone/src/com/android/phone/PhoneApp.java
· PhoneFactory.java
framework/base/telephony/java/com/android/internal/telephony/PhoneFactory.java
· RIL.java
framework/base/telephony/java/com/android/internal/telephony/RIL.java
· PhoneUtils.java
package/apps/Phone/src/com/android/phone/PhoneUtils.java
9.1 概述
本章將分析Android系統中兩個比較重要的程式,它們分別是:
· Vold:Volume Daemon,用於管理和控制Android平臺外部儲存裝置的後臺程式,這些管理和控制,包括SD卡的插拔事件檢測、SD卡掛載、解除安裝、格式化等。
· Rild:Radio Interface Layer Daemon,用於智慧手機的通訊管理和控制的後臺程式,所有和手機通訊相關的功能,例如接打電話、收發簡訊/彩信、GPRS等都需要Rild的參與。
Vold和Rild都是Native的程式,另外Java世界還有和它們互動的模組,它們分別是:
· MountService和Vold互動,一方面它可以接收來自Vold的訊息,例如,在應用程式中經常監聽到的ACTION_MEDIA_MOUNTED/ACTION_MEDIA_EJECT等廣播,就是由MountService根據Vold的資訊而觸發的。另一方面,它可以向Vold傳送控制命令,例如掛載SD卡為磁碟驅動器的操作,就是由MountService傳送命令給Vold來執行的。
· Phone和Rild互動,它是一個比較複雜的應用程式。簡單來說,Phone撥打電話時需要傳送對應的命令給Rild來執行。後面在Rild的例項分析中會做相關介紹。
這兩個Daemon程式碼的結構都不算太複雜。本章將和大家一起來領略一下它們的風采。
9.2 Vold的原理與機制分析
Vold是Volume Daemon的縮寫,它是Android平臺中外部儲存系統的管控中心,是一個比較重要的程式。雖然它的地位很重要,但其程式碼結構卻遠沒有前面的Audio和Surface系統複雜。欣賞完Audio和Surface的大氣磅礴後,再來感受一下Vold的小巧玲瓏也會別有一番情趣。Vold的架構可用圖9-1來表示:
圖9-1 Vold架構圖
從上圖中可知:
· Vold中的NetlinkManager模組(簡稱NM)接收來自Linux核心的uevent訊息。例如SD卡的插拔等動作都會引起Kernel向NM傳送uevent訊息。
· NM將這些訊息轉發給VolumeManager模組(簡稱VM)。VM會對應做一些操作,然後把相關資訊通過CommandListener(簡稱CL)傳送給MountService,MountService根據收到的訊息會傳送相關的處理命令給VM做進一步的處理。例如待SD卡插入後,VM會將來自NM的“Disk Insert”訊息傳送給MountService,而後MountService則傳送“Mount”指令給Vold,指示它掛載這個SD卡。
· CL模組內部封裝了一個Socket用於跨程式通訊。它在Vold程式中屬於監聽端(即是服務端),而它的連線端(即客戶端)則是MountService。它一方面接收來自MountService的控制命令(例如解除安裝儲存卡、格式化儲存卡等),另一方面VM和NM模組又會通過它,將一些資訊傳送給MountService。
相比於Audio和Surface系統,Vold的架構確實比較簡單,並且Vold和MountService所在的程式(這個程式其實就是system_server)在進行程式間通訊時,也沒有利用Binder機制,而是直接使用了Socket,這樣,在程式碼量和程式中類的派生關係上也會簡單不少。
9.2.1 Netlink和Uevent的介紹
在分析Vold的程式碼前,先介紹一下Linux系統中的Netlink和Uevent。
1. Netlink的介紹
Netlink是Linux系統中一種使用者空間程式和Kernel進行通訊的機制,通過這個機制,位於使用者空間的程式,可接收來自Kernel的一些資訊(例如Vold中用到的USB或SD的插拔訊息),同時應用層也可通過Netlink向Kernel傳送一些控制命令。
目前,Linux系統並沒有為Netlink單獨設計一套系統呼叫,而是複用了Socket的操作介面,只在建立Socket時會有一些特殊的地方。Netlink的具體使用方法,在進行程式碼分析時再來了解,讀者目前只需知道,通過Netlink機制應用層,可接收來自Kernel的訊息即可。
2. Uevent介紹
Uevent和Linux的Udev裝置檔案系統和裝置模型有關係,它實際上就是一串字串,字串的內容可告知發生了什麼事情。下面通過一個例項來直觀感受Uevent:
在SD卡插入手機後(我們這裡以SD卡為例),系統會檢測到這個裝置的插入,然後核心會通過Netlink傳送一個訊息給Vold,Vold將根據接收到的訊息進行處理,例如掛載這個SD卡。核心傳送的這個訊息,就是Uevent,其中U代表User space(應用層空間)。下面看SD卡插入時Vold截獲到的Uevent訊息。在我的G7手機上,Uevent的內容如下,注意,其中//號或/**/號中的內容是為方便讀者理解而加的註釋:
[-->SD卡插入的Uevent訊息]
//mmc表示MultiMedia Card,這裡統稱為SD卡
add@/devices/platform/msm_sdcc.2/mmc_host/mmc1/mmc1:c9f2/block/mmcblk0
ACTION=add //add表示裝置插入,另外還有remove和change等動作
//DEVPATH表示該裝置位於/sys目錄中的裝置路徑
DEVPATH=/devices/platform/msm_sdcc.2/mmc_host/mmc1/mmc1:c9f2/block/mmcblk0
/*
SUBSYSTEM表示該裝置屬於哪一類裝置,block為塊裝置,磁碟也屬於這一類裝置,另外還有
character(字元)裝置等型別。
*/
SUBSYSTEM=block
MAJOR=179//MAJOR和MINOR分別表示該裝置的主次裝置號,二者聯合起來可以標識一個裝置
MINOR=0
DEVNAME=mmcblk0
DEVTYPE=disk//裝置Type為disk
NPARTS=3 //這個表示該SD卡上的分割槽,我的SD卡上有三塊分割槽
SEQNUM=1357//序號
由於我的SD卡上還有分割槽,所以還會接收到和分割槽相關的Uevent。簡單看一下:
[-->SD卡插入後和分割槽相關的Uevent訊息]
add@/devices/platform/msm_sdcc.2/mmc_host/mmc1/mmc1:c9f2/block/mmcblk0/mmcblk0p1
ACTION=add
//比上面那個DEVPATH多了一個mmcblk0p1
DEVPATH=/devices/platform/msm_sdcc.2/mmc_host/mmc1/mmc1:c9f2/block/mmcblk0/mmcblk0p1
SUBSYSTEM=block
MAJOR=179
MINOR=1
DEVNAME=mmcblk0p1
DEVTYPE=partition //裝置型別變為partition,表示分割槽
PARTN=1
SEQNUM=1358
通過上面例項,我們和Uevent來了一次親密接觸,具體到Vold,也就是核心通過Uevent告知外部儲存系統發生了哪些事情,那麼Uevent在什麼情況下會由Kernel發出呢?
· 當裝置發生變化時,這會引起Kernel傳送Uevent訊息,例如裝置的插入和拔出等。如果Vold在裝置發生變化之前已經建立了Netlink IPC通訊,那麼Vold可以接收到這些Uevent訊息。這種情況是由裝置發生變化而觸發的。
· 裝置一般在/sys對應的目錄下有一個叫uevent的檔案,往該檔案中寫入指定的資料,也會觸Kernel傳送和該裝置相關的Uevent訊息,這是由應用層觸發的。例如Vold啟動時,會往這些uevent檔案中寫資料,通過這種方式促使核心傳送Uevent訊息,這樣Vold就能得到這些裝置的當前資訊了。
根據上面介紹可知,Netlink和Uevent的目的,就是讓Vold隨時獲悉外部儲存系統的資訊,這至關重要。我們總不會希望發生諸如SD卡都被拔了,而Vold卻一無所知的情況吧?
9.2.2 初識Vold
下面來認識一下Vold,它的程式碼在main.cpp中,如下所示:
[-->Main.cpp]
int main() {
VolumeManager *vm;
CommandListener *cl;
NetlinkManager *nm;
SLOGI("Vold 2.1 (the revenge) firing up");
//建立資料夾/dev/block/vold
mkdir("/dev/block/vold", 0755);
//①建立VolumeManager物件
if(!(vm = VolumeManager::Instance())) {
SLOGE("Unable to create VolumeManager");
exit(1);
};
//②建立NetlinkManager物件
if(!(nm = NetlinkManager::Instance())) {
SLOGE("Unable to create NetlinkManager");
exit(1);
};
//③建立CommandListener物件
cl = new CommandListener();
vm->setBroadcaster((SocketListener *) cl);
nm->setBroadcaster((SocketListener *) cl);
//④啟動VM
if(vm->start()) {
......
exit(1);
}
//⑤根據配置檔案來初始化VM
if(process_config(vm)) {
......
}
//⑥啟動NM
if(nm->start()) {
......
exit(1);
}
//通過往/sys/block目錄下對應的uevent檔案寫”add\n”來觸發核心傳送Uevent訊息
coldboot("/sys/block");
{
FILE *fp;
char state[255];
/*
Android支援將手機上的外部儲存裝置作為磁碟掛載到電腦上。下面的程式碼可檢視是否開啟了
磁碟掛載功能。這裡涉及UMS(Usb Mass Storage,USB大容量儲存)方面的知識。
*/
if((fp = fopen("/sys/devices/virtual/switch/usb_mass_storage/state",
"r"))) {
if (fgets(state, sizeof(state), fp)) {
if (!strncmp(state, "online", 6)) {
//⑧VM通過CL向感興趣的模組(如MountService)通知UMS的狀態
vm->notifyUmsConnected(true);
} else {
vm->notifyUmsConnected(false);
}
}
......
fclose(fp);
}
......
}
......
//⑨啟動CL
if(cl->startListener()) {
......
exit(1);
}
//無限迴圈
while(1) {
sleep(1000);
}
SLOGI("Vold exiting");
exit(0);
}
上面程式碼中列出了九個關鍵點。由於Vold將其功能合理分配到了各個模組中,所以這九個關鍵點將放到圖9-1所示Vold的三個模組中去討論。
下面,看第一個模組NetlinkManager,簡稱NM。
9.2.3 NetlinkManager模組的分析
在Vold程式碼中,使用NM模組的流程是:
· 呼叫Instance建立一個NM物件。
· 呼叫setBroadcaster設定CL物件。
· 呼叫start啟動NM。
接下來,按這三個步驟來分析NM模組。
1. 建立NM
Vold呼叫Instance函式建立了一個NM物件。看到Instance這個函式,讀者應能想到,這裡可能是採用了單例模式。來看是否如此,程式碼如下所示。
[-->NetlinkManager.cpp]
NetlinkManager *NetlinkManager::Instance() {
if(!sInstance)
sInstance = new NetlinkManager();//果然是單例模式
returnsInstance;
}
NM的建立真是非常簡單。再看第二個被呼叫的函式setBroadcaster。
2. setBroadcaster的分析
setBroadcaster就更簡單了,它的實現在NetlinkManger類的宣告中,如下所示:
[-->NetlinkManager.h]
void setBroadcaster(SocketListener *sl) {mBroadcaster = sl; }
setBroadcaster引數中的那個sl其實際型別為CommandListener。需要說明的是,雖然NM設定了CL物件,但Vold的NM並沒有通過CL傳送訊息和接收命令,所以在圖9-1中,NM模組和CL模組並沒有連線線,這一點務請注意。
下面看最後一個函式start。
3. start的分析
前面說過,NM模組將使用Netlink和Kernel進行IPC通訊,那麼它是怎麼做到的呢?來看程式碼,如下所示:
[-->NetlinkManager.cpp]
int NetlinkManager::start() {
//PF_NETLINK使用的socket地址結構是sockaddr_nl,而不是一般的sockaddr_in
structsockaddr_nl nladdr;
int sz= 64 * 1024;
memset(&nladdr, 0, sizeof(nladdr));
nladdr.nl_family = AF_NETLINK;
nladdr.nl_pid = getpid(); //設定自己的程式pid
nladdr.nl_groups = 0xffffffff;
/*
建立PF_NETLINK地址簇的socket,目前只支援SOCK_DGRAM型別,第三個引數
NETLINK_KOBJECT_UEVENT表示要接收核心的Uevent事件。
*/
if((mSock = socket(PF_NETLINK,
SOCK_DGRAM,NETLINK_KOBJECT_UEVENT)) < 0) {
......
return -1;
}
//設定Socket接收緩衝區大小
if(setsockopt(mSock, SOL_SOCKET, SO_RCVBUFFORCE, &sz, sizeof(sz)) < 0) {
......
return -1;
}
//必須對該socket執行bind操作
if(bind(mSock, (struct sockaddr *) &nladdr, sizeof(nladdr)) < 0) {
......
return -1;
}
//建立一個NetlinkHandler物件,並把建立好的Socket控制程式碼傳給它。
mHandler = new NetlinkHandler(mSock);
//呼叫NetlinkHandler物件的start
if(mHandler->start()) {
SLOGE("Unable to start NetlinkHandler: %s", strerror(errno));
return -1;
}
return0;
}
從程式碼上看,NM的start函式分為兩個步驟:
· 建立地址簇為PF_NETLINK型別的socket並做一些設定,這樣NM就能和Kernel通訊了。關於Netlink的使用技巧網上有很多資料,讀者可在Linux系統上通過man netlink命令來查詢相關資訊。
· 建立NetlinkHandler物件,並呼叫它的start。看來,後續工作都是由NetlinkHandler來完成的。
據上文分析可看出,NetlinkHandler才是真正的主角,下面就來分析它。為書寫方便起見,NetlinkHandler簡稱為NLH。
4. NetlinkHandler的分析
(1)建立NLH
程式碼結構簡單的Vold程式中,NetlinkHandler卻有一個相對不簡單的派生關係,如圖9-2所示:
圖9-2 NLH的派生關係圖
直接看程式碼,來認識這個NLH:
[-->NetlinkHandler.cpp]
NetlinkHandler::NetlinkHandler(int listenerSocket):
NetlinkListener(listenerSocket) {
//呼叫基類NetlinkListener的建構函式。注意傳入的引數是和Kernel通訊的socket
//控制程式碼。注意,檔案描述符和控制程式碼表示的是同一個東西,這裡不再區分二者。
}
再看基類NetlinkListener的建構函式:
[-->NetlinkListener.cpp]
NetlinkListener::NetlinkListener(int socket) :
SocketListener(socket, false) {
//呼叫基類SocketListener的建構函式,第二個引數為false。
}
基類SocketListener的建構函式是:
[-->SocketListener.cpp]
SocketListener::SocketListener(int socketFd,bool listen) {
mListen = listen; //這個引數是false
mSocketName = NULL;
mSock = socketFd;//儲存和Kernel通訊的socket描述符
//初始化一個mutex,看來會有多個執行緒存在
pthread_mutex_init(&mClientsLock, NULL);
/*
SocketClientCollection的宣告如下,它是一個列表容器。
typedef android::List<SocketClient *>SocketClientCollection
其中,SocketClient代表和Socket服務端通訊的客戶端。
*/
mClients = new SocketClientCollection();
}
NLH的建立分析完了。此過程中沒有什麼新鮮內容。下面看它的start函式。
本章內容會大量涉及Socket,所以讀者應先了解與Socket有關的知識,如果需要深入研究,建議閱讀《Unix NetworkingProgramming Volume I》①一書。
(2)start的分析
在分析前面的程式碼時,曾看到NetlinkHandler會建立一個同步互斥物件,這表明NLH會在多執行緒環境中使用,那麼這個執行緒會在哪裡建立呢?來看start的程式碼,如下所示:
[-->NetlinkHandler.cpp]
int NetlinkHandler::start() {
returnthis->startListener();//startListener由SocketListener實現。
}
[-->SocketListener.cpp]
int SocketListener::startListener() {
if(!mSocketName && mSock == -1) {
errno = EINVAL;
return -1;
} elseif (mSocketName) {
if((mSock = android_get_control_socket(mSocketName)) < 0) {
return -1;
}
}
/*
還記得構造NLH時的引數嘛?mListen為false,這表明NLH不是監聽端(listen)。
這裡為了程式碼和操作的統一,用mSock做引數構造了一個SocketClient物件,
並加入到mClients列表中,但這個SocketClient並不是真實客戶端的代表。
*/
if(mListen && listen(mSock, 4) < 0) {
......
return -1;
} else if (!mListen)//以mSock為引數構造SocketClient物件,並加入到對應列表中
mClients->push_back(new SocketClient(mSock));
/*
pipe系統呼叫將建立一個匿名管道,mCtrlPipe是一個int型別的二元陣列。
其中mCtrlPipe[0]用於從管道讀資料,mCtrlPipe[1]用於往管道寫資料
*/
if(pipe(mCtrlPipe)) {
......
return -1;
}
//建立一個工作執行緒,執行緒函式是threadStart。
if(pthread_create(&mThread, NULL, SocketListener::threadStart, this)) {
......
return -1;
}
return0;
}
如果熟悉Socket程式設計,理解上面的程式碼就非常容易了。下面來看NLH的工作執行緒。
(3)工作執行緒的分析
工作執行緒的執行緒函式threadStart的程式碼如下所示:
[-->SocketListener.cpp]
void *SocketListener::threadStart(void *obj) {
SocketListener *me = reinterpret_cast<SocketListener *>(obj);
me->runListener();//呼叫runListener。
pthread_exit(NULL);
returnNULL;
}
//直接分析runListener
void SocketListener::runListener() {
while(1) {
SocketClientCollection::iterator it;
fd_set read_fds;
int rc = 0;
int max = 0;
FD_ZERO(&read_fds);
if(mListen) {//mListen為false,所以不走這個if分支
max = mSock;
FD_SET(mSock, &read_fds);
}
/*
計算max,為什麼要有這個操作?這是由select函式決定的,它的第一個引數的取值
必須為它所監視的檔案描述符集合中最大的檔案描述符加1。
*/
FD_SET(mCtrlPipe[0], &read_fds);
if(mCtrlPipe[0] > max)
max = mCtrlPipe[0];
//還是計算fd值最大的那個
pthread_mutex_lock(&mClientsLock);
for (it = mClients->begin(); it != mClients->end(); ++it) {
FD_SET((*it)->getSocket(), &read_fds);
if ((*it)->getSocket() > max)
max = (*it)->getSocket();
}
pthread_mutex_unlock(&mClientsLock);
/*
注意select函式的第一個引數,為max+1。讀者可以通過man select來查詢
select的用法,注意,在Windows平臺上的select對第一個引數沒有要求。
*/
if((rc = select(max + 1, &read_fds, NULL, NULL, NULL)) < 0) {
sleep(1);
continue;
}else if (!rc)
continue;
//如果管道可讀的話,表示需要退出工作執行緒。
if(FD_ISSET(mCtrlPipe[0], &read_fds))
break;
if(mListen && FD_ISSET(mSock, &read_fds)) {
//如果是listen端的話,mSock可讀表示有客戶端connect上
struct sockaddr addr;
socklen_t alen = sizeof(addr);
int c;
//呼叫accept接受客戶端的連線,返回用於和客戶端通訊的Socket描述符
if ((c = accept(mSock, &addr, &alen)) < 0) {
SLOGE("accept failed (%s)", strerror(errno));
sleep(1);
continue;
}
pthread_mutex_lock(&mClientsLock);
//根據返回的客戶端Socket描述符構造一個SocketClient物件,並加入到對應list
mClients->push_back(new SocketClient(c));
pthread_mutex_unlock(&mClientsLock);
}
do{
pthread_mutex_lock(&mClientsLock);
for (it = mClients->begin(); it !=mClients->end(); ++it) {
int fd = (*it)->getSocket();
if (FD_ISSET(fd, &read_fds)) {
pthread_mutex_unlock(&mClientsLock);
/*
有資料通過Socket傳送過來,所以呼叫onDataAvailable進行處理。
如果在onDataAvailable返回false,表示需要關閉該連線。
*/
if (!onDataAvailable(*it)){
close(fd);
pthread_mutex_lock(&mClientsLock);
delete *it;
it =mClients->erase(it);
pthread_mutex_unlock(&mClientsLock);
}
FD_CLR(fd, &read_fds);
continue;
}
}
pthread_mutex_unlock(&mClientsLock);
}while (0);
}
}
從程式碼中可看到:
· 工作執行緒退出的條件是匿名管道可讀,但在一般情況下不需要它退出,所以可以忽略此項內容。
· 不論是服務端還是客戶端,收到資料後都會呼叫onDataAvailable進行處理。
下面就來看NLH的資料處理。
(4)資料處理
根據前面的分析,收到資料後首先呼叫onDataAvailable函式進行處理,這個函式由NLH的基類NetlinkListener實現。程式碼如下所示:
[-->NetlinkListener]
bool NetlinkListener::onDataAvailable(SocketClient*cli)
{
intsocket = cli->getSocket();
intcount;
/*
呼叫recev接收資料,如果接收錯誤,則返回false,這樣這個socket在
上面的工作執行緒中就會被close。
*/
if((count = recv(socket, mBuffer, sizeof(mBuffer), 0)) < 0) {
SLOGE("recv failed (%s)", strerror(errno));
return false;
}
//new一個NetlinkEvent,並呼叫decode來解析接收到的Uevent資料
NetlinkEvent *evt = new NetlinkEvent();
if(!evt->decode(mBuffer, count)) {
goto out;
}
//呼叫onEvent,並傳遞NetlinkEvent物件。
onEvent(evt);
out:
deleteevt;
return true;
decode函式就是將收到的Uevent資訊填充到一個NetlinkEvent物件中,例如Action是什麼,SUBSYSTEM是什麼等,以後處理Uevent時就不用再解析字串了。
看onEvent函式,此函式是由NLH自己實現的,程式碼如下所示:
[-->NetlinkHandler.cpp]
void NetlinkHandler::onEvent(NetlinkEvent *evt){
VolumeManager *vm = VolumeManager::Instance();
constchar *subsys = evt->getSubsystem();
if(!subsys) {
return;
}
if (!strcmp(subsys, "block")) {
vm->handleBlockEvent(evt); //呼叫VM的handleBlockEvent
} elseif (!strcmp(subsys, "switch")) {
vm->handleSwitchEvent(evt);//呼叫VM的handleSwitchEvent
} else if (!strcmp(subsys, "battery")){
//這兩個事件和外部儲存系統沒有關係,所以不處理
} elseif (!strcmp(subsys, "power_supply")) {
}
}
NLH的工作已介紹完,下面總結一下NM模組的工作。
5. NM模組的總結
NM模組的功能就是從Kernel接收Uevent訊息,然後轉換成一個NetlinkEvent物件,最後會呼叫VM的處理函式來處理這個NetlinkEvent物件。
9.2.4 VolumeManager模組的分析
Vold使用VM模組的流程是:
· 呼叫Instance建立一個VM物件。
· 呼叫setBroadcaster設定CL物件,這個函式和NM的setBroadcaster一樣,所以本節不再介紹它。
· 呼叫start啟動VM。
· 呼叫process_config配置VM。
現在來看除setBroadcaster之外的三個函式。
1. 建立VM和start的分析
VM的建立及start函式都非常簡單,程式碼如下所示。
[-->VolumeManager.cpp]
VolumeManager *VolumeManager::Instance() {
if(!sInstance)
sInstance = new VolumeManager();
returnsInstance;
}
可以看到,VM也採用了單例的模式,所以全程式只會存在一個VM物件。
下面看VM的start:
[-->VolumeManager.cpp]
int VolumeManager::start() {
return 0;
}
start很簡單,沒有任何操作。
2. process_config的分析
process_config函式會根據配置檔案配置VM物件,其程式碼如下所示:
[-->Main.cpp]
static int process_config(VolumeManager *vm) {
FILE*fp;
int n= 0;
charline[255];
//讀取/etc/vold.fstab檔案
if(!(fp = fopen("/etc/vold.fstab", "r"))) {
return -1;
}
while(fgets(line, sizeof(line), fp)) {
char *next = line;
char *type, *label, *mount_point;
n++;
line[strlen(line)-1] = '\0';
if(line[0] == '#' || line[0] == '\0')
continue;
if(!(type = strsep(&next, " \t"))) {
goto out_syntax;
}
if(!(label = strsep(&next, " \t"))) {
goto out_syntax;
}
if(!(mount_point = strsep(&next, " \t"))) {
goto out_syntax;
}
if(!strcmp(type, "dev_mount")) {
DirectVolume *dv = NULL;
char *part, *sysfs_path;
if (!(part = strsep(&next, " \t"))) {
......
goto out_syntax;
}
if (strcmp(part, "auto") && atoi(part) == 0) {
goto out_syntax;
}
if (!strcmp(part, "auto")) {
//①構造一個DirectVolume物件
dv = new DirectVolume(vm, label, mount_point, -1);
} else {
dv = new DirectVolume(vm, label, mount_point, atoi(part));
}
while((sysfs_path = strsep(&next, " \t"))) {
//②新增裝置路徑
if (dv->addPath(sysfs_path)) {
......
goto out_fail;
}
}
//為VolumeManager物件增加一個DirectVolume物件
vm->addVolume(dv);
}
......
}
......
return-1;
}
從上面程式碼中發現,process_config的主要功能就是解析/etc/vold.fstab。這個檔案的作用和Linux系統中的fstab檔案很類似,就是設定一些儲存裝置的掛載點,我的HTC G7手機上這個檔案的內容如圖9-3所示:
圖9-3 我的手機上vold.fstab內容
從上圖的紅框中可知:
· sdcard為volume的名字。
· /mnt/sdcard表示mount的位置。
· 1表示使用儲存卡上的第一個分割槽,auto表示沒有分割槽。現在有很多定製的ROM要求SD卡上存在多個分割槽。
· /devices/xxxx等內容表示MMC裝置在sysfs中的位置。
根據G7的vold.fstab檔案,可以構造一個DirectVolume物件。
注意,根據手機刷的ROM的不同,vold.fstab檔案會有較大差異。
3. DirectVolume的分析
DirectVolume從Volume類派生,可把它看成是一個外部儲存卡(例如一張SD卡)在程式碼中的代表物。它封裝了對外部儲存卡的操作,例如載入/解除安裝儲存卡、格式化儲存卡等。
下面是process_config函式中和DirectVolume相關的地方:
· 一個是建立DirectVolume。
· 另一個是呼叫DirectVolume的addpath函式。
它們的程式碼如下所示:
[-->DirectVolume.cpp]
DirectVolume::DirectVolume(VolumeManager *vm,const char *label,
const char*mount_point, int partIdx) :
Volume(vm, label, mount_point) {//初始化基類
/*
注意其中的引數:
label為”sdcard”,mount_point為”/mnt/sdcard”,partIdx為1
*/
mPartIdx = partIdx;
//PathCollection定義為typedef android::List<char *> PathCollection
//其實就是一個字串list
mPaths= new PathCollection();
for(int i = 0; i < MAX_PARTITIONS; i++)
mPartMinors[i] = -1;
mPendingPartMap = 0;
mDiskMajor = -1; //儲存裝置的主裝置號
mDiskMinor = -1; //儲存裝置的次裝置號,一個儲存裝置將由主次兩個裝置號標識。
mDiskNumParts = 0;
//設定狀態為NoMedia
setState(Volume::State_NoMedia);
}
//再來看addPath函式,它主要目的是新增裝置在sysfs中的路徑,G7的vold.fstab上有兩個路
//徑,見圖9-3中的最後一行。
int DirectVolume::addPath(const char *path) {
mPaths->push_back(strdup(path));
return0;
}
這裡簡單介紹一下addPath的作用。addPath把和某個儲存卡介面相關的裝置路徑與這個DirectVolume繫結到一起,並且這個裝置路徑和Uevent中的DEVPATH是對應的,這樣就可以根據Uevent的DEVPATH找到是哪個儲存卡的DirectVolume發生了變動。當然手機上目前只有一個儲存卡介面,所以Vold也只有一個DirectVolume。
4. NM和VM互動
在分析NM模組的資料處理時發現,NM模組接收到Uevent事件後,會呼叫VM模組進行處理,下面來看這塊的內容。
先回顧一下NM呼叫VM模組的地方,程式碼如下所示:
[-->NetlinkHandler.cpp]
void NetlinkHandler::onEvent(NetlinkEvent *evt){
VolumeManager *vm = VolumeManager::Instance();
constchar *subsys = evt->getSubsystem();
......
if (!strcmp(subsys, "block")) {
vm->handleBlockEvent(evt); //呼叫VM的handleBlockEvent
} elseif (!strcmp(subsys, "switch")) {
vm->handleSwitchEvent(evt);//呼叫VM的handleSwitchEvent
}
......
}
在上面程式碼中,如果Uevent是block子系統,則呼叫handleBlockEvent;如果是switch,則呼叫handleSwitchEvent。handleSwitchEvent主要處理SD卡掛載磁碟的通知,比較簡單。這裡只分析handleBlockEvent事件。
[-->VolumeManager.cpp]
void VolumeManager::handleBlockEvent(NetlinkEvent*evt) {
constchar *devpath = evt->findParam("DEVPATH");
/*
前面在process_config中構造的DirectVolume物件儲存在了mVolumes中,它的定義如下:
typedef android::List<Volume *>VolumeCollection,也是一個列表。
注意它儲存的是Volume指標,而我們的DirectVolume是從Volume派生的
*/
VolumeCollection::iterator it;
boolhit = false;
for (it = mVolumes->begin(); it !=mVolumes->end(); ++it) {
//呼叫每個Volume的handleBlockEvent事件,就我的G7手機而言,實際上將呼叫
//DirectVolume的handleBlockEvent函式。
if(!(*it)->handleBlockEvent(evt)) {
hit = true;
break;
}
}
}
NM收到Uevent訊息後,DirectVolume也將應聲而動,它的handleBlockEvent的處理是:
[-->DirectVolume.cpp]
int DirectVolume::handleBlockEvent(NetlinkEvent*evt) {
constchar *dp = evt->findParam("DEVPATH");
PathCollection::iterator it;
//將Uevent的DEVPATH和addPath新增的路徑進行對比,判斷屬不屬於自己管理的範圍。
for(it = mPaths->begin(); it != mPaths->end(); ++it) {
if(!strncmp(dp, *it, strlen(*it))) {
int action = evt->getAction();
const char *devtype = evt->findParam("DEVTYPE");
if (action == NetlinkEvent::NlActionAdd) {
int major = atoi(evt->findParam("MAJOR"));
int minor = atoi(evt->findParam("MINOR"));
char nodepath[255];
snprintf(nodepath,
sizeof(nodepath),"/dev/block/vold/%d:%d",
major, minor);
//建立裝置節點
if (createDeviceNode(nodepath, major, minor)) {
......
}
if (!strcmp(devtype, "disk")) {
handleDiskAdded(dp, evt);//新增一個磁碟
} else {
/*
對於有分割槽的SD卡,先收到上面的“disk”訊息,然後每個分割槽就會收到
一個分割槽新增訊息。
*/
handlePartitionAdded(dp,evt);
}
} else if (action == NetlinkEvent::NlActionRemove) {
......
} else if (action == NetlinkEvent::NlActionChange) {
......
}
......
return 0;
}
}
errno= ENODEV;
return-1;
}
關於DirectVolume針對不同Uevent的具體處理方式,後面將通過一個SD卡插入案例來分析。
5. VM模組的總結
從前面的程式碼分析中可知,VM模組的主要功能是管理Android系統中的外部儲存裝置。圖9-4描述了VM模組的功能:
圖9-4 VM模組的職責
通過對上圖和前面程式碼的分析可知:
· SD卡的變動(例如熱插拔)將導致Kernel傳送Uevent訊息給NM模組。
· NM模組呼叫VM模組處理這些Uevent訊息。
· VM模組遍歷它所持有的Volume物件,Volume物件根據addPath新增的DEVPATH和Uevent訊息中的DEVPATH來判斷,自己是否可以處理這個訊息。
至於Volume到底如何處理Uevent訊息,將通過一個例項來分析。
9.2.5 關於 CommandListener模組的分析
Vold使用CL模組的流程是:
· 使用new建立一個CommandListener物件;
· 呼叫CL的startListener函式。
來看這兩個函式。
1. 建立CommandListener的分析
和NetlinkerHandler一樣,CommandListener也有一個相對不簡單的派生關係,它的家族圖譜如圖9-5所示:
圖9-5 CommandListener家族圖譜
根據上圖可以知道:
· CL定義了一些和Command相關的內部類,這裡採用了設計模式中的Command模式,每個命令的處理函式都是runCommand。注意,上圖只列出了部分Command類。
· CL也是從SocketListener派生的,不過它是Socket的監聽(listen)端。
下面看它的程式碼:
[-->CommandListener.cpp]
CommandListener::CommandListener() :
FrameworkListener("vold") {
//CL模組支援的命令
registerCmd(new DumpCmd());
registerCmd(new VolumeCmd());
registerCmd(new AsecCmd());
registerCmd(new ShareCmd());
registerCmd(new StorageCmd());
registerCmd(new XwarpCmd());
}
/*
registerCmd函式將Command儲存到mCommands中,mCommands的定義為一個列表,如下:
typedef android::List<FrameworkCommand *>FrameworkCommandCollection;
*/
voidFrameworkListener::registerCmd(FrameworkCommand *cmd) {
mCommands->push_back(cmd);
}
從上面的程式碼可知,CommandListener的基類是FrameworkListener,而FrameworkListener又從SocketListener類派生。之前在分析NM模組的NetLinkerHandler時,已介紹過SocketListener相關的知識了,所以此處不再贅述,只總結一下CL建立後的結果,它們是:
· CL會建立一個監聽端的socket,這樣就可以接收客戶端的連結。
· 客戶端傳送命令給CL,CL則從mCommands中找到對應的命令,並交給該命令的runCommand函式處理。
下面來關注第二個函式startListener,這個函式由SocketListener實現。
2. startListener的分析和資料處理
其實在分析NetlinkerHandler時,已經介紹了startListener函式,這裡再簡單回顧一下,有些具體內容和本章對NetlinkerHandler的分析有關。
[-->SocketListener.cpp]
int SocketListener::startListener() {
if(!mSocketName && mSock == -1) {
......
errno = EINVAL;
return -1;
} else if (mSocketName) {
//mSOcketName為字串“vold”。android_get_control_socket函式返回
//對應的socket控制程式碼
if((mSock = android_get_control_socket(mSocketName)) < 0) {
......
return -1;
}
}
//CL模組是監聽端
if(mListen && listen(mSock, 4) < 0) {
......
return -1;
} elseif (!mListen)
mClients->push_back(new SocketClient(mSock));
if(pipe(mCtrlPipe)) {
......
return -1;
}
//建立工作執行緒threadStart
if(pthread_create(&mThread, NULL, SocketListener::threadStart, this)) {
return -1;
}
return0;
}
當CL收到資料時,會呼叫onDataAvailable函式,它由FrameworkListener實現。
[-->FrameworkListener.cpp]
boolFrameworkListener::onDataAvailable(SocketClient *c) {
charbuffer[255];
intlen;
//讀取資料
if((len = read(c->getSocket(), buffer, sizeof(buffer) -1)) < 0) {
......
return errno;
} elseif (!len)
return false;
intoffset = 0;
int i;
for (i= 0; i < len; i++) {
if(buffer[i] == '\0') {
//分發命令,最終會呼叫對應命令物件的runCommand進行函式處理。
dispatchCommand(c, buffer + offset);
offset = i + 1;
}
}
returntrue;
}
dispatchCommand最終會根據收到的命令名(如“Volume”,“Share”等)來呼叫對應的命令物件(如VolumeCmd,ShareCmd)的runCommand函式以處理請求。這一塊非常簡單,這裡就不再詳述了。
3. CL模組的總結
CL模組的主要工作是:
· 建立一個監聽端的socket。
· 接收客戶端的連線和請求,並呼叫對應Command物件的runComand函式處理。
目前,CL模組唯一的客戶端就是MountService。來看看它。
9.2.6 Vold例項的分析
這一節將分析一個實際案例,即插入一張SD卡引發的事件及其處理過程。在分析之前,還是應先介紹MountService。
1. MountService的介紹
有些應用程式需要檢測外部儲存卡的插入/拔出事件,這些事件是由MountService通過Intent廣播發出的,例如外部儲存卡插入後,MountService就會傳送ACTION_MEDIA_MOUNTED訊息。從某種意義上說,可把MountService看成是Java世界的Vold。來簡單認識一下這個MountService,它的程式碼如下所示:
[-->MountService.java]
class MountService extends IMountService.Stub
implements INativeDaemonConnectorCallbacks {
//MountService實現了INativeDaemonConnectorCallbacks介面
......
public MountService(Context context) {
mContext = context;
......
//建立一個HandlerThread,在第5章中曾介紹過。
mHandlerThread = new HandlerThread("MountService");
mHandlerThread.start();
/*
建立一個Handler,這個Handler使用HandlerThread的Looper,也就是說,派發給該Handler
的訊息將在另外一個執行緒中處理。可回顧第5章的內容,以加深印象。
*/
mHandler = new MountServiceHandler(mHandlerThread.getLooper());
......
/*
NativeDaemonConnector用於Socket通訊,第二個引數“vold”表示將和Vold通訊,也就是
和CL模組中的那個socket建立通訊連線。第一個引數為INativeDaemonConnectorCallbacks
介面。它提供兩個回撥函式:
onDaemonConnected:當NativeDaemonConnector連線上Vold後回撥。
onEvent:當NativeDaemonConnector收到來自Vold的資料後回撥。
*/
mConnector = new NativeDaemonConnector(this, "vold",
10, "VoldConnector");
mReady= false;
//再啟動一個執行緒用於和Vold通訊。
Threadthread = new Thread(mConnector,
NativeDaemonConnector.class.getName());
thread.start();
}
......
}
MountService通過NativeDaemonConnector和Vold的CL模組建立通訊連線,這部分內容比較簡單,讀者可自行研究。下面來分析SD卡插入後所引發的一連串處理。
2. 裝置插入事件的處理
(1)Vold處理Uevent事件
在插入SD卡後,Vold的NM模組接收到Uevent訊息,假設此訊息的內容是前面介紹Uevent知識時使用的add訊息,它的內容如下所示:
[-->SD卡插入的Uevent訊息]
add@/devices/platform/msm_sdcc.2/mmc_host/mmc1/mmc1:c9f2/block/mmcblk0
ACTION=add //add表示裝置插入動作,另外還有remove和change等動作
//DEVPATH表示該裝置位於/sys目錄中的裝置路徑
DEVPATH=/devices/platform/msm_sdcc.2/mmc_host/mmc1/mmc1:c9f2/block/mmcblk0
/*
SUBSYSTEM表示該裝置屬於哪一類裝置,block為塊裝置,磁碟也屬於這一類裝置,另外還有
character(字元)裝置等型別。
*/
SUBSYSTEM=block
MAJOR=179//MAJOR和MINOR分別表示該裝置的主次裝置號,二者聯合起來可以標識一個裝置
MINOR=0
DEVNAME=mmcblk0
DEVTYPE=disk//裝置Type為disk
NPARTS=3 //表示該SD卡上的分割槽,我的SD卡上有三塊分割槽
SEQNUM=1357//序號
根據前文分析可知,NM模組中的NetlinkHandler會處理此訊息,請回顧一下相關程式碼:
[-->NetlinkHandler.cpp]
void NetlinkHandler::onEvent(NetlinkEvent *evt){
VolumeManager *vm = VolumeManager::Instance();
constchar *subsys = evt->getSubsystem();
......
//根據上面Uevent訊息的內容可知,它的subsystem對應為block,所以我們會走下面這個if分支
if (!strcmp(subsys, "block")) {
vm->handleBlockEvent(evt); //呼叫VM的handleBlockEvent
}
......
}
[-->VolumeManager.cpp]
voidVolumeManager::handleBlockEvent(NetlinkEvent *evt) {
constchar *devpath = evt->findParam("DEVPATH");
VolumeCollection::iterator it;
boolhit = false;
for(it = mVolumes->begin(); it != mVolumes->end(); ++it) {
//呼叫各個Volume的handleBlockEvent
if(!(*it)->handleBlockEvent(evt)) {
hit = true;
break;
}
}
......
}
我的G7手機只有一個Volume,其實際型別就是之前介紹過的DirectVolume。請看它是怎麼對待這個Uevent訊息的,程式碼如下所示:
[-->DirectVolume.cpp]
int DirectVolume::handleBlockEvent(NetlinkEvent*evt) {
constchar *dp = evt->findParam("DEVPATH");
PathCollection::iterator it;
for(it = mPaths->begin(); it != mPaths->end(); ++it) {
if(!strncmp(dp, *it, strlen(*it))) {
int action = evt->getAction();
const char *devtype =evt->findParam("DEVTYPE");
if (action == NetlinkEvent::NlActionAdd) {
int major = atoi(evt->findParam("MAJOR"));
int minor = atoi(evt->findParam("MINOR"));
char nodepath[255];
snprintf(nodepath,
sizeof(nodepath),"/dev/block/vold/%d:%d",
major, minor);
//內部呼叫mknod函式建立裝置節點
if (createDeviceNode(nodepath, major, minor)) {
SLOGE("Error makingdevice node '%s' (%s)", nodepath,
strerror(errno));
}
if (!strcmp(devtype, "disk")) {
//對應Uevent訊息的DEVTYPE值為disk,所以走這個分支
handleDiskAdded(dp, evt);
} else {
//處理DEVTYPE為Partition的情況
handlePartitionAdded(dp,evt);
}
} else if (action == NetlinkEvent::NlActionRemove) {
//對應Uevent的ACTION值為remove
......
} else if (action == NetlinkEvent::NlActionChange) {
//對應Uevent的ACTION值為change
......
}
......
return 0;
}
}
errno= ENODEV;
return-1;
}
插入SD卡,首先收到的Uevent訊息中DEVTYPE的值是“disk”,這將導致DirectVolume的handleDiskInserted被呼叫。下面來看它的工作。
[-->DirectVolume.cpp]
void DirectVolume::handleDiskAdded(const char*devpath, NetlinkEvent *evt) {
mDiskMajor = atoi(evt->findParam("MAJOR"));
mDiskMinor = atoi(evt->findParam("MINOR"));
constchar *tmp = evt->findParam("NPARTS");
if(tmp) {
mDiskNumParts = atoi(tmp);//這個disk上的分割槽個數。
} else{
......
mDiskNumParts = 1;
}
charmsg[255];
intpartmask = 0;
inti;
/*
Partmask會記錄這個Disk上分割槽載入的情況,前面曾介紹過,如果一個Disk有多個分割槽,
它後續則會收到多個分割槽的Uevent訊息。
*/
for (i= 1; i <= mDiskNumParts; i++) {
partmask |= (1 << i);
}
mPendingPartMap = partmask;
if(mDiskNumParts == 0) {
......//如果沒有分割槽,則設定Volume的狀態為Idle。
setState(Volume::State_Idle);
}else {
......//如果還有分割槽未載入,則設定Volume狀態為Pending
setState(Volume::State_Pending);
}
/*
設定通知內容,snprintf呼叫完畢後msg的值為:
“Volume sdcard/mnt/sdcard disk inserted (179:0)”
*/
snprintf(msg, sizeof(msg), "Volume %s %s disk inserted(%d:%d)",
getLabel(), getMountpoint(), mDiskMajor, mDiskMinor);
/*
getBroadcaster函式返回setBroadcaster函式設定的那個Broadcaster,也就是CL物件。
然後呼叫CL物件的sendBroadcast給MountService傳送訊息,注意它的第一個引數是ResponseCode::VolumeDiskInserted。
*/
mVm->getBroadcaster()->sendBroadcast(ResponseCode::VolumeDiskInserted,
msg, false);
}
handleDiskAdded把Uevent訊息做一些轉換後傳送給了MountService,實際上可認為CL模組和MountService通訊使用的是另外一套協議。那麼,MountService會做什麼處理呢?
(2)MountService的處理
[-->MountService.javaonEvent函式]
publicboolean onEvent(int code, String raw, String[] cooked) {
Intent in = null;
//關於onEvent函式,MountService的介紹中曾提過,當NativeDaemonConnector收到
//來自vold的資料後都會呼叫這個onEvent函式。
......
if(code == VoldResponseCode.VolumeStateChange) {
......
}else if (code == VoldResponseCode.ShareAvailabilityChange) {
......
}else if ((code == VoldResponseCode.VolumeDiskInserted) ||
(code ==VoldResponseCode.VolumeDiskRemoved) ||
(code ==VoldResponseCode.VolumeBadRemoval)) {
final String label = cooked[2]; //label值為”sdcard”
final String path = cooked[3]; //path值為”/mnt/sdcard”
int major = -1;
int minor = -1;
try {
String devComp = cooked[6].substring(1, cooked[6].length() -1);
String[] devTok = devComp.split(":");
major = Integer.parseInt(devTok[0]);
minor = Integer.parseInt(devTok[1]);
} catch (Exception ex) {
......
}
if (code == VoldResponseCode.VolumeDiskInserted) {
//收到handleDiskAdded傳送的VolumeDiskInserted訊息了
//然後單獨啟動一個執行緒來處理這個訊息。
new Thread() {
public void run() {
try {
int rc;
//呼叫doMountVolume處理。
if ((rc =doMountVolume(path)) !=
StorageResultCode.OperationSucceeded) {
}
} catch (Exception ex){
......
}
}
}.start();
}
doMountVolume函式的程式碼如下所示:
[-->MountService.java]
private int doMountVolume(String path) {
int rc = StorageResultCode.OperationSucceeded;
try {
//通過NativeDaemonConnector給Vold傳送請求,請求內容為:
//volume mount /mnt/sdcard
mConnector.doCommand(String.format("volume mount %s", path));
}catch (NativeDaemonConnectorException e) {
......//異常處理
}
走了一大圈,最後又回到Vold了。CL模組將收到這個來自MountService的請求,請求的內容為字串“volume mount/mnt/sdcard”,其中的volume表示命令的名字,CL會根據這個名字找到VolumeCmd物件,並交給它處理這個命令。
(3)Vold處理MountService的命令
Vold處理MountService命令的程式碼如下所示:
[-->CommandListener.cppVolumeCmd類]
intCommandListener::VolumeCmd::runCommand(SocketClient *cli,
int argc, char **argv) {
......
VolumeManager *vm = VolumeManager::Instance();
int rc= 0;
if(!strcmp(argv[1], "list")) {
return vm->listVolumes(cli);
} elseif (!strcmp(argv[1], "debug")) {
......
} elseif (!strcmp(argv[1], "mount")) {
......//呼叫VM模組的mountVolume來處理mount命令,引數是“/mnt/sdcard”
rc= vm->mountVolume(argv[2]);
} elseif (!strcmp(argv[1], "unmount")) {
......
rc= vm->unmountVolume(argv[2], force);
} elseif (!strcmp(argv[1], "format")) {
......
rc = vm->formatVolume(argv[2]);
} elseif (!strcmp(argv[1], "share")) {
......
rc= vm->shareVolume(argv[2], argv[3]);
} elseif (!strcmp(argv[1], "unshare")) {
......
rc= vm->unshareVolume(argv[2], argv[3]);
}
......
if(!rc) {
//傳送處理結果給MountService
cli->sendMsg(ResponseCode::CommandOkay, "volume operationsucceeded", false);
}
......
return0;
}
看mountVolume函式:
[-->VolumeManager.cpp]
int VolumeManager::mountVolume(const char*label) {
/*
根據label找到對應的Volume。label這個引數的名字含義上有些歧義,根據loopupVolume
的實現來看,它其實比較的是Volume的掛載路徑,也就是vold.fstab中指定的那個
/mnt/sdcard。而vold.fstab中指定的label叫sdcard。
*/
Volume*v = lookupVolume(label);
......
returnv->mountVol();//mountVol由Volume類實現。
}
找到對應的DirectVolume後,也就找到了代表真實儲存卡的物件。它是如何處理這個命令的呢?程式碼如下所示:
[-->Volume.cpp]
int Volume::mountVol() {
dev_tdeviceNodes[4];
int n,i, rc = 0;
charerrmsg[255];
......
//getMountpoint返回掛載路徑,即/mnt/sdcard
//isMountpointMounted判斷這個路徑是不是已經被mount了
if(isMountpointMounted(getMountpoint())) {
setState(Volume::State_Mounted);//設定狀態為State_Mounted
return 0;//如果已經被mount了,則直接返回
}
n =getDeviceNodes((dev_t *) &deviceNodes, 4);
......
for (i= 0; i < n; i++) {
char devicePath[255];
sprintf(devicePath, "/dev/block/vold/%d:%d",MAJOR(deviceNodes[i]),
MINOR(deviceNodes[i]));
......
errno = 0;
setState(Volume::State_Checking);
//預設SD卡為FAT分割槽,只有這樣,當載入為磁碟的時候才能被Windows識別。
if(Fat::check(devicePath)) {
......
return -1;
}
/*
先把裝置mount到/mnt/secure/staging,這樣/mnt/secure/staging下的內容
就是該裝置的儲存內容了
*/
errno = 0;
if(Fat::doMount(devicePath, "/mnt/secure/staging", false, false, 1000,
1015, 0702,true)) {
......
continue;
}
/*
下面這個函式會把儲存卡中的autorun.inf檔案找出來並刪掉,這個檔案就是“臭名昭著”的
自動執行檔案,在Windows系統上,把SD卡掛載為磁碟後,雙擊這個磁碟就會自動執行
這個檔案,很多病毒和木馬都是通過它傳播的。為了安全起見,要把這個檔案刪掉。
*/
protectFromAutorunStupidity();
//①下面這個函式比較有意思,需要看看:
if(createBindMounts()) {
......
return -1;
}
//將儲存卡mount路徑從/mnt/secure/staging移到/mnt/sdcard
if(doMoveMount("/mnt/secure/staging", getMountpoint(), false)) {
......
return -1;
}
//②設定狀態為State_Mounted,這個函式將傳送狀態資訊給MountService
setState(Volume::State_Mounted);
mCurrentlyMountedKdev = deviceNodes[i];
return 0;
}
......
setState(Volume::State_Idle);
return-1;
}
上面程式碼中有個比較有意思的函式,就是createBindMounts,其程式碼如下所示:
[-->Volume.cpp]
int Volume::createBindMounts() {
unsigned long flags;
/*‘
將/mnt/secure/staging/android_secure目錄名改成
/mnt/secure/staging/.android_secure,SEC_STG_SECIMGDIR的值就是
/mnt/secure/staging/.android_secure,也就是把它變成Linux平臺上的隱藏目錄
*/
if(!access("/mnt/secure/staging/android_secure", R_OK | X_OK)&&
access(SEC_STG_SECIMGDIR, R_OK | X_OK)) {
if(rename("/mnt/secure/staging/android_secure", SEC_STG_SECIMGDIR)) {
SLOGE("Failed to rename legacy asec dir (%s)",strerror(errno));
}
}
......
/*
使用mount命令的bind選項,可將/mnt/secure/staging/.android_secure這個目錄
掛載到/mnt/secure/asec目錄下。/mnt/secure/asec目錄是一個secure container,
目前主要用來儲存一些安裝在SD卡上的APP資訊。APP2SD是Android 2.2引入的新機制,它
支援將APP安裝在SD卡上,這樣可以節約內部的儲存空間。
mount的bind選項允許將檔案系統的一個目錄掛載到另外一個目錄下。讀者可以通過man mount
查詢具體資訊。
*/
if(mount(SEC_STG_SECIMGDIR, SEC_ASECDIR, "", MS_BIND, NULL)) {
......
return -1;
}
......
/*
將tempfs裝置掛載到/mnt/secure/staging/.android_secure目錄,這樣之前
.android_secure目錄中的內容就只能通過/mnt/secure/asec訪問了。由於那個目錄只能
由root訪問,所以可以起到安全和保護的作用。
*/
if(mount("tmpfs", SEC_STG_SECIMGDIR, "tmpfs", MS_RDONLY,
"size=0,mode=000,uid=0,gid=0")){
......
umount("/mnt/asec_secure");
return -1;
}
return0;
}
createBindMounts的作用就是將儲存卡上的.android_secure目錄掛載到/mnt/secure/asec目錄下,同時對.android_secure進行一些特殊處理,這樣,沒有許可權的使用者就不能更改或破壞.android_secure目錄中的內容了,因此它起到了一定的保護作用。
在手機上,受保護的目錄內容,只能在用adb shell登入後,進入/mnt/secure/asec目錄來檢視。注意,這個asec目錄的內容就是.android_secure未掛載tmpfs時的內容(亦即它儲存著那些安裝在儲存卡上的應用程式的資訊)。另外,可把SD卡拔出來,通過讀卡器直接插到桌上型電腦上,此時,這些資訊就能在.android_secure目錄中被直接看到了。
(4)MountService處理狀態通知
volume的mountVol完成相關工作後,就通過下面的函式,傳送資訊給MountService:
setState(Volume::State_Mounted); //感興趣的讀者可自行分析此函式的實現。
MountService依然會在onEvent函式中收到這個訊息。
[-->MountService.java]
public boolean onEvent(int code, String raw,String[] cooked) {
Intent in = null;
......
if(code == VoldResponseCode.VolumeStateChange) {
/*
狀態變化由notifyVolumeStateChange函式處理,由於Volume的狀態
被置成Mounted,所以下面這個notifyVolumeStateChange會傳送
ACTION_MEDIA_MOUNTED這個廣播。我們就不再分析這個函式了,讀者
可自行研究。
*/
notifyVolumeStateChange(
cooked[2], cooked[3],Integer.parseInt(cooked[7]),
Integer.parseInt(cooked[10]));
}
例項分析就到這裡。中間略去了一些處理內容,例如對分割槽的處理等,讀者可自行研讀,相信已沒有太大難度了。另外,在上述處理過程中,稍微難懂的是mountVol這個函式在掛載方面的處理過程。用圖9-6來總結一下這個處理過程:
圖9-6 SD卡插入事件處理流程圖
由上圖可知,Vold在安全性上還是做了一定考慮的。如果沒有特殊需要,讀者瞭解上面這些知識也就夠了。
9.2.7 Vold總結
Vold和MountService之間的通訊使用了Socket。之前在第6章介紹Binder時也提到過它。除了Binder外,Socket是Android系統中最常用的IPC通訊機制了。本章介紹的Vold和Rild都是使用Socket進行IPC通訊的。
Vold及Java層的MountService都比較簡單,所以我在工作中碰到這兩位出問題的機率基本為零。雖然二者比較簡單,這裡還是要提個小小的問題,以幫助大家加深印象:
當SD卡拔出,或者掛載到磁碟上時,都會導致sd卡被解除安裝,在這個切換過程中,有一些應用程式會被系統kill掉,這是為什麼?
請讀者閱讀相關程式碼尋找答案,這樣或許能解釋很多測試人員在做測試時提出這種Bug的原因了:為什麼SD卡mount到電腦後,有些應用程式突然退出了?
9.3 Rild的的原理與機制分析
這裡,先回顧一下智慧手機的架構。目前,很多智慧手機的硬體架構都是兩個處理器:一個處理器用來執行作業系統,上面可以跑應用程式,這個處理器稱作Application Processor,簡稱AP;另一個處理器負責和射頻無線通訊相關的工作,叫Baseband Processor,簡稱BP。AP和BP晶片之間採用串列埠進行通訊,通訊協議使用的是AT指令。
什麼是AT指令呢?AT指令最早用在Modem上,後來幾大手機廠商如摩托羅拉、愛立信、諾基亞等為GSM通訊又設計了一整套AT指令。AT指令的格式比較簡單,是一個以AT開頭,後跟字母和數字表示具體功能的字串。瞭解具體的AT指令,可參考相關的規範參考或手機廠商提供的手冊,這裡就不再多說了。
在Android系統中,Rild執行在AP上,它是AP和BP在軟體層面上通訊的中樞,也就是說,AP上的應用程式將通過Rild傳送AT指令給BP,而BP的資訊通過Rild傳送給AP上的應用程式。
現介紹在Rild程式碼中常會碰到的兩個詞語:
· 第一個solicited Respose,即經過請求的回覆。它代表的應用場景是AP傳送一個AT請求指令給BP進行處理,處理後,BP會對應回覆一個AT指令告知處理結果。這個回覆指令是針對之前的那個請求指令的,此乃一問一答式,所以叫solicitedResponse。
· 第二個unsolicited Response,即未經請求的回覆。很多時候,BP主動給AP傳送AT指令,這種指令一般是BP通知AP當前發生的一些事情,例如一路電話打了過來,或者網路訊號中斷等。從AP的角度來看,這種指令並非由它傳送的請求所引起的,所以稱之為unsolicited Response。
上面這兩個詞語,實際指明瞭AP和BP兩種互動型別:
· AP傳送請求BP,BP響應並回復AP。
· BP傳送通知給AP。
這兩種型別對軟體而言有什麼意義呢?先來看Rild在軟體架構方面遇到的挑戰:
· 有很多把AP和BP整合在一塊晶片上的智慧手機,它們之間的通訊可能就不是AT指令了。
· 另外,即使AP和BP通訊使用的是AT指令,不同的手機廠商在AT指令上也會有很大的不同,而且這些都屬於商業祕密,所以手機廠商不可能共享原始碼,它只能給出二進位制的庫。
Rild是怎麼解決這個問題的呢?結合前面提到的AP/BP互動的兩種型別,大體可以勾畫出圖9-7:
圖9-7 Rild解決問題的方法
從上圖中可以看出:
· Rild會動態載入廠商相關的動態庫,這個動態庫載入在Linux平臺上則使用dlopen系統呼叫。
· Rild和動態庫之間通過介面進行通訊,也就是說Rild輸出介面供動態庫使用,而動態庫也輸出對應的介面供Rild使用。
· AP和BP互動的工作由動態庫去完成。
Rild和動態庫執行在同一個程式上,為了方便理解,可把這兩個東西分離開來。
根據上面的分析可知,對Rild的分析包括兩部分:
· 對Rild本身的分析。
· 對動態庫的分析。Android提供了一個用作參考的動態庫叫libReference_ril.so,這個庫實現了一些標準的AT指令。另外,它的程式碼結構也頗具參考價值,所以我們的動態庫分析就以它為主。
分析Rild時,為書寫方便起見,將這個動態庫簡稱為RefRil庫。
9.3.1 初識Rild
Rild的程式碼在Rild.c中,它是一個應用程式。從它的main開始分析,程式碼如下所示:
[-->Rild.c]
int main(int argc, char **argv)
{
//動態庫的位置由rilLibPath決定
constchar * rilLibPath = NULL;
char**rilArgv;
void*dlHandle;
/*
Rild規定動態庫必須實現一個叫Ril_init函式,這個函式的第一個引數指向結構體
RIL_Env,而它的返回值指向結構體RIL_ RadioFunctions。這兩個結構體就是在
圖9-7中提到的介面。這兩個介面的具體內容,後文再做分析。
*/
constRIL_RadioFunctions *(*rilInit)(const struct RIL_Env *, int, char **);
constRIL_RadioFunctions *funcs;
charlibPath[PROPERTY_VALUE_MAX];
unsigned char hasLibArgs = 0;
int i;
//Rild由init啟動,沒有對應的啟動引數,所以這個for迴圈不會進來。
for (i= 1; i < argc ;) {
if(0 == strcmp(argv[i], "-l") && (argc - i > 1)) {
rilLibPath = argv[i + 1];
i += 2;
}else if (0 == strcmp(argv[i], "--")) {
i++;
hasLibArgs = 1;
break;
}else {
usage(argv[0]);
}
}
if(rilLibPath == NULL) {
/*
讀取系統屬性,LIB_PATH_PROPERTY的值為"rild.libpath“,模擬器上
和RIL相關的屬性值有兩個,分別是:
rild.libpath=/system/lib/libreference-ril.so
rild.libargs=-d /dev/ttyS0
上面這些值都定義在build/target/board/generic/system.prop檔案中
不同廠商可以有自己對應的實現。
*/
if( 0 == property_get(LIB_PATH_PROPERTY, libPath, NULL)) {
goto done;
}else {
/*
這裡,使用參考的動態庫進行分析,它的位置為
/system/lib/libreference-ril.so。
*/
rilLibPath = libPath;
}
}
...... //和模擬器相關的一些內容
switchUser();//設定Rild的組使用者為radio
//通過dlopen系統載入動態庫
dlHandle = dlopen(rilLibPath, RTLD_NOW);
......
//① 啟動EventLoop,事件處理
RIL_startEventLoop();
//得到RefRil庫中RIL_Init函式的地址
rilInit= (const RIL_RadioFunctions *(*)(const struct RIL_Env *, int,
char **))dlsym(dlHandle, "RIL_Init");
......
rilArgv[0]= argv[0];
//②呼叫RefRil庫輸出的RIL_Init函式,注意傳入的第一個引數和它的返回值。
funcs= rilInit(&s_rilEnv, argc, rilArgv);
//③註冊上面rilInit函式的返回值(一個RIL_RadioFunctions型別的結構體)到Rild中。
RIL_register(funcs);
done:
while(1) {
//主執行緒sleep,具體工作交給工作執行緒完成。
sleep(0x00ffffff);
}
}
將上面的程式碼和分析結合起來,就知道了Rild解決問題的方法,程式碼中列出了三個關鍵點。我們將逐一對其進行分析。
9.3.2 RIL_startEventLoop的分析
第一個關鍵點是RIL_startEventLoop函式,這個函式實際上是由libRil.so實現的,它的程式碼在Ril.cpp中,程式碼如下所示:
[-->Ril.cpp]
extern "C" void RIL_startEventLoop(void){
intret;
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 void * eventLoop(void *param) {
intret;
intfiledes[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();
returnNULL;
}
工作執行緒的工作並不複雜,主要有三個關鍵點。
(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 {
structril_event *next;
structril_event *prev;//next和prev將ril_event組織成了一個雙向連結串列
intfd; //該任務對應的檔案描述符,以後簡稱FD。
intindex; //這個任務在監控表中的索引
/*
是否永久儲存在監控表中,一個任務處理完畢後將根據這個persist引數來判斷
是否需要從監控表中移除。
*/
boolpersist;
structtimeval 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() {
intret;
/*
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_setrfds;
structtimeval tv;
structtimeval * 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,該執行緒主要用來完成一些任務處理,而目前還沒有給它新增任務。
9.3.3 RIL_Init的分析
下面看第二個關鍵函式RIL_Init。這個函式必須由動態庫實現,對於下面這個例子來說,它將由RefRil庫實現,這個函式定義在Reference_ril.c中:
[-->Reference_ril.c]
pthread_t s_tid_mainloop;//看來又會建立一個執行緒
//動態庫必須實現的RIL_Init函式。
const RIL_RadioFunctions *RIL_Init(const structRIL_Env *env,
int argc, char **argv)
{
intret;
int fd= -1;
intopt;
pthread_attr_t attr;
s_rilenv = env; //將外部傳入的env儲存為s_rilenv。
......//一些引數處理,不必管它
pthread_attr_init (&attr);
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
//建立一個工作執行緒mainLoop
ret =pthread_create(&s_tid_mainloop, &attr, mainLoop, NULL);
/*
s_callbacks也為一個結構體
staticconst RIL_RadioFunctions s_callbacks = {
RIL_VERSION, //RIL的版本
onRequest, //下面是一些函式指標
currentState,
onSupports,
onCancel,
getVersion
};
*/
return&s_callbacks;
}
RefRil的RIL_Init函式比較簡單,主要有三項工作要做:
· 儲存Rild傳入的RIL_Env結構體。
· 建立一個叫mainLoop的工作執行緒。
· 返回一個RIL_RadioFunctions的結構體。
上面的RIL_Env和RIL_RadioFunctions結構體,就是Rild架構中用來隔離通用程式碼和廠商相關程式碼的介面。先來看RIL_RadioFunctions,這個結構體由廠商的動態庫實現,它的程式碼如下:
//函式指標定義
typedef void (*RIL_RequestFunc) (int request,void *data,
size_tdatalen, RIL_Token t);
typedef RIL_RadioState(*RIL_RadioStateRequest)();
typedef int (*RIL_Supports)(int requestCode);
typedef void (*RIL_Cancel)(RIL_Token t);
typedef void (*RIL_TimedCallback) (void *param);
typedef const char * (*RIL_GetVersion) (void);
typedef struct {
intversion; //RIL的版本
//通過這個介面可向BP提交一個請求,注意這個函式的返回值為空,這是為什麼?
RIL_RequestFunc onRequest;
RIL_RadioStateRequest onStateRequest;//查詢BP的狀態
RIL_Supports supports;
RIL_CancelonCancel;
//查詢動態庫的版本,RefRil庫中該函式的實現將返回字串”android reference-ril 1.0”
RIL_GetVersion getVersion;
} RIL_RadioFunctions;
對於上面的結構體,應重點關注函式onRequest,它被Rild用來向動態庫提交一個請求,也就是說,AP向BP傳送請求的介面就是它,但是這個函式卻沒有返回值,那麼該請求的執行結果是怎麼得到的呢?
這裡不賣關子,直接告訴大家。Rild架構中最大的特點就是採用了非同步請求/處理的方式。這種方式和非同步I/O有異曲同工之妙。那麼什麼是非同步請求/處理呢?它的執行流程如下:
· Rild通過onRequest向動態庫提交一個請求,然後返回去做自己的事情。
· 動態庫處理這個請求,請求的處理結果通過回撥介面通知。
這種非同步請求/處理的流程和酒店的MorningCall服務很類似,具體相似之處如下所示:
· 在前臺預約了一個Morning Call,這好比向酒店提交了一個請求。預約完後,就可以放心地做自己的事情了。
· 酒店登記了這個請求,記錄是哪個房間申請的服務,然後由酒店安排工作人員值班,這些都是酒店對這個請求的處理,作為房客則無須知道處理細節。
· 第二天早上,約好的時間一到,酒店給房客打電話,房客就知道這個請求被處理了。為了檢查一下賓館服務的效果,最好是拿表看看接到電話的時間是不是之前預約的時間。
這時,讀者對非同步請求/處理機制或許有了一些直觀的感受。那麼,動態庫是如何通知請求的處理結果的呢?這裡用到了另外一個介面RIL_Env結構,它的定義如下所示:
[-->Ril.h]
struct RIL_Env {
//動態庫完成一個請求後,通過下面這個函式通知處理結果,其中第一個引數標明是哪個請求
//的處理結果
void(*OnRequestComplete)(RIL_Token t, RIL_Errno e,
void *response,size_t responselen);
//動態庫用於進行unsolicited Response通知的函式
void(*OnUnsolicitedResponse)(int unsolResponse, const void *data,
size_t datalen);
//給Rild提交一個超時任務
void*(*RequestTimedCallback) (RIL_TimedCallback callback,
void *param,const struct timeval *relativeTime);
//從Rild的超時任務佇列中移除一個任務
void(*RemoveTimedCallback) (void *callbackInfo);
};
結合圖9-7和上面的分析可以發現,Rild在設計時將請求的應答介面和動態庫的通知介面都放在了RIL_Env結構體中。
關於Rild和動態庫的互動介面就分析到這裡。相信讀者已經明白其中的原理了。下面來看RefRil庫建立的工作執行緒mainLoop。
1. 工作執行緒mainLoop的分析
RefRil庫的RIL_Init函式會建立一個工作執行緒mainLoop,其程式碼如下所示:
[-->Reference_Ril.c]
static void *
mainLoop(void *param)
{
intfd;
intret;
......
/*
為AT模組設定一些回撥函式,AT模組用來和BP互動,對於RefRil庫來說,AT模組就是對
串列埠裝置通訊的封裝,這裡統稱為AT模組。
*/
at_set_on_reader_closed(onATReaderClosed);
at_set_on_timeout(onATTimeout);
for(;;) {
fd= -1;
//下面這個while迴圈的目的是為了得到串列埠裝置的檔案描述符,我們省略其中的一些內容
while (fd < 0) {
if (s_port > 0) {
fd = socket_loopback_client(s_port, SOCK_STREAM);
} else if (s_device_socket) {
if (!strcmp(s_device_path, "/dev/socket/qemud")) {
......
} else if (s_device_path != NULL) {
fd = open (s_device_path, O_RDWR);
if ( fd >= 0 && !memcmp( s_device_path,"/dev/ttyS", 9 ) ) {
struct termios ios;
tcgetattr( fd, &ios );
ios.c_lflag = 0;
tcsetattr( fd, TCSANOW,&ios );
}
}
......
}
s_closed = 0;
//①開啟AT模組,傳入一個回撥函式onUnsolicited
ret = at_open(fd, onUnsolicited);
......
//②下面這個函式向Rild提交一個超時任務,該任務的處理函式是initializeCallback
RIL_requestTimedCallback(initializeCallback,NULL, &TIMEVAL_0);
sleep(1);
/*
如果AT模組被關閉,則waitForClose返回,但是該執行緒並不會退出,而是從for迴圈那
開始重新執行一次。所以這個mainLoop工作執行緒是用來監控AT模組的,一旦它被關閉,就
需要重新開啟。也就是說不允許AT模組被關閉。
*/
waitForClose();
......
}
}
可以看到,mainLoop的工作其實就是初始化AT模組,並監控AT模組,一旦AT模組被關閉,那麼mainLoop就要重新開啟並初始化它。這幾項工作主要由at_open和超時任務的處理函式initializeCallback完成。
(1)at_open分析
來看at_open這個函式,其程式碼如下所示:
[-->Atchannle.c]
int at_open(int fd, ATUnsolHandler h)
{
//at_open的第一個引數是一個代表串列埠裝置的檔案描述符。
intret;
pthread_t tid;
pthread_attr_t attr;
s_fd =fd;
s_unsolHandler = h;
s_readerClosed = 0;
s_responsePrefix = NULL;
s_smsPDU = NULL;
sp_response = NULL;
......//和電源管理相關的操作
pthread_attr_init (&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
//建立一個工作執行緒readerLoop,這個執行緒的目的就是從串列埠裝置讀取資料
ret =pthread_create(&s_tid_reader, &attr, readerLoop, &attr);
......
return0;
}
at_open函式會另外建立一個工作執行緒readerLoop,從名字上看,它會讀取串列埠裝置。下面來看它的工作,程式碼如下所示:
[-->Atchannle.c]
static void *readerLoop(void *arg)
{
for(;;) {
const char * line;
line = readline(); //從串列埠裝置讀取資料
......
if(isSMSUnsolicited(line)) {
char *line1;
const char *line2;
line1 = strdup(line);
line2 = readline();
if (line2 == NULL) {
break;
}
if (s_unsolHandler != NULL) {
s_unsolHandler (line1, line2);//呼叫回撥,處理SMS的通知
}
free(line1);
}else {
//處理接收到的資料,也就是根據line中的AT指令呼叫不同的回撥
processLine(line);
}
......//電源管理相關
//這個執行緒退出前會呼叫通過at_set_on_reader_closed設定的回撥函式,以通知
//AT模組關閉
onReaderClosed();
returnNULL;
}
readerLoop工作執行緒比較簡單,就是從串列埠裝置中讀取資料,然後進行處理。這些資料有可能是solicited response,也有可能是unsolicited response,具體的處理函式我們在後續的例項分析中再來介紹,下面我們看第二個函式RIL_requestTimedCallback。
(2)initializeCallback的分析
在分析initializeCallback函式前,我們先看看RefRil向Rild提交超時任務的RIL_requestTimedCallback函式,它其實是一個巨集,不過這個巨集比較簡單,就是封裝了RIL_Env結構體中對RequestTimedCallback函式的呼叫,程式碼如下所示:
#define RIL_requestTimedCallback(a,b,c) \
s_rilenv->RequestTimedCallback(a,b,c)
//向Rild提交一個超時處理函式
下面我們看看Rild實現的這個RequestTimedCallback函式,程式碼如下所示。
[-->Ril.cpp]
extern "C" void *
RIL_requestTimedCallback (RIL_TimedCallbackcallback, void *param,
const structtimeval *relativeTime) {
returninternalRequestTimedCallback (callback, param, relativeTime);
}
/*
呼叫internalRequestTimedCallback,其實就是構造一個Ril_event事件然後加入到
timer_list,並觸發event_loop工作執行緒執行
*/
static UserCallbackInfo * internalRequestTimedCallback(
RIL_TimedCallback callback, void *param,
const structtimeval *relativeTime)
{
structtimeval myRelativeTime;
UserCallbackInfo *p_info;
p_info= (UserCallbackInfo *) malloc (sizeof(UserCallbackInfo));
p_info->p_callback = callback;
p_info->userParam = param;
if(relativeTime == NULL) {
memset (&myRelativeTime, 0, sizeof(myRelativeTime));
} else{
memcpy (&myRelativeTime, relativeTime, sizeof(myRelativeTime));
}
ril_event_set(&(p_info->event), -1, false, userTimerCallback,p_info);
//將該任務新增到timer_list中去
ril_timer_add(&(p_info->event), &myRelativeTime);
triggerEvLoop(); //觸發eventLoop執行緒
returnp_info;
}
從上面的程式碼可知,RIL_requestTimedCallback函式就是向eventLoop提交一個超時任務,這個任務的處理函式則為initialCallback,下面直接來看該函式的內容,如下所示。
[-->Reference_ril.c]
static void initializeCallback(void *param)
{
/*
這個函式就是通過傳送一些AT指令來初始化BP中的無線通訊Modem,不同的modem可能有
不同的AT指令。這裡僅列出部分程式碼。
*/
ATResponse *p_response = NULL;
interr;
setRadioState (RADIO_STATE_OFF);
at_handshake();
......
err =at_send_command("AT+CREG=2", &p_response);
......
at_response_free(p_response);
at_send_command("AT+CGREG=1", NULL);
at_send_command("AT+CCWA=1", NULL);
......
if(isRadioOn() > 0) {
setRadioState (RADIO_STATE_SIM_NOT_READY);
}
......
}
2. RIL_Init的總結
RIL_Init函式由動態庫提供,以上面RefRil庫的程式碼為參考,這個函式執行完後,將完成RefRil庫的幾項重要工作,它們是:
· 建立一個mainLoop工作執行緒,mainLoop執行緒的任務是初始化AT模組,並監控AT模組,一旦AT模組被關閉,則會重新初始化AT模組。
· AT模組內部會建立一個工作執行緒readerLoop,該執行緒的作用是從串列埠裝置中讀取資訊,也就是直接和BP打交道。
· mainLoop通過向Rild提交超時任務,完成了對Modem的初始化工作。
在Rild的main函式中還剩下最後一個關鍵函式RIL_register沒有分析了,下面來看看它。
9.3.4 RIL_register的分析
1. 建立對外通訊的鏈路
RIL_register函式將建立兩個監聽端socket,它們的名字分別是:
· “rild”:這個socket用來和Java層的應用通訊。這一點與Vold中的MountService類似。
· “rild-debug”:這個socket用來接收測試程式的測試命令。
下面來看RIL_register函式的程式碼,如下所示:
[-->Ril.cpp]
extern "C" void RIL_register (constRIL_RadioFunctions *callbacks) {
//RIL_RadioFunctions結構體由RefRil庫輸出
intret;
intflags;
......//版本檢測
if(s_registerCalled > 0) {
return;
}
//拷貝這個結構體的內容到s_callbacks變數中。
memcpy(&s_callbacks, callbacks, sizeof (RIL_RadioFunctions));
s_registerCalled = 1;
//Rild定義了一些Command,這裡做一個小小的檢查
for(int i = 0; i < (int)NUM_ELEMS(s_commands); i++) {
assert(i == s_commands[i].requestNumber);
}
for(int i = 0; i < (int)NUM_ELEMS(s_unsolResponses); i++) {
assert(i + RIL_UNSOL_RESPONSE_BASE
== s_unsolResponses[i].requestNumber);
}
......
//start listen socket
#if 0
......
#else
//SOCKET_NAME_RIL的值為“Ril”,這個socket由init程式根據init.rc的配置建立
s_fdListen = android_get_control_socket(SOCKET_NAME_RIL);
......
//監聽
ret =listen(s_fdListen, 4);
......
#endif
/*
構造一個非超時任務,處理函式是listenCallback。這個任務會儲存在監控表中,一旦它的FD
可讀就會導致eventLoop的select函式返回。根據前面的介紹可知,listen端的socket
可讀表示有客戶connect上。由於該任務的persist被設定為false,待listenCallback
處理完後,這個任務就會從監控表中移除。也就是說下一次select的readFDs中將不會有
這個監聽socket了,這表明Rild只支援一個客戶端的連線。
*/
ril_event_set (&s_listen_event, s_fdListen, false,
listenCallback, NULL);
//觸發eventLoop工作
rilEventAddWakeup (&s_listen_event);
#if 1
/*
Rild為了支援除錯,還增加了一個Ril_debug的socket,這個socket專門用於
測試程式傳送測試命令
*/
s_fdDebug = android_get_control_socket(SOCKET_NAME_RIL_DEBUG);
ret =listen(s_fdDebug, 4);
......
//新增一個非超時任務,該任務對應的處理函式是debugCallback,它是專門用來處理測試命令的。
ril_event_set (&s_debug_event, s_fdDebug, true,
debugCallback, NULL);
rilEventAddWakeup (&s_debug_event);
#endif
}
根據上面的分析,如果有一個客戶端connect上Rild,eventLoop就會被觸發,並且對應的處理函式listenCallback會被呼叫,下面就去看看這個函式的實現。
[-->Ril.cpp]
static void listenCallback (int fd, short flags,void *param) {
intret;
interr;
intis_phone_socket;
RecordStream *p_rs;
structsockaddr_un peeraddr;
socklen_t socklen = sizeof (peeraddr);
structucred creds;
socklen_t szCreds = sizeof(creds);
structpasswd *pwd = NULL;
//接收一個客戶端的連線,並將返回的socket儲存在s_fdCommand中
s_fdCommand = accept(s_fdListen, (sockaddr *) &peeraddr,&socklen);
......
errno= 0;
is_phone_socket = 0;//許可權控制,判斷連線的客戶端有沒有對應的許可權。
......//如果沒有對應的許可權則中止後面的流程
//設定這個socket為非阻塞,所以後續的send/recv呼叫都不會阻塞
ret =fcntl(s_fdCommand, F_SETFL, O_NONBLOCK);
......
/*
p_rs為RecordStream型別,它內部會分配一個緩衝區來儲存客戶端發來的資料,
這些都是socket程式設計常用的做法。
*/
p_rs =record_stream_new(s_fdCommand, MAX_COMMAND_BYTES);
/*
構造一個新的非超時任務,這樣在收到來自客戶端的資料後就會由eventLoop呼叫對應的
處理函式processCommandsCallback了。
*/
ril_event_set (&s_commands_event, s_fdCommand, 1,
processCommandsCallback, p_rs);
rilEventAddWakeup (&s_commands_event);
onNewCommandConnect();//作一些後續處理,有興趣的讀者可以看看。
}
2. RIL_register小結
RIL_register函式的主要功能是初始化了兩個用來和外部程式通訊的socket,並且向eventLoop新增了對應的任務。
至此,Rild的main函式就都分析完了。下面對main函式進行總結。
9.3.5 Rild main函式的總結
前面所有的內容都是在main函式中處理的,下面給出main函式執行後的結果,如圖9-9所示:
圖9-9 Rild main函式執行後的結果示意圖
其中:
· Rild和RefRil庫的互動通過RIL_Env和RIL_RadioFunctions這兩個結構體來完成。
· Rild的eventLoop處理任務。對於來自客戶端的任務,eventLoop呼叫的處理函式是processCommandsCallback。
· RefRil庫的readerLoop用來從串列埠裝置中讀取資料。
· RefRil庫中的mainLoop用來監視readerLoop。
上圖畫出的模組都是靜態的,前面提到的非同步請求/處理的工作方式不能體現出來。那麼,來分析一個例項,看看這些模組之間是如何配合與聯動的。
9.3.6 Rild例項的分析
其實,Rild沒什麼難度,相信見識過Audio和Surface系統的讀者都會有同感。但Java層的Phone應用及相關的Telephony模組卻相當複雜,這裡不去討論Phone的實現,而是通過例項來分析一個電話是如何撥打出去的。這個例子和Rild有關的東西比較簡單,但在分析程式碼的路途上,讀者可以領略到Java層Phone程式碼的複雜。
1. 建立Phone
Android支援GSM和CDMA兩種Phone,到底建立哪種Phone呢?來看PhoneApp.java是怎麼做的:
[-->PhoneApp.java]
public void onCreate() {
......
if(phone == null) {
//建立一個Phone,這裡使用了設計模式中的Factory(工廠)模式
PhoneFactory.makeDefaultPhones(this);
phone = PhoneFactory.getDefaultPhone();
......
}
工廠模式的好處在於,將Phone(例如程式碼中的GSMPhone或CDMAPhone)建立的具體複雜過程遮蔽起來了,因為使用者只關心工廠的產出物Phone,而不關心建立過程。通過工廠模式可降低使用者和建立者程式碼之間的耦合性,即使以後增加TDPhone,使用者也不需要修改太多的程式碼。
下面來看這個Phone工廠:
[-->PhoneFactory.java]
public static void makeDefaultPhones(Context context){
makeDefaultPhone(context);//呼叫makeDefaultPhone函式,直接去看看
}
public static void makeDefaultPhone(Contextcontext) {
synchronized(Phone.class) {
......
//根據系統設定獲取通訊網路的模式
intnetworkMode = Settings.Secure.getInt(context.getContentResolver(),
Settings.Secure.PREFERRED_NETWORK_MODE,preferredNetworkMode);
intcdmaSubscription =
Settings.Secure.getInt(context.getContentResolver(),
Settings.Secure.PREFERRED_CDMA_SUBSCRIPTION,
preferredCdmaSubscription);
//RIL這個物件就是rild socket的客戶端,AT命令由它傳送給Rild
sCommandsInterface= new RIL(context, networkMode,cdmaSubscription);
int phoneType =getPhoneType(networkMode);
if(phoneType == Phone.PHONE_TYPE_GSM) {
//先建立GSMPhone,然後建立PhoneProxy,這裡使用了設計模式中的Proxy模式
sProxyPhone = new PhoneProxy(newGSMPhone(context,
sCommandsInterface,sPhoneNotifier));
}else if (phoneType == Phone.PHONE_TYPE_CDMA) {
//建立CDMAPhone
sProxyPhone = new PhoneProxy(new CDMAPhone(context,
sCommandsInterface,sPhoneNotifier));
}
sMadeDefaults = true;
}
}
}
假設建立的是GSMPhone,makeDefaultPhones函式將返回PhoneProxy物件,不過這是一個代理Phone,具體工作還是會由GSMPhone完成。
Phone建立完後,就要撥號了。
2. Dial撥號
Phone應用提供了一個PhoneUtils類,最終的撥號是由它完成的:
[-->PhoneUtils.java]
static int placeCall(Phone phone, String number,Uri contactRef) {
int status = CALL_STATUS_DIALED;
try {
//呼叫Phone的dial函式,這個Phone的真實型別是PhoneProxy,number就是電話號碼
Connectioncn = phone.dial(number);
......
}
......
}
前面說過,PhoneProxy代理的物件是GSMPhone,直接去看它的dial函式:
[-->GSMPhone.java]
public Connection
dial(String dialString) throws CallStateException {
returndial(dialString, null);//呼叫另外一個dial函式
}
public Connection dial (String dialString,UUSInfo uusInfo)
throws CallStateException {
StringnewDialString = PhoneNumberUtils.stripSeparators(dialString);
if (mmi== null) {
//mCT是GsmCallTrack型別,呼叫它的dial函式。
returnmCT.dial(newDialString, uusInfo);
} elseif (mmi.isTemporaryModeCLIR()) {
returnmCT.dial(mmi.dialingNumber, mmi.getCLIRMode(), uusInfo);
}
......
}
又呼叫GSMCallTrack的dial函式,它的程式碼如下所示:
[-->GSMCallTrack.java]
Connection dial (String dialString, intclirMode, UUSInfo uusInfo)
throws CallStateException {
......
//構造一個GsmConnection物件
pendingMO = newGsmConnection(phone.getContext(), dialString,
this, foregroundCall);
hangupPendingMO = false;
/*
cm物件的真實型別就是我們前面提到的RIL類,它實現了CommandInterface。
下面將呼叫它的dial函式。
*/
cm.dial(pendingMO.address, clirMode, uusInfo,
obtainCompleteMessage());
......
returnpendingMO;
}
3. Java層RIL類的分析
Phone應用是不是很複雜?從建立Phone開始,頗費周折才來到了Java層的RIL類,RIL將是Rild中rild socket的唯一客戶端。下面來認識一下RIL。
(1)RIL的架構
RIL的建構函式的程式碼如下所示。
[-->RIL.java]
public RIL(Context context, int networkMode, intcdmaSubscription) {
super(context);
mCdmaSubscription =cdmaSubscription;
mNetworkMode = networkMode;
//建立一個HandlerThread,從名字上看它是用來傳送訊息的
mSenderThread = new HandlerThread("RILSender");
mSenderThread.start();
Looper looper = mSenderThread.getLooper();
mSender = new RILSender(looper);
mReceiver = new RILReceiver();
//建立一個RILReceiver執行緒,從名字上看它是用來接收訊息的
mReceiverThread = new Thread(mReceiver, "RILReceiver");
mReceiverThread.start();
......
}
那麼和Rild中rild socket通訊的socket是在哪建立的呢?答案是在接收執行緒中,其程式碼:
[-->RIL.java]
class RILReceiver implements Runnable {
byte[] buffer;
......
public void
run() {
int retryCount = 0;
try {for (;;) {
LocalSocket s = null;
LocalSocketAddress l;
try {
s = new LocalSocket();
l = newLocalSocketAddress(SOCKET_NAME_RIL,
LocalSocketAddress.Namespace.RESERVED);
//和Rild進行連線
s.connect(l);
......
}
mSocket = s;
int length = 0;
try {
InputStream is =mSocket.getInputStream();
for (;;) {
Parcel p;
//讀資料
length = readRilMessage(is,buffer);
//解析資料
p = Parcel.obtain();
p.unmarshall(buffer, 0,length);
p.setDataPosition(0);
//處理請求,以後再看
processResponse(p);
p.recycle();
}
}
......
}
從上面程式碼中可知,RIL封裝了兩個執行緒:
· mSenderThread,用來向Rild傳送訊息。
· mReceiverThread,用來從Rild中接收訊息。
待RIL建立後,dail函式該幹什麼呢?
(2)傳送dail請求
dial的處理過程,其程式碼如下所示:
[-->RIL.java]
public void dial(String address, int clirMode,UUSInfo uusInfo, Message result) {
//建立一個Java層的RIL請求包
RILRequest rr =RILRequest.obtain(RIL_REQUEST_DIAL, result);
rr.mp.writeString(address);
rr.mp.writeInt(clirMode);
rr.mp.writeInt(0);
if(uusInfo == null) {
rr.mp.writeInt(0); // UUS information is absent
}else {
rr.mp.writeInt(1); // UUS information is present
rr.mp.writeInt(uusInfo.getType());
rr.mp.writeInt(uusInfo.getDcs());
rr.mp.writeByteArray(uusInfo.getUserData());
}
//傳送資料
send(rr);
}
private void send(RILRequest rr) {
Messagemsg;
//傳送EVENT_SEND訊息,由mSender這個Handler處理
msg= mSender.obtainMessage(EVENT_SEND, rr);
acquireWakeLock();
msg.sendToTarget();//由傳送執行緒處理
}
下面看handleMessage函式:
[-->RIL.java]
public void handleMessage(Message msg) {
RILRequest rr = (RILRequest)(msg.obj);//請求訊息
RILRequestreq = null;
......
switch (msg.what) {
caseEVENT_SEND:
booleanalreadySubtracted = false;
try{
LocalSocket s;
s = mSocket; //這個mSocket就是和Rild通訊的socket
/*
執行非同步請求/處理時,請求方需要將請求包儲存起來,待收到完成通知後再從請求佇列
中找到對應的那個請求包並做後續處理。請求包一般會儲存請求時的上下文資訊。
以酒店的Morning Call服務為例。假設預約了7、8、9點的服務,那麼當7點鐘
接到電話時,一看錶便知道是7點的那個請求完成了,而不是8點或9點的請求完成了。
這個7便是請求號的標示,而且完成通知必須回傳這個請求號。至於上下文資訊,則
儲存在請求包中。例如酒店會在電話中通知說7點鐘要開一個會,這個開會的資訊是
預約服務的時候由你提供給酒店的。
儲存請求包是非同步請求/處理或非同步I/O中常見的做法,不過這種做法有一個
很明顯的缺點,就是當請求量比較大的時候,會佔用很多記憶體來儲存請求包資訊。
*/
synchronized (mRequestsList) {
mRequestsList.add(rr);
}
byte[] data;
data = rr.mp.marshall();
rr.mp.recycle();
rr.mp = null;
......
s.getOutputStream().write(dataLength);
s.getOutputStream().write(data); //傳送資料
}
......
}
至止,應用層已經通過RIL物件將請求資料傳送了出去。由於是非同步模式,請求資料傳送出去後應用層就直接返回了,而且目前還不知道處理結果。那麼Rild是如何處理這個請求的呢?
4. Rild處理請求的分析
根據前面對Rild的分析可知,當收到客戶端的資料時會由eventLoop呼叫對應的任務處理函式進行處理,而這個函式就是processCommandsCallback。看它的程式碼:
(1)Rild接收請求
Rild接收請求的程式碼如下所示:
[-->Ril.cpp]
static void processCommandsCallback(int fd,short flags, void *param) {
RecordStream *p_rs;
void*p_record;
size_trecordlen;
intret;
//RecordStream為processCommandsCallback的引數,裡面維護了一個接收緩衝區並
//有對應的緩衝讀寫位置控制
p_rs =(RecordStream *)param;
for(;;) {
/*
下面這個函式將從socket中read資料到緩衝區,並從緩衝區中解析命令。
注意,該緩衝區可能累積了多條命令,也就是說,客戶端可能傳送了多個命令,而
Rild通過一次read就全部接收到了。這個特性是由TCP的流屬性決定的。
所以這裡有一個for迴圈來接收和解析命令。
*/
ret = record_stream_get_next(p_rs, &p_record, &recordlen);
if(ret == 0 && p_record == NULL) {
/* end-of-stream */
break;
}else if (ret < 0) {
break;
}else if (ret == 0) {
//處理一條命令
processCommandBuffer(p_record, recordlen);
}
}
if(ret == 0 || !(errno == EAGAIN || errno == EINTR)) {
......//出錯處理,例如socket read出錯
}
}
每解析出一條命令,就呼叫processCommandBuffer函式進行處理,看這個函式:
[-->Ril.cpp]
static int processCommandBuffer(void *buffer,size_t buflen) {
Parcelp;
status_t status;
int32_t request;
int32_t token;
RequestInfo *pRI;
intret;
p.setData((uint8_t *) buffer, buflen);
status= p.readInt32(&request);
status= p.readInt32 (&token);
......
//s_commands定義了Rild支援的所有命令及其對應的處理函式
if(request < 1 || request >= (int32_t)NUM_ELEMS(s_commands)) {
......
return 0;
}
//Rild內部處理也是採用的非同步模式,所以它也會儲存請求,又分配一次記憶體。
pRI =(RequestInfo *)calloc(1, sizeof(RequestInfo));
pRI->token= token;
//s_commands是什麼?
pRI->pCI = &(s_commands[request]);
//請求資訊儲存在一個單向連結串列中。
ret =pthread_mutex_lock(&s_pendingRequestsMutex);
pRI->p_next = s_pendingRequests;//p_next指向連結串列的後繼結點
s_pendingRequests = pRI;
ret =pthread_mutex_unlock(&s_pendingRequestsMutex);
//呼叫對應的處理函式
pRI->pCI->dispatchFunction(p, pRI);
return0;
}
上面的程式碼中,出現了一個s_commands陣列,它儲存了一些CommandInfo結構,這個結構封裝了Rild對AT指令的處理函式。另外Rild還定義了一個s_unsolResponses陣列,它封裝了unsolicited Response對應的一些處理函式。這兩個陣列,如下所示:
[-->Ril.cpp]
typedef struct {//先看看CommandInfo的定義
intrequestNumber; //請求號,一個請求對應一個請求號
//請求處理函式
void(*dispatchFunction) (Parcel &p, struct RequestInfo *pRI);
//結果處理函式
int(*responseFunction) (Parcel &p, void *response, size_tresponselen);
} CommandInfo;
//下面是s_commands的定義
static CommandInfo s_commands[] = {
#include "ril_commands.h"
};
//下面是s_unsolResponses的定義
static UnsolResponseInfo s_unsolResponses[] = {
#include "ril_unsol_commands.h" //這個標頭檔案讀者可以自己去看看
};
再來看ril_commands.h的定義:
[-->ril_commands.h]
{0, NULL, NULL}, //除了第一條外,一共定義了103條CommandInfo
{RIL_REQUEST_GET_SIM_STATUS, dispatchVoid,responseSimStatus},
......
{RIL_REQUEST_DIAL, dispatchDial, responseVoid},//打電話的處理
......
{RIL_REQUEST_SEND_SMS, dispatchStrings,responseSMS}, //發簡訊的處理
......
根據上面的內容可知,在Rild中打電話的處理函式是dispatchDial,它的結果處理函式是responseVoid。
(2)Rild處理請求
Rild處理請求的程式碼如下所示:
[-->Ril.c]
static void dispatchDial (Parcel &p,RequestInfo *pRI) {
RIL_Dial dial; //建立一個RIL_Dial物件,它儲存打電話時所需要的一些引數。
RIL_UUS_Info uusInfo;
int32_t sizeOfDial;
int32_tt;
int32_t uusPresent;
status_t status;
memset(&dial, 0, sizeof(dial));
dial.address = strdupReadString(p);
status= p.readInt32(&t);
dial.clir = (int)t;
...... //中間過程我們略去
//呼叫RIL_RadioFunctions的onRequest函式,也就是向RefRil庫傳送一個請求。
s_callbacks.onRequest(pRI->pCI->requestNumber, &dial,sizeOfDial, pRI);
......
return;
}
下面去RefRil庫,看這個onRequest的處理:
[-->Reference_Ril.c]
static void onRequest (int request, void *data,size_t datalen, RIL_Token t)
{
ATResponse *p_response;
interr;
......
switch(request) {
......
case RIL_REQUEST_DIAL: //打電話處理
requestDial(data, datalen, t);
break;
......
caseRIL_REQUEST_SEND_SMS: //發簡訊處理
requestSendSMS(data, datalen, t);
break;
default:
RIL_onRequestComplete(t, RIL_E_REQUEST_NOT_SUPPORTED, NULL, 0);
break;
}
}
//我們看看requestDial函式
static void requestDial(void *data, size_tdatalen, RIL_Token t)
{
RIL_Dial*p_dial;
char*cmd;
constchar *clir;
intret;
p_dial= (RIL_Dial *)data;
......
//at_send_command將往串列埠裝置傳送這條AT指令
ret =at_send_command(cmd, NULL);
free(cmd);
/*
對於dail請求,把資料傳送給串列埠就算完成了,所以dial傳送完資料後直接呼叫
RIL_onRequestComplete函式來通知請求處理的結果。而有一些請求需要先由
AT模組的readLoop執行緒從串列埠中讀取BP的處理結果後再行通知。
*/
RIL_onRequestComplete(t, RIL_E_SUCCESS, NULL, 0);
}
看RIL_onRequestComplete函式,這個函式的實現由RIL_Env提供。
[-->Ril.c]
extern "C" void
RIL_onRequestComplete(RIL_Token t, RIL_Errno e,void *response,
size_t responselen) {
RequestInfo *pRI;
intret;
size_terrorOffset;
pRI= (RequestInfo *)t;
//由於已經收到了請求的處理結果,這表明該請求已經完成,所以需要從請求佇列中去掉這個請求。
if(!checkAndDequeueRequestInfo(pRI)) {
}
......
if(pRI->cancelled == 0) {
Parcel p;
p.writeInt32 (RESPONSE_SOLICITED);
p.writeInt32 (pRI->token);
errorOffset = p.dataPosition();
p.writeInt32 (e);
if(response != NULL) {
//dial請求的responseFunction函式是responseVoid,讀者可以看這個函式
ret =pRI->pCI->responseFunction(p, response, responselen);
if (ret != 0) {
p.setDataPosition(errorOffset);
p.writeInt32 (ret);
}
}
......
sendResponse(p);//將結果傳送給Java的RIL
}
done:
free(pRI);
}
Rild內部也採用了非同步請求/處理的結構,這樣做有它的道理,因為有一些請求執行的時間較長,例如在訊號不好的地方搜尋網路訊號往往會花費較長的時間。採用非同步的方式,能避免工作執行緒阻塞在具體的請求函式中,從而騰出手來做一些別的工作。
(3)Java RIL處理完成的通知
Rild將dial請求的結果,通過socket傳送給Java中的RIL物件。前面說過,RIL中有一個接收執行緒,它收到資料後會呼叫processResponse函式進行處理,看這個函式:
[-->RIL.java]
private void processResponse (Parcel p) {
int type;
type = p.readInt();
if(type == RESPONSE_UNSOLICITED) {
processUnsolicited (p);
}else if (type == RESPONSE_SOLICITED) {
processSolicited (p); //dial是應答式的,所以走這個分支
}
releaseWakeLockIfDone();
}
private void processSolicited (Parcel p) {
int serial, error;
boolean found = false;
serial= p.readInt();
error= p.readInt();
RILRequestrr;
//根據完成通知中的請求包編號從請求佇列中去掉對應的請求,以釋放記憶體
rr= findAndRemoveRequestFromList(serial);
Objectret = null;
if(error == 0 || p.dataAvail() > 0) {
try {
switch (rr.mRequest) {
......
//呼叫responseVoid函式處理結果
caseRIL_REQUEST_DIAL: ret = responseVoid(p);break;
......
if (rr.mResult != null) {
/*
RILReceiver執行緒將處理結果投遞到一個Handler中,這個Handler屬於
另外一個執行緒,也就是處理結果最終將交給另外一個執行緒做後續處理,例如切換介面顯示等工作,
具體內容就不再詳述了。為什麼要投遞到別的執行緒進行處理呢?因為RILReceiver
負責從Rild中接收資料,而這個工作是比較關鍵的,所以這個執行緒除了接收資料外,最好
不要再做其他的工作了。
*/
AsyncResult.forMessage(rr.mResult, ret,null);
rr.mResult.sendToTarget();
}
rr.release();
}
例項分析就到此為止。相信讀者已經掌握了Rild的精髓。
9.3.7 Rild總結
從整體來說,Rild並不複雜,其程式框架非常清晰,它和其他系統惟一不同的是,Rild採用了非同步請求/處理的工作方式,而非同步方式對程式碼編寫能力的要求是幾種I/O模式中最高的。讀者在閱讀Rild這一節內容時,要牢記非同步處理模式的流程。
另外,和Rild對應的Java中的Phone程式非常複雜,個人甚至覺得有些過於複雜了。讀者如有興趣,可以看看Phone的程式碼,寫得很漂亮,其中也使用了很多設計模式方面的東西,但我覺得這個Phone應用在設計上,還有很多地方可以改進。這一點,在擴充思考部分再來討論。
9.4 擴充思考
本章的擴充思考包括,嵌入式系統的儲存知識介紹以及Phone應用改進探討兩部分。
9.4.1 嵌入式系統的儲存知識介紹
用adb shell登入到我的G7手機上,然後用mount檢視資訊後,會得到如圖9-10所示的結果:
圖9-10 mount命令的執行結果
其中,可以發現系統的幾個重要的分割槽,例如/system對應的裝置是mtdblock3,那麼mtdblock是什麼呢?
1. MTD的介紹②
Linux系統提供了MTD(Memory Technology Device,記憶體技術裝置)系統來建立針對Flash裝置的統一、抽象的介面,也就是說,有了MTD,就可以不用考慮不同Flash裝置帶來的差異了,這一點和FBD(FrameBuffer Device)的作用很類似。下面看Linux MTD的系統層次圖,如圖9-11所示。
圖9-11 Linux MTD系統層次圖
從上圖中可以看出:
· MTD將檔案系統與底層的Flash儲存器進行了隔離,這樣應用層就無須考慮真實的硬體情況了。
· 圖9-11中的mtdblock表示MTD塊裝置。
有了MTD後,就不用關心Flash是NOR還是NAND了。另外,我們從圖9-10“mount命令的執行結果”中還可看見mount指定的檔案系統中有一個yaffs2,它又是什麼呢?
2. Flash檔案系統③
先來說說Flash的特性。常見的檔案系統(例如FAT32、NTFS、Ext2等)是無法直接用在Flash裝置上的,因為無法重複地在Flash的同一塊儲存位置上做寫入操作(必須事先擦除該塊後才能寫入)。為了能夠在Flash裝置上使用這些檔案系統,必須透過一層轉換層(TranslationLayer),將邏輯塊地址對應到Flash儲存器的實體地址上,以便系統能把Flash當做普通的磁碟處理,可稱這一層為FTL(Flash Translation Layer)。Flash轉換層的示意圖如圖9-12所示:
圖9-12 FTL和NFTL
從上圖中可以看到:
· 如果想使用FAT32或NTFS檔案系統,必須通過FTL或NTFL進行轉換,其中FTL針對NORFlash,而NTFL針對NAND Flash。
· 儘管有了FTL,但畢竟多了一層處理,這樣對I/O效率的影響較大,所以人們開發了專門針對Flash的檔案系統,其中YAFFS就是應用比較廣泛的一種。
YAFFS是Yet Another Flash File System的簡稱,目前有YAFFS和YAFFS2兩個版本。這兩個版本的主要區別是,YAFFS2可支援大容量的NADN Flash,而YAFFS只支援頁的大小為512位元組的NAND Flash。YAFFS使用OOB(Out Of Bind)來組織檔案的結構資訊,所以在Vold程式碼中,可以見到OOB相關的字樣。
關於嵌入式儲存方面的知識就介紹到這裡。有興趣深入瞭解的讀者可閱讀有關驅動開發方面的書籍。
3. Android mtd裝置的介紹
這裡以我的HTC G7手機為例,分析Android系統中MTD裝置的使用情況。
通過adb cat /proc/mtd,得到圖9-13所示的MTD裝置的使用情況:
圖9-13 G7 MTD裝置使用情況
這幾個裝置對應儲存空間的大小和作用如下:
· MTD0,主要用於儲存開機畫面。此開機畫面在Android系統啟動前執行,由Bootloader呼叫,大小為1MB。
· MTD1,儲存恢復模式的映象,大小為4.5MB。
· MTD2,儲存kernel映象,大小為3.25MB。
· MTD3,儲存sytem映象,該分割槽掛載在/system目錄下,大小為250MB。
· MTD4,緩衝臨時檔案,該分割槽掛載在/cache目錄下,大小為40MB。
· MTD5,儲存使用者安裝的軟體和一些資料,我的G7把這個裝置掛載在/mnt/asec/mtddata目錄下,大小為150.75MB。
注意,上面的裝置和掛載點與具體的機器及所刷的ROM有關。
9.4.2 Rild和Phone改進探討
在使用G7的時候,最不滿意的就是,群發簡訊的速度太慢,而且有時會出現ANR的情況,就G7的硬體配置來說,按理不至於發生這種情況。原因究竟何在?通過對Rild和Phone的分析認為,原因和Rild以及Phone的設計有些許關係,下面來探討一下這個問題。
以Rild和RefRil庫為例,來分析Rild和Phone的設計上有哪些特點和問題。注意,這裡,將簡訊程式和Phone程式統稱為Phone。
· Rild沒有使用Binder通訊機制和Phone進行互動,這一點,雖感覺較奇怪,不過也好理解,因為實現一個用Socket進行IPC通訊的架構,比用Binder要簡單,至少程式碼量要少一些。
· Rild使用了非同步請求/處理的模式,這種模式對於Rild來說是合適的。因為一個請求的處理可能耗時很長,另外一點就是Rild會收到來自BP的unsolicited Response。
· Phone這個應用也使用了非同步模式。其實,這也好理解,因為Phone和Rild採用了Socket進行通訊,如把Phone中的Socket看做是Rild中的串列埠裝置,就發現這個Phone竟然是Rild在Java層的翻版。這樣設計有問題嗎?其明顯缺陷就是一個請求訊息在Java層的Phone中要儲存一個,傳遞到Rild中還要儲存一個。另外,Phone和Rild互動的是AT命令。這種直接使用AT命令的方式,對以後的擴充套件和修改都會造成不少麻煩。
· 再來看群發簡訊問題。群發簡訊的實現,就是同一個資訊傳送到不同的號碼。對於目前Phone的實現而言,就是一個for迴圈中呼叫一個傳送函式,引數中僅有號碼不同,而簡訊內容是一樣的。這種方式是否太浪費資源了呢?假設群發目標人數為二百個,那麼Java層要儲存二百個請求資訊,而Rild層也要儲存二百個請求資訊。並且Rild每處理一個命令就會來一個完成通知。對於群發簡訊功能來說,本人更關心的是,所有簡訊傳送完後的統一結果,而非單條簡訊傳送的結果。
以上是我關於Rild和Phone設計特點的一些總結。如果由我來實現Phone,該怎麼做呢?這裡,願將自己的一些想法與讀者分享。
· 在Phone和Rild的程式間通訊上,將使用Binder機制。這樣,需首先定義一個Phone和Rild通訊的介面,這個介面的內容和Rild提供的服務有關,例如定義一個dial函式,定義一個sendSMS函式。除此之外,需要定義Rild向Phone回傳Response的通知介面。也就是說,Rild直接利用Binder回撥Phone程式中的函式,把結果傳過去。採用Binder至少有三個好處。第一,Phone和Rild的互動基於介面函式,就不用在Phone中做AT命令的轉換了,另外基於介面的互動使得程式的可擴充套件性也得到了提高。第二,可以定義自己的函式,例如提供一個函式用來實現群發簡訊,通過這個函式可將一條簡訊內容和多個群發目標打包傳遞給Rild,然後由Rild自己去解析成多條AT命令進行處理。第三,Phone程式碼將會被精簡不少。
· 在記憶體使用方面,有可能Phone和Rild都需儲存請求,這時可充分利用共享記憶體的優勢,將請求資訊儲存在共享記憶體中。這樣,可減少一部分記憶體使用。另外,這塊記憶體中儲存的資訊可能需要使用一定的結構來組織,例如採用優先順序佇列。這樣,那些優先順序高的請求就能首先得到處理了。
以上是本人在研究Rild和Phone程式碼過程中一些不成熟的想法,希望能引起讀者共同的思考。讀者還可以參考網上名為《RIL設計思想解析》的一篇文章。
9.5 本章小結
本章對Vold和Rild兩個重要的daemon程式進行了分析。其中:
· Vold負責Android平臺上儲存系統的管理和控制。重點關注Vold的兩方面的內容,一是它如何接收和處理來自核心的Uevent事件,一是如何處理來自Java層MountService的請求。
· Rild是Android平臺上的射頻通訊控制中樞,接打電話、收發簡訊等,都需要Rild的參與。對Rild的架構進行了重點分析,尤其對非同步請求/響應的知識進行了較詳細的介紹。另外,還分析了Phone中撥打電話的處理流程。
本章擴充部分,首先介紹了嵌入式系統中和儲存,檔案系統相關的知識。另外,還探討了Phone和Rild設計的特點以及可以改進的某些地方。
相關文章
- [深入理解Android卷一全文-第十章]深入理解MediaScannerAndroid
- [深入理解Android卷二 全文-第五章]深入理解PowerManagerServiceAndroid
- [深入理解Android卷一全文-第八章]深入理解Surface系統Android
- [深入理解Android卷一全文-第七章]深入理解Audio系統Android
- [深入理解Android卷二 全文-第六章]深入理解ActivityManagerServiceAndroid
- [深入理解Android卷二 全文-第四章]深入理解PackageManagerServiceAndroidPackage
- [深入理解Android卷二 全文-第三章]深入理解SystemServerAndroidServer
- [深入理解Android卷二 全文-第二章]深入理解Java Binder和MessageQueueAndroidJava
- [深入理解Android卷二 全文-第一章]開發環境部署Android開發環境
- 深入理解AndroidAndroid
- 深入理解HashMap(一)HashMap
- 深入理解 GCD(一)GC
- 《深入理解Android:卷IIIA》一一2.4本章小結Android
- 深入學習和理解 ReduxRedux
- 深入理解Java反射(一)Java反射
- Android 深入理解 Notification 機制Android
- 深入理解 Android 中的 MatrixAndroid
- Blocks深入理解和詳解BloC
- 深入理解原型和原型鏈原型
- 深入理解Java的==和equalsJava
- 深入理解Isolate
- 深入理解HashMapHashMap
- 深入理解TransformORM
- 深入理解KVO
- 深入理解 JVMJVM
- 深入理解 GitGit
- 深入理解AQSAQS
- 深入理解JVMJVM
- 深入理解 TypeScriptTypeScript
- 深入理解JavaScriptCoreJavaScript
- 深入理解MVCMVC
- 深入理解 PWA
- 深入理解margin
- 深入理解ReactReact
- 深入理解BFC
- 深入理解reduxRedux
- BFC深入理解
- 深入理解 GCDGC