【Linux】-- 进程间通信

目录

一、进程间通信介绍

二、管道

1.什么是管道(pipe)

2.重定向和管道

(1)为什么要有管道的存在

(2)重定向和管道的区别

3.匿名管道

(1)匿名管道原理 

(2)站在文件描述符角度理解匿名管道

(3)创建匿名管道

(4)匿名管道读写规则

(5)匿名管道特点

(6)匿名管道4种特殊情况

(7)匿名管道大小

4.命名管道

(1)命名管道原理 

(2)创建命名管道

(3)命名管道的数据不会刷新到磁盘

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

三、System V IPC

1.System V标准

2.共享内存

(1)原理 

(2)步骤

(3)函数 

shmget

shmctl

shmat

shmdt

(4)使用

3.共享内存和管道区别 

四、消息队列 

1.原理 

2.数据结构

3.步骤

4.函数 

(1)msgget

(2)msgctl

(3)msgsnd

(4)msgrcv

五、信号量

1.原理 

2.数据结构

3.函数

(1)semget

(2)semctl

(3)semop

六、System V IPC总结


一、进程间通信介绍

        之前学习的进程,都是各自运行,互不干扰,进程之间没有协同。然而有许多场景下是需要进程之间相互协同的,由于进程是程序员写的,因此进程之间的协同本质上就是程序员之间的协同,比如一个程序员从数据库里面拿数据,另一个程序员要把从数据库里面拿到的数据进行格式化,写成特定格式,还有一个程序员根据格式化的数据进行统计,如果把这些工作量当成意见工作去处理的话,如果其中这三个环节有任何一个环节出错了,那么这个工作就进行不下去了,需要逐一去排查到底是哪个环节出错了,耗时久且效率低。

        因此把这个工作可以分为3个部分,分别让3个不同的进程去做:1个进程从数据库拿数据,1个进程做数据格式化,1个进程做数据分析。这就做到了在业务层面上用进程进行解耦。一旦拿数据有问题就去找拿数据的进程,一旦格式化有问题就去找格式化的进程,一旦数据分析有问题就去找数据分析的进程。业务层面上的解耦能够增加代码的可维护性,这就是进程之间的协同。比如过滤出文件中含字母'i'的行:

cat fdProcess.c运行起来就是一个进程,核心工作只是打印数据,用grep来过滤含有字母'i'的行,数据源是从上一个进程cat fdProcess.c通过管道来交给grep的。这就叫做协同。

就算是父子进程,共享了进程的代码和数据,写的时候都必须分开,用写时拷贝来写。两个相互独立的进程,交互数据,成本很高,各自连对方保存数据的地址空间都看不到,因为独立的进程使用独立的进程地址空间,页表映射到不同的物理内存,所以看不到对方的数据,因此要完成进程间通信,不能只在应用层解决,必须也要操作系统参与进来,要让操作系统设计通信方式。

通信的本质就是传递数据,这些数据需要一个进程向公共资源里面去放,另一个进程从公共资源向外拿,而公共资源还需要有暂存数据的能力。这个公共资源肯定不属于这两个进程,因为进程具有独立性,如果这个公共资源是进程A的,那么进程B是看不到的:

从上图可以看出进程间通信有以下3种方式,目的是为了让不同的进程看到同一份资源:

  • 管道
  • System V进程间通信
  • POSIX进程间通信 

同时需要先了解以下概念:

数据传输:一个进程需要将它的数据发送给另一个进程

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

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

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

二、管道

1.什么是管道(pipe)

管道是Unix最古老的进程间通信的形式。把从一个进程连接到另一个进程的数据流称为管道,Linux 管道使用竖线'|'连接多个命令,这被竖线'|'称为管道符。

当在两个命令之间设置管道时,管道符'|'左边命令的输出就变成了右边命令的输入。只要第一个命令向标准输出写入,而第二个命令是从标准输入读取,那么这两个命令就可以形成一个管道。大部分的 Linux 命令都可以用来形成管道。

如下所示,对于命令cat fdProcess.c|grep -i 'i',管道符'|'之前的进程cat fdProcess.c是标准输入进程,管道符'|'之后的进程grep -i 'i'是标准输出进程,第一个命令向标准输出写入,第二个命令是从标准输入读取,这两个命令形成了管道,管道作用于内核。

如果没有管道,那么这两条命令就得分两次执行。因此用管道执行也能达到同样的效果。对于一些备份压缩复制需求的命令就可以避免创建临时文件。 

管道特点:

  • 命令的语法紧凑并且使用简单。
  • 管道将多个命令串联到一起完成复杂任务。
  • 从管道输出的标准错误会混合到一起。

2.重定向和管道

(1)为什么要有管道的存在

既然有重定向,为什么还要有管道呢?比如如下命令使用重定向将可执行程序process1的输出都放入file中:

process1 > file

但是如果想让可执行程序process1 的输出传递到可执行程序process2呢?需要:

process1 > temp && process2 < temp

这个命令做了3步:

  • 运行名为process1 
  • 将输出保存到名为temp的文件中
  • 运行名为的程序process2,假装用户在键盘上输入temp的内容。

有没有发现这样做很麻烦,既要创建临时文件,又要用户在键盘上输入呢,但是管道就很简单呀:

process1 | process2

的效果和命令process1 > temp && process2 < temp的作用是一样的。

(2)重定向和管道的区别

管道也有重定向的作用,因为它改变了数据的输入输出方向。冲重定向使用">"将文件和命令连接起来,用文件来接收命令的输出,而管道使用"I"将命令和命令连接起来,用第二个命令来接收第一个命令的输出。

使用重定向一定要小心一些,如果连续键入如下两条命令:

cd /usr/bin
ls > less

第一条命令将当前目录切换到了大多数程序所存放的目录,第二条命令是告诉 Shell 用 ls 命令的输出重写文件 less。因为 /usr/bin 目录已经包含了名称为 less的文件,第二条命令用 ls 输出的文本重写了 less 程序,因此破坏了文件系统中的 less 程序,这就破坏了less文件。这是使用重定向操作符错误重写文件的一个教训,所以在使用重定向时要谨慎。

管道分为匿名管道和命名管道。

3.匿名管道

(1)匿名管道原理 

匿名管道仅限于本地父子进程之间通信,不支持跨网络之间的两个进程之间的通信。

进程在操作文件时,通过文件描述符找到文件,如果需要读文件就直接执行读方法。使用fork创建子进程之后,那么子进程就拥有了自己的PCB,父进程指向的struct file文件描述符表结构也需要给子进程拷贝一份。

 这是因为:

  • file_struct结构是属于进程的,因为file_struct能够让进程看到已经打开了多少个文件以及文件之间的关系,因此file_struct是属于进程的。file_struct属于进程,那么它一定属于父进程,在创建子进程的时候,也必须为子进程复制这份file_struct结构。因为进程具有独立性,所以内核数据结构也必须保持独立。
  • 如果让子进程也看到了父进程的文件了,那么父进程的文件进行读写时,缓冲区也被子进程看到了,这就没有做好进程独立性。

