进程间通信(二)消息队列、共享内存、信号量

news2024/10/24 22:38:03

文章目录

  • 进程间通信
    • System V IPC概述
    • System V IPC 对象的访问
      • 消息队列
        • 示例--使用消息队列实现进程间的通信
      • 共享内存
        • 示例--使用共享内存实现父子进程间的通信(进程同步)
        • 示例--使用进程实现之前的ATM案例(进程互斥)
      • 信号量
        • 示例--利用信号量集实现进程的互斥(ATM案例)
        • 示例--利用信号量实现进程之间的同步(读者和写者)

进程间通信

System V IPC概述

  • Unix系统存在信号、管道、命名管道、信号量和信号灯等基本进程间通讯机制。
  • System V引入了三种高级进程间通信机制
    • 消息队列、共享内存和信号量
  • IPC对象(消息队列、共享内存和信号量)存在于内核而非是文件系统中,由用户控制释放(用户管理IPC对象的生命周期),不像管道的释放由内核控制。也就是说当IPC对象从被创建出来的那一刻起,除非用户主动释放或者重启,要不然IPC对象会一直存在于内核中。
  • IPC对象通过其标识符来引用和访问,所有的IPC对象在内核空间中有唯一性标识ID,在用户空间中的唯一性标识称为key。
  • Linux IPC继承自System V IPC

System V IPC 对象的访问

  • IPC对象是全局对象

    • 使用ipcs查看IPC对象,也可使用ipcs -qipcs -mipcs -s分别查看消息队列、共享内存、信号量,还可以使用ipcrm删除IPC对象。下图中的键和id分别是在用户空间和内核空间中IPC对象的唯一性表示

      image-20241017083801242

  • 每个IPC对象都由后缀为get的函数创建

    • msggetshmgetsemget
    • 调用get函数时必须指定关键字key,系统会根据用户指定的键值来为IPC对象在内核中创建一个唯一性的标识。

消息队列

  • 消息队列是内核中的一个链表
  • 消息队列进行进程间通信的过程:首先要创建一个消息队列,然后由一个进程将消息放到消息队列里边,另外一个消息从消息队列里边将消息读出来。
  • 用户进程将数据(文本或者二进制等数据)传输到内核后,内核重新添加一些如用户ID,组ID,读写进程的ID和优先级等相关信息后并打成一个数据包称为消息。然后内核将这个数据包插入到链表中去,放入到消息队列中。
  • 允许一个或者多个进程往消息队列中写消息和读消息,但一个消息只能被一个进程读取,读取完后内核会自动将这个消息删除。
  • 消息队列具有一定的先进先出的特性(本质上是一个队列),消息可以按照顺序发送到队列中,也可以以几种不同的方式从队列中读取(通过函数msgrcv()中的参数mtype来设置)。每一个消息队列在内核中用一个唯一的IPC标识ID标识。
  • 消息队列的实现包括创建(msgget)和打开队列(msgget),发送消息(msgsnd)、读取消息(msgrcv)和控制消息队列(msgctl)四种操作。

消息队列属性

struct msqid_ds
{
    struct ipc_perm msg_perm;					 /* ipc对象的权限 */
    time_t msg_stime;										/* last msgsnd time */
    time_t msg_rtime;										/* last msgrcv time */
    time_t msg_ctime;										/* lsat change time */
    unsigned short msg_cbytes;					/* current number of bytes on queue */
    unsigned short  msg_qnum;					/* number of message in queue */
    unsigned short msg_qbytes;					/* max number of bytes on queue */
    pid_t msg_lspid;											/* pid of last msgsnd */
    pid_t msg_lrpid;											/* last receive pid */
};

打开或创建消息队列

#include <sys/msg.h>
#include <sys/ipc.h>

int msgget(key_t key, int flag);

/*
	功能:若消息队列不存在则创建消息队列,若消息队列存在则打开消息队列
	参数:key		用户指定的消息队列的键值,还可以通过ftok()函数获取
				flag	IPC_CREAT、IPC_EXCL等权限组合,如果消息队列存在且指定了IPC_EXCL参数,会报错并设置errno为EEXIST
	返回值:成功执行返回内核中消息队列的标识ID,出错返回-1
*/

消息队列控制

#include <sys/msg.h>
#include <sys/ipc.h>

int msgctl(int msgid, int cmd, struct msqid_ds *buf);

/*
	功能:用于控制消息队列的属性(例如获取或设置消息队列的状态信息,删除消息队列等)
	参数:msgid	消息队列的ID
				buf			指向消息队列属性的指针
				cmd			IPC_STAT:获取消息队列的属性,然后将获取到的属性存放到buf所指向的结构体中
								IPC_SET:设置消息队列的属性,按照buf所指向的结构体中的内容设置消息队列的属性
								IPC_RMID:删除队列,从系统中删除该消息队列以及仍在该队列上的所有数据。由于消息队列在读取完成后并不会自动释放,所以需要手动删除。
*/

发送消息

#include <sys/ipc.h>
#include <sys/msg.h>

int msgsnd(int msgqid, const void *ptr, size_t nbytes, int flag);

struct mymesg
{
    long mtype;										/* positive message type */
    char mtext[512];						/* message data,of length nbytes */
};

/*
	功能:向消息队列里边发送消息
	参数:msgqid		消息队列的ID
				ptr		由用户自定义的一个结构体,但第一个成员必须是mtype,结构体用来存放要发送的消息
				mtype是消息的类型,它由一个整数来代表,并且它只能是大于0的整数
				nbytes指定消息的大小,但是不包括myteps的大小,只包含mtext消息数据本身的大小
				flag	0 阻塞 IPC_NOWAIT非阻塞(类似于文件中的O_NOBLOCK标志)
				若消息队列已满(或者是队列中的消息总数等于系统限制值,或队列中的字节数等于系统限制值),指定了IPC_NOWAIT使得msgsnd立即出错返回EAGAIN。如果指定0,则进程阻塞直到
				阻塞直到有空间可以容纳要发送的消息(别的进程将消息队列中读走后消息会被删除直到剩余的空间足够容纳要发送的消息后发送的进程才继续运行)
				或从系统中删除了此队列(如果消息队列被删除,那么它发送的消息也就没意义了)
				或捕捉到一个信号,并从信号处理程序返回(若进程调用signal函数向内核注册了信号和信号处理函数,产生此信号后会从阻塞状态转而去处理信号处理函数)
		返回值:成功执行返回0,出错返回-1	
*/

