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


        进程间通信(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         信号量名字

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

【系统编程】线程基础_祐言QAQ的博客-CSDN博客

        因此我们不能忘了最后编译时加上-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)开发板实战

📢写在最后

  • 今天的分享就到这啦~
  • 觉得博主写的还不错的烦劳 一键三连喔~
  • 🎉🎉🎉感谢关注🎉🎉🎉
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

祐言QAQ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值