B2G Architecture

来源:互联网 发布:mac os固件验证失败 编辑:程序博客网 时间:2024/05/16 08:14
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 inIDL, 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 ishere

  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 implementedhere
  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" protocolhere. 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 byteshere
  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 DOMhere
  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