当前位置: 首页 > news >正文

Linux练级宝典->进程间通信

目录

进程间通信介绍

为什么要进程间通信

进程间通信的本质

进程间通信的分类

匿名管道

匿名管道

pipe函数

 管道读写规则

管道的特点

 管道的四种情况

​编辑 管道的大小

 命名管道

命令行创建命名管道

 在代码中创建命名管道

实现server 和 client 通信

基于server实现一个计算器功能

 服务器执行命令行指令

 用命名管道实现文件拷贝

system V进程间通信

system V共享内存

 共享内存的数据结构

共享内存的建立和释放

共享内存的创建

共享内存的释放

 共享内存的关联

共享内存去关联

用共享内存实现serve & client 通信

 共享内存和管道对比

system V消息队列

信息队列的基本原理

消息队列的数据结构

消息队列的创建:

消息队列的释放:

发送数据:

获取数据:

system V信号量:

概念:

信号量的数据机构

信号量相关函数

信号量集的创建

信号量集的删除

信号量集的操作

进程互斥

system V IPC


进程间通信介绍

进程间通信简称IPC(interprocess communication),进程间通信就是在不同进程之间传播或交换信息。

为什么要进程间通信

数据传输,资源共享,通知事件,进程控制。

通知事件:子进程向父进程发送中止事件,父进程可以回收子进程了。

进程间通信的本质

让不同的进程看到一份资源。

我们知道进程具有独立性,每个进程都认为自己享有整个内存空间,但实际是用多少拿多少,只是虚拟地址给了进程自信,而在物理地址中则是用多少给多少。

所以进程的数据应该是独一份的,除了父子进程没有进行写时拷贝时共用一份数据。

而我们想要做的就是让多个进程看到同一份资源。那就需要规定一块内存第三方资源来进行摆放信息,这里的信息就是多个进程都可以看见的。

进程间通信的分类

管道:匿名管道,命名管道。

System V IPC:System V 消息队列 System 共享队列 System V 信号量

POSIX IPC:消息队列 共享内存 信号量 互斥量 条件变量 读写锁

匿名管道

平常我们使用指令连接时比如,统计当前云服务器上的登录用户个数

 命令都是程序,所以上面有两个程序who和wc,who本来输出到屏幕的数据到了管道里,wc从管道里拿到数据。此时who的数据就被wc拿到了

who是以行表示当前登录用户个数 wc -l是统计当前的行数。

匿名管道

匿名管道的原理:用于父子进程之间的通信。

让不同的进程看到同一个资源,父子进程看到同一份被打开的文件,然后决定父子进程谁写入和读取。所以实现父子进程的通信

这个文件不会收到写时拷贝的影响,因为这个文件是共享的。

这里的数据不会刷新到磁盘里,因为增加了IO降低效率,所以共享资源当二者关闭后自然消失,而没有磁盘保存。 

pipe函数
int pipe(int pipefd[2]);

因为传入的是一个数组,所以是一个输入输出型参数。数组名就是一个指针。

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

 匿名管道流程

1.父进程调用pipe函数创建管道

2.父进程创建子进程,此时子进程和父进程对于管道是都连接的状态

3. 父进程关闭写端,子进程关闭读端,即父读子写

 注意点:

管道是半双工的,即只可以一端写,一端读,所以需要确认谁写谁读。

写端写入的数据会被内核缓冲,直到管道的读端读取。

看看文件描述符的视角

 1.父进程创建管道

2.父进程创建子进程

 

3.父进程关闭写端,子进程关闭读端

 代码如下:

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

int main()
{
    //fd[0]是读端fd[1]是写端
    int fd[2] = {0};
    if(pipe(fd) < 0)
    {
        perror("pipe");
        return 1;
    }
    pid_t id = fork();
    if(id == 0)
    {
        close(fd[0]);
        const char* msg = "hello father,i am child.......";
        int count = 10;
        while(count--)
        {
            write(fd[1],msg,strlen(msg));
            sleep(1);
        }
        close(fd[1]);
        exit(0);
    }
    //父进程关闭写端,然后读取管道数据并输出
    close(fd[1]);
    char buff[64];
    while(1)
    {
        ssize_t s = read(fd[0],buff,sizeof(buff));
        if(s > 0)
        {
            //说明有数据
            buff[s] = '\0';
            printf("child send to father:%s\n",buff);
        }
        else if(s == 0)
        {
            //说明文件读取完毕,后续没有
            printf("read file end\n");
            break;
        }
        else 
        {
            printf("read error\n");
            break;
        }
    }
    close(fd[0]);
    waitpid(id,NULL,0);
    return 0;
}

 管道读写规则

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

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

多了一个flags选项,

选项名字为O_NONBLOCK,即不阻塞的意思。

开启时,read函数如果没有数据读本来会把程序卡主,如果加了选项,此时read调用直接返回-1,并且错误码值为EAGAIN,即下次继续,先去执行后续逻辑。

