使用fork

62 阅读2分钟

1. 进程

Linux下一个进程在内存里有三部份的数据,就是“数据段”,“堆栈段”和“代码段”,其实学过汇编语言的人一定知道,一般的CPU象I386,都有上述三种段寄存器,以方便操作系统的运行。“代码段”,顾名思义,就是存放了程序代码的数据,假如机器中有数个进程运行相同的一个程序,那么它们就可以使用同一个代码段。 堆栈段存放的就是子程序的返回地址、子程序的参数以及程序的局部变量。而数据段则存放程序的全局变量,常数以及动态数据分配的数据空间(比如用malloc之类的函数取得的空间)。这其中有许多细节问题,这里限于篇幅就不多介绍了。系统如果同时运行数个相同的程序,它们之间就不能使用同一个堆栈段和数据段。

1.1 一个fork例子

/*************************************************************************
  > File Name:    test_fork.c
  > Author:       basilguo@163.com
  > Created Time: Thu 03 Mar 2022 12:34:19 AM PST
  > Description:
 ************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
    pid_t fpid;
    int count = 0;
    fpid = fork();

    while (1)
    {
        if (fpid < 0)
            fprintf(stderr, "Error in fork!");
        else if (fpid == 0)
        {
            printf("I am the child process, fpid is %d, my process id is %d."
                    "my parent's pid is %d.\n",
                    fpid, getpid(), getppid());
            count ++;
        } else
        {
            printf("I am the parent process, fpid is %d, my process id is %d,"
                    "my parent's pid is %d.\n",
                    fpid, getpid(), getppid());
            count ++;
        }

        sleep(3);

        printf("count is %d.\n", count);

    }

    return 0;
}
$ ./test_fork
I am the parent process, fpid is 49367, my process id is 49366,my parent's pid is 47463.
I am the child process, fpid is 0, my process id is 49367.my parent's pid is 49366.
count is 1.
I am the child process, fpid is 0, my process id is 49367.my parent's pid is 49366.
count is 1.
I am the parent process, fpid is 49367, my process id is 49366,my parent's pid is 47463.
count is 2.
I am the child process, fpid is 0, my process id is 49367.my parent's pid is 49366.
count is 2.
I am the parent process, fpid is 49367, my process id is 49366,my parent's pid is 47463.
count is 3.
I am the parent process, fpid is 49367, my process id is 49366,my parent's pid is 47463.
count is 3.
I am the child process, fpid is 0, my process id is 49367.my parent's pid is 49366.
count is 4.
I am the parent process, fpid is 49367, my process id is 49366,my parent's pid is 47463.
count is 4.
I am the child process, fpid is 0, my process id is 49367.my parent's pid is 49366.
count is 5.
I am the parent process, fpid is 49367, my process id is 49366,my parent's pid is 47463.
count is 5.
I am the child process, fpid is 0, my process id is 49367.my parent's pid is 49366.
count is 6.
I am the parent process, fpid is 49367, my process id is 49366,my parent's pid is 47463.
count is 6.
...

1.2 解释

这个程序并不直观,让我们直接看进程吧。

$ ps -aux | grep test_fork
savax     49366  0.0  0.0   4516   800 pts/7    S+   00:54   0:00 ./test_fork
savax     49367  0.0  0.0   4516    68 pts/7    S+   00:54   0:00 ./test_fork

$ ps -aux | grep 47463
savax     47463  0.0  0.2  23328  5848 pts/7    Ss   Mar02   0:00 -bash

可以看到确实是创建了两个进程,然后两个进程交替执行,但是并不是严格同步的,可以看到并不是严格的交替。哪个进程先执行要看系统的进程调度策略。其中的count变量不是两个进程共用的,而是各用各的。

  • 在父进程中,fork返回新创建子进程的进程ID;
  • 在子进程中,fork返回0;
  • 如果出现错误,fork返回一个负值。fork出错可能有两种原因:
    1. 当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN
    2. 系统内存不足,这时errno的值被设置为ENOMEM

getpid()可以获取当前进程号,而getppid()可以获取父进程号,所谓的47463编号其实是一个bash进程,这个bash进程就是我在执行test_fork这个程序的进程。

使用fork出来的子进程,继承了父进程的地址空间,打开的文件描述符等。如果想要fork 创建的子进程不继承父进程打开的文件描述符,可以在打开文件的时候,设置 FD_CLOSEXEC标志位,如果文件描述符中这个标志位置位,那么调用 exec 时会自动关闭对应的文件。

2. 一个进程如何来启动另一个程序的执行

在Linux中要使用exec类的函数,exec类的函数不止一个,但大致相同,在Linux中,它们分别是:execlexeclpexecleexecvexecveexecvp,下面我只以execlp为例,其它函数究竟与execlp有何区别,请通过man exec命令来了解它们的具体情况。

一个进程一旦调用exec类函数,它本身就“死亡”了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。(不过exec类函数中有的还允许继承环境变量之类的信息。)

那么如果我的程序想启动另一程序的执行但自己仍想继续运行的话,怎么办呢?那就是结合forkexec的使用。下面一段代码显示如何启动运行其它程序:

/*************************************************************************
  > File Name:    test_exec_fork.c
  > Author:       basilguo@163.com
  > Created Time: Thu 03 Mar 2022 01:07:24 AM PST
  > Description:
 ************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

int main(void)
{
    char command[256];
    int ret;

    while (1)
    {
        printf(">>>");
        fgets(command, 256, stdin);
        command[strlen(command)-1] = '\0';

        if (fork() == 0)
        {
            execlp(command, command); // run by sub-process
            perror(command);
            exit(errno);
        }
        else
        {
            // wait for sub-process
            wait(&ret);
            printf("child process returns %d.\n", ret);
        }

    }
    return 0;
}

此程序从终端读入命令并执行之,执行完成后,父进程继续等待从终端读入命令。

另外,有一个更简单的执行其它程序的函数system,它是一个较高层的函数,实际上相当于在SHELL环境下执行一条命令,而exec类函数则是低层的系统调用。

参考文献

  1. linux下多进程、多线程编程 - dzqabc - 博客园 (cnblogs.com)

  2. linux中fork() 函数详解_牛客博客 (nowcoder.net)

  3. 记录一次腾讯c/c++ linux后台开发岗面试经历(面试题含答案)_c++_linux大本营_InfoQ写作平台