进程间通信

一.进程间通信介绍

进程间通信目的

(1). 数据传输:一个进程需要将它的数据发送给另一个进程(如A进程要把数据传输给B进程,让B进程进行一些业务处理)
举例 :
log.txt 中有一些内容

cat log.txt | grep hello

cat 进程将log.txt中的数据通过管道交给grep进程

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

补充知识 :

(1). 前面的文章说过进程之间是具有独立性的,那就说明两个进程要想完成数据共享成本是比较高的,因为进程的独立性就体现在数据各自私有,所以进程间通信,一般一定要借助第三方(OS)资源

(2). 通信的本质就是"数据的拷贝"
进程A->数据拷贝给OS->OS把数据拷贝给进程B
OS一定要提供一段内存区域,能够被双方进程看到

(3). 进程间通信本质 : 让不同的进程,看到同一份资源(内存,文件内核缓冲等),资源由操作系统哪些模块提供,就有了不同的进程间通信方式

(4). 进程间通信是有标准的(system V标准/POSIX标准)

进程间通信发展

(1). 管道

(2). System V进程间通信
实现一台机器上的若干进程通信

(3). POSIX进程间通信
实现进程间可以跨网络通信(QQ聊天时就实现了跨主机通信)

进程间通信分类

管道 :
(1). 匿名管道pipe
(2). 命名管道

System V IPC :
(1). System V 共享内存
(2). System V 消息队列
(3). System V 信号量

POSIX IPC :
(1). 消息队列
(2). 共享内存
(3). 信号量
(4).互斥量
(5).条件变量
(6). 读写锁

二.管道

匿名管道

(1). 管道是Unix中最古老的进程间通信的形式。
(2). 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
(3). 管道只能进行单向通信

管道通信 : 管道本质上就是一个文件,前面的进程以写方式打开文件,后面的进程以读方式打开。这样前面写完后面读,于是就实现了通信。实际上管道的设计也是遵循UNIX的“一切皆文件”设计原则的,它本质上就是一个文件。虽然实现形态上是文件,但是管道本身并不占用磁盘或者其他外部存储的空间。在Linux的实现上,它占用的是内存空间。所以,Linux上的管道就是一个操作方式为文件的内存缓冲区。

在这里插入图片描述

pipe函数介绍

#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
// 以读方式和写方式打开同一个文件,返回的两个文件描述符填入fd数组中
返回值:成功返回0,失败返回错误代码

匿名管道实现步骤 :
(1). 调用pipe函数创建匿名管道,传入的输出型参数fd[2]调用后fd[0]为读端,fd[1]为写端
(2). fork创建子进程,子进程fd[0]同样为读端,fd[1]为写端
(3). 关闭父进程读端,子进程写端(或关闭父进程写端,子进程读端)
(4). 父进程向管道写,子进程从管道读,从而实现进程间通信

在这里插入图片描述

// 父子进程通过匿名管道实现通信的测试代码

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
// child->write  father->read
int main()
{
        int fd[2] = {0};
        if(pipe(fd) < 0)
        {
                perror("pipe!\n");
                return -1;
        }
        //printf("fd[0] : %d\n",fd[0]); 3
        //printf("fd[1] : %d\n",fd[1]); 4
        pid_t id = fork();
        if(id == 0)
        {
                close(fd[0]);
                const char* msg = "I am a child\n";
                int count = 10;
                while(count)
                {
                        write(fd[1],msg,strlen(msg));
                        count--;
                        sleep(1);
                }
                exit(0);
        }
        close(fd[1]);
        char buf[64];
        while(1)
        {
                ssize_t s = read(fd[0],buf,sizeof(buf));
                if(s > 0)
                {
                        buf[s] = '\0';
                        printf("child send to father : %s",buf);
                }
                else if(s == 0)
                {
                        printf("read file end !\n");
                        break;
                }
                else
                {
                        perror("read\n");
                        break;
                }
        }
        waitpid(id,NULL,0);
        return 0;
}                                                                                                                             

关于这段代码的几点分析和扩展

(1). 为什么子进程休眠1秒,运行结果父进程也休眠1秒呢 ?

