Linux操作系统-进程间通信

目录

进程间通信(ipc)的种类

进程间通信目的

进程通信的前提

管道

匿名管道

pipe函数

管道的特性 

属性特征

命名管道

mkfifo函数

通信程序

匿名管道与命名管道的区别

共享内存

基本概念

具体操作

唯一标识符key

shmget函数

shmctl函数

shmat函数

shmdt函数

例子


进程间通信(ipc)的种类

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


进程间通信目的

1、数据传输:一个进程需要将他的数据发送给另一个进程。

2、资源共享:多进程之间共享同样的资源。

3、通知事件:一个进程通知其他的进程发生了某种事件(例如进程终止通知父进程)。

4、进程控制:一个进程控制另一个进程的执行。


进程通信的前提

两个进程可以看到同一块资源(内存)


管道

同一块资源是由文件提供的,把这种方式叫做管道。

为什么用文件的形式?

因为子进程创建时,按照父进程的模板打开这些文件,所以所打开的文件都是相同的,并且具有相同的属性(读写等)。

匿名管道

pipe函数

创建一个匿名管道,参数为一个文件描述符数组,pipefd[0]表示读端的文件描述符,pipefd[1]表示写端的文件描述符。

如果创建成功则返回0,否则返回-1。

父进程子进程无法同时对管道数据进行操作,会存在干扰,只能单向通信(半双工)

#include<iostream>
#include<unistd.h>
#include<cstring>
using namespace std;
int main()
{
  int pipefd[2];
  pipe(pipefd);
  cout<<"read fd is "<<pipefd[0]<<" "<<"write fd is "<<pipefd[1]<<endl;
  return 0;
}

管道的特性 

当读完数据时,会把所读走的数据在管道中清除掉。

1、当读条件不满足时(即管道为空),则会被阻塞

#include<iostream>                                                      
#include<unistd.h>
#include<cstring>
using namespace std;
int main()
{
  int pipefd[2];
  pipe(pipefd);//创建管道
  cout<<"read fd is "<<pipefd[0]<<" "<<"write fd is "<<pipefd[1]<<endl;
  
  pid_t id = fork();//创建进程
  if(id < 0)
  {
    cerr<<"fork error!"<<endl;
    exit(1);
  }
  else if(id == 0)
  {
    // child
    close(pipefd[0]);//子进程不关心读
    while(true)
    {
      const char* buf = "hellow father, i am child1";
      write(pipefd[1],buf,strlen(buf));//往管道里写数据
      cout<<"write sucessfully!"<<endl;
      sleep(1);//写入速度慢
    }
  }
  else 
  {
    // father
    char buf[64];
    close(pipefd[1]);//父进程不关心写
    while(true)
    {
      ssize_t s = read(pipefd[0],buf,sizeof(buf)-1);//读取管道的数据
      if(s > 0)
      {
        buf[s] = '\0';
        cout<<"i am father,recv msg is: "<<buf<<endl;
      }
      else if(s < 0)
      {
        cerr<<"read error!"<<endl;
      }
    }
  }                                                      
   return 0;
 }

 

 此时是父进程被阻塞了起来

2、当写条件不满足时(即管道为满),则会被阻塞

#include<iostream>                                                      
#include<unistd.h>
#include<cstring>
using namespace std;
int main()
{
  int pipefd[2];
  pipe(pipefd);//创建管道
  cout<<"read fd is "<<pipefd[0]<<" "<<"write fd is "<<pipefd[1]<<endl;
  
  pid_t id = fork();//创建进程
  if(id < 0)
  {
    cerr<<"fork error!"<<endl;
    exit(1);
  }
  else if(id == 0)
  {
    // child
    close(pipefd[0]);//子进程不关心读
    while(true)
    {
      const char* buf = "hellow father, i am child1";
      write(pipefd[1],buf,strlen(buf));//往管道里写数据
      cout<<"write sucessfully!"<<endl;
    }
  }
  else 
  {
    // father
    char buf[64];
    close(pipefd[1]);//父进程不关心写
    while(true)
    {
      ssize_t s = read(pipefd[0],buf,sizeof(buf)-1);//读取管道的数据
      if(s > 0)
      {
        buf[s] = '\0';
        cout<<"i am father,recv msg is: "<<buf<<endl;
      }
      else if(s < 0)
      {
        cerr<<"read error!"<<endl;
      }
      sleep(1);//读入速度慢
    }
  }                                                      
   return 0;
 }

