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(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(m_appData->argv()));
return retVal;
}
方案的优点
- 开发者可⾃定义 Booster 类,定制需要预加载的资源及应⽤启动前的准备流程
- 可配置沙盒及能⼒控制,确保系统的安全
- 利⽤ fork 系统调⽤的 COW 机制,节约系统内存
改进思路
个⼈感觉 zygote 的结构更好合理,mapplauncherd 的 daemon 服务完全可以作为应⽤孵化器,监听所有 invoker 请求,当有 invoker 请求连⼊时才开始 fork ⼦进程。⽽不是预先 fork 出⼀个 booster,在 load main 函数之后再让 daemon fork 另⼀个 booster,这个过程感觉上是多余的,zygote 的流程更加简洁。