Linux——进程操作之创建
创建进程是编程的常见操作。本节我们将对创建进程进行学习。
目录
一.fork()使用
(一).返回值
(二).进程独立
(三).子进程退出
二.写时拷贝
三.EIP寄存器(PC指针)
一.fork()使用
#include<unistd.h>
pid_t fork(NULL);
(一).返回值
fork创建进程失败会返回-1。
fork成功的返回值有两个。对于父进程而言是创建的子进程的pid(大于0的值);对于子进程而言返回值是0。
讲的通俗一些,fork之后的代码我们可以看成有两份。父进程、子进程分别执行这两份,各自都会拿到fork相应的返回值。即fork对于父子进程分别返回了不同值。
小编喜欢画图来使问题清晰易懂:
(二).进程独立
看过上图应该也就清楚了,如果想让子进程分别执行不同代码,只需要对id值进行判断即可。
if(id == 0)
{
//子进程代码
}
else if(id > 0)
{
//父进程代码
}
else
{
//进程创建失败
}
(三).子进程退出
当子进程执行完毕后,会由当前父进程回收。
1.如果父进程一直不回收,子进程会变成僵尸进程(Z)
2.如果父进程先退出,子进程会变成孤儿进程,由Bash“收养”,改为后台运行。
二.写时拷贝
可先参考这篇文章稍作了解:Linux——进程地址空间
我们知道,子进程与父进程会有一份该程序代码。这本质上就是当父进程创建子进程时,CPU把父进程的PCB结构体(稍作改变)、进程地址空间拷贝一份放入运行队列,形成子进程。因此,子进程代码段与父进程一样。
但是,进程地址空间通过页表后映射的物理内存与父进程相同。假如我们在fork之前创建了一个变量,只要子进程不改变变量值,那么父子就会一直共用这一个物理地址,即实际上只有一个变量真实存在。
如果子进程改变了变量值,那么内存管理模块会为子进程分配一块属于它自己的空间,页表改变映射,指向这块新分配的物理地址。这样的行为就叫做写时拷贝。
对于内存而言,写时拷贝具有节约空间,提高效率的意义,有利于内存的合理分配。
这也就解释了为什么fork成功返回值会有两个:
pid_t id = fork();
if(id == 0) {...}
else if(id > 0) {...}
else {...};
因为在fork内部创建子进程后,子进程改变了id值,因此内存为子进程专门开辟了空间存放id。
但是这里要注意,如果我们打印id地址的话会发现,父子进程id地址相同。
#include<string.h>
#include<unistd.h>
#include<stdio.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
printf("I am child process, id_address: %p\n", &id);
exit(0);
}
else{
printf("I am father process, id_address: %p\n", &id);
}
return 0;
}
这是因为虽然物理地址分开了,但是别忘了子进程的mm_struct拷贝自父进程,因此id虚拟地址与父进程一致。
当然,小编还是利用图片进一步解释:
三.EIP寄存器(PC指针)
我们可能会疑惑,为什么子进程在创建之后会从fork之后代码开始执行呢。
这是因为有EIP寄存器存在。
EIP寄存器只有一个(在CPU中),用于记录每一个进程的下一条指令的地址。
对于父进程而言,EIP寄存器内数据属于自己的上下文数据,会在PCB结构体中有所记录,task_struct结构体中成员cpu_context用于记录CPU内寄存器相关数据,包括EIP寄存器。
因此当子进程进行创建时,会复制PCB结构体,因而其EIP值与父进程相同,父进程下一条指令自然是fork之后的,所以子进程也是从fork后开始执行。
If you can't write it down in English, you can't code it. —— Peter Halpern
如有错误,敬请斧正