进程间通信

进程间通信

本节内容
  • 进程间通信介绍
  • 管道
  • 消息队列
  • 共享内存
  • 信号量
为什么需要进程间通信
  • 因为每一个进程都有他自己的独立的虚拟地址空间,导致了进程的独立性,从而每一个进程和每一个进程都是分开的,进程是操作系统分配资源的基本单位,那么, 假如说现在有一个工作是需要两个进程协助完成的,比如说一个去读,一个去写,但是,资源又是各自独立的,所以我们现在需要一种体制,打破这种资源的独立性,那么就想到了,我们可以在操作系统内核中去定义一个东西,这个东西是这个进程可以访问的,当然别的进程同样也是可以访问的,那么进程和进程就可以拿到各自所需要的空间了
  • 通过进程间通信可以让不同的进程进行协作
进程间通信目的
  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知(它们)发生了某种事件(如进程终止时要通知父进程)
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
    在这里插入图片描述
进程间通信发展
  • 管道
  • System V进程间通信
  • POSIX进程间通信
进程间通信分类
管道
  • 匿名管道pipe
  • 命名管道
System V IPC
  • System V 消息队列
  • System V 共享内存
  • System V 信号量
POSIX IPC
  • 消息队列

  • 共享内存

  • 信号量

  • 互斥量

  • 条件变量

  • 读写锁

  • 目前最大的进程间通信的技术是网络

管道
什么是管道
  • 管道是Unix中最古老的进程间通信的形式。

  • 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

  • ps和grep是命令,同时也是一个程序

  • 管道其实就是在内核中开辟了一块内存,也可以称之为内核缓冲区
    在这里插入图片描述

  • 执行我们自己所写的代码的时候,我们就是处在用户态的

  • 如果执行的是库函数或者说是其他的代码的话,那么执行的就是内核态,在内核中是有一块缓冲区的,这块缓冲区就是用来支持不同的进程进行进程间通信的,ps是一个程序,这个程序跑起来的话,就会产生一个进程,同理,grep也是一个程序,这个程序跑起来的时候也会产生一个进程(当程序跑起来的时候是会去产生一个进程的)

  • ps aux的输出结果是通过管道传递给grep程序的,从而grep程序可以对其进行进一步的处理
    在这里插入图片描述

管道的分类
  • 匿名管道—在内核中创建出来的一块内存是没有标识符的(匿名管道也就是说,在内核中的那一块内存缓冲区是没有标识符的)
  • 那么,如何去创建一个管道?
  • 使用man 2 pipe来进行查看—成功的话,返回0,失败的话,返回-1
    在这里插入图片描述
  • 函数中的参数是fd本质上是一个出参
  • 那么现在又有一个问题了,什么是出参和入参呢以及什么是输入输出型参数呢
  • 出参(输出型参数)—参数在调用函数的内部赋值,在函数外部来进行使用
  • 入参(输入性参数)—参数在函数内部使用
    在这里插入图片描述
  • 这个时候我们称fd为输出形参数
void func(int* fd)
{
	//这个时候,fd是在函数内部进行赋值的操作,在函数外部使用,所以本质上fd其实是相当于一个出参的
    *fd = 10;
}
int main()
{
    int fd;   //进行定义
    func(&fd);   //然后在函数的内部进行赋值的操作  那么称fd为输出型参数
    printf("%d\n", fd);
    return 0;
}
  • 这个时候我们称fd为输入型参数
void func1(int fd)
{
    //fd --> 
    //也就是说fd是在函数内部使用的
    //在这里拿到的值是10
}

int main()
{
    int fd;
    func(&fd);
    func1(fd); //传值
    printf("%d\n", fd);
    return 0;
}
创建管道的代码
  • fd没有被我们赋值,fd的赋值操作是发生在pipe函数内部的—出参
#include <stdio.h>
#include <unistd.h>

