实验一 进程管理与进程通信
- 实验目的
1、掌握进程的概念,明确进程的含义。
2、认识并了解进程并发执行的实质,进程的阻塞与唤醒,终止与退出的过程。
3、熟悉进程的睡眠、同步、撤消等进程控制方法。
4、分析进程竞争资源的现象,学习解决进程互斥的方法 。
5、了解什么是信号,利用信号量机制熟悉系统中进程之间软中断通信的基本原理,
6、熟悉消息传送的机理 ,共享存储机制 。
- 实验环境
Ubuntu16.04
- 实验内容
1、编写一段程序,使用系统调用fork( )创建两个子进程。当此程序运行时,在系统中有一个父进程和两个子进程并发执行,观察实验结果并分析原因。
- 用fork( )创建一个进程,再调用exec( ),用新的程序替换该子进程的内容,利用wait( )来控制进程执行顺序,掌握进程的睡眠、同步、撤消等进程控制方法,并根据实验结果分析原因。
- 编写一段多进程并发运行的程序,用lockf( )来给每一个进程加锁,以实现进程之间的互斥,观察并分析出现的现象及原因。
4、编写程序:用fork( )创建两个子进程,再用系统调用signal( )让父进程捕捉键盘上来的中断信号(即按^c键);捕捉到中断信号后,父进程用系统调用kill( )向两个子进程发出信号,子进程捕捉到信号后分别输出下列信息后终止:
Child process1 is killed by parent!
Child process2 is killed by parent!
父进程等待两个子进程终止后,输出如下的信息后终止:
Parent process is killed!
分析利用信号量机制中的软中断通信实现进程同步的机理。
5、使用系统调用msgget( ),msgsnd( ),msgrev( ),及msgctl( )编制一长度为1k的消息发送和接收的程序,并分析消息的创建、发送和接收机制及控制原理。
6、编制一长度为1k的共享存储区发送和接收的程序,并设计对该共享存储区进行互斥访问及进程同步的措施,必须保证实现正确的通信。
- 实验原理 实验中用到的系统调用函数(包括实验原理中介绍的和自己采用的),实验步骤
实验原理:
1、进程创建与进程并发执行
Linux中,进程既是一个独立拥有资源的基本单位,又是一个独立调度的基本单位。一个进程实体由若干个区(段)组成,包括程序区、数据区、栈区、共享存储区等。每个区又分为若干页,每个进程配置有唯一的进程控制块PCB,用于控制和管理进程。
系统为每个进程配置了一张进程区表。表中,每一项记录一个区的起始虚地址及指向系统区表中对应的区表项。核心通过查找进程区表和系统区表,便可将区的逻辑地址变换为物理地址。
进程是进程映像的执行过程,也就是正在执行的进程实体。它由三部分组成:
(1)用户级上、下文。主要成分是用户程序;
(2)寄存器上、下文。由CPU中的一些寄存器的内容组成,如PC,PSW,SP及通用寄存器等;
(3)系统级上、下文。包括OS为管理进程所用的信息,有静态和动态之分。
进程创建所涉及的系统调用:
fork( ) 创建一个新进程。
系统调用格式: pid=fork( )
参数定义:int fork( )
fork( )返回值意义如下:
0:在子进程中,pid变量保存的fork( )返回值为0,表示当前进程是子进程。
>0:在父进程中,pid变量保存的fork( )返回值为子进程的id值(进程唯一标识符)。
-1:创建失败。
如果fork( )调用成功,它向父进程返回子进程的PID,并向子进程返回0,即fork( )被调用了一次,但返回了两次。此时OS在内存中建立一个新进程,所建的新进程是调用fork( )父进程(parent process)的副本,称为子进程(child process)。子进程继承了父进程的许多特性,并具有与父进程完全相同的用户级上下文。父进程与子进程并发执行。
核心为fork( )完成以下操作:
(1)为新进程分配一进程表项和进程标识符
进入fork( )后,核心检查系统是否有足够的资源来建立一个新进程。若资源不足,则fork( )系统调用失败;否则,核心为新进程分配一进程表项和唯一的进程标识符。
(2)检查同时运行的进程数目
超过预先规定的最大数目时,fork( )系统调用失败。
(3)拷贝进程表项中的数据
将父进程的当前目录和所有已打开的数据拷贝到子进程表项中,并置进程的状态为“创建”状态。
(4)子进程继承父进程的所有文件
对父进程当前目录和所有已打开的文件表项中的引用计数加1。
(5)为子进程创建进程上、下文
进程创建结束,设子进程状态为“内存中就绪”并返回子进程的标识符。
(6)子进程执行
虽然父进程与子进程程序完全相同,但每个进程都有自己的程序计数器PC(注意子进程的PC开始位置),然后根据pid变量保存的fork( )返回值的不同,执行了不同的分支语句。
2、进程的睡眠、同步、撤消等进程控制
用fork( )创建一个进程,再调用exec( )用新的程序替换该子进程的内容,然后利用wait( )来控制进程执行顺序。
(1)exec( )系列
系统调用exec( )系列,也可用于新程序的运行。fork( )只是将父进程的用户级上下文拷贝到新进程中,而exec( )系列可以将一个可执行的二进制文件覆盖在新进程的用户级上下文的存储空间上,以更改新进程的用户级上下文。exec( )系列中的系统调用都完成相同的功能,它们把一个新程序装入内存,来改变调用进程的执行代码,从而形成新进程。如果exec( )调用成功,调用进程将被覆盖,然后从新程序的入口开始执行,这样就产生了一个新进程,新进程的进程标识符id 与调用进程相同。
exec( )没有建立一个与调用进程并发的子进程,而是用新进程取代了原来进程。所以exec( )调用成功后,没有任何数据返回,这与fork( )不同。exec( )系列系统调用在UNIX系统库unistd.h中,共有execl、execlp、execle、execv、execvp五个,其基本功能相同,只是以不同的方式来给出参数。
一种是直接给出参数的指针,如:
int execl(path,arg0[,arg1,...argn],0);
char *path,*arg0,*arg1,...,*argn;
另一种是给出指向参数表的指针,如:
int execv(path,argv);
char *path,*argv[ ];
具体使用可参考有关书。
(2)exec( )和fork( )联合使用
系统调用exec和fork( )联合使用能为程序开发提供有力支持。用fork( )建立子进程,然后在子进程中使用exec( ),这样就实现了父进程与一个与它完全不同子进程的并发执行。
一般,wait、exec联合使用的模型为:
int status;
............
if (fork( )= =0)
{
...........;
execl(...);
...........;
}
wait(&status);
(3)wait( )
等待子进程运行结束。如果子进程没有完成,父进程一直等待。wait( )将调用进程挂起,直至其子进程因暂停或终止而发来软中断信号为止。如果在wait( )前已有子进程暂停或终止,则调用进程做适当处理后便返回。
系统调用格式:
int wait(status)
int *status;
其中,status是用户空间的地址。它的低8位反应子进程状态,为0表示子进程正常结束,非0则表示出现了各种各样的问题;高8位则带回了exit( )的返回值。exit( )返回值由系统给出。
核心对wait( )作以下处理:
1)首先查找调用进程是否有子进程,若无,则返回出错码;
2)若找到一处于“僵死状态”的子进程,则将子进程的执行时间加到父进程的执行时间上,并释放子进程的进程表项;
3)若未找到处于“僵死状态”的子进程,则调用进程便在可被中断的优先级上睡眠,等待其子进程发来软中断信号时被唤醒。
(4)exit( )
终止进程的执行。
系统调用格式:
void exit(status)
int status;
其中,status是返回给父进程的一个整数,以备查考。
为了及时回收进程所占用的资源并减少父进程的干预,UNIX/LINUX利用exit( )来实现进程的自我终止,通常父进程在创建子进程时,应在进程的末尾安排一条exit( ),使子进程自我终止。exit(0)表示进程正常终止,exit(1)表示进程运行有错,异常终止。
如果调用进程在执行exit( )时,其父进程正在等待它的终止,则父进程可立即得到其返回的整数。核心须为exit( )完成以下操作:
1)关闭软中断
2)回收资源
3)写记帐信息
4)置进程为“僵死状态”
3、多进程通过加锁互斥并发运行
用lockf( )来给每一个进程加锁,以实现多进程之间的互斥。
所涉及的系统调用:lockf(files,function,size),用作锁定文件的某些段或者整个文件。
本函数的头文件为
#include "unistd.h"
参数定义:
int lockf(files,function,size)
int files,function;
long size;
其中:files是文件描述符;function是锁定和解锁:1表示锁定,0表示解锁。size是锁定或解锁的字节数,为0,表示从文件的当前位置到文件尾。
4、进程间通过信号机制实现软中断通信
(1)信号的基本概念
每个信号都对应一个正整数常量(称为signal number,即信号编号。定义在系统头文件<signal.h>中),代表同一用户的诸进程之间传送事先约定的信息的类型,用于通知某进程发生了某异常事件。每个进程在运行时,都要通过信号机制来检查是否有信号到达。若有,便中断正在执行的程序,转向与该信号相对应的处理程序,以完成对该事件的处理;处理结束后再返回到原来的断点继续执行。实质上,信号机制是对中断机制的一种模拟,故在早期的UNIX版本中又把它称为软中断。
信号与中断的相似点:
1)采用了相同的异步通信方式;
2)当检测出有信号或中断请求时,都暂停正在执行的程序而转去执行相应的处理程序;
3)都在处理完毕后返回到原来的断点;
4)对信号或中断都可进行屏蔽。
信号与中断的区别:
1)中断有优先级,而信号没有优先级,所有的信号都是平等的;
2)信号处理程序是在用户态下运行的,而中断处理程序是在核心态下运行;
(3)中断响应是及时的,而信号响应通常都有较大的时间延迟。
信号机制具有以下三方面的功能:
1)发送信号。发送信号的程序用系统调用kill( )实现;
2)预置对信号的处理方式。接收信号的程序用signal( )来实现对处理方式的预置;
3)收受信号的进程按事先的规定完成对相应事件的处理。
(2)信号的发送
信号的发送,是指由发送进程把信号送到指定进程的信号域的某一位上。如果目标进程正在一个可被中断的优先级上睡眠,核心便将它唤醒,发送进程就此结束。一个进程可能在其信号域中有多个位被置位,代表有多种类型的信号到达,但对于一类信号,进程却只能记住其中的某一个。
进程用kill( )向一个进程或一组进程发送一个信号。
(3)对信号的处理
当一个进程要进入或退出一个低优先级睡眠状态时,或一个进程即将从核心态返回用户态时,核心都要检查该进程是否已收到软中断。当进程处于核心态时,即使收到软中断也不予理睬;只有当它返回到用户态后,才处理软中断信号。对软中断信号的处理分三种情况进行:
1)如果进程收到的软中断是一个已决定要忽略的信号(function=1),进程不做任何处理便立即返回;
2)进程收到软中断后便退出(function=0);
3)执行用户设置的软中断处理程序。
(4)所涉及的中断调用
(1)kill( )
系统调用格式:int kill(pid,sig)
参数定义:int pid,sig;
其中,pid是一个或一组进程的标识符,参数sig是要发送的软中断信号。
1)pid>0时,核心将信号发送给进程pid。
2)pid=0时,核心将信号发送给与发送进程同组的所有进程。
3)pid=-1时,核心将信号发送给所有用户标识符真正等于发送进程的有效用户标识号的进程。
(2)signal( )
预置对信号的处理方式,允许调用进程控制软中断信号。
系统调用格式
signal(sig,function)
头文件为
#include <signal.h>
参数定义
signal(sig,function)
int sig;
void (*func) ( )mi
其中sig用于指定信号的类型,sig为0则表示没有收到任何信号,余者如下表:
值 | 名 字 | 说 明 |
01 | SIGHUP | 挂起(hangup) |
02 | SIGINT | 中断,当用户从键盘按^c键或^break键时 |
03 | SIGQUIT | 退出,当用户从键盘按quit键时 |
04 | SIGILL | 非法指令 |
05 | SIGTRAP | 跟踪陷阱(trace trap),启动进程,跟踪代码的执行 |
06 | SIGIOT | IOT指令 |
07 | SIGEMT | EMT指令 |
08 | SIGFPE | 浮点运算溢出 |
09 | SIGKILL | 杀死、终止进程 |
10 | SIGBUS | 总线错误 |
11 | SIGSEGV | 段违例(segmentation violation),进程试图去访问其虚地址空间以外的位置 |
12 | SIGSYS | 系统调用中参数错,如系统调用号非法 |
13 | SIGPIPE | 向某个非读管道中写入数据 |
14 | SIGALRM | 闹钟。当某进程希望在某时间后接收信号时发此信号 |
15 | SIGTERM | 软件终止(software termination) |
16 | SIGUSR1 | 用户自定义信号1 |
17 | SIGUSR2 | 用户自定义信号2 |
18 | SIGCLD | 某个子进程死 |
19 | SIGPWR | 电源故障 |
function:在该进程中的一个函数地址,在核心返回用户态时,它以软中断信号的序号作为参数调用该函数,对除了信号SIGKILL,SIGTRAP和SIGPWR以外的信号,核心自动地重新设置软中断信号处理程序的值为SIG_DFL,一个进程不能捕获SIGKILL信号。
function 的解释如下:
1)function=1时,进程对sig类信号不予理睬,亦即屏蔽了该类信号;
2)function=0时,缺省值,进程在收到sig信号后应终止自己;
3)function为非0,非1类整数时,function的值即作为信号处理程序的指针。
5、消息的发送与接收
使用系统调用msgget( ),msgsnd( ),msgrev( ),及msgctl( )编制一长度为1k的消息发送和接收的程序。
消息(message)是一个格式化的可变长的信息单元。消息机制允许由一个进程给其它任意的进程发送一个消息。当一个进程收到多个消息时,可将它们排成一个消息队列。消息使用二种重要的数据结构:一是消息首部,其中记录了一些与消息有关的信息,如消息数据的字节数;二个消息队列头表,其每一表项是作为一个消息队列的消息头,记录了消息队列的有关信息。
(1)消息机制的数据结构
(1)消息首部
记录一些与消息有关的信息,如消息的类型、大小、指向消息数据区的指针、消息队列的链接指针等。
(2)消息队列头表
其每一项作为一个消息队列的消息头,记录了消息队列的有关信息如指向消息队列中第一个消息和指向最后一个消息的指针、队列中消息的数目、队列中消息数据的总字节数、队列所允许消息数据的最大字节总数,还有最近一次执行发送操作的进程标识符和时间、最近一次执行接收操作的进程标识符和时间等。
(3) 消息队列的描述符
UNIX中,每一个消息队列都有一个称为关键字(key)的名字,是由用户指定的;消息队列有一消息队列描述符,其作用与用户文件描述符一样,也是为了方便用户和系统对消息队列的访问。
涉及的系统调用
(1) msgget( )
创建一个消息,获得一个消息的描述符。核心将搜索消息队列头表,确定是否有指定名字的消息队列。若无,核心将分配一新的消息队列头,并对它进行初始化,然后给用户返回一个消息队列描述符,否则它只是检查消息队列的许可权便返回。
系统调用格式:
msgqid=msgget(key,flag)
该函数使用头文件如下:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
参数定义
int msgget(key,flag)
key_t key;
int flag;
其中:
key是用户指定的消息队列的名字;flag是用户设置的标志和访问方式。如 IPC_CREAT |0400 是否该队列已被创建。无则创建,是则打开;
IPC_EXCL |0400 是否该队列的创建应是互斥的。
msgqid 是该系统调用返回的描述符,失败则返回-1。
(2) msgsnd()
发送一消息。向指定的消息队列发送一个消息,并将该消息链接到该消息队列的尾部。
系统调用格式:
msgsnd(msgqid,msgp,size,flag)
该函数使用头文件如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
参数定义:
int msgsnd(msgqid,msgp,size,flag)
I int msgqid,size,flag;
struct msgbuf * msgp;
其中msgqid是返回消息队列的描述符;msgp是指向用户消息缓冲区的一个结构体指针。缓冲区中包括消息类型和消息正文,即
{
long mtype; /*消息类型*/
char mtext[ ]; /*消息的文本*/
}
size指示由msgp指向的数据结构中字符数组的长度;即消息的长度。这个数组的最大值由MSG-MAX( )系统可调用参数来确定。flag规定当核心用尽内部缓冲空间时应执行的动作:进程是等待,还是立即返回。若在标志flag中未设置IPC_NOWAIT位,则当该消息队列中的字节数超过最大值时,或系统范围的消息数超过某一最大值时,调用msgsnd进程睡眠。若是设置IPC_NOWAIT,则在此情况下,msgsnd立即返回。
对于msgsnd( ),核心须完成以下工作:
1)对消息队列的描述符和许可权及消息长度等进行检查。若合法才继续执行,否则返回;
2)核心为消息分配消息数据区。将用户消息缓冲区中的消息正文,拷贝到消息数据区;
3)分配消息首部,并将它链入消息队列的末尾。在消息首部中须填写消息类型、消息大小和指向消息数据区的指针等数据;
4)修改消息队列头中的数据,如队列中的消息数、字节总数等。最后,唤醒等待消息的进程。
(3) msgrcv( )
接受一消息。从指定的消息队列中接收指定类型的消息。
系统调用格式:
msgrcv(msgqid,msgp,size,type,flag)
本函数使用的头文件如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
参数定义:
int msgrcv(msgqid,msgp,size,type,flag)
int msgqid,size,flag;
struct msgbuf *msgp;
long type;
其中,msgqid,msgp,size,flag与msgsnd中的对应参数相似,type是规定要读的消息类型,flag规定倘若该队列无消息,核心应做的操作。如此时设置了IPC_NOWAIT标志,则立即返回,若在flag中设置了MS_NOERROR,且所接收的消息大于size,则核心截断所接收的消息。
对于msgrcv系统调用,核心须完成下述工作:
1)对消息队列的描述符和许可权等进行检查。若合法,就往下执行;否则返回;
2)根据type的不同分成三种情况处理:
type=0,接收该队列的第一个消息,并将它返回给调用者;
type为正整数,接收类型type的第一个消息;
type为负整数,接收小于等于type绝对值的最低类型的第一个消息。
3)当所返回消息大小等于或小于用户的请求时,核心便将消息正文拷贝到用户区,并从消息队列中删除此消息,然后唤醒睡眠的发送进程。但如果消息长度比用户要求的大时,则做出错返回。
(4) msgctl( )
消息队列的操纵。读取消息队列的状态信息并进行修改,如查询消息队列描述符、修改它的许可权及删除该队列等。
系统调用格式:
msgctl(msgqid,cmd,buf);
本函数使用的头文件如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
参数定义:
int msgctl(msgqid,cmd,buf);
int msgqid,cmd;
struct msgqid_ds *buf;
其中,函数调用成功时返回0,不成功则返回-1。buf是用户缓冲区地址,供用户存放控制参数和查询结果;cmd是规定的命令。命令可分三类:
1)IPC_STAT。查询有关消息队列情况的命令。如查询队列中的消息数目、队列中的最大字节数、最后一个发送消息的进程标识符、发送时间等;
2)IPC_SET。按buf指向的结构中的值,设置和改变有关消息队列属性的命令。如改变消息队列的用户标识符、消息队列的许可权等;
3)IPC_RMID。消除消息队列的标识符。
msgqid_ds 结构定义如下:
struct msgqid_ds
{ struct ipc_perm msg_perm; /*许可权结构*/
short pad1[7]; /*由系统使用*/
ushort msg_qnum; /*队列上消息数*/
ushort msg_qbytes; /*队列上最大字节数*/
ushort msg_lspid; /*最后发送消息的PID*/
ushort msg_lrpid; /*最后接收消息的PID*/
time_t msg_stime; /*最后发送消息的时间*/
time_t msg_rtime; /*最后接收消息的时间*/
time_t msg_ctime; /*最后更改时间*/
};
struct ipc_perm
{ ushort uid; /*当前用户*/
ushort gid; /*当前进程组*/
ushort cuid; /*创建用户*/
ushort cgid; /*创建进程组*/
ushort mode; /*存取许可权*/
{ short pid1; long pad2;} /*由系统使用*/
}
6、进程的共享存储区通信
编制一长度为1k的共享存储区发送和接收的程序。
(2)涉及的系统调用
1)shmget( )
创建、获得一个共享存储区。
系统调用格式:
shmid=shmget(key,size,flag)
该函数使用头文件如下:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
参数定义
int shmget(key,size,flag);
key_t key;
int size,flag;
其中,key是共享存储区的名字;size是其大小(以字节计);flag是用户设置的标志,如IPC_CREAT。IPC_CREAT表示若系统中尚无指名的共享存储区,则由核心建立一个共享存储区;若系统中已有共享存储区,便忽略IPC_CREAT。
附:
操作允许权 八进制数
用户可读 00400
用户可写 00200
小组可读 00040
小组可写 00020
其它可读 00004
其它可写 00002
控制命令 值
IPC_CREAT 0001000
IPC_EXCL 0002000
例:shmid=shmget(key,size,(IPC_CREAT|0400))
创建一个关键字为key,长度为size的共享存储区
2)shmat( )
共享存储区的附接。从逻辑上将一个共享存储区附接到进程的虚拟地址空间上。
系统调用格式:
virtaddr=shmat(shmid,addr,flag)
该函数使用头文件如下:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
参数定义
char *shmat(shmid,addr,flag);
int shmid,flag;
char * addr;
其中,shmid是共享存储区的标识符;addr是用户给定的,将共享存储区附接到进程的虚地址空间;flag规定共享存储区的读、写权限,以及系统是否应对用户规定的地址做舍入操作。其值为SHM_RDONLY时,表示只能读;其值为0时,表示可读、可写;其值为SHM_RND(取整)时,表示操作系统在必要时舍去这个地址。该系统调用的返回值是共享存储区所附接到的进程虚地址viraddr。
3)shmdt( )
把一个共享存储区从指定进程的虚地址空间断开。
系统调用格式:
shmdt(addr)
该函数使用头文件如下:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
参数定义
int shmdt(addr);
char addr;
其中,addr是要断开连接的虚地址,亦即以前由连接的系统调用shmat( )所返回的虚地址。调用成功时,返回0值,调用不成功,返回-1。
4)shmctl( )
共享存储区的控制,对其状态信息进行读取和修改。
系统调用格式:
shmctl(shmid,cmd,buf)
该函数使用头文件如下:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
参数定义
int shmctl(shmid,cmd,buf);
int shmid,cmd;
struct shmid_ds *buf;
其中,buf是用户缓冲区地址,cmd是操作命令。命令可分为多种类型:
第一种:用于查询有关共享存储区的情况。如其长度、当前连接的进程数、共享区的创建者标识符等;
第二种:用于设置或改变共享存储区的属性。如共享存储区的许可权、当前连接的进程计数等;
第三种:对共享存储区的加锁和解锁命令;
第四种:删除共享存储区标识符等。
上述的查询是将shmid所指示的数据结构中的有关成员,放入所指示的缓冲区中;而设置是用由buf所指示的缓冲区内容来设置由shmid所指示的数据结构中的相应成员。
实验中用到的系统调用函数:
Fork, exec, wait, exit, getpid, sleep, lockf, kill, signal, read, write, msgget, msgsnd, msgrcv, msgctl,shmget, shmat, shmdt, shmctl。
- 实验结果分析(截屏的实验结果,与实验结果对应的实验分析)
实验1:
1)实验结果:
2)实验分析:父进程执行fork函数时,父进程会复制出一个子进程,而且在父子进程的代码从fork()函数的返回开始分别在两个地址空间中同时进行,根据返回值来判定该进程是父进程还是子进程。当p1=0时,该进程则是子进程1,输出b,当p2=0时,对应子进程2,输出c,父进程则输出a。子进程在创建后和父进程同时执行,竞争系统资源,谁先谁后,取决于内核所使用调度算法,因此会出现a>b>c,b>a>c,a>c>b等多种情况。
3)思考题:
(1)系统是怎样创建进程的?
1.申请一个空闲的PCB
2.为新进程分配资源
3.将新进程的PCB初始化
4.将新进程加入就绪队列
(2)当首次调用新创建进程时,其入口在哪里?
PCB 控制块
(3)利用strace 和ltrace -f -i -S ./executable-file-name查看程序执行过程,并分析原因,画出进程家族树。
strace:
ltrace:
原因:子进程在创建后和父进程同时执行,竞争系统资源,谁先谁后,取决于内核所使用调度算法,运行结果就会出现随机性。
进程家族树:
4)流程图:
实验2:
- 实验结果:
2.实验分析:
父进程通过fork()函数创建子进程,在子进程中,exec()函数根据指定的文件名或目录名找到可执行文件,并用他来取代原调用进程的数据段,代码段和堆栈段,可执行文件执行完之后,原调用进程的内容除了进程号之外,其他全部被新的进程替换了,而wait()函数使父进程阻塞,在子进程结束之后运行输出“ls completed!”,之后执行exit()函数结束进程
3)思考题:
(1)可执行文件加载时进行了哪些处理?
C源程序>编译预处理>编译>优化程序>汇编程序>链接程序>可执行文件
(2)什么是进程同步?wait( )是如何实现进程同步的?
进程同步:逻辑上相关的两个或多个进程为完成一项任务,通过协调活动来使用同一资源。
wait()函数用于使父进程阻塞,直到子进程结束或者该进程收到一个指定的信号为止,如果该父进程没有子进程或者它的子进程已经结束,则wait()会立刻返回。
(3)wait( )和exit()是如何控制实验结果的随机性的?
wait()用于使父进程阻塞,直到一个子进程结束或者收到一个指定信号为止,而exit()可以终止进程,执行到exit()时,进程会无条件地停止剩下的所以操作,清除各种数据结构,终止本进程的运行
4)流程图:
实验3:
- 实验结果:
- 实验分析:
同时在父进程和子进程中上锁,父子进程同时竞争系统资源,优先争夺到资源的进程先被处理,则其他进程被阻塞,当该进程处理完释放资源,在轮到其他进程
- 思考题:
(1)进程加锁和未上锁的输出结果相同吗? 为什么?
不相同,当未上锁时,各进程就会分别竞争资源,导致以下结果
- 流程图:
实验4:
- 实验结果:
- 实验分析:
用signal()让各进程进入阻塞状态,当输入ctrl+c时,父进程分别向两个子进程发生信号,并使用wait函数使父进程阻塞,同时子进程分别收到信号,开始运行,分别输出 Child Process 1 is killed by Parent!和Child Process 2 is killed by Parent!之后终止进程,同时wait()收到返回值,父进程输出Parent Process is killed!之后终止
3)思考题:
(1)为了得到实验内容要求的结果,需要用到哪些系统调用函数来实现及进程间的通信控制和同步?
通信控制:kill(),signal()
同步:exit(),wait()
(2)kill( )和signal( )函数在信号通信中的作用是什么?如果分别注释掉它们,结果会如何?
注释掉kill(),则父进程无法向子进程发送信息,导致子进程一直被阻塞,同时父进程的wait()也无法得到返回值,父进程也会一直被阻塞,程序无法正常运行
注释掉signal(),则没有signal()函数处理中断信号,导致所有进程中断,程序被终止
4)流程图:
实验5:
- 实验结果:
- 实验分析:
父进程使用msgget创建队列,同时使用msgrcv()函数反复读取队列的消息,但是队列里没有消息,直到子进程创建队列,并使用msgsnd()函数向队列发送消息,且输出“(client)sent”,父进程的msgrcv()函数才接收到队列的消息并输出“(sever)received”,并反复循环,最终用过msgclt删除队列
3)思考题:
(1)为了便于操作和观察结果,需要编制几个程序分别用于消息的发送与接收?
为了便于操作和观察结果,需要编制两个程序client.c和server.c
先运行server.c,msgrcv()会一直等待队列传来消息,接着运行client.c,向队列里发送消息,并输出(client)sent,接着msgrcv()接收到消息后输出(server)received
(2)这些程序如何进行编辑、编译和执行?为什么?
1.创建client.c和server.c,并在里面在编辑代码
2.使用gcc client.c -o client和gcc server.c -o server 对程序进行编译
3.最后通过./client和./server对程序进行执行
为了便于操作和观察结果,优先执行server.c,在执行client.c
(3)如何实现消息的发送与接收的同步?
server()通过msgget()创建队列,并使用msgrcv()循环检测队列里是否传来消息,而client()先通过msgget()打开队列,并使用msgsnd()往队列里循环发送消息,server()接收到消息,最后通过msgclt删除队列
4)流程图:
实验6:
- 实验结果:
- 实验分析:
创建子进程调用server(),通过shmget()创建共享存储区,接着使用shmat()将创建的共享内存映射到进程空间中,在使用system()函数调用shell命令“ipc”查看共享内存,之后该进程在创建一个子进程,调用client(),先通过shimget()打开共享存储区,在使用shmat()获取首地址,接着把数据写入共享内存,输出(client)sent,sever()在接收到数据后输出(server)received,最后调用shmctl撤销共享存储区
3)思考题:
(1)为了便于操作和观察结果,需要如何合理设计程序来实现子进程间的共享存储区通信?
首先创建一个共享内存区,然后子进程讲共享内存分别映射到各种的进程地址空间中,进程就可以直接读写共享内存区。
(2)比较消息通信和共享存储区通信这两种进程通信机制的性能和优缺点。
共享存储区:
共享内存是一种最为高效的进程间通信方式,因为进程可以直接读写内存,不需要任何数据的复制;由于多个进程共享一段内存,共享内存自身不提供同步机制,所以应额外实现不同的进程间的同步
消息通信:
消息队列具有一定的FIFO特性,但是可以实现消息的随机性查询,消息存在于内核中,由队列ID来标识;消息的发送端和接受端进程不需要额外实现进程间的同步
4)流程图:
六、实验总结
1.掌握了进程的概念,以及明白通过fork()创建子进程,并且搞清楚了子进程和父进程的关系,以及新建进程的入口处。
2.明白了进程之间会竞争资源,熟悉了进程的控制方法,学会了如何解决进程互斥和如何去同步进程等
3.了解了信号量机制,熟悉了进程之间软中断的基本原理
4.明白了消息队列,以及共享内存之间的消息是如何传送的