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

文章目录

进程间通信

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对象的唯一性表示

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

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

消息队列

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

消息队列属性

c 复制代码
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 */
};

打开或创建消息队列

c 复制代码
#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
*/

消息队列控制

c 复制代码
#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:删除队列,从系统中删除该消息队列以及仍在该队列上的所有数据。由于消息队列在读取完成后并不会自动释放,所以需要手动删除。
*/

发送消息

c 复制代码
#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	
*/

接收消息

c 复制代码
#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
*/
示例--使用消息队列实现进程间的通信
c 复制代码
//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;
}
c 复制代码
//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;	
}	

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

共享内存

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

共享内存属性

c 复制代码
	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函数将共享内存从内核中移除

创建共享内存

c 复制代码
#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(没有足够的内存来创建内存段)
*/

共享内存控制

c 复制代码
#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		解除共享内存段页面锁定
			当内存空间不足时,就会在外存(硬盘)上开辟一块空间,将暂时不用的数据放到外存上,当需要使用这部分数据的时候再调回内存中,这个操作叫做换入换出操作
*/

共享内存映射和解除映射

c 复制代码
#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			存取权限不够
		子进程不继承父进程创建的共享内存,大家是共享的。子进程继承父进程映射的地址
*/
示例--使用共享内存实现父子进程间的通信(进程同步)
c 复制代码
//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]);
}
c 复制代码
//#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;
}

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

示例--使用进程实现之前的ATM案例(进程互斥)
c 复制代码
//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;
}
c 复制代码
//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;
}

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

信号量

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

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

信号量集属性

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

信号量集的创建

c 复制代码
#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
*/

信号量集控制

c 复制代码
#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
*/

信号量操作

c 复制代码
#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案例)
c 复制代码
//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");
	}
}
c 复制代码
//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;
}
c 复制代码
//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;
}

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

示例--利用信号量实现进程之间的同步(读者和写者)
c 复制代码
#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;
}

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

相关推荐
HLC++5 小时前
Linux的进程间通信
android·linux·服务器
华清远见IT开放实验室6 小时前
实验室建设案例 | 石家庄科技信息职业学院嵌入式实验室——从底层硬件到系统应用,一所应用型高校的嵌入式人才培养这样落地
linux·arm开发·stm32·嵌入式硬件·高校·实验室建设
groundhappy9 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python
通信小小昕10 小时前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
张小姐的猫11 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
栩栩云生11 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
imc.1112 小时前
linux基础IO
linux·运维·服务器
BelongPanda14 小时前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
酷可达拉斯14 小时前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_7779983415 小时前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器