Linux进程间通信

进程间通信

进程间通信的概念

进程间通信简称IPC(interprocess communication),进程间通信就是在不同进程之间传播或交换信息。

进程间通信的目的

1.数据传输

一个进程需要将它的数据发送给另一个进程。

2.资源共享

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

3.通知事件

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

4.进程控制

有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

进程间通信的本质

让不同的进程能够看到同一份资源

我们前面学到过,各个运行进程之间具有独立性,这个独立性主要体现在数据层面,而代码逻辑层面可以私有也可以公有(例如父子进程),因此各个进程之间要实现通信是非常困难的。

各个进程之间若想实现通信,一定要借助第三方资源,这些进程就可以通过向这个第三方资源写入或读取数据,从而实现通信,这个第三方区域就是系统提供的内存区域

进程间通信的分类

1.管道

匿名管道

命名管道 

2.system V IPC

system V 消息队列 

system V 共享内存 

system V 信号量 

管道

什么是管道

管道是Unix中最古老的进程间通信形式,我们把一个进程连接到另一个进程的数据流称为一个“管道”。

我们可以在linux中查看登录用户个数

who | wc -l

 

我们可以知道,who和wc是两个命令,首先who进程通过标准输出流将数据打到“管道”中,wc进程再从“管道”中读取数据,从而完成数据的处理

who命令是查看云服务器的登录用户(一行显示一个),wc -l用来统计行数

所以输出的是用户的个数

匿名管道

原理

匿名管道用于进程间通信,仅限于本地父子进程之间的通信

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

需要注意:

1.父子进程看到的同一份文件资源是由操作系统维护的,所以父子进程对文件进行写入操作时,该文件缓冲区当中的数据并不会进行写时拷贝。

2.管道用的是文件方案,但是操作系统不会把进程进行通信的数据刷新到磁盘中,因为这样会有IO参与从而降低效率

创建匿名管道

pipe函数

创建匿名管道需要用到pipe函数,函数如下:

int pipe(int pipefd[2]);

pipe函数的参与是一个输出型参数,用于返回两个指向管道读端与写端的文件描述符:

pipefd[0]:管道读端的文件描述符

pipefd[1]:管道写端的文件描述符

调用成功返回0,失败返回-1

使用步骤

在使用匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数一起使用,步骤如下:

1.父进程调用pipe函数创建管道

 

2.父进程创建子进程

 

3.父进程关闭写端,子进程关闭读端

 

需要注意的是:

1.管道只能单向通信,所以创建子进程之后去要确定到底是谁读谁写,然后关闭相应的读写段

2.从管道写端写入的数据会被内核缓冲,直到管道的读端被读取。

再从文件描述符角度来看整个过程:

1.父进程调用pipe函数创建匿名管道

2.父进程创建子进程

3.父进程关闭写端,子进程关闭读端

知道了使用步骤之后,我们可以来写一个例子:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>

int main()
{
  int fd[2] = {0};//创建数组接收输出参数
  if(pipe(fd) < 0)
  {
    perror("pipe");
    return 1;
  }
  pid_t id = fork();//创建子进程
  if(id == 0)//child
  {
    close(fd[0]);//关闭子进程的读端
    const char* msg = "this is child";
    int count = 10;
    while(count--)
    {
      //子进程将消息写入写端
      write(fd[1],msg,strlen(msg));
      sleep(1);
    }
    close(fd[1]);
    exit(0);
  }

  close(fd[1]);//父进程关闭写端
  char buff[64];//用来接受数据
  while(1)
  {
    ssize_t s = read(fd[0],buff,sizeof(buff));//读取
    if(s>0)
    {
      buff[s] = '\0';//方便打印
      printf("child send to father:%s\n",buff);
    }
    else if (s == 0)
    {
      printf("read file end\n");
      break;
    }
    else{
      printf("read error\n");
      break;
    }
  }
  close(fd[0]);
  waitpid(id,NULL,0);
  return 0;
}


运行结果如下:

读写规则

 pipe2函数与pipe函数类似,也是用于创建匿名管道:

int pipe2(int pipefd[2], int flags);

第二个参数为设置选项: 

当没有数据可读时:

1.O_NONBLOCK disable:write调用阻塞,直到有进程读走数据。

2.O_NONBLOCK enable:write调用返回-1,errno值为EAGAIN。

当管道满的时候:

1.O_NONBLOCK disable:write调用阻塞,直到有进程读走数据。

2.O_NONBLOCK enable:write调用返回-1,errno值为EAGAIN。

如果所有管道写端对应的文件描述符被关闭,则read返回0.

如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,可能导致write进程退出

当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。

当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。

特点

1.管道内部带有同步与互斥机制。

我们将一次只允许一个进程使用的资源,称为临界资源。管道在同一个时刻只允许一个进程对其进行写入或是读取操作,因此管道也是一种临界资源。

临界资源需要被保护,如果我们不对管道这种临界资源进行保护,那么可能出现同一时刻有多个进程对同一个管道进行操作的情况,从而导致数据不一致等情况。

所以,产生了同步和互斥:

同步:两个或两个以上的进程在运行过程中协同步调,按预定的先后顺序运行。比如:A任务的运行依赖于B任务产生的数据。

互斥:一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源。

实际上,同步是更复杂的互斥,互斥是特殊的同步,例如管道场景:互斥就是两个进程不可以同时对管道进行操作,会相互排斥,必须等一个进程操作完毕,另一个才能操作;同步其实也是指两个进程不能同时对管道进行操作,但是需要按照顺序来对管道进行操作。

2.管道的生命周期随进程。

 管道本质上是通过文件进行通信,依赖于文件系统,那么所有打开该文件的进程都退出后,文件也会释放掉。

3.管道提供的是流式服务。

对于进程A写入管道当中的数据,进程B从管道读取的数据多少是任意的,这叫做流式服务;那么相对应的就是数据报服务:

1.流式服务:数据没有明确的分割,不分一定的报文段。

2.数据报服务:数据有明确的分割,拿数据报文段拿。

 4.管道是半双工通信的。

数据在线路上的传送方式可以分为以下三种:

1.单工通信:单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定位接收端

2.半双工通信:半双工数据传输指的是数据可以在一个信号载体的两个方向上传输,但是不能同时传输。

3.全双工通信:全双工通信允许数据在两个方向上同时传输,它的能力相当于两个单通道方式结合。全双工可以同时进行信号双向传输。

管道是半双工的,数据只能向一个方向流动,需要双方通信的时候,就会建立起两个管道:

 

四种特殊情况

1.写端进程不写,读端进程一直读,那么此时会因为管道里没有数据可读,对应的读端进程会被挂起,直到管道里有数据后,读端进程才会被唤醒。

2.读端进程不读,写端进程一直写,那么管道写满后,对于的写端进程会被挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。

3.写端进程将数据写完后将写端关闭,那么读端进程将管道当中的数据读完后,将会继续执行之后的代码逻辑,而不会挂起。

4.读端进程将读端关闭,而写端进程还在写入数据,那么系统会将写端进程杀掉。

匿名管道大小

我们前面知道,管道是在内存中的,那么就会占据内存空间,况且管道也会溢出,那么管道的最大容量是多少呢?

方法一:man手册

我们可以使用 man 7 pipe 查看pipe大小:

可以看到在2.6.11之后的Linux版本中,pipe最大为65536字节。

 方法二:ulimit命令

我们可以使用 ulimit -a 来查看管道大小:

可以看到,显示的最大容量为 512 x 8 = 4096字节

方法三:自行测试

方法一和二的容量不同,那么我们可以自行测试:

#include<unistd.h>    
#include<stdio.h>    
#include<stdlib.h>    
#include<sys/wait.h>    
int main()    
{    
  int fd[2] = {0};    
  if(pipe(fd)< 0) //创建匿名管道   
  {    
    perror("pipe");    
    return 1;    
  }    
  pid_t id = fork();//创建子进程    
  if(id == 0)    
  {    
   close(fd[0]);//子进程关闭读端    
   char c = 'a';    
   int count = 0;    
   while(1)//循环写入,每次一个字节,并打印count++    
   {    
     write(fd[1],&c,1);    
     count++;    
     printf("%d\n",count);    
   }    
    close(fd[1]);//子进程关闭写端    
    exit(0);//子进程退出    
  }    
  close(fd[1]);//父进程关闭写端    
  waitpid(id,NULL,0);//等待子进程    
  close(fd[0]);//父进程关闭读端
  return 0;//退出   
}    

运行结果如下:

 可以看到,我们当前的版本管道最大容量为65536字节。

命名管道

原理

我们知道,匿名管道只适用在有亲缘关系的进程,例如一个管道由一个进程创建,然后该进程创建子进程,最后父子进程就可以使用该管道了。

那么如果要实现两个不想关进程的通信,那么此时就需要用到命名管道了,命名管道时一个特殊类型的文件,两个进程通过命名管道的文件名来打开同一个管道文件,那么此时也就看到了同一份资源,就可以进行通信了

需要注意的是:

1.普通文件很难做到通信,即使能做到也无法解决一些安全问题

2.命名管道和匿名管道一样,是内存文件,只不过命名管道在磁盘有一个映像,大小为0,因为命名管道和匿名管道都不会将数据刷新到磁盘中

创建命名管道文件

我们可以使用 mkfifo 创建一个命名管道:

 可以看到,有着特殊黄色的fifo就是我们刚创建的命名管道文件,开头的文件类型为P(管道文件)。

此时,我们就可以使用这个文件来实现两个进程之间的通信了。例如,我们在进程A用shell脚本向命名管道每秒写入一个字符串,在进程B用cat命令在命名管道中读取。

while :;do echo "this is fifo";sleep 1;done > fifo

 结果:

命名管道函数原型

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

这里有两个参数:

参数一:pathname,表示要创建的命名管道文件 

1.如果pathname是路径名,那么命名管道会创建在pathname的路径下。

2.如果pathname为文件名,那么命名管道会创建在当前路径。 

参数二:mode,表示创建命名管道文件的默认权限 

 我们将mode设置成0666,那么命名管道文件的默认权限为:

但是,我们还知道有一个umask的影响,实际创建出来文件的权限为:mode&(~umask),umask一般为0002,所以我们设置mode为0666时,实际的文件权限为0664。

我们可以将umask将默认掩码设置为0,那么就不会受到umask的影响了。

umask(0); 

 mkfifo函数的返回值

1.创建成功返回0

2.创建失败返回-1

接下来我们尝试创建一个命名管道fifo:

#include<stdio.h>    
#include<sys/types.h>    
#include<sys/stat.h>    
    
#define FILE_NAME "fifo"//将命名管道文件名设置为fifo
    
int main()    
{    
  umask(0);//让mode直接控制权限    
  if(mkfifo(FILE_NAME,0666) < 0)    
  {    
    perror("mkfifo");    
    return 1;    
  }    
  return 0;    
} 

运行之后,就会发现当前路径下多出一个fifo文件:

打开规则

如果打开操作是为读去打开FIFO时:

1.O_NONBLOCK disable:阻塞直到有相应进程为写而打开FIFO。

2.O_NONBLOCK enable:返回成功。

如果打开操作是为写而打开FIFO时:

1.O_NONBLOCK disable:阻塞直到有相应进程为读而打开FIFO。

2.O_NONBLOCK disable:返回失败,错误码为ENXIO。 

实现serve&client通信

要实现服务端与客户端之间的通信,我们需要先让服务端运行起来,创建一个命名管道,再打开管道,然后再运行客户端,继续打开管道,两个端分别一个读一个写,这样就可以相互通信了

服务端:

#include"comm.h"    
    
int main()    
{    
  umask(0);//将mode直接控制权限    
    
  //创建管道文件    
  if(mkfifo(FILE_NAME,0666)<0)    
  { 
    perror("mkfifo");    
    return 2;    
  }    
    
  //以读方式打开命名管道文件    
  int fd = open(FILE_NAME,O_RDONLY);    
  if(fd<0)    
  {    
    perror("open");    
    return 2;    
  }    
    
  char msg[120];//接收管道信息    
  while(1)    
  {    
    msg[0] = '\0';//读取前先将msg置空    
    ssize_t s = read(fd,msg,sizeof(msg)-1);//读取管道内容    
    if(s>0)    
    {    
      msg[s] = '\0';//方便打印    
      printf("client:%s\n",msg);    
    }    
    else if(s == 0)    
    {    
      printf("client quit!\n");    
      break;    
    }    
    else{    
      printf("read error!\n");    
      break;    
    }    
  }    
  close(fd);    
  return 0;    
}

 客户端:

#include"comm.h"    
    
int main()    
{    
  int fd = open(FILE_NAME,O_WRONLY);    
  if(fd < 0)    
  {    
    perror("open");    
    return 1;    
  }    
  char msg[128];//接收数据    
  while(1)    
  {    
    msg[0] = '\0';//清空msg    
    printf("pleas enter: ");//提示输入    
    fflush(stdout);//将输入的内容刷新到stdout    
    ssize_t s = read(0,msg,sizeof(msg)-1);//从输入流读取信息    
    if(s>0)    
    {    
      msg[s-1] = '\0';//方便读取
      write(fd,msg,strlen(msg));    
    }    
  }    
  close(fd);    
  return 0;    
}

 头文件如下:

#pragma once    
    
#include<stdio.h>    
#include<unistd.h>    
#include<sys/types.h>    
#include<sys/stat.h>    
#include<string.h>    
#include<fcntl.h>    
    
#define FILE_NAME "myfifo"

当我们运行服务端(client)之后:可以看到,当前路径多了一个myfifo命名管道文件,这个就是我们在头文件下定义的的名称:

 接着运行客户端之后,我们可以在服务端开始输入数据,那么此时服务端就会接收到客户端输入的数据

至此,我们完成了让两个不相关的进程进行了通信。 

实现派发计算任务

这个其实与上面的通信相类似,只是这里需要服务端对接收到的数据进行处理,所以我们只需要更改服务端的处理信息方式即可。

服务端:

  #include"comm.h"
  
  int main()
  {
    umask(0);//将mode直接控制权限
  
    //创建管道文件
    if(mkfifo(FILE_NAME,0666)<0)
    {
      perror("mkfifo");
      return 2;
    }
  
    //以读方式打开命名管道文件
    int fd = open(FILE_NAME,O_RDONLY);
    if(fd<0)
    {
      perror("open");
      return 2;
    }
  
    char msg[120];//接收管道信息
    while(1)
    {
      msg[0] = '\0';//读取前先将msg置空
      ssize_t s = read(fd,msg,sizeof(msg)-1);//读取管道内容
      if(s>0)//开始处理
      {
        msg[s] = '\0';//方便打印
        printf("client: %s\n",msg);
        //开始处理计算
        char* lable = "+-*/%";
        char* p = msg;
        int flag = 0;
        while(*p)
        {   
          switch(*p)
          {
            case '+':
              flag = 0;
              break;
            case '-':
              flag = 1;
              break;
            case '*':
              flag = 2;
              break;
            case '/':
              flag = 3;
              break;
            case '%':
              flag = 4;
              break;       
          }
          ++p;
        }
        char* data1 = strtok(msg,"+-*/%");
        char* data2 = strtok(NULL,"+-*/%");
        int num1 = atoi(data1);
        int num2 = atoi(data2);
        int ret = 0;
        switch(flag)
        {
          case 0:
            ret = num1+num2;
            break;
          case 1:
            ret = num1-num2;
            break;
          case 2:
            ret = num1*num2;
            break;
          case 3:
            ret = num1/num2;
            break;
          case 4:
            ret = num1%num2;
            break;
        }
        printf("%d %c %d = %d\n",num1,lable[flag],num2,ret);
      }
      else if(s == 0)
      {
        printf("client quit!\n");
        break;
      }
      else{
        printf("read error!\n");
        break;
      }
    }
    close(fd);
    return 0;
  }

