Chromium进程间通信Mojo 进程间建立连接

invitation

Mojo中两个进程建立连接是通过发送和接收invitation实现的,一方发送OutgoingInvitation,另一方去接收IncomingInvitation。

Mojo底层的进程间通信技术依赖具体的系统,比如Windows是named pipe,UNIX系统是domain socket。以Windows系统为例,在建立好named pipe连接之后,通过往named pipe里面写入和读取一个特殊的invitation消息,来实现两个进程Mojo的连接。

这里有人可能会觉得奇怪了:建立好系统的named pipe的连接之后,就意味着进程间已经建立好IPC连接了。为什么还需要通过invitation去建立Mojo的连接?Mojo底层虽然还是通过系统的IPC连接去发送接收数据,但是Mojo对其做了一层封装,定义属于自己的一套IPC原语。另外可以把系统named pipe看作物理上IPC连接,在这之上还有逻辑上Mojo的MessagePipe连接。系统的named pipe上面可以同时承载着多个逻辑上的上MessagePipe。每个MessagePipe都有自己独立的消息队列。

建立连接

我们以一个例子来描述具体怎么建立Mojo的IPC连接。首先会创建一个parent进程,它创建好OutgoingInvitation并创建了child子进程,然把OutgoingInvitation发送给child子进程。child子进程那边则去接收IncomingInvitation来建立连接。

parent进程代码:

int main(int argc, char* argv[]) {
  base::AtExitManager exit_manager;
  base::CommandLine::Init(0, 0);
  logging::LoggingSettings settings;
  settings.logging_dest = logging::LOG_TO_ALL;
  settings.delete_old = logging::DELETE_OLD_LOG_FILE;
  settings.log_file_path = L"mojo.log";
  logging::InitLogging(settings);

  base::MessageLoop loop;

  mojo::core::Init();
  base::Thread ipc_thread("ipc");
  ipc_thread.StartWithOptions(
      base::Thread::Options(base::MessagePumpType::IO, 0));
  mojo::core::ScopedIPCSupport ipc_support(
      ipc_thread.task_runner(),
      mojo::core::ScopedIPCSupport::ShutdownPolicy::CLEAN);

  mojo::PlatformChannel channel;
  mojo::OutgoingInvitation invitation;
  mojo::ScopedMessagePipeHandle pipe =
      invitation.AttachMessagePipe("pipe-name");

  base::CommandLine command_line(base::CommandLine::NO_PROGRAM);
  base::FilePath executable_path;
  base::PathService::Get(base::DIR_MODULE, &executable_path);
  base::FilePath client_path =
      executable_path.AppendASCII("ipc_connect_platform_channel_child.exe");
  command_line.SetProgram(client_path);

  base::LaunchOptions options;
  channel.PrepareToPassRemoteEndpoint(&options, &command_line);
  base::Process child_process = base::LaunchProcess(command_line, options);
  channel.RemoteProcessLaunchAttempted();
  mojo::OutgoingInvitation::Send(std::move(invitation), child_process.Handle(),
                                 channel.TakeLocalEndpoint());

  base::RunLoop().Run();

  return 0;
}

child子进程代码:

int main(int argc, char* argv[]) {
  base::AtExitManager exit_manager;
  base::CommandLine::Init(0, 0);
  logging::LoggingSettings settings;
  settings.logging_dest = logging::LOG_TO_ALL;
  settings.delete_old = logging::DELETE_OLD_LOG_FILE;
  settings.log_file_path = L"mojo.log";
  logging::InitLogging(settings);

  base::MessageLoop loop;

  mojo::core::Init();
  base::Thread ipc_thread("ipc");
  ipc_thread.StartWithOptions(
      base::Thread::Options(base::MessagePumpType::IO, 0));
  mojo::core::ScopedIPCSupport ipc_support(
      ipc_thread.task_runner(),
      mojo::core::ScopedIPCSupport::ShutdownPolicy::CLEAN);

  mojo::IncomingInvitation invitation = mojo::IncomingInvitation::Accept(
      mojo::PlatformChannel::RecoverPassedEndpointFromCommandLine(
          *base::CommandLine::ForCurrentProcess()));
  mojo::ScopedMessagePipeHandle pipe =
      invitation.ExtractMessagePipe("pipe-name");

  base::RunLoop().Run();

  return 0;
}