一瞬间刷屏,子进程写入速度快, 直接把管道写满了(说明管道大小是有限制的),此时子进程写入阻塞状态,父进程每次读取的字节数都是最大值。

3、写端关闭文件描述符,读端读取完数据后,会读到文件结尾,不会发生阻塞

#include<iostream>                                                      
#include<unistd.h>
#include<cstring>
using namespace std;
int main()
{
  int pipefd[2];
  pipe(pipefd);//创建管道
  cout<<"read fd is "<<pipefd[0]<<" "<<"write fd is "<<pipefd[1]<<endl;
  
  pid_t id = fork();//创建进程
  if(id < 0)
  {
    cerr<<"fork error!"<<endl;
    exit(1);
  }
  else if(id == 0)
  {
    // child
    close(pipefd[0]);//子进程不关心读
    int count = 0;
    while(true)
    {
      const char* buf = "hellow father, i am child1";
      write(pipefd[1],buf,strlen(buf));//往管道里写
      cout<<"write sucessfully!"<<endl;
      if(count++ == 5)
        break;
      sleep(1);
    }
    close(pipefd[1]);
  }
  else 
  {
    // father
    char buf[64];
    close(pipefd[1]);//父进程不关心写
    while(true)
    {
      ssize_t s = read(pipefd[0],buf,sizeof(buf)-1);//读取管道的数据
      if(s > 0)
      {
        buf[s] = '\0';
        cout<<"i am father,recv msg is: "<<buf<<endl;
      }
      else if(s < 0)
      {
        cerr<<"read error!"<<endl;
      }
      else 
      {
        cout<<"child is exit"<<endl;//读到文件结尾
      }
      sleep(1);
    }
  }                                                      
   return 0;
 }

当关闭写端时,读端会读到文件结尾,也就是0。

 子进程退出,变成僵尸进程,那是因为父进程没有进行wait,参考这篇文章

Linux操作系统-进程控制_TangguTae的博客-CSDN博客

4、读端关闭文件描述符,写端进程会在后续被杀掉

#include<iostream>                                                      
#include<unistd.h>
#include<cstring>
using namespace std;
int main()
{
  int pipefd[2];
  pipe(pipefd);//创建管道
  cout<<"read fd is "<<pipefd[0]<<" "<<"write fd is "<<pipefd[1]<<endl;
  
  pid_t id = fork();//创建进程
  if(id < 0)
  {
    cerr<<"fork error!"<<endl;
    exit(1);
  }
  else if(id == 0)
  {
    // child
    close(pipefd[0]);//子进程不关心读
    while(true)
    {
      const char* buf = "hellow father, i am child1";
      write(pipefd[1],buf,strlen(buf));//往管道里写
      cout<<"write sucessfully!"<<endl;
      if(count++ == 5)
        break;
      sleep(1);
    }
    close(pipefd[1]);
  }
  else 
  {
    // father
    char buf[64];
    close(pipefd[1]);//父进程不关心写
    int count = 0;
    while(true)
    {
      ssize_t s = read(pipefd[0],buf,sizeof(buf)-1);//读取管道的数据
      if(s > 0)
      {
        buf[s] = '\0';
        cout<<"i am father,recv msg is: "<<buf<<endl;
      }
      else if(s < 0)
      {
        cerr<<"read error!"<<endl;
      }
      else 
      {
        cout<<"child is exit"<<endl;//读到文件结尾
      }
      if(count++ == 5)//读取六次就关闭读端
      {
        close(pipefd[0]);
        break;
      }
      sleep(1);
    }
    int status;
    waitpid(id,&status,0);
    printf("this child is killed by %d signal\n",status&0x7f); //获取进程退出码
  }                                                      
   return 0;
 }

 13号信号是什么呢?kill -l 查看所有信号

属性特征

1、只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。

2、管道提供流式服务

3、一般而言,进程退出,管道释放,所以管道的生命周期随进程

4、一般而言,内核会对管道操作进行同步互斥

5、管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

命名管道

如果是两个不相关的进程如何通信呢?