write函数本来在管道写满时,就要被卡住,此时就不会卡住,一样返回EAGAIN,先去执行后续逻辑,直到管道有空间。

如果所有写端文件描述符都被关闭,read返回0.

如果所有读端文件描述符都被关闭,说明没有接收端,此时写端没意义,则会发送SIGPIPE信号终止进程。

原子性问题:

当PIPE_BUF能一次接收完全部数据时,此时就是原子性,即数据一次性全打入缓冲区。

如果数据量很大,此时要分多次打印,又由于我们是无阻塞状态,此时的整体数据写入的原子性了。

管道的特点

1.管道自带的同步与互斥机制

一次只允许一个进程使用的资源,称为临界资源。管道在同一时刻只允许一个进程对其进行读写,因此管道也称为一种临界资源。

所以当管道被多个进程使用时,这个临界资源是需要保护的,如果同一时间多个进程都对管道进行操作,同时读写,交叉读写,就会乱。

内核中对管道操作进行同步与互斥:

  • 同步:两个或两个以上的进程按照先后次序运行,比如A任务的运行依赖于B任务产生的数据。
  • 互斥:一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源。

同步就是我资源的完成,是有特定的进程接收,等会写好后,这个进程先运行,把数据接收了以后,后面排队的进程继续写入,重复上述步骤

互斥就是说,只有一个进程能使用这个公共资源,读也只能一个人读,写也是同理,不能同时有多个进程一起使用公共资源。 

2.管道的生命周期随文件

 管道也是通过文件进行通信的,如上面的图示一样,管道依赖于文件系统,文件描述符全部关闭后,此时管道就没用了,然后就被回收,所以说当使用管道的文件都关闭后,管道关闭

3.管道提供的是流式服务

进程A写入管道的数据,进程B每次读取都是任意的,这就是流式服务。

流式服务:数据没有明确的分割。

数据报服务:数据有明确的分割,数据按报文段拿。

4.管道是半双工通信

单工通信:一端固定为发送端,一端固定为接收端

半双工通信:双方都可以当发送端或者接收端,但是同一时间只能有一边进行传输

全双工通信:双方都可以当发送端或者接收端,同一时间可以一起传输 。

管道是半双工的,但是上面我们只看出他是一端接收一端发送,我们可以用两个管道就能实现

 管道的四种情况
  • 1.写端不写,读端读完后,没数据就挂起。直到写端写数据,读端唤醒
  • 2.读端不读,写端一直写,管道写满后,写端就会挂起,直到数据被读取,写端唤醒。
  • 3.写端进程将数据写完后将写端关闭,读端会把数据读取完后继续执行逻辑,而不是被挂起。
  • 4.读端进程关闭,此时没人读,写端就没有必要写了,此时写端进程也会被关闭。

前两个情况很好说明了,管道自带的同步和互斥机制,读端和写端是有步调的,一个写一个读,没有两个一起动这个说法。这就是同步,互斥就是说二者同一时间只能一人使用管道。

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

int main()
{
    //1.创建管道
    int fd[2] = {0};
    if(pipe(fd) < 0)
    {
        perror("pipe error");
        return 1;
    }
    //2.创建子进程
    pid_t id = fork();
    if(id == 0)
    {
        //关闭释放操作一定要清清楚楚
        //子进程
        close(fd[0]);//关闭读端
        int count = 10;
        const char* msg = "father,this is child,over....";
        while(count--)
        {
            write(fd[1],msg,strlen(msg));
            sleep(1);
        }
        close(fd[1]);
        exit(0);
    }
    //3.父进程关闭读端,子进程被强制关闭(看状态码)
    close(fd[1]);//关闭写端
    close(fd[0]);//强制关闭读端。
    int status;
    waitpid(id,&status,0);
    printf("child get signal:%d\n",status);
    return 0;
}

我们发现子进程和父进程这次没有打印数据,因为父进程读端关闭后,此时没人读,子进程被信号13杀掉,信号13:SIGPIPE

 管道的大小

1.使用指令:

ulimit -a

下面有一个pipe size:512 * 8 = 4096字节。

 2.自行测试

 代码如下:

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

int main()
{
    //1.创建管道
    int fd[2] = {0};
    if(pipe(fd) < 0)
    {
        perror("pipe");
        return 1;
    }
    //2.创建子进程,并且子进程一直写1字节,然后每次打印count
    pid_t pid = fork();
    if(pid == 0)
    {
        //子进程
        char a = 'a';
        //关闭读端
        close(fd[0]);
        int count = 0;
        while(1)
        {
            count++;
            write(fd[1],&a,1);
            printf("%d\n",count);
        }
        //关闭写端
        close(fd[1]);
        exit(0);
    }
    //3.父进程
    close(fd[1]);//关闭写端
    waitpid(pid,NULL,0);
    close(fd[0]);
    return 0;
}

