Linux系统编程3(进程间通信详解)

进程间通信,顾名思义,就是进程与进程之间互通信交流,OS保证了各进程之间相互独立,但这不意味着进程与进程之间就必须完全隔离开,在不少的情况下,进程之间需要相互配合共同完成某项任务,这就要求各进程之间能够互相交流,此篇博客就是讲述进程之间通信(即交流) 的方法和原理,笔者尽可能将大家会产生疑惑的点写出来,大家可以收藏慢慢观看,笔者并非大佬,文章有错误在所难免,望读者指出共同讨论

目录

进程间通信的目的

如何实现进程间通信

管道通信

匿名管道通信 

命名管道通信

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

命名管道的打开规则

共享内存 

共享内存的优缺点

消息队列 

信号量

OS对进程间通信方法的管理 


 

进程间通信的目的

进程间通信的目的和原因,有如下几个点

 

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

 

总得来说,实现进程间通信就是为了进程之间能够协同完成某项任务

如何实现进程间通信

进程间通信起初有很多不同的的相关协议,随着不断的实践发展,目前主要有两个主流的通信规则,一个是System V,另一个是POSIX

POSIX:让通信过程可以跨主机

System V:聚焦在本地通信

由于System V由于制定的比较早,不支持跨主机间的通信,在今天属于比较陈旧的标准了,因此我们会将更多精力放在POSIX上,不过POSIX通信并不是这篇文章的内容,因此不会提及,而System V我们关注比较重要的共享内存的概念,通信规则并非只局限于POSIX和System V,我们先介绍比较简单易接受的管道通信

管道通信

管道通信主要是借助文件系统来实现的,怎么理解呢?我们假设现在系统上的进程A和进程B要互相通信,A不能直接去B里面读数据,因为进程具有独立性,那该怎么办呢?这就需要找一块空间C,空间C用来存放通信双方通信的数据,现在进程A要给B发送数据,那么A和B要向系统声明建立连接,申请一块空间C,然后A往空间C里发送数据,B从空间C里读取数据,这样A就实现了和B的通信,这块空间C就像一根管道一样,连接着A与B,整个管道通信的基本原理就是如此,当然这只解释了管道名称的由来,并没有解释管道通信是借助文件系统来实现的

 

我们要理清楚如何在Linux系统中让两个进程读取到同一块内存空间,如果看过IO篇的同学应该会想到,那就是通过文件,进程从磁盘中或除自身以外的其他可读写的内存区域中读取或写入数据主要是通过文件系统来解决的,只要系统在内存中创建一个文件,A进程打开这个文件,B进程也打开这个文件,那么A与B就通过这个文件连接起来进行通信了,这就是管道通信是借助文件系统来实现的原因

 

管道通信具体分为匿名管道通信和命名管道通信,接下来跟随笔者逐个来了解什么是匿名通信和命名通信,它们的作用是什么?

1771dddd41f24f5891cd9ee507b5c42c.png

匿名管道通信 

经过上述的说明,我们已经明白了管道通信就是用来实现进程与进程之间的通信,但是进程与进程之间的通信也分为两种,一种是父子进程或兄弟进程之间的通信,另一种则是没有亲属关系的进程间的通信

匿名管道通信就是用来解决有父子或兄弟关系的进程间通信,在敲Linux代码时,经常会用到匿名管道通信,例如ps ajx | grep pid,这个命令是用来查询进程id为pid的进程状态,ps ajx是查询当前系统所有的进程状态,而grep则是筛选函数,用来筛选进程id为pid的进程,两者之间就是通过管道进行数据通信,也就是命令中的符号 '|',学完匿名管道通信,你会理解这个命令的实现原理,接下来我们以父子进程间通信为例来讲解匿名管道通信

父子进程之间是共享代码和数据的,但这个数据共享只能用来读,一旦一方试图使数据发生变化会触发写时拷贝,父进程与子进程的数据就存放到了不同的地址,这时父子双方该如何通知对方数据发生了变化呢?这就是匿名管道通信要研究的东西

