linux在初始的时候会调用fork系统调用来创建第一个进程,他被成为零号进程,创建后会一直存在并且零号进程是所有进程的父进程。首先看任务的结构体,前面是进程的状态、时间片、优先级、信号、打开文件等等。重要的是最后TSS结构体。
struct task_struct {
/* these are hardcoded - don't touch */
long state; /* -1 unrunnable, 0 runnable, >0 stopped */
long counter;
long priority;
long signal;
struct sigaction sigaction[32];
long blocked; /* bitmap of masked signals */
/* various fields */
int exit_code;
unsigned long start_code,end_code,end_data,brk,start_stack;
long pid,father,pgrp,session,leader;
unsigned short uid,euid,suid;
unsigned short gid,egid,sgid;
long alarm;
long utime,stime,cutime,cstime,start_time;
unsigned short used_math;
/* file system info */
int tty; /* -1 if no tty, so it must be signed */
unsigned short umask;
struct m_inode * pwd;
struct m_inode * root;
struct m_inode * executable;
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];
/* tss for this task */
struct tss_struct tss;
};
TSS段存放的是进程的一些寄存器的状态标识,当CPU运行某个进程时,需要将这个进程的TSS段放入CPU中,TSS实际就是CPU在运行时产生的一些结果会放到这些寄存器中,当再次调用这个进程时再把TSS段放进CPU中。如下图:
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;
};
下面前面的代码是内存拷贝,需要将linux的信息从硬盘中移到内存里,之后进行内存、向量等的初始化。这次首先先看sched_init的函数。
// 内核初始化主程序。初始化结束后将以任务0(idle任务即空闲任务)的身份运行。
void main(void) /* This really IS void, no error here. */
{ /* The startup routine assumes (well, ...) this */
/*
* Interrupts are still disabled. Do necessary setups, then
* enable them
*/
// 下面这段代码用于保存:
// 根设备号 ->ROOT_DEV;高速缓存末端地址->buffer_memory_end;
// 机器内存数->memory_end;主内存开始地址->main_memory_start;
// 其中ROOT_DEV已在前面包含进的fs.h文件中声明为extern int
ROOT_DEV = ORIG_ROOT_DEV;
drive_info = DRIVE_INFO; // 复制0x90080处的硬盘参数
memory_end = (1<<20) + (EXT_MEM_K<<10); // 内存大小=1Mb + 扩展内存(k)*1024 byte
memory_end &= 0xfffff000; // 忽略不到4kb(1页)的内存数
if (memory_end > 16*1024*1024) // 内存超过16Mb,则按16Mb计
memory_end = 16*1024*1024;
if (memory_end > 12*1024*1024) // 如果内存>12Mb,则设置缓冲区末端=4Mb
buffer_memory_end = 4*1024*1024;
else if (memory_end > 6*1024*1024) // 否则若内存>6Mb,则设置缓冲区末端=2Mb
buffer_memory_end = 2*1024*1024;
else
buffer_memory_end = 1*1024*1024; // 否则设置缓冲区末端=1Mb
main_memory_start = buffer_memory_end;
// 如果在Makefile文件中定义了内存虚拟盘符号RAMDISK,则初始化虚拟盘。此时主内存将减少。
#ifdef RAMDISK
main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
#endif
// 以下是内核进行所有方面的初始化工作。阅读时最好跟着调用的程序深入进去看,若实在
// 看不下去了,就先放一放,继续看下一个初始化调用。——这是经验之谈。o(∩_∩)o 。;-)
mem_init(main_memory_start,memory_end); // 主内存区初始化。mm/memory.c
trap_init(); // 陷阱门(硬件中断向量)初始化,kernel/traps.c
blk_dev_init(); // 块设备初始化,kernel/blk_drv/ll_rw_blk.c
chr_dev_init(); // 字符设备初始化, kernel/chr_drv/tty_io.c
tty_init(); // tty初始化, kernel/chr_drv/tty_io.c
time_init(); // 设置开机启动时间 startup_time
sched_init(); // 调度程序初始化(加载任务0的tr,ldtr)(kernel/sched.c)
// 缓冲管理初始化,建内存链表等。(fs/buffer.c)
buffer_init(buffer_memory_end);
hd_init(); // 硬盘初始化,kernel/blk_drv/hd.c
floppy_init(); // 软驱初始化,kernel/blk_drv/floppy.c
sti(); // 所有初始化工作都做完了,开启中断
// 下面过程通过在堆栈中设置的参数,利用中断返回指令启动任务0执行。
move_to_user_mode(); // 移到用户模式下执行
if (!fork()) { /* we count on this going ok */
init(); // 在新建的子进程(任务1)中执行。
}
提示语句后就是设置TSS段和LDT段,gdt是一个全局描述符表数组,gdt加上TSS的初始化地址,gdt加上LDT的起始地址,再把初始化任务的LDT和TSS段放入这两个地址中。他的目的就是在全局描述符表中设置初始任务(任务0)的任务状态段描述符和局部数据表描述符。
后面清任务数组TASK[]和描述符表项(注意 i=1 开始,所以初始任务的描述符还在)。最后设置时间中断和系统调用。
// 内核调度程序的初始化子程序
void sched_init(void)
{
int i;
struct desc_struct * p; // 描述符表结构指针
// Linux系统开发之初,内核不成熟。内核代码会被经常修改。Linus怕自己无意中修改了
// 这些关键性的数据结构,造成与POSIX标准的不兼容。这里加入下面这个判断语句并无
// 必要,纯粹是为了提醒自己以及其他修改内核代码的人。
if (sizeof(struct sigaction) != 16) // sigaction 是存放有关信号状态的结构
panic("Struct sigaction MUST be 16 bytes");
// 在全局描述符表中设置初始任务(任务0)的任务状态段描述符和局部数据表描述符。
// FIRST_TSS_ENTRY和FIRST_LDT_ENTRY的值分别是4和5,定义在include/linux/sched.h
// 中;gdt是一个描述符表数组(include/linux/head.h),实际上对应程序head.s中
// 全局描述符表基址(_gdt).因此gtd+FIRST_TSS_ENTRY即为gdt[FIRST_TSS_ENTRY](即为gdt[4]),
// 也即gdt数组第4项的地址。
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
// 清任务数组和描述符表项(注意 i=1 开始,所以初始任务的描述符还在)。描述符项结构
// 定义在文件include/linux/head.h中。
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 */
// NT标志用于控制程序的递归调用(Nested Task)。当NT置位时,那么当前中断任务执行
// iret指令时就会引起任务切换。NT指出TSS中的back_link字段是否有效。
__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl"); // 复位NT标志
ltr(0);
lldt(0);
// 下面代码用于初始化8253定时器。通道0,选择工作方式3,二进制计数方式。通道0的
// 输出引脚接在中断控制主芯片的IRQ0上,它每10毫秒发出一个IRQ0请求。LATCH是初始
// 定时计数值。
outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff , 0x40); /* LSB */
outb(LATCH >> 8 , 0x40); /* MSB */
// 设置时钟中断处理程序句柄(设置时钟中断门)。修改中断控制器屏蔽码,允许时钟中断。
// 然后设置系统调用中断门。这两个设置中断描述符表IDT中描述符在宏定义在文件
// include/asm/system.h中。
set_intr_gate(0x20,&timer_interrupt);
outb(inb_p(0x21)&~0x01,0x21);
set_system_gate(0x80,&system_call);
}
再回到main函数,初始化结束后,调用sti函数打开中断,之后调用move_to_user_mode函数从内核态切换回用户态,然后调用fork创建零号进程,创建成功后调用init函数初始化。
总结:在内核初始化的过程中,会手动创建0号进程。
下来看看init函数做了什么事情,在main中已经完成了系统的初始化和0号进程的创建,init函数运行在任务0第一次创建进程1中,初始化shell环境,登录shell的方式加载程序执行。打开标准输入输出错误输出。再次调用fork创建进程1,关闭标准输入以只读的方式打开/etc/rc文件,再调用execve系统调用将自身程序换成了shell程序,执行后所携带的参数和环境变量分别由argv_rc和envp_rc数组给出。关闭句柄0并立即打开/etc/rc文件的作用是把标准输入stdin重定向到/etc/rc文件。 这样shell程序/bin/sh就可以运行rc文件中的命令。由于这里的sh的运行方式是非交互的,因此在执行完rc命令后就会立刻退出,进程2也随之结束。后面是父进程为子进程收尸。
第一个进程结束后,进入while1循环创建进程,如果成功则关闭0/1/2句柄,新建一个会话并设立进程组号,重新打开/dev/tty0作为标准输入,打开句柄1/2,再次执行shell,这次的环境参数改变了。最后留下wait一直循环收尸,他也会处理以后运行留下的孤儿进程。(如果进程的父进程没了,系统会默认这个进程的父进程为1号进程)
// 在main()中已经进行了系统初始化,包括内存管理、各种硬件设备和驱动程序。init()函数
// 运行在任务0第1次创建的子进程(任务1)中。它首先对第一个将要执行的程序(shell)的环境
// 进行初始化,然后以登录shell方式加载该程序并执行。
void init(void)
{
int pid,i;
// setup()是一个系统调用。用于读取硬盘参数包括分区表信息并加载虚拟盘(若存在的话)
// 和安装根文件系统设备。该函数用25行上的宏定义,对应函数是sys_setup(),在块设备
// 子目录kernel/blk_drv/hd.c中。
setup((void *) &drive_info); // drive_info结构是2个硬盘参数表
// 下面以读写访问方式打开设备"/dev/tty0",它对应终端控制台。由于这是第一次打开文件
// 操作,因此产生的文件句柄号(文件描述符)肯定是0。该句柄是UNIX类操作系统默认的
// 控制台标准输入句柄stdin。这里再把它以读和写的方式别人打开是为了复制产生标准输出(写)
// 句柄stdout和标准出错输出句柄stderr。函数前面的"(void)"前缀用于表示强制函数无需返回值。
(void) open("/dev/tty0",O_RDWR,0);
(void) dup(0); // 复制句柄,产生句柄1号——stdout标准输出设备
(void) dup(0); // 复制句柄,产生句柄2号——stderr标准出错输出设备
// 打印缓冲区块数和总字节数,每块1024字节,以及主内存区空闲内存字节数
printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS,
NR_BUFFERS*BLOCK_SIZE);
printf("Free mem: %d bytes\n\r",memory_end-main_memory_start);
// 下面fork()用于创建一个子进程(任务2)。对于被创建的子进程,fork()将返回0值,对于
// 原进程(父进程)则返回子进程的进程号pid。该子进程关闭了句柄0(stdin)、以只读方式打开
// /etc/rc文件,并使用execve()函数将进程自身替换成/bin/sh程序(即shell程序),然后
// 执行/bin/sh程序。然后执行/bin/sh程序。所携带的参数和环境变量分别由argv_rc和envp_rc
// 数组给出。关闭句柄0并立即打开/etc/rc文件的作用是把标准输入stdin重定向到/etc/rc文件。
// 这样shell程序/bin/sh就可以运行rc文件中的命令。由于这里的sh的运行方式是非交互的,
// 因此在执行完rc命令后就会立刻退出,进程2也随之结束。
// _exit()退出时出错码1 - 操作未许可;2 - 文件或目录不存在。
if (!(pid=fork())) {
close(0);
if (open("/etc/rc",O_RDONLY,0))
_exit(1); // 如果打开文件失败,则退出(lib/_exit.c)
execve("/bin/sh",argv_rc,envp_rc); // 替换成/bin/sh程序并执行
_exit(2); // 若execve()执行失败则退出。
}
// 下面还是父进程(1)执行语句。wait()等待子进程停止或终止,返回值应是子进程的进程号(pid).
// 这三句的作用是父进程等待子进程的结束。&i是存放返回状态信息的位置。如果wait()返回值
// 不等于子进程号,则继续等待。
if (pid>0)
while (pid != wait(&i))
/* nothing */;
// 如果执行到这里,说明刚创建的子进程的执行已停止或终止了。下面循环中首先再创建
// 一个子进程,如果出错,则显示“初始化程序创建子进程失败”信息并继续执行。对于所
// 创建的子进程将关闭所有以前还遗留的句柄(stdin, stdout, stderr),新创建一个会话
// 并设置进程组号,然后重新打开/dev/tty0作为stdin,并复制成stdout和sdterr.再次
// 执行系统解释程序/bin/sh。但这次执行所选用的参数和环境数组另选了一套。然后父
// 进程再次运行wait()等待。如果子进程又停止了执行,则在标准输出上显示出错信息
// “子进程pid挺直了运行,返回码是i”,然后继续重试下去....,形成一个“大”循环。
// 此外,wait()的另外一个功能是处理孤儿进程。如果一个进程的父进程先终止了,那么
// 这个进程的父进程就会被设置为这里的init进程(进程1),并由init进程负责释放一个
// 已终止进程的任务数据结构等资源。
while (1) {
if ((pid=fork())<0) {
printf("Fork failed in init\r\n");
continue;
}
if (!pid) { // 新的子进程
close(0);close(1);close(2);
setsid(); // 创建一新的会话期
(void) open("/dev/tty0",O_RDWR,0);
(void) dup(0);
(void) dup(0);
_exit(execve("/bin/sh",argv,envp));
}
while (1)
if (pid == wait(&i))
break;
printf("\n\rchild %d died with code %04x\n\r",pid,i);
sync(); // 同步操作,刷新缓冲区。
}
// _exit()和exit()都用于正常终止一个函数。但_exit()直接是一个sys_exit系统调用,
// 而exit()则通常是普通函数库中的一个函数。它会先执行一些清除操作,例如调用
// 执行各终止处理程序、关闭所有标准IO等,然后调用sys_exit。
_exit(0); /* NOTE! _exit, not exit() */
}
这里注意一下 _exit()和exit()都用于正常终止一个函数。但_exit()直接是一个sys_exit系统调用,而exit()则通常是普通函数库中的一个函数。它会先执行一些清除操作,例如调用执行各终止处理程序、关闭所有标准IO等,然后调用sys_exit。
进程的初始化总结:在零号进程中,打开标准输入输出错误的控制台句柄,再创建1号进程,如果创建成功,则在一号进程中打开/etc/re文件,并将程序转化为shell程序‘/bin/sh’。0号进程不可能结束,他会在空闲时被调用,只会执行等待的命令。就像ucos的空闲任务。
下面看进程是如何创建的:
fork:
1、在task链表中找一个进程空位存放当前的进程。
2、创建一个task_struct
3、设置task_struct
进程的创建也可以被视为0号进程或者当前进程的复制,本质还是对task[0]对应的task_struct复制给新建的task_struct。
下面看fork.c里面的函数:因为fork是系统调用,所以下面的函数是在system_call.s中被调用。
void verify_area(void * addr,int size)进程空间区域写前验证函数
int copy_mem(int nr,struct task_struct * p)复制内存页表
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)
int find_empty_process(void) 为新进程取得不重复的进程号last_pid.函数返回在任务数组中的任务号(数组项)。
### sys_fork()调用,用于创建子进程,是system_call功能2.
# 首先调用C函数find_empty_process(),取得一个进程号PID。若返回负数则说明目前任务数组
# 已满。然后调用copy_process()复制进程。
.align 2
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
在sys_fork中首先调用了find_empty_process函数,这个函数可以在任务数组里找到一个不重复的进程号,后面调用copy_process函数复制进程,这样一个进程就创建了。下面先放find_empty_process函数代码:
// 为新进程取得不重复的进程号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项被排除在外
if (!task[i])
return i;
return -EAGAIN;
}
copy_process:入口参数包含了许多寄存器,这是为了代码的灵活性,可以直接继承父进程,也可以自行修改。p = (struct task_struct *) get_free_page();相当于从堆中申请内存,可以用kalloc来实现,将申请成功的内存页面连入任务链表中,再把当前任务的结构内容给申请的内存页面。后面是对传入结构的成员进行一些修改,将进程状态改为不可中断,防止内核调度,然后设置新进程的pid和父进程号,并初始化进程运行时间片等一系列结构,tss段的初始化是直接寄存器赋值。之后调用copy_mem函数来拷贝内存,如果失败则会将申请的任务释放退出,如果父进程中有文件是打开的,则将对应文件的打开次数增1,因为这里创建的子进程会与父进程共享这些打开的文件。将当前进程(父进程)的pwd,root和executable引用次数均增1与上面同样的道理,子进程也引用了这些i节点。随后在GDT中设置新任务的LDT和TSS段,将任务状态由不可中断态转为就绪态,最后返回新进程的进程号,这样就组装了一个进程。
// 复制进程
// 该函数的参数进入系统调用中断处理过程开始,直到调用本系统调用处理过程
// 和调用本函数前时逐步压入栈的各寄存器的值。这些在system_call.s程序中
// 逐步压入栈的值(参数)包括:
// 1. CPU执行中断指令压入的用户栈地址ss和esp,标志寄存器eflags和返回地址cs和eip;
// 2. 在刚进入system_call时压入栈的段寄存器ds、es、fs和edx、ecx、ebx;
// 3. 调用sys_call_table中sys_fork函数时压入栈的返回地址(用参数none表示);
// 4. 在调用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;
// 首先为新任务数据结构分配内存。如果内存分配出错,则返回出错码并退出。
// 然后将新任务结构指针放入任务数组的nr项中。其中nr为任务号,由前面
// find_empty_process()返回。接着把当前进程任务结构内容复制到刚申请到
// 的内存页面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值
// 接着复位新进程的信号位图、报警定时值、会话(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; // 报警定时值(滴答数)
p->leader = 0; /* process leadership doesn't inherit */
p->utime = p->stime = 0; // 用户态时间和和心态运行时间
p->cutime = p->cstime = 0; // 子进程用户态和和心态运行时间
p->start_time = jiffies; // 进程开始运行时间(当前时间滴答数)
// 再修改任务状态段TSS数据,由于系统给任务结构p分配了1页新内存,所以(PAGE_SIZE+
// (long)p)让esp0正好指向该页顶端。ss0:esp0用作程序在内核态执行时的栈。另外,
// 每个任务在GDT表中都有两个段描述符,一个是任务的TSS段描述符,另一个是任务的LDT
// 表描述符。下面语句就是把GDT中本任务LDT段描述符和选择符保存在本任务的TSS段中。
// 当CPU执行切换任务时,会自动从TSS中把LDT段描述符的选择符加载到ldtr寄存器中。
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用于把协处理器的所有状态保存到目的操作数
// 指定的内存区域中。
if (last_task_used_math == current)
__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
// 接下来复制进程页表。即在线性地址空间中设置新任务代码段和数据段描述符中的基址和限长,
// 并复制页表。如果出错(返回值不是0),则复位任务数组中相应项并释放为该新任务分配的用于
// 任务结构的内存页。
if (copy_mem(nr,p)) {
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()在system.h中定义。"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;
}
上面复制进程时提到了拷贝内存,下面看看copy_mem函数。
会创建大小为64MB的一块内存,新进程与父进程共享所有内存页面。首先获取父进程的代码段和数据段的基地址,判断数据段和代码段基地址是否一致,限长是否一致。后面设置新的局部描述符,将父进程的LDT中的数据段给子进程,即复制父进程的页目录表项和页表项。
copy_mem总结就是进行老进程向新进程代码段数据段(LDT)的拷贝。
// 复制内存页表
// 参数nr是新任务号:p是新任务数据结构指针。该函数为新任务在线性地址空间中
// 设置代码段和数据段基址、限长,并复制页表。由于Linux系统采用了写时复制
// (copy on write)技术,因此这里仅为新进程设置自己的页目录表项和页表项,而
// 没有实际为新进程分配物理内存页面。此时新进程与其父进程共享所有内存页面。
// 操作成功返回0,否则返回出错号。
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;
// 首先取当前进程局部描述符表中代表中代码段描述符和数据段描述符项中的
// 的段限长(字节数)。0x0f是代码段选择符:0x17是数据段选择符。然后取
// 当前进程代码段和数据段在线性地址空间中的基地址。由于Linux-0.11内核
// 还不支持代码和数据段分立的情况,因此这里需要检查代码段和数据段基址
// 和限长是否都分别相同。否则内核显示出错信息,并停止运行。
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()返回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;
}