结果如下: 

我们发现管道的大小有65536字节而不是4096字节。

 命名管道

我们发现匿名管道,好像是和子进程和父进程相关,所以是有亲缘关系的进程才能使用。

但是如果我们想要两个不想干的进程使用管道通信呢?这时就要用到命名管道了。

  • 命名管道实际就是一个文件,两个进程通过这个管道文件进行数据的运输和获取。
  • 匿名管道就像C++中,匿名对象一样,用完就丢,而命名管道实际是创造了一个文件映像出来。
  • 但是命名管道虽然有文件,但是大小依然为0,和匿名管道一样二者都是不刷新到磁盘中的。内存中用完就可以丢了。 
命令行创建命名管道

mkfifo指令:

发现我们上面出现了一个fifo文件,并且文件类型为p ,pipe的意思

 并且字节数为0,说明它没有占用磁盘。

我们用一个脚本指令,持续向fifo文件输入数据,然后另一个终端读取这个fifo的数据。

两个终端的指令如下:

while :; do echo "hello fifo";sleep 1; done > fifo
cat < fifo

持续打印hello fifo进入fifo,另一个把fifo中的数据打印出来。 

 在代码中创建命名管道

函数名也是mkfifo原型如下:

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

pathname:1.以路径给出,则在指定路径下创建一个命名管道。

2.以文件名给出,则在当前路径在当前路径下。

mode:创建命名管道文件的权限。

mode 666如下:

但是由于有umask(文件默认掩码)的影响,实际创建出来的权限会变成 mode&(~umask) , 就是说掩码的值为多少,在那个比特位的值都会变为0,如果umask == 2,上面的权限就会变为

mode 664如下:

 所以可以把umask的值直接设置为0,在创建文件前使用umask(0)函数把umask的值提前关掉。

mkfifo的返回值:

创建成功,返回0,。

失败,返回-1.

实现代码如下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
//设置宏变量文件名
#define FILE_NAME "myfifo"

int main()
{
    //设置掩码值
    umask(0);
    //创建命名管道
    if(mkfifo(FILE_NAME,0666) < 0)
    {
        perror("mkfifo");
        return 1;
    }
    //创建成功后的逻辑 ......
    return 0;
}

实现server 和 client 通信

服务器逻辑:

1.服务端创建一个命名管道

2.关闭写端

3.等待数据

#include "common.h"
int main()
{
    //创建命名管道
    umask(0);
    if(mkfifo(FILE_NAME,0666) < 0)
    {
        perror("mkfifo");
        return 1;
    }
    //管道创建成功
    //读端打开管道文件
    int fd = open(FILE_NAME,O_RDONLY);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    char msg[128];
    while(1)
    {
        //读端重复读取命名管道
        msg[0] = '\0';
        ssize_t size = read(fd,msg,sizeof(msg)-1);
        if(size > 0)
        {
            //说明此时管道中存在数据
            msg[size] = '\0';
            printf("client say: %s\n",msg); 
        }
        else if(size == 0)
        {
            printf("client quit\n");
            break;
        }
        else 
        {
            printf("read error");
            break;
        }
    }
    close(fd);
    return 0;
}

对于客户端来说,因为服务端创建了管道,此时客户端以写的方式打开管道写入即可。服务器一直在读取的。

我们启动两个终端,一个终端用作服务器,一个终端用作客户端,我们发现,启动服务器server后,客户端中myfifo文件创建,此时打开启动客户端。

我们客户端输入信息,此时我们的服务器就收到信息了。 这就是命名管道对不同进程的通信形式。

基于server实现一个计算器功能

我们的client就不用改了,因为只是发信息,而server因为要处理计算任务,所以逻辑不是显示了,而是计算后显示,所以就需要server在收到信息时做出相应的逻辑。 

#include "common.h"
int main()
{
    //创建命名管道
    umask(0);
    if(mkfifo(FILE_NAME,0666) < 0)
    {
        perror("mkfifo");
        return 1;
    }
    //管道创建成功
    //读端打开管道文件
    int fd = open(FILE_NAME,O_RDONLY);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    char msg[128];
    while(1)
    {
        //读端重复读取命名管道
        msg[0] = '\0';
        ssize_t size = read(fd,msg,sizeof(msg)-1);
        if(size > 0)
        {
            //说明此时管道中存在数据
            //对信息进行解析
            msg[size] = '\0';
            printf("client say: %s\n",msg);
            char* label = "+-*/%";
            char* p = msg;
            int flag = 0;
            while(*p)
            {
                //获取是什么符号
                switch(*p)
                {
                    case '+':
                        flag = 0;
                        break;
                    case '-':
                        flag = 1;
                        break;
                    case '*':
                        flag = 2;
                        break;
                    case '/':
                        flag = 3;
                        break;
                    case '%':
                        flag = 4;
                        break;
                }
                p++;
            }
            //解析出两个数字
            char* snum1 = strtok(msg,"+-*/%");
            char* snum2 = strtok(NULL,"+-*/%");
            int num1 = atoi(snum1);
            int num2 = atoi(snum2);
            int ret = 0;
            switch (flag)
            {
                case 0:
                    ret = num1 + num2;
                    break;
                case 1:
                    ret = num1 - num2;
                    break;
                case 2:
                    ret = num1 * num2;
                    break;
                case 3:
                    ret = num1 / num2;
                    break;
                case 4:
                    ret = num1 % num2;
                    break;
            }
            //打印计算结果
            printf("%d %c %d = %d\n",num1,label[flag],num2,ret);
        }
        else if(size == 0)
        {
            printf("client quit\n");
            break;
        }
        else 
        {
            printf("read error");
            break;
        }
    }
    close(fd);
    return 0;
}