临界资源 : 被多个进程共享的资源
临界区 : 访问临界资源的代码
进程互斥 : 在使用系统资源时,一个进程正在使用,另一个进程必须阻塞等待,不能同时使用(子进程在向管道写入时,父进程如果去管道读,可能会导致读写数据不一致)
管道自带同步与互斥机制,进程互斥可以解决数据混乱的问题,如果没有互斥机制,子进程在写入的时候,父进程就有可能来读取,就会发生意料之外的结果

管道内部已经自动提供了互斥(子进程写入,父进程不能读取)与同步(子进程写完,父进程再来读取)机制,当子进程向管道写入数据,然后sleep,父进程去管道读取数据打印,然后read 识别到管道为空,父进程不再进行读取,阻塞式的等待子进程写入,所以并非是父进程sleep了,而是因为子进程写的慢,导致父进程阻塞式等待

(2). 子进程一直写,父进程sleep不去读,写满缓冲区之后,子进程会等待父进程读取之后再写入,子进程被挂起

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
// child->write  father->read
int main()
{
        int fd[2] = {0};
        if(pipe(fd) < 0)
        {
                perror("pipe!\n");
                return -1;
        }
        //printf("fd[0] : %d\n",fd[0]);
        //printf("fd[1] : %d\n",fd[1]);
        pid_t id = fork();
        if(id == 0)
        {
                close(fd[0]);
                const char* msg = "I am a child\n";
                while(1)
                {
                        write(fd[1],msg,strlen(msg));
                }
                exit(0);
        }
        close(fd[1]);
        char buf[64];
        while(1)
        {
        		sleep(100);
                ssize_t s = read(fd[0],buf,sizeof(buf));
                if(s > 0)
                {
                        buf[s] = '\0';
                        printf("child send to father : %s",buf);
                }
                else if(s == 0)
                {
                        printf("read file end !\n");
                        break;
                }
                else
                {
                        perror("read\n");
                        break;
                }
        }
        waitpid(id,NULL,0);
        return 0;
}                                  

(3). 父进程一直在读取,子进程写入5行后不再写入,父进程会等待子进程写入之后再读取,父进程被挂起

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
// child->write  father->read
int main()
{
        int fd[2] = {0};
        if(pipe(fd) < 0)
        {
                perror("pipe!\n");
                return -1;
        }
        //printf("fd[0] : %d\n",fd[0]);
        //printf("fd[1] : %d\n",fd[1]);
        pid_t id = fork();
        if(id == 0)
        {
                close(fd[0]);
                const char* msg = "I am a child\n";
                int count = 10;
                while(count)
                {
                        if(count == 5)
                        {
                        	sleep(1000);
                        }
                        else
                        {
                        	sleep(1);
                         	write(fd[1],msg,strlen(msg));
                        	count--;
                        }
                }
                exit(0);
        }
        close(fd[1]);
        char buf[64];
        while(1)
        {
                ssize_t s = read(fd[0],buf,sizeof(buf));
                if(s > 0)
                {
                        buf[s] = '\0';
                        printf("child send to father : %s",buf);
                }
                else if(s == 0)
                {
                        printf("read file end !\n");
                        break;
                }
                else
                {
                        perror("read\n");
                        break;
                }
        }
        waitpid(id,NULL,0);
        return 0;
}                      

(4). 父进程读端关闭,子进程写端再写入就无意义,操作系统会杀掉子进程,子进程会收到13号信号(SIGPIPE)

运行结果 :

