linux0.11源码-fork


fork

static _inline _syscall0(int,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 80h:触发0x80号软中断。将eax寄存器里值赋值为__NR_fork,这是个宏定义,值是2。
  • 0x80号中断是在sched_init里面设置的。set_system_gate(0x80, &system_call);

system_call

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
  • call sys_call_table(,%eax,4):操作数的含义是调用地址=[_sys_call_table + %eax * 4]

sys_call_table

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 };
  • 找到下标为2的函数即sys_fork,跳转过去执行它。

sys_fork

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
  • call find_empty_process:就是找到空闲的进程槽位。存储进程的数据结构是一个task[64]数组,这是在之前sched_init函数的时候设置的。先在这个数组中找一个空闲的位置,准备存一个新的进程的结构 task_struct。
  • call copy_process:复制进程。

find_empty_process

long last_pid = 0;

int find_empty_process(void) {
int i;
repeat:
    if ((++last_pid)<0) last_pid=1;
    for(i=0 ; i<64 ; i++)
        if (task[i] && task[i]->pid == last_pid) goto repeat;
for(i=1 ; i<64; i++)
    if (!task[i])
        return i;
return -EAGAIN;
}
  • 第一个for循环,判断last_pid在所有task[]数组中,是否已经被某进程占用了。如果被占用了,那就重复执行,再次加一,然后再次判断,直到找到一个pid号没有被任何进程用为止。
  • 第二个for循环,刚刚已经找到一个可用的pid号了,那这一步就是再次遍历这个task[]试图找到一个空闲项,找到了就返回素组索引下标。

call copy_process

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;


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->father = current->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"::"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++)
    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++;
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;
}
  • p = (struct task_struct *) get_free_page();:遍历mem_map[]数组,找出值为零的项,就表示找到了空闲的一页内存。然后把该项置为1,表示该页已经被使用。最后,算出这个页的内存起始地址,返回。
  • task[nr] = p;:p记录在进程管理结构task[]中。
  • *p = *current;:把当前进程的task_struct的全部值都复制给即将创建的进程p。
  • 对一些值进行个性化处理。
  • copy_mem(nr,p):新进程 LDT 表项的赋值,以及页表的拷贝。

get_free_page

unsigned long get_free_page(void)
{
register unsigned long __res asm("ax");

__asm__("std ; repne ; scasb\n\t"   // 置方向位,al(0)与对应每个页面的(di)内容比较
    "jne 1f\n\t"                    // 如果没有等于0的字节,则跳转结束(返回0).
    "movb $1,1(%%edi)\n\t"          // 1 => [1+edi],将对应页面内存映像bit位置1.
    "sall $12,%%ecx\n\t"            // 页面数*4k = 相对页面其实地址
    "addl %2,%%ecx\n\t"             // 再加上低端内存地址,得页面实际物理起始地址
    "movl %%ecx,%%edx\n\t"          // 将页面实际其实地址->edx寄存器。
    "movl $1024,%%ecx\n\t"          // 寄存器ecx置计数值1024
    "leal 4092(%%edx),%%edi\n\t"    // 将4092+edx的位置->dei(该页面的末端地址)
    "rep ; stosl\n\t"               // 将edi所指内存清零(反方向,即将该页面清零)
    "movl %%edx,%%eax\n"            // 将页面起始地址->eax(返回值)
    "1:"
    :"=a" (__res)
    :"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
    "D" (mem_map+PAGING_PAGES-1)
    );
return __res;           // 返回空闲物理页面地址(若无空闲页面则返回0).
}

copy_mem

int copy_mem(int nr,struct task_struct * p) {
    // 局部描述符表 LDT 赋值
    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);
    new_code_base = nr * 0x4000000;
    new_data_base = nr * 0x4000000;
    set_base(p->ldt[1],new_code_base);
    set_base(p->ldt[2],new_data_base);
    // 拷贝页表
    old_code_base = get_base(current->ldt[1]);
    old_data_base = get_base(current->ldt[2]);
    copy_page_tables(old_data_base,new_data_base,data_limit);
    return 0;
}
  • code_limit = get_limit(0x0f);data_limit = get_limit(0x17);:段限长,就是取自进程0设置好的段限长
  • new_code_base = nr * 0x4000000;new_data_base = nr * 0x4000000;:设置段基址,段基址是取决于当前是几号进程,也就是nr的值。
  • set_base(p->ldt[1],new_code_base);set_base(p->ldt[2],new_data_base);:把 LDT 设置进了 LDT 表里。
  • copy_page_tables(old_data_base,new_data_base,data_limit):页表的复制。

copy_page_tables

/*
 * Well, here is one of the most complicated functions in mm. It
 * copies a range of linerar addresses by copying only the pages.
 * Let's hope this is bug-free, 'cause this one I don't want to debug :-)
 */
