关注微信公众号《云原生CTO》更多云原生干货等你来探索
专注于 云原生技术
分享
提供优质 云原生开发
视频技术培训
面试技巧
,及技术疑难问题 解答

云原生技术分享不仅仅局限于Go
、Rust
、Python
、Istio
、containerd
、CoreDNS
、Envoy
、etcd
、Fluentd
、Harbor
、Helm
、Jaeger
、Kubernetes
、Open Policy Agent
、Prometheus
、Rook
、TiKV
、TUF
、Vitess
、Argo
、Buildpacks
、CloudEvents
、CNI
、Contour
、Cortex
、CRI-O
、Falco
、Flux
、gRPC
、KubeEdge
、Linkerd
、NATS
、Notary
、OpenTracing
、Operator Framework
、SPIFFE
、SPIRE
和 Thanos
等


如何启动进程(主要在 Linux 中)
介绍
你想从你的程序中运行一个可执行文件吗?还是以编程方式执行 shell
命令?或者也许只是并行化您的代码?您是否阅读了大量有关execve()
函数的代码,fork()
但仍然脑子里一团糟?那么这篇文章是给你的。
如何启动Linux进程
系统调用
让我们保持简单,从头开始。我们正在为 Linux
开发一个程序。让我们来看看所谓的系统调用——Linux
为我们提供的用于请求内核功能的接口。
Linux
使用系统调用来处理进程:
fork(void)( man 2 fork)
- 创建调用进程的完整副本。由于需要复制进入进程的地址空间,因此听起来效率低下,但使用了写时复制优化。这是在Linux
中创建进程的唯一(意识形态)方法。然而,在新版本的内核中fork()
是在棘手的clone()
系统调用之上实现的,现在可以clone()
直接使用来创建进程,但为了简单起见,我们将跳过这些细节。execve(path, args, env)( man 2 execve)
- 通过执行指定的文件将调用进程转换为新进程path
。实际上,它用一个新的过程镜像替换了当前的过程镜像,并且不会创建任何新的进程。pipe(fildes[2] __OUT)( man 2 pipe)
- 创建一个管道,它是一个进程间通信原语。通常管道是单向的数据流。数组的第一个元素连接到管道的读取端,第二个元素连接到写入端。写入的数据fildes[1]
可以从fildes[0]
我们不会看前面提到的系统调用源代码,因为它是内核的一部分,很难理解。
我们考虑的另一个重要部分是 Linux shell
- 命令解释器实用程序(即常规程序)。shell
进程不断地从标准输入中读取。用户通常通过键入一些命令和按键来与外壳交互enter
。然后 shell
进程执行提供的命令。这些进程的标准输出连接到 shell
进程的标准输出。但是,shell
进程可以自己作为子进程启动,并且可以通过-c
参数指定要执行的命令。例如。bash -c "date"
.
C标准库
当然,我们正在开发我们的程序C
以尽可能接近操作系统级原语。C
有一个所谓的标准库 libc
- 一组广泛的功能来简化用这种语言编写程序。它还提供环绕系统调用的功能。
C
标准库具有以下功能(在基于 Debian
的发行版上apt-get download glibc-source
):
system(command)( man 3 system)
- 启动一个shell
进程来执行所提供的command
。调用进程被阻塞,直到底层shell
进程执行结束。system()
返回 shell
进程的退出代码。让我们在看看执行的STDLIB
此功能:
int system(char *command)
{
// ... skip signals tricks for simplicity ...
switch(pid = vfork()) {
case -1: // error
// ...
case 0: // child
execl("/bin/sh", "sh", "-c", command, (char *)NULL);
_exit(127); // will be called only if execl() returns, i.e. a syscall faield.
}
// ... skip signals tricks for simplicity ...
waitpid(pid, (int *)&pstat, 0); // waiting for the child process, i.e. shell.
return pstat.w_status;
}
所以实际上,system()
只是使用fork()+ exec()+
的组合waitpid()
。
popen(command, mode = 'r|w')( man 3 popen)
- 使用执行提供的命令的 shell
实例分叉并替换分叉的进程。听起来很像system()?
区别在于通过其标准输入或标准输出与子进程通信的能力。但通常采用单向方式。为了与这个进程通信,pipe
使用了一个。真正的实现可以在这里和这里找到,但主要思想如下:
http://www.retro11.de/ouxr/211bsd/usr/src/lib/libc/gen/popen.c.html
https://github.com/bminor/glibc/blob/09533208febe923479261a27b7691abef297d604/libio/iopopen.c
FILE * popen(char *program, char *type)
{
int pdes[2], fds, pid;
pipe(pdes); // create a pipe
switch (pid = vfork()) { // fork the current process
case -1: // error
// ...
case 0: // child
if (*type == 'r') {
dup2(pdes[1], fileno(stdout)); // bind stdout of the child process to the writing end of the pipe
close(pdes[1]);
close(pdes[0]); // close reading end of the pipe on the child side
} else {
dup2(pdes[0], fileno(stdin)); // bind stdin of the child process to the reading end of the pipe
close(pdes[0]);
close(pdes[1]); // close writing end of the pipe on the child side
}
execl("/bin/sh", "sh", "-c", program, NULL); // replace the child process with the shell running our command
_exit(127); // will be called only if execl() returns, i.e. a syscall faield.
}
// parent
if (*type == 'r') {
result = pdes[0];
close(pdes[1]);
} else {
result = pdes[1];
close(pdes[0]);
}
return result;
}
恭喜,到此为止!
NB1:子进程启动的shell
实现非常相似。即fork()+ execve()
。
NB2:值得一提的是,其他编程语言通常实现与操作系统 libc
的绑定(并为方便起见进行一些包装)以提供特定于操作系统的功能。
为什么要启动Linux进程
并行执行
最简单的一种。我们只需要fork()
. 调用fork()
实际上重复了您的程序过程。但是由于这个进程使用完全独立的地址空间与它通信,我们无论如何都需要进程间通信原语。甚至分叉进程的指令集与父进程的指令集相同,它是程序的不同实例。
进程间通信原语: https://en.wikipedia.org/wiki/Inter-process_communication
只需从您的代码运行程序
如果您只需要运行一个程序,而不需要与其 stdin/stdout
通信,那么 libcsystem()
函数是最简单的解决方案。是的,您也可以fork()
在您的进程中运行,然后exec()
在子进程中运行,但由于这是一个非常常见的场景,因此有system()
函数。
运行一个进程并读取其标准输出(或写入其标准输入)
我们需要popen()libc
函数。是的,你仍然可以只通过组合实现的目标pipe()+ fork()+exec()
如上图所示,但popen()
在这里,以减少样板代码量。
运行一个进程,写入其标准输入并从其标准输出读取
最有趣的一个。由于某些原因,默认popen()
实现通常是单向的。但看起来我们可以很容易地提出双向解决方案:我们需要两个管道,第一个将连接到孩子的标准输入,第二个连接到孩子的标准输出。剩下的部分是fork()
子进程,通过dup2()IO
描述符和execve()
命令连接管道。一种潜在的实现可以在我的 GitHub popen2()
项目中找到。在开发此类功能时,您应该注意的另一件事是泄漏先前通过以下方式打开的管道的打开文件描述符popen()
过程。如果我们忘记在每个子 fork
中明确关闭外部文件描述符,就有可能对兄弟的stdins
和stdouts
进行 IO
操作。听起来像是一个漏洞。为了能够关闭所有这些文件描述符,我们必须跟踪它们。我使用了一个static
带有此类描述符链接列表的变量:
代码地址:https://github.com/iximiuz/popen2
泄漏:https://gist.github.com/iximiuz/65c7d2d128c374ef83d885dfef74bed7
static files_chain_t *files_chain;
file_t *popen2(const char *command) {
file_t *fp = malloc(); // allocate new element of the chain
_do_popen(fp, command);
// add the current result to the chain
fp->next = files_chain;
files_chain = fp;
}
_do_popen() {
// open pipes
// fork()
// if is_child:
// for (fp in files_chain):
// close(fp->in); close(fp->out);
}
int pclose2(file_t *fp) {
// if (fp in files_chain):
// ... do payload ...
// remove fp from the chain
free(fp); // DO NOT FORGET TO FREE THE MEMORY WE ALLOCATED DURING popen2() CALL
}
关于 Windows 的几句话
Windows
操作系统家族在处理进程方面的范例略有不同。如果我们跳过Windows 10
上引入的新Unix
兼容层并尝试为 Windows
移植POSIX API
支持,我们将只有老派 WinAPI
的两个函数:
CreateProcess(filename)
- 为给定的可执行文件启动一个全新的进程。ShellExecute(Ex)(command)
- 启动一个 shell
(是的,Windows
也有一个 shell
概念)进程来执行提供的命令。所以,没有forks
和execves
。但是,也可以使用管道与启动的进程进行通信。
参考地址[1]
参考资料
参考地址: https://iximiuz.com/en/posts/how-to-on-processes/




