Linux 0.11 fork 函数(二)

Linux 0.11 系列文章


Linux 0.11启动过程分析(一)
Linux 0.11 fork 函数(二)
Linux0.11 缺页处理(三)
Linux0.11 根文件系统挂载(四)
Linux0.11 文件打开open函数(五)
Linux0.11 execve函数(六)
Linux0.11 80X86知识(七)
Linux0.11 内核体系结构(八)
Linux0.11 系统调用进程创建与执行(九)
Linux0.11 进程切换(十)
Linux0.11 管道(十一)
Linux0.11 信号(十二)



一、描述

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

二、task_struct 结构体

1、tss_struct 结构体

struct tss_struct {
	long	back_link;	/* 指向前一任务的 TSS 选择符 */  /* 16 high bits zero */  
	long	esp0;		/* 特权级 0 栈中偏移量指针 */
	long	ss0;		/* 特权级 0 堆栈段选择符 */ /* 16 high bits zero */ 
	long	esp1;		/* 特权级 1 栈中偏移量指针 */
	long	ss1;		/* 特权级 1 堆栈段选择符 */  /* 16 high bits zero */  
	long	esp2;		/* 特权级 2 栈中偏移量指针 */
	long	ss2;		/* 特权级 2 堆栈段选择符 */  /* 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;		/* LDT 段选择符 */  /* 16 high bits zero */
	long	trace_bitmap;	/* I/O 位图基地址字段 */	/* bits: trace 0, bitmap 16-31 */  
	struct i387_struct i387;  /*  */
};

struct i387_struct {
	long	cwd;
	long	swd;
	long	twd;
	long	fip;
	long	fcs;
	long	foo;
	long	fos;
	long	st_space[20];	/* 8*10 bytes for each FP-reg = 80 bytes */
};

2、task_struct 结构体

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);
}

四、系统调用中断响应

1、前提知识

系统调用接口

    系统调用(通常称为 syscalls)是 Linux 内核与上层应用程序进行交互通信的唯一接口,参见图 5-4 所示。从对中断机制的说明可知,用户程序通过直接或间接(通过库函数)调用中断 int 0x80,并在 eax 寄存器中指定系统调用功能号,即可使用内核资源,包括系统硬件资源。不过通常应用程序都是使用具有标准接口定义的 C 函数库中的函数间接地使用内核的系统调用,见图 5-19 所示。
在这里插入图片描述

    通常系统调用使用函数形式进行调用,因此可带有一个或多个参数。对于系统调用执行的结果,它会在返回值中表示出来。通常负值表示错误,而 0 则表示成功。在出错的情况下,错误的类型码被存放在全局变量 errno 中。通过调用库函数 perror(),我们可以打印出该错误码对应的出错字符串信息。
    在 Linux 内核中,每个系统调用都具有唯一的一个系统调用功能号。这些功能号定义在文件 include/unistd.h 中第 60 行开始处。例如,write 系统调用的功能号是 4,定义为符号 ___NR_write 。这些系统调用功能号实际上对应于 include/linux/sys.h 中定义的系统调用处理程序指针数组表 sys_call_table[] 中项的索引值。因此 write() 系统调用的处理程序指针就位于该数组的项 4 处。
    另外,我们从 sys_call_table[] 中可以看出,内核中所有系统调用处理函数的名称基本上都是以符号 ‘sys_’ 开始的。例如系统调用 read()在内核原代码中的实现函数就是 sys_read()。

系统调用处理过程

    当应用程序经过库函数向内核发出一个中断调用 int 0x80 时,就开始执行一个系统调用。其中寄存器 eax 中存放着系统调用号,而携带的参数可依次存放在寄存器 ebxecxedx 中。因此 Linux 0.11 内核中用户程序能够向内核最多直接传递三个参数,当然也可以不带参数。处理系统调用中断 int 0x80 的过程是程序 kernel/system_call.s 中的 system_call
    为了方便执行系统调用,内核源代码在 include/unistd.h 文件( 133-183 行)中定义了宏函数 _syscalln(),其中 n 代表携带的参数个数,可以分别 03。因此最多可以直接传递 3 个参数。若需要传递大块数据给内核,则可以传递这块数据的指针值。例如对于 read()系统调用,其定义是:

int read(int fd, char *buf, int n);

若我们在用户程序中直接执行对应的系统调用,那么该系统调用的宏的形式为:

#define __LIBRARY__
#include <unistd.h>

