进程通信知识基础【Linux】——下篇

发布时间:2023年12月17日

目录

前文

一,命名管道

创建命名管道

1. getline——c++库

2. unlink——系统接口

实践代码

common.hpp

client.cpp

server.cpp

Log.cpp

二,共享内存(system V接口)

1. 创建共享内存

shmget接口

2. 删除共享内存

常见ipc指令

shmctl接口

3. 映射到虚拟内存(挂起)

shmat接口

去关联

shmdt接口

小结:

三,信号量概念

概念引入?

小结


嘿!收到一张超美的风景图,希望你每天都能开心顺心

?

前文

经过进程池的实践(哈希思想应用【C++】(位图,布隆过滤器,海量数据处理面试题)-CSDN博客),我们对管道有了一定的理解,本篇继续给大家分享其余一些进程通信的知识。

一,命名管道

管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。如果我们想在 不相关的进程之间交换数据,可以使用 FIFO文件来做这项工作,它经常被称为 命名管道
命名管道是一种 特殊类型文件

创建命名管道

在指令端创建

$? mkfifo? ?filename

在程序中创建

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

其中,pathname管道文件路径mode权限掩码,mkfifo函数返回值为0表示成功-1表示失败,错误原因存储在errno中。

下面我们来一个例子:

创建好一个命名管道后,执行下面指令,我们会遇到光标不动的现象。

echo "hello" > FilePipe

