Linux 0.11 系列文章



Linux 0.11启动过程分析(一)Linux 0.11 fork 函数(二)
Linux0.11 缺页处理(三)
Linux0.11 根文件系统挂载(四)
Linux0.11 文件打开open函数(五)
Linux0.11 execve函数(六)


文章目录

  • Linux 0.11 系列文章
  • 一、描述
  • 二、task_struct 结构体
  • 三、fork 函数定义
  • 四、系统调用中断响应
  • 1、system_call 函数
  • 2、sys_fork 函数
  • find_empty_process 函数
  • copy_process 函数
  • copy_mem 函数

一、描述

    Linux 系统中创建新进程使用 fork() 系统调用。所有进程都是通过复制进程 0 而得到的,都是进程 0 的子进程。在创建新进程的过程中,系统首先在任务数组中找出一个还没有被任何进程使用的空项( task[NR_TASKS] )。
    然后系统为新建进程在主内存区中申请一页内存( 4K 大小)来存放其任务数据结构信息,并复制当前进程任务数据结构中的所有内容作为新进程任务数据结构的模板。
    随后对复制的任务数据结构进行修改。设置初始运行时间片为 15 个系统滴答数( 150ms )。接着根据当前进程设置任务状态段 TSS 中各寄存器的值。新建进程 内核态堆栈指针 tss.esp0 被设置成新进程任务数据结构所在内存页面的 顶端,而 堆栈段 tss.ss0 被设置成 内核数据段选择符tss.ldt 被设置为 局部表描述符 在GDT中的索引值。
    此后系统设置新任务的代码和数据段基址、限长,并复制当前进程内存分页管理的页表。注意,此时系统并不为新的进程分配实际的物理内存页面,而是让它共享其父进程的内存页面。只有当父进程或新进程中任意一个有写内存操作时,系统才会为执行写操作的进程分配相关的独立使用的内存页面。
    随后,如果父进程中有文件是打开的,则应将对应文件的打开次数增加1。接着在GDT中设置新任务的 TSSLDT 描述符项,其中基地址信息指向新进程任务结构中的 tssldt 。最后再将新任务设置成可运行状态并返回新进程号。

二、task_struct 结构体

struct tss_struct {
	long	back_link;	/* 16 high bits zero */
	long	esp0;
	long	ss0;		/* 16 high bits zero */
	long	esp1;
	long	ss1;		/* 16 high bits zero */
	long	esp2;
	long	ss2;		/* 16 high bits zero */
	long	cr3;
	long	eip;
	long	eflags;
	long	eax,ecx,edx,ebx;
	long	esp;
	long	ebp;
	long	esi;
	long	edi;
	long	es;		/* 16 high bits zero */
	long	cs;		/* 16 high bits zero */
	long	ss;		/* 16 high bits zero */
	long	ds;		/* 16 high bits zero */
	long	fs;		/* 16 high bits zero */
	long	gs;		/* 16 high bits zero */
	long	ldt;		/* 16 high bits zero */
	long	trace_bitmap;	/* bits: trace 0, bitmap 16-31 */
	struct i387_struct i387;
};
 
