Render 进程启动过程分析2

接下来,我们主要分析第 1、3 和 4 件事情,第 2 件事情在接下来的一篇文章中分析 IPC 消息分发机制时再分析。

第一件事情涉及到 IPC::Channel 类的静态成员函数 GenerateVerifiedChannelID 和 IPC::ChannelProxy 类的静态成员函数 Create。
IPC::Channel 类的静态成员函数 GenerateVerifiedChannelID 的实现如下所示:

std::string Channel::GenerateVerifiedChannelID(const std::string& prefix) {
  // A random name is sufficient validation on posix systems, so we don't need
  // an additional shared secret.
 
  std::string id = prefix;
  if (!id.empty())
    id.append(".");
 
  return id.append(GenerateUniqueRandomChannelID());
}

这个函数定义在文件 external/chromium_org/ipc/ipc_channel_posix.cc 中。

IPC::Channel 类的静态成员函数 GenerateVerifiedChannelID 实际上是调用另外一个静态成员函数 GenerateUniqueRandomChannelID 生成一个唯一的随机名字,后者的实现如下所示:

base::StaticAtomicSequenceNumber g_last_id;
......
 
std::string Channel::GenerateUniqueRandomChannelID() {
  ......
  int process_id = base::GetCurrentProcId();
  return base::StringPrintf("%d.%u.%d",
      process_id,
      g_last_id.GetNext(),
      base::RandInt(0, std::numeric_limits<int32>::max()));
}

这个函数定义在文件 external/chromium_org/ipc/ipc_channel.cc 中。
从这里就可以看到,这个用来创建 UNIX Socket 的名字由当前进程的 PID、一个顺序数和一个随机数通过"."符号连接而成的。

回到 RenderProcessHostImpl 类的成员函数 Init 中,有了用来创建 UNIX Socket 的名字之后,就可以调用 IPC::ChannelProxy 类的静态成员函数 Create 创建一个 Channel 了,如下所示:

scoped_ptr<ChannelProxy> ChannelProxy::Create(
    const IPC::ChannelHandle& channel_handle,
    Channel::Mode mode,
    Listener* listener,
    base::SingleThreadTaskRunner* ipc_task_runner) {
  scoped_ptr<ChannelProxy> channel(new ChannelProxy(listener, ipc_task_runner));
  channel->Init(channel_handle, mode, true);
  return channel.Pass();
}

这个函数定义在文件 external/chromium_org/ipc/ipc_channel_proxy.cc 中。

IPC::ChannelProxy 类的静态成员函数 Create 首先是创建了一个 ChannelProxy 对象,然后再调用该 ChannelProxy 对象的成员函数 Init 执行初始化工作,最后返回该 ChannelProxy 对象给调用者。

ChannelProxy 对象的创建过程如下所示:

ChannelProxy::ChannelProxy(Listener* listener,
                           base::SingleThreadTaskRunner* ipc_task_runner)
    : context_(new Context(listener, ipc_task_runner)), did_init_(false) {
}

这个函数定义在文件 external/chromium_org/ipc/ipc_channel_proxy.cc。
ChannelProxy 类的构造函数主要是创建一个 ChannelProxy::Context 对象,并且将该 ChannelProxy::Context 对象保存在成员变量 context_中。
ChannelProxy::Context 对象的创建过程如下所示: 

ChannelProxy::Context::Context(Listener* listener,
                               base::SingleThreadTaskRunner* ipc_task_runner)
    : listener_task_runner_(base::ThreadTaskRunnerHandle::Get()),
      listener_(listener),
      ipc_task_runner_(ipc_task_runner),
      ......
      message_filter_router_(new MessageFilterRouter()),
      ...... {
  ......
}

这个函数定义在文件 external/chromium_org/ipc/ipc_channel_proxy.cc 中。

ChannelProxy::Context 类有三个成员变量是需要特别关注的,它们分别是:

1. listenter_task_runner_。这个成员变量的类型为 scoped_refptr<base::SingleThreadTaskRunner>,它指向的是一个 SingleThreadTaskRunner 对象。这个 SingleThreadTaskRunner 对象通过调用 ThreadTaskRunnerHandle 类的静态成员函数 Get 获得。从前面 Chromium 多线程模型设计和实现分析一文可以知道,ThreadTaskRunnerHandle 类的静态成员函数 Get 返回的 SingleThreadTaskRunner 对象实际上是当前线程的一个 MessageLoopProxy 对象,通过该 MessageLoopProxy 对象可以向当前线程的消息队列发送消息。当前线程即为 Browser 进程的主线程。

