Linux进程间通信

Linux进程间通信

进程间通信介绍
进程间通信的概念

进程间通信简称IPC(Interprocess communication),进程之间可能会存在特定的协同工作的场景!一个进程要把自己数据交付给另外一个进程,让其进行处理这就做进程间通信

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

我们之前的时候说过进程之间是具有独立性的,其中独立性主要体现在数据层面,即使是我们的父子进程,他们之间虽然代码是共享的但是数据还是私有的。正因为进程是具有独立性的,所以交互数据,成本一定很高(一个进程是看不到另一个进程的资源的)。

因为进程是具有独立性的,那么两个进程要实现互相通信就必须得先看到一份公共的资源!!! 这里的资源其实就是一段内存。它可能以文件方式提供,也可能以队列的方式也有可能提供的就是原始的内存块。这也就是进程通信方式有很多种的原因。

进程间通信的前提本质: 其实是由OS参与,提供一份所有通信进程都能看到的公共资源。

进程间通信分类

管道

  • 匿名管道pipe
  • 命名管道

System V IPC

  • System V 消息队列
  • System V 共享内存
  • System V 信号量

POSIX IPC

  • 消息队列
  • 共享内存
  • 互斥量
  • 条件变量
  • 读写锁
管道
什么是管道

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

比如:

在这里插入图片描述

其中who和wc是我们的两个进程,who进程通过标准输出将数据放到管道中,wc通过标准输入从管道中读取数据,如此一来便实现了进程间通信。

注意:who命令用于查看当前云服务器的登录用户(一行显示一个用户),wc-l用于统计当前的行数。

匿名管道
匿名管道的概念

我们在上面说过,要想让两个进程之间进行通信,前提是必须得让他们看到一份公共的资源。而我们这里的匿名管道就是其中的一种公共资源。 匿名管道用于进程间通信,使用匿名管道进行通信的进程之间必须具有亲缘关系,常用于父子之间通信。

创建匿名管道

我们常用pipe函数来创建匿名管道,我们可以通过man来查看一下pipe函数

在这里插入图片描述

这个函数的用法如下:

 #include <unistd.h>
 功能:创建一无名管道原型
 int pipe(int fd[2]);
 参数fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
 返回值:成功返回0,失败返回错误代码

匿名管道的创建原理:

匿名管道的创建原理如下图所示:

在这里插入图片描述

我们知道一个进程的0,1,2文件描述符是会分配给标准输入、标准输出与标准错误的,那我们这里操作系统为管道文件分配的两个文件描述符应该是3和4吧?

到底是不是呢?我们用一段代码来验证一下即可

#include<stdio.h>
#include<unistd.h>

int main()
{
    int fd[2];
    int ret = pipe(fd);
    if(ret<0)
    {
        perror("pipe");
        return 1;
    }
    else
    {
        printf("fd[0]:%d fd[1]:%d\n",fd[0],fd[1]);
    }
    
    return 0;
}

运行结果:

在这里插入图片描述

可以看到果真如我们所料OS为管道文件分配的两个文件描述符应该是3和4。

注: 大家记管道读写端可以这么来记忆:把0想象成我们的嘴巴因此fd[0]是读端,把1想象成一支笔使用fd[1]是写端。这样记忆可以帮我们记得更深刻且不容易忘记。

在讲了匿名管道的创建原理之后,我们下面再来说一下父子进程通过匿名管道通信的原理

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

  1. fork之前父进程通过pipe函数创建管道

在这里插入图片描述

  1. 父进程通过调用fork函数创建子进程

在这里插入图片描述

  1. fork之后父子进程各自关掉不用的文件描述符

在这里插入图片描述

如此一来父子进程便可以通过匿名管道进行进程间通信了。

我们还可以站在文件描述符的角度来进一步理解管道:

在这里插入图片描述

下面我们来用代码来实现一下父子进程通过匿名管道实现进程间通信

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>

