所谓父子进程,就是在一个进程的基础上创建出另一条完全独立的进程,这个就是子进程,相当于父进程的副本。
在Linux中,程序员可以通过pid_t fork()
函数即可为当前进程创建出一个子进程:
【注意】:fork()
函数如果成功返回,其返回值有两个,但两个返回值并不可能返回给一个进程,而是分别返回给父子进程:父进程收到的返回值 > 0、子进程收到的返回值 = 0;
例:为一个进程创建子进程并让父子进程执行不同的任务(通过fork函数的返回值区分父子进程)[请在Linux环境测试,windows不支持fork函数]
#include
#include
// 测试创建子进程函数 pid_t fork();
int main()
{
pid_t pid = fork();
if(-1 == pid)
{
//创建子进程失败
return -1;
}
if(0 == pid)
{
//子进程
printf("I am child, my pid:%d, ppid:%d\n",getpid(),getppid());
sleep(5);
}
else
{
//父进程
printf("I am father, my pid:%d, ppid:%d\n",getpid(),getppid());
sleep(5);
}
return 0;
}
ppid 指当前进程的父进程的PID号,可以看到:child 的 ppid == father 的pid
子进程完全拷贝父进程的PCB,但并不是同一个(后序会讲到写时拷贝)
代码共享,数据独有。
我们知道进程的PCB中有指向该程序在内存上数据的指针,并且保留了上下文信息,那么子进程修改一个变量是否会影响到地址相同的父进程变量?
我们在子进程中修改变量a的值,并将变量a的值打印处理,看到子进程对变量a的修改竟然并不影响父进程相同地址的变量值 !
这样的结果也说明了父子进程虽然代码共享,但数据是各自独有的。这是因为OS中的虚拟内存机制(详见后续博客),这样的机制保证了父子进程独立运行互不干扰。
子进程从代码的fork之后才汇编执行
子进程拷贝父进程PCB,而父进程执行时在PCB存储了程序计数器与上下文信息,因此虽然父子进程代码共享,但子进程并不会从代码起始从新运行。
这种机制也避免了子进程创建后再次执行
fork()
函数,从而无限创建子进程
用户能够在命令行中执行的进程,其父进程都是:bash(centos的shell)
也就是说,创建子进程有什么作用?
通过父子进程特性可以看到,子进程与父进程有着很强的关联,但其运行过程并不影响父进程;
因此子进程也被称为父进程的守护进程:当一个进程需要做一些可能发生阻塞或中断的任务,父进程可通过子进程去做,来避免自己进程的崩溃。
直白来说,就是子进程先于父进程退出,子进程就会变成僵尸进程
#include
#include
// 制作僵尸进程
// 子进程在父进程运行时退出
// 子进程的退出状态信息父进程就无法回收
int main()
{
// 创建子进程
int ret = fork();
if(ret < 0)
{
return -1;
}
else if(ret == 0)
{
// 子进程
printf("this is child!\n");
}
else
{
// 父进程
// 一直死循环运行
while(1)
{
printf("this is father!\n");
sleep(1);
}
}
return 0;
}
一个进程在退出的时,会关闭所有的文件描述符,释放在用户空间中分配的内存,但是该进程的 PCB 仍会暂时保留,因为里面还存放着进程的退出状态以及统计信息等,这些PCB的信息均需该进程的父进程接收。
Linux下任何进程都有父进程,即每个进程的PCB都需由其父进程回收(除了0号进程)
Linux下有3个特殊的进程,idle进程(PID=0)
,init进程(PID=1)
和 kthreadd进程(PID=2)
idle进程(0号进程)
是系统所有进程的先祖,内核静态创建的,运行在内核态;这也是唯一一个没有通过fork或者kernel_thread产生的进程;
init进程(1号进程)
是系统中所有其它用户进程的祖先进程,由0进程创建,完成系统的初始化;Linux中的所有进程都是有 init进程 创建并运行的,这个流程大概是:首先Linux内核启动,然后在用户空间中启动init进程,再启动其他系统进程。在系统进程启动完成后,init将变为守护进程监视系统其他进程。
若用户形成一个孤儿进程,该孤儿进程会转由1号进程回收其PCB信息;(后续详谈)
kthreadd进程(2号进程)
由0进程创建,始终运行在内核空间, 负责所有内核线程的调度和管理;
父进程如何回收子进程的PCB信息:
SIGCHLD信号
(进程信号详谈)但是当父进程正处于运行或睡眠状态,是无法接收子进程的退出信号, 子进程的退出状态信息无法被回收,其PCB将一直存在于内存(也就是变成所谓的僵尸态),久而久之便会造成内存泄漏。
危害:
子进程在内存上的PCB无法释放,会造成内存泄漏!
且一旦造成僵尸进程,通过kill -9
强杀指令也无法终止该指令;因此在编码时要坚决避免僵尸进程!
解决方法:
直白来说,就是父进程先于子进程退出,子进程就会变成孤儿进程;
注意:没有孤儿状态!
#include
#include
// 测试孤儿进程
// 让父进程先于子进程退出
int main()
{
int ret = fork();
if(ret < 0)
{
return -1;
}
if(ret == 0)
{
// 子进程
while(1) //让子进程不退出观察其状态
{
printf("this is child!\n");
printf("my pid:%d, my ppid:%d\n",getpid(),getppid());
sleep(3);
}
}
else
{
// 父进程
printf("this is father!\n");
printf("my pid:%d, my ppid:%d\n",getpid(),getppid());
sleep(10);
}
return 0;
}
孤儿进程的出现流程:
孤儿进程的危害:孤儿进程由系统回收,没有危害;
参考:
- 进程概念——进程本质与PCB(进程控制块)
- Linux下1号进程的前世(kernel_init)今生(init进程)----Linux进程的管理与调度(六)