图片 20

二、进度的成立

系统中的进度是由父进度调用fork(卡塔尔函数来创造的,那么调用fork()函数的时候到底会发生哪些吧?

linux 新历程的创导

仿效资料

《Linux内核设计与实现》原书第三版

Sawoom原创文章转发请评释出处
《Linux内核解析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

前言


Unix标准的复制进程的系统调用时fork(即分叉),可是Linux,BSD等操作系统并不仅达成那二个,确切的说linux完毕了七个,fork,vfork,clone(确切说vfork成立出来的是轻量级进程,也叫线程,是分享财富的经过)

系统调用 描述
fork fork创造的子进程是父进程的完整副本,复制了父亲进程的资源,包括内存的内容task_struct内容
vfork vfork创建的子进程与父进程共享数据段,而且由vfork()创建的子进程将先于父进程运行
clone Linux上创建线程一般使用的是pthread库 实际上linux也给我们提供了创建线程的系统调用,就是clone

关于客商空间利用fork, vfork和clone, 请参见

Linux中fork,vfork和clone详明(分裂与调换)

fork, vfork和clone的种类调用的进口地址分别是sys_fork,
sys_vfork和sys_clone, 而他们的概念是信赖于系统布局的,
因为在客户空间和功底空间之间传递参数的艺术因类别布局而异

系统调用的参数字传送递

系统调用的达成与C库不一样,
普通C函数通过将参数的值压入到进程的栈中实行参数的传递。由于系统调用是因此暂停进度从客户态到内核态的一种奇特的函数调用,未有客户态或许内核态的旅社能够被用来在调用函数和被调函数之间开展参数字传送递。系统调用通过CPU的贮存器来举行参数字传送递。在进展系统调用以前,系统调用的参数被写入CPU的寄放器,而在实际调用系统服务例程在此之前,内核将CPU贮存器的源委拷贝到内核旅社中,完结参数的传递。

由此差别的种类构造或者行使分裂的办法或许不相同的贮存器来传递参数,而地点函数的职务正是从微机的寄放器中领取客商空间提供的音讯,
并调用系统布局非亲非故的_do_fork(或许开始时期的do_fork)函数,
肩负进程的复制

昔不近些日子的种类布局可能供给使用不相同的秘籍可能寄存器来囤积函数调用的参数,
因而linux在考虑系统调用的时候,
将其分割成连串构造有关的层系和系列布局非亲非故的等级次序,
前面三个复杂提抽取信任与系统布局的一定的参数,
前面一个则基于参数的安装实行一定的实在操作

4、copy_process()

收获进度号还要将一些寄放器的值压栈后,开端举办copy_process(State of Qatar,该函数根本担任以下的剧情。

  • 为子进度创制task_struct,将父进度的task_struct复制给子进程。
  • 为子进程的task_struct,tss做性子化设置。
  • 为子进度成立第叁个页表,也将父进度的页表内容赋给那么些页表。
  • 子进程分享父进度的文书。
  • 设置子进程的GDT项。
  • 聊到底将子进程设置为伏贴状态,使其得以涉足进度间的滚动。

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;         // 将新任务结构指针放入任务数组中。  
// 其中nr 为任务号,由前面find_empty_process()返回。  
  *p = *current;        /* NOTE! this doesn't copy the supervisor stack */  
/* 注意!这样做不会复制超级用户的堆栈 */ (只复制当前进程内容)。  
    p->state = TASK_UNINTERRUPTIBLE; // 将新进程的状态先置为不可中断等待状态。  
  p->pid = last_pid;     // 新进程号。由前面调用find_empty_process()得到。  
  p->father = current->pid;   // 设置父进程号。  
  p->counter = p->priority;  
  p->signal = 0;     // 信号位图置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->tss.back_link = 0;  
  p->tss.esp0 = PAGE_SIZE + (long) p;    // 堆栈指针(由于是给任务结构p 分配了1 页  
// 新内存,所以此时esp0 正好指向该页顶端)。  
  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;   // 段寄存器仅16 位有效。  
  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);    // 该新任务nr 的局部描述符表选择符(LDT 的描述符在GDT 中)。  
  p->tss.trace_bitmap = 0x80000000;   
// 如果当前任务使用了协处理器,就保存其上下文。  
    if (last_task_used_math == current)  
    __asm__ ("clts ; fnsave %0"::"m" (p->tss.i387));  
// 设置新任务的代码和数据段基址、限长并复制页表。如果出错(返回值不是0),则复位任务数组中  
// 相应项并释放为该新任务分配的内存页。  
  if (copy_mem (nr, p))  
    {               // 返回不为0 表示出错。  
      task[nr] = NULL;  
      free_page ((long) p);  
      return -EAGAIN;  
    }  
// 如果父进程中有文件是打开的,则将对应文件的打开次数增1。  
  for (i = 0; i < NR_OPEN; i++)  
    if (f = p->filp[i])  
      f->f_count++;  
// 将当前进程(父进程)的pwd, root 和executable 引用次数均增1。  
  if (current->pwd)  
    current->pwd->i_count++;  
  if (current->root)  
    current->root->i_count++;  
  if (current->executable)  
    current->executable->i_count++;  
// 在GDT 中设置新任务的TSS 和LDT 描述符项,数据从task 结构中取。  
// 在任务切换时,任务寄存器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;      // 返回新进程号(与任务号是不同的)。  
}

