进程通信学习

学习目标

理解进程间通信

掌握管道

掌握共享内存

了解消息队列、信号量

通信之前,让不同的进程看到同一份资源(文件、内存块)

资源不同,决定了不同种类的通信方式

内存级的通信

IPC

进程间通信(Inter-Process Communication,IPC)是指操作系统提供的一组机制和技术,用于不同进程之间进行数据传输、信息交换和协调操作的方式。在多进程或分布式系统中,不同的进程可能需要共享信息、进行协作处理、传递数据等,而IPC机制提供了可靠和有效的方式来实现这种通信。

通信之前,让不同的进程看到同一份资源(文件、内存块),而资源不同决定了不同种类的通信方式

1、ipc指令

  • ipcs -q:查看消息队列的信息。

  • ipcrm -q msqid:删除指定的消息队列,其中msqid是消息队列的标识符。

  • ipcs -s:查看信号量的信息。

  • ipcrm -s semid:删除指定的信号量,其中semid是信号量的标识符。

  • ipcs -l:列出所有的IPC资源的详细信息。

  • ipcrm -a:删除所有的IPC资源。

  • ipcs -m用于查看共享内存的信息。

  • ipcrm -m shmid : 删除共享内存

2、相关概念

1)临界资源:

在操作系统中,临界资源是指一次只能由一个进程或线程访问的共享资源。临界资源可以是共享内存区域、文件、设备、数据结构等。当多个进程或线程需要访问同一个临界资源时,需要通过同步机制来协调它们的访问。

2)临界区:

临界区是指程序中访问临界资源的那段代码或代码段。临界区的目的是确保对临界资源的访问是原子的、互斥的,即同一时间只有一个进程或线程可以进入临界区进行访问。

3)原子性:

原子性是指一个操作在执行过程中不会被中断的特性。原子操作是不可再分的,要么完全执行,要么完全不执行,没有中间状态。在多线程或多进程环境中,如果多个线程或进程同时访问共享资源,如果对共享资源的操作不是原子的,就可能导致数据不一致或竞态条件的问题。

4)互斥:

互斥是一种同步机制,用于保护共享资源,确保在同一时间只有一个进程或线程可以访问共享资源。互斥机制通过对临界区的访问进行互斥,即同一时间只允许一个进程或线程进入临界区,其他进程或线程需要等待。常见的互斥机制包括互斥锁(Mutex)和信号量(Semaphore)。互斥机制可以防止多个进程或线程同时对共享资源进行访问,从而避免数据不一致或竞态条件的问题。

匿名管道

1、原理:子进程继承父进程的文件标识符和文件描述表

2、特点

半双工通信:匿名管道是一种单向通信机制,只能在一个方向上传递数据。通常,管道被创建时,会产生两个文件描述符,一个用于读取数据(pipeid[0]),另一个用于写入数据(pipeid[1])。

亲缘进程通信:匿名管道通常用于具有父子关系的进程之间进行通信。

共享内核缓冲区:匿名管道使用内核中的缓冲区作为数据传输的中介,并不会将管道刷新至磁盘。

进程同步机制:匿名管道读写的阻塞特性可以用于进程之间的同步。

生命周期:随进程结束。

3、相关函数

1)pipe函数
#include <unistd.h>
int pipe(int pipefd[2]);

pipefd[2]:该数组用于存储创建的管道的文件描述符。pipefd[0]表示读端,pipefd[1]表示写端

返回值:函数成功时返回0,失败时返回-1

4、简单实操:

1)父子进程的传递数据
#include <iostream>
#include <cerror>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
    int pipefd[2];
    if(pipe(pipefd)!=0)//创建管道
    {
        cerr<<"pipe error"<<strerror(errno)<<endl;//失败返回-1
        exit(1);
    }
    pid_t id=fork();//创建子进程
    if(id==0)//子进程进行单向读操作
    {
        close(pipefd[1]);//保持单向关闭写端
        while(true) 
        {
            char buffer[1024];
            ssize_t n=read(pipefd[0],buffer,sizeof(buffer)-1);//从管道读取内容,给数组留'\0'的位置
            if(n>0)//返回读取的字符
            {
                buffer[n]='\0';
                printf("%s",buffer);
            }
            else if//写端关闭返回0,读取错误返回-1
            {
                break;
            }
        }
        close(pipefd[0]);//关闭写端
        exit(1);//子进程退出
    }
    //父进程进行读操作
    close(pipeid[0]);//关闭读端
    while(true)
    {
        sleep(2);
        char s[1024];
        fgets(s,sizeof(s),stdin);
        write(pipeid[1],s,strlen(s));//给管道写入内容
    }
    close(pipeid[1]);//关闭写端
    if(waitpid(id,nullptr,0)>0)
        cout<<"wait sucess"<<endl;
}
2)进程池
#include <unordered_map>
#include <time.h>
#include <vector>
#include <assert.h>
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <string>
#include <stdio.h>
using namespace std;
typedef void(*functor)();
vector <functor> functors;
unordered_map<size_t,string> info;
vector<pair<size_t,size_t>> assignTask;
void f1()
{
    cout << "这是一个处理日志的任务, 执行的进程 ID [" << getpid() << "]"
        << "执行时间是[" << time(nullptr) << "]" << endl;
}
void f2()
{
    cout << "这是一个备份数据任务, 执行的进程 ID [" << getpid() << "]"
        << "执行时间是[" << time(nullptr) << "]" << endl;
}
void f3()
{
    cout << "这是一个处理网络连接的任务, 执行的进程 ID [" << getpid() << "]"
        << "执行时间是[" << time(nullptr) << "]" << endl;
}
void Loading()
{
    functors.push_back(f1);
    info.insert({functors.size(),"处理日志"});
    functors.push_back(f2);
    info.insert({functors.size(),"备份数据"});
    functors.push_back(f3);
    info.insert({functors.size(),"网络连接"});
}
void sendTask(vector<pair<size_t,size_t>>&processFds)
{
    while(true)
    {
        sleep(2);
        size_t pickProcess = rand() % processFds.size();
        size_t pickTask = rand() % functors.size();
        write(processFds[pickProcess].second, &pickTask, sizeof(size_t));
        cout<<"给子进程pid"<<processFds[pickProcess].first<<"分配:"<<info[pickTask]<<endl;
    }
}
void operateTask(size_t blockFd)
{
    while(true)
    {
        size_t Task=0;
        srand((long long)time(nullptr));
        ssize_t s=read(blockFd,&Task,sizeof(size_t));
        if(s==0||s==-1) 
        {
            break;
        }
        assert(s==sizeof(size_t));
        (void)s;//assert在debug模式下有效,在release无效会因定义未使用报错
        if(Task<functors.size())
        {
            functors[Task]();
        }
    }
}
int main()
{
    Loading();
    int processNum=5;
    for(int i=0;i<processNum;i++)
    {
        int pipefd[2];
        pipe(pipefd);
        pid_t id = fork();
        if (id == 0)
        {
            close(pipefd[1]);
            operateTask(pipefd[0]);
            close(pipefd[0]);
            exit(1);
        }
        close(pipefd[0]);
        assignTask.push_back({id,pipefd[1]});
    }
    sendTask(assignTask);
    for(int i=0;i<processNum;i++)
    {
        close(assignTask[i].second);
        waitpid(assignTask[i].first,nullptr,0);
    }
    return 0;
}

命名管道:

命名管道(Named Pipe),也称为FIFO(First In, First Out),是一种在进程间进行通信的方法。它允许不相关的进程通过一个共享的命名管道来交换数据。

1、原理

在文件系统中创建一个特殊的文件,进程可以通过读写这个文件来进行通信。

2、特点

命名管道是匿名管道的改良,解决了只有血缘关系的进程才能通信的问题

共享性:多个进程可以共享同一个命名管道,从而实现进程间的数据交换。不同进程可以通过读写同一个管道文件进行通信。

3、相关函数

1)mkfifo

在文件系统中创建一个特殊的文件,用于命名管道的数据传输

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int mkfifo(const char *pathname, mode_t mode);

pathname:指定要创建的命名管道的路径和名称。

mode:指定创建的管道的权限。

返回值:返回0表示成功,-1表示失败,并通过errno变量来指示具体的错误原因。

2)unlink

在文件系统中删除指定的路径文件

#include <unistd.h>
int unlink(const char *pathname);

pathname:指定要删除的文件的路径和名称。

返回值:返回0表示删除成功,-1表示删除失败,并通过errno变量来指示具体的错误原因。

4、简单实操:

这是一个简单的使用命名管道进行进程间通信的示例代码。

代码分为serverFifo.cppclientFifo.cpp两个文件,以及一个共享的头文件comm.h。下面对代码进行说明:

1)comm.h
#pragma once
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <cstring>
#include <cstdlib>
#include <fcntl.h>
#define IPC_PATH "./.fifo"//定义了一个宏`IPC_PATH`,表示命名管道的路径。
2)serverFifo.cpp
#include "comm.h"
using namespace std;
int main()
{
    if(mkfifo(IPC_PATH,0600)!=0)//在程序开始处,通过`mkfifo`函数创建了一个命名管道,路径为`IPC_PATH`。
    {
        cerr<<"mkfifo error"<<strerror(errno)<<endl;
        exit(1);
    }
    int fd=open(IPC_PATH,O_RDONLY);//打开命名管道,以只读方式打开。
    while(true)//进入一个循环,不断读取从客户端写入的数据。
    {
        char buffer[1024];
        ssize_t s=read(fd,buffer,sizeof(buffer)-1);
        if(s>0)//如果读取到数据,将其打印输出。
        {
            buffer[s]='\0';
            printf("接收到命令:%s",buffer);
        }
        else//如果读取到的数据长度为0或者错误,说明客户端已经关闭了写端,退出循环。
        {
            break;
        }
    }
    close(fd);
    return 0;
}
3)clientFifo.cpp
#include "comm.h"
int main()
{
    int fd=open(IPC_PATH,O_WRONLY);//打开命名管道,以只写方式打开。
    while(true)//进入一个循环,读取用户输入的命令,并将其写入到命名管道中。
    {
        printf("请输入命名:");
        fflush(stdout);
        char s[1024];
        fgets(s,sizeof(s),stdin);
        write(fd,s,strlen(s));
    }
    close(fd);
    return 0;
}

这个示例展示了一个简单的服务器-客户端模型,使用命名管道进行进程间通信。服务器创建了一个命名管道,并等待客户端写入数据。客户端可以输入命令,并将其写入到命名管道中,服务器则读取管道中的数据并进行处理。这种方式可以实现简单的进程间通信,用于传输命令、数据等。

需要注意的是,这个示例中的命名管道是单向的,即只能从客户端向服务器传输数据。如果需要双向通信,可以创建两个命名管道,分别用于客户端向服务器发送数据和服务器向客户端发送数据。

共享内存

共享内存是一种用于实现进程间通信的机制,它允许多个进程在它们的地址空间中共享同一块内存区域。这种方式可以实现高效的数据交换,因为进程可以直接访问共享内存,而无需进行复制或通过中间介质传输数据。

1、原理

创建共享内存,进程可以通过对共享内存的访问来进行通信

2、特点

高效性:由于共享内存允许进程直接访问共享数据,而无需复制或传输,因此在进程间交换大量数据时非常高效。

实时性:共享内存可以提供实时的数据共享,因为数据在共享内存中的修改可以立即被其他进程看到,无需等待数据传输或同步操作。也就意味着没有进程同步机制,直接共享数据而没有提供任何保护机制。

灵活性:共享内存可以用于各种数据结构和数据类型,因为它仅提供一块内存区域,对数据的组织和管理完全由应用程序自己决定。

生命周期:随内核,创建共享内存后,进程退出时,共享内存依旧存在,其内存是随内核的,需要显示删除

3、相关函数

1)shmget

shmget是一个用于创建或获取共享内存段的系统调用

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

key:用于标识共享内存段的键值,可以是一个正整数或使用ftok函数生成的键值。

key_t ftok(const char *pathname, int proj_id);

size:要创建或获取的共享内存段的大小(字节数)。内存在操作系统按页(4KB)管理,size一般设为页的整数倍

shmflg:控制共享内存的权限和其他选项的标志。

  • IPC_CREAT|IPC_EXCL:保证shmget调用成功,一定是一个全新的共享内存

返回值:返回一个非负整数shmid的共享内存标识符,该标识符用于后续操作

注意

对于不同的进程,要访问同一个共享内存段,需要使用相同的键值和权限标志。

2)shmctl

shmctl函数用于控制和操作共享内存段的属性。例如获取和修改共享内存的权限、大小、状态等。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

shmid:共享内存标识符,由shmget函数返回的共享内存段的标识符。

  • IPC_STAT:获取共享内存的信息,并将其存储在buf指向的结构体中。

  • IPC_SET:修改共享内存的权限和其他属性,修改的属性由buf指向的结构体中的相应字段指定。

  • IPC_RMID:删除共享内存段。

cmd:控制命令,指定对共享内存的操作。

buf:指向struct shmid_ds结构的指针,用于存储共享内存的信息。

返回值:如果返回值为-1,表示操作失败,可以通过查看errno变量来获取错误信息。

