【Linux操作系统】进程间通信

1. 进程间通信介绍

进程之间会出现协同工作的场景,一个进程需要把自己的数据交付给另一个进程,让其处理,这就产生了进程间通信,因此操作系统需要来设计通信方式

进程之间是具有独立性的,一个进程无法看到另一个进程的资源,因此交互数据会比较麻烦,两个进程要想相互通信,就得先看到一份公共的资源,也就是一段内存,这段内存属于操作系统

进程间通信的本质:由操作系统参与,提供一份所有通信进程能看到的公共资源,这可能以文件方式提供,可以能是队列的方式,也可能就是原始的内存块,这也就产生了多种通信方式

1.1 进程间通信的目的

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

1.2 进程间通信的发展

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

1.3 进程间通信分类

  • 管道
    • 匿名管道pipe
    • 命名管道
  • System V IPC
    • System V 消息队列
    • System V 共享内存
    • System V 信号量
  • POSIX IPC
    • 消息队列
    • 共享内存
    • 信号量
    • 互斥量
    • 条件变量
    • 读写锁

2. 管道

2.1 匿名管道

基于文件的通信方式——管道

管道可以理解为文件的内核缓冲区,它为父子进程所共享的一块内存

在这里插入图片描述

匿名管道特点:

  • 是一个只能单向通信的通信管道
  • 管道是面向字节流的
  • 仅限于父子进程之间通信
  • 管道自带同步机制,原子性写入
  • 管道的生命周期是随进程的

匿名管道通信实例

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

int main()
{
    int pipefd[2] = {0};
    if (pipe(pipefd) != 0) {
        perror("pipe error!");
        return 1;
    }

    // 0:对应读取端    1:对应写入端
    printf("pipefd[0]: %d\n", pipefd[0]);
    printf("pipefd[1]: %d\n", pipefd[1]);
    
    // 让父进程进行读取,子进程进行写入
    if (fork() == 0) {
        // 子进程
        close(pipefd[0]);

        const char *msg = "hello\n";
        while (1) {
            // pipe只要有缓冲区就会一直写入
            write(pipefd[1], msg, strlen(msg)); // 这里不需要+1,\n是C语言级别的东西
            sleep(1);
        }

        exit(0);
    }

    // 父进程
    close(pipefd[1]);
    while (1) {
        // 没有让父进程sleep,写的快读的慢
        char buffer[64] = {0};
        // 只要有数据就可以一直读
        ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1); // 如果read的返回值是0,意味着子进程关闭文件描述符
        if (s == 0) {
            break;
        } else if (s > 0) {
            buffer[s] = 0;
            printf("%s", buffer);
        } else {
            break;
        }
    }

    return 0;
}

// int pipe(int pipefd[2]);
// pipefd[2]是一个输出型参数,可以通过这个参数读取到打开的两个fd

验证管道是由大小的

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

int main()
{
    int pipefd[2] = {0};
    if (pipe(pipefd) != 0) {
        perror("pipe error!");
        return 1;
    }
    
    // 让父进程进行读取,子进程进行写入
    if (fork() == 0) {
        // 子进程
        close(pipefd[0]);

        const char *msg = "hello\n";
        int count = 0;
        while (1) {
            // pipe只要有缓冲区就会一直写入
            write(pipefd[1], "a", 1);
            count++;
            printf("count: %d\n", count);
        }

        exit(0);
    }

    // 父进程
    close(pipefd[1]);
    while (1) {
		// 父进程不进行读取   
    }

    return 0;
}

在这里插入图片描述

write写入65535个字节即64KB后就不再写了,说明管道是由大小的,会等待read来读,不继续写的本质就是等待对方来读

父进程只读取少量的字节,子进程并不会继续进行写入

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

int main()
{
    int pipefd[2] = {0};
    if (pipe(pipefd) != 0) {
        perror("pipe error!");
        return 1;
    }
    
    // 让父进程进行读取,子进程进行写入
    if (fork() == 0) {
        // 子进程
        close(pipefd[0]);

        const char *msg = "hello\n";
        int count = 0;
        while (1) {
            // pipe只要有缓冲区就会一直写入
            write(pipefd[1], "a", 1);
            count++;
            printf("count: %d\n", count);
        }

        exit(0);
    }

    // 父进程
    close(pipefd[1]);
    while (1) {
        sleep(5);
        char c[64] = {0};
        read(pipefd[0], c, 64);
        printf("%s\n", c);
    }
    return 0;
}