parent进程里首先我们创建了一个mojo::PlatformChannel,它实际上会以当前进程id+线程id+随机数为名创建一个named pipe,比如\\.\pipe\mojo.19776.17344.14378832871110571506mojo::PlatformChannel里面包含这个named pipe两端的句柄。把其中一端句柄发送给另外一个进程,那就建立了底层的named pipe连接。上面例子中是通过channel.PrepareToPassRemoteEndpoint(&options, &command_line);把其中一端句柄值放到创建child子进程参数,另外把句柄设置成可继承,然后child子进程通过mojo::PlatformChannel::RecoverPassedEndpointFromCommandLine(*base::CommandLine::ForCurrentProcess())来解析命令行参数获得named pipe一端的句柄。我们可以看到child子进程的子进程命令行参数是--mojo-platform-channel-handle=260,其中260就是named pipe一端的句柄值。

其实我们也不必使用mojo提供的方法来传递named pipe一端的句柄值来建立IPC连接。只要我们自己能够把mojo::PlatformChannel里面的remote_endpoint句柄传递给另外一个进程就可以了。我们可以通过自己定义的命令行参数或者其他已经建立好的途径。

parent进程创建了一个OutgoingInvitation,然后就在它上面附加了一个MessagePipeMessagePipe是Mojo自己发送和接收数据的逻辑管道。然后在child子进程里从IncomingInvitation里面把MessagePipe抽取出来。后面两个进程就可以通过MessagePipe来读写数据了。

Isolated Invitation

上面建立Mojo连接的两个进程是由父子关系的。如果是两个独立没关系的进程,可以通过Isolated Invitation来建立连接。Isolated Invitation需要配合mojo::NamedPlatformChannel来使用。

mojo::PlatformChannelmojo::NamedPlatformChannel建立named pipe的机制有些差异。mojo::PlatformChannel是在一个进程中把named pipe创建连接好,然后把其中的一个端口传递给另一个进程来使用。而mojo::NamedPlatformChannel则是两个进程通过一个预先约定好的名称来创建建立named pipe连接。

parent进程代码:

int main(int argc, char* argv[]) {
  base::AtExitManager exit_manager;
  base::CommandLine::Init(0, 0);
  logging::LoggingSettings settings;
  settings.logging_dest = logging::LOG_TO_ALL;
  settings.delete_old = logging::DELETE_OLD_LOG_FILE;
  settings.log_file_path = L"mojo.log";
  logging::InitLogging(settings);

  base::MessageLoop loop;

  mojo::core::Init();
  base::Thread ipc_thread("ipc");
  ipc_thread.StartWithOptions(
      base::Thread::Options(base::MessagePumpType::IO, 0));
  mojo::core::ScopedIPCSupport ipc_support(
      ipc_thread.task_runner(),
      mojo::core::ScopedIPCSupport::ShutdownPolicy::CLEAN);

  mojo::NamedPlatformChannel::Options options;
  options.server_name = L"test";
  mojo::NamedPlatformChannel named_channel(options);
  mojo::ScopedMessagePipeHandle pipe = mojo::OutgoingInvitation::SendIsolated(
    named_channel.TakeServerEndpoint(), nullptr);

  base::RunLoop().Run();

  return 0;
}

child子进程代码:

int main(int argc, char* argv[]) {
  base::AtExitManager exit_manager;
  base::CommandLine::Init(0, 0);
  logging::LoggingSettings settings;
  settings.logging_dest = logging::LOG_TO_ALL;
  settings.delete_old = logging::DELETE_OLD_LOG_FILE;
  settings.log_file_path = L"mojo.log";
  logging::InitLogging(settings);

  base::MessageLoop loop;

  mojo::core::Init();
  base::Thread ipc_thread("ipc");
  ipc_thread.StartWithOptions(
      base::Thread::Options(base::MessagePumpType::IO, 0));
  mojo::core::ScopedIPCSupport ipc_support(
      ipc_thread.task_runner(),
      mojo::core::ScopedIPCSupport::ShutdownPolicy::CLEAN);

  mojo::PlatformChannelEndpoint endpoint =
      mojo::NamedPlatformChannel::ConnectToServer(L"test");
  mojo::ScopedMessagePipeHandle pipe =
      mojo::IncomingInvitation::AcceptIsolated(std::move(endpoint));

  base::RunLoop().Run();

  return 0;
}

parent进程的mojo::NamedPlatformChannel指定了一个test名字,实际上上创建一个名为\Device\NamedPipe\mojo.test的named pipe。child子进程也通过test这个名字去连接上parent进程的named pipe连接。

此外Mojo还提供了IsolatedConnection来进行连接。