Q1:以下代码会输出几个’-’?

首先看个面试题:以下代码会输出几个’-’?

#include <iostream>
#include <unistd.h>

int main()
{
    
    for (int i = 0; i < 2; i++)
    {
        fork();
        printf("-\n");
    }

    return 0;
}

这里的答案是6,原因如下:

  • i=0时,存在2个进程,输出2个‘-’
  • i=1时,存在4个进程,输出4个‘-’
  • 所以一共输出6个‘-’

记住一句话:

  • fork()函数会创建一个新的进程,并从内核中为此进程分配一个新的可用的PID,之后为这个子进程分配进程空间,并将父进程的进程空间中的内容(包括父进程的数据段、堆栈段)复制到子进程空间中,并且和父进程共享代码段,此时父子进程都从fork的下一句开始并发执行。
    • 一个问题:为什么fork执行成功后,返回两次?
      • fork产生的子进程完全复制父进程的堆栈数据,所以,子进程的函数调用栈上也有fork函数,子进程的返回和父进程的返回是相互独立的,是因为函数调用栈的返回。

为了探究这个问题,可通过如下代码进行分析:

#include <iostream>
#include <unistd.h>

int main()
{
    
    for (int i = 0; i < 2; i++)
    {
        fork();
        printf("i=%d, ppid:%d, pid:%d\n", i, getppid(), getpid());
        wait(NULL);
    }

    return 0;
}

执行结果为:
当fork遇上for循环的问题分析 & fork函数_#include

运行该代码,主进程PID是8204(7764进程是shell进程),8204因为fork,产生8205进程,由于父子进程都从fork的下一句开始并发执行,所以此时8204、8205进程都从printf处开始执行(8204也不会陷入无限循环中)。具体流程为:

  • 在i = 0时,主进程8204产生8205
  • i = 1时,8204产生8207进程,8207执行其后的printf代码,由于8207中i=1,所以直接退出,不继续调用fork
  • 8205进程在i=1时fork出8206进程

所以,按照执行情况,

  • 在i=0时,8204、8205各输出一个‘-’
  • 在i=1时,8204、8205、8206、8207各输出一个‘-’

当fork遇上for循环的问题分析 & fork函数_子进程_02

Q2:以下代码会输出几个’-’?

#include <iostream>
#include <unistd.h>

int main()
{
    
    for (int i = 0; i < 2; i++)
    {
        fork();
        printf("-");
    }

    return 0;
}

因为缓冲区中存在数据的原因,在fork时会直接复制缓冲区中的数据到子进程,所以,这里输出8个‘-’。具体为:

  • i=0时,8204、8205进程缓冲区各一个‘-’
  • i=1时,8204、8205进程先执行fork,各产生8206、8207进程,8206、8207进程复制了父进程的缓冲区,所以在fork之后,printf之前,8204~8207这4个进程缓冲区中各有一个‘-’,这4个进程再分别执行printf,此时缓冲区共8个‘-’。
  • 综上,该代码由于没有刷新缓冲区,共输出8个‘-’。

fork函数

  • fork()函数会创建一个新的进程,并从内核中为此进程分配一个新的可用的PID,之后为这个子进程分配进程空间,并将父进程的进程空间中的内容(包括父进程的数据段、堆栈段)复制到子进程空间中,并且和父进程共享代码段,此时父子进程都从fork的下一句开始并发执行。
    • 一个问题:为什么fork执行成功后,返回两次?
      • fork产生的子进程完全复制父进程的堆栈数据,所以,子进程的函数调用栈上也有fork函数,子进程的返回和父进程的返回是相互独立的,是因为函数调用栈的返回。
  • 在循环中使用fork产生子进程,将会发生 1–>2–>4–>8…这样的裂变反应。
  • 现在的Linux内核中fork函数往往在创建子进程并不复制父进程的数据段和堆栈段,而是当子进程修改这些数据时,复制操作才会发生。–> **写时复制(COW)**机制,是现代操作系统中一个重要的概念。

  • 下面的测试代码中,可以看出,写后地址也并没改变,这时因为 虚地址的原因。可以参见: 【fork() system call and memory space of the process】中获得一些答案。
  • 自己对该问题的理解:现代操作系统加入了 “写时复制”机制(COW),只有子进程修改数据时,复制操作才会发生,但复制操作发生后,由于virtual address的原因,所以看上去变量的地址空间没有发生变化。具体OS层面是什么时候完成的复制操作,对用户是无感的,也体验不到。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int g_value = 1;

int main()
{
    int *heap = new int(3);
    static int s_value = 1;
    int value = 1;
    printf("g_value: %d, s_value: %d, heap: %d, value: %d\n", g_value, s_value, *heap, value);
    printf("addr::g_value: %X, s_value: %X, heap: %X, value: %X\n", &g_value, &s_value, heap, &value);

    int pid = fork();
    if (pid < 0)
    {
        perror("fail to fork");
        exit(-1);
    }
    else if (pid == 0)
    {
        //child
        printf("\tchild::ppid: %u, pid: %u\n", getppid(), getpid());
        printf("\tchild::before g_value: %d, s_value: %d, heap: %d, value: %d\n", g_value, s_value, *heap, value);
        printf("\tchild::addr::g_value: %p, s_value: %p, heap: %p, value: %p\n", &g_value, &s_value, heap, &value);

        g_value++;
        s_value++;
        (*heap)++;
        value++;

        printf("\tchild::new g_value: %d, s_value: %d, heap: %d, value: %d\n", g_value, s_value, *heap, value);
        printf("\tchild::addr::g_value: %X, s_value: %X, heap: %X, value: %X\n", &g_value, &s_value, heap, &value);
        printf("\tchild::addr::g_value: %p, s_value: %p, heap: %p, value: %p\n", &g_value, &s_value, heap, &value);
        sleep(5);

        exit(3); //这里如果不退出,子进程将会继续执行end if后面的代码,最后一行的print函数将会执行
    }
    else
    {
        printf("ppid: %u, curr pid: %u, pid: %u\n", getppid(), getpid(), pid);
        printf("before g_value: %d, s_value: %d, heap: %d, value: %d\n", g_value, s_value, *heap, value);
        // sleep(1);

        g_value++;
        printf("new g_value: %d, s_value: %d, heap: %d, value: %d\n", g_value, s_value, *heap, value);
    }//end if
        int status = 0;
        pid_t pr = wait(&status); //等待子进程退出后进行回收,如果子进程还未退出,则阻塞在这里继续等,直到有一个出现为止
        printf("ppid: %u, pid: %u, child pid: %u, status: %d, WIFEXITED(status):%d, WEXITSTATUS(status): %d\n", 
        		getppid(), getpid(), pr, status, WIFEXITED(status), WEXITSTATUS(status));
    printf("end~~~~pid:%u addr::g_value: %p, s_value: %p, heap: %p, value: %p\n", pid, &g_value, &s_value, heap, &value);
}