Linux进程间通信

 vscode使用说明

vscode的介绍使用

vscode 编辑器 :vscode + 插件 == 打造本地式的IDE

vscode远程开发连接我们的云Linux、vscode->vim

下载
官网下载地址:https://code.visualstudio.com/Download

 

要配置:

添加GDB调试
安装插件: GDB Debug - 支持调试
先解决服务器gdb debug missing问题:https://blog.csdn.net/kxlalala/article/details/123864419
解决过程中,可能涉及到yum 源更新问题:
https://blog.csdn.net/qq_24023151/article/details/127407887
调试过程采用

常见问题:

1. 管道过程写入不存在,可以试着将自己的config路径配置⼀下

2. 其他异常登录问题,可以试着在对应登录用户的家目录下, ls -al , 找到.vscode-server隐藏目录,删掉这个目录,重新登录
3. 编写代码的时候,支持C++11

进程间通信

进程间通信(IPC,Interprocess Communication)是指在操作系统中,不同进程之间交换信息或传播数据的过程。

为了确保数据独立性和安全性,每个进程都有自己的地址空间,进程之间通常不能直接访问对方的资源。因此,当进程需要交换数据或进行通信时,必须通过特定的机制或接口进行。进程间通信的方法包括但不限于管道(Pipe)、消息队列、信号量(Semaphore)、共享内存(Shared Memory)、套接字(Socket)等。其中,共享内存是进程间通信的一种方式,多个进程可以同时访问同一片内存空间,实现高效的数据交换;套接字用于网络中的进程间通信,尤其适用于不同主机之间的进程通信。这些通信机制通常由操作系统提供,隐藏了底层实现的细节,简化了编程过程。

此外,进程间通信不仅限于同一台机器上的进程,还包括通过网络连接的不同主机上的进程。这种跨主机的进程间通信通常使用套接字实现,它提供了进程间同步和互斥的机制,确保了数据的一致性和完整性。

每个进程各自有不同的用户地址空间任何一个进程的全局变量在另一个进程中都看不到所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信

  • 进程之间具有独立性,拥有自己的虚拟地址空间,因此无法通过各自的虚拟地址进行通信(A的地址经过B的页表映射不一定映射在什么位置)
  • 进程间的通信除了用内核中的缓冲区之外还有文件以及网络通信的方式可以实现

进程间通信分类

管道

  • 匿名管道pipe
  • 命名管道

System V IPC

  • System V 消息队列
  • System V 共享内存
  • System V 信号量
POSIX IPC
  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

进程间通信目的

数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

进程间通信的本质

进程间通信的本质是:让不同的进程看到同一份资源。

进程是具有独立性的,我们每一个进程不能看到其他进程的代码和数据。

进程 = 内核数据结构 + 代码 + 数据。因此,我们需要借助第三方资源实现进程间的通信。

进程间的通信成本较高,我们需要先将某一个进程需要通信,在OS操作系统中创建一个共享资源,OS操作系统必须提供很多的系统调用,由于OS操作系统创建的共享资源的不同,函数调用的不同,进程间通信会有不同的类型。让不同的进程看到同一份(操作系统)资源(内存)。
 

                  

进程间通信的原因

进程之间是需要进行某种协同的,而协同的前提条件是通信。通信时的数据是有类别的:通知就绪的数据,控制相关信息的数据,单纯地传递给我的数据……

事实上:进程是具有独立性的,在进程间通信的本质中说到:进程间通信的本质是什么?以及进程 = 内核数据结构 + 代码 + 数据。

协同是指协调的两个或者两个以上的不同资源的个体协同一致地完成某个任务的过程或能力。

管道

把一个进程连接到另一个进程的一个数据流称为一个“管道”,通常是用作把一个进程的输出通过管道连接到另一个进程的输入。管道本质上是内核的一块缓存

举例:

在shell中输入命令:ls -l | grep string,我们知道ls命令(其实也是一个进程)会把当前目录中的文件都列出来,但是它不会直接输出,而是把本来要输出到屏幕上的数据通过管道输出到grep这个进程中,作为grep这个进程的输入,然后这个进程对输入的信息进行筛选,把存在string的信息的字符串(以行为单位)打印在屏幕上
                     

匿名管道

为什么叫匿名管道呢??因为不需要文件路径和文件名

为什么父子进程向同一个显示器终端打印数据

进程会默认打开三种标准输入和输出,0,1,2  三个文件描述符代表的是标准输入端,标准输出端和标准错误流。我们可以通过bash来进行理解,bash如果打开了3端口,那么bash其所有的子进程都会默认打开3端口。因为标准输入输出流的管道也被bash的子进程连接了,所以会默认打开。

