理解fork子进程为什么返回0

从linux0.11源码分析fork函数的作用

fork函数最早出现在main.cmain方法中

void main(void)     
{       
    ...
    sched_init();
    ...
    if (!fork()) {      
        init();    // 在新建的子进程(任务1)中执行。
    }
    ...
}

头文件在unistd.h里面,

int fork(void);

fork函数的实现其实是一个宏定义

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

展开后就是

int fork(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
    : "=a" (__res) \
    : "0" (__NR_fork)); \
if (__res >= 0) \
    return (type) __res; \
errno = -__res; \
return -1; \
}

__NR_fork也位于该头文件中

#define __NR_fork   2

现在调用了中断(0x80),调用中断就会执行相应的中断服务程序,
kernel/sched.c中的sched_init方法中就对0x80号中断进行了配置,就是说发生该中断就会调用system_call方法。

void sched_init(void)
{
    ...
    set_system_gate(0x80,&system_call);
}

system_call方法并不在我们的C代码中,而是在汇编代码里面
位于kernel/system_call.s里面,
咱们只看重点,首先发生中断,先把寄存器中的值压栈保存数据或者也可以叫保存环境上下文,然后切换到内核态,然后去到sys_call_table表里面找函数

system_call:
    cmpl $nr_system_calls-1,%eax    # 调用号如果超出范围的话就在eax中置-1并退出
    ja bad_sys_call
    push %ds                        # 保存原段寄存器值
    push %es
    push %fs
# 一个系统调用最多可带有3个参数,也可以不带参数。下面入栈的ebx、ecx和edx中放着系统
# 调用相应C语言函数的调用函数。这几个寄存器入栈的顺序是由GNU GCC规定的,
# ebx 中可存放第1个参数,ecx中存放第2个参数,edx中存放第3个参数。
# 系统调用语句可参见头文件include/unistd.h中的系统调用宏。
    pushl %edx
    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
    mov %dx,%es
# fs指向局部数据段(局部描述符表中数据段描述符),即指向执行本次系统调用的用户程序的数据段。
# 注意,在Linux 0.11 中内核给任务分配的代码和数据内存段是重叠的,他们的段基址和段限长相同。
    movl $0x17,%edx     # fs points to local data space
    mov %dx,%fs
# 下面这句操作数的含义是:调用地址=[_sys_call_table + %eax * 4]
# sys_call_table[]是一个指针数组,定义在include/linux/sys.h中,该指针数组中设置了所有72
# 个系统调用C处理函数地址。
    call sys_call_table(,%eax,4)        # 间接调用指定功能C函数
    pushl %eax                          # 把系统调用返回值入栈
# 下面几行查看当前任务的运行状态。如果不在就绪状态(state != 0)就去执行调度程序。如果该
# 任务在就绪状态,但其时间片已用完(counter = 0),则也去执行调度程序。例如当后台进程组中的
# 进程执行控制终端读写操作时,那么默认条件下该后台进程组所有进程会收到SIGTTIN或SIGTTOU
# 信号,导致进程组中所有进程处于停止状态。而当前进程则会立刻返回。
    movl current,%eax                   # 取当前任务(进程)数据结构地址→eax
    cmpl $0,state(%eax)     # state
    jne reschedule
    cmpl $0,counter(%eax)       # counter
    je reschedule
# 以下这段代码执行从系统调用C函数返回后,对信号进行识别处理。其他中断服务程序退出时也
# 将跳转到这里进行处理后才退出中断过程,例如后面的处理器出错中断int 16.
ret_from_sys_call:
# 首先判别当前任务是否是初始任务task0,如果是则不比对其进行信号量方面的处理,直接返回。
    movl current,%eax       # task[0] cannot have signals
    cmpl task,%eax
    je 3f                   # 向前(forward)跳转到标号3处退出中断处理
# 通过对原调用程序代码选择符的检查来判断调用程序是否是用户任务。如果不是则直接退出中断。
# 这是因为任务在内核态执行时不可抢占。否则对任务进行信号量的识别处理。这里比较选择符是否
# 为用户代码段的选择符0x000f(RPL=3,局部表,第一个段(代码段))来判断是否为用户任务。如果不是
# 则说明是某个中断服务程序跳转到上面的,于是跳转退出中断程序。如果原堆栈段选择符不为
# 0x17(即原堆栈不在用户段中),也说明本次系统调用的调用者不是用户任务,则也退出。
    cmpw $0x0f,CS(%esp)     # was old code segment supervisor ?
    jne 3f
    cmpw $0x17,OLDSS(%esp)      # was stack segment = 0x17 ?
    jne 3f
