关于fork( )函数父子进程返回值的问题

时间:2022-07-24 15:18:11

fork()是linux的系统调用函数sys_fork()的提供给用户的接口函数,fork()函数会实现对中断int 0x80的调用过程并把调用结果返回给用户程序。

fork()的函数定义是在init/main.c中(这一点我感到奇怪,因为大多数系统调用的接口函数都会单独封装成一个.c文件,然后在里面进行嵌入汇编展开执行int 0x80中断从而执行相应的系统调用,如/lib/close.c中:

 #define __LIBRARY__
#include <unistd.h> _syscall1(int,close,int,fd)

但fork()函数确实在mai.c中进行嵌入汇编展开定义的,呃,可能是我目前还没有完全理解这一部分,但这就是我目前的认识)

以下是init/main.c中fork()函数的嵌入汇编定义:

 static inline _syscall0(int,fork)

其中_syscall0()是include/linux/sys/unistd.h中的内嵌宏代码,它以嵌入汇编的形式调用linux的系统调用中断int 0x80。

 #define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "" (__NR_##name)); \
if (__res >= ) \
return (type) __res; \
errno = -__res; \
return -; \
}

对其进行宏展开,即得到fork()函数的代码:

 int fork(void)
{
long __res;
__asm__ volatile ("int $0x80"
: "=a" (__res) //eax保存的是int 0x80中断调用的返回值
: "" (__NR_##fork)); //同时eax也是int 0x80中断调用的系统调用功能号
if (__res >= )
return (type) __res; //返回int 0x80的返回值作为fork()函数的返回值
errno = -__res;
return -;
}

理解这个函数的关键,就在于理解系统调用中断int 0x80。

在main.c进行初始化时,设置好了int 0x80的系统调用中断门。

 void main(void)
{
......
sched_init(); //在sched_init()中设置了系统调用中断int 0x80的中断门
......
move_to_user_mode();
if (!fork()) { /* we count on this going ok */
init();
}
for(;;) pause();
}

sched_init()定义在kernel/sched.c中:

 void sched_init(void)
{
......
set_system_gate(0x80,&system_call); //在IDT中设置系统调用中断int 0x80的描述符
}

其中,set_system_gate(0x80,&system_call)的宏定义在文件include/asm/system.h中:

 #define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
"movw %0,%%dx\n\t" \
"movl %%eax,%1\n\t" \
"movl %%edx,%2" \
: \
: "i" ((short) (0x8000+(dpl<<)+(type<<))), \
"o" (*((char *) (gate_addr))), \
"o" (*(+(char *) (gate_addr))), \
"d" ((char *) (addr)),"a" (0x00080000))
......
#define set_system_gate(n,addr) \
_set_gate(&idt[n],,,addr)

其作用也就是填写IDT中int 0x80的中断描述符,中断描述符的格式如下:

关于fork( )函数父子进程返回值的问题

输入参数:%0: i(立即数) = 0x8000(0b1000,0000,0000,0000)

          | (dpl<<13)(0b0110,0000,0000,0000)

          | (type<<8)(0b0000,1111,0000,0000)

          = 0b1110,1111,0000,0000

      (这里我有个疑问,按照编号%0指的是输出寄存器,虽然这里并没有用到输出寄存器,但%0是否依然指的是输出寄存器?)

      o(内存变量) =%2:  (高四位) *(4+&idt[0x80])

             %1: (低四位)*(&idt[0x80])

      %3: d(edx,32位)=&system_callcall

      %4: a(eax,32位)=0x0008,0000(0b0000,0000,0000,1000,0000,0000,0000,0000)

汇编语句的执行过程:

 movw %%dx,%%ax    //ax=dx,即eax的低两个字节等于edx的低两个字节,也就是&system_call的低两个字节
movw %,%%dx //dx=i(0b1110,1111,0000,0000),即edx的低两个字节等于i
movl %%eax,% //*(&idt[0x80]) = 0000,0000,0000,1000 &system_call(低两个字节)
movl %%edx,% //*(4+&idt[0x80]) = &system_call(高两个字节) 1110,1111,0000,0000

这样,IDT表中的int 0x80的中断门描述符就填写好了,如下所示:

关于fork( )函数父子进程返回值的问题

(这里我还存在一个疑问,就是int 0x80中断门描述符的高四位中的第8位填充的是1,但表中要求是写0)

int 0x80的中断调用是一个有趣的过程:

首先应用程序通过库函数fork()调用系统调用sys_fork(),由于应用程序运行在特权级3,是不能访问内核代码的中断处理函数system_call()以及system_call()要进一步调用的具体系统调用函数sys_fork(),所以在int 0x80初始化在填写IDT表中int 0x80的描述符时,将其DPL置为3,这样应用程序得以进入内核,而在跳转到中断处理函数system_call()时,将对应的选择符置为0000,0000,0000,1000,即cs=0b0000,0000,0000,1000,表示访问特权级为0、使用全局描述符表GDT中的第2个段描述符项(内核代码段),即访问的基地址是内核代码段,偏移地址是system_call()的代码,使其又变为以最高特权级0访问system_call()函数,这样就完成了从应用程序到内核代码段的转移。

并且在执行int 0x80中断时,会发生堆栈的切换,即从用户栈切换到用户的内核堆栈。具体过程是:

处理器从当前执行任务的TSS段中得到中断处理过程使用的用户内核堆栈的段选择符和栈指针(例如tss.ss0、tss.esp0)。然后将应用程序的用户栈的段选择符和栈指针压栈,接着将EFLAGS、CS、EIP的值也压栈,而此刻EIP的值就是fork()函数中嵌入汇编int 0x80后的下一条指令的地址,即指令:

 5             : "=a" (__res)
 1 int fork(void)
2 {
3 long __res;
4 __asm__ volatile ("int $0x80"
5 : "=a" (__res) //eax保存的是int 0x80中断调用的返回值
6 : "0" (__NR_##fork)); //同时eax也是int 0x80中断调用的系统调用功能号
7 if (__res >= 0)
8 return (type) __res; //返回int 0x80的返回值作为fork()函数的返回值
9 errno = -__res;
10 return -1;
11 }
这一点对于理解为什么fork()函数返回时子进程的返回值是0非常关键。
接下来我们要关注下system_call的执行过程。system_call函数在kernel/system_call.s中:
 nr_system_calls = 

 bad_sys_call:
movl $-,%eax
iret reschedule:
pushl $ret_from_sys_call
jmp schedule system_call:
cmpl $nr_system_calls-,%eax #检查eax中的功能号是否有效(在给定的范围内)
ja bad_sys_call #跳转到bad_sys_call,即eax=-,中断返回
push %ds
push %es
push %fs
pushl %edx
pushl %ecx # 将edx,ecx,ebx压栈,作为system_call的参数
pushl %ebx
movl $0x10,%edx # ds,es用于内核数据段
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx # fs用于用户数据段
mov %dx,%fs
call sys_call_table(,%eax,) # 跳转到对应的系统调用函数中,此处是sys_fork()
pushl %eax # 把系统调用的返回值入栈
movl current,%eax # 取当前任务数据结构地址->eax
cmpl $,state(%eax) # 判断当前任务的状态
jne reschedule # 不在就绪状态(state != )就去执行调度程序schedule()
cmpl $,counter(%eax) # 判断当前任务时间片是否已用完
je reschedule # 时间片已用完(counter = )也去执行调度程序schedule()
ret_from_sys_call: # 执行完调度程序schedule()返回或没有执行调度程序直接到该处
movl current,%eax # task[] cannot have signals
cmpl task,%eax # 判断当前任务是否是初始任务task0
je 3f # 如果是不必对其进行信号量方面的处理,直接退出中断
cmpw $0x0f,CS(%esp) # 判断调用程序是否是用户任务
jne 3f # 如果不是,直接退出中断
cmpw $0x17,OLDSS(%esp) # 判断是否为用户代码段的选择符
jne 3f # 如果不是,则说明是某个中断服务程序跳转到这里,直接退出中断
movl signal(%eax),%ebx # 处理当前用户任务中的信号
movl blocked(%eax),%ecx
notl %ecx
andl %ebx,%ecx
bsfl %ecx,%ecx
je 3f
btrl %ecx,%ebx
movl %ebx,signal(%eax)
incl %ecx
pushl %ecx
call do_signal
popl %eax
: popl %eax # eax含有系统调用的返回值
popl %ebx
popl %ecx
popl %edx
pop %fs
pop %es
pop %ds
iret # 中断返回

这里说明了系统调用int 0x80的中断处理过程。每次执行完对应的系统调用,操作系统都会检查本次调用进程的状态。如果由于上面的系统调用操作或其他情况而使进程的状态从执行态变成了其他状态,或者由于进程的时间片已经用完,则调用进程调度函数schedule()。schedule()也是个有趣的函数,schedule()会从就绪队列中选择一个就绪进程,将此就绪进程与当前进程执行状态切换,而跳转到新的进程中去(即选中的就绪进程),只有当schedule()执行进程切换,再次切换回当前进程时,此次的中断调用int 0x80才会继续返回执行,进行信号处理,并中断返回。对于schedule()函数的理解,也是理解为什么fork()函数父子进程返回值不同的关键点。

接下来看一下系统调用sys_fork(),它定义在kernel/system_call.s中:

 sys_fork:
call find_empty_process # 调用find_empty_process()
testl %eax,%eax # 在eax中返回进程号pid。若返回负数则退出
js 1f
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call copy_process # 调用c函数 copy_process()
addl $,%esp # 丢弃这里所有压栈内容
: ret

其中,find_empty_process()和copy_process()在kernel/fork.c中定义:

 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[]);
old_data_base = get_base(current->ldt[]);
if (old_data_base != old_code_base)
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;
p->start_code = new_code_base;
set_base(p->ldt[],new_code_base);
set_base(p->ldt[],new_data_base);
if (copy_page_tables(old_data_base,new_data_base,data_limit)) { //复制当前进程(父进程)的页目录表项和页表项作为子进程的页目录表项和页表项,则子进程共享父进程的内存页面
printk("free_page_tables: from copy_mem\n");
free_page_tables(new_data_base,data_limit);
return -ENOMEM;
}
return ;
} /*
* Ok, this is the main fork-routine. It copies the system process
* information (task[nr]) and sets up the necessary registers. It
* also copies the data segment in it's entirety.
*/
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) //该函数的参数是进入系统调用中断处理过程(system_call.s)开始,直到sys_fork()和调用copy_process()前时逐步压入栈的各寄存器的值,所以新建子进程的状态会保持为父进程即将进入中断过程前的状态
{
struct task_struct *p;
int i;
struct file *f; p = (struct task_struct *) get_free_page();
if (!p)
return -EAGAIN;
task[nr] = p;
*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 = ;
p->alarm = ;
p->leader = ; /* process leadership doesn't inherit */
p->utime = p->stime = ;
p->cutime = p->cstime = ;
p->start_time = jiffies;
p->tss.back_link = ;
p->tss.esp0 = PAGE_SIZE + (long) p; //任务内核栈指针指向系统给任务结构p分配的1页新内存的顶端
p->tss.ss0 = 0x10; //内核态栈的段选择符(与内核数据段相同)
p->tss.eip = eip;
p->tss.eflags = eflags;
p->tss.eax = ; //这是当fork()返回时新进程会返回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;
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);
p->tss.trace_bitmap = 0x80000000;
if (last_task_used_math == current)
__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
if (copy_mem(nr,p)) {
task[nr] = NULL;
free_page((long) p);
return -EAGAIN;
}
for (i=; i<NR_OPEN;i++)
if ((f=p->filp[i]))
f->f_count++;
if (current->pwd)
current->pwd->i_count++;
if (current->root)
current->root->i_count++;
if (current->executable)
current->executable->i_count++;
//在GDT表中设置新任务TSS段和LDT段描述符项
set_tss_desc(gdt+(nr<<)+FIRST_TSS_ENTRY,&(p->tss));
set_ldt_desc(gdt+(nr<<)+FIRST_LDT_ENTRY,&(p->ldt));
p->state = TASK_RUNNING; /* do this last, just in case */
//最后才将新任务置成就绪态,以防万一
return last_pid; //最后返回新进程的pid
}

