Linux0.11系统调用之fork流程解析

Linux0.11系统调用之fork流程解析

  • 前言
  • fork功能介绍
  • fork本质
  • fork系统调用流程
  • 总结

前言

本文是基于Linux0.11源码来叙述该功能。本文就不贴Linux0.11的源码了,可以参考赵炯博士的书籍,下文的叙述中,父进程等于当前进程。

fork功能介绍

fork函数是用于进程的创建,是linux编程中常用的一个系统调用类函数。fork会复制当前进程的几乎所有信息,包括可访问的内存资源。

fork本质

在main.c文件中fork被这样定义static inline _syscall0(int,fork),其中_syscall0()是一个宏,将其展开后如下:

static inline int fork(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
	: "=a" (__res) \
	: "0" (__NR_fork)); \
if (__res >= 0) \
	return (int) __res; \
errno = -__res; \
return -1; \
}

可以看到fork本质是系统调用int 0x80(类似于软中断的触发),传入的参数为__NR_fork由eax寄存器传入。

fork系统调用流程

中断触发后,eax携入的参数是__NR_fork是系统调用函数表sys_call_table[]的索引项,根据索引项找到sys_fork()函数并调用。

sys_fork()函数调用到了fork.c文件当中的find_empty_process()copy_process()函数。

find_empty_process()

  1. 找到一个与已有进程不重复的pid号,并存入全局变量last_pid
  2. 找到struct task_struct * task[NR_TASKS]中的一项空项(用于存储新进程的任务结构体指针),并返回对应的索引号。

copy_process()

  1. 该函数传入了很多参数,这些参数以如下的结构存在于堆栈当中:
    Linux0.11系统调用之fork流程解析_第1张图片
    黄色部分: 中断(int 0x80)自动push入堆栈的数据,因为此刻堆栈段、堆栈指针及代码段需要切到内核态,EFLAGS可能在中断中被改变需要保存起来,EIP为中断返回地址,这些入栈操作寄存器(CS,SS,ESP)的更替都是中断产生时硬件自动完成。
    蓝色部分_system_call_sys_fork中由软件push入堆栈的数据,除了其中的EIP是call _sys_call_table(,%eax,4)的返回地址(也即下一条指令的地址)。
    参数本质: 这些传入的参数其实都是保存了用户态下的寄存器的原值,目的是为了复制当前进程的现场。

  2. 通过get_free_page()函数获取一个page的空间(4KB),返回该空间首地址指针,保存在struct task_struct * task[NR]之中(NR是在find_empty_process的返回值),该page用于新任务的堆栈及存放任务结构体数据信息,如下图所示:
    Linux0.11系统调用之fork流程解析_第2张图片

可以看到,4KB的空间地址的首部用于存储task_struct结构体用于记录新进程的进程信息,尾端开始用于新进程的内核堆栈(堆栈自上而下,一般不会覆盖掉任务结构体),指针esp0指向尾端(用户陷入内核态时会将tss.esp0tss.ss0与用户堆栈置换)。
有人看到这里肯定会问,那么新进程的用户堆栈在哪?用户堆栈就如同代码数据一般被copy,所以可以看到函数中tss.esp被赋值当前进程的用户寄存器(即上上图黄色部分的中断时推入的内核堆栈用户堆栈ESP),例如main函数(位于main.c文件)中的fork,所复制的就是全局结构体user_stack(位于sched.c文件),子进程会复制该数据(写时复制)作为用户堆栈。
3. 填充task_struct结构体,与当前进程不同的是,task_struct结构体中的eax寄存器值填为0(这就是为什么子进程返回值为0的原因),tss中的eip赋值EIP中断返回地址(int 0x80返回地址),因此任务一旦切换到子进程,便会立马跳转到用户调用的fork()函数中的int 0x80后一条代码执行,此时的返回值是eax为0。
4. copy_mem()函数做了2件事:

  • 其一,复制了当前进程的页表项,也就是几乎保持一致,唯独不同的是页表项的访问权限被修改为了只读,仅仅是复制页表项哦,页的内容并没有被复制(因为没有写权限所以子进程会触发页异常,在页异常中复制页的内容,这样可以节省很多CPU资源,避免不必要的copy),fork()出来的子进程与父进程拥有不同的逻辑地址与线性地址(页表),但最终映射到的物理内存是一致的!复制的长度由父进程的LDT的限长决定,页表在此也不做详述,可以查阅相关资料;
  • 其二,在Linux0.11中,每个进程占据的逻辑地址为64M,最多可以有NR_TASKS(64)个进程,正好平分32位地址线可以访问的4G空间(64*64M=4G),由此根据传入的nr(全局变量task的索引值),设定子进程的段基址为64M*nr,写入子进程的LDT局部描述符之中,并设定可访问长度与当前进程一致。
  1. 在全局描述符表GDT中加入子进程的LDT与TSS描述符的指针,实际指向为&task[nr]->ldt&task[nr]->tss,可以看到在task_struct结构体中几乎存有关于进程的所有信息。
  2. 设置子进程的stateTASK_RUNNING,因此一旦发生schedule系统调度,就会调用到子进程。
  3. 最终父进程的fork()会返回last_pid(子进程的pid号),而子进程的fork()如上所述,会返回0。last_pid永远不会为0,因为0号进程是系统的最初进程,他永远不会消亡,0号进程永远占据着pid=0,所以我们可以断言fork()不为0就是父进程。

总结

fork()最核心的函数就是copy_process(),几乎复制了当前进程所有信息,寄存器、页表项、LDT表、GDT全局表中插入子进程的LDT与TSS,将所有信息存于task_struct之中,而子进程的task_struct与堆栈处于同一个页表的两端。

你可能感兴趣的:(Linux内核,Old,Linux,操作系统,linux)