1 进程结构
union task_union{
struct task_struct task ;
char stack[PAGE_SIZE] ;
}
这实际上是一个内存页,页的底部是进程控制块结构。其余部分是作为进程的内核态堆栈使用。
2 task 数组
struct task_struct * task[NR_TASKS] = {&(init_task.task), };
这个数组中存储的是task_struct 结构的指针,但是实际上数组中的每一项都指着一块内存页。
3 任务段数据
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;
};
4 进程控制块
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段
};
5 linux进程结构
(1) 在linux中gdt中的每一项,都有两个表项,一个是ldt描述符,另一个是tss描述符。
(2) 在task数组中占有一项,每一项是一个物理页面,物理内存页面底端是进程控制块,内存页面的其余部分是内核态堆栈。
(3) task数组中的表项和gdt中的表项是一一对应的。 对于一个在task数组中的任务项是nr的任务来说,它的tss描述符在gdt中描述符
的位置是,gdtr + 3*8 + 16 * nr ,ldt描述符在gdt中的描述符的位置是 gdtr + 3 * 8 + 16 * nr + 8 。
(4) 对应于表项为nr的进程,它对应的页目录项是16 * nr --------16 * (nr + 1) 。
6 进程0
进程0是一个特殊的进程,它是所有进程的祖先进程,所有其他的进程都是复制进程0或者其后代进程产生的。 但是进程0不是。
下面主要讲一下 进程0的创建顺序:
(1) 进程控制块和页目录页表的手动创建
以下就是一个任务的初始过程
#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处
{} \
}, \
}
进程0的数据段基址为0,段限长为640KB ,代码段基址为0,段限长为640KB。任务0的数据段和代码段 和系统的代码段和数据段是重合的。
进程0的内核态堆栈和进程控制块都是位于系统模块内。
(2)在main模块中调用了,sched_init()函数加载了 进程0的进程0的tss段描述符,ldt段描述符,并且加载TR寄存器,使它指向进程0的tss段,这时候
进程0才完成了启动。
/*****************************************************************************/
/* 功能: 1. 初始化task数组和GDT(包括设置进程1的LDT和TSS) */
/* 2. 加载TR和IDTR寄存器 */
/* 3. 设置时钟中断门和系统调用中断门 */
/* 参数: (无) */
/* 返回: (无) */
/*****************************************************************************/
void
sched_init(
void
)
{
int i;
struct desc_struct * p;
![](https://i-blog.csdnimg.cn/blog_migrate/df37983f39daa189b8c814e01a6a9011.gif)
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); // 设置系统调用中断门
}
(3)切换回用户态。
//
把进程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
"
)
6 用fork创建进程
除了进程0,所有其他的进程都是由fork()系统调用创建的,子进程是通过复制父进程的数据和代码而产生的。
创建结束之后,子进程与父进程的代码和数据共享,但是子进程有自己的进程控制块,内核堆栈和页表。
一个进程需要以下三中数据结构
(1) 进程控制块 task__struct 。
(2) gdt中的tss 和ldt描述符。
(3)页目录项和页表项。
所以fork系统调用的任务就是创建进程的上述三个部分。
sys_fork()函数分两步实现,第一步 首先调用,find_empty_process() 函数,第二步调用 copy_process()函数,复制进程。
_sys_fork:
//
第一步,调用find_empty_process()函数,找task[]中的空闲项。
//
找到后数组下标放在eax中。如果没找到直接跳转到ret指令。
call _find_empty_process
testl
%
eax,
%
eax
js 1f
push
%
gs
//
中断时没有入栈的寄存器入栈,
//
作为copy_process() 函数的参数
pushl
%
esi
pushl
%
edi
pushl
%
ebp
pushl
%
eax
//
第二步,调用copy_process() 函数复制进程。
call _copy_process
addl $
20
,
%
esp
1
: ret
内存复制函数
copy_mem
![](https://i-blog.csdnimg.cn/blog_migrate/cbef093dcc044b2793832001e2365e43.gif)
/*****************************************************************************/
/* 功能:设置新进程的LDT项(数据段描述符和代码段描述符)中的基地址部分 */
/* 并且复制父进程(也就是当前进程)的页目录和页表, */
/* 实现父子进程数据代码共享 */
/* 参数: nr 新进程任务数组下标 */
/* p 新进程的进程控制块 */
/* 返回: 0 (成功), -ENOMEM(出错) */
/*****************************************************************************/
![](https://i-blog.csdnimg.cn/blog_migrate/cbef093dcc044b2793832001e2365e43.gif)
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;
![](https://i-blog.csdnimg.cn/blog_migrate/df37983f39daa189b8c814e01a6a9011.gif)
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;
}
复制进程
/*****************************************************************************/
/* 功能:复制进程,把当前进程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;
}
7 进程的结束
进程结束的时候,需要关闭的资源主要有:
(1) 释放所有的物理页面。(
子进程自己清除)
(2) 关闭所有打开的文件。(
子进程自己清除)
(3) 清除task[] 中的相应的项。(
父进程自己清除)
子进程通过exit()清除前面两个选项,将自身的状态变为TASK_ZOMBIE 。
父进程通过调用waitpid() 将task[] 数组清空。
一个进程的经过exit()之后,物理页表被清除 , 页表页目录项也被清除,但是它的进程控制块和内核堆栈还在,,
此时进程的状态变为TASK_ZOMBIE ,不会再被处理器处理。不被处理但是还占用着task数组中的一个表项,这
就成为了僵尸进程。
子进程调用了exit()函数之后,就通知父进程,父进程调用waitpid() 来清除 task数组中的表项。但是很有可能,
父进程没有执行waitpid()操作,情况如下:
(1) 父进程早于子进程执行exit()函数。
(2) 子进程僵死,但是父进程没有调用waitpid()操作。
(3) 父进程调用了waitpid(),但是因为某种愿意没有释放资源。
解决方法:
如果父进程无法释放资源,那么就让进程1来释放资源。
当一个父进程早于子进程exit()的时候,它把所有的子进程过继给父进程。