【进程间通信】IPC对象(进程间通信的精髓)

news2024/11/16 13:04:08
  • (꒪ꇴ꒪ ),Hello我是祐言QAQ
  • 我的博客主页:C/C++语言,数据结构,Linux基础,ARM开发板,网络编程等领域UP🌍
  • 快上🚘,一起学习,让我们成为一个强大的攻城狮!
  • 送给自己和读者的一句鸡汤🤔:集中起来的意志可以击穿顽石!
  • 作者水平很有限,如果发现错误,请在评论区指正,感谢🙏


        进程间通信(IPC)在操作系统中是至关重要的,它使不同的进程能够交换信息、共享资源以及协调任务。本篇博客将深入探讨三种主要的IPC对象:消息队列、共享内存和信号量,带你深入了解它们的特性、用法。    

        ftok 函数是一个用于生成合法键值(KEY)的函数,通常用于创建 System V IPC 对象,如消息队列、共享内存和信号量的键值。它通过将文件路径名和项目ID结合起来生成一个唯一的键值。

  • pathname:一个合法的文件路径名,用于生成键值的一部分。
  • proj_id:一个整数,用于生成键值的另一部分。

        下面是一个使用 ftok 函数生成键值的示例:

#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    const char *pathname = "/tmp/myfile"; // 合法的文件路径
    int proj_id = 'A'; // 项目ID,可以是任意整数,通常用字符表示

    // 使用 ftok 函数生成键值
    key_t key = ftok(pathname, proj_id);
    if (key == -1) {
        perror("ftok");
        exit(EXIT_FAILURE);
    }

    printf("Generated key: %d\n", key);

    return 0;
}

注意事项:

  • 如果两个参数相同,那么产生的 key 值也相同;
  • 第一个参数一般取进程所在的目录,因为在一个项目中需要通信的几个进程通常会 出现在同一个目录当中;
  • 如果同一个目录中的进程需要超过 1 IPC 对象,可以通过第二个参数来标识;
  • 系统中只有一套 key 标识,也就是说,不同类型的 IPC 对象也不能重复。

        此外在Linux操作系统中,可以使用一系列命令来管理和操作IPC(进程间通信)对象。这些命令允许您查看和删除消息队列、共享内存以及信号量等IPC对象。以下是这些命令的详细注释:

查看ipc对象的命令:
        ipcs -a          //查看所有的ipc对象

        ipcs -q          //查看消息队列
        ipcs -m         //查看共享内存
        ipcs -s          //查看信号量
删除ipc对象的命令:

        ipcrm  -q  msqid  或 ipcrm  -Q  键值        //删除消息队列
        ipcrm  -m  shmid  或 ipcrm  -M  键值       //删除共享内存
        ipcrm  -s  semid  或 ipcrm  S   键值        //删除信号量

一、消息队列

        消息队列是一种进程间通信机制,是一种异步的信息交换方式,通过消息的发送和接收来实现数据交换。因此它适用于小量数据的异步通信,且允许进程在不等待的情况下传递消息

1. 概述

        消息队列是一种基于内核的通信方式它允许不同进程通过发送和接收消息来交换数据。每个消息都有一个类型和内容,使得进程可以根据类型选择性地接收消息。

        你也可以认为消息队列提供一种带有数据标识的特殊管道,使得每一段被写入的数据都变成带标识的消息,读取该段消息的进程只要指定这个标识就可以正确地读取,而不会受到其他消息的干扰,从运行效果来看,一个带标识的消息队列,就像多条并存的管道一样。

2. 创建和访问

        消息队列的创建和访问需要使用一系列API函数,如msgget()用于创建或获取消息队列msgsnd()用于发送消息msgrcv()用于接收消息

                                                        获取消息队列的 ID

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>      //头文件
原型:

        int msgget(key_t key, int msgflg);
参数:
    key         消息队列的键值
    msgflg
        IPC_CREAT         如果 key 对应的 MSG 不存在,则创建该对象
        IPC_EXCL            如果该 key 对应的 MSG 已经存在,则报错
返回值:
    成功         该消息队列的 ID
    失败         -1

注意:

  • 选项 msgflg 是一个位屏蔽字,因此 IPC_CREATIPC_EXCL 和权限 mode 可以用
    的方式叠加起来,比如:msgget(key, IPC_CREAT | 0666); 表示如果 key 对应的消息队列不存在就创建,且权限指定为 0666,若已存在则直接获取 ID。
  • 权限只有读和写,执行权限是无效的,例如 0777 0666 是等价的。
  • 当 key 被指定为 IPC_PRIVATE 时,系统会自动产生一个未用的 key 来对应一个新的
    消息队列对象。

                                                        发送、接收消息

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
原型:
        int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
        ssize_t msgrcv(int msqid, void *msgp,size_t msgsz, long msgtyp, int msgflg);