由于我们只实现了计算逻辑,而对其他信息的逻辑没进行处理,所以会导致输入其他内容直接出错。

 服务器执行命令行指令

#include "common.h"
int main()
{
    //创建命名管道
    umask(0);
    if(mkfifo(FILE_NAME,0666) < 0)
    {
        perror("mkfifo");
        return 1;
    }
    //管道创建成功
    //读端打开管道文件
    int fd = open(FILE_NAME,O_RDONLY);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    char msg[128];
    while(1)
    {
        //读端重复读取命名管道
        msg[0] = '\0';
        ssize_t size = read(fd,msg,sizeof(msg)-1);
        if(size > 0)
        {
            //说明此时管道中存在数据
            msg[size] = '\0';
            printf("client say: %s\n",msg); 
            if(fork() == 0)
            {
                //进程替换,使用对应的指令
                execlp(msg,msg,NULL);
                exit(1);
            }
            waitpid(-1,NULL,0);
        }
        else if(size == 0)
        {
            printf("client quit\n");
            break;
        }
        else 
        {
            printf("read error");
            break;
        }
    }
    close(fd);
    return 0;
}

 用命名管道实现文件拷贝

先来介绍一下为什么要学这个实现文件拷贝,事实就是我们文件的拷贝,其实就是网络在做的事情,网络就是从客户端或者服务器,把文件粘贴到另一端的过程。

所以这里实现文件拷贝,其实和网络下载的总体道理是差不多的。

逻辑就是我们客户端的一个文件,将数据传到管道,服务器端通过读取数据,在服务器下面创建一个副本。

服务器代码如下:

#include "common.h"
int main()
{
    //创建命名管道
    umask(0);
    if(mkfifo(FILE_NAME,0666) < 0)
    {
        perror("mkfifo");
        return 1;
    }
    //管道创建成功
    //读端打开管道文件
    int fd = open(FILE_NAME,O_RDONLY);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    int fdout = open("file-copy.txt",O_CREAT | O_WRONLY,0666);
    if(fd < 0)
    {
        perror("open");
        return 3;
    }
    char msg[128];
    while(1)
    {
        //读端重复读取命名管道
        msg[0] = '\0';
        ssize_t size = read(fd,msg,sizeof(msg)-1);
        if(size > 0)
        {
            //说明此时管道中存在数据
            write(fdout,msg,size);
        }
        else if(size == 0)
        {
            printf("client quit\n");
            break;
        }
        else 
        {
            printf("read error");
            break;
        }
    }
    close(fd);
    close(fdout);
    return 0;
}

客户端代码如下:

#include "common.h"


int main()
{
    //打开管道
    int fd = open(FILE_NAME,O_WRONLY);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    //打开file.txt文件
    int fdin = open("file.txt",O_RDONLY);
    if(fdin < 0)
    {
        perror("open");
        return 3;
    }
    //写入数据
    char msg[128];
    while(1)
    {
        msg[0] = '\0';
        
        size_t size = read(fdin,msg,sizeof(msg));
        if(size > 0)
        {
            msg[size - 1] = '\0';
            //信息写入管道
            write(fd,msg,size);
        }
        else if(size == 0)
        {
            printf("read end of file\n");
            break;
        }
        else
        {
            printf("read error\n");
            break;
        }
    }
    close(fdin);
    close(fd);
    return 0;
}

 结果如下:

我们发现此时就出现了copy文件了。 

二者内容一样。 

system V进程间通信

和管道一样,都是为了让不同的进程看到同一份资源。

三种通信方式

1. system V共享内存

2. system V消息队列

3. system V信号量

共享内存和消息队列是用来进行数据传输的,而信号量则是为了进程间同步与互斥设计的。

system V共享内存

共享内存,顾名思义就是在物理内存申请一块共享内存,这块内存在每一个进程中的虚拟内存中都建立了映射,之后不管是任何进程只要使用这个共享内存,映射后都是同一个物理内存,所以实现了不同进程看到同一份信息。

 共享内存的数据结构
