Unix/Linux进程管理

多任务处理

一般来说,多任务处理指的是同时进行几项独立活动的能力

多任务处理指的是同时执行几个独立的任务。在单处理器(单CPU)系统 中.一次只能执行一个任务-多任务处理是通过在不同任务之间多路复用CPU的执行时间 来实现的,即将CPU执行操作从一个任务切换到另一个任务:不同任务之间的执行切换机 制称为上下文切换,将一个任务的执行环境更改为另一个任务的执行环境如果切换速度 足够快,就会给人一种同时执行所有任务的错觉 这种逻辑并行性称为“并发”。

进程的概念

进程的定义:进程是对映像的执行操作系统,内核将一系列执行视为使用系统资源的单一实体。

系统资源包括内存空间、 1/0设备以及最重要的CPU时间

多任务处理系统

多任务处理系统,简称MT,由以下几个部分组成。

type.h 文件

type.h文件定义了系统常数和表示进程的简单PROC结构体

#define NPROC  9
#define SSIZE 1024
#define FREE   0
#define READY  1
#define SLEEP  2
#define ZOMBIE 3
typedef struct proc{
     struct proc *next;
     int *ksp;
     int pid;
     int ppid;
     int status;
     int priority;
     int kstack
}PROC;

ts.s 文件

tswitch:
SAVE:	pushl	% e ax	
	pushl	%ebx	
	pushl	%ecx	
	pushl	%edx	
	pushl	% ebp	
	pushl	%esi	
	pushl	%edi	
	pushfl	
	movl	running,%ebx	# ebx -> PROC
	movl	%espz 4(%ebx)	# PORC.save_sp = esp
FIND:	call	scheduler	
RESUME:	movl	running,%ebx	# ebx -> PROC
	movl	4(%ebx),%esp	# esp = PROC.saved_sp
	pop fl		
	popl	%edi	
	POP1	%esi	
	popl	% e bp	
	popl	%edx	
	popl	%ecx	
	popl	%ebx	
	POP1	% e ax	
	ret		
# stack	contents = |retPC|eax	|ebx|ecx|edx|ebp|esi|edi|eflag|
#		-1	-2	-3 -4 -5 -6 -7 -8	-9

queue.c 文件

queue.c文件可实现队列和链表操作函数.enqueue()函数按优先级将PROC输入队列中。 在优先级队列中,具有相同优先级的进程按先进先出(FIFO)的顺序排序。dequeue()函数 可返回从队列或链表中删除的第一个元素。printList()函数可打印链表元素。

 int enqueue(PROC **queue, PROC *p)
{
PROC *q = *queue;
if (q == 0 I I p->priority > q->priority)(
*queue = p; p->next = q;
}
else(
while (q->next && p->priority <= q->next->priority)
q = q->next;
p->next = q->next;
q->next = p;
)
} PROC *dequeue(PROC **queue)
(
PROC *p = *queue;
if (P)
*queue = (*queue)->next;
return p;
)
int printList(char *name, PROC *p)
(
printf("%s = ", name);
while(p)(
printf("[%d %d]->", p->pid, p->priority);
p = p->next;
} printf(wNULL\nw);
}

t.c 文件

t.c文件定义MT系统数据结构、系统初始化代码和进程管理函数。

#include <stdio.h>
#include "type.h"
// NPROC PROCs
// £reeList of PROCs
// priority queue of READY procs // current running proc pointer


#include "queue.c"
// include (;ueue.c file


