进程间通信-共享内存 & 信号量
一.共享内存
1. 简单介绍
(1)共享内存是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间的数据传递不再涉及内核,即进程不再通过执行进入内核的系统调用来传递彼此的数据。
(2)共享内存的生命周期随内核。
(3)注意:
共享内存未提供任何保护资源,即共享内存自身没有同步与互斥机制,但它是临界资源,所以我们需要利用其它机制来保证数据的正确性,Linux下就可以用信号量达到同步的目的。
(4)
linux共享内存有两种方式(本文主要介绍shmget方式):
1)mmap方式,适用于父子进程之间,创建的内存非常大时;
2)shmget方式,适用于同一台电脑上不同进程之间,创建的内存相对较小。
(5)进程间利用共享内存实现消息队列的基本原理如下图
2. 相关函数介绍
(1)shmget函数
1)函数原型:
2)函数功能:创建共享内存
3)参数:
key:共享内存段名字
size:共享内存大小
shmflg:九个权限标志构成,用法与创建文件时用的mode一致
4)返回值:成功返回一个非负整数,即该共享内存段标识码;失败返回-1
(2)shmat函数
1)函数原型:
2)函数功能:将共享内存段连接到进程地址空间
3)参数:
shmid:共享内存标识码
shmaddr:指定连接的地址
shmflg:两个可能取值SHM_RND和SHM_RDONLY
4)返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
5)说明:
shmaddr为NULL时,核心自动选择一个地址;
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址;
shmaddr不为NULL时,且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍,相应公式为:shmadrr - (shmadrr % SHMLBA);
shmflg = SHM_RDONLY,表示连接操作用来只读共享内存
(3)shmdt函数
1)函数原型:
2)函数功能:将共享内存段与当前进程脱离
3)参数:
shmadrr:由shmat函数返回的指针
4)返回值:成功返回0;失败返回-1
5)注意:
将共享内存段与当前进程脱离不等于删除共享内存段
(4)shmctl函数
1)函数原型:
2)函数功能:用于控制共享内存
3)参数:
shmid:由shmget函数返回的共享内存标识码
cmd:将要采取的动作,有三个可取值
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
4)返回值:成功返回0;失败返回-1
3. 共享内存实现进程间的单向通信
(1)创建一个comm.c文件用于实现创建、销毁共享内存,向共享内存里发消息收消息等函数,comm.h文件为它相应的头文件等内容
comm.h
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #define PATHNAME "." #define PROJ_ID 0x6666
comm.c
#include "comm.h" static int _CreateShm(int size, int flags) { key_t key = ftok(PATHNAME, PROJ_ID); if(key < 0) { perror("ftok"); return -1; } int shmid;//共享内存标识码(非负整数) if((shmid = shmget(key, size, flags)) < 0)//创建共享内存失败时返回-1 { perror("shmget"); return -2; } return shmid;//将创建的共享内存的标识码返回,以实现后续代码 } int CreateShm(int size)//创建一个新的共享内存 { return _CreateShm(size, IPC_CREAT | IPC_EXCL | 0666);//已有该共享内存,返回-1;否则创建再返回标识码 } int GetShm(int size)//获得共享内存标识码 { return _CreateShm(size, IPC_CREAT);//已有该共享内存,返回标识码即可;没有则创建则创建 } int DestroyShm(int shmid) { if((shmctl(shmid, IPC_RMID, NULL)) < 0) { perror("shmctl"); return -1; } return 0; }
(2)
创建一个server.c文件,用于创建、销毁共享内存,接收消息,实现代码如下:
include "comm.c" #include<unistd.h> int main() { int shmid = CreateShm(4096);//创建一个共享内存 char* addr = shmat(shmid, NULL, 0);//将共享内存段连接到进程的地址空间,成功返回指向共享内存的第一个字节 sleep(6);//为了能够等到client进程向共享内存放消息,具体等几秒自己把握,如不等,接收的消息会少一点 int i = 0; while(i++ < 26) { printf("client# %s\n",addr); sleep(1);//这里不等的话,26次循环很快就执行完了,你还没等到client向共享内存里放消息,它就运行完了 } shmdt(addr);//将共享内存段与当前进程脱离 sleep(2); DestroyShm(shmid); return 0; }
(3)创建一个client.c文件,用于向共享内存里发消息
#include "comm.c" #include<unistd.h> int main() { int shmid = GetShm(4096);//获得一个共享内存的标识码 sleep(1); char* addr = shmat(shmid, NULL, 0); sleep(2); char x = 'A'; for(x='A'; x<='Z'; x++)//依次发送字母A-Z { addr[x-'A'] = x; addr[x-'A'+1] = '\0'; sleep(1); } shmdt(addr); sleep(2); return 0; }
(4)运行结果如下:
同样在运行时,要先运行server.c文件,因为它要先创建消息队列,才能实现进程间通信。可以看到,当server打印26次消息后,就会结束此次通信。我们的预想是,server接到client发送的A-Z的26条消息,并依次打印,但在代码中sleep的时间不同,会有不一样的结果,这个得根据自身在运行时的速度以及等待的时间决定。
4. 删除消息队列IPC资源
之前在另一文章我提到过一句话:IPC资源在用完后必须删除。如以上的代码,若是正常跑完IPC资源会被代码删除,若是我们用ctrl+c终止进程,则代码不能删除IPC资源。这样在下次运行代码时,就会出现以下问题:
这个问题产生的原因就是IPC资源未删除,所以我们要学两条命令用来删除消息队列的IPC资源:
删除后,该IPC资源就没有了
信号量
主要用于同步与互斥。本质上是一个计数器,里面记录了临界资源的数目。信号量的生命周期也随内核。
1. 进程互斥
(1)由于各进程要求共享资源,而且有些资源需要互斥使用。因此各进程间竞争使用这些资源,进程的这种关系即为进程的互斥;
(2)系统中某些资源一次只能让一个进程使用,这样的资源叫做临界资源或互斥资源;
(3)在进程中涉及到互斥资源的程序段叫做临界区。
2. 进程同步
进程同步是指多个进程需要相互配合共同完成同一项任务
3. 信号量和P、V原语
(1)信号量和P、V原语由迪杰斯特拉(Dijkstra)提出,信号量值为1的为二元信号量,又称为互斥锁。
(2)信号量
同步:P、V在不同进程中
互斥:P、V在同一进程中
(3)信号量值含义:
S>0:S表示可用资源的数目
S=0:表示无可用资源,无等待进程
S<0:ISI表示等待队列中的进程数
(4)信号量结构体伪代码
信号量本质上其实是一个计数器(整型变量),它维护等待队列。
struct semaphore { int value; pointer_PCB queue; };
(5)P原语
//减1操作 P(s) { s.value = s.value--; if(s.value < 0) { 该进程状态置为等待状态 将该进程的PCB插入到相应的等待队列s.queue队尾 } }
(6)V原语
//加1操作 V(s) { s.value = s.value++; if(s.value >= 0) { 唤醒相应等待队列s.queue中等待的一个进程 改变其状态为就绪态 并将其插入到就绪队列 } }
注意:P、V原语都是原子操作
4. 信号量集相关函数
信号量是以多个即集申请的,而不是单个申请。维护一种临界资源需要一个信号量,所以维护多种临界资源就需要多个信号量。多种信号量组成一个信号量集。信号量集可以看做是计数器的个数,信号量值可看作计数器的个数。信号量集是以数组形式进行组织的,以下标来提取各个信号,数组元素表示信号量的值即临界资源的数目。
(1)semget函数
1)函数原型
2)函数功能:创建和访问一个信号量集
3)参数
key:信号集的名字
nsems:信号集中信号量的个数
semflg:九个权限标志构成,用法与创建文件的mode模式标志一致
4)返回值:成功返回一个非负整数即该信号集的标识码;失败返回-1
(2)semctl函数
1)函数原型
2)函数功能:控制信号量集
3)参数:
semid:由semget函数返回的信号量集标识码
semnum:信号量集中信号量的序号
cmd:将要采取的动作(有三个可取值)
4)返回值:成功返回0,失败返回-1
(3)semop函数
1)函数原型
2)函数功能:创建和访问一个信号量集
3)参数
semid:由semget函数返回的信号量的标识码
sops:是个指向一个结构数值的指针
nsops:信号量的个数
4)返回值:成功返回0;失败返回-1
5)说明:
struct sembuf { short sem_num;//信号量的编号 short sem_op;//信号量一次PV操作时加减的数值,一般只会用到两个值: //一个是“-1”,即P操作,等待信号量变得可用 //另一个是“+1”,即V操作,发出信号量已经变得可用 short sem_flg;//默认设为0,另外两个取值是IPC_NOWAIT或SEM_UNDO };
5. 程序实现信号量的作用(采用二元信号量来测试)
创建一个comm.c文件,以封装信号量相关操作的函数,代码如下
//实现信号量的相关操作的函数 #include "comm.h" static int _CreateSem(int nsems, int flags)//创建一个信号量 { key_t key = ftok(PATHNAME, PROJ_ID);//产生key值 if(key < 0) { perror("ftok"); return -1; } int semid = semget(key, nsems, flags);//创建一个信号量 if(semid < 0) { perror("semget"); return -2; } return semid; } int CreateSem(int nsems)//获得一个新的信号量集 { return _CreateSem(nsems, IPC_CREAT | IPC_EXCL | 0666); } int GetSem(int nsems)//获得一个信号量集的标识码 { return _CreateSem(nsems, IPC_CREAT); } int InitSem(int semid, int semnum, int initval)//对信号量集进行初始化 { union semun _un; _un.val = initval; if(semctl(semid, semnum, SETVAL, _un ) < 0)//设置信号量集中信号量的计数值 { perror("semctl"); return -1; } return 0; } static int SemPV(int semid, int who, int op)//PV操作实现 { struct sembuf _sf; _sf.sem_num = who;//通过信号量的编号确定对哪个信号量进行操作 _sf.sem_op = op;//信号量一次PV操作时加减的数值 _sf.sem_flg = 0; if(semop(semid, &_sf, 1) < 0) { perror("semop"); return -1; } return 0; } int P(int semid, int who)//对信号量进行P操作 { return SemPV(semid, who, -1);//减1操作 } int V(int semid, int who)//对信号量进行V操作 { return SemPV(semid, who, +1);//即加1操作 } int DestroySem(int semid)//销毁信号量集 { if(semctl(semid, 0, IPC_RMID) < 0)//删除信号量集中序号为0的信号量 { perror("semctl"); return -1; } }
它对应的头文件comm.h,代码如下:
#pragma once #include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <wait.h> #include <unistd.h> #define PATHNAME "." #define PROJ_ID 0x6666 union semun { int val;//SETVAL用的值 struct semid_ds* buf;//IPC_STAT、IPC_SET用的 unsigned short* array;//GETALL、SETALL用的数组值 struct seminfo* _buf;//为IPC_INFO提供的缓存 };
测试代码如下:
#include "comm.c" int main() { pid_t id = fork(); if(id == 0)//child { while(1) { printf("A"); fflush(stdout); usleep(123456); printf("A "); fflush(stdout); usleep(321456); } } else//father { while(1) { printf("B"); fflush(stdout); usleep(223456); printf("B "); fflush(stdout); usleep(121456); } wait(NULL);//不关心子进程的退出状态 } return 0; }
运行结果如下:
可以看到,程序输出的结果很乱。因为程序中的父子进程都要向显示器打印数据,所以此时显示器就是一个临界资源。我们希望父子进程互斥使用它,即输出AA、BB而不会出现AB混合的情况,这种情况是父子进程在竞争的使用它,我们不知道它在什么时刻就会被会切换进程,造成这样的输出结果。所以我们要让它们互斥的访问以输出我们想要的结果,
这时就可以用到信号量以实现互斥与同步,所以我们修改代码如下:
#include "comm.c" int main() { int semid = CreateSem(1);//申请信号量为1的信号量集 InitSem(semid, 0 , 1);//将信号量的计数值初始化为1 pid_t id = fork(); if(id == 0)//child { int _semid = GetSem(0); while(1) { P(_semid, 0); printf("A"); fflush(stdout); usleep(123456); printf("A "); fflush(stdout); usleep(321456); V(_semid, 0); } } else//father { while(1) { P(semid, 0); printf("B"); fflush(stdout); usleep(223456); printf("B "); fflush(stdout); usleep(121456); V(semid, 0); } wait(NULL);//不关心子进程的退出状态 } DestroySem(semid); return 0; }
此时,运行结果如下:
其实,我们只是在原有代码的基础上,对父子进程的每一次打印加了一个信号量的PV操作。当子进程开始打印时先进行P操作,将资源减去1,此时没有资源了,父进程只能等待,知道子进程打印一次之后进行V操作,此时资源数目为1。父进程也是同样的原理,我们不能控制让谁申请到临界资源,但是我们可以保证在当前进程使用资源时,不被其他进程切换进来,从而造成的数据不正确。
6. 信号量资源的释放
以上的代码,因为我们设置的是死循环,所以代码不能执行到删除创建的信号量资源就被我们终止。这样在下次运行时会出现以下情况:
所以我们要学两条命令手动删除信号量资源:
再说一遍:IPC资源必须删除,否则不会自动清除,除非重启,所以System V IPC资源随内核。