虽然操作系统为新进程在GDT表中设置了它的TSS段和LDT段的描述符项,也为其在线性地址空间设置了它的页目录项和页表项,但由于其页目录项和页表项是复制父进程的,所以内核并不会立刻为新进程分配代码和数据内存页。新进程将与父进程共同使用父进程已有的代码和数据内存页面。只有当以后执行过程中如果其中有一个进程以写方式访问内存时被访问的内存页面才会在写操作前被复制到新申请的内存页面中。而此后父进程和子进程就各有拥有其独立的页面。

这里我们可以看到,对于父进程来说,当它使用接口函数fork()引发系统调用,到进入系统调用中断int 0x80执行相应的系统调用中断处理过程(system_call.s)以及调用对应的系统调用函数(sys_fork()),再到可能被schedule()函数调度让出CPU使用权,到最后重新得到CPU使用权从int 0x80中断返回,父进程的返回值就是新建子进程的pid。而子进程当被schedule()函数调度获得CPU的使用权后,它会继续执行int 0x80下面的那条指令,即:

5             : "=a" (__res)
又由于已经将子进程TSS中的eax置为0,所以当子进程被切换入运行态时,将会把子进程TSS段的各寄存器的值作为CPU此时各寄存器的值,然后执行标号5的指令,将eax=0作为中断调用的返回值返回到fork()函数结尾处,所以对于子进程来说,它的返回值是0。

好累,第一次写博客,终于完成了,用了将近一天。