为什么我们主动关闭子进程0,1,2文件描述符,不会影响父进程继续使用显示器文件?

因为struct file 是通过内存级的引用计数,在主动关闭子进程0,1,2文件描述符,会使struct file 的引用计数减一。当引用计数为0时,才会释放struct file。

工作原理 

进程间通信的本质是,让不同的进程看到同一份资源,使用匿名管道实现父子进程间通信的原理是,让两个父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或读取操作,进而实现父子进程间的通信。

父子进程通信原理

这里父子进程看到的同一份文件资源是由操作系统来维护的,所以当父子进程对该文件进行写入操作时,该文件缓冲区当中的数据并不会进行写时拷贝.父子进程对于文件系统是浅拷贝
管道虽然用的是文件的方案,但操作系统一定不会把进程进行通信的数据刷新到磁盘当中,因为这样做有IO参与会降低效率,而且也没有必要。也就是说,这种文件是一批不会把数据写到磁盘当中的文件,换句话说,磁盘文件和内存文件不一定是一一对应的,有些文件只会在内存当中存在,而不会在磁盘当中存在。

单进程通信原理


 

创建管道

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

pipe(fd[2]); 

参数:pipefd为文件描述符数组,其中pipefd[0]表示读端,pipefd[1]表示写端
返回值:成功返回0,失败返回-1

 pipe()函数的参数部分是一个数组类型的指针,并将一对打开的文件描述符值填入pipefd参数指向的数组fd[2]

通过pipe函数创建的这两个文件描述符 fd[0] 和 fd[1] 分别构成管道的两端,往 fd[1] 写入的数据可以从 fd[0] 读出。并且 fd[1] 一端只能进行写操作fd[0] 一端只能进行读操作,不能反过来使用。要实现双向数据传输,可以使用两个管道。

pipe函数的参数是输出型参数,数组pipefd[2]表示的是对于一个管道的读端和写端的文件描述符。

数组元素数组元素中代表的含义
pipefd[0]管道读端的文件描述符
pipefd[1]管道写端的文件描述符

命名规范

const & 输入型参数

& 输入输出型参数

* 输出型参数

使用实例

单进程实例

例子:从键盘读取数据,写入管道,读取管道,写到屏幕
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main( void )
{
 int fds[2];
 char buf[100];
 int len;
 if ( pipe(fds) == -1 )
 perror("make pipe"),exit(1);
 // read from stdin
 while ( fgets(buf, 100, stdin) ) {
 len = strlen(buf);
 // write into pipe
 if ( write(fds[1], buf, len) != len ) {
 perror("write to pipe");
 break;
 }
 memset(buf, 0x00, sizeof(buf));
 
 // read from pipe
 if ( (len=read(fds[0], buf, 100)) == -1 ) {
 perror("read from pipe");
 break;
 }
 // write to stdout
 if ( write(1, buf, len) != len ) {
 perror("write to stdout");
break;
 }
 }
}

父子进程实例
  1. 父进程创建管道,得到两个文件描述符指向管道的两端
  2. 父进程fork出子进程,子进程也有两个文件描述符指向同一管道。
  3. 父进程关闭fd[0],子进程关闭fd[1],即父进程关闭管道读端,子进程关闭管道写端(因为管道只支持单向通信)。父进程可以往管道里写,子进程可以从管道里读,管道是用环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信。

父子既然要关闭不需要的id,为什么刚开始要打开呢??可以不关闭吗??

 为了使子进程继承下去。也可以不关闭不需要的id。文件描述符数组中的文件描述符是有限的,而且还会导致文件描述符泄漏,但是还是建议关闭,万一误写!

补充:函数介绍

写端函数write

系统接口函数

#include <unisted.h>
ssize_t write(size_t fd, void* buf, int count);

write()函数的fd参数:

是文件描述符,可以是套接字、文件等。
write()函数的buf参数:

是一个指向要读取数据的缓冲区的指针。
write()函数的count参数:

是要读取的字节数。
write()函数的返回值:

如果成功,返回读取字符的长度(可能为0,表示读到文件的结尾)
如果时报,则返回-1,并设置errno表示读取失败的原因

读端函数read

系统接口函数

#include <unisted.h>
ssize_t read(int fd, void *buf, size_t count);

read()函数的fd参数:

是文件描述符,可以是套接字、文件等。
read()函数的buf参数:

是一个指向要读取数据的缓冲区的指针。
read()函数的count参数

是要读取的字节数。
read()函数的返回值为:

如果成功,返回读取字符的长度(可能为0,表示读到文件的结尾)
如果时报,则返回-1,并设置errno表示读取失败的原因

