欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

【Chromium】GPU进程启动流程

程序员文章站 2022-04-28 10:50:03
本篇文档以gpu进程的创建和启动为例,讲述chormium如何启动一个browser进程的子进程 PS:本文使用的chromium代码版本为71 前言 GPU进程的启动时机是由browser进程负责的,browser进程会在进入message loop之前启动两个进程,先是启动zygote进程,然后 ......

本篇文档以gpu进程的创建和启动为例,讲述chormium如何启动一个browser进程的子进程
ps:本文使用的chromium代码版本为71

前言

gpu进程的启动时机是由browser进程负责的,browser进程会在进入message loop之前启动两个进程,先是启动zygote进程,然后是gpu进程

gpu进程的创建和命令行参数的准备

下面是在文件browser_main_loop.cc中的函数browserthreadsstarted的代码片段

int browsermainloop::browserthreadsstarted() {
...
  if (gpudatamanagerimpl::getinstance()->gpuprocessstartallowed() &&
      !established_gpu_channel && always_uses_gpu && browser_is_viz_host) {
    trace_event_instant0("gpu", "post task to launch gpu process",
                         trace_event_scope_thread);
    base::posttaskwithtraits(
        from_here, {browserthread::io},
        base::bindonce(base::ignoreresult(&gpuprocesshost::get),
                       gpuprocesshost::gpu_process_kind_sandboxed,
                       true /* force_create */));
  }
...
}

其中gpuprocesshost::get函数就是一切的开始,但是在开始之前先说下browserthreadstarted这个函数所处的位置和地位,从文件名可以看出browsermainloop这个类负责browser进程的主要工作:即主循环,在主循环刚启动时,需要启动一些必需的任务,负责启动这些任务的函数是browsermainloop::createstartuptasks,说到这,其实还不太清楚是什么时机启动的,负责调用这个函数的是browsermainrunnerimp;(可以看成是browser进程的入口函数的等价)

// main routine for running as the browser process.
int browsermain(const mainfunctionparams& parameters) {
  scopedbrowsermainevent scoped_browser_main_event;

  base::trace_event::tracelog::getinstance()->set_process_name("browser");
  base::trace_event::tracelog::getinstance()->setprocesssortindex(
      ktraceeventbrowserprocesssortindex);

  std::unique_ptr<browsermainrunnerimpl> main_runner(
      browsermainrunnerimpl::create());

  int exit_code = main_runner->initialize(parameters);
  if (exit_code >= 0)
    return exit_code;

  exit_code = main_runner->run();

  main_runner->shutdown();

  return exit_code;
}
int browsermainrunnerimpl::initialize(const mainfunctionparams& parameters) {
...
  main_loop_->createstartuptasks();
...
}

那么继续聊方才提到的gpuprocesshost::get,他干了点什么呢?主要是初始化gpuprocesshost对象并调用gpuprocesshost::init函数,下面是init函数片段