接收消息

#include <sys/ipc.h>
#include <sys/msg.h>

ssize_t msgrcv(int msgqid, void *ptr, size_t nbytes, long type, int flag);

/*
	功能:获取消息队列中的消息
	参数:msgqid		消息队列的ID
				ptr		指向存放消息的缓存(用户自定义和发送端的结构体一样)
				nbytes		消息缓存的大小,不包括mtype的大小,计算方式为sizeof(struct mymesg)-sizeof(long)
				type		消息类型
							type==0:获取消息队列中第一条消息
							type>0:获取消息队列中类型为type中的第一条消息(类型为type的可能有多条消息,获取它里边的第一条)
							type<0:获取消息队列中小于或等于type绝对值的消息(类型最小的)
				flag		0 阻塞 IPC_NOWAIT f
*/
示例–使用消息队列实现进程间的通信
//msgq_r.c

#include "header.h"

typedef struct
{
	long mtype;
	int start;
	int end;
}MSG;

int main(int argc, char **argv)
{
	if(argc < 3)
	{
		fprintf(stderr,"usage:%s key_value, mtype\n",argv[0]);
		exit(EXIT_FAILURE);
	}

	key_t key;
	MSG msg;
	int msgqid;
	ssize_t nbytes;

	//从外部传参获取键值和要读取消息的类型
	key = atoi(argv[1]);
	msg.mtype = atoi(argv[2]);

	//打开消息队列,这里的键值要和消息发送的键值一样才能操作相同的消息队列
	if((msgqid = msgget(key, S_IRWXU | S_IRWXG | S_IROTH)) < 0)
	{
		perror("msgget error");
		exit(EXIT_FAILURE);
	}
	printf("msgqid:%d\n",msgqid);			//将消息队列的唯一ID打印出来
	
	//读取消息存放在msg结构体中
	nbytes = msgrcv(msgqid, &msg, sizeof(MSG)-sizeof(long), msg.mtype, IPC_NOWAIT);
	//将读取到的消息打印出来,读取到消息的顺序会根据type的值而变化
	printf("msg.mtype:%ld msg.start:%d msg.end:%d\n",msg.mtype, msg.start, msg.end);

	struct msqid_ds buf;
	msgctl(msgqid, IPC_STAT, &buf);			//获取消息队列的属性
	//将获取消息的时间,内核中剩下的消息数,读取消息的进程id打印出来
	printf("last msgrcv time:%s number of messages in queue:%ld\nlast receive pid:%d\n",
													ctime(&buf.msg_rtime),buf.msg_qnum,buf.msg_lrpid);
	if(nbytes < 0)
	{
		perror("msgrcv error");
		//如果内核中已经没有消息后再次读取就会报错,此时将消息队列从内核中移除
		//这里需要注意的是它报错的原因可能是类型输入错误,所以如果判断消息队列中还有消息就不删除消息队列
		//直到消息队列中的消息数0说明读取完成可以将消息队列从内核中移除了
		if(!strcmp(strerror(errno),"No message of desired type") && buf.msg_qnum == 0)
			msgctl(msgqid, IPC_RMID, NULL);

		exit(EXIT_FAILURE);
	}

	return 0;
}
//msgq_w.c

#include "header.h"

typedef struct 
{
	long mtype;
	int start;
	int end;
}MSG;

int main(int argc, char **argv)
{
	if(argc < 2)
	{
		fprintf(stderr,"usage:%s key_value\n",argv[0]);
		exit(EXIT_FAILURE);
	}

	key_t key;
	int msgqid;
	int i;
	struct msqid_ds buf;
	MSG msg[5];
	memset(msg, 0, sizeof(msg));

	key = atoi(argv[1]);			//将外部参数argv[1]赋值给键值
	
	//使用键值创建消息队列,并在内核中生成唯一的队列ID
	//IPC_CREAT | IPC_EXCL如果不存在就创建,如果存在就返回EEXIST
	//S_IRWXU | S_IRWXG | S_IROTH创建的权限为队列的拥有者,同组人拥有可读可写可执行的权限,其他人只有可读的权限								
	if((msgqid = msgget(key, IPC_CREAT | IPC_EXCL | S_IRWXU | S_IRWXG | S_IROTH)) < 0)
	{
		perror("msgget error");
		exit(EXIT_FAILURE);
	}
	printf("msgqid:%d\n",msgqid);			//将消息队列的ID打印出来和内核中的作对比
	
	//对要发送的数据进行初始化
	for(i = 0; i < 5; i++)
	{
		msg[i].mtype = 10 + i;
		msg[i].start = 100 + i;
		msg[i].end = 1000 + i;
	}

	for(i = 0; i < 5; i++)
	{
		//将数据先发送到内核由内核添加一些数据打成一个包发送到消息队列中
		if(msgsnd(msgqid, (msg+i), sizeof(MSG)-sizeof(long), IPC_NOWAIT) < 0)
		{
			perror("msgsnd error");
			exit(EXIT_FAILURE);
		}
		//将数据打印出来
		printf("mtype:%ld start:%d end:%d\n",msg[i].mtype,msg[i].start,msg[i].end);
	}
		
	//获取消息队列中的属性并打印出来
	msgctl(msgqid, IPC_STAT, &buf);
	printf("last msgsnd time:%snumber of message in queue:%ld\npid of last msgsnd:%d\n",
														ctime(&buf.msg_stime), buf.msg_qnum, buf.msg_lspid);

	return 0;	
}	

image-20241017163131810

image-20241017164149552image-20241017164746751

image-20241017164905764

image-20241017165015738

