不管在什么系统中,所有的任务都是以进程为载体的,所以理解进程的创建对于理解操作系统的原理是非常重要的,本文是我在学习linux内核中所做的笔记,如有错误还请大家批评指正。注:我所阅读的内核版本是0.11。
一、关于PCB
对于一个进程来说,PCB就好像是他的记账先生,当一个进程被创建时PCB就被分配,然后有关进程的所有信息就全都存储在PCB中,例如,打开的文件,页表基址寄存器,进程号等等。在linux中PCB是用结构task_struct来表示的,我们首先来看一下task_struct的组成。
代码位于linux/include/linux/Sched.h
struct task_struct {
long state;
long counter;
long priority;
long signal;
struct sigaction sigaction[32 ];
long blocked;
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;
int tty;
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];
struct desc_struct ldt[3 ];
struct tss_struct tss;
};
二、进程的创建
系统中的进程是由父进程调用fork()函数来创建的,那么调用fork()函数的时候究竟会发生什么呢?
1、引发0x80中断
进程1是由进程0通过fork()创建的,其中的fork代码如下:
init/main.c
#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 0x80中断,调用sys_fork系统调用来创建进程。
2、sys_fork()
_sys_fork:
call _find_empty_process
testl %eax ,%eax
js 1 f
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call _copy_process
addl $2 0 ,%esp
1 : ret
虽然是一段汇编代码,但是我们可以很清楚的看到首先调用的是find_empty_process(),然后又调用了copy_process(),而这两个函数就是fork.c中的函数。下面我们来看一下这两个函数。
3、find_empty_process()
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++)
if (!task [i])
return i;
return -EAGAIN;
}
find_empty_process的作用就是为所要创建的进程分配一个进程号。在内核中用全局变量last_pid来存放系统自开机以来累计的进程数,也将此变量用作新建进程的进程号。内核第一次遍历task[64],如果&&条件成立说明last_pid已经被别的进程使用了,所以++last_pid,直到获取到新的进程号。第二次遍历task[64],获得第一个空闲的i,也就是任务号。因为在linux0.11中,最多允许同时执行64个进程,所以如果当前的进程已满,就会返回-EAGAIN。
4、copy_process()
获得进程号并且将一些寄存器的值压栈后,开始执行copy_process(),该函数主要负责以下的内容。
为子进程创建task_struct,将父进程的task_struct复制给子进程。
为子进程的task_struct,tss做个性化设置。
为子进程创建第一个页表,也将父进程的页表内容赋给这个页表。
子进程共享父进程的文件。
设置子进程的GDT项。
最后将子进程设置为就绪状态,使其可以参与进程间的轮转。
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;
(只复制当前进程内容)。
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 ;
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;
return last_pid;
}
进入copy_prossess函数后,调用get_free_page()函数,在主内存申请一个空闲页面,并将申请到的页面清0。将这个页面的指针强制类型转化成task_struct类型的指针,并挂接在task[nr]上,nr就是在find_empty_process中返回的任务号。
接下来的
*p=*current
将当前进程的指针赋给了子进程的,也就是说子进程继承了父进程一些重要的属性,当然这是不够的,所以接下来的一大堆代码都是为子进程做个性化设置的。
一般来讲,每个进程都要加载属于自己的代码、数据,所以copy_process设置子进程的内存地址。通过copy_mem来设置新任务的代码和数据段基址、限长并复制页表。
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))
{
free_page_tables (new_data_base, data_limit);
return -ENOMEM;
}
return 0 ;
}
然后是对文件,pwd等资源的修改,接着要设置子进程在GDT中的表项,最后将进程设置为就绪状态,并返回进程号。
三、创建过程总结
可以将上面繁琐的创建过程总结为一下的几步:
1、调用fork()函数引发0x80中断
2、调用sys_fork
3、通过find_empty_process为新进程分配一个进程号
4、通过copy_process函数使子进程复制父进程的资源,并进行一些个性化设置后,返回进程号。