[Chromium] 个人对chromium mojo的理解

2024/11/20 update
mojo本质是Uinx Domain Socket(posix),在本机的进程间进行通信时不会经过网卡,可以少一次拷贝。在Windows上则是管道来实现的IPC。

mojom的用途

主要用于进程间通信,减少模块间依赖。

用接口描述语言书写mojom接口文件,经过编译之后会自动生成对应的mojo类

mojo底层管道的创建

mojo的通信依赖于底层MessagePipe,管道的创建在mojo/core/core.cc
core::CreateMessagePipe,传入管道的两端mojohandle,这里的handle就是mojo handle了,管道创建成功并不意味着底层socket创建,发送invitation的时候才会真正创建socket

Node网络

可以简单地将node看成进程,不同node之间的通信就是通过IPC实现的
用于给mojo提供消息路由功能,在全局对象core中存在一个node_controller_在创建MessagePipeDispatcher的时候,会将NodeController对象传入。

NodeController持有Node(由于全局对象core持有NodeController,说明NodeController对象也是全局唯一)

  // core.cc
  // This is lazily initialized on first access. Always use GetNodeController()
  // to access it.
  std::unique_ptr<NodeController> node_controller_;

Node维护了一系列的Port,Port有name

NodeController有一个peers_的map,当收到invitation的时候(OnAcceptInvitation)建立连接,并将对端存入这个map

using NodeMap = std::unordered_map<ports::NodeName, scoped_refptr<NodeChannel>>;
NodeMap peers_;

