Liunx进程间通信

进程间通信

进程间通信的基本概念

进程间通信就是在不同进程之间传播或交换信息。

进程间通信的目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程

  • 资源共享:多个进程之间共享同样的资源。

  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止 时要通知父进程)。

  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态。
    进程通信的本质
    进程通信的本质就是让不同的进程看到同一个资源。
    在这里插入图片描述
    进程的通信是通过管道的,只能单向通信。

管道

我们在之前用过很多次管道,|现在我们在用一次,用管道链接两个不同的进程。
在这里插入图片描述

匿名管道

匿名管道是管道的一种,匿名管道是一个单向通信管道,是父子进程通信的管道。
在这里插入图片描述
pipe函数

int pipe(int pipefd[2]);

pipe函数可以为进程创建两个端口,一个读端口,一个是写端口。
创建管道流程

  1. 父进程打开双通道
    在这里插入图片描述
  2. 父进程创建子进程
    在这里插入图片描述
  3. 父进程和子进程关闭读写端
    在这里插入图片描述
    注意: 这里不能共用一个端口。读端和写端指向不同,不能用读端写入,写端读出。
    传输简单使用:
    首先介绍两个系统调用
    read和write
   ssize_t write (int fd,const void * buf,size_t count);

write( ) 会把参数buf所指的内存写入count个字节到参数fd所指的文件内。当然,文件读写位置也会随之移动。

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

接下来我们实际操作:以一个简单的模型
注意:必须严格按照fd[0]读取 fd[1]写入。否则就会出现下面这种情况。

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


int main()
{

int fd[2]={0};  
//pipe创建两个端口 其中fd[0]为读端,fd[1]为写端
if(pipe(fd)<0)
{
    perror("worring");
    return 1;
}

pid_t id=fork();
//子进程写入 父进程读出
if(id==0)
{//子进程关闭读端
    close(fd[1]);
string mes("I am son,hello..");
int con=10;
while(con)
{
    cout<<"doing"<<endl;
    write(fd[0],mes.c_str(),mes.size());
    con--;
    sleep(1);
}
exit(0);
}
close(fd[0]);
char buffer[64];
string x;
while(1)
{
size_t s=read(fd[1],buffer,sizeof(buffer));
if(s>0)
{
    
cout<<"i am father"<<buffer<<endl;

}
else if(s==0)
{
    cout<<"have done"<<endl;
    break;
}
else
{
    cout<<"worring"<<endl;
    break;
}
}
 return 0;
}

错误示范!
在这里插入图片描述
我们在接收的时候,开辟的buffer也是要注意,不能太小。
正确的实现代码:

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


int main()
{

int fd[2]={0};  
//pipe创建两个端口 其中fd[0]为读端,fd[1]为写端
if(pipe(fd)<0)
{
    perror("worring");
    return 1;
}

pid_t id=fork();
//子进程写入 父进程读出
if(id==0)
{//子进程关闭读端
    close(fd[0]);
string mes("I am son,hello..");
int con=2000;
while(con)
{
    cout<<"doing:"<<con<<endl;
    write(fd[1],mes.c_str(),mes.size());
    con--;
    
}
exit(0);
}
close(fd[1]);
char buffer[17];
string x;
sleep(5);
while(1)
{
size_t s=read(fd[0],buffer,sizeof(buffer)-1);//读的尺寸小一个可以填充\0
if(s>0)
{
  
   buffer[s]='\0';
cout<<"i am father:"<<buffer<<"size:"<<s<<endl;

}
else if(s==0)
{
    cout<<"have done"<<endl;
    break;
}
else
{
    cout<<"worring"<<endl;
    break;
}
}
 return 0;
}

管道的特点

  1. 写端进程不写,读端进程一直读,那么此时会因为管道里面没有数据可读,对应的读端进程会被挂起,直到管道里面有数据后,读端进程才会被唤醒。
  2. 读端进程不读,写端进程一直写,那么当管道被写满后,对应的写端进程会被挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。
  3. 写端进程将数据写完后将写端关闭,那么读端进程将管道当中的数据读完后,就会继续执行该进程之后的代码逻辑,而不会被挂起。
  4. 读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么操作系统会将写端进程杀掉。

测试管道大小

