Linux环境编程-进程通信

一、基本概念

什么是进程间通信:

是指两个或多个进程之间需要协同工作、交互数据的过程,因为进程之间是相互独立工作的,为了协同工作就需要进行通信来交互数据

进程间通信的分类:

  • 简单的进程间通信:

信号(携带附加信息)、文件、环境变量、命令行参数等

  • 传统的进程间通信:

管道文件(有名管道、匿名管道)

  • XSI的进程间通信:

共享内存、消息队列、信号量

  • 网络的进程间通信:

socket套接字

二、传统的进程间通信技术-管道

管道是UNIX系统中最古老的进程间通信方式,古老就意味着所有的系统都支持,早期的管道是半双工,现在有些系统的管道是全双工(假定以半双工来编写代码)

管道是一种特殊的文件,它的数据在管道文件中是流动的读取后就会自动消失,如果文件中没有数据要读取会阻塞等待

有名管道:

基于有文件名的管道文件的进程间通信,可以是任何进程之间的通信

  • 编程模型:
          进程A               进程B
          创建管道               ...
          打开管道             打开管道
          写数据                读数据
          关闭管道             关闭管道
          删除管道               ...
  • 创建有名管道文件
命令: mkfifo filename
函数:int mkfifo(const char *pathname, mode_t mode);
        功能:创建有名管道文件
        pathname:文件的路径
        mode:文件的权限掩码

实例:

  • 进程A
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc,const char* argv[])
{
    //    创建管道
    if(mkfifo("fifo",0664))
    {
        perror("mkfifo");
        return EXIT_FAILURE;
    }

    //    打开管道
    int fd = open("fifo",O_WRONLY);
    if(0 > fd)
    {
        perror("open");
        unlink("fifo");
        return EXIT_FAILURE;
    }

    char buf[256] = {};
    for(;;)
    {
        printf(">>>");
        scanf("%s",buf);
        write(fd,buf,strlen(buf)+1);
        if(0 == strncmp(buf,"quit",4))
        {
            printf("通信结束\n");
            break;
        }
    }

    //    关闭管道
    close(fd);
    usleep(1000);    // 为了让进程B有时间处理关闭管道
    //    删除管道
    unlink("fifo");
}
  • 进程B
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc,const char* argv[])
{
    //    打开管道
    int fd = open("fifo",O_RDONLY);
    if(0 > fd)
    {
        perror("open");
        return EXIT_FAILURE;
    }

    char buf[256] = {};
    for(;;)
    {
        read(fd,buf,sizeof(buf));
        if(0 == strncmp(buf,"quit",4))
        {
            printf("通信结束\n");
            break;
        }
        printf("read:%s\n",buf);
    }

    //    关闭管道
    close(fd);
}

匿名管道

没有名字的管道文件

只适合通过fork创建的有血缘关系的进程间通信(父子进程、兄弟进程之间)

  • 编程模型:
          父进程                 子进程
           获取一对fd               ...
           创建子进程             共享一对fd
          关闭读fd[0]             关闭写fd[1]
          fd[1]写数据             fd[0]读数据
          关闭fd[1]               关闭fd[0]
  • 创建匿名管道
int pipe(int pipefd[2]);
        功能:创建并打开一个匿名管道文件
        pipefd:输出型参数 返回该匿名管道文件的读权限fd和写权限fd
            pipefd[0]   用于读管道文件
            pipefd[1]   用于写管道文件

实例:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(int argc,const char* argv[])
{
    int fd[2] = {};
    //    创建并打开获取匿名管道
    if(pipe(fd))
    {
        perror("pipe");
        return EXIT_FAILURE;
    }

    if(fork())
    {
        //    父进程 关闭读 负责写    
        close(fd[0]);
        for(;;)
        {
            char buf[256] = {};
            printf(">>>");
            scanf("%s",buf);
            write(fd[1],buf,strlen(buf)+1);
            if(0 == strncmp(buf,"quit",4))
            {
                printf("通信结束!我是父进程%u\n",getpid());
                break;
            }
            usleep(1000);
        }
        //    关闭写
        close(fd[1]);
    }
    else
    {
        //    子进程 关闭写 负责读        
        close(fd[1]);
        for(;;)
        {
            char buf[256] = {};
            read(fd[0],buf,sizeof(buf));
            if(0 == strncmp(buf,"quit",4))
            {
                printf("通信结束!我是子进程%u\n",getpid());
                break;
            }
            printf("read:%s\n",buf);
        }
        //    关闭写
        close(fd[0]);
    }
}    