OnAcceptInvation的实质是收到一个Message(来源于OnChannelMessage
发送邀请过程如下,A邀请B
(A)SendInvitation->(A)AcceptInvitee->(B)OnAcceptInvitee->(B)AcceptInvitation->(A)OnAcceptInvitation->(A)AddPeer,邀请结束,链接建立成功(两次握手)其中,AcceptInviteeAcceptInvitation的消息都是通过NodeChannelChannel对象发送消息
按照文档说明,node与node之间一开始只有IPC点对点通信,假如要发给另一个node,但是和当前node之间没有直接的IPC,则需要通过broker建立一个新的IPC通道,REQUEST_INTRODUCTION消息就是这个过程

NodeChannel

NodeName一起保存在peers_中。对上封装的接口,用于Node to Node的IPC接口。

NodeChannel::Introduce负责介绍两个不认识的node相互认识(建立IPC通道)

也是在Invitation阶段创建的,和PlatformChannel一致,连接两端时创建
持有Channel对象,封装了Channel的读写接口,也就是说搞到了NodeChannel基本就可以往对应的Node(进程)发送消息了

Channel

NodeChannel对应的各平台实现,自身对象由NodeChannel持有
NodeChannel::WriteChannelMessage会通过Channel::Write发送消息,对端信息包含在参数的message中,channel本身持有socket(posix),这个socket其实就是PlatformChannel创建的,message参数中包含对端的PlatformHandle,这里的PlatformHandle其实是系统对应的handle,Windows的HANDLE,posix文件描述符,Handle里面存的就是最底层的通信手段,例如socket或者是管道

port

消息Messages在Ports的两端传输,一组Ports就代表了一个外层的MessagePipe

PlatformChannel

mojo底层用来通信的通道,本质是Unix Domain Socket,在Core::SendInvitation的时候创建底层的sockcet
Core::AcceptInvitation时也会创建(Windows不是socket,而是named pipe),这个东西还会塞给broker

public c system to core

对外提供c接口,调用mojo使用这些接口,当调用这些接口时,将会通过dispatcher.cc(抽象基础类,具体有不同的实现,例如MessagePipeDispatcher)进行分发处理

对应接口的实现经过thunks.cc转换后调用到core.cc中的函数

entrypoints.cc中有全局变量g_thunks来持有g_core变量执行到core,完成函数转换这就完成了一次调用

dispatcher

dispatcher负责分发实现通用接口的不同实现方式,dispatcher在core内被创建时会存放到HandleTable中,并返回对应的MojoHandle

例如MessagePipeDispatcher就持有NodeController,是由全局对象core传过来的

MessagePipeDispatcher对象还持有一个port,也是在一创建就传入的(由NodeController和其Node通过CreatePortPair创建的,在CreateMessagePipe的时候)

Broker 和 Broker process

Broker process在chromium中只会是browser process,由于持有Broker Channel的NodeController对象唯一,并且Broker Process也是唯一,所以,Broker Channel唯一也并不奇怪,因此邀请的流程中NodeController这个持有了node map和broker channel的对象的参与十分重要。

broker process作为一个单独的node存在于node网络中,它的作用是

  • 提供介绍机制(Introduce)帮助一对node建立IPC链接
  • 复制node的handle(由于沙盒,node自身可能办不到)
  • 创建共享内存

规定只有它能introduce(通过调用NodeController的能力找到两个node)并发送Introduce message,这就要求broker process的这个node需要跟每个node都有连接,连接使用的channel为特殊channel,在代码中被称为broker

在接受invitation和创建invitation的时候,例如在NodeController::OnAcceptInvitation中,执行AddPeer添加对端后,如果broker存在,还给broker channel添加这个新增的client

NodeController类中有std::unique_ptr<Broker> broker_;成员

mojo邀请机制在进程之间的传递

参考invitation.h文件中对两种类型invitation的描述,有两个类型,分别是IncomingInvitationOutgoingInvitation,其中OutgoingInvitation是用来发起邀请准备的,而IncomingInvitation是接受邀请准备的

可以看到IncomingInvitationcontent_main_runer_impl.cc这个子线程的代码逻辑中有接受邀请的相关代码

mojo::ScopedMessagePipeHandle MaybeAcceptMojoInvitation() {
  const auto& command_line = *base::CommandLine::ForCurrentProcess();
  if (!mojo::PlatformChannel::CommandLineHasPassedEndpoint(command_line))
    return {};

  mojo::PlatformChannelEndpoint endpoint =
      mojo::PlatformChannel::RecoverPassedEndpointFromCommandLine(command_line);
  auto invitation = mojo::IncomingInvitation::Accept(std::move(endpoint));
  return invitation.ExtractMessagePipe(0);
}

这个函数会在子线程的主要Run函数中调用用于接受邀请建立连接

那么这个邀请是在哪发送的呢?应该是在子进程创建的时候通过browser进程获取并通过命令行参数传递过来的,那么在browser_child_process_host_impl.cc文件中有LaunchWithoutExtraCommandLineSwitches函数用于发送邀请

void BrowserChildProcessHostImpl::LaunchWithoutExtraCommandLineSwitches(
    std::unique_ptr<SandboxedProcessLauncherDelegate> delegate,
    std::unique_ptr<base::CommandLine> cmd_line,
    std::unique_ptr<ChildProcessLauncherFileData> file_data,
    bool terminate_on_shutdown) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(!in_process_);

  const base::CommandLine& browser_command_line =
      *base::CommandLine::ForCurrentProcess();
  static const char* const kForwardSwitches[] = {
      switches::kDisableInProcessStackTraces,
      switches::kDisableBestEffortTasks,
      switches::kIPCConnectionTimeout,
      switches::kLogBestEffortTasks,
      switches::kPerfettoDisableInterning,
      switches::kTraceToConsole,
  };
  cmd_line->CopySwitchesFrom(browser_command_line, kForwardSwitches);

  // All processes should have a non-empty metrics name.
  if (data_.metrics_name.empty())
    data_.metrics_name = GetProcessTypeNameInEnglish(data_.process_type);

  data_.sandbox_type = delegate->GetSandboxType();

  // Note that if this host has a legacy IPC Channel, we don't dispatch any
  // connection status notifications until we observe OnChannelConnected().
#if BUILDFLAG(CLANG_PROFILING_INSIDE_SANDBOX)
  bool is_elevated = false;
#if BUILDFLAG(IS_WIN)
  is_elevated = (delegate->GetSandboxType() ==
                 sandbox::mojom::Sandbox::kNoSandboxAndElevatedPrivileges);
#endif
  if (!is_elevated)
    child_process_host_->SetProfilingFile(OpenProfilingFile());
#endif

  auto tracing_config_memory_region =
      tracing::CreateTracingConfigSharedMemory();

  child_process_launcher_ = std::make_unique<ChildProcessLauncher>(
      std::move(delegate), std::move(cmd_line), data_.id, this,
      std::move(*child_process_host_->GetMojoInvitation()),
      base::BindRepeating(&BrowserChildProcessHostImpl::OnMojoError,
                          weak_factory_.GetWeakPtr(),
                          base::SingleThreadTaskRunner::GetCurrentDefault()),
      std::move(file_data), metrics_shared_region_.Duplicate(),
      std::move(tracing_config_memory_region), terminate_on_shutdown);
  ShareMetricsAllocatorToProcess();

  if (!has_legacy_ipc_channel_)
    OnProcessConnected();
}