四种情况

  • 情况一:管道内部是空的 && write fd 未关闭,读取条件不具备,读进程会被阻塞,等待读取条件具备,写入数据 (一读一写)
  • 情况二:管道被写满 && read fd 不读且没有关闭,写进程被阻塞,等待写条件具备,读取数据
  • 情况三:管道一直在读,写端关闭,读端会一直读到结尾
  • 情况四:读端直接关闭,写端一直在写,写端进程会被操作系统直接使用13号信号进行关闭,相当于程序出现异常

  其中前面两种情况就能够很好的说明,管道是自带同步与互斥机制的,读端进程和写端进程是有一个步调协调的过程的,不会说当管道没有数据了读端还在读取,而当管道已经满了写端还在写入。读端进程读取数据的条件是管道里面有数据,写端进程写入数据的条件是管道当中还有空间,若是条件不满足,则相应的进程就会被挂起,直到条件满足后才会被再次唤醒

  第三种情况也很好理解,读端进程已经将管道当中的所有数据都读取出来了,而且此后也不会有写端再进行写入了,那么此时读端进程也就可以执行该进程的其他逻辑了,而不会被挂起

  第四种情况也不难理解,既然管道当中的数据已经没有进程会读取了,那么写端进程的写入将没有意义,因此操作系统直接将写端进程杀掉。而此时子进程代码都还没跑完就被终止了,属于异常退出,那么子进程必然收到了某种信号。                       

管道特点

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
  • 管道提供流式服务,面向字节流,写入的次数和读入的次数不是一一匹配的
  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程
  • 一般而言,内核会对管道操作进行同步与互斥
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

管道大小

管道是一个文件,既然是一个文件,必然会有容量的大小,那么管道的大小是多少呢??当管道容量变满后,会进行阻塞。那么如何计算出管道的大小呢?

  • 使用man手册

根据man手册,在2.6.11之前的Linux版本中,管道的最大容量与系统页面大小相同,从Linux 2.6.11往后,管道的最大容量是65536字节。

  • 使用ulimit命令

我们可以使用 ulimit -a 命令来查看当前资源限制的设定。

512 × 8 = 4096 512\times8=4096 512×8=4096 字节。

  • 自行测试

测试管道容量大小只需要将写端一直写,读端不读且不关闭fd[0],即可。

#include<iostream>
#include <algorithm>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;

void SubProcessWrite(int fd)
{
	char c= 'A';	
	int count=0	
	while (true)	
   {
	count++:	
	write(fd,&c,sizeof c);	
	cout << count << endl;	
   }
}


 int main()
 {
	int pipefd[2];	
	int n = pipe(pipefd);	
	if(n != 0)	
    {
	//perror("pipe:");	
	return 1;	
    }
	}	
	pid_t id = fork();	
	if(id ==0)	
    {
	close(pipefd[0]);	
	SubProcessWrite(pipefd[1]);	
	close(pipefd[1]);	
	exit(0);	
    }
	close(pipefd[1]);	
	waitpid(id,NULL.0);	
	close(pipefd[0]);	
	return 	
}

最大65536字节

总结:

 父进程不断创建子进程和管道的过程

匿名管道没有文件实体,有名管道有文件实体但不存储数据

命名管道

 命名管道本质上是一个管道文件,可以通过命令创建也可以通过函数创建,用户可以看到

匿名管道虽然实现了进程间通信,但是它具有一定的局限性:首先,这个管道只能是具有血缘关系的进程之间通信;第二,它只能实现一个进程写另一个进程读,而如果需要两者同时进行时,就得重新打开一个管道。 
为了使任意两个进程之间能够通信,就提出了命名管道(named pipe 或 FIFO)。 
1、与匿名管道的区别:提供了一个路径名与之关联,以FIFO文件的形式存储于文件系统中,能够实现任何两个进程之间通信。而匿名管道对于文件系统是不可见的,它仅限于在父子进程之间的通信。
2、FIFO是一个设备文件,在文件系统中以文件名的形式存在,因此即使进程与创建FIFO的进程不存在血缘关系也依然可以通信,前提是可以访问该路径。

3、FIFO(first input first output)总是遵循先进先出的原则,即第一个进来的数据会第一个被读走

管道特点

1. 可以进行不相干进程间的通信

2. 命名管道是一个文件,对于文件的相关操作对其同样适用

读写规则

1. 对于管道文件,当前进程操作为只读时,则进行阻塞,直至有进程对其写入数据

2. 对于管道文件,当前进程操作为只写时,则进行阻塞,直至有进程从管道中读取数据

命名管道的创建

1. 利用命令创建