参数:
    msqid         发送、接收消息的消息队列 ID
    msgp         要发送的数据、要接收的数据的存储区域指针
    msgsz         要发送的数据、要接收的数据的大小
    msgtyp         这是 msgrcv 独有的参数,代表要接收的消息的标识
    msgflg (一般设置为0)
                IPC_NOWAIT 非阻塞读出、写入消息
                MSG_EXCEPT 读取标识不等于 msgtyp 的第一个消息
                MSG_NOERROR 消息尺寸比 msgsz 大时,截断消息而不报错     
返回值: 
        成功  
            msgsnd( )          0

            msgrcv( )          真正读取的字节数
        失败   -1

使用这两个收、发消息函数需要注意以下几点:
        (1)发送消息时,消息必须被组织成以下形式:

struct msgbuf
{
    long  mtype;            // 消息的标识
    char  mtext[1];         // 消息的正文
};

        也就是说,发送出去的消息必须以一个 long 型数据打头,作为该消息的标识,后面的数据则没有要求;
        (2)消息的标识可以是任意长整型数值,但不能是 0L;
        (3)参数 msgsz 是消息中正文的大小,不包含消息的标识。 

3. 设置或者获取消息队列的相关属性(一般用于删除消息队列)

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
原型: int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数:
    msqid         消息队列 ID
    cmd
        IPC_STAT          获取该 MSG 的信息,储存在结构体 msqid_ds 中
        IPC_SET           设置该 MSG 的信息,储存在结构体 msqid_ds
        IPC_RMID         立即删除该MSG,并且唤醒所有阻塞在该MSG上的进程,同时忽略第三个参数
        IPC_INFO           获得关于当前系统中 MSG 的限制值信息
        MSG_INFO         获得关于当前系统中 MSG 的相关资源消耗信息
        MSG_STAT         同 IPC_STAT,但 msgid 为该消息队列在内核中记录所有消息队列信息的数组的下标,因此通过迭代所有的下标可以获得系统中所有消息队列的相关信息
    buf         相关信息结构体缓冲区

4.举例

        巴巴一堆了是时候用一个实例来展示如何用消息队列完成一些操作,下面有一个趣味题目来完成一下:

        利用消息队列,模仿三角恋,小明跟小刚对小红说:“i like you”,小红收到小明的信息,发送:“i like you honey”,但小红收到小刚的信息,则发送:“sorry,i like xiaoming,you are good boy”。

        代码有点多,完整的代码在这个链接下免费下载:

       三角恋

二、共享内存

        共享内存是效率最高的 IPC因为他抛弃了内核这个“代理人”,直截了当地将一块裸露的内存放在需要数据传输的进程面前,让他们自己搞,这样的代价是:这些进程必须小心谨慎地操作这块裸露的共享内存,做好诸如同步、互斥等工作,毕竟现在没有人帮他们来管理了,一切都要自己动手。也因为这个原因,共享内存一般不能单独使用,而要配合信号量、 互斥锁等协调机制,让各个进程在高效交换数据的同时,不会发生数据践踏、破坏等意外。

        共享内存通过映射到不同进程的虚拟空间,进程访问虚拟空间达到访问其他进程数据的目的。使用共享内存的一般步骤是:

  • 获取共享内存对象的 ID;
  • 然后将共享内存映射至本进程虚拟内存空间的某个区域;
  • 当不再使用时,解除映射关系;
  • 当没有进程再需要这块共享内存时,删除它。

1. 概述

        共享内存允许多个进程直接访问同一块内存,避免了数据的复制和传输。这使得共享内存成为一种高效的数据共享方式。

2. 创建和访问

        共享内存的创建和访问需要使用API函数,如shmget()用于创建共享内存shmat()用于附加共享内存到进程地址空间

                                                        获取共享内存的 ID

#include <sys/ipc.h>
#include <sys/shm.h>
原型: int shmget(key_t key, size_t size, int shmflg);
参数:
            key         共享内存的键值
            size         共享内存的尺寸(PAGE_SIZE 的整数倍)
            shmflg
                        IPC_CREAT                 如果 key 对应的共享内存不存在,则创建之
                        IPC_EXCL                    如果该 key 对应的共享内存已存在,则报错
                        SHM_HUGETLB          使用“大页面”来分配共享内存
                        SHM_NORESERVE     不在交换分区中为这块共享内存保留空间
                        mode                             共享内存的访问权限(八进制,如 0644)
返回值:
            成功         该共享内存的 ID
            失败         -1
所谓的“大页面”指的是内核为了提高程序性能,对内存实行分页管理时,采用比默认尺寸(4KB)更大的分页,以减少缺页中断。Linux 内核支持以 2MB 作为物理页面分页的基
本单位。

                                   对共享内存进行映射,或者解除映射 