# 下面这段代码用于处理当前任务中的信号。首先取当前任务结构中的信号位图(32位,每位代表1种
# 信号),然后用任务结构中的信号阻塞(屏蔽)码,阻塞不允许的信号位,取得数值最小的信号值,
# 再把原信号位图中该信号对应的位复位(置0),最后将该信号值作为参数之一调用do_signal().
# do_signal()在kernel/signal.c中,其参数包括13个入栈信息。
    movl signal(%eax),%ebx          # 取信号位图→ebx,每1位代表1种信号,共32个信号
    movl blocked(%eax),%ecx         # 取阻塞(屏蔽)信号位图→ecx
    notl %ecx                       # 每位取反
    andl %ebx,%ecx                  # 获得许可信号位图
    bsfl %ecx,%ecx                  # 从低位(位0)开始扫描位图,看是否有1的位,若有,则ecx保留该位的偏移值
    je 3f                           # 如果没有信号则向前跳转退出
    btrl %ecx,%ebx                  # 复位该信号(ebx含有原signal位图)
    movl %ebx,signal(%eax)          # 重新保存signal位图信息→current->signal.
    incl %ecx                       # 将信号调整为从1开始的数(1-32)
    pushl %ecx                      # 信号值入栈作为调用do_signal的参数之一
    call do_signal                  # 调用C函数信号处理程序(kernel/signal.c)
    popl %eax                       # 弹出入栈的信号值
3:  popl %eax                       # eax中含有上面入栈系统调用的返回值
    popl %ebx
    popl %ecx
    popl %edx
    pop %fs
    pop %es
    pop %ds
    iret

sys_call_table位于linux/sys.h

fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, ... };

因为__NR_fork的值是2,所以从表里面获取的就是sys_fork函数,该函数也位于汇编代码里面,
首先find_empty_process找到一个可用的进程id,注意这里的%eax里面的值是就是我们刚才找到的id,接者调用copy_process

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