int main()
{
    int fd[2];//这个时候我我们没有去个fd赋值,fd是在函数内部来进行给赋值操作的,那么此时fd就是出参
    int ret = pipe(fd);   //在定义的时候我们要给他一个出参,就是这一行上面那一行的内容
    if(ret < 0)
    {
        perror("pipe");
        return -1;
    }
    
    printf("fd[0]: %d\n", fd[0]);
    printf("fd[1]: %d\n", fd[1]);
    
    while(1)
    {
        sleep(1);
    }
    return 0;
}
  • 运行结果为:
    在这里插入图片描述
  • 3和4其实代表的就是文件描述符
  • 之所以闪烁是因为有两个可能性,一个可能性是这个文件是软链接文件,还有一个可能型就是这个东西指向内存中的一块缓冲区,并且这块缓冲区是没又标识符的
    在这里插入图片描述
  • 由上图可以看出fd的值其实就是文件描述符,同时说明pipe是一个软链接文件,闪烁表示其是软链接文件,或者说是他指向内核中的一块内存,而且这块内存没有标识符,所以会又闪烁的现象发生
  • 数据流向只能从写端流向读端,是单向的,不能从另外的一段移向另一端
  • 调用pipe函数就可以在内核空间当中去创建一个管道,pipe函数的功能其实就是去创建一个管道的
    在这里插入图片描述
#include <stdio.h>
#include <unistd.h>

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

在这里插入图片描述

如何查看当前进程打开了哪些文件描述符
  • 我们可以通过去查看那个文件,那个文件在根目录下,proc这个文件夹中以进程号命名的文件
    在这里插入图片描述
    在这里插入图片描述
  • 如果说这个时候进程结束了,或者说挂掉了,那么那个以进程号命名的文件夹也就随之而没有了
  • 进程如果结束了,那么进程所对应的文件描述符其实也就不复存在了
    在这里插入图片描述
  • 现在我们往管道中去进行读写的操作
#include <stdio.h>
#include <unistd.h>

int main()
{
        int fd[2];
        int ret = pipe(fd);
        if (ret < 0)
        {
                perror("pipe");
                return -1;
        }
        printf("fd[0]:%d, fd[1]:%d\n", fd[0], fd[1]);
        write(fd[1],"linux-11",8);
        char buf[1024]={0};
        read(fd[0],buf,sizeof(buf)-1);  //-1是为了放置\0
        printf("buf is %s\n",buf);
        while (1)
        {
                sleep(1);
        }
        return 0;
}
  • 那么,现在有新的问题了,我到底是把这个内容拷贝了,还是说,我从根本上把这段内容读走了
  • 那么,如何去验证这个问题呢?我们可以再进行一次读,如果说,第二次仍然读到了, 那么我们就只是拷贝了一下,如果说,我们第二次读的时候不再能读出来了,那么就是把内容读走了
#include <stdio.h>
#include <unistd.h>
#include<string.h>

int main()
{
        int fd[2];
        int ret = pipe(fd);
        if (ret < 0)
        {
                perror("pipe");
                return -1;
        }
        printf("fd[0]:%d, fd[1]:%d\n", fd[0], fd[1]);
        write(fd[1],"linux-11",8);
        char buf[1024]={0};
        read(fd[0],buf,sizeof(buf)-1);  //-1是为了放置\0
        printf("buf is %s\n",buf);
        //因为之前已经读过一次了,所以需要把buf的内容进行清空,然后再去再次进行读的操作
        memset(buf,'\0',sizeof(buf));

        read(fd[0],buf,sizeof(buf)-1);  //-1是为了放置\0
        printf("buf2 is %s\n",buf);
        while (1)
        {
                sleep(1);
        }
        return 0;
}
  • 对上面的代码进行运行之后,我们发现我们希望第二次可以打印出来的东西实际上是没有打印出来的,得到的结论就是,读的时候是把内容读走了
  • 那么,我们如何去佐证代码其实是运行到了我们第二个read的位置,然后没有把我们希望所打印出来的东西打印出来的呢?我们其实可以想到,通过查看函数的调用栈来查看当前正在运行着什么样子的代码,查看函数调用栈,就是用pstack命令
  • 调用堆栈是从下往上看的,那么下面这张图就可以看出来再main函数的第25行调用了一个函数,这个函数是read_nocancel,就说明阻塞在第二个函数的位置了,所以就相当于是第一次读之后,管道里面就不再有内容了
    在这里插入图片描述
  • 25行的代码,是第二次的read函数的调用,卡在这一行的原因是因为管道中现在已经没有内容了,所以是不能进行第二次的读取的
    在这里插入图片描述
