fork() 用于创建 一个新的进程,一次调用两次返回。父进程返回子进程的PID 子进程是 0.
fork() 采用写时复制,也就是 创建的时候 就复制了页表,并没有实际的内存空间,子进程这个时候和父亲共享内存,但是子进程只有 读的权限,当修改的时候 才复制一份。
知乎大佬的nettle的回答:
linux下的fork()函数

  1. 传统的fork()系统调用直接把所有的资源复制给新创建的进程.linux的fork()使用写时拷贝(copy-on-write)页实现.写时拷贝是一种可以推迟甚至免除拷贝数据的技术,内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝. 只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝,也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读的方式共享.这种技术使地址空间上的页的拷贝被推迟到实发生写入的时候才进行.在页跟本不会被写入的情况下(比如:fork()后立即调用exec())它们就无需复制了.
  2. linux通过系统调用clone()来实现fork().然后由clone()来调用do_fork().
    附:
    linux下fork()函数的实现:
    linux通过clone()系统调用实现fork()。这个调用通过一系列的参数标志来指明父,子进程需要共享的资源。fork(),vfork()和__clone()库函数都根据各自需要的参数标志去调用clone().然后由clone()去调用do_fork(). do_frok完成了创建中的大部分工作,它的定义在ker/frok.c文件中。该函数调用copy_process()的函数,然后让进程开始运行。copy_process()函数完成的工作很有意思:

    1. 调用dup_task_struct()为新进程创建一个内核栈,thread_info结构和task_struct,这些值与当前进程的值相同。此时,子进程和父进程的描述符是完全相同的。
    2. 检查新创建的这个子进程后,当前用户所拥有的进程数目没有超出给它分配的资源的限制。
    3. 现在,子进程着手使自己与父进程区别开来。进程描述符内的许多成员都要被清0或者设为初始值。进程描述符的成员值并不是继承而来的,而主要是统计信息。进程描述符中的大多数数据都是共享的.
    4. 接下来,子进程的状态被设置为TASK_UNINTERRUPTIBLE以保证它不会投入运行。
    5. copy_process()调用copy_flags()以更新task_struct的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV的标志被清0.表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。
    6. 调用get_pid()为新进程获取一个有效的PID。
    7. 根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件,文件系统信息,信号处理函数,进程地址空间和命名空间等。再一半情况下,这些资源会被给定进程的所有线程共享;否则,这些资源对每个进程是不同的,因此被拷贝到了这里。
    8. 让父进程和子进程平分剩余的时间片。
    9. 最后,copy_process()做扫尾工作并返回一个指向子进程的指针。 再回到do_fork()函数,如果copy_process()函数返回成功,新创建的子进程被唤醒并让其投入运行。内核有意选择子进程首先执行。因为一半子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销,如果父进程首先执行的话,有可能会开始向地址空间写入。

可以看看这篇大佬的博客

对于.Linux 2.6.38 下的 fork() ,vfork(),clone

asmlinkage int sys_fork(unsigned long r4, unsigned long r5,unsigned long r6, unsigned long r7,struct pt_regs __regs)
{#ifdef CONFIG_MMUstruct pt_regs *regs = RELOC_HIDE(&__regs, 0);return do_fork(SIGCHLD, regs->regs[15], regs, 0, NULL, NULL);
#else/* fork almost works, enough to trick you into looking elsewhere :-( */return -EINVAL;
#endif
}
asmlinkage int sys_vfork(unsigned long r4, unsigned long r5,unsigned long r6, unsigned long r7,struct pt_regs __regs)
{struct pt_regs *regs = RELOC_HIDE(&__regs, 0);return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs->regs[15], regs,0, NULL, NULL);
}asmlinkage int sys_clone(unsigned long clone_flags, unsigned long newsp,unsigned long parent_tidptr,unsigned long child_tidptr,struct pt_regs __regs)
{struct pt_regs *regs = RELOC_HIDE(&__regs, 0);if (!newsp)newsp = regs->regs[15];return do_fork(clone_flags, newsp, regs, 0,(int __user *)parent_tidptr,(int __user *)child_tidptr);
}

LINUX0.11 fork.c源码