三、XSI进程间通信

XSI是X/open 公司制定的用于进程间通信的系统(S)接口(I)标准

IPC键值

XSI进程间通信标准都需要借助系统内核完成,需要创建内核对象,内核对象以整数形式返回给用户,相当于文件描述符\文件指针,代表了某个内核对象完成某次进程间通信任务,也叫做IPC标识符

类似文件的创建需要借助文件名一样,IPC标识符的创建也需要借助IPC键值(整数),也跟文件名一样,要确保IPC键值是独一无二的

一般是通过函数生成一个独一无二的IPC键值

  • ftok :生成IPC键值
key_t ftok(const char *pathname, int proj_id);
    功能:生成一个独一无二的IPC键值
    pathname:项目路径
    proj_id:项目编号
    返回值:根据项目路径+项目编号自动计算出IPC键值    

注意:计算IPC键值的方式不是根据pathname的字符串内容,而是依靠路径的位置,如果提供了假的路径,不管编号如何,都会得到相同的IPC键值,不正确

实例:

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

int main(int argc,const char* argv[])
{
    key_t key1 = ftok("djflkdj",1234);    
    key_t key2 = ftok("xjcidjf",5678);    
    printf("%u %u\n",key1,key2);
}

1. 共享内存:

基本特点:

两个或多个进程共享同一块由内核负责维护的物理内存,该物理内存是需要与进程的虚拟内存进行映射后使用

  • 优点:不需要进行磁盘读写操作,无需复制操作,是最快的一种IPC机制

  • 缺点:需要考虑通信的同步问题,一般采用信号解决

相关函数

  • shmget :创建/获取共享内存
int shmget(key_t key, size_t size, int shmflg);
        功能:创建\获取共享内存
        key:IPC键值
        size:共享内存的大小,当是获取时此参数无意义写0即可
        shmflg:
            IPC_CREAT   创建共享内存,已存在时会直接获取
            IPC_EXCL    配合CREAT,如果已存在则返回错误
            获取时直接给0即可
            注意:如果是创建IPC_CREAT,需要在后面额外按位或这段共享内存的读写权限IPC_CREAT|0644
        返回值:成功返回该共享内存的IPC标识符,失败返回-1
  • shmat    :映射内存
void *shmat(int shmid, const void *shmaddr, int shmflg);
        功能:虚拟内存与物理共享内存映射
        shmid:IPC标识符
        shmaddr:想要映射的虚拟内存首地址,一般给NULL就会自动安排
        shmflg:
            SHM_RND 只有当shmaddr不是NULL时才有效,当映射的字节数不足一页的整数倍时,会向下取整数倍的页数来映射
            SHM_RDONLY  以只读方式映射共享内存
            如不需要以上操作,一般给0即可
        返回值:成功返回映射的虚拟内存的首地址,失败返回(void*)-1
  • shmdt :取消映射
int shmdt(const void *shmaddr);
        功能:取消映射
        shmaddr:映射过的虚拟内存首地址
  • shmctl      :删除/控制共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
        功能:删除/控制共享内存
        shmid:IPC标识符
        cmd:
            IPC_STAT    获取共享内存属性信息  buf输出型参数
            IPC_SET     修改共享内存的属性 buf输入型参数
            IPC_RMID    删除共享内存  buf给NULL即可

编程模型:

            进程A                       进程B
         创建共享内存                获取共享内存
         映射共享内存                映射共享内存
         写数据到内存并通知         接到通知就读内存数据
         接到通知就读内存数据       写数据到内存并通知
         取消映射                      取消映射
         删除共享内存

实例

  • 进程A
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

char* shm;     //虚拟内存的首地址
int shmid;     //IPC标识符

