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 的流程更加简洁。