bool gpuprocesshost::init() {
  init_start_time_ = base::timeticks::now();
...
  if (in_process_) {
    dcheck_currently_on(browserthread::io);
    dcheck(getgpumainthreadfactory());
    gpu::gpupreferences gpu_preferences = getgpupreferencesfromcommandline();
    gpudatamanagerimpl::getinstance()->updategpupreferences(&gpu_preferences);
    in_process_gpu_thread_.reset(getgpumainthreadfactory()(
        inprocesschildthreadparams(
            base::threadtaskrunnerhandle::get(),
            process_->getinprocessmojoinvitation(),
            process_->child_connection()->service_token()),
        gpu_preferences));
    base::thread::options options;
#if defined(os_win) || defined(os_macosx)
    // wgl needs to create its own window and pump messages on it.
    options.message_loop_type = base::messageloop::type_ui;
#endif
#if defined(os_android) || defined(os_chromeos)
    options.priority = base::threadpriority::display;
#endif
    in_process_gpu_thread_->startwithoptions(options);

    onprocesslaunched();  // fake a callback that the process is ready.
  } else if (!launchgpuprocess()) {
    return false;
  }
...

其中launchgpuprocess就是启动gpu进程的元凶,而这个函数的主要任务是构造进程使用的参数,也就是cmd_line,然后把cmd_line交给真正启动进程的browserchildprocesshostimpl对象,调用browserchildprocesshostimpl::launch启动一个browser的子进程

子进程创建

为什么要把这个部分独立出来呢?google除了browser以外的进程都是用下面的流程创建出来的,因此独立出来作为通用部分讲解。

void browserchildprocesshostimpl::launch(
    std::unique_ptr<sandboxedprocesslauncherdelegate> delegate,
    std::unique_ptr<base::commandline> cmd_line,
    bool terminate_on_shutdown) {
  dcheck_currently_on(browserthread::io);

  getcontentclient()->browser()->appendextracommandlineswitches(cmd_line.get(),
                                                                data_.id);

  const base::commandline& browser_command_line =
      *base::commandline::forcurrentprocess();
  static const char* const kforwardswitches[] = {
      service_manager::switches::kdisableinprocessstacktraces,
      switches::kdisablebackgroundtasks,
      switches::kdisablelogging,
      switches::kenablelogging,
      switches::kipcconnectiontimeout,
      switches::klogginglevel,
      switches::ktracetoconsole,
      switches::kv,
      switches::kvmodule,
  };
  cmd_line->copyswitchesfrom(browser_command_line, kforwardswitches,
                             arraysize(kforwardswitches));

  if (child_connection_) {
    cmd_line->appendswitchascii(
        service_manager::switches::kservicerequestchanneltoken,
        child_connection_->service_token());
  }

  // all processes should have a non-empty metrics name.
  dcheck(!data_.metrics_name.empty());

  notify_child_disconnected_ = true;
  child_process_.reset(new childprocesslauncher(
      std::move(delegate), std::move(cmd_line), data_.id, this,
      std::move(mojo_invitation_),
      base::bind(&browserchildprocesshostimpl::onmojoerror,
                 weak_factory_.getweakptr(),
                 base::threadtaskrunnerhandle::get()),
      terminate_on_shutdown));
  sharemetricsallocatortoprocess();
}

先不看该函数的第一个参数std::unique_ptr<sandboxedprocesslauncherdelegate> delegate(和沙盒有关,所有的子进程多多少少都被有沙盒所限制),重点在该函数的最后childprocesslauncher,这个类的构造函数中会构造另一个类childprocesslauncherhelper的实例

  helper_ = new childprocesslauncherhelper(
      child_process_id, client_thread_id_, std::move(command_line),
      std::move(delegate), weak_factory_.getweakptr(), terminate_on_shutdown,
      std::move(mojo_invitation), process_error_callback);
  helper_->startlaunchonclientthread();

关键就是这个helper的startlaunchonclientthread()函数,这个函数会在client线程上启动一个新的进程,但是在,71目前的版本中browser中已经移除了名称为client的线程,这说明什么?说明google可能还没给这个函数改名字,在64版本的chromium代码中确实是由client线程创建进程,但是在71中则是交给了一个线程池的worker去创建进程了

void childprocesslauncherhelper::launchonlauncherthread() {
  dcheck(currentlyonprocesslaunchertaskrunner());

  begin_launch_time_ = base::timeticks::now();

  std::unique_ptr<filemappedforlaunch> files_to_register = getfilestomap();

  bool is_synchronous_launch = true;
  int launch_result = launch_result_failure;
  base::launchoptions options;

  process process;
  if (beforelaunchonlauncherthread(*files_to_register, &options)) {
    process =
        launchprocessonlauncherthread(options, std::move(files_to_register),
                                      &is_synchronous_launch, &launch_result);

    afterlaunchonlauncherthread(process, options);
  }

  if (is_synchronous_launch) {
    postlaunchonlauncherthread(std::move(process), launch_result);
  }
}

从该函数就能明显的看出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());
  dcheck(mojo_channel_);
  dcheck(mojo_channel_->remote_endpoint().is_valid());

  // todo(750938): implement sandboxed/isolated subprocess launching.
  process child_process;
  child_process.process = base::launchprocess(*command_line(), options);
  return child_process;
}

可以看到进程的创建其实是使用了base库的launchprocess函数,熟悉chromium代码的话会知道,base库是基础库,提供一些常用组件(例如智能指针,字符串等等结构),那么到这步的话就能知道真的要开始见到熟悉的进程创建代码了。因为我是在linux环境下运行的chromium,因此要在base/process/launch_posix.cc文件中看函数实现,如果是windows环境可以在base/process/launch_windows.cc文件中看该函数的实现。

process launchprocess(const std::vector<std::string>& argv,
                      const launchoptions& options) {
  trace_event0("base", "launchprocess");
...
  {
    pid = fork();
  }

  // always restore the original signal mask in the parent.
  if (pid != 0) {
    base::timeticks after_fork = timeticks::now();
    setsignalmask(orig_sigmask);

    base::timedelta fork_time = after_fork - before_fork;
    uma_histogram_times("mparch.forktime", fork_time);
  }

  if (pid < 0) {
    dplog(error) << "fork";
    return process();
  }
  if (pid == 0) {
    // child process
...
    const char* executable_path = !options.real_path.empty() ?
        options.real_path.value().c_str() : argv_cstr[0];

    execvp(executable_path, argv_cstr.data());

    raw_log(error, "launchprocess: failed to execvp:");
    raw_log(error, argv_cstr[0]);
    _exit(127);
  } else {
    // parent process
    if (options.wait) {
      // while this isn't strictly disk io, waiting for another process to
      // finish is the sort of thing threadrestrictions is trying to prevent.
      scopedblockingcall scoped_blocking_call(blockingtype::may_block);
      pid_t ret = handle_eintr(waitpid(pid, nullptr, 0));
      dpcheck(ret > 0);
    }
  }

  return process(pid);
}