struct shmid_ds {
	struct ipc_perm     shm_perm;   /* operation perms */
	int         shm_segsz;  /* size of segment (bytes) */
	__kernel_time_t     shm_atime;  /* last attach time */
	__kernel_time_t     shm_dtime;  /* last detach time */
	__kernel_time_t     shm_ctime;  /* last change time */
	__kernel_ipc_pid_t  shm_cpid;   /* pid of creator */
	__kernel_ipc_pid_t  shm_lpid;   /* pid of last operator */
	unsigned short      shm_nattch; /* no. of current attaches */
	unsigned short      shm_unused; /* compatibility */
	void            *shm_unused2;   /* ditto - used by DIPC */
	void            *shm_unused3;   /* unused */
};

因为共享内存不止一个,并且使用不同共享内存的进程也可能是不同的,所以要把这些数据管理起来,可以看到我们结构体中存在了cpid 和 lpid 这两个id 就是记录创造共享内存的id和最近使用共享内存id的进程。

其中的shm_perm中这里面就是用来识别不同的共享内存的,它的内部结构如下:

struct ipc_perm{
	__kernel_key_t  key;
	__kernel_uid_t  uid;
	__kernel_gid_t  gid;
	__kernel_uid_t  cuid;
	__kernel_gid_t  cgid;
	__kernel_mode_t mode;
	unsigned short  seq;
};

可以看到里面存在一个key变量,这个变量就类似于pid,pid用来识别不同的进程,key就是用来识别不同的共享内存。

共享内存的建立和释放

建立:

  1. 在物理内存中申请共享内存的空间。
  2. 申请到的共享内存的物理地址和创建的进程虚拟内存建立映射关系。

释放:

  1. 把建立好的映射关系都去掉。
  2. 释放共享内存的内存空间。
共享内存的创建

函数:

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

 参数说明:

  • key就是共享内存的id,但是是在系统层面的。
  • size表示要创建的共享内存的大小。
  • shmflg表示要创建的共享内存的方式。

返回值说明:

  • 调用成功,返回一个用户层面的共享内存id。
  • 调用失败,返回-1.

attention:

句柄的意思就是用来表达某种资源能力的东西,shmget返回的用户层面的共享内存id,就是一个句柄,后续要对共享内存进行操作,都要搭配这个句柄进行使用。

ftok函数

我们的key需要用ftok函数进行获取。 

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

传入一个pathname和proj_id 此时会转换出一个key值,pathname必须要存在且可取。

所以说这个ftok函数有什么用?如果让我们自己指定key值,用数字一个个来写有时候我们是记不住的。而用pathname + id 的方法构建出一个key,此时记忆路径名会相对简单。

shmget的第三个参数

上述两种方法就是:不存在都创建,上面那个存在则返回这个共享内存,下面的则是存在出错返回。

 我们可以使用ftok和shmget函数创建共享内存观察一下:

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

#define PATHNAME "/home/ubuntu/blog-code"

#define PROJ_ID 0x6666//整数标识符
#define SIZE 4096

int main()
{
    //1.ftok获取key
    key_t key = (PATHNAME,PROJ_ID);
    if(key < 0)
    {
        perror("ftok");
        return 1;
    }
    //2.shmget 获取 用户级id句柄
    int shmid = shmget(key,SIZE,IPC_CREAT | IPC_EXCL);
    if(shmid < 0)
    {
        perror("shmget");
        return 1;
    }
    //3.打印 key 和 id句柄
    printf("key : %x\n",key);
    printf("shmid : %d\n",shmid);
    return 0;
}

 使用ipcs,会默认弹出消息队列,共享内存以及信号量相关的信息

 

attention:key是内核层面用来保证共享内存唯一性的,shmid是用户层面保证唯一性的。

类似于fd 和 FILE*的关系,一个是给用户使用的,一个是系统使用的

共享内存的释放

和其他文件一样,共享内存也是要释放的,但是我们发现上述我们在程序结束后,共享内存依然存在,也就变相的说明共享内存不是进程管理的,虽然由进程创建但是不跟随进程的生命周期,所以共享内存的生命周期是跟随系统的。

释放共享内存的两种方法:

1.使用命令释放共享内存

 ipcrm -m + shmid 删除指定shmid 的 共享内存

ipcrm -m 1

 attention: 要用的是 shmid 因为我们是用户层。

2.使用程序释放共享内存资源

 shmctl函数:

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

参数说明:

shmid :用户层使用的共享内存标识符

cmd :控制动作

buf :获取或设置控制共享内存的数据结构

返回值说明:

shmctl成功,返回0。

失败,返回-1 。

第二个参数可传入的选项

 释放共享内存代码测试:

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

#define PATHNAME "/home/ubuntu/blog-code"

#define PROJ_ID 0x6666//整数标识符
#define SIZE 4096