原因是:我们只是向命名管道进行写入没有打开命名管道的读端(从命名管道中读取数据), 进入了一种阻塞状态。(这也是管道的一种控制

解决方法也很简单,就是在另一个窗口,对命名管道进行读数据,这时就会中断写入的阻塞状态。

cat? <? FilePipe?

实践包含一些其他接口:

1. getline——c++库

istream& getline (istream& is, string& str, char delim);

其中,is是输入流,str是存储读取结果的字符串,delim是指定的分隔符(可选,默认为'\n'cin, scanf等是以空格和换行符为分隔符)。函数的返回值是输入流is的引用

getline函数的作用是从输入流is中读取一行文本,并将其存储到字符串str中。如果读取成功,则返回输入流is的引用。如果读取失败,则返回输入流的状态为false。

2. unlink——系统接口

int unlink(const char *pathname);

参数pathname为要删除的文件的路径名。当调用unlink接口时,系统会删除指定路径的文件。如果文件不存在或者无法访问,则会返回-1,并设置errno变量来指示错误类型。

下面头文件中:?

_COMM_H(名字自定义)?宏则是用来防止头文件的重复包含,通过定义这个宏可以在编译过程中检测到头文件是否被多次包含,避免出现重复定义的错误。

实践代码

common.hpp

#ifndef _COMM_H_
#define _COMM_H_
#include <iostream>
#include <vector>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include "Log.hpp"

using namespace  std;

#define MOODE 0666
#define SIZE 128
#define CHNUMS 3

string Pipename = "FilePipe";
string PipeAddress = "./FilePipe";

#endif

client.cpp

#include "common.hpp"

int main()
{
   // 1. 客户向服务端,传输请求
   int fd = open(PipeAddress.c_str(), O_WRONLY);
   if (fd < 0 )
   {
    cout << "open fail" << endl;
   }
   
   string buff;
   while (1)
   {
      cout << " Please set your information :> ";
      getline(cin, buff);  //getline接口的优势在于它可以读取一行文本,并且自动将换行符(\n)作为行结束符
      write(fd, buff.c_str(), buff.size());
   }
   
    
    // 关闭命名管道
    close (fd);
    // 用户信息读取完后,需要我们删除这个命名管道文件。
    unlink(Pipename.c_str());
    //unlink接口是用于删除一个指定的文件或符号链接的系统调用。 
    return 0;
}

server.cpp

#include "common.hpp"
#include <sys/wait.h>

static void Getmessage(int fd) // 设置成静态函数的目的是,让其他文件,无法使用该函数
{
   char buff[SIZE];
   Log("处理数据开始", Test) << "[" << getpid() << "] "<< " step 3 " << endl;
   while (1)
   {
    memset(buff, '\0', sizeof(buff));
    ssize_t byte = read(fd, buff, sizeof(buff) - 1);
    if (byte > 0)
    {
        cout << "[" << getpid() << "] " << buff << endl;
    }else if ( byte == 0)
    {
        cout << " write close " << endl;
        break;
    }else
    {
        perror(" read fail");
        cout << "[" << getpid() << "] " << " read error "  << endl;
        break;
    }
   }
}


int main()
{
   // 1. 创建命名管道
   if (mkfifo(PipeAddress.c_str(), MOODE) < 0 )
   {
      perror( " mkfifo fail ");
   }

   Log("创建命名管道成功", debug) << "step 1" << endl;
   // 2. 文件操作
   int fd = open(PipeAddress.c_str(), O_RDONLY);
   if (!fd)
   {
      perror(" open fail");
      exit(-1);
   }
    Log("文件操作成功", debug) << "step 2" << endl;

   pid_t id;
   vector<int> FdList;
   for (int i = 0; i < CHNUMS; i++)
   {
       id = fork();
       if (id == 0)
       {
           Getmessage(fd);
           exit(1);  // 子进程完美退出,等待父进程接收
       }
   }

   // 父进程接受子进程退出
   int status = 0;
   for (int i = 0; i < CHNUMS; i++)
   {
       // waitpid(FdList[i], nullptr, 0);  这样子写会些坑,不知道哪一个子进程先状态改变
       waitpid(-1, &status, 0); // -1则是随机等待一个子进程状态改变
   } 
   
   // 4. 关闭文件
   close(fd);
   unlink(PipeAddress.c_str());
   Log("关闭文件", warning) << "step 4" << endl;
   return 0;
}

Log.cpp

#ifndef _LOG_
#define _LOG_
#include <iostream>
#include <ctime>
#include <string>
using namespace std;

#define debug 0
#define warning 1
#define Test 2

string list_level[3] = {
   "debug",
   "warning",
   "Test"  
};
   ostream&  Log(const char* str, int level)
{
    cout <<  str << " | " << (unsigned)time(0) << " | " << list_level[level] << " ";
    return cout;
}

#endif

下面是上面代码功能的形象图:

二,共享内存(system V接口)

共享内存是一种操作系统进程通信的方式,它允许多个进程共享同一块物理内存区域,从而实现高效的数据交换和共享。

共享内存的原理是将一块物理内存映射到多个进程的虚拟地址空间中,这样多个进程就可以直接读写这块内存,而不需要通过复制数据或者消息传递等方式进行通信

共享内存通常使用信号量来实现进程间的同步和互斥访问。

多个进程共享一块物理内存,操作系统一定得这个操作进行有序管理,而一旦涉及管理,必然要进行 先描述,再组织。

所有共享内存 =? 共享内存块 +? 对应共享内存块的内核数据结构。

1. 创建共享内存

shmget接口

返回值:一个操作系统运行时,肯定不只一个共享内存,众多的共享内存也被一个内核数据结构管理起来,而返回值就类似文件描述中的fd

1.? ?int shmflg:??IPC_CREAT(单独用以0替代) and IPC_EXCL 两个宏,前者单独用,共享内存不存在则创建;存在,则返回之。? ? 后者,单独用没有意义,两者通过按位与一起用,底层不存在则创建;存在则出错返回(意义:保证每次共享内存都是最新的)。

2.? ?key_t key: 我们知道一个操作系统中有众多的共享内存,而key通过特殊的算法规则处理,形成唯一的通行码,找到精确共享内存。

那如何获取 key,通过ftok接口(就是一个对数据进行算法规则处理)。

?

  • pathname:? 一个我们可以访问的路径(操作系统会访问其inode值(一个存储标识值));?
  • proj_id :? ??就是一个int值。如果想获取一个相同的key值,proj_id也得相同。

返回值:-1失败;大于0则是key值。

3.? ?size_t size :? ?共享空间的字节大小。最后是页(4096)的整数倍。原因:假设要4097,则操作系统会创建8192字节,但中间空间我们又无法访问这就是浪费资源了。

2. 删除共享内存

输入指令: 查看共享内存资源

手动删除:

ipcs -m? ? ??

然后输入:ipcrm? -m? shmid值

?这里我们要注意的是:共享内存的生命周期随内核!!,除非重启!

perms: 权限值,为0时,谁也无法访问该共享内存,需要在shmget时需要输入权限,比如: shmget(123,? 4096 , IPC_CREAT |?IPC_EXCL | 0666),权限同文件一样

常见ipc指令

1. ipcs - 显示系统中的IPC资源信息
2. ipcrm - 删除IPC资源
3. ipcs -q - 显示消息队列信息
4. ipcs -s - 显示信号量信息
5. ipcs -m - 显示共享内存信息?

代码删除:

shmctl接口

cmd:? 可以理解为功能选项,我们可以选择删除,获取共享空间属性信息等等。IPC_STAT, IPC_RMID等等

shmid:理解为共享内存的标识符吧。

*buf? :? 这是管理共享内存的数据结构指针,可用通过buf获取或者修改其属性。

返回值: -1表示失败。

3. 映射到虚拟内存(挂起)

shmat接口

shmid:? 共享内存唯一的id?

shmaddr:? 去设置一个虚拟地址。(由于我们不清楚虚拟地址的使用情况,这里建议null/nullptr,让计算机来产生

shmflg: 挂接方式,以只读(SHM_R默认0),只写(SHM_W),还是读写(SHM_R | SHM_W )方式挂接。

返回值: 就是计算机返回共享内存虚拟地址的开头; 返回0则失败。

当一旦挂起成功,进程关联数就会增加

去关联

shmdt接口

shmaddr:? ?共享内存虚拟地址起始地址。?

返回值:-1失败; 0成功,成功删除后同时,进程关联数减一。

成功创建共享内存后位置分布的逻辑图

代码示例:

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

int main() {
    key_t key = ftok("shmfile",65);  // 创建一个key
    int shmid = shmget(key,1024,IPC_CREAT|0666);  // 创建共享内存区

    char *str = (char*) shmat(shmid,(void*)0,0);  // 将共享内存区连接到当前进程的地址空间

    printf("Write data : ");
    gets(str);

    printf("Data written in memory: %s\n",str);

    shmdt(str);  // 分离共享内存区
    shmctl(shmid,IPC_RMID,NULL);  // 删除共享内存区

    return 0;
}

这里介绍一个比较重要的格式打印数据的接口: snprintf()接口?

小结:

1. 共享内存是进程通信(IPC)中,速度最快的。因为这个方法不需要过多的拷贝数据,而且不用经过操作系统,在用户层直接读取修改。

2. 由于共享内存没有访问控制(读写端独立),容易引发并发问题(两端数据不一致问题)。

三,信号量概念

概念引入?

小结

我们需要理解这个概念

没理解?没事,这个我们在多线程里,用代码的形式进行解释。

下期预告:信号!!

结语

? ?本小节就到这里了,感谢小伙伴的浏览,如果有什么建议,欢迎在评论区评论,如果给小伙伴带来一些收获请留下你的小赞,你的点赞和关注将会成为博主创作的动力

文章来源:https://blog.csdn.net/qq_72112924/article/details/134696903
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。