mkfifo filename

 2. 函数创建

        #include <sys/stat.h>

        int  mknod(const  char*  path, mode_t mode,  dev_t dev)

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

        【参数】:
            path:创建的有名管道的全路径名
            filename:创建的有名管道的全路径名或当前路径下的有名管道文件名        
            mode:创建的命名管道的模式,指明其存取权限
            dev:为设备值,改值取决于文件创建的种类,它只在创建设备文件是才会用到

            返回值:这两个函数都是成功返回 0 ,失败返回 -1

注释:这两个函数都能创建一个FIFO文件,该文件是真实存在于文件系统中的

命名管道创建完成后就可以使用,其使用方法与匿名管道一样区别在于:命名管道使用之前需要使用open()打开。这是因为:命名管道是设备文件,它是存储在硬盘上的,而匿名管道是存在内存中的特殊文件

但是需要注意的是,命名管道调用open()打开有可能会阻塞

但是如果以读写方式(O_RDWR)打开则一定不会阻塞;

以只读(O_RDONLY)方式打开时,调用open()的函数会被阻塞直到有数据可读;

如果以只写方式(O_WRONLY)打开时同样也会被阻塞,直到有以读方式打开该管道。

A,B进程通信实例

namedPipe.h

#include <iostream>
#include <cstdio>
#include <cerrno>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

const std::string comm_path = "./myfifo";
#define DefaultFd -1
#define Creater 1
#define User 2
#define Read O_RDONLY
#define Write O_WRONLY
#define BaseSize 4096

class NamePiped
{
private:
    bool OpenNamedPipe(int mode)
    {
        _fd = open(_fifo_path.c_str(), mode);
        if (_fd < 0)
            return false;
        return true;
    }

public:
    NamePiped(const std::string &path, int who)
        : _fifo_path(path), _id(who), _fd(DefaultFd)
    {
        if (_id == Creater)
        {
            int res = mkfifo(_fifo_path.c_str(), 0666);
            if (res != 0)
            {
                perror("mkfifo");
            }
            std::cout << "creater create named pipe" << std::endl;
        }
    }
    bool OpenForRead()
    {
        return OpenNamedPipe(Read);
    }
    bool OpenForWrite()
    {
        return OpenNamedPipe(Write);
    }
    // const &: const std::string &XXX
    // *      : std::string *
    // &      : std::string & 
    int ReadNamedPipe(std::string *out)
    {
        char buffer[BaseSize];
        int n = read(_fd, buffer, sizeof(buffer));
        if(n > 0)
        {
            buffer[n] = 0;
            *out = buffer;
        }
        return n;
    }
    int WriteNamedPipe(const std::string &in)
    {
        return write(_fd, in.c_str(), in.size());
    }
    ~NamePiped()
    {
        if (_id == Creater)
        {
            int res = unlink(_fifo_path.c_str());
            if (res != 0)
            {
                perror("unlink");
            }
            std::cout << "creater free named pipe" << std::endl;
        }
        if(_fd != DefaultFd) close(_fd);
    }

private:
    const std::string _fifo_path;
    int _id;
    int _fd;
};

A进程:server.cc

#include "namedPipe.hpp"

int main()
{

     //创建管道
     NamePiped fifo(comm_path, Creater);
     fifo.OpenForRead();

     while(true)
     {
          std::string temp;
          fifo.ReadNamedPipe(&temp);

        std::cout << "shm memory content: " << shmaddr << std::endl;
     }


return 0;

}

B进程:client.cc

#include "namedPipe.hpp"

int main()
{

    NamePiped fifo(comm_path, User);
    fifo.OpenForWrite();

    // 当成string
    char ch = 'A';
    while (ch <= 'Z')
    {
        shmaddr[ch - 'A'] = ch;

        std::string temp = "wakeup";
        std::cout << "add " << ch << " into Shm, " << "wakeup reader" << std::endl;
        fifo.WriteNamedPipe(temp);
        sleep(2);
        ch++;
    }

  return 0;

}

总结

类型进程关系不同点本质
匿名管道必须是亲缘关系由pipe创建并打开内核的一块缓存
命名管道两个毫不相干进程由mkfifo创建,open打开一个文件

system V

共享内存

共享内存是System V版本的最后一个进程间通信方式。共享内存,顾名思义就是允许两个不相关的进程访问同一个逻辑内存,共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常为同一段物理内存。进程可以将同一段物理内存连接到他们自己的地址空间中,所有的进程都可以访问共享内存中的地址。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程

共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据 

特别提醒:共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取,所以我们通常需要用其他的机制来同步对共享内存的访问,例如信号量