我们知道,子进程会继承父进程的代码和数据,父子进程要想进行通信,父进程就要向操作系统声明通信(也就是创建一个管道),创建管道的代码如下

#include <unistd.h>

int pipe(int fd[2]);
//功能:创建一个无名管道

fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

注意:该函数的参数是输出型参数,在传参fd时要先创建fd,也就是事先声明 int fd[2];

这段代码中的内容笔者慢慢解释,前面我们说过,创建一个管道也就是系统在内存中创建一个文件,进程A与进程B通过这个文件互相读取数据,这就涉及到另一个问题,是否进程A和进程B可以双向通信,即都可向管道文件中读写数据,若可以,则进程A与B又该如何分辨自己该读取哪部分数据呢?

可能你会说等A写完,B赶紧读,然后B再写,A再读,这样会有潜在的隐患,因为A写的时候你要阻止B写入,如果这个时候B有很重要的数据不能及时写入,就造成数据丢失

因此我们规定管道通信都是单向通信,创建一个管道时,只能由一方负责写,一方负责读,这是在创建管道时就要决定好的,如果要实现双方都可以读写,那就创建两个管道,创建两个管道无非是创建两个文件罢了,开销并不大

cb7d995bfdd2484c973d4ccabe47e6d6.png

管道的通信是单向的,也就是A进程在管道通信时,既可以做写方,又可以做读方,系统如何区分此时A是写还是读呢?解决办法就是让父进程以读和写两种形式分别打开管道文件,也就是我们需要一个数组,这个数组只有两个元素,用来记录以读的形式打开管道文件的fd以及以写的形式打开管道文件的fd

然后根据相关情况,选择关闭其中一个,这也是为什么在声明管道通信前要先声明

int fd[2];这个语句的原因,这个语句就是用来记录进程分别以读写端打开管道文件的fd

int main() {

    int fd[2];
    int check = pipe(fd);
    
    if (check != 0) {
        printf("create pipe error\n");
        return 0;
    }

}

 

3136e193f7734aec90576867f63848c1.png

父进程在创建子进程时,子进程会拷贝一份父进程的进程地址空间,同样的,子进程也会拷贝父进程的文件描述符表

int main() {

    int fd[2];
    int check = pipe(fd);
    
    if (check != 0) {
        printf("create pipe error\n");
        return 0;
    }

    pid_t id = fork();

    if (id > 0) { /*执行父进程代码*/ };
    if (id = 0) { /*执行子进程代码*/ };

    return 0;

}

7e816d5d5bdc4b739456a450748d7560.png

 

接下来,我们明确父子进程谁是读端,谁是写端,就可以进行通信了,这里我们让父进程写数据给子进程,那么父进程就要关闭自己的读端,子进程就要关闭自己的写端

fd[0]是读端,fd[1]是写端

巧记:按照读音的顺序,读写,01,正好对应。还有1像一支笔,所以是写端,0像张开的嘴,所以是读端

int main() {

    int fd[2];
    int check = pipe(fd);
    
    if (check != 0) {
        printf("create pipe error\n");
        return 0;
    }

    pid_t id = fork();

    if (id > 0) { 
       close(fd[0]);
       /*关闭父进程的读端,接着执行父进程代码*/ 
    };


    if (id = 0) { 
       close(fd[1]);
       /*关闭子进程的写端,接着执行子进程代码*/ 
    };

    return 0;

}

现在读写双方都确定了,那写方如何给读方发数据,读方又如何读取写方的数据呢?

既然管道通信是借助文件系统实现的,那么是不是......没错,就是使用read和write函数,接下来通过一个demo来示例这个通信过程

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

int main()
{
    int fd[2];
    int check = pipe(fd);//声明创建管道
    if (check != 0) { printf("create pipe error"); return 0; }

    char test_buff[64] = "this is a communication test";
    pid_t id = fork();
    if (id > 0) {
        close(fd[0]);
        write(fd[1], test_buff, sizeof(test_buff));
        wait();
    }

    if (id == 0) {
        close(fd[1]);
        memset(test_buff, 0, sizeof(test_buff));
        read(fd[0], test_buff, sizeof(test_buff));
        printf("测试结果:%s\n", test_buff);
    }
    
    return 0;
}

