说明:
一、fork ()函数
(1)函数介绍:【采用写时复制的方法】。
1️⃣函数功能:创建一个与原进程(父进程)几乎完全相同的新进程(子进程)
子进程将获得父进程数据空间、堆、栈等资源的副本(写时复制)。
父子进程并不共享这些存储空间部分,但共享正文文段。
项目 | 说明 |
---|---|
函数原型 | pid_t fork(void); |
头文件 | sys/types.h、unistd.h |
参数说明 | 无 |
返回值 | 调用一次,却能够返回两次,它可能有三种不同的返回值 ①在父进程中,返回新创建子进程的PID; ②在子进程中,返回0; ③若出现错误,fork返回一个负值;同时设置errno的错误返回码 |
注意 | ①父子进程间不共享存储空间,有独立的地址空间。 |
2️⃣特点:
①新分配的pid,通常为父进程的pid+1.
②子进程的ppid,为父进程pid
③子进程的资源统计信息会清零。
④任何挂起信号会清除,不会被子进程继承
⑤任何文件锁都不会被子进程继承
⑥子进程存在,父进程消失,则会将子进程挂在systemd进程ID下。
3️⃣错误返回码:
项目 | 说明 |
EAGAIN | ①内核申请一些资源时失败了,如:新的pid ②达到了RIIMIT_NPROC设置的资源限制。 |
ENOMEM | 没有足够的内核内存来满足所请求的操作 |
(2)示例实践:
1️⃣源文件:fork.c
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
int main (int argc,char *argv[])
{
pid_t pid;
printf("Before the fork!\n"); //fork之前输出(仅父进程输出)
pid = fork(); //一分为二:父进程,子进程
printf("After the fork!\n"); //fork之后输出(两进程均会输出)
if (pid < 0) //创建出错返回-1,<0
{
printf("fork error!");
}
else if (pid == 0) //子进程返回0,=0
{
printf("子进程,返回值:%d, PID:%d, PPID:%d \n",pid,getpid(),getppid());
}
else if (pid > 0) //父进程返回pid,> 0
{
wait(NULL); //暂停父进程,子进程执行完,父进程会重新执行
printf("父进程,返回值:%d, PID:%d, PPID:%d \n",pid,getpid(),getppid());
}
return 0;
}
2️⃣ 编译运行及结果
1.编译gcc fork_1.c -o fork
2.运行:./fork
3.结果:
Before the fork!
After the fork!
After the fork!
子进程,返回值:0, PID:24606, PPID:24605
父进程,返回值:24606, PID:24605, PPID:23616
(3)结论
1️⃣fork()
出来的进程是调用fork()
语句之后的内容。而不是重头开始的。
故实践中,Before the fork!
只实现一次,而After the fork!
实现两次。
2️⃣getppid()
= 父进程ID,getpid()
= 当前进程ID,pid
= 创建返回的pid
父进程得到返回值为子进程ID(24605)。
子进程得到返回值为0。
返回值为-1的情形:
①进程总数达到系统规定最大数
②用户可建进程数达到系统最大数
表现:errno中含有出错代码EAGAIN
3️⃣父进程需等待子进程结束才可结束,否则子进程的PPID为systemd的PID,
故此处需在父进程中调用wait()
函数让父进程等待。若不调用则会出现结果为:
Before the fork!
After the fork!
父进程,返回值:24255, PID:24254, PPID:23616
After the fork!
子进程,返回值:0, PID:24255, PPID:4024
调用ps与grep命令:ps -x|grep 4024
,可查看该进程对象为:
4024 ? Ss 0:02 /lib/systemd/systemd --user
24395 pts/1 S+ 0:00 grep 4024
(4)写时复制(Cope-on-write)
写时复制是一种采取惰性优化方式避免复制时的系统开销,简单说:
若有多个进程想读取某部分相同资源,此处称为资源A,则复制是不必要的,只需让每个进程保存一个指向该部分资源的指针即可。看起来好像每个进程独占那份资源,避免复制的负担。
若有进程需修改资源A,此时便会复制一份出来进行修改使用,但不影响其他进程共享资源A。
好处:若进程无需修改资源,则无需复制,即:尽量推迟代价高昂的操作,直到必要时才会执行。
(5)情况讨论
正常情形:子进程会将其终止状态返回给父进程。
1️⃣情形讨论:若父进程在子进程之前终结。
子进程的父进程将会改为init进程,称作被init进程领养【确保每个进程都有一个父进程】
2️⃣情形讨论:父进程如何得到子进程状态?【内核为每个终止子进程保存一定量信息】
父进程调用wait()
或waitpid()
可得到信息并释放存储区和关闭所有打开文件【信息至少有:PID、终止状态、进程cpu时间总量】。
3️⃣情形讨论:若子进程已终止,但父进程未对其进行善后处理【获取信息和释放资源】
该进程被称作僵死进程。
shell命令:ps
会将僵死进程状态打印为Z.
;4️⃣情形讨论:init领养的进程最后会成为僵死进程么?
不会,因为init进程下的子进程终止则会调用wait函数取得其终止状态。
二、vfork()函数
函数功能:克隆调用进程,但不复制整个地址空间。父进程将被挂起,直到子进程退出或被调用execve
替换(运行一个新的可执行文件映像)。
与fork()
的区别:
保证子进程先运行,调用exec()
或_exit()
之后父进程才可能被调度运行。
不拷贝父进程的页表项,子进程必须立刻执行一次exec()
或_exit()
退出。
PS:
没啥用,历史遗留产物。
项目 | 说明 |
---|---|
函数原型 | pid_t vfork(void); |
头文件 | sys/types.h、unistd.h |
参数说明 | 无 |
返回值 | 调用一次,却能够返回两次,它可能有三种不同的返回值 ①在父进程中,fork返回新创建子进程的进程ID; ②在子进程中,fork返回0; ③若出现错误,fork返回一个负值;同时设置errno的错误返回码 |
注意 | ①避免地址空间的按页复制(该过程父进程和子进程共享相同地址空间和页表项(不使用写实时复制),即:复制内部的内核数据结构(故不能修改地址空间中的任何内存),)。 |