#include <sys/types.h>
#include <sys/shm.h>
原型:
            void *shmat(int shmid, const void *shmaddr, int shmflg);
            int shmdt(const void *shmaddr);
参数:
    shmid         共享内存 ID
    shmaddr
                shmat( )
            1,如果为 NULL,则系统会自动选择合适的虚拟内存空间地址去映射共享内存。
            2,如果不为 NULL,则系统会根据 shmaddr 来选择一个合适的内存区域。
                shmdt( )         共享内存的首地址
    shmflg
                SHM_RDONLY 以只读方式映射共享内存
                SHM_REMAP 重新映射,此时 shmaddr 不能为 NULL
                SHM_RND 自动选择比 shmaddr 小的最大页对齐地址
返回值:
            成功         共享内存的首地址
            失败         -1

注意:

        (1)共享内存只能以只读或者可读写方式映射,无法以只写方式映射。

        (2)shmat( )第二个参数 shmaddr 一般都设为 NULL,让系统自动找寻合适的地址。但当其确实不为空时,那么要求 SHM_RND 在 shmflg 必须被设置,这样的话系统将会选择比shmaddr 小而又最大的页对齐地址(即为 SHMLBA 的整数倍)作为共享内存区域的起始地址。
        如果没有设置 SHM_RND,那么 shmaddr 必须是严格的页对齐地址。
        总之,映射时将 shmaddr 设置为 NULL 是更明智的做法,因为这样更简单,也更具移
植性。

        (3)解除映射之后,进程不能再允许访问 SHM。

                                                获取或者设置共享内存的相关属性 

#include <sys/ipc.h>
#include <sys/shm.h>
原型: int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
    shmid        共享内存 ID
    cmd
                IPC_STAT               获取属性信息,放置到 buf 中
                IPC_SET                 设置属性信息为 buf 指向的内容
                IPC_RMID               将共享内存标记为“即将被删除”状态
                IPC_INFO                获得关于共享内存的系统限制值信息
                SHM_INFO              获得系统为共享内存消耗的资源信息
                SHM_STAT               同 IPC_STAT,但 shmid 为该 SHM 在内核中记录所有 SHM 信息的数组的下标,因此通过迭代所有的下标可以获得系统中所有 SHM 的相关信息
                SHM_LOCK              禁止系统将该 SHM 交换至 swap 分区
                SHM_UNLOCK         允许系统将该 SHM 交换至 swap 分区
    buf        属性信息结构体指针

luomiou.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <sys/shm.h>

int main(int argc, char const *argv[])
{
	//获取键值
	key_t key = ftok("./", 3);
	if (key == -1)
	{
		perror("ftok");
		return -1;
	}

	//获取共享内存的ID
	int shmid = shmget(key, 4096, IPC_CREAT | 0777);
	if (shmid == -1)
	{
		perror("shmget");
		return -1;
	}

	//对共享内存进行映射
	char *s = (char *)shmat(shmid, NULL, 0);

	//往共享内存写入数据
	while(1)
	{
		scanf("%[^\n]", s);
		while(getchar()!='\n');
	}

	//解除映射
	shmdt(s);

	//删除共享内存

	return 0;
}

zhuliye .c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <sys/shm.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
	//获取键值
	key_t key = ftok("./", 3);
	if (key == -1)
	{
		perror("ftok");
		return -1;
	}

	//获取共享内存的ID
	int shmid = shmget(key, 4096, IPC_CREAT | 0777);
	if (shmid == -1)
	{
		perror("shmget");
		return -1;
	}

	//对共享内存进行映射
	char *s = (char *)shmat(shmid, NULL, 0);

	//获取共享内存的数据
	while(1)
	{
		printf("共享内存数据:%s\n", s);
		sleep(5);
	}

	//解除映射
	shmdt(s);

	//删除共享内存
	// shmctl(shmid, IPC_RMID, NULL);


	return 0;
}

3. 同步机制

        共享内存虽然高效,但容易引发竞态条件。因此,需要额外的同步机制,信号量,来保证多个进程对共享内存的安全访问,接下来我们就来学习一下如何使用信号量来实现同步。

三、信号量:进程间的同步与协调

        信号量是一种用于协调多个进程访问共享资源的机制,它可以实现进程之间的同步与互斥解决上述共享内存中必须一直接收否则就会错过消息,或者重复接收消息的弊端,做到真正的一对一接收,这就是信号量的加入带给共享内存的好处。

