实验5 利用信号量实现进程互斥
【实验目的】
(1) 理解互斥概念、信号量机制及信号量结构;
(2) 掌握信号量的使用方法;
(3) 掌握PV操作的定义;
(4) 掌握PV操作实现互斥的方法。
【实验原理/实验基础知识】
一、 互斥
多进程不能同时访问共享的临界资源的现象称为互斥。
二、 Linux信号量结构
每一个信号量集由一个sem_array结构来描述,该结构定义在文件/usr/src/linux-2.4/include/linux/sem.h中(若实验使用操作系统为ubuntu,则在以下路径中查找/usr/src/linux-header2.6.32-21/include/linux/sem.h)。
三、 有关信号量的系统调用函数
- 创建一个新的信号量集或获取一个已经存在的信号量集
命令格式:int semget(key_t key, int nsems, int semflg)
返回值:正确时返回信号量集的标识符号,错误时返回-1。
参数说明:
- key:信号量集的key值。
a) 使用IPC_PRIVATE,由系统产生key值并返回标识符,或者返回key值已存在的信号量集的标识符。
b) 若key值不使用IPC_PRIVATE,而是由用户指定一个非0的整型数值,则对信号量集的打开或存取操作依赖于semflag参数的取值。 - nsems:指定打开或者新创建的信号量集将包括的信号量的数目。如果该key值的信号量集已存在,而semflg只指定IPC_CREAT标志,那么参数nsems必须与原来的值一致,否则也会返回错误信息。该参数最大值在linux/sem.h中定义如下。
#define SEMMSL 250 /* <= 8000 */ - semflg:当key值不为IPC_PRIVATE时使用。
a) 若只设置semflag的IPC_CREAT位,则创建一个信号量集,如果该信号量集已经存在,则返回其标识符号。
b) 若设置semflag的IPC_CREAT|IPC_EXCL位,则创建一个新的信号量集,如果该key值的信号量集已经存在,则返回错误信息。
c) 只设置IPC_EXCL位而不设置IPC_CREAT位没有任何意义。
- 对信号量的P、V操作
命令格式:int semop(int semid, struct sembuf * sops, unsigned nsops);
返回值:正确时返回0,错误时返回-1。
参数说明:
(1) semid:信号量集的标识符号,由semget()得到。
(2) sops:指向一个sembuf结构数组,该数组的每个元素对应一次信号量操作。其sembuf数据结构如下。
struct sembuf
{
unsigned short sem_num;
short sem_op;
short sem_flg;
}
其参数含义如下:
sem_num:标明是信号量集的第几个元素,第一个信号量为0,第二个信号量为1,依次类推。
sem_op:确定对信号量采取何种操作,可以为负数、正数、零。
- sem_op为负数则相当于P操作,从信号量的值中减去sem_op的绝对值:
差值大于0:表示该进程可以使用临界资源进入临界区;
差值小于0:在没有指定IPC_NOWAIT的情况下,该进程睡眠,并插入sem_queues等待队列尾部,直到请求的条件得到满足;如果指定了IPC_NOWAIT,则出错返回。 - sem_op为正数,此时相当于V操作,把它的值加到信号量中,意味着该进程释放资源。如果是互斥则出临界区,释放临界资源。
- sem_op为0,则该进程进入睡眠,直到信号量的值也为0。
系统会按顺序检查信号量等待队列(sem_pending)中的每一个成员,查看在当前信号量的状态下,其请求的操作是否可以成功,如果可以,则将它从等待队列中唤醒,并插入就绪队列等待调度运行。
sem_flg:指明操作的执行模式,它有两个标志位。一个是IPC_NOWAIT,指明以非阻塞方式操作信号量。另一个是SEM_UNDO,指明内核为信号量操作保留恢复值。
(3) nsops:是第二个参数所指向的sembuf结构数组中元素的个数,如果只有一个信号量,则为1。
实验中使用该系统调用实现P、V操作,使用格式为:
struct sembuf P,V;
semop(semid,&P,1);
semop(semid,&V,1);
- 信号量集的控制函数
命令格式:int semctl(int semid, int semnum, int cmd, union semun arg);
返回值:正确时根据cmd的不同返回需要的值或0,错误时返回-1。
参数说明:
(1) semid:信号量集的标识符,由semget()得到。
(2) semnum:指定semid信号量集的第几个信号量,在撤销信号量集时,此时参数可以默认。
(3) cmd用于指定操作类别。其取值如下:
GETVAL:返回semnum指定的信号量的semval域值。
SETVAL:置信号量semval域值为arg.val。
GETPID:返回semnum指定的信号量的sempid,即最近对该信号量进行操作的进程ID。
GETNCNT:返回semncnt。
GETZNCT:返回semzcnt。
GETALL:返回所有信号量的值。
SETALL:通过对arg.arrary更新所有信号量的值。
IPC_STAT:获取信号量集的sem_array,存入arg.buf。
IPC_SET:将arg.buf数据结构的sem_perm.uid、sem_perm.gid、sem_perm.mode成员赋给信号量的sem_array结构。
IPC_RMID:删除指定信号量集。
IPC_INFO:获取信号量集相关的信息,存放在arg.buf中。
(4) arg为5种数据的共用体类型semun,该类型在include/linux/sem.h中定义如下:
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *_buf;
void *_pad;
};
- 该数据表示int数值时,用于GETVAL、SETVAL或GETPID等设置或获取整型数据的操作。
- 该数据指向semid_ds(即sem_array)结构数据时,用于IPC_STAT或IPC_SET等针对信号量集进行的操作。
- 该数据指向unsigned short数组时,用于GETALL或SETALL。
- 该数据指向seminfo结构时,用于IPC_INFO操作,所指向的seminfo结构在include/linux/sem.h中定义,其定义如下:
struct seminfo
{
int semmap;
int semmni;
int semmns;
int semmnu;
int semmsl;
int semopm;
int semume;
int semusz;
int semvmx;
int semaem;
};
上述系统调用使用系列头文件:
#include<sys/types.h>
#inclue<linux/sem.h>
四、 信号量及其P、V操作的实现方式
- 定义信号量标识符号
int semid; - 定义信号量数据结构
- 定义P、V操作所用的数据结构
struct sembuf P,V; - 定义给信号量赋初值的参数数据结构
union semun arg;
- 申请只有一个信号量的信号量集
semid=semget(IPC_PRIVATE,1,IPC_0666);
IPC_PRIVATE:可由系统产生key值;
1:表示信号量集中只有一个信号量;
IPC_0666:表示操作权限。0666表示任意用户可读可写,只设置semflag的IPC_CREAT位,则创建一个信号量集。 - 分别对每个信号量semid赋初值
arg.val=初值;
semctl(semid,0,SETVAL,arg);
其中0表示第0个信号量,arg的值由arg.val决定,所以必须事先为arg.val赋值。 - 定义信号量P操作
P.sem_num=0;
P.sem_op=-1;
P.sem_flg=SEM_UNDO; - 定义信号量V操作
V.sem_num=0;
V.sem_op=1;
V.sem_flg=SEM_UNDO; - 对信号量semid执行P操作
semop(mutexid,&P,1); - 对信号量semid执行V操作
semop(mutexid,&V,1); - 撤销信号量
semctl(semid,IPC_REID,0);
信号量非普通变量,对它的赋值操作只能通过semctl(semid,0,SETVAL,arg)进行,其值的修改只能通过P、V操作,而不能使用普通的赋值语句。因此其初值和信号量的P、V操作需要事先定义好后才能在进程中执行。
【实验环境】VMware Workstation、RedHat
【实验步骤】
一、父子进程以非互斥方式共享访问临界资源
- 创建父子进程
- 父子进程共享一个临界资源,每个进程循环进入该临界区3次。
- 父进程每次进入临界区后显示“parent in”,出临界区显示“parent out”。
printf(“parent in\n”);
sleep(1);
printf(“parent out\n”); - 子进程每次进入临界区后显示“child in”,出临界区显示“child out”。
printf(“child in\n”);
sleep(1);
printf(“child out\n”); - 观察执行结果。
#include<stdio.h>标准输入输出函数库
#include<stdlib.h>是C语言库头文件之一,包含了以下函数:
1 字符串转换为数字的函数,包括atoi, atof, strtol等。
2 随机数函数,包括srand, rand等。
3 内存分配释放函数,如malloc,calloc,realloc,free等。
4 程序运行控制函数,如exit, abort等。
5 系统访问相关函数,如printenv, setenv,system等。
6 常用算法函数,如qsort, bsearch, abs,div等。
#include<unistd.h>不是c语言的东西,是linux/unix的系统调用,包含了许多U N I X系统服务的函数原型,例如 read,write和getpid函数。unistd.h在unix中类shu似于window中的windows.h!
#include<sys/types.h>此头文件还包含适当时应使用的多个基本派生类型。尤其是以下类型更为重要:clock_t 表示系统时间(以时钟周期为单位)。 dev_t 用于设备号。 off_t 用于文件大小和偏移量。 ptrdiff_t 是一种带符号整型,用于对两个指针执行减法运算后所得的结果。 size_t 反映内存中对象的大小(以字节为单位)。 ssize_t 供返回字节计数或错误提示的函数使用。 time_t 以秒为单位计时。 所有这些类型在 ILP32 编译环境中保持为 32 位值,并会在 LP64 编译环境中增长为 64 位值。
#include<sys/sem.h>是glibc提供的调用接口,#include <linux/sem.h>是系统提供的调用接口,本身不是同一个实现。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<linux/sem.h>
int mutexid;
int main()
{
int chld,i,j;
while((chld=fork())-1);
if(chld>0)
{
i=1;
while(i<=3)
{
sleep(1);
printf(“parent in.\n”);
sleep(1);
printf(“parent out.\n”);
i++;
}
wait(0);
exit(0);
}
else
{
j=1;
while(j<=3)
{
sleep(1);
printf(“child in.\n”);
sleep(1);
printf(“child out.\n”);
j++;
}
exit(0);
}
}
二、PV操作实现进程互斥。以实验步骤一为基础,增加PV操作,实现父子进程互斥访问临界区。
(1) 定义与PV操作相关的数据结构;
(2) 定义信号量,给信号量赋值;
(3) 定义PV操作;
(4) PV操作实现进程互斥。
(5) 观察执行结果,并与实验步骤一的结果比较。
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<linux/sem.h>
int mutexid;
int main()
{
int chld,i,j;
struct sembuf P,V;
union semun arg;
untexid = semget(IPC_PRIVATE,1,0666|IPC_CREAT);
arg.val=1;
if(semctl(mutexid,0,SETVAL,arg)-1)
perror(“semctl setval error.”);
P.sem_num=0;
P.sem_op=-1;
P.sem_flg=SEM_UNDO;
V.sem_num=0;
V.sem_op=1;
V.sem_flg=SEM_UNDO;
while((chld=fork())==-1)
if(chld>0)
{
i=1;
while(i<=3)
{
sleep(1);
semop(mutexid,&P,1);//进入临界区,执行P操作
printf(“parent in.\n”);
sleep(1);
printf(“parent out.\n”);//出临界区,执行V操作
semop(mutexid,&V,1);
i++;
}
wait(0);
semctl(mutexid,IPC_RMID,0);
exit(0);
}
else
{
j=1;
while(j<=3)
{
sleep(1);
semop(mutexid,&P,1);
printf(“child in.\n”);
sleep(1);
printf(“child out.\n”);
semop(mutexid,&V,1);
j++;
}
exit(0);
}
}
【实验报告】
填写《信息技术学院学生上机实验报告》。
【思考题】
(1) 实验步骤一的结果说明什么?
(2) P、V操作在各进程代码中的位置应该如何处理?