child send to father : I am a child
child exit sigal : 13
// kill -l 查看信号
13) SIGPIPE
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
// child->write  father->read
int main()
{
        int fd[2] = {0};
        if(pipe(fd) < 0)
        {
                perror("pipe!\n");
                return -1;
        }
        //printf("fd[0] : %d\n",fd[0]);
        //printf("fd[1] : %d\n",fd[1]);
        pid_t id = fork();
        if(id == 0)
        {
                close(fd[0]);
                const char* msg = "I am a child\n";
                int count = 5;
                while(count)
                {
                        if(count == 2)
                        {
                        	sleep(1000);
                        }
                        else
                        {
                        	sleep(1);
                         	write(fd[1],msg,strlen(msg));
                        	count--;
                        }
                }
                exit(0);
        }
        close(fd[1]);
        char buf[64];
        while(1)
        {
                ssize_t s = read(fd[0],buf,sizeof(buf));
                if(s > 0)
                {
                        buf[s] = '\0';
                        printf("child send to father : %s",buf);
                }
                else if(s == 0)
                {
                        printf("read file end !\n");
                        break;
                }
                else
                {
                        perror("read\n");
                        break;
                }
                close(fd[0]);
                break;
        }
        int status;
        waitpid(id,&status,0);
        
        printf("child exit,sign : %d\n",status & 0x7F);
        return 0;
}                      

(5). 对挂起的理解 :
所谓挂起是将进程的PCB由R状态设置成非R,然后将进程的PCB链入等待队列.
进程被唤醒(将进程的PCB由非R状态设置为R状态,将PCB链入运行队列中)

(6). 为什么子进程退出,父进程也退出了呢 ?
如果子进程写端关闭,父进程读端read返回值为0,代表文件结束

(7). 如果打开文件的进程退出了,文件也会被释放掉,所以管道的生命周期是随进程的

(8). 管道是半双工,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
全双工 : 指可以同时(瞬时)进行信号的双向传输(A→B且B→A)
半双工 : 只允许甲方向乙方传送信息,而乙方不能向甲方传送

(9). 匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道

(10). 管道提供流式服务(父/子进程都可以向管道读取/写入任意字节数据)

(11). 管道的大小是多大呢?

ulimit 是一条查看系统资源的命令,可以看到管道的大小

ulimit -a

在这里插入图片描述

// 代码测试管道的大小

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
// child->write  father->read
int main()
{
        int fd[2] = {0};
        if(pipe(fd) < 0)
        {
                perror("pipe!\n");
                return -1;
        }
        pid_t id = fork();
        if(id == 0)
        {
                close(fd[0]);
                char c = 'a';
                int count = 0;
                while(1)
                {
                        write(fd[1],&c,1);
                        count++;
                        printf("%d\n",count);
                }
                exit(0);
        }
        close(fd[1]);
        char buf[64];
        while(1)
        {
                sleep(1000);
                ssize_t s = read(fd[0],buf,sizeof(buf));
                if(s > 0)
                {
                        buf[s] = '\0';
                        printf("child send to father : %s",buf);
                }
                else if(s == 0)
                {
                        printf("read file end !\n");
                        break;
                }
                else
                {
                        perror("read\n");
                        break;
                }
                close(fd[0]);
                break;
        }

        waitpid(id,NULL,0);

        return 0;
}                                                                                                      

在这里插入图片描述

让父进程一直不读取,子进程一直在写入,就可以得出管道的大小为 65536 字节

总结:
(1). 写端不写,读端一直读,读端阻塞
(2). 读端不读,写端一直写,写端阻塞
前两点体现了管道具有互斥与同步机制
(3). 写端关闭,读端读到 0
(4). 读端关闭,写端被操作系统杀死

命名管道

匿名管道可以让具有亲缘关系的进程完成通信,那毫无关系的进程怎么完成通信呢? 我们可以借助命名管道来完成,不同进程可以通过文件名打开同一个文件,就可以让不同进程看到同一份资源,但普通文件是很难做到通信的,实际上这个文件在磁盘上只是一个标识符,没有任何内容

命令创建命名管道

mkfifo fifo

举例 :
一个bash进程下执行如下命令

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

另一个bash进程下执行如下命令

cat < fifo

在这里插入图片描述

可以看到两个进程通过 fifo 命名管道完成了通信,当把读端关闭的时候,写端就无意义了,操作系统将写端杀掉了

函数创建命名管道

int mkfifo(const char *filename,mode_t mode);
创建成功返回0,失败返回-1

下面来写代码完成两个不相关的进程间的通信

// commit.h

#pragma once
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
#define FILE_NAME "myfifo"

// colient.c