1. 概述

        信号量是一种用于解决资源竞争和进程同步的机制,它通过管理一个计数器来控制对资源的访问。那么要想弄明白信号量我们就要从以下几个方面入手:

        临界资源:临界资源是多个进程或线程有可能同时访问的资源,例如变量、链表、文件等。这些资源的同时访问可能导致数据不一致性或者其他问题。在并发编程中,对临界资源的正确管理非常重要,以确保多个进程或线程能够安全地访问和修改这些资源。

        临界区:临界区是指访问临界资源的代码部分在这个区域内,多个进程或线程可能会相互竞争访问临界资源,从而引发竞态条件(Race Condition)。为了避免竞态条件,需要使用同步机制来保护临界区,以确保同一时间只有一个进程或线程可以进入临界区

        P操作:P操作,也称为申请操作或减操作,用于申请资源。在信号量机制中,信号量的值表示可用的资源数量。当一个进程或线程希望访问临界资源时,它执行P操作,该操作会尝试将信号量的值减少。如果资源可用,信号量的值会减少,允许进程或线程访问临界区;如果资源不可用,P操作可能会使进程或线程阻塞,直到资源可用。

        V操作:V操作,也称为释放操作或加操作,用于释放资源。在信号量机制中,执行V操作会将信号量的值增加,表示释放了一个资源。当进程或线程完成对临界资源的访问时,它执行V操作来释放资源,使得其他等待资源的进程或线程有机会进入临界区

        解决资源竞态:资源竞态(Race Condition)是指多个进程或线程同时访问临界资源时可能发生的不可预测行为。为了解决资源竞态,需要使用同步机制来保护临界区,以确保在同一时间只有一个进程或线程可以访问资源。常见的同步机制包括互斥锁、信号量、条件变量等,它们可以确保临界区的互斥访问,从而避免竞态条件的发生,保证数据的一致性和正确性。

2. system V信号量

        我们首先来了解一下,system V信号量的PV操作的核心特征:原子性,也就是说对信号量元素的值的增加和减少,系统保证在 CPU 的电气特性级别上不可分割,这跟整型数据的加减法有本质的区别。

       此外 system V信号量还提供了API函数,如semget()用于创建和访问信号量semop()用于执行P/V操作semctl()用于信号量的控制

                                                        获取信号量ID
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

原型:
        int semget(key_t key, int nsems, int semflg);
参数:
            key              信号量的键值
            nsems         信号量元素的个数
            semflg
                IPC_CREAT      如果 key 对应的信号量不存在,则创建之
                IPC_EXCL         如果该 key 对应的信号量已存在,则报错
                mode                 信号量的访问权限(八进制,如 0644)
返回值:
            成功         该信号量的 ID
            失败         -1

        信号量读写资源进行初始化(初始化读资源和写资源)。

union semun 
{ 
	int val; /* 当 cmd 为 SETVAL 时使用 */ 
	struct semid_ds *buf; /* 当 cmd 为 IPC_STAT 或 IPC_SET 时使用 */ 
	unsigned short *array; /* 当 cmd 为 GETALL 或 SETALL 时使用 */ 
	struct seminfo *__buf; /* 当 cmd 为 IPC_INFO 时使用 */ 
};

//信号量初始化函数
static void seminit(int semid, int semnum, int value)
{
	union semun x;
	x.val = value;
	semctl(semid, semnum, SETVAL, x);
}

                                                信号量属性的获取和设置
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
原型: int semctl(int semid, int semnum, int cmd, ...);
参数:
    semid             信号量 ID
    semnum         信号量元素序号(数组下标)
    cmd
        IPC_STAT          获取属性信息
        IPC_SET           设置属性信息
        IPC_RMID         立即删除该信号量,参数 semnum 将被忽略
        IPC_INFO          获得关于信号量的系统限制值信息
        SEM_INFO        获得系统为共享内存消耗的资源信息
        SEM_STAT        同 IPC_STAT,但 shmid 为该 SEM 在内核中记录所有SEM 信息的数组的下标,因此通过迭代所有的下标可以获得系统中所有 SEM 的相关信息
        GETALL             返回所有信号量元素的值,参数 semnum 将被忽略
        GETNCNT         返回正阻塞在对该信号量元素 P 操作的进程总数
        GETPID             返回最后一个队该信号量元素操作的进程 PID
        GETVAL            返回该信号量元素的值
        GETZCNT         返回正阻塞在对该信号量元素等零操作的进程总数
        SETALL             设置所有信号量元素的值,参数 semnum 将被忽略
        SETVAL            设置该信号量元素的值(资源个数)

        虽然它的参数很多但是别怕,很多我们只是需要了解即可,真正用起来再查表也不迟。

3. P/V操作    

        P操作(等待)和V操作(释放)是信号量的基本操作,P操作会减少信号量值,V操作会增加信号量值。

        信号量操作结构体的定义如下:

struct sembuf
{
    unsigned short sem_num; /* 信号量元素序号(数组下标) */
    short sem_op; /* 操作参数 */
    short sem_flg; /* 操作选项 */
}

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
原型:

        int semop(int semid, struct sembuf sops[ ], unsigned nsops);