int main()
{
    int fd[2];
    int ret = pipe(fd);
    pid_t id = fork();
    if(ret<0)
    {
        perror("pipe");
        return 1;
    }
    
    if(id==0)
    {
        //child
        //子进程关闭读端
        close(fd[0]);
        const char* msg = "I am a child\n";
        write(fd[1],msg,strlen(msg));
    }
    else
    {
        //parent
        //父进程关闭写端
        close(fd[1]);
        char buffer[64];
        ssize_t s = read(fd[0],buffer,sizeof(buffer));
        if(s>0)
        {
            buffer[s] = 0;
            printf("father get a message: %s",buffer);
        }
    }
    
    return 0;
}

运行结果:

在这里插入图片描述

如此一来父子进程便通过匿名管道完成了进程间通信。

匿名管道读写规则

对于管道的读写规则下面我会分为四种情况来为大家介绍。

一、管道里面没有数据可读,读端阻塞

这里我们让子进程每5秒写一次,父进程每1秒读一次来观察一下现象

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

int main()
{
    int fd[2];
    int ret = pipe(fd);
    pid_t id = fork();
    if(ret<0)
    {
        perror("pipe");
        return 1;
    }
    
    if(id==0)
    {
        //child
        //子进程关闭读端
        close(fd[0]);
        const char* msg = "I am a child\n";
        while(1)
        {
            write(fd[1],msg,strlen(msg));
            sleep(5);//每隔5秒写一次
        }
    }
    else
    {
        //parent
        //父进程关闭写端
        close(fd[1]);
        char buffer[64];
        while(1)
        {
            ssize_t s = read(fd[0],buffer,sizeof(buffer));
        	if(s>0)
            {
                buffer[s] = 0;
            	printf("father get a message: %s",buffer);
        	}
            sleep(1);
        }
    }
    
    return 0;
}

运行结果:

在这里插入图片描述

可以看到由于管道里面一直没数据或者写端速度小于读端速度,导致我们这里的读端被阻塞了。

总结: 当实际在读的时候,如果读端条件不满足(写端速度小于读端速度或者管道里面没数据)读端可以会长时间阻塞。

二、管道里面写满数据,写端阻塞

这里我们让子进程一直写,父进程每5秒读一次来观察一下现象

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

int main()
{
    int fd[2];
    int ret = pipe(fd);
    pid_t id = fork();
    if(ret<0)
    {
        perror("pipe");
        return 1;
    }
    
    if(id==0)
    {
        //child
        //子进程关闭读端
        close(fd[0]);
        const char* msg = "I am a child\n";
        int count = 0;
        while(1)
        {
            count++;
            write(fd[1],msg,strlen(msg));
            printf("count:%d\n",count);
        }
    }
    else
    {
        //parent
        //父进程关闭写端
        close(fd[1]);
        char buffer[64];
        while(1)
        {
            ssize_t s = read(fd[0],buffer,sizeof(buffer));
        	if(s>0)
            {
                buffer[s] = 0;
            	printf("father get a message: %s",buffer);
        	}
            sleep(5);
        }
    }
    
    return 0;
}

运行结果:

在这里插入图片描述

可以看到管道被写满了或者读端速度小于写端速度,导致我们这里的写端阻塞了。

总结:当实际在写的时候,如果写入条件不满足(管道被写满了或者读端速度小于写端速度),写端就可能一直阻塞

三、关闭写端文件描述符

这里我们让子进程每隔1秒写1次,父进程每隔1秒读一次,5秒之后关闭写端文件描述符观察现象。

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

int main()
{
    int fd[2];
    int ret = pipe(fd);
    pid_t id = fork();
    if(ret<0)
    {
        perror("pipe");
        return 1;
    }
    
    if(id==0)
    {
        //child
        //子进程关闭读端
        close(fd[0]);
        const char* msg = "I am a child\n";
        int count = 5;
        while(1)
        {
            count--;
            write(fd[1],msg,strlen(msg));
            sleep(1);
            //5秒后关闭写端文件描述符
            if(count==0)
            {
                close(fd[1]);
                break;
            }
        }
    }
    else
    {
        //parent
        //父进程关闭写端
        close(fd[1]);
        char buffer[64];
        while(1)
        {
            ssize_t s = read(fd[0],buffer,sizeof(buffer));
        	if(s>0)
            {
                buffer[s] = 0;
            	printf("father get a message: %s",buffer);
        	}
            printf("father exit return: %d\n",s);
            sleep(1);
        }
    }
    
    return 0;
}