#include"comm.h"
int main()
{
        int fd = open(FILE_NAME,O_WRONLY);
        if(fd < 0)
        {
                perror("open\n");
                return 1;
        }
        char buf[128];
        while(1)
        {
                printf("Please Enter# :\n");
                ssize_t s = read(0,buf,sizeof(buf) - 1);
                if(s > 0)
                {
                        buf[s - 1] = 0;
                        write(fd,buf,strlen(buf));
                }
        }

        close(fd);
        return 0;
}

// server.c

#include "comm.h"
int main()
{
        if(mkfifo(FILE_NAME,0664) < 0)
        {
                perror("mkfifo\n");
                return 1;
        }

        int fd = open(FILE_NAME,O_RDONLY);
        if(fd < 0)
        {
                perror("open\n");
                return 2;
        }
        char msg[128];
        while(1)
        {
                msg[0] = 0;
                ssize_t s = read(fd,msg,sizeof(msg) - 1);
                if(s > 0)
                {
                        msg[s] = 0;
                        printf("%s\n",msg);
                }
                else if(s == 0)
                {
                        printf("client quit!\n");
                        break;
                }
                else
                {
                        printf("read error!\n");
                        break;
                }

        }
        close(fd);
        return 0;
}				

// Makefile

.PHONY:all
all:client server

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

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

.PHONY:clean
clean:
        rm -f client server

在这里插入图片描述

命名管道相关补充 :

(1). 我们让服务端不再读取,当我们的客户端在写入的时候,我们的管道文件的大小并没有发生变化,说明数据并没有被刷到磁盘当中,也就意味着双方通信依然在内存当中通信
在这里插入图片描述

(2). 应用场景一 : 我们在客户端输入指令,服务端解析我们的指令
将服务端的代码用进程程序替换函数 execlp 修改一下即可

// server.c

if(s > 0)
{
      msg[s] = 0;
      printf("%s\n",msg);
      if(fork() == 0)
      {
           execlp(msg,msg,NULL);
           exit(1);
      }
      waitpid(-1,NULL,0);
}

应用场景二 : 客户端给服务端派发计算任务
// server.c

if (s > 0)
{
    msg[s] = 0;
    printf("%s\n", msg);
    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* data1 = strtok(msg, "+-*/%");
    char* data2 = strtok(NULL, "+-*/%");

    int x = atoi(data1);
    int y = atoi(data2);
    int z;
    switch (flag)
    {
    case 0:
        z = x + y;
        break;
    case 1:
        z = x - y;
        break;
    case 2:
        z = x * y;
        break;
    case 3:
        z = x / y;
        break;
    case 4:
        z = x % y;
        break;
    }
    printf("%d\n", z);
}

(3). 进程间通信的意义 : 让多个进程进行协同完成某种事情,如执行命令,发送字符串,完成计算任务等等

(4). 命令行中使用的 | 是匿名管道,因为 | 两侧的进程都是由共同的bash进程创建的

三.system V 共享内存(shm)

管道通信的本质是基于文件的,OS没有做过多的设计工作,而system V进程间通信是OS特地设计的通信方式,system V进程间通信有3种 : 共享内存,消息队列,信号量,前两种以传送数据为目的,最后一种是为了保证进程的同步与互斥

共享内存,顾名思义就是允许两个不相关的进程访问同一个逻辑内存,共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常为同一段物理内存。进程可以将同一段物理内存连接到他们自己的地址空间中,所有的进程都可以访问共享内存中的地址。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

共享内存是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据

在这里插入图片描述

共享内存建立的过程
(1). 申请共享内存
(2). 将共享内存关联到地址空间(修改页表,建立映射关系)
(3). 去关联共享内存(修改页表,取消映射关系)
(4). 释放共享内存(内存归还给系统)

进程通过共享内存实现通信的相关函数

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

