Linux进程间通信之IPC通信

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/OIPC
openmsg_get(创建/打开消息队列) shm_get(创建/打开共享内存) sem_get(创建/打开信号量)
read writemsgsnd msgrecv(消息队列读写) shmat shmdt(共享内存读写) semop(信号量读写)
closemsgctrl shmctrl semctrl

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U55ujsBM-1620634559580)(linux%E9%AB%98%E7%BA%A7%E7%BC%96%E7%A8%8B.assets/image-20210428125649471.png)]

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.uidmsg_perm.gidmsg_perm.modemsg_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来读取消息

参数msgflg0 如果消息队列中没有指定类型的消息,则阻塞

实例 实现双向数据收发
/*************************************************************************
    > File Name: msgsnd.c
    > 作者:YJK
    > Mail: 745506980@qq.com
    > 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返回索引为semnumsemval
SETVAL设置索引为semnumsemval值, 该值有 arg.val指定
GETPID返回索引为semnumsempid(在信号量上最后一次操作的进程号)值
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:从共享内存区输出到文件

上述过程不涉及到内核的拷贝,所以花的时间较少。

共享内存映射原理图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TKM8Fgth-1620634559590)(linux%E9%AB%98%E7%BA%A7%E7%BC%96%E7%A8%8B.assets/image-20210506112030815.png)]

内核为每个共享存储段维护着一个结构,该结构至少腰围每个共享存储段包含以下成员

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.uidshm_perm.gidshm_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;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值