共享内存不随着进程的结束而自动释放,一直存在直到系统重启

共享内存生命周期随内核,而文件的生命周期随进程

通信原理

在Linux中,每个进程都有属于自己的进程控制块(PCB)和地址空间(Addr Space),并且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射,通过内存管理单(MMU)进行管理。两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存.

对于上图的理解是:当两个进程通过页表将虚拟地址映射到物理地址时,在物理地址中有一块共同的内存区,即共享内存,这块内存可以被两个进程同时看到。这样当一个进程进行写操作,另一个进程读操作就可以实现进程间通信。但是,我们要确保一个进程在写的时候不能被读,因此我们使用信号量来实现同步与互斥。

对于一个共享内存,实现采用的是引用计数的原理,当进程脱离共享存储区后,计数器减一,挂架成功时,计数器加一,只有当计数器变为零时,才能被删除。当进程终止时,它所附加的共享存储区都会自动脱离。

共享内存速度最快的原因

Proc A 进程给内存中写数据, Proc B 进程从内存中读取数据,在此期间一共发生了两次复制

(1)Proc A 到共享内存       (2)共享内存到 Proc B

因为直接在内存上操作大大减少了数据拷贝次数,所以共享内存的速度也就提高了。

使用方法

  • 查看系统中的共享存储段
ipcs -m

  • 删除系统中的共享存储段
ipcrm -m [shmid]

  • 创建共享内存
key_t ftok(const char *pathname, int proj_id);

[参数pathname]:任意一个路径名

[参数proj_id]:随机的数字

[返回值]:返回一个包含路径名和数字的字符串key

int shmget(key_t key, size_t size, int shmflg);

[参数key]:由ftok生成的key标识,标识系统的唯一IPC资源。

[参数size]:需要申请共享内存的大小。在操作系统中,申请内存的最小单位为页,一页是4k字节,为了避免内存碎片,我们一般申请的内存大小为页的整数倍。

[参数shmflg]:

位图操作

IPC_CREAT:如果你要创建的共享内存不存在则创建它,如果存在,获取该共享内存并返回

IPC_EXCL:单独使用没有意义,只有和IPC_CREAT组合才有意义

IPC_CREAT|IPC_EXCL:如果你要创建的共享内存不存在则创建它,如果存在,出错返回

[返回值]:成功时返回一个新建或已经存在的的共享内存标识符shmid,取决于shmflg的参数。失败返回-1并设置错误码。

  • 挂接共享内存
void *shmat(int shmid, const void *shmaddr, int shmflg);

[参数shmid]:共享存储段的标识符。

[参数*shmaddr]:指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的虚拟地址

[参数shmflg]:若指定了SHM_RDONLY位,则以只读方式连接此段,否则以读写方式连接此段。

[返回值]:成功返回共享存储段的起始指针(虚拟地址),并且内核将使其与该共享存储段相关的shmid_ds结构中的shm_nattch计数器加1(类似于引用计数);出错返回-1。

  • 去关联共享内存

当一个进程不需要共享内存的时候,就需要去关联。该函数并不删除所指定的共享内存区,而是将之前用shmat函数连接好的共享内存区脱离目前的进程,如果不去掉关联,进程结束时默认也会自动去掉关联。

int shmdt(const void *shmaddr);

[参数*shmaddr]:参数shmaddr是shmat函数返回的起始虚拟地址

[返回值]:成功返回0,并将shmid_ds结构体中的 shm_nattch计数器减1;出错返回-1。

  • 销毁共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

[参数shmid]:共享内存标识符。

[参数cmd]:指定的执行操作

IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
IPC_RMID:删除共享内存段