在这里插入图片描述
子进程一直写,父进程不读。则子进程会写满缓冲区。可以看到缓冲区大小是4096 4KB。
管道通信的过程
管道的4种情况

  1. 正常情况,如果管道没有数据了,读端必须等待,直到有数据为止(写端写入数据了)
  2. 正常情况,如果管道被写满了,写端必须等待,直到有空间为止(读端读走数据)
  3. 写端关闭,读端一直读取, 读端会读到read返回值为0, 表示读到文件结尾
  4. 读端关闭,写端一直写入,OS会直接杀掉写端进程,通过想目标进程发送SIGPIPE(13)信号,终止目标进程

进程池

接下来我们实现一个进程池。

命名管道

在上面中,我们理解了匿名管道,匿名管道是父子进程通信的管道。然而,不是父子又该如何呢。这时命名管道应运而生。命名管道实际上就是一个管道文件。他的注意事项:

  1. 普通文件是很难做到通信的,即便做到通信也无法解决一些安全问题。
  2. 命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像,但这个映像的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中。
    创建管道指令:mkfifo
    在这里插入图片描述
    此时我们像管道中写入:
while : ; do echo "hello fifo" ; sleep 1 ; done > fifo

在这里插入图片描述
cat重定向到我们的屏幕,就可以了。
创建一个命名管道
接下来我们用代码创建一个管道,并且让两个不同的进程进行通信。
在程序中创建命名管道使用mkfifo函数,mkfifo函数的函数原型如下:

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

mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件。

  • 若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。
  • 若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。(注意当前路径的含义)
    mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限。
    我们首选创建一个管道:
bool MakeFifo()// 创建管道
{
    int n = mkfifo(FILENAME, 0666);
    if(n < 0)
    {
        std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
        return false;
    }

    std::cout << "mkfifo success... read" << std::endl;
    return true;
}

如果创建失败,则返回false。后面,则是吧管道当成一个普通文件,往里面读,往里面写就行了。就这么简单。

// server.cpp
#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include"comm.h"

//#include "comm.h"

using namespace std;

bool MakeFifo()// 创建管道
{
    int n = mkfifo(FILENAME, 0666);
    if(n < 0)
    {
        std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
        return false;
    }

    std::cout << "mkfifo success... read" << std::endl;
    return true;
}
int main()
{
  start:
int rfd = open(FILENAME, O_RDONLY);
    if(rfd < 0)
    {
        std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
          if(MakeFifo())
          {
            goto start;
          }
          else
          { 
            return 2;
        }
       
    }
    std::cout << "open fifo success..." << std::endl;
 char buffer[1024];
 //rfd是管道.fifo的fd
 while(1)
 {
 size_t s=read(rfd,buffer,sizeof(buffer)-1);

 if(s>0)
 {
    buffer[s]=0;
    cout<<"client say#"<<buffer<<endl;
 }   
 }


close(rfd);
    return 0;
}

接下来是写端:

//client.cpp
#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include"comm.h"

using namespace std;
int main()
{
int wfd=open(FILENAME,O_WRONLY);//写端口 wfd
if(wfd < 0)
    {
        std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
        return 1;
    }

string meassage; 
while(1)
{
    cout<<"please Enter#";
    getline(cin,meassage);

   size_t s=write(wfd,meassage.c_str(),meassage.size());//往管道的文件描述符wfd写。只需要吧管道当成文件就行了。
if(s<0)
{
    std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
    break;
}

}
return 0;
}

接下来,我们利用这一机制,进行进程间通信,将命令写入管道,然后提取命令。再利用execlp函数进行程序替换。

if(s>0)
 {
    buffer[s]=0;
    if (fork() == 0){
				//child
                 cout<<"client say#"<<buffer<<endl;
				execlp(buffer, buffer, NULL); //进程程序替换
				exit(1);
			}
			waitpid (-1, NULL, 0); //等待子进程
    
   
 }   

我们只需要加入这几句代码,让子进程帮我们进行代码置换。
命名管道和匿名管道的区别:

  1. 匿名管道由pipe函数创建并打开。
  2. 命名管道由mkfifo函数创建,由open函数打开。
  3. FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在于它们创建与打开的方式不同,一旦这些工作完成之后,它们具有相同的语义。
    那么在命令行当中的管道(“|”)到底是匿名管道还是命名管道呢?
    匿名管道喔。他们都是bash的子进程。

