二十二、Linux 进程与信号---进程创建
22.1 fork 和 vfork 函数
22.1.1 函数说明
1 #include <unistd.h> 2 #include <sys/types.h> 3 pid_t fork( void);
- 函数说明:
- 一个现有进程可以调用fork函数创建一个新进程。
- 由fork创建的新进程被称为子进程(child process)。
- fork函数被调用一次但返回两次。
- 两次返回的唯一区别是子进程中返回 0 值而父进程中返回子进程 ID。
- 子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。
- 注意,子进程持有的是上述存储空间的“副本”,这意味着父子进程间不共享这些存储空间。
- UNIX将复制父进程的地址空间内容给子进程,因此,子进程有了独立的地址空间。
- 在不同的UNIX (Like)系统下,我们无法确定fork之后是子进程先运行还是父进程先运行,这依赖于系统的实现。
- 所以在移植代码的时候我们不应该对此作出任何的假设。
- 函数返回值:
- 子进程中为 0,父进程中为子进程 ID,出错为 -1
- 子进程的继承属性
- 用户信息和权限、目录信息、信号信息、环境、共享存储段、资源限制、堆、栈和数据段,共享代码段。
- 子进程特有属性
- 进程 ID、锁信息、运行时间、未决信号
- 操作文件时的内核结构变化
- 子进程只继承父进程的文件描述表,不继承但共享文件表项和 i-node。
- 父进程创建一个子进程后,文件表项中的引用计数器加1变成2,当父进程作 close 操作后,计数器减 1,子进程还是可以使用文件表项,只有当计数器为 0 时,才会释放文件表项。
1 #include <unistd.h> 2 pid_t vfork(void);
- 函数说明
- vfork()会产生一个新的子进程,其子进程会复制父进程的数据与堆栈空间,并继承父进程的用户代码,组代码,环境变量、已打开的文件代码、工作目录和资源限制等。
- Linux 使用 copy-on-write(COW)技术,只有当其中一进程试图修改欲复制的空间时才会做真正的复制动作,由于这些继承的信息是复制而来,并非指相同的内存空间,因此子进程对这些变量的修改和父进程并不会同步。
- 此外,子进程不会继承父进程的文件锁定和未处理的信号。
- 注意,Linux不保证子进程会比父进程先执行或晚执行,因此编写程序时要留意死锁或竞争条件的发生。
- 返回值
- 如果 vfork()成功则在父进程中会返回新建立的子进程代码(PID),而在新建立的子进程中则返回 0。
- 如果 vfork 失败则直接返回-1,失败原因存于errno中。
- 错误代码
- EAGAIN 内存不足。
- ENOMEM 内存不足,无法配置核心所需的数据结构空间。
两个函数的附加说明:
- fork 创建的新进程被称为子进程,该函数被调用一次,但返回两次。两次返回的区别是:在子进程中的返回值是 0,而在父进程中的返回值则是新子进程的进程 ID。
- 创建子进程,父子进程哪个先运行根据系统调度且子进程会复制父进程的内存空间
- vfork 创建的子进程会先运行,但不会复制父进程的内存空间
22.1.2 例子
22.1.2.1 查看父子进程的运行
process_fork.c
1 #include <unistd.h> 2 #include <string.h> 3 #include <fcntl.h> 4 #include <stdio.h> 5 #include <stdlib.h> 6 7 int main(void) 8 { 9 printf("pid: %d", getpid()); 10 11 pid_t pid; 12 pid = fork();//创建子进程 13 //在 fork 后,会运行两个进程(父进程和子进程) 14 if(pid < 0) { 15 perror("fork error"); 16 } else if(pid > 0) { 17 //父进程(在父进程中返回的是子进程的 pid) 18 //父进程执行的代码 19 printf("I am parent process pid is %d, ppid is %d, fork return is %d\n", 20 getpid(), getppid(), pid); 21 } else { 22 //子进程(在子进程中 fork 返回的是0) 23 //子进程执行的代码 24 printf("I am child process pid is %d, ppid is %d, fork return is %d\n", 25 getpid(), getppid(), pid); 26 } 27 28 //这里的代码是父子进程都要执行的代码 29 printf("pid: %d\n", getpid()); 30 sleep(1); 31 32 return 0; 33 }
可以看见 父子进程的 pid 是不同的,父进程中通过fork 获得是子进程的ID号,子进程的父进程是102839,fork 后返回的是0
子进程的ID 和 父进程的ID 在公共代码区都打印了出来。
22.1.2.2 两进程循环启动后,各自输出内容
1 /* 父子进程交替运行 */ 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <unistd.h> 5 6 int main(void) 7 { 8 printf("current pid : %d\n", getpid()); 9 10 pid_t pid = fork(); 11 if(pid < 0) { 12 perror("fork error"); 13 } else if(pid > 0) { 14 //父进程执行 15 int i; 16 for(i = 0; i < 10; i++) { 17 printf("This is parent process pid is : %d\n", getpid()); 18 sleep(1); 19 } 20 } else { 21 //子进程 22 int i; 23 for(i = 0; i < 10; i++) { 24 printf("This is child process pid is : %d\n", getpid()); 25 sleep(1); 26 } 27 } 28 29 return 0; 30 }
编译运行:
虽然设置了交替运行,但是不是真的交替运行,这个与系统调度来决定的。
22.1.2.3 子进程内存的复制
案例:
1 #include <unistd.h> 2 #include <string.h> 3 #include <fcntl.h> 4 #include <stdio.h> 5 #include <stdlib.h> 6 7 int g_val = 30;//全局变量,存放在数据段 8 9 int main(void) 10 { 11 int a_val = 30;//局部变量,调用的时候存放在栈中 12 static int s_val = 30;//静态变量,存放在数据段 13 printf("pid: %d", getpid()); 14 15 pid_t pid; 16 pid = fork();//创建子进程 17 //在 fork 后,会运行两个进程(父进程和子进程) 18 if(pid < 0) { 19 perror("fork error"); 20 } else if(pid > 0) { 21 //父进程(在父进程中返回的是子进程的 pid) 22 //父进程执行的代码 23 g_val = 40; 24 a_val = 40; 25 s_val = 40; 26 27 printf("I am parent process pid is %d, ppid is %d, fork return is %d\n", 28 getpid(), getppid(), pid); 29 printf("g_val: %p, a_val: %p, s_val: %p\n", &g_val, &a_val, &s_val); 30 } else { 31 //子进程(在子进程中 fork 返回的是0) 32 //子进程执行的代码 33 g_val = 50; 34 a_val = 50; 35 s_val = 50; 36 printf("I am child process pid is %d, ppid is %d, fork return is %d\n", 37 getpid(), getppid(), pid); 38 printf("g_val: %p, a_val: %p, s_val: %p\n", &g_val, &a_val, &s_val); 39 } 40 41 //这里的代码是父子进程都要执行的代码 42 printf("pid: %d, g_val: %d, a_val: %d, s_val: %d\n", getpid(), g_val, a_val, s_val); 43 sleep(1); 44 45 return 0; 46 }
可以看见变量所在的虚拟地址都是一样的,但是输出的值却不一样。
值不一样是因为,物理空间不一样,子进程和父进程指向的物理空间不一样。数据段、堆和栈都由各自的物理空间。虚拟内存存放的数据最终都会存放在物理地址上。最终修改的也是物理内存中的内容。程序中最终输出的也是物理地址的内容。