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

    技术2024-06-27  173

    最近几天刚开始在读代码,读的挺爽的,不过碰到了很多问题,慢慢来吧。。 有一个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#includeint 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;
    最新回复(0)