mojo-基础知识

Mojo

Mojo是一个跨平台的IPC框架,诞生于chromium,用来实现chromium进程内或进程间通信。目前也被用于Chromeos。

Mojo的分层

从图中看Mojo分为4层:

  1. Mojo Core:Mojo的实现层,不能独立使用,由** C++ **实现;
  2. Mojo System API(C):Mojo的C API层,它和Mojo Core对接,可以在程序中独立使用;
  3. Mojo System API(C++/Java/JS): Mojo的各种语言包装层,它将Mojo C API包装成多种语言的库,让其他语言可以使用。这一层也可以在程序中独立使用。
  4. Mojo Bindings: 这一层引入一种称为Mojom的idl(接口定义)语言,通过它可以定义通信接口,这些接口生成接口类,使用户只要实现这些接口就可以和Mojo进行通信,这一层使得IPC两端不需要通过原始字节流进行通信,而是通过接口进行通信,有些类似于Protobuf和Thrift。

除了上述提到的那些层之外,在Chromium中还有两个模块对Mojo进行了包装,分别是Services(//services)模块和IPC(//ipc)模块。

  1. services:一种更高层次的IPC机制,构建于Mojo之上,以service的级别来进行IPC通信,Chromium大量使用这种IPC机制来包装各种服务,用来取代Leagcy Chrome IPC,比如device服务,preferences服务,audio服务,viz服务。
  2. Leagcy Chrome IPC:已经不推荐使用的Chrome IPC机制,提供IPC::Channer接口以及大量的宏来定义messages类,目前底层也是基于Mojo来实现的,但是上层接口和旧的Chrome IPC保持一致。Chromium中还有很多IPC来使用这种方式,但是不应该在新的服务中使用这种机制。可以在ipc/ipc_message_start.h 中来查看还有哪些服务使用了这种机制。

Mojo在Chromium中的分层

在Chromium中,还有两个基础模块使用Mojo,分别是Services和IPC::Channel。

Mojo的设计

在使用Mojo之前,我们来看一下Mojo的设计,这对理解后面的使用至关重要。

Mojo支持在多个进程之间相互通信,这一点和其他的IPC有很大不同, 其他大多只支持两个进程之间进行通信。由Mojo组成的这些可以互相通信的进程就形成了一个网络,在这个网络内的任意两个进程都可以进行通信,并且每个进程只能处于一个mojo网络中,在这个网络内每一个进程内部有且只有一个Node,每一个Node都可以提供多个Port,每个Port都对应一种服务,这点类似于TCP/IP中的IP地址和端口的关系。一个Node:Port对可以唯一确定一个服务。Node和Node之间通过Channel来实现通信,在不同平台上channel有不同的实现方式,在Linux上是domain socket,在windows上是name pipe,在MAC os上是Mach Port。在Port上一层,Mojo封装了三个“应用层协议”,分别为MessagePipe,DataPipeSharedBuffer(类似于在TCP上封装了HTTP,SMTP)。整体结构如下图

上图展示了在两个进程之间使用Mojo的数据流。它有以下几个特点:

  1. Channel: Mojo内部的实现细节,对外不可见,用于包装系统底层的通信通道,在Linux下是domain socket,Windows下是name pipe,在Mac os下是mach port。
  2. Node: 每个进程都只有一个Node,它在Mojo中的作用相当于TCP/IP中的IP地址,同样是内部实现细节,对外不可见。
  3. Port: 每个进程可以有成千上百个Port,它在Mojo中的作用相当于TCP/IP的端口,每个Port都会对应一种应用层接口,目前Mojo支持三种应用层接口。
  4. MessagePipe: 应用层接口,用于进程间的双向通信,类似于UDP,消息是基于数据包的,底层使用Channel通道。
  5. DataPipe: 应用层接口,用于进程间单向块数据传递,类似TCP,消息是基于数据流的,底层使用系统的Shared Memory实现
  6. SharedBuffer: 应用层接口,支持双向块数据传递,底层使用系统Shared Memory实现;
  7. MojoHandle: 所有的 MessagePipe,DataPipe,SharedBuffer 都使用MojoHandle来包装,有了这个Handle就可以对它们进行读写操作。还可以通过MessagePipe将MojoHandle发送到网络中的任意进程。
  8. PlatformHandle: 用来包装系统的句柄或文件描述符,可以将它转换为MojoHandle然后发送到网络中的任意进程。

MessagePipe

一个进程中可以有N多个MessagePipe,所有的都共享底层的一条通信通道,就像下图这样

Mojo保证同一个MessagePipe中的数据发送和接收顺序一致,但是不保证多个MessagePipe之间的数据的有序

Mojo的应用

Mojo不仅可以在Chromium中使用,也可以在任何第三方程序中使用,因为其本身不依赖于Chromium中的业务逻辑部分。不过由于其源码在Chromium中,使用起来可能不是那么方便。

Mojo提供了不同层次的API,外部可以根据自己的需要选择使用的层次,下面我们简单介绍每种API的使用方法

初始化Mojo

初始化Mojo有两种方式,一种适用于静态链接的Mojo程序,一种适用于动态链接Mojo的程序。一下是静态链接时的初始化方法,动态链接时只需要把mojo:::core::Init()替换为MojoInitialize()即可

初始化接口头文件为

1
2
#include <mojo/core/embedder/embedder.h>
#include <mojo/core/embedder/scoped_ipc_support.h>

初始化方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main(int argc,char** argv){
//初始化commandLine,DataPipe依赖它
base::commandLine::Init(argc, argv);
//初始化mojo
mojo::core::Init();
//创建一个线程,用于Mojo内部收发数据
base::Thread ipc_thread("ipc!");
ipc_thread.StartWithOptions(
base::Thread::Options(base::MessageLoop::TYPE_IO, 0));

//初始化Mojo的IPC支持,只有初始化后进程间的mojo通信才能有效
//这个对象要保证一直存活,否则IPC通信就会断开
mojo::core::ScopedIPCSupport ipc_support(
ipc_thread.task_runner(),
mojo::core::ScopedIPCSupport::ShutdownPolicy::CLEAN);

//
..
}

Mojo c API

Mojo C API都比较简单,主要的头文件位于:

1
2
3
4
5
6
// SharedBuffer API
#include "mojo/public/c/system/buffer.h"
// DataPipe API
#include "mojo/public/c/system/data_pipe.h"
// MessagePipe API
#include "mojo/public/c/system/message_pipe.h"

下面是在单进程中使用MessagePipe发送和接收数据的方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//使用C接口创建一条MessagePipe
//MessagePipe只是一对数字,只用于ID标识,并不对应任何系统资源
//因此可以非常快速不可能失败的创建大量messagepipe

MojoHandle sender_handle, receiver_handle;
MojoResult result =
MojoCreateMessagePipe(NULL, &sender_handle, &receiver_handle);
DCHEK_EQ(result, MOJO_RESULT_OK);

//使用C接口发送一条消息

{
//创建一条message
MojoMessageHandle message;
result = MojoCreateMessage(nullptr, &message);
DCHEK_EQ(result, MOJO_RESULT_OK);
MojoAppendMessageDataOptions options;
options.struct_size = sizeof(options);
//这个选项表示这条消息完整了,底层可以发送了
options.flags = MOJO_APPEND_MESSAGE_DATA_FLAG_COMMIT_SIZE;
void* buffer;
uint32_t buffer_size;
// 给message填充数据
result = MojoAppendMessageData(message, 6, nullptr, 0, &options, &buffer, &buffer_size);
DCHECK_EQ(result, MOJO_RESULT_OK);
memcpy(buffer, "hello", 6);
LOG(INFO) << "send: "<< (const char*)buffer;
//发送message
result = MojoWriteMessage(sender_handle, message, nullptr);
DCHECK_EQ(result, MOJO_RESULT_OK);
}
{
MojoMessageHandle message;
MojoResult result = MojoReadMessage(receiver_handle, nullptr, &message);
DCHECK_EQ(result, MOJO_RESULT_OK);

void* buffer = NULL;
uint32_t num_bytes;
result = MojoGetMessageData(message, nullptr, &buffer, &num_bytes, nullptr, nullptr);
LOG(INFO) << "receive: " << (const char*)buffer;
}

其他关于DataPipe和SharedBuffer的使用方法都类似。由于实际项目很少直接使用C API,所以使用方法在这里省略。

Mojo C++ API

单进程

以下是在单进程中使用MessagePipe的方法,其中最重要的是要注意mojo::MessagePipe pipe;这一行(内部调用Moojo的C APIMojoCreateMessagePipe),它创建了一个MessagePipe, 本质上只是创建了一对随机数,对应pipe中的两个属性handle0handle1,这两个功能上没有任何区别,向其中的一个handle写的数据可以从另外一个handle中读取出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

#include "mojo/public/cpp/system/buffer.h"
#include "mojo/public/cpp/system/data_pipe.h"
#include "mojo/public/cpp/system/message_pipe.h"
#include "mojo/public/cpp/system/simple_watcher.h"
#include "mojo/public/cpp/system/wait.h"

// 使用C++接口创建一条MessagePipe

mojo::MessagePipe pipe;
//使用C++接口发送一条消息
{
const char kMessage[] = "Hello";
result = mojo::WriteMessageRaw(pipe.handle0.get(), kMessage, sizeof(kMessage), nullptr, 0, MOJO_WRITE_MESSAGE_FLAG_NONE);
DCHECK_EQ(result, MOJO_RESULT_OK);
LOG(INFO) << "send: " <<kMessage;
}
//使用C++接口接收一条消息
{
std::vector<uint8_t> data;
result = mojo::ReadMessageRaw(pipe.hanle1.get(), &data, nullptr, MOJO_READ_MESSAGE_FLAG_NONE);
DCHECK_EQ(result, MOJO_RESULT_OK);
LOG(INFO) << "receive msg: " <<(char*)&data[0];
}

关于DataPipe和SharedBuffer的使用这里不再赘述。

多进程

一个**MessagePipe中有一对handle,分别对handle0和handle1,向其中一个handle写的数据可以从另外一个handle中读出来,这是前面已经说过的,如果把其中一个handle发送到另外一个进程,这一对handle之间依然可以相互收发数据。Mojo提供了多种方法来发送handle到其他的进程,最简单的则是使用Invitation

要在多个进程之间使用MOJO,必须先通过Invitation将这些进程“连接”起来,这需要一个进程发送Invitation,另外一个进程接收Invitation,发送Invitation的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//创建一条系统级的IPC通信通道
//在Linux上是domain socket, Windows上是named pipe,Macos是Mach port,该通道用于支持跨进程的消息通信

mojo::PlatformChannel channel;
LOG(INFO) << "local: "
<< channel.local_endpoint().platform_handle().GetFD().get()
<< "remote: "
<< channel.remote_endpoint().platform_handle().GetFD().get():

mojo::OutgoingInvitation invitation;

//创建1个Message Pipe用来和其他进程通信
// 这里的pipe就相当于单进程中的pipe.handle0
// handle1会被存储在invitation中,随后杯发送出去
//可以多次调用,以使Attach多个MessagePipe带Invitation中

mojo::ScopedMessagePipeHandle pipe = invitation.AttachMessagePipe("my raw pipe");

LOG(INFO) << "pipe: " << pipe->value();

base::LaunchOptions options;
base::CommandLine command_line(
base::CommandLine::ForCurrentProcess()->GetProgram());
// 将PlatformChannel中的RemoteEndpoint的fd作为参数传递给子进程
// 在posix中,fd会被复制到新的随机的fd,fd号改变
// 在windows中,fd被复制后会直接进行传递,fd号不变

channel.PreareToPassRemoteEndPoint(&options, &command_line);

//启动新进程
base::Process child_process = base::LaunchProcess(command_line, options);
channel.RemoteProcessLaunchAttempted();

//发送Invitation
mojo::OutgoingInvitation::Send(
std::move(invitation), child_process.Handle(),
channel.TakeLocalEndpoint(),
base::BindRepeating(
[](const std::string& error) {LOG(ERROR) << error; }));

在新进程接收Invitation的方法如下

1
2
3
4
5
6
7
mojo::IncomingInvitation invitation = mojo::IncomingInvitation::Accept(
mojo::PlatformChannel::RecovePassedEndpointFromCommdLine(
*base::CommandLine::ForCurrentProcess()));
//取出Invitation中的pipe
mojo::ScopedMessagePipeHandle pipe =
invitation.ExtractMessagePipe("my raw pipe");
LOG(INFO) << "pipe: " << pipe->value();

这样就实现了将piep中的一个handle发送到其他进程了,这两个进程可以开始使用pipe进行收发数据了。
以上只是将handle从一个进程发送到另一个进程的一种方法,这种方法一般用在新进程创建的时候,如果两个进程已经通过Invitation连接起来了,那么可以通过已经建立起来的MessagePipe来发送新的MessagePipehandle到接收进程,发送端的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
const std::string kMessage("MessagePipe\0", 12);
mojo::ScopedMessagePipeHandle client;
mojo::ScopedMessagePipeHandle server;
//创建一个新的MessagePipe(的一堆handle)
//也可以使用mojo::MessagePipe来创建,这两者是一样的
result = mojo::CreateMessagePipe(nullptr, &client, &server);
DCHECK_EQ(result, MOJO_RESULT_OK);

//使用已经连接起来的pipe将'client'发送到对方
result = mojo::Windows(pipe.get(), kMessage.c_str(),
kMessage.length(), &client->value(), 1,
MOJO_WRITE_MESSAGE_FLAG_DONE);
DCHECK_EQ(result, MOJO_RESULT_OK);

//接收端代码如下

1
2
3
4
5
6
7
8
9
10
std::vector<uint8_t> data;
//用来存储数据的handle
std::vector<mojo::ScopedHandle> handles;
result = mojo::ReadMessageRaw(pipe.get(), &data, &handles, MOJO_READ_MESSAGE_FLAG_NONE);

//将接收到的handle转换为MessagePipeHandle
//后续就可以使用这个handle和对端通信了

mojo::ScopedMessagePipeHandle client =
mojo::ScopedMessagePipeHandle::From(std::move(handles[0]));

Mojo C++ Bindings API

Bindings API是使用mojo的重点,在项目中会大量使用

Mojom

Mojo在Binding层引入了Mojom这种IDL语言,用它来定义接口。接口定义文件的后缀一般为.mojo,,一个简单的接口定义如下