进程管理_linux0.11
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
Linux0.11的地址
有3类地址需要区分清楚:
1. 程序(进程)的虚拟地址和逻辑地址
虚拟地址(virtual address )指程序产生的有段选择符和段内偏移两部分组成的地址。
一个程序的虚拟地址空间有GDT 映射的全地址空间和LDT 映射的局部地址空间组成。
逻辑地址(logical address )指程序产生的段内偏移地址。
应用程序只与逻辑地址打交道,分段分页对应用程序来说是透明的。
也就是说C 语言中的&,汇编语言中的符号地址,C 中嵌入式汇编的”m ”对应的都是逻辑地址。
2. CPU 的线性地址
线性地址(linear address )是逻辑地址到物理地址变换的中间层,是处理器可寻址空间的地址。
程序代码产生的逻辑地址加上段基地址就产生了线性地址。
3. 实际物理内存地址
物理地址(physical address )是CPU 外部地址总线上的寻址信号,是地址变换的最终结果,一个物理地址始终对应实际内存中的一个存储单元。
对80386保护模式来说,如果开启分页机制,线性地址经过页变换产生物理地址。
如果没有开启分页机制,线性地址直接对应物理地址。
页目录表项、页表项对应都是物理地址。
Linux 0.11的内核数据段,内核代码段基地址都是0,所以对内核来说,逻辑地址就是线性地址。
又因为1个页目录表和4个页表完全映射16M 物理内存,所以线性地址也就是物理地址。
故对linux0.11内核来说,逻辑地址,线性地址,物理地址重合。
与80386段相关的宏定义
set_seg_desc
/*****************************************************************************/ /* 功能: 设置存储段描述符,把指定属性的段描述符放入gate_addr 处 */ /* 参数: gate_addr 段描述符的目的地址上面格式 */ /* type 描述符中类型域,具体见80386基础一节中的表格 */ /* dpl 描述符中特权级 */ /* base 段基地址,这是线性地址 */ /* limit 段限长 */ /* 返回: (无) */ /*****************************************************************************/ #define _set_seg_desc(gate_addr,type,dpl,base,limit) {\
0 7 15 23 31
// 把段描述符的第4-7字节放入gate_addr处
*((gate_addr)+1) = ((base) & 0xff000000) | \ // base的31..24位放入gate_addr的31..24位(((base) & 0x00ff0000)>>16) | \ //base的23..16位放入gate_addr的7..0位
((limit) & 0xf0000) | \ //limit的19..16位放入gate_addr的19..16位
((dpl)<<13) | \ // dpl放入gate_addr的14..13位
(0x00408000) | \ // 把P位和D位设置位1,G置为0
((type)<<8); \ // type放入gate_addr的11..8位
// 把段描述符的第0-3字节放入gate_addr+1处
*(gate_addr)= (((base) & 0x0000ffff)<<16) | \ //base的15..0放入gate+1的31..15位((limit) & 0x0ffff); } //limit的15..0位放入gate+1的15..0位
set_tssldt_desc
/*****************************************************************************/ /* 功能: 设置系统段描述符,把指定属性的段描述符放入GDT中*/ /* 表项n对应的地址处,*/ /* 参数: n GDT中表项n对应的地址*/ /* addr 系统段的基地址,这是一个线性地址*/ /* type 描述符中类型域,具体见80386基础一节中的表格*/ /* 0x89表示386TSS段描述符,0x82表示LDT段*/ /* 这里8是为了设置P位为1 */ /* 返回: (无) */ /*****************************************************************************/ // %0 寄存器eax addr
// %1-%6 物理地址符号项n地址-n+7的地址
#define _set_tssldt_desc(n,addr,type) \
__asm__ ("movw $104,%1\n\t" \ // 把TSS的限长104字节放入n地址处,
// 这样ldt的限长也定为104,这没有关系,因为linux0.11
// 中一个任务的ldt只有3个表项
"movw %%ax,%2\n\t" \ // 把addr的15..0位(在ax中)放入n+2处
"rorl $16,%%eax\n\t" \ // 把addr的高16位(eax中)放入ax中
"movb %%al,%3\n\t" \ //addr的23..16位放入n+4中
"movb $" type ",%4\n\t" \ // 把type字段放入n+5中
"movb $0x00,%5\n\t" \ // 把G置为0,说明粒度是字节。
// 因为限长定死为104,所以高位肯定是0
"movb %%ah,%6\n\t" \ // 把addr的31..24位放入n+7中
"rorl $16,%%eax" \ // eax清0
::"a" (addr), "m" (*(n)), "m" (*(n+2)), "m" (*(n+4)), \
"m" (*(n+5)), "m" (*(n+6)), "m" (*(n+7)) \
)
#define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x89")
#define set_ldt_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x82")
对n,addr地址的说明
set_tss_desc() 和set_ldt_desc()在两个地方被调用:sched_init()和copy_process ()中。
在sched_init()中:
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
在copy_process ()中
set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt))
这里传入的地址都是逻辑地址,但是因为3种地址重合,所以也没问题。
内核空间、用户空间之间的数据传输内核空间数据段的选择符为0x10,用户空间数据段选择符为0x17。
内核空间、用户空间之间的数据传输,是段间数据传输。
C语言中的赋值语句编译成汇编后,“=”两边的变量默认段选择符都是DS,因此只能用于同一段内数据传输。
在segment.h中定义了一系列用于内核空间和用户空间传输数据的函数。
从用户空间取得数据的函数中,mov指令的源操作数段寄存器都明确指出是fs,向用户空间写数据的函数中,mov指令的目的操作数段寄存器都是fs。
当系统调用发生时,int 0x80处理函数会把fs设成用户数据段选择符(0x17),参见中断异常处理和系统调用一章。
下面分析一组对byte操作的函数,其他的对word和long操作的函数与之类似。
get_fs_byte()
// 功能:从用户空间中addr地址处取出一个字节
// 参数:addr 用户空间中的逻辑地址
// 返回:fs:[addr]处的一个字节内容
extern inline unsigned char get_fs_byte(const char * addr)
{
unsigned register char _v;
// addr是逻辑地址,也就是用户数据段内的偏移。
// 而当前数据段为内核数据段,所以要写成fs:[addr],这是虚拟地址
__asm__ ("movb %%fs:%1,%0":"=r" (_v):"m" (*addr));
return _v;
}
put_fs_byte()
// 功能:向用户空间中addr地址处写一个字节的内容
// 参数:val 要写入的数据
// addr 用户空间中的逻辑地址
// 返回:(无)
extern inline void put_fs_byte(char val,char *addr)
{ // addr是相对于用户数据段的偏移,而当前数据段为内核数据段
// 所以要写成fs:[addr]的形式
__asm__ ("movb %0,%%fs:%1"::"r" (val),"m" (*addr));
}
Linux 0.11相关进程数据结构
主要有4个数据结构
task_union (sched.c第53行)
// 这实际上是一页内存,页面低端头部放的是task_struct(进程控制块)结构,页面
// 其他部分当作进程的内核态堆栈使用
union task_union {
struct task_struct task;
char stack[PAGE_SIZE];
};
task[NR_TASKS] (sched.c第65行)
// task_struct指针数组,每个进程的task_struct指针都保存在这个数组中。
虽然指针类型是// task_struct*,但实际上指向的是一页内存,其中包括了进程的内核态堆栈。
// task[0] 以及被手工初始化成init_task
struct task_struct * task[NR_TASKS] = {&(init_task.task), };
tss_struct(sched.h第53行)
// 任务段数据,与80386的tss结构对应
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;
};
task_struct(sched.c第80行)
// 进程控制块
struct task_struct {
/*----------------------- these are hardcoded - don't touch -----------------------*/
long state; // 进程运行状态(-1不可运行,0可运行,>0以停止)
long counter; // 任务运行时间片,递减到0是说明时间片用完
long priority; // 任务运行优先数,刚开始是counter=priority
long signal; // 任务的信号位图,信号值=偏移+1
struct sigaction sigaction[32]; //信号执行属性结构,对应信号将要执行的操作和标志信息long blocked; // 信号屏蔽码
/*----------------------------------- various fields--------------------------------- */
int exit_code; // 任务退出码,当任务结束时其父进程会读取
unsigned long start_code,end_code,end_data,brk,start_stack;
// start_code 代码段起始的线性地址
// end_code 代码段长度
// end_data 代码段长度+数据段长度
// brk 代码段长度+数据段长度+bss段长度
// start_stack 堆栈段起始线性地址
long pid,father,pgrp,session,leader;
// pid 进程号
// father 父进程号
// pgrp 父进程组号
// session 会话号
// leader 会话首领
unsigned short uid,euid,suid;
// uid 用户标id
// euid 有效用户id
// suid 保存的用户id
unsigned short gid,egid,sgid;
// gid 组id
// egid 有效组id
// sgid 保存组id
long alarm; // 报警定时值
long utime,stime,cutime,cstime,start_time;
// utime 用户态运行时间
// stime 内核态运行时间
// cutime 子进程用户态运行时间
// cstime 子进程内核态运行时间
// start_time 进程开始运行时刻
unsigned short used_math; // 标志,是否使用了387协处理器
/* ----------------------------------file system info-------------------------------- */
int tty; // 进程使用tty的子设备号,-1表示没有使用
unsigned short umask; //文件创建属性屏蔽码
struct m_inode * pwd; // 当前工作目录的i节点
struct m_inode * root; // 根目录的i节点
struct m_inode * executable; // 可执行文件的i节点
unsigned long close_on_exec; // 执行时关闭文件句柄位图标志
struct file * filp[NR_OPEN]; // 进程使用的文件
/*------------------ ldt for this task 0 - zero 1 - cs 2 - ds&ss -------------------*/ struct desc_struct ldt[3]; // 本任务的ldt表,0-空,1-代码段,2-数据和堆栈段/* ---------------------------------tss for this task ---------------------------------*/ struct tss_struct tss; // 本任务的tss段
};
进程在线性地址空间的分布(start_code,end_code,end_data,brk,start_stack):
内存映像
Linux0.11完整的内存映像和进程n(n不为0和1)的运行环境:
进程结构
Linux0.11中的每个进程都有如下的结构:
1.在gdt中占有两项,一项是tss段描述符,一项是ldt段描述符。
2.在task数组中占有一项,指向一页物理内存,该物理内存低端是进程控制块task_struct(里面包括tss段和ldt段),其余部分是进程的内核态堆栈。
3.在页目录表和页表中设置有相关项。
Linux0.11中,最多只有64个进程,task数组大小也定义成了64,每个进程与一个task 数组中的项一一对应。
虽然gdt中有256项,但是并不是都用到。
第一个gdt项保留不用,内核用2个项,然后系统为每个进程保留2项。
这样一个进程与2个gdt项一一对应。
也就是linux0.11中task数组和gdt的分配是定死的,比如一个进程的任务数组项好为nr,即它的进程控制块由task[nr]指向,那么它的tss描述符和ldt描述符分别位于gdtr+3*8+nr*16和gdtr+3*8+nr*16+8处。
Linux0.11内存物理地址0处开始放着一页页目录表和四页页表。
这一个页目录表是所有进程共享的。
其后的四页页表正好映射16M物理内存,是内核和进程0的页表。
以后创建进程时页表需要从主内存区申请,而页目录项直接从页目录表中取。
这样进程和页目录表页存在这一一对应关系,任务号为nr的进程,对应页目录的第nr*16~(nr+1)*16一共16个目录项。
这样4G的线性空间由64个进程共享,每个进程分到64M。
三个结构的一一对应关系如图所示
页目录表(所有进程共享)task[] 数组GDT
若干页表
8
进程0
进程0是一个特殊的进程,它是所有其他进程的祖先进程,位于进程树的根节点处。
所有其他的进程都是fork通过系统调用,复制进程0或者其后代进程产生的。
但是进程0却不是通过fork产生的。
进程0什么时候开始运行,或者什么时候出现很难定义,因为进程0的代码就是内核system模块的代码,所以可以认为系统一启动进程0就开始运行。
但是我认为此时进程0还不算真正存在,因为它在gdt中还没设置tss和ldt段描述符。
直到sched_init()中才设置了tss和ldt并且把tss加载到tr寄存器。
所以知道这时进程0才算真正产生。
进程0的另外两个个结构——进程控制块(tss_struct)和页目录、页表是手工设置的,以程序代码形式直接编写在内核代码中。
这也是为什么进程0不是通过fork产生的原因。
在系统启动时,进程0的进程控制块已经在task数组的第0项中了。
下面我们按顺序说明进程0的产生过程。
第一步:进程控制块和页目录、页表的手工设置
进程控制块是直接手工设置好的,在sched.c第115行
INIT_TASK
#define INIT_TASK \
/* state etc */{ 0,15,15, \
/* signals */0,{{},},0, \
/* ec,brk... */0,0,0,0,0,0, \
/* pid etc.. */ 0,-1,0,0,0, \
/* uid etc */0,0,0,0,0,0, \
/* alarm */0,0,0,0,0,0, \
/* math */0, \
/* fs info */-1,0022,NULL,NULL,NULL,0, \
/* filp */{NULL,}, \
{ \
{0,0}, \ // ldt第0项是空
/* ldt */{0x9f,0xc0fa00}, \ //代码段长640K,基地0,G=1,D=1,DPL=3,P=1,TYPE=0x0a {0x9f,0xc0f200}, \ //数据段长640K,基地0,G=1, D=1, DPL=3,P=1, TYPE=0x02 }, \
/*tss*/{0,PAGE_SIZE+(long)&init_task,0x10,0,0,0,0,(long)&pg_dir,\
// esp0 = PAGE_SIZE+(long)&init_task 内核态堆栈指针初始化为页面最后
// ss0 = 0x10 内核态堆栈的段选择符,指向系统数据段描述符,进程0的进程控制
// 块和内核态堆栈都在system模块中
// cr3 = (long)&pg_dir 页目录表,其实linux0.11所有进程共享一个页目录表0,0,0,0,0,0,0,0, \
0,0,0x17,0x17,0x17,0x17,0x17,0x17, \
_LDT(0),0x80000000, \ // ldt表选择符指向gdt中的LDT0处
{} \
}, \
}
task数组的初始化在sched.c第65行,这里task数组的的一项直接指向了进程0的进程控制块。
struct task_struct * task[NR_TASKS] = {&(init_task.task), };
从手工设置的进程控制块,我们可以看到:
1.进程0的代码基地址为0段长为640K,数据段基地址也为0段长也为640K。
也就是说进程0的代码段、进程0的数据段、系统代码段和系统数据段4者重合。
也可以认为系统的代码就是进程0的代码。
2.进程0的进程控制块和内核态堆栈都位于系统模块内。
3.页目录、页表的设置在head.s中完成,进程0的页目录、页表就是系统的页目录、页表。
进程0的页表也位于内核模块中,就是内核模块中的4个页表,它们完全映射了16M 物理内存。
第二步:tss段描述符、ldt段描述符的设置和TR寄存器的加载
系统启动的顺序大致是
bootsect.s、setup.s、head.s和main.c的前一部分内虽然运行的代码可以认为属于进程0,但是tss,ldt和TR寄存器都没有设置好,所以我们认为这时进程0并没有产生。
直到main函数中调用了sched_init()函数,在该函数中设置了进程0的tss段描述符,ldt 段描述符,并且加载TR寄存器,使它指向进程0的tss段。
这时进程0所有的结构才齐全,才可以参与进程调度。
我们可以认为当sched_init()函数结束后进程0正式产生。
sched_init
/*****************************************************************************/ /* 功能: 1. 初始化task数组和GDT(包括设置进程1的LDT和TSS)*/ /* 2. 加载TR和IDTR寄存器*/ /* 3. 设置时钟中断门和系统调用中断门*/ /* 参数:(无)*/ /* 返回:(无)*/ /*****************************************************************************/ void sched_init(void)
{
int i;
struct desc_struct * p;
if (sizeof(struct sigaction) != 16)
panic("Struct sigaction MUST be 16 bytes");
// 在gdt中设置进程0的tss段描述符
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
// 在gdt中设置进程0的ldt段描述符
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
// 下面的循环把gdt和task中其他的项清空
p = gdt+2+FIRST_TSS_ENTRY;
for(i=1;i<NR_TASKS;i++) {
task[i] = NULL;
p->a=p->b=0;
p++;
p->a=p->b=0;
p++;
}
/* Clear NT, so that we won't have troubles with that later on */
__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");
ltr(0); // 把进程0的tss段加载到TR寄存器
lldt(0); // 把进程0的ldt段加载到IDTR寄存器。
// 这是将gdt中进程0的ldt描述符对应的选择符加载到TR中。
CPU将
// 选择符加载到可见部分,将tss的基地址和段长等加载到不可见部分。
// TR寄存器只在这里明确加载一次,以后新任务ldt的加载是CPU根据
// TSS段中LDT字段自动加载。
// 初始化8253定时器
outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff , 0x40); /* LSB */
outb(LATCH >> 8 , 0x40); /* MSB */
set_intr_gate(0x20,&timer_interrupt); // 设置时钟中断门
outb(inb_p(0x21)&~0x01,0x21);
set_system_gate(0x80,&system_call); // 设置系统调用中断门
}
sched_init()函数返回后,进程0的结构已经齐全。
但是请注意这时进程0运行在内核态。
目前进程0使用的堆栈没有改变过,仍然系统在head.s中设置的堆栈。
也就是sched.c 第67行的user_stack。
具体设置过程如下:
在sched.c第69行定义了一个SS:ESP结构
struct {
long * a;
short b;
} stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 };
// SS=0x10,为GDT中内核数据段选择符
// ESP= user_stack [PAGE_SIZE>>2],指向user_stack的末尾
在head.s 第31行有一条lss 指令,把上面stack_start 定义的结构加载到SS 和ESP 寄存器中。
lss _stack_start, %esp
第三步 切换到用户态运行
接下来需要把进程0从内核态
转移到用户态。
在main.c 的137行有一句move_to_user_mode()把进程0从内核态移到用户态运行。
move_to_user_mode()是一个宏定义,在system.h 的第1行。
它利用模拟中断返回的方法把进程0从内核态切换到用户态。
进程0数据段的基地址还是0,所以(原SS : 原ESP )指向的还是当前堆栈(user_stack )的栈顶。
进程0的代码段基地址仍然是0,所以iret 返回的(CS : EIP )指向的就是iret 下一条指令。
这样执行iret 指令后PC 指向iret 下一条指令,堆栈还是user_stack 没变换,改变的仅仅是CPL ,CPL 从0变成了3,进程0完成了从内核态到用户态的切换。
move_to_user_mode()
// 把进程0从内核态切换到用户态去执行,使用的方法是模拟中断调用返回 // 利用指令iret 完成特权级的转变。
#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \ // 当前堆栈指针保存到eax 中
"pushl $0x17\n\t" \ // 当前堆栈段选择符0x17入栈,它指向进程0的数据段描述符
// 因为进程0的代码段、数据段、内核代码段、数据段4者重 // 合,所以它指向的仍然是内核模块区域。
"pushl %%eax\n\t" \ // 把当前堆栈指针入栈。
这样模拟外层堆栈的SS:ESP 。
// 由于进程0数据段选择符0x17对应的还是内核模块,和
// 内核数据段选择符0x10的差别仅在与对应描述符的dpl 和 // 本身rpl 的不同,所以外层堆栈指针指向的还是原来的堆栈 // 即user_stack
"pushfl\n\t" \ // eflags 入栈 "pushl $0x0f\n\t" \ // 进程0代码段选择符入栈,模拟返回的CS "pushl $1f\n\t" \ // 下面标号1的偏移地址入栈,模拟返回的EIP
// 也是由于4段重合,所以这里返回的CS对应的段的基地址与
// 内核代码段基地址一样,都是0,故将返回的CS:EIP就是下
// 面标号1处。
"iret\n" \ // 中断返回。
由于当前CPL=0,将返回的CS的RPL=3,所以
// 不仅仅要改变CS,EIP,还要发生堆栈切换(但实际上堆栈
// 还是user_stack),同时CPL变成3。
"1:\tmovl $0x17,%%eax\n\t" \ // 把数据段寄存器的值设为进程0的数据段
"movw %%ax,%%ds\n\t" \
"movw %%ax,%%es\n\t" \
"movw %%ax,%%fs\n\t" \
"movw %%ax,%%gs" \
:::"ax")
move_to_user_mode()之后进程0运行在了用户态,但是请注意,这时进程0的堆栈仍然是user_stack。
也就是说进程0的用户态堆栈和系统堆栈是共享的,都是user_stack。
在move_to_user_mode(),user_stack当做系统堆栈使用,在之后,变成了进程0的用户堆栈。
进程0的内核态堆栈和其他进程的内核态堆栈一样,都在进程控制块task_struct所在页的高端。
可以看出,进程0的进程控制块,内核堆栈以及用户堆栈都嵌入在系统内核模块中,而不是从主内存区分配的。
之后,进程0通过fork系统调用创建进程1,而它自己永远循环执行pause()。
这样进程0相当与一个空闲进程,只要当前系统中没有其他进程可以运行,就好切换到进程0。
最后一点说明,关于内联函数。
由于fork创建进程是通过完全复制父进程的代码段和数据段的方式实现的,所以在进程0在创建进程1时要求其用户堆栈必须空(fork创建的新进程的内核堆栈是新分配的,不复制父进程堆栈,故内核堆栈肯定是空的)。
为防止堆栈中有额外内容,进程0在move_to_user_mode()之后禁止使用用户态堆栈。
这样fork()和pause()被申明为内联的。
在mian.c的第23-24行可以看到:
static inline _syscall0(int,fork)
static inline _syscall0(int,pause)
用fork创建进程
除了进程0,其它所有的进程都是fork产生的。
子进程是通过复制父进程的数据和代码产生的。
创建结束后,子进程和父进程的代码段、数据段共享。
但是子进程有自己的进程控制块、内核堆栈和页表。
我们知道一个进程需要有如下3个结构
1.task[]数组中的一项,即进程控制块(task_struct)
2.GDT中的两项,即TSS段和LDT段描述符
3.页目录和页表
所以fork()的任务就是为一个新进程构造这3个结构。
sys_fork() 系统调用的实现在2个文件中。
fork.c中的全部和system_call.s中208-291行。
sys_fork()系统调用分成2步完成,第一步调用函数find_empty_process(),在task[]数组中找一项空闲项,第二步调用copy_process() 函数,复制进程。
copy_mem
/*****************************************************************************/ /* 功能:设置新进程的LDT项(数据段描述符和代码段描述符)中的基地址部分*/ /* 并且复制父进程(也就是当前进程)的页目录和页表,*/ /* 实现父子进程数据代码共享*/ /* 参数:nr 新进程任务数组下标*/ /* p 新进程的进程控制块*/ /* 返回:0 (成功),-ENOMEM(出错)*/ /*****************************************************************************/
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]); // 取当前进程数据段基地址,这是线性地址// 0.11进程代码段和数据段基地址必须重合
if (old_data_base != old_code_base)
panic("We don't support separate I&D");
//0.11中数据段代码段的基地址是重合的,都是nr*64M(nr是task[]数组下标),所以//数据段的长度肯定大于代码段长度。
而且copy_page_tables()传入的是data_limit,这// 把代码和数据都包含进去了。
if (data_limit < code_limit)
panic("Bad data_limit");
// 新进程的代码段基地址= 数据段基地址= 64M*nr
new_data_base = new_code_base = nr * 0x4000000;
// 设置进程的起始线性地址
p->start_code = new_code_base;
// 设置新进程的ldt项。
在copy_process()中完全复制父进程的ldt,所以
// 只需重新设置ldt的基地址字段,其他字段和父进程一样
set_base(p->ldt[1],new_code_base);
set_base(p->ldt[2],new_data_base);
// 把线性地址old_data_base处开始,一共data_limit个字节的内存对应的页目录、// 页表复制到线性地址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;
}
copy_process
/*****************************************************************************/ /* 功能:复制进程,把当前进程current复制到task[nr] */ /* 参数:当前进程(current)内核堆栈的所有内容*/ /* 当前进程内核堆栈保存了所有寄存器的值,在程序中要把这些寄存器的值*/ /* 全部复制给子进程,从而给子进程创造和父进程一样的运行环境*/ /* 返回:子进程pid */ /*****************************************************************************/ 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;
// 在主内存区申请一页新的内存,用来放置子进程的task_struct和内核堆栈
// get_free_page()返回的是物理地址
p = (struct task_struct *) get_free_page();
if (!p)
return -EAGAIN;
// 设置task数组中相关项
task[nr] = p;
// 下面的赋值语句仅仅把父基础的task_struct部分全部复制给子进程
// 注意:仅仅复制task_struct部分,内核堆栈不复制,因此子程序的内核堆栈
// 是空的,这也是我们希望的
*p = *current; /* NOTE! this doesn't copy the supervisor stack */
// 下面的很多赋值语句修改子进程的task_struct中若干字段
// 这些字段跟父进程是有差别的
p->state = TASK_UNINTERRUPTIBLE; //子进程设为不可中断状态
p->pid = last_pid; // 设置子进程pid
p->father = current->pid; // 把当前进程pid舍为子进程的father
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;
// 子进程的内核堆栈指针设置为task_struct所在页面的最高端
p->tss.esp0 = PAGE_SIZE + (long) p;
// 子进程的内核堆栈选择符为0x10,指向GDT中系统数据段。
// 注意虽然子进程的内核堆栈位于内核system模块外,在主内存区,但是因为系统数据段// 基地址为0,限长为16M,函概了所有物理内存,故子进程内核堆栈也位于系统数// 段内。
esp0要的是段内偏移,也是因为系统数据段基地址为0,物理地址
// PAGE_SIZE + (long) p 也是段内偏移。
p->tss.ss0 = 0x10;
// 把父进程系统调用返回地址赋给子进程当前运行的eip。
这样当子进程被调度程序选中
// 后他从fork返回地址处开始执行。
p->tss.eip = eip;
p->tss.eflags = eflags;
// eax是函数返回值存放的地方,把子进程的eax设置为0,这样fork在子进程中返回的是0。
// 注意子进程并没有执行fork()函数,子进程的系统堆栈没有进行过操作,当然不会有像// 父进程那样的fork函数调用。
但是当子进程开始运行时,就好像它从fork中返回。
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;
// 设置子进程的ldt。
从这里可以看到,task下标为nr的进程在GDT中的2项一定是
// _LDT(nr)和_TSS(nr)。
task[]中的项和GDT中的2项一一对应。
p->tss.ldt = _LDT(nr);
p->tss.trace_bitmap = 0x80000000;
if (last_task_used_math == current)
__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
// 在copy_mem函数中设置子进程的代码段描述符,数据段描述符,并且复制父进程的
// 页目录、页表。
实现和父进程代码数据的共享。
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++;
// GDT中对应位置(和nr对应)放入子进程的TSS描述符、LDT描述符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 */
// 父进程返回子进程的pid
return last_pid;
}
对copy_process()函数的几点说明
1.子进程的task_struct和内核堆栈位于主内存区,不像进程0那样嵌在内核模块中。
2.子进程复制父进程的LDT段,但是不复制父进程的内核堆栈。
所以父子进程的用户堆栈是共享的,但内核堆栈独立。
同时子进程TSS中esp0设置为页面最高端,也就是内核堆栈为空。
在copy_process()执行过程中,子进程的内核堆栈从来不用。
3.子进程TSS中的eip设置为父进程系统调用返回地址,这样当schedule()选中子进程后,子进程fork()返回地址开始执行。
这也是子进程开始执行的地方。
4.虽然我们进程提到fork()在子进程中返回值为0,但是请注意子进程根本没有执行过任何fork中的代码,跟不可能从fork中返回。
从第3点知道,子进程执行的第一条指令是fork()返回地址处的指令。
从第2点知道,子进程的内核堆栈永远是空的,它根本不可能调用函数。
当子进程开始执行它的第一条指令时,eax为0,同时用户态堆栈内容和父进程一样,这就好像子进程刚刚从fork()返回,且返回值是0。
这仅仅是“好像”而已,但程序无法区分子进程“好像”和父进程的真正返回。
5.新创建的子进程复制了父进程的页目录项和页表,和父进程共享数据、代码和用户堆栈。
从copy_page_tables()中可以看到,子进程在主内存区申请新的空闲页面当作页表,而页目录项直接使用页目录表中从16*nr开始的16个页目录项。
所以linux0.11一共64个进程共享一个页目录表,分享4G的线性地址,故一个进程最大线性空间是64M(64M = 4G/64)。
6.父子进程的页表项中R/W位都变成0,因此任何一个进程对页面执行写操作时,都会产生页写保护异常,进而调用相应的异常处理函数do_wp_page()。
do_wp_page()函数实现写时复制。
子进程的页目录
父进程的页目录
用execve加载可执行文件
进程加载可执行文件是通过系统调用execve()实现的。
一般fork()创建子进程后,会在子进程中调用execve(),把子进程的线性空间映射到新的物理空间,使它跟父进程的物理空间分开,并且加载可执行文件,设置环境参数,设置eip和esp。
execve()完成后子进程有了自己的代码段、和数据段,可以运行加载的可执行文件了。
execve()的申明在/lib/execve.c里面:
_syscall3(int,execve,const char *,file,char **,argv,char **,envp);
需要三个参数,file是要加载运行的文件名,argv,envp都是字符串数组,分别存放参数和环境变量。
实现execve()的代码在/fs/exec.c 和/kernel/system_call.s 中。
/kernel/system_call.s定义了execve()的内核函数sys_execve(),该函数又要调用/fs/exec.c中的do_execve()函数。
sys_execve()
_sys_execve:
lea EIP(%esp),%eax # 把堆栈中存放中断返回的EIP的内存有效地址放入eax中pushl %eax # 把该有效地址入栈,当作do_execve()的一个参数eip
# 在do_execve()中要重新设置进程中断返回后的eip和esp
# 这样eip[0]就是返回的eip,eip[3]就是esp。
call _do_execve
addl $4,%esp
ret
copy_strings()
/*****************************************************************************/ /* 功能:把个数为argc的参数数组argv,复制到参数环境空间p偏移处。
*/ /* 如果p处没有物理页面,则申请一页新的物理页面。
同时设置*/ /* 参数空间页表page中相应项,填入物理地址*/ /* 参数:argc 参数个数*/ /* argv 参数数组*/ /* page 参数空间页表,存放的是页面的物理地址*/ /* p 参数空间中的偏移,始终指向已复制串的头*/ /* from_kmem 字符串来源标志*/ /* from_kmem 字符串来源字符串数组来源*/ /* 0 用户空间用户空间*/ /* 1 用户空间内核空间*./ /* 2 内核空间内核空间*/ /* 返回:参数空间当前头部指针(最后p的值)*/。