因此操作系统会将这个结构给子进程也拷贝一份:

基于文件的通信方式就叫做管道。进程、struct_file、缓冲区、操作方法等都是操作系统提供的,文件不属于进程,属于操作系统。父进程先打开文件,让子进程继承,虽然结构上互相独立,但它们指向同一个文件,一个向文件写,另一个从文件读,两个进程看到了同一份公共资源,这就满足了进程通信的前提。

(2)站在文件描述符角度理解匿名管道

  • 父进程创建管道

管道可以看做文件的内核缓冲区,父进程创建管道时,分别以读方式和写方式打开同一文件:

  •  父进程fork出子进程

 当父进程创建出子进程后,父进程的所有文件描述符表信息会被子进程继承,虽然父子进程各自拥有独立的文件描述符,但是内容是一样的,所以父子进程都可以看到曾经打开的读端和写端进行读写,不过管道只能单向通信,只能有一个读端,一个写端。

所以父进程一开始就有两个文件描述符,一个读端,一个写端,这样子进程继承复制了父进程的文件描述符后,也有读端和写端。否则如果父进程一开始只有读端,没有写端,那么子进程也只有读端,没有写端,那么两个读端是不能进行读写的。

  •  父进程关闭读端(写端),子进程关闭写端(读端)

 至于父子进程谁关闭读端,谁关闭写端,取决于父进程读还是子进程读,现在来看一下父进程写,子进程读的情况,现在关闭父进程的读端和子进程的写端:

(3)创建匿名管道

第一步:父进程使用pipe函数来创建管道

 #include <unistd.h>

 int pipe(int pipefd[2]);

参数:pipefd文件描述符数组,元素个数为2,是输出型参数,通过这个参数读取到打开的两个文件描述符。其中pipefd[0]为读操作,pipefd[1]为写操作,且顺序不能颠倒。

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

现在来创建一个管道:

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

int main()
{
        int pipefd[2] = {0};
        if(pipe(pipefd) != 0)//匿名管道创建失败
        {
                perror("pipe error!");
                return 1;
        }

        printf("pipefd[0]:%d\n",pipefd[0]);
        printf("pipefd[1]:%d\n",pipefd[1]);

        return 0;
}

执行结果如下:

 可以看到文件描述符分别为3和4,因为0、1、2都被标准输入、标准输出、标准错误占用了:

第二步:父进程fork出子进程

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

int main()
{
        int pipefd[2] = {0};
        if(pipe(pipefd) != 0)//匿名管道创建失败了
        {
                perror("pipe error!");
                return 1;
        }

        printf("pipefd[0]:%d\n",pipefd[0]);
        printf("pipefd[1]:%d\n",pipefd[1]);

        if(fork() == 0)//子进程
        {

        }

        //父进程

        return 0;
}

第3步:创建单向信道

现在如果想让父进程读,子进程写,那么就要关闭父进程的写端和子进程的读端,即关闭父进程的写文件描述符和子进程的读文件描述符。为了让子进程关闭读文件描述符后不要继续向后执行,使用eixt函数来终止。

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

int main()
{
        int pipefd[2] = {0};
        if(pipe(pipefd) != 0)//匿名管道创建失败了
        {
                perror("pipe error!");
                return 1;
        }

        printf("pipefd[0]:%d\n",pipefd[0]);
        printf("pipefd[1]:%d\n",pipefd[1]);

        if(fork() == 0)//子进程
        {
                close(pipefd[0]);//子进程关闭读文件描述符
                exit(0);
        }

        //父进程
        close(pipefd[1]);//父进程关闭写文件描述符

        return 0;
}

现在已经建立了父子进程,并且父子进程都看到了同一份资源。现在让子进程写入,需要调用write方法,让父进程读取,需要调用read方法,write和read方法的使用请参考文章【Linux】-- 基础IO和动静态库第一章节第1节的内容 第一章节第1节的内容。

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
        int pipefd[2] = {0};
        if(pipe(pipefd) != 0)//匿名管道创建失败了
        {
                perror("pipe error!");
                return 1;
        }

        printf("pipefd[0]:%d\n",pipefd[0]);
        printf("pipefd[1]:%d\n",pipefd[1]);

        if(fork() == 0)//子进程
        {
                close(pipefd[0]);//子进程关闭读文件描述符

                const char *string_write = "lunch ";
                while(1)
                {
                        write(pipefd[1],string_write,strlen(string_write));//子进程向文件缓冲区写,pipe只要有缓冲区就一直写入
                }
                close(pipefd[1]);

                exit(0);
        }

        //父进程
        close(pipefd[1]);//父进程关闭写文件描述符

        while(1)
        {
                sleep(1);
                char string_read[64] ={0};
                size_t readLength = read(pipefd[0],string_read,sizeof(string_read));//父进程从文件缓冲区读,pipe只要有缓冲区就一直读
                if(readLength == 0)//读到内容为空
                {
                        printf("child quit...\n");
                        break;
                }
                else if(readLength > 0)//读到了正常内容
                {
                        string_read[readLength] = 0;
                        printf("child write# %s\n",string_read);
                }
                else//读出错
                {
                        printf("read error...\n");
                        break;
                }
                close(pipefd[0]);

        }
        return 0;
}

 可以看到执行结果如下,子进程写入,父进程读取:

 对于字节流,只要缓冲区有数据,就把缓冲区的所有数据全部读出来,一次读取一个字节。

(4)匿名管道读写规则

pipe2函数与pipe函数类似,也是用于创建匿名管道,其函数原型如下:

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

对于flags: 

  • 当没有数据可读时

O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。

  •  当管道满的时候

O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN

  • 如果所有管道写端对应的文件描述符被关闭,则read返回0
  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
  • 当要写入的数据量<=PIPE_BUF时,linux将保证写入的原子性。
  • 当要写入的数据量>PIPE_BUF时,linux将不再保证写入的原子性。

(5)匿名管道特点

  • 只能用于具有共同祖先的进程之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,父、子进程之间就可用该管道通信(具有亲缘关系的进程,祖孙进程也可以)
  • 管道提供流式服务,原子性写入(读端读取的数据是任意的,底层没有对数据做明确分割,报文段不定,因此是流式服务)
  • 父子进程退出,管道文件释放,所以管道的生命周期随进程
  • 内核会对管道操作进行同步与互斥
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

(6)匿名管道4种特殊情况

  • 读端不读或者读的慢,写端要等读端
  • 读端关闭,写端收到SIGPIPE信号直接终止
  • 写端不写或写的慢,读端要等写端
  • 写端关闭,读端读完pipe内部的数据然后再读,会读到0,表明读到文件结尾