小结
  • 管道是单双工通信,数据只能从写端流向读端(网络和TCP就是全双工通信,其实就是可以理解为双向通信)
  • 管道提供流式服务,也就是说管道中的内容一旦被读走了,里面的内容其实也就不存在了,好处在于:读端可以决定每次读多少个字节(流式读取,可以决定一次读取的大小到底是多少,这是其中的一个优点)
  • 管道的生命周期跟随进程,因为管道其实是在我们的内核当中去创建的,当进程退出的时候,这个管道的内存其实也就是释放了
那么,我们如何去验证他们是读端和写端呢?
  • 通过文件描述符的属性
那么,我们其实就是需要去了解文件描述符的属性的
fcntl函数(可以知道文件描述符的相关属性)
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main()
{
        int fd[2];
        int ret = pipe(fd);
        if (ret < 0)
        {
                perror("pipe");
                return -1;
        }
        printf("fd[0]:%d, fd[1]:%d\n", fd[0], fd[1]);
        int flag = fcntl(fd[0], F_GETFL);
        printf("flag : %d\n", flag);
        while (1)
        {
                sleep(1);
        }
        return 0;
}
  • 代码运行结果如下所示:
    在这里插入图片描述
  • 因为打印出来的flag的值是0,我们通过宏的查看,可以知道0表示的是8进制或者说是10进制,当然,在16进制下,都是0,那么0表示的含义其实就是只读的操作,那么,这样其实就是验证了fd[0]是只读端
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main()
{
        int fd[2];
        int ret = pipe(fd);
        if (ret < 0)
        {
                perror("pipe");
                return -1;
        }
        printf("fd[0]:%d, fd[1]:%d\n", fd[0], fd[1]);
        int flag = fcntl(fd[0], F_GETFL);
        printf("flag-fd[0] : %d\n", flag);
        flag= fcntl(fd[1], F_GETFL);
        printf("flag-fd[1] : %d\n", flag);
        while (1)
        {
                sleep(1);
        }
        return 0;
}
  • 代码运行结果如下所示:

  • 那么就说明了fd[1]只具有只写的操作,连读的操作都不具有
    在这里插入图片描述

  • 我们在打印fd[0]的时候,发现,打印出来的结果是0,那么这个0到底代表的是什么意思呢

  • 那么我们取看看源码到底怎么说
    在这里插入图片描述
    在这里插入图片描述

  • 从上面的图中我们就可以看出来0表示的是只读,1表示的是只写,2表示的是可读可写(这里所说的都是八进制),八进制的0转换成10进制的0还是0,转换成16进制的0还是0,如果我们对代码进行运行,发现运行出来的结果是0的话,那么就说名这个文件描述符具有只读的属性

  • 因为…叫做可变参数列表,所以这个函数是一个不定参函数,后面的arg其实是可以忽略的,也就是说这个函数的参数的个数其实是不确定个的

  • 如果设置属性失败的话,其实是会返回-1的

  • 如果设置属性失败的话,就会返回-1
    在这里插入图片描述

如何让两个进程通过匿名管道进行进程间通信
  • 当前的进程是通过操作fd[0]和fd[1]从管道中去读取数据的,那么假如说要支持两个不同的进程去操作这个管道中的数据的话,应该使用什么样的方法呢?
  • 可以想到的方式是我们可以通过复制调用父进程,从而创建一个子进程出来,子进程会拷贝父进程的task_struct结构体,那么就相当于是把所有的东西全部都拷贝过来了,全部拷贝就相当于是把管道也拷贝过来了
  • 通过创建一个子进程,子进程就把父进程的所有的东西全部都拷贝过来了
  • 所以得出来的结论就是匿名管道只支持具有亲缘关系的进程进行进程间通信,同时要注意,需要先去创建管道然后再去创建子进程
  • 如果两个进程之间是没有任何关系的话, 那么他们其实是不可以对匿名管道来进行一些操作的,因为匿名管道是没有标识符的
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

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

在这里插入图片描述

  • 我们可以让父进程去读,子进程去写,当然,我们也可以让子进程去写,父进程去读,这其实都是可以的
  • 下面的代码就是让子进程去进行写的操作,然后让父进程去进行读的操作,当然,父子进程之间的读写时可以进行交换的操作的,这是完全OK的
    在这里插入图片描述
    在这里插入图片描述
  • 那么我们如何去知道到底是父进程先去执行还是子进程先去执行呢?----其实这个问题的答案我们是不知道的,因为父子进程他们其实是抢占式执行的
  • 那么假如说现在先是我的父进程先去运行的话,那么其实父进程运行都read的地方其实就是回去阻塞掉的,因为现在管道里面是没有任何东西的,那么现在子进程去写,他往管道里面去写了东西,这个时候父进程就可以读到内容了
  • 那么现在继续去换另一种可能性,假设说现在是子进程先去运行,那么子进程就会先去进行写的操作,然后父进程去进行读的操作,因为你现在管道里面是又内容的,那么其实就是直接一读其实就是读取到了的
    在这里插入图片描述
  • 同时需要注意的一点是,父子进程中虽然fd[0]和fd[1]的值是一样的,但是本质上其实是两个不同的东西,比如说你关闭了父进程中的fd[0],这个时候是不会影响你子进程中的fd[0]的(数值一样,但是本质上其实还是不一样的东西)
  • 虽然说,值是一样的,但是本质上实际还是两个不一样的东西,一个的关闭与否,是不回去影响另一个的
  • 那么,现在问题又来了,管道到底能存储多大的数据
管道到底能存储多大的数据
  • 那么,如何去验证管道现在到底能存储多大的数据呢?一个不是那么好的方式其实就是我现在不读的,我就只是往管道中去写数据,看我到底能写多少数据,然后我就可以知道这个管道到底能存储多大的数据了,每次只写一个字节,总会有把他写满的时候
  • 一直往管道里面去写,看什么时候可以写满
#include <stdio.h>
#include <unistd.h>

int main()
{
    int fd[2];
    int ret = pipe(fd);
    if(ret < 0)
    {
        perror("pipe");
        return -1;
    }

    int count = 0;
    while(1)
    {
        write(fd[1], "h", 1);
        count++;
        printf("count:%d\n",count);
    }
    return 0;
}
  • 运行结果如下所示:
    在这里插入图片描述
  • 然后我们使用65536/1024=64k,单位是k
    在这里插入图片描述
  • 现在假设我们所需要写入的linux和python都是大于4k大小的,那么他在进行操作的时候就很有可能是会被打断的,有可能刚刚写入lin这个操作就被打断了,因为要操作的东西是大于4k的,所以现在的操作已经不具有原子性了
    在这里插入图片描述
  • 但是,如果你现在操作的数据是一个小于4k的数据的话,操作起来就是具有原子性的,也就是说我进程A在进行写操作的时候,你的进程2是不可以进行写操作的,也就是说,其是具有原子特性的
管道对应的读写端文件描述符设置成为非阻塞属性(也就是说如果read发现你现在管道中其实是没有数据的,就返回了,而不是阻塞在read函数内部)

在这里插入图片描述

  • 1.1的条件下其实是不一定会会产生僵尸进程的,系统只是会把调用write函数的哪个进行杀死掉,如果说你当前是子进程在调用wirte的话,那么你的子进程就会被杀死掉,但是这个时候子进程的资源没有被释放,那么从而来看,子进程就会成为僵尸进程,当你是父进程来调用write函数的时候,这个时候你的子进程会成为孤儿进程,因为你的父进程已经退出了,父进程如果推出的话,会被bash所收
  • 1.2的情况下,此时我的写端是非阻塞的属性,也就是说当写满的时候他不会再阻塞再write函数的地方了,当写满的话,他就会直接告诉我说资源不可用了,-1其实就是write函数的返回值
  • SIGPIPE是一个信号,是管道破裂的信号
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

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

    int flag = fcntl(fd[0], F_GETFL);
    fcntl(fd[0], F_SETFL, flag | O_NONBLOCK);

#if 0
    //写端文件描述符被设置成为非阻塞属性
    int flag = fcntl(fd[1], F_GETFL);
    printf("flag-fd[1] : %d\n", flag);

    fcntl(fd[1], F_SETFL, flag | O_NONBLOCK);

    flag = fcntl(fd[1], F_GETFL);
    printf("flag-fd[1] : %d\n", flag);
#endif

    int pid  = fork();
    if(pid < 0)
    {
        perror("fork");
        return -1;
    }
    else if(pid == 0)
    {
        //child
        //close(fd[1]);

        //while(1)
        {
            char buf[1024] = {0};
            ssize_t ret = read(fd[0], buf, sizeof(buf) - 1);
            printf("ret = %d\n", ret);
            if(ret < 0)
            {
                perror("read");
               // break;
            }
            sleep(1);
        }


        //child
#if 0
        close(fd[0]);
        int count = 0;
        while(1)
        {
            ssize_t w_size = write(fd[1], "l", 1);
            printf("w_size : %d\n", w_size);
            if(w_size <= 0)
            {
                perror("write");
                break;
            }
            count++;
            printf("write : %d\n", count);
        }
#endif
    }
    else
    {
        //close(fd[1]);
        while(1)
        {
            printf("i am father\n");
            sleep(1);
        }
#if 0
        //father
        //close(fd[0]);
        //char buf[1024] = {0};
        //read(fd[0], buf, sizeof(buf) - 1);
        //printf("i am father: buf is %s\n", buf);
        while(1)
        {
            printf("i am father\n");
            sleep(1);
        }
#endif
    }
    while(1)
    {
        sleep(1);
    }
    return 0;
}
命名管道
  • 命名管道也是在内核中开辟的一段缓冲区,只不过是这段缓冲区是有标识符的,意味着不同的进程不需要有亲缘关系,只需要通过标识符就能找到这个缓冲区了
命名管道的创建
  • 那么,我们现在通过mkfifo fifo创建出来了一个fifo这样的文件,那么我们现在来试验一下,我们使用vim fifo可以对fifo这个文件进行写操作吗,通过验证,我们的出来的结论是fifo这个文件是不可以被我们写的,就是说fifo这个命名管道的文件并不是让我们拿来直接写的,而是内核中那块缓冲区的标识符那么,当我们使用open打开这个文件之后,我们就可以操作内核中那个的那块内存了,我们可以通过fifo这个管道文件找到内核当中创建的那一段缓冲区,使用open打开之后我们就可以对他进行正常的读写操作了
  • 也就说,我们可以通过这个文件找到内核中的那块缓冲区
  • fifo的意思其实是first in first out,因为管道是先进先出的,这其实很好理解
    在这里插入图片描述
  • fifo是管道文件,因为前面有一个p,p表示他是一个管道文件,这个文件是无法直接进行打开来写入的
  • 管道文件没有办法直接进行打开,如果你要直接进行打开的话,其实就会卡死了
    在这里插入图片描述
  • 创建成功返回0,创建失败返回-1
  • 使用man 3 mkfifo,打开mkfifo函数的一些解释
    在这里插入图片描述
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
int main()
{
        int ret=mkfifo("./fifo_test",0664);  //返回值,成功的话返回0,失败的话返回-1 
        if(ret<0)
        {
                perror("mkfifo");
                return -1;
        }
        printf("create success\n");
        return 0;
}

在这里插入图片描述

  • 只要进程把fifo_test这个文件打开了,他就可以去操作内核中的那块缓冲区了
  • 现在假如说右边的进程调用write方法向管道中去写一些东西,然后左边的进程调用read方法去读取管道中的内容的话,那么这样,其实就是完成了一次进程间的通信
    在这里插入图片描述
命名管道的特性
  • 命名管道的生命周期也是跟随进程的
  • 命名管道是具有标识符的
  • 其他的特性和匿名管道是一摸一样的(除了亲缘关系),什么非阻塞属性啊,管道的大小啊,原子性啊都是和匿名管道是一模一样的
共享内存
  • (只有知道了共享内存的原理,才会知道其为什么支持两个进程进行数据交换)
  • 内存指针是指向进程虚拟地址空间的,进程虚拟地址空间实际上是不能真正去存储数据的,真正存储数据的地方其实是物理内存
  • 共享内存不是在虚拟地址上去开辟一块内存空间,而是说,实在物理地址上去开辟一块内存空间,而这一段空间我们可以通过页表去映射到虚拟地址空间中的共享区中,可以映射到我们自己的独立的虚拟地址空间当中,把那一段缓冲区的内容拷贝过来
  • 共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
    在这里插入图片描述
    在这里插入图片描述
共享内存数据结构

在这里插入图片描述

共享内存的原理
  • 在物理内存中开辟了一段空间,这段物理内存的空间可以被不同的进程附加到自己的共享区当中,附加的进程通过操作共享区来交换数据
  • 假设进程A操作共享区的某一块地址,向共享区中写入了呵呵这样子的内容,然后他把地址附加到了自己的共享区中,通过页表这个映射关系,把呵呵这样子的内容写入到了共享内存,然后与此同时,进程B也在附加着,然后它可以通过他附加的地址把呵呵这样子的内容读入自己的进程,也就是说我只是去访问呵呵,并没有把呵呵这个内容拿走,并没有把呵呵这个内容拿走,可以理解成只是对呵呵这个内容进行访问的操作
  • 进程映射到自己的虚拟地址空间中的哪个位置是不确定的,但是物理地址一定是确定的,真正的物理地址中的内容一定是一摸一样的
  • 通过页表来进行映射,映射到各自虚拟地址空间上的哪个位置其实是不确当的,但是物理地址一定是一摸一样的
共享内存的接口
  • 我们需要通过key这个标识符来找到这一块共享内存,标识符的名称可以随意地给出,只要不和系统中地标识符重名就可以随意地给出标识符的名称,通过key这个标识符,我们可以唯一的去标识一块共享内存

  • 那么,首先第一步其实就是去创建一个共享内存

  • 首先需要去创建共享内存shmget(sharememoryget)

  • 第一个参数其实算是共享内存标识符,也就是说,我们需要通过过这个标识符,来找到这个共享内存,不同的共享内存通过共享内存标识符来进行区分

  • 共享内存大小的单位是字节

  • 在使用shmget这个函数的时候一定要自己刚刚创建出来的共享内存

  • 返回值返回的是共享内存的操作句柄
    在这里插入图片描述

#include <stdio.h>
#include <unistd.h>
#include <sys/shm.h>
#include <string.h>

#define KEY 0x89898989

int main()
{
    int shmid = shmget(KEY, 1024, IPC_CREAT | 0664);
    if(shmid < 0)
    {
        perror("shmget");
        return -1;
    }
    //create sucess
    while(1)
    {
        sleep(1);
    }
    return 0;
}
  • 使用ipcs -m 来查看共享内存
    在这里插入图片描述
    在这里插入图片描述
  • key是标识符,shmid是共享内存操作句柄,就是通过共享内存操作句柄来操作这块共享内存的,owner是所属用户(也就是说这个共享内存是谁创建的),perms是权限,bytes是共享内存的大小,nattch是附加的进程数量(就是说现在有多少个进程附加到了这个共享内存的上面,dest是destroy,就是已经销毁了
  • 我们在创建这个共享内存的时候,可以给他按位或上一个八进制的权限
  • 共享内存的声明周期是跟随操作系统内核的
  • 如何删除一个共享内存—使用命令ipcrm -m 后面加上共享内存的操作句柄,就可以把这个共享内存删除掉了
  • 有两个遗留的问题,一个是权限,另一个是key为0x00000000,这个是什么情况或者说这个是什么意思,如果收一开始创建共享内存的时候,没有给出的权限的话,那么使用ipcs -m 所查看到的,其实就是权限是0,如果给出了那么,查看到的就是你所给出的权限,设置权限的话,可以给它或上八进制的数字
  • 同时需要注意共享内存的生命周期是跟随操作系统内核的
    在这里插入图片描述
共享内存的标识符和共享内存的操作句柄的区别是什么
  • 比如说,现在想要去看电视,电视其实就是一个标识符, 是用来标识的,但是我现在如果希望去操控这个电视的话,我其实是需要用到遥控器去操控这个电视的,那么,遥控器其实就是操作句柄
    在这里插入图片描述
将共享内存附加到进程

在这里插入图片描述

#include <stdio.h>
#include <unistd.h>
#include <sys/shm.h>
#include <string.h>

#define KEY 0x89898989

int main()
{
    int shmid = shmget(KEY, 1024, IPC_CREAT | 0664);
    if(shmid < 0)
    {
        perror("shmget");
        return -1;
    }
    //create sucess
    
    //0 :可读可写
    //这个位置的可读可写是用来约束我的这个进程的,意思其实就是当前的这个进程对我的这个共享内存是可读可写的
    //上面的0664指的是,我的这个共享内存在创建的时候他是具有0664这样的权限的
    //两个的限制是完全不一样的
    void* lp = shmat(shmid, NULL, 0);
    strcpy((char*)lp, "linux-57-hahaha");   //这个和写hehe的道理其实是一摸一样的
	printf("%s\n",(char*)lp);   //就可以把lp里面的内容打印出来了
    while(1)
    {
        sleep(1);
    }
    return 0;
}
  • 如果多次读取,那么就是可以多次读取到的,所以得出的结论就是:进程在读取共享内存的时候是访问,而不是拿走
将共享内存和进程分离
  • detach—将共享内存和进程分离
  • 参数其实就是你之前把这个共享内存附加在了进程虚拟地址空间中的哪个位置,是一个地址的参数
    在这里插入图片描述
#include <stdio.h>
#include <unistd.h>
#include <sys/shm.h>
#include <string.h>

#define KEY 0x89898989

int main()
{
    int shmid = shmget(KEY, 1024, IPC_CREAT | 0664);
    if(shmid < 0)
    {
        perror("shmget");
        return -1;
    }
    //create sucess
    
    //0 :可读可写
    void* lp = shmat(shmid, NULL, 0);
    strcpy((char*)lp, "linux-57-hahaha");
    shmdt(lp)
    
    while(1)
    {
        sleep(1);
    }
    return 0;
}
操作 共享内存
  • 告诉函数要做什么操作,其实是通过一系列的宏去告诉的
  • 要把信息放在buf中,也就是说buf其实是一个出参
    在这里插入图片描述
  • 下面这个是描述共享内存的结构体
  • 信息分别为,共享内存的权限,共享内存有多大,共享内存最后一次attach和最后一次detach的时间都分别是什么
    在这里插入图片描述
  • 打印出来的buf的大小应该是1024个字节大小,因为在创建共享内存的饿时候,我们给共享内存指定的大小就是1024个字节
#include <stdio.h>
#include <unistd.h>
#include <sys/shm.h>
#include <string.h>

#define KEY 0x89898989

int main()
{
    int shmid = shmget(KEY, 1024, IPC_CREAT | 0664);
    if(shmid < 0)
    {
        perror("shmget");
        return -1;
    }
    //create sucess
    
    //0 :可读可写
    void* lp = shmat(shmid, NULL, 0);
    printf("%s\n", (char*)lp);

    struct shmid_ds buf;
    
    shmctl(shmid, IPC_STAT, &buf);

    printf("shm size : %d\n", buf.shm_segsz);
    
    while(1)
    {
        sleep(1);
    }
    return 0;
}
删除共享内存
#include <stdio.h>
#include <unistd.h>
#include <sys/shm.h>
#include <string.h>

#define KEY 0x89898989

int main()
{
    int shmid = shmget(KEY, 1024, IPC_CREAT | 0664);
    if(shmid < 0)
    {
        perror("shmget");
        return -1;
    }
    //create sucess
    
    //0 :可读可写
    void* lp = shmat(shmid, NULL, 0);
    strcpy((char*)lp, "linux-57-hahaha");
 
	//在删除共享内存之前,首先需要把共享内存分离掉
    shmdt(lp);

    shmctl(shmid, IPC_RMID, NULL);

    
    while(1)
    {
        sleep(1);
    }
    return 0;
}
  • 只有当附加的进程的数量是-个的时候,这个共享内存才会被释放掉
    在这里插入图片描述
消息队列
  • 先进先出指的是同一类型保证先进先出
    在这里插入图片描述
  • 一些接口
  • key是标识符,标识的是哪个消息队列,flag的意思是
    在这里插入图片描述
    在这里插入图片描述
System V信号量
  • System V信号量里面他其实是有一个计数器的,这个计数器用来记录当前信号的数量
  • 信号量本质上来说不是用来传输数据的,主要是用来进程控制的
  • 其实本质上也是两个进程之间通过一个共享的东西来传输数据,从而达到控制的目的
    在这里插入图片描述
  • 也就说,信号量的底层其实是一个计数器
  • 信号量是用来进行进程控制的
  • 下面来举一个例子来看一下,通过停车场的例子我们可以看出,假设我现在有六个进程,然后同时还有一个共享内存,那么这六个进程其实是都可以去访问这块共享内存的,那么这六个进程谁能去访问这块物理内存,谁不能去访问这块物理内存,这是由信号量来决定的,因为共享内存其实现在是存在覆盖写的
  • 计数器其实就是可用资源的计数
    在这里插入图片描述
  • 还有一种情况就是说,我现在停车场中只有一个车位,有四台车,那么在同一时刻,有几辆车能拥有这个停车位呢,答案其实很简单,在同一时刻只可以有一个汽车可以拥有这个停车位,其实这种情况就是互斥
  • 临界资源:多个进程都能访问到的资源,成为临界资源,同样,多线程当中,多个线程都能访问到的资源,被称之为临界资源,访问临界资源的那块代码,成为临界区
    在这里插入图片描述
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值