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函数,子进程的返回和父进程的返回是相互独立的,是因为函数调用栈的返回。
- 一个问题:为什么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;
}
执行结果为:
运行该代码,主进程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各输出一个‘-’
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执行成功后,返回两次?
- 在循环中使用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);
}