88d9d838217b4c609e736cf6c19ecb9e.png

我们成功实现了父子进程之间的通信,接下来我们修改部分代码,然后刨析一下通信的过程

如下是修改后的代码以及运行结果

#include<iostream>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include<sys/wait.h>

using namespace std;

int main()
{
    int fd[2];
    int check = pipe(fd);
   
    if (check != 0)   {  std::cout << "create pipe error" <<endl; return 0; }
    
    char test_buff[64] = "this is a communication test";
    pid_t id = fork();
    if (id > 0)
    {
        close(fd[0]);
        write(fd[1], test_buff, sizeof(test_buff));
        std::cout << "我是父进程,我的pid是"<<getpid() << endl;
    }

    if (id == 0)
    {
        close(fd[1]);
        while (true)
        {
            sleep(1);
            memset(test_buff, 0, sizeof(test_buff));
            read(fd[0], test_buff, sizeof(test_buff)-1);
            std::cout << "我是子进程,我的pid是"<<getpid() << endl;
            std::cout << test_buff << endl;
        }
    }
    
    wait(nullptr);
    return 0;
}

9cd87f9821dc46649f578cba01b3597e.png

观察运行结果可以发现,子进程循环两次后就卡在了那里不动了,这是什么原因呢?

这是因为read函数是一个阻塞式函数,在上述程序中,父进程往管道中写入一次数据后,就进入了wait阻塞,等待回收子进程,子进程则是循环从管道中读取数据,等到把管道中的数据读完的时候,而父进程又没有关闭它的写端,此时子进程的read函数就会进入读堵塞状态,等待父进程继续向管道中写入数据,父进程已经进入了进程阻塞等待,自然不会再向管道中写入数据,因此就进入了卡死状态

细心的小伙伴可能会有疑惑,父进程只向管道中写入一次数据,子进程读取一次就应该将数据读取完了呀,子进程循环一次就该进入堵塞,而运行结果显示子进程的循环进行了两次呢?这是因为,我们使用write函数进行写入时,写入的大小是sizeof(test_buff)也就是64个字节,而我们用read函数读取数据的时候,读取的是sizeof(test_buff)-1,也就是63个字节,此时管道中还剩一个字节,管道并不为空,因此read函数还可以读取一次,所以循环就进行了两次

可能你会想为什么要读取sizeof(test_buff)-1个字节呢?一次读完不好吗?这是因为在C语言中有些函数会自动给字符数组的末尾添加/0,而有的函数又不会自动添加,如果一次读完,遇到了末尾自动添加/0的函数,就会将末尾的数据给覆盖掉,导致数据丢失,因此在不能分辨某个字符处理函数是否会自动在字符末尾添加/0的时候,为了安全,我们统一把字符数组的最后一位给留出来,也就是数据只读取字符数组大小-1个

上述的情况是父进程进入阻塞等待时,并没有关闭写端,导致子进程的read函数误认为父进程还会向管道中写入数据,于是就进入阻塞状态一直等待。

如果父进程写完了,并且关闭了自己的写端呢?如果管道中还有数据,那么子进程的read函数会继续读取数据,如果管道中没有数据了,那么read函数就会返回0,不会进入阻塞等待状态,因此在循环读取的场景下,一定要注意接收read函数的返回值,不然会进入死循环的状态的

如果父进程往管道中写入的数据很快,而子进程读取的速度比较慢的话,会出现什么情况呢?我们前面说过管道通信文件是借助文件系统实现的,但是管道通信文件跟一般的文件还不太一样,管道通信文件不像普通文件一样可以存放到磁盘中,管道通信文件不存放到磁盘上,和磁盘没有关系且没有inode,是操作系统临时分配的一块固定大小的内存,我们也称其为管道缓冲区,所以当写的速度太快,读的速度太慢,管道缓冲区被写满的时候,此时写方就会进入写入阻塞状态,直到缓冲区足够再次容纳写入的数据时,才会再次允许写入

bd6c7a1ffb74490396fcbeb8eece29c7.png

 