通过编译执行可以发现当调用读取进程去读取消息队列的消息时,若输入的type =0,则读取队列中的第一条消息,并且每读取一次,消息队列中的消息就少一条,当type < 0时,会获取消息队列中小于或等于type绝对值的消息(类型最小的),当type > 0时,会获取消息队列中类型为type中的第一条消息。并且可以看到当消息全部读取完成后,消息队列并不会删除而是仍然会存在于内核中,直到调用msgctl()函数指定IPC_RMID参数或者使用指令ipcrm -q才能够将消息队列从内核中移除

共享内存

  • 共享内存区域是被多个进程共享的一部分物理内存
  • 多个进程都可把该共享内存映射到自己的虚拟空间(之前有讲过系统都是将物理内存映射为虚拟内存,后续用户的操作都是基于虚拟内存,然后通过内存管理单元映射到物理内存上去)。所有的用户空间的进程若想要操作共享内存,都要将其映射到自己的虚拟内存空间去,通过映射的虚拟内存空间地址去操作共享内存, 从而达到进程间通信。
  • 共享内存是进程间数据共享的一种最快的方法,一个进程向共享内存区域写入数据,共享这个内存区域的所有进程尽可以立即看到其中的内容。
  • 提升数据处理效率,一种效率最高的IPC机制
  • 但共享内存作为一种共享资源其本身并不提供同步机制,所以要借助信号量来实现共享内存的同步

共享内存属性

	struct shmid_ds
    {
        struct ipc_perm shm_perm;			/* operation perms*/
        int shm_segsz;							/* size of segment(bytes)*/
        time_t shm_atime;			/*last attach time*/
        time_t shm_dtime;			/*last detach time*/
        time_t shm_ctime;			/*last change time*/
        pid_t shm_cpid;					/*pid of creator*/
        pid_t shm_lpid;					/*pid of last operator*/
    };

共享内存使用步骤

  • 使用shmget函数创建共享内存
  • 使用shmat函数映射共享内存,将这段创建的共享内存映射到具体的进程虚拟内存空间中
  • 向共享内存村写入数据或者从共享内存中读取数据
  • 使用shmdt函数解除共享内存的映射
  • 对共享内存的操作完毕后,使用shmctl函数将共享内存从内核中移除

创建共享内存

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key; size_t size; int shmflg);

/*
	功能:创建共享内存用于进程间通信
	参数:key		由用户指定的共享内存的键值
				size	共享内存的大小
				shmflg		创建共享内存的权限(IPC_CREAT IPC_EXCL等权限组合)
	返回值:如果成功创建,返回内核中共享内存的唯一标识ID。如果失败,返回-1并设置errno
	errno
				EINVAL(无效的内存段大小)
				EEXIST(内存段已经存在,无法创建)
				EIDRM(内存段已经被删除)
				ENOENT(内存段不存在)
				EACCES(权限不够)
				ENOMEM(没有足够的内存来创建内存段)
*/

共享内存控制

#include <sys/ipc.h>
#incldue <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

/*
	功能:用于控制共享内存,例如获取和设置共享内存的属性等
	参数:shmid		共享内存的ID
				buf				共享内存属性指针
				cmd
						IPC_STAT		获取共享内存段属性
						IPC_SET				设置共享内存段属性
						IPC_RMID			删除共享内存段
						SHM_LOCK			锁定共享内存段页面(页面映射到物理内存不和外村进行换入换出操作)
						SHM_UNLOCK		解除共享内存段页面锁定
			当内存空间不足时,就会在外存(硬盘)上开辟一块空间,将暂时不用的数据放到外存上,当需要使用这部分数据的时候再调回内存中,这个操作叫做换入换出操作
*/

共享内存映射和解除映射

#include <sys/ipc.h>
#include <sys/shm.h>

void *shmat(int shmid, char *shmaddr, int shmflg);
int shmdt(char *shmaddr);

/*
		功能:shmat	将共享内存映射到当前进程的虚拟内存空间中
					shmdt	将共享内存从当前进程的虚拟内存空间中删除
					
		参数:shmid	共享内存的ID
					shmaddr		映射到进程虚拟内存空间的地址,建议设为0,由操作系统分配,防止用户自己设置出问题
					shmflg		若shmaddr设置为0,则shmflg也设置为0
									SHM_RND							控制共享内存的地址对齐方式
									SHMLBA							地址为2的乘方
									SHM_RDONLY					只读方式连接
		返回值:成功返回共享内存映射到进程虚拟空间中的地址,失败返回-1并设置errno
		errno
					EINVAL			无效的IPC ID值或者无效的地址
					ENOMEM			没有足够的内存
					EACCES			存取权限不够
		子进程不继承父进程创建的共享内存,大家是共享的。子进程继承父进程映射的地址
*/
示例–使用共享内存实现父子进程间的通信(进程同步)
//tell.c

#include "header.h"

static int pipe_fd[2];

//初始化管道用于控制进程间执行的先后顺序
//通过调用read函数利用管道的机制:若管道中没有数据会一直阻塞直到
//使用write函数向管道中写入数据后才能继续执行
void init_pipe()
{
	if(pipe(pipe_fd) < 0)
	{
		perror("pipe error");
		exit(EXIT_FAILURE);
	}
}

void wait_pipe()
{
	char c;

	if(read(pipe_fd[0], &c, sizeof(char)) < 0)
	{
		perror("read error");
		exit(EXIT_FAILURE);
	}
}

void notify_pipe()
{
	char c = 'x';

	if(write(pipe_fd[1], &c, sizeof(c)) != sizeof(c))
	{
		perror("read error");
		exit(EXIT_FAILURE);
	}
}

void destroy_pipe()
{
	close(pipe_fd[0]);
	close(pipe_fd[1]);
}

//#include "header.h"
#include "tell.h"

#define size 1024

