Linux下并发程序设计(4)——System V进程间通信

       我们都知道Linux是从Unix发展来的,早期的Unix进程间通过无名管道(pipe)、有名管道(fifo)、信号(signal)的方式进行通信。在System V中进程间通信又有共享内存(share memory)、消息队列(message queue)、信号灯级(semaphore set)的方式。在BSD Unix中进程间通信是通过套接字(socket)。以上的这些进程间通信的方式Linux都继承了下来。
       上一篇文章主要内容是Linux继承Unix进程间通信的方式,主要包括管道、信号量的方式实现进程间通信。这篇文章主要讲的是Linux继承System V的进程间通信的方式。
往期推荐
       Linux下并发程序设计(1)——进程
       Linux下并发程序设计(2)——线程
       Linux下并发程序设计(3)——进程间通信

System V IPC

  • IPC对象包括:共享内存、消息队列和信号灯集
  • 每个IPC对象有唯一的ID
  • IPC对象创建后一直存在,直到被显式的删除
  • 每个IPC对象有一个关联的KEY
  • ipcs/ipcrm命令可以查看/删除System 的IPC对象

在这里插入图片描述
       在使用IPC对象通信时,进程创建IPC对象前需要指定一个Key(key为0时,只能被当前进程访问。要被多个进程访问,就需要指定非零Key值)。ftok函数也可以创建Key值,通过key值创建一个共享内存、消息队列或者信号灯集。

ftok

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *path,int proj_id);
  • 成功时返回合法的key值,失败时返回EOF
  • path存在且可访问的文件路径
  • proj_id用于生成key的数字,不能为0
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
    
    
	key_t key;//key_t等价于一个整形
	if((key=ftok(".",'a'))==-1)//每一个进程必须生成相同的key,参数必须一致。参数一致才能得到相同的key值
	{
    
    
		perror("key");
		exit(-1);
	}
}

共享内存

  • 共享内存是一种效率最为高效的进程间通信方式,进程可以直接读写内存,而不是需要任何数据的拷贝
  • 共享内存在内核空间创建,可以被映射到用户空间访问,使用灵活
  • 由于多个进程可以同时访问共享内存,因此需要同步和互斥机制配合使用

共享内存使用步骤

  • 创建/打开共享内存
  • 映射共享内存,即把指定的共享内存映射到线程的地址空间用于访问
  • 读写共享内存
  • 撤销共享内存映射

共享内存创建——shmget

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key,int size,int shmflg);
  • 成功时返回共享内存的id,失败时返回EOF
  • key和共享内存关联的key,IPC_PRIVATE(0 私有的进程)或ftok生成(多个进程)
  • shmflg指定共享内存标志位 IPC_CREAT|0666(是否新建,读写运算)

       示例1:创建一个私有的共享内存,大小为512字节,权限为0666

int shmid;
if((shmid=shmget(IPC_PRIVATE,512,0666))<0){
    
    
	perror("shmget");
	exit(-1);
}

       示例2:创建/打开一个和key关联的共享内存,大小为1024字节,权限为0666

key_t key;
int shmid;
if((key=ftok(".",'m'))==-1)//使用相同的参数才能得到相同值的key
{
    
    
	perror("ftok");
	exit(-1);
}
if((shmid=shmget(key,1024,IPC_CREAT|0666))<0){
    
    //不存在就创建,存在就打开
	perror("shmget");
	exit(-1);
}

共享内存映射——shmat

#include <sys/ipc.h>
#include <sys/shm.h>
void *shmat(int shmid,const void *shmaddr,int shmflg);
  • 成功时返回映射后的地址,失败时返回(void *)-1
  • shmid 要映射共享内存id
  • shmaddr映射后地址,NULL表示由系统自动映射
  • shmflg标志位0表示可读可写;SHM_RDONLY表示只读

共享内存读写

  • 通过指针访问共享内存,指针类型取决于共享内存中存放的收据类型

       例如:在共享内存中存放键盘输入的字符串