可以采用命名管道的方式。

mkfifo函数

 

#include<iostream>
#include<sys/types.h>
#include<sys/stat.h>
using namespace std;

int main()
{
  umask(0);//设置umask为0
  if(-1 == mkfifo("./fifo",0644))//创建命名管道,叫fifo,权限644    
  {
    cerr<<"mkfifo error!"<<endl;
    exit(1);
  }

  return 0;
}

 开头的p指名改文件是管道。

通信程序

client

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

int main()
{
  int fd = open("fifo",O_WRONLY);//打开命名管道
  if(fd < 0)
  {
    cerr<<"open fail!"<<endl;
    exit(1);
  }
  pid_t id = fork();//创建一个子进程去执行它,好观察他的退出状态
  if(id == 0)
  {
    char buf[128];
    while(true)
    {
      cout<<"please enter msg# ";
      fflush(stdout);//刷新一下缓冲区
      ssize_t s = read(0,buf,sizeof(buf)-1);//从标准输入读数据
      if(s < 0)
      {
        cerr<<"read error!"<<endl;
      }
      else if(s > 0) 
      {
        buf[s] = '\0';
        write(fd,buf,s);//将读到的数据发送给Server端
      }
    }
  }
  int status;//观察进程退出状态
  waitpid(id,&status,0);
  printf("the client is killed by %d signal\n",status&0x7f);
  close(fd);
  return 0;                                                            
}                                                 

server

#include<iostream>                                   
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
using namespace std;

int main()
{
  int fd = open("fifo",O_RDONLY);
  if(fd < 0)
  {
    cerr<<"open fail!"<<endl;
    exit(1);
  }
  char buf[128];
  while(true)
  {
    ssize_t s = read(fd,buf,sizeof(buf)-1);//从管道中读取数据
    if(s < 0)
    {
      cerr<<"read error!"<<endl;
    }
    else if(s > 0)
    {
      buf[s] = '\0';
      cout<<buf<<endl;
    }
    else //读到结束标志
    {
      cout<<"client is exit!"<<endl;
      break;
    }
  }
  close(fd);
  return 0;
}

运行结果

写端先关闭:

读端先关闭:

匿名管道与命名管道的区别

FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。

为什么不直接采用文件来传输数据?

如果直接采用文件相当于还需要对磁盘进行读写,速度很慢,效率低

而管道的内容是驻留在内存中而不是写到硬盘上,更高效。


共享内存

基本概念

进程间速度最快的通信方式。

为什么说是最快的方式?参考这篇文章说的还不错

【操作系统】 为什么说共享内存是最快的一种IPC方式呢

总结一下:

主要是两个进程直接看到同一块内存,当一个进程修改时,另一个进程同时看到,相当于两个进程都有一个指针指向同一块内存空间,减少拷贝的次数(出去从外设的拷贝不需要额外的拷贝,而管道还需要将数据拷贝给进程)。

具体操作

唯一标识符key

在创建共享内存之前得需要先生成唯一标识符,这个唯一标识符是为了标识唯一的资源,并且在系统里保持唯一性。

ftok函数

他的本质就是把pathname 和 proj_id这两个信息转换成一个系统里唯一的标识符。

pathname:路径,proj_id:影响不大(随便取,出错的话换一个)

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

using namespace std;
const char* PATHNAME = ".";
const int PROJ_ID = 6666;

int main()
{
  key_t key = ftok(PATHNAME,PROJ_ID);
  printf("key = %d\n",key);                    
  return 0;
}

shmget函数

创建一个共享内存

第一个参数就是之前创建的key值,第二个参数就是共享内存的大小,第三个参数是创建的标志位

共享内存的大小一般取页的整数倍

shmflg 一般取 IPC_CREAT and IPC_EXCL ,而且这个可以设置权限

IPC_CREAT:表明创建一个新的空间

IPC_EXCL :表明需要返回创建的错误(如果出现错误的话)

返回值:

创建成功返回shmid,否则返回-1。

key与shmid:key是用来在系统中标识唯一性的,shmid才是操作共享内存真正需要关心的。

例子: 

#include<iostream>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
using namespace std;
const char* PATHNAME = "./";
const int PROJ_ID = 6666;