int main(int argc, char **argv)
{
	if(argc < 2)
	{
		fprintf(stderr, "usage:%s key_value\n",argv[0]);
		exit(EXIT_FAILURE);
	}
	
	key_t key;
	int shmid;
	pid_t pid;

	//创建管道
	init_pipe();

	//将外部传参赋值给键值
	key = atoi(argv[1]);

	//创建共享内存,共性内存的权限为0774
	if((shmid = shmget(shmid, size, IPC_CREAT | IPC_EXCL | S_IRWXU | S_IRWXG | S_IROTH)) < 0)
	{
		perror("shmget error");
		exit(EXIT_FAILURE);
	}
	printf("shmid:%d\n",shmid);
	
	if((pid = fork()) < 0)
	{
		perror("fork error");
		exit(EXIT_FAILURE);
	}
	else if(pid > 0)		//parent process
	{
		//将共享内存映射到当前进程的虚拟空间
		int *shmaddr = (int*)shmat(shmid, 0, 0);
		if(shmaddr == (int*)-1)
		{
			perror("shmat error");
		}
		
		//获取共享内存的属性
		struct shmid_ds ds;
		shmctl(shmid, IPC_STAT, &ds);
		printf("creator pid:%d last detach time:%slast change time:%s last attach time:%s\n",
								ds.shm_cpid,ctime(&ds.shm_dtime),ctime(&ds.shm_ctime),ctime(&ds.shm_atime));

		//将数据放入到共享内存中
		*shmaddr = 100; *(shmaddr+1) = 200;

		//操作完毕后解除共享内存的映射
		shmdt(shmaddr);

		//唤醒子进程让其继续执行
		notify_pipe();

		//关闭管道
		destroy_pipe();

		//等待子进程退出并回收它的资源
		wait(NULL);
	}
	else					//child process
	{
		//阻塞子进程等待父进程往共享内存中写入数据后再执行
		wait_pipe();

		int *shmaddr = (int *)shmat(shmid, 0, 0);
		if(shmaddr == (int*)-1)
		{
			perror("shmat error");
		}

		//获取父进程写入的数据
		printf("start:%d end:%d\n",*shmaddr,*(shmaddr+1));

		//解除共享内存的映射
		shmdt(shmaddr);

		//获取共享内存的属性
		struct shmid_ds ds;
		shmctl(shmid, IPC_STAT, &ds);
		printf("creator pid:%d last detach time:%slast change time:%s last attach time:%s\n",
								ds.shm_cpid,ctime(&ds.shm_dtime),ctime(&ds.shm_ctime),ctime(&ds.shm_atime));

		//关闭管道
		destroy_pipe();

		//全部操作完成后将共享内存从内核中移除
		shmctl(shmid, IPC_RMID, NULL);
	}


	return 0;
}

image-20241022113232991

image-20241022113431531

通过编译执行可以看出通过共享内存可以实现进程间的通信,并且可以看到父子进程操作的是同一个共享内存。和消息队列一样,共享内存在读取完后也不会自己销毁,它会一直存在与内核中,所以要调用shmctl函数或者ipcrm -m 共享内存ID将其从内核中移除。

示例–使用进程实现之前的ATM案例(进程互斥)
//account.c

#include "account.h"
#include "header.h"

double withdrawal(Account *a, double amount)
{
	assert(a != NULL);

	if(amount <= 0 || amount > a->balance)
	{
		return 0.0;
	}

	double balance = a->balance;
	sleep(1);		//模拟ATM机延迟
	balance -= amount;
	a->balance = balance;		//将余额balance取出amount后再存放回a账户                           
	return amount;
}

double deposit(Account *a, double amount)
{
	assert(a != NULL);
	
	if(amount <= 0)
	{
		return 0.0;
	}
	double balance = a->balance;
	sleep(1);
	balance += amount;
	a->balance = balance;

	return amount;
}

double get_balance(Account *a)
{
	assert(a != NULL);
	double balance = a->balance;
	return balance;
}
//account_test.c

#include "header.h"
#include "account.h"

int main(int argc, char **argv)
{
	if(argc < 2)
	{
		fprintf(stderr, "%s|%s|%d error! usage:%s key_value\n",__FILE__,__func__,__LINE__,argv[0]);
		exit(EXIT_FAILURE);
	}

	key_t key;
	int shmid;
	pid_t pid;

	//从外部获取键值
	key = atoi(argv[1]);
	//创建共享内存用来进程间通信
	if((shmid = shmget(key, sizeof(Account), IPC_CREAT | IPC_EXCL | S_IRWXU | S_IRWXG | S_IROTH)) < 0)
	{
		perror("shmget error");
		exit(EXIT_FAILURE);
	}

	//将共享内存映射到当前进程的虚拟空间中
	Account *a = (Account*)shmat(shmid, 0, 0);
	if(a == (Account*)-1)
	{
		perror("shmat error");
	}
	a->acc_num = 100001;
	a->balance = 10000;

	//创建子进程用于模拟两个用户去操作银行帐户
	if((pid = fork()) < 0)
	{
		perror("fork error");
		exit(EXIT_FAILURE);
	}
	else if(pid > 0)			//parent process
	{
		double amount = withdrawal(a, 10000);

		printf("pid:%d operate the num:%d balance:%f get the money:%f\n",getpid(),a->acc_num,a->balance,amount);

		//将共享内存从当前进程中解除映射
		shmdt(a);
		//等待子进程退出并回收其资源
		wait(NULL);
	}
	else						//child process
	{
		double amount = withdrawal(a, 10000);

		printf("pid:%d operate the num:%d balance:%f get the money:%f\n",getpid(),a->acc_num,a->balance,amount);
		//将共享内存从当前进程中解除映射
		shmdt(a);
		//将共享内存从内核中移除
		shmctl(shmid, IPC_RMID, NULL);
	}
	
	return 0;
}

image-20241022151147095

通过编译执行可以发现两个进程都拿到了钱,且最后账户余额为0。原因是在代码中没有加任何的互斥操作,所以导致它两个进程都能够取到钱,要实现进程之间的互斥要通过信号量来操作,这个代码后续修改。

信号量