还有一种场景,如果写方还在继续向管道缓冲区写入数据时,而读方却关闭了读端,那么此时系统就会终止并杀死写端,因为读方都已经关闭读端了,再写也没有意义了

说了这么多,貌似并没有解释为什么会叫匿名管道,父子进程之间进行通信时,临时创建的这个管道文件并没有对应的文件名和inode,只是系统分配的一块内存空间,可以以文件的形式被父子进程打开或关闭,这一切工作都在不知不觉中由OS全部完成了,所以称为匿名管道,等命名管道文件看完,也可以回头对比着理解

现在我们回过头来理解开头讲的命令ps ajx | grep pid, 管道符|用于将一个命令的输出作为另一个命令的输入。在这个命令中,ps ajx命令的输出将作为grep pid命令的输入。 当这个命令在shell中执行时,shell会创建一个匿名管道。ps ajx命令形成的进程作为管道的写端,将其输出写入管道;而grep pid命令形成的进程作为管道的读端,从管道中读取输入。 因此,ps ajx和grep pid都作为shell的子进程,通过匿名管道进行通信。ps ajx将其输出写入管道,而grep pid从管道中读取数据,实现了两个命令之间的通信

命名管道通信

匿名管道应用的一个限制就是只能在具有亲缘关系的进程间通信
如果我们想在没有亲缘关系的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道,命名管道也是一种特殊类型的文件

 

命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
$ mkfifo filename


命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);

de0b1952ff404df49a4db29760bd2b57.png

int main()
{
    umask(0);
    if ( mkfifo("name_pipe_file", 0644) == -1) 
        perror("create name_pipe fail"), exit(0);
    return 0;
}

//在程序中创建命令管道,第二个参数是设置该命名管道文件的权限

运行结束会出现这么个文件

170c42128edc4677a32b806e8bbc20dc.png

命名管道文件是真的有文件名的,而且有自己的inode,但是它不会与磁盘进行IO 

除了创建一个命名管道,在不使用的时候需要由创建方取消命名管道连接

int unlink(const char *pathname);

ddccf7bb82cf4d85b8bc2434fd10c393.png 

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

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

命名管道的打开规则

如果当前打开操作是为读而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable:立刻返回成功
如果当前打开操作是为写而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO 

2e3e702b9d6543a880c4e263ed2f7e7d.png

父子间通信,子可以继承父的文件描述符表,父子生来就指向相同的文件,故而可以使用匿名通信的形式

两个不相干的进程,要想指向同一个文件进行通信,只能我们人为创建一个命名管道文件,让两个进程都指向这个命名管道文件,与磁盘进行IO操作的速度相较于内存来说非常慢,没必要为了扩大管道缓冲区的容量,将管道缓冲区的数据写往磁盘,再读出来,这会严重影响通信速度,故而管道通信都不会与磁盘进行IO

 下面是测试命名管道通信的demo,大家可以自行测试一下

                                  /* process A */


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


int main()
{  
    umask(0);
    if ( mkfifo("name_pipe_file", 0644) == -1) perror("create name_pipe fail"), exit(0);

    int fd = open("name_pipe_file", O_RDWR | O_APPEND);
    if (fd < 0) perror("open name_pipe_file fail"), exit(0);
    
    //send message to process B
    while(true)
    {
        char buff[1024] = "hey ,this's process A, can you receive me?";
        int n = write(fd, buff, sizeof(buff));
        if(n < 0) perror("write fail"), exit(0);
        sleep(1);
    }

    return 0;
}



                                 // process B
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>


int main()
{
    int fd = open("name_pipe_file", O_RDWR | O_APPEND);
    if (fd < 0) perror("open name_pipe_file fail"), exit(0);

    //receive message from name_pipe
    while(true)
    {
        char buff[1024];
        int n = read(fd, buff, sizeof(buff));
        if (n < 0) perror("read fail"), exit(0);
        fprintf(stdout, "%s\n", buff);
        sprintf(buff, "%s", "YES, this's process B, I can receive your message");
        fprintf(stdout, "%s\n", buff);
        printf("\n");
    }
    return 0;
}

 

共享内存 