_syscall3(int, read, int, fd, char *, buf, int, n)

    因此我们可以在用户程序中直接使用上面的 _syscall3()来执行一个系统调用 read(),而不用通过 C 函数库作中介。实际上 C 函数库中函数最终调用系统调用的形式和这里给出的完全一样。
    对于 include/unistd.h 中给出的每个系统调用宏,都有 2+2*n 个参数。其中第 1 个参数对应系统调用返回值的类型;第 2 个参数是系统调用的名称;随后是系统调用所携带参数的类型和名称。这个宏会被
扩展成包含内嵌汇编语句的 C 函数,见如下所示。
在这里插入图片描述

   可以看出,这个宏经过展开就是一个读操作系统调用的具体实现。其中使用了嵌入汇编语句以功能
_NR_read(3) 执行了 Linux 的系统中断调用 0x80。该中断调用在 eax(__res)寄存器中返回了实际读取的字节数。若返回的值小于 0,则表示此次读操作出错,于是将出错号取反后存入全局变量 errno 中,并向调用程序返回-1 值。
   当进入内核中的系统调用处理程序 kernel/system_call.s 后,system_call 的代码会首先检查 eax 中的系统调用功能号是否在有效系统调用号范围内,然后根据 sys_call_table[]函数指针表调用执行相应的系统调用处理程序。

call _sys_call_table(,%eax, 4)		// kernel/system_call.s 第 94 行。

   这句汇编语句操作数的含义是间接调用地址在_sys_call_table + %eax * 4 处的函数。由于 sys_call_table[] 指针每项 4 个字节,因此这里需要给系统调用功能号乘上 4。然后用所得到的值从表中获取被调用处理函数的地址。

Linux 系统调用的参数传递方式

   关于 Linux 用户进程向系统中断调用过程传递参数方面,Linux 系统使用了通用寄存器传递方法,例如寄存器 ebxecxedx。这种使用寄存器传递参数方法的一个明显优点就是:当进入系统中断服务程序而保存寄存器值时,这些传递参数的寄存器也被自动地放在了内核态堆栈上,因此用不着再专门对传递参数的寄存器进行特殊处理。这种方法是 Linus 当时所知的最简单最快速的参数专递方法。另外还有一种使用 Intel CPU 提供的系统调用门(System Call gate)的参数传递方法,它在进程用户态堆栈和内核态堆栈自动复制传递的参数。但这种方法使用起来步骤比较复杂。
   另外,在每个系统调用处理函数中应该对传递的参数进行验证,以保证所有参数都合法有效。尤其是用户提供的指针,应该进行严格地审查。以保证指针所指的内存区域范围有效,并且具有相应的读写权限。

调用流程

    对于系统调用( int 0x80 )的中断处理过程,可以把它看作是一个"接口"程序。实际上每个系统调用功能的处理过程基本上都是通过调用相应的 C 函数进行的。即所谓的 “Bottom half” 函数。
    这个程序在刚进入时会首先检查 eax 中的功能号是否有效(在给定的范围内),然后保存一些会用到的寄存器到堆栈上。Linux 内核默认地把 段寄存器 dses 用于 内核数据段,而 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 的程序。

系统调用流程:

在这里插入图片描述

2、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 };

3、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 函数。

3.1 find_empty_process 函数

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

// kernel/fork.c
// 为新进程取得不重复的进程号 last_pid。函数返回在任务数组中的任务号(数组项)。
int find_empty_process(void)
{
	int i;
// 首先获取新的进程号。如果 last_pid 增 1 后超出进程号的正数表示范围,则重新从 1 开始
// 使用 pid 号。 然后在任务数组中搜索刚设置的 pid 号是否已经被任何任务使用。如果是则
// 跳转到函数开始处重新获得一个 pid 号。 接若在任务数组中为新任务寻找一个空闲项,并
// 返回项号。 last_pid 是一个全局变量,不用返回。如果此时任务数组中 64 个项已经被全
// 部占用,则返回出错码。
	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 项被排除在外。任务0项,因为常驻,所以不使用
		if (!task[i])
			return i;
	return -EAGAIN;
}

3.2 copy_process 函数

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

