【Linux】进程间通信——消息队列、共享内存与信号量

标识符与键key

​内核中的IPC结构(消息队列、信号量、共享内存)都用一个非负整数的标识符加以引用。标识符是IPC对象的内部名,为使多个进程能够使用同一IPC对象,需要提供一个外部命名方案。为此,每个IPC对象都与一个键key相关联,将这个键作为该对象的外部名。创建IPC结构时都应指定一个键,这个键的数据类型是基本系统数据类型key_t,这个键由内核变换成标识符。

#include 
#include 
key_t ftok(const char *pathname, int proj_id);
// 返回值:若成功返回键 key,失败返回(key_t)-1

参数:

  1. pathname:当前操作系统中一个存在的路径,使用该文件属性的st_dev和st_ino填充键
  2. proj_id:产生键时,使用该参数的低8位
    【Linux】进程间通信——消息队列、共享内存与信号量_第1张图片
// 该结构规定了权限和所有者
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;	// 序号
};

消息队列

消息队列是消息的链表,存放在内核中,由消息队列标识符标识。

  • 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
  • 消息队列独立于进程,不受进程生命周期的影响。
  • 消息队列的消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
    【Linux】进程间通信——消息队列、共享内存与信号量_第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*/
};

消息的数据结构如下:

struct msg_msg 
{
	struct list_head m_list;	
	long m_type;				// 消息的类型
	size_t m_ts;				// 消息的大小
	struct msg_msgseg *next;	// 下一个节点
	void *security;				// 真正的消息的位置
};

创建/获取消息队列

#include 
#include 
#include 
int msgget(key_t key, int msgflg);
//返回值,若成功返回消息队列ID,失败返回-1

参数:

  1. key:消息队列的键key
  2. msgflg:创建消息队列时指定的属性
  • IPC_CREAT:创建新的消息队列,同时需要指定对消息队列的操作权限
  • IPC_EXCL:检测消息队列是否存在,必须和IPC_CREAT一起使用

操作消息队列

#include 
int msgctl(int msqid,int cmd,struct msqid_ds* buf);
//返回值:成功返回0,失败返回-1

参数:

  1. shmid:消息队列ID
  2. cmd
  • IPC_STAT:得到当前消息队列的状态
  • IPC_SET:设置消息队列的状态
  • IPC_RMID:删除该消息队列以及仍在该队列中的所有数据,删除立即生效。仍在使用这一队列的其他进程,再次操作时,会得到EIDRM错误
  1. buf:
    cmd=IPC_STAT,作为传出参数,会得到消息队列的相关属性信息
    cmd=IPC_SET,作为传入参数,将用户的自定义属性设置到消息队列中
    cmd=IPC_RMID,buf无意义,指定为NULL即可

发送消息

#include 
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
// 成功返回0, 失败返回-1

参数:

  1. msqid:消息队列ID
  2. msgp:指向自定义缓冲区
struct msgbuf
{   	
	long mtype;			// 消息的类型
	char mtext[512]; 	// 消息内容,在使用时,自己重新定义此结构
};
  1. msgsz:待发送的消息内容的长度,即mtext的长度
  2. msgflg
  • 0:阻塞式函数。进程解除阻塞:有空间可以容纳要发送的消息;从系统中删除此队列;捕捉到一个信号
  • IPC_NOWAIT:类似文件I/O的非阻塞I/O标志,若消息队列已满,或者队列中的消息总数等于系统限制值,或队列中的字节总数等于系统限制值,则函数立即出错,返回EAGAIN

接收消息

#include 
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
// 成功返回消息数据部分的长度,出错返回-1
  1. msqid:消息队列ID
  2. msgp:与msgsnd函数一致
  3. msgsz:数据缓冲区的长度
  4. msgtype:从消息队列中取出哪一类型的消息
  • type=0:返回队列中的第一个消息。
  • type>0:返回队列中消息类型为type的第一个消息。
  • type<0:返回队列中消息类型值小于等于type绝对值的消息,如果这种消息有若干个,则取类型值最小的消息。
  1. msgflg参数
  • 0:阻塞式函数。
  • IPC_NOWAIT:类似文件I/O的非阻塞I/O标志。
  • MSG_NOERROR:若返回的消息长度大于msgsz,则该消息会被截断,系统不会通知。若没有设置这一标志,则出错返回E2BIG,消息仍留在队列中。

示例

#include 
#include 
#include 
#include 
#include 
#include 