key_t ftok(const char *pathname, int proj_id);
功能 : 由文件路径和proj_id生成的用来唯一标识系统的shm资源
返回值 : 成功返回一个 key_t ,失败返回-1
#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
key : 由ftok生成的key标识,标识系统的唯一shm
size : 创建的共享内存的大小,要设置为 PAGE_SIZE 的整数倍
shmflg : 创建共享内存时的选项
IPC_CREAT : 如果共享内存存在,直接返回共享内存,不存在,创建(调用成功的情况下,一定会获得一个shm,但无法确定是否为全新的shm)
IPC_EXCL : 单独使用无意义
IPC_CREAT|IPC_EXCL : 如果共享内存存在,出错返回,不存在则创建,即调用成功,一定会获得一个全新的shm
返回值: 成功时返回一个新建或已经存在的的共享内存标识符,取决于shmflg的参数。失败返回-1并设置错误码

PAGE_SIZE : 4096 字节(4KB)(一页数据),在操作系统层面上,进行内存申请/释放,尤其是和外设进行IO的时候,内存并不是按字节去操作的,而是按页框(页帧)来操作的

shmget()中的size设置为4096字节,OS分配一页空间,设置为4097字节,OS分配两页空间(虽然 ipcs -m bytes为4097字节)

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

void *shmat(int shmid, const void *shmaddr, int shmflg);
功能 : 建立共享内存和进程地址空间的映射关系 ,将共享内存映射到进程地址空间的共享区
shmid : 共享内存id
shmaddr : 共享内存映射到进程地址空间的起始地址,绝大多数情况下,设置为NULL,因为自己没有办法知道具体映射到了哪里(映射到共享区),一般由操作系统去设置
shmflg : 设置为0默认为读写共享内存
返回值 : 返回共享内存映射到进程地址空间中的起始地址,出错返回(void*)-1
#include <sys/types.h>
#include <sys/shm.h>

int shmdt(const void *shmaddr);
功能 : 取消共享内存和进程地址空间的关联
shmaddr : 由shmat所返回的指针
返回值 : 成功返回0,失败返回-1
#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid : 共享内存id 
cmd : 可选项
IPC_RMID : 删除共享内存的选项
shmid_ds : 指向 shmid_ds 结构体的指针
返回值 : 成功返回0,出错返回-1

几点补充

(1). 系统中可能存在多个共享内存,OS对共享内存进行管理依然遵循先描述再组织,建立 struct shmid_ds 结构体后再组织,内核的数据结构在文章下面

(2). ipcs 查看消息队列/共享内存/信号量
ipcs -m 查看共享内存
ulimit -a(-q/-m可以改大小)
在这里插入图片描述

(3). 进程已经退出,但是曾经创建的共享内存还存在,说明共享内存的周期是随内核的,进程不主动删除或者用命令删除,共享内存一直存在直到关机重启,且IPC资源一定是由内核提供并维护的

(4). 使用 ipcrm -m shmid 命令删除共享内存
使用 shmctl 删除共享内存

(5). 怎么保证两个进程通信时映射的物理内存是同一个呢?

共享内存本身在建立的时候,会有对应的key值,key值会被写入操作系统内部,作为共享内存在系统层面上的唯一标识符,只要通信双方拿到同一key值,就能访问同一物理内存

(6). 创建共享内存的步骤(消息队列和信号量同理)
(1). 调用shmget()函数申请一块物理内存, 创建 struct shmid_ds 内核数据结构,将 key 值填入 struct ipc_perm 中的 key 中
(2). 为其分配一个 shmid 编号(struct ipc_perm* arr[]的下标),将 struct ipc_perm 的地址填入数组下标对应的内容中
(3). 当我们通过 shmid 编号去找共享内存时,通过全局的 struct ipc_ids 结构体找到 struct ipc_perm* arr[]数组,找到数组下标为shmid中的 struct ipc_perm 的地址,* (struct shmid_ds*)p 就可以访问struct shmid_ds 结构体(C++中的切片技术)

在这里插入图片描述

在这里插入图片描述

shm_segsz : 共享内存大小
shm_atime : 最近 attach 时间
shm_dtime : 最近 detach 时间
shm_ctime : 最近 change 时间
shm_cpid :  共享内存创建者的pid
shm_lpid :  最后一个操作共享内存的进程pid
shm_nattch : 当前关联的进程数量 