前面说过,管道通信只是进程间通信的其中一种方式,接下来探讨的进程间通信形式都属于System V通信规则,比较重要的是共享内存,不管用哪种通信方法,通信的本质就是让通信双方能够使用看到和使用同一块空间,有了前面内容的铺垫,我们理解共享内存的理论并不困难,但共享内存的操作稍微有一些复杂,不过请放心,笔者会按照步骤顺序逐步进行,只要按照正常步骤走,就能减少缺漏的情况

eca3cc518ac642eaa79a6b98e30306f4.png

如上图,这是两个普通进程A和B在系统上工作的原理图,现在想让进程A和B之间进行通信,共享内存的方法是如何做的呢?首先由通信的其中一方负责向系统申请共享内存通信,这里就让进程A负责好了,OS收到请求后,在物理内存划出一块内存区域,用来这保证了进程A和进程B能够看到并使用同一块内存空间,如下图的红色操作

d744beadc1944690a423697dd06bef54.png

共享内存申请好了,但是并不意味着就可以直接用了,因为进程A,B的页表里并没有关于共享内存区域的映射,因此,进程A和B要分别与共享内存区域进行挂接,挂接的过程就是将共享内存区域的物理地址添加到进程的页表映射中,这样进程就能通过页表映射到共享内存区域了,如下图的蓝色操作

196227bb15f144c681ceb3fe732c6871.png

等到挂接完成后,进程A和B就能看到并使用同一块内存空间了,至此就可以开始通信,等到通信结束之后,通信双方要分别取消掉对共享内存区域的挂接操作,如下图绿色操作

4b2c7c60f4604d65b402991d25afb03a.png

取消挂接了并不算彻底结束了,因为共享内存的申请是直接在物理内存上进行的,不会随着进程的退出而释放,只有手动释放,或者系统重启的时候才会释放,因此,进程不再通信后,应当由共享内存申请方在进程退出前释放共享内存,如下图黄色操作 

2beab29cf772436788f0be01f1d7f06c.png

 

至此,共享内存的原理已经完成,总共分成了4个步骤实现共享内存通信,接下来就是实践检验理论的部分了

首先就是进程A向系统申请共享内存通信,那么该用哪些系统调用来完成呢?

int shmget(key_t key, size_t size, int shmflg)  功能:用来创建共享内存

key_t key:共享内存的字段标识
size_t size:要申请的共享内存的大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的

常用的标志有两个:

1.IPC_CREAT:如果该共享内存字段已经存在,就获取它,如果不存在就创建它

2.IPC_EXCL:搭配IPC_CREATE使用,如果创建失败就返回错误码

 

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

 


这里需要解释一下key,共享内存是用来进程间通信的,那么系统中那么多进程,肯定会存在很多的共享内存,那么系统要管理这些共享内存就要给这些共享内存标号,标明它的唯一性,这个key值就是这段共享内存在系统中的唯一性编号,通过这个唯一性编号,以及你要申请的共享内存的大小,系统就可以帮你申请一块共享内存了

key的值通过ftok()函数来创建,ftok()的说明如下

29fa14f4ee2046bfbd24705866731853.png

简单来说就是给一个文件路径名和一个int值,那么ftok函数就会生成一个能唯一标识共享内存的key值,也就是shmget()函数中第一个参数的key值

当shmget()三个参数齐全,创键共享内存成功的时候,就会返回一个共享内存的标识码shmid

,可能你会惊讶,刚才用ftok已经生成了标识共享内存的码,怎么这里又返回了一个

其实这两个码都可以用来标识共享内存,shmid与key的关系就类似于文件系统中的fd和inode,key就像inode一样,是给系统看的,shmid和fd类似,是给应用层的进程使用的,为何要多此一举呢?这是为了系统层和应用层之间的解耦,避免因应用层的shmid出现错误而影响了系统层的正常工作

 

接下来陈述共享内存创建的过程,当进程A和B开始通信时,进程A和B会根据指定的路径和int值,用ftok函数生成相同的key值,进程A来负责申请共享内存,填上key值,申请共享内存的大小和相关标志位,shmget()函数会返回一个shmid,这个shmid是供进程来标识共享内存的