进入copy_prossess函数后,调用get_free_page()函数,在主内部存储器申请一个悠闲页面,并将提请到的页面清0。将那几个页面包车型地铁指针强逼类型转造成task_struct类型的指针,并挂接在task[nr]上,nr就是在find_empty_process中回到的任务号。

接下去的*p=*current将最近进程的指针赋给了子进度的,也正是说子进程继续了父进度一些第一的属性,当然那是远远不够的,所以接下去的一大堆代码皆感到子进度做性情化设置的。

日常来说,各种进度都要加载归属本人的代码、数据,所以copy_process设置子进程的内部存款和储蓄器地址。通过copy_mem来设置新职分的代码和数据段基址、限长并复制页表。

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;  

  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)   // 0.11 版不支持代码和数据段分立的情况。  
    panic ("We don't support separate I&D");  
  if (data_limit < code_limit)   // 如果数据段长度 < 代码段长度也不对。  
    panic ("Bad data_limit");  
  new_data_base = new_code_base = nr * 0x4000000;   // 新基址=任务号*64Mb(任务大小)。  
  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;  
}

接下来是对文件,pwd等财富的修正,接着要设置子进程在GDT中的表项,最终将经过设置为安妥状态,并赶回进度号。

一、背景知识:

1、进度与程序的关联:

进度是动态的,而前后相继是静态的;从布局上看,各类进度的实业都以由代码断和呼应的数目段两部分构成的,那与程序的意思很相近;二个历程能够提到多少个程序的实行,三个顺序也可以对应两个经过,即二个程序段可在分歧数量集结上运营,构成不相同的进程;并发性;进度具有成立其余进程的效劳;操作系统中的每三个进程都以在一个进度现场中运作的。

linux中客户进程是由fork系统调用创造的。Computer的内存、CPU
等财富都以由操作系统来分配的,而操作系统在分配能源时,大多数意况下是以进程为个人的。

每一个进度唯有贰个父进度,然而二个父进度却足以有三个子进度,当进程成立时,操作系统会给子进度创制新之处空间,并把父进度的地址空间的投射复制到子进度的地点空间去;父进度和子进度分享只读数据和代码段,不过货仓和堆是抽离的。

2、进程的重新整合:

经过调控块代码数据

经过的代码和数量由程序提供,而经过调整块则是由操作系统提供。

3、进度调整块的整合:

经过标志符进度上下文情况进程调节新闻进度序调节制消息

经过标记符:

进度ID进度名经过亲族关系有着该进程的顾客标志

经过的上下文境况:(主要指进程运营时CPU的各贮存器的内容)

通用寄放器程序状态在贮存器货仓指针寄放器指令指针贮存器标记存放器等

进程调解音信:

进程的景色进度的调治计策进程的开始的一段时期级进程的周转睡眠时间经过的不通原因进程的类别指针等

当进度处于不相同的事态时,会被放到分化的行列中。

经过调整音讯:

经过的代码、数据、饭店的胚胎地址进度的能源支配(进程的内部存款和储蓄器描述符、文件描述符、连续信号描述符、IPC描述符等)

进程使用的兼具财富都会在PCB中描述。

经过创制时,内核为其分配PCB块,当进度需要能源时内核会将相应的财富描述新闻参预到进程的PCB中,进度退出时内核会释放PCB块。平日来讲进度退出时应当释放它申请的能源,如文件汇报符等。为了防微杜渐进度遗忘某个财富(或是某个恶意进度)进而诱致能源泄漏,内核平常会依靠PCB中的音信回笼进度使用过的财富。

4、task_struct 在内部存款和储蓄器中的存款和储蓄:

在linux中经过调节块定义为task_struct, 下图为task_struct的根本成员:

图片 1