struct task_struct {
/* these are hardcoded - don't touch */
    /* 任务的运行状态(-1 不可运行,0 可运行(就绪), >0 已停止) */
	long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
    /* 任务运行时间计数(递减)(滴答数),运行时间片 */
	long counter;
    /* 运行优先数。任务开始运行时 counter = priority,越大运行越长 */
	long priority;
    /* 信号。是位图,每个比特位代表一种信号,信号值=位偏移值+1 */
	long signal;
    /* 信号执行属性结构,对应信号将要执行的操作和标志信息 */
	struct sigaction sigaction[32];
    /* 进程信号屏蔽码(对应信号位图) */
	long blocked;	/* bitmap of masked signals */
/* various fields */
    /* 任务执行停止的退出码,其父进程会取 */
	int exit_code;
    /* 代码段地址 */
	unsigned long start_code;
    /* 代码长度(字节数) */
    unsinged long end_code;
    /* 代码长度 + 数据长度(字节数) */
    unsigned long end_data;
    /* 总长度(字节数) */
    unsigned long brk;
    /* 堆栈段地址 */
    unsigned long start_stack;
    /* 进程标识号(进程号) */
	long pid;
    /* 父进程号 */
    long father;
    /* 进程组号 */
    long pgrp;
    /* 会话号 */
    long session;
    /* 会话首领 */
    long leader;
    /* 用户标识号(用户 id) */
	unsigned short uid;
    /* 有效用户 id */
    unsigned short euid;
    /* 保存的用户 id */
    unsigned short suid;
    /* 组标识号(组 id) */
	unsigned short gid;
    /* 有效组 id */
    unsigned short egid;
    /* 保存的组 id */
    unsigned short sgid;
    /* 报警定时值(滴答数) */
	long alarm;
    /* 用户态运行时间(滴答数) */
	long utime;
    /* 系统态运行时间(滴答数) */
    long stime;
    /* 子进程用于态运行时间 */
    long cutime;
    /* 子集成系统态运行时间 */
    long cstime;
    /* 进程开始运行时刻 */
    long start_time;
    /* 标志:是否使用了协处理器 */
	unsigned short used_math;
/* file system info */
    /* 进程使用 tty 的子设备号。-1 表示没有使用 */
	int tty;		/* -1 if no tty, so it must be signed */
    /* 文件创建属性屏蔽位 */
	unsigned short umask;
    /* 当前工作目录 i 节点结构 */
	struct m_inode * pwd;
    /* 根目录 i 节点结构 */
	struct m_inode * root;
    /* 执行文件 i 节点结构 */
	struct m_inode * executable;
    /* 执行时关闭文件句柄位图标志 */
	unsigned long close_on_exec;
    /* 进程使用的文件表结构 */
	struct file * filp[NR_OPEN];
    /* 本任务的局部表描述符。 0 空, 1 代码段 cs, 2 数据段和堆栈段 ds&ss */
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
	struct desc_struct ldt[3];
    /* 本进程的任务状态段信息结构 */
/* tss for this task */
	struct tss_struct tss;
};

三、fork 函数定义

fork() 函数在main.c中定义如下:

// init/main.c
static inline _syscall0(int,fork)

_syscall0 是一个宏定义,其实现如下:

// include/unistd.h
#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; \
}

宏定义中 ## 为连接符,即 _NR##name 将替换成 __NR_fork ,这也是一个宏定义:

// include/unistd.h
#define __NR_fork    2

所以 _syscall0(int,fork) 展开后为:

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

从上可知,其定义了 fork 函数,其通过 0x80 中断进入系统调用。 0x80 对应的中断实现为 system_call 函数,其在 sched.c 文件中有定义,如下:

// kernel/sched.c
void sched_init(void)
{
    // ...
    set_system_gate(0x80,&system_call);
}

四、系统调用中断响应

    对于系统调用( int 0x80 )的中断处理过程,可以把它看作是一个"接口"程序。实际上每个系统调用功能的处理过程基本上都是通过调用相应的 C 函数进行的。即所谓的 “Bottom half” 函数。
    这个程序在刚进入时会首先检查 eax 中的功能号是否有效(在给定的范围内),然后保存一些会用到的寄存器到堆栈上。Linux 内核默认地把 段寄存器 ds,es 用于 内核数据段,而 fs 用于 用户数据段。接着通过一个地址跳转表( sys_call_table )调用相应系统调用的 C 函数。在 C 函数返回后,程序就把返回值压入堆栈保存起来。
    接下来,该程序查看执行本次调用进程的状态。如果由于上面 C 函数的操作或其他情况而使进程的状态从执行态变成了其他状态,或者由于时间片已经用完(counter==0),则调用进程调度函数 schedule()(jmp _schedule) 。由于在执行 “jmp _schedule” 之前已经把返回地址 ret_from_sys_call 入栈,因此在执行完 schedule() 后最终会返回到 ret_from_sys_call 处继续执行。
    从 ret_from_sys_call 标号处开始的代码执行一些系统调用的后处理工作。主要判断当前进程是否是初始进程 0,如果是就直接退出此次系统调用中断返回。否则再根据代码段描述符和所使用的堆栈来判断本次统调用的进程是否是一个普通进程,若不是则说明是内核进程(例如初始进程 1 )或其他。则也立刻弹出堆栈内容退出系统调用中断。末端的一块代码用来处理调用系统调用进程的信号。若进程结构的信号位图表明该进程有接收到信号,则调用信号处理函数 do_signal()。
