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

文章目录

  • 进程间通信
    • 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	消息队列的IDbuf			指向消息队列属性的指针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		消息队列的IDptr		由用户自定义的一个结构体,但第一个成员必须是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		消息队列的IDptr		指向存放消息的缓存(用户自定义和发送端的结构体一样)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并设置errnoerrnoEINVAL(无效的内存段大小)EEXIST(内存段已经存在,无法创建)EIDRM(内存段已经被删除)ENOENT(内存段不存在)EACCES(权限不够)ENOMEM(没有足够的内存来创建内存段)
*/

共享内存控制

#include <sys/ipc.h>
#incldue <sys/shm.h>int shmctl(int shmid, int cmd, struct shmid_ds *buf);/*功能:用于控制共享内存,例如获取和设置共享内存的属性等参数:shmid		共享内存的IDbuf				共享内存属性指针cmdIPC_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	共享内存的IDshmaddr		映射到进程虚拟内存空间的地址,建议设为0,由操作系统分配,防止用户自己设置出问题shmflg		若shmaddr设置为0,则shmflg也设置为0SHM_RND							控制共享内存的地址对齐方式SHMLBA							地址为2的乘方SHM_RDONLY					只读方式连接返回值:成功返回共享内存映射到进程虚拟空间中的地址,失败返回-1并设置errnoerrnoEINVAL			无效的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 1024int 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]);//创建共享内存,共性内存的权限为0774if((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	信号量集的idsemnum		要操作的信号量集中的哪一个信号量 例如:0表示对所有的信号量操作,1表示操作信号量集中的第二个信号量,信号量的标号从0开始cmd		IPC_STAT 获取信号量集的属性							--->bufIPC_SET		设置信号量集的属性							--->bufIPC_RMID	删除信号量集										--->bufGETVAL			返回信号量的值									--->valSETVAL			设置semnum信号量的值					--->valGETALL			获取所有信号量的值						--->arraySETALL			设置所有信号量的初始值					--->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	信号量集的IDsops		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)操作,对信号量作加一操作,信号量的值变为1V(a->semid, 0, 1);return 0.0;}double balance = a->balance;sleep(1);		//模拟ATM机延迟balance -= amount;a->balance = balance;		//将余额balance取出amount后再存放回a账户                           //V(1)操作,对信号量作加一操作,信号量的值变为1V(a->semid, 0, 1);return amount;
}double deposit(Account *a, double amount)
{assert(a != NULL);//P(1)操作,对信号量作减一操作,信号量的值变为0//操作编号为0的信号量,步长为1P(a->semid, 0, 1);if(amount <= 0){//V(1)操作,对信号量作加一操作,信号量的值变为1V(a->semid, 0, 1);return 0.0;}double balance = a->balance;sleep(1);balance += amount;a->balance = balance;//V(1)操作,对信号量作加一操作,信号量的值变为1V(a->semid, 0, 1);return amount;
}double get_balance(Account *a)
{assert(a != NULL);//P(1)操作,对信号量作减一操作,信号量的值变为0//操作编号为0的信号量,步长为1P(a->semid, 0, 1);double balance = a->balance;//V(1)操作,对信号量作加一操作,信号量的值变为1V(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 | 0774if((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.rhkb.cn/news/455969.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux笔记---vim的使用

1. vim的基本概念 Vim是一款功能强大的文本编辑器&#xff0c;它起源于Unix系统的vi编辑器&#xff0c;并在其基础上进行了许多改进和增强。 Vim以其高效的键盘操作、高度的可定制性和强大的文本处理能力而闻名&#xff0c;尤其受程序员和系统管理员的欢迎。 Vim支持多种模式…

STM32之基本定时器TIM6和TIM7

1.定时器概念和作用 在编程任务中&#xff0c;定时器是非常常用的一个问题。当需要定时发送数据&#xff0c;定时起某个任务&#xff0c;定时做某个操作等等&#xff0c;这些都离不开定时器。本文基于以STM32F4xx系列开发板&#xff0c;介绍一下基本定时器。 2.基本定时器TIM…

基于Ubuntu24.04,下载并编译Android12系统源码 (二)

1. 前言 上篇文章&#xff0c;我们基于Ubuntu24.04&#xff0c;已经成功下载下来了Android12的源码&#xff0c;这篇文章我们会接着上文&#xff0c;基于Ubuntu24.04来编译Android源码。 2. 编译源码 2.1 了解源码编译的名词 Makefile &#xff1a; Android平台的一个编译系…

鸿蒙网络编程系列28-服务端证书锁定防范中间人攻击示例

1. TLS通讯中间人攻击及防范简介 TLS安全通讯的基础是基于对操作系统或者浏览器根证书的信任&#xff0c;如果CA证书签发机构被入侵&#xff0c;或者设备内置证书被篡改&#xff0c;都会导致TLS握手环节面临中间人攻击的风险。其实&#xff0c;这种风险被善意利用的情况还是很…

PHP企业门店订货通进销存系统小程序源码

订货通进销存系统&#xff0c;企业运营好帮手&#xff01; &#x1f4e6; 开篇&#xff1a;告别繁琐&#xff0c;企业运营新选择 嘿&#xff0c;各位企业主和创业者们&#xff01;今天我要给大家介绍一款超实用的企业运营神器——“订货通进销存系统”。在这个数字化时代&…

Docker入门之构建

Docker构建概述 Docker Build 实现了客户端-服务器架构&#xff0c;其中&#xff1a; 客户端&#xff1a;Buildx 是用于运行和管理构建的客户端和用户界面。服务器&#xff1a;BuildKit 是处理构建执行的服务器或构建器。 当您调用构建时&#xff0c;Buildx 客户端会向 Bui…

Element UI

Element ui 就是基于vue的一个ui框架,该框架基于vue开发了很多相关组件,方便我们快速开发页面。 官网: https://element.eleme.io/#/zh-CN 安装Element UI vue init webpack element(项目名)确认项目是否构建成功&#xff1a;进入到项目的根路径 执行 npm start 访问 h…

NSSCTF

[NSSRound#1 Basic]basic_check nikto扫描 nikto -h url PUT请求&#xff0c;如果不存在这个路径下的文件&#xff0c;将会创建&#xff0c;如果存在&#xff0c;会执行覆盖操作。 [NSSRound#8 Basic]MyDoor if (isset($_GET[N_S.S])) {eval($_GET[N_S.S]); } php特性&#…

形式架构定义语言(ADL)

简介 形式规范 多年来&#xff0c;学术界一直在试图通过使用与测试截然不同且更加主动的方法来确保程序语义的正确执行&#xff1a;形式化方法。研究者们认为这种方法通过更加精确、无二义性的描述来达到让程序绝对地按照设计者的思想执行的目的。这种思想早期体现在Floyd在1…

STM32之OLED驱动函数

类似51单片机中的LCD1602驱动差不多&#xff0c; 1.oled驱动代码 oled.c #include "stm32f10x.h" #include "OLED_Font.h"/*引脚配置*/ #define OLED_W_SCL(x) GPIO_WriteBit(GPIOB, GPIO_Pin_8, (BitAction)(x)) #define OLED_W_SDA(x) GPIO_WriteBi…

Python入门(二)编程中的“真”与“假”,单双向选择的判断

编程中的“真”与“假” 在编程中&#xff0c;这种“真”、“假”状态我们用布尔数来表示&#xff0c;“真”是True&#xff0c;“假”是False。 另一种方式&#xff0c;是通过比较运算得到。 如图&#xff0c;3赋值给a&#xff0c;1赋值给b&#xff0c;进行大小的比较。 a &g…

U9的插件开发之BE插件(1)

U9插件可分为&#xff1a;BE插件、BP插件、UI插件&#xff1b; BE(Business Entity) 简单就是指实体&#xff0c;U9的元数据。 我的案例是设置BE默认值&#xff0c;即在单据新增时&#xff0c;设置单据某一个字段的默认值&#xff0c;具体如下&#xff1a; 1.插件开发工具&a…

Linux的目录结构 常用基础命令(2)

Linux的目录结构 根目录&#xff1a; 所有分区、目录、文件等的位置起点 整个树形目录结构中&#xff0c;使用独立的一个“/”表示 常见的子目录 /root /bin /boot /dev /etc /home /var /usr /sbin 基础知识 以 . 开头的文件均为隐藏文件 路径用/分开 / 不在第一位就…

plsql 高版本用不了 expaste 插件 问题

plsql 高版本用不了 expaste 插件 问题 其实不是版本问题&#xff0c;而是高版本的咩有在用这个插件&#xff0c;在另外一个功能里面&#xff0c; 查询你要的数据&#xff0c; 选择数据&#xff0c;右键&#xff0c;点 右键 复制为表达式列表&#xff0c;即可 在空白处粘贴…

【C++】C++11基础入门

目录 一、C11发展史&#xff1a; 二、列表初始化&#xff1a; 1、初始化&#xff1a; 2、initializer_list函数&#xff1a; 三、声明&#xff1a; 1、auto自动识别类型&#xff1a; 2、decltype&#xff1a; 3、nullptr&#xff1a; 四、范围for&#xff1a; 五、STL…

vue3+vue-baidu-map-3x 实现地图定位

文档地址&#xff1a;一个是2一个是3 https://dafrok.github.io/vue-baidu-map/#/zh/index vue-baidu-map-3x 1.首先要到百度地图开放平台上建一个账号&#xff0c;如果有百度账号可以直接登录百度地图-百万开发者首选的地图服务商,提供专属的行业解决方案 2.点击控制台&am…

V2X介绍

文章目录 什么是V2XV2X的发展史早期的DSRC后起之秀C-V2XC-V2X 和DSRC 两者的对比 什么是V2X 所谓V2X&#xff0c;与流行的B2B、B2C如出一辙&#xff0c;意为vehicle to everything&#xff0c;即车对外界的信息交换。车联网通过整合全球定位系统&#xff08;GPS&#xff09;导…

C#使用log4net结合sqlite数据库记录日志

0 前言 为什么要把日志存到数据库里? 因为结构化的数据库存储的日志信息,可以写专门的软件读取历史日志信息,通过各种条件筛选,可操作性极大增强,有这方面需求的开发人员可以考虑。 为什么选择SQLite? 轻量级数据库,免安装,数据库的常用的基本功能都有,可以随程序…

如何打开/解包星露谷物语XNB文件(附软件资源)

一、什么是 XNB 文件&#xff1f; 游戏将数据、地图和纹理存储在 .xnb 这种压缩数据文件中&#xff0c;它们在游戏的 Content 文件夹中。例如&#xff0c;对话期间显示的阿比盖尔的头像来自这个文件&#xff1a; Content\Portraits\Abigail.xnb。解包这个文件&#xff0c;你会…

SIP 业务举例之 Call Forwarding - No Answer(无应答呼叫转移)

目录 1. Call Forwarding - No Answer 简介 2. RFC5359 的 Call Forwarding - No Answer 信令流程 呼转开始 呼转完成 3. Call Forwording - No Answer 过程总结 博主wx:yuanlai45_csdn 博主qq:2777137742 想要 深入学习 5GC IMS 等通信知识(加入 51学通信),或者想要 …