[深入理解Android卷一全文-第九章]深入理解Vold和Rild

阿拉神農發表於2015-08-02

由於《深入理解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設計的特點以及可以改進的某些地方。

 



該書中文版名為《UNIX網路程式設計第3版.第1卷,套接字聯網API》,人民郵電出版社,2009年版

參考資料為《Linux裝置驅動開發詳解》,宋寶華,第530頁-531頁,人民郵電出版社,2008年。

參考資料為《Linux裝置驅動開發詳解》,宋寶華,第556頁-560頁,人民郵電出版社,2008年。

相關文章