(7)匿名管道大小

 如果让子进程无限循环每次往管道里写一个字符,并且计数,父进程从管道里面不读取数据,当计数不再增长时,计数值就为管道的大小:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
        int pipefd[2] = {0};
        if(pipe(pipefd) != 0)//匿名管道创建失败了
        {
                perror("pipe error!");
                return 1;
        }

        printf("pipefd[0]:%d\n",pipefd[0]);
        printf("pipefd[1]:%d\n",pipefd[1]);

        if(fork() == 0)//子进程
        {
                close(pipefd[0]);//子进程关闭读文件描述符

                int count = 0;
                while(1)
                {
                        write(pipefd[1],"a",1);
                        count++;
                        printf("count:%d\n",count);
                }

                close(pipefd[1]);
                exit(0);
        }

        //父进程
        close(pipefd[1]);//父进程关闭写文件描述符
        while(1)//父进程不读取
        {
                sleep(1);
        }
        return 0;
}

 运行结果如下,从1打印到65536:

 这说明管道大小为65536B=64KB。这也说明了如果写端向管道写满数据以后,那么写端就不写了,等待读端读;同理,如果读端把管道数据读完了,管道没数据,那么读端就不读了,等待写端写。

 管道在被写端写满以后,读端要拿走数据,如果一次拿走4KB,写端才会写,否则不会触发写端去写,为什么是4KB呢?让父进程读取的时候,存放数据的数组大小从1KB开始向上递增到4KB的时候,写端才写:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
        int pipefd[2] = {0};
        if(pipe(pipefd) != 0)//匿名管道创建失败了
        {
                perror("pipe error!");
                return 1;
        }

        printf("pipefd[0]:%d\n",pipefd[0]);
        printf("pipefd[1]:%d\n",pipefd[1]);

        if(fork() == 0)//子进程
        {
                close(pipefd[0]);//子进程关闭读文件描述符

                const char *string_write = "lunch ";
                int count = 0;
                while(1)
                {
                        //write(pipefd[1],string_write,strlen(string_write));//子进程向文件缓冲区写,pipe只要有缓冲区就一直写入
                        write(pipefd[1],"a",1);
                        count++;
                        printf("count:%d\n",count);
                }

                close(pipefd[1]);
                exit(0);
        }

        //父进程
        close(pipefd[1]);//父进程关闭写文件描述符
		while(1)
        {
                sleep(3);
                char string_read[1024*4+1] ={0};//按照1024*1   1024*2   1024*3   1024*4向上递增
                size_t readLength = read(pipefd[0],string_read,sizeof(string_read));//父进程从文件缓冲区读,pipe只要有缓冲区就一直读
                printf("readLength = %d\n",readLength);
                string_read[readLength] = 0;
                printf("father take:%c\n",string_read[0]);
		}
        return 0;
}

可以看到,管道写入字符的计数一开始增加到了65536B,父进程读走4KB之后,子进程继续写,每写一次,count计数就会++ :

 为什么读走4KB的时候,写端才写,而读走1KB 2KB 3KB时不写呢?这是因为要保证写入和读取的原子性:假如还没读够4KB,就把写端唤醒了,那么写端就要来写了,这就变成了,写端在写的同时,读端要来读,这就违背了管道半双工通信,不能同时读写的原则。同理,如果写端写的特别慢,读端读的特别快,当缓冲区没有数据时,会等待数据写入进去后,读端再读 。因此要保证同步。

4.命名管道

(1)命名管道原理 

 匿名管道用于有血缘关系的进程间通信,那么对于没有血缘关系的进程,他们之间如何通信呢?这就要用到命名管道,命名管道是一种特殊的文件,使用FIFO(First In First Out)来进行通信。

 如何让两个没有血缘关系的不相干的进程看到操作系统提供的同一份资源?对于文件系统来说当进程A把磁盘文件打开,向磁盘里面写数据,写完之后关闭这个磁盘文件,进程B再把这个磁盘文件打开并读取数据:

但是这样做有点慢,因为进程A再内存中打开这个文件,为这个文件建立内存相关的数据结构和缓冲区,进程B也在内存中打开同一个文件,这样就是一个通过读的方式打开,一个通过写的方式打开,进程可以向这个内存文件写,进程B可以从这个内存文件读,暂时先不把数据刷新到磁盘,否则效率会降低,这是基于内存进行数据之间的通信,那么A进程和B进程就可以通过这个内存文件进行不相关的进程间的通信。

不相关的A进程和B进程是如何看到同一份资源的呢?路径+文件名能唯一指定一个文件,这样就能让进程A和进程B打开同一份文件。现在需要1个文件,同时满足:

  • 文件被打开时,数据不要被刷新到磁盘上,而是保存临时数据
  • 这个文件也必须在磁盘上也有对应的文件名

符合这些条件的只有命名管道。而且这个文件是有名字的,通过路径+文件名确定唯一性来做到的:

(2)创建命名管道

命名管道有两种创建方式: 

  • 通过mkfifo命令创建
mkfifo name

如创建一个名为testFifo的管道文件:

 可以看到文件类型为p,p表明这是一个管道文件。创建了命名管道文件后,就可以通信了:

 echo和cat是两个不同的指令,但是运行起来是两个进程,左侧的消息打印到了右侧的屏幕上,一个进程把自己的内容写入到了命名管道文件中,通过命名管道文件把数据传递给另一个进程。

  • 通过mkfifo函数创建

mkfifo函数的作用是生成一个FIFO的特殊文件,即命名管道 

 #include <sys/types.h>
 #include <sys/stat.h>

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

pathname:文件名

mode:管道的默认权限,可用过umask来设置

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

现在使用mkfifo函数创建命名管道,server.c创建管道文件,并给管道文件分配权限:

#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>

#define fifo_file ./fifo_file

int main()
{
        if(mkfifo(fifo_file,0666) < 0)//创建一个命名管道
        {
                perror("mkfifo");
                return 1;
        }

        return 0;
}

client.c暂时什么都不做:

#include<stdio.h>

int main()
{
        return 0;
}

Makefile一次生成两个可执行文件:

.PHONY:all
all:client server

client:client.c
        gcc -o $@ $^

server:server.c
        gcc -o $@ $^

.PHONY:clean
clean:
        rm -rf client server fifo_file

编译后,生成两个可执行程序:

现在通信想让client和server可执行程序互相传递详细,那么 client和server可执行程序运行起来就是两个进程,而且是两个毫不相干的进程,没有血缘关系。

执行srver课执行程序后,生成fifo_file命名管道,文件类型是p,但是权限是644,并不是666:

 这是因为fifo文件的参数mode受系统umask影响,可以查看到Umask的值是2:

 那么可以看出mode = mode & ~umask(666&~002),如果修改umask的值,比如创建命名管道文件时将umask清0:

server.c

#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#define fifo_file "./fifo_file"

