一、fork函数
头文件
#include <unistd.h>
函数原型
pid_t fork(void);
如
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t pid;
pid = getpid(); //等于父进程pid值
fork(); //创建进程
printf("my pid is %d %d\n",pid,getpid());
return 0;
}
/* pid_t pid;
pid_t pid2;
pid_t retpid;
pid = getpid();
printf("before fork pid : %d\n",pid); //父进程
retpid = fork();
pid2 = getpid();
printf("after fork pid : %d\n",pid2); //第一遍打印父进程,第二遍打印子进程
if(pid == pid2){
printf("this is father pid : %d\n",retpid); //父进程
}
else{
printf("this is child pid : %d\n",getpid()); //子进程
}
*/
运行结果中第一排打印的是父进程的信息,在fork()函数后面创建了一个子进程,第二排打印的是子进程的信息
返回值
fork函数调用成功,返回两次
返回值为0,代表当前进程是子进程
返回值为非负数,代表当前进程是父进程
父进程返回的是新子进程的PID号
调用失败返回 -1
根据fork函数的返回值可以做一个判断
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t pid;
printf("father pid : %d\n",getpid()); //打印的是父进程的pid
pid = fork(); //创建子进程
if(pid > 0){
printf("this is father pid : %d\n",getpid()); //父进程pid
}
else if(pid == 0){
printf("this is child pid : %d\n",getpid()); //子进程pid
}
return 0;
}
第一排打印的是父进程的pid值,根据fork函数的返回值执行if else语句,刚开始在父进程里面,所以返回值大于0,所以打印了第二句话,因为创建了子进程,所以会在执行一遍 pid = fork()函数下面的语句,又因为在子进程里面,所以打印第三句话
简单来说就是在fork()函数之前的语句只会被执行一次,fork函数后面的语句将会分别被父进程和子进程分别执行
二、 父进程给子进程拷贝了什么内容
fork函数调用成功,早期的Unix内核:父进程会把所有的内容都拷贝一分给子进程,包括正文(代码段)、初始化的数据段、未初始化的数据(BSS段)、堆、栈、命令行参数和环境变量等。
现在的Unix内核(包括Linux):现在是共享数据,而不是把父进程里面所有的东西都拷贝到子进程,只有当子进程需要更改数据时,才会将父进程中需要更改的数据拷贝给子进程,也就是写时拷贝。
在子进程里面修改的值和父进程没有关系
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
int data = 100;
pid_t pid;
pid = fork();
if(pid == 0){
printf("child"); //子进程
data += 100;
}else{
printf("father"); //父进程
}
printf("data = %d\n",data);
return 0;
}
三、fork为什么要创建子进程
1、一个父进程希望复制自己,使父,子进程同时执行不同的代码段
这在网络服务进程中是常见的——父进程等待客户端的服务请求,当这种请求达到要求时,父进程调用fork函数,使子进程处理此请求,父进程则继续等待下一个服务请求达到。
如图
用代码可以模拟上面场景
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t pid;
int data = 0;
while(1){
printf("please input a data\n");
scanf("%d",&data);
if(data == 1){ //判断是否输入了1
pid = fork(); //创建子进程
if(pid > 0){ //表示在父进程,什么事都不做
}
else if(pid == 0){ //进入子进程
while(1){ //子进程里面不断打印当前子进程的pid
printf("do some things, pid = %d\n",getpid());
sleep(3); //延时3s打印一次
}
}
}else{
printf("wait, do nothing\n");
}
return 0;
}
当输入1时每过3秒打印创建的子进程pid,并且子进程并不影响父进程的数据输入,当再输入一个1时,又创建一个子进程,同时打印两个子进程的pid,这样就模拟了服务器和客户端。
在系统里面也可以看见两个创建了两个pid值为18491和18493的进程。
2、一个进程要执行一个不同的程序,这对shell是常见的情况,在这种情况下,子进程从fork函数返回后立即调用exec。
四、总结:
由fork创建的新进程称为子进程(chile process),fork函数被调用一次,但返回两次,两次返回的唯一区别是子进程的返回值是0,而父进程的返回值是则是新子进程的进程PID,将子进程PID返回给父进程的理由是:因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程PID。fork函数使子进程得到返回值0的理由是:一个进程只会有一个父进程,所有子进程总是可以调用getppid以获得父进程的进程PID,(进程PID 0总是由内核交换进程使用,所以一个子进程的进程PID 不可能为0)。
子进程和父进程继续执行fork函数调用后的指令。子进程是父进程的副本,例如,子进程获得父进程数据空间,堆和栈的副本。注意,这是子进程所拥有的副本,父,子进程并不共享这些存储空间部分,父,子进程共享正文段。
由于在fork之后经常跟随着exec,所以现在的很多实现并不执行一个父进程数据段,栈和堆的完全复制。作为代替,使用了写时复制(Copy-On-Writ,COW)技术。这些区域由父,子进程共享,而且内核将它们的访问权限改变为只读的,如果父,子进程中的任何一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储器系统中的一“页”。