如何启动进程(主要在 Linux 中)

语言: CN / TW / HK

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
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]

参考资料

[1]

参考地址: https://iximiuz.com/en/posts/how-to-on-processes/