以Linux-v0.11为例。
目录结构
函数声明
在include/unistd.h
文件中,声明fork()函数为:int fork(void)
。说明fork()为无参函数,返回值为int型。在新版linux中声明方式为pid_t fork(void)
。但在include/sys/types.h
中,使用了typedef定义了pid_t:typedef int pid_t
,所以本质上是一样的。
函数实现
实际上,在Linux-v0.11中并没有关于fork()的函数实现,Linus采用的是一种非常巧妙的函数实现方法。在include/unistd.h
中,Linus使用了宏定义定义了如下一段代码:
1 |
|
这里使用了两个标识符:type和name,并用标识符编写了一段通用的函数定义模板。
然后在/init/main.c
中,即需要调用fork()函数作初始化的地方,Linus使用了static inline _syscall0(int,fork)
将type和name传入,完成了fork()函数的函数实现。
所以fork()的函数实现应如下:
1 | int fork(void) |
函数调用过程(个人理解)
1 | __asm__ volatile ( |
这一段为GCC的内联汇编语句,语法格式为__asm__ __volatile__("Instruction List" : Output : Input : Clobber/Modify)
。
其中,__asm__
为GCC关键字asm的宏定义,用来声明一个内联汇编表达式。__volatile__
为GCC关键字volatile的宏定义,向GCC声明不允许对该内联汇编优化。Instruction List
为汇编指令序列,可以为空
。Output
指定当前内联汇编语句的输出。Input
指定当前内联汇编语句的输入。Clobber/Modify
向GCC声明当前内联汇编语句可能会对某些寄存器或者内存进行修改,希望GCC在编译时能将这一点考虑进去。一般发生在一个寄存器出现在Instruction List但却不在Output/Input表达式中出现的,仅做当前内联汇编临时使用寄存器的情况。
此处int $0x80
为内部中断,传入"=a" (__res)
用于存放返回值,"0" (__NR_fork)
为对应的中断功能号。"=a"
是GCC内联汇编的约束字,"="号为Output表达式特有,指定使用的寄存器为Write-Only,"a"表示使用eax寄存器存放Output返回值,即fork()函数的返回值会先存放到eax中,再由寄存器eax将值更新到__res
中。"0"
指定内联汇编表达式的第一个操作数,即Output语句的eax,即Input表达式也使用eax寄存器,只是与Output相反,先由__NR_fork
将值更新到eax中,再由eax传给int 0x80
中断处理函数。此处的"0"
可以用"a"
代替。具体参见GCC内联汇编手册:GCC-Inline-Assembly-HOWTO (ibiblio.org)中5.2 Operands这一节的内容。
而在/kernel/sched.c
中,最后一段内联汇编:
1 | __asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl"); |
其中的最后一句set_system_gate(0x80,&system_call)
将0x80号中断与系统调用关联在一起,所以fork()函数此处调用的0x80号中断其实是系统调用。
在/kernel/system_call.s
中,对系统调用进行了处理。调用部分:
1 | .align 2 |
其中nr_system_calls在此文件开头处定义为nr_system_calls = 72
,为系统调用数量。cmpl $nr_system_calls-1,%eax
即为比较eax中存放的中断功能号(系统调用号)与71(0~71共72个系统调用)的关系,若中断功能号大于71则ja跳转到bad_sys_call进行错误中断处理。若中断功能号合法,则保存现场准备进入系统调用程序。
call _sys_call_table(,%eax,4)
这一句调用了在/include/linux/sys.h
中定义的函数数组,%eax为传入的系统调用号,4表示4字节,因为系统调用表数组为int型数组,一个int型数据需要占用4字节。
查表获得对应的系统调用处理函数入口地址,然后查看当前进程状态,并运行进程调度函数reschedule准备转入系统调用处理函数。reschedule同样在/kernel/system_call.s
中:
1 | .align 2 |
第一句将同在system_call.s中的函数ret_from_sys_call的入口地址入栈($ret_from_sys_call表示取地址,因为这里的ret_from_sys_call是一个标号,而前面的$nr_system_calls-1表示的是取立即数,因为nr_system_calls可以认为是立即数72的宏定义,而在AT&T语法中取立即数也需要使用$符号),然后第二句jmp跳转到进程调度函数schedule处执行,该函数在/kernel/sched.c
中定义如下:
1 | void schedule(void) |
进程调度这段还没仔细看。总之调度完成后返回ret_from_sys_call函数继续执行:
1 | ret_from_sys_call: |
这段也还没仔细看,不过这段完成之后应该就结束系统调用了,因为最后是个iret中断返回。
而调用sys_fork()中断处理函数应该是在schedule进程调度中完成的,其函数实现在/kernel/system_call.s
中,代码如下:
1 | .align 2 |
这里调用的find_empty_process和copy_process均为C函数,在/kernel/fork.c
中定义:
1 | int find_empty_process(void) |
此处可以看到copy_process的函数操作使用了大量的指针,对原有进程进行了复制。同时需要注意的是由于此函数需要传递的参数过多,有部分参数是通过栈传递进来的,这就是为什么在call之前有四句pushl。
至于为何fork()函数能给子进程和父进程分别返回一个返回值目前还没有看懂,后续看懂了再进行补充。
发布时间: 2021-04-27
最后更新: 2024-05-12
本文标题: Linux系统调用:fork()
本文链接: https://cloudflare.luhawxem.com/2021/04/27/Linux-syscall-fork/
版权声明: 本作品采用 CC BY-NC-SA 4.0 许可协议进行许可。转载请注明出处!