这里的信号量指的是进程信号量而不是前边的线程信号量,通过进程信号量能够实现进程之间的数据传输,它也属于IPC对象的一种

  • 本质和线程信号量类似,信号量指的就是共享资源的数目,拥有控制对共享资源的访问,通过进程信号量能够实现进程之间的同步和互斥
  • 每种共享资源对应一个信号量,在进程信号量中引入了一个**信号量集(包含若干个信号量)**方便当操作大量共享资源时的同步和互斥问题。对信号量集中所有操作可以要求全部成功,也可以要求部分成功。
  • 对信号量集的操作实际上就是**P(减)V(加)**操作

信号量集属性

struct semid_ds
{
    struct ipc_perm sem_perm				/*有关信号量集操作的权限*/
    unsigned short sem_nsems;				/*信号量集中信号量的个数*/
    time_t  sem_otime;								/*最后一次操作信号量集的时间*/
    time_t 	 sem_ctime;								/*最后一次信号量集改变的时间*/
};

信号量集的创建

#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>

int semget(key_t key, int nsems, int flag);

/*
	功能:创建信号量集
	参数:key	用户指定的信号量集的键值,可通过IPC_PRIVATE指定或者通过ftok函数获取
				nsems	信号量集中信号量的个数
				flag	创建信号量指定的权限,例如IPC_CREAT IPC_EXCL S_IRWXU 等权限组合
	返回值:成功执行返回信号量集在内核中的ID,失败返回-1
*/

信号量集控制

#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>

int semctl(int semid, int semnum, int cmd, .../*union semun arg*/);

/*
	功能:设置/获取信号量集的属性,销毁信号量集等
	参数:semid	信号量集的id
				semnum		要操作的信号量集中的哪一个信号量 例如:0表示对所有的信号量操作,1表示操作信号量集中的第二个信号量,信号量的标号从0开始
				cmd		IPC_STAT 获取信号量集的属性							--->buf
							IPC_SET		设置信号量集的属性							--->buf
							IPC_RMID	删除信号量集										--->buf
							GETVAL			返回信号量的值									--->val
							SETVAL			设置semnum信号量的值					--->val
							GETALL			获取所有信号量的值						--->array
							SETALL			设置所有信号量的初始值					--->array
--------------------------------------------------------------------
由于信号量集中包含若干个信号量,所以对它的属性、初始值都封装在一个联合体中,但是联合体有一个特性就是它们的地址是共用的,也就是说同一时间只能有一个成员去使用。所以上边的前三个指令使用的联合体成员是buf,中间两个使用的成员是val,最后两个使用的成员是array,通过获取或设置联合体中的成员就可以控制(初始化、销毁、获取属性)信号量集
			union semun
			{
					int val;					//放置获取或设置信号量集中某个信号量的值
					struct semid_ds *buf;		//用来存储信号量集的属性
					unsigned short *array;		//放置获取或设置所有信号量的值
			};
	返回值:成功执行返回0,出错返回-1
*/

信号量操作

#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>

int semop(int semid, struct sembuf *sops, size_t nsops);

/*
	功能:用于信号量集中信号量的加和减操作(PV操作),用于进程间的同步和互斥
	参数:semid	信号量集的ID
				sops		sembuf结构体数组指针
				nsops		数组中元素的个数
---------------------------------------------------------------------
				struct sembuf
				{
						unsigned short semnum;				
						short sem_op;									
						short sem_flg;									
				};
				semnum		信号量集中信号量的编号(要对哪个信号量操作)
				sem_op		正数为V操作,负数为P操作,0可用于对共享资源是否已用完的测试
				sem_flg		IPC_NOWAIT为非阻塞模式,若指定SME_UNDO,当进程意外终止,系统会自动撤销对该信号量的操作,将信号量恢复到之前的状态,防止造成
*/
示例–利用信号量集实现进程的互斥(ATM案例)
//pv.c

#include "header.h"

union semun
{
	int val;
	struct semid_ds *buf;
	unsigned short *array;
};

//创建信号量集并对里边信号量的值进行初始化
int I(int nsems, int value)
{
	/*
	 * 创建信号量集,信号量集中的信号量个数为nsems
	 * 权限为IPC_CREAT | IPC_EXCL | 0774
	 */ 
	int semid = semget(IPC_PRIVATE, nsems, IPC_CREAT | IPC_EXCL | 0774);
	if(semid < 0)
	{
		perror("semid error");
		return -1;
	}

	//对信号量集中的信号量进行赋值
	//在堆空间上开辟创建数组赋值,然后赋值给联合体中的array
	//然后使用semctl函数给信号量集中的信号量进行赋值
	union semun un;

	unsigned short *array = (unsigned short*)malloc(sizeof(unsigned short) * nsems);
	int i;
	for(i = 0; i < nsems; i++)
	{
		array[i] = value;
	}
	un.array = array;

	/*
	 *设置信号量集中的信号量 0表示要对信号量集中的所有信号量进行操作,执行SETALL flag
	 *然后以un联合体中的array对所有的信号量进行赋值
	 */
	if(semctl(semid, 0, SETALL, un) < 0)
	{
		perror("semctl error");
	}
	free(array);			//释放堆空间
	return semid;
}

//对信号量集(semid)中的某一个信号量(semnum)作P操作(value)
void P(int semid, int semnum, int value)
{
	assert(value >= 0);

	/*
	 *定义一个结构体数组,里边包含了要对信号量集中的哪个信号量作P操作
	 *指定SEM_UNDO flag表示如果进程意外退出,程序会取消对信号量的操作,恢复到信号量的上一个状态
	 */
	struct sembuf semops[] = {{semnum, -value, SEM_UNDO}};
	if(semop(semid, semops, sizeof(semops)/sizeof(semops[0])) < 0)
	{
		perror("semop error");
	}
}

//对信号量集(semid)中的某一个信号量(semnum)作V操作(value)
void V(int semid, int semnum, int value)
{
	assert(value >= 0);

	/*
	 *定义一个结构体数组,里边包含了要对信号量集中的哪个信号量作V操作
	 *指定SEM_UNDO flag表示如果进程意外退出,程序会取消对信号量的操作,恢复到信号量的上一个状态
	 */
	struct sembuf semops[] = {{semnum, value, SEM_UNDO}};
	if(semop(semid, semops, sizeof(semops)/sizeof(semops[0])) < 0)
	{
		perror("semop error");
	}
}