int main()
{
    //1.ftok获取key
    key_t key = (PATHNAME,PROJ_ID);
    if(key < 0)
    {
        perror("ftok");
        return 1;
    }
    //2.shmget 获取 用户级id句柄
    int shmid = shmget(key,SIZE,IPC_CREAT | IPC_EXCL);
    if(shmid < 0)
    {
        perror("shmget");
        return 1;
    }
    //3.打印 key 和 id句柄
    printf("key : %x\n",key);
    printf("shmid : %d\n",shmid);
    sleep(2);
    //4.释放共享内存
    shmctl(shmid,IPC_RMID,NULL);
    sleep(2);
    return 0;
}

我们发现这一次的共享内存直接被释放了。 

使用脚本指令:持续的观察共享内存的状况

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

 共享内存的关联

shmat函数

shared memory attach, 贴上共享内存,就可以当做,共享内存的关联了。

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

参数说明:

shmid:用户级共享内存标识符

shmaddr:指定共享内存映射到进程地址空间的某一位置,通常设置为NULL,让内核决定

shmflg:关联共享内存时设置的某些属性。

返回值说明:

调用成功,返回一个共享内存映射到进程地址空间中的起始地址

调用失败,返回(void*)-1.

第三个参数的选项:

 进行共享内存关联的代码测试:

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

#define PATHNAME "/home/ubuntu/blog-code" //路径名

#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小

int main()
{
    //1.ftok函数做key
    key_t key = ftok(PATHNAME,PROJ_ID);
    if(key < 0) 
    {
        perror("key");
        return 1;
    }
    //2.shmget制造共享内存
    int shmid = shmget(key,SIZE,IPC_CREAT | IPC_EXCL | 0666);
    if(shmid < 0)
    {
        perror("shmid");
        return 1;
    }
    printf("key:%x\n",key);
    printf("shmid:%d\n",shmid);
    //3.shmat关联共享内存
    printf("attach begin\n");
    sleep(2);
    //以读写打开共享内存
    char* mem = shmat(shmid,NULL,SHM_RDONLY);
    if(mem == (void*)-1)
    {
        perror("shmat");
        return 1;
    }
    printf("attach end\n");
    sleep(2);
    //4.释放共享内存
    shmctl(shmid,IPC_RMID,NULL);
    return 0;
}

上面可以看到我们d 

attention:这里的权限要注意,普通用户不能打开共享内存,所以切换为root用户

 当然也可以在创建共享内存时,就给它设置好权限,就是更改shmget函数传参如下:

    int shmid = shmget(key,SIZE,IPC_CREAT | IPC_EXCL | 0666);

发现此时我们除了root用户也可以执行shmat命令了。 

共享内存去关联
int shmdt(const void *shmaddr);

shared memory detach 取消关联。

参数说明:

shmaddr:共享内存在进程空间的起始地址,在上面shmat中的返回值

返回值说明:

调用成功返回0,失败返回-1

解除关联的代码测试:

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

#define PATHNAME "/home/ubuntu/blog-code" //路径名

#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小

int main()
{
    //1.ftok函数做key
    key_t key = ftok(PATHNAME,PROJ_ID);
    if(key < 0) 
    {
        perror("key");
        return 1;
    }
    //2.shmget制造共享内存
    int shmid = shmget(key,SIZE,IPC_CREAT | IPC_EXCL | 0666);
    if(shmid < 0)
    {
        perror("shmid");
        return 1;
    }
    printf("key:%x\n",key);
    printf("shmid:%d\n",shmid);
    //3.shmat关联共享内存
    printf("attach begin\n");
    sleep(2);
    //以读写打开共享内存
    char* mem = shmat(shmid,NULL,SHM_RDONLY);
    if(mem == (void*)-1)
    {
        perror("shmat");
        return 1;
    }
    printf("attach end\n");
    sleep(2);
    //解除关联
    printf("detach begin\n");

    sleep(2);
    shmdt(mem);
    printf("detach end\n");
    sleep(2);
    //4.释放共享内存
    shmctl(shmid,IPC_RMID,NULL);
    return 0;
}

上图我们发现我attach后 nattch数量为1,detach后 nattch 数量为0,最后共享内存被释放。

 attention:

共享内存的解关联不是释放共享内存,只是当前进程和共享内存之间的关系解除,最后还是需要释放共享内存

用共享内存实现serve & client 通信

我们要实现的就是让客户端和服务器都连接到同一个共享内存,服务器创建并连接,客户端负责连接。

服务器代码:

#include "common.h"