[参数*buf]:buf是一个结构指针,它指向共享内存模式和访问权限的结构(共享内存数据结构

struct shmid_ds {
 struct ipc_perm shm_perm; /* operation perms */
 int shm_segsz; /* size of segment (bytes) */
 __kernel_time_t shm_atime; /* last attach time */
 __kernel_time_t shm_dtime; /* last detach time */
 __kernel_time_t shm_ctime; /* last change time */
 __kernel_ipc_pid_t shm_cpid; /* pid of creator */
 __kernel_ipc_pid_t shm_lpid; /* pid of last operator */
 unsigned short shm_nattch; /* no. of current attaches */
 unsigned short shm_unused; /* compatibility */
 void *shm_unused2; /* ditto - used by DIPC */
 void *shm_unused3; /* unused */
};

这是一个输出型参数,把共享内存数据结构参数信息给你

[返回值]:成功返回0,失败返回-1。

key和shmid的关系

A,B进程通信实例

Shm.h

#include <string>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

const int gCreater = 1;
const int gUser = 2;
const std::string gpathname = "/home/whb/code/111/code/lesson22/4.shm";
const int gproj_id = 0x66;
const int gShmSize = 4096; // 4096*n  4kB*n

class Shm
{
private:
    key_t GetCommKey()
    {
        key_t k = ftok(_pathname.c_str(), _proj_id);
        if (k < 0)
        {
            perror("ftok");
        }
        return k;
    }
    int GetShmHelper(key_t key, int size, int flag)
    {
        int shmid = shmget(key, size, flag);
        if (shmid < 0)
        {
            perror("shmget");
        }

        return shmid;
    }
    std::string RoleToString(int who)
    {
        if (who == gCreater)
            return "Creater";
        else if (who == gUser)
            return "gUser";
        else
            return "None";
    }
    void *AttachShm()
    {
        if (_addrshm != nullptr)
            DetachShm(_addrshm);
        void *shmaddr = shmat(_shmid, nullptr, 0);
        if (shmaddr == nullptr)
        {
            perror("shmat");
        }
        std::cout << "who: " << RoleToString(_who) << " attach shm..." << std::endl;
        return shmaddr;
    }
    void DetachShm(void *shmaddr)
    {
        if (shmaddr == nullptr)
            return;
        shmdt(shmaddr);
        std::cout << "who: " << RoleToString(_who) << " detach shm..." << std::endl;
    }

public:
    Shm(const std::string &pathname, int proj_id, int who)
        : _pathname(pathname), _proj_id(proj_id), _who(who)
        : _pathname(pathname), _proj_id(proj_id), _who(who), _addrshm(nullptr)
    {
        _key = GetCommKey();
        if (_who == gCreater)
            GetShmUseCreate();
        else if (_who == gUser)
            GetShmForUse();
        _addrshm = AttachShm();

        std::cout << "shmid: " << _shmid << std::endl;
        std::cout << "_key: " << ToHex(_key) << std::endl;
    }
    ~Shm()
    {
        if (_who == gCreater)
        {
            int res = shmctl(_shmid, IPC_RMID, nullptr);
        }
        std::cout << "shm remove done..." << std::endl;
    }

    std::string ToHex(key_t key)
    {
        char buffer[128];
        snprintf(buffer, sizeof(buffer), "0x%x", key);
        return buffer;
    }
    bool GetShmUseCreate()
    {
        if (_who == gCreater)
        {
            _shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | IPC_EXCL);
            sleep(10);
            _shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | IPC_EXCL | 0666);
            if (_shmid >= 0)
                return true;
            std::cout << "shm create done..." << std::endl;
        }
        return false;
    }
    bool GetShmForUse()
    {
        if (_who == gUser)
        {
            _shmid = GetShmHelper(_key, gShmSize, IPC_CREAT);
            _shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | 0666);
            if (_shmid >= 0)
                return true;
            std::cout << "shm get done..." << std::endl;
        }
        return false;
    }
    void Zero()
    {
        if(_addrshm)
        {
            memset(_addrshm, 0, gShmSize);
        }
    }

    void *Addr()
    {
        return _addrshm;
    }

    void DebugShm()
    {
        struct shmid_ds ds;
        int n = shmctl(_shmid, IPC_STAT, &ds);
        if(n < 0) return;
        std::cout << "ds.shm_perm.__key : " << ToHex(ds.shm_perm.__key)  << std::endl;
        std::cout << "ds.shm_nattch: " << ds.shm_nattch << std::endl;
    }

private:
    key_t _key;
    int _shmid;

    std::string _pathname;
    int _proj_id;

    int _who;
    void *_addrshm;
};

A进程:server.cc

#include "Shm.hpp"

int main()
{
    //创建共享内存
    Shm shm(gpathname, gproj_id, gCreater);
    char *shmaddr = (char*)shm.Addr();

    shm.DebugShm();

    sleep(5);
    
    return 0;
}

B进程:client.cc

#include "Shm.hpp"

int main()
{
   
 //使用共享内存

    Shm shm(gpathname, gproj_id, gUser);
    shm.Zero();
    char *shmaddr = (char *)shm.Addr();
    sleep(3);


return 0;
}

总结:

(1)优点:我们可以看到使用共享内存进行进程之间的通信是非常方便的,而且函数的接口也比较简单,数据的共享还使进程间的数据不用传送,而是直接访问内存,加快了程序的效率。

(2)缺点:共享内存没有提供同步机制,这使得我们在使用共享内存进行进程之间的通信时,往往需要借助其他手段来保证进程之间的同步工作。

消息队列

工作原理