// 自定义消息
struct msg_buf
{
    long mtype;      // 消息类型
    char mtext[512]; // 消息缓冲区
};

int main(int argc, char *argv[])
{
    key_t key;
    struct msg_buf msg_buf_snd = {1, "1-hello world!"};
    struct msg_buf msg_buf_recv;
    memset(&msg_buf_recv, 0, sizeof(msg_buf_recv));
    // 获取Key
    key = ftok("./key.c", 0xFF);
    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)
            {
                perror("msgget");
                return -1;
            }
        }
        else
        {
            perror("msgget");
            return -1;
        }
    }
    printf("message 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("message receive: %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;
}

#include 
#include 
#include 
#include 
#include 
#include 

// 自定义消息
struct msg_buf
{
	long mtype;		 // 消息类型
	char mtext[512]; // 消息缓冲区
};

int main(int argc, char *argv[])
{
	key_t key;
	struct msg_buf msg_buf_snd = {2, "2-hello world!"};
	struct msg_buf msg_buf_recv;
	memset(&msg_buf_recv, 0, sizeof(msg_buf_recv));
	// 获取Key
	key = ftok("key.c", 0xFF);
	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)
			{
				perror("msgget");
				return -1;
			}
		}
		else
		{
			perror("msgget");
			return -1;
		}
	}
	printf("message 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("message receive: %s\n", msg_buf_recv.mtext);
	}
	return 0;
}

共享内存

       共享内存允许两个或多个进程共享同一块存储区,通过地址映射将这块物理内存映射到不同进程的地址空间中,多个进程可以通过这块物理空间进行数据的交互,达到进程间通信的目的。

  • 共享内存既可以实现有血缘关系的进程间通信也可以实现没有血缘关系的进程间通信。
  • 共享内存独立于进程,不受进程生命周期的影响。
  • 共享内存操作默认不阻塞,共享内存不保证进程间的数据同步。
  • 当共享内存被标记为删除状态之后,并不会马上被删除,直到所有的进程全部与共享内存解除关联(引用计数原理),共享内存才会被删除。
  • 共享内存是最快的进程间通信方式,进程间通信不涉及到内核,不用通过系统调用来传递数据。
    【Linux】进程间通信——消息队列、共享内存与信号量_第3张图片

内核为每块共享内存维护着一个结构体,数据结构如下:

struct shmid_ds
{
	struct ipc_perm		shm_perm;		/* 操作权限 */
	int					shm_segsz;		/* 共享内存段的大小,单位:字节 */
	__kernel_time_t		shm_atime;		/* 最后挂载时间 */
	__kernel_time_t		shm_dtime;		/* 最后分离时间 */
	__kernel_time_t		shm_ctime;		/* 最后更改时间 */
	__kernel_ipc_pid_t	shm_cpid;		/* 创建者pid */
	__kernel_ipc_pid_t	shm_lpid;		/* 最后操作的进程pid */
	unsigned short		shm_nattch;		/* 当前连接数 */
	unsigned short 		shm_unused;		/* compatibility */
	void 				*shm_unused2;	/* ditto - used by DIPC */
	void				*shm_unused3;	/* unused */
};

创建/获取共享内存

#include 
int shmget(key_t key, size_t size, int shmflg);
// 返回值:成功返回共享内存ID,失败返回-1

参数:

  1. key:共享内存的键key
  2. size:创建共享内存时,指定共享内存的大小(单位:字节),而如果引用一个已存在的共享内存,则将size指定为0。
  3. shmflg:创建共享内存时指定的属性
  • IPC_CREAT:创建新的共享内存,指定对共享内存的操作权限
  • IPC_EXCL:检测共享内存是否存在,必须和IPC_CREAT一起使用

连接共享内存到当前进程的地址空间

#include 
void *shmat(int shmid, const void *shmaddr, int shmflg);
// 返回值:连接成功,返回共享内存的起始地址,连接失败返回(void *)-1

参数:

  1. shmid:共享内存ID
  2. shmaddr:共享内存的起始地址,指定为NULL,让内核指定
  3. shmflg:对共享内存的操作权限
  • SHM_RDONLY:读权限
  • 0:读写权限

断开与共享内存的连接

#include 
int shmdt(const void *shmaddr);
// 返回值:成功返回0,失败返回-1

参数:

  1. shmaddr:共享内存的起始地址

操作共享内存

