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:指向一个保存着共享内存的模式状态和访问权限的数据结构
3、共享内存实现进程间的单向通信
(1)创建一个comm.c文件用于实现创建、销毁共享内存,向共享内存里发消息收消息等函数,comm.h文件为它相应的头文件等内容
#include
#include
#include
#define PATHNAME "."
#define PROJ_ID 0x6666
#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
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
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
#include
#include
#include
#include
#include
#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提供的缓存
};
测试代码如下:
运行结果如下:
可以看到,程序输出的结果很乱。因为程序中的父子进程都要向显示器打印数据,所以此时显示器就是一个临界资源。我们希望父子进程互斥使用它,即输出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资源随内核。