进程作为操作系统中的基本管理单位,起着重要作用,那么进程到底是怎么被创建的,创建进程时到底都发生了些什么,本文将会讨论这些问题。
进程是如何被创建的
从现象上看,当我们执行一个main方法时,会创建一个进程,当我们执行一条linux命令时,也会创建一个进程。要说进程究竟是如何被创建的,这里给出两个回答:
- 进程是被它的父进程创建的
- 进程是被fork出来的
这两个回答都对,只是从不同层面回答了这个问题。
进程是被它的父进程创建的
如果去一台linux机器上ps -eaf
一下,你会看到每个进程都会对应一个父进程,下图中第二列为当前进程的pid,第三列为父进程的pid。并且所有的进程都会有一个祖先进程,即pid为1的进程。

如果将这些进程的父子关系组织起来,那它们应该是一个树状的结构,可以看到,所有进程都有一个公共的根进程,即pid为1的systemd:

那么问题来了,这个pid为1的进程到底是何方神圣?
pid为1的进程是什么?
Pid为1的进程被称为init进程,它是系统启动时最早启动的用户级进程,是所有其他用户级进程的祖先。init进程负责执行系统引导过程中所需的初始化任务,然后它会启动其他系统服务和用户级别的进程。实际上,init进程有很多不同的实现,如SysVinit
, launched
, upstart
,systemd
等,现行的linux操作系统重,基本systemd
已成为主流。

进程是被fork出来的
如前文所述,进程是被它的父进程创建的,而创建的具体方式,正是fork. 理论上来讲,fork的时候子进程会得到父进程地址空间的一个副本,包括代码段、数据段、堆栈等,但是由于通常子进程和父进程执行并不是同样的内容,而是会通过exec系统调用装填一段新的代码进去,因此出于效率考虑,linux使用了一种叫做写时复制(Copy-On-Write)的概念,即当fork发生时,子进程并不会完全拷贝一份父进程地址空间的内容,而是把父进程的地址空间内容改为只读状态,此时如果父进程或者子进程尝试修改这些区域,内核会为要被修改的区域所在的页制作一个副本。
COW机制
我们在linux上使用c语言创建一个进程,并fork出一个子进程,来解释这个问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/stat.h> #include <fcntl.h> #include <stdint.h>
int main() { int value = 42;
pid_t pid = fork();
if (pid == 0) { printf("Child process. PID: %d\n", getpid()); printf("Child: Initial value = %d\n", value);
value = 99; printf("Child: Modified value = %d\n", value);
} else if (pid > 0) { sleep(5); printf("Parent process. PID: %d\n", getpid()); printf("Parent: Initial value = %d\n", value);
wait(NULL);
printf("Parent: After child's modification, value = %d\n", value); } else { fprintf(stderr, "Fork failed\n"); return 1; }
return 0; }
|
在上面代码中,我们在父进程中初始化了一个int类型的value变量,值为42,并在子进程中修改value, 最后在父进程中观察value的值。理论上来讲,由于COW机制,当子进程修改value时,操作系统会将父进程中value所在的页复制出来,并分配给子进程,因此,子进程对value的修改并不会影响到父进程中的value.
输出如下:
1 2 3 4 5 6
| Child process. PID: 2059 Child: Initial value = 42 Child: Modified value = 99 Parent process. PID: 2058 Parent: Initial value = 42 Parent: After child's modification, value = 42
|
可以看到,父进程中的value依然是42.
需要注意的是,COW对于共享型的内存映射(shared mmap)是不生效的(毕竟听名字就知道,它是进程共享的),举个🌰:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/mman.h> #include <sys/wait.h>
int main() { size_t size = sizeof(int); int *shared_data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (shared_data == MAP_FAILED) { perror("mmap"); exit(EXIT_FAILURE); }
*shared_data = 42;
pid_t pid = fork();
if (pid == 0) { printf("Child process. PID: %d\n", getpid());
*shared_data = 99;
printf("Child: Modified value = %d\n", *shared_data);
munmap(shared_data, size);
} else if (pid > 0) { printf("Parent process. PID: %d\n", getpid());
printf("Parent: Initial value = %d\n", *shared_data);
wait(NULL);
printf("Parent: After child's modification, value = %d\n", *shared_data);
munmap(shared_data, size);
} else { perror("fork"); exit(EXIT_FAILURE); }
return 0; }
|
对于上面代码,我们创建了一个匿名的共享的mmap,它的初始值为42,并在子进程中修改该内存中的值为99,那么该值的变化最后将会反映到父进程中,输出如下:å
1 2 3 4 5
| Parent process. PID: 2364 Child process. PID: 2365 Child: Modified value = 99 Parent: Initial value = 99 Parent: After child's modification, value = 99
|
如果我们把第十行的SHARED改为PRIVATE:
1
| int *shared_data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
那么子进程对共享内存的修改将不会影响到父进程中的该值(当然了,多进程之间使用匿名mmap的用之一就是用于通信,如果设为PRIVATE就失去了其意义,这里这么做只是为了说明COW的效果。)
参考
https://coolshell.cn/articles/17998.html