void sigread(int num)
{
    //    信号来,要读内存
    printf("\nread:%s\n>>>",shm);
    fflush(stdout);
    if(0 == strncmp(shm,"quit",4))
    {
        printf("对方已关闭通信\n");

        //    取消映射
        if(shmdt(shm))
        {
            perror("shmdt");    
        }

        usleep(1000);

        //    删除共享内存
        if(shmctl(shmid,IPC_RMID,NULL))
        {
            perror("shmctl");    
        }
        exit(EXIT_SUCCESS);
    }
}

int main(int argc,const char* argv[])
{
    signal(SIGRTMIN,sigread);
    printf("我是进程%u\n",getpid());

    //    创建共享内存
    shmid = shmget(ftok(".",110),4096,IPC_CREAT|0644);
    if(0 > shmid)
    {
        perror("shmget");
        return EXIT_FAILURE;
    }

    //    映射共享内存
    shm = shmat(shmid,NULL,0);
    if((void*)-1 == shm)
    {
        perror("shmat");
        //    删除共享内存
        shmctl(shmid,IPC_RMID,NULL);
        return EXIT_FAILURE;
    }

    //    等待获取对方的进程号
    pid_t pid = 0;
    printf("请输入与本进程通信的进程pid:");
    scanf("%u",&pid);

    //    写数据并通知
    for(;;)
    {
        printf(">>>");
        scanf("%s",shm);    //    写数据
        kill(pid,SIGRTMIN);
        if(0 == strncmp(shm,"quit",4))
        {
            printf("你已结束通信\n");
            break;
        }
    }
    //    取消映射
    if(shmdt(shm))
    {
        perror("shmdt");    
    }

    usleep(1000);

    //    删除共享内存
    if(shmctl(shmid,IPC_RMID,NULL))
    {
        perror("shmctl");    
    }
    exit(EXIT_SUCCESS);
}
  • 进程B
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

char* shm;
int shmid;

void sigread(int num)
{
    //    信号来,要读内存
    printf("\nread:%s\n>>>",shm);
    fflush(stdout);
    if(0 == strncmp(shm,"quit",4))
    {
        printf("对方已关闭通信\n");

        //    取消映射
        if(shmdt(shm))
        {
            perror("shmdt");    
        }
        exit(EXIT_SUCCESS);
    }
}

int main(int argc,const char* argv[])
{
    signal(SIGRTMIN,sigread);
    printf("我是进程%u\n",getpid());

    //    获取共享内存
    shmid = shmget(ftok(".",110),0,0);
    if(0 > shmid)
    {
        perror("shmget");
        return EXIT_FAILURE;
    }

    //    映射共享内存
    shm = shmat(shmid,NULL,0);
    if((void*)-1 == shm)
    {
        perror("shmat");
        return EXIT_FAILURE;
    }

    //    等待获取对方的进程号
    pid_t pid = 0;
    printf("请输入与本进程通信的进程pid:");
    scanf("%u",&pid);

    //    写数据并通知
    for(;;)
    {
        printf(">>>");
        scanf("%s",shm);    //    写数据
        kill(pid,SIGRTMIN);
        if(0 == strncmp(shm,"quit",4))
        {
            printf("你已结束通信\n");
            break;
        }
    }
    //    取消映射
    if(shmdt(shm))
    {
        perror("shmdt");    
    }
    exit(EXIT_SUCCESS);
}

2. 消息队列:

基本特点

是一个由内核维护管理的数据链表,通过消息类型来匹配正确后收发数据

相关函数

  • msgget :创建/获取消息队列