注意事项:

  • 使用IPC_STAT命令获取共享内存的信息时,需要提前分配好存储信息的结构体,并将其指针传递给buf参数。

  • 使用IPC_SET命令修改共享内存的属性时,需要在struct shmid_ds结构中指定要修改的属性。

  • 使用IPC_RMID命令删除共享内存段时,需要确保没有进程正在使用该共享内存段,否则删除操作将失败。

shmctl函数是操作System V共享内存的关键函数之一,通过它可以灵活地管理和控制共享内存段的属性和状态。

3)shmat

shmat用于将共享内存段附加到进程的地址空间中,使得进程可以访问共享内存中的数据。

void *shmat(int shmid, const void *shmaddr, int shmflg);

参数说明:

shmid:共享内存标识符,由shmget函数返回。

shmaddr:共享内存段希望附加到的地址。如果shmaddrNULL,表示让系统自动选择一个合适的地址进行附加。

shmflg:附加标志,用于指定共享内存段的访问权限。

返回值:

成功时,返回附加后的共享内存段的起始地址。类似于malloc的使用

失败时,返回-1,并设置相应的错误码。

4)shmdt

shmdt用于将共享内存段从进程的地址空间中分离

int shmdt(const void *shmaddr);

参数说明:

shmaddr:共享内存段的起始地址。

返回值:

成功时,返回0

失败时,返回-1,并设置相应的错误码。

4、简单实操:

1)comm.h
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstring>
#define PATH_NAME "/home/lxy/"
#define PROJ_ID 0x15
#define MEM_SIZE 4096
using namespace std;
key_t CreatKey()
{
    key_t key=ftok(PATH_NAME,PROJ_ID);//这个键值在不同的进程中是唯一的,可以用于进程间通信的 IPC 对象的标识
    if(key<0)
    {
        cout<<"ftok error"<<strerror(errno)<<endl;
        exit(1);
    }
    return key;
}

2)serverShm.cpp
#include "comm.hpp"
const int flags = IPC_CREAT | IPC_EXCL;//保证shmget调用成功,一定是一个全新的共享内存
int main()
{
    //创建
    key_t key=CreatKey();
    int shmid=shmget(key,MEM_SIZE,flags|0666);//注意需要给定读写权限
    if(shmid<0)
    {
        cout<<"shmget error"<<strerror(errno)<<endl;
        exit(1);
    }
    //连接
    char *s=(char*)shmat(shmid,nullptr,0);//类似于malloc的使用
    //通信
    while(true)
    {
        cout<<s<<endl;//可以直接进行访问,而不需要系统调用
        sleep(1);
    }
    //断连
    shmdt(s);
    //删除
    shmctl(shmid,IPC_RMID,nullptr);
    return 0;
}
3)clientShm.cpp
#include "comm.hpp"
int main()
{
    //创建
    key_t key=CreatKey();
    int shmid=shmget(key,MEM_SIZE,IPC_CREAT);
    if(shmid<0)
    {
        cout<<"shmget error"<<strerror(errno)<<endl;
        exit(1);
    }
    //关联
    char *s=(char*)shmat(shmid,nullptr,0);
    //通信
   while(true)
    {
        printf("Please Enter# ");
        fflush(stdout);
        fgets(s,MEM_SIZE,stdin);//从键盘获取
    }
    //断联
    shmdt(s);
    return 0;
}

消息队列

信号队列(Signal Queue)是一种在操作系统中用于进程间通信的机制。它允许进程通过发送和接收信号来进行通信和同步操作。

信号队列通过维护一个队列来存储进程发送的信号。每个信号都包含一个特定的标识符和相关的数据。当一个进程发送一个信号时,它会将信号添加到队列的末尾。接收进程可以从队列的头部获取信号并进行相应的处理。

信号量

信号量(Semaphore)是一种在操作系统中用于进程间同步和互斥的机制。它允许多个进程共享一个计数器,并通过对该计数器进行操作来实现同步和互斥的控制。

特点

计数器:信号量维护一个计数器,该计数器的初值可以是任意非负整数。计数器的值表示可用的资源数量或者某个共享资源的状态。

P操作:当进程需要访问一个共享资源时,它首先执行P(等待)操作,该操作会将信号量的计数器减1。如果计数器的值大于等于0,表示资源可用,进程可以继续访问资源;如果计数器的值小于0,出现互斥情况,表示资源不可用,进程需要等待。

V操作:当进程使用完共享资源时,它执行V(释放)操作,该操作会将信号量的计数器加1。这样,其他等待资源的进程就有机会继续执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值