(7). 通过如下代码看一下 nattch 的变化

// comm.h

#ifndef _COMM_H
#define _COMM_H

#define PATHNAME "/home/luanyiping/test/10_28/shm"
#define PROJ_ID 0x66
#define SIZE 4096

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


#endif

// server.c

#include"comm.h"
int main()
{
        key_t k = ftok(PATHNAME,PROJ_ID);
        if(k < 0)
        {
                perror("ftok\n");
                return 1;
        }

        printf("k : %x\n",k);

        int shmid = shmget(k,SIZE,IPC_CREAT|IPC_EXCL|0600);
        if(shmid < 0)
        {
                perror("shmget\n");
                return 2;
        }

        printf("shmid : %d\n",shmid);


        printf("attach begin!\n");
        sleep(5);
        char* mem = shmat(shmid,NULL,0);
        sleep(5);
        printf("attach end!\n");


        printf("dettach begin!\n");
        sleep(5);
        shmdt(mem);
        printf("dettach end!\n");
        sleep(5);

        shmctl(shmid,IPC_RMID,NULL);

        return 0;
}

运行如下监控脚本,可以看到共享内存链接数由0变为1,再由1变为0,最后删掉共享内存

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

(8). 使用共享内存完成进程间通信代码

// server.c

#include"comm.h"
int main()
{
        key_t k = ftok(PATHNAME,PROJ_ID);
        if(k < 0)
        {
                perror("ftok\n");
                return 1;
        }


        int shmid = shmget(k,SIZE,IPC_CREAT|IPC_EXCL|0600);
        if(shmid < 0)
        {
                perror("shmget\n");
                return 2;
        }


        char* mem = shmat(shmid,NULL,0);
        while(1)
        {
                printf("%s\n",mem);
                sleep(1);
        }

        shmdt(mem);


        shmctl(shmid,IPC_RMID,NULL);

        return 0;
}

// client.c

#include"comm.h"
int main()
{
        key_t k = ftok(PATHNAME,PROJ_ID);
        if(k < 0)
        {
                perror("ftok\n");
                return 1;
        }

        int shmid = shmget(k,SIZE,IPC_CREAT);
        if(shmid < 0)
        {
                perror("shmget\n");
                return 2;
        }

        char* mem = shmat(shmid,NULL,0);
        int i = 0;
        while(1)
        {
                mem[i] = 'A' + i;
                sleep(1);
                i++;
                mem[i] = '\0';
        }

        shmdt(mem);
}                            

(9).

1). 读写共享内存时,并没有使用系统调用接口,因此和管道通信相比拷贝次数少,速度快
2). 不提供任何保护机制(不存在互斥和同步)

将 server 端的 sleep(1) 去掉,让 server 端一直读,观察运行结果我们发现 client 端在写入时,server 端仍然在读,会造成数据混乱(即不存在互斥)

// server端一直在读取
while(1)
{
	printf("%s\n",mem);
}

四.system V消息队列(msg)

消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法
每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值

消息队列的节点可以简单的理解为由两部分组成,一部分是类型,另一部分是传递的信息,用链表的方式将节点连接起来,刚开始的时候,消息队列为空,两个进程都可以通过某种方式看到同一个队列

消息队列相关函数

// 创建消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);
key : 某个消息队列的名字
msgflg : 由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值 : 成功返回一个非负整数,即该消息队列的标识码,失败返回-1
// 控制消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds *buf);
msqid : 由msgget函数返回的消息队列标志码
cmd : 将要采取的动作(3个可取值)
返回值 : 成功返回0,失败返回-1
// 向消息队列中发送数据
#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 : 由msgget函数返回的消息队列标志码
msgp : 指针指向要发送的数据
struct msgbuf {
               long mtype;       /* message type, must be > 0 */
               char mtext[1];    /* message data */
           };