2. listener_。这是一个 IPC::Listener 指针,它的值设置为参数 listener 的值。从前面的图 3 可以知道,RenderProcessHostImpl 类实现了 IPC::Listener 接口,而且从前面的调用过程过程可以知道,参数 listener 指向的就是一个 RenderProcessHostImpl 对象。以后正在创建的 ChannelProxy::Context 对象在 IO 线程中接收到 Render 进程发送过来的 IPC 消息之后,就会转发给成员变量 listener_指向的 RenderProcessHostImpl 对象处理,但是并不是让后者直接在 IO 线程处理,而是让后者在成员变量 listener_task_runner_描述的线程中处理,即 Browser 进程的主线程处理。也就是说,ChannelProxy::Context 类的成员变量 listener_task_runner_和 listener_是配合在一起使用的,后面我们分析 IPC 消息的分发机制时就可以看到这一点。

3. ipc_task_runner_。这个成员变量与前面分析的成员变量 listener_task_runner 一样,类型都为 scoped_refptr<base::SingleThreadTaskRunner>,指向的者是一个 SingleThreadTaskRunner 对象。不过,这个 SingleThreadTaskRunner 对象由参数 ipc_task_runner 指定。从前面的调用过程可以知道,这个 SingleThreadTaskRunner 对象实际上是与 Browser 进程的 IO 线程关联的一个 MessageLoopProxy 对象。这个 MessageLoopProxy 对象用来接收 Render 进程发送过来的 IPC 消息。也就是说,Browser 进程在 IO 线程中接收 IPC 消息。

ChannelProxy::Context 类还有一个重要的成员变量 message_filter_router_,它指向一个 MessageFilterRouter 对象,用来过滤 IPC 消息,后面我们分析 IPC 消息的分发机制时再详细分析。

回到 ChannelProxy 类的静态成员函数 Create 中,创建了一个 ChannelProxy 对象之后,接下来就调用它的成员函数 Init 进行初始化,如下所示:

void ChannelProxy::Init(const IPC::ChannelHandle& channel_handle,
                        Channel::Mode mode,
                        bool create_pipe_now) {
  ......
 
  if (create_pipe_now) {
    ......
    context_->CreateChannel(channel_handle, mode);
  } else {
    context_->ipc_task_runner()->PostTask(
        FROM_HERE, base::Bind(&Context::CreateChannel, context_.get(),
                              channel_handle, mode));
  }
 
  // complete initialization on the background thread
  context_->ipc_task_runner()->PostTask(
      FROM_HERE, base::Bind(&Context::OnChannelOpened, context_.get()));
 
  ......
}

这个函数定义在文件 external/chromium_org/ipc/ipc_channel_proxy.cc 中。

从前面的调用过程知道,参数 channel_handle 描述的是一个 UNIX Socket 名称,参数 mode 的值为 IPC::Channel::MODE_SERVER,参数 create_pipe_now 的值为 true。这样,ChannelProxy 类的成员函数 Init 就会马上调用前面创建的 ChannelProxy::Context 对象的成员函数 CreateChannel 创建一个 IPC 通信通道,也就是在当前线程中创建一个 IPC 通信通道 。

另一个方面,如果参数 create_pipe_now 的值等于 false,那么 ChannelProxy 类的成员函数 Init 就不是在当前线程创建 IPC 通信通道,而是在 IO 线程中创建。因为它先通过前面创建的 ChannelProxy::Context 对象的成员函数 ipc_task_runner 获得其成员变量 ipc_task_runner_描述的 SingleThreadTaskRunner 对象,然后再将创建 IPC 通信通道的任务发送到该 SingleThreadTaskRunner 对象描述的 IO 线程的消息队列去。当该任务被处理时,就会调用 ChannelProxy::Context 类的成员函数 CreateChannel。

当调用 ChannelProxy::Context 类的成员函数 CreateChannel 创建好一个 IPC 通信通道之后,ChannelProxy 类的成员函数 Init 还会向当前进程的 IO 线程的消息队列发送一个消息,该消息绑定的是 ChannelProxy::Context 类的成员函数 OnChannelOpened。因此,接下来我们就分别分析 ChannelProxy::Context 类的成员函数 CreateChannel 和 OnChannelOpened。

ChannelProxy::Context 类的成员函数 CreateChannel 的实现如下所示:

void ChannelProxy::Context::CreateChannel(const IPC::ChannelHandle& handle,
                                          const Channel::Mode& mode) {
  ......
  channel_ = Channel::Create(handle, mode, this);
}