消息队列是消息的链表,存放在内核中并由消息队列标识符表示。
  消息队列提供了一个从一个进程向另一个进程发送数据块的方法,每个数据块都可以被认为是有一个类型,接受者接受的数据块可以有不同的类型。
  但是同管道类似,它有一个不足就是每个消息的最大长度是有上限的(MSGMAX),每个消息队列的总的字节数(MSGMNB),系统上消息队列的总数上限(MSGMNI)。可以用cat/proc/sys/kernel/msgmax查看具体的数据。
  内核为每个IPC对象维护了一个数据结构struct ipc_perm,用于标识消息队列,让进程知道当前操作的是哪个消息队列。每一个msqid_ds表示一个消息队列,并通过msqid_ds.msg_first、msg_last维护一个先进先出的msg链表队列,当发送一个消息到该消息队列时,把发送的消息构造成一个msg的结构对象,并添加到msqid_ds.msg_first、msg_last维护的链表队列。在内核中的表示如下:

  • 生命周期随内核,消息队列会一直存在,需要我们显示的调用接口删除或使用命令删除
  • 消息队列可以双向通信
  • 克服了管道只能承载无格式字节流的缺点

使用方法

  • msgget

功能创建和访问一个消息队列
原型

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflag);

参数:
key:某个消息队列的名字,用ftok()产生
msgflag

IPC_CREAT:如果消息队列不存在则创建之,如果存在则打开返回

单独使用IPC_EXCL是没有意义的

IPC_CREAT | IPC_EXCL: 如果消息队列不存在则创建之,如果存在则出错返回
返回值:成功返回一个非负整数,即消息队列的标识码,失败返回-1

  • msgctl

功能消息队列的控制函数
原型

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

参数:
msqid:由msgget函数返回的消息队列标识码
cmd:有三个可选的值

IPC_STAT : 把msqid_ds结构中的数据设置为消息队列的当前关联值
IPC_SET: 在进程有足够权限的前提下,把消息队列的当前关联值设置为msqid_ds数据结构中给出的值
IPC_RMID: 删除消息队列

buf:指向msgid_ds结构的指针,它指向消息队列模式和访问权限的结构。

struct msqid_ds {//Linux系统中的定义

struct ipc_perm msg_perm; /* Ownership andpermissions*/

time_t msg_stime; /* Time of last msgsnd()*/

time_t msg_rtime; /* Time of last msgrcv()*/

time_t msg_ctime; /* Time of last change */

unsigned long __msg_cbytes; /* Currentnumber of bytes inqueue (non-standard) */

msgqnum_t msg_qnum; /* Current number of messagesinqueue */

msglen_t msg_qbytes; /* Maximum number ofbytesallowed in queue */

pid_t msg_lspid; /* PID of last msgsnd() */

pid_t msg_lrpid; /* PID of last msgrcv() */

};//不同的系统中此结构会有不同的新成员

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

  • msgsnd

功能把一条消息添加到消息队列中
原型

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

参数
msgid:由msgget函数返回的消息队列标识码
msgp:指针指向准备发送的消息的结构体
msgsz:msgp指向的消息的长度(不包括消息类型的long int长整型)指向的消息的长度,注意是消息的长度,而不是整个结构体的长度,也就是说msgsz是不包括长整型消息类型成员变量的长度
msgflg:默认为0,用于控制当前消息队列满或队列消息到达系统范围的限制时将要发生的事情。
返回值如果调用成功,消息数据的一分副本将被放到消息队列中,并返回0,失败时返回-1。msgflg=0,当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列;msgflg=IPC_NOWAIT,当消息队列已满的时候,msgsnd函数不等待立即返回;msgflg=IPC_NOERROR,若发送的消息大于size字节,则把该消息截断,截断部分将被丢弃,且不通知发送进程。

消息结构一方面必须小于系统规定的上限,另一方面必须以一个long int长整型开始,接受者以此来确定消息的类型

struct msgbuf
{
     long mtye;
     char mtext[1];
};

  • msgrcv

功能:是从一个消息队列接受消息
原型:

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

msgid:msgget函数返回的消息队列标识符。

msgp:指向准备接收消息的结构体指针

  消息结构定义:

struct my_message{  

    long mtye; //频道(你可以理解为消息通道号)

 
/* The data you wish to transfer*/  //消息主体
    char mtext[1]; 

};  

msgsz:msg_ptr指向的消息的长度,注意是消息的长度,而不是整个结构体的长度,也就是说msg_sz是不包括长整型消息类型成员变量的长度。

