fork()

基础理解:

在这里插入图片描述
完完整整的复制父进程,包括父进程的打开的文件描述符,信号,代码段,数据段,堆栈内存等…

父子进程一模一样,如何区分:

尽管内容一样,但是fork()调用以后,在父子进程中的返回值不尽相同:

  • 如果子进程创建失败,返回-1
  • 在子进程中返回0
  • 在父进程中返回子进程的pid,一般在父进程中需要等到子进程运行结束。

当然,一些fork()实现时,会将只读的一些内存仅仅作引用,比如用子进程的指针指向父进程中的资源,或者仅仅复制父进程的虚拟页表项,不进行真正的拷贝,对于可读可写的数据,子进程可能才会做真正意义上的拷贝。

linux内核中的大概实现:

fork():sys_fork()

系统调用 fork() 通过 sys_fork() 进入do_fork()时,
其clone_flags为 SIGCHLD,也就是说,所有的标志位均为0,
所以copy_files(),copy_fs(),copy_sighand()以及copy_mm()全都真正执行了
也就是这四项资源全都复制了,linux32位上的3G地址空间都被子进程复制

子进程拥有了父进程地址空间3G的所有内容,不同的是父子进程PCB内容一些不同
不过遵循读时共享,写时拷贝原则

还有一点,经过fork()产生了子进程,和父进程的运行顺序与调度进程有关,
不是想当然的 父进程先执行,最终由调度进程决定谁先使用CPU

我们看一下linux内核sys_fork() 函数的大概实现
见注释

asmlinkage int sys_fork(struct pt_regs *regs)
{
#ifdef CONFIG_MMU
	return do_fork(SIGCHLD, regs->ARM_sp, regs, 0, NULL, NULL);
#else
	return(-EINVAL);
#endif
}

进入do_fork()函数后,根据标志不同,调用的函数也不同,由于这里标志是SIGCHLD
所以最终调用copy_process()
long do_fork(unsigned long clone_flags, //设置的标志位,SIGCHLD
	      unsigned long stack_start,    //内核栈顶
	      struct pt_regs *regs,         //内核栈寄存器
	      unsigned long stack_size,     //栈的大小
	      int __user *parent_tidptr,    //父进程的地址空间
	      int __user *child_tidptr)     //子进程的地址空间
{
	struct task_struct *p;   //申请一张空的PCB
	......
	p = copy_process(clone_flags, stack_start, regs, stack_size,
			 child_tidptr, NULL, trace);
	//将父进程地址空间的内容考到子进程中,保存到子进程的PCB即p中
	........
}

子进程拷贝父进程地址空间的资源以及初始化子进程的PCB发生在:

最终子进程拷贝父进程的资源的过程发生在copy_process()函数中,简单的看一下整个过程:
static struct task_struct *copy_process(unsigned long clone_flags,
					unsigned long stack_start,
					struct pt_regs *regs,
					unsigned long stack_size,
					int __user *child_tidptr,
					struct pid *pid,
					int trace)
{
	int retval;
	struct task_struct *p;
	//做一些安全检查,如标志是否一致


	//给用户进程申请一个进程描述符
	p = dup_task_struct(current);
	if (!p)
		goto fork_out;
   .......
	//用户空间的进程数目是否超标、将用户空间进程自增1等等信息
	atomic_inc(&p->user->__count);
	atomic_inc(&p->user->processes);
	get_group_info(p->group_info);
   ......
   //系统进程数量nr_threads不能超过max_threads
	if (nr_threads >= max_threads)
		goto bad_fork_cleanup_count;
	......
	
	......
	//将新进程的状态设置位TASK_RUNNING,父子进程共享父进程的时间片
	//最后将子进程放到调度链表头
	sched_fork(p);
	......
	
	//这里进行真正的一些拷贝,如文件系统、信号等
	
	if ((retval = security_task_alloc(p)))
		goto bad_fork_cleanup_policy;
	if ((retval = audit_alloc(p)))
		goto bad_fork_cleanup_security;
	/* copy all the process information */
	if ((retval = copy_semundo(clone_flags, p)))
		goto bad_fork_cleanup_audit;
	if ((retval = copy_files(clone_flags, p)))
		goto bad_fork_cleanup_semundo;
	if ((retval = copy_fs(clone_flags, p)))
		goto bad_fork_cleanup_files;
	if ((retval = copy_sighand(clone_flags, p)))
		goto bad_fork_cleanup_fs;
	if ((retval = copy_signal(clone_flags, p)))
		goto bad_fork_cleanup_sighand;
	if ((retval = copy_mm(clone_flags, p)))
		goto bad_fork_cleanup_signal;
	if ((retval = copy_keys(clone_flags, p)))
		goto bad_fork_cleanup_mm;
	if ((retval = copy_namespaces(clone_flags, p)))
		goto bad_fork_cleanup_keys;
	if ((retval = copy_io(clone_flags, p)))
		goto bad_fork_cleanup_namespaces;
	
	//设置子进程内核栈中的内容
	retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);
	if (retval)
		goto bad_fork_cleanup_io;
	.........

	//包括设置pid tgid等内容
	p->pid = pid_nr(pid);
	p->tgid = p->pid;
	if (clone_flags & CLONE_THREAD)
		p->tgid = current->tgid;

	if (current->nsproxy != p->nsproxy) {
		retval = ns_cgroup_clone(p, pid);
		if (retval)
			goto bad_fork_free_pid;
	}
	.........
	//将新的进程描述符插入到进程描述符表,是一个hash表
	return p;
	.........

我们写一个简单的程序验证一下,父子进程读时共享写时拷贝原则:

//我们读取父子进程中相同的数据
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>

int main()
{
	pid_t pid;
	pid = fork();

	int val = 0;

	if(pid == -1)
	{
		perror("fork erro\n");
		exit(1);
	}

	if(pid == 0)
	{
		printf("child id = %d,val = %d,add = %x\n",getpid(),val,&val);
	}
	else
	{
		printf("parent id = %d,val = %d,add = %x\n",getpid(),val,&val);
	}
	return 0;
}
执行结果:
parent id = 15610,val = 0,add = a86d7470
child id = 15611,val = 0,add = a86d7470


//我们修改一个数字
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>

int main()
{
	pid_t pid;
	int val = 0;
	pid = fork();
	
	if(pid == -1)
	{
		perror("fork erro\n");
		exit(1);
	}

if(pid == 0)
	{
		val+=100;
		printf("child id = %d,val = %d,addr = %x\n",getpid(),val,&val);
	}
	else
	{
		val +=1;
		printf("parent id = %d,val = %d,addr = %x\n",getpid(),val,&val);
	}
	return 0;
}
执行结果:
parent id = 15865,val = 1,addr = 9a8f8730
child id = 15866,val = 100,addr = 9a8f8730
两个值不同,地址是一样的,当然这里的地址是虚拟地址空间上的地址
足以说明两个数据不同的地址空间上
父子进程没有使用一个地址空间,只不过是子进程的指针可能执行父进程的资源

fork()和vfork()使用场景和区别以及关联在vfork文章中会提到。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值