Mozilla Firefox os系統構架詳解

原文地址: http://www.firefoxos.cc/thread-711-1-1.html

Terminology(術語)

Gaia : B2G系統的用戶界面。B2G系統啓動後,手機屏幕上所繪製的所有內容都屬於Gaia的一部分。也就是說在B2G系統中,用戶所見到的幾乎所有的UI界面都是基於Gaia實現的。Gaia實現了鎖屏、主屏、撥號鍵盤、短信應用、相機應用......。Gaia是完全由HTML、CSS和JAVASCRIPT實現的,Open Web APIs(由Gecko實現)是它與操作系統(即:系統內核)之間通訊的唯一接口。 Gaia 可以完美的運行於B2G之上,除此之外,由於它是基於標準的Web API實現的,因此可以在其他的操作和瀏覽器上工作,第三方應用也可以與 Gaia一起被安裝。

Gecko : B2G的應用運行時環境(application runtime)。 站在比較高的角度來看,Gecko實現了HTML、CSS和JS的開放標準並且使這些接口在所有Gecko支持的系統中運行良好。這就是說Gecko包含了: 網絡棧、 圖形棧、佈局引擎、 JS虛擬機以及端口層。

Gonk : B2G系統的底層。Gonk由linux 內核和用戶空間硬件抽象層(HAL)組成。內核和幾個用戶空間庫包含一些常見的開源項目:: linux, libusb, bluez, 等。HAL中的一些部與Android項目中是一樣的,比如說: GPS, camera, 等,在HAL層,除了極個別的之外。你可以認爲Gonk是一個極端簡化過的linux分發版。 Gonk is a porting target of Gecko; there is a port of Gecko to Gonk, just like there is a port of Gecko to OS X, and a port of Gecko to Android. Since the b2g project has full control over Gonk, we can expose interfaces to Gecko that aren't possible to expose on other OSes. For example, Gecko has direct access to the full telephony stack and display framebuffer on Gonk, but doesn't have this access on any other OS.

Booting(啓動)

開機後,主引導程序開始執行。接着,引導主系統內核的過程以通常的方式進行: 一系列高級bootloader依次引導鏈中的下一個引導程序。在執行的最後過程,執行由bootloader轉交到linux內核。.

對於啓動過程沒有什麼可說的,但是,下面這些內容有必要了解一下。

  • Bootloader通常用於在設備啓動過程中顯示用於展示廠商logo等信息的啓動畫面(splash screen)。
  • Bootloader實現了映像到設備的映射。不同的設備使用不同的協議。大部分手機使用的都是fastboot協議,但在三星的 Galaxy S II手機上, 使用的事 "odin" 協議。
  • 在引導過程結束的時候,modem映像文件通常會被加載並在modem處理器中運行。至於modem映像的加載以及是如何在modem處理器上運行,是與特定的設備相關的,也是設備專有的。


Kernel (Linux)

Gonk中的linux內核一定程度上接近upstream(The Linux community) linux,但是,AOSP(Android Open Source Project)做了少量的修改,這些修改並不在upstream linux之中。供應商也在按照他們自己的計劃修改linux內核和 upstream。不過,總的來說,Gonk中linux的內核與倉庫中的還是非常接近的。

Linux的啓動過程在互聯網上有大量詳細的文檔介紹,因此這裏就不再包含。在內核啓動的最後,如同其他類Unix系統一樣,用戶空間中的init進程被啓動。此時,只有內存虛擬盤(ramdisk)被掛載。Ramdisk在B2G系統構建的過程中被構建,包含了關鍵工具(critical utilities (like init))、其他啓動腳本腳本和可裝載內存模塊(loadable kernel modules)。

在init進程啓動後,linux內核服務系統可以從硬件設備從用戶空間和中斷等調用。許多設備通過sysfs(documented elsewhere on the internet)暴露給用戶空間。例如:下面是一些Gecko中獲取電池狀態的代碼 (lnk to original code)

  1. FILE *capacityFile = fopen("/sys/class/power_supply/battery/capacity", "r");
  2.   double capacity = dom::battery::kDefaultLevel * 100;
  3.   if (capacityFile) {
  4.     fscanf(capacityFile, "%lf", &capacity);
  5.     fclose(capacityFile);
  6.   }