// kernel/fork.c
/*
* OK,下面是主要的 fork 子程序。它复制系统进程信息(task[n])
* 并且设置必要的寄存器。它还整个地复制数据段。
*/
// 复制进程。
// 该函数的参数是进入系统调用中断处理过程(system_call.s)开始,直到调用本系统调用处理
// 过程(system_call.s 第 208 行)和调用本函数前时(system_call. s 第 217 行)逐步压入栈的
// 各寄存器的值。这些在 system_call.s 程序中逐步压入找的值(参数)包括:
// 1) CPU 执行中断指令压入的用户栈地址 ss 和 esp、标志寄存器 eflags 和返回地址 cs 和 eip;
// 2) 第 83--88 行在进入 system_call 时压入栈的段寄存器 ds、es、fs 和 edx、ecx、ebx;
// 3) 第 94 行调用 sys_call_table 中 sys_fork 函数时压入栈的返回地址(用参数 none 表示);
// 4) 第 212--216 行在调用 copy_process(之前压入栈的 gs、esi、edi、ebp 和 eax(nr)值。
// 其中参数 nr 是调用 find_empty_process0 分配的任务数组项号。
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_processO 返回。
// 接着把当前进程任务结构内容复制到刚申请到的内存页面 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 值(一般为 15 个响嘴)。接着
// 复位新进程的信号位图、报警定时值、会话(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;					// 报警定时值(滴答数)。
	/* process leadership doesn't inherit */
	p->leader = 0;					/* 进程的领导权是不能继承的 */
	p->utime = p->stime = 0;		// 用户态时间和核心态运行时间。
	p->cutime = p->cstime = 0;		// 子进程用户态和核心态运行时间。
	p->start_time = jiffies;		// 进程开始运行时间(当前时间篇答数)。

// 再修改任务状态段 TSS 数据(参见列表后说明)。由于系统给任务结构 p 分配了 1 页新
// 内存,所以(PAGE_SIZE +(long) p) 让 esp0 正好指向该页顶端。 ss0:esp0 用作程序。
// 在内核态执行时的栈。另外,在第 3 章中我们已经知道,每个任务在 GDT 表中都有两个。
// 段描述符,一个是任务的 TSS 段描述符,另一个是任务的 LDT 表段描述符。下面 111 行
// 语句就是把 GDT 中本任务 LDT 段描述符的选择符保存在本任务的 TSS 段中。当 CPU 执行
// 切换任务时,会自动从 TSS 中把 LDT 段描述符的选择符加载到 1dtr 寄存器中。
	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 用于把协处理器
// 的所有状态保存到目的操作数指定的内存区域中(tss.i387)。
	if (last_task_used_math == current)
		__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
// 接下来复制进程页表。即在线性地址空间中设置新任务代码段和数据段描述符中的基址
// 和限长,并复制页表。如果出错(返回值不是 0),则复位任务数组中相应项并释放为
// 该新任务分配的用于任务结构的内存页。
	if (copy_mem(nr,p)) {		// 返回不为 0 表示出错。
		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() 的定义参见 include/asm/system.h 文件
// 52-66 行代码。"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;
}
3.2.1 copy_mem 函数

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

// 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;
// 首先取当前进程局部描述符表中代码段描述符和数据段描述符项中的段限长(字节数)。
// 0xOf 是代码段选择符; 0x17 是数据段选择符。然后取当前进程代码段和数据段在线性地址。
// 空间中的基地址。由于 Linux 0.11 内核还不支持代码和数据段分立的情况,因此这里需要。
// 检查代码段和数据段基址和限长是否都分别相同。否则内核显示出错信息,并停止运行。
// get_limitO 和 get_base(定义在 include/linux/sched.h 第 226 行处。
	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");
		
// 然后设置创建中的新进程在线性地址空间中的基地址等于(64MB * 其任务号),并用该值
// 设置新进程局部描述符表中段描述符中的基地址。接着设置新进程的页目录表项和页表项,
// 即复制当前进程(父进程)的页目录表项和页表项。 此时子进程共享父进程的内存页面。
// 正常情况下 copy_page_tables O 返回 0,否则表示出错,则释放刚申请的页表项。
	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;
}
3.2.1.1 copy_page_tables 函数

   一个系统中可以同时存在多个页目录表,而在某个时刻只有一个页目录表可用。当前的页目录表示用 CPU 的寄存器 CR3 来确定的,它存储着当前页目录表的物理内存地址。但本书所讨论的 Linux 内核只使用了一个页目录表。

在这里插入图片描述
   Linux 0.11 中共享一个页目录表,即变量 pg_dir ,其地址为 0,值如下图:
在这里插入图片描述