//销毁semid指定的信号量集
void D(int semid)
{
	if(semctl(semid, 0, IPC_RMID, NULL) < 0)
	{
		perror("semctl error");
	}
}
//account.c

#include "account.h"
#include "header.h"
#include "pv.h"

double withdrawal(Account *a, double amount)
{
	assert(a != NULL);
	
	//P(1)操作,对信号量作减一操作,信号量的值变为0
	//另外一个进程进来后就会被阻塞,直到当前这个进程作V(1)操作
	P(a->semid, 0, 1);
	if(amount <= 0 || amount > a->balance)
	{
		//V(1)操作,对信号量作加一操作,信号量的值变为1
		V(a->semid, 0, 1);
		return 0.0;
	}

	double balance = a->balance;
	sleep(1);		//模拟ATM机延迟
	balance -= amount;
	a->balance = balance;		//将余额balance取出amount后再存放回a账户                           

	//V(1)操作,对信号量作加一操作,信号量的值变为1
	V(a->semid, 0, 1);

	return amount;
}

double deposit(Account *a, double amount)
{
	assert(a != NULL);
	
	//P(1)操作,对信号量作减一操作,信号量的值变为0
	//操作编号为0的信号量,步长为1
	P(a->semid, 0, 1);

	if(amount <= 0)
	{
		//V(1)操作,对信号量作加一操作,信号量的值变为1
		V(a->semid, 0, 1);
		return 0.0;
	}
	double balance = a->balance;
	sleep(1);
	balance += amount;
	a->balance = balance;

	//V(1)操作,对信号量作加一操作,信号量的值变为1
	V(a->semid, 0, 1);

	return amount;
}

double get_balance(Account *a)
{
	assert(a != NULL);

	//P(1)操作,对信号量作减一操作,信号量的值变为0
	//操作编号为0的信号量,步长为1
	P(a->semid, 0, 1);

	double balance = a->balance;
	
	//V(1)操作,对信号量作加一操作,信号量的值变为1
	V(a->semid, 0, 1);

	return balance;
}
//account_test.c

#include "header.h"
#include "account.h"
#include "pv.h"

int main(int argc, char **argv)
{
	if(argc < 2)
	{
		fprintf(stderr, "%s|%s|%d error! usage:%s key_value\n",__FILE__,__func__,__LINE__,argv[0]);
		exit(EXIT_FAILURE);
	}

	key_t key;
	int shmid;
	pid_t pid;

	//从外部获取键值
	key = atoi(argv[1]);
	//创建共享内存用来进程间通信
	if((shmid = shmget(key, sizeof(Account), IPC_CREAT | IPC_EXCL | S_IRWXU | S_IRWXG | S_IROTH)) < 0)
	{
		perror("shmget error");
		exit(EXIT_FAILURE);
	}

	//将共享内存映射到当前进程的虚拟空间中
	Account *a = (Account*)shmat(shmid, 0, 0);
	if(a == (Account*)-1)
	{
		perror("shmat error");
	}
	a->acc_num = 100001;
	a->balance = 10000;

	//创建信号量个数为1,初始值为1的信号量集
	a->semid = I(1,1);

	//创建子进程用于模拟两个用户去操作银行帐户
	if((pid = fork()) < 0)
	{
		perror("fork error");
		exit(EXIT_FAILURE);
	}
	else if(pid > 0)			//parent process
	{
		double amount = withdrawal(a, 10000);

		printf("pid:%d operate the num:%d balance:%f get the money:%f\n",getpid(),a->acc_num,a->balance,amount);
		
		//等待子进程退出并回收其资源
		wait(NULL);
		//将信号量集从内核中移除
		D(a->semid);
		//将共享内存从当前进程中解除映射
		shmdt(a);
		//将共享内存从内核中移除
		shmctl(shmid, IPC_RMID, NULL);
	}
	else						//child process
	{
		double amount = withdrawal(a, 10000);

		printf("pid:%d operate the num:%d balance:%f get the money:%f\n",getpid(),a->acc_num,a->balance,amount);

		//将共享内存从当前进程中解除映射
		shmdt(a);
	}
	
	return 0;
}

image-20241024205932311

通过编译执行可以发现通信进程信号量实现了进程之间的互斥,由于共享内存没有同步的机制,所以要借助信号量集才能够实现对共享资源的访问。

示例–利用信号量实现进程之间的同步(读者和写者)
#include "header.h"

typedef struct
{
	int val;
	int semid;
}Storage;

void init_sem(Storage *s)
{
	assert(s != NULL);
	//创建信号量集,信号量的个数为2,权限为IPC_CREAT | IPC_EXCL | 0774

	if((s->semid = semget(IPC_PRIVATE, 2, IPC_CREAT | IPC_EXCL | 0774)) < 0)
	{
		perror("semget error");
		exit(EXIT_FAILURE);
	}

	//对信号量集中的信号量初值进行初始化
	union semun
	{
		int val;
		struct semid_ds *buf;
		unsigned short *array;
	};

	union semun un;

	unsigned short array[2] = {0, 0};
	un.array = array;

	//使用semctl函数给信号量赋初值
	//参数1表示信号量集的id,参数2表示要对信号量集中所有的信号量进行设置
	//参数3指定SETALL cmd设置所有的信号量,参数4里边包含信号量要设置的初值
	if(semctl(s->semid, 0, SETALL, un) < 0)
	{
		perror("semctl error");
		exit(EXIT_FAILURE);
	}
}