複製代碼

init(初始化)

在Gonk中,init進程掛載必要的文件系統,孵化系統服務並且在服務啓動後充當服務管理器。這與其他類Unix系統中的init進程非常相似。init解析init*.rc腳本(rc腳本有一系列的命令組成,可以參加linux中的相關介紹)。下面是 三星 Galaxy S II 的注意腳本。

Main init script for the Galaxy S II

特別的,就像下面介紹的,init負責啓動B2G進程。下面是啓動B2G的代碼片段。

  1. service b2g /system/b2g/b2g
  2.     onrestart restart media
複製代碼

Userspace process architecture(用戶控件進程架構)

此時,我們後退一小步,從比較高的層次上看一下B2G中的各種組件是如何協調工作的。

下圖展示了B2G系統用戶空間中的主要進程。其中,虛線表示被init孵化的進程,實線表示其他的通信管道。

b2g進程是主系統進程( the main system process)。它具有很好的運行優先級:它可以方法大多數硬件設備。b2g與modem進行通信,繪製到顯示用的幀緩衝器以及與GPS、相機和其他設備進行交互。在內部,b2g運行Gecko代碼(libxul.so)。下面討論b2g與硬件進行通信的詳細內容。
b2g進程可能會會孵化一些低權限的內容進程(content processes)。Web應用和其他一些網頁內容就是在這些進行中被加載。這些內容進程通過IPDL(一個消息傳遞系統)與主Gecko服務進程通信。

rild進程是訪問modem處理器的接口。 "RIL" 是無線接口層(radio interface layer"),"rild" 是Ril守護進程(RIL daemon)。 這是由硬件廠商實現的一段代碼。. rild 允許客戶端連接到它所綁定的UNIX-domain socket

  1. service ril-daemon /system/bin/rild
  2.     socket rild stream 660 root radio
複製代碼

在B2G中,rild客戶端是rilproxy進程,它僅僅在rild和b2g之間充當了一個靜默轉發代理(dumb forwarding proxy)。爲什麼我們需要這個代理是一個實現細節,並不重要。. 代碼在這裏.。

mediaserver進程控制音頻和視頻的播放,Gecko通過RPC機制與它進行交互。 mediaserver部分的代碼在這裏,感興趣的讀者可以自行研究。. Gecko支持的一些媒體(OGG Vorbis audio, OGG Theora video, and WebM video)由Gecko負責解碼並直接發送到mediaserver,其他媒體文件由libstagefright解碼,它能訪問專有解碼器和硬件解碼器。(注意:長遠的來看,B2G在當前的實現中,將不會使用mediaserver進程)

netd進程用於配置網絡接口。wpa_supplicant是通過端點連接到WiFi的標準UNIX-ish 守護進程。


Gecko: Processing input events(處理輸入事件)

Gecko中的大部分動作都是由輸入事件觸發的,這些事件包括:按下按鈕、觸摸觸摸屏等等。輸入事件通過"app shell" (在b2g進程中)進入Gonk。例如:

  1. void
  2. GeckoInputDispatcher::notifyKey(nsecs_t eventTime,
  3.                                 int32_t deviceId,
  4.                                 int32_t source,
  5.                                 uint32_t policyFlags,
  6.                                 int32_t action,
  7.                                 int32_t flags,
  8.                                 int32_t keyCode,
  9.                                 int32_t scanCode,
  10.                                 int32_t metaState,
  11.                                 nsecs_t downTime)
  12. {
  13.     UserInputData data;
  14.     data.timeMs = nanosecsToMillisecs(eventTime);
  15.     data.type = UserInputData::KEY_DATA;
  16.     data.action = action;
  17.     data.flags = flags;
  18.     data.metaState = metaState;
  19.     data.key.keyCode = keyCode;
  20.     data.key.scanCode = scanCode;
  21.     {
  22.         MutexAutoLock lock(mQueueLock);
  23.         mEventQueue.push(data);
  24.     }
  25.     gAppShell->NotifyNativeEvent();
  26. }
複製代碼

這些輸入事件起源於標準的linux輸入事件系統(the standard linux input_event system),由輸入設備驅動( input-device drivers)對這些事件進行轉發(dispatcher)。我們在提供了一些良好特性的事件(如:事件過濾器)之上,使用了一個簡單抽象。下面的代碼展示了輸入事件是如何產生的。

  1. if (pfd.revents & POLLIN) {
  2.                 int32_t readSize = read(pfd.fd, mInputBufferData,
  3.                         sizeof(struct input_event) * INPUT_BUFFER_SIZE);
  4.                 if (readSize < 0) {
  5.                     if (errno != EAGAIN && errno != EINTR) {
  6.                         LOGW("could not get event (errno=%d)", errno);
  7.                     }
  8.                 } else if ((readSize % sizeof(struct input_event)) != 0) {
  9.                     LOGE("could not get event (wrong size: %d)", readSize);
  10.                 } else {
  11.                     mInputBufferCount = readSize / sizeof(struct input_event);
  12.                     mInputBufferIndex = 0;
  13.                 }
  14.             }
  15.         }