参数
            semid         信号量 ID
            sops           信号量操作结构体数组
            nsops         结构体数组元素个数

        P/V操作还是较为简单的,我们只需要看结构体sembuf 中 sem_op 的数值即可:

        sem_op < 0         表示 P 操作  P操作当资源不够的时候,如果设置了IPC_NOWAIT那么不会等待,直接返回,带有错误码;否则会阻塞等待
        sem_op > 0         表示 V 操作  V操作永远不会导致进程阻塞
        sem_op = 0         表示 0 操作

        又巴巴一大堆概念,那么下面就让我们用一个实例来看看信号量如何实现共享内存中输入和显示信息:

send.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/shm.h>
#include <sys/sem.h>

union semun
{
    int val;				/* 当 cmd 为 SETVAL 时使用 */
    struct semid_ds *buf;	/* 当 cmd 为 IPC_STAT 或 IPC_SET 时使用 */
    unsigned short *array; 	/* 当 cmd 为 GETALL 或 SETALL 时使用 */
    struct seminfo *__buf; 	/* 当 cmd 为 IPC_INFO 时使用 */
};

//           信号量ID   资源下标   资源个数
void seminit(int semid, int semnum, int num)
{
	union semun sem;
	sem.val = num;
	semctl(semid, semnum, SETVAL, sem);
}

//P操作
void sem_p(int semid, int semnum)
{
	struct sembuf sops[1];
	sops[0].sem_num = semnum;
	sops[0].sem_op = -1;
	sops[0].sem_flg = 0;
	semop(semid, sops, 1);
}

//V操作
void sem_v(int semid, int semnum)
{
	struct sembuf sops[1];
	sops[0].sem_num = semnum;
	sops[0].sem_op = 1;
	sops[0].sem_flg = 0;
	semop(semid, sops, 1);
}


int main(int argc, char const *argv[])
{
	//获取共享内存键值
	key_t key_shm = ftok("./", 3);
	if (key_shm == -1)
	{
		perror("ftok key_shm");
		return -1;
	}

	//获取共享内存的ID
	int shmid = shmget(key_shm, 4096, IPC_CREAT | 0777);
	if (shmid == -1)
	{
		perror("shmget");
		return -1;
	}

	//对共享内存进行映射
	char *s = (char *)shmat(shmid, NULL, 0);

	//获取信号量键值
	key_t key = ftok("./", 4);
	if (key == -1)
	{
		perror("ftok");
		return -1;
	}

	//获取系统信号量ID
	int semid;
	semid = semget(key, 2, IPC_CREAT | 0644);
	if (semid == -1)	//失败的情况下,不需要退出,直接获取信号量ID
	{
		semid = semget(key, 2, 0644);
	}
	else				//成功的情况下,需要做信号量元素的初始化
	{
		//初始化 读资源下标0   资源个数:0
		seminit(semid, 0, 0);
		//初始化 写资源下标1   资源个数:1
		seminit(semid, 1, 1);
		//第一个参数,信号量ID
		//第二个参数,用来表示读资源和写资源的下标
		//第三个参数,用来表示读资源和写资源的资源个数
	}

	//发送消息
	while(1)
	{
		//申请资源	P操作  写资源-1
		sem_p(semid, 1);

		scanf("%[^\n]", s);
		while(getchar()!='\n');

		//释放资源	v操作  读资源+1
		sem_v(semid, 0);
	}
	return 0;
}

read.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/shm.h>
#include <sys/sem.h>

union semun
{
    int val;				/* 当 cmd 为 SETVAL 时使用 */
    struct semid_ds *buf;	/* 当 cmd 为 IPC_STAT 或 IPC_SET 时使用 */
    unsigned short *array; 	/* 当 cmd 为 GETALL 或 SETALL 时使用 */
    struct seminfo *__buf; 	/* 当 cmd 为 IPC_INFO 时使用 */
};

//           信号量ID   资源下标   资源个数
void seminit(int semid, int semnum, int num)
{
	union semun sem;
	sem.val = num;
	semctl(semid, semnum, SETVAL, sem);
}

//P操作
void sem_p(int semid, int semnum)
{
	struct sembuf sops[1];
	sops[0].sem_num = semnum;
	sops[0].sem_op = -1;
	sops[0].sem_flg = 0;
	semop(semid, sops, 1);
}

//V操作
void sem_v(int semid, int semnum)
{
	struct sembuf sops[1];
	sops[0].sem_num = semnum;
	sops[0].sem_op = 1;
	sops[0].sem_flg = 0;
	semop(semid, sops, 1);
}