copy_process定义在kernel/fork.c里面,贴出代码能看懂的人就看,看不懂的不用在意,就是把父进程资源复制到子进程中,设置子进程的内存页等信息,如果父进程调用fork结束,就会返回__res,因为__res%eax寄存器是绑定的,寄存器里面存的就是进程的pid,因为copy_process的返回值就是last_pid,也就是子进程pid,注意这里是父进程返回,子进程还没执行呢,但是子进程也会返回__res__res的值就是寄存器%eax的值。
睁大眼睛看代码力里有这么一段p->tss.eax = 0;,父进程已经把子进程%eax寄存器中的值设置为0了。明白为什么使用fork时,返回0就代表是子进程了吧。

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)
{
    struct task_struct *p;
    int i;
    struct file *f;

    // 首先为新任务数据结构分配内存。如果内存分配出错,则返回出错码并退出。
    // 然后将新任务结构指针放入任务数组的nr项中。其中nr为任务号,由前面
    // find_empty_process()返回。接着把当前进程任务结构内容复制到刚申请到
    // 的内存页面p开始处。
    p = (struct task_struct *) get_free_page();
    if (!p)
        return -EAGAIN;
    task[nr] = p;
    *p = *current;  /* NOTE! this doesn't copy the supervisor stack */
    // 随后对复制来的进程结构内容进行一些修改,作为新进程的任务结构。先将
    // 进程的状态置为不可中断等待状态,以防止内核调度其执行。然后设置新进程
    // 的进程号pid和父进程号father,并初始化进程运行时间片值等于其priority值
    // 接着复位新进程的信号位图、报警定时值、会话(session)领导标志leader、进程
    // 及其子进程在内核和用户态运行时间统计值,还设置进程开始运行的系统时间start_time.
    p->state = TASK_UNINTERRUPTIBLE;
    p->pid = last_pid;              // 新进程号。也由find_empty_process()得到。
    p->father = current->pid;       // 设置父进程
    p->counter = p->priority;       // 运行时间片值
    p->signal = 0;                  // 信号位图置0
    p->alarm = 0;                   // 报警定时值(滴答数)
    p->leader = 0;      /* process leadership doesn't inherit */
    p->utime = p->stime = 0;        // 用户态时间和和心态运行时间
    p->cutime = p->cstime = 0;      // 子进程用户态和和心态运行时间
    p->start_time = jiffies;        // 进程开始运行时间(当前时间滴答数)
    // 再修改任务状态段TSS数据,由于系统给任务结构p分配了1页新内存,所以(PAGE_SIZE+
    // (long)p)让esp0正好指向该页顶端。ss0:esp0用作程序在内核态执行时的栈。另外,
    // 每个任务在GDT表中都有两个段描述符,一个是任务的TSS段描述符,另一个是任务的LDT
    // 表描述符。下面语句就是把GDT中本任务LDT段描述符和选择符保存在本任务的TSS段中。
    // 当CPU执行切换任务时,会自动从TSS中把LDT段描述符的选择符加载到ldtr寄存器中。
    p->tss.back_link = 0;
    p->tss.esp0 = PAGE_SIZE + (long) p;     // 任务内核态栈指针。
    p->tss.ss0 = 0x10;                      // 内核态栈的段选择符(与内核数据段相同)
    p->tss.eip = eip;                       // 指令代码指针
    p->tss.eflags = eflags;                 // 标志寄存器
    p->tss.eax = 0;                         // 这是当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;                // 段寄存器仅16位有效
    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);                  // 任务局部表描述符的选择符(LDT描述符在GDT中)
    p->tss.trace_bitmap = 0x80000000;       // 高16位有效
    // 如果当前任务使用了协处理器,就保存其上下文。汇编指令clts用于清除控制寄存器CRO中
    // 的任务已交换(TS)标志。每当发生任务切换,CPU都会设置该标志。该标志用于管理数学协
    // 处理器:如果该标志置位,那么每个ESC指令都会被捕获(异常7)。如果协处理器存在标志MP
    // 也同时置位的话,那么WAIT指令也会捕获。因此,如果任务切换发生在一个ESC指令开始执行
    // 之后,则协处理器中的内容就可能需要在执行新的ESC指令之前保存起来。捕获处理句柄会
    // 保存协处理器的内容并复位TS标志。指令fnsave用于把协处理器的所有状态保存到目的操作数
    // 指定的内存区域中。
    if (last_task_used_math == current)
        __asm__("clts ; fnsave %0"::"m" (p->tss.i387));
    // 接下来复制进程页表。即在线性地址空间中设置新任务代码段和数据段描述符中的基址和限长,
    // 并复制页表。如果出错(返回值不是0),则复位任务数组中相应项并释放为该新任务分配的用于
    // 任务结构的内存页。
    if (copy_mem(nr,p)) {
        task[nr] = NULL;
        free_page((long) p);
        return -EAGAIN;
    }
    // 如果父进程中有文件是打开的,则将对应文件的打开次数增1,因为这里创建的子进程会与父
    // 进程共享这些打开的文件。将当前进程(父进程)的pwd,root和executable引用次数均增1.
    // 与上面同样的道理,子进程也引用了这些i节点。
    for (i=0; 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段描述符项。这两个段的限长均被设置成104字节。
    // set_tss_desc()和set_ldt_desc()在system.h中定义。"gdt+(nr<<1)+FIRST_TSS_ENTRY"是
    // 任务nr的TSS描述符项在全局表中的地址。因为每个任务占用GDT表中2项,因此上式中
    // 要包括'(nr<<1)'.程序然后把新进程设置成就绪态。另外在任务切换时,任务寄存器tr由
    // CPU自动加载。最后返回新进程号。
    set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
    set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
    p->state = TASK_RUNNING;    /* do this last, just in case */
    return last_pid;
}

参考:
GCC-Inline-Assembly-HOWTO

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,636评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,890评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,680评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,766评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,665评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,045评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,515评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,182评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,334评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,274评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,319评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,002评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,599评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,675评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,917评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,309评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,885评论 2 341

推荐阅读更多精彩内容