複製代碼

在被Gecko的讀取之後,輸入在這裏被分發到DOM。

  1. static nsEventStatus
  2. sendKeyEventWithMsg(PRUint32 keyCode,
  3.                     PRUint32 msg,
  4.                     uint64_t timeMs,
  5.                     PRUint32 flags)
  6. {
  7.     nsKeyEvent event(true, msg, NULL);
  8.     event.keyCode = keyCode;
  9.     event.time = timeMs;
  10.     event.flags |= flags;
  11.     return nsWindow::DispatchInputEvent(event);
  12. }
複製代碼

從這裏開始,事件或者被Gecko內部消耗(consumed),或者以DOM事件的形式被分發到Web應用。


Gecko: Graphics(圖形)

在非常底層的地方,Gecko使用OpenGL ES 2.0進行繪製,繪製到一個包含硬件幀緩衝的叫做圖形上下文(glcontext )的地方。Gecko設置圖形上下文( graphics context)的代碼 如下

  1.     gNativeWindow = new android::FramebufferNativeWindow();
  2.         sGLContext = GLContextProvider::CreateForWindow(this);
複製代碼

FramebufferNativeWindow的實現在這裏。它使用了到圖形驅動的 "gralloc"硬件接口映射幀緩衝到緩衝區。

Gecko使用它的圖層系統將內容進行組合,繪製顯示到屏幕上。關於圖層的詳細介紹已經超出了本文的範圍,感興趣的讀者可以自行查閱相關資料。下面做一個簡單的介紹:

  • Gecko繪製不同的頁面區域到內存緩衝中。有時,這些緩衝就是系統內存;另外一些時候,他們是映射到Gecko地址空間的紋理(textures) 。即:Gecko直接繪製到VRAM(Video Random Access Memory:顯存)。通常情況下,繪製發生在這裏
  • Gecko使用GL指令組合這些紋理到屏幕上。上面繪製的像素的組合發生在這裏

The details of how Gecko actually draws web content are beyond the scope of this document.


Gecko: Hardware Abstraction Layer (hal:硬件抽象層)

(注意:請不要混淆Gecko的硬件抽象層與Gonk的硬件抽象層。爲了避免這種混淆的發生,Gecko硬件層將被成爲 "hal",而Gonk的硬件抽象層將被稱爲 "HAL"。)

"hal"位於Gecko的移植層( porting layers)。它被低層通過多平臺訪問系統接口。hal爲Gecko中的更高層次提供了一個跨平臺的C++ API,這些API實在hal內部實現的,而實現的本身是與平臺相關的。hal沒有被直接暴露給Gecko中除C++代碼之外的任何東西,換句話說,hal只能被Gecko中的C++代碼所訪問。