重点在于*child_process_host_->GetMojoInvitation()这里,这里是获取一个mojo邀请对象,这个对象的类型在下述代码中所示

std::optional<mojo::OutgoingInvitation>&
ChildProcessHostImpl::GetMojoInvitation() {
  return mojo_invitation_;
}

这个OutgoingInvitation类型的邀请,显而易见就是为了发送邀请准备的,而且LaunchWithoutExtraCommandLineSwitches这个函数直接被gpu_process_host.cc使用用于创建gpu进程。

那接下来看看创建沙盒进程的时候是否有调用到这个函数用来给沙盒进程发送邀请
沙盒进程是通过child_process_launcher_helper_win.cc中的LaunchProcessOnLauncherThread拉起来的

ChildProcessLauncherHelper::Process
ChildProcessLauncherHelper::LaunchProcessOnLauncherThread(
    const base::LaunchOptions* options,
    std::unique_ptr<FileMappedForLaunch> files_to_register,
    bool* is_synchronous_launch,
    int* launch_result) {
  DCHECK(CurrentlyOnProcessLauncherTaskRunner());
  *is_synchronous_launch = true;
  if (delegate_->ShouldLaunchElevated()) {
    DCHECK(options->elevated);
    // When establishing a Mojo connection, the pipe path has already been added
    // to the command line.
    base::LaunchOptions win_options;
    win_options.start_hidden = true;
    win_options.elevated = true;
    ChildProcessLauncherHelper::Process process;
    process.process = base::LaunchProcess(*command_line(), win_options);
    *launch_result = process.process.IsValid() ? LAUNCH_RESULT_SUCCESS
                                               : LAUNCH_RESULT_FAILURE;
    return process;
  }
  *is_synchronous_launch = false;
  *launch_result = StartSandboxedProcess(
      delegate_.get(), *command_line(), options->handles_to_inherit,
      base::BindOnce(&ChildProcessLauncherHelper::
                         FinishStartSandboxedProcessOnLauncherThread,
                     this));
  return ChildProcessLauncherHelper::Process();
}

这个函数的调用是在ChildProcessLauncher类在实例化对象的时候在构造函数中调用的,那么是谁在创建ChildProcessLauncher对象呢?

其中一个调用方就是LaunchWithoutExtraCommandLineSwitches这个函数,这说明在创建沙盒进程之前已经准备好邀请了,剩下的就是通过创建沙盒进程时候的通过命令行参数进行传递。即LaunchWithoutExtraCommandLineSwitches -> Create ChildProcessLauncher object -> LaunchProcessOnLauncherThread -> Create sandbox process

另一个调用方是RenderProcessHostImpl::Init是在创建渲染进程的时候初始化调用的(这是符合的,因为渲染进程也是沙盒进程,渲染器进程自然也需要接受邀请和browser打通mojo)这里创建进程走的不是LaunchWithoutExtraCommandLineSwitches这个函数,而是直接走了沙盒。即RenderProcessHostImpl::Init -> Create ChildProcessLauncher object -> LaunchProcessOnLauncherThread -> Create sandbox process
到此为止,mojo的邀请就是通过上述方式和browser进程打通了。

posted @ 2022-10-08 19:03  leno米雷  阅读(1857)  评论(0)    收藏  举报