mapplauncherd 项⽬解析【鲸鲮JingOS】

鲸鲮JingOS -
mapplauncherd 项⽬解析【鲸鲮JingOS】

mapplauncherd 是 sailfishos 使⽤的⼀种应⽤启动加速的模块,类似于 Android 的 zygote。最初 mapplauncherd 是由 MeeGo 开发,后被各 Linux based 系统⽤于应⽤启动的模块。本⽂主要分析 mapplauncherd 的基本运⾏原理

源码参考

https://github.com/sailfishos/mapplauncherd.git

编译
安装依赖
sudo apt-get install libcap-dev libsystemd-dev libdbus-1-dev mkdir build
&& cd build
cmake ../ 
make
使用方法
# 安装
cd build 
mkdir testbin
DESTDIR=./testbin make install

# 运⾏ daemon
LD_LIBRARY_PATH=./usr/local/lib ./usr/local/libexec/mapplauncherd/booster-generic

# 再打开另⼀个 terminal 运⾏ invoker
./usr/local/bin/invoker -t generic /path/to/exec
源码分析

文件布局,关键文件解释

invoker ⽬录,⽤来将应⽤信息传递给 launcher daemon 的⼯具
launcherlib ⽬录,其中定义了核⼼的功能类
  appdata 应⽤信息
  booster 启动加速类
  connection 连接管理
  daemon 守护进程
框架简述

mapplauncherd 整体上分为两个部分

daemon service,主控服务,其作⽤是整体管控应⽤的启动、结束、异常等流程invoker,应⽤启动⼯具,⽤来通知 daemon service 启动某个应⽤基础类说明

Daemon 类,对 daemon 基础功能的封装,是 mapplauncherd 的主控模块,Daemon 服务进程负责 fork 出 booster 加速进程。

SocketManager ⽤来管理 Booster 监听的 socket ⽂件,该 socket ⽤于 invoker 发送应⽤启动请求。

Booster 类,对于所有 booster 类型的抽象,顾名思义,这 booster 是来⽤做应⽤启动加速的基类,⽽被启动应⽤⼀般会被分成⼏种类型,如 Qt/QML 应⽤,普通 native 应⽤,或⽤户⾃定义类型的应⽤。其能够加速的原因就是 Booster 预加载了某些公共资源,如QML 控件、公共库等,根本上提⾼了应⽤的启动速度。Booster 进程还⽤于接收 invoker 发送来的启动请求。

我们可以创建⼀个新的继承⾃ Booster 基类的 JBooster 类,⽤来加载 JingOS ⾃定义的公共组件。

示例如下:

class JBooster : public Booster
{
public:
    JBooster() {}
protected:
    bool preload() {
         // 加载公共库⽂件
        // 加载 QML 公共控件
    }
};

类关系如下:


 

关键流程分析初始化流程

⽤户需先⾏确定好⼀种启动 booster daemon 服务的⽅法,如利⽤ systemd 机制开机⾃启动。

⾸先创建⾃定义 Booster,即 JBooster,然后创建 Daemon 类对象,将 JBooster 对象传⼊ Daemon。

Daemon 构造函数中创建⼀个 socketpair,⽤来与 fork 出来的 booster 加速进程通信,具体通信的内容会在后⽂中介绍。

Daemon 构造后调⽤ run 进⼊主循环,为⼦进程(booster进程)创建⽤于接收 invoker 请求的 socket,其实在 booster 进程 fork 之后再创建这个 socket 也是可以的,mapplauncherd 在 Daemon 进程中就将 socket 创建好也应该是为了加速的⽬的。

资源准备好后开始 fork booster ⼦进程,⼦进程对 Booster 类对象进⾏初始化,主要设置两个 socket,与⽗进程通信的newBoosterLauncherSocket和⽤于接收 invoker 请求的 socketFd

初始化结束即进⼊主循环等待 invoker 的连接。


 

signal 信号处理流程

如果⽤户 kill daemon 进程的话,mapplauncherd 需要做怎样的处理呢? 在 Daemon 构造函数中定义了信号处理函数

以下代码仅展示 signal 处理相关的内容

Daemon::Daemon(int &argc, char *argv[]) {

    // Install signal handlers. The original handlers are saved
   // in the daemon instance so that they can be restored in boosters. 
  setUnixSignalHandler(SIGCHLD, write_to_signal_pipe);// reap zombies 
  setUnixSignalHandler(SIGINT, write_to_signal_pipe); // exit launcher 
  setUnixSignalHandler(SIGTERM, write_to_signal_pipe);// exit launcher 
  setUnixSignalHandler(
      SIGUSR1, write_to_signal_pipe);// enter normal mode from boot mode 
  setUnixSignalHandler(
      SIGUSR2, write_to_signal_pipe);    // enter boot mode (same as --boot-mode) 
  setUnixSignalHandler(SIGPIPE, write_to_signal_pipe);// broken invoker's pipe 
  setUnixSignalHandler(SIGHUP, write_to_signal_pipe); // re-exec
}

