我可以派生进程和执行内部功能吗?

时间:2018-08-27 04:24:37

标签: c fork exec

我已经进行了真正的搜索,发现可以执行execvp()shell命令。

我想知道我是否可以派生进程,然后让它们运行程序内部的功能? (例如,我已经在代码中编写了一个函数)

4 个答案:

答案 0 :(得分:4)

当然,您可以让子级在同一可执行文件中执行一个功能,而父级在同一可执行文件中执行其他(甚至相同)功能。

pid_t pid = fork();
if (pid < 0)
    err_syserr("failed to fork: ");
else if (pid == 0)
    be_childish();
else
    be_parental();

您可以根据需要向be_childish()be_parental()添加参数。在代码执行fork()之前,您可以创建管道或套接字以在它们之间进行通信-或信号量,共享内存或所需的任何IPC。

答案 1 :(得分:1)

fork一个process(正在运行的 ,程序的实例),您execve一个executable(一个被动文件,通常采用ELF格式)-某些功能。再次读取fork(2)execve(2)credentials(7)。另请参见Operating Systems: Three Easy Pieces,以更好地了解操作系统的作用。

  

可以执行execvp()shell命令。

错误。execvp调用execve并运行可执行文件(不是shell命令;例如,您不能execvp内置cd shell)。它不使用外壳程序(但是execvp像外壳程序一样搜索PATH variable)。

请注意,每个进程都有自己的virtual address spacefork用自己的新虚拟地址空间(恰好是父级虚拟地址空间的副本)创建一个 new 进程。该副本很懒惰,请阅读{{3 }}技术)。 execve正在用新地址(在可执行文件中描述)来替​​换虚拟地址过程。

由于新的虚拟地址空间(在子进程中成功执行fork之后)是父级副本的副本,因此它还包含您梦dream以求的程序中每个内部函数的代码。因此,在fork之后,您可以调用这些内部函数。

使用copy-on-writepmap(1)了解进程的虚拟地址空间。首先,在终端中运行cat /proc/$$/maps,然后运行pmap $$,并尝试了解其输出(它描述了Shell进程的虚拟地址空间)。

进程运行时,可以使用proc(5)扩展其虚拟地址空间。 mallocmmap(2)(这使您可以将dlopen(3)加载到您的进程中)都使用它。

PS。我猜您正在使用Linux。

答案 2 :(得分:1)

调用fork时,将继承父进程的当前上下文来创建一个新进程。子进程和父进程可以独立执行,以调用程序中的任何函数。但是,如果他们需要彼此通信/同步,则需要使用IPC机制之一,例如共享内存,管道,信号灯等。

答案 3 :(得分:1)

我怀疑这里最简单的答案是一个例子:

#define _POSIX_C_SOURCE  200809L
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>

static int child_foo(void)
{
    /* This child process just counts to three. */

    int i;

    for (i = 1; i <= 3; i++) {
        printf("Foo: %d\n", i);
        fflush(stdout);
        sleep(1);
    }

    printf("Foo is done.\n");
    return EXIT_SUCCESS;
}

static int child_bar(unsigned long  n)
{
    /* This child process checks if n is prime or not. */ 
    const unsigned long  imax = (n + 1) / 2;
    unsigned long        i;

    for (i = 2; i <= imax; i++)
        if (!(n % i)) {
            printf("Bar: Because %lu is divisible by %lu, %lu is not a prime.\n", n, i, n);
            return EXIT_FAILURE;
        }

    printf("Bar: %lu is prime.\n", n);
    return EXIT_SUCCESS;
}