system V进程间通信

system V进程间通信基本概念

管道通信本质是基于文件的,也就是说操作系统并没有为此做过多的设计工作,而system V IPC是操作系统特地设计的一种通信方式。但是不管怎么样,它们的本质都是一样的,都是在想尽办法让不同的进程看到同一份由操作系统提供的资源。
system V 进程通信三种模式:

  1. system V共享内存
  2. system V消息队列
  3. system V信号量
    接下来,我们将详细谈论三种模式

system V共享内存

共享内存让不同进程看到同一份资源的方式就是,在物理内存当中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫做共享内存。
在这里插入图片描述
进程在通信之时,在堆栈之间的共享内存开辟一块物理内存,该物理内存就是通信的场所。这个场所就是快递站,你把消息放快递站,我把快递拿出来。
共享内存的建立与释放
共享内存的建立大致包括以下两个过程:

  1. 在物理内存当中申请共享内存空间。

  2. 将申请到的共享内存挂接到地址空间,即建立映射关系。
    共享内存的释放大致包括以下两个过程:

  3. 将共享内存与地址空间去关联,即取消映射关系。

  4. 释放共享内存空间,即将物理内存归还给系统
    共享内存的创建
    创建共享内存我们需要用shmget函数,shmget函数的函数原型如下:

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

shmget函数的参数说明:

第一个参数key,表示待创建共享内存在系统当中的唯一标识。
第二个参数size,表示待创建共享内存的大小。
第三个参数shmflg,表示创建共享内存的方式。
shmget函数的返回值说明:

shmget调用成功,返回一个有效的共享内存标识符(用户层标识符)。
shmget调用失败,返回-1。
接下来我们介绍key值,key为了保证唯一性,所以有了系统函数!
传入shmget函数的第一个参数key,需要我们使用ftok函数进行获取

ftok函数的函数原型如下:

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

ftok函数的作用就是,将一个已存在的路径名pathname和一个整数标识符proj_id转换成一个key值,称为IPC键值,在使用shmget函数获取共享内存时,这个key值会被填充进维护共享内存的数据结构当中。
接下来我们用这两个函数产生一个共享内存接口!

#include<iostream>
#include<string>
#include<cstdlib>
#include<sys/ipc.h>
#include<sys/shm.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#include<sys/types.h>
#include<string.h>
using namespace std;
const string pathname="/home/zsc/lesson15";
const int proj_id=0x11223344;
const size_t size =4096;
key_t Getkey()
{
    key_t key=ftok(pathname.c_str(),proj_id);
 if(key < 0)
    {
        std::cerr << "errno: " << errno << ", errstring: " <<  strerror(errno) << std::endl;
        exit(1);
    }
cout<<"key:"<<key<<endl;
    return key;

}

我们将getkey封装ftok,运行完毕就可以获得一个key。

#include<iostream>

#include"commond.hpp"


int main()
{
//IPC_CREAT不存在就创建 存在就获取并返回
//IPC——CREAT|IPC_EXCL shm不存在就创建,存在就返回错误
    key_t key=Getkey();
    int shmid=shmget(key,size,IPC_CREAT|IPC_EXCL);
    if(shmid<0)
    {
        std::cerr << "errno: " << errno << ", errstring: " <<  strerror(errno) << std::endl;
        return 1;
    }
    cout<<"shmid:"<<shmid<<endl;
    return 0;
}

此时,我们就创建了一个共享内存:通过指令

ipcs -m

在这里插入图片描述
此时我们就查询到了刚刚开辟的共享内存。
当我们关闭之后再次启动xshell发现,共享内存依然存在!
key和shmid的特点比较:
key:不在应用层使用,只用来在内核中标识shm的唯一性!
shmid:应用这个共享内存是,我们用shmid进行操作共享内存。
shmid共享内存的删除:
在这里插入图片描述

ipcrm -m shmid

共享内存的权限:
共享内存的权限可以在shmget中加上:
在这里插入图片描述
此时查看,便给共享内存获得了权限。perms就是权限
在这里插入图片描述
共享内存的挂载
共享内存的挂载就是将虚拟内存映射到物理内存。