运行结果:

在这里插入图片描述

我们可以看到当5秒过后,关闭写端文件描述符,读端读到的数据个数为0,这是为什么呢?

在这里插入图片描述

通过查看man手册我们可以知道,如果read的返回值为0,表示读到了文件结尾。

总结: 如果写端不写了并且关闭文件描述符,读端在读取完管道数据后会读到文件结尾。

四、关闭读端文件描述符

这里我们让子进程每隔1秒写1次,父进程每隔1秒读一次,5秒之后关闭读端文件描述符观察现象。

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

int main()
{
    int fd[2];
    int ret = pipe(fd);
    pid_t id = fork();
    if(ret<0)
    {
        perror("pipe");
        return 1;
    }
    
    if(id==0)
    {
        //child
        //子进程关闭读端
        close(fd[0]);
        const char* msg = "I am a child\n";
        while(1)
        {
            write(fd[1],msg,strlen(msg));
            sleep(1);
        }
    }
    else
    {
        //parent
        //父进程关闭写端
        close(fd[1]);
        char buffer[64];
        int count = 0;
        while(1)
        {
            ssize_t s = read(fd[0],buffer,sizeof(buffer));
        	if(s>0)
            {
                buffer[s] = 0;
            	printf("father get a message: %s",buffer);
                sleep(1);
        	}
            if(count++==5)
            {
                close(fd[0]);
            }
        }
    }
    
    return 0;
}

运行结果:

在这里插入图片描述

我们可以看到读端关闭以后,写端变成了僵尸进程,说明写端是被操作系统发信号给终止了。

那写端是被操作系统发多少号信号给终止掉的呢?我们通过代码来验证一下

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

int main()
{
    int fd[2];
    int ret = pipe(fd);
    pid_t id = fork();
    if(ret<0)
    {
        perror("pipe");
        return 1;
    }
    
    if(id==0)
    {
        //child
        //子进程关闭读端
        close(fd[0]);
        const char* msg = "I am a child\n";
        while(1)
        {
            write(fd[1],msg,strlen(msg));
            sleep(1);
        }
    }
    else
    {
        //parent
        //父进程关闭写端
        close(fd[1]);
        char buffer[64];
        int count = 0;
        while(1)
        {
            ssize_t s = read(fd[0],buffer,sizeof(buffer));
        	if(s>0)
            {
                buffer[s] = 0;
            	printf("father get a message: %s",buffer);
                sleep(1);
        	}
            if(count++==5)
            {
                close(fd[0]);
                break;
            }
        }
        int status = 0;
        waitpid(id,&status,0);
        printf("child get a signal:%d\n",status&0x7f);
    }
    return 0;
}

在这里插入图片描述

我们可以看到子进程是被操作系统发送的13号信号给终止掉的。

为什么读端关闭,写端进程可能会被操作系统杀掉呢?

这是因为操作系统不会做浪费系统资源的事情,读端关闭了,证明就没人再去这个管道里面读数据了,既然没人读数据了,那我即使再去写那也没有意义了,所以操作系统会通过给写端发送信号从而将它终止掉。

管道的特点
  1. 匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常用于父子进程间通信,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
  2. 管道提供流式服务。实际读取的时候,每次读取的字符个数是由该进程具体想读多少,和管道里由多少来决定的。这就好像我们开水龙头接水一样,你想多接点水就把水龙头的开关开到最大,如果不想接了关闭水龙头即可。
  3. 进程退出,管道释放,管道的生命周期随进程。
  4. 内核会对管道操作进行同步与互斥,同步:你快我快、你慢我慢,这也就是为什么读端和写端快的一方会被阻塞。互斥:在我读的时候你不能够写,在我写的时候你不能够读。
  5. 管道是半双工的,数据只能从一个方向往另一个方向流动;需要双方通信时,需要建立起两个管道
命名管道
命名管道的原理

匿名管道只能用于用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信。那我们有没有办法让两个没有亲缘关系的进程之间进行通信呢?

为了解决这个问题,我们的命名管道它诞生了。命名管道就是一种特殊类型的文件,其类型是管道文件。