这个函数定义在文件 external/chromium_org/ipc/ipc_channel_proxy.cc 中。
ChannelProxy::Context 类的成员函数 CreateChannel 调用 Channel 类的成员函数 Create 创建了一个 IPC 通信通道,如下所示: 

scoped_ptr<Channel> Channel::Create(
    const IPC::ChannelHandle &channel_handle, Mode mode, Listener* listener) {
  return make_scoped_ptr(new ChannelPosix(
      channel_handle, mode, listener)).PassAs<Channel>();
}

这个函数定义在文件 external/chromium_org/ipc/ipc_channel_posix.cc 中。
从这里可以看到,对于 Android 平台来说,IPC 通信通道通过一个 ChannelPosix 对象描述,该 ChannelPosix 对象的创建过程如下所示: 

ChannelPosix::ChannelPosix(const IPC::ChannelHandle& channel_handle,
                           Mode mode, Listener* listener)
    : ChannelReader(listener),
      mode_(mode),
      ......
      pipe_(-1),
      client_pipe_(-1),
#if defined(IPC_USES_READWRITE)
      fd_pipe_(-1),
      remote_fd_pipe_(-1),
#endif  // IPC_USES_READWRITE
      pipe_name_(channel_handle.name),
      ...... {
  ......
  if (!CreatePipe(channel_handle)) {
    ......
  }
}

这个函数定义在文件 external/chromium_org/ipc/ipc_channel_posix.cc 中。

从前面的调用过程可以知道,参数 channel_handle 描述的是一个 UNIX Socket 名称,参数 mode 的值等于 IPC::Channel::MODE_SERVER,参数 listener 指向的是前面创建的 ChannelProxy::Context 对象。

ChannelPosix 类继承了 ChannelReader 类,后者用来读取从 Render 进程发送过来的 IPC 消息,并且将读取到的 IPC 消息发送给参数 listener 描述的 ChannelProxy::Context 对象,因此这里会将参数 listener 描述的 ChannelProxy::Context 对象传递给 ChannelReader 的构造函数。

ChannelPosix 类通过 UNIX Socket 来描述 IPC 通信通道,这个 UNIX Socket 的 Server 端和 Client 文件描述符分别保存在成员变量 pipe_和 client_pipe_中。如果定义了宏 IPC_USES_READWRITE,那么当发送的消息包含有文件描述时,就会使用另外一个专用的 UNIX Socket 来传输文件描述符给对方。这个专用的 UNIX Socket 的 Server 端和 Client 端文件描述符保存在成员变量 fd_pipe_和 remote_fd_pipe_中。后面分析 IPC 消息的分发过程时,我们再详细分析这一点。

ChannelPosix 类的构造函数最后调用了另外一个成员函数 CreatePipe 开始创建 IPC 通信通道,如下所示: 

bool ChannelPosix::CreatePipe(
    const IPC::ChannelHandle& channel_handle) {
  ......
 
  int local_pipe = -1;
  if (channel_handle.socket.fd != -1) {
    ......
  } else if (mode_ & MODE_NAMED_FLAG) {
    ......
  } else {
    local_pipe = PipeMap::GetInstance()->Lookup(pipe_name_);
    if (mode_ & MODE_CLIENT_FLAG) {
      if (local_pipe != -1) {
        ......
        local_pipe = HANDLE_EINTR(dup(local_pipe));
        ......
      } else {
        ......
 
        local_pipe =
            base::GlobalDescriptors::GetInstance()->Get(kPrimaryIPCChannel);
      }
    } else if (mode_ & MODE_SERVER_FLAG) {
      ......
      base::AutoLock lock(client_pipe_lock_);
      if (!SocketPair(&local_pipe, &client_pipe_))
        return false;
      PipeMap::GetInstance()->Insert(pipe_name_, client_pipe_);
    } 
    ......
  }
 
#if defined(IPC_USES_READWRITE)
  // Create a dedicated socketpair() for exchanging file descriptors.
  // See comments for IPC_USES_READWRITE for details.
  if (mode_ & MODE_CLIENT_FLAG) {
    if (!SocketPair(&fd_pipe_, &remote_fd_pipe_)) {
      return false;
    }
  }
#endif  // IPC_USES_READWRITE
 
  ......
 
  pipe_ = local_pipe;
  return true;
}

这个函数定义在文件 external/chromium_org/ipc/ipc_channel_posix.cc 中。
ChannelHandle 类除了用来保存 UNIX Socket 的名称之外,还可以用来保存与该名称对应的 UNIX Socket 的文件描述符。在我们这个情景中,参数 channel_handle 仅仅保存了即将要创建的 UNIX Socket 的名称。