在这里插入图片描述
这是用到的函数:shmat

char *s=(char*)shmat(shmid,nullptr,0);

如此我们便可以把内存挂载到系统分配的物理内存之中。
nattch:是检测多少个进程链接到共享内存上。
在这里插入图片描述
进程结束,则链接断开。
那么如何将shm从进程中移除呢?

shmdt

在这里插入图片描述
那我想彻底从OS删除怎么办呢?

 shmctl(shmid,IPC_RMID,nullptr);

所以共享内存的程序级别操作是以下流程:
在这里插入图片描述
接下来,我们进行进程通信
在这里插入图片描述
我们可以看到 写完了,但是另一边一直在读取。说明这种通信方式进程不是同步的。 删除线格式
共享内存特点:

  1. 共享内存的生命周期是随内核的!
  2. 共享内存不同步通信
  3. 共享内存是所有进程通信速度最快的

共享内存和管道的对比

共享内存的速度是进程通信最快的。当共享内存创建好后就不再需要调用系统接口进行通信了,而管道创建好后仍需要read、write等系统接口进行通信。实际上,共享内存是所有进程间通信方式中最快的一种通信方式。
我们先来看看管道通信:
在这里插入图片描述
从这张图可以看出,使用管道通信的方式,将一个文件从一个进程传输到另一个进程需要进行四次拷贝操作:

  1. 服务端将信息从输入文件复制到服务端的临时缓冲区中。
  2. 将服务端临时缓冲区的信息复制到管道中。
  3. 客户端将信息从管道复制到客户端的缓冲区中。
  4. 将客户端临时缓冲区的信息复制到输出文件中。
    我们再来看看共享内存通信:
    在这里插入图片描述

从这张图可以看出,使用共享内存进行通信,将一个文件从一个进程传输到另一个进程只需要进行两次拷贝操作:

  1. 从输入文件到共享内存。
  2. 从共享内存到输出文件。
    所以共享内存是所有进程间通信方式中最快的一种通信方式,因为该通信方式需要进行的拷贝次数最少。

system V 信号量

信号量

信号量本质是一个计数器。
目的是让多个执行流看到同一个资源。

  1. 由于进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系叫做进程互斥。
  2. 系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。
    在进程中涉及到临界资源的程序段叫临界区。
  3. IPC资源必须删除,否则不会自动删除,因为system V IPC的生命周期随内核。

同步和互斥

在这里插入图片描述
特殊的信号量,二元信号量只有0和1,也就是互斥锁。完成互斥功能!
信号量:信号量表示对资源数目的计数器,每一个执行流访问公共资源内的一份资源,不应该让执行流直接访问,而是先申请信号资源,只要信号量减一成功,完成了对资源的预定机制。
如果申请不成功就挂起!
申请资源称为p操作,释放资源称为v操作!
p操作:申请资源 V操作:释放资源!
进程互斥
进程间通信通过共享资源来实现,这虽然解决了通信的问题,但是也引入了新的问题,那就是通信进程间共用的临界资源,若是不对临界资源进行保护,就可能产生各个进程从临界资源获取的数据不一致等问题。

保护临界资源的本质是保护临界区,我们把进程代码中访问临界资源的代码称之为临界区,信号量就是用来保护临界区的,信号量分为二元信号量和多元信号量。

比如当前有一块大小为100字节的资源,我们若是一25字节为一份,那么该资源可以被分为4份,那么此时这块资源可以由4个信号量进行标识。
在这里插入图片描述

system V IPC联系
通过对system V系列进程间通信的学习,可以发现共享内存、消息队列以及信号量,虽然它们内部的属性差别很大,但是维护它们的数据结构的第一个成员确实一样的,都是ipc_perm类型的成员变量。

这样设计的好处就是,在操作系统内可以定义一个struct ipc_perm类型的数组,此时每当我们申请一个IPC资源,就在该数组当中开辟一个这样的结构。
在这里插入图片描述

也就是说,在内核当中只需要将所有的IPC资源的ipc_perm成员组织成数组的样子,然后用切片的方式获取到该IPC资源的起始地址,然后就可以访问该IPC资源的每一个成员了

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值