int msgget(key_t key, int msgflg);
        功能:创建\获取消息队列
        key:IPC键值
        msgflg:
            IPC_CREAT   创建消息队列,已存在时会直接获取
            IPC_EXCL    配合CREAT,如果已存在则返回错误
            获取时直接给0即可
            注意:如果是创建IPC_CREAT,需要在后面额外按位或这段消息队列的读写权限IPC_CREAT|0644
        返回值:成功返回该消息队列的IPC标识符,失败返回-1
  • msgsnd     :发送消息包
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
        功能:向消息队列发送消息包
        msqid:IPC标识符
        msgp:要发送的消息包的首地址
            struct msgbuf {
               long mtype;  //第一个成员必须是long类型的消息类型
               char mtext[1];   //根据需要存储数据
           };// 该结构由程序员自己设计
        msgsz:只需要数据的字节数,不包括消息类型
        msgflg:
            阻塞一般写0
            IPC_NOWAIT  当消息队列满时,不等待立即返回
        返回值:成功0 失败-1
  • msgrcv :从消息队列中接收数据
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
        功能:从消息队列中接收读取数据
        msqid:IPC标识符
        msgp:消息包内存首地址
        msgsz:数据内存的字节数大小,不包括消息类型,因为不确定对方发送的字节数,因此建议给大一点
        msgtyp:要接收的消息类型
            >0  读取消息类型等于msgtyp的消息
            =0  读取消息队列中的第一条消息
            <0  读取消息队列中的消息类型小于等于abs(msgtyp)的消息如果同时有多个,则读取最小的那一个
        msgflg:
            阻塞等待一般给0即可
            IPC_NOWAIT  如果当时消息类型没有匹配,则不阻塞立即返回
        返回值:成功返回读取到的数据字节数,失败-1        
  • msgctl :删除/控制消息队列
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
        功能:删除/控制消息队列
        msqid:IPC标识符
        cmd:
            IPC_STAT    获取消息队列属性信息  buf输出型参数
            IPC_SET     修改消息队列的属性 buf输入型参数
            IPC_RMID    删除消息队列  buf给NULL即可        

编程模型:

             进程A                   进程B
          创建消息队列            获取消息队列
          发送消息msg-a           接收msg-a消息
          接收msg-b消息           发送消息msg-b
          删除消息队列

实例

  • 进程A
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include "msg.h"

int main(int argc,const char* argv[])
{
    //    创建消息队列
    int msqid = msgget(ftok(".",112),IPC_CREAT|0644);
    if(0 > msqid)
    {
        perror("msgget");
        return EXIT_FAILURE;
    }

    Msg msg = {};
    for(;;)
    {
        //    发送消息
        msg.type = 1;
        printf(">>>");
        scanf("%s",msg.data);
        if(msgsnd(msqid,&msg,strlen(msg.data)+1,0))
        {
            perror("msgend");    
            break;
        }
        if(0 == strncmp(msg.data,"quit",4))
        {
            printf("你已关闭消息队列\n");
            break;
        }

        //    等待接收消息
        if(-1 == msgrcv(msqid,&msg,DATA_MAX,2,0))
        {
            perror("msgrcv");
            break;
        }
        printf("recv:%s\n",msg.data);
        if(0 == strncmp(msg.data,"quit",4))
        {
            printf("对方已关闭消息队列\n");
            break;
        }
    }
    usleep(1000);
    //    删除消息队列
    if(msgctl(msqid,IPC_RMID,NULL))
    {
        perror("msgctl");    
    }

}


  • 进程B
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include "msg.h"

int main(int argc,const char* argv[])
{
    //    获取消息队列
    int msqid = msgget(ftok(".",112),0);
    if(0 > msqid)
    {
        perror("msgget");
        return EXIT_FAILURE;
    }

    Msg msg = {};
    for(;;)
    {
        //    等待接收消息
        if(-1 == msgrcv(msqid,&msg,DATA_MAX,1,0))
        {
            perror("msgrcv");
            break;
        }
        printf("recv:%s\n",msg.data);
        if(0 == strncmp(msg.data,"quit",4))
        {
            printf("对方已关闭消息队列\n");
            break;
        }

        //    发送消息
        msg.type = 2;
        printf(">>>");
        scanf("%s",msg.data);
        if(msgsnd(msqid,&msg,strlen(msg.data)+1,0))
        {
            perror("msgend");    
            break;
        }
        if(0 == strncmp(msg.data,"quit",4))
        {
            printf("你已关闭消息队列\n");
            break;
        }

    }
}
  • msg.h
#ifndef MSG_H
#define MSG_H

#define DATA_MAX 256

typedef struct Msg
{
    long type;
    char data[DATA_MAX];
}Msg;

#endif//MSG_H

3. 信号量:

基本特点

由内核维护的一个"全局变量",用于记录进程之间共享资源的数量,限制进程对共享资源的访问

信号量更像是一种数据操作锁,本身是不具备数据交换功能,而是通过控制其他的通信资源(共享内存、消息队列、文件等)来实现进程间通信的协调

