本章讨论了Unix/Linux 中的进程管理;阐述了多任务处理原则;介绍了进程概念;并以一个编程示例来说明多任务处理、上下文切换和进程处理的各种原则和方法。多任务处理系统支持动态进程创建、进程终止,以及通过休眠与唤醒实现进程同步、进程关系,以及以二叉树的形式实现进程家族树,从而允许父进程等待子进程终止;提供了一个具体示例来阐释进程管理函数在操作系统内核中是如何工作的;然后,解释了Unix/Linux中各进程的来源,包括系统启动期间的初始进程、INIT进程、守护进程、登录进程以及可供用户执行命令的 sh 进程;接着,对进程的执行模式进行了讲解,以及如何通过中断、异常和系统调用从用户模式转换到内核模式;再接着,描述了用于进程管理的Unix/Linux 系统调用,包括fork、wait、exec 和 exit;阐明了父进程与子进程之间的关系,包括进程终止和父进程等待操作之间关系的详细描述;解释了如何通过INIT进程处理孤儿进程,包括当前Linux 中的subreaper 进程,并通过示例演示了subreaper 进程;接着,详细介绍了如何通过exec 更改进程执行映像,包括 execve 系统调用、命令行参数和环境变量;解释了I/O重定向和管道的原则及方法,并通过示例展示了管道编程的方法。
思维导图
知识点总结
1.进程的正式定义
进程是对映像的执行。
2.多任务处理系统
(1)type.h文件
type.h文件定义了系统常数和表示进程的简单PROC结构体
/*********** type.h file ************/ #define NPROC 9 #define SSIZE 1024 // PROC status #define FREE 0 #define READY 1 #define SLEEP 2 #define ZOMBIE 3 typedef struct proc{ struct proc *next; int *ksp; int pid; int status; int priority; int kstack [SSIZE]; }PROC;
后面,我们在扩展 MT系统时,应向PROC结构体中添加更多的字段。
(2)ts.s文件
ts.s在32位GCC汇编代码中可实现进程上下文切换。
#------------- ts,s file file------- .globl running,scheduler, tswitch tSwitch: SAVE:pushl %eax : pushl %ebx pushl %ecx pushl %edx pushl %ebp pushl %esi pushl %edi pushf1 movl running, Sebx mov1 # esp,4(%ebx) FIND: call scheduler RESUME: movl running,8ebx Movl 4(%ebx),%esp popf1 popl %edi popl %esi popl %ebp popl %edx popl %ecx popl %ebx popl %eax ret # stack contents=|retPC|eax|ebx|ecx|edx|ebp|esi|edi|eflag| # -2 -3 -4 -5 -6 -7 -8 -9 -1
(3)queue.c文件
queue.c文件可实现队列和链表操作函数。
/***************** queue。C file*****************/ int enqueue(PROC **queue,PROC *p) { PROC *q = *queue; if(q == 0 || p->priority> q->priority){ *queue = p; p->next = q; } else{ while(g->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("[8d %d]->",p->pid,p->priority); p = p->next; } printf("NULL\n"); }
(4)t.c文件
t.c文件定义MT系统数据结构、系统初始化代码和进程管理函数。
3.多任务处理系统代码介绍
(1)虚拟CPU:MT系统在Linux下编译链接为
gcc -m32 t.c ts.s
(2)init():当MT系统启动时,main()函数调用init()以初始化系统。
(3)P0调用kfork()来创建优先级为1的子进程P1,并将其输入就绪队列中。
(4)tswitich():tswitch()函数实现进程上下文切换。
(5).1 tswitch()中的SAVE函数:当正在执行的某个任务调用tswitch()时,它会把返回地址保存在堆栈上,并在汇编代码中进入tswitch()。
(6).2 scheduler():在执行了SAVE函数之后,任务调用scheduler()来选择下一个正在运行的任务。
(7).3 tswitch()中的RESUME函数
(8)kfork():kfork()函数创建一个子任务并将其输入readyQueue中。
(9)body():所有创建的任务都执行同一个body()函数。
(10)空闲任务 P0:P0的特殊之处在于它所在任务中具有最低的优先级
(11)运行多任务处理(MT)系统
MT系统的输出示例:
4.进程同步
(1)睡眠模式
为实现休眠操作,我们可以在 PROC结构体中添加一个event字段,并实现ksleep(int event)函数,使进程进入休眠状态。接下来,我们将假设对 PROC结构体进行修改以包含加粗显示的添加字段。
typedef struct proc{ struct proc *next; int*ksp; int pid; int ppid; int status; int priority; int event; int exitCode; struct proc *child; struct proc *sibling; struct proc *parent; int kstack[1024]; }PROC;
(2)唤醒操作
当某个等待时间发生时,另一个执行实体(可能是某个进程或中断处理程序)将会调用 kwakeup(event)。唤醒正处于休眠状态等待该事件值的所有程序。如果没有任何程序休眠等待该程序,kwakeup()就不工作,即不执行任何操作。Kwakeup()的算法是:
/********** Algorithm of kwakeup(int event)*********/ // Assume SLEEPing proCs are in a global sleepiist for each PROC *p in sleepList do { if (p->event == event){ delete D from sleepLiBt; p->8tatu8 = READY; enqueue(EreadyQueue,p); } }
5.进程终止
● 正常终止:进程调用exit(value),发出 exit(value)系统调用来执行在操作系统内核
中的 kexit(value),这就是我们本节要讨论的情况。
● 异常终止:进程因某个信号而异常终止。信号和信号处理将在后面第6章讨论。
在这两种情况下,当进程终止时,最终都会在操作系统内核中调用kexit()。
进程家族树
等待子进程终止
在任何时候,进程都可以调用内核函数pid = kwait(int *status)
等待僵尸子进程。如果成功,则返回的 pid是僵尸子进程的 pid,而 status包含僵尸子进程的退出代码。此外,kwait()还会将僵尸子进程释放回 freeList 以便重用.
6.MT系统中的进程管理
完善基础MT系统,实现MT系统的进程管理函数:
(1)用二叉树的形式实现进程家族树。
(2)实现 ksleepO()和kwakeup()进程同步函数。
(3)实现kexit()和kwait()进程管理函数。
(4)添加"w"命令来测试和演示等待操作。
修改后的MT系统的输出示例
7.Unix/Linux中的进程
(1)守护进程
例子:
syslogd: log daemon process inetd :Internet service daemon process httpd : HTTP server daemon process etc.
(2)进程的执行模式
1.中断:中断是外部设备发送给 CPU的信号,请求CPU服务。
2.陷阱:陷阱是错误条件,例如无效地址、非法指令、除以0等、这些错误条件被CPU识别为异常,使得CPU进入 Kmode 来处理错误。
3.系统调用:系统调用(简称syscall)是一种允许Umode 进程进入Kmode 以执行内核函数的机制。如果发生错误,外部全局变量 errno(在errno. h中)会包含一个ERROR代码,用于标识错误。用户可使用库函数
perror( "error message");
8.进程管理的系统调用
(1)fork()操作:
(1)进程终止
1.正常终止:回顾前面的内容,我们知道,每个C程序的 main()函数都是由C启动代码 crt0.o调用的。如果程序执行成功,main()最终会返回到 crt0.o,调用库函数 exit((0)来终止进程。首先,exit(value)函数会执行一些清理工作,如刷新 stdout、关闭I/O流等。然后,它发出一个_exit(value)系统调用,使进入操作系统内核的进程终止。
2.异常终止:在执行某程序时,进程可能会遇到错误,如非法指令、越权、除零等,这些错误会被 CPU识别为异常。当某进程遇到异常时,它会进入操作系统内核。内核的异常处理程序将陷阱错误类型转换为一个函数,称为信号,将信号传递给进程,使进程终止。
9.I/O重点向
重定向标准输出
当进程执行库函数
printf("format=%s\n",items);
它试图将数据写入 stdout 文件FILE 结构体中的 fbuf[],这是缓冲行。如果 fbuf[]有一个完整的行,它会发出一个write系统调用,将数据从 fbuf[]写入文件描述符1,映射到终端屏幕上。要想将标准输出重定向到一个文件,需执行以下操作。
c1ose(1);
open("filename",O_WRONLY|O_CREAT,0644);
更改文件描述符1,指向打开的文件名。然后,stdout 的输出将会转到该文件而不是屏幕。同样,我们也可以将stderr重定向到一个文件。当某进程(在内核中)终止时,它会关闭所有打开的文件。
二、最有收获的内容
管道
1.概念
管道是用于进程交换数据的单向进程间通信通道。管道有一个读取端和一个写入端。可从管道的读取端读取写入管道写入端的数据。自从管道在最初的Unix 中首次出现以来,已经被用于几乎所有的操作系统中,有许多变体。一些系统允许双向管道,在双向管道上,数据可以双向传输。普通管道用于相关进程。命名管道是不相关进程之间的 FIFO通信通道。但是,如果管道不再有读进程,写进程必须将这种情况视为管道中断错误,并中止写入。
2.Unix/Linux中的管道编程
注意,管道中断状况并不具有对称性。这是一种只有读进程没有写进程的通信通道。实际上,管道并未中断,因为只要管道有数据,读进程就仍可继续读取。下面的程序演示了Unix/Linux中的管道。
#include <stdio.h> #include <stdlib.h> #include <string.h> int pd[2],n,i; char line[256]; int main() { pipe(pd); printf("pd=[$d,%d]\n",pd[0],pd[1]); if (fork(){ printf("parent $d close pd[0]\n",getpid()); close(pd[0]); // parent as pipe WRITER while(i++ <10){ // parent writes to pipe 10 times printf("parent 8d writing to pipe\n",getpid()); n = write(pd[1],"I AM YOUR PAPA",16); printf("parent %d wrote %d bytes to pipe\n",getpid(),n); } printf("parent $d exit\n",getpid()); } else{ printf("child $d close pd[1]\n",getpid()); close(pd[1]); // child as pipe READER while(1) { // child read from pipe printf("child %d reading from pipe\n",getpid()); if((n = read(pd[0],line,128))){ // try to read 128 bytes line[n]=0; printf("child read $d bytes from pipe: 8s\n",n,line); } else // pipe has no data and no writer exit(0);; } } }
读进程可在Linux下编译和运行程序来观察它的行为。图3.8显示了运行示例3.7的程序的样本输出。
三、问题与解决思路
问题:Linux中的Linux sh是如何工作的?
解决思路:我通过查找相关资料,以及在我的Linux里运行linux sh指令来查看该命令的作用:
Linux下运行 SH文件步骤如下:
1、首先新建一个用来存放.sh文件的文件夹:
touch test.sh
2、编译.sh文件的内容,如:
#! /bin/Bash
Echo ‘hello world’
3、给.sh文件添加x执行权限:
chmod a+x test.sh
4、执行.sh文件:以test.sh文件为例,sh test.sh即可执行test.sh文件:
四、实践内容(截图、代码链接)本次实践是基于OpenEuler系统下实现的
编写一个C语言程序来模拟Linux sh实现对命令的执行。目标是让读者理解Linux sh 如何工作。使用了fork()、exec(O)、close()、exit()、pipe()系统调用和字符串操作。
C语言代码链接:
https://gitee.com/two_thousand_and_thirteen/codes/tnbrk8j6vio5qhela971u81
#include<stdio.h> #include<string.h> #include<stdlib.h> #include<unistd.h> int split(char *s,char *delim[]) { int j=0; int k=0; char temp[100]; for(int i=0;s[i]!='\0';i++) { if(s[i]!=' ') { temp[j++]=s[i]; }else { temp[j]='\0'; delim[k] = (char *)malloc(strlen(temp)); strcpy(delim[k],temp); j=0; k++; } } temp[j]='\0'; delim[k] = (char *)malloc(strlen(temp)); strcpy(delim[k],temp); k++; return k; } int main() { char *commands[] = {"/bin/ls","/bin/cat","/bin/ps"}; int parent_pid = getpid(); while(1) { printf("Please input your command: "); char *command; fgets(command,100,stdin); command[strlen(command)-1]='\0'; fork(); int pid = getpid(); if(pid==parent_pid) { wait(NULL); continue; }else { int flag = 0; char *argv[5]; int k = split(command,argv); argv[k]=NULL; for(int i=0;i<3;i++) { if(strstr(commands[i],argv[0])) { flag=1; char *envp[] = {NULL}; execve(commands[i],argv,envp); break; } } if(!flag) { printf("command not found: %s\n",argv[0]); } break; } } return 0; }
编译运行结果截图:(利用c语言模拟实现对sh命令的执行)