基于Linux0.11内核分析:进程管理

进程控制

程序是一个可执行的文件,进程是一个执行中的程序实例。利用分时技术在linux操作系统上同时可以运行多个进程。分时技术的就是把CPU运行时间分成规定长度的时间片,每个进程的时间片用完时就会进行调度切换。
linux中利用fork系统调用来创建新进程,被创建的进程称为子进程,创建者称为父进程,内核程序使用进程标识号来标记每个进程。
进程通常被称为任务(task)
task_struct
内核程序通过进程表对进程进行管理,linux中进程表项是一个task_struct 任务结构指针,任务数据结构也被称为PCB 或PD ,其中保存着用于控制和管理进程的所有信息。

typedef struct desc_struct
{				// 定义了段描述符的数据结构。该结构仅说明每个描述
  unsigned long a, b;		// 符是由8 个字节构成,每个描述符表共有256 项。
}
desc_table[256];
extern unsigned long pg_dir[1024];	// 内存页目录数组。每个目录项为4 字节。从物理地址0 开始。
extern desc_table idt, gdt;	// 中断描述符表,全局描述符表。
// 下面是数学协处理器使用的结构,主要用于保存进程切换时i387 的执行状态信息。
struct i387_struct
{
	long cwd;			// 控制字(Control word)。
	long swd;			// 状态字(Status word)。
	long twd;			// 标记字(Tag word)。
	long fip;			// 协处理器代码指针。
	long fcs;			// 协处理器代码段寄存器。
	long foo;
	long fos;
	long st_space[20];		/* 8*10 bytes for each FP-reg = 80 bytes */
};
// 任务状态段数据结构(参见列表后的信息)。
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;
};
struct task_struct
{
	long state;			                      //任务的运行状态(-1不可运行,0可运行,>0已停止)
	long counter; 							  //任务运行时间计数,时间片
	long priority;                              //运行优先级。任务开始时counter = priority,越大运行时间越长
	long signal;                                //信号。是位图,每个比特位代表一种信号,信号值 = 位偏移值+1
	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;                //用户标识号,有效用户id,保存的用户id
	unsigned short gid, egid, sgid;                //组标识号,有效组id,保存的组id
	long alarm;                                //报警定时值
	long utime, stime, cutime, cstime, start_time;         //用户态运行时间,系统态运行时间,子进程用户态运行时间,子进程系统态运行时间,进程开始运行时刻
	unsigned short used_math;        //标志:是否使用协处理器
	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];        //文件结构指针表
	struct desc_struct ldt[3];            //局部描述符表(0- 空,1- 代码段,2- 数据和堆栈段 ds&ss)
	struct tss_struct tss;                  //进程的任务状态段信息结构
};

进程运行状态
一个进程在其生存期内可处于一组不同的状态下,称为进程状态。进程状态保存在进程任务结构的state 字段中。
在这里插入图片描述

  • 运行状态(TASK_RUNNING)
    当一个进程在内核代码中运行时,称为内核运行态
    当一个进程在用户代码中运行时,称为用户运行态
    当一个进程资源可用时,进程被唤醒进入准备,称为就绪态
  • 可中断睡眠状态(TASK_INTERRUPTIBLE)
    当进程处于可中断等待状态,系统不会调度该进程执行。当系统产生一个中断或者释放进程正在等待的资源,或者进程收到一个信号,都可以唤醒进程转换到就绪态。
  • 不可中断睡眠状态(TASK——UNINTERRUPTIBLE)
    该状态的进程只有被使用wake_up() 函数明确唤醒后才可以转换到可运行的就绪态,该状态通常在进程不受干扰的等待或者等待事件会很快发生时使用
  • 暂停状态(TASK_STOPPED)
    当进程收到信号 SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU 时就会进入暂停状态,可向其发送 SIGCONT 信号让进程转换到可运行状态,在进程调试期间收到任何信号均会进入该状态。
  • 僵死状态(TASK_ZOMBIE)
    当进程停止运行,但其父进程还没有调用wait() 询问其状态时,则称该进程处于僵死状态。为了让父进程能够获取其停止运行的信息,此时子进程的任务数据结构信息还需保留着,一旦父进程调用 wait() 取得了子进程的信息,则处于该状态进程的任务数据结构就会被释放掉

只有当进程从“内核运行态”转移到“睡眠状态”时,内核才会进行进程切换。在内核态下运行的进程不能被其它进程抢占,而一个进程不能改变另一个进程的状态,为了避免进程切换时造成内核数据错误,内核在执行临界区代码时会禁止一切中断
进程初始换
引导程序把内核从磁盘加载到内存中,让系统进入保护模式下运行后,就开始执行系统初始化程序main.c。该程序首先确定如何分配使用系统物理内存,然后调用内核各部分的初始化函数分别对内存管理、中断管理、块设备、字符设备、进程管理、硬盘和软盘硬件进行初始化处理,完成后各部分已经处于可运行状态。
执行完main.c 通过move_to_user_mode把程序从内核态移动到用户态的任务0 中继续执行,再通过fork() 调用创建出进程1,在进程1中程序将继续进行应用环境的初始化并执行shell 登录程序,进程0 则会在系统空闲时被调度执行pause() 系统调用。
在这里插入图片描述