int main(int argc, char const *argv[])
{
	//获取共享内存键值
	key_t key_shm = ftok("./", 3);
	if (key_shm == -1)
	{
		perror("ftok key_shm");
		return -1;
	}

	//获取共享内存的ID
	int shmid = shmget(key_shm, 4096, IPC_CREAT | 0777);
	if (shmid == -1)
	{
		perror("shmget");
		return -1;
	}

	//对共享内存进行映射
	char *s = (char *)shmat(shmid, NULL, 0);

	//获取信号量键值
	key_t key = ftok("./", 4);
	if (key == -1)
	{
		perror("ftok");
		return -1;
	}

	//获取系统信号量ID
	int semid;
	semid = semget(key, 2, IPC_CREAT | 0644);
	if (semid == -1)	//失败的情况下,不需要退出,直接获取信号量ID
	{
		semid = semget(key, 2, 0644);
	}
	else				//成功的情况下,需要做信号量元素的初始化
	{
		//初始化 读资源下标0   资源个数:0
		seminit(semid, 0, 0);
		//初始化 写资源下标1   资源个数:1
		seminit(semid, 1, 1);
		//第一个参数,信号量ID
		//第二个参数,用来表示读资源和写资源的下标
		//第三个参数,用来表示读资源和写资源的资源个数
	}

	//接收消息
	while(1)
	{
		//申请资源	P操作  读资源-1
		sem_p(semid, 0);

		printf("共享资源:%s\n", s);

		//释放资源	v操作  写资源+1
		sem_v(semid, 1);
	}

	return 0;
}

        看到这里并且能理解,那么你对进程间通信也就掌握的差不多了,进程间通信其实也不难,关键是你要理解这其中很多抽象的概念。

4. POSIX有名信号量

        POSIX有名信号量通过路径名作为标识符,允许多个进程共享一个信号量。使用sem_open()sem_close()进行打开和关闭,使用sem_wait()sem_post()进行等待和释放操作。

        使用 sem_open( )来创建或者打开一个有名信号量。

#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>

原型:
        sem_t *sem_open(const char *name, int oflag);
        sem_t *sem_open(const char *name, int oflag,mode_t mode, unsigned int value);

        这里其实就是函数的重载,参数不同所调用的函数也不同,但函数名字一样
参数:
    name         信号量的名字,必须以正斜杠 ”/” 开头
    oflag
                O_CREATE         如果该名字对应的信号量不存在,则创建
                O_EXCL              如果该名字对应的信号量已存在,则报错
                mode                   八进制读写权限,比如 0666
                value                   初始值(资源数)
返回值:
            成功         信号量的地址
            失败         SEM_FAILED    

        使用 sem_wait( )进行 P 操作。

#include <semaphore.h>
原型:
        int sem_wait(sem_t *sem);
参数:
    sem         信号量指针
    

示例:

 ret = sem_trywait(s);
 struct timespec timeout;
 clock_gettime(CLOCK_REALTIME, &timeout);
 timeout.tv_sec += 3;
 ret = sem_timedwait(s, &timeout);
 if (ret < 0)
 {
     perror("sem_timedwait");
     // printf("超时\n");
     continue;
 }

        使用 sem_post( )来进行V操作。

 #include <semaphore.h>

原型:
            int sem_post(sem_t *sem);
参数:
            sem         信号量指针


        使用 sem_close( )来关闭它。使用 sem_unlink( )来删除它,并释放系统资源。

#include <semaphore.h>

原型:
        int sem_close(sem_t *sem);
        int sem_unlink(const char *name);
参数:
            sem           信号量指针
            name         信号量名字

        下面是一个简单的例子,当然我们要想演示这几个函数的用法就需要用到线程,但是没关系,讲解线程的博客在这:

        因此我们不能忘了最后编译时加上-lpthread 链接线程库。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <fcntl.h> // 包含对 O_CREAT 的定义
#include <unistd.h>

// 定义有名信号量的名称
#define SEM_NAME "/my_semaphore"

// 全局变量,表示有名信号量
sem_t *semaphore;

// 线程1的任务函数
void *thread1_task(void *arg) {
    printf("线程1:等待信号量...\n");
    sem_wait(semaphore); // 等待信号量

    printf("线程1:进入临界区。\n");
    sleep(5);// 在临界区执行一些操作,这里我们睡5秒,就不写复杂的内容了
    printf("线程1:退出临界区。\n");

    sem_post(semaphore); // 释放信号量
    printf("线程1:释放信号量。\n");

    return NULL;
}

// 线程2的任务函数
void *thread2_task(void *arg) {
    printf("线程2:等待信号量...\n");
    sem_wait(semaphore); // 等待信号量

    printf("线程2:进入临界区。\n");
    sleep(5);// 在临界区执行一些操作,这里我们睡5秒,就不写复杂的内容了
    printf("线程2:退出临界区。\n");

    sem_post(semaphore); // 释放信号量
    printf("线程2:释放信号量。\n");

    return NULL;
}

