• Linux进程间通信


    🌲进程通信引论

    🌴概念

    进程通信是指在进程间传输数据(交换信息)。–百度百科

    人与人之间的通信是什么?交换信息。进程之间的信息是什么?是数据!所以进程通信可以理解为进程之间传递数据。

    IPC(InterProcess Communicatio)是进程间通信的简称。

    进程通信要做的事可以理解为让多个进程看到同一块资源。

    🌴目的

    进程通信的目的

    • 数据传输:一个进程发送数据给另一个进程,数据量在一个字节到几M字节之间
    • 共享数据:多个进程共享同一块资源,做到这点需要内核提供同步和互斥机制。
    • 通知事件:一个进程向另一个进程发消息,比如一个进程结束时应该通知其父进程
    • 进程控制:一个进程完全控制另一个进程,如debug进程。

    这些听起来都太抽象了,联系实际场景才能理解。

    🌴本质

    进程通信的本质就是让多个进程看到同一块资源

    这块资源常指的是内存资源,在linux下一切皆文件,所以也可以理解成文件资源。

    进程通信的方式也是围绕着这一句话展开的。

    🌴分类

    System V和POSIX我理解为不同的标准。

    image-20220720154922782

    我们主要了解匿名管道、命名管道、共享内存和信号量,通信方式不同,自然通信策略也不同。

    System v 和 Posix作用和区别(进程间通信IPC)_山农的博客-CSDN博客_system v

    System V是Unix系统的某个版本。Posix是由IEEE和ISO开发的一套标准,两个都可以说是一种系统接口的协议。

    🌲管道

    🌴管道概念

    管道是一种常见的进程通信工具。

    Linux里的管道命令是’|'。

    ls -l | grep *.txt
    
    • 1

    image-20220720162349474image-20220720164405847

    🌴为什么需要管道

    我们为什么需要管道,或者说两个进程间能直接传递数据吗?因为进程间不能直接通信,每个进程都是独立的,进程有自己的地址空间,如果两个进程直接传递数据,会发生写时拷贝。所以此时我们需要一个媒介,也就是这里讲的管道。借助管道我们可以实现两个进程之间的单向通信。

    所以管道的作用自然也就出来了,管道的作用就是让两个进程看到同一份资源,这个资源常常指的是内存资源,也可抽象为文件资源。

    image-20220720161823499

    image-20220720161833247

    🌴匿名管道

    匿名管道用于有亲子关系的进程,常见于父子也可以用于兄弟等。

    image-20220720170746651

    • 管道实现形态上是文件,但是管道本身不占用磁盘或其他外部存储的空间–摘自知乎。

    • 管道文件的内容在内存的缓冲区上,缓冲区的内容不会刷新到磁盘的文件上

      个人认为是没必要刷新到磁盘上,从内存读取的速度比从从磁盘上快的多,管道传输数据也是单向的,数据被接收的进程读取后没必要刷新到磁盘上。硬要刷新到磁盘上的话那没必要用管道…让这两个进程加载磁盘上的同一个文件不就行了。

      大佬对于管道的一些理解,从中摘录一段话:作为管道,一旦创建成功,应用除了传递数据外,能够做的只有删除操作了。

    🌵创建匿名管道

    匿名管道用于两个有亲缘关系的进程之间的单向通信,比如父进程写子进程读。如果有多个进程需要通信,那就建立多个管道。

    🥝pipe函数

    int pipe(int pipefd[2]);

    传入参数为一个数组,也是输出型参数,pipefd[0]放管道的读端文件描述符,pipefd[1]放管道的写端文件描述符

    image-20220720161833247

    pipe函数的作用

    image-20220720224446834

    这里的箭头表示指针指向,而不是数据流向

    pipe结合fork之后的图

    image-20220720225237600

    父子进程通过关闭文件描述符来实现管道的单向通信

    对管道里面写入数据的操作,和对文件里写入数据的操作一样,印证了Linux下的一切皆文件。

    管道是单向的,肯定是一个写一个读。所以建议写之前关闭读,读之前关闭写

    既然是写之前要关闭读,读之前要关闭写,那一开始为什么还要打开读写呢,只打开读或者只打开写不就行了吗。因为父子进程拿到的文件打开方式是一样的,如果父进程只打开读那子进程也只能读,也就不能写了,我们有提到管道肯定是一个写一个读,所以父进程打开文件的方式肯定是读写(rw)。

    为什么建议读之前关闭写,写之前关闭读?防止误操作。

    下面的代码:父进程创建管道,父进程创建子进程,子进程在管道里每隔1s写入一次数据,父进程去读。

    #include 
    #include
    #include
    #include 
    #include
    #include 
    int main()
    {
      int pipe_fd[2]={0};
      int ret=pipe(pipe_fd);
      if(ret<0)
      {
        perror("pipe");
        return 1;
      }
      pid_t id=fork();
      if(id==0)
      {
        //child write
        close(pipe_fd[0]);//写之前关闭读
        int cnt=5;
        const char* msg="hello world!\n";
        while(cnt--)
        {
          write(pipe_fd[1],msg,strlen(msg));
          sleep(1);
        }
        close(pipe_fd[1]);
        exit(0);
      }
      else if(id>0)
      {
        //father read 
        close(pipe_fd[1]);//读之前关闭写
        char buffer[128];
        while(1)
        {
          ssize_t num=read(pipe_fd[0],buffer,127);//读到几个字节
          if(num>0)
          {
            buffer[num]='\0';
            printf("father get the msg from child:%s\n",buffer);
          }
          else if(num==0) 
          {
            printf("piple file close,child quit\n");
            break;
          }
          else 
          {
            perror("read");
          }
        }
        close(pipe_fd[0]);
        int status=0;
        if(waitpid(id,&status,0)>0)
        {
          printf("wait success!\n");
        }
      }
      else 
      {
        perror("fork");
      }
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    运行效果

    image-20220720232449493

    随笔记录:僵尸进程的产生是因为父进程没有 wait () 子进程。,所以如果我们自己写程序的话一定要在父进程中通过 wait () 来避免僵尸进程的产生,僵尸进程的产生会导致内存泄漏。子进程僵尸,父进程没回收,父进程退出后僵尸子进程会交给Init进程,Init进程会发现并回收这个僵尸进程,如果父进程一直在运行,子进程一直僵尸,内存就泄漏了。

    🌵管道读写的四种特殊情况

    1. 写端进程一直写,读端进程不读。对应的文件描述符未被关闭

    如果管道满了,就要等管道有空闲空间才能继续写,也就是被读端进程读走后才能继续写。简单来说就是write阻塞

    #include 
    #include
    #include
    #include 
    #include
    #include 
    int main()
    {
      int pipe_fd[2]={0};
      int ret=pipe(pipe_fd);
      if(ret<0)
      {
        perror("pipe");
        return 1;
      }
      pid_t id=fork();
      if(id==0)
      {
       //  child write
        close(pipe_fd[0]);
          
        const char* msg="hello world!\n";
        int cnt=1;
        while(cnt)//写端进程不断写
        {
          write(pipe_fd[1],msg,strlen(msg));
          printf("child writes the msg:%d\n",cnt);
          cnt++;
        }
          
        close(pipe_fd[1]);
        exit(0);
      }
      else if(id>0)
      {
         // father read 
        close(pipe_fd[1]);
          
        int status=0;
        if(waitpid(id,&status,0)>0)
        {
          printf("wait success!\n");
        }
      }
      else 
      {
        perror("fork");
      }
        
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    image-20220721093754149

    1. 写端进程不写,读端进程一直读。对应的文件描述符未被关闭

    读端进程一直读,因为管道里没有数据,读端进程就被挂起了,简单来说就是read阻塞。

    #include 
    #include
    #include
    #include 
    #include
    #include 
    int main()
    {
      int pipe_fd[2]={0};
      int ret=pipe(pipe_fd);
      if(ret<0)
      {
        perror("pipe");
        return 1;
      }
      pid_t id=fork();
      if(id==0)
      {
       //  child write
        close(pipe_fd[0]);
        while(1)//子进程不写数据
        {
          printf("pipe is NULL\n");
          sleep(1);
        }
        exit(0);
      }
      else if(id>0)
      {
         // father read 
        close(pipe_fd[1]);
         char buffer[128];
         while(1) //父进程一直在读
         { 
            ssize_t num=read(pipe_fd[0],buffer,127);
            if(num>0)
            {
              buffer[num]='\0';
              printf("father get the msg from child:%s\n",buffer);
            }
            else if(num==0) 
            {
              printf("piple file close,child quit\n");
              break;
            }
            else 
            {
              perror("read");
            }
         } 
         close(pipe_fd[0]);
        int status=0;
        if(waitpid(id,&status,0)>0)
        {
          printf("wait success!\n");
        }
      }
      else 
      {
        perror("fork");
      }
    
      return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    随笔记录:gdb模式下set listsize n,修改list默认显示的代码行数

    image-20220721100749537

    1. 写端进程写完后被关闭,读端进程还在读。

    不会出现write和read阻塞的问题,而是读端发现写端关闭后read直接返回0,然后继续执行后面的代码。

    #include 
    #include
    #include
    #include 
    #include
    #include 
    int main()
    {
      int pipe_fd[2]={0};
      int ret=pipe(pipe_fd);
      if(ret<0)
      {
        perror("pipe");
        return 1;
      }
      pid_t id=fork();
      if(id==0)
      {
       //  child write
        close(pipe_fd[0]);
        
        int cnt=5;
        const char* msg="hello world!\n";
        while(cnt--)
        {
          write(pipe_fd[1],msg,strlen(msg));
          printf("child writes the msg:%d\n",cnt);
          sleep(1);
        }
        close(pipe_fd[1]);
        cnt=3;
        while(cnt--)
        {
          printf("pipe has closed\n");
          sleep(1);
        }
        exit(0);
      }
      else if(id>0)
      {
         // father read 
        close(pipe_fd[1]);
         char buffer[128];
         while(1) { 
            ssize_t num=read(pipe_fd[0],buffer,127);
            if(num>0)
            {
              
              buffer[num]='\0';
              printf("father get the msg from child:%s\n",buffer);
            }
            else if(num==0) 
            {
              printf("piple file close,child quit\n");
              break;
            }
            else 
            {
              perror("read");
            }
         } 
         close(pipe_fd[0]);
        int status=0;
        if(waitpid(id,&status,0)>0)
        {
          printf("wait success!\n");
        }
      }
      else 
      {
        perror("fork");
      }
    
      return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76

    image-20220721101704562

    1. 写端进程一直写,读端进程被关闭。

    写的目的不就是读吗,读都被关闭了,写自然也就没有意义了。而写端还在一直写入数据,系统会干掉写端进程。

    #include 
    #include
    #include
    #include 
    #include
    #include 
    int main()
    {
      int pipe_fd[2]={0};
      int ret=pipe(pipe_fd);
      if(ret<0)
      {
        perror("pipe");
        return 1;
      }
      pid_t id=fork();
      if(id==0)
      {
       //  child write
        close(pipe_fd[0]);
        
        int cnt=5;
        const char* msg="hello world!\n";
        while(cnt--)
        {
          write(pipe_fd[1],msg,strlen(msg));
          printf("child writes the msg:%d\n",cnt);
          sleep(1);
        }
        close(pipe_fd[1]);
        cnt=3;
        while(cnt--)
        {
          printf("pipe has closed\n");
          sleep(1);
        }
        exit(0);
      }
      else if(id>0)
      {
         // father read 
        close(pipe_fd[1]);
        close(pipe_fd[0]);
        int status=0;
        if(waitpid(id,&status,0)>0)
        {
          printf("wait success!\n");
        }
      }
      else 
      {
        perror("fork");
      }
    
      return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    image-20220721102500499

    略微修改代码,拿到终止子进程的信号。

    image-20220721104514670

    🌵管道读写的特点

    根据上面几种情况,我们可以窥探出管道的一些特点。

    • 管道自带同步机制

    可以发现管道里有数据才会被读

    管道里没有数据读端进程就会在那等

    管道写满了就不会再写,就在那等待被读,这些都是同步的体现,表现为进程的运行有先后顺序。

    个人理解同步的进程运行起来如同有条件约束一般,比如当管道有数据才去读,这种机制有点“束手束脚”的意思,而不是各运行各的,不会发生管你有没有数据我都去读这种情况。同步机制一般由加锁实现

    • 管道是单向通信的,肯定是一个写一个读
    • 匿名管道只能保证是具有血缘关系的进程进行通信,常常用于父子。

    linux下的管道命令‘|’指的就是匿名管道,他们有同一个父进程,也就是bash进程。

    • 管道是面向字节流的(网络那块会详细阐述面向字节流)
    • 管道可以保证一定程度的数据读取的原子性(原子性:事务的不可分割性,一个操作要么被全部执行,要么全部不执行,这个操作也就是原子操作,原子操作一般通过同步机制完成,非原子操作存在线程安全问题,一般通过加锁保证操作的原子性)
    • 管道的生命周期随进程,管道也是文件,进程退出管道自然也会被关闭。

    🌵管道的大小

    ulimit -a
    
    • 1

    0.5KB*8=4KB

    image-20220721104817801

    写程序可以验证管道容量大小最大是65526个字节(64KB)

    代码实现上注意写满管道前子进程不能退出了,子进程退出了的话表示读端没了,写端(父进程)就会被系统干掉,我当时没注意到这种情况打印到五千多就停了。

    #include 
    #include
    #include 
    #include
    #include 
    #include
    int main()
    {
      int pipe_fd[2]={0};
      pipe(pipe_fd);
      pid_t id=fork();
      if(id==0)
      {
        close(pipe_fd[1]);
        while(1)//子进程不能提前退出
        {
          sleep(1);
        }
    
        close(pipe_fd[0]);
      }
      else
      {   
        close(pipe_fd[0]);
        char c='a';
        int cnt=1;
        while(1)
        {
          write(pipe_fd[1],&c,1);
          printf("%d\n",cnt);
          cnt++;
        }
        close(pipe_fd[1]);
        waitpid(id,NULL,0);
    
      }
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    image-20220721153501198

    为什么ulimits -a打印的是4KB,我们测试的时候是64KB呢?4KB是内核管道的大小,由内核设定,64KB可以理解为管道大小的最大值,即容量(可以类比vector的size和capacity)

    参考资料👉Linux管道的容量大小及管道的数据结构

    随笔记录:取消43,70行的注释

    image-20220721111710687

    :43,70 s/^g 可以注释43-70行

    🌴命名管道

    匿名管道只能用于两个有亲缘关系的进程通信,如果没有亲缘关系又要通信的话,此时就需要命名管道了。

    image-20220721162609034

    🌵创建命名管道

    mkfifo命令

    image-20220721154338535

    🥝mkfifo函数

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

    文件路径具有唯一性,所以可以让两个进程看到同一份资源。

    image-20220721155720841

    在当前目录下创建一个管道文件的例子。

    #include
    #include
    #include 
    
    int main()
    {
      if(mkfifo("./myfifo",0644)<0)
      {
        perror("mkfifo");
        return 1;
      }
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    运行效果

    image-20220721160915504

    🌵server和client的小demo

    server,服务端读。

    client,客户端写。

    在服务端创建一个管道文件,客户端打开文件后进行写入,服务端再打开文件读取数据就实现了服务端和客户端两个进程的通信。

    启动的时候是先启动服务端,因为要创建管道文件。

    server.c

    #include 
    #include
    #include 
    #include
    #include 
    
    int main()
    {
      if(mkfifo("./myfifo",0644)<0)
      {
        perror("mkfifo");
        return 1;
      }
      int fd=open("myfifo",O_RDWR);
      if(fd<0)
      {
        perror("open");
        return 2;
      }
      while(1)
      {
        char buf[128];
        ssize_t num=read(fd,buf,sizeof(buf)-1);
        buf[num]='\0';
        printf("server has got the msg:%s\n",buf);
        sleep(1);
      }
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    client.c

    #include 
    #include
    #include 
    #include
    #include 
    #include 
    
    int main()
    {
      int fd=open("myfifo",O_RDWR);
      if(fd<0)
      {
        perror("open");
        return 1;
      }
      while(1)
      {
        const char* msg="hello world\n";
        write(fd,msg, strlen(msg));
        printf("client has wrote the msg!\n\n");
        sleep(1);
      }
      return 0;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    Makefile

    .PHONY:all
    all:client server 
    
    client:client.c
    	gcc -o $@ $^
    
    server:server.c
    	gcc -o $@ $^
    
    .PHONY:clean
    clean:
    	rm -f client server
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    运行效果

    image-20220721171251655

    命名管道还有很多用途。比如进程遥控,还可以结合进程替换传递命令,等等。

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

    • 匿名管道用于有亲缘关系的进程,命名管道用于任意两个进程。
    • 匿名管道的打开交给了pipe函数,pipe函数会创建并打开。命名管道的创建交给了mkfifo函数,管道的打开交给了open。
    • 两者唯一区别就是打开和创建方式不同,创建和打开完成后,两者其实都是一样的。(因为创建和打开的工作就是让两个进程看到一块资源,怎么操作资源就是文件操作的事了)

    🌲System V IPC

    我的理解System V就是一个标准,这个标准下有几种通信方式,如共享内存,信号量等。

    🌴共享内存

    共享内存的原理:多个进程的页表映射到一块物理内存。

    image-20220722091711415

    这个过程可以简单概括为:OS申请一块物理内存,将该内存映射进对应进程的共享区中,再将映射之后的虚拟地址给用户。此时两个进程就看到了同一块资源,就可以对这块资源实施操作,自然也就实现通信了。

    那有这么多的共享内存,我们怎么保证多个进程看到的是同一块共享内存呢,通过一个key值,这个key值保证了这块空间的唯一性,多个进程可以通过一个传入路径的函数生成同一个key值,那多个进程就能看到同一块空间了。

    那自然引申出了一个问题:OS提供了这么多的共享内存,那OS要不要管理这么多的共享内存呢?答案是肯定的。

    OS要管理一个东西,那自然是那六个字了:先描述再组织

    内核里描述共享内存的是一个结构体,即struct shmid_ds,里面存着这块空间的属性,比如空间的大小,最后的修改时间,谁创建的等等,shmid_ds这个结构体里面还有一个名为shm_perm的结构体,类型是struct ipc_perm,里面存放了一个key,用来保证这块空间的唯一性。

    shm:share memory

    image-20220722093028545

    image-20220722094840195

    🌵ftok函数生成key值

    ftok函数生成key值,key值的类型key_t,也就是int

    image-20220722101603743

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

    image-20220722095837793

    这两个参数可以随便填, 反正目的是生成一个key值,只要第二个参数别传0就行(按文档说的来呗

    比如两个进程传入相同的参数就可以得到同一个key值,就可以看到同一块资源(对资源的操作不是根据key来的,OS给了一个标识符shmid,对资源的操作通过shmid,key只标识空间的唯一性)。

    🌵shmget函数创建共享内存

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

    创建一块共享内存,传入参数的key用来保证这块空间的唯一性,size是大小,shmflg是怎么创建这块空间,即选项。创建成功返回这块空间的标识符,创建失败返回-1,注意这块空间的标识符不是key,我们不通过key操作共享内存。

    image-20220722094309099

    关于函数的第三个参数shmflg,创建共享内存时建议用IPC_CREAT|IPC_EXCL,判断共享区存不存在的依据是key值是否重复。

    IPC_CREAT|IPC_EXCL|0644表示创建一个权限为644的共享内存,如果这块共享内存已经存在则创建失败。

    单独用IPC_CREAT表示获取已存在的共享内存标识符。比如父进程创建好了共享内存,那子进程就只需要获取这个标识符就够了。

    关于共享内存创建的大小,建议是4KB,因为OS申请的大小会是4KB的倍数,比如申请4097个字节,实际分配到的是8KB,只不过我们只能用4KB.

    关于返回值,创建成功返回的是这块空间的标识符shmid,需要与key区分开来。(文件的inode唯一标识文件,但是我们通过fd对文件进行操作,类比过来这里的shmid相当于fd)

    🌵shmat函数挂载进程

    之前创建了共享内存,但是创建的共享内存和进程还没连接上,所以此时需要挂载进程,或者说关联进程。

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

    image-20220722103328930

    第一个参数传共享内存标识符,第二个传NULL(表示系统自动分配地址),第三个传0,表示以读写的方式连接此段。(除非用系统提供的宏指定以只读方式连接,否则默认以读写的方式连接)。

    👉shmat 函数的使用

    返回值

    成功就返回这块空间的实际地址(当然是虚拟的),失败返回-1.我们拿到了地址自然就可以对这块空间进行写入了。

    image-20220722103850192

    🌵shmdt函数去关联(shmat的反义词)

    int shmdt(const void *shmaddr);

    进程调用这个函数,传入这块共享内存的地址,也就是shmat函数的返回值,即可将这个进程与这块共享内存去关联。

    我猜dt是delete attach的意思。

    🌵shmctl函数释放共享内存

    共享内存的生命周期是随内核的,而不是随进程的,所以需要我们手动释放。

    释放这块内存可以用指令,也可以进程退出前调用函数,再或者OS重启

    这个函数不止可以释放共享内存,但是常用来来释放共享内存。

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

    image-20220722102345608

    创建成功返回0,创建失败返回-1.

    删除共享内存这种操作对应的cmd是IPC_RMID。

    image-20220722102741035

    第三个参数在别的操作里作为输出型参数。

    综上所述,shmctl(shmid,IPC_RMID,NULL)这样就可以释放shmid指向的那块共享空间

    🌵ipcs命令

    image-20220722153809401

    ipcrm -m shmid可以删掉shmid这块共享内存,或者说释放这块共享内存。-m命令表示如果没有进程绑定时就删除这块共享内存。

    Linux 命令(52)—— ipcrm 命令_

    🌵server和client的小demo

    大体思路如下

    image-20220722111350075

    comm.h

    #pragma once 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #define PATH "/home/ck/lesson18/shm_pipe"
    #define SIZE 4097
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    server.c

    #include"comm.h"
    
    int main()
    {
      //生成key值
      key_t key=ftok(PATH,1); 
       //创建共享牛才能
      int shmid=shmget(key,4097,IPC_CREAT|IPC_EXCL|0644);
      if(shmid<0)
      {
        perror("shmget");
        return 1;
      }
       //与进程绑定并拿到地址
      char* start=(char*)shmat(shmid,NULL,0);
      if(start==(char*)-1)
      {
        perror("shmat");
        return 2;
      }
      //TODO 每隔一秒读一次
      int cnt=0;
      while(1)
      {
        printf("%s\n",start);
        cnt++;
        sleep(1);
      }
    
    //与这块共享内存段去关联
      shmdt(start);
    //释放掉这块内存
      shmctl(shmid,IPC_RMID,NULL);
    
    
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    client.c

    #include "comm.h"
    
    int main()
    {
      //生成key值
      key_t key=ftok(PATH,1); 
      //获取这块内存的shmid
      int shmid=shmget(key,4097,IPC_CREAT);
      if(shmid<0)
      {
        perror("shmget");
        return 1;
      }
      //绑定这块内存并拿到地址
      char* start=(char*)shmat(shmid,NULL,0);
      if(start==(char*)-1)
      {
        perror("shmat");
        return 2;
      }
      //TODO 每隔1s写入一个字符
      int cnt=0;
      while(1)
      {
        start[cnt]='A'+cnt;
        cnt++;
        start[cnt]='\0';
        sleep(1);
      }
      //与这块内存去关联
      shmdt(start);
    
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    运行操作和效果

    先运行server程序,创建共享内存,发现server有一段时间空读,因为此时client程序还没运行起来,两个程序都运行起来后用ipcs查看这块共享内存的信息,之后先结束client进程,再用ipcs -m命令发现这块内存下挂载的进程数减1,再结束server进程,再用ipcs命令观察,发现共享内存依旧存在,原因是程序在读写时被信号终止了,没有运行到去关联和释放共享内存的代码。

    Markdown博客演示47

    从运行效果也可以看出来,共享内存没有管道那样的同步机制,注意client运行之前的server进程一直都是空读,这两个程序看起来就像各运行各的。

    🌵共享内存的特点

    • 共享内存的生命周期随系统,或者说随内核
    • 共享内存不提供任何同步与互斥的操作,双方彼此独立
    • 共享内存时所有进程间通信中,速度最快的(比如管道和消息队列数据都要拷贝四次,而共享内存数据只需要拷贝两次)
    • 关于共享内存的大小,OS分配共享内存是一页一页分配的,一页是4KB,所以我们要4097个字节拿到的实际是8KB,只不过我们只能用4KB

    🌵共享内存与管道的对比

    两个角度,一个是速度方面,一个是数据同步上。

    数据同步方面,管道是有数据同步和互斥方面的一些机制的,但是共享内存没有。要有的话就得自己控制了。

    速度方面,共享内存的数据只需要拷贝两次,管道的数据需要拷贝四次,所以共享内存的速度更快。

    下面画的两幅图是我结合网上的说法和自己的理解画的,不一定对。

    image-20220722163100533

    image-20220722163247940

    待了解:mmap,现在简单理解为一种将文件映射到进程地址空间的方式,效果上也是减少拷贝(数据量很大时其实减少一次拷贝对于性能都会有不错的提升)

    认真分析mmap:是什么 为什么 怎么用 - 胡潇 - 博客园 (cnblogs.com)

    🌴消息队列

    没学。

    查阅了些许资料,意思是消息队列和管道不同,管道中的数据是按字节流来的,而消息队列里的“消息”是具有结构的。通信的过程是通过key建立链接再发送“消息”

    Linux进程间通信——消息队列 - 知乎 (zhihu.com)

    🌴信号量

    • 进程看到的同一份资源,我们叫做临界资源。进程不是所有的代码都在访问资源,只是部分代码在访问,造成数据不一致的正是这小部分代码,我们称之为临界区

    • 为了避免数据不一致,需要对临界资源进行某种保护,即互斥,互斥是一部分空间任何时候有且只能有一个进程在访问,可以通过锁来实现。简而言之,我们通过对临界区进行加锁和解锁完成互斥操作来保护临界资源。

    • 信号量的本质是一个具有原子性的计数器,用来描述可用资源的数目,可以保护临界资源的安全。

    • 为什么信号量可以保护临界资源?信号量的计数器是结合了锁的,具有互斥的性质,由于锁的缘故这个操作是有原子性的,解决了同一块资源可能会被多个进程同时使用导致的数据不一致的问题。

    //写一段伪代码  只是大概描述这个过程,信号量的实现肯定不是这么简单
    lock()
    if(count<=0)//count就是计数器
    {
        pause;
    }
    else
    {
        count--;//“预定了这块资源”
    }
    unlock()
    //从这可以知道,只要“预定”到了资源,要用的时候就肯定有
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • PV操作(原语),P操作申请信号量(预定一块资源),那我们要用时候肯定有,V可以理解为用完了释放一个资源(计数器count++)

    原语就是一段不可被中止的程序指令集合。就是只要运行了,肯定就要运行完。原子性也是一个道理,要做就做完,不然就一点都不做。

    • 消息量在内核里的描述也是一个结构体,系统给了许多相关的接口(有兴趣可以查阅相关资料)。信号量本身就是一个临界资源,因为每个进程申请信号量的前提是他们都得看到同一个信号量。

    我当时理解信号量时的一个疑惑,在这记录一下:为什么信号量没有数据交换也是通信的一种方式

  • 相关阅读:
    HTML + CSS + 小程序+js教程
    深度解读智能媒体服务的重组和进化
    LeetCode 137. 只出现一次的数字 II
    【WSL】安装WSL和Docker-20220828
    ubuntu18.04服务搭建yolov5开发环境
    最佳联盟营销软件解决方案:简化你的联盟管理
    GDAL库学习
    挂载硬盘相关操作-linux004
    服务器CPU占用过高如何解决
    IntelliJ Idea 常用快捷键列表
  • 原文地址:https://blog.csdn.net/m0_53005929/article/details/125936385