//// 切换到用户模式运行。
// 该函数利用iret 指令实现从内核模式切换到用户模式(初始任务0)
__asm__ ( "movl %%esp,%%eax\n\t" \	
"pushl $0x17\n\t" \		
  "pushl %%eax\n\t" \		
  "pushfl\n\t" \		
  "pushl $0x0f\n\t" \		
  "pushl $1f\n\t" \		
  "iret\n" \			
  "1:\tmovl $0x17,%%eax\n\t" \	
  "movw %%ax,%%ds\n\t" \	// 初始化段寄存器指向本局部表的数据段。
"movw %%ax,%%es\n\t" "movw %%ax,%%fs\n\t" "movw %%ax,%%gs":::"ax")

创建新进程
fork() 系统调用,用来复制进程创建新进程。
首先系统在任务数组中找出一个还没有被任何进程使用的空项,然后系统为新建进程在内存中申请一页内存来存放其任务数据结构信息,并复制当前进程任务数据结构中的所有内容作为新进程任务数据结构的模板,此时设置进程状态为不可中断的等待状态。
随后对复制的任务数据结构进行修改,把当前进程设置为新进程的父进程,清除位图并复位新进程各统计值,设置时间片15个系统滴答,设置当前进程任务状态段TSS 中各寄存器的值,修改内核态堆栈指针,指向自己的内存页面顶端,进程返回值设置为0,堆栈段设置为内核数据段选择符,ldt被设置为局部描述符在GDT中的索引值,如果使用了协处理器,还需保存协处理器的状态信息。
此后系统设置新任务的代码和数据段基址、限长,并复制当前进程内存分页管理的页表。如果父进程中有文件打开,则将对应的文件打开次数增1。接着在GDT中设置新任务的TSS 和LDT 描述符项,其中基地址信息指向新进程任务数据结构中的tss 和 ldt ,最后再将新任务设置为可运行状态并返回新进程号。

//部分fork() 代码:

/*
*下面是主要的fork 子程序。它复制系统进程信息(task[n])并且设置必要的寄存器。
* 它还整个地复制数据段。
*/
// 复制进程。
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;
	struct i387_struct *p_i387;

	p = (struct task_struct *) get_free_page ();	// 为新任务数据结构分配内存。
	if (!p)			// 如果内存分配出错,则返回出错码并退出。
		return -EAGAIN;
	task[nr] = p;			// 将新任务结构指针放入任务数组中。
// 其中nr 为任务号,由前面find_empty_process()返回。
	*p = *current;		/* NOTE! this doesn't copy the supervisor stack */
/* 注意!这样做不会复制超级用户的堆栈 (只复制当前进程内容)。*/ 
	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->tss.back_link = 0;
	p->tss.esp0 = PAGE_SIZE + (long) p;	// 堆栈指针(由于是给任务结构p 分配了1 页
// 新内存,所以此时esp0 正好指向该页顶端)。
	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;	// 段寄存器仅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);	// 该新任务nr 的局部描述符表选择符(LDT 的描述符在GDT 中)。
	p->tss.trace_bitmap = 0x80000000;
// 如果当前任务使用了协处理器,就保存其上下文。
	p_i387 = &p->tss.i387;
	if (last_task_used_math == current)
	_asm{
		mov ebx, p_i387
		clts
		fnsave [p_i387]
	}
//    __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。
	for (i = 0; i < NR_OPEN; i++)
		if (f = p->filp[i])
			f->f_count++;
// 将当前进程(父进程)的pwd, root 和executable 引用次数均增1。
	if (current->pwd)
		current->pwd->i_count++;
	if (current->root)
		current->root->i_count++;
	if (current->executable)
		current->executable->i_count++;
// 在GDT 中设置新任务的TSS 和LDT 描述符项,数据从task 结构中取。
// 在任务切换时,任务寄存器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;		// 返回新进程号(与任务号是不同的)。
}

fork() 和 exec() ,一个是创建新进程,一个是加载运行可执行文件。
执行块设备上的程序时:在子进程中运行exec 系统调用,子程序原来的代码和数据区被清除,接着运行新程序时,由于内核中没有从块设备上加载相应的代码,CPU会立刻产生页面不存在的异常,此时内存管理程序会从块设备上加载相应的代码页面,接着CPU重新执行引起异常的指令,此刻新程序真正开始执行。
进程调度
首先通过扫描任务数组,通过比较每个就绪态任务的运行时间counter 值来确定当前哪个进程运行时间最少,就切换该任务,如果所有处于就绪态的进程时间片都用完,系统就会根据优先级priority对所有进程重新计算每个任务需要运行的时间片值counter,公式:counter = counter / 2 +priorty
这样对于正在睡眠的进程当它们被唤醒时就具有较高的时间片counter值,然后从新扫描,重复上述动作,直到选出一个进程并执行进程切换。
切换图片:
在这里插入图片描述
终止进程
当进程结束或中止运行,就会需要释放所占用的系统资源,包括进程运行时打开的文件、申请的内存等。
当用户程序调用exit() 系统调用:
首先释放进程代码段和数据段占用的内存页面,关闭进程打开的所有文件,对进程使用的当前工作目录、根目录、运行程序的i 节点进行同步操作,如果进程有子进程,则让init 作为其所有子进程的父进程。
在子进程执行期间,父进程通常使用wait() 和 waitpid() 函数等待其某个子进程终止,当子进程终止并处于僵死状态,父进程会把子进程运行所使用的时间累加到自己的进程,最终释放子进程任务数据结构的内存页面,并置空子进程在任务数组中占用的指针项。

猜你喜欢

转载自blog.csdn.net/qq_42856154/article/details/89901964