#include 
#include 
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
// 返回值:函数调用成功返回0,调用失败返回-1

参数:

  1. shmid:共享内存ID
  2. cmd
  • IPC_STAT:得到当前共享内存的状态
  • IPC_SET:设置共享内存的状态
  • IPC_RMID:标记共享内存为删除状态
  1. buf:
  • cmd=IPC_STAT,作为传出参数,会得到共享内存的相关属性信息
  • cmd=IPC_SET,作为传入参数,将用户的自定义属性设置到共享内存中
  • cmd=IPC_RMID,buf无意义,指定为NULL即可

相关shell命令

// 查看系统中共享内存的详细信息
ipcs -m

       当共享内存被标记为删除状态之后,共享内存的状态也会发生变化,共享内存内部维护的键key从一个正整数变为 0,其属性从公共的变为私有的。这里的私有是指只有已经关联成功的进程才允许继续访问共享内存,不再允许新的进程和这块共享内存进行关联了。当共享内存被标记为删除状态并且这个引用计数变为0之后,共享内存才会被真正的被删除。
【Linux】进程间通信——消息队列、共享内存与信号量_第4张图片

示例

// 写进程
#include 
#include 
#include 

int main()
{
    // 1. 创建共享内存, 大小为4k
    int shmid = shmget(1000, 4096, IPC_CREAT | 0664);
    if (shmid == -1)
    {
        perror("shmget error");
        return -1;
    }

    // 2. 当前进程和共享内存关联
    void *ptr = shmat(shmid, NULL, 0);
    if (ptr == (void *)-1)
    {
        perror("shmat error");
        return -1;
    }

    // 3. 写共享内存
    const char *p = "hello world";
    memcpy(ptr, p, strlen(p) + 1);

    // 阻塞程序
    printf("按任意键继续, 删除共享内存\n");
    getchar();

    // 进程和共享内存解除关联
    shmdt(ptr);

    // 删除共享内存
    // 当共享内存被标记为删除状态之后,并不会马上被删除,直到所有的进程全部和共享内存解除关联,共享内存才会被删除。
    shmctl(shmid, IPC_RMID, NULL);
    printf("共享内存已经被删除...\n");

    return 0;
}
// 读进程
#include 
#include 
#include 

int main()
{
    // 1. 打开已经存在的共享内存
    int shmid = shmget(1000, 0, 0);
    if (shmid == -1)
    {
        perror("shmget error");
        return -1;
    }

    // 2. 当前进程和共享内存关联
    void *ptr = shmat(shmid, NULL, 0);
    if (ptr == (void *)-1)
    {
        perror("shmat error");
        return -1;
    }

    // 3. 读共享内存
    printf("共享内存数据: %s\n", (char *)ptr);

    // 阻塞程序
    printf("按任意键继续, 删除共享内存\n");
    getchar();

    // 进程和共享内存解除关联
    shmdt(ptr);

    // 删除共享内存
    shmctl(shmid, IPC_RMID, NULL);
    printf("共享内存已经被删除...\n");

    return 0;
}

信号量

信号量与以上IPC结构不同,它是一个计数器,信号量用于进程间的互斥与同步,若要在进程间传递数据需要结合共享内存。

  • 信号量基于操作系统的P/V操作,程序对信号量的操作都是原子操作,是在内核中实现。
  • 对信号量的PV操作不仅限于对信号量值+1或-1,可以加减任意正整数。
    【Linux】进程间通信——消息队列、共享内存与信号量_第5张图片
    内核为每个信号量集合维护了一个结构体,数据结构如下:
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; 		/* 在信号量数组上的信号量编号 信号量的数量*/
};

单个信号量的数据结构如下:

struct sem
{ 
   	unsigned short semval; 	/* 信号量的值 */
	pid_t sempid; 			/* 在信号量上最后一次操作的进程号 */
	unsigned short semncnt;/* 待信号量的值递增的进程数 */
	unsigned short semzcnt;/* 等待信号量的值递减的进程数 */
};

创建/获取信号量集合

#include 
int semget(key_t key, int nsems, int semflg);
// 返回值:成功返回信号量ID,失败返回-1

参数:

  1. key:信号量集合的键key
  2. nsems:如果是创建新信号量集合,那么nsems代表新信号量集合中的信号量的数目。如果是获取当前存在的信号量集合,那么此设置参数为0。
  3. semflg:创建信号量时指定的属性
  • IPC_CREAT:创建新的信号量,指定对信号量的操作权限
  • IPC_EXCL:检测信号量是否存在,必须和IPC_CREAT一起使用