int main() {
    // 创建有名信号量
    semaphore = sem_open(SEM_NAME, O_CREAT, 0666, 1); // 初始值为1
    if (semaphore == SEM_FAILED) {
        perror("sem_open");
        return -1;
    }

    pthread_t thread1, thread2;
    // 创建线程1和线程2
    pthread_create(&thread1, NULL, thread1_task, NULL);
    pthread_create(&thread2, NULL, thread2_task, NULL);

    // 等待线程1和线程2结束
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    // 关闭并销毁有名信号量
    sem_close(semaphore);
    sem_unlink(SEM_NAME);

    return 0;
}

        这一万多字不容易呢,点个赞支持一下博主吧,谢谢啦~

        更多C/C++语言Linux系统数据结构ARM板实战相关文章,关注专栏:

   手撕C语言

            玩转linux

                    脚踢数据结构

                            系统、网络编程

                                     探索C++

                                             6818(ARM)开发板实战

📢写在最后

  • 今天的分享就到这啦~
  • 觉得博主写的还不错的烦劳 一键三连喔~
  • 🎉🎉🎉感谢关注🎉🎉🎉

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/957182.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023年8月随笔之有顾忌了

1. 回头看 日更坚持了243天。 读《发布&#xff01;设计与部署稳定的分布式系统》终于更新完成 选读《SQL经典实例》也更新完成 读《高性能MySQL&#xff08;第4版&#xff09;》开更&#xff0c;但目前暂缓 读《SQL学习指南&#xff08;第3版&#xff09;》开更并持续更新…

3、QT 的基础控件的使用

一、qFileDialog 文件窗体 Header: #include <QFileDialog> qmake: QT widgets Inherits: QDialog静态函数接口&#xff1a; void Widget::on_pushButton_clicked() {//获取单个文件的路径名QString filename QFileDialog :: getOpenFileName(this, tr("Open Fi…

【taro react】(游戏) ---- 贪吃蛇

1. 预览 2. 实现思路 实现食物类&#xff0c;食物坐标和刷新食物的位置&#xff0c;以及获取食物的坐标点&#xff1b;实现计分面板类&#xff0c;实现吃食物每次的计分以及积累一定程度的等级&#xff0c;实现等级和分数的增加&#xff1b;实现蛇类&#xff0c;蛇类分为蛇头和…

16 Linux之JavaEE定制篇-搭建JavaEE环境

16 Linux之JavaEE定制篇-搭建JavaEE环境 文章目录 16 Linux之JavaEE定制篇-搭建JavaEE环境16.1 概述16.2 安装JDK16.3 安装tomcat16.4 安装idea2020*16.5 安装mysql5.7 学习视频来自于B站【小白入门 通俗易懂】2021韩顺平 一周学会Linux。可能会用到的资料有如下所示&#xff0…

Matlab图像处理-灰度插值法

最近邻法 最近邻法是一种最简单的插值算法&#xff0c;输出像素的值为输入图像中与其最邻近的采样点的像素值。是将(u0,v0)(u_0,v_0)点最近的整数坐标u,v(u,v)点的灰度值取为(u0,v0)(u_0,v_0)点的灰度值。 在(u0,v0)(u_0,v_0)点各相邻像素间灰度变化较小时&#xff0c;这种方…

使用ELK收集解析nginx日志和kibana可视化仪表盘

文章目录 ELK生产环境配置filebeat 配置logstash 配置 kibana仪表盘配置配置nginx转发ES和kibanaELK设置账号和密码 ELK生产环境配置 ELK收集nginx日志有多种方案&#xff0c;一般比较常见的做法是在生产环境服务器搭建filebeat 收集nginx的文件日志并写入到队列&#xff08;k…

图解 STP

网络环路 现在我们的生活已经离不开网络&#xff0c;如果我家断网&#xff0c;我会抱怨这什么破网络&#xff0c;影响到我刷抖音、打游戏&#xff1b;如果公司断网&#xff0c;那老板估计会骂娘&#xff0c;因为会影响到公司正常运转&#xff0c;直接造成经济损失。网络通信中&…

传统分拣弊端明显,AI机器视觉赋能物流行业包裹分类产线数智化升级

随着电子商务的快速发展&#xff0c;物流行业的包裹数量持续增长&#xff0c;给物流企业带来了巨大的运营压力。目前&#xff0c;国内大型物流运转中心已开始采用机器视觉自动化设备&#xff0c;但多数快递公司处于半自动化状态&#xff0c;中小型物流分拣中心目前仍靠人工录入…

iOS系统修复软件 Fix My iPhone for Mac