最后,该程序恢复保存的寄存器内容,退出此次中断处理过程并返回调用程序。若有信号时则程序会首先"返回"到相应信号处理函数中去执行,然后返回调用 system_call 的程序。

系统调用流程:


Linux 0.11 fork 函数(二)_运维

1、system_call 函数

system_call 函数定义在 kernel/system_call.s 文件中

# kernel/system_call.s
.globl system_call,sys_fork,timer_interrupt,sys_execve
.globl hd_interrupt,floppy_interrupt,parallel_interrupt
.globl device_not_available, coprocessor_error
 
.align 2
bad_sys_call:
    movl $-1,%eax
    iret
.align 2
reschedule:
    pushl $ret_from_sys_call
    jmp schedule
.align 2
system_call:
    cmpl $nr_system_calls-1,%eax
    ja bad_sys_call
    push %ds
    push %es
    push %fs
    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
    movl $0x17,%edx        # fs points to local data space
    mov %dx,%fs
    call *sys_call_table(,%eax,4)  # 调用地址sys_call_table + 4 * %eax
    pushl %eax
    movl current,%eax
    cmpl $0,state(%eax)        # state
    jne reschedule
    cmpl $0,counter(%eax)        # counter
    je reschedule
ret_from_sys_call:
    movl current,%eax        # task[0] cannot have signals
    cmpl task,%eax
    je 3f
    cmpw $0x0f,CS(%esp)        # was old code segment supervisor ?
    jne 3f
    cmpw $0x17,OLDSS(%esp)        # was stack segment = 0x17 ?
    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
3:    popl %eax
    popl %ebx
    popl %ecx
    popl %edx
    pop %fs
    pop %es
    pop %ds
    iret

其会调用 sys_call_table 中预定义的函数,此处即为 sys_fork

// include/linux/sys.h
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, sys_iam, sys_whoami };

2、sys_fork 函数

sys_fork 函数定义在 system_call.s中。

# kernel/system_call.s
.align 2
sys_fork:
    call find_empty_process
    testl %eax,%eax
    js 1f
    push %gs
    pushl %esi
    pushl %edi
    pushl %ebp
    pushl %eax
    call copy_process
    addl $20,%esp
1:    ret

sys_fork 先调用 find_empty_process 函数找到空闲的进程(内核中定义了64个,NR_TASKS),其返回内部进程序列。然后调用 copy_process 函数。

find_empty_process 函数

该函数为新进程取得不重复的进程号。函数返回在任务数组中的任务号。

// kernel/fork.c
int find_empty_process(void)
{
    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) goto repeat;
    for(i=1 ; i<NR_TASKS ; i++)    // 任务0项,因为常驻,所以不使用
        if (!task[i])
            return i;
    return -EAGAIN;
}

copy_process 函数

    用于创建并复制进程的代码段和数据段以及环境。在进程复制过程中,工作主要牵涉到进程数据结构中信息的设置。系统首先为新建进程在主内存区中申请一页内存来存放其任务数据结构信息,并复制当前进程任务数据结构中的所有内容作为新进程任务数据结构的模板。

// kernel/fork.c
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;
    
    // NOTE!: the following statement now work with gcc 4.3.2 now, and you
    // must compile _THIS_ memcpy without no -O of gcc.#ifndef GCC4_3
    *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;
}

copy_mem 函数

  该函数复制内存页表。参数 nr 是新任务号,p 是新任务数据结构指针。该函数为新任务在线性地址空间中设置代码段和数据段基址、限长,并复制页表。由于Linux采用了写时复制技术,因此这里仅为新进程设置自己的页目录表项和页表项,而没有实际为新进程分配物理内存页面。此时新进程与其父进程共享所有内存页面。

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 * 0x4000000;
    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)) {
        printk("free_page_tables: from copy_mem\n");
        free_page_tables(new_data_base,data_limit);
        return -ENOMEM;
    }
    return 0;
}