int main(void)
{
    pid_t  p, foo, bar;
    int    status;

    printf("Forking child processes.\n");
    fflush(stdout);

    foo = fork();
    if (foo == -1) {
        fprintf(stderr, "Cannot fork: %s.\n", strerror(errno));
        return EXIT_FAILURE;
    } else
    if (!foo)
        return child_foo();

    bar = fork();
    if (bar == -1) {
        fprintf(stderr, "Cannot fork: %s.\n", strerror(errno));
        /* Wait until all child processes (here, foo only) have completed. */
        do {
            p = wait(NULL);
        } while (p != -1 || errno == EINTR);
        return EXIT_FAILURE;
    } else
    if (!bar)
        return child_bar(227869319);

    /* Wait until all child processes have completed. */
    do {
        p = wait(&status);
        if (p == foo || p == bar) {
            /* Report exit status. */
            if (p == foo)
                printf("child_foo()");
            else
                printf("child_bar()");
            if (WIFEXITED(status)) {
                if (WEXITSTATUS(status) == EXIT_SUCCESS)
                    printf(" exited successfully (EXIT_SUCCESS).\n");
                else
                if (WEXITSTATUS(status) == EXIT_FAILURE)
                    printf(" exited with failure (EXIT_FAILURE).\n");
                else
                    printf(" exited with status %d.\n", WEXITSTATUS(status));
            } else
            if (WIFSIGNALED(status))
                printf(" died from signal %d (%s).\n", WTERMSIG(status), strsignal(WTERMSIG(status)));
            else
                printf(" was lost.\n");
            fflush(stdout);
        }
    } while (p != -1 || errno == EINTR);

    printf("All done.\n");
    return EXIT_SUCCESS;
}

以上,main()分叉两个子进程。其中一个运行child_foo()并退出,另一个运行child_bar(227869319)并退出。父进程会收获所有子进程,并返回退出原因和退出状态(如果可用),然后也退出自身。

fflush(stdout)之前的fork()是为了提醒内部缓存(包括C库完成的缓存)在派生之前应先清除,否则子进程将继承缓存内容。对于stdout,这意味着输出重复。

在实践中,使用exit(EXIT_SUCCESS)exit(EXIT_FAILURE)从子流程函数中“返回”通常比return更具可维护性(人类程序员的理解)。然后,在main()中,您不必说return child_foo(),而是说

        child_foo(); /* Never returns. */
        exit(EXIT_FAILURE);

exit(EXIT_FAILURE)只是一个错误捕获器,以防万一child_foo()的修改导致它返回而不是退出。

在某些情况下,如果必须始终在子进程退出之前执行清理工作,则返回模式很有用。在这种情况下,通常将清理工作放在单独的函数中,然后将return child_foo()替换为

        int  exitstatus;

        exitstatus = child_foo();
        cleanup_function();
        exit(exitstatus);

请注意,在main()中,return exitstatus;exit(exitstatus);完全等效(C89 2.1.2.2,C99 5.1.2.2.3p1,C11 5.1.2.2.3p1)。对我们人类而言,唯一的区别是:与exit(exitstatus)相比,我们人类更容易正确地解释main()中间return exitstatus;后面的意图。我自己,我同样地“解析”了它们,但是我似乎更多地使用了return

strsignal()函数在POSIX-1.2008中定义,仅当您设法通过外部信号杀死其中一个子进程而又不杀死其父进程时,才调用该函数。如果您的C库不支持它,只需删除它(也删除%s printf说明符)即可。

与往常一样,第二个最重要的事情是确保未来的开发人员正确理解其意图。我只包含了很少的注释,但是子进程函数中的函数名称和初始注释都应使意图清楚。我相信您可以做得更好。 (我确实认为注释是我编程中最难的部分。编写无用的注释来描述代码的作用很容易,但忽略了 intent 。即使您自己查看代码,也是如此。几个月后,您忘记了自己的想法,必须依靠注释(并从代码间接推断)来正确理解其意图。好的注释很难编写!)

在我看来,最重要的是确保程序健壮,并且不会无提示地破坏数据。这意味着要进行足够的错误检查(在我的情况下,是偏执狂-我真的不想静默破坏数据),并且对“应该”成功的假设最少。有时,它会导致代码“嵌套”,例如statuswait()循环中的main()检查。我故意在此处省略注释,因为我相信您应该仔细阅读,保持浏览器或终端窗口对man 2 wait保持打开状态,并添加必要的注释,以便您确切地了解代码嵌套的作用。绝对可以帮助您了解有关进程如何终止或终止以及父进程如何检测到该终止的很多知识。