int main()
{
        umask(0);//将umask清0
        if(mkfifo(fifo_file,0666) < 0)//创建一个命名管道
        {
                perror("mkfifo");
                return 1;
        }

        int fd = open(fifo_file,O_RDONLY);
        if(fd < 0)
        {
                perror("open");
                return 2;
        }

        while(1)
        {
                char buffer[64] = {0};
                ssize_t read_length = read(fd,buffer,sizeof(buffer)-1);
                if(read_length > 0)//读取成功
                {
                        buffer[read_length-1] = 0;
                        printf("client # %s\n",buffer);
                }
                else if(read_length == 0)
                {
                        printf("client quit\n");
                }
                else
                {
                        perror("read");
                        break;
                }
        }
        close(fd);
        return 0;
}

这时可以看到命名管道文件的权限变成了666:

 对于client和server进程,想让server读,client写,不推荐用c/c++接口,有缓冲区,而系统调用没有缓冲区,推荐使用系统调用接口,client使用系统调用接收标准输入并写入到命名管道文件中:

client.c 

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

#define fifo_file "./fifo_file"

//client不需要创建命名管道文件,只需要获取就可以了
int main()
{
        int fd = open(fifo_file,O_WRONLY);
        if(fd < 0)
        {
                perror("open");
                return 1;
        }

        while(1)
        {
                printf("请输入# ");//client的输入提示
                fflush(stdout);//刷新一下标准输出
                char buffer[64] = {0};
                //先把数据从标准输入拿到client进程内部
                ssize_t read_length = read(0,buffer,sizeof(buffer)-1);
                if(read_length > 0)
                {
                        buffer[read_length-1] = 0;

                        //拿到了数据
                        write(fd,buffer,strlen(buffer));
                }
        }
        close(fd);
        return 0;
}

 现在运行,得先让server跑起来创建一个命名管道,然后再运行client端,就可以再client端写入数据了:

 从以上就可以看出,对于两个不想管的进程,通过命名管道,一个进程把消息发给了另外一个进程。因此一旦有了命名管道,只需要让通信双方进程按照文件操作即可。由于命名管道也是基于字节流的,因此实际上,信息传递的时候,需要通信双方定制“协议”。

现在让client控制server,让server去执行任务。可以让server执行程序替换,比如当client接收标准输入写入到命名管道文件中的字符串为"show"时,就会执行ls命令:

server.c

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

#define fifo_file "./fifo_file"

int main()
{
        umask(0);
        if(mkfifo(fifo_file,0666) < 0)//创建一个命名管道
        {
                perror("mkfifo");
                return 1;
        }

        int fd = open(fifo_file,O_RDONLY);
        if(fd < 0)
        {
                perror("open");
                return 2;
        }

        //业务逻辑,进行读写
        while(1)
        {
                char buffer[64] = {0};
                ssize_t read_length = read(fd,buffer,sizeof(buffer)-1);
                if(read_length > 0)//读取成功
                {
                        buffer[read_length] = 0;
                        if(strcmp(buffer,"show") == 0)
                        {
                                printf("the string is show\n");
                                if(fork() == 0)
                                {
                                        execl("/usr/bin/ls","ls","-l",NULL);//程序替换
                                        exit(1);
                                }
                                waitpid(-1,NULL,0);
                        }
                        else
                        {
                                printf("client # %s\n",buffer);
                        }
                }
                else if(read_length == 0)
                {
                        printf("client quit\n");
                }
                else
                {
                        perror("read");
                        break;
                }
        }
        close(fd);
        return 0;
}

client.c

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

#define fifo_file "./fifo_file"

//client不需要创建命名管道文件,只需要获取就可以了
int main()
{
        int fd = open(fifo_file,O_WRONLY);
        if(fd < 0)
        {
                perror("open");
                return 1;
        }
        while(1)
        {
                printf("请输入# ");//client的输入提示
                fflush(stdout);//刷新一下标准输出
                char buffer[64] = {0};
                //先把数据从标准输入拿到client进程内部
                ssize_t read_length = read(0,buffer,sizeof(buffer)-1);
                if(read_length > 0)
                {
                        buffer[read_length - 1] = 0;

                        //拿到了数据
                        write(fd,buffer,strlen(buffer));

                }
        }
        close(fd);
        return 0;
}

现在运行,得先让server跑起来创建一个命名管道,然后再运行client端,就可以再client端写入数据了,在client输入"show"之后,server就将ls的内容展示出来了:

 可以看到通过命名管道把数据从一个进程传递给另外一个进程,并且也实现了让一个进程控制了另外一个进程去执行任务,达到了进程间通信的目的。

(3)命名管道的数据不会刷新到磁盘

假如让server进程每隔20秒读一次,而client不断往管道发消息,那么数据只能在管道文件:

server.c

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

#define fifo_file "./fifo_file"

int main()
{
        umask(0);
        if(mkfifo(fifo_file,0666) < 0)//创建一个命名管道
        {
                perror("mkfifo");
                return 1;
        }

        int fd = open(fifo_file,O_RDONLY);
        if(fd < 0)
        {
                perror("open");
                return 2;
        }

        //业务逻辑,进行读写
        while(1)
        {
                char buffer[64] = {0};
                sleep(20);//等待20秒再读
                ssize_t read_length = read(fd,buffer,sizeof(buffer)-1);
                if(read_length > 0)//读取成功
                {
                        buffer[read_length] = 0;
                        if(strcmp(buffer,"show") == 0)
                        {
                                printf("the string is show\n");
                                if(fork() == 0)
                                {
                                        execl("/usr/bin/ls","ls","-l",NULL);//程序替换
                                        exit(1);
                                }
                                waitpid(-1,NULL,0);
                        }
                        else
                        {
                                printf("client # %s\n",buffer);
                        }
                }
                else if(read_length == 0)
                {
                        printf("client quit\n");
                }
                else
                {
                        perror("read");
                        break;
                }
        }
        close(fd);
        return 0;
}

client.c不用修改:

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

#define fifo_file "./fifo_file"

//client不需要创建命名管道文件,只需要获取就可以了
int main()
{
        int fd = open(fifo_file,O_WRONLY);
        if(fd < 0)
        {
                perror("open");
                return 1;
        }
        while(1)
        {
                printf("请输入# ");//client的输入提示
                fflush(stdout);//刷新一下标准输出
                char buffer[64] = {0};
                //先把数据从标准输入拿到client进程内部
                ssize_t read_length = read(0,buffer,sizeof(buffer)-1);
                if(read_length > 0)
                {
                        buffer[read_length - 1] = 0;

                        //拿到了数据
                        write(fd,buffer,strlen(buffer));

                }
        }
        close(fd);
        return 0;
}

这20秒内,client向命名管道写,但是server没有从命名管道读,按理来说,命名管道里面有内容,大小不为0,但是在这20秒之内发现命名管道的fifo_file的大小为0,这就说明了命名管道的数据,由于效率问题,不会刷新到磁盘。

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

