【Linux】进程复制fork()

(一)使用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的区别

转载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, &regs, 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;
}

おすすめ

転載: blog.csdn.net/xiaoxiaoguailou/article/details/121456241