命名管道的原理就是让两个进程打开同一个管道文件,此时这两个进程就看到了同一份资源,因此他们之间就可以进行通信了。

创建命名管道

我们常用mkfifo函数来创建命名管道,我们可以通过man来查看一下mkfifo函数

在这里插入图片描述

这个函数的用法如下:

#include<sys/types.h>
#include<sys/stat.h>
int mkfifo(const char *pathname,mode_t mode);

pathname:创建管道时所用文件的路径
mode:创建管道文件的权限

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

注意:

  • 若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。
  • 若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。

下面我们来用一下mkfifo函数来创建一下命名管道吧

#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<string.h>
#include<fcntl.h>

int main()
{
    if(-1==mkfifo("./fifo",0666))
    {
        perror("mkfifo");
        return 1;
    }
    
    return 0;
}

运行结果:

在这里插入图片描述

命名管道的打开规则
  • 如果当前打开操作是为读而打开FIFO时
    • O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
    • O_NONBLOCK enable:立刻返回成功
  • 如果当前打开操作是为写而打开FIFO时
    • O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
    • O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
使用命名管道实现server&&client通信

我们若是想让服务端(server)与client(客户端)之间完成通信,首先我们需要让服务端与客户端看到同一份资源,这一份资源就是我们的命名管道。首先让我们的服务端创建一个命名管道,客户端打开刚刚服务端所创建的命名管道,然后客户端往这个管道里面写数据,服务端从这个管道里面读数据。如此一来便通过命名管道实现了服务端与客服端之间的进程间通信。

服务端代码:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
#include<fcntl.h>

int main()
{
  if(-1==mkfifo("./fifo",0666))
  {
    perror("mkfifo");
    return 1;
  }

  //读端
  int fd = open("./fifo",O_RDONLY,0666);
  char buffer[64];
  while(1)
  {
    ssize_t s = read(fd,buffer,sizeof(buffer)-1);
    if(s>0)
    {
      buffer[s] = 0;
      printf("client # %s",buffer);
    }
    else if(s==0)
    {
      printf("client quit me too#\n");
      break;
    }
    else
    {
      perror("read");
    }
}
return 0;
}

客户端代码:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
#include<fcntl.h>

int main()
{
  //写端
  int fd = open("./fifo",O_WRONLY,0666);
  char buffer[64];
  if(fd>=0)
  {
    while(1)
    { 
    printf("Please Enter Message # ");
    fflush(stdout);
    //先从标准输入中读取数据
    ssize_t s = read(0,buffer,sizeof(buffer)-1);
    if(s>0)
    {
      buffer[s] = 0;
      write(fd,buffer,s);
    }
    }
  return 0;
  }

}

下面我们来运行一下代码,实现一下server与client之间的通信吧

1.首先让我们的服务端运行起来,创建命名管道

在这里插入图片描述

2.将我们的客户端运行起来,客户端往管道里面写数据,服务端从管道里面读取数据

在这里插入图片描述

当客户端和服务端运行起来时,我们还可以通过ps命令查看这两个进程的信息,可以发现这两个进程确实是两个毫不相关的进程,因为它们的PID和PPID都不相同。这也就证明了,命名管道是可以让两个毫不相关进程之间实现进程间通信的。

在这里插入图片描述

前面在学习匿名管道的时候,当写端不写了关闭文件描述符,读端在读取完管道数据后会读到文件结尾。当读端不读了关闭文件描述符,写端进程后续有可能会被操作系统直接杀掉。

那在我们命名管道这里如果客户端退出会发什么情况呢?

在这里插入图片描述

可以看到当我们使用Ctrl+c杀掉客户端进程后,服务端进程会读到文件结尾,此时read返回0,我们的服务端的进程也退出了。

下面我有一个问题,为什么在客户端和服务端通信的时候,管道文件的大小一直为0呢?

在这里插入图片描述

这是因为这个文件只是一个符号性的文件,操作系统在内存当中为这两个进程创建了对应的管道文件,在内存中就直接进行通信了。因此内存中的数据是不会刷新到磁盘中的,如果两个进程进行通信,一个进程把数据写到文件里,然后另外一个进程再从文件中读取数据,本质其实这两个进程都在进行IO操作,而IO操作的效率是很低的。