msgsz : msgp所指向的消息长度,不含保存消息类型的 long int 类型
msgflg : 控制当前消息队列满或达到系统上限时发生的事情
msgflg = IPC_NOWATT 表示队列满不等待,返回 EAGIN 错误
返回值 : 成功0,失败-1
// 获取消息队列的数据块
#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);
msgtyp=0 返回队列第一条消息
msgtyp > 0 返回队列第一条类型等于msgtyp的信息
msgtyp < 0 返回队列第一条类型小于等于msgtyp绝对值的消息,并且是满足条件的消息类型最小的信息
msgflg=IPC_NOWATT 队列没有可读信息不等待,返回ENOMSG错误
msgflg=MSG_NOERROR 消息大小超过 msgsz 时被截断
msgtyp > 0 且 msgflg=MSG_EXCEPT 接受类型不等于 msgtyp 的第一条信息

消息队列的内核数据结构 struct msqid_ds
在这里插入图片描述

msg_first : 指向队列的头
msg_last :  指向队列的尾
msg_stime : 最近一次发送数据的时间
msg_rtime : 最近一次获取数据的时间
msg_ctime : 最近一次改变的时间
msg_cbytes : 消息队列的大小
msg_qnum : 消息队列中消息的数量

补充 :
我们发现共享内存,消息队列,信号量的内核数据结构第一个字段都是 struct ipc_perm

进程间通信

msg_snd.cc

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

using namespace std;

struct l_msgbuf{
	long mtype;
	int v;
};

int main()
{
	int msgid = msgget(0x5000,0640|IPC_CREAT);
	if(-1 == msgid)
	{
		perror("msgget");
		return -1;
	}

	l_msgbuf msg;
	msg.mtype = 1;
	msg.v = 100;

	int ret = msgsnd(msgid,&msg,sizeof(msg.v),IPC_NOWAIT);
	if(-1 == ret)
	{
		perror("msgsnd");
		return -1;
	}


	return 0;

}

msg_rcv.cc

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

using namespace std;


typedef struct s_msgbuf
{
	long mtype;
	int v;
} l_msgbuf;

int main()
{
	int msgid = msgget(0x5000,0640|IPC_CREAT);
	if(-1 == msgid)
	{
		perror("msgget");
		return -1;
	}

	l_msgbuf msg;

	int ret = msgrcv(msgid,&msg,sizeof(msg.v),1,IPC_NOWAIT);
	if(-1 == ret)
	{
		perror("msgrcv");
		return -1;
	}
	
	printf("msg type : %d,msg v : %d\n",msg.mtype,msg.v);

	return 0;

}

五. 信号量(sem)概念

信号量本质上是一个计数器,它是一个IPC对象,内部有相应的数据结构来维护,一般最常用的是二值信号量,用于进程间同步与互斥

标准 : POSIX,system V

信号量内核数据结构

在这里插入图片描述

截止到现在,进程间通信我们学了匿名管道,命名管道,共享内存,消息队列,本质都是让不同的进程看到同一份资源,一旦开始通信后,就会出现一个问题,两个进程有可能一个正在写入,另一个就来读取了,会导致数据混乱的问题,为了解决这个问题,我们需要加锁或实现同步和互斥,加锁会导致效率的降低

临界资源 : 被多个进程共享的资源
临界区 : 访问临界资源的代码

(1). 信号量的使用主要是用来保护共享资源,使得资源在一个时刻只有一个进程(线程)所拥有。
(2). 信号量本质是一个计数器,用来描述临界资源中资源数目
(3). 信号量分为二元信号量,多元信号量;二元信号量将临界资源看作一个,实现了互斥的功能

举例如下 :
进程A和进程B竞争式的使用临界资源,当进程A想要写入共享内存,申请信号量,将信号量- -,开始写入共享内存,这时进程B就会被挂起,只有当进程A写入完毕后,释放信号量,信号量++,进程B才会被唤醒,进行读取共享内存,从而实现互斥
在这里插入图片描述

补充 :

(1). 原子性 : 一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行(一个进程访问共享内存,要么没有访问,要么访问完毕了)

(2). 由上面所说我们可以使用信号量来保护临界资源,但两个进程也要共享信号量,信号量也是临界资源,哪该怎么保护信号量呢? 实际上我们的 PV 操作必须保证原子性

(3). IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核

Linux信号量

  • 19
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值