void write_func(Storage *s, int value)
{
	assert(s != NULL);

	s->val = value;
	printf("write process:%d write %3d\n",getpid(),s->val);

	//要实现两个进程之间的读者和写者问题要借助两个信号量来控制,写者写完通知读者,读者写完通知写者
	//这里的semops_v中的0表示信号量集中的第1个信号量,1表示对此信号量作加1操作V(1)
	//这里的semops_p中的1表示信号量集中的第2个信号量,-1表示对此信号量作减1操作P(1)
	//指定SEM_UNDO cmd表示若进程异常退出,不执行此次信号量的操作,返回到信号量的上一个状态
	struct sembuf semops_v[1] = {{0, 1, SEM_UNDO}};
	struct sembuf semops_p[1] = {{1, -1, SEM_UNDO}};

	//V(1)操作,写者进程写完后通知读者进程读取,所以要将信号量的值作V(1)操作使得读者进程能够继续执行
	if(semop(s->semid, semops_v, sizeof(semops_v)/sizeof(semops_v[0])) < 0)
	{
		perror("semop error");
		exit(EXIT_FAILURE);
	}

	//P(1)操作,写者进程作V(1)操作唤醒读者进程后,自己要调用P(1)操作将自己阻塞直到读者进程作V(1)操作
	//表示读者进程已经读取完毕,写者进程继续写入
	if(semop(s->semid, semops_p, sizeof(semops_p)/sizeof(semops_p[0])) < 0)
	{
		perror("semop error");
		exit(EXIT_FAILURE);
	}
}

void read_func(Storage *s)
{
	assert(s != NULL);

	struct sembuf semops_p[1] = {{0, -1, SEM_UNDO}};
	struct sembuf semops_v[1] = {{1, 1, SEM_UNDO}};

	//读者进程作P(1)操作,等待写者进程写完后通知读者进程读取
	if(semop(s->semid, semops_p, sizeof(semops_p)/sizeof(semops_p[0])) < 0)
	{
		perror("semop error");
		exit(EXIT_FAILURE);
	}

	//读者进程读取数据
	printf("read process:%d read:%5d\n",getpid(),s->val);

	//读者进程读取完毕后给写者进程作V(1)操作表示读者进程已经读取完毕,写者进程可以继续写入
	if(semop(s->semid, semops_v, sizeof(semops_v)/sizeof(semops_v[0])) < 0)
	{
		perror("semop error");
		exit(EXIT_FAILURE);
	}
}

void destroy_sem(Storage *s)
{
	assert(s != NULL);

	//指定IPC_RMID cmd表示要将信号量集从内核中移除
	if(semctl(s->semid, 0, IPC_RMID, NULL) < 0)
	{
		perror("semctl error");
		exit(EXIT_FAILURE);
	}
}

int main(void)
{
	//创建共享内存,共享内存的大小为Storage的大小
	int shmid = shmget(IPC_PRIVATE, sizeof(Storage), IPC_CREAT | IPC_EXCL | 0774);
	if(shmid < 0)
	{
		perror("shmget error");
		exit(EXIT_FAILURE);
	}

	//将共享内存映射到当前进程的虚拟空间中
	Storage *s = (Storage*)shmat(shmid, 0, 0);

	//初始化信号量集
	init_sem(s);

	if(s == (Storage*)-1)
	{
		perror("shmat error");
		exit(EXIT_FAILURE);
	}

	pid_t pid;
	if((pid = fork()) < 0)
	{
		perror("fork error");
		exit(EXIT_FAILURE);
	}
	else if(pid > 0)		//父进程作写者进程
	{
		int i = 1;
		for(; i <= 100; i++)
		{
			write_func(s, i);
		}
		wait(NULL);			//等待子进程退出并回收其资源	
		shmdt(s);			//解除共享内存的映射
		shmctl(shmid, IPC_RMID, NULL);			//将共享内存从内核中移除
	}
	else					//子进程作读者进程
	{
		int i = 1;
		for(; i <= 100; i++)
		{
			read_func(s);
		}
		//子进程读取完毕后销毁信号量集
		destroy_sem(s);
		//解除共享内存的映射
		shmdt(s);
	}

	return 0;
}

image-20241024210532618

通过进程信号量来控制读者和写者进程之间的同步,写者写完通知读者读取,读者读完通知写者写入,以此来实现两个进程的交替运行。

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

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

相关文章

上传Gitee仓库流程图

推荐一个流程图工具 登录 | ProcessOnProcessOn是一个在线协作绘图平台&#xff0c;为用户提供强大、易用的作图工具&#xff01;支持在线创作流程图、思维导图、组织结构图、网络拓扑图、BPMN、UML图、UI界面原型设计、iOS界面原型设计等。同时依托于互联网实现了人与人之间的…

大厂常问iOS面试题–Runloop篇

大厂常问iOS面试题–Runloop篇 一.RunLoop概念 RunLoop顾名思义就是可以一直循环(loop)运行(run)的机制。这种机制通常称为“消息循环机制” NSRunLoop和CFRunLoopRef就是实现“消息循环机制”的对象。其实NSRunLoop本质是由CFRunLoopRef封装的&#xff0c;提供了面向对象的AP…

6个RAG进阶优化方案,对应14篇论文案例解析

本文对近几月我了解到的RAG优化策略进行总结, 每个优化策略都有相应的研究论文作为支撑。在01先总结优化方向, 02细化说明相应论文 在介绍RAG优化策略之前, 先说说知识库数据处理: 增强数据粒度&#xff1a;旨在提升文本标准化、一致性、事实准确性和丰富的上下文&#xff0c…

Acrel-1000变电站综合自动化系统及微机在化工企业中的应用方案

文&#xff1a;安科瑞郑桐 摘要&#xff1a;大型化工企业供配电具有的集约型特点&#xff0c;化工企业内35kV变电站和10kV变电所数量大、分布广&#xff0c;对于老的大多大型及中型化工企业而言&#xff0c;其变电站或变电所内高压电气设备为旧式继电保护装置&#xff0c;可靠…

详解Java的类文件结构(.class文件的结构)

this_class 指向常量池中索引为 2 的 CONSTANT_Class_info。super_class 指向常量池中索引为 3 的 CONSTANT_Class_info。由于没有接口&#xff0c;所以 interfaces 的信息为空。 对应 class 文件中的位置如下图所示。 06、字段表 一个类中定义的字段会被存储在字段表&#x…

