关于fork( )函数父子进程返回值的问题
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的中断描述符,中断描述符的格式如下:
输入参数:%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的中断门描述符就填写好了,如下所示:
(这里我还存在一个疑问,就是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。
好累,第一次写博客,终于完成了,用了将近一天。
关于fork( )函数父子进程返回值的问题的更多相关文章
- 关于fork()父子进程返回值的问题
我们都知道,父进程fork()之后返回值为子进程的pid号,而子进程fork()之后的返回值为0.那么,现在就有一个问题了,子进程fork()的返回值是怎么来的?如果子进程又执行了一遍fork()函数 ...
- 【C语言入门教程】5.1 函数说明 与 返回值
C 语言是结构化语言,它的主要结构成分是函数.函数被作为一种构件,用以完成程序中的某个具体功能.函数允许一个程序的各个任务被分别定义和编码,使程序模块化.本章介绍 C 语言函数的设计,如何用函数分解程 ...
- linux shell自定义函数(定义、返回值、变量作用域)介绍
http://www.jb51.net/article/33899.htm linux shell自定义函数(定义.返回值.变量作用域)介绍 linux shell 可以用户定义函数,然后在shell ...
- C++ 需要返回值的函数却没有返回值的情况 单例模式
昨天在看前些天写的代码,发现一个错误. #include <iostream> using namespace std; class singleton { public: static ...
- 函数指针的返回值是指针数组,数组里放的是int;函数指针的返回值是指针数组,数组里放的是int指针
函数指针的返回值是指针数组,数组里放的是int 函数指针的返回值是指针数组,数组里放的是int指针 #include <stdio.h> #include <stdlib.h> ...
- python函数进阶(函数参数、返回值、递归函数)
函数进阶 目标 函数参数和返回值的作用 函数的返回值 进阶 函数的参数 进阶 递归函数 01. 函数参数和返回值的作用 函数根据 有没有参数 以及 有没有返回值,可以 相互组合,一共有 4 种 组合形 ...
- fork后父子进程文件描述问题
[fork后父子进程文件描述问题] 一张图可以浅析的解释: 参考:http://wenku.baidu.com/view/dd51581bff00bed5b9f31d8e.html
- 9 - Python函数定义-位置参数-返回值
目录 1 函数介绍 1.1 为什么要使用函数 1.2 Python中的函数 2 函数的基本使用 3 函数的参数 3.1 参数的默认值 3.2 可变参数 3.2.1 可变位置传参 3.2.2 可变关键字 ...
- go语言基础之函数只有一个返回值
1.函数只有一个返回值 示例1: package main //必须有一个main包 import "fmt" func myfunc01() int { return 666 } ...
随机推荐
- 文件夹oradiag_是如何产生的
如果sqlnet.ora不可用或者ADR_BASE参数未定义,那么11g的 SQL*Net将创建这些文件夹 (详情:http://download.oracle.com/docs/cd/B28359_ ...
- xgboost在windows上的安装
xgboost是一个boosting+decision trees的工具包,看微博上各种大牛都说效果很好,于是下载一个,使用了一下,安装步骤如下. 第一步,编译生成xgboost.exe(用于CLI) ...
- Telecasting station - SGU 114(带劝中位数)
题目大意:在一个数轴上有N个点,每个点都有一个权值,在这个数轴上找一个点,是的每个点到这个点的距离之和乘上权值的总和最小. 分析:以前也遇到过类似的问题,不过并不知道这是带权值的中位数问题,百度百科有 ...
- iOS 应用程序本地化
由于iPhone,iPad等苹果产品在全世界范围内的广泛流行,那么通过App Store下载应用程序的用户也将是来自世界范围的人们,所以开发者在开发过程中势必要考虑到不同语言环境下用户使用,好在iOS ...
- 一款很不错的html转xml工具-Html Agility Pack
之前发个一篇关于实现html转成xml的劣作<实现html转Xml>,受到不少网友的关心.该实现方法是借助htmlparser去分解html内容,然后按照dom的结构逐个生成xml字符串. ...
- thinkphp 统计某个字段不重复数 总数
$this->batch->count('DISTINCT intobatch');
- CSU1312:榜单(模拟)
Description ZZY很喜欢流行音乐,每周都要跟踪世界各地各种榜单,例如Oricon和Billboard,现在给出每周各个单曲的销量请给出每周的TOP5以及TOP5中各个单曲的浮动情况. 量的 ...
- android 37 线程通信Looper
安卓程序的主线程也叫UI线程. 工作线程和主线程的差别:安卓主线程已经调用了Looper.prepare()方法了,已经有一个MessageQueue对象了,所以才可以在工作线程用Handler发消息 ...
- 使用systemtap调试linux内核
http://blog.csdn.net/heli007/article/details/7187748 http://linux.chinaunix.net/docs/2006-12-15/3479 ...
- 深入分析 Java I/O 的工作机制--转载
Java 的 I/O 类库的基本架构 I/O 问题是任何编程语言都无法回避的问题,可以说 I/O 问题是整个人机交互的核心问题,因为 I/O 是机器获取和交换信息的主要渠道.在当今这个数据大爆炸时代, ...