int main()
{
    //1.创造key值
    key_t key = ftok(PATHNAME,PROJ_ID);
    if(key < 0)
    {
        perror("ftok");
        return 1;
    }
    //2.创建共享内存
    int shmid = shmget(key,SIZE,IPC_CREAT | IPC_EXCL | 0666);
    if(shmid < 0)
    {
        perror("shmget");
        return 1;
    }
    //3.连接并死循环等待client连接
    char* mem = shmat(shmid,NULL,0);
    if(mem == (void*)-1)
    {
        perror("shmat");
        return 1;
    }
    while(1)
    {
        //......
    }
    //4.解除连接并释放共享内存
    int ret = shmdt(mem);
    if(ret < 0)
    {
        perror("shmdt");
        return 1;
    }
    shmctl(shmid,IPC_RMID,NULL);
    return 0;
}

客户端代码:

#include "common.h"

int main()
{
    //1.创造key值
    key_t key = ftok(PATHNAME,PROJ_ID);
    if(key < 0)
    {
        perror("ftok");
        return 1;
    }
    //2.创建共享内存
    int shmid = shmget(key,SIZE,IPC_CREAT);
    if(shmid < 0)
    {
        perror("shmget");
        return 1;
    }
    //3.连接并死循环等待client连接
    char* mem = shmat(shmid,NULL,0);
    if(mem == (void*)-1)
    {
        perror("shmat");
        return 1;
    }
    while(1)
    {
        //......
    }
    //4.解除连接
    int ret = shmdt(mem);
    if(ret < 0)
    {
        perror("shmdt");
        return 1;
    }
    
    return 0;
}

common.h:

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

#define PATHNAME "/home/ubuntu/blog-code" //路径名

#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小

二者代码的区别就在于:服务器是创建并且打开,所以要释放 ,客户端只用打开就可以了。

但是注意我们的共享内存可能释放不了,但是可以用命令行关闭,因为我们代码死循环卡在一半了

执行情况:

可以发现我们此时客户端和服务器都连接上了共享内存 (nattch为2)。

客户端向共享内存不断的写入数据:

int i = 0;
    while(1)
    {
        //向共享内存写入数据
        mem[i] = 'A' + i;
        i++;
        mem[i] = '\0';
        sleep(1);
    }

服务器不断读取客户端数据: 

while(1)
    {
        //不断从共享内存读取数据
        printf("client say: %s\n",mem);
        sleep(1);
    }

 结果:

 共享内存和管道对比

共享内存创建后,只用向共享内存直接添加数据就好,而管道创建好后,是有文件创立的,我们要对文件写入和读取还是需要用系统调用的例如:open write read......

管道通信步骤:

由上图看出我们管道在一次通信中,有4次拷贝

第一次拷贝:从输入设备或者文件复制数据到服务器的缓冲区。

第二次拷贝:从服务器的缓冲区的数据复制到管道中。

第三次拷贝:从管道中将数据拷贝到客户端的缓冲区中。

第四次拷贝:从客户端的缓冲区中拷贝到要输出的设备或文件中。

共享内存通信步骤:

 由上图我们看出我们的文件传输只用了两步:

首先我们创建好共享内存,之后的读取和输入都是直接到位的。

所以共享内存是所有进程间通信方式中最快的一种,因为该通信方式只需要两次拷贝。

缺点:我们知道管道自带同步与互斥机制,同步:输入给谁谁从管道拿。互斥:同一时间只有一个进程使用管道。共享内存没有任何保护机制。

system V消息队列

信息队列的基本原理

顾名思义:队列就是在系统中创建了一个队列,多个进程可以向这个队列发送数据,获取数据时都从队头获取数据,发送数据都是发送到队尾和队列一样。

怎么判别数据是给谁的,在数据块类型。

消息队列的数据结构
struct msqid_ds {
	struct ipc_perm msg_perm;
	struct msg *msg_first;      /* first message on queue,unused  */
	struct msg *msg_last;       /* last message in queue,unused */
	__kernel_time_t msg_stime;  /* last msgsnd time */
	__kernel_time_t msg_rtime;  /* last msgrcv time */
	__kernel_time_t msg_ctime;  /* last change time */
	unsigned long  msg_lcbytes; /* Reuse junk fields for 32 bit */
	unsigned long  msg_lqbytes; /* ditto */
	unsigned short msg_cbytes;  /* current number of bytes on queue */
	unsigned short msg_qnum;    /* number of messages in queue */
	unsigned short msg_qbytes;  /* max number of bytes on queue */
	__kernel_ipc_pid_t msg_lspid;   /* pid of last msgsnd */
	__kernel_ipc_pid_t msg_lrpid;   /* last receive pid */
};

我们发现第一个变量依然是结构体 ipc_perm 和共享内存一样的。

内容如下:

struct ipc_perm{
	__kernel_key_t  key;
	__kernel_uid_t  uid;
	__kernel_gid_t  gid;
	__kernel_uid_t  cuid;
	__kernel_gid_t  cgid;
	__kernel_mode_t mode;
	unsigned short  seq;
};
消息队列的创建:
int msgget(key_t key, int msgflg);

我们发现和共享内存一样也需要key,所以也要用到ftok函数制作key。