通過下面的例子我們可以更好的理解hal。在這個例子中使用了vibration API。hal 中vibration API在這裏

  1. void Vibrate(const nsTArray<uint32>& pattern);
複製代碼

(注意:真正的API比這裏要稍微複雜一些。爲了便於理解,與本例無關的比較複雜的API在舉例時進行的刪除)

這個API根據pattern中所指定的樣式(pattern)控制馬達(vibration motor)開和關。Gonk中的API實現在此

  1. void
  2. Vibrate(const nsTArray<uint32> &pattern)
  3. {
  4.   EnsureVibratorThreadInitialized();
  5.   sVibratorRunnable->Vibrate(pattern);
  6. }
複製代碼

這裏發生“關”請求到另一個線程, 也就是真正工作的地方

  1. while (!mShuttingDown) {
  2.     if (mIndex < mPattern.Length()) {
  3.       uint32 duration = mPattern[mIndex];
  4.       if (mIndex % 2 == 0) {
  5.         vibrator_on(duration);
  6.       }
  7.       mIndex++;
  8.       mMonitor.Wait(PR_MillisecondsToInterval(duration));
  9.     }
  10.     else {
  11.       mMonitor.Wait();
  12.     }
  13.   }
複製代碼

vibrator_on()是打開馬達的Gonk HAL API 。 vibrator_on() 寫了一個值到通過sysfs暴露的內存對象中,達到往kernel driver發送一條消息的效果。

hal API被所有的平臺所支持。Gecko在一個沒有提供震動馬達接口的平臺上被構建時,hal API的反饋("fallback" )將被使用。 對於震動, 這裏

  1. void
  2. Vibrate(const nsTArray<uint32>& pattern)
  3. {}
複製代碼

大多數web內容運行在低權限的內容進程中(如上)。我們可以認爲這些內容進程( content processes)擁有開啓震動馬達的必要的特權,並且我們也想在中心位置解決競爭條件。在hal中,這些都是在hal中實現的沙箱( "sandbox")中完成的。沙箱的實現方案僅僅代理從內容進程到"Gecko server"進程的請求。代理請求通過IPDL被髮送。對於震動,請求在這裏被觸發:

  1. void
  2. Vibrate(const nsTArray<uint32>& pattern)
  3. {
  4.   Hal()->SendVibrate(pattern);
  5. }
複製代碼

我們發生一條由如下接口定義的信息(message):

  1. Vibrate(uint32[] pattern);
複製代碼

