实验相关知识
一、进程概念
1.进程
UNIX中,进程既是一个独立拥有资源的基本单位,又是一个独立调度的基本单位。一个进程实体由若干个区(段)组成,包括程序区、数据区、栈区、共享存储区等。每个区又分为若干页,每个进程配置有唯一的进程控制块PCB,用于控制和管理进程。PCB的数据结构如下:
⑴ 进程表项(Process Table Entry)。
包括一些最常用的核心数据,如: 进程标识符PID、用户标识符UID、进程状态、事件描述符、进程和U区在内存或外存的地址、软中断信号、计时域、进程的大小、偏置值nice、指向就绪队列中下一个PCB的指针P_Link、指向U区进程正文、数据及栈在内存区域的指针。
⑵ U区(U Area)。
用于存放进程表项的一些扩充信息。每一个进程都有一个私用的U区,其中含有:进程表项指针、真正用户标识符u-ruid(read user ID)、有效用户标识符u-euid(effective user ID)、用户文件描述符表、计时器、内部I/O参数、限制字段、差错字段、返回值、信号处理数组。
由于UNIX系统采用段页式存储管理,为了把段的起始虚地址变换为段在系统中的物理地址,便于实现区的共享,所以还有:
⑶ 系统区表项。
以存放各个段在物理存储器中的位置等信息。系统把一个进程的虚地址空间划分为若干个连续的逻辑区,有正文区、数据区、栈区等。这些区是可被共享和保护的独立实体,多个进程可共享一个区。为了对区进行管理,核心中设置一个系统区表,各表项中记录了以下有关描述活动区的信息:区的类型和大小、区的状态、区在物理存储器中的位置、引用计数、指向文件索引结点的指针。
⑷ 进程区表
系统为每个进程配置了一张进程区表。表中,每一项记录一个区的起始虚地址及指向系统区表中对应的区表项。核心通过查找进程区表和系统区表,便可将区的逻辑地址变换为物理地址。
注意
进程(Process)和程序(Program)是计算机科学中两个相关但不同的概念。
程序(Program):
- 定义:程序是一组指令和数据的集合,它们被组织成一种特定的顺序,以完成特定的任务或解决特定的问题。程序通常是存储在计算机的文件系统中,以可执行文件或源代码的形式存在。
- 性质: 程序本身只是一种静态的存在,它并没有直接在计算机上执行的能力。执行程序时,计算机会创建一个或多个进程来运行程序的实例。
进程(Process):
- 定义: 进程是程序的执行实例。它是计算机中运行的程序的动态实体,包括代码、数据、寄存器、堆栈等。每个进程都有自己的内存空间,独立于其他进程。
- 性质: 进程是操作系统中的基本概念,负责管理计算机系统的资源。一个进程可以包含一个或多个线程,这些线程共享进程的资源。
区别:
程序是静态的,进程是动态的: 程序是存储在磁盘上的一组指令和数据,而进程是程序在运行时的实例,具有动态性。
程序不占用系统资源,进程占用系统资源: 在计算机上运行程序时,操作系统会为其创建一个进程。进程具有自己的内存空间、寄存器等系统资源。
程序可以看作是被 passively 存储的实体,而进程是 actively 执行的实体: 程序只是存储在磁盘上等待执行,而进程是程序在运行时的实体,有自己的执行状态。
2.进程映像
UNIX系统中,进程是进程映像的执行过程,也就是正在执行的进程实体。它由三部分组成:
⑴ 用户级上、下文。主要成分是用户程序;
⑵ 寄存器上、下文。由CPU中的一些寄存器的内容组成,如PC,PSW,SP及通用寄存器等;
⑶ 系统级上、下文。包括OS为管理进程所用的信息,有静态和动态之分。
3.进程树
在UNIX系统中,只有0进程是在系统引导时被创建的,在系统初启时由0进程创建1进程,以后0进程变成对换进程,1进程成为系统中的始祖进程。UNIX利用fork( )为每个终端创建一个子进程为用户服务,如等待用户登录、执行SHELL命令解释程序等,每个终端进程又可利用fork( )来创建其子进程,从而形成一棵进程树。可以说,系统中除0进程外的所有进程都是用fork( )创建的。
二、并发执行
并发执行是指在同一时间段内,多个任务被同时执行的一种计算机执行模式。这与并行执行有所不同,后者是指在同一时刻,多个任务同时执行。并发执行的实质在于系统的有效利用和管理资源,提高系统的响应性、效率和吞吐量。
以下是并发执行的一些关键概念和实质:
资源共享: 多个任务可以同时访问共享的资源,如内存、文件、网络等。并发执行的实质在于系统有效地管理这些共享资源,以确保不会发生冲突或竞争条件,从而保证数据的一致性和正确性。
任务切换: 并发执行涉及到在不同任务之间进行切换,即使在单处理器系统中也可以通过时间片轮转等机制实现。任务切换的实质在于保持系统的响应性,确保每个任务都有机会执行。
异步性: 并发执行中的任务可以是异步的,它们不需要等待其他任务完成才能继续执行。这增加了系统的灵活性,允许任务在不同的时间和速度上执行。
提高系统吞吐量: 并发执行有助于提高系统的吞吐量,即在单位时间内完成的任务数量。通过并发执行,系统能够更好地响应用户的请求,处理多个任务。
并发性解决实际问题: 在现实生活中,许多问题可以通过并发执行来更有效地解决。例如,一个网络服务器可以并发处理多个客户端请求,提高系统的性能和用户体验。
避免阻塞: 并发执行可以避免任务之间的相互阻塞。即使一个任务被阻塞,其他任务仍然可以继续执行,提高系统的鲁棒性。
总的俩说,并发执行的实质在于充分利用系统资源,提高系统的并行度,从而提高系统的性能、响应性和效率。
三、所涉及的中断调用
1、fork( )
创建一个新的子进程。其子进程会复制父进程的数据与堆栈空间,并继承父进程的用户代码、组代码、环境变量、已打开的文件代码、工作目录和资源限制。
系统调用格式:
int fork()
如果Fork成功则在父进程会返回新建立的子进程代码(PID),而在新建立的子进程中则返回0。如果fork失败则直接返回-1。
2、exec( )系列(exec替换进程映像)
系统调用exec( )系列,也可用于新程序的运行。fork( )只是将父进程的用户级上下文拷贝到新进程中,而exec( )系列可以将一个可执行的二进制文件覆盖在新进程的用户级上下文的存储空间上,以更改新进程的用户级上下文。exec( )系列中的系统调用都完成相同的功能,它们把一个新程序装入内存,来改变调用进程的执行代码,从而形成新进程。如果exec( )调用成功,调用进程将被覆盖,然后从新程序的入口开始执行,这样就产生了一个新进程,新进程的进程标识符id 与调用进程相同。
exec( )没有建立一个与调用进程并发的子进程,而是用新进程取代了原来进程。所以exec( )调用成功后,没有任何数据返回,这与fork( )不同。exec( )系列系统调用在UNIX系统库unistd.h中,共有execl、execlp、execle、execv、execvp五个,其基本功能相同,只是以不同的方式来给出参数。
#include<unistd.h> int execl(const char *pathname, const char *arg, …); int execlp(const char *filename, const char *arg, …); int execle(const char *pathname, const char *arg, …, const char *envp[ ]); int execv(const char *pathname, char *const argv[ ]); int execvp(const char *filename, char *const argv[ ]);
参数:
path参数表示你要启动程序的名称包括路径名。
arg参数表示启动程序所带的参数,一般第一个参数为要执行命令名,不是带路径且arg必须以NULL结束。
返回值:成功返回0,失败返回-1
注:上述exec系列函数底层都是通过execve系统调用实现。
1)带l 的exec函数:execl,execlp,execle,表示后边的参数以可变参数的形式给出且都以一个空指针结束。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(void) { printf("entering main process---\n"); execl("/bin/ls","ls","-l",NULL); printf("exiting main process ----\n"); return 0; )
运行结果:利用execl将当前进程main替换掉,所有最后那条打印语句不会输出。
2)带 p 的exec函数:execlp,execvp,表示第一个参数path不用输入完整路径,只有给出命令名即可,它会在环境变量PATH当中查找命令
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(void) { printf("entering main process---\n"); if(execl("ls","ls","-l",NULL)<0) // if(execlp("ls","ls","-l",NULL)<0) perror("excl error"); return 0; }
结果不能替换,因没有指定路径名。若将蓝色语句换成红色部分内容执行,则可以替换成功。
3)不带 l 的exec函数:execv,execvp表示命令所需的参数以char *arg[]形式给出且arg最后一个元素必须是NULL。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(void) { printf("entering main process---\n"); int ret; char *argv[] = {"ls","-l",NULL}; ret = execvp("ls",argv); if(ret == -1) perror("execl error"); printf("exiting main process ----\n"); return 0; }
0;
}
4)带 e 的exec函数:execle表示,将环境变量传递给需要替换的进程
3、exec( )和fork( )联合使用
系统调用exec和fork( )联合使用能为程序开发提供有力支持。用fork( )建立子进程,然后在子进程中使用exec( ),这样就实现了父进程与一个与它完全不同子进程的并发执行。
一般,wait、exec联合使用的模型为:
int status; ............ if (fork( )= =0) { ...........; execl(...); ...........; } wait(&status);
4、wait( )
当一个子进程先于父进程结束运行时,它与其父进程之间的关联还会保持到父进程也正常地结束运行,或者父进程调用了wait才告终止。
子进程退出时,内核将子进程置为僵尸状态,这个进程称为僵尸进程,它只保留最小的一些内核数据结构,以便父进程查询子进程的退出状态。
进程表中代表子进程的数据项是不会立刻释放的,虽然不再活跃了,可子进程还停留在系统里,因为它的退出码还需要保存起来以备父进程中后续的wait调用使用。它将称为一个“僵进程”。
• 调用wait函数查询子进程退出状态,若子进程没有运行完,则父进程会被挂起,等待子进程运行结束。如果子进程没有完成,父进程一直等待。wait( )将调用进程挂起,直至其子进程因暂停或终止而发来软中断信号为止。如果在wait( )前已有子进程暂停或终止,则调用进程做适当处理后便返回。
系统调用格式:
#include<sys/types.h> #include<sys/wait.h> int wait(status) int *status;
其中,status是用户空间的地址。它的低8位反应子进程状态,为0表示子进程正常结束,非0则表示出现了各种各样的问题;高8位则带回了exit( )的返回值。exit( )返回值由系统给出。
当一个进程结束时,Linux 系统将产生一个SIGCHLD 信号通知其父进程。在父进程未查询子进程结束的原因时,该子进程虽然停止了,但并未完全结束。此时这一子进程被称为僵尸进程(zombie process)。例如,在有些情况下父进程先于子进程退出,于是会看到在系统提示符“$”后子进程仍然在连续输出信息,这对用户是非常不友好的。我们可以使用系统调用wait,来让父进程处于等待状态,直到子进程退出后才继续执行后面的语句。
参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就象下面这样:
pid = wait(NULL);
如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1。
如果参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的,以及正常结束时的返回值,或被哪一个信号结束的等信息。由于这些信息被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(macro)来完成这项工作。
5、exit( )
进程结束可通过相应的函数实现:
#include<stdlib.h> void exit(int status); //终止正在运行的程序,关闭所有被该文件打开的文件描述符。其中,status是返回给父进程的一个整数,以备查考。 int atexit(void (*function)(void)); //用于注册一个不带参数也没有返回值的函数以供程序正常退出时被调用。参数function 是指向所调用程序的文件指针。调用成功返回0,否则返回-1,并将errno 设置为相应值 int on_exit(void (*function)(int,void *),void *arg); //作用与atexit 类似,不同是其注册的函数具有参数,退出状态和参数arg 都是传递给该函数使用 void abort(void); //用来发送一个SIGABRT 信号,该信号将使当前进程终止 WIFEXITED(status):这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。 WEXITSTATUS(status)取得子进程exit()返回的结束代码,一般会先用WIFEXITED 来判断是否正常结束才能使用此宏。 WIFSIGNALED(status)如果子进程是因为信号而结束则此宏值为真终止进程的执行。
为了及时回收进程所占用的资源并减少父进程的干预,UNIX/LINUX利用exit( )来实现进程的自我终止,通常父进程在创建子进程时,应在进程的末尾安排一条exit( ),使子进程自我终止。exit(0)表示进程正常终止,exit(1)表示进程运行有错,异常终止。
如果调用进程在执行exit( )时,其父进程正在等待它的终止,则父进程可立即得到其返回的整数。核心须为exit( )完成以下操作:
(1)关闭软中断
(2)回收资源
(3)写记帐信息
(4)置进程为“僵死状态”
实验设备与软件环境
安装环境:分为软件环境和硬件环境
硬件环境:内存ddr3 4G及以上的x86架构主机一部
系统环境:windows 、linux或者mac os x
软件环境:运行vmware或者virtualbox
软件环境:Ubuntu操作系统
实验内容
如何在Ubuntu内创建C语言程序并编写运行?
解决方法:
使用vim创建hello.c文件(vim hello.c),接着使用gcc将其转化为可执行文件(gcc hello.c -o hello),最后运行hello可执行文件(./hello)。
如何调用fork( )创建子进程?
解决方法:
利用语句while((pid1=fork())==-1),确保子进程创建成功。
1、进程的创建
编写一段程序,使用系统调用fork( )创建两个子进程,在系统中有一个父进程和两个子进程活动。让每个进程在屏幕上显示一个字符;父进程显示字符“a”,子进程分别显示字符“b” 和“c”。试观察记录屏幕上的显示结果,并分析原因。
# include<stdio.h> # include<unistd.h> int main() { int p1, p2; while((p1=fork())==-1); if(p1==0) putchar('b'); else { while((p2=fork())==-1); if(p2==0) putchar('c'); else putchar('a'); } }
A、创建流程:
创建两个子进程,利用语句while((pid1=fork())==-1),确保子进程创建成功。
当子进程的返回值为0时,输出两个子进程分别输出字符b和c。返回值非0时,为父进程,输出字符a。
B、结果分析:
字符a为父进程,字符c和b分别为第二个子进程和第三个子进程。所以先运行父进程输出字符a,父进程结束后子进程的返回值为0,此时两个子进程分别输出字符b和c。
2、修改已编写的程序,将每个进程的输出由单个字符改为一句话,再观察程序执行时屏幕上出现的现象,并分析其原因。
# include<stdio.h> # include<unistd.h> int main() { int p1, p2, i; while((p1=fork())==-1); if(p1==0) for(i=0; i<500; i++) printf("child%d\n”,i); else { while((p2=fork())==-1); If(p2==0) for(i=0; i<500; i++) printf("son%d\n”,i); else for(i=0; i<500; i++) printf("daughter%d\n”,i); } }
A、创建流程:
修改已编写的程序,利用语句while((pid1=fork())==-1),确保子进程创建成功。利用for语句,对每一进程进行500次循环处理。
B、结果分析:
两进程分别执行,当较长次数循环的时候,两者并发执行输出。
3、编写程序创建进程树如图3.1和图3.2所示,在每个进程中显示当前进程识别码和父进程识别码。
创建图3.1进程树代码:
# include<stdio.h> # include<unistd.h> int main() { int p1,p2,p3; while((p1=fork())== -1); if(p1==0) { while((p2=fork())==-1); if(p2==0) { while((p3=fork())==-1); if(p3==0) { //putchar('d'); printf("I am D,My pid is %d, my parent's pid is %d\n", getpid(), getppid()); } else { //putchar('c'); printf("I am C,My pid is %d, my parent's pid is %d\n", getpid(), getppid()); } } else { //putchar('b'); printf("I am B,My pid is %d, my parent's pid is %d\n", getpid(), getppid()); } } else { //putchar('a'); printf("I am A,My pid is %d\n", getpid()); } printf("\n"); getchar(); }
A、创建流程:
按照图的顺序创建b,c,d三个子进程,利用语句while((pid1=fork())==-1),确保子进程创建成功。
返回值非0时,为父进程,输出父进程的内容。当子进程的返回值为0时,输出子进程b的内容。
B、结果分析:
返回值p1非0时,a为父进程,输出父进程a,如图所示此时父进程a的进程ID为15939,父进程a完成p1变为0创建子进程b。
当子进程b的返回值p1为0时,输出子进程b,此时子进程b的进程ID为15940,可以看出他的父进程的ID是15939,子进程b完成p2变为0创建子进程c。
当子进程c的返回值p2为0时,输出子进程c,此时子进程c的进程ID为15941,可以看出他的父进程的ID是15940,子进程c完成p3变为0创建子进程d。
当子进程d的返回值p3为0时,输出子进程3,此时子进程3的进程ID为15942,可以看出他的父进程的ID是15941,子进程d完成。
创建图3.2进程树代码:
#include<stdio.h> #include<unistd.h> int main() { int p1_B,p1_C,p2_D,p2_E; while((p1_B=fork())== -1); if(p1_B==0) { printf("I am B,My pid is %d, my parent's pid is %d\n", getpid(), getppid()); while((p1_C=fork())== -1); if(p1_C==0) printf("I am C,My pid is %d, my parent's pid is %d\n", getpid(), getppid()); } else { printf("I am A,My pid is %d\n",getpid()); while((p2_D=fork())==-1); if(p2_D==0) { printf("I am D,My pid is %d, my parent's pid is %d\n", getpid(), getppid()); while((p2_E=fork())== -1); if(p2_E==0) printf("I am E,My pid is %d, my parent's pid is %d\n", getpid(), getppid()); } } printf("\n"); getchar(); }
A、创建流程:
按照图的顺序创建b,d两个子进程,在子进程b里面再创建子进程c,在子进程d里面再创建子进程e。利用语句while((pid1=fork())==-1),确保子进程创建成功。
返回值非0时,为父进程,输出父进程的内容。当子进程的返回值为0时,输出子进程b的内容。
B、结果分析:
返回值非0时,a为父进程,输出父进程a,如图所示此时父进程a的进程ID为16157,父进程a完成p1_B变为0创建子进程b和p2_D变为0创建子进程d。
当子进程的返回值为0时,输出子进程b和d,此时子进程b的进程ID为16158,子进程d的进程ID为16159,可以看出他们的父进程的ID都是16157(即父进程a的进程ID)。
此时子进程b,d完成,父进程b完成p1_C变为0创建子进程c,此时子进程c的进程ID为16161,他的父进程的ID是16158(即父进程b的进程ID);父进程d完成p2_E变为0创建子进程e,此时子进程e的进程ID为16160,他的父进程的ID是16159(即父进程d的进程ID)。
4、了解系统调用(execl,execlp,execle,execv,execvp)使用
创建一个进程,并使用execl将其替换成其他程序的进程
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/types.h> #include<sys/wait.h> int main() { int pid; pid=fork(); switch(pid) { case -1: printf("fork fail!\n"); exit(1); case 0: printf("Child process PID:%d\n",getpid()); execl("/bin/ls","ls","-1",NULL); printf("exec fail!\n"); exit(1); default: printf("Parent process PID: %d\n",getpid()); wait(NULL); printf("ls completed !\n"); exit(0); } }
运行程序并回答以下问题:
问题1:该程序中一共有几个进程并发?
答:该程序中一共有2个进程并发。
问题2:程序的运行结果为什么含义?
答:程序的运行结果说明先根据返回值进行输出,之后两个并发程序先后执行。带l 的exec函数:execl表示后边的参数以可变参数的形式给出且都以一个空指针结束。利用execl将当前进程main替换掉,所有最后那条打印语句不会输出。所以在父进程后会运行子进程并打印出当前文件所在的文件夹的所有文件的不加后缀名的名字以及加后缀名的名字。
5、父进程查询子进程的退出
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/wait.h> #include<sys/types.h> int main() { pid_t pid; char *message; int n; printf("Fork program starting\n"); pid=fork(); switch(pid) { case -1: printf("Fork error!\n"); exit(1); case 0: message="Child process is printing."; n=5; break; default: message="Parent process id printing."; n=3; break; } for(; n>0; n--) { puts(message); sleep(1); } //红色部分开始 if(pid) { pid_t child_pid; int stat_val; child_pid=wait(&stat_val); printf("Child has finished: PID=%d\n",child_pid); } //红色部分结束 exit(0); }
运行结果
不加红色代码运行结果
运行程序并回答以下问题:
问题1:该程序如果不加红色代码部分其运行结果是什么?为什么结果会出现在下一行的提示符“#”或“S”后?
答:该程序如果不加红色代码部分其运行结果如图3所示,我们可以看到程序在系统提示符“$”后子进程仍然在连续输出信息。
结果会出现在下一行的提示符“#”或“S”后的原因是:父进程先于子进程退出,于是会看到在系统提示符“S”后子进程仍然在连续输出信息。我们可以通过使用系统调用wait函数,来让父进程处于等待状态,直到子进程退出后才继续执行后面的语句。
问题2:添加红色部分程序的作用是什么?
答:添加红色部分程序的作用是便于父进程查询子进程的退出,防止父进程先于子进程退出导致结果会出现在下一行的提示符“#”或“$”后。
调用wait函数可以查询子进程退出状态,若子进程没有运行完,则父进程会被挂起,等待子进程运行结束。如果子进程没有完成,父进程一直等待。wait( )将调用进程挂起,直至其子进程因暂停或终止而发来软中断信号为止。如果在wait( )前已有子进程暂停或终止,则调用进程做适当处理后便返回。
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/types.h> #include<sys/wait.h> int main() { int status; pid_t pc,pr; pc=fork(); if(pc<0) printf("error ocurred!\n"); else if(pc==0) { printf("This is child process with pid of %d.\n",getpid()); exit(3); } else { pr=wait(&status); //红色部分开始 if(WIFEXITED(status)) { printf("the child process %d exit normally\n",pr); printf("the return code is %d\n",WEXITSTATUS(status)); } //红色部分结束 else printf("the child process %d exit abnormally\n",pr); } }
运行程序并回答以下问题:
问题1:该程序红色代码部分的含义是什么?
答:该程序红色代码部分的含义是可以通过WIFEXITED(status)返回的值判断子进程是否为正常退出的,若此值为非0,表明进程正常结束。若上宏为真,此时可通过WEXITSTATUS(status)获取进程退出状态(exit时参数)。
问题2:程序的运行结果是什么,理解wait函数的作用?
答:程序的运行是先进入父进程,但是因为父进程中的wait()函数阻塞了父进程,会暂停当前父进程的执行等待子进程结束。fork函数调用没有错误不会返回负数所以不会出现error ocurred!的报错,而是返回0进入子进程,在子进程完成后,父进程继续开始执行。通过WIFEXITED(status)返回的值判断子进程是否为正常退出,若返回的值为非0 ,此时通过WEXITSTATUS(status)获取进程退出状态(exit时参数)。
父进程调用wait函数可以回收子进程终止信息。该函数(wait())有三个功能:
① 阻塞等待子进程退出。
② 回收子进程残留资源。
③ 获取子进程结束状态(退出原因)存放在参数中。
6、进程的终止
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/types.h> #include<sys/wait.h> void h_exit(int status); static void forkerror(void); static void waiterror(void); int main(void) { pid_t pid; int status; if(pid=fork()<0) atexit(forkerror); else if(pid==0) abort(); if(wait(&status)!=pid) atexit(waiterror); h_exit(status); } void h_exit(int status) { if(WIFEXITED(status)) printf("Normal termination, exit status=%d.\n",WEXITSTATUS(status)); else if(WIFSIGNALED(status)) printf("Abnormal termination, exit status=%d.\n",WEXITSTATUS(status)); } void forkerror(void) { printf("Fork error!\n"; } void waiterror(void) { printf("Wait error!\n"); }
运行程序并回答以下问题:
问题1:该程序的含义是什么?
答:程序含义:可以理解为进程的终止过程。当进程完成最后语句时,会调用h_exit()函数请求操作系统删除自身,最后进程终止。
问题2:程序的运行结果是什么,请解释h_exit函数的作用?
答:程序运行结果是出现Aborted (core dumped)。Core的意思是内存, Dump的意思是扔出来, 堆出来。表示进程异常终止,进程用户空间的数据被写到磁盘。
函数h_exit()要引入头文件#include<stdlib.h>,它的作用就是结束当前进程或者程序。
7、了解system 函数的用法
用户可以使用该函数来在自己的程序中调用系统提供的各种命令。
#include<stdlib.h> int system(const char *cmdstring);
参数cmdstring 是一个字符串指针,指向表示命令行的字符串。该函数的实现是通过调用fork、exec 和waitpid 函数来完成的,其中任意一个调用失败则system 函数的调用失败,故返回值较复杂。
system()会调用fork()产生子进程,由子进程来调用/bin/sh-c string来执行参数string字符串所代表的命令,此命令执行完后随即返回原调用的进程。
下面的程序通过system(“ps –ax”);查看所有正在运行的进程。
#include<stdlib.h> #include<stdio.h> int main() { printf("running ps with system.\n"); system("ps –ax"); printf("Done.\n"); exit(0); }
思考
1、系统是怎样创建进程的?
简单的说有四步,首先申请空白PCB(进程控制块),为新进程分配资源,接着初始化PCB,最后新进程插入就绪队列。
详细的说操作系统可以创建新的进程,进程也可以创建新的进程。
最初的进程是由操作系统创建的,操作系统在初始化的过程中会创建一系列进程。这些进程中有的是用户可见的,主要用来和用户进行交互,比如Windows或Linux开机后输入用户密码,这就是一个进程,这类进程被称之为前端进程。有的是用户看不到的在背后默默运行的进程,比如用来检测系统是否有更新的进程,这类进程被称之为后端进程。
用户程序可以通过系统调用来创建新的进程。在Linux下这个系统调用是fork,系统通过调用fork函数创建进程,当一个进程调用了fork以后,系统会创建一个子进程,这个子进程和父进程不同的地方只有他的进程ID和父进程ID,其他的都是一样,而此时子进程也与父进程分开执行,各自执行自己的操作。而对于父进程和子进程的执行顺序,取决于操作系统的调度算法。一旦子进程被创建,父子进程相互竞争系统的资源。注意:子进程总是从fork之后开始复制父进程的。如果Fork调用成功,则在父进程会返回新建立的子进程标识符(PID),而在新建立的子进程中则返回0。如果fork失败则直接返回-1。可以利用语句while((pid1=fork())==-1),确保子进程创建成功。
2、当首次调用新创建进程时,其入口在哪里?
当首次调用新创建进程时,其入口在fork()后的下一个语句。
fork()函数被调用一次,但返回两次;两次返回区别在于:子程序返回值是0,而父进程返回值是子进程的ID。子进程和父进程运行相同的代码,但是有自己的数据空间。
3、当前运行的程序(主进程)的父进程是什么?
这里调用fork()创建新进程的进程即为父进程,而相对应的为其创建出的进程则为子进程,因而除了进程0以外的进程都只有一个父进程,但一个进程可以有多个子进程。
4、怎样用C程序实现进程的控制? 当首次调用新创建进程时,其入口在哪里?
C程序通过fork函数实现进程的控制。
当首次调用新创建进程时,其入口在fork()后的下一个语句。
5、系统调用fork( )是如何创建进程的?系统调用exit( )是如何终止一个进程的?
fork()函数被调用一次,但返回两次;两次返回的区别是:子进程的返回值是0,而父进程的返回值是子进程的ID。子进程和父进程运行相同的代码,但是有自己的数据空间。exit函数先要进行处理操作,然后将控制权交给内核。
6、系统调用exec 系列函数是如何更换进程的可执行代码的?
exec( )系列中的系统调用都完成相同的功能,它们把一个新程序装入内存,来改变调用进程的执行代码,从而形成新进程。如果exec( )调用成功,调用进程将被覆盖,然后从新程序的入口开始执行,这样就产生了一个新进程。exec( )没有建立一个与调用进程并发的子进程,而是用新进程取代了原来进程。