3.4 进程信号量
(1)进程信号量简介
①本质上就是共享资源的数目,用来控制对共享资源的访问。
②用于进程间的互斥和同步。
③每种共享资源对应一个信号量,为了便于大量共享资源的操作引入信号量集,可对所有信号量一次性操作。对信号量集中所有操作可以要求全部成功,也可以部分成功。
④二元(也叫双态)信号量(信号灯)值为0或1。
⑤对信号量做PV操作(P减,V加)
(2)信号量集属性
(3)创建信号量集
头文件 |
#include <sys/sem.h> |
函数 |
int semget(key_t key, int nsems, int flag); |
参数 |
key: 用户指定的信号量集键值 size:信号量集中信号量的个数 flag:IPC_CREAT、IPC_EXCL等权限组合 |
功能 |
创建信号量集 |
返回值 |
成功返回内核中信号量集的标识ID,出错返回-1 |
(4)信号量集的控制
头文件 |
#include <sys/sem.h> |
函数 |
int semctl(int semid, int semnum, int cmd, …/*union semun arg*/); |
参数 |
(1)semid: 信号量集ID (2)semnum:0表示对所有信号量操作,信号量编号从0开始。 (3)union semun{ int val; //放置获取或设置信号量集中某个信号量的值。 struct semid_ds* buf;//信号量集属性指针 unsigned short* array; //放置获取或设置信号量集中所有信号量的值。 }; (4)cmd参数:通过参数设定对信号量集要执行的操作 ①IPC_STAT:获取信号量集的属性 ==> buf ②IPC_SET: 设置信号量集的属性 ==> buf ③IPC_RMID:删除信号量集 ==> buf ④GETVAL: 返回信号量的值 ==> val ⑤SETVAL: 设置semnum信号量的值 ==> val ⑥GETALL: 获取所有信号量的值 ==> array ⑦SETALL: 设置所有信号量的初始值 ==> array |
功能 |
控制信号量集 |
返回值 |
除GETALL以外所有GET命令,semctl函数都返回相应的值。其他命令的返回值为0.成功返回内核中信号量集的标识ID,出错返回-1 |
(5)信号量集的操作
头文件 |
#include <sys/sem.h> |
函数 |
int semop(int semid, struct sembuf* sops, size_t nsops); |
参数 |
(1)semid: 信号量集ID (2)sops:sembuf结构体数组指针 (3)nsops:第2个参数中结构体数组的长度 (4)struct sembuf{ unsigned short sem_num; //信号量集中的信号量的编号 short sem_op; //正数为V操作,负数为P操作,0可用于测试资源是否用完。 short sem_flg;//SEM_UNDO标志,表示在进程结束时,相应的操作将被取消。如果设置了该标志,那么在进程没有释放共享资源就退出时,内核将代为释放。 }; |
功能 |
信号量集的操作 |
返回值 |
成功返回0,出错返回-1 |
备注 |
(1)用于信号量集中信号量的加和减操作(PV操作,注意P为减操作,V为加操作) (2)可用于进程间的互斥和同步。 |
【编程实验】进程信号量实现ATM的互斥
(1)利用进程信号量的PV操作实现多进程对银行账户操作的互斥。
(2)I(1):在银行账户上绑定信号量/信号灯(初始值为1)
(3)任何进程在取款时进行P(1)操作,然后withdraw(),取完后V(1)操作。
//pv.h
#ifndef __PV_H__ #define __PV_H__ //初始化semnums个信号灯/信号量的值(value) extern int I(int semnums, int value); //对信号集(semid)中的信号灯(semindex)作P(value)操作 extern void P(int semid, int semindex,int value); //对信号量集(semid)中的信号灯(semindex)作V(value)操作 extern void V(int semid, int semindex, int value); //销毁信号量集(semid) extern void D(int semid); #endif
//pv.c
#include "pv.h" #include <sys/sem.h> #include <stdio.h> #include <stdlib.h> #include <malloc.h> #include <assert.h> //编译命令:gcc -o bin/pv.o -Iinclude -c src/pv.c /*封装对信号集的PV操作*/ //定义信号量集的Union union semun{ int val; struct semid_ds *buf; unsigned short *array; }; //创建信号量集,并初始化semnums个信号灯/信号量的值为value int I(int semnums, int value) { //创建信号量集 int semid = semget(IPC_PRIVATE, semnums, IPC_CREAT | IPC_EXCL | 0777); if(semid < 0){ return -1; //错误时 } union semun un; unsigned short* array = (unsigned short*)calloc(semnums, sizeof(unsigned short)); int i = 0; for(; i< semnums; i++){ array[i] = value; } un.array = array; //初始化信号量集中的所有的信号灯的初值 //0:表示要初始化所有的信号灯 if(semctl(semid, 0, SETALL, un) < 0){ perror("semctl error"); return -1; } free(array); return semid; //返回信号量集ID } //对信号集(semid)中的信号灯(semindex)作P(value)操作 void P(int semid, int semindex, int value) { assert(value >= 0); /* * 定义sembuf类型的结构体数组,放置若个结构体变量 * 对应要操作的信号量、要作的P或V操作 */ struct sembuf ops[] = {{semindex, -value, SEM_UNDO}};//只有1个元素,表示只操作 //一个信号量.-value表示P操作 if(semop(semid, ops, sizeof(ops)/sizeof(ops[0])) < 0){ perror("P semop error"); } } //对信号量集(semid)中的信号灯(semindex)作V(value)操作 void V(int semid, int semindex, int value) { assert( value >= 0); /* * 定义sembuf类型的结构体数组,放置若个结构体变量 * 对应要操作的信号量、要作的P或V操作 */ struct sembuf ops[] = {{semindex, value, SEM_UNDO}};//只有1个元素,表示只操作 //一个信号量。+value表示V操作 if(semop(semid, ops, sizeof(ops)/sizeof(ops[0])) < 0){ perror("semop error"); } } //销毁信号量集(semid) void D(int semid) { if(semctl(semid, 0, IPC_RMID, NULL) < 0){ perror("semctl error"); } }
//account.h
#ifndef __ACCOUNT_H__ #define __ACCOUNT_H__ typedef struct { int code; //帐号 double balance; //余额 int semid; //在共享资源上绑定一个信号量集 }Account; //取款 extern double withdraw(Account* a, double amt); //amt == amount //存款 extern double deposit(Account* a, double amt); //查看帐户余额 extern double get_balance(Account* a); #endif //__ACCOUNT_H__
//account.c
#include "account.h" #include "pv.h" #include <string.h> #include <assert.h> //取款 double withdraw(Account* a, double amt) //amt == amount { assert(a != NULL); //对信号量集semid中的0号信号灯作P操作 P(a->semid, 0, 1); //P操作 if(amt < 0 || amt > a->balance){ V(a->semid, 0, 1); return 0.0; } double balance = a->balance; //先取余额 sleep(1); //为模拟进程下可能出现的问题 balance -= amt; a->balance = balance; //更新余额。在读取余额和更新余额之间有 //故意留出“时间窗口”。 //对信号量集semid中的0号信号灯作V操作 V(a->semid, 0, 1); //V操作 return amt; } //存款 double deposit(Account* a, double amt) { assert(a != NULL); if(amt < 0){ return 0.0; } P(a->semid, 0, 1); //P操作 double balance = a->balance; //先取余额 sleep(1); //为模拟多进程下可能出现的问题 balance += amt; a->balance = balance; //更新余额。 V(a->semid, 0, 1); //V操作 return amt; } //查看帐户余额 double get_balance(Account* a) { assert(a != NULL); P(a->semid, 0, 1); //P操作 double balance = a->balance; V(a->semid, 0, 1); //V操作 return balance; }
//account_test.c
#include "account.h" #include "pv.h" #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/shm.h> int main(void) { //在共享内存中创建银行帐户 int shmid; if((shmid = shmget(IPC_PRIVATE, sizeof(Account), IPC_CREAT | IPC_EXCL | 0777)) < 0){ perror("shmget error"); exit(1); } //进程共享内存映射(a为返回的映射地址) Account* a= (Account*)shmat(shmid, 0, 0); if(a == (Account*)-1){ perror("shmat error"); exit(1); } //银行帐户初始化 a->code = 100001; a->balance = 10000; printf("balance: %f ", a->balance); //创建信号量集并初始化 a->semid = I(1, 1);//1个信号量,初始值为1 if(a->semid < 0){ perror("I(1, 1) init error"); exit(1); } //父子进程都进行取款 pid_t pid; if((pid = fork()) < 0){ perror("fork error"); exit(1); }else if(pid > 0){ //parent process //父进程进行取款操作 double amt = withdraw(a, 10000); printf("pid %d withdraw %f from code %d ", getpid(), amt, a->code); int semid = a->semid; //解除映射 shmdt(a); wait(0); //删除共享内存区 shmctl(shmid, IPC_RMID, NULL); //销毁信号量 D(semid); }else{ //child process //子进程会继承父进程映射的共享内存地址 //子进程进行取款操作 double amt = withdraw(a, 10000); printf("pid %d withdraw %f from code %d ", getpid(), amt, a->code); //解除映射 shmdt(a); } return 0; } /*输出结果: [root@localhost 11.IPC]# gcc -o bin/account_test -Iinclude src/pv.c src/account.c src/account_test.c [root@localhost 11.IPC]# bin/account_test balance: 10000.000000 pid 2229 withdraw 10000.000000 from code 100001 pid 2230 withdraw 0.000000 from code 100001 */
【编程实验】进程信号量实现的读者/写者的同步
(1)目的:利用进程信号量的PV操作实现进程间的同步问题
(2)在共享内存中读写数据(读者和写者问题)
(3)设置两个信号量并初始化为0:控制读的信号量I(s1, 0),控制写的信号量I(s2,1)
//read_writer.c
#include <sys/shm.h> #include <sys/sem.h> #include <stdio.h> #include <stdlib.h> #include <assert.h> //共享资源 typedef struct{ int val; int semid; //一个资源绑定一定信号量集 }Storage; //初始化信号量集 void init(Storage* s) { assert(s != NULL); //创建信号量集(包含2个信号量) if((s->semid = semget(IPC_PRIVATE, 2, IPC_CREAT | IPC_EXCL | 0777)) < 0){ perror("semget error"); exit(1); } //对信号 量中的所有信号量初始化 union semun{ int val; struct semid_ds *ds; unsigned short *array; }; union semun un; //信号量的初始设置 unsigned short array[2] = {0, 1}; //分别为控制读、写 un.array = array; if(semctl(s->semid, 0, SETALL, un) < 0){ perror("semctl error"); exit(1); } } //销毁信号量集 void destroy(Storage* s) { assert( s != NULL); if(semctl(s->semid, 0, IPC_RMID, NULL) < 0){ perror("semctl error"); exit(1); } } void write(Storage* s, int value) { struct sembuf ops_p = {1, -1, SEM_UNDO}; struct sembuf ops_v = {0, 1, SEM_UNDO}; //等待可写:P(s1)操作 if(semop(s->semid, &ops_p, 1) < 0){ perror("semop error"); } s->val = value; //写入数据 printf("%d write %d ", getpid(), value); //通知己可读:V(s0) if(semop(s->semid, &ops_v, 1) < 0){ perror("semop error"); } } void read(Storage* s) { struct sembuf ops_p = {0, -1, SEM_UNDO}; struct sembuf ops_v = {1, 1, SEM_UNDO}; //等待可读:P(s0)操作 if(semop(s->semid, &ops_p, 1) < 0){ perror("semop error"); } int value = s->val; //读取数据 printf("%d read %d ", getpid(), value); //通知己可写:V(s1) if(semop(s->semid, &ops_v, 1) < 0){ perror("semop error"); } } int main(void) { //将共享资源Storage创建在共享内存中 int shmid; if((shmid = shmget(IPC_PRIVATE, sizeof(Storage), IPC_CREAT | IPC_EXCL | 0777)) < 0){ perror("shmget error"); exit(1); } //共享内存映射 Storage* s = (Storage*)shmat(shmid, 0, 0); if(s == (Storage*)-1){ perror("shmat error"); exit(1); } //创建信号量集并初始化 init(s); pid_t pid = fork(); if(pid < 0){ perror("fork error"); exit(1); }else if (pid > 0){ //parent process int i = 1; for(; i<=10; i++){ write(s, i); } wait(0); destroy(s);//释放信号量集 shmdt(s); //解除映射 shmctl(shmid, IPC_RMID, NULL); //删除共享内存 }else{ //child process int i = 1; for(; i<=10; i++){ read(s); } shmdt(s);//解除共享内存映射 } return 0; } /*输出结果 [root@localhost 11.IPC]# bin/read_writer 2376 write 1 2377 read 1 2376 write 2 2377 read 2 2376 write 3 2377 read 3 2376 write 4 2377 read 4 2376 write 5 2377 read 5 2376 write 6 2377 read 6 2376 write 7 2377 read 7 2376 write 8 2377 read 8 2376 write 9 2377 read 9 2376 write 10 2377 read 10 */