ChannelPosix 类的成员变量 mode_的值等于 IPC::Channel::MODE_SERVER,它的 MODE_NAMED_FLAG 位等于 0。Render 进程启动之后,也会调用到 ChannelPosix 类的成员函数 CreatePipe 创建一个 Client 端的 IPC 通信通道,那时候用来描述 Client 端 IPC 通信通道的 ChannelPosix 对象的成员变量 mode_的值 IPC::Channel::MODE_CLIENT,它的 MODE_NAMED_FLAG 位同样等于 0。因此,无论是在 Browser 进程中创建的 Server 端 IPC 通信通道,还是在 Render 进程中创建的 Client 端 IPC 通信通道,在调用 ChannelPosix 类的成员函数 CreatePipe 时,都按照以下逻辑进行。

对于 Client 端的 IPC 通信通道,即 ChannelPosix 类的成员变量 mode_的 MODE_CLIENT_FLAG 位等于 1 的情况,首先是在一个 Pipe Map 中检查是否存在一个 UNIX Socket 文件描述符与成员变量 pipe_name_对应。如果存在,那么就使用该文件描述符进行 IPC 通信。如果不存在,那么再到 Global Descriptors 中检查是否存在一个 UNIX Socket 文件描述符与常量 kPrimaryIPCChannel 对应。如果存在,那么就使用该文件描述符进行 IPC 通信。实际上,当网页不是在独立的 Render 进程中加载时,执行的是前一个逻辑,而当网页是在独立的 Render 进程中加载时,执行的是后一个逻辑。

Chromium 为了能够统一地处理网页在独立 Render 进程和不在独立 Render 进程加载两种情况,会对后者进行一个抽象,即会假设后者也是在独立的 Render 进程中加载一样。这样,Browser 进程在加载该网页时,同样会创建一个图 1 所示的 RenderProcess 对象,不过该 RenderProcess 对象没有对应的一个真正的进程,对应的仅仅是 Browser 进程中的一个线程。也就是这时候,图 1 所示的 RenderPocessHost 对象和 RenderProcess 对象执行的仅仅是进程内通信而已,不过它们仍然是按照进程间的通信规则进行,也就是通过 IO 线程来间接进行。不过,在进程内建立 IPC 通信通道和在进程间建立 IPC 通信通道的方式是不一样的。具体来说,就是在进程间建立 IPC 通信通道,需要将描述该通道的 UNIX Socket 的 Client 端文件描述符从 Browser 进程传递到 Render 进程,Render 进程接收到该文件描述符之后,就会以 kPrimaryIPCChannel 为键值保存在 Global Descriptors 中。而在进程内建立 IPC 通信通道时,描述 IPC 通信通道的 UNIX Socket 的 Client 端文件描述符直接以 UNIX Socket 名称为键值,保存在一个 Pipe Map 中即可。后面我们分析在进程内在进程间创建 Client 端 IPC 通信通道时,会继续看到这些相关的区别。

对于 Server 端的 IPC 通信通道,即 ChannelPosix 类的成员变量 mode_的 MODE_SERVER_FLAG 位等于 1 的情况,ChannelPosix 类的成员函数 CreatePipe 调用函数 SocketPair 创建了一个 UNIX Socket,其中,Server 端文件描述符保存在成员变量 pipe_中,而 Client 端文件描述符保存在成员变量 client_pipe_中,并且 Client 端文件描述符还会以与前面创建的 UNIX Socket 对应的名称为键值,保存在一个 Pipe Map 中,这就是为建立进程内 IPC 通信通道而准备的。

最后,如果定义了 IPC_USES_READWRITE 宏,如前面提到的,那么还会继续创建一个专门用来在进程间传递文件描述的 UNIX Socket,该 UNIX Socket 的 Server 端和 Client 端文件描述符分别保存在成员变量 fd_pipe_和 remote_fd_pipe_中。

这一步执行完成之后,一个 Server 端 IPC 通信通道就创建完成了。回到 ChannelProxy 类的成员函数 Init 中,它接下来是发送一个消息到 Browser 进程的 IO 线程的消息队列中,该消息绑定的是 ChannelProxy::Context 类的成员函数 OnChannelOpened,它的实现如下所示: 

void ChannelProxy::Context::OnChannelOpened() {
  ......
 
  if (!channel_->Connect()) {
    OnChannelError();
    return;
  }
 
  ......
}