char *addr
int shmid;
......
if((addr=(char *)shmat(shmid,NULL,0))==(char *)-1)//0表示对共享地址可读可写,并且shmid强转为字符指针
{
    
    
	perror("shmat");
	exit(-1);
}
fgets(addr,N,stdin);
...

共享内存撤销映射——chamdt

#include <sys/ipc.h>
#include <ays/shm.h>
int shmdt(void *shmaddr);
  • 成功时返回0,失败时返回EOF
  • 不使用共享内存时应撤销映射
  • 进程结束时自动撤销

共享内存控制——shmctl

#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid,int cmd,struct shmid_ds *buf);
  • 成功时返回0,失败时返回EOF
  • shmid要操作的共享内存的id
  • cmd要执行的操作IPC_STAT(属性保存到buf) IPC_SET(设置共享内存属性) IPC_RMID(删除id)
  • buf保存或设置共享内存属性的地址

共享内存注意事项

  • 每块共享内存大小有限制
    • ipcs -l可以查看共享内存的详细信息
    • cat /proc/sys/kernel/shmmax存放的为当前系统中共享内存最大大小
  • 共享内存删除的时间点
    • shmctl(shmid,IPC_RMID,NULL)添加删除标记
    • nattach变成0时真正删除

消息队列

  • 消息队列是System V IPC对象的一种
  • 消息队列由消息队列ID来唯一标识
  • 消息队列就是一个消息列表。用户可以在消息队列中添加消息、读消息等
  • 消息队列可以按照类型来发送/接收消息

消息队列结构

在这里插入图片描述

       当我们创建好一个消息队列以后,系统会创建一个结构体(包括消息队列的大小、个数、权限、key值等等)。在消息队列中不同的消息是分别存储的,对于每种不同的消息,消息列表中会有对应的链表,链式的队列来存放。当收到某种消息的时候会存放在对应的消息队列中,当收到一个新的消息类型时,系统会创建一个新的消息列表。

消息队列的使用步骤

  • 打开/创建消息队列 msgget
  • 向消息队列发送信息 msgsnd
  • 向消息队列接收消息 msgrcv
  • 控制消息队列 msgctl

消息队列创建/打开 —— msgget

#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key,int msgflg);
  • 成功时返回消息队列的id,失败时返回EOF
  • key和消息队列关联的key IPC_PRIVATE或ftok
  • msgflg标志位IPC_CREAT|0666

消息发送——msgsnd

#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msgid,const void *msgp,size_t size,int msgflg);
  • 成功时返回0,失败时返回-1
  • msgid 消息队列id
  • msgp 消息缓冲区地址
  • size 消息正文长度
  • msgflg 标志位0(阻塞)或IPC_NOWAIT(队列有空间返回0,队列无空间返回-1)

消息格式:

  • 通信双方先定义好统一的消息格式
  • 用户根据应用需求定义结构体类型
  • 首成员类型为long,代表消息类型(正整数)
  • 其他消息属于消息正文

消息发送示例:

typedef struct{
    
    
	long mtype;
	char mtext[64];
}MSG;
#define LEN (sizeof(MSG)-sizeof(long))
int main()
{
    
    
	MSG buf;
	...
	buf.mtype=100;
	fgets(buf.mtext,64,stdin);//键盘输入字符串保存到buf.mtext
	msgsnd(msgid,&buf,LEN,0);
	...
	return 0;
}

消息接收——msgrcv

#include <sys/ipc.h>
#include <sys/msg.h>
int msgrcv(int msgid,void *msgp,size_t size,long msgtype,int msgflg);
  • 成功时返回收到的消息长度,失败时返回-1
  • msgid消息队列id
  • msgp消息缓冲区地址
  • size指定接收的消息长度
  • msgtype 指定接收的消息类型
  • msgflg 标志位0 或 IPC_NOWAIT

消息接收示例:

