红联Linux门户
Linux帮助

Linux0.11中的fork实现和一些注意事项

发布时间:2015-02-18 09:45:33来源:linux网站作者:linux人

Linux0.11中有一个fork的系统调用一直没弄明白,自己添加一些自己的想法。下面是思路和提问。

内核是linux0.11版本,里面的fork()用于创建子进程。
但我现在在找这个函数的具体定义时遇到了一些困难。
先把我的查找过程说下:


1、init里的main.c中有static inline _syscall0 (int, fork);


2、在unistd.h中找到_syscall0是个宏,定义如下

Assembly code
#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \                 // 调用系统中断0x80。
: "=a" (__res) \                            // 返回值??eax(__res)。
: "0" (__NR_##name)); \                     // 输入为系统中断调用号__NR_name。
if (__res >= 0) \                               //如果返回值>=0,则直接返回该值。
return (type) __res; \                      // 否则置出错号,并返回-1。
errno = -__res; \
return -1; \
}
进行参数替换
感觉是int fork(void){....};
具体实现主要是:
__asm__ volatile ( "int $0x80" \     // 调用系统中断0x80。
:"=a" (__res) \                      // 返回值??eax(__res)。
:"" (__NR_##name)); 其中的__NR_##name是2,用来在一个指针函数的数组中作为索引号用的
这个数组定义如下:

C/C++ code
typedef int (*fn_ptr) ();

在sys.h中 fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read, sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link, sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod, sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount, sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm, sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access, sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir, sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid, sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys, sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit, sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid, sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask, sys_setreuid, sys_setregid };
而__NR_##name的值为2感觉正好对应着上面那个数组里的sys_call_table[2](也就是sys_fork)
感觉有点眉目了


3、继续解释这个指令

Assembly code
__asm__ volatile ( "int $0x80" \ // 调用系统中断0x80。

:"=a" (__res) \ // 返回值eax (__res)。

:"" (__NR_ ##name));
感觉主要是调用系统中断0x80


4、在汇编代码system_call.s中找到该中断的中断处理函数
大概如下
#### int 0x80 --linux 系统调用入口点(调用中断int 0x80,eax 中是调用号)。

Assembly code
.align 2
_system_call:
cmpl $nr_system_calls-1,%eax # 调用号如果超出范围的话就在eax 中置-1 并退出。
ja bad_sys_call
5.5 system_call.s 程序
push %ds # 保存原段寄存器值。
push %es
push %fs

pushl %edx # ebx,ecx,edx 中放着系统调用相应的C 语言函数的调用参数。

pushl %ecx # push %ebx,%ecx,%edx as parameters

pushl %ebx # to the system call
movl $0x10,%edx # set up ds,es to kernel space
mov %dx,%ds # ds,es 指向内核数据段(全局描述符表中数据段描述符)。
mov %dx,%es

movl $0x17,%edx # fs points to local data space
mov %dx,%fs # fs 指向局部数据段(局部描述符表中数据段描述符)。

# 下面这句操作数的含义是:调用地址 = _sys_call_table + %eax * 4。参见列表后的说明。
# 对应的C 程序中的sys_call_table 在include/linux/sys.h 中,其中定义了一个包括72 个
# 系统调用C 处理函数的地址数组表。
call _sys_call_table(,%eax,4)
如果上面的eax放的是__NR_##name,即2
那么call _sys_call_table(,%eax,4)
相当于call sys_fork()
在该文件的下面一点找到该函数的定义
#### sys_fork()调用,用于创建子进程,是system_call 功能2。原形在include/linux/sys.h 中。
# 首先调用C 函数find_empty_process(),取得一个进程号pid。若返回负数则说明目前任务数组
# 已满。然后调用copy_process()复制进程。

Assembly code
.align 2
_sys_fork:
call _find_empty_process # 调用find_empty_process()(kernel/fork.c,135)。

testl %eax,%eax
js 1f push %gs

pushl %esi

pushl %edi

pushl %ebp

pushl %eax
call _copy_process       # 调用C 函数copy_process()(kernel/fork.c,68)。
addl $20,%esp            # 丢弃这里所有压栈内容。
1: ret
里面的_find_empty_process和copy_process()就是fork的主要子函数了.


问题1:请问我这个思路是不是对的。
问题2:汇编调用C函数为什么名字不一样啊
汇编里是_find_empty_process 而在fork.c里该函数名字是find_empty_process()
汇编这样调用能找到这个函数么????
问题3:__asm__ volatile ( "int $0x80" \         // 调用系统中断0x80。
:"=a" (__res) \                                 // 返回值??eax(__res)。
:"" (__NR_##name));这个内嵌汇编里的__NR_##name值是怎么传给eax的?以让后面的call _sys_call_table(,%eax,4)正确调用sysfork;
希望能给我详细讲解这条指令:__asm__ volatile (仅这条)调用中断int $0x80的具体操作
问题1:
差不多,但是有一個地方需要強調一下。static inline _syscall0 (int, fork),這個實際上隱含指出了系統調用的參數個數,注意這裡是以0結尾的,也就是說這個系統調用不需要傳遞參數。你應該還能從代碼中看到 _syscall1,_syscall2這樣的定義。
问题2:
gcc的C編譯器compile的時候,會在函數名字前面加上一個下劃綫,所以在彙編裏面調用C的函數的時候,要手動的加上這個下劃綫。注意:這裡說的是C編譯器,不包括C++的,C++的編譯器和C的有很多的不同,所以才會有extern "C"的存在。
问题3:
开头你自己也说了NR_name 是定义的好的值。具体在 <>unistd.h>头文件里。怎么传给eax的?:"" (__NR_
##name) 这条语句就是传给eax的意思。引号中省略表示用和输出一样的寄存器,也就是输出中的a(eax)。gcc嵌入汇编的格式你应该了解吧。int $0x80就是调用系统中断了,设置系统中断的操作在sched.c 最后一行 set_system_gate(0x80,&system_call); set_system_gate是一个嵌入式宏汇编语句,设置中断描述符表表项。具体实现在system.h.这样调用0x80系统中断后,执行的是 system_call  eax中存放的是定义好的NR_name的值,以后处理过程你都说了。


fork函数的实现(返回2次原因)
在Linux中,有一个特殊的函数fork()。这个函数会向父进程返回子进程的进程号PID,而向子进程返回0。有没有想过一个函数怎么可能有两个不同的返回值呢?
QUOTE:
#include
#include
int main(int argc, char **argv) {
if (fork() == 0) {
printf("I am the child process.\n");
} else {
printf("I am the parent process.\n");
}
}


在Linux0.11中,每个进程都有一个进程控制块结构task_struct。系统支持最多64个进程,定义在全局数组task中。
QUOTE:
struct task_struct * task[NR_TASKS] = {&(init_task.task), };

其中进程0为初始进程,其它所有的进程都是通过fork产生的。用户态的fork函数最终调用系统调用sys_fork()。sys_fork()系统调用分成2步完成,第一步调用函数find_empty_process(),在task[]数组中找一项空闲项;第二步调用copy_process()函数,复制进程。

对所有fork()调用产生的进程,通过递增并循环的方式为其分配进程号。有一个全局变量last_pid用来记录上次使用的进程号:

long last_pid=0;

在find_empty_process中,不断递增last_pid,寻找第一个未被其它进程使用的进程号作为新进程的进程号。如果递增后的值超出正数表示范围,则重新从1开始。

进程控制块中还保存有进程的任务状态段数据结构tss,用于存储处理器管理进程的所有信息。也就是说,在任务切换过程中,首先将处理器中各寄存器的当前值被自动保存当前进程的tss中;然后,下一进程的tss被加载并从中提取出各个值送到处理器的寄存器中。由此可见,通过在tss中保存任务现场各寄存器状态的完整映象,实现任务的切换。

struct tss_struct tss;

因此,一旦在task[]数组中找到空闲项和进程号,我们就可以为该进程的进程控制块结构申请一个页面的内存。这个工作是在copy_process() 函数中完成的。当然copy_process()函数的最主要的任务是为子进程复制父进程信息,并设置子进程的任务状态段,其中最关键的两步是:

1. 把子进程tss中的eip设置为父进程系统调用返回地址,这样当子进程被调度程序选中后,将从父进程的fork()返回地址处开始执行。

p->tss.eip = eip;

2. 把子进程tss中的eax设置为0,而eax是存放函数返回值的地方,这样子进程中返回的是0。注意子进程并没有执行fork()函数,子进程的系统堆栈没有进行过操作,当然不会有像父进程那样的fork函数调用。但是当子进程开始运行时,就好像它从fork中返回。

p->tss.eax = 0;