文章目录
(一)使用fork()创建子进程
(1)系统调用fork()特点:
- fork()调用一次,生成一个新的子进程
- 父进程、子进程各自返回一次
- 父进程中返回子进程的PID(进程号),子进程返回0
(2)简单使用fork()
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if(pid == -1)
return -1;
if(pid == 0)
{
printf("child return value = %d\n", pid);
printf("child pid = %d\n", getpid());
}
else
{
printf("father return value = %d\n", pid);
printf("father pid = %d\n", getpid());
}
return 0;
}
- 结果:
(3)分析程序的结果1
#include <stdio.h>
#include <unistd.h>
int main()
{
for(int i =0; i < 2; i++)
{
if(fork())
{
printf("a\n");
}
else
{
printf("b\n");
}
}
return 0;
}
-
分析fork流程:
-
结果:
-
拓展:那如果把printf中的\n去掉呢
分析:去掉\n后,输出缓冲区保存的数据也会被子进程复制到自己的输出缓冲区
-
拓展结果:4a 4b
(4)分析程序结果2
#include <stdio.h>
#include <unistd.h>
int main()
{
if(fork() && fork())
{
printf("a\n");
}
else
{
printf("b\n");
}
return 0;
}
-
分析过程:注意截断与 &&
-
结果:
-
同理,去掉printf中的\n
- 分析:
不影响,因为fork一次后,父进程返回值是子进程1的pid为真,父进程继续fork一个子进程2,两次fork时,父进程的输出缓冲区都是空的,所以结果无影响。
- 结果:
- 分析:
(二)fork深入
(1)父子进程数据继承问题
- 代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int gdata = 10;
int main()
{
int ldata = 10;
int* hdata = (int*)malloc(sizeof(int));
*hdata = 10;
pid_t pid = fork();
if(pid == -1)
return -1;
if(pid == 0)
{
printf("gdata = %d, &gdata = %p\n", gdata, &gdata);
printf("ldata = %d, &ldata = %p\n", ldata, &ldata);
printf("*hdata = %d, hdata = %p\n", gdata, &gdata);
gdata = 20;
ldata = 20;
*hdata = 20;
}
else
{
sleep(3);
printf("gdata = %d, &gdata = %p\n", gdata, &gdata);
printf("ldata = %d, &ldata = %p\n", ldata, &ldata);
printf("*hdata = %d, hdata = %p\n", gdata, &gdata);
}
free(hdata);
return 0;
}
- 结果:
- 结论:
子进程会继承父进程的.data .bss .heap .stack (fork之前的)区域
系统会为每个进程提供4G的虚拟空间, 系统会为每个进程维护一个不同的页表
父子进程的虚拟地址空间地址相同,通过不同页表的映射,对应的物理地址不同
(2)子进程物理内存何时分配?
讲解之前我们先看看下面这个代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main()
{
int size = 1024 * 1024 * 1024; //1G
char* space = (char*)malloc(sizeof(char) * size);
if(space == NULL) return 0;
int stepsize = size / 32; //32M
for(int i = 0; i < 32; i++)
{
memset(space + stepsize * i, 'a', stepsize);
sleep(1);
}
printf("space memset 'a' ok, will call fork\n");
pid_t pid = fork();
if(pid == -1)
return -1;
if(pid == 0)
{
printf("son start\n");
for(int i = 0; i < 32; i++)
{
memset(space + stepsize * i, 'b', stepsize);
sleep(1);
}
printf("son died\n");
}
else
{
sleep(40);
}
free(space);
return 0;
}
分析这个程序干了什么:(验证写时拷贝技术)
- 父进程开辟堆区空间,并进行a赋值。
- 创建子进程,用b修改父进程开辟的这份空间。
这是程序运行中的内存消耗图:
结论:
- 调用malloc时,系统从虚拟地址空间的堆区空间分配一块空间,并没有分配物理内存,使用该空间才会分配;
- 调用fork创建子进程,fork时候并不会真正将物理内存拷贝给子进程,只有当子进程修改空间内的任意数据时,系统就会为修改的数据所在空间以“页”的单位复制一个副本。
- 写时拷贝技术:fork之后,父子进程的共享同一块物理空间(共享空间只读)
(3)父子进程对fork之前打开的文件描述符如何处理?
- 代码测试:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
//父进程先打开a.txt文件
int fd = open("./a.txt", O_RDONLY);
if(fd == -1)
{
return -1;
}
pid_t pid = fork();
if(pid == -1)
{
return -1;
}
//子进程
if(pid == 0)
{
char buff[128] = {
0};
int ret = read(fd, buff, 9);
if(ret <= 0)
{
return 0;
}
printf("child read:%s\n", buff);
exit(0);
}
else
{
char buff[128] = {
0};
int ret = read(fd, buff, 9);
if(ret <= 0)
{
return 0;
}
printf("father read:%s\n", buff);
}
close(fd);
return 0;
}
- 结果:
- 结论:
- fork之前打开的文件,在fork之后子进程可以通过该文件描述符访问该文件;
- fork之前打开的文件,共享文件光标偏移位置(f_pos);
(4)fork与vfork的区别
网上抄的一段,可以再理解理解:
为什么会有vfork,因为以前的fork 很傻, 它创建一个子进程时,将会创建一个新的地址
空间,并且拷贝父进程的资源,而往往在子进程中会执行exec 调用,这样,前面的拷贝工
作就是白费力气了,这种情况下,聪明的人就想出了vfork,它产生的子进程刚开始暂时与
父进程共享地址空间(其实就是线程的概念了),因为这时候子进程在父进程的地址空间中
运行,所以子进程不能进行写操作,并且在儿子 霸占”着老子的房子时候,要委屈老子一
下了,让他在外面歇着(阻塞),一旦儿子执行了exec 或者exit 后,相 于儿子买了自己的
房子了,这时候就相当于分家了。
转载原文链接:
https://blog.csdn.net/jianchi88/article/details/6985326
(5)windows系统malloc申请空间的机制
转载原文链接:
https://www.cnblogs.com/jikexianfeng/p/6192492.html
(6)windows系统和Linux系统能够申请的最大空间是多少(开启交换分区情况下)
转载原文链接:
https://blog.csdn.net/qq_37200329/article/details/97949658
(7)fork的源码
asmlinkage int sys_fork(struct pt_regs regs)
{
return do_fork(SIGCHLD, regs.esp, ®s, 0, NULL, NULL);
}
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p;
int trace = 0;
/**
* 通过查找pidmap_array位图,为子进程分配新的pid参数.
*/
long pid = alloc_pidmap();
if (pid < 0)
return -EAGAIN;
/**
* 如果父进程正在被跟踪,就检查debugger程序是否想跟踪子进程.
* 并且子进程不是内核进程(CLONE_UNTRACED未设置)
* 那么就设置CLONE_PTRACE标志.
*/
if (unlikely(current->ptrace))
{
trace = fork_traceflag (clone_flags);
if (trace)
clone_flags |= CLONE_PTRACE;
}
/**
* copy_process复制进程描述符.如果所有必须的资源都是可用的
* 该函数返回刚创建的task_struct描述符的地址.
* 这是创建进程的关键步骤.
*/
p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid);
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
*/
if (!IS_ERR(p))
{
struct completion vfork;
if (clone_flags & CLONE_VFORK)
{
p->vfork_done = &vfork;
init_completion(&vfork);
}
/**
* 如果设置了CLONE_STOPPED,或者必须跟踪子进程.
* 就设置子进程为TASK_STOPPED状态,并发送SIGSTOP信号挂起它.
*/
if ((p->ptrace & PT_PTRACED) || (clone_flags & CLONE_STOPPED))
{
/*
* We'll start up with an immediate SIGSTOP.
*/
sigaddset(&p->pending.signal, SIGSTOP);
set_tsk_thread_flag(p, TIF_SIGPENDING);
}
/**
* 没有设置CLONE_STOPPED,就调用wake_up_new_task
* 它调整父进程和子进程的调度参数.
* 如果父子进程运行在同一个CPU上,并且不能共享同一组页表(CLONE_VM标志被清0).那么,就把子进程插入父进程运行队列.
* 并且子进程插在父进程之前.这样做的目的是:如果子进程在创建之后执行新程序,就可以避免写时复制机制执行不必要时页面复制.
* 否则,如果运行在不同的CPU上,或者父子进程共享同一组页表.就把子进程插入父进程运行队列的队尾.
*/
if (!(clone_flags & CLONE_STOPPED))
wake_up_new_task(p, clone_flags);
else/*如果CLONE_STOPPED标志被设置,就把子进程设置为TASK_STOPPED状态。*/
p->state = TASK_STOPPED;
/**
* 如果进程正被跟踪,则把子进程的PID插入到父进程的ptrace_message,并调用ptrace_notify
* ptrace_notify使当前进程停止运行,并向当前进程的父进程发送SIGCHLD信号.子进程的祖父进程是跟踪父进程的debugger进程.
* dubugger进程可以通过ptrace_message获得被创建子进程的PID.
*/
if (unlikely (trace))
{
current->ptrace_message = pid;
ptrace_notify ((trace << 8) | SIGTRAP);
}
/**
* 如果设置了CLONE_VFORK,就把父进程插入等待队列,并挂起父进程直到子进程结束或者执行了新的程序.
*/
if (clone_flags & CLONE_VFORK)
{
wait_for_completion(&vfork);
if (unlikely (current->ptrace & PT_TRACE_VFORK_DONE))
ptrace_notify ((PTRACE_EVENT_VFORK_DONE << 8) | SIGTRAP);
}
}
else
{
free_pidmap(pid);
pid = PTR_ERR(p);
}
return pid;
}