按照前面的步骤分别打开运行两个进程:然后在客户端输入需要计算的任务即可: 

实现进程遥控

我们还可以通过通信来使一个进程来控制另一个进程,利用在进程控制学过的进程替换即可,还是一样,只需要对处理信息的服务端处理即可:

  #include"comm.h"
  
  int main()
  {
    umask(0);//将mode直接控制权限
  
    //创建管道文件
    if(mkfifo(FILE_NAME,0666)<0)
    {
      perror("mkfifo");
      return 2;
    }
  
    //以读方式打开命名管道文件
    int fd = open(FILE_NAME,O_RDONLY);
    if(fd<0)    
    {    
      perror("open");    
      return 2;    
    }    
      
    char msg[120];//接收管道信息
    while(1)    
    {    
      msg[0] = '\0';//读取前先将msg置空    
      ssize_t s = read(fd,msg,sizeof(msg)-1);//读取管道内容    
      if(s>0)//开始处理    
      {    
        msg[s] = '\0';//方便打印    
        printf("client: %s\n",msg);    
        //开始    
        if(fork()==0)    
        {    
          execlp(msg,msg,NULL);//进程程序替换    
          exit(1);                                                                          
        }    
        waitpid(-1,NULL,0);//等待子进程    
      }    
      else if(s == 0)    
      {    
        printf("client quit!\n");    
        break;    
      }    
      else{    
        printf("read error!\n");
        break;
      }
    }
    close(fd);
    return 0;
  }

运行之后,我们在客户端输入指令: 

 此时服务端就会输出相应的结果:

实现文件拷贝

例如,我们需要拷贝file.txt文件:

利用管道的原理,我们先用客户端通过管道将file.txt文件发送给服务端,然后在服务端创建一个file-copy.txt文件,然后通过服务端读取管道的数据写入file-copy.txt文件中,从而实现文件拷贝。

服务端:创建管道文件,然后创建file-copy.txt文件,然后读取管道内的数据写入到file-copy.txt文件

#include"comm.h"

int main()
{    
  umask(0);    
  if(mkfifo(FILE_NAME,0666) < 0)    
  {    
    perror("mkfifo");    
    return 1;    
  }    
    
  int fd = open(FILE_NAME,O_RDONLY);    
  if(fd<0)    
  {    
    perror("open1");    
    return 2;    
  }    
    
  int fdout = open("file-copy.txt",O_CREAT | O_WRONLY, 0666);    
  if(fdout < 0)    
  {    
    perror("open2");    
    return 3;    
  }                                                                                                                                                                                         
    
  char msg[128];

  while(1)    
  {    
    msg[0] = '\0';    
    ssize_t s = read(fd,msg,sizeof(msg)-1);    
    if(s>0)    
    {    
      write(fdout,msg,s);    
    }    
    else if(s == 0)    
    {    
      printf("client quit!\n");    
      break;    
    }    
    else{    
      printf("read error!\n");    
      break;    
    }    
  }    
  close(fd);
  close(fdout);
  return 0;
}

客户端:打开管道,读取file.txt文件里的数据写入到管道里

#include"comm.h"    
    
int main()    
{    
  umask(0);    
  int fd = open(FILE_NAME,O_WRONLY);    
  if(fd < 0)    
  {    
    perror("open1");    
    return 1;    
  }    
    
  int fdin = open("file.txt",O_RDONLY);    
  if(fdin < 0)    
  {    
    perror("open2");    
    return 2;    
  }    
    
    
  char msg[128];//接收数据    
  while(1)    
  {    
    ssize_t s = read(fdin,msg,sizeof(msg));    
    if(s>0)    
    {    
      write(fd,msg,s);    
    }    
    else if(s == 0)    
    {    
      printf("read end of all!\n");    
      break;    
    }    
    else{    
      printf("read error");    
      break;    
    }    
  }    
  close(fd);    
  close(fdin);                                                                                                                                                                              
  return 0;    
}

我们首先运行serve进程,然后再运行client进程,结果如下: 

此时当前路径就会多出一个file-copy.txt文件:

其实我们可以发现,这个文件拷贝其实就相当于windows上的文件上传和下载,我们只要将管道想象成网络, 本地向服务器通过网络传输叫做上传,反过来就是下载。

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

1.匿名管道由pipe函数创建并打开