在这里插入图片描述

原因:需要一次性读走4KB的数据,才会继续进行写入

在这里插入图片描述

  • 当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性
  • 当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性

4种情况

  • 读端不读或者读的慢,写端要等待读端

  • 读端关闭,写端收到SIGPIPE信号直到终止

    这本质是在浪费系统资源,操作系统会终止写入进程,OS给目标进程发送信号SIGPIPE

  • 写端不写或者写的慢,读端要等待写端

  • 写端关闭,读端读完pipe内部的数据然后再读,会读到0,表明读到文件结尾

2.2 命名管道

使用命名管道,双方通信只需要按照文件操作即可

因为命名管道是基于字节流的,所以实际上在信息传递的时候,是需要通信双方制定协议的

命令行上直接使用

创建管道文件

mkfifo fifo

在这里插入图片描述

这个管道是有名字的——fifo,使用它即可在命令行上进行简单的字符串通信

在这里插入图片描述

使用代码操作管道

server.c

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

#define MY_FIFO "./fifo"

int main()
{
    umask(0); // 将系统的umask清零,这样下面我们创建管道时的文件权限就不会受系统的umask影响
    if (mkfifo(MY_FIFO, 0666) < 0) {
        perror("mkfifo");
        return 1;
    }
    
    int fd = open(MY_FIFO, O_RDONLY);
    if (fd < 0) {
        perror("open");
        return 2;
    }

    while (1) {
        char buffer[64] = {0};
        ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
        if (s > 0) {
            buffer[s] = 0;
            if (strcmp(buffer, "show") == 0) {
                if (fork() == 0) {
                    execl("/usr/bin/ls", "ls", "-l", NULL);
                    exit(1);
                }

                waitpid(-1, NULL, 0);
            } else if (strcmp(buffer, "run") == 0) {
                if (fork() == 0) {
                    execl("/usr/bin/sl", "sl", NULL);
                    exit(1);
                }

                waitpid(-1, NULL, 0);
            } else {
                printf("client say#%s\n", buffer);
            }
        } else if (s == 0) {
            printf("client quit\n");
            break;
        } else {
            perror("read");
        }
    }
    
    close(fd);

    return 0;
}

client.c

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

#define MY_FIFO "./fifo"


int main()
{
    int fd = open(MY_FIFO, O_WRONLY);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    while (1) {
        printf("请输入#");
        fflush(stdout);
        char buffer[64] = {0};
        ssize_t s = read(0, buffer, sizeof(buffer) - 1);
        if(s > 0) {
            buffer[s - 1] = 0;
            printf("%s\n", buffer);
            write(fd, buffer, strlen(buffer));
        }
    }
    close(fd);
    return 0;
}

makefile

.PHONY:all
all:server client

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

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

.PHONYE:clean
clean:
	rm -f server client fifo

在这里插入图片描述

在这里插入图片描述

命名管道的数据不会刷新到磁盘,这样可以提高效率,刷新到磁盘需要时间

在这里插入图片描述

为什么pipe叫做匿名管道,fifo叫做命名管道

  • 匿名管道的文件没有名字,它是通过父子继承的方式来看到同一份资源,不需要名字来标识同一个资源
  • fifo一定要有名字,为了保证不同的进程看到同一个文件,所以必须要有名字

3. System V

同样是基于文件的通信方式,在OS层面专门为进程间通信设计的一个方案

操作系统不相信任何用户,给用户提供功能的时候,采用系统调用

System V进程间通信,一定会存在专门用来通信的接口(system call)

在同一主机内的进程间通信方法:System V方案

进程间通信的本质:让不同的进程看到同一份资源,因而System V有三种方案

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

3.1 共享内存

如何让不同进程看到同一份资源

  • 通过某种调用,在内存中创建一份内存空间
  • 通过某种调用,让参与通信的多个进程“挂接”到这份新开辟的内存空间上
  • 去关联
  • 释放共享内存

