基础理解:
完完整整的复制父进程,包括父进程的打开的文件描述符,信号,代码段,数据段,堆栈内存等…
父子进程一模一样,如何区分:
尽管内容一样,但是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文章中会提到。