如何启动进程(主要在 Linux 中)
关 注 微 信 公 众 号 《 云 原 生 C T O 》 更 多 云 原 生 干 货 等 你 来 探 索
专 注 于 云原生技术
分 享
提 供 优 质 云原生开发
视 频 技 术 培 训
面试技巧
, 及 技 术 疑 难 问 题 解答
云 原 生 技 术 分 享 不 仅 仅 局 限 于 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
http://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()
实际上重复了您的程序过程。但是由于这个进程使用完全独立的地址空间与它通信,我们无论如何都需要进程间通信原语。甚至分叉进程的指令集与父进程的指令集相同,它是程序的不同实例。
进程间通信原语: http://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
带有此类描述符链接列表的变量:
代码地址:http://github.com/iximiuz/popen2
泄漏:http://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]
参考资料
参考地址: http://iximiuz.com/en/posts/how-to-on-processes/
- 多个维度分析k8s多集群管理工具,到底哪个才真正适合你
- 使用 Kube-capacity CLI 查看 Kubernetes 资源请求、限制和利用率
- 使用 Go 在 Kubernetes 中构建自己的准入控制器
- 云原生数仓如何破解大规模集群的关联查询性能问题?
- 云原生趋势下的迁移与灾备思考
- 2022 年不容错过的六大云原生趋势!
- 使用 Prometheus 监控 Golang 应用程序
- 云原生时代下的机遇与挑战 DevOps如何破局
- 如何在云原生格局中理解Kubernetes合规性和安全框架
- 设计云原生应用程序的15条基本原则
- 使用 Operator SDK 为 Pod 标签编写Controller
- Kubernetes Visitor 模式
- 为什么云原生是第二次云革命
- 构建云原生安全的六个重要能力
- 扩展云原生策略的步骤有哪些?
- 七个值得关注的开源云原生工具
- K8S - 创建一个 kube-scheduler 插件
- 如何诊断 Kubernetes 应用程序中的 OOMKilled 错误
- 云原生 Kubernetes 分布式存储平台 Longhorn 初体验
- 坐上时光机飞到1960年代末,探索容器的历史