操作系统中可能会存在多个进程,这样就会有很多不同的共享内存,因此OS就必须对这些共享内存进行管理,使不同的共享内存来进行进程间通信,管理的方式与之前类似:先描述,在组织;这就带来了一个问题,如何保证两个或多个进程看到的是同一份共享内存呢?共享内存一定会有标识唯一性的ID,方便让不同的进程识别同一个共享内存资源,而这个ID一定是存在于描述共享内存的数据结构中。这个唯一的标识符,就是用来进行进程间通信的,而这个ID也是需要由用户自己设定的,这样才能达到通信的目的

接口介绍

  • 创建共享内存

    在这里插入图片描述

  • 控制共享内存

    在这里插入图片描述

  • 装载/卸载共享内存

    在这里插入图片描述

① shmget

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

key_t key:这个key就是会设置进内核中的关于shm在内核中的数据结构中

key如果由我们自己指定会很麻烦,且不能保证都记住,因此使用ftok函数进行生成

在这里插入图片描述

  • const char *pathname :自定义路径名,如"/tmp/Xxx"
  • int proj_id:自定义项目ID,如:0x6666

这样的话只要我们形成key的算法+原始数据是一样的,形成的key就是一样的,ID唯一

size_t size:建议是4KB的整数倍

int shmflg:标志位

在这里插入图片描述

  • IPC_CREAT:如果单独使用这个或者标志位为0,则创建一个共享内存,如果创建的共享内存以及存在,则直接返回当前已经存在的共享内存
  • IPC_EXCL:单独使用没有意义,IPC_CREAT | IPC_EXCL:如果不存在这个共享内存则直接创建,如果已经存在则报错
  • 还可以加上要创建的这个共享内存的权限:IPC_CREAT | IPC_EXCL | 0666,对应于perms

comm.h

#pragma once

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

#define PATH_NAME "./"
#define PROJ_ID 0x6666
#define SIZE 4096

server.c

#include "comm.h"

int main()
{
    key_t key = ftok(PATH_NAME, PROJ_ID);
    if (key < 0) {
        perror("ftok");
        return 1;
    }

    int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid < 0) {
        perror("shmget");
        return 2;
    }

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

    return 0;
}

在这里插入图片描述

shmid为-1就表示创建失败了,即该共享内存已经存在

ipcs -m:查看系统存在的共享内存

在这里插入图片描述

./server已经执行结束了,但是发现共享内存是一直存在的,并没有被释放,这表明System V的IPC资源生命周期是随内核的,只能程序员手动释放或者操作系统手动重启

ipcrm -m [shmid]:释放共享内存

在这里插入图片描述

② shmctl

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

int shmid:共享内存的shmid

int cmd:对共享内存的操作

在这里插入图片描述

  • IPC_RMID:释放该共享内存——shmctl(shmid, IPC_RMID, NULL)
  • IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值
  • IPC_RMID:在进程有1足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值

struct shmid_ds *buf:是一块结构体,描述的是shmid的数据结构

key vs shmid

  • key:只是在系统层面上来进行唯一性标识的,不能用来管理共享内存
  • shmid:是操作系统给用户返回的id,用来在用户层对共享内存进行管理
③ shmat

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

DESCRIPTION
shmat() attaches the System V shared memory segment identified by shmid to the address space of the calling process.

RETURN VALUE
On success shmat() returns the address of the attached shared memory segment; on error (void *) -1 is returned, and errno is set to indicate the cause of the error.

这里的地址也是虚拟地址,跟malloc出来的一样

④ shmdt

int shmdt(const void *shmaddr);

并不是释放共享内存,而是取消当前进程与共享内存之间的关系

DESCRIPTION

shmdt() detaches the shared memory segment located at the address specified by shmaddr from the address space of the calling process.

RETURN VALUE

On success shmdt() returns 0; on error -1 is returned, and errno is set to indicate the cause of the error.


共享内存的整个生命周期

server.c

#include "comm.h"

int main()
{
    key_t key = ftok(PATH_NAME, PROJ_ID);
    if (key < 0) {
        perror("ftok");
        return 1;
    }

    int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid < 0) {
        perror("shmget");
        return 2;
    }

    printf("key: %#x, shmid: %d\n", key, shmid);
    sleep(5);
    
    char *mem = (char*)shmat(shmid, NULL, 0);
    printf("attach shm success\n");
    sleep(5);

    // 在这里进行通信


    shmdt(mem);
    printf("detach shm success\n");
    sleep(5);

    shmctl(shmid, IPC_RMID, NULL);
    printf("key: %#x, shmid: %d -> shm delete success\n", key, shmid);

    return 0;
}