/**  linux/kernel/fork.c**  (C) 1991  Linus Torvalds*//**  'fork.c' contains the help-routines for the 'fork' system call* (see also system_call.s), and some misc functions ('verify_area').* Fork is rather simple, once you get the hang of it, but the memory* management can be a bitch. See 'mm/mm.c': 'copy_page_tables()'*/
#include <errno.h>#include <linux/sched.h>
#include <linux/kernel.h>
#include <asm/segment.h>
#include <asm/system.h>
//写页面 ,如果不可写 就复制页面
extern void write_verify(unsigned long address);long last_pid=0;
进程空间区域写前验证函数。
//对于80386CPU,在执行特权级0代码时不会理会用户空间中的页面是否是页保护的,因此
//在执行内核代码时用户空间中数据页面保护标志起不了作用,写时复制机制也就失去了作用。
//verify_area)函数就用于此目的。但对于80486或后来的CPU,其控制寄存器CRO中有一个
//写保护标志WP(位16),内核可以通过设置该标志来禁止特权级0的代码向用户空间只读
//页面执行写数据,否则将导致发生写保护异常。从而486以上CPU可以通过设置该标志来达
//到本函数的目的。
//该函数对当前进程逻辑地址从addr到addr+size这一段范围以页为单位执行写操作前
//的检测操作。由于检测判断是以页面为单位进行操作,因此程序首先需要找出addr所在页
//面开始地址start,然后start加上进程数据段基址,使这个start变换成CPU4G线性空
//间中的地址。最后循环调用write_verify()对指定大小的内存空间进行写前验证。若页面
//是只读的,则执行共享检验和复制页面操作(写时复制)。void verify_area(void * addr,int size)
{unsigned long start;//调整start 为 所在页左边界 开始位置,并 更改size//简单来说 就是设置成 4K 的倍数,然后每次循环验证start = (unsigned long) addr;size += start & 0xfff;//低12位置零start &= 0xfffff000;//加上数据段在线性空间中的起始基址start += get_base(current->ldt[2]);while (size>0) {//每次变 4096=4Ksize -= 4096;write_verify(start);start += 4096;}
}//复制内存页表。
//参数nr是新任务号;p是新任务数据结构指针。该函数为新任务在线性地址空间中设置代码
//段和数据段基址、限长,并复制页表。由于Linux系统采用了写时复制(copy on write)
//技术,因此这里仅为新进程设置自己的页目录表项和页表项,而没有实际为新进程分配物理
// 内存页面。此时新进程与其父进程共享所有内存页面。操作成功返回0,否则返回出错号。
int copy_mem(int nr,struct task_struct * p)
{unsigned long old_data_base,new_data_base,data_limit;unsigned long old_code_base,new_code_base,code_limit;
//首先取当前进程局部描述符表中代码段描述符和数据段描述符项中的段限长(字节数)。
//0x0f是代码段选择符;0x17是数据段选择符。然后取当前进程代码段和数据段在线性地址
// 空间中的基地址。由于Linux0.11内核还不支持代码和数据段分立的情况,因此这里需要
//检查代码段和数据段基址和限长是否都分别相同。否则内核显示出错信息,并停止运行。
//get_limit()和get base()定义在include/linux/sched.h第226行处。code_limit=get_limit(0x0f);data_limit=get_limit(0x17);old_code_base = get_base(current->ldt[1]);old_data_base = get_base(current->ldt[2]);if (old_data_base != old_code_base)panic("We don't support separate I&D");if (data_limit < code_limit)panic("Bad data_limit");
//然后设置创建中的新进程在线性地址空间中的基地址等于(64MB*其任务号),并用该值
//设置新进程局部描述符表中段描述符中的基地址。接着设置新进程的页目录表项和页表项,
//即复制当前进程(父进程)的页目录表项和页表项。
//正常情况下copy_page_tables)返回0,否则表示出错,则释放刚申请的页表项。new_data_base = new_code_base = nr * 0x4000000;p->start_code = new_code_base;set_base(p->ldt[1],new_code_base);set_base(p->ldt[2],new_data_base);if (copy_page_tables(old_data_base,new_data_base,data_limit)) {free_page_tables(new_data_base,data_limit);return -ENOMEM;}return 0;
}/**  Ok, this is the main fork-routine. It copies the system process* information (task[nr]) and sets up the necessary registers. It* also copies the data segment in it's entirety.*/
/*0K,下面是主要的fork子程序。它复制系统进程信息(task[n])
*并且设置必要的寄存器。它还整个地复制数据段。* */
//复制进程。
//该函数的参数是进入系统调用中断处理过程(system_call.s)开始,直到调用本系统调用处理
//过程(systemcall.s第208行)和调用本函数前时(system_call.s第217行)逐步压入栈的
//各寄存器的值。这些在system_cal1.s程序中逐步压入栈的值(参数)包括:
//①CPU执行中断指令压入的用户栈地址ss和esp、标志寄存器eflags和返回地址cs和eip;
//②第83-88行在刚进入system_cal1时压入栈的段寄存器ds、es、fs和edx、ecx、ebx;
//③第94行调用syscall_table中sys fork函数时压入栈的返回地址(用参数none表示);
//④第212--216行在调用copy process()之前压入栈的gs、esi、edi、ebp和eax(nr)值。
//其中参数nr是调用find empty process()分配的任务数组项号。
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,long ebx,long ecx,long edx,long fs,long es,long ds,long eip,long cs,long eflags,long esp,long ss)
{struct task_struct *p;int i;struct file *f;p = (struct task_struct *) get_free_page();if (!p)return -EAGAIN;task[nr] = p;*p = *current; /* NOTE! this doesn't copy the supervisor stack */p->state = TASK_UNINTERRUPTIBLE;p->pid = last_pid;p->father = current->pid;p->counter = p->priority;p->signal = 0;p->alarm = 0;p->leader = 0;      /* process leadership doesn't inherit */p->utime = p->stime = 0;p->cutime = p->cstime = 0;p->start_time = jiffies;//再修改任务状态段TSS数据。由于系统给任务结构p分配了1页新
//内存,所以(PAGE_SIZE+(1ong)p)让esp0正好指向该页顶端。ss0:esp0用作程序p->tss.back_link = 0;p->tss.esp0 = PAGE_SIZE + (long) p;p->tss.ss0 = 0x10;p->tss.eip = eip;p->tss.eflags = eflags;p->tss.eax = 0;p->tss.ecx = ecx;p->tss.edx = edx;p->tss.ebx = ebx;p->tss.esp = esp;p->tss.ebp = ebp;p->tss.esi = esi;p->tss.edi = edi;p->tss.es = es & 0xffff;p->tss.cs = cs & 0xffff;p->tss.ss = ss & 0xffff;p->tss.ds = ds & 0xffff;p->tss.fs = fs & 0xffff;p->tss.gs = gs & 0xffff;p->tss.ldt = _LDT(nr);p->tss.trace_bitmap = 0x80000000;//如果当前任务使用了协处理器,就保存其上下文。汇编指令clts用于清除控制寄存器CRO
//中的任务已交换(TS)标志。每当发生任务切换,CPU都会设置该标志。该标志用于管理
//数学协处理器:如果该标志置位,那么每个ESC指令都会被捕获(异常7)。如果协处理
//器存在标志MP也同时置位的话,那么WAIT指令也会捕获。因此,如果任务切换发生在一//个ESC指令开始执行之后,则协处理器中的内容就可能需要在执行新的ESC指令之前保存
//起来。捕获处理句柄会保存协处理器的内容并复位TS标志。指令fnsave用于把协处理器
//的所有状态保存到目的操作数指定的内存区域中(tss.i387)。if (last_task_used_math == current)__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
//接下来复制进程页表。即在线性地址空间中设置新任务代码段和数据段描述符中的基址
// 和限长,并复制页表。如果出错(返回值不是0),则复位任务数组中相应项并释放为
//该新任务分配的用于任务结构的内存页。if (copy_mem(nr,p)) {task[nr] = NULL;free_page((long) p);return -EAGAIN;}//如果父进程中有文件是打开的,则将对应文件的打开次数增1。因为这里创建的子进程
//会与父进程共享这些打开的文件。将当前进程(父进程)的pwd,root和executable
//引用次数均增1。与上面同样的道理,子进程也引用了这些i节点。for (i=0; i<NR_OPEN;i++)if (f=p->filp[i])f->f_count++;if (current->pwd)current->pwd->i_count++;if (current->root)current->root->i_count++;if (current->executable)current->executable->i_count++;//随后在GDT表中设置新任务TSS段和LDT段描述符项。这两个段的限长均被设置成104
//字节。set tss desc()和set 1dt desc()的定义参见include/asm/system.h文件
//52-66行代码。“gdt+(nr<<1)+FIRST_TSS ENTRY”是任务nr的TSS描述符项在全局
//表中的地址。因为每个任务占用GDT表中2项,因此上式中要包括’(nr<<1)'。//程序然后把新进程设置成就绪态。另外在任务切换时,任务寄存器tr由CPU自动加载。
//最后返回新进程号。set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));p->state = TASK_RUNNING;   /* do this last, just in case */return last_pid;
}
//为新进程取得不重复的进程号last pid。函数返回在任务数组中的任务号(数组项)。
int find_empty_process(void)
{int i;
//首先获取新的进程号。如果last_pid增1后超出进程号的正数表示范围,则重新从1开始
//使用pid号。然后在任务数组中搜索刚设置的pid号是否已经被任何任务使用。如果是则
//跳转到函数开始处重新获得一个pid号。接着在任务数组中为新任务寻找一个空闲项,并
//返回项号。last pid是一个全局变量,不用返回。如果此时任务数组中64个项已经被全
//部占用,则返回出错码。repeat:if ((++last_pid)<0) last_pid=1;for(i=0 ; i<NR_TASKS ; i++)if (task[i] && task[i]->pid == last_pid) goto repeat;for(i=1 ; i<NR_TASKS ; i++)if (!task[i])return i;return -EAGAIN;
}

