0.System V IPC机制:
①.IPC对象包含:共享内存、消息队列和信号灯集。
②.每个IPC对象有唯一的ID。
③.IPC对象创建后一直存在,直到被显示地删除。
④.每一个IPC对象有一个关联的KEY。(其他进程通过KEY访问对应的IPC对象)
⑤.ipcs(显示系统所有的IPC对象)、ipcrm
一. 生成KEY值 - ftok
功能:
成功时返回(合法的key值),失败时返回(EOF)。
若传入的参数一样,那么得到的key值也一样
参数:
path:存在且可访问的文件的路径
proj_id:用于生成key的数字,不能为0,一般为字符常量
int key_t ftok(const char *path,int proj_id);
int main(int argc,char *argv[])
{
key_t key;
key = ftok(".",'a');
if(-1 == key)
{
perror("key error");
exit(-1);
}
}
二.共享内存
0.特点
①.共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝。
②.共享内存在内核空间创建,可被进程映射到用户空间访问,使其灵活。
③.由于多个进程可同时访问共享内存,因此需要同步和互斥机制配合使用。
④.共享内存的大小有限制
1.创建/打开共享内存 - shmget()
功能:
创建/打开共享内存对象
成功时返回(共享内存的id),失败时(EOF)
参数:
key:和共享内存关联的key,IPC_PRIVATE或由ftok生成。
size:共享内存的大小,单位为字节。
shmflg:共享内存标志位 IPC_CREATE | 0666(不存在就创建,存在了就打开)。
int shmget(key_t key,int size,int shmflg);
示例1:创建一个私有的共享内存,大小为512字节,权限为0666
int shmid;
shmid = shmget(IPC_PRIVATE,512,0666);
if(0 > shmid)
{
perror("shmget error");
exit(-1);
}
示例2:创建/打开一个和key关联的共享内存,大小为1024字节,权限为0666
key_t key;
int shmid;
/* 1.先生成key(相关的进程调用ftok时,传入相同的参数才能得到相同的key) */
key = ftok(".",'m');
if(-1 == key)
{
perror("ftok");
exit(-1);
}
/* 2.获取共享内存 */
shmid = shmget(key,1024,IPC_CREATE | 0666);
if(0 > shmid)
{
perror("shmget");
exit(-1);
}
2.映射共享内存 - shmat
功能:
映射共享内存;
成功时返回(映射后的地址),失败时返回((void *)-1),通过指针访问共享内存,指针类型取决于共享内存中存放的数据的类型;
参数:
shmid:要映射的共享内存的id。
shmaddr:指定映射后的地址(通常传入NULL),NULL表示由系统自动映射。
shmflg:标志位,0表示可读写;SHM_RDONLY表示只读。
void *shmat(int shmid,const void *shmaddr,int shmflg);
示例1:在共享内存中存放键盘输入的字符串
char *addr; //保存映射后的首地址
int shmid;
/* 映射共享内存 */
addr = (char *)shmat(shmid,NULL,0);
id((char *)-1 == addr)
{
perror("shmat");
exit(-1);
}
fgets(addr,N,stdin);
3.读写共享内存
操作映射的共享内存的地址
4.撤销共享内存映射 - shmdt
功能:
撤销共享内存映射
成功时返回(0),失败时返回(EOF)
参数:
shmaddr:映射共享内存的首地址
int shmdt(void *shmaddr);
5.共享内存控制 - shmctl
功能:
控制共享内存。
成功时返回(0),失败时返回(-1)并设置errno。
参数:
shmid:要操作的共享内存的id。
cmd :要执行的操作
IPC_STAT:获取当前共享内存的属性(大小,关联的key值,权限等),存入到 shmid_ds *buf中
IPC_SET :将属性(先在shmid_ds *buf中填充)设置到共享内存的id中
IPC_RMID:删除一个共享内存的id
buf:保存或设置共享内存属性的地址
int shmctl(int shmid,int cmd,struct shmid_ds *buf);
6.删除共享内存对象 - shmctl
用shmctl来删除共享内存对象(应该由最后一个进程来执行此操作)
功能:
添加删除标记,当每一个进程都撤销共享内存映射后(nattach == 0)才真正的删除。
shmctl(shmid,IPC_RMID,NULL);
三.信号灯
1.含义:
信号灯也叫信号量,用于进程/线程同步或互斥的机制;
System V 信号灯是一个或多个计数信号灯的集合;
System V 信号灯可同时操作集合中的多个信号灯,要么都能申请到资源,要么都申请不到资 源;
System V 信号灯申请多个资源时避免死锁;
2.信号灯的类型:
Posix无名信号灯:通常用于线程同步
Posix有名信号灯:通常用于进程间的同步和互斥
System V 信号灯:通常用于进程间的同步和互斥
四.System V 信号灯使用步骤
1.打开/创建信号灯 - semget
功能:
打开/创建信号灯
成功时返回(信号灯集合的id),失败时返回(-1),返回(-1)有可能是 打开/创建 失败。所以在返回(-1)时 要对 errno 进行判断,若 errno = EEXIST ,则是该信号灯已经存在,此时进行打开操作;否则应该退出程序,并打开错误信息。
参数:
key:和信号灯关联的key值(IPC_PRIVATE 或 ftok生成的);
nsems:集合中包含的计数信号灯的个数;
semflg:标志位
IPC_CREAT | 0666:没有则创建;
IPC_EXCL:检查信号灯集合是不是第一次被创建,若不是第一次被创建则会返回 (EEXIST),保证信号灯只被初始化一次;
int semget(key_t key,int nsems,int semflg);
semid = semget(sem_key,1,IPC_CREATE|IPC_EXCL|0666);
/* 若打开/创建失败 */
if(-1 == semid)
{
/* errno == EEXIST 时表示该信号灯已经存在,只能进行打开操作 */
if(errno == EEXIST)
{
semid = semget(sem_key,1,0777);
}
/* 否则为出错,应该退出程序 */
else
{
perror("fail to semget");
exit(-1);
}
}
/* 若创建成功,则初始化信号灯 */
else
{
/* 初始化操作 */
}
2.信号灯初始化 - semctl
功能:
初始化信号灯集合;
成功时返回(0),失败时返回(EOF);
参数:
semid:要操作的信号灯集合id
semnum:要操作的集合中的信号灯编号(信号灯集合中的哪一个信号灯)
cmd:要执行的操作
SETVAL:设置信号灯的值
IPC_RMID:删除信号灯集合
union semun :要设置的信号灯的值
int semctl(int semid,int semnum,int cmd,...);
union semun myun;
/* 1.填充第一个要初始化的信号的值 */
myun.val = 2;
/* 把第一个信号灯的值初始化为2 */
if(0 > semctl(semid,0,SETVAL,myun))
{
perror("semctl");
exit(-1);
}
/* 1.填充第二个要初始化的信号的值 */
myun.val = 0;
/* 把第二个信号灯的值初始化为0 */
if(0 > semctl(semid,1,SETVAL,myun))
{
perror("semctl");
exit(-1);
}
3.P/V 操作 - semop
功能:
对信号灯集合中的一个或多个信号灯执行操作;
成功时返回(0),失败时返回(-1);
参数:
semid:要操作的信号灯集id;
sops:描述对某一个信号灯操作的结构体。若要操作多个信号灯,则要传入结构体数组;
nsops:要操作的信号灯的个数;
int semop(int semid,struct sembuf *sops,unsigned nsops);
struct sembuf
{
short semnum; //指定要操作的信号灯的编号
/* 指定要执行的操作(-1:P操作,1:V操作) */
short sem_op;
/* 一般为 0/IPC_NOWAIT/SEM_UNDO (0 :表示以阻塞的方式,直到完成操作
IPC_NOWAIT : 表示不阻塞,直接返回)
SEM_UNDO : 进程退出后,将信号量返回到其初始值(防止死锁) */
short sem_flg;
};
struct sembuf buf[3];
/* 填充第 0 个信号 */
buf[0].sem_num = 0; //操作第 0 个信号灯
buf[0].sem_op = -1; //指定执行 P 操作
buf.sem_flg = 0; //以阻塞的方式完成操作
/* 填充第 1 个信号 */
buf[1].sem_num = 1; //操作第 1 个信号灯
buf[1].sem_op = -1; //指定执行 P 操作
buf[1].sem_flg = 0; //以阻塞的方式完成操作
/* 调用 semop 完成P/V操作,操作的信号灯的个数为 2 */
semop(semid,buf,2);
3.删除信号灯 - semctl
功能:
删除信号灯集合;
成功时返回(0),失败时返回(EOF);
参数:
semid:要操作的信号灯集合id
semnum:要操作的集合中的信号灯编号(信号灯集合中的哪一个信号灯)
cmd:要执行的操作
SETVAL:设置信号灯的值
IPC_RMID:删除信号灯集合
int semctl(int semid,int semnum,int cmd,...);
semctl(semid,0,IPC_RMID);
五.用信号灯集实现共享内存同步
1.目的:
父子进程通过System V 信号灯同步对共享内存的读写,需要创建两个信号灯,一个用于写资源,一个用于读资源。
父进程从键盘输入字符串到共享内存
子进程删除字符串中的空格并打印
父进程输入 quit 后,删除共享内存和信号灯集,程序结束
2.流程图
3.代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
void init_sem(int semid , int s[] , int n);
void pv(int semid,int num,int op);
#define N 64 //共享内存的大小
#define READ 0 //信号灯集合中的信号灯的编号,代表可读缓冲区
#define WRITE 1 //信号灯集合中的信号灯的编号,代表可写缓冲区
/* 用于信号灯集合操作, man 手册里有这个共用体,但是得自己定义 */
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
int main(void)
{
/**
* 0.定义所要用到的变量
* (1)共享内存的id
* (2)信号灯集合id
* (3)信号灯初始化数组
* (4)pid
* (5)key
* (6)共享内存映射后的首地址
*/
int shmid; //共享内存的id
int semid; //信号灯集合id
int s[] = {0,1}; //信号灯初始化数组,s[0]用于读,s[1]用于写
pid_t pid; //用于创建进程
key_t key; //用于生成key
char *shmaddr; //用于接收映射的共享内存的首地址
/* 1.创建/获取key */
key = ftok(".",'s');
if(-1 == key)
{
perror("ftok error");
exit(-1);
}
/* 2.创建/获取共享内存 */
shmid = shmget(key,N,IPC_CREAT | 0666);
if(0 > shmid)
{
perror("shmget error");
exit(-1);
}
/* 3.创建信号灯集合(若失败则在退出程序前,要先把创建号的共享内存删除) */
semid = semget(key,2,IPC_CREAT | 0666);
if(0 > semid)
{
perror("semget error");
goto _error1; //处理错误
}
/* 4.对信号灯集合进行初始化,初始化 2 个信号量 */
init_sem(semid,s,2);
/* 5.映射共享内存(64)个字节,(若映射共享内存失败,在退出程序前,要先把之前创建的信号灯集合删除) */
shmaddr = (char *)shmat(shmid,NULL,0);
if(((void *)-1) == shmaddr)
{
perror("shmat error");
goto _error2;
}
/* 6.创建子进程 */
pid = fork();
switch(pid)
{
case -1:
perror("fork error");
goto _error2;
break;
/* 子进程 */
case 0:
{
char *p,*q;
while(1)
{
/* 1.对可读资源进行 P 操作 */
pv(semid,READ,-1);
/* 2.进行读操作(去除共享内存中的空格) */
p = q = shmaddr;
while(*q)
{
if(*q != ' ')
{
*p++ = *q;
}
q++;
}
*p = '\0';
printf("result:%s\n",shmaddr);
/* 3.对可写资源执行 V 操作,使其他进程可写 */
pv(semid,WRITE,1);
}
break;
}
/* 此进程 */
default:
while(1)
{
/* 7.要写缓冲区,所以先进行P操作,看有没有资源可写。若没有资源可写,则阻塞等待 */
pv(semid,WRITE,-1);
/* 8.执行写操作 */
printf("input >\n");
fgets(shmaddr,N,stdin);
/* 9.若输入了quit,则跳出循环 */
if(0 == strcmp(shmaddr,"quit\n"))
break;
/* 10.执行完写操作后,执行 V 操作,使其他进程可读 */
pv(semid,READ,1);
}
/* 发出信号,结束子进程 */
kill(pid,SIGUSR1);
break;
}
/* 若创建信号灯集合失败,则在退出程序前,要先把创建号的共享内存删除 */
_error1:
shmctl(shmid,IPC_RMID,NULL);
/* 若映射共享内存失败,在退出程序前,要先把之前创建的信号灯集合删除 */
_error2:
semctl(semid,0,IPC_RMID);
return 0;
}
/**
* @description: 初始化信号灯集合
* @param - semid: 信号灯集合的id
* @param - s[] : 信号灯初始化值的数组
* @param - n : 初始化信号灯的个数
* @return : 无
*/
void init_sem(int semid , int s[] , int n)
{
int i;
union semun myun; //创建信号灯集合共用体
/* 循环完成信号灯集合的初始化 */
for(i = 0;i < n;i++)
{
myun.val = s[i];
semctl(semid,i,SETVAL,myun);
}
}
/**
* @description: 对信号灯集合的某一个信号灯实现P/V操作
* @param - semid : 信号灯集合id
* @param - num : 信号灯集合内的信号灯的编号
* @param - op : 实现的操作
* @return : 无
*/
void pv(int semid,int num,int op)
{
struct sembuf buf;
buf.sem_num = num; //指定操作编号为 num 的信号灯
buf.sem_op = op; //指定要执行的操作
buf.sem_flg = 0; //阻塞等待,直到该操作完成
/* 进行P/V操作 */
semop(semid,&buf,1);
}