命名管道和匿名管道的区别
  1. 匿名管道由pipe函数创建并打开,命名管道由mkfifo函数创建,打开用open。
  2. 匿名管道只能让具有亲缘关系的进程之间实现进程间通信,命名管道可以让两个毫不相干的进程之间实现进程间通信
  3. FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在于它们创建与打开的方式不同,一旦这些工作完成之后,它们具有相同的语义。
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值创建一块共享内存,这样我们就可以让两个进程看到同一份内存了,这个key值是在系统层面来标识共享内存唯一性的。

那这个时候就有人会问了:我在共享内存的数据结构中没看到你说的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;
 };

注: 其中ipc_perm结构体在/usr/include/linux/ipc.h中定义。

共享内存函数

进程通过共享内存实现进程间通信主要分为以下四个步骤:

  1. 创建共享内存
  2. 将进程与创建的共享内存关联起来
  3. 取消进程与共享内存之间的关联
  4. 释放共享内存资源

下面我主要以上面四个步骤来为大家讲解进程通过共享内存实现进程间通信

共享内存的创建

我们一般通过shmget函数来创建共享内存,我们可以通过man来看一下shmget函数

在这里插入图片描述

这个函数的用法如下:

//功能:用来创建共享内存原型 
int shmget(key_t key, size_t size, int shmflg);
//参数 
//key:表示待创建共享内存在系统当中的唯一标识。
//size:共享内存大小 
//shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
//常用下面这两种组合方式:
//1.IPC_CREAT:如果发现内核中不存在由key值创建的共享内存,那就用key值创建一个共享内存,如果存在了用key值创建的共享内存,就返回该共享内存的shmid
//2.IPC_CREAT | IPC_EXCL:如果发现内核中不存在用key值创建的共享内存,那就用key值创建一个共享内存,如果存在了用key值创建的共享内存,就出错返回

    
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

注:shmget函数的第三个参数shmflg,常用的组合方式有以下两种:

  1. IPC_CREAT:如果发现内核中不存在由key值创建的共享内存,那就用key值创建一个共享内存,如果存在了用key值创建的共享内存,就返回该共享内存的shmid
  2. IPC_CREAT | IPC_EXCL:如果发现内核中不存在用key值创建的共享内存,那就用key值创建一个共享内存,如果存在了用key值创建的共享内存,就出错返回,这样就保证了以这种方式创建的共享内存一定是全新的。

在我们使用shmget创建共享内存之前,我们还需要调用一个ftok函数来获取key值

ftok函数的函数原型如下:

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

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

注意:

  • 需要进行通信的多个进程之间,在使用ftok函数获取key值时,都必须采用同样的路径名和和整数标识符,进而生成同一种key值,然后才能看到同一个共享资源。

下面我们就一起来创建一下共享内存吧

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

#define PATHNAME "root/Code/shm/test.c"
#define PROJ_ID 0x067873

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");
        return 1;
    }
    printf("shmid:%d\n",shmid);
    return 0;
}

运行结果:

在这里插入图片描述

共享内存的释放

释放共享内存的方式有两种:

  1. 通过指令释放共享内存资源
  2. 通过系统调用释放共享内存资源

我们先来介绍一下使用指令释放共享内存资源:

首先我们可以通过ipcs -m指令来查看共享内存资源:

在这里插入图片描述

注:可以看到我们之前的进程已经退出了,但是我们上次创建的共享内存资源它现在还在,所以我们可以得出 共享内存的生命周期是随内核的。

ipcs命令输出的每列信息的含义如下:

参数含义
key唯一标识一块共享内存(系统层面)
shmid共享内存的id(用户层)
owner共享内存的拥有者
perms共享内存的权限
bytes共享内存的大小
nattch关联共享内存的进程数
status共享内存的状态

我们除了使用ipcs指令来查看共享内存资源,我们还可以来查看消息队列以及信号量只是他们带的选项不一样。

  • -q:查看消息队列资源
  • -s:查看信号量资源
  • -m:查看共享内存资源

在这里插入图片描述

我们一般使用shmctl函数来释放共享内存资源,我们可以通过man来看一下shmctl函数

在这里插入图片描述