msgtyp:msgtype可以实现一种简单的接收优先级,消息是按照接收顺序存于消息队列中,每条消息对应一个消息类型号(频道号)。如果msgtype为0,就获取队列中的第一条消息。如果它的值大于零,将获取具有相同类型号(频道号)的第一条消息。如果它小于零,就获取频道号等于或小于msgtype的绝对值的第一个消息。

msgflg:用于控制当队列中没有相应类型的消息可以接收时将发生的事情。msgflg=0,阻塞式接收消息,没有该类型的消息msgrcv函数一直阻塞等待;msgflg=IPC_NOWAIT,如果没有返回条件的消息调用立即返回,此时错误码为ENOMSG;msgflg=IPC_EXCEPT,与msgtype配合使用返回队列中第一个类型不为msgtype的消息;msgflg=IPC_NOERROR,如果队列中满足条件的消息内容大于所请求的size字节,则把该消息截断,截断部分将被丢弃。

返回值:调用成功时,该函数返回放到接收缓存区中的字节数,消息被复制到由msg_ptr指向的用户分配的缓存区中,然后删除消息队列中的对应消息。失败时返回-1。

相关指令

  • 查看系统中的消息队列段
ipcs -q
  •  删除系统中的消息队列段
ipcrm -q [msqid]

信号量

概念理论渗透

a.多个执行流(进程),能看到的一份资源:共享资源

b.被保护起来的资源 --- 临时资源 --- 同步和互斥 --- 用互斥的方式保护共享资源 --- 临时资源

c.互斥:任何时刻只能有一个进程在访问共享资源

d.资源 --- 要被程序员访问 --- 资源被访问,朴素的认识,就是通过代码访问 

   --- 代码 = 访问共享资源的代码(临界区) + 不访问共享资源的代码(非临界区)

e.所谓的对共享资源进行保护 --- 临界资源 --- 本质是对共享资源的代码进行保护(临界区)

为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。

信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。这里主要讨论二进制信号量。

工作原理

由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.

举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。

信号量:本质是一个计数器

申请信号量的本质:就是对公共资源的一种预定机制

使用方法

Linux提供了一组精心设计的信号量接口来对信号进行操作,它们不只是针对二进制信号量,下面将会对这些函数进行介绍,但请注意,这些函数都是用来对成组的信号量值进行操作的。它们声明在头文件:

#include <sys/ipc.h>

 #include <sys/types.h>

 #include <sys/sem.h>

  • semget函数

它的作用是创建一个新信号量或取得一个已有信号量,原型为:

int semget(key_t key,int nsems,int semflg);

[参数key]:是整数值(唯一非零),不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,程序先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),只有semget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。如果多个程序使用相同的key值,key将负责协调工作。

[参数nsems]:指定需要的信号量数目,它的值几乎总是1。

[参数semflg]:是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。

[返回值]:semget函数成功返回一个相应信号标识符(非零),失败返回-1.

  • semop函数

它的作用是改变信号量的值,原型为:

int semop(int semid,struct sembuf *sops,unsigned nsops)

 semid是由semget返回的信号量标识符,sembuf结构的定义如下:

struct sembuf{
    short sem_num;//除非使用一组信号量,否则它为0
    short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,
                    //一个是+1,即V(发送信号)操作。
    short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号,
                    //并在进程没有释放该信号量而终止时,操作系统释放信号量
};

[参数semid]:信号量集ID

[参数sops]:指向数组的每一个sembuf结构都刻画一个在特定信号量上的操作

[参数nsops]:sops指向数组的大小

[返回值]:成功,返回0;失败,返回-1。

  • semctl函数

该函数用来直接控制信号量信息,它的原型为:

int semtcl(int semid,int semnum,int cmd,...);

如果有第四个参数,它通常是一个union semun结构,定义如下:

semun是在linux/sem.h中定义的:
  /*arg for semctl systemcalls.*/
  union semun{
  int val;/*value for SETVAL*/
  struct semid_ds *buf;/*buffer for IPC_STAT&IPC_SET*/
  ushort *array;/*array for GETALL&SETALL*/
  struct seminfo *__buf;/*buffer for IPC_INFO*/
  void *__pad; 
}

semid:信号量集ID

semnum:指定对哪个信号量操作,只对几个特殊的cmd操作有意义

cmd:指定具体的操作类型

所指的操作:

SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。

IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。

semun arg:设置或返回信号灯信息

相关指令

  • 查看系统中的信号量段
ipcs -s

  •  删除系统中的信号量段
ipcrm -s [semid]

总结性问题

操作系统是如何把共享内存,消息队列,信号量统一管理起来?

共享内存数据结构

消息队列数据结构

信号量数据结构

答:先描述,再组织

​​​​​​​

  • 11
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你好,赵志伟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值