2.命名管道由mkfifo函数创建,open函数打开 

3.FIFO与pipe区别就是创建和打开方式不同 

命令行中的管道

我们现在有一份data.txt的文件,内容如下:

我们可以通过管道符号来使用cat和grep命令来抓取我们想要的特定内容:

那么问题来了,这里的管道是匿名管道还是命名管道呢?

因为匿名管道是用于有亲缘关系的进程通信,命名管道是用于两个不相关的进程通信。

若是两个进程之间采用的是命名管道,那么在磁盘上必须有一个对应的命名管道文件名,而实际上我们在使用命令的时候并不存在类似的命名管道文件名,因此命令行上的管道实际上是匿名管道。

system V进程通信

在前面我们了解到,管道通信本质时基于文件的,也就是说操作系统并没有为此做过多的设计工作,而system V IPC是操作系统设计的,与管道一样,都是为了让不同进程看到同一份资源而通信

system V IPC提供的通信方式有三种:

1.system V共享内存 

2.system V消息队列 

3.system V信号量 

system V共享内存和消息队列是以传送数据为目的,system V信号量是为了保证进程间的同步与互斥而设计的,可以说system V信号量是为了维护通信的标准。 

system V共享内存

原理

共享内存让不同进程看到同一份资源的方式就是,在物理内存当中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表建立映射,然后再虚拟地址空间中开辟空间,将虚拟内存填充到页表的对应位置,使得物理内存和虚拟内存建立起对应关系,那么这就叫做共享内存。简而言之,就是让两个进程的虚拟地址都映射到同一个物理空间。

上述操作都是调用系统接口完成,也就是操作系统自己完成。

共享内存的数据结构

数据结构如下:

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 */
};

当我们申请了一块共享内存后,为了让要实现通信的进程能够看到同一个共享内存,因此每一个共享内存被申请时都有一个key值,这个key值用于标识系统中共享内存的唯一性。
可以看到上面共享内存数据结构的第一个成员是shm_perm,shm_perm是一个ipc_perm类型的结构体变量,每个共享内存的key值存储在shm_perm这个结构体变量当中,其中ipc_perm结构体的定义如下:

struct ipc_perm{
	__kernel_key_t  key;
	__kernel_uid_t  uid;
	__kernel_gid_t  gid;
	__kernel_uid_t  cuid;
	__kernel_gid_t  cgid;
	__kernel_mode_t mode;
	unsigned short  seq;
};

共享内存的创建

建立一个共享内存需要用到下列函数

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

参数说明:

第一个参数:key,表示待创建的共享内存在系统当中的唯一标识

第二个参数size:标识待创建的共享内存大小 

第三个参数shmflg,表示创建共享内存的方式 

 返回值说明:

1.shmget调用成功,返回一个有效的共享内存标识符

2.shmget调用失败,返回-1 

需要注意的是:shmget返回的标识符其实是共享内存的句柄,我们把可以标定某种资源能力的东西叫做句柄,这个句柄可以在用户层标识共享内存,当共享内存被创建后,我们在后续使用共享内存的相关接口时,都是需要通过这个句柄对指定共享内存进行各种操作。

 shmget函数的第一个参数key,需要用ftok函数进行获取

ftok函数:

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

ftok函数的作用:

将一个已存在的路径名pathname和一个整数标识符proj_id转换成一个key值,称为IPC键值,在使用shmget函数获取共享内存时,这个key值会被填充进维护共享内存的数据结构当中,需要注意的是,pathname所指定的文件必须存在且可存取

需要注意的是:

1.ftok函数生成的key值可能会冲突,可以对ftok函数参数进行修改 

2.我们令两个需要进行通信的进程用相同的参数来构成ftok函数,就能获取到相同的key值,就可以找到同一个共享内存了。

  shmget函数的第三个参数shmflg,有着以下两种组合方式

1.IPC_CREAT:如果内核中不存在键值与key相等的共享内存,那么新建一个共享内存并返回该共享内存的句柄;如果存在,那么直接返回存在的共享内存句柄

2.IPC_CREAT | IPC_EXCL:如果内核中不存在与key值相等的共享内存,那么新建一个共享内存并返回该共享内存的句柄;如果存在,那么出错返回

现在,我们可以使用刚刚学到的知识创建一块共享内存了,代码如下:

#include<stdio.h>    
#include<sys/types.h>    
#include<sys/ipc.h>    
#include<sys/shm.h>    
#include<unistd.h>    
    
#define PATHNAME "/home/bear/testcode/IPC/shmget.c"    
    
#define PROJ_ID 0x6666    
#define SIZE 4096    
    
int main()    
{    
  key_t key = ftok(PATHNAME,PROJ_ID);    
  if(key<0)    
  {    
    perror("ftok");    
    return 1;    
  }    
  int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL);    
  if(shm < 0)    
  {    
    perror("shmget");    
    return 2;    
  }    
  printf("key : %x\n",key);    
  printf("shm : %d\n",shm);    
  return 0; 
    
}

运行结果如下: 

那么如何查看呢?我们可以使用ipcs命令进行查看,单独使用的话,会列出消息队列、共享内存和信号量的信息,如果只想查看某一个的信息,就需要携带选项了。