这个函数的用法如下:

//功能:控制共享内存
//原型 int shmctl(int shmid, int cmd, struct shmid_ds *buf);

//参数 
//shmid:由shmget返回的共享内存标识码 
//cmd:将要采取的动作(有三个可取值)
//buf:指向一个保存着共享内存的模式状态和访问权限的数据结构


//返回值:成功返回0;失败返回-1

注意:shmctl函数的第二个参数传入的常用的选项有以下三个

选项说明
IPC_STAT讲shmid_ds结构中的数据设置为共享内存的当前关联值
IPC_SET在进程有足够权限的前提下,将共享内存的当前关联值设置为shmid_ds数据结构种给出的值
IPC_RMID删除共享内存段

下面我们使用shmct函数来释放一下共享内存资源

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

#define PATHNAME "root/Code/shm/test.c"
#define PROJ_ID 0x067873

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");
        return 1;
    }
    printf("shmid:%d\n",shmid);
    sleep(5);
    shmctl(shmid,IPC_RMID,NULL);
    sleep(3);
    
    return 0;
}

我们在程序运行的时候,使用下面监控脚本来实时检测共享内存资源:

[root@izuf65cq8kmghsipojlfvpz shm]# while :;do ipcs -m;echo "####################"; sleep 1;done

运行结果:

在这里插入图片描述

可以看到5秒后,我们创建的共享内存资源被释放了。

关联共享内存

我们一般使用shmat函数来让进程关联共享内存资源,我们可以通过man来看一下shmat函数

在这里插入图片描述

这个函数的用法如下:

//功能:将共享内存段连接到进程地址空间
//原型 void *shmat(int shmid, const void *shmaddr, int shmflg);

//参数 shmid: 共享内存标识 
//shmaddr:指定连接的地址 
//shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
//返回值:成功返回一个指针,指向共享内存第一个字节;失败返回-1

关于shmat函数第二个参数的说明:

  • shmaddr为NULL,核心自动选择一个地址
  • shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
  • shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr -(shmaddr % SHMLBA)

shmat函数的第三个参数传入的常用的选项有以下三个:

选项说明
SHM_RDONLY关联共享内存后只进行读取操作
SHM_RND若shmaddr不为NULL,则关联地址自动向下调整为SHMLBA的整数倍。公式:shmaddr-(shmaddr%SHMLBA)
0默认为读写权限

下面我们一起来使用一下shmat函数来让进程关联共享内存

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

#define PATHNAME "root/Code/shm/test.c"
#define PROJ_ID 0x067873

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");
        return 1;
    }
    printf("shmid:%d\n",shmid);
    sleep(2);
    //讲当前进程与共享内存关联起来
    char* str = (char*)shmat(shmid,NULL,0);
    sleep(3);
    //释放共享内存
    shmctl(shmid,IPC_RMID,NULL);
    
    return 0;
}

在我们运行代码时,使用下面监控脚本来实时检测管理共享内存的进程数:

[root@izuf65cq8kmghsipojlfvpz shm]# while :;do ipcs -m;echo "####################"; sleep 1;done

运行结果:

在这里插入图片描述

可以看到两秒后当前共享内存的进程关联数由0变成1,关联共享内存成功。

共享内存的去关联

我们一般使用shmdt函数来让取消进程与关联共享之间的关联,我们可以通过man来看一下shmdt函数

在这里插入图片描述

这个函数的用法如下:

//功能:取消当前进程与共享内存资源的关联
//原型:int shmdt(const void *shmaddr);

//参数 shmaddr: 由shmat所返回的指针
//返回值:成功返回0;失败返回-1

//注意:将共享内存段与当前进程脱离不等于删除共享内存段

我们先让当前进程关联内存资源,三秒后我们再取消当前进程与共享内存的关联,下面我们来看一下代码吧

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

#define PATHNAME "root/Code/shm/test.c"
#define PROJ_ID 0x067873

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");
        return 1;
    }
    printf("shmid:%d\n",shmid);
    sleep(2);
    //讲当前进程与共享内存关联起来
    char* str = (char*)shmat(shmid,NULL,0);
    sleep(3);
    //取消当前进程与共享内存的关联
    shmdt(str);
    sleep(2);
    //释放共享内存
    shmctl(shmid,IPC_RMID,NULL);
    
    return 0;
}