在2.6原先的基本中,各样进度的task_struct存放在他们内核栈的尾端。那样做是为着让那个像X86那样寄存器少之甚少的硬件系统构造只要经过栈指针就能够总结出它的职位,而幸免接纳额外的贮存器来特意记录。由于以后利用slab分配器动态生成task_struct,所以只需在栈底或栈顶创立一个新的结果struct
thread_info(在文件 asm/thread_info.h中定义)
struct thread_info{
struct task_struct *task;
struct exec_domain *exec_domain;
__u32 flags;
__u32 status;
__u32 cpu;
int preempt_count;
mm_segment addr_limit;
struct restart_block restart_block;
void *sysenter_return;
int uaccess_err;
};

图片 2

5、fork()、vfork()的联系:

Fork(卡塔尔国在2.6本子的基业中Linux通过clone(卡塔尔国系统调用达成fork(卡塔尔。这几个体系调用通过一种类的参数标识来指明父、子进度需求分享的能源。Fork(卡塔尔、vfork(卡塔尔国和库函数都基于各自供给的参数标识去调用clone(卡塔尔(قطر‎,然后由clone(State of Qatar去调用do_fork().
do_fork(卡塔尔完毕了创制中的抢先八分之四做事,它的概念在kernel/fork.c文件中。该函数调用copy_process(State of Qatar函数,然后经过开始运营。Copy_process(State of Qatar函数完结的工作很有趣:
1)、调用dup_task_struct(State of Qatar为新历程创立二个基石酒馆、thread_info结构和task_struct布局,那几个值与当前经过的值完全雷同。当时子进度和父进度的描述符是完全相近的。
2)、检查并确认保障新创造那一个子进度后,当前客商所具有的进度数目未有超过给他分配的能源的限量。
3)、子进度最先是自身与父进度分歧开来。进度描述符内的不在少数分子变量都要被清零或设为初阶值。那几个不是一连而来的历程描述符成员,重假诺统计音讯。Task_struc中的大大多据都依旧未被改换。
4)、子进度的状态棉被服装置为TASK_UNINT奔驰M级RUPTIBLE,以确认保障它不会被投入运作。
5)、copy_process()调用copy_flags()以更新task_struct
的flags成员。注解进度是或不是持有最棒客商权限的PF_SUPERPRAV4IV标识被清0.标注进度还没有曾调用exec(卡塔尔(قطر‎函数的PF_FORAV4KNOEXEC标记棉被服装置。
6)、调用alloc_pid(卡塔尔国为新进度分配八个得力的PID。
7)、依据传递给clone()的参数标识,copy_process(卡塔尔国拷贝或分享张开的公文、文件系统音信、时限信号管理函数、进程地址空间和命名空间等。在相符景况下,这个能源会被给定进度的富有线程分享;不然,那么些财富对各样进度是不一致的之所以被拷贝到这里。
8)、最后copy_process(State of Qatar做结束工作并回到叁个指向子进度的指针。
在回到do_fork()函数,如果copy_process(State of Qatar函数成功重回,新创制的子进度被唤醒并让其投运。内核有意接受子进程首先实施(即使连年想子进度先运营,可是不用总能如此)。因为日常子进度都会马上调用exec(卡塔尔函数,那样能够幸免写时拷贝(copy-on-write)的额外花销,纵然父进度首西施行的话,有希望会开头入地点空间写入。
Vfork(卡塔尔国除了不拷贝父进度的页表项外vfork(State of Qatar和fork(卡塔尔的效应相通。子进度作为父进度的叁个单身的线程在它的地点空间里运维,父进程被封堵,直到子进程退出或执行exec(卡塔尔(قطر‎。子进度不可能向地点空间写入(在未曾完结写时拷贝的linux版本中,这一优化是很有用的)。

do_fork() –> clone() –> fork() 、vfork() 、__clone()
—–>exec()

clone(State of Qatar函数的参数及其意思如下:

CLONE_FILES 父亲和儿子进程分享展开的文件
CLONE_FS 父亲和儿子进度分享文件系统新闻
CLONE_IDLETASK 将PID设置为0(只供idle进度使用)
CLONE_NEWNS 为子进度创设新的命名空间
CLONE_PARENT 钦点子进度与父进度具有同三个父进度
CLONE_PTRACE 继续调节和测量试验子进度
CLONE_SETTID 将TID写回到客商空间
CLONE_SETTLS 为子进度创建新的TLS
CLONE_SIGHAND 父亲和儿子进度分享时限信号管理函数以致被堵嘴的时限信号
CLONE_SYSVSEM 老爹和儿子进度共享System V SEM_UNDO语义
CLONE_THREAD 父亲和儿子进程放进相近的进程组
CLONE_VFOTucsonK 调用Vfork(State of Qatar,所以父进程希图睡觉等待子进度将其唤醒
CLONE_UNTRACED 防止追踪进度在子进程上强逼实行CLONE_PTRACE
CLONE_STOP 以TASK_SROPPED状态开头施行
CLONE_SETTLS 为子进度创建新的TLS(thread-local storage)
CLONE_CHILD_CLEARTID 淹没子进度的TID
CLONE_CHILD_SETTID 设置子进程的TID
CLONE_PARENT_SETTID 设置父进度的TID

CLONE_VM 父亲和儿子进度分享地址空间

二、GDB追踪fork(卡塔尔(قطر‎系统调用。

GDB 调节和测量试验的相关内容可以参照:GDB追踪内核运营 篇
这里不再占用过多篇幅赘述。上边先直接上海教室,在详细分析代码的运营进度。

图片 3

开发银行GDB后各自在sys_clone、do_fork、copy_process、copy_thread、ret_from_fork、syscall_exit等岗位设置好断点,目睹fork(State of Qatar函数的推行进度(运转景况与GDB追踪内核运维篇完全一致)

图片 4

能够观察,当我们在menuos中运营fork
命令的时候,内核会先调用clone,在sys_clone 断点处停下来了。

图片 5

在调用sys_clone()后,内核依据不一样的参数去调用do_fork(卡塔尔(قطر‎系统调用。步入do_fork(卡塔尔后就去又运行了copy_process().

图片 6

图片 7

在copy_process(卡塔尔(قطر‎ 中又运营了copy_thread(卡塔尔,然后跳转到了ret_from_fork
处运转一段汇编代码,再然后就跳到了syscall_exit(这是在arch/x86/kernel/entry_32.S中的二个标记,是奉行系统调用后用于退出根本空间的汇编制程序序。),

图片 8

可以见见,GDB追踪到syscall_exit 后就无法继续追踪了……………..

三、代码剖析(3.18.6本子的内核)

在3.18.6本子的内核 kernel/fork.c文件中:

#ifdef __ARCH_WANT_SYS_FORK
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
return do_fork(SIGCHLD, 0, 0, NULL, NULL);
#else
/* can not support in nommu mode */
return -EINVAL;
#endif
}
#endif
#ifdef __ARCH_WANT_SYS_VFORK
SYSCALL_DEFINE0(vfork)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0, 0, NULL,
NULL);

}
#endif
#ifdef __ARCH_WANT_SYS_CLONE
#ifdef CONFIG_CLONE_BACKWARDS
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long,
newsp, int __user *, parent_tidptr, int, tls_val,int __user
*, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long,
clone_flags, int __user *, parent_tidptr, int __user *,
child_tidptr, int, tls_val)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long,
newsp, int, stack_size, int __user *, parent_tidptr, int
__user *, child_tidptr, int, tls_val)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long,
newsp, int __user *, parent_tidptr, int __user *,
child_tidptr, int, tls_val)
#endif
{
return do_fork(clone_flags, newsp, 0, parent_tidptr,
child_tidptr);

}
#endif

从上述fork(卡塔尔、vfork(卡塔尔(قطر‎、clone(卡塔尔的定义能够看来,三者都以依据不相同的意况传递分化的参数直接调用了do_fork(State of Qatar函数,去掉了中档环节clone()。

进入do_fork 后:

图片 9

在do_fork中率先是对参数做了大气的参数检查,然后就施行就施行copy_process将父进度的PCB复制一份到子进程,作为子进程的PCB,再然后基于copy_process的归来值P判别进度PCB复制是或不是中标,假设成功就先唤醒子进度,让子进度就绪打算运营。

所以在do_fork中最重大的也正是copy_process(State of Qatar了,它产生了子进度PCB的复制与初阶化操作。上面就步入copy_process中看看内核是怎么着完成的:

图片 10

先精练单的讲一下,开采,copy_process中先导一部分的代码相似是参数的检查和依附分裂的参数履行一些连锁的操作,然后创造了三个职务,接着dup_task_struct(current卡塔尔国将最近历程的task_struct
复制了一份,并将新的task_struct地址作为指针再次来到!

图片 11

在dup_task_struct中为子进度创建了一个task_struct结构体、一个thread_info
构造体,并开展了简便易行的初叶化,可是那是子进程的task_struct依旧空的所以接下去的上游一部显然是要将父子进度task_struct中相像的有个别从父进度拷贝到子进程,然后不一样的有的再在子进度中展开开始化。

终极面包车型客车一有个别则是,现身各类错误后的退出口。

上面来看一下中间那部分:如何将父子进度雷同的、分化的局地界别开来。

图片 12

能够见见,内核先是将父进程的stask_struct中的内容不管三七三十六全都拷贝到子过程的stask_struct中了(那当中山大学部分的原委都是和父进度相似,唯有少部分依据参数的例外稍作修改),每三个模块拷贝甘休后都进展了对应的检查,看是还是不是拷贝成功,纵然战败就跳到对应的出口处推行苏醒操作。末了又举办了一个copy_thread(),

图片 13

在copy_thread这么些函数中做了两件极其首要的事情:1、正是把子进程的 eax
赋值为 0,
childregs->ax = 0,使得 fork 在子过程中回到
0;2、将子进程唤醒后进行的首先条指令定向到
ret_from_fork。所以这里能够见到子进程的进行从ret_from_fork开始。

借来继续看copy_process中的代码。拷贝完父进程中的内容后,将要对子进度展开“天性化”,

图片 14

从代码也得以看出,这里是对子进度中的别的成员进程开首化操作。然后就淡出了copy_process,回到了do_fork()中。

再跟着看一下do_fork(卡塔尔中“扫尾“工作是咋做的:

图片 15

前面植根据参数做一些变量的改革,后边多个操作特别主要,假设是经过fork(卡塔尔创立子进度,那么最终就直接将子进度唤醒,不过即便是因而vfork(卡塔尔(قطر‎来创制子进度,那么快要布告父进度必须等子进度运营停止技巧起头运营。

总结:

归纳:内核在成立七个新进度的时候,首要施行了弹指间职务:

1、父进程推行二个体系调用fork(卡塔尔(قطر‎或vfork(卡塔尔;但最终都以透过调用do_fork(卡塔尔国函数来操作,只然则fork(卡塔尔(قطر‎,vfork(卡塔尔传递给do_fork(卡塔尔国的参数差别。

2、在do_fork(卡塔尔国函数中,前面做参数检查,后边负担唤醒子进度(假使是vfork则让父进度等待),中间某个承受成立子进度和子进度的PCB的开始化,那几个职业都在copy_process()中完成。

3、在copy_process(卡塔尔(قطر‎中先是例行的参数检查和依照参数举办陈设;然后是调用大批量的copy_*****
函数将父进度task_struct中的内容拷贝到子进度的task_struct中,然后对于子进程与父进度之间不等的地点,在子进度中开头化或是清零。

4、实现子进度的创始和开首化后,将子进度唤醒,优先让子进度先运行,因为一旦让父进度先运维以来,由于linux的写时拷贝机制,父进度很大概会对数码进行写操作,当时就必要拷贝数据段和代码断的剧情了,但一旦施夷光行子进度来讲,子进度经常都会通过exec(卡塔尔(قطر‎转去试行另外的职分,直接将新任务的数量和代码拷过来就能够了,而无需像前边那么先把父进程的数码代码拷过来,然后拷新职责的代码的时候又将其遮住掉。

5、执行完copy_process(State of Qatar后就赶回了do_fork(卡塔尔国中,接着父进度回到system_call中执行syscall_exit:
后边的代码,而子进度则先从ret_from_fork:
处起头奉行,然后在再次回到system_call 中去实践syscall_exit:.

ENTRY(ret_from_fork)
CFI_STARTPROC
pushl_cfi %eax
call schedule_tail
GET_THREAD_INFO(%ebp)
popl_cfi %eax
pushl_cfi $0x0202 # Reset kernel eflags
popfl_cfi
jmp syscall_exit
CFI_ENDPROC
END(ret_from_fork)

6、父进度和子进度最终都以经过system_call
的说话从基本空间回到客商空间,回到客商空间后,由于fork(卡塔尔函数对父亲和儿子进程的重返值差异,所以传闻重回值判定出回来的是父进程依旧子进程,然后分别实行不一的操作。

图片 16

新进程的始建 一、背景知识:
1、进度与程序的涉嫌:
进度是动态的,而前后相继是静态的;从布局上看,每个进度的实业都以由代码断和…

子进度是从哪开端实行的?

当试行到

p->thread.ip = (unsigned long) ret_from_fork;
//调节到子进度时的首先条指令地址。

时,即子进程取得CPU时它从这几个职位上马实施的。

而进行那条语句

*childregs = *current_pt_regs(卡塔尔; //复制内核货仓

管教了新进度的施行起源和根本货仓的一致性。
日常来讲图gdb追踪所示。

图片 17

关于do_fork和_do_frok


The commit 3033f14ab78c32687 (“clone: support passing tls argument via
C
rather than pt_regs magic”) introduced _do_fork() that allowed to
pass
@tls parameter.

参见

linux2.5.32以后, 添加了TLS(Thread Local Storage)机制,
clone的标识CLONE_SETTLS采纳一个参数来设置线程的本土存款和储蓄区。sys_clone也因而扩张了二个int参数来传播相应的点tls_val。sys_clone通过do_fork来调用copy_process完毕经过的复制,它调用特定的copy_thread和copy_thread把相应的系统调用参数从pt_regs存放器列表中提抽取来,可是会招致意外的事态。

only one code path into copy_thread can pass the CLONE_SETTLS flag,
and
that code path comes from sys_clone with its architecture-specific
argument-passing order.

前面大家说了,
在落到实处函数调用的时候,小编iosys_clone等将一定体系布局的参数从寄放器中领抽出来,
然后达到do_fork那步的时候已经应该是系统布局无关了,
可是大家sys_clone需求设置的CLONE_SETTLS的tls仍然为个依据与系统布局的参数,
这里就能够现身难题。

因此linux-4.2之后分选引进二个新的CONFIG_HAVE_COPY_THREAD_TLS,和二个新的COPY_THREAD_TLS接受TLS参数为
额外的长整型(系统调用参数大小)的对立。改换sys_clone的TLS参数unsigned
long,并传递到copy_thread_tls。

/* http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L2646  */
extern long _do_fork(unsigned long, unsigned long, unsigned long, int __user *, int __user *, unsigned long);
extern long do_fork(unsigned long, unsigned long, unsigned long, int __user *, int __user *);


/* linux2.5.32以后, 添加了TLS(Thread Local Storage)机制, 
    在最新的linux-4.2中添加了对CLONE_SETTLS 的支持 
    底层的_do_fork实现了对其的支持, 
    dansh*/
#ifndef CONFIG_HAVE_COPY_THREAD_TLS
/* For compatibility with architectures that call do_fork directly rather than
 * using the syscall entry points below. */
long do_fork(unsigned long clone_flags,
              unsigned long stack_start,
              unsigned long stack_size,
              int __user *parent_tidptr,
              int __user *child_tidptr)
{
        return _do_fork(clone_flags, stack_start, stack_size,
                        parent_tidptr, child_tidptr, 0);
}
#endif

 

我们会发觉,新本子的类别中clone的TLS设置标志会通过TLS参数字传送递,
因而_do_fork取代了老版本的do_fork。

老版本的do_fork独有在如下意况才会定义

  • 唯有当系统不援救通过TLS参数通过参数字传送递而是使用pt_regs贮存器列表传递时

  • 未定义CONFIG_HAVE_COPY_THREAD_TLS宏

参数 描述
clone_flags 与clone()参数flags相同, 用来控制进程复制过的一些属性信息, 描述你需要从父进程继承那些资源。该标志位的4个字节分为两部分。最低的一个字节为子进程结束时发送给父进程的信号代码,通常为SIGCHLD;剩余的三个字节则是各种clone标志的组合(本文所涉及的标志含义详见下表),也就是若干个标志之间的或运算。通过clone标志可以有选择的对父进程的资源进行复制;
stack_start 与clone()参数stack_start相同, 子进程用户态堆栈的地址
regs 是一个指向了寄存器集合的指针, 其中以原始形式, 保存了调用的参数, 该参数使用的数据类型是特定体系结构的struct pt_regs,其中按照系统调用执行时寄存器在内核栈上的存储顺序, 保存了所有的寄存器, 即指向内核态堆栈通用寄存器值的指针,通用寄存器的值是在从用户态切换到内核态时被保存到内核态堆栈中的(指向pt_regs结构体的指针。当系统发生系统调用,即用户进程从用户态切换到内核态时,该结构体保存通用寄存器中的值,并被存放于内核态的堆栈中)
stack_size 用户状态下栈的大小, 该参数通常是不必要的, 总被设置为0
parent_tidptr 与clone的ptid参数相同, 父进程在用户态下pid的地址,该参数在CLONE_PARENT_SETTID标志被设定时有意义
child_tidptr 与clone的ctid参数相同, 子进程在用户太下pid的地址,该参数在CLONE_CHILD_SETTID标志被设定时有意义

其中clone_flags如下表所示

图片 18

一、关于PCB

对于一个进度来讲,PCB就象是是他的记账先生,当叁个经过被成立时PCB就被分配,然后有关进度的持有消息就全都存款和储蓄在PCB中,比如,张开的文件,页表基址存放器,进程号等等。在linux中PCB是用布局task_struct来代表的,大家率先来看一下task_struct的组成。

代码坐落于linux/include/linux/Sched.h

struct task_struct {

    long state; //表示进程的状态,-1表示不可执行,0表示可执行,>0表示停止
    long counter;/* 运行时间片,以jiffs递减计数 */
    long priority; /* 运行优先数,开始时,counter = priority,值越大,表示优先数越高,等待时间越长. */
    long signal;/* 信号.是一组位图,每一个bit代表一种信号. */
    struct sigaction sigaction[32]; /* 信号响应的数据结构, 对应信号要执行的操作和标志信息 */
    long blocked;   /* 进程信号屏蔽码(对应信号位图) */
/* various fields */
    int exit_code; /* 任务执行停止的退出码,其父进程会取 */
    unsigned long start_code,end_code,end_data,brk,start_stack;/* start_code代码段地址,end_code代码长度(byte),
end_data代码长度+数据长度(byte),brk总长度(byte),start_stack堆栈段地址 */
    long pid,father,pgrp,session,leader;/* 进程号,父进程号 ,父进程组号,会话号,会话头(发起者)*/
    unsigned short uid,euid,suid;/* 用户id 号,有效用户 id 号,保存用户 id 号*/
    unsigned short gid,egid,sgid;/* 组标记号 (组id),有效组 id,保存的组id */
    long alarm;/* 报警定时值 (jiffs数) */
    long utime,stime,cutime,cstime,start_time;/* 用户态运行时间 (jiffs数),
系统态运行时间 (jiffs数),子进程用户态运行时间,子进程系统态运行时间,进程开始运行时刻 */
    unsigned short used_math;/* 是否使用了协处理器 */
/* file system info */
    int tty;        /* 进程使用tty的子设备号. -1表示设有使用 */
    unsigned short umask; /* 文件创建属性屏蔽位 */
    struct m_inode * pwd; /* 当前工作目录 i节点结构 */
    struct m_inode * root; /* 根目录i节点结构 */
    struct m_inode * executable;/* 执行文件i节点结构 */
    unsigned long close_on_exec; /* 执行时关闭文件句柄位图标志. */
    struct file * filp[NR_OPEN];
/* 文件结构指针表,最多32项. 表项号即是文件描述符的值 */
    struct desc_struct ldt[3];
/* 任务局部描述符表.0-空,1-cs段,2-Ds和Ss段 */
    struct tss_struct tss; /* 进程的任务状态段信息结构 */
};

新历程施行起源对应的旅馆状态深入分析

gdb调节和测验分析

图片 19

copy_process流程


  1. 调用 dup_task_struct 复制当前的 task_struct

  2. 检查进度数是或不是当先节制

  3. 早先化自旋锁、挂起功率信号、CPU 放大计时器等

  4. 调用 sched_fork 初阶化进度数据构造,并把进度意况设置为
    TASK_RUNNING

  5. 复制全部进程信息,包罗文件系统、功率信号管理函数、非确定性信号、内部存款和储蓄器管理等

  6. 调用 copy_thread_tls 开首化子进度内核栈

  7. 为新历程分配并安装新的 pid

相比较之下,大家从《深远linux内核构造》中找到了开始的一段时代的do_fork流程图,基本一致,能够用来参谋学习和对照

图片 20

尤为重要的区分其实就是最终的copy_thread订正成为copy_thread_tls

/*
 * This creates a new process as a copy of the old one,
 * but does not actually start it yet.
 *
 * It copies the registers, and all the appropriate
 * parts of the process environment (as per the clone
 * flags). The actual kick-off is left to the caller.
 */
static struct task_struct *copy_process(unsigned long clone_flags,
                    unsigned long stack_start,
                    unsigned long stack_size,
                    int __user *child_tidptr,
                    struct pid *pid,
                    int trace,
                    unsigned long tls)
{
    int retval;
    struct task_struct *p;

    retval = security_task_create(clone_flags);
    if (retval)
        goto fork_out;
    //  复制当前的 task_struct
    retval = -ENOMEM;
    p = dup_task_struct(current);
    if (!p)
        goto fork_out;

    ftrace_graph_init_task(p);

    //初始化互斥变量
    rt_mutex_init_task(p);

#ifdef CONFIG_PROVE_LOCKING
    DEBUG_LOCKS_WARN_ON(!p->hardirqs_enabled);
    DEBUG_LOCKS_WARN_ON(!p->softirqs_enabled);
#endif

    //检查进程数是否超过限制,由操作系统定义
    retval = -EAGAIN;
    if (atomic_read(&p->real_cred->user->processes) >=
            task_rlimit(p, RLIMIT_NPROC)) {
        if (p->real_cred->user != INIT_USER &&
            !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
            goto bad_fork_free;
    }
    current->flags &= ~PF_NPROC_EXCEEDED;

    retval = copy_creds(p, clone_flags);
    if (retval < 0)
        goto bad_fork_free;

    /*
     * If multiple threads are within copy_process(), then this check
     * triggers too late. This doesn't hurt, the check is only there
     * to stop root fork bombs.
     */
    //检查进程数是否超过 max_threads 由内存大小决定
    retval = -EAGAIN;
    if (nr_threads >= max_threads)
        goto bad_fork_cleanup_count;

    delayacct_tsk_init(p);  /* Must remain after dup_task_struct() */
    p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER);
    p->flags |= PF_FORKNOEXEC;
    INIT_LIST_HEAD(&p->children);
    INIT_LIST_HEAD(&p->sibling);
    rcu_copy_process(p);
    p->vfork_done = NULL;

    //  初始化自旋锁
    spin_lock_init(&p->alloc_lock);
    //  初始化挂起信号
    init_sigpending(&p->pending);

    //  初始化 CPU 定时器
    posix_cpu_timers_init(p);
    //  ......

    /* Perform scheduler related setup. Assign this task to a CPU. 
        初始化进程数据结构,并把进程状态设置为 TASK_RUNNING
    */
    retval = sched_fork(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_policy;
    retval = perf_event_init_task(p);

    /*  复制所有进程信息,包括文件系统、信号处理函数、信号、内存管理等
       形式类似于copy_xxx的形式   */
    if (retval)
        goto bad_fork_cleanup_policy;
    retval = audit_alloc(p);
    if (retval)
        goto bad_fork_cleanup_perf;
    /* copy all the process information */
    shm_init_task(p);
    retval = copy_semundo(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_audit;
    retval = copy_files(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_semundo;
    retval = copy_fs(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_files;
    retval = copy_sighand(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_fs;
    retval = copy_signal(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_sighand;
    retval = copy_mm(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_signal;
    retval = copy_namespaces(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_mm;
    retval = copy_io(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_namespaces;
    /*    初始化子进程内核栈
        linux-4.2新增处理TLS
        之前版本是   retval = copy_thread(clone_flags, stack_start, stack_size, p);
        */
    retval = copy_thread_tls(clone_flags, stack_start, stack_size, p, tls);
    if (retval)
        goto bad_fork_cleanup_io;

    /*  为新进程分配新的pid  */
    if (pid != &init_struct_pid) {
        pid = alloc_pid(p->nsproxy->pid_ns_for_children);
        if (IS_ERR(pid)) {
            retval = PTR_ERR(pid);
            goto bad_fork_cleanup_io;
        }
    }

    /*  设置子进程的pid  */
    /* ok, now we should be set up.. */
    p->pid = pid_nr(pid);
    if (clone_flags & CLONE_THREAD) {
        p->exit_signal = -1;
        p->group_leader = current->group_leader;
        p->tgid = current->tgid;
    } else {
        if (clone_flags & CLONE_PARENT)
            p->exit_signal = current->group_leader->exit_signal;
        else
            p->exit_signal = (clone_flags & CSIGNAL);
        p->group_leader = p;
        p->tgid = p->pid;
    }

    p->nr_dirtied = 0;
    p->nr_dirtied_pause = 128 >> (PAGE_SHIFT - 10);
    p->dirty_paused_when = 0;

    p->pdeath_signal = 0;
    INIT_LIST_HEAD(&p->thread_group);
    p->task_works = NULL;

    /*
     * Make it visible to the rest of the system, but dont wake it up yet.
     * Need tasklist lock for parent etc handling!
     */
    write_lock_irq(&tasklist_lock);

    /*  调用fork的进程为其父进程  */
    /* CLONE_PARENT re-uses the old parent */
    if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
        p->real_parent = current->real_parent;
        p->parent_exec_id = current->parent_exec_id;
    } else {
        p->real_parent = current;
        p->parent_exec_id = current->self_exec_id;
    }

    spin_lock(&current->sighand->siglock);

    // ......

    return p;
}

 

三、成立进程总括

能够将上面繁杂的创办进度总括为一下的几步:

1、调用fork()函数引发0×80抛锚
2、调用sys_fork
3、通过find_empty_process为新进度分配叁个进度号
4、通过copy_process函数使子进程复制父进度的财富,并开展一些本性化设置后,再次回到进程号。

总结

Linux创制一个新历程的历程:系统经过sys_fork、sys_clone、sys_vfork多个系统调用中的放肆二个创办新进程,那多个种类调用都调用do_fork()函数,由do_fork(卡塔尔(قطر‎函数调用别的函数复制父过程的PCB,创造新进程的内核栈,然后根据创制时的参数修正新进程PCB中的消息将其与父进程区分开来,为子进程分配新的PID号,最后将其归来客户态。

sys_vfork的实现


前期完结

架构 实现
arm arch/arm/kernel/sys_arm.c, line 254
i386 arch/i386/kernel/process.c, line 737
x86_64 arch/x86_64/kernel/process.c, line 728
asmlinkage long sys_vfork(struct pt_regs regs)
{
    return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.rsp, &regs, 0);
}

 

新版本

#ifdef __ARCH_WANT_SYS_VFORK
SYSCALL_DEFINE0(vfork)
{
        return _do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
                        0, NULL, NULL, 0);
}
#endif

 

能够见到sys_vfork的落成与sys_fork只是微微不相同,
前面一个使用了附加的标识CLONE_VFORK | CLONE_VM

admin

相关文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注