-q:消息队列

-m:共享内存

-s:信号量

 我们刚刚创建的是共享内存,所以使用 ipcs -m:

 ipcs命令输出的结果的每列含义:

标题含义
key系统区别各个共享内存的唯一标识
shmid共享内存的用户层id(句柄)
owner共享内存的拥有者
perms共享内存的拥有者
bytes共享内存的大小
nattch关联共享内存的进程数
status共享内存的状态

需要注意的是:key是在内核层面上保证共享内存唯一性的方式,而shmid是在用户层面上保护共享内存的唯一性,类似fd和FILE*的关系。

共享内存的释放

共享内存是随内核的,只要不关机,那么共享内存就不会删除。那么如何主动删除共享内存呢?

1.使用命令释放共享内存

2.在进程通信完毕后调用释放共享内存的函数进行释放

使用命令释放

使用 ipcrm -m shmid 命令释放:

使用共享内存的用户层id,也就是shmid

使用函数释放

int shmctl(int shmid, int cmd, struct shmid_ds *buf)

参数说明:

第一个参数shmid:标识控制共享内存的用户级标识符 

第二个参数cmd:表示具体的控制动作 

第三个参数buf:用户获取或设置所控制共享内存的数据结构 

 返回值说明:

调用成功:返回0

调用失败:返回-1 

shmctl的第二个参数选项:

1.IPC_STAT:获取共享内存的关联值:此时参数buf为输出型参数

2.IPC_SET:进程有足够权限下,将共享内存关联值设置为buf所指的数据结构中的值

3.IPC_RMID:删除共享内存段

例如下列代码:

#include<stdio.h>    
#include<sys/types.h>    
#include<sys/ipc.h>    
#include<sys/shm.h>    
#include<unistd.h>    
    
#define PATHNAME "/home/bear/testcode/IPC/shmget.c"    
    
#define PROJ_ID 0x6666    
#define SIZE 4096    
    
int main()    
{    
  key_t key = ftok(PATHNAME,PROJ_ID);    
  if(key<0)    
  {    
    perror("ftok");    
    return 1;    
  }    
  int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL);    
  if(shm < 0)    
  {    
    perror("shmget");    
    return 2;    
  }    
  printf("key : %x\n",key);    
  printf("shm : %d\n",shm);    
    
  sleep(3);    
  shmctl(shm,IPC_RMID,NULL);    
  sleep(2);     
  return 0; 
} 

运行结果如下: 

可以发现,运行结束后,共享内存也不见了。 

共享内存的关联

 我们需要使用shmat函数将共享内存连接到进程地址空间:

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

参数说明:

第一个参数shmid:表示待关联共享内存的用户级标识符 

第二个参数shmaddr:指定共享内存映射到进程地址空间的某一地址,通常为NULL,表示让内核自己决定合适的地址位置。 

第三个参数shmflg:表示关联共享内存时设置的某些属性 

返回值说明:

调用成功:返回映射到进程空间地址的起始地址

调用失败:返回(void*) -1 

shmat函数的第三个参数有以下选项:

1.SHM_RDONLY:关联共享内存后只进行读取操作

2.SHM_RND:若shmaddr不为NULL,则关联地址自动向下调整为SHMLBA的整数倍。公式为:shmaddr-(shmaddr%SHMLBA)

3.0:默认为读写权限 

此时我们就可以尝试关联共享内存了:

  #include<stdio.h>    
  #include<sys/types.h>    
  #include<sys/ipc.h>    
  #include<sys/shm.h>    
  #include<unistd.h>    
      
  #define PATHNAME "/home/bear/testcode/IPC/shmget.c"    
      
  #define PROJ_ID 0x6666    
  #define SIZE 4096    
      
  int main()    
  {    
    key_t key = ftok(PATHNAME,PROJ_ID);    
    if(key<0)    
    {    
      perror("ftok");    
      return 1;    
    }    
    int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL);    
    if(shm < 0)    
    {    
      perror("shmget");    
      return 2;    
    }    
    printf("key : %x\n",key);    
    printf("shm : %d\n",shm);    
      
    printf("attach begin!\n");    
    sleep(3);    
    char* mem = shmat(shm, NULL, 0);                                                        
    if(mem == (void*)-1)    
    {    
      perror("shmat");    
      return 1;    
    }    
    printf("attach end!\n");    
    sleep(2);    
      
    shmctl(shm, IPC_RMID,NULL);    
    return 0;    
      
  }

运行结果如下: 

可以看到nattch为1,说明已经关联成功。

另外,有些情况下创建出来的共享内存权限不够,所以不能关联,我们需要在第三个参数设置权限

int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666)

共享内存的去关联

int shmdt(const void *shmaddr);

参数说明:

第一个参数为关联共享内存的起始地址,也就是shmat函数返回的起始地址

返回值说明:

调用成功,返回0

调用失败:返回-1