linux0.11操作系统源码剖析fork.c相关推荐

  1. Linux0.11内核源码解析-setup.s

    学习资料: Linux内核完全注释 操作系统真像还原 极客时间-Linux内核源码趣读 Linux0.11内核源码 ->setup程序将system模块从0x10000~0x8ffff整块向下移 ...

  2. Linux0.11内核源码解析-bootsect.s

    学习资料: Linux内核完全注释 操作系统真像还原 极客时间-Linux内核源码趣读 Linux0.11内核源码 ->上电 ->80x86架构CPU会自动进入实模式 ->从地址0x ...

  3. linux-0.11 内核源码学习笔记一(嵌入式汇编语法及使用)

    linux内核源码虽然是用C写的,不过其中有很多用嵌入式汇编直接操作底层硬件的"宏函数",要想顺利的理解内核理论和具体实现逻辑,学会看嵌入式汇编是必修课,下面内容是学习过程中的笔记 ...

  4. Linux0.11内核源码分析(bootsect.s)

    Intel 80x86系列的CPU可以分别在16位实模式和32位保护模式下运行.为了兼容,也为了解决最开始的启动问题,Intel将所有80x86系列的CPU,包括最新型号的CPU的硬件都设计为加电即进 ...

  5. Linux0.11内核源码解析01

    系统整体布局 第一部分:进入内核前的苦力活 第二部分:大战前期的初始化工作 第三部分:一个新进程的诞生 第四部分:shell 程序的到来 第五部分:从一个命令的执行看操作系统各模块的运作 第六部分:操 ...

  6. Linux0.11内核源码分析1-main函数运行之前的准备

    在阅读该文章之前,你起码有点操作系统的知识,了解实模式与保护模式的概念,了解分段机制,如果不懂得建议去阅读<操纵系统真象还原>这本书

  7. tomcat(11)org.apache.catalina.core.StandardWrapper源码剖析

    [0]README 0.0)本文部分文字描述转自 "how tomcat works",旨在学习 "tomcat(11)StandardWrapper源码剖析" ...

  8. libuv访问mysql_libuv源码剖析

    libuv-v1.11.0 - 源码剖析 - 01 - uv_loop_t > uv_loop_t struct { void *data; unsigned int active_handle ...

  9. Python envoy 模块源码剖析

    Kenneth Reitz 是公认的这个世界上 Python 代码写得最好的人之一.抱着学习的心态,我阅读了 Reitz 写的 envoy 模块的源码,将笔记记录如下. 介绍 和 requests 模 ...

最新文章

  1. 微信 小程序布局 水平菜单
  2. 计算Android屏幕解锁组合数
  3. 相位语谱图或将打破机械音
  4. 蓝桥杯 ADV-78 算法提高 最长单词
  5. OAuth 及 移动端鉴权调研
  6. 怎么让jsp中的按钮置灰不能使用_拆解按钮规范
  7. OpenCL编程基本流程及完整示例
  8. matlab里的timer,关于Matlab中用timer来实现多线程机制
  9. 2018美赛B题总结
  10. 电容麦克风测试软件,章和电气AudioExpress麦克风测试解决方案———您的音频测试专家...
  11. 电力职称计算机水平考试题库 2019,2019职称计算机考试Excel练习及答案汇总1
  12. 微信刷票怎么查实_怎么检查“微信公众平台投票”是否有刷票?
  13. 关闭ADOX.Catalog创建Access的链接,避免ldb锁定
  14. hashcat破解WiFi显示No hashes loaded的解决方法
  15. Hive的nvl函数
  16. 使用docker安装ubuntu镜像
  17. 关于StringUtils的各种方法的功能、解析
  18. 第七周:字符串 + 数组 + 指针
  19. Java高级--->多线程的学习
  20. HFirst解读和复现心得

热门文章

  1. 对接支付宝支付通道接口
  2. 一文读懂标量、向量、矩阵、张量的关系
  3. 一次网站性能优化经历
  4. office2013如何开启宏命令word or excel
  5. java 简历面试经验总结
  6. 每天看三页《深入Linux内核架构》——第十天
  7. C++新手项目实践 — 智能人机对战五子棋
  8. 重装系统当识别不到硬盘的解决办法
  9. win7官方原版iso镜像_如何才能下载到纯净的windows各个版本官方原版镜像?
  10. 《数据安全法》发布后为什么数据保护官DPO变得炙手可热?