可以看到fork出来的子进程会去执行一个程序并将之前准备的cmd_line放入argv_cstr中,execvp(executable_path, argv_cstr.data());那么executable_path就成为子进程执行什么程序的关键。子进程到这里就创建完毕了。在调试browser进程的时候是无法调试到if (pid == 0)的子进程的部分的┓( ´∀` )┏,还是用日志打来看吧。

创建gpu进程时关键参数executable_path的值是/proc/self/exe,而对应的参数是

--type=gpu-process
--field-trial-handle=...
--user-data-dir=...
--homedir=...
--gpu-preferences=...
--service-request-channel-token=...

其中...代表一些具体设置的值

chromium子进程创建流程

以为到这里就结束了?还没呢!难道对/proc/self/exe不感兴趣么?这明显不是个gpu程序吧?在chromium代码中有个content/gpu/gpu_main.cc文件,其中有个int gpumain(const mainfunctionparams& parameters)函数,这看着才像是gpu进程的入口啊(事实证明也是如此),那么是如何完成这个跳转的?

首先先看/proc/self/exe,这个东西的功能是再执行自己一次,没错自己执行自己,例如你在bash下执行这个可执行程序就会又进入一个bash。那么google让browser进程的子进程执行这个东西是为了让子进程走一遍主入口函数的流程进行同样的初始化,然后在入口后不久就区分进程类型,这就是这个--type=gpu-process参数的意义用于区分进程类型,然后确定子进程执行的入口,比如gpu就去执行gpumain,renderer进程执行renderermain等等。没错,browser进程也是在这部分区分为主进程的,主进程在启动时没有--type参数,所以在区分会被命名为browser进程

那么这个谁都会走的流程是什么样的呢?下面是运行堆栈

#3 0x7fe228646e77 content::contentmainrunnerimpl::run()
#4 0x7fe22863cbac content::contentservicemanagermaindelegate::runembedderprocess()
#5 0x7fe22e442bb1 service_manager::main()
#6 0x7fe228642d25 content::contentmain()
#7 0x55d02587b566 chromemain
#8 0x55d02587b472 main
#9 0x7fe1fdbe9830 __libc_start_main
#10 0x55d02587b34a _start

其中main就是熟悉的入口啦,那么区分进程类型的关键就在content::contentmainrunnerimpl::run()

int contentmainrunnerimpl::run(bool start_service_manager_only) {
  dcheck(is_initialized_);
  dcheck(!is_shutdown_);
  const base::commandline& command_line =
      *base::commandline::forcurrentprocess();
  std::string process_type =
      command_line.getswitchvalueascii(switches::kprocesstype);
...
  if (process_type.empty()) {
...
      return runbrowserprocessmain(main_params, delegate_);
  } // if (process_type.empty())
...
  return runothernamedprocesstypemain(process_type, main_params, delegate_);
}

这里区分了browser进程和其他类型进程,runothernamedprocesstypemain这个函数会完成gpu进程的区分。

ps:如果是fork出来的进程的话,这里是已经在子进程中了,也就是说除了browser进程,一般都是runothernamedprocesstypemain

int runothernamedprocesstypemain(const std::string& process_type,
                                 const mainfunctionparams& main_function_params,
                                 contentmaindelegate* delegate) {
#if !defined(chrome_multiple_dll_browser)
  static const mainfunction kmainfunctions[] = {
#if buildflag(enable_plugins)
    {switches::kppapipluginprocess, ppapipluginmain},
    {switches::kppapibrokerprocess, ppapibrokermain},
#endif  // enable_plugins
    {switches::kutilityprocess, utilitymain},
    {switches::krendererprocess, renderermain},
    {switches::kgpuprocess, gpumain},
  };

  for (size_t i = 0; i < base::size(kmainfunctions); ++i) {
    if (process_type == kmainfunctions[i].name) {
      int exit_code = delegate->runprocess(process_type, main_function_params);
      if (exit_code >= 0)
        return exit_code;
      return kmainfunctions[i].function(main_function_params);
    }
  }
#endif  // !chrome_multiple_dll_browser

#if buildflag(use_zygote_handle)
  // zygote startup is special -- see runzygote comments above
  // for why we don't use zygotemain directly.
  if (process_type == service_manager::switches::kzygoteprocess)
    return runzygote(delegate);
#endif  // buildflag(use_zygote_handle)

  // if it's a process we don't know about, the embedder should know.
  return delegate->runprocess(process_type, main_function_params);
}

从整个流程就能看出gpumain并不能算是gpu子进程的入口函数,只是个被调用的函数而已。delegate->runprocess(process_type, main_function_params);这句代码十分有迷惑性,其实并不是在跑进程,而是和kmainfunctions[i].function(main_function_params);一样在执行函数,但是由于delegate并不处理gpu,所以暂且不看runprocess的实现了,gpumain的执行交给了mainfunction这个结构体,结构体如下

// we dispatch to a process-type-specific foomain() based on a command-line
// flag.  this struct is used to build a table of (flag, main function) pairs.
struct mainfunction {
  const char* name;
  int (*function)(const mainfunctionparams&);
};

一个很简单的函数指针,所以直接执行就完事了。

chromium 采用了这种方式去初始化进程,我还需要多多学习啊