Fix My iPhone for Mac是一款iOS系统恢复工具。修复您的iPhone卡在Apple徽标&#xff0c;黑屏&#xff0c;冻结屏幕&#xff0c;iTunes更新/还原错误和超过20个iOS 12升级失败。这个macOS桌面应用程序提供快速&#xff0c;即时的解决方案来修复您的iOS系统问题&#xff0c;而不…

基于单片机的遥控器设计

一、项目介绍 随着科技的不断发展&#xff0c;红外遥控器已经成为我们日常生活中普遍使用的一种电子设备。它能够给我们带来便捷和舒适&#xff0c;减少人工操作的繁琐性。然而&#xff0c;在实际应用中&#xff0c;有时候我们可能需要制作一个自己的红外遥控器&#xff0c;以…

【云原生进阶之PaaS中间件】第一章Redis-2.4缓存更新机制

1 缓存和数据库的数据一致性分析 1.1 Redis 中如何保证缓存和数据库双写时的数据一致性&#xff1f; 无论先操作db还是cache&#xff0c;都会有各自的问题&#xff0c;根本原因是cache和db的更新不是一个原子操作&#xff0c;因此总会有不一致的问题。想要彻底解决这种问题必须…

目标检测YOLO算法,先从yolov1开始

学习资源 有一套配套的学习资料&#xff0c;才能让我们的学习事半功倍。 yolov1论文原址&#xff1a;You Only Look Once: Unified, Real-Time Object Detection 代码地址&#xff1a;darknet: Convolutional Neural Networks (github.com) 深度学习经典检测方法 one-stag…

React 18 对 state 进行保留和重置

参考文章 对 state 进行保留和重置 各个组件的 state 是各自独立的。根据组件在 UI 树中的位置&#xff0c;React 可以跟踪哪些 state 属于哪个组件。可以控制在重新渲染过程中何时对 state 进行保留和重置。 UI 树 浏览器使用许多树形结构来为 UI 建立模型。DOM 用于表示 …

3DCAT携手华为,打造XR虚拟仿真实训实时云渲染解决方案

2023年5月8日-9日&#xff0c;以 因聚而生 众志有为 为主题的 华为中国合作伙伴大会2023 在深圳国际会展中心隆重举行。本次大会汇聚了ICT产业界的广大新老伙伴朋友&#xff0c;共同探讨数字化转型的新机遇&#xff0c;共享数字化未来的新成果。 华为中国合作伙伴大会2023现场&…

Python小知识 - 使用Python进行数据分析

使用Python进行数据分析 数据分析简介 数据分析&#xff0c;又称为信息分析&#xff0c;是指对数据进行综合处理、归纳提炼、概括总结的过程&#xff0c;是数据处理的第一步。 数据分析的目的是了解数据的内在规律&#xff0c;为数据挖掘&#xff0c;并应用于商业决策、科学研究…

04ShardingSphere-JDBC垂直分片

1、准备服务器 比如商城项目中&#xff0c;有用户、订单等系统&#xff0c;数据库在设计时用户信息与订单信息在同一表中。这里创建用户服务、订单服务实现数据库的用户信息和订单信息垂直分片 服务器规划&#xff1a;使用docker方式创建如下容器 服务器&#xff1a;容器名se…

P13 VUE 二级menu实现

主要修改以下几个点&#xff1a; CommonAside.vue中 外层便利有孩子节点&#xff0c;关键词key是对应的标签&#xff0c;class动态图表渲染 内层遍历不能再用item&#xff0c;用subitem&#xff0c;遍历该item.childeren&#xff0c;关键词是path&#xff0c; <templat…

java 批量下载将多个文件(minio中存储)压缩成一个zip包

我的需求是将minio中存储的文件按照查询条件查询出来统一压成一个zip包然后下载下来。 思路&#xff1a;针对这个需求&#xff0c;其实可以有多个思路&#xff0c;不过也大同小异&#xff0c;一般都是后端返回流文件前端再处理下载&#xff0c;也有少数是压缩成zip包之后直接给…

【C++设计模式】详解装饰模式

2023年8月31日&#xff0c;周四上午 这是我目前碰到的最难的设计模式..... 非常难以理解而且比较灵活多半&#xff0c;学得贼难受&#xff0c;写得贼费劲..... 2023年8月31日&#xff0c;周四晚上19:48 终于写完了&#xff0c;花了一天的时间来学习装饰模式和写这篇博客。 …

《Kubernetes部署篇:Ubuntu20.04基于二进制安装安装kubeadm、kubelet和kubectl》

一、背景 由于客户网络处于专网环境下&#xff0c; 使用kubeadm工具安装K8S集群&#xff0c;由于无法连通互联网&#xff0c;所有无法使用apt工具安装kubeadm、kubelet、kubectl&#xff0c;当然你也可以使用apt-get工具在一台能够连通互联网环境的服务器上下载kubeadm、kubele…