实验目的
- 加深对进程概念的理解,明确进程和程序的区别;
- 进一步认识并发执行的实质;
- 分析进程争用资源的现象,学习解决进程互斥的方法;
- 了解Linux系统中进程通信的基本原理;
实验步骤
函数介绍
fork函数
在Linux中,fork函数是非常重要的函数,它的作用是从已经存在的父进程中创建一个子进程。
1
2
pid_t fork(void); //fork的返回类型为空1当一个进程调用fork函数之后,就有两个二进制代码相同的进程,运行在相同的位置,但是每个进程都开始自己的活动。
调用fork函数,当控制转移到内核中的fork代码后,内核开始做如下工作:
- 分配新的内存块和内核数据结构给子进程;
- 将父进程部分数据结构内容拷贝至子进程;
- 将子进程添加到系统进程列表;
- fork返回开始调度器,调度。
调用fork函数返回值的三种情况:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15int fork();
pid = fork();
if(pid < 0) { //错误
printf("fork error\n");
exit(0);
}
if(pid == 0) { //子进程
printf("The child process is running now!\n");
exit(0);
}
if(pid > 0) { //父进程
printf("The parent process is running now!\n");
exit(0);
}pid < 0
,运行错误;pid > 0
,运行父进程;pid = 0
,运行子进程。
wait函数
函数原型:
1
2
3
int wait(int *status)函数功能:父进程一旦调用了wait函数就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
备注:
当父进程忘了用wait函数等待已终止的子进程时,子进程就会进入一种无父进程的状态,此时子进程就是僵尸进程;
wait()要与fork()配套出现,如果在使用fork()之前调用wait(),wait()的返回值则为-1,正常情况下wait()的返回值为子进程的PID;
如果先终止父进程,子进程将继续正常进行,只是它将由init进程(PID 1)继承,当子进程终止时,init进程捕获这个状态;
参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉毫不在意,只想把这个僵尸进程消灭掉,我们就可以设定这个参数为NULL,就像下面这样:
1
pid = wait(NULL);
如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。
如果参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中,
这是一个整数值(int),指出子进程是正常退出还是被非正常结束的,以及正常结束时的返回值,或被哪一个信号结束的等信息。由于这些信息被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,因此设计了一套专门的宏(macro)来完成这项工作,其中最常用的两个如下:WIFEXITED(status)
这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。WEXITSTATUS(status)
当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status)就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。如果进程不是正常退出的,也就是说,
WIFEXITED返回0,这个值就毫无意义。
exit函数
进程结束最常用函数,表示正常结束;
1 |
|
kill函数
删除执行中的程序和任务,异常结束;
1 | kill(int PID,int IID); |
signal函数
1 | void(* signal(int sig, void(* func)(int)))(int); |
设置处理信号的功能指定使用sig指定的信号编号处理信号的方法。 参数func指定程序可以处理信号的三种方式之一:
- 默认处理(SIG_DFL):信号由该特定信号的默认动作处理;
- 忽略信号(SIG_IGN):忽略信号,即使没有意义,代码执行仍将继续;
- 函数处理程序:定义一个特定的函数来处理信号
参数
SIG
设置处理功能的信号值;
FUNC
指向函数的指针;
返回值
返回类型与参数func的类型相同。
常用的signal
信号 功能 值 SIGNHUP 挂起 1 SIGINT 键盘中断,键盘按 Ctrl+C
2 SIGQUIT 键盘按 QUIT
3 SIGILL 非法指令 4 SIGTRAP 跟踪中断 5 SIGIOT IOT指令 6 SIGBUS 总线错 7 SIGFPE 浮点运算溢出 8 SIGKILL 要求终止进程 9 SIGUSR1 用户定义信号#1 10 SIGSEGV 段违法 11 SIGUSR2 用户定义信号#2 12 SIGPIPE 向没有读进程的管道上写 13 SIGALRM 定时器告警,时间到 14 SIGTERM kill发出的软件结束信号 15 SIGSTKFLT Linux专用,数学协处理器的栈异常 16 SIGCHLD 子进程死 17 SIGCOUNT 若已停止则继续 18 SIGPWR 电源故障 30
pipe函数
管道是一种把两个进程之间的标准输入和标准输出连接起来的机制,从而提供一种让多个进程间通信的方法,当进程创建管道时,每次都需要提供两个文件描述符来操作管道。其中一个对管道进行写操作,另一个对管道进行读操作。对管道的读写与一般的IO系统函数一致,使用write()函数写入数据,使用read()读出数据。
1 |
|
返回值:成功,返回0,否则返回-1。
参数数组包含pipe使用的两个文件的描述符。
- fd[0]:读管道;
- fd[1]:写管道;
- fd[2]:提供进程使用的文件描述符数组。
必须在fork()中调用pipe(),否则子进程不会继承文件描述符。两个进程不共享祖先进程,就不能使用pipe。
编制实现软中断通信的程序
程序分析
使用系统调用fork()创建两个子进程,再用系统调用signal()让父进程捕捉键盘上来的中断信号(即按Ctrl+c键),当父进程接受到这两个软中断的其中某一个后,父进程用系统调用kill()向两个子进程分别发送整数值为16和17软中断信号,子进程获得对应软中断信号后,分别输出下列信息后终止:
1 | Child process 1 is killed by parent !! |
父进程调用wait()函数等待两个子进程终止后,输出以下信息后终止:
1 | Parent process is killed!! |
程序流程图
程序源代码
1 |
|
程序运行结果
运行结果分析
程序运行两次,软中断后,结果输出顺序不相同。理论上,父程序在同一时间下达指令,软中断子程序1和子程序2并发执行,并发执行并不是同时执行,而可能是交叉执行,两个子程序执行完成时间不一致,即第一次运行程序2先完成,第二次运行程序1先完成。
编制实现进程的管道通信的程序
程序分析
使用系统调用pipe()建立一条管道线,两个子进程分别向管道写一句话:
1 | Child process 1 is sending a message! |
父进程从管道中先后读出来自于两个子进程p1、p2的消息,显示在屏幕上。
程序流程图
程序源代码
1 |
|
程序运行结果
运行结果分析
根据对源代码的注释分析,父进程必须先接收子程序1的消息,再接收子程序2的消息。因此必须先创建成功子程序1,再创建子程序2,这里用到了两个while()循环如下
1 | while ((pid1=fork())<0); /*如果进程1创建不成功,则空循环*/ |
因此会先打印出子程序1的PID和消息,再打印子程序2的PID和消息。
实验难点与收获
难点
- 对并发执行的理解;
- 对管道通信过程的学习等;
收获
- 学习了进程管理的相关函数如fork()、wait()、kill()、signal()、pipe()等的使用;
- 知道了并发执行并不是同一时间执行两个程序,对并发有了更确切的理解;
- 深入理解了进程互斥的概念;
- 了解了Linux系统中进程通信的基本原理等;
实验思考
通过这次实验,我对操作系统进程管理的内容有了更深入的理解。在linx操作系统中,进程的创建需要调用fork函数,该函数调用一次,返回两次。深入理解了进程互斥的概念,即两个或两个以上的进程,不能同时进入关于同一组共享变量的临界区域。进程互斥通过lockf()来实现。通过kill()函数和signal()函数深入理解进程的之间的软中断。前者是发送软中断信号,后者是接收软中断信号。通过pipe()函数理解进程之间的管道通信。