4、IPC通信
IPC通信: 消息队列、信号量以及共享内存
4.1、标识符和键 key
每个内核中的IPC结构(消息队列、信号量或共享存储段)都用一个非负整数的标识符(identifier)加以引用。
标识符key 是IPC对象的内部名。为使多个合作进程能够在同一IPC对象上汇聚,需要提供一个外部命名方案。为此,每个IPC对象都与一个键key相关联,将这个键作为该对象的外部名。
无论何时创建IPC结构(通过msgget(消息队列)、semget(信号量)、shmget(共享内存)创建),都应指定一个键,这个键的数据类型是基本系统数据类型key_t。这个键由内核变换成标识符
1.1、如何获取Key
获取key可以使用ftok函数
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
//返回值:若成功返回 key, 失败返回 (key_t) -1
参数 | 描述 |
---|---|
pathname | 必须是一个现有的文件, 根据该文件的stat结构,使用 st_dev 和 st_ino 填充key |
proj_id | 产生键时,使用 该参数的 低8位 |
key 是一个int 型变量 这是key的组成
键并不是唯一的,因为如果两个路径名引用的是两个不同的文件,那么ftok通常会为这两个路径名返回不同的键。但是,因为i节点编号和键通常都放在一个长整型中,所以创建键时,可能会丢失信息。这意味着,对于不同文件的两个路径名,如果使用同一项目ID,可能产生相同的键。
1.2 IPC_PRIVATE 键
#define IPC_PRIVATE ((__kernel_key_t) 0)
使用IPC_PRIVATE创建的IPC对象, key值属性为0,和IPC对象的编号就没有了对应关系。这样毫无关系的进程,就不能通过key值来得到IPC对象的编号(因为这种方式创建的IPC对象的key值都是0)。因此,这种方式产生的IPC对象,和无名管道类似,不能用于毫无关系的进程间通信。
4.2 IPC权限结构
struct ipc_perm // 该结构规定了权限和所有者
{
__kernel_key_t key; //key
__kernel_uid_t uid; // 拥有者ID
__kernel_gid_t gid; // 拥有者组ID
__kernel_uid_t cuid; // 创建者ID
__kernel_gid_t cgid; //创建者组ID
__kernel_mode_t mode; //读写权限
unsigned short seq; //序号
};
4.2IPC与文件IO函数类比
文件I/O | IPC |
---|---|
open | msg_get(创建/打开消息队列) shm_get(创建/打开共享内存) sem_get(创建/打开信号量) |
read write | msgsnd msgrecv(消息队列读写) shmat shmdt(共享内存读写) semop(信号量读写) |
close | msgctrl shmctrl semctrl |
4.3消息队列
消息队列是消息的链接表,存储在内核中,有消息队列标识符标识。
消息队列,其实就是存在与内核空间的一个链式队列
4.3.1消息队列模型
4.3.2消息队列数据结构
struct msqid_ds {
struct ipc_perm msg_perm; //ipc_perm 权限等信息
struct msg *msg_first; /* 指向消息队列头 */
struct msg *msg_last; /* 指向消息队列尾 */
__kernel_time_t msg_stime; /* 最后发送消息的时间 */
__kernel_time_t msg_rtime; /* 最后接收消息的时间 */
__kernel_time_t msg_ctime; /* 最后修改的时间 */
unsigned long msg_lcbytes; /*重用32位垃圾字段 */
unsigned long msg_lqbytes; /* 重用32位垃圾字段*/
unsigned short msg_cbytes; /*当前队列大小 */
unsigned short msg_qnum; /*当前队列的消息个数 */
unsigned short msg_qbytes; /* 队列的最大字节数 */
__kernel_ipc_pid_t msg_lspid; /* 最后mesgsnd 的pid*/
__kernel_ipc_pid_t msg_lrpid; /* 最后recevice的pid*/
};
4.3.3消息的数据结构
struct msg_msg {
struct list_head m_list;
long m_type; //消息的类型
size_t m_ts; /* 消息的大小 */
struct msg_msgseg *next; //下一个节点
void *security; //真正的消息的位置
/* the actual message follows immediately */
};
4.3.4 msgget 创建/打开一个消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
//返回值,若成功,返回消息队列ID; 失败返回-1
参数 | 描述 |
---|---|
key | 由ftok创建的 或者 IPC_PRIVATE IPC_PRIVATE只能用于创建 不能用于打开 |
msgflag | 用于设置权限ipc_perm 中的mode mode 占低12位 root group other 权限 |
msgflag参数
msgflag参数 | 描述 |
---|---|
IPC_CREAT 0x00001000 | 如果key不存在则创建,存在则打开 |
IPC_EXCL 0x00002000 | 如果key存在,返回失败 |
IPC_NOWAIT 0x00004000 | 如果需要等待直接返回错误, 非阻塞方式打开或创建 |
在一个进程创建消息队列时,可以将flag同时指定为 IPC_CREAT | IP_EXCL 这样做的好处是可以检测消息队列是否已存在,如果存在则返回EEXIST(这与指定了O_CREAT和O_EXCL标志的open相类似);
如果不存在,则创建,如果存在则出错,然后以打开方式打开msg
4.3.5 msgctl函数 对队列执行操作
msgctl函数对队列执行多种操作。它和另外两个与信号量及共享存储有关的函数(semctl和shmctl)都是IPC的类似于ioctl的函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
//返回值。 成功返回0 , 失败返回 -1
cmd参数 | 描述 |
---|---|
IPC_STAT | 取此队列的msqid_ds 结构,并将它存放在buf指向的结构中 |
IPC_SET | 将字段msg_perm.uid、msg_perm.gid 、msg_perm.mode和msg_qbytes从buf指向的结构复制到与这个队列相关的msqid_ds结构中。此命令只能由两种进程执行:1、uid == msg_perm.uid 或 uid == msg_perm.cuid 也就是拥有者或创建者2、root用户, 只有root用户才能增加msg_qbytes的值 |
IPC_RMID | 从系统中删除该消息队列以及仍在该队列中的所有数据。 这种删除立即生效。仍在使用这一队列的其他进程,再次操作时,会得到EIDRM错误 |
4.3.6 msgsnd 将数据放到消息队列中
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//成功返回0 , 失败返回 -1
每个消息都由3部分组成:一个正的长整型类型的字段、一个非负的长度以及实际数据(对应于长度)。消息总是放在队列尾端
第二个参数为用于自定义的缓冲区
struct msgbuf{
long mtype;//消息的类型 (消息队列在储存信息时是按发送的先后顺序放置的)
char mtext[512]; //消息内容,在使用是自己重新定义此结构
};
第3个参数为接收信息的大小 *用于指定mtext的大小*
第4个参数如果为0 则按阻塞方式:进程将阻塞到出现一下情况:有空间可以容纳发送的消息;从系统中删除了次队列,或捕捉到一个信号
如果指定为IPC_NOWAIT 这类似与文件I/O的非阻塞I/O标志
若消息队列已满,或是队列中的消息总数等于系统限制值,或队列中的字节总数等于系统限制值,则指定IPC_NOWAIT是的msgsnd立即出错返回EAGAIN
4.3.7 msgrcv 从队列中取用消息
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
int msgflg);
//成功返回 消息数据部分的长度, 出错返回 -1
参数 msgp 存放实际消息的缓冲区
参数msgsize 说明缓冲区长度,若返回的消息大于msgsz,而且在flag中设置了MSG_NOERR,则该消息被截断(在这种情况下,不通知我们消息截短了,消息的截去部分被丢弃)。如果没有设置这一标志,而消息又太长,则出错返回E2BIG(消息仍然留在队列中)
参数msgtyp
msgtyp==0 返回队列中的第一个消息
Type>0 返回队列中消息类型为type的第一个消息
Type<0 返回队列中消息类型小于或等于type绝对值的消息
Type非0 用于以非先进先出的次序读消息 也就是说,通过type来读取消息
参数msgflg 为0 如果消息队列中没有指定类型的消息,则阻塞
实例 实现双向数据收发
/*************************************************************************
> File Name: msgsnd.c
> 作者:YJK
> Mail: [email protected]
> Created Time: 2021年04月28日 星期三 21时01分46秒
************************************************************************/
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
#include <string.h>
struct msg_buf{ // 自定义消息
long mtype; // 消息类型
char mtext[512]; //消息缓冲区
};
int main(int argc,char *argv[])
{
key_t key;
struct msg_buf msg_buf_snd = {
.mtype = 1,
.mtext = "hello world snd"
};
struct msg_buf msg_buf_recv;
memset(&msg_buf_recv, 0 , sizeof(msg_buf_recv));
key = ftok("key.c", 0xFF); // 获取Key
if (key == -1)
{
perror("ftok");
return -1;
}
//创建消息队列
int msgid = msgget(key, 0777 | IPC_CREAT | IPC_EXCL);
if (msgid == -1)
{
if (errno = EEXIST)
{
msgid = msgget(key, 0777);
if (msgid == -1)
goto MSG_GET_ERR;
}
else
goto MSG_GET_ERR;
}
printf("msg id :%d\n", msgid);
/*消息队列数据收发*/
int ret = 0;
for(int i =0; i < 10; i++)
{
ret = msgrcv(msgid, &msg_buf_recv, sizeof(msg_buf_recv.mtext),2, 0);
if (ret == -1)
{
perror("msgrecv");
return -1;
}
printf("msgrecv:%s\n", msg_buf_recv.mtext);
ret = msgsnd(msgid, &msg_buf_snd, strlen(msg_buf_snd.mtext), 0);
if (ret == -1)
{
perror("msgsnd");
return -1;
}
}
return 0;
MSG_GET_ERR:
perror("msgget");
return -1;
}
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
#include <string.h>
struct msg_buf{ // 自定义消息
long mtype; // 消息类型
char mtext[512]; //消息缓冲区
};
int main(int argc,char *argv[])
{
key_t key;
struct msg_buf msg_buf_recv;
struct msg_buf msg_buf_snd = {
.mtype = 2,
.mtext = "hello world msgrecv"
};
memset(&msg_buf_recv,0,sizeof(msg_buf_recv));
key = ftok("key.c", 0xFF); // 获取Key
if (key == -1)
{
perror("ftok");
return -1;
}
//创建消息队列
int msgid = msgget(key, 0777 | IPC_CREAT | IPC_EXCL);
if (msgid == -1)
{
if (errno = EEXIST)
{
msgid = msgget(key, 0777);
if (msgid == -1)
goto MSG_GET_ERR;
}
else
goto MSG_GET_ERR;
}
printf("msg id :%d\n", msgid);
/*消息队列数据收发*/
int ret = 0;
for(int i =0; i < 10; i++)
{
ret = msgsnd(msgid, &msg_buf_snd, strlen(msg_buf_snd.mtext), 0);
if (ret == -1)
{
perror("msgsnd");
return -1;
}
ret = msgrcv(msgid, &msg_buf_recv, sizeof(msg_buf_recv.mtext),1, 0);
if (ret == -1)
{
perror("msgrecv");
return -1;
}
printf("msgrecv:%s\n", msg_buf_recv.mtext);
}
return 0;
MSG_GET_ERR:
perror("msgget");
return -1;
}
4.4 信号量
信号量与已经介绍过的(管道、FIFO以及消息队列不同),。 他是一个计数器,用于为多个进程提供对共享数据对象的同步访问
获取共享资源的步骤
为了获得共享资源,进程需要执行下列操作
(1) 测试控制该资源的信号量
(2) 若此信号量的值为正,则进程可以使用该资源。在这种情况下,进程会将信号量值减1,表示它使用了一个资源单位
(3) 否则,若此信号量的值为0,则进程进入休眠状态,直至信号量的值大于0。进程被唤醒后,它返回至步骤(1)
当进程不在使用一个信号量控制的共享资源时,该信号量值增1。如果有进程正在休眠等待此信号量,则唤醒它们
为了正确的使用信号量,信号量值的测试及减1操作应当是原子操作。为此,信号量通常是在内核中实现的。
常用的信号量形式被称为二元信号量。它控制单个资源,其初始值为1
4.4.1 单个信号量 数据结构
struct sem {
unsigned short semval; /* 信号量的值 */
pid_t sempid; /*在信号量上最后一次操作的进程号 */
unsigned short semncnt; //等待信号量的值递增的进程数
unsigned short semzcnt; //等待信号量的值递减的进程数
};
4.4.2信号量集合数据结构semid_ds
struct semid_ds {
struct ipc_perm sem_perm; /* IPC权限 */
long sem_otime; /* 最后一次对信号量操作(semop)的时间 */
long sem_ctime; /* 对这个结构最后一次修改的时间 */
struct sem *sem_base; /* 在信号量数组中指向第一个信号量的指针 */
struct sem_queue *sem_pending; /* 待处理的挂起操作*/
struct sem_queue **sem_pending_last; /* 最后一个挂起操作 */
struct sem_undo *undo;/* 在这个数组上的undo 请求 */ //用于恢复初始信号量值
ushort sem_nsems; /* 在信号量数组上的信号量编号 信号量的数量*/
};
4.4.3 获取信号量ID semget函数
当我们想使用IPC信号量时,需要先使用semget函数来获得一个信号量ID
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
// 成功 返回信号量 ID, 失败返回-1
semflag 参数 与 IPC消息队列相同, 用于初始化ipc_perm结构。该结构中的mode设置为semflg的相应权限位。
semflag参数
msgflag参数 | 描述 |
---|---|
IPC_CREAT 0x00001000 | 如果key不存在则创建,存在则打开 |
IPC_EXCL 0x00002000 | 如果key存在,返回失败 |
IPC_NOWAIT 0x00004000 | 如果需要等待直接返回错误, 非阻塞方式打开或创建 |
在一个进程创建消息队列时,可以将flag同时指定为 IPC_CREAT | IP_EXCL 这样做的好处是可以检测消息队列是否已存在,如果存在则返回EEXIST(这与指定了O_CREAT和O_EXCL标志的open相类似);
如果不存在,则创建,如果存在则出错,然后以打开方式打开msg
nsems参数 设置该集合中信号量的个数也就是将sem_nsems成员设置为nsems,如果是创建新集合,则必须指定nsems,如果是引用一个现有的集合,则将nsems设置为0
4.4.4 信号量操作函数 semctl
作用 设置信号量的值, 获取信号量的值, 释放信号量等等
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, union semun arg);
//成功返回0 , 失败返回-1
第4个参数是可选的,是否使用取决于所请求的命令,如果使用该参数,则其类型是semun,他是多个命令特定参数的联合(union)
union semun
{
int val; // for SETVAL
struct semid_ds *buf; //for IPC_STAT and IPC_SET
unsigned short *array; //for GETALL and SETALL
};
semnum 参数 需要操作的信号量的下标, 0 ~ nsems - 1
CMD参数 | 描述 |
---|---|
IPC_STAT | 获取信号量集合的semid_ds结构,并存储在arg.buf指向的结构中 |
IPC_SET | 按arg.buf指向的结构中的值,设置与此集合结构中的sem_perm.uid、sem_perm.gid、sem_perm.mode字段。此命令只能有两种进程执行 1、uid == sem_perm.uid 或sem_perm.cuid == uid 2、root用户 |
IPC_RMID | 从系统中删除该信号量集合。这种删除是立即发生的,删除时,仍在使用此信号量集合的进程,在他们再次对信号量集合进程操作时,将出错返回EIDRM。此命令只能有两种进程执行 1、uid == sem_perm.uid 或sem_perm.cuid == uid 2、root用户 |
GETVAL | 返回索引为semnum的semval值 |
SETVAL | 设置索引为semnum的semval值, 该值有 arg.val指定 |
GETPID | 返回索引为semnum的sempid(在信号量上最后一次操作的进程号)值 |
GETNCNT | 返回索引为semnum的semncnt (等待信号量的值递增的进程数)值 |
GETZCNT | 返回索引为semnum的semzcnt(等待信号量的值递减的进程数)值 |
GETALL | 取该集合中所有的信号量值。这些值存储在arg.array指向的数组中 |
SETALL | 将该集合中所有信号量值设置成arg.array指向的数组中的值 |
4.4.5semop函数,操作信号量
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
//返回值,成功 返回0 , 失败返回 -1
参数sops ,指向一个有sembuf结构表示的信号量操作数组
参数nsops , 操作的信号量的个数
struct sembuf {
unsigned short sem_num; /* semaphore index in array 数组中信号量的索引*/
short sem_op; /* semaphore operation PV操作 P操作 -1 V操作 1*/
short sem_flg; /*操作标识 IPC_NOWAIT , SEM_UNDO*/
};
SEM_UNDO标志
当进程退出时,会将信号量置为初始值。
当操作信号量(semop)时,sem_flg可以设置SEM_UNDO标识;SEM_UNDO用于将修改的信号量值在进程正常退出(调用exit退出或main执行完)或异常退出(如段异常、除0异常、收到KILL信号等)时归还给信号量。
如信号量初始值是20,进程以SEM_UNDO方式操作信号量减2,减5,加1;在进程未退出时,信号量变成20-2-5+1=14;在进程退出时,将修改的值归还给信号量,信号量变成14+2+5-1=20。
关于sem_op的操作
(1) 最容易处理的情况是sem_op为正值。这对应于进程释放的占用的资源数。sem_op值会加到信号量的值上,如果指定了SEM_UNDO标志,进程退出时,则从该进程的此信号量中调整值中减去sem_op
sem_op | 描述 |
---|---|
sem_op > 0 | 这对应于进程释放的占用的资源数。sem_op值会加到信号量的值上 |
sem_op < 0 | 则表示进程 要获取该信号量的资源 |
sem_op == 0 | 表示调用进程希望等待到该信号量值变为0 |
sem_op < 0
如果信号量小于sem_op的绝对值(资源不能满足要求)
(a) 若指定了IPC_NOWAIT,则semop出错返回EAGAIN
(b)若未指定IPC_NOWAIT,则该信号量的**semncnt + 1(**因为调用进程将进入休眠状态),然后调用进程被挂起直至下列事件之一发生。
(1)此信号量值变成大于等于sem_op的绝对值(即某个进程已释放了某些资源)。此信号量的semncnt-1(因为已结束等待) ,并且从信号量中减去sem_op的绝对值(表示已经获取到资源)。
(2) 从系统中删除了此信号量。在这种情况下,函数出错返回EIDRM
(3) 进程捕捉到一个信号,并从信号处理程序返回,在这种情况下,此信号量的semncnt值减1(因为调用进程不再等待),并且函数出错返回EINTR(中断)
sem_op == 0
若sem_op 为0, 这表示调用进程希望等待到该信号量值变为0
如果信号量值当前是0,则此函数立即返回
如果当前信号量的值不为0,则适用以下条件
(a) 若指定了IPC_NOWAIT, 则出错返回 EAGAIN
(b) 若未指定IPC_NOWAIT, 则该信号量的semzcnt + 1(因为调用进程将进入休眠状态),然后调用进程被挂起,直至下列的一个事件发生。
(1) 此信号变成0。此信号量的semzcnt - 1 (因为调用进程已结束等待)。
(2) 从系统中删除了此信号量。在这种情况下,函数出错返回EIDRM
(3) 进程捕捉到一个信号,并从信号处理函数返回。在这种情况下,此信号量的semzcnt - 1
(因为调用进程不再等待),并且函数出错返回EINTR(中断)。
PV 原语
P 获取资源 P执行的操作是 获取资源然后对信号量-1或-n 表示获取资源,如果信号量==0 则阻塞,表示当前无资源可获取。
V释放资源 V释放资源,当执行完操作之后,对信号量执行+1或+n操作,表示释放资源
例如两个进程A、B,假设数据只有一个方向,也就是说 进程A写入 进程B读取
因为进程调度的竞争状态,如果只有一个信号量的话,当A 执行P操作,写入数据,然后再执行V操作之后,此时此信号量是可使用的,而两个进程都有可能执行P操作,那么并不能很好得实现A、B进程之间的同步问题。
所以这里使用两个信号量假设 信号量为 S、Q
对于进程A来说,
进程A 进程B
S = 1; Q=0;
Q = 0; S=1;
进程A执行写入操作 此时信号量的变化为
进程A 执行P 操作 S -1 S = 0
操作临界区
进程A操作完临界区执行V操作 Q + 1 Q = 1
对于进程B来说,此时的信号量Q = 1, S = 0; Q = 1表明当前资源可用
进程A执行写入操作 此时信号量的变化为
进程A 执行P 操作 Q - 1 Q = 0
操作临界区
进程A操作完临界区执行V操作 S + 1 S = 1
这样便完成了对资源的同步访问
使用信号量报错
: Numerical result out of range
出现这个问题的原因是因为使用了 SEM_UNDO标志 因为在sem结构体中sem_flg的定义为
short sem_flg; linux信号量值大值为32767 即 0x7FFF semop使用次数超过此数值的话就会出现该错误
解决办法
上面分析了问题产生的原因,那么解决方法就是将sem_flag = 0;即可
sem.sem_num = index; //要操作信号量的索引(下标)
sem.sem_op = opt; //P -1 V 1
// sem.sem_flg = SEM_UNDO;// 退出时恢复至当前进程第一次使用时的信号量值
sem.sem_flg = 0;
4.5 共享内存
共享存储允许两个或多个进程共享一个给定的存储区,因为数据不需要再客户进程和服务器进程之间复制,所以这是最快的一种IPC。使用共享存储时要掌握的唯一窍门是,在多个进程之间同步访问一个给定的存储区。若服务器进程正在将数据放入共享存储区,则在它做完这一操作之前,客户进程不应当去读取这些数据。通常,信号量用于同步共享存储访问(也可以使用记录锁或互斥量)
共享内存和消息队列,FIFO,管道传递消息的区别:
消息队列,FIFO,管道的消息传递方式一般为
1:服务器得到输入
2:通过管道,消息队列写入数据,通常需要从进程拷贝到内核。 内核copy_from_user
3:客户从内核拷贝到进程 内核copy_to_user
4:然后再从进程中拷贝到输出文件
上述过程通常要经过4次拷贝,才能完成文件的传递。
共享内存只需要
1:从输入文件到共享内存区
2:从共享内存区输出到文件
上述过程不涉及到内核的拷贝,所以花的时间较少。
共享内存映射原理图
内核为每个共享存储段维护着一个结构,该结构至少腰围每个共享存储段包含以下成员
shmid_ds结构体
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms 操作权限*/
int shm_segsz; /* size of segment (bytes) 共享内存段的大小 */
__kernel_time_t shm_atime; /* last attach time 最近挂载时间*/
__kernel_time_t shm_dtime; /* last detach time 最后分离时间*/
__kernel_time_t shm_ctime; /* last change time 最后更改时间*/
__kernel_ipc_pid_t shm_cpid; /* pid of creator 创建者pid*/
__kernel_ipc_pid_t shm_lpid; /* pid of last operator 最后操作的进程pid*/
unsigned short shm_nattch; /* no. of current attaches 当前连接数*/
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
shmget函数
调用的第一个函数通常是shmget,它获得一个共享存储的标识符
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
//返回值: 成功返回共享存储ID , 失败返回-1
ipc_perm结构中的mode根据shmflg中的相应权限设置位设置。 详情见4.2节
shm_lpid、shm_nattach、shm_atime和shm_dtime 都设置为0
shm_ctime 设置为当前时间
shm_segsz 设置为请求的size
参数size是该共享存储段的长度,以字节为单位。如果正在创建一个新段(通常是在服务器进程中),则必须指定其size。如果正在引用一个现存的段(一个客户进程),则将size指定为0。当创建一个新段时,段内的内容被初始化为0
shmctl函数
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
//成功返回0, 失败返回-1
cmd参数 | 描述 |
---|---|
IPC_STAT | 取此队列的shmid_ds 结构,并将它存放在buf指向的结构中 |
IPC_SET | 将字段shm_perm.uid、shm_perm.gid 、shm_perm.mode从buf指向的结构复制到与这个队列相关的msqid_ds结构中。此命令只能由两种进程执行:1、uid == msg_perm.uid 或 uid == msg_perm.cuid 也就是拥有者或创建者2、root用户 |
IPC_RMID | 从系统中删除该共享存储段。因为每一个共享存储段维护着一个连接计数(shmid.shm_nattch字段),所以除非使用该段的最后一个进程终止或与该段分离,否则不会实际上删除该存储段。不管此段是否仍在使用,该段标识符都会被立刻删除,所以不能再用shmat与此段连接。此命令只能由两种进程执行:1、uid == msg_perm.uid 或 uid == msg_perm.cuid 也就是拥有者或创建者2、root用户 |
SHM_LOCK | 在内存中对共享存储段加锁,此命令只能有root用户执行 |
SHM_UNLOCK | 解锁共享存储段 |
SHM_LOCK的意义在于锁定这块共享内存并禁止它交换。
被锁定的区段不允许被交换出内存(否则它可能被交换到虚拟内存中,swap分区)。这样做的优势在于,与其把内存区段交换到文件系统,在某个应用程序调用时再交换回内存, 不如让它一直处于内存中,且对多个应用程序可见。从提升性能的角度来看,很重要的。
但是,这并不代表一个进程锁定住共享内存以后其它进程禁止访问**。锁定后只是不会交换到文件系统里(虚拟内存,swap分区)**,而不是指它的访问性。
shmat函数连接共享内存
一旦创建了一个共享内存,进程可以调用shmat函数将其连接(挂载)到它的地址空间中
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
//返回值 成功返回指向共享内存的指针, 失败返回-1;
共享存储段连接到调用进程的哪个地址上与shmaddr参数以及shmflg中是否指定SHM_RND有关
addr:=NULL:表示映射地址由内核自动选择(推荐方法) ;
shaddr !=NULL:映射地址取决于flag的值;
flag!=SHM_RND,映射地址等于addr指定的地址;
指定SHM_RND,进位到最低可用地址;
flag
SHM_RND:表示第二个参数指定的地址应被向下靠拢到内存页面大小的整数倍
SHM_RDONLY:以只读方式映射到进程的地址空间;
SHM_REMAP:替代掉与指定参数重叠的现存共享区段的映射;
返回值是该段所连接的实际地址,如果出错则返回-1.如果shmat成功执行没那么内核将使与该共享存储段相关的shmid_ds结构中的shm_nattch计数器加1。
shmdt函数
当对共享存储段的操作已经结束时,则调用shmdt与该段分离。注意,这并不从系统中删除其标识符以及其相关的数据结构。该标识符仍然存在,直至某个进程(一般是服务器进程)带IPC_RMID命令的调用shmctl特地删除它为止
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
shmaddr是调用shmat时的返回值。如果成功,shmdt将使相关shmid_ds结构中的shm_nattch计数器减1
实例
两个进程间使用共享内存通信,然后使用信号量进行同步
这个是读进程,对于写进程将PV操作修改即可
#include <stdio.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <errno.h>
#include <string.h>
#define P -1
#define V 1
#define READ 0
#define WRITE 1
#define SEM_NUM 2 //二元信号量
#define SHM_SIZE 4096
union semun{
int val;
struct semid_ds *buf;
unsigned short *array; //信号量集合
};
int PVopt(const int semid, const short opt, const unsigned short index)
{
struct sembuf sem;
memset(&sem, 0, sizeof(sem));
sem.sem_num = index; //要操作信号量的索引(下标)
sem.sem_op = opt; //P -1 V 1
// sem.sem_flg = SEM_UNDO;// 退出时恢复至当前进程第一次使用时的信号量值
sem.sem_flg = 0;
if (semop(semid, &sem, 1) == -1)
{
perror("semop");
return -1;
}
}
int main(int argc,char *argv[])
{
//1、获取key
key_t key = ftok(".", 0xFF);
if (key == -1)
{
perror("ftok");
return -1;
}
//2、获取信号量 二元信号量
unsigned short array[] = {0x0, 0x1};
union semun arg;
arg.array = array;
//获取信号量id
int semid = semget(key, SEM_NUM, 0777 | IPC_CREAT | IPC_EXCL);
if (semid == -1)
{
if (errno ==EEXIST) //如果已存在,则打开
{
semid = semget(key, 0, 0);
if (semid == -1)
goto SEM_ERR;
}
else
goto SEM_ERR;
}
else { //只设置一次信号量
if (semctl(semid, 0, SETALL, arg) == -1)
{
perror("semclt");
return -1;
}
}
//设置信号量集
// 3、获取共享内存ID
int shmid = shmget(key, SHM_SIZE, 0777 | IPC_CREAT | IPC_EXCL);
if (shmid == -1)
{
if (errno == EEXIST)
{
shmid = shmget(key, 0, 0);
if (shmid == -1)
goto SHM_ERR;
}
else
goto SHM_ERR;
}
// 将共享内存挂载到当前进程空间
void * shm_addr = shmat(shmid, NULL, 0);
if (shm_addr == (void *)-1)
{
perror("shmat");
semctl(semid, 0, IPC_RMID);
shmctl(semid, IPC_RMID, NULL);
return -1;
}
/*数据操作*/
int i = 0xFF;
char * buf = "hello world";
while(i)
{
PVopt(semid, P, WRITE);
memcpy(shm_addr, buf, strlen(buf));
PVopt(semid, V, READ);
i--;
}
//读写完毕释放资源
shmdt(shm_addr); //卸载连接的共享内存
/*读进程*/
/*
int i = 0xFF;
char buf[256] = {0};
while(i)
{
printf("read :%d\n",semctl(semid, 1, GETVAL));
PVopt(semid, P, READ);
memcpy(buf, shm_addr, strlen(shm_addr));
printf("%s\n",buf);
PVopt(semid, V, WRITE);
i--;
}
//读完毕释放资源
shmdt(shm_addr); //卸载连接的共享内存
shmctl(shmid, IPC_RMID, NULL);
/*删除信号量*/
semctl(semid, 0, IPC_RMID);
*/
return 0;
SEM_ERR:
perror("semget");
return -1;
SHM_ERR:
perror("shmget");
semctl(semid, 0, IPC_RMID);
return -1;
}