//// 复制页目录表项和页表项
// 复制指定线性地址和长度内存对应的页目录项和页表项,从而被复制的页目录和页表对
// 应的原物理内存页面区被两套页表映射而共享使用。复制时,需申请新页面来存放新页
// 表,原物理内存区将被共享。此后两个进程(父进程和其子进程)将共享内存区,直到
// 有一个进程执行谢操作时,内核才会为写操作进程分配新的内存页(写时复制机制)。
// 参数from、to是线性地址,size是需要复制(共享)的内存长度,单位是byte.
int copy_page_tables(unsigned long from,unsigned long to,long size)
{
    unsigned long * from_page_table;
    unsigned long * to_page_table;
    unsigned long this_page;
    unsigned long * from_dir, * to_dir;
    unsigned long nr;

    // 首先检测参数给出的原地址from和目的地址to的有效性。原地址和目的地址都需要
    // 在4Mb内存边界地址上。否则出错死机。作这样的要求是因为一个页表的1024项可
    // 管理4Mb内存。源地址from和目的地址to只有满足这个要求才能保证从一个页表的
    // 第一项开始复制页表项,并且新页表的最初所有项都是有效的。然后取得源地址和
    // 目的地址的其实目录项指针(from_dir 和 to_dir).再根据参数给出的长度size计
    // 算要复制的内存块占用的页表数(即目录项数)。
    if ((from&0x3fffff) || (to&0x3fffff))
        panic("copy_page_tables called with wrong alignment");
    from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
    to_dir = (unsigned long *) ((to>>20) & 0xffc);
    size = ((unsigned) (size+0x3fffff)) >> 22;
    // 在得到了源起始目录项指针from_dir和目的起始目录项指针to_dir以及需要复制的
    // 页表个数size后,下面开始对每个页目录项依次申请1页内存来保存对应的页表,并
    // 且开始页表项复制操作。如果目的目录指定的页表已经存在(P=1),则出错死机。
    // 如果源目录项无效,即指定的页表不存在(P=1),则继续循环处理下一个页目录项。
    for( ; size-->0 ; from_dir++,to_dir++) {
        if (1 & *to_dir)
            panic("copy_page_tables: already exist");
        if (!(1 & *from_dir))
            continue;
        // 在验证了当前源目录项和目的项正常之后,我们取源目录项中页表地址
        // from_page_table。为了保存目的目录项对应的页表,需要在住内存区中申请1
        // 页空闲内存页。如果取空闲页面函数get_free_page()返回0,则说明没有申请
        // 到空闲内存页面,可能是内存不够。于是返回-1值退出。
        from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
        if (!(to_page_table = (unsigned long *) get_free_page()))
            return -1;    /* Out of memory, see freeing */
        // 否则我们设置目的目录项信息,把最后3位置位,即当前目录的目录项 | 7,
        // 表示对应页表映射的内存页面是用户级的,并且可读写、存在(Usr,R/W,Present).
        // (如果U/S位是0,则R/W就没有作用。如果U/S位是1,而R/W是0,那么运行在用
        // 户层的代码就只能读页面。如果U/S和R/W都置位,则就有读写的权限)。然后
        // 针对当前处理的页目录项对应的页表,设置需要复制的页面项数。如果是在内
        // 核空间,则仅需复制头160页对应的页表项(nr=160),对应于开始640KB物理内存
        // 否则需要复制一个页表中的所有1024个页表项(nr=1024),可映射4MB物理内存。
        *to_dir = ((unsigned long) to_page_table) | 7;
        nr = (from==0)?0xA0:1024;
        // 此时对于当前页表,开始循环复制指定的nr个内存页面表项。先取出源页表的
        // 内容,如果当前源页表没有使用,则不用复制该表项,继续处理下一项。否则
        // 复位表项中R/W标志(位1置0),即让页表对应的内存页面只读。然后将页表项复制
        // 到目录页表中。
        for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
            this_page = *from_page_table;
            if (!(1 & this_page))
                continue;
            this_page &= ~2;
            *to_page_table = this_page;
            // 如果该页表所指物理页面的地址在1MB以上,则需要设置内存页面映射数
            // 组mem_map[],于是计算页面号,并以它为索引在页面映射数组相应项中
            // 增加引用次数。而对于位于1MB以下的页面,说明是内核页面,因此不需
            // 要对mem_map[]进行设置。因为mem_map[]仅用于管理主内存区中的页面使
            // 用情况。因此对于内核移动到任务0中并且调用fork()创建任务1时(用于
            // 运行init()),由于此时复制的页面还仍然都在内核代码区域,因此以下
            // 判断中的语句不会执行,任务0的页面仍然可以随时读写。只有当调用fork()
            // 的父进程代码处于主内存区(页面位置大于1MB)时才会执行。这种情况需要
            // 在进程调用execve(),并装载执行了新程序代码时才会出现。
            // *from_page_table = this_page; 这句是令源页表项所指内存页也为只读。
            // 因为现在开始有两个进程公用内存区了。若其中1个进程需要进行写操作,
            // 则可以通过页异常写保护处理为执行写操作的进程匹配1页新空闲页面,也
            // 即进行写时复制(copy on write)操作。
            if (this_page > LOW_MEM) {
                *from_page_table = this_page;
                this_page -= LOW_MEM;
                this_page >>= 12;
                mem_map[this_page]++;
            }
        }
    }
    invalidate();
    return 0;
}
  • 页表的复制,使得进程0和进程1又从不同的线性地址空间,被映射到了相同的物理地址空间。

声明:Hello World|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - linux0.11源码-fork


我的朋友,理论是灰色的,而生活之树是常青的!