信息接收的定義在這裏

  1. NS_OVERRIDE virtual bool
  2.   RecvVibrate(const InfallibleTArray<unsigned int>& pattern)
  3.   {
  4.     // Forward to hal::, not hal_impl::, because we might be a
  5.     // subprocess of another sandboxed process. The hal:: entry point
  6.     // will do the right thing.
  7.     hal::Vibrate(pattern);
  8.     return true;
複製代碼

(omitting some details that aren't relevant to this discussion). So a vibration request sent by a content process on the Gecko port to Gonk eventually ends up in the GonkHal implementation of Vibrate(), discussed above.

Gecko: DOM APIs

"DOM interfaces", approximately, are how web content communicates with Gecko. There's a lot more to the DOM than that, but the longer discussion is beyond the scope of this document. DOM interfaces are defined in IDL, which comprises both a foreign function interface (ffi) and object model (OM) between JavaScript and C++. Again, there are more details that could be discussed here which are beyond the scope of this document. Let's learn a bit of IDL by example.

The very simple vibration API is exposed to web content through an IDL interface. That interface is here

  1. [implicit_jscontext]
  2.   void mozVibrate(in jsval aPattern);
複製代碼

The jsval argument indicates that mozVibrate (our vendor-prefixed implementation of the vibration specification) accepts any JS value. Details of working with jsvals are beyond the scope of this document. The IDL compiler generates a C++ interface that's implemented here

  1. NS_IMETHODIMP
  2. Navigator::MozVibrate(const jsval& aPattern, JSContext* cx)
  3. {
  4.   // ...
  5.   hal::Vibrate(pattern);
  6.   return NS_OK;
  7. }
複製代碼

There's quite a lot of code that's hidden here by the ellipsis ("..."), but let's ignore for it now.

The call to hal::Vibrate() transfers control from the DOM to hal. From there, we enter the hal implementation discussed above. A key point to note here is that the DOM implementation doesn't care what platform it's running on (Gonk or Windows or OS X or ...), nor does it care whether the code is running in a "content process" or the Gecko "server process". This is all left to lower levels of the system.

The vibration API happens to be quite simple, so it's a good example. The SMS API, which is much more complicated and has its own "remoting" layer from content processes to the server, is here.


RIL: Telephony

The RIL was discussed briefly above. In this section, we'll how the various pieces interact in a little more detail. The main actors are

  • rild : the proprietary bit of code that talks to the proprietary modem firmware
  • rilproxy : the daemon that proxies messages between rild and Gecko (the b2g process)
  • Gecko (the b2g process) : implements the higher-level telephony stack

Let's start with an example that demonstrates the lower-level parts of the system. When the modem receives an incoming call, it notifies the rild using a proprietary mechanism. The rild then prepares a message for its client according to the "open" protocol here. In this case, an incoming call generates theRIL_UNSOL_RESPONSE_CALL_STATE_CHANGED message. This message is sent by rild to its client and received by rilproxy here

  1. ret = read(rilproxy_rw, data, 1024);
  2.           if(ret > 0) {
  3.             writeToSocket(rild_rw, data, ret);
  4.           }
複製代碼

where it's forwarded along to Gecko, on the socket connecting rilproxy and Gecko. Gecko receives these forwarded bytes here

  1. int ret = read(fd, mIncoming->mData, 1024);
  2.             // [handle errors]
  3.             mIncoming->mSize = ret;
  4.             sConsumer->MessageReceived(mIncoming.forget());
複製代碼

The consumer is here, and repackages the message and dispatches it to a "worker thread" that implements the RIL state machine

  1.   virtual void MessageReceived(RilRawData *aMessage) {
  2.     nsRefPtr<DispatchRILEvent> dre(new DispatchRILEvent(aMessage));
  3.     mDispatcher->PostTask(dre);
  4.   }
複製代碼

The task posted to that thread is sent into the JS code on the RIL worker here

  1. return JS_CallFunctionName(aCx, obj, "onRILMessage", NS_ARRAY_LENGTH(argv),
  2.                              argv, argv);
複製代碼

which calls a function onRILMessage() in JS. That function is defined here. The message bytes are processed a bit, until finally we dispatch the messagehere

  1. handleParcel: function handleParcel(request_type, length) {
  2.     let method = this[request_type];
  3.     if (typeof method == "function") {
  4.       if (DEBUG) debug("Handling parcel as " + method.name);
  5.       method.call(this, length);
  6.     }
  7.   }
  8. };
複製代碼

which calls into a function defined here

  1. RIL[UNSOLICITED_RESPONSE_CALL_STATE_CHANGED] = function UNSOLICITED_RESPONSE_CALL_STATE_CHANGED() {
  2.   Phone.onCallStateChanged();
  3. };
複製代碼

and then we bounce around for a bit and get to

  1. getCurrentCalls: function getCurrentCalls() {
  2.     Buf.simpleRequest(REQUEST_GET_CURRENT_CALLS);
  3.   },
複製代碼

This sends a request back to the rild to request the state of all currently-active calls. The request follows a similar path back to rild (out from ril_worker.js, to SystemWorkerManager.cpp, off to Ril.cpp, then to rilproxy.c and finally written to the rild socket). The response from rild is processed similarly to above, and then finally we detect that a new call has been received and notify the DOM here

  1. _handleChangedCallState: function _handleChangedCallState(changedCall) {
  2.     let message = {type: "callStateChange",
  3.                    call: {callIndex: changedCall.callIndex,
  4.                           state: changedCall.state,
  5.                           number: changedCall.number,
  6.                           name: changedCall.name}};
  7.     this.sendDOMMessage(message);
  8.   },
複製代碼

This sets off a chain of events that results in the currently-active dialer application being notified of an incoming call. In gaia, this notification is receivedhere

  1. handleEvent: function fm_handleEvent(evt) {
  2.     console.log('Call changed state: ' + evt.call.state);
  3.     switch (evt.call.state) {
  4.       case 'incoming':
  5.         console.log('incoming call from ' + evt.call.number);
  6.         this.incoming(evt.call);
  7.         break;
  8.       case 'connected':
  9.         this.connected();
  10.         break;
  11.       case 'disconnected':
  12.         this.disconnected();
  13.         break;
  14.       default:
  15.         break;
  16.     }
  17.   },
複製代碼

at the 'incoming' case in the switch statement.


RIL: 3G Data

There is a RIL message that places a "data call" to the cellular tower, which enables data-transfer mode in the modem. This data call ends up creating/activating a PPP interface device in the linux kernel that can be configured through usual interfaces. TODO

WiFi

Note: Much of the interesting stuff here depends deeply on the possible state changes in wpa_supplicant.

Overview

The wifi backend for B2G simply uses wpa_supplicant to do all of the heavy lifting. That means that its main purpose is to simply manage supplicant (and do some auxiliary tasks like loading the wifi driver and enabling or disabling the network interface). Effectively, this means that the backend is a state machine, with the states following the state of the supplicant. Bugs in the backend tend to stem from the supplicant following a state change that the code wasn't prepared to deal with.

The implementation of the wifi component is broken up in two files:

  1. - DOMWifiManager.js - The implementation of the API exposed to web
  2.                       pages (defined in nsIWifi.idl).
  3. - WifiWorker.js - The implementation of the state machine and the code
  4.                   that drives the supplicant.
複製代碼

The two files talk to each other via the message manager. The backend listens for messages requesting certain actions, such as associate and responds with a message when it's done. The DOM side listens for the response methods as well as several "event" messages indicating state changes and information updates. Note that one side effect of this communication is that any synchronous DOM APIs are implemented by caching data on that side of the pipe. In general, we avoid synchronous messages.

WifiWorker.js

This file implements the main logic behind the wifi. That means that it runs in the chrome process (in e10s builds) and is instantiated by the SystemWorkerManager. The file is generally broken into two sections: a giant anonymous function and WifiWorker (and its prototype). The giant anonymous function, ends up being the WifiManager. It provides a local API including notifications for events like connection to the supplicant and scan results being available. In general, it contains relatively little logic, letting its one consumer "drive" while it notifies it of events and controls the details of the connection with the supplicant.

The second part of WifiWorker.js sits between the WifiManager and the DOM. It reacts to events and forwards them to the DOM and it receives requests from the DOM and performs the appropriate actions on the supplicant. It also maintains state about the wpa_supplicant and what it needs to do next.

DOMWifiManager.js

This file implements the DOM API, ferrying messages back and forth to the actual worker. There is very little logic in this file. That being said: one note: in order to avoid synchronous messages to the chrome process, we do need to cache the state based on the event that came in. There is a single synchronous message, sent at the time that the DOM API is instantiated in order to get the current state of the supplicant.

MISC

DHCP: DHCP (and DNS) is handled by dhcpcd (the standard Linux DHCP client). However, it isn't able to react when we lose the connection to a network. So we kill and restart dhcpcd for each time we connect to a given wireless network. dhcpcd is also responsible for setting the default route. We call into the network manager in order to tell the kernel about DNS servers.

Network Manager

The network manager configures network interfaces opened by the 3g-data and wifi components. TODO

發佈了98 篇原創文章 · 獲贊 3 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章