zl程序教程

您现在的位置是:首页 >  系统

当前栏目

Linux - 如何启动进程

Linux进程 如何 启动
2023-09-11 14:22:09 时间

介绍/Intro

你想从你的代码中运行一个可执行文件吗?或者在代码中来运行shell命令?或者只是想并行执行你的代码?你是否读了很多关于execve( )系列函数和fork( )函数的信息,但脑子里还是一片混乱?那么这篇文章就是为你准备的。

如何启动Linux进程

1,System calls / 系统调用

让我们保持简单,从头开始。我们正在为Linux开发一个程序。让我们来看看所谓的系统调用,即Linux为我们提供的请求内核功能的接口。

Linux有以下的系统调用和进程的处理相关:

- fork(void) (man 2 fork) 

创建一个调用进程的完整副本。由于需要复制进程的地址空间,听起来很不方便,但它使用了写时复制(copy-on-write)的优化。这是在Linux中创建进程的唯一方法,一般观念上是这样的。然而,在新版本的内核中,fork( )是基于clone( )系统调用来实现的,现在可以直接使用clone( )来创建进程,但为了简单起见,我们将跳过这些细节。

- execve(path, args, env) (man 2 execve) 

通过执行指定路径下的文件,将调用进程转变为一个新的进程。实际上,它用一个新的进程映像替换了当前的进程映像,并不创建任何新的进程。

- pipe(fildes[2] __OUT) (man 2 pipe) 

创建一个管道,这是一个进程间的通信原语。通常,管道是单向的数据流。数组的第一个元素连接到管道的读取端,而第二个元素连接到写入端。写入fildes[1]的数据可以从fildes[0]中读取。

我们不打算看一下上述的系统调用源代码,因为它是内核的一部分,可能很难理解。

我们考虑的另一个重要部分是Linux shell,一个命令解释器工具,本身也是一个普通程序。shell进程不断地从stdin中读取信息。用户通常通过输入一些命令并按下回车键与shell进行交互。然后shell进程执行所提供的命令,使用上面提到的方式来创建新的进程,这些进程的标准输出被连接到shell进程的stdout。

C standard library / C标准库

当然,我们用C语言开发程序是为了尽可能地接近操作系统级的实现方式。C语言有一个所谓的标准库libc,一个丰富的函数集,协助程序编写。同时,它提供了对系统调用的封装。

在基于Debian系列的发行版上,C标准库用的是glibc。运行命令下载glibc的源码:apt-get download glibc-source

C语言标准库包含以下函数:

- system(command) (man 3 system) 

启动一个shell进程来执行提供的命令。调用进程被阻塞,直到被启动的shell进程执行结束。system()返回shell进程的退出代码(exit code)。让我们看一下这个函数在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) 

调用fork创建一个进程,用执行所提供命令的shell进程替换生成的进程。看起来和system( )很像?这里的区别在于可以通过子进程的stdin或stdout与之通信。但通常是单向的。为了与该进程通信,使用了一个管道。实现层面的摘要如下:

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;

}

上面的代码,是标准库里使用Linux系统调用来操作进程,看过之后,对Linux中进程的创建就会更清楚一些了。

而shell的实现方式,启动子进程的方式,也是类似的: fork( ) + execve( )

为什么要启用Linux进程?

1,并行执行代码

最简单的原因。我们只需要fork()。调用fork()实际上是复制了当前程序进程。但是由于这个进程使用完全独立的地址空间,要想与之通信,我们无论如何都需要进程间的通信原语。Fork出来的进程,其程序代码和指令与父进程相同,可以把它看作是当前运行程序的另一个的实例。

2,从你的代码中运行另外一个程序

如果你只需要运行一个程序,而不需要与它的stdin/stdout通信,libc里的system()函数是最简单的解决方案。是的,你也可以fork()你的进程,然后在子进程中运行exec()。但由于这是一个相当常用的功能,所以提供了一个system()函数,方便使用。

3,运行一个进程并读取其stdout(或写入其stdin)。

我们需要使用libc中的popen( )函数。是的,你仍然可以通过组合pipe()+fork()+exec()来实现上述目标,但popen()是为了减少重复代码。

运行一个进程,向其stdin写入并从其stdout读出

这是一种有趣的情况。由于某些原因,默认的popen()实现通常是单向的。但看起来我们可以很容易地想出双向的解决方案:我们需要两条管道,第一条连接到子进程的stdin,第二条连接到子进程的stdout。剩下的部分是fork()一个子进程,通过dup2()将管道连接到IO描述符,然后execve()命令。其中一个潜在的实现可以在GitHub popen2()项目中找到,见参考。在开发这种功能时,你应该注意一个额外的问题,那就是之前通过popen()进程打开的管道的文件描述符的泄露。如果我们忘记在每个fork出的子进程中明确关闭外来文件描述符,就有可能对兄弟姐妹的stdins和stdouts进行IO操作。要避免这种问题。

关于Windows平台的一些说明

Windows操作系统家族处理进程方面的范式和Linux有所不同。如果我们跳过Windows 10上引入的新的Unix兼容层,并试图让Windows支持POSIX API,我们能用的只有两个古老的底层WinAPI函数:

- CreateProcess(filename) 

为一个指定的可执行文件启动一个全新的进程。

- ShellExecute(Ex)(command) 

启动一个shell(是的,Windows也有一个shell概念)进程来执行所提供的命令。

所以,没有fork和execve等函数。然而,为了与已经启动的进程进行通信,管道是可以使用的。

参考:

How to on starting processes (mostly in Linux)

https://github.com/iximiuz/popen2