/******************************************************* kfork() creates a child process; returns child pid. When scheduled to run, child PROC resumes to body();
********************************************************/ int kfork()
int i;
PROC *p = dequeue(&freeList);
if (!p)(
printf("no more proc\n");
return(-1);
/* initialize the new proc and its stack */
p->status = READY;
p->priority = 1;	// ALL PROCs priority=l,except PO
p->ppid = running->pid;
/************ new task initial stack contents ************ kstack contains: |retPC|eax|ebx|ecx|edx|ebp|esi|edi|eflag| -1	-2 -3 -4 -5 -6 -7 -8	-9



int kexit()
{
running->status = FREE; running->priority = 0; enqueue(&freeList, running); printList("freeList", freeList); tswitch();
int do_kfork()
{
int child = kfork();
if (child < 0)
printf("kfork failed\n");
else(
printf("proc %d kforked a child = %d\n", running->pidz child); printList("readyQueue", readyQueue);
}
return child;
}
int do_switch()
{
tswitch();
}
int do_exit
(
kexit();
int body()	// process body function
(
int c;
printf("proc %d starts from body()\n"z running->pid); whiled) {


printf("***************************************\n");
printf("proc %d running: parent=%d\n", running->pid,running->ppid); printf("enter a key [f|s|q] c = getchar(); getchar();
switch(c)(
case 頌:do_kfork ();
case do_switch();
case 'q': do_exit();
// kill the \r key
break; break; break;
// initialize the MT system; int init()
create PO as initial running process
int i;
PROC *p;
for (i=0; i<NPROC; i++)( p = &proc[i];
p->pid = i;
p->status = FREE; p->priority = 0; p->next = p+1;
//
//
initialize PROCs
PID = 0 to NPROC-1
proc[NPROC-1].next = freeList = &proc[0]; readyQueue = 0; all PROCs in freeList readyQueue = empty


// create PO as the initial
p = running = dequeue(&freeList); // use proc[0] p->status = READY;
p->ppid = 0;	// PO is its own parent
printList("freeList", freeList);
printf("init complete: PO running\n");
running process
*** main() function
int main()
printf("Welcome to the MT Multitasking System\n"); init();	//
kforkO ;	//
whiled) (
printf("PO:
initialize system; create and run PO kfork Pl into readyQueue
switch process\n");
if (readyQueue)
tswitch();
/*********** scheduler *************/
int scheduler()
(
printf("proc %d in scheduler()\n"z running->pid); if (running->status == READY)
enqueue(&readyQueuez running);
printList("readyQueue", readyQueue);
running = dequeue(SreadyQueue);
printf(nnexC running = %d\n", running->pid);
}

多任务处理系统代码介绍

MT系统的基本代码

(1)虚拟CPU: MT系统在Linux下编译链接为
gcc -m32 t.c ts.s
然后运行a.outo整个MT系统在用户模式下作为Linux进程运行。在Linux进程中,我们 创建了多个独立执行实体(叫作任务),并通过我们自己的调度算法将它们调度到Linux进程 中运行。对于MT系统中的任务,Linux进程就像一个虚拟CPUo为避免混淆,我们将MT 系统中的执行实体叫作任务或进程。
(2)init():当MT系统启动时,main。函数调用init()以初始化系统。init()初始化PROC 结构体,并将它们输入freeList中。它还将readyQueue初始化为空。然后使用proc[0]创建 P0,作为初始运行进程。P0的优先级最低,为0。所有其他任务的优先级都是1,因此它们 将轮流从readyQueue运行。
(3 ) P0调用kfork()来创建优先级为1的子进程PI,并将其输入就绪队列中。然后P0 调用tswitchO,将会切换任务以运行PI °
(4)tswitch。: tswitch。函数实现进程上下文切换。它就像一个进程交换箱,一个进程 进入时通常另一个进程会出现"switch。由3个独立的步骤组成,下面将详细解释这些步骤。
(4) .1 tswitch0中的SAVE函数:当正在执行的某个任务调用tswitch()时,它会把返 回地址保存在堆栈上,并在汇编代码中进.人tswitch。。在tswitch()中,SAVE函数将CPU 寄存器保存到调用任务的堆栈中,并将堆栈指针保存到proc.ksp中。32位Intel x86 CPU有 许多寄存器,但在用户模式下,只有eax、ebx、ecx、edx、ebp、esi、edi和eflag对Linux 进程可见,它是MT系统的虚拟CPU。因此,我们只需要保存和恢复虚拟CPU的这些寄存 器.下图显示了在执行tswitch()的SAVE函数后,调用任务的堆栈内容和保存的堆栈指针, 其中xxx表示调用tswitch。之前的堆栈内容。

在基于Intel x86的32位PC中,每个CPU寄存器的宽度为4字节,堆栈操作始终以4字节 为单位。因此,我们可以将PROC结构体中的每个PROC堆栈定义为一个整数数如
(4 ) .2 scheduler():在执行了 tswitch。中的SAVE函数之后,任务调用scheduler()来 选择下一个正在运行的任务。在scheduler()中,如果调用任务仍然可以运行,则会调用 enqueue()将自己按优先级放.入readyQueue中。否则’它不会在readyQueue中,因此也就 无法运行。然后,它会调用dequeue(),将从readyQueue中删除的第一个PROC作为新的运 行任务返回°
(4) .3 tswitch()中的RESUME函数:当执行从scheduler。返回时、“运行"可能 已经转而指向另一个任务的PROC运行指向的那个PROC,就是当前正在运行的任务。 tswitch。中的RESUME函数将CPU的堆栈指针设置为当前运行任务的已保存堆栈指针 然 后弹出保存的寄存器,接着是弹出RET,使当前运行的任务返回到之前调用tswitch。的 位置。
(5 ) kfork() : kforkf)函数创建一个子任务并将其输入readyQueue中。每个新创建的任 务都从同一个body。函数开始执行。虽然新任务以前从未存在过,但我们可以假装它不仅 存在过,而且运行过。它现在不运行是因为它调用了 tswitch。,所以提前放弃了使用CPU。 如果是这样的话,它的堆栈必须包含tswitch()中的SAVE函数保存的-个帧,而且它保存 的ksp必须指向栈顶。由于新任务之前从未真正运行过,所以我们可以假设它的堆栈为空, 而且当它调用tswitch()时,所有CPU寄存器内容都是0。因此,在kfork()中,我们按以下 方法初始化新任务的堆栈。
(6)body():为便于演示,所有创建的任务都执行同一个body()函数。这说明了进程和 程序之间的区别。多个进程可执行同一个程序代码,但是每个进程都只能在自己的上下文中 执行。例如,body()中的所有(自动)局部变量都供进程专用,因为它们都是在每个进程堆 栈中分配的-如果body。函数调用其他函数,则所有调用序列都会保存在每个进程堆栈中, 等等。在body()中执行时,进程提示输入char = [fls|q]命令,其中:
f: kfork -个新的子进程来执行body()
s;切换进程
q:终止进程,并将进程以freeList中的FREE函数的形式返回
(7)空闲任务P0: P0的特殊之处在于它在所有任务中具有最低的优先级 在系统初始 化之后,P0创建P1并切换到运行P"当且仅当没有可运行任务时,P0将会再次运行。在 这种情况下,P0会一直循环,当readyQueue变为非空时,它将切换到另一个任务。在基本 MT系统中,如果所有其他进程都已终止,则P0将会再次运行.要想结束MT系统,用户 可按下“Ctrl+C”组合键来终止Linux进程。
(8)运行多任务处理(MT)系统:在Linux T,输入:
gcc -m32 t.c s.s
编译链接MT系统并运行所得到的a.outo图3.1为运行MT系统的样本输出示意图。图中显 示了 MT系统初始化,即初始进程P0,它创建P1并将任务切换到运行Pl。Pl kfork子进程 P2,并将任务切换到运行P2。在进程运行时,读者可输入其他命令来测试系统。

进程同步

一个操作系统包含许多并发进程,这些进程可以彼此交互。进程同步是指控制和协调 进程交互以确保其正确执行所需的各项规则和机制。最简单的进程同步工具是休眠和唤醒操作

睡眠模式

当某进程需要某些当前没有的东西时,例如申请独占一个存储区域、等待用户通过标准 输入来输入字符等,它就会在某个事件值上进入休眠状态,该事件值表示休眠的原因

typedef struct proc(
struct proc *next;	// next proc pointer
int *ksp;	// saved sp: at byte offset 4
int pid;	// process ID
int ppid;	// parent process pid
int status;	// PROC status=FREEI READY, etc
int priority;	// scheduling priority
int	event;	// event value to sleep on
int exitcode;	// exit value
struct proc *child; struct proc *sibling; struct proc *parent;
int kstack[1024]; }PROC


唤醒操作

/*********** Algorithm of kwakeup(int event) **********/
for each PROC *p in sleepList do( if (p->event == event)( delete p from sleepList; p->status = READY; enqueue(&readyQueue, p);
11 Assume SLEEPing procs are in a global sleepList

进程终止

kexit()的算法

/**************** Algorithm of kexit(int exitValue> *****************/
1 . Era Re proceRR uRer-niode context:, e. g. close file descr iptors, release resourcesr deallocate user-mode image memory, etc.
2.Dispose of children processes, if any
3.Record exitvalue in PROC.exitCode for parent to get
4.Become a ZOMBIE (but do not free the PROC)
5.Wakeup parent and, if needed, also the INIT process Pl

MT系统中的所有进程都以操作系统(OS)模拟内核模式运行。因此,它们没有任何用 户模式上下文。因此,我们首先来讨论kexit()的步骤2。在某些操作系统中,某个进程的 执行环境可能依赖于其父进程的执行环境。

进程家族树

进程家族树通过个PROC结构中的一对子进程和兄弟进程指针以二叉树的形式实现
20191321骆毅第三章读书笔记_子进程

等待子进程终止

在任何时候,进程都可以调用内核函数

pid = kwait(int *status)

等待僵尸子进程。

如果成功,则返回的pid是僵尸子进程的pid,而status包含僵戸子进程 的退出代码’此外,kwait()还会将僵尸子进程释放回freeList以便重用。

Unix/Linux中的进程

操作系统启动时,内核会强行创建PID=0的初始进程,然后系统执行P0。系统挂载文件,然后初始化完成后,复刻出子进程P1。

P1运行时,执行映像更改为INIT程序,复刻出更多子进程,用于提供系统服务,这样的进程成为守护进程。

登录进程:登录后进程打开三个文件流,分别是stdin、stdout、stderr。

I/O重定向

sh进程有三个用于终端I/O的文件流:stdin(标准输入)、stdout(标准输出)、stderr(标准错误)。其文件描述符分别对应0、1、2。

在执行scanf("%s", &item);时,就会从stdin读入,如果其FILE结构体fbuf[]为空,它就会向Linux内核发出read系统调用,从终端/dev/ttyX或为终端/dev/pts/#上读入。

管道

管道是用于进程交换数据的单向进程间通信的通道。管道有一个输入端、一个输出端。在之前我们使用man -k | grep xx时,就用到管道的功能。
管道的使用可以通过程序完成,也可以在命令行中处理完成。

代码截图及存在的相关问题

实现的是MT系统的代码:
20191321骆毅第三章读书笔记_多任务处理_02
20191321骆毅第三章读书笔记_子进程_03
20191321骆毅第三章读书笔记_多任务处理_04
20191321骆毅第三章读书笔记_初始化_05
20191321骆毅第三章读书笔记_初始化_06
20191321骆毅第三章读书笔记_初始化_07
得到的结果与书上的结果不相符,即没有生成对应P1子程序,但是我的代码与书上一致,找不到错误