共计 4764 个字符,预计需要花费 12 分钟才能阅读完成。
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 的流程更加简洁。