1、如果信号量的值>0,说明有可以使用的资源,使用时需要将信号量-1,然后再使用

2、如果信号量的值等于0,说明没有资源可以使用,此时进程会进入休眠,直到信号量的值大于0,进程就会被唤醒,执行步骤1

3、当资源使用完毕后,需要将信号量+1,正在休眠的进程就可以被唤醒执行步骤1

相关函数

  • semget :创建\获取信号量的IPC标识符
int semget(key_t key, int nsems, int semflg);
        功能:创建\获取信号量的IPC标识符
        key:IPC键值
        nsems:信号量的数量
        semflg:
            IPC_CREAT   创建信号量,已存在时会直接获取
            IPC_EXCL    配合CREAT,如果已存在则返回错误
            获取时直接给0即可
            注意:如果是创建IPC_CREAT,需要在后面额外按位或这段信号量的读写权限IPC_CREAT|0644
        返回值:成功返回该信号量的IPC标识符,失败返回-1
  • semop    :对某个或某些信号量进行加减操作
int semop(int semid, struct sembuf *sops,size_t nsops);
        功能:对某个或某些信号量进行加减操作
        semid:IPC标识符
        sops:
            struct sembuf{
                unsigned short sem_num;  // 信号量的下标
                short          sem_op;   //
                    1   信号量+1
                    -1  信号量尝试-1,如果为0则休眠阻塞
                    0   等待信号量的值为0,否则阻塞休眠
                short          sem_flg;
                    0           阻塞
                    IPC_NOWAIT  不阻塞
                    SEM_UNDO  如果进程终止了没有手动还原信号量+1,系统会自动还原+1  
            };
        nsops:表示sops指针指向多少个连续的结构体,意味着要加减多少个信号量,一般每次只操作一个时写1即可
  • semctl :删除\控制信号量
int semctl(int semid, int semnum, int cmd, ...);
        功能:删除\控制信号量
        semid:IPC标识符
        semnum:第几个信号量 下标从0开始
        cmd:
            IPC_STAT   获取信号量属性
            IPC_SET    设置信号量属性
            IPC_RMID   删除信号量
            GETALL      获取所有信号量的值
            GETNCNT     获取正在等待减信号量的进程数
            GETVAL     通过返回值获取下标为semnum信号量的值
            GETZCNT    获取正在等待信号量的值为0的进程数
            SETVAL      设置下标为semnum信号量的值
            SETALL      设置所有信号量的值
        ...:        
  • union semun
union semun {
               int              val;    // 用于设置信号量的值
               struct semid_ds *buf;// 用于获取\设置信号量的属性
               unsigned short  *array;  //用于批量设置\获取信号量的值
           };        

使用流程:

1、创建信号量

2、设置信号量管理的资源数

3、减\加信号量

4、删除信号量

实例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/wait.h>

//	十个进程 使用5个共享资源
int main(int argc,const char* argv[])
{
	//	创建信号量
	int semid = semget(ftok(".",119),1,IPC_CREAT|0644);
	if(0 > semid)
	{
		perror("semget");
		return EXIT_FAILURE;
	}
	//	设置信号量的值
	if(semctl(semid,0,SETVAL,5))
	{
		perror("semctl");
		semctl(semid,0,IPC_RMID);
		return EXIT_FAILURE;
	}

	//	创建10个子进程抢夺5个资源
	for(int i=0; i<10; i++)
	{
		pid_t pid = fork();
		if(0 == pid)
		{
			struct sembuf buf = {0,-1,0};
			semop(semid,&buf,1);
			printf("我是子进程%u,我抢到了资源,还剩%d个资源\n",
				getpid(),semctl(semid,0,GETVAL));
			sleep(3);
			buf.sem_op = 1;
			semop(semid,&buf,1);
			printf("我是子进程%u,我还了资源,还剩%d个资源\n",
				getpid(),semctl(semid,0,GETVAL));
			return EXIT_SUCCESS;
		}
	}

	while(-1 != wait(NULL));
	semctl(semid,0,IPC_RMID);
}


posted @   冲他丫的  阅读(29)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
点击右上角即可分享
微信分享提示