第16课-信号量互斥编程
16.1 公示栏问题(问题引入)
1. 问题描述
这里面我们举一个小例子。在一个班级里就有一个公示栏,A同学想写“数学课考试”,B同学想写“英语课取消”。但是有一个时间,A同学只写下了“数学课”三个字,没来得及写后面的内容就出去了,但是这个时候B同学来写下了“英语课取消”。这样让同学们看来就成了“数学课英语课取消”,给班级的其他同学造成了歧义。
这也就是我们说的同时访问一个资源,造成了,数据的混乱。若是有多个进程同时访问一个资源,同样会造成这个问题。
2. 问题程序化
A同学用进程A代替,B同学用进程B代替,公示栏用文件来代替。我们在先关课程的文件夹里面建立student1.c和student2.c。
注:这里加入一个操作---ctil+shift+t可以完成复制一个一样的命令栏窗口。
stduent1程序:
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
void main()
{
int fd = 0;
//0.打开文件
fd = open("./board.txt",O_RDWR|O_APPEND,0777);
//1.向公告板文件里面写入“数学课”
write(fd,"math class",11);
//2.暂停休息
sleep(5);
//3.向公告板文件里写入“取消”
write(fd,"is cancel",11);
close(fd);
}
stduent2程序:
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
void main()
{
int fd = 0;
//0.打开文件
fd = open("./board.txt",O_RDWR|O_APPEND,0777);
//1.向公告板文件里面写入“英语课考试”
write(fd,"English exam",20);
//2.关闭文件
close(fd);
}
运行结果:我们开起两个中断在一个终端中运行:./stduent1,5s内在另一个终端中运行./stduent2。我们查看提前创建好的board.txt文件显示为:math class English exam
;is cancel
3. 信号量概念
为了能够是一个程序不会出现访问混乱的情况,我们可以说白一点,像厕所一样,有人进入就有标识牌显示有人,即使时间很长。从而达到了互斥的访问。这一个概念就是信号量。
l 概念:信号量(又名:信号灯)与其他进程间通讯方式不大相同,主要用途是保护临界资源(进程互斥)。进程可以根据它判定是否能够访问某些共享资源。除了用于控制外,还可以用于进程同步。
l 分类:
二值信号灯:信号灯的值只能取0或1;
计数信号灯:信号灯的值可以取任意非负值。
l 信号量使用流程:
1) 打开信号量,获得标示符
2) 利用标示符,进行一系列操作
16.2 函数学习
1. 创建/打开信号量
(1)函数名
semget
(2)函数原型
int semget(key_t key, int nsems, int semflg);
(3)函数功能
获取信号量集合(一个信号或者多个信号)的标示符(get a semaphore set identifier),当key所指的信号量不存在,并且semflg里面包含了IPC_CREAT,就会创建这个信号量集合。
(4)所属头文件
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
(5)返回值
成功:返回信号量集合的标示符;
失败:-1.
(6)参数说明
l 什么是“键值”:(文件创建有文件名,使用文件的时候有文件的fd(id号);ipc对象创建的时候有键值(一个数字),使用的时候有标示符)对于一个文件,我们只要找到文件名,我们就能找到文件id(fd),打开文件。对于一个信号量(ipc对象)无论使用还是没有使用都是具有键值的,我们要知道他的“键值”我们就能打开它的标示符。标示符是打开这个ipc对象才会有的,键值是无论打开与没打开都有的。无论是A进程还是B进程,我们要找到它的信号量,我们都要知道它的“键值”。在我们创建信号量的时候(创建之后就会产生),我们就为它指定了键值。键值如何选取呢(在ipc对象创建的时候)?这里我们给出两个方法:
(1)任意指定一个数
缺点:这个数已经被别的IPC对象(消息队列,共享内存)所使用了,在于新创建的信
量关联时就会失败。
(2)构造一个尽量不会被别的IPC对象用到的数的方法:使用key_t ftok(char *fname,int id)
这个函数有两个参数,第一个是文件名,第二个是项目id。根据这个文件名,我们可以得到一个数字,这个数字,是文件在内核里面保存文件信息的一个文字。将这个数字再和项目id组合起来,就形成我们所要的键值了。这个键值的数字的产生:内核中保存该文件名的结构中的一个数字 + 项目id,这样形成的一个数字。也就是我们知道文件名和项目id可以得到ipc对象的键值,知道ipc对象的键值可以得到ipc对象的标示符。
每个进程的标示符有唯一的键值与之对应。
key:打开的信号量对应的键值。
semflg:标志,可以取IPC_CREAT,当key所指的信号量不存在,并且semflg里面包含了IPC_CREAT,就会创建这个信号量集合。
nsems:创建的这个信号量集合里面包含的信号量数目,是个数字。
2. 操作信号量
(1)函数名
semop
(2)函数原型
int semop(int semid, struct sembuf *sops, unsigned nsopos);
(3)函数功能
操作信号量集合里面的信号量。
(4)所属头文件
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
(5)返回值
成功:0;
失败:-1.
(6)参数说明
semid:要操作的信号量集合(可以一个可以多个)的标示符。
sembuf:它包含下面的集合,
unsigned short sem_num; /*semaphore number*/ 操作的信号量集合的编号
short sem_op; /*semphore operation*/ 正数表示释放(+1),负数表示获取(-1),不成功导致程序等待
short sem_flg; /*operation flags*/
nsops:一共操作了多少的信号量
sops:对信号量执行了什么样的操作,一个指针对应一个操作,若是有三个操作,就是组数为3的数组。
16.3 综合实例编程
A在使用黑板前获取信号量,然后开始使用黑板。使用黑板的时候,信号量的值应该是0。使用完了黑板释放信号量,信号量变成1。由于是A同学率先使用这个信号量的,所以要由A同学来创建这个信号量。
B同学在使用的时候在先获取信号量,能够获取的到,就直接使用。若在获取的过程中,发现这个信号量被A获取了,就等待直到A同学使用完毕。B同学使用完信号量,释放信号量。
stduent1.c
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/sem.h>
void main()
{
int fd = 0;
int semid;
key_t key;
struct sembuf sops;
//创建信号量的键值
key = ftok("/home",1); //使用文件名,和数字的方式,可以任意组合。
//创建并打开信号量
semid = semget(key,1,IPC_CREAT);//在系统中没有这个信号量,用参数IPC_CREAT;//就用一个信号量,使用数字1
//0.打开文件
fd = open("./board.txt",O_RDWR|O_APPEND,0777);
//1.1 获取信号量
sops.sem_num = 0; //我们的信号量集合里面只有一个信号,我们操作的信号的序//号一定是0
sops.sem_op = -1; //-1表示获取这个信号量
semop(semid,&sops,1); //操作一个信号量,第三个参数用1;第二个参数表示执行什么样的操作
//1.向公告板文件里面写入“数学课”
write(fd,"math class",11);
//2.暂停休息
sleep(10);
//3.向公告板文件里写入“取消”
write(fd,"is cancel",11);
//释放信号量
sops.sem_num = 0; //第一个信号量
sops.sem_op = 1; //释放信号量,+1的操作
semop(semid,&sops,1);
close(fd);
}
Stduent2.c
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/sem.h>
void main()
{
int fd = 0;
int semid;
key_t key;
struct sembuf sops;
//创建信号量的键值
key = ftok("/home",1); //保证两个学生操同一个信号量,键值的组成是一样的。
//打开信号量集合
semid = semget(key,1,IPC_CREAT);
//0.打开文件
fd = open("./board.txt",O_RDWR|O_APPEND,0777);
//获取信号量
sops.sem_num = 0;
sops.sem_op = -1;
semop(semid,&sops,1);
//1.向公告板文件里面写入“英语课考试”
write(fd,"English exam",12);
//释放信号量
sops.sem_num = 0;
sops.sem_op = 1;
semop(semid,&sops,1);
//2.关闭文件
close(fd);
}
运行结果:当我们在两个终端中分别运行./student1和./student2时,我们按道理应该看到的情况是,第一个程序在等待,第二个程序也在等待,直到第一个程序完成。并且写入的数据不混乱。但是,我们这两种期望都没看到。运行B程序,瞬间就结束了;写入的数据也不对。这里我们忽略了一个东西,我们前面一直在做一个假设,就是信号量的初始值是1。
实际的信号量的值不一定是1,可能是4、5、6等其他值。
这里我们引入另一个函数:semctl
函数原型
int semctl(int semid , int sennum, int cmd,.../*union semum arg*/); //有10种,我们用到GETVAL这个命令
下面在student.c加入这两行命令: int ret;
ret = semctl(semid,1,GETVAL);
发现它的初始值是:-1;
下面是更改后的程序
student1.c
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/sem.h>
void main()
{
int fd = 0;
int semid;
key_t key;
int ret;
struct sembuf sops;
//创建信号量的键值
key = ftok("/home",1);
//创建并打开信号量集合
semid = semget(key,1,IPC_CREAT);
ret = semctl(semid,0,SETVAL,1); //给信号量赋初值:1
printf("init value of sem is %d ",ret);
//0.打开文件
fd = open("./board.txt",O_RDWR|O_APPEND,0777);
//1.1 获取信号量
sops.sem_num = 0;
sops.sem_op = -1;
semop(semid,&sops,1);
//1.向公告板文件里面写入“数学课”
write(fd,"math class",11);
//2.暂停休息
sleep(10);
//3.向公告板文件里写入“取消”
write(fd,"is cancel",11);
//释放信号量
sops.sem_num = 0;
sops.sem_op = 1;
semop(semid,&sops,1);
close(fd);
}
student2:
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/sem.h>
void main()
{
int fd = 0;
int semid;
key_t key;
struct sembuf sops;
int ret;
//创建信号量的键值
key = ftok("/home",1);
//打开信号量集合
semid = semget(key,1,IPC_CREAT);
//0.打开文件
fd = open("./board.txt",O_RDWR|O_APPEND,0777);
ret = semctl(semid,0,GETVAL); //看看信号量的值是多少
printf("ret is %d ",ret);
//获取信号量
sops.sem_num = 0;
sops.sem_op = -1;
semop(semid,&sops,1);
//1.向公告板文件里面写入“英语课考试”
write(fd,"English exam",12);
//释放信号量
sops.sem_num = 0;
sops.sem_op = 1;
semop(semid,&sops,1);
//2.关闭文件
close(fd);
}
运行结果:在两个终端中分别运行./student1和./student2。我们能看到等待的出现。board.txt中的内容是:math class is cancel English exam
这就是我们要的结果。