在我们运行代码时,使用下面监控脚本来实时检测管理共享内存的进程数:

[root@izuf65cq8kmghsipojlfvpz shm]# while :;do ipcs -m;echo "####################"; sleep 1;done

运行结果:

在这里插入图片描述

可以看到刚开始关联共享内存的进程数为0,过了两秒后关联共享内存的进程数为1,又过了两秒关联共享内存的进程数又变为0,共享内存的去关联成功。

使用共享内存实现server&&client通信

了解了共享内存的创建,释放、关联共享内存以及去关联之后,下面我们来使用共享内存实现一下客户端与服务端之间的通信吧。

服务端负责创建共享内存,创建好后将共享内存和服务端进行关联,之后服务端进入死循环等待客户端发送消息。最后通过客户端释放共享内存资源

服务端代码如下:

#include<stdio.h>
#include<unistd.h>
#include<sys/ipc.h>
#include<sys/types.h>
#include"comm.h"
#include<sys/shm.h>

int main()
{
  //key 是用来在内核中唯一标识一块共享内存的
  key_t key = ftok(PATHNAME,PROJ_ID);
  printf("key:%d\n",key);
  int shmid = shmget(key,SIZE,IPC_CREAT | IPC_EXCL | 0666);
  if(shmid<0)
  {
    perror("shmget");
    return 1;
  }
  printf("shmid:%d\n",shmid);

  char* str = (char*)shmat(shmid,NULL,0);
  while(1)
  {
    printf("client # %s\n",str);
    sleep(1);
  }


  //将该进程与共享内存取消关联
  shmdt(str);

  //释放该共享内存
  shmctl(shmid,IPC_RMID,NULL);
  
  return 0;

}

为了让服务端和客户端在使用ftok函数获取key值时,能够得到同一个key值,那么服务端和客户端传入ftok函数的路径名和和整数标识符必须相同,这样才能生成同一种key值,进而找到同一个共享资源进行关联。这里我们可以将这些需要共用的信息放入一个头文件当中,服务端和客户端共用这个头文件即可。

共用头文件的代码如下:

#pragma once 

#define PATHNAME "root/Code/shm/test.c"
#define PROJ_ID 0x067873

#define SIZE 4096

客户端通过关联服务端创建的共享内存,然后往共享内存里面写数据,最后写完了就取消与该共享内存的关联。

客户端代码如下:

#include<stdio.h>
#include<unistd.h>
#include<sys/ipc.h>
#include<sys/types.h>
#include"comm.h"
#include<sys/shm.h>

int main()
{
  //key 是用来在内核中唯一标识一块共享内存的
  key_t key = ftok(PATHNAME,PROJ_ID);
  printf("key:%d\n",key);
  //因为在服务端已经创建共享内存了
  //我们这边只需要打开就行了
  int shmid = shmget(key,SIZE,0);
  if(shmid<0)
  {
    perror("shmget");
    return 1;
  }
  printf("shmid:%d\n",shmid);

  char* str = (char*)shmat(shmid,NULL,0);
  char ch = 'a';
  for(;ch<='z';ch++)
  {
    str[ch-'a'] = ch;
    sleep(1);
  }


  //将该进程与共享内存取消关联
  shmdt(str);
  
  return 0;

}

运行结果:

在这里插入图片描述

在这里插入图片描述

我们这里的客户端与服务端关联到了同一块共享内存上,并完成了进程间通信。

我们可以看到客户端这里是没五秒往共享内存里面写一个字符,但是服务端每隔一秒往共享内存里面读取数据,但是我们这里的服务端5秒之内读取的都是客户端之前写入的数据,它并没有阻塞,5秒后读取到的是客户端新写的内容。

通过这个现象我们可以得出一个结论:

共享内存底层不提供任何同步与互斥机制。

Syetem V信号量(了解即可)

信号量主要用于同步和互斥的,下面我们来看一下信号量的一些概念

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

以上就是本篇文章的所有内容了,觉得对你有帮助的话,可以给作者三连一波支持一下!!!

  • 15
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值