信号统⼀由 write_to_signal_pipe 函数处理

static void write_to_signal_pipe(int sig) { 
    char v =   (char) sig;
    if (write(Daemon::instance()->sigPipeFd(), &v, 1) != 1) {
        /* If we can't write to internal signal forwarding
    * pipe, we might as well quit */
       const char m[] = "*** signal pipe write failure - terminating\n";
       if (write(STDERR_FILENO, m, sizeof m - 1) == -1) {
           // dontcare
       }
       _exit(EXIT_FAILURE);
    }    

write_to_signal_pipe 函数很简单,只是向 sigPipeFd() 中写⼊具体是什么信号,pipe 是在 Daemon 构造函数中创建,读端已经加⼊到了 poll set 中,写⼊时即触发 poll,处理相应的信号。这样处理的原因是在 signal handler 中最好不要做太多的逻辑处理,更不能操作 heap memory,如 malloc 之类的调⽤,这样会导致死锁,详⻅《Unix 环境⾼级编程》中的讲解。

Daemon 是系统关键服务,如果它退出之后需要将所有经由 booster 启动的应⽤退掉。

case SIGINT:
case SIGTERM: { for (;;) {
// 遍历所有 booster 进程 pid
PidVect::iterator iter(m_children.begin()); if (iter == m_children.end())
// 遍历结束后 break 出循环
break;
pid_t booster_pid = *iter;

/* Terminate booster */ kill_process("booster", booster_pid);
}

Logger::logDebug("booster exit");

// Daemon 进程退出
exit(EXIT_SUCCESS);
break;
}

当接收到应⽤进程退出的信号后回收⼦进程,即调⽤ waitpid

case SIGCHLD:
     reapZombies(); 
     break;
invoker 请求流程

桌⾯启动应⽤实质上是调⽤ invoker 命令,invoker 的参数中包含需要启动应⽤的可执⾏程序,如⽂章前⾯介绍的使⽤⽅法的中提到的。

invoker 连接 booster 的 socket ⽂件,将需要启动的应⽤的可执⾏⽂件路径传给 booster,booster 需要为应⽤准备沙盒环境,如uid等配置,出于安全⽅⾯的考虑,需要指定应⽤可以具有的能⼒,⼀切就绪后开始加载 main 函数。

最后向 daemon 发送启动成功的信息,daemon 再次启动⼀个 booster ⽤于⼀次应⽤的启动请求。

应⽤启动流程

为了⽀持 mapplauncherd 的启动机制,应⽤的可执⾏程序需要是 shared object ⽽不能是 executable,这就需要在编译时加⼊ -pie (position independent executable) 选项(gcc ),并将 main 函数 export 出来。

加载应⽤ main 函数的过程如下:

void *Booster::loadMain() {
  // Setup flags for dlopen 

  int dlopenFlags = RTLD_LAZY;

  if (m_appData->dlopenGlobal())
    dlopenFlags |= RTLD_GLOBAL;

  else
    dlopenFlags |= RTLD_LOCAL;


#if (PLATFORM_ID == Linux) && defined( GLIBC ) 
  if (m_appData->dlopenDeep())
    dlopenFlags |= RTLD_DEEPBIND; 
#endif


  // 打开 invoker 发送过来的可执⾏程序
  void *module = dlopen(m_appData->fileName().c_str(), dlopenFlags);


  dlerror();
  // 导出 main 函数
  m_appData->setEntry(reinterpret_cast<entry_t>(dlsym(module, "main")));


  const char *error_s = dlerror(); 
  if (error_s != NULL)
    throw std::runtime_error(
       std::string("Booster: Loading symbol 'main' failed: '") + error_s +
       "'\n");


  return module;
}

~
~

执行过程

int Booster::launchProcess() { 
  setEnvironmentBeforeLaunch();

  // 加载 main 函数
  loadMain();

  // 调⽤ main 函数
  const int retVal = m_appData->entry()(m_appData->argc(),
                                        const_cast<char **>(m_appData->argv()));

  return retVal;
}
方案的优点开发者可⾃定义 Booster 类,定制需要预加载的资源及应⽤启动前的准备流程可配置沙盒及能⼒控制,确保系统的安全利⽤ fork 系统调⽤的 COW 机制,节约系统内存改进思路

个⼈感觉 zygote 的结构更好合理,mapplauncherd 的 daemon 服务完全可以作为应⽤孵化器,监听所有 invoker 请求,当有 invoker 请求连⼊时才开始 fork ⼦进程。⽽不是预先 fork 出⼀个 booster,在 load main 函数之后再让 daemon fork 另⼀个 booster,这个过程感觉上是多余的,zygote 的流程更加简洁。

特别申明:本文内容来源网络,版权归原作者所有,如有侵权请立即与我们联系(cy198701067573@163.com),我们将及时处理。
下一篇: Linux命令补充

Tags 标签

linuxgitubuntuandroid应用开发

扩展阅读

加个好友,技术交流

1628738909466805.jpg