/*
* 好了,下面是内存管理 nn 中最为复杂的程序之一。它通过只复制内存页面
* 来拷贝一定范围内线性地址中的内容。 希望代码中没有错误。因为我不想。
* 再调试这块代码了:-)。
* 注意!我们并不复制任何内存块 - 内存块的地址需要是 4Mb 的倍数(正好
* 一个页目录项对应的内存长度),因为这样处理可使函数很简单。 不管怎
* 样,它仅被 fork() 使用。
* 注意 2!!   当 from=0 时,说明是在为第一次 fork() 调用复制内核空间。
* 此时我们就不想复制整个页目录项对应的内存,因为这样做会导致内存严
* 重浪费 -  我们只须复制开头 160 个页面 - 对应 640kB。即使是复制这些
* 页面也已经超出我们的需求,但这不会占用更多的内存 - 在低 1Mb 内存
* 范围内我们不执行写时复制操作,所以这些页面可以与内核共享。因此这
* 是 nr=xxxx 的特殊情况(nr 在程序中指页面数)。
*/
// 复制页目录表项和页表项。
// 复制指定线性地址和长度内存对应的页目录项和页表项,从而被复制的页目录和页表对应。
// 的原物理内存页面区被两套页表映射而共享使用。复制时,需申请新页面来存放新页表,
// 原物理内存区将被共享。此后两个进程(父进程和其子进程》将共享内存区,直到有一个
// 进程执行写操作时,内核才会为写操作进程分配新的内存页(写时复制机割)。
// 参数 from,to 是线性地址,size 是需要复制(共享)的内在长度,单位是字节。
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 只有满足这个要求才能保证从一个页表的第 1 项开始复制页表
// 项,并且新页表的最初所有项都是有效的。然后取得源地址和目的地址的起始目录项指针
// (from_dir 和 to_dir》。再根据参数给出的长度 size 计算要复制的内存块占用的页表数
// (即目录项数)。参见前面对 114、115 行的解释。

	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=0),则继续循环处理下一个页目录项。
	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/V 都置位,则就有读写的权限)。然后针对当前处理的页目录项对应
// 的页表,设置需要复制的页面项数。如果是在内核空间,则仅需复制头 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_maр[],
// 于是计算页面号,并以它为索引在页面映射数组相应项中增加引用次数。而对于位于 1MB
// 以下的页面。说明是内核页面。因此不需要对 mem_map 进行设置。因为 mem_map 仅用
// 于管理主内存区中的页面使用情况。 因此对于内核移动到任务 0 中并且调用 fork()创建
// 任务 1 时(用于运行 init()),由于此时复制的页面还仍然都在内核代码区域,因此以下
// 判断中的语句不会执行,任务 0 的页面仍然可以随时读写。只有当调用 fork()的父进程
// 代码处于主内存区《页面位置大于 1MB)时才会执行。这种情况需要在进程调用 execve(),
// 并装载执行了新程序代码时才会出现。
// 180 行语句含义是令源页表项所指内存页也为只读。因为现在开始有两个进程共用内存区了。
// 若其中 1 个进程需要进行写操作,则可以通过页异常写保护处理为执行写操作的进程分配
// 1 页新空闲页面,也即进行写时复制(copy on vrite)操作。
			if (this_page > LOW_MEM) {
				*from_page_table = this_page;
				this_page -= LOW_MEM;
				this_page >>= 12;
				mem_map[this_page]++;
			}
		}
	}
	invalidate();		// 刷新页变换高速缓冲。
	return 0;
}
(a)进程 0 创建 进程 1
// from 的值为 0, to 的值为 67108864 (64MB),size 的值为 655360 (640K)
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;

	if ((from&0x3fffff) || (to&0x3fffff))
		panic("copy_page_tables called with wrong alignment");
	// from_dir 值为 0, 指向 pg_dir 页目录表		
	from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
	// to_dir 值为 0x40
	to_dir = (unsigned long *) ((to>>20) & 0xffc);
	// size 值为 1
	size = ((unsigned) (size+0x3fffff)) >> 22;
	for( ; size-->0 ; from_dir++,to_dir++) {
		if (1 & *to_dir)
			panic("copy_page_tables: already exist");
		if (!(1 & *from_dir))
			continue;
		// 由页目录的值指向页表,pg_dir[0]=4135=0x1027
		// 所以 from_page_table 值为 0x1000, *from_page_table 为 39 			
		from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
		// 分配一个页表,4K 大小
		if (!(to_page_table = (unsigned long *) get_free_page()))
			return -1;	/* Out of memory, see freeing */
		// 在 0x40 处写入刚分配的页表地址,
		// pg_dir 是 unsinged long 类型数组,unsinged long 占 4 字节
		// 所以 0x40 指向的即为 pg_dir[16] 处,
		// 从这可以推测出: 每个进程占用全局页表项 16 个空间		
		*to_dir = ((unsigned long) to_page_table) | 7;
		nr = (from==0)?0xA0:1024;
		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;
			if (this_page > LOW_MEM) {
				*from_page_table = this_page;
				this_page -= LOW_MEM;
				this_page >>= 12;
				mem_map[this_page]++;
			}
		}
	}
	invalidate();
	return 0;
}

   由上可以推测出,Linux 0.11 中每个进程页基址都为 0, 即 CR3 都为 0

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值