在Linux系统下学习一个系统函数最好的方法就是阅读其源码,首先,给出fork函数的源码
- /*
- * linux/kernel/fork.c
- * //--fork()用于创建子进程
- * (C) 1991 Linus Torvalds
- */
- /*
- * 'fork.c' contains the help-routines for the 'fork' system call
- * (see also system_call.s), and some misc functions ('verify_area').
- * Fork is rather simple, once you get the hang of it, but the memory
- * management can be a bitch. See 'mm/mm.c': 'copy_page_tables()'
- */
- #include <errno.h>
- #include <linux/sched.h>
- #include <linux/kernel.h>
- #include <asm/segment.h>
- #include <asm/system.h>
- //--写页面验证,若页面不可写,则复制页面
- extern void write_verify(unsigned long address);
- long last_pid=0;
- //--进程空间区域写前验证函数
- void verify_area(void * addr,int size)
- {
- unsigned long start;
- start = (unsigned long) addr;
- size += start & 0xfff;
- start &= 0xfffff000;
- start += get_base(current->ldt[2]); //--逻辑地址到线性地址的转换
- while (size>0) {
- size -= 4096;
- write_verify(start);
- start += 4096;
- }
- }
- 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[1]);
- old_data_base = get_base(current->ldt[2]);
- 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 * TASK_SIZE;
- p->start_code = new_code_base;
- set_base(p->ldt[1],new_code_base);
- set_base(p->ldt[2],new_data_base);
- if (copy_page_tables(old_data_base,new_data_base,data_limit)) { //--复制页表
- free_page_tables(new_data_base,data_limit);
- return -ENOMEM;
- }
- return 0;
- }
- /*
- * 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.
- */ //--fork()子程序,它复制系统进程信息,设置寄存器,复制数据段(代码段)
- int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
- long ebx,long ecx,long edx, long orig_eax,
- 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;
- 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;
- p->counter = p->priority;
- p->signal = 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;
- 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;
- 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 ; frstor %0"::"m" (p->tss.i387));
- if (copy_mem(nr,p)) {
- task[nr] = NULL;
- free_page((long) p);
- return -EAGAIN;
- }
- for (i=0; i<NR_OPEN;i++) //--如果父进程中有文件是打开的,则将对应文件的打开次数增1
- 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++;
- if (current->library)
- current->library->i_count++;
- set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss)); //--在GDT表中设置新任务的TSS和LDT
- set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
- p->p_pptr = current;
- p->p_cptr = 0;
- p->p_ysptr = 0;
- p->p_osptr = current->p_cptr;
- if (p->p_osptr)
- p->p_osptr->p_ysptr = p;
- current->p_cptr = p;
- p->state = TASK_RUNNING; /* do this last, just in case */
- return last_pid;
- }
- int find_empty_process(void) //--为新进程取得不重复的进程号last_pid
- {
- int i;
- repeat:
- if ((++last_pid)<0) last_pid=1;
- for(i=0 ; i<NR_TASKS ; i++)
- if (task[i] && ((task[i]->pid == last_pid) ||
- (task[i]->pgrp == last_pid)))
- goto repeat;
- for(i=1 ; i<NR_TASKS ; i++)
- if (!task[i])
- return i;
- return -EAGAIN;
- }
以下给出说明:
fork函数
#include <sys/types.h>
#include <unistd.h> pid_t fork(void);
fork
调用失败则返回-1,调用成功的返回值见下面的解释。我们通过一个例子来理解fork
是怎样创建新进程的。
这个程序的运行过程如下图所示。
-
父进程初始化。
-
父进程调用
fork
,这是一个系统调用,因此进入内核。 -
内核根据父进程复制出一个子进程,父进程和子进程的PCB信息相同,用户态代码和数据也相同。因此,子进程现在的状态看起来和父进程一样,做完了初始化,刚调用了
fork
进入内核,还没有从内核返回。 -
现在有两个一模一样的进程看起来都调用了
fork
进入内核等待从内核返回(实际上fork
只调用了一次),此外系统中还有很多别的进程也等待从内核返回。是父进程先返回还是子进程先返回,还是这两个进程都等待,先去调度执行别的进程,这都不一定,取决于内核的调度算法。 -
如果某个时刻父进程被调度执行了,从内核返回后就从
fork
函数返回,保存在变量pid
中的返回值是子进程的id,是一个大于0的整数,因此执下面的else
分支,然后执行for
循环,打印"This is the parent\n"
三次之后终止。 -
如果某个时刻子进程被调度执行了,从内核返回后就从
fork
函数返回,保存在变量pid
中的返回值是0,因此执行下面的if (pid == 0)
分支,然后执行for
循环,打印"This is the child\n"
六次之后终止。fork
调用把父进程的数据复制一份给子进程,但此后二者互不影响,在这个例子中,fork
调用之后父进程和子进程的变量message
和n
被赋予不同的值,互不影响。 -
父进程每打印一条消息就睡眠1秒,这时内核调度别的进程执行,在1秒这么长的间隙里(对于计算机来说1秒很长了)子进程很有可能被调度到。同样地,子进程每打印一条消息就睡眠1秒,在这1秒期间父进程也很有可能被调度到。所以程序运行的结果基本上是父子进程交替打印,但这也不是一定的,取决于系统中其它进程的运行情况和内核的调度算法,如果系统中其它进程非常繁忙则有可能观察到不同的结果。另外,读者也可以把
sleep(1);
去掉看程序的运行结果如何。 -
这个程序是在Shell下运行的,因此Shell进程是父进程的父进程。父进程运行时Shell进程处于等待状态,当父进程终止时Shell进程认为命令执行结束了,于是打印Shell提示符,而事实上子进程这时还没结束,所以子进程的消息打印到了Shell提示符后面。最后光标停在
This is the child
的下一行,这时用户仍然可以敲命令,即使命令不是紧跟在提示符后面,Shell也能正确读取。
fork
函数的特点概括起来就是“调用一次,返回两次”,在父进程中调用一次,在父进程和子进程中各返回一次。从上图可以看出,一开始是一个控制流程,调用fork
之后发生了分叉,变成两个控制流程,这也就是“fork”(分叉)这个名字的由来了。子进程中fork
的返回值是0,而父进程中fork
的返回值则是子进程的id(从根本上说fork
是从内核返回的,内核自有办法让父进程和子进程返回不同的值),这样当fork
函数返回后,程序员可以根据返回值的不同让父进程和子进程执行不同的代码。
fork
的返回值这样规定是有道理的。fork
在子进程中返回0,子进程仍可以调用getpid
函数得到自己的进程id,也可以调用getppid
函数得到父进程的id。在父进程中用getpid
可以得到自己的进程id,然而要想得到子进程的id,只有将fork
的返回值记录下来,别无它法。
fork
的另一个特性是所有由父进程打开的描述符都被复制到子进程中。父、子进程中相同编号的文件描述符在内核中指向同一个file
结构体,也就是说,file
结构体的引用计数要增加。