接下来进程B同样使用shmget()函数,因为要互相通信,进程A和B使用的是相同的路径和int值,所以ftok生成的key值就相同,将key值,共享内存大小,相关标志位填完以后,shmget函数会检测出当前这个key值已经申请过共享内存,于是将这个key值标识的共享内存的shmid返回去,至此,进程B和进程A都拿到了相同的shmid,表明进程A,B都能够找到这块共享内存,这便是第一步的整个过程

进程A和B都能够通过shmid找到用于通信的共享内存段,但是不意味着可以直接开始通信,接下来双方要执行前面说过的第二个步骤,就是与共享内存段建立连接

 

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

shmaddr表示从哪个地址处开始连接,我们填null会默认从共享内存段的首地址处开始连接,shmflg是根据实际需要填写的标志位,我们用不到填0就好

将三个参数填入后,shmat就可以连接了,连接成功会返回一个地址,通过这个地址就可以使用共享内存了,连接失败会返回-1

b9945f49588946a3abe98c9257bdb856.png

有了地址,使用共享内存就和使用指针以及数组一样,就可以进行从中写入数据,读取数据等操作,笔者将在后面用实例展示

接下来是通信结束,取消挂接的操作

int shmdt(const void *shmaddr);

直接往参数中填入挂接时返回的地址,就可以取消挂接了

最后由共享内存申请方释放掉申请的共享内存,不然进程退出后,共享内存并不会自动释放

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

第二个参数填入IPC_REID表示移除共享内存,第三个参数不需要操心,直接填null

至此就可以移除共享内存,通信结束

1dc70914fba8467ba677568c42c551e8.png

用一个demo来演示共享内存的用法

//通信双方所引用的头文件   share_test.h

#include<iostream>
#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<cstring>

//如下两个定义用于ftok创建唯一的标识符
#define PATHNAME "."
#define PROJ_ID 0x22


//通信进程A,读取共享内存的信息,同时负责共享内存的申请和释放
#include"share_test.h"


int main()
{
   
    key_t _key = ftok(PATHNAME, PROJ_ID);
    if (_key < 0) perror("get key fail"), exit(0);

    //0666是权限标志,即申请的共享内存有哪些可用权限,不加权限无法读写
    int shmid  = shmget(_key, 4096, IPC_CREAT|IPC_EXCL|0666);
    if (shmid < 0) perror("shmget fail"), exit(0);

    char *start = (char*)shmat(shmid, NULL, 0);
    
    for (int i = 0; i < 45; i++){
            
         printf("%s\n", start);
         sleep(1);
    }

    sleep(3);
    shmdt(start);
    int n = shmctl(shmid, IPC_RMID, NULL);
    if (n < 0) perror("destory share_memory fail");

    return 0;
}
//通信进程B,往共享内存中写入数据
#include"share_test.h"

int main()
{
    
    key_t _key = ftok(PATHNAME, PROJ_ID);
    if (_key < 0) perror("get key fail"), exit(0);

    int shmid  = shmget(_key, 4096, IPC_CREAT);
    if (shmid < 0) perror("shmget fail"), exit(0);

    char *start = (char*)shmat(shmid, NULL, 0);

    for (int i = 0; i < 26; i++ ){
        start[i] = 'A'+i;
        sleep(1);
    }
    
    shmdt(start);
    sleep(2);
    return 0;
}

共享内存的优缺点

共享内存的优点:所有的进程间通信,速度是最快的

能很大程度较少数据的拷贝次数

共享内存的缺点:不支持同步和互斥操作,没有对数据做任何保护

不支持同步与互斥也就是说,允许同时写入或读取数据,这会导致数据杂乱无法使用,大家可以使用此篇文章管道的知识来帮助共享内存实现互斥功能

消息队列 

消息队列也是System V通信的一种,不过消息队列现在的应用场景很少,几乎不怎么用,因此,这里笔者大概描述一下消息队列的工作原理,不再深入细究

消息队列这种通信形式可以看作是OS为通信双方建立一个消息队列结构,双方的数据交换就通过消息队列进行,在这个消息队列的数据元素中包含该进程的标识符,以及该进程要写入数据,读取方则可以根据数据元素的进程标识符来确定是否要接收该进程的数据

cdf9a605c76b4449979b39401baaa4c6.png

这个通信原理很类似管道,不过和管道不同的是,管道发送的是数据流,要人为判断数据边界,消息队列的数据类型是一个独立的结构,每发一次数据都被独立包装,消息队列的数据会长期保存,但当容量填满时就会拒绝接收消息直到空间足够,并且消息队列允许通信双方同时读写

信号量

信号量并不像前面的通信形式那样,给通信双方传递内容,而是一种计数器,在了解信号量是什么之前,我们先来了解一些概念

 

公共资源:被多个进程同时可以访问的资源

两个或多个进程之间相互通信,所能看到的共同的内容就是公共资源,公共资源的概念不难理解,因为是公共资源,所以多个进程就可以同时修改或读取,其他进程没有阻拦的权利,这会导致数据不一致,前一个进程刚准备读,后一个进程立马把数据更改了,这些问题要归究到公共资源受不受保护

 

我们称被保护起来的公共资源为临界资源,不被保护的就是非临界资源

同样的道理进程中肯定有对应的代码来访问这些临界资源,访问这些临界资源的代码我们就称之为临界区,其他的代码称为非临界区

 

那么如何做到公共资源受到保护呢?那就是支持同步与互斥功能(在线程这部分内容会详细说明),要支持同步与互斥功能就要保证操作的原子性

所谓原子性即不可分割性,一个操作要么成功完成,要么执行失败,没有中间状态

信号量究竟是什么呢?信号量我们可以理解成是一个计数器,用来统计公共资源的数目,这就像看演出一样,演出的门票数量是一定的,门票这种可以被公众购买的就是公共资源,那么门票的数量就是信号量,如果门票数量没了,那么这场演出就没法看了

进程申请公共资源也是如此,首先公共资源是有限的,信号量就是用来记录公共资源数目的,进程要想访问公共资源,首先要先申请信号量,这就要求信号量本身就得能被所有的进程看到,而能被所有进程看到和使用的资源不就是公共资源嘛,因此信号量本身就是公共资源,其次,进程每申请一份资源,信号量的数目就会--,进程每释放一份资源,信号量的数目就会++,这就要保证++,--操作必须是原子性的,要么完成,要么失败

关于信号量,后在线程那部分详细讲解其作用,这里理解这些概念就好,不理解也无妨,学完线程,再看这些概念就会很轻松

OS对进程间通信方法的管理 

共享内存,消息队列,信号量同属System V通信协议,进程之间相互通信,OS必然会存在大量的共享内存,消息队列,信号量用来通信,那么OS该如何管理这些共享内存,消息队列以及信号量的呢?

想搞明白OS如何管理这些通信工具,那么就得查看共享内存,消息队列,信号量的结构字段,前面并没有提及这些内容,因为会给读者增加很多负担,这里我们也不打算详细介绍其内核字段,这里只给出其结构,感兴趣的可自行研究 

共享内存的内核结构字段 

e26e7fba8993401eb1f7e78ffc14e7b0.png

消息队列的内核结构字段

f092b5a4f9cc4236a62973c3be010208.png

 信号量的内核结构字段

c8ac6dbd10a64b05bb18838855e92f00.png

可以发现,这些字段中第一个总是struct ipc_perm类型,不知道大家是否还记得结构体的规则呢 ?结构体的地址就是第一个数据类型的起始地址,也就是说我们通过struct ipc_perm我们就能找到 struct shmid_ds,  struct msqid_ds,  struct semid_ds

5a08191892b84b3e9467255898371663.png

 这就是OS管理System V中这三种通信形式的理论方法

至此,我们就一同完成了进程间通信的学习,这篇文章中比较重要的部分就是管道通信和共享内存了,这两种也是笔者着重描述的通信形式,像消息队列这种使用情况不多的通信形式,感兴趣可以自行查阅,信号量我们在后面还会谈到

 

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浪雨123

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值