int main()
{
  key_t key = ftok(PATHNAME,PROJ_ID);//生成唯一的key
  printf("key = %d\n",key);
  int shmid = shmget(key,4096,IPC_CREAT|IPC_EXCL);//大小4096
  if(shmid < 0)
  {
    perror("shmget");                                 
    exit(1);
  }
  return 0;
}

这里的权限因为没有设置默认为0,此时是对共享内存没办法操作的。

当再次运行时,会报错,因为共享内存已经存在

查看已有共享内存的方法

ipcs命令

 

ipcs -m是查看共享内存

-q查看消息队列

-s查看信号量 

删除共享内存的命令

ipcrm

根据所带的选项,选择通过什么删除。 

解释一下ipcs -m里面的一些参数

perms:代表权限

nattch:代表有多少个进程挂靠

其他的不用多说。

补充:

看看共享内存的实际的数据结构

里面有很多东西是可以和上面的对应起来

例如 shm_segsz 对应的就是大小

shm_nattch 对应的就是nattch

包括第一个结构体

 key就是那唯一的标识,mode 就是创建时后面对应的shmflg

shmctl函数

这个函数是对共享内存进行控制的,

第一个参数shmid,第二个参数就是想怎么控制共享内存,第三个参数是一个结构体,系统有默认的结构体

用户可以跟据自身的所需要的的去更改结构体。

例子:

删除一个共享内存,cmd =  IPC_RMID;

#include<iostream>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>

using namespace std;
const char* PATHNAME = "./";
const int PROJ_ID = 6666;

int main()
{
  key_t key = ftok(PATHNAME,PROJ_ID);
  printf("key = %d\n",key);
  int shmid = shmget(key,4096,IPC_CREAT|IPC_EXCL);
  if(shmid < 0)
  {
    perror("shmget");
    exit(1);
  }
  sleep(5);
  shmctl(shmid,IPC_RMID,nullptr);//5秒后删除共享内存
  return 0;
}                                                  

 

shmat函数

 

shmat函数用来将共享内存与进程所关联起来。

第一个参数shmid

第二个参数是一个地址

一般设置为空,如果shmaddr为空,系统将选择一个合适的(未使用的)地址(虚拟地址)来连接段。本质上是在内存上开辟的物理地址空间需要映射到该进程上的虚拟地址上。这个过程最好让系统自动帮你寻找哪些没有使用。

第三个参数shmflg是用来设置模式。

返回值

 

返回共享内存的虚拟地址(注意是void*),如果不成功返回-1。

shmdt函数

用来取关联

参数直接传入shmat的返回值就可以。

例子

Server

#include<iostream>                                           
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>

using namespace std;
const char* PATHNAME = "./";
const int PROJ_ID = 6666;

int main()
{
  key_t key = ftok(PATHNAME,PROJ_ID);
  printf("key = %d\n",key); 
  int shmid = shmget(key,4096,IPC_CREAT|IPC_EXCL|0666);//创建共享内存
  if(shmid < 0)
  {
    perror("shmget");
    exit(1);
  }
  char* str =(char*) shmat(shmid,nullptr,0);//关联起来
  int i=0;
  while(true)
  {
    *str = 'A'+i;
    i++;
    str++;
    i%=26;
    sleep(2);
  }
  shmdt(str);//解关联
  shmctl(shmid,IPC_RMID,nullptr);//删除共享内存
  return 0;
}

Client

#include<iostream>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>

using namespace std;
const char* PATHNAME = "./";
const int PROJ_ID = 6666;

int main()
{
  key_t key = ftok(PATHNAME,PROJ_ID);
  printf("key = %d\n",key);
  int shmid = shmget(key,4096,0);//由Server端创建,这边默认打开就行
  if(shmid < 0)
  {
    perror("shmget");
    exit(1);
  }
  char* str =(char*) shmat(shmid,nullptr,0);
  while(true)
  {
    cout<<str<<endl;//打印共享内存
    sleep(1);
  }
  shmdt(str);                                           
  return 0;
}

运行结果

 

注意:

共享内存底层不提供任何同步与互斥的机制,需要用户自己提供。也可以用信号量的方式进行同步互斥。

暂时说这两种进程间通信的方式。

信号与信号量这两种通信方式在之后的信号与多线程里面在说。

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值