在这里插入图片描述


使用共享内存进程间通信

server.c

#include "comm.h"

int main()
{
    key_t key = ftok(PATH_NAME, PROJ_ID);
    if (key < 0) {
        perror("ftok");
        return 1;
    }

    int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid < 0) {
        perror("shmget");
        return 2;
    }

    printf("key: %#x, shmid: %d\n", key, shmid);
    // sleep(5);
    
    char *mem = (char*)shmat(shmid, NULL, 0);
    printf("attach shm success\n");
    // sleep(5);

    // 在这里进行通信
    while (1) {
        sleep(1);
        printf("%s\n", mem);
    }

    shmdt(mem);
    printf("detach shm success\n");
    // sleep(5);

    shmctl(shmid, IPC_RMID, NULL);
    printf("key: %#x, shmid: %d -> shm delete success\n", key, shmid);

    return 0;
}

client.c

#include "comm.h"

int main()
{
    key_t key = ftok(PATH_NAME, PROJ_ID);
    if (key < 0) {
        perror("ftok");
        return 1;
    }

    int shmid = shmget(key, SIZE, IPC_CREAT | 0666);
    if (shmid < 0) {
        perror("shmget");
        return 2;
    }

    char *mem = shmat(shmid, NULL, 0);
    printf("client process attach success\n");
    // sleep(5);

    // 在这里进行通信
    char c = 'A';
    while (c <= 'Z') {
        sleep(1);
        mem[c - 'A'] = c;
        c++;
        mem[c - 'A'] = 0;
    }

    shmdt(mem);
    printf("client process deattach success\n");
    // sleep(5);

    // client不需要删除共享内存

    return 0;
}

在这里插入图片描述

使用共享内存写入和读取时没有调用类似于在pipe和fifo中使用的read、write接口,read和write的本质是:将数据从内核拷贝到用户或者从用户拷贝到内核

共享内存无需这样,因为共享内存一旦建立好并映射自己进程的地址空间,该进程就可以直接看到该共享内存,就如同malloc出来一块空间一样,不需要使用任何系统调用接口,因此共享内存是所有进程间通信中速度最快的

server会认为共享内存里放的是一个长字符串,因此可以直接用%s格式化输出

当clien没有写入的时候,server端还是会一直读取共享内存,不需要等待client,所以共享内存其实没有提供任何同步或者互斥机制,需要程序员自行保证数据的安全,特别是在多线程的时候

⑤ 申请共享内存大小的问题

建议申请大小为4096字节的整数倍,因为共享内存在内核中申请的基本单位是页,一页的大小为4KB

如果我们申请4097个字节,那么实际共享内存的大小为4096Byte * 2,这样可能会带来麻烦,虽然显示的是4097,但操作系统上还是8192

在这里插入图片描述

⑥ struct shmid_ds

在这里插入图片描述

在内核中,所有的ipc资源都是通过数组组织起来的,所有的System V标准的IPC资源,XXXid_ds结构体的第一个成员都是ipc_perm

3.2 信号量

信号量

匿名管道/命名管道、共享内存、消息队列都是以传输数据为目的的

信号量不是以传输数据为目的,通过共享“资源”的方式,来达到多个进程的同步和互斥的目的

信号量的本质是一个计数器,类似于int count,是衡量临界资源中资源数目的

信号量可能被多个线程访问,因此它就是临界资源;因为他保证临界资源的安全性,所以其P()、V()操作都是具有原子性的

什么是临界资源

凡是被多个执行流同时能够访问的资源就是临界资源

进程间通信的时候,管道、共享内存、消息队列等都是临界资源

凡是要进程间通信,必定要引入被多个进程看到的资源(通信需要),同时也就引入了一个新的问题:临界资源的问题

什么是临界区

进程的代码很多,其中用来访问临界资源的代码就叫做临界区

什么是原子性

一件事情要么不做,要做就做完,没有中间态,就叫做原子性

什么是互斥

在任意一个时刻,只能允许一个执行流进入临界资源,执行它自己的临界区

什么是同步

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Ricky_0528

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

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

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

打赏作者

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

抵扣说明:

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

余额充值