接下来我们就可以取消关联了:

  #include<stdio.h>
  #include<sys/types.h>
  #include<sys/ipc.h>
  #include<sys/shm.h>
  #include<unistd.h>
  
  #define PATHNAME "/home/bear/testcode/IPC/shmget.c"
  
  #define PROJ_ID 0x6666
  #define SIZE 4096

  int main()
  {    
    key_t key = ftok(PATHNAME,PROJ_ID);    
    if(key<0)    
    {    
      perror("ftok");                      
      return 1;    
    }    
    int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL);    
    if(shm < 0)    
    {    
      perror("shmget");                                   
      return 2;    
    }    
    printf("key : %x\n",key);    
    printf("shm : %d\n",shm);    
         
    printf("attach begin!\n");    
    sleep(3);                    
    char* mem = shmat(shm, NULL, 0);    
    if(mem == (void*)-1)          
    {            
      perror("shmat");                  
      return 1;             
    }    
    printf("attach end!\n");    
    sleep(3);      
                                                                                                                                                                                            
    printf("detach begin!\n");    
    sleep(3);    
      
    shmdt(mem);                   
    printf("detach end!\n");    
    sleep(3);    
      
    shmctl(shm, IPC_RMID,NULL);    
    return 0;    
      
  }

运行结果如下: 

可以看到,共享内存的nattch从1变回了0,说明取消关联成功了: 

共享内存实现serve&client通信

我们现在可以用共享内存再现serve&client通信了:

服务端:创建共享内存,与服务端关联,随后等待。

  #include"comm.h"    
      
  int main()    
  {    
    key_t key = ftok(PATHNAME,PROJ_ID); //获取key    
    if(key < 0)    
    {    
      perror("ftok");    
      return 1;    
    }    
      
    int shm = shmget(key,SIZE,IPC_CREAT | IPC_EXCL | 0666);//创建共享内存    
    if(shm < 0)    
    {    
      perror("shmget");    
      return 2;    
    }    
      
    printf("key: %x\n",key);    
    printf("shm: %d\n",shm);    
      
    char* mem = shmat(shm,NULL,0);//关联共享内存                                            
      
    while(1)    
    {    
      //等待    
    }    
      
    shmdt(mem);//去关联    
    shmctl(shm,IPC_RMID,NULL);//释放共享内存    
    return 0;    
      
  }

客户端: 获取相同的key与shmid值,关联与服务端同一份共享内存:

  #include "comm.h"    
      
  int main()    
  {    
    key_t key = ftok(PATHNAME, PROJ_ID); //获取与server进程相同的key值    
    if (key < 0){    
      perror("ftok");    
      return 1;    
    }    
    int shm = shmget(key, SIZE, IPC_CREAT); //获取server进程创建的共享内存的用户层id    
    if (shm < 0){    
      perror("shmget");    
      return 2;    
    }    
      
    printf("key: %x\n", key);                                                               
    printf("shm: %d\n", shm);    
      
    char* mem = shmat(shm, NULL, 0); //关联共享内存    
      
    int i = 0;    
    while (1){    
      //不进行操作    
    }    
      
    shmdt(mem); //共享内存去关联    
    return 0;    
  }

先运行newserve,再运行newclient,可以看到共享内存的nattch变为2了: 

此时说明可以正常通信了。 

共享内存与管道进行对比

先看管道的通信:

可以看到,管道创建好之后还是需要read、write等系统接口进行通信,并且管道将一个文件进行通信需要四次拷贝操作

1.服务端将信息从输入文件复制到服务端临时缓冲区中 

2.将服务端临时缓冲区的信息复制到管道 

 3.客服端将信息从管道复制到客服端缓冲区

4.将客户端临时缓冲区的信息复制到输出文件中 

 再看共享内存的通信:

共享内存是最快的通信方式,只需要拷贝两次:

1.从输入文件到共享内存

2.从共享内存到输出文件 

这是共享内存的优点,但是也有缺点:

管道是自带同步与互斥的,而共享内存没有任何保护机制,当然也没有同步与互斥机制

system V消息队列

基本原理

消息队列就是在系统中创建了一个队列,队列中每个成员都是一个数据块,数据块包含了类型和信息两个部分,两个相互通信的进程看到同一个消息队列,这两个进程互相向对方发生数据时,都在队尾添加数据块,同样的,都从对头获取数据块。

消息队列的数据块也是需要自行删除的。 

消息队列的数据结构

struct msqid_ds {
	struct ipc_perm msg_perm;
	struct msg *msg_first;      /* first message on queue,unused  */
	struct msg *msg_last;       /* last message in queue,unused */
	__kernel_time_t msg_stime;  /* last msgsnd time */
	__kernel_time_t msg_rtime;  /* last msgrcv time */
	__kernel_time_t msg_ctime;  /* last change time */
	unsigned long  msg_lcbytes; /* Reuse junk fields for 32 bit */
	unsigned long  msg_lqbytes; /* ditto */
	unsigned short msg_cbytes;  /* current number of bytes on queue */
	unsigned short msg_qnum;    /* number of messages in queue */
	unsigned short msg_qbytes;  /* max number of bytes on queue */
	__kernel_ipc_pid_t msg_lspid;   /* pid of last msgsnd */
	__kernel_ipc_pid_t msg_lrpid;   /* last receive pid */
};

 第一个成员是msg_perm,和shm_perm是同一个类型的结构体变量,ipc_perm的结构体定义如下:

struct ipc_perm{
	__kernel_key_t  key;
	__kernel_uid_t  uid;
	__kernel_gid_t  gid;
	__kernel_uid_t  cuid;
	__kernel_gid_t  cgid;
	__kernel_mode_t mode;
	unsigned short  seq;
};

消息队列的创建

这里需要使用到msgget函数:

int msgget(key_t key, int msgflg);

1.和前面一样,创建消息队列也需要ftok函数生成key值,作为msgget的第一个参数

2.msgget函数的第二个参数,与shmget函数的第三个参数相同

3.创建成功时,会返回一个有效的消息队列标识符,即用户层标识符

消息队列的释放

这里需要用到msgctl函数:

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

 这里的参数与shmctl的相同,只是第三个参数传入的是消息队列相关的数据结构

发送数据

 这里需要用到msgsnd函数:

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

参数说明: 

第一个参数msqid:表示消息队列的用户级标识符 

第二个参数msgp:表示待发送的数据块 

 这个参数的必须为下列结构体:

struct msgbuf{
    long mtype;//数据类型
    char mtext[1];//数据
};

 mtext的大小可以在定义结构体时自己指定。

 第三个参数msgsz:表示所发送数据块的大小

第四个参数msgflg:表示发送数据块的方式,一般默认为0即可 

返回值说明:

调用成功返回0

调用失败返回-1

接收数据

这里需要用到msgrcv函数:

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

参数说明:

第一个参数msqid:表示消息队列的用户级标识符 

第二个参数msgp:表示获取到的参数块,是输出型参数 

第三个参数msgsz:表示要获取数据块的大小 

第四个参数msgtyp:表示要接收数据块的类型 

返回值说明:

调用成功:返回获取到的mtext数组中的字节数

调用失败:返回-1 

system V信号量

信号量的概念

1.由于进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系叫做进程互斥。

2.系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源 

3.在进程中涉及到临界资源的程序段叫做临界区 

4.IPC资源必须删除,否则不会自动删除,因为system V IPC的生命周期随内核 

信号量的数据结构

struct semid_ds {
	struct ipc_perm sem_perm;       /* permissions .. see ipc.h */
	__kernel_time_t sem_otime;      /* last semop time */
	__kernel_time_t sem_ctime;      /* last change time */
	struct sem  *sem_base;      /* ptr to first semaphore in array */
	struct sem_queue *sem_pending;      /* pending operations to be processed */
	struct sem_queue **sem_pending_last;    /* last pending operation */
	struct sem_undo *undo;          /* undo requests on this array */
	unsigned short  sem_nsems;      /* no. of semaphores in array */
};

第一个成员时ipc_perm,该结构体定义如下:

struct ipc_perm{
	__kernel_key_t  key;
	__kernel_uid_t  uid;
	__kernel_gid_t  gid;
	__kernel_uid_t  cuid;
	__kernel_gid_t  cgid;
	__kernel_mode_t mode;
	unsigned short  seq;
};

信号量的相关函数

信号集的创建:semget函数 

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

参数说明:

第一个参数为key:含义与上面的key相同 

第二个参数nsems:表示创建信号量的个数

第三个参数semflg:与shmget函数的第三个参数相同 

返回值说明:

创建成功时,返回一个有效的信号量标识符(用户层标识) 

信号量集的删除

这里需要使用到semctl函数:

int semctl(int semid, int semnum, int cmd, ...)

信号集的操作 

 这里需要用到semop函数:

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

进程互斥

前面说到,共享资源来实现通信很快,但是缺乏保护机制,使用不当会引发许多问题,信号量就是用来保护临界区的。

例如当前又一份100字节的资源,25字节一份,那么资源可以分为4份,这个资源就可以用4个信号量来标识

信号量的本质是一个技术器,信号量分为二元信号量和多元信号量。

在二元信号量中,信号量的个数为1,即整个临界资源看成一块。

二元信号量的本质解决了临界资源的互斥问题,我们用下列伪代码类比一下:

例如先运行的进程A,进程A访问共享内存资源,如果sem为1,那么访问成功,此时需要将sem减减,那么进程A就可以对共享内存进行操作了,但是在这期间进程B访问共享内存,又因为sem=0,那么进程B就会被挂起,知道进程A访问结束,sem++,sem为1了,进程B才会唤起,才能对共享内存进行访问操作。

这里的sem--就是P操作,sem++就是V操作。

P操作就是申请信号量,V操作就是释放信号量。

 

system IPC联系

我们了解了共享内存、消息队列和信号量,虽然内部差别很大,但是我们可以查看数据结构,发现他们的第一个成员都是一样的,都为ipc_perm类型的成员变量。

这样有个好处,我们在操作系统定义一个struct ipc_perm类型的数组,每申请一个IPC资源,就开辟一个这样的结构:

也就是说,我们只要将所有IPC资源的ipc_perm成员组织成数组样子,然后切片获取到IPC资源的起始地址,这样就可以访问到IPC资源的所有成员了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值