共享内存,指的是两个不相关的进程访问同一个逻辑内存,进程可以将同一段物理内存连接到他们自己的地址空间中,所有的进程都可以访问共享内存中的地址。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。如果我们不允许两个进程同时对共享内存进行读写操作,光靠共享内存的机制是做不到的。共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取,所以我们通常需要用其他的机制来同步对共享内存的访问,例如信号量。
1、创建共享内存(创建或者打开一块内存后,会返回该内存的索引,进程通过该索引可以对共享内存进行连接)(记录一个知识点:fortk第一个参数是输入一个路径,如果路劲不存在会返回一个默认值)
int shmget(key_t key, size_t size, int shmflg)创建共享内存
/* key:由ftok生成的key标识,标识系统的唯一IPC资源。
size:需要申请共享内存的大小。在操作系统中,申请内存的最小单位为页,一页是4k字节,为了避免内存碎片,我们一般申请的内存大小为页的整数倍。
shmflg:如果要创建新的共享内存,需要使用IPC_CREAT,IPC_EXCL,如果是已经存在的,可以使用IPC_CREAT或直接传0。
*/
2、连接共享内存(不同的进程通过此函数,挂载到同一片内存上)
void *shmat(int shmid, const void *shmaddr, int shmflg);
/*
shmid:共享存储段的标识符。
shmaddr:shmaddr = 0,则存储段连接到由内核选择的第一个可以地址上(推荐使用)。
shmflg:若指定了SHM_RDONLY位,则以只读方式连接此段,否则以读写方式连接此段。
返回值:成功返回共享存储段的指针(虚拟地址),并且内核将使其与该共享存储段相关的shmid_ds结构中的shm_nattch计数器加1(类似于引用计数);
出错返回-1。
*/
3、解除共享内存的连接(当一个进程不需要共享内存的时候,就需要解除与该共享内存的连接。该函数并不删除所指定的共享内存区,而是将之前用shmat函数连接好的共享内存区脱离目前的进程。)
int shmdt(const void *shmaddr)
/*
shmaddr:连接以后返回的地址。
返回值:成功返回0,并将shmid_ds结构体中的 shm_nattch计数器减1;出错返回-1。
*/
4、销毁共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
/*
shmid:共享存储段标识符。
参数cmd:指定的执行操作,设置为IPC_RMID时表示可以删除共享内存。
参数buf:设置为NULL即可。
返回值:成功返回0,失败返回-1。
*/
说信号量之前,先提一句,信号量和信号是完全两个不同的东西,关于信号可以去看我这篇博客 ,点击进入
信号量就是用来解决进程间的同步与互斥问题的一种进程间通信机制。值得一提的是,二值信号量与自旋锁的效果差不多,但有一点需要注意是,信号量会引起进程的睡眠属于睡眠锁,不要在中断中用信号量,不要在持锁时,进行进程的切换!
1、信号量的建立(会返回一个信号标识符,用来指向这个信号量结构)
int semget(key_t key, int nsems, int semflg)
/*
key : 定义一个唯一的标识符
nsems: 需要创建的信号量数量
semflg : IPC_CREAT(一般用这个):当semflg&IPC_CREAT为真时,如果内核中不存在键值与key相等的信号量集,则新建一个信号量集;如果存在这样的信号量集,返回此信号量集的标识符 。
IPC_CREAT|IPC_EXCL:如果内核中不存在键值与key相等的信号量集,则新建一个消息队列;如果存在这样的信号量集则报错
*/
2、对信号量进行设置
int semctl(int semid, int semnum, int cmd, union semun arg)
/*
semid: 就是由semget()得到的
semnum: 当用到信号量集的时候才用,默认为0就好了
cmd:你就用SETVAL这个好了,用来把信号量初始化为一个已知的值。其他的我也没用过哈哈...
arg: union semun {
short val; /*就把这个val值设置一下就好了,其他的不用管,默认就好*/
struct semid_ds* buf; /*IPC_STAT、IPC_SET用的semid_ds结构*/
unsigned short* array; /*SETALL、GETALL用的数组值*/
struct seminfo *buf; /*为控制IPC_INFO提供的缓存*/
} arg;
*/
3、信号量的P操作或V操作(可以简单的理解为,信号量的获取与释放,主要看各个val值,如果你把val设置为1 ,也就是资源只支持一个进程访问,该进程访问时进程P操作,会把val减1,当另一进程请求访问时,发现val已经等于0了,就会被堵塞)
int semop(int semid, struct sembuf *sops, unsigned nsops)
/*
semid:信号量集标识符
struct sembuf * sops :
struct sembuf {
short semnum; /*信号量集合中的信号量编号,0代表第1个信号量*/
short val;/*若val>0进行V操作信号量值加val,表示进程释放控制的资源 */
/*若val<0进行P操作信号量值减val,则调用进程阻塞,直到资源可用;若设置IPC_NOWAIT不会睡眠,进 程直接返回EAGAIN错误*/
/*若val==0时阻塞等待信号量为0,调用进程进入睡眠状态,直到信号值为0;若设置IPC_NOWAIT,进程不会 睡眠,直接返回EAGAIN错误*/
short flag; /*0 设置信号量的默认操作*/
/*IPC_NOWAIT设置信号量操作不等待*/
/*SEM_UNDO 选项会让内核记录一个与调用进程相关的UNDO记录,
如果该进程崩溃,则根据这个进程的UNDO记录自动恢复相应信号量的计数值*//*
nsops:进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。最常见设置此值等于1,只完成对一个信号量的操作*/
*/
comm.h文件
//comm.h
#ifndef _COMM_H__
#define _COMM_H__
#include
#include
#include
#include
#include
#define PATHNAME "."
#define PROJ_ID 0x6666
#define PROJ_mm 0x6667
int CreateShm(int size);
int DestroyShm(int shmid);
int GetShm(int size);
int init_sem(int semid, int num, int val);
int sem_p(int semid, int num) ;
int sem_p(int semid, int num) ;
int sem_v(int semid, int num) ;
#endif
comm.c
//comm.c
/*
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数shmid:共享存储段的标识符。
参数*shmaddr:shmaddr = 0,则存储段连接到由内核选择的第一个可以地址上(推荐使用)。
参数shmflg:若指定了SHM_RDONLY位,则以只读方式连接此段,否则以读写方式连接此段。
返回值:成功返回共享存储段的指针(虚拟地址),并且内核将使其与该共享存储段相关的shmid_ds结构中的shm_nattch计数器加1(类似于引用计数);
出错返回-1。
*/
#include"comm.h"
//以下定义的是共享内存的函数
static int CommShm(int size,int flags)
{
key_t key = ftok(PATHNAME,PROJ_ID); //由ftok生成的key标识,标识系统的唯一IPC资源。“PATHNAME”就是引用一个绝对路劲
if(key < 0)
{
perror("ftok");
return -1;
}
int shmid = 0;
/*
int shmget(key_t key, size_t size, int shmflg)创建共享内存
参数key:由ftok生成的key标识,标识系统的唯一IPC资源。
参数size:需要申请共享内存的大小。在操作系统中,申请内存的最小单位为页,一页是4k字节,为了避免内存碎片,我们一般申请的内存大小为页的整数倍。
参数shmflg:如果要创建新的共享内存,需要使用IPC_CREAT,IPC_EXCL,如果是已经存在的,可以使用IPC_CREAT或直接传0。
*/
if((shmid = shmget(key,size,flags)) < 0)
{
perror("shmget");
return -2;
}
return shmid;
}
int DestroyShm(int shmid)
{
if(shmctl(shmid,IPC_RMID,NULL) < 0)
{
perror("shmctl");
return -1;
}
return 0;
}
int CreateShm(int size)
{
return CommShm(size,IPC_CREAT | IPC_EXCL | 0666);
}
int GetShm(int size)
{
return CommShm(size,IPC_CREAT);
}
//以下定义的是信号量的函数
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
int init_sem(int semid, int num, int val)// num = 0 val = 1
{
union semun myun;
myun.val = val;
if(semctl(semid, num, SETVAL, myun) < 0) /*用来直接控制信号量信息:对信号量进行删除或者控制
// 0代表对1个信号来量初始化,即有1个资源 SETVAL:用来把信号量初始化为一个已知的值。
p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。
IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。*/
{
perror("semctl");
exit(1);
}
return 0;
}
int sem_p(int semid, int num)
{
struct sembuf mybuf;// 对信号量做减1操作,即等待P(sv)
mybuf.sem_num = num;
mybuf.sem_op = -1;
mybuf.sem_flg = SEM_UNDO;
if(semop(semid, &mybuf, 1) < 0){
perror("semop");
exit(1);
}
return 0;
}
int sem_v(int semid, int num)
{
struct sembuf mybuf; 这是一个释放操作,它使信号量变为可用,即发送信号V(sv)
mybuf.sem_num = num;
mybuf.sem_op = 1;
mybuf.sem_flg = SEM_UNDO;
if(semop(semid, &mybuf, 1) < 0){
perror("semop");
exit(1);
}
return 0;
}
client.c
//client.c
#include"comm.h"
int main()
{
int semid ;
key_t sem_key;
if((sem_key = ftok(PATHNAME,PROJ_mm)) < 0){
perror("ftok failed .\n");
exit(-1);
}
semid = semget(sem_key, 0, IPC_CREAT); //创建一个新信号量或取得一个已有信号量,权限为666
int shmid = GetShm(4096);
sleep(1);
char *addr = shmat(shmid,NULL,0);
sleep(2);
int i = 0;
sem_p(semid,0);//获取信号量
while(i < 26)
{
addr[i] = 'A' + i;
i++;
addr[i] = 0;
sleep(1);
}
sem_v(semid,0);//释放信号量
shmdt(addr);
semctl (semid, 1, IPC_RMID, NULL);
DestroyShm(shmid);
sleep(2);
return 0;
}
server.c
//server.c
#include"comm.h"
int main()
{
int semid ;
key_t sem_key;
if((sem_key = ftok(PATHNAME,PROJ_mm)) < 0){
perror("ftok failed .\n");
exit(-1);
}
semid = semget(sem_key,1,IPC_CREAT|IPC_EXCL|0666); //创建一个新信号量或取得一个已有信号量,权限为666
init_sem (semid, 0, 1); //初始化这个信号量
int shmid = CreateShm(4096);
sleep(1);
char *addr = shmat(shmid,NULL,0);
sleep(2);
int i = 0;
getchar(); //暂停在这,让客服端先拿到信号量,当client运行后,按下回车键,让server程序继续运行
sem_p(semid,0);//获取信号量
while(i++ < 26)
{
printf("client# %s\n",addr);
sleep(1);
}
sem_v(semid,0);//释放信号量
shmdt(addr);
sleep(2);
return 0;
}
Makefile
all:server client
client:client.c comm.c
gcc -o $@ $^
server:server.c comm.c
gcc -o $@ $^
clean:
rm -f client server
不加信号量的实验结果如下
加上信号量的实验结果如下
总结 ,从这个两个结果可以看出,不加信号量的时候,读取数据和写入数据是宏观是同时进行的,会存在读取到的数据不完整的现象,而加上信号量后,读取数据进程,会等待数据全部写完后,在进行读取。保证了读到的数据的完整性和读写进程的同步性