创建与打开的方式不同:

  • 匿名管道由pipe函数创建并打开
  • 命名管道由mkfifo函数创建,由open函数打开

后面就有相同的语义了

三、System V IPC

1.System V标准

System V是一种用于在操作系统层面上进行进程间通信的标准,system V标准给用户提供了系统调用接口,只要用户使用它所提供的系统调用就可以完成进程间通信。IPC(Inter-Process Communication)是进程间通信。System V IPC不用基于文件进行通信。

如何把系统调用接口提供给用户使用呢?System V是操作系统内核的一部分,是为操作系统中多进程提供的一种通信方案。但是操作系统不相信任何用户,采用系统调用为用户提供功能。所以System V进程间通信,存在专门用来通信的接口:System call(系统调用)

这就需要制定一套标准用来在同一主机内进行进程间通信:System V。System V进程间通信分为3种:

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

消息队列模型通过在协作进程间交换消息来实现通信。共享内存模型会建立起一块供协作进程共享的内存区域,进程通过向此共享区域读出或写入数据来交换信息。以下是消息队列和共享内存的通信模型:

 消息队列的实现经常采用系统调用,因此需要消耗更多时间使内核介入,但是共享内存只在建立共享内存区域时需要系统调用,一旦建立共享内存,所有访问都是常规内存访问,不需要借助内核。

由于消息队列和共享内存用来传递消息,信号量用来实现进程间同步和互斥。因此主要来看看进程间通信方式中效率较高的共享内存。

2.共享内存

(1)原理 

把申请的共享内存映射到不同进程的地址空间当中。有进程A和进程B,进程A通过页表映射找到进程A的代码和数据,同样,进程B也通过页表映射找到进程B的代码和数据,由于两个进程的数据结构相互独立,且物理内存当中的代码和数据也相互独立,因此两个进程不会互相干扰。

在物理内存开辟一块共享内存空间后,需要通过系统调用把开辟的内存空间经过页表映射到进程地址空间,那么共享内存在进程地址空间也有了虚拟地址,叫做共享存储器映射区,再把共享存储器映射区的虚拟地址填到页表当中,这样共享内存的虚拟地址和物理地址就建立起了对应关系,而且各个进程也就看到了共享内存同一份资源。

 以上的过程也是让进程挂接到共享内存空间上的过程。操作系统内可能存在多个共享内存,那么操作系统需要管理这些共享内存,管理还是先描述再组织。

 如何保证能够让多个进程看到同一个共享内存呢?

 共享内存一定要有唯一标识ID,就能让不同进程识别到同一个共享内存资源。那么这个ID一定在描述共享内存的数据结构中。

(2)步骤

可以总结出使用共享内存的过程:

  • 创建共享内存
  • 关联(挂接)
  • 去关联(去挂接)
  • 释放共享内存

(3)函数 

shmget

使用shmget函数创建共享内存,来申请一块共享内存空间:

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
key通过ftok函数生成
size建议为4KB的整数倍,操作系统为了提高内存和硬盘的数据交换的速度,以4KB为单位
shmflghmflg标志有多个,先了解最常用的两个标志IPC_CREAT和IPC_EXCL就可以了
返回值成功就返回共享内存地址,失败就返回-1

 其中,shmget第一个参数key是通过ftok函数生成的:

#include <sys/types.h>
#include <sys/ipc.h>

key_t ftok(const char *pathname, int proj_id);
pathname自定义的文件路径名
proj_id序号,低8位被使用,非0
返回值返回key,会被设置进共享内存在内核的数据结构里面

shmget第三个参数shmflg标志有多个,先了解最常用的两个标志IPC_CREAT和IPC_EXCL就可以了:

 创建共享内存后,如何查看共享内存呢?ipcs命令用于报告进程间通信设施状况,其中:

ipcs -m //查看共享内存(Shared Memory Segments)
ipcs -q //查看消息队列(Message Queue)
ipcs -s //查看信号量(Semaphore Arrays)

shmctl

使用完共享内存后,如果不删除的话,共享内存会一直存在,直到系统重启。如何删除呢?有两种删除方式,一种是命令删除:

ipcrm -m shmid

key只是用来在系统层面进行唯一标识,不能用来管理共享内存。而shmid是操作系统给用户返回的id,用来在用户层进行共享内存管理,所以ipcrm是用户层的命令。 以上是命令删除,那么如何在代码中删除共享内存呢?

 因此另外一种删除共享内存的方式就是使用shmctl函数控制共享内存:

#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid操作系统给用户返回的id
cmd选项,有多个
bufdata structure数据结构类型指针
返回值删除成功返回0,失败返回-1

其中cmd选项有多个:

IPC_STAT将shmid的内核数据结构拷贝到buf指向的shmid_ds结构中
IPC_SET将buf指向的shmid_ds结构的一些成员的值写入与此共享内存段相关的内核数据结构,同时更新其shm_ctime成员
IPC_RMID删除共享内存

第三个参数​​ 

 其中,shmid_ds数据结构如下:

struct shmid_ds
{
	struct ipc_perm shm_perm;    /* Ownership and permissions */
	size_t          shm_segsz;   /* Size of segment (bytes) */
	time_t          shm_atime;   /* Last attach time */
	time_t          shm_dtime;   /* Last detach time */
	time_t          shm_ctime;   /* Last change time */
	pid_t           shm_cpid;    /* PID of creator */
	pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
	shmatt_t        shm_nattch;  /* No. of current attaches */
	...
};

shmat

 使用shmat把共享内存映射到调用进程的地址空间(关联:增加共享内存和进程地址空间映射关系的页表项)

#include <sys/types.h>
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid操作系统给用户返回的id
shmaddr表明把共享内存挂接到进程地址空间的哪些范围中 
shmflg有多个,先了解最常用的两个标志IPC_CREAT和IPC_EXCL就可以了,同shmget函数的shmflg标志,这里设置为0就可以了
返回值返回共享内存挂接到进程地址空间的虚拟地址,同申请堆空间的malloc返回值是一样的

shmdt

 shmdt用来断开共享内存和进程地址空间的映射(去关联:删除共享内存和进程地址空间映射关系的页表项,而不是释放共享内存)

#include <sys/types.h>
#include <sys/shm.h>

int shmdt(const void *shmaddr);
shmaddr要断开映射的共享内存地址,且必须和shmat的参数shmaddr相同
返回值成功断开返回0,失败返回-1

(4)使用

两个进程使用共享内存通信,需要进行创建、关联、去关联、删除的步骤,现在使用上面的函数来进行server和client两个进程间的通信。

comm.h来包含头文件

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

#define PATH_NAME "/home/delia/linux/20230627-sharedMemory/shared/server.c"  //ftok的路径
#define PROJ_ID 0x6666
#define SIZE 4097