设置信号量集合

# include
int semctl(int semid, int semnum, int cmd, union semun arg);
// 返回值:成功返回0,失败返回-1
  1. semid: 信号量集合ID
  2. semunm: 某一信号量在信号量集合中对应的序号
  3. cmd: 用来指定对信号量集合的操作
  • SETVAL:用于初始化信号量为一个已知的值。所需要的值作为联合semun的val成员来传递。在信号量第一次使用之前需要设置信号量。
  • IPC_RMID:删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。
union semun
{
   int              val;   // 信号量的值
   struct semid_ds* buf;
   unsigned short* array; 
   struct seminfo* _buf;
};

操作信号量集合

# include
int semop(int semid, struct sembuf* sops, unsigned nsops);
// 返回值:成功返回0,失败返回-1

参数:

  1. semid: 信号量集合ID
  2. sops:指针指向一个sembuf类型的结构体,指明对某信号量的操作。
struct sembuf
{
     unsigned short sem_num;	// 该信号量在信号量集合中对应的序号,[0, sem_nums-1]
     short          sem_op; 	// 信号量值在一次操作中的改变量
     short          sem_flg;	// 操作标识,IPC_NOWAIT、SEM_UNDO(当进程退出时,会将信号量置为初始值)
}
  1. nsops:操作的信号量的个数

示例

#include 
#include 
#include 
#include 

// 联合体
union semun
{
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

// 初始化信号量集合
int init_sem(int sem_id, int value)
{
    union semun tmp;
    tmp.val = value;
    if (semctl(sem_id, 0, SETVAL, tmp) == -1)
    {
        perror("Init Semaphore Error");
        return -1;
    }
    return 0;
}

// P操作
int sem_p(int sem_id)
{
    struct sembuf sbuf;
    // 该信号量在信号量集合中对应的序号
    sbuf.sem_num = 0;
    // P操作
    sbuf.sem_op = -1;
    sbuf.sem_flg = SEM_UNDO;

    if (semop(sem_id, &sbuf, 1) == -1)
    {
        perror("P operation Error");
        return -1;
    }
    return 0;
}

// V操作
int sem_v(int sem_id)
{
    struct sembuf sbuf;
    // 该信号量在信号量集合中对应的序号
    sbuf.sem_num = 0;
    // V操作
    sbuf.sem_op = 1;
    sbuf.sem_flg = SEM_UNDO;

    if (semop(sem_id, &sbuf, 1) == -1)
    {
        perror("V operation Error");
        return -1;
    }
    return 0;
}

// 删除信号量集合
int del_sem(int sem_id)
{
    union semun tmp;
    if (semctl(sem_id, 0, IPC_RMID, tmp) == -1)
    {
        perror("Delete Semaphore Error");
        return -1;
    }
    return 0;
}

int main()
{
    // 获取key
    key_t key = ftok("./key.c", 0xFF);
    if (key == -1)
    {
        perror("ftok");
        return -1;
    }

    // 创建信号量集合,其中只有一个信号量
    int sem_id = semget(key, 1, 0777 | IPC_CREAT | IPC_EXCL);
    if (sem_id == -1)
    {
        if (errno == EEXIST)
        {
            int sem_id = semget(key, 0, 0777);
            if (sem_id == -1)
            {
                perror("semget error");
                return -1;
            }
        }
        else
        {
            perror("semget error");
            return -1;
        }
    }
    // 初始化信号量,初始值为0
    init_sem(sem_id, 0);

    // 创建子进程
    pid_t pid = fork();
    if (pid == -1)
    {
        perror("Fork Error");
    }
    // 子进程
    else if (pid == 0)
    {
        sleep(2);
        printf("Process child: pid=%d\n", getpid());
        // V操作,释放资源
        sem_v(sem_id);
    }
    // 父进程
    else
    {
        // P操作,等待资源
        sem_p(sem_id);
        printf("Process father: pid=%d\n", getpid());
        // 删除信号量集合
        del_sem(sem_id);
    }
    return 0;
}

参考:https://subingwen.cn/linux/shm/
参考:https://www.cnblogs.com/CheeseZH/p/5264465.html
参考:https://blog.csdn.net/weixin_43937576/article/details/116599068

你可能感兴趣的:(Linux,linux,c语言)