zotero文献管理学习

1 zotero软件简介 zotero是一款开源的文献管理软件。如果你听说或使用过EndNote&#xff0c;那么可能会对“文献管理”有一定的概念。可以简单地这样理解&#xff1a;zotero一定程度上可以作为EndNote的平替。 EndNote需要注册付费&#xff0c;对于无专业科研机构隶属关系的企…

MATLAB运动目标检测系统

应用背景 运动目标的定位跟踪&#xff0c;检测识别&#xff0c;运动分析在图像压缩、运动分析、交通检测&#xff0c;智能监控等方面有主要的应用。 首先&#xff0c;在图像压缩中&#xff0c;运动目标检测技术可以在背景区域中将前景区域提取分割出来&#xff0c;只传递部分…

植物端粒到端粒(T2T)基因组研究进展与展望

鼠鼠跳槽了&#xff0c;因为现在公司发(bu)展(zhang)受(gong)限(zi)&#xff0c;只能跳一次&#xff0c;从大兴到昌平了。从二代ivd行业去三代T2T和泛基因组了。在这里我们分享一篇文章。 摘要&#xff1a;高质量的参考基因组是基因组学研究的基础。目前&#xff0c;大多数的参…

笨蛋学习FreeMarker

笨蛋学习FreeMarker FreeMarker参考网址创建实例引入Maven创建工具类创建实例并进行输出 FreeMarker数据类型布尔型&#xff1a;日期型&#xff1a;数值型&#xff1a;字符型&#xff1a;需要处理字符串为null的情况&#xff0c;否则会报错字符串为空不会报错cap_firstuncap_fi…

【银河麒麟高级服务器操作系统实例】金融行业TCP连接数猛增场景的系统优化

了解更多银河麒麟操作系统全新产品&#xff0c;请点击访问 麒麟软件产品专区&#xff1a;https://product.kylinos.cn 开发者专区&#xff1a;https://developer.kylinos.cn 文档中心&#xff1a;https://documentkylinos.cn 服务器环境以及配置 物理机/虚拟机/云/容器 物理…

12 django管理系统 - 注册与登录 - 登录

为了演示方便&#xff0c;我就直接使用models里的Admin来演示&#xff0c;不再创建用户模型了。 ok&#xff0c;先做基础配置 首先是在base.html中&#xff0c;新增登录和注册的入口 <ul class"nav navbar-nav navbar-right"><li><a href"/ac…

使用 VSCode 通过 Remote-SSH 连接远程服务器详细教程

使用 VSCode 通过 Remote-SSH 连接远程服务器详细教程 在日常开发中&#xff0c;许多开发者需要远程连接服务器进行代码编辑和调试。Visual Studio Code&#xff08;VSCode&#xff09;提供了一个非常强大的扩展——Remote-SSH&#xff0c;它允许我们通过 SSH 协议直接连接远程…

一图读懂“低空经济”

&#x1f482; 个人主页: 同学来啦&#x1f91f; 版权: 本文由【同学来啦】原创、在CSDN首发、需要转载请联系博主 &#x1f4ac; 如果文章对你有帮助&#xff0c;欢迎关注、点赞、收藏和订阅专栏哦 文章目录 ✈️ 一、低空经济简介&#x1f534; 1、基本含义&#x1f7e0; 2、…

【免费领取】基于javaweb实现的的日志管理系统

主营内容&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app等设计与开发。 收藏点赞不迷路 关注作者有好处 文末获取源码 项目描述 本工作日志管理系统是一个面向中小企业的简单的工作管理系统&#xff0c;它主要实现公…

【Python】Pandas基础操作手册(上)

哈喽&#xff0c;哈喽&#xff0c;大家好~ 我是你们的老朋友&#xff1a;保护小周ღ 今天给大家带来的是【Python】Pandas基础操作手册&#xff08;上&#xff09;本次主要讲解, python pandas 模块的一些基本概念, 以及了解 Dataframe 对象的创建, 赋值, 保存. 一起来看看叭…

【SpringBoot】17 多文件上传(Thymeleaf + MySQL)

Git仓库 https://gitee.com/Lin_DH/system 文件上传 可参考上一篇【SpringBoot】16 文件上传&#xff08;Thymeleaf MySQL&#xff09; https://blog.csdn.net/weixin_44088274/article/details/143004298 介绍 文件上传是指将本地的图片、视频、音频等文件上传到服务器&…

php生成PDF文件(FPDF)

FPDF即“Free PDF”&#xff0c;FPDF类库提供了基本的PDF创建功能&#xff0c;其源代码和使用权是免费的。 PDF格式文档优势 通用&#xff1a;PDF文档在UNIX和Windows系统均可正常使用。 安全&#xff1a;PDF文档可设置为只读模式&#xff0c;并且可以添加密码等保护措施。 美…

【PDF文件】默认被某种软件打开,如何进行修改?

当有时下载某种软件后&#xff0c;电脑中的PDF文件就默认由该种软件打开&#xff0c;每次需要右键选择打开方式才能选择需要的其他软件打开。如下图所示。 修改方法&#xff1a; &#xff08;1&#xff09;点击电脑的“设置”&#xff0c;选择应用 &#xff08;2&#xff09;…

深入浅出理解BLE AUDIO CSIS

CSIS是Coordinate Sets Identification service,翻译过来就是协调集识别服务。什么是协调集&#xff0c;可以理解为具有相同特征的一伙设备&#xff0c;最典型的就是左右两个蓝牙耳机是一个协调集&#xff0c;所以它们具有相同的协调集标志&#xff0c;但是具有相同协调集的设备…

disql 全量备份SQL脚本DM7/DM8

disql 全量备份SQL脚本DM7/DM8 环境介绍1 全量备份前准备工作2 全量备份2.1 cd 到数据库bin 目录,并编辑文件2.2 编写数据库全量备份SQL 脚本2.3 执行编写的sql脚本2.4 编写Linux定时任务 , 每月执行全量备份 3 备份还原4 增量备份配置5 更多达梦数据库学习使用列表 环境介绍 …