typedef struct{
    
    
	long mtype;
	char mtext[64];
}MSG;
#define LEN (sizeof(MSG)-sizeof(long))
int main()
{
    
    
	MSG buf;
	...
	if(msgrcv(msgid,&buf,200,0)<0{
    
    
		perror("msgrcv");
		exit(-1);
	}
	...
}

控制消息队列——msgctl

#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msgid,int cmd,struct msqid_ds *buf);
  • 成功时返回0,失败时返回-1
  • msgid 消息队列id
  • cmd 要执行的操作 IPC_STAT/IPC_SET/IPC_RMID
  • buf存放消息队列属性的地址

示例

       两个进程通过消息队列轮流将键盘输入的字符串发送给对方,接收并打印对方发送的信息,直到有一方退出为止。

       clientA.c内容如下:

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

typedef struct
{
    
    
    long mtype;
    char mtext[64];
}MSG;

#define LEN  (sizeof(MSG)-sizeof(long))
#define TypeA 100
#define TypeB 200
int main()
{
    
    
    key_t key;
    int msgid;
    MSG buf;
    if((key=ftok(".",'q'))==-1){
    
    
        perror("ftok");
        exit(-1);
    }
    if((msgid=msgget(key,IPC_CREAT|0666))<0)
    {
    
    
        perror("msgget");
        exit(-1);
    }
    while(1)
    {
    
    
        buf.mtype=TypeB;
        printf("input >");
        fgets(buf.mtext,64,stdin);
        msgsnd(msgid,&buf,LEN,0);//发送消息
	if(strcmp(buf.mtext,"quit\n")==0)
	{
    
    
	    msgctl(msgid,IPC_RMID,0);
	    exit(0);
	}
        if(msgrcv(msgid,&buf,LEN,TypeA,0)<0)
        {
    
    
            perror("msgrcv");
        }
        printf("recv from clienB: %s",buf.mtext);
    }

    return 0;
}

       clientB内容如下:

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

typedef struct
{
    
    
    long mtype;
    char mtext[64];
}MSG;

#define LEN  (sizeof(MSG)-sizeof(long))
#define TypeA 100
#define TypeB 200
int main()
{
    
    
    key_t key;
    int msgid;
    MSG buf;
    if((key=ftok(".",'q'))==-1){
    
    
        perror("ftok");
        exit(-1);
    }
    if((msgid=msgget(key,IPC_CREAT|0666))<0)
    {
    
    
        perror("msgget");
        exit(-1);
    }
    while(1)
    {
    
    
        if(msgrcv(msgid,&buf,LEN,TypeB,0)<0)
        {
    
    
            perror("msgrcv");
        }
		if(strcmp(buf.mtext,"quit\n")==0)//判断输入是不是quit
		{
    
    
	    	msgctl(msgid,IPC_RMID,0);
            	exit(0);
		}
        printf("recv from clienA: %s",buf.mtext);
        buf.mtype=TypeA;
        printf("input >");
        fgets(buf.mtext,64,stdin);
        msgsnd(msgid,&buf,LEN,0);
        
    }

    return 0;
}

信号灯

       在System V中进程间通信除了消息队列和共享内存的方式外还有信号灯机制。信号灯也叫信号量,用于进程/线程同步或者互斥机制。信号灯有三种类型,包括Posix无名信号灯(线程间同步或互斥)、Posix有名信号灯(进程间同步和互斥)、System V信号灯。Posix中的信号灯属于计数信号灯,而System V中的信号灯是一个或者多个计数信号灯的集合,由于是一个集合,因此可以对集合中的多个信号灯进行操作,还能避免申请多个资源时产生死锁的情况。

System V信号灯使用步骤

  • 打开信号灯 semget
  • 信号灯初始化 semctl
  • P/V操作 semop
  • 删除信号灯 semctl

信号灯创建/打开——semget

#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key,int nsems,int semflg);
  • 成功时返回信号灯id,失败时返回-1
  • key和消息队列关联的key IPC_PRIVATE或ftok
  • nsems集合中包含的计数信号灯个数
  • semflg标志位IPC_CREAT|0666(一般情况下) IPC_EXCL(信号灯不存在则创建,存在了则会报错)

信号灯初始化——semctl

#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid,int semnum,int cmd,...);
  • 成功时返回0,失败时返回EOF
  • semid要操作的信号灯集id
  • semnum要操作的集合中信号灯编号
  • cmd执行的操作SETVAL(用到第四个参数) IPC_RMID(不会用到第四个参数)
  • union semun取决于cmd

示例:假设信号灯集合中包含两个信号灯;第一个初始化为2,第二个初始化为0

union semun myun;
myun.val=2;
if(semctl(semid,0,SETVAL,myun)<0)
{
    
    
	perror("semctl");
	exit(-1);
}

信号灯的P/V操作——semop

#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid,struct sembuf *sops,unsigned nsops)
  • 成功时返回0,失败时返回-1
  • semid要操作的信号灯集id
  • sops描述对信号灯操作的结构体(数组)
  • nsops要操作的信号灯个数

       sembuf是系统定义好的结构体,内容如下:

struct sembuf
{
    
    
	short semnum;//指定信号灯编号
	short sem_op;//指定操作 -1:P操作  1:V操作
	short sem_flg;//操作方式 0/IPC_NOWAIT
}

综合示例

       父进程通过System V信号灯同步对共享内存的读写,父进程从键盘输入字符串到共享内存,子进程删除字符串中的空格并打印,父进程输入quit后删除共享内存和信号灯集程序结束。

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <string.h>

#define N 64
#define READ 0
#define WRITE 1

union semun
{
    
    
    int val;
    struct semd_ds *buf;
    unsigned short *array;
    struct seminfo *__buf;
};

void init_sem(int semid,int s[],int n)
{
    
    
    int i;
    union semun myun;
    for(i=0;i<n;i++)
    {
    
    
        myun.val=s[i];
        semctl(semid,i,SETVAL,myun);
    }
}
void pv(int semid,int num,int op)
{
    
    
    struct sembuf buf;
    buf.sem_num=num;
    buf.sem_op=op;
    buf.sem_flg=0;
    semop(semid,&buf,1);
}
int main()
{
    
    
    int shmid,semid,s[]={
    
    0,1};
    pid_t pid;
    key_t key;
    char *shmaddr;
    if((key=ftok(".",'s'))==-1)
    {
    
    
        perror("ftok");
        exit(-1);
    }
    if((shmid=shmget(key,N,IPC_CREAT|0666))<0)
    {
    
    
        exit(-1);
    }
    if((semid=semget(key,2,IPC_CREAT|0666))<0)
    {
    
    
        perror("semget");
        goto _error1;
    }
    init_sem(semid,s,2);
    if((shmaddr=(char *)shmat(shmid,NULL,0))==(char*)-1)
    {
    
    
        perror("shmat");
        goto _error2;
    }
    if((pid=fork())<0)
    {
    
    
        perror("fork");
        exit(-1);
    }
    else if(pid == 0)
    {
    
    
        char *p,*q;
        while(1)
        {
    
    
            pv(semid,READ,-1);//可读的缓冲区进行P操作
            p=q=shmaddr;
            while(*q)
            {
    
    
                if(*q!=' ')
                {
    
    
                    *p++ = *q;
                }
                q++;
            }
            *p='\0';
            printf("%s",shmaddr);
            pv(semid,WRITE,1);//可写的进行V操作
        }
    }
    else
    {
    
    
        while(1)
        {
    
    
            pv(semid,WRITE,-1);//可写执行P操作
            printf("input >");
            fgets(shmaddr,N,stdin);
            if(strcmp(shmaddr,"quit\n")==0) break;
            pv(semid,READ,1);//如果不是quit,唤醒子进程可读
        }
        kill(pid,SIGUSR1);//结束子进程
    }
    
_error2:
    shmctl(semid,0,IPC_RMID);

_error1:
    shmctl(shmid,IPC_RMID,NULL);
    return 0;
}

       不积小流无以成江河,不积跬步无以至千里。而我想要成为万里羊,就必须坚持学习来获取更多知识,用知识来改变命运,用博客见证成长,用行动证明我在努力。
       如果我的博客对你有帮助、如果你喜欢我的博客内容,记得“点赞” “评论” “收藏”一键三连哦!听说点赞的人运气不会太差,每一天都会元气满满呦!如果实在要白嫖的话,那祝你开心每一天,欢迎常来我博客看看。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_44895651/article/details/107972045