第二个参数和共享内存的第三个参数一样。设定消息队列的信息。

创建成功返回一个用户级标识符,失败返回-1.

消息队列的释放:
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

第一个参数:msqid 是 msgget的返回值。

第二个参数:cmd 是 要怎么操控信息队列

第三个参数:buf是消息队列结构体的相关数据结构

发送数据:
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

第一个参数:消息队列用户级标识符

第二个参数:msgp 待发送的数据块

第三个参数:msgsz 待发送的数据块的大小

第四个参数:发送数据块的模式,一般填0就好

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

attention:第二个参数必须为以下结构

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

第二个成员就是我们要发送的数据,当使用时可以自己改变大小。

获取数据:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

第一个参数:消息队列用户级标识符

第二个参数:msgp 表示获取的数据块,是输出型参数

第三个参数:msgsz表示获取的数据块的大小

第四个参数:msgtyp表示要接收数据块的类型

第五个参数:获取数据块的方式

返回值:调用成功返回实际获取到mtext数组中的字节数,失败返回-1.

system V信号量:

概念:

我们知道上述两种共享方式,消息队列和共享内存,如果多进程竞争使用,是不保证安全的。

互斥资源 临界资源:多进程竞争使用一个资源。这个资源一次只能一个进程正在使用。

在进程中涉及到临界资源的程序段叫临界区。

IPC资源必须删除,不会自动删除,因为IPC资源的生命周期跟随内核

信号量的数据机构
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

信号量相关函数
信号量集的创建
int semget(key_t key, int nsems, int semflg);

我们可以看到有key 所以也要用 ftok生成一个key值。

第二个参数:nsems 代表要创建信号量的个数

第三个参数:semflg 表示怎么创建信号量

返回值:创建成功返回用户级标识符,失败返回-1.

信号量集的删除
int semctl(int semid, int semnum, int cmd, ...);
信号量集的操作
int semop(int semid, struct sembuf *sops, unsigned nsops);
进程互斥

我们进程共享资源是爽了,但是也有新的问题,就是我们进程共用的临界资源,若是不保护,就会出现数据不一致的情况。

信号量就是用来保护临界区的,信号量分为二元信号量和多元信号量

假设我们有100字节的数据,如果算他25字节一份,此时就有4份,那就可以用4个信号量进行标识。

 信号量本质就是一个计数器,在二元信号量中,信号量的个数为1(相当于将临界资源看成一整块)。

 由上面的伪代码中我们可以看到,我们有一个sem,在sem==1时,我们进入的是 sem--这个逻辑,代表这个信号量我拿走了,其他进程此时进来就发现sem == 0走到else中挂起。而使用信号量的这个进程完成操作后对sem++就相当于将信号量归还了。

这样就保证了同一时间内只有一个进程能访问临界区。

PV操作

 

我们把计数器sem--的操作叫做P操作,sem++叫做V操作,P就是申请信号量,V就是释放信号量。 

system V IPC

我们学习了上述三个共享内存,消息队列,信号量后我们发现他们的函数中都有一个ipc perm这个结构体大家都是一样的,所以操作系统维护system V 的东西的时候,就进行了统一管理。

相关文章:

  • 卷积神经网络(笔记03)
  • 缓存及其问题解决
  • linux中yum和wget指令的区别
  • 【目标检测】【CVPR 2025】DEIM:具有改进匹配机制的DETR以实现快速收敛
  • 【fnOS飞牛云NAS本地部署跨平台视频下载工具MediaGo与远程访问下载视频流程】
  • 001 | How To Take Study Notes:五种做笔记的方法(中英)
  • 【Node】Node.js环境变量配置,及下载地址
  • SpringBoot动态加载JAR包实战:实现插件化架构的终极指南
  • RHCE(RHCSA复习:虚拟的安装和设置)
  • 记第一次跟踪seatunnel的任务运行过程三——解析配置的具体方法getLogicalDag
  • 论文调研 | 一些开源的AI代码生成模型调研及总结【更新于250313】
  • 专题三x的平方根
  • 动态调试实战:Frida脚本编写与内存注入
  • 【实战ES】实战 Elasticsearch:快速上手与深度实践-附录-1-常用命令速查表-集群健康检查、索引生命周期管理、故障诊断命令
  • stable Diffusion 中的 VAE是什么
  • P3390 【模板】矩阵快速幂
  • Redis项目_黑马点评
  • 【JavaEE】Spring Web MVC
  • 蓝队基本技能 web入侵指南 日志分析 后门查杀 流量分析
  • Houdini学习笔记
  • baidu网站建设/如何拥有自己的网站
  • 网站seo快速/荆门网络推广
  • 如何做可以微信转发的网站/河南网站优化公司
  • 做日本贸易哪个网站好/seo每日
  • 找人做网站要密码吗/100种宣传方式
  • 网站建设套餐报价/广州seo全网营销