由于一节讲的东西太多了,所以我分成了两部分写,继续跟着上一篇讲完的消息队列,接下来复习的是信号量于共享内存。
信号量
信号量是一个特殊的变量,一般取正数值。它的值代表允许访问的资源数目。
获取资源时,需要对信号量的值进行原子减一,该操作被称为 P 操作。当信号量值为 0 时,代表没有资源可用,P 操作会阻塞。
释放资源时,需要对信号量的值进行原子加一,该操作被称为 V 操作。
信号量主要用来同步进程。信号量的值如果只取 0,1,将其称为二值信号量。如果信号量的值大于 1,则称之为计数信号量。
简单来说这里的信号量学习就是OS课程中学习PV操作的代码版,所以这玩意儿并不会用来进行数据传输,它仅仅只是用来解决进程同步与互斥问题的一种通信机制,这也是它和前面俩种方法最大的不同。
使用信号量进行进程间通信的步骤如下:
①创建信号量/信号量集或者获取系统中已有的信号量/信号量集;
②初始化信号量:早期信号量通常初始化为1,但有些进程一次需要多个同类的临界资源或多个不同类且唯一的临界资源,因此可能需要初始化信号量集;
③信号量的P、V操作:根据进程请求修改信号量的数量,P操作使信号量-1,V操作使信号量+1;
④从系统中删除不需要的信号量。
简单介绍几种实现该方法的函数:
semget函数
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
功能:创建一个新的信号量集或获取一个已经存在的信号量集。执行成功返回信号量的标识符,执行失败返回-1;
参数解释:
key:传入参数,信号量的键值,通常为一个整数;
nsems:创建的信号量数目;
semflg:标志位,同open和msgget函数的标志位功能相似,用来设置权限
– 权限位可与IPC_CREAT及IPC_EXCL发生位或;
– IPC_PRIVATE:表示该信号量为当前进程的私有信号量。
semctl函数
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
功能:对信号量或信号量集进行控制。执行成功根据参数cmd的取值返回相应信息,通常为一个非负整数。执行失败返回-1;
参数解释:
semid:信号量标识符,通常为semget函数的返回值;
semnum:信号量在信号量集中的编号,该参数在使用信号量集时才会使用,通常设置为0,表示取第一个信号;
cmd:对信号量进行操作,常用的设置为SETVAL和IPC_RMID:
– SETVAL:表示semctl函数的功能为初始化信号量的值,信号量的值通过可选参数传入,信号量在使用前应先对其值进行设置;
– IPC_RMID:表示semctl函数的功能为删除指定信号量。信号量的删除应由其所有者或创建者进行,没有被删除的信号量将会一直存在于系统中。
最后一个参数是可选参数,依赖于参数cmd,使用该参数时,用户必须在程序自定义一个如下所示的共用体:
union semun{
int val; //cmd = SETVAL, 用于指定信号量值
struct semid_ds *buf; //cmd = IPC_STAT或IPC_SET, 该项才生效
unsigned short *array; //cmd = GETALL或SETALL, 该项才生效
struct seminfo *_buf; //cmd = IPC_INFO, 该项才生效
};
//semid_ds是一个由内核维护的记录信号量属性信息的结构体
struct semid_ds{
struct ipc_perm sem_perm; //所有者和标识权限
time_t sem_otime; //最后操作时间
time_t sem_ctime; //最后更改时间
unsigned short sem_nsems; //信号集中的信号数量
};
semop函数
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned nsops);
功能:改变信号量的值。执行成功返回0,执行失败返回-1;
参数解释:
semid:信号量标识符,通常为semget函数的返回值
nsops:表示参数sops所指数组中元素的个数。
sops:一个struct sembuf类型的数组指针,该数组中的每个元素设置了要对信号量集的哪个信号做哪种操作;
struct sembuf{
short sem_num; //信号量在信号量集中的编号
short sem_op; //信号量操作
short sem_flag; //标志位
};
– sem_op = -1:表示P操作;
– sem_op = +1:表示V操作;
– 通常设置sem_flag = SEM_UNDO:若进程退出前没有删除信号量,则信号量将会由系统自动释放。
案例
使用信号量实现父子进程同步,防止父子进程抢夺CPU。
#include <stdio.h>
#include <stdlib.h>
#include <sys/sem.h>
#include <sys/wait.h>
#include <unistd.h>
//自定义共用体
union semu{
int val;
struct semid_ds* buf;
unsigned short* array;
struct seminfo* _buf;
};
static int semId;
//设置信号量值
static int setSemValue() {
union semu tempSemUnion;
tempSemUnion.val = 1;
if (semctl(semId, 0, SETVAL, tempSemUnion) == -1){
return 0;
}//of if
return 1;
}//of setSemValue
//p操作,获取信号量
static int semaphoreP() {
struct sembuf tempSemBuf;
tempSemBuf.sem_num = 0;
tempSemBuf.sem_op = -1;
tempSemBuf.sem_flg = SEM_UNDO;
if (semop(semId, &tempSemBuf, 1) == -1){
perror("sem_p err");
return 0;
}//of if
return 1;
}//of semaphoreP
//V操作,释放信号量
static int semaphoreV() {
struct sembuf tempSemBuf;
tempSemBuf.sem_num = 0;
tempSemBuf.sem_op = 1;
tempSemBuf.sem_flg = SEM_UNDO;
if (semop(semId, &tempSemBuf, 1) == -1){
perror("sem_v err");
return 0;
}//of if
return 1;
}//of semaphoreV
//删除信号量
static void delSemValue() {
union semu tempSemUnion;
if (semctl(semId, 0, IPC_RMID, tempSemUnion) == -1){
perror("del err");
}//of if
}//of delSemValue
int main() {
int i;
pid_t temPid;
char tempChar = 'C';
semId= semget((key_t)1000, 1, 0664 | IPC_CREAT);//创建信号量
if (semId== -1){
perror("sem_c err");
exit(-1);
}//of if
if (!setSemValue()){ //设置信号量值
perror("init err");
exit(-1);
}//of if
temPid = fork(); //创建子进程
if (temPid== -1){ //若创建失败
delSemValue(); //删除信号量
exit(-1);
} else if (temPid == 0){ //设置子进程打印的字符
tempChar = 'Z';
} else { //设置父进程打印的字符
tempChar = 'C';
}//of if
srand((unsigned int)getpid()); //设置随机数种子
for (i = 0; i < 8; i++) { //循环打印字符
semaphoreP(); //获取信号量
printf("%c", tempChar);
fflush(stdout); //将字符打印到屏幕
sleep(rand() % 4); //沉睡
printf("%c", tempChar);
fflush(stdout); //再次打印到屏幕
sleep(1);
semaphoreV(); //释放信号量
}//of for i
if (temPid > 0){
wait(NULL); //回收子进程
delSemValue(); //删除信号量
}//of if
printf("\nprocess %d finished.\n", getpid());
return 0;
}//of main
共享内存
共享内存,顾名思义就是允许两个不相关的进程访问同一个逻辑内存,共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。
共享内存允许两个或多个进程访问给定的同一块存储区域。它是效率最高的一种进程通信方式,节省了不同进程间多次读写的时间,也是先写完数据才能读读数据。而如果多个进程访问同一块区域就会出现一个进程冲突的问题,而解决读写同步的操作就需要用到我们上一个讲到的信号量,他们共同使用即可解决这个同步的问题。
实现共享内存的申请、管理与释放的系统调用有如下:
shmget函数
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
功能:创建一个新的共享内存或打开一块已存在的共享内存。执行成功返回共享内存的标识符,一个非负整数。执行失败返回-1;
参数解释:
key:传入参数,共享内存的键值,通常为一个整数;
size:设置共享内存的大小;
shmflg:设置shmget函数的创建条件(一般设置为IPC_CREAT或IPC_EXCL)及进程对共享内存的读写权限。
shmat函数
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:进行地址映射,将共享内存映射到进程虚拟地址空间中。执行成功返回映射的地址并更改共享内存shmid_ds结构中的属性信息,执行失败返回-1;
参数解释:
shmid:共享内标识符,通常为shmget函数的返回值;
shmaddr:指针类型的传入参数,用于指定共享内存映射到虚拟内存时的虚拟地址,设置为NULL时,映射地址由系统决定;
shmflg:设置共享内存的使用方式,如果shmflg = SHM_RDONLY,则共享内存将以只读的方式进行映射,当前进程只能从共享内存中读取数据。
shmdt函数
#include <sys/shm.h>
int shmdt(const void *shmaddr);
功能:解除物理内存与进程虚拟地址空间的映射关系。执行成功返回映射的地址并更改共享内存shmid_ds结构中的属性信息,执行失败返回-1;
参数:
shmaddr:shmat函数返回的虚拟空间地址。
shmctl函数
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
//为了方便对共享内存进行管理,由内核维护的存储共享内存属性信息的结构体:
struct shmid_ds{
struct ipc_perm shm_perm; /* 操作权限*/
size_t shm_segsz; /*段的大小(以字节为单位)*/
time_t shm_atime; /*最后一个进程附加到该段的时间*/
time_t shm_dtime; /*最后一个进程离开该段的时间*/
time_t shm_ctime; /*最后一个进程修改该段的时间*/
unsigned short shm_cpid; /*创建该段进程的pid*/
unsigned short shm_lpid; /*在该段上操作的最后1个进程的pid*/
short shm_nattch; /*当前附加到该段的进程的个数*/
/*下面是私有的*/
unsigned short shm_npages; /*段的大小(以页为单位)*/
unsigned long *shm_pages; /*指向frames->SHMMAX的指针数组*/
struct vm_area_struct *attaches; /*对共享段的描述*/
};
功能:对已存在的共享内存进行操作,具体的操作由参数确定。执行成功返回0,执行失败返回-1;
参数解释:
shmid:共享内存标识符;
cmd:要执行的操作,常用的设置为IPC_RMID,功能为删除共享内存;
buf:对共享内存的管理信息进行设置,该参数是结构体shmid_ds的指针。
特殊说明:
共享内存与消息队列、信号量相同,在使用完毕后都应该进行释放;
调用fork函数创建子进程时,子进程会继承父进程已绑定的共享内存;
当调用exec函数更改子进程功能及调用exit函数时,子进程都会解除与共享内存的映射关系,因此在必要时仍应使用shmctl函数对共享内存进行删除。
案例
使用共享内存机制实现两个进程间的通信。
//shm_w.c
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#define SEGSIZE 4096 //定义共享内存容量
typedef struct{ //读写数据结构体
char name[8];
int age;
} Stu;
int main() {
int tempShmId, i;
key_t tempKey;
char tempName[8];
Stu *tempSmap;
/*ftok函数的作用:系统建立IPC通讯(如消息队列、共享内存时)必须指定一个ID值。通常情况下,该id值通过ftok函数得到。*/
tempKey = ftok("/", 0); //获取关键字
if (tempKey == -1) {
perror("ftok error");
return -1;
}//of if
printf("key=%d\n", tempKey);
//创建共享内存
tempShmId= shmget(tempKey, SEGSIZE, IPC_CREAT | IPC_EXCL | 0664);
if (tempShmId == -1) {
perror("create shared memory error\n");
return -1;
}//of if
printf("shm_id=%d\n", tempShmId);
tempSmap = (Stu*)shmat(tempShmId, NULL, 0); //将进程与共享内存绑定
memset(tempName, 0x00, sizeof(tempName));
strcpy(tempName, "Jhon");
tempName[4] = '0';
for (i = 0; i < 3; i++){ //写数据
tempName[4] += 1;
strncpy((tempSmap+ i)->name, tempName, 5);
(tempSmap + i)->age = 20 + i;
}//of for i
if (shmdt(tempSmap) == -1){ //解除绑定
perror("detach error");
return -1;
}//of if
return 0;
}//of main
//shm_r.c
#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
typedef struct{
char name[8];
int age;
} Stu;
int main() {
int tempShmId, i;
key_t tempKey;
Stu *tempSmap;
struct shmid_ds tempBuf;
tempKey = ftok("/", 0); //获取关键字
if (tempKey == -1) {
perror("ftok error");
return -1;
}//of if
printf("key=%d\n", tempKey);
tempShmId = shmget(tempKey, 0, 0); //创建共享内存
if (tempShmId == -1) {
perror("shmget error");
return -1;
}//of if
printf("shm_id=%d\n", tempShmId);
tempSmap = (Stu*)shmat(tempShmId, NULL, 0); //将进程与共享内存绑定
for (i = 0; i < 3; i++){ //读数据
printf("name:%s\n", (*(tempSmap + i)).name);
printf("age :%d\n", (*(tempSmap + i)).age);
}//of for i
if (shmdt(tempSmap) == -1){ //解除绑定
perror("detach error");
return -1;
}//of if
shmctl(tempShmId, IPC_RMID, &tempBuf); //删除共享内存
return 0;
}//of main
总结
简单总结四个方法:
管道:分为俩种匿名和实名,匿名使用条件需要是亲缘关系进程,而且使用一次后就被销毁;实名对亲缘关系的要求没有这么严苛,并且使用一次不主动销毁还能继续使用。
消息队列:实质是链表,存数据即插入结点,取数据即删除结点。存数据和取数据不需要同时进行。
信号量:即PV操作,是一个进程同步于互斥的解决方法,没有数据的传输。
共享内存:为了实现多个进程间的相互通信,以信号量为方法解决进程冲突访问数据的情况。