server端需要生成唯一ID,创建共享内存,关联共享内存,去关联共享内存,删除共享内存:

server.c

#include "comm.h"

int main()
{
        key_t key = ftok(PATH_NAME,PROJ_ID);//生成唯一ID保证在统一系统当中
找到共享内存
        if(key < 0)
        {
                perror("fork");
                return 1;
        }

        //1.创建共享内存
        int shmid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);//共享内存不存在就创建,权限为666,共享内存可以用文件权限来约束

        if(shmid < 0)
        {
                perror("shmget");
                return 2;
        }

        printf("key = %u,shmid = %d\n",key,shmid);

        sleep(1);

        //2.关联
        char *mem = shmat(shmid,NULL,0);
        printf("attaches shm success\n");
        sleep(15);

        //通信逻辑
        while(1)
        {
                sleep(1);
                //printf("%s\n",mem);
        }

        //3.去关联
        shmdt(mem);
        printf("detaches shm success\n");

        //4.删除共享内存
        shmctl(shmid,IPC_RMID,NULL);
        sleep(5);

        printf("key = %u,shmid = %d after shmctl\n",key,shmid);
        return 0;
}
   

client端需要和客户端一样生成同一个唯一ID,创建使用同一个共享内存,关联共享内存,去关联共享内存,不需要删除共享内存,因为server端已经删除了:

client.c

#include "comm.h"

int main()
{
        key_t key = ftok(PATH_NAME,PROJ_ID);//生成唯一ID保证在统一系统当中
找到共享内存
        if(key < 0)
        {
                perror("ftok");
                return 1;
        }

        printf("%u\n",key);
        
        //1.创建共享内存
        int shmid = shmget(key,SIZE,IPC_CREAT);//共享内存已存在就返回已存在共享内存
        if(shmid < 0)
        {
                perror("shmget");
                return 2;
        }
        
        //2.关联
        char *mem = shmat(shmid,NULL,0);
        sleep(5);
        printf("client process attaches success\n");

        //通信逻辑
        char c = 'A';
        while(c <= 'G')
        {
                mem[c - 'A'] = c;
                c++;
                        mem[c - 'A'] = 0;
                sleep(2);
        }

        //3.去关联
        shmdt(mem);

        printf("client process detaches success\n");
        return 0;
}

 Makefile

.PHONY:all
all:server client

server:server.c
        gcc -o $@ $^
client:client.c
        gcc -o $@ $^

.PHONY:clean
clean:
        rm -f server client

make之后,使用命令

while :; do ipcs -m;sleep 1;echo "#################"; done

来查看共享内存的挂接进程的数量变化:当server端和client端进程都没有开启时,看到共享内存信息的nattch的个数为0,当server和client端都运行起来之后,发现nattch的个数变成了2,client所写的消息就会被server读取,当client端去关联之后,nattch变成了1,最后当server端退出时,共享内存被删除,nattch又变成了0:

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

从以上可以看出,共享内存有以下特点:

  • 共享内存一旦建立好并映射进自己进程的地址空间,该进程就可以看到该共享内存,就像malloc的空间一样,不需要任何系统调用接口(比如read、write会将数据从内核拷贝到用户或从用户拷贝到内核)。
  • 共享内存是所有进程间通信中速度最快的,这是因为将一块共享内存映射到不同的进程地址空间,共享内存地址对应在内存上的空间就拿到了,所以server和Client有任何一方写了,另一方马上就看到了。
  • 生命周期随内核,而且不提供同步互斥机制,需要程序员自行保证数据的安全。

3.共享内存和管道区别 

从共享内存的特点可以看出:

(1)创建好共享内存后,就不需要再调用系统接口进行通信了, 而管道创建好后还需要调用read、write等系统接口进行通信。

(2) 共享内存没有同步互斥机制,但是管道有同步互斥机制。

(3)共享内存是所有进程间通信方式中速度最快的,将数据从一个进程传输带另一个进程,管道需要进行4次拷贝,共享内存需要进行2次拷贝,共享内存需要的拷贝次数少。

使用管道,将文件从一个进程传到另一个进程需要4次拷贝:

  • 服务端把信息从输入文件复制到服务端的临时缓冲区
  • 把服务端的临时缓冲区信息复制到管道中
  • 客户端把信息从管道复制到客户端的缓冲区
  • 把客户端临时缓冲区的信息复制到输出文件中

 使用共享内存,将文件从一个进程传到另一个进程需要2次拷贝:

  • 将信息从输入文件拷贝到共享内存
  • 将信息从共享内存拷贝到输出文件

四、消息队列 

1.原理 

消息队列是一个消息的链表,可以把消息看作一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息,对消息队列有读权限的进程则可以从消息队列中读走消息。消息队列的生命周期是随内核的。 

队列的每个成员都是数据块,每个数据块包含类型和信息两部分。这个队列也遵循先进先出,即从队头读取消息,向队尾写入消息:

每个数据块都有类型,这就说明,各个数据块的类型可以不同,因此,接收者进程接收的数据块可以有不同的类型值。消息队列的资源必须手动删除,因为system V IPC资源的生命周期是随内核的。

2.数据结构

 消息对中的数据块如何管理呢?还是先描述,再组织。使用命令:

cat /usr/include/linux/msg.h

就能够看到消息队列的数据结构如下: 

struct msqid_ds 
{ 
    struct ipc_perm msg_perm;     /* Ownership and permissions */
    time_t          msg_stime;    /* Time of last msgsnd(2) */
    time_t          msg_rtime;    /* Time of last msgrcv(2) */
    time_t          msg_ctime;    /* Time of last change */
    unsigned long   __msg_cbytes; /* Current number of bytes in
                                     queue (nonstandard) */
    msgqnum_t       msg_qnum;     /* Current number of messages
                                     in queue */
    msglen_t        msg_qbytes;   /* Maximum number of bytes
                                     allowed in queue */
    pid_t           msg_lspid;    /* PID of last msgsnd(2) */
    pid_t           msg_lrpid;    /* PID of last msgrcv(2) */
};

 第一个ipc_perm 结构体是不是有点熟悉呢?它和shm_perm是同类型的结构体,使用命令:

cat /usr/include/linux/ipc.h

就能够看到ipc_perm 的结构体定义如下:

struct ipc_perm 
{
   key_t          __key;       /* Key supplied to msgget(2) */
   uid_t          uid;         /* Effective UID of owner */
   gid_t          gid;         /* Effective GID of owner */
   uid_t          cuid;        /* Effective UID of creator */
   gid_t          cgid;        /* Effective GID of creator */
   unsigned short mode;        /* Permissions */
   unsigned short __seq;       /* Sequence number */
};

3.步骤

消息队列使用过程如下: 

  • 创建
  • 发送
  • 接收
  • 释放

4.函数 

(1)msgget

使用msgget来创建消息队列:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);
key通过ftok函数生成
msgflgmsgflg标志有多个,先了解最常用的两个标志IPC_CREAT和IPC_EXCL就可以了
返回值创建成功就返回消息队列标识符,失败就返回-1

同shmget一样,msgget第一个参数key是通过ftok函数生成的:

#include <sys/types.h>
#include <sys/ipc.h>

key_t ftok(const char *pathname, int proj_id);
pathname自定义的文件路径名
proj_id序号,低8位被使用,非0
返回值返回key,会被设置进共享内存在内核的数据结构里面

msgget第三个参数msgflg标志有多个,先了解最常用的两个标志IPC_CREAT和IPC_EXCL就可以了:

(2)msgctl

使用msgctl来释放消息队列:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

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

使用完消息队列后,如果不删除的话,消息队列会一直存在,直到系统重启。如何删除呢?有两种删除方式,一种是命令删除:

ipcrm -q msqid

那么如何在代码中删除共享内存呢?因此另外一种删除消息队列的方式就是使用msgctl函数控制消息队列:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds *buf);
msqid消息队列的用户层id
cmd选项,有多个
bufdata structure数据结构类型指针
返回值删除成功返回0,失败返回-1

其中cmd选项有多个: 

IPC_STAT将msqid的内核数据结构拷贝到buf指向的msqid_ds结构中
IPC_SET将buf指向的msqid_ds结构的一些成员的值写入与此共享内存段相关的内核数据结构,同时更新其msq_ctime成员
IPC_RMID删除共享内存

(3)msgsnd

使用msgsnd向消息队列发送数据:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
msqid操作系统给用户返回的id
msgp待发送数据块
msgsz待发送数据块大小
msgflg发送数据块的方式,一般为0
返回值0表示调用成功,-1表示调用失败

其中第二个参数msgp的结构为:

struct msgbuf{
	long mtype;       /* message type, must be > 0 */
	char mtext[1];    /* message data */
};

其中mutex为待发送的信息,mutex大小可以由我们自己指定。

(4)msgrcv

使用msgrcv从消息队列获取消息:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
msqid操作系统给用户返回的id
msgp获取到的数据块
msgsz获取到的数据块大小
msgtyp获取到的数据块的类型
msgflg获取数据块的方式,一般为0
返回值>0表示实际获取的字节数,-1表示调用失败


 

五、信号量

1.原理 

前面的管道、共享内存、消息队列都以传输数据为目的,但是信号量不以传输数据为目的,通过共享资源的方式,来达到多个进程同步互斥的目的。 

信号量,有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。本质就是一个计数器,衡量临界资源中的资源数。

这就像坐火车一样,并不是因为坐在座位上,这个作为才属于某一个人,而是买了票的时候,这个作为就已经属于买票的人了,因此买票的本质就是对临界资源的预订,票的数量就是信号量。如以下代码:

 

信号量相关概念:

  • 临界资源:被多个执行流同时访问的资源,一次只允许一个进程使用。比如管道、共享内存、消息队列、信号量。
  • 临界区:进程中访问临界资源的代码(和临界资源配套)为了保护数据安全,就要把临界区保护起来,就有了信号量。
  • 原子性:一件事情要么做完,要么不做,没有中间状态。
  • IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核。

 信号量本质是对临界资源的统计,更是操作系统对临界资源的预定机制,信号量要诶预订,所有线程要访问临界资源,得先申请信号量,那么所有的进程就得先看到信号量,信号量就是临界资源,要保护信号量这个临界资源,信号量的常见操作即PV操作就必须保证原子性。

2.数据结构

使用命令:

cat /usr/include/linux/sem.h

就能够看到信号量的数据结构如下: 

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 结构体是不是有点熟悉呢?它和shm_perm、msg_perm是同类型的结构体,使用命令:

cat /usr/include/linux/ipc.h

就能够看到ipc_perm 的结构体定义如下:

struct ipc_perm 
{
   key_t          __key;       /* Key supplied to msgget(2) */
   uid_t          uid;         /* Effective UID of owner */
   gid_t          gid;         /* Effective GID of owner */
   uid_t          cuid;        /* Effective UID of creator */
   gid_t          cgid;        /* Effective GID of creator */
   unsigned short mode;        /* Permissions */
   unsigned short __seq;       /* Sequence number */
};

3.函数

(1)semget

 使用semget创建信号量:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);
key操作系统给用户返回的id
nsems创建的信号量的个数
semflgsemflg标志有多个,先了解最常用的两个标志IPC_CREAT和IPC_EXCL就可以了
返回值

创建成功就返回信号量标识符,-1表示创建失败

(2)semctl

 使用semctl删除信号量:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, ...);
semid信号量的用户层id
semnum信号量序号
cmd信号量的控制操作标识
返回值

创建成功就返回信号量标识符,-1表示创建失败

(3)semop

使用semop来进行信号量的PV操作:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, unsigned nsops);
semid信号量的用户层id
sops是sembuf类型的操作指针
nsops单个信号量的操作
返回值

创建成功就返回信号量标识符,-1表示创建失败

使用命令:

cat /usr/include/linux/sem.h

可以看到sembuf结构体:

struct sembuf
{
    unsigned short  sem_num;        /* semaphore index in array */
    short           sem_op;         /* semaphore operation */
    short           sem_flg;        /* operation flags */
};
sem_num指定要操作的信号量,0表示第一个信号量,1表示第二个信号量,……
sem_op信号量操作
sem_flg操作标识

六、System V IPC总结

 从以上内容可以看出,共享内存、消息队列、信号量,虽然属性和实现起来有差别,但是他们维护的数据结构的成员却是一样的,即ipc_perm结构体,这样每次要申请System V IPC时,无论是共享内存、消息队列、信号量,都会在数组中开辟ipc_perm这样的结构:

 那么内核可以分配一个ipc_perm数组,用来指向每一个IPC资源。

 

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/67979.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

mysql索引的数据结构(Innodb)

首选要注意,这里的数据结构是存储在硬盘上的数据结构,不是内存中的数据结构,要重点考虑io次数. 一.不适合的数据结构: 1.Hash:不适合进行范围查询和模糊匹配查询.(有些数据库索引会使用Hash,但是只能精准匹配) 2.红黑树:可以范围查询和模糊匹配,但是和硬盘io次数比较多. 二…

机器学习笔记 - 关于GPT-4的一些问题清单

一、简述 据报道,GPT-4 的系统由八个模型组成,每个模型都有 2200 亿个参数。GPT-4 的参数总数估计约为 1.76 万亿个。 近年来,得益于 GPT-4 等高级语言模型的发展,自然语言处理(NLP) 取得了长足的进步。凭借其前所未有的规模和能力,GPT-4为语言 AI​​设立了新标准,并为机…

vue-pc端实现按钮防抖处理-自定义指令

前言 我们经常在移动端会处理按钮和输入框的防抖和节流处理&#xff0c;在pc端很少进行这样的操作 但是在pc端也是可以进行按钮的防抖操作&#xff0c;这样也是比较合理&#xff0c;可以不用但不可以不会 我们只要配合vue项目自定义指令加上全局注册&#xff0c;就可以实现按…

Linux下安装VMware虚拟机

目录 1. 简介 2. 工具/原料 2.1. 下载VMware 2.2. 安装 1. 简介 ​ VMware Workstation&#xff08;中文名“威睿工作站”&#xff09;是一款功能强大的桌面虚拟计算机软件&#xff0c;提供用户可在单一的桌面上同时运行不同的操作系统&#xff0c;和进行开发、测试 …

同步代码块使用错误示范 | 用了synchronized还是出现“超取”问题

记录一下错误&#xff0c;吸取经验&#x1f914;&#x1f60b; 出问题的代码 public class Test {public static void main(String[] args) {new Thread(new Account()).start(); //&#xff01;&#xff01;new Thread(new Account()).start(); //&#xff01;&#xff01;}…

跨境电商线上店铺智能装修系统源码开发

搭建一个跨境电商线上店铺智能装修系统源码开发需要以下步骤&#xff1a; 1. 确定需求&#xff1a;首先&#xff0c;需要明确线上店铺智能装修系统的具体需求。 2. 选择开发语言和框架&#xff1a;根据需求&#xff0c;选择合适的开发语言和框架进行开发&#xff0c;可以提高…

docker容器监控:Cadvisor+InfluxDB+Grafana的安装部署

目录 CadvisorInfluxDBGrafan安装部署 1、安装docker-ce 2、阿里云镜像加速器 3、下载组件镜像 4、创建自定义网络 5、创建influxdb容器 6、创建Cadvisor 容器 7、查看Cadvisor 容器&#xff1a; &#xff08;1&#xff09;准备测试镜像 &#xff08;2&#xff09;通…

【TypeScript】中关于 { 声明合并 } 的使用及注意事项

概念&#xff1a; 在TS中&#xff0c;如果定义了多个相同命名的函数&#xff0c;接口或者class 类&#xff0c;那么它们会自动合并成一个类型 函数的合并&#xff1a; 前面章节讲解的函数重载就是使用了定义多个函数的类型进行合并&#xff1a; function reverse(x: number):…

MPP架构和Hadoop架构的区别

1. 架构的介绍 mpp架构是将许多数据库通过网络连接起来&#xff0c;相当于将一个个垂直系统横向连接&#xff0c;形成一个统一对外的服务的分布式数据库系统。每个节点由一个单机数据库系统独立管理和操作该物理机上的的所有资源&#xff08;CPU&#xff0c;内存等&#xff09…

MySQL索引1——索引基本概念与索引结构(B树、R树、Hash等)

目录 索引(INDEX)基本概念 索引结构分类 BTree树索引结构 Hash索引结构 Full-Text索引 R-Tree索引 索引(INDEX)基本概念 什么是索引 索引是帮助MySQL高效获取数据的有序数据结构 为数据库表中的某些列创建索引&#xff0c;就是对数据库表中某些列的值通过不同的数据结…

桶排序算法

桶排序算法 算法思想概述&#xff1a;桶排序的主要步骤如下&#xff1a; 算法goland实现&#xff1a;图解演示&#xff1a; 算法思想概述&#xff1a; 桶排序&#xff08;Bucket Sort&#xff09;是一种非比较性的排序算法&#xff0c;它将待排序的元素分到有限数量的桶&#…

Gitee+Jenkins(docker版)自动推送并部署Springboot项目到远程服务器

如果要参考gitlab配置请参考GitlabWebhook自动推送并更新Springboot项目 Gitlab的配置部分 环境介绍 Jenkins服务器(Centos7.6): docker安装的jenkins,参考Jenkins(docker安装)部署Springboot项目JDK1.8Maven3.6.3 注意docker安装的jenkins,而且是较新的版本,所以jenkins容器…

php获取随机订单号(封装函数)

作为一个开发人员&#xff0c;生成订单时常常需要获取一段随机码来表示订单号&#xff0c;并且订单号一般包含的特定的时间日期等信息&#xff0c;临时现写一个比较浪费时间&#xff0c;这里有一个封装好的生成随机订单号的函数&#xff0c;需要时直接调用即可。 代码如下&…

AtcoderABC224场

A - TiresA - Tires 题目大意 题目要求判断给定字符串S的末尾是以"er"还是"ist"结尾&#xff0c;并输出对应的结果。 思路分析 使用substr函数获取字符串S的末尾2个字符或3个字符。 判断获取到的子字符串是否等于"er"或"ist"&#…

利用NtDuplicateObject进行Dump

前言 由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;文章作者不为此承担任何责任。&#xff08;本文仅用于交流学习&#xff09; 这是国外老哥2020年提出的一种蛮有意思的思路。 我们先来看看大致的思路是…

java Spring Boot yml多环境配置

我们项目 线上和线下 环境配置不是特别一样 例如 运行的URL 数据库地址 数据库的账号密码 这些经常是不一样的 如果每次上线钱改 也不是特别方便 甚至可能忘记 那么 进入我们代码中 所谓的多环境 就是在不同的环境下配置不同的值 终端还是在application配置文件中 多环境的话…

【Linux】云服务器自动化部署VuePress博客(Jenkins)

前言 博主此前是将博客部署在 Github Pages&#xff08;基于 Github Action&#xff09;和 Vercel 上的&#xff0c;但是这两种部署方式对于国内用户很不友好&#xff0c;访问速度堪忧。因此将博客迁移到自己的云服务器上&#xff0c;并且基于 Jenkins&#xff08;一款开源持续…

【C语言】经典题目(四)

HI&#xff0c;大家好~&#x1f61d;&#x1f61d;这是一篇C语言经典题目的博客。 更多C语言经典题目及刷题篇&#xff0c;可以参考&#xff1a; &#x1f338; 【C语言】经典题目(一) &#x1f338; 【C语言】经典题目(二) &#x1f338; 【C语言】经典题目(三) &#x1f338;…

k8s dns 解析service异常

查看kube-dns日志 for p in $(kubectl get pods --namespacekube-system -l k8s-appkube-dns -o name); \ do kubectl logs --namespacekube-system $p; done k8s教程&#xff08;service篇&#xff09;-总结_阿甘兄的技术博客_51CTO博客

兰州https证书申请

https证书是由CA认证机构颁发的数字证书&#xff0c;可以为域名网站或者公网IP网站提供信息加密服务&#xff0c;正规CA认证机构签发的https证书可以兼容99%的主流浏览器和IOS、Windows系统&#xff0c;同样&#xff0c;现在流行的小程序也需要https证书。那么&#xff0c;该怎…