Linux学习日记16——进程间通讯

学习视频链接

黑马程序员-Linux系统编程_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1KE411q7ee?p=100

目录

一、IPC 方法

1.1 进程间通信

1.2 常用的进程间通信方式

二、管道

2.1 Linux下创建管道

2.2 管道的概念

2.3 创建管道

2.4 父子进程通信

2.5 兄弟进程通信

2.6 多个写端一个读端测试

2.7 管道缓冲区大小

2.8 管道的优劣

三、FIFO

3.1 简介

3.2 创建方式

3.3 实现非血缘关系进程间通信

四、共享存储映射

4.1 文件进程间通信

4.2 存储映射 I/O

4.3 mmap 函数

4.4 mmap 注意事项

4.5 父子进程间 mmap 通信

4.6 进程间通信

4.7 匿名映射


一、IPC 方法

1.1 进程间通信

Linux 环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信 (IPC, InterProcess Communication)

1.2 常用的进程间通信方式

在进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者弃用。现今常用的进程间通信方式有:

(1) 管道(使用最简单)
(2) 信号(开销最小)
(3) 共享映射区(无血缘关系)
(4) 本地套接字(最稳定)

二、管道

2.1 Linux下创建管道

1、伪文件

Linux系统所见皆文件,管道是伪文件,伪文件不需要占用磁盘空间

2、创建一个管道

2.2 管道的概念

1、管搭配的特质

管道是一种最基本的 IPC 机制,作用于有血缘关系的进程之间,完成数据传递。调用 pipe 系统函数即可创建一个管道。有如下特质:

(1) 其本质是一个伪文件(实为内核缓冲区)

(2) 由两个文件描述符引用,一个表示读端,一个表示写端

(3) 规定数据从管道的写端流入管道,从读端流出

2、管道的原理

管道实为内核使用环形队列机制,借助内核缓冲区 (4k) 实现

3、管道的局限性

(1) 数据不能进程自己写,自己读

(2) 管道中数据不可反复读取。一旦读走,管道中不再存在

(3) 采用半双工通信方式,数据只能在单方向上流动

(4) 只能在有公共祖先的进程间使用管道

4、常见的管道通信方式

(1) 单工通信

(2) 半双工通信

(3) 全双工通信

2.3 创建管道

1、pipe函数

int pipe(int pipefd[2]);

参数:fd[0] 读端、fd[1] 写端

返回值:成功 0、失败 -1 errno

2、代码原理

 父进程先创建管道

根据父进程再创建一份子进程,子进程同样也是指向这个管道

关闭子进程的写端,关闭父进程的读端

 这样就形成了一个半双工通信

3、代码

4、总结

读管道:

(1) 管道中有数据,read 返回实际读到的字节数

(2) 管道中无数据

① 管道写端被全部关闭,read 返回 0(好像读到文件结尾)

② 写端没有全部被关闭,read 阻塞等待(不久的将来可能有数据递达,此时会让出 cpu)

写管道:

(1) 管道读端全部被关闭,进程异常终止(也可使用捕捉 SIGPIPE 信号,使进程不终止)

(2) 管道读端没有全部关闭

① 管道己满,write 阻塞

② 管道未满,write 将数据写入,并返回实际写入的字节数

测试等待

测试写端关闭

2.4 父子进程通信

1、实现功能

2、命令解释

在 linux 中,竖线是管道符的意思,该符号的作用是连接两个命令,将第一个命令的输出作为第二个命令的输入,语法为 “command 1 | command 2”

在默认的情况下,wc 将计算指定文件的行数、字数,以及字节数

wc 默认从标准输入读数据

3、测试代码

 

这么写父进程经常早于子进程结束,导致要打印的数字出现的比较晚,可以设置让父进程去读,子进程去写 

 

2.5 兄弟进程通信

使用管道实现兄弟进程间通信。兄:Is;弟:wc-I;父:等待回收子进程

 

注意:父进程要关闭管道的读写,因为有 wait 所以父进程阻塞等待两个子进程结束然后回收子进程。

2.6 多个写端一个读端测试

 

 

2.7 管道缓冲区大小

1、查看方式一 

管道文件对应的内核缓冲区大小默认是 4KB

栈只有 8MB,系统还有东西要放,所以要用 malloc 在堆上开辟内存

2、查看方式二

引入头文件 <unistd.h>.

long fpathconf(int fd, int name);

成功:返回管道的大小

失败:-1,设置 errno

查看管道大小,fd 传入 fd[0] 或 f[1] 都可以,匿名管道不能使用下面的那个函数

 name 中输入这个代表查看管道缓冲区的大小

name 中传入这个表示能向终端输入的最大值

2.8 管道的优劣

1、优点:

简单,相比信号,套接字实现进程间通信,简单很多

2、缺点:

(1) 只能单向通信,双向通信需建立两个管道

(2) 只能用于父子、兄弟进程(有共同祖先)间通信。该问题后来使用 fifo 有名管道解决

三、FIFO

3.1 简介

FIFO 常被称为命名管道,以区分管道 (pipe)。管道 (pipe) 只能用于 “有血缘关系” 的进程间。但通过 FIFO,不相关的进程也能交换数据。

FIFO 是 Linux 基础文件类型中的一种。但 FIFO 文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。各进程可以打开这个文件进行 read/write,实际上是在读写内核通道,这样就实现了进程间通信。

3.2 创建方式

1、命令 

mkfifo 管道名

2、库函数:int mkfifo(const char *pathname,mode_t mode);

成功:0; 失败:-1

一旦使用 mkfifo 创建了一个 FIFO,就可以使用 open 打开它,常见的文件 I/O 函数都可用于 fifo

如:close、read、write、unlink 等。

3、测试

 

3.3 实现非血缘关系进程间通信

1、一个写入端一个读出端

2、两个写入端一个读出端

3、两个读出端一个写入端

usleep(10000):睡眠 10000 微秒  = 10 毫秒

四、共享存储映射

4.1 文件进程间通信

1、使用文件也可以完成 IPC, 理论依据是,fork 后, 父子进程共享文件描述符。也就共享打开的文件。

2、 没有血缘关系的进程可以打开同一个文件进行通信

 

4.2 存储映射 I/O

1、原理

存储映射 I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不使用 read 和 write 函数的情况下,使用地址(指针)完成 I/O 操作。使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap 函数来实现。

4.3 mmap 函数

void *mmap(void *addr size_t length, int prot, int flags, int fd, off_t offset);    创建共享内存映射

1、参数:

addr:指定映射区的首地址。通常传NULL,表示让系统自动分配

length:共享内存映射区的大小(<=文件的实际大小)

prot:共享内存映射区的读写属性。PROT_READ、PROT_WRITE、PROT_READ | PROT_VRITE

flags:标注共享内存的共享属性。MAP_SHARED、MAP_PRIVATE

fd:用于创建共享内存映射区的那个文件的文件描述符

offset:默认0,表示映射文件全部。偏移位置。需是 4k 的整数倍

2、返回值:

成功:映射区的首地址

失败:MAP_FAILED, errno

3、代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

int main(int argc, char *argv[])
{
    char *p = NULL;
    int fd; 
    
    fd = open("testmmap", O_RDWR | O_CREAT | O_TRUNC, 0644);

    if (fd == -1) {
        sys_err("open error");
    }   
    
    // 拓展文件大小的方法
    ftruncate(fd, 20);
    int len = lseek(fd, 0, SEEK_END);

    p = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); 
    if (p == MAP_FAILED) {
        sys_err("mmap error");
    }   

    // 使用p对文件进行读写操作
    strcpy(p, "hello mmap");  // 写操作
    
    printf("1. %s\n", p); 

    int ret = munmap(p, len);
    if (ret == -1) {
        sys_err("mmap error");
    }

    close(fd);

    return 0;
}

 

4、释放映射区

int munmap(void *addr, size_t length);   释放映射区

addr:mmap的返回值

length:大小

4.4 mmap 注意事项

注意事项1

用于创建映射区的大小为 0,实际指定非 0 大小创建映射区,出现 “总线错误” 

总线错误的信号是 7

注意事项2

用于创建映射区的文件大小为 0,实际指定 0 大小创建映射区,出 “无效参数” 错误

注意事项3 

首先拓展文件大小为 20,ftruncate(fd, 20); 需要写权限,才能拓展文件大小,后续只读的时候不能再使用这个函数了

(1) open 只读、mmap 读写。用于创建映射区的文件读写属性为 只读,映射区属性为 读 、写,出 “没有权限” 错误

(2) open 只读、mmap 只读,mmap 不出现错误

出现了段错误,通过 gdb 调试,找到段错误的断点在 strcpy

注意事项4

创建映射区,需要 read 权限。当访问权限指定为 “MAP_SHARED” 时,mmap 的读写权限,应该 <= 文件的 open 权限。出 “没有权限” 错误

open 只写、mmap 只写

 

 

注意事项5

文件描述符 fd,在 mmap 创建映射区完成即可关闭。后续访问文件,用地址访问

 

注意事项6

offset 必须是 4096 的整数倍 (MMU 映射的最小单位是 4K)

注意事项7 

对申请的内存,不能越界访问。因为访问可能是正常的,但内存不受保护.(我们只需要明确正确的写法,错误的写法不需要深究)

注意事项8

munmap 用于释放的地址,必须是 mmap 申请返回的地址

如果必须要做++等操作,则需要定义一个变量 char *temp = p; 再对 temp 进行操作

注意事项9

映射区访问权限为 “私有” MAP_PRIVATE,对内存所做的所有修改,只在内存有效,不会反应到物理磁盘上

注意事项10

映射区访问权限为 “私有” MAP_PRIVATE,只需要 open 文件时,有读权限,用于创建映射区即可。

4.5 父子进程间 mmap 通信

1、流程

父进程先创建映射区。open(O_RDWR) mmap(MAP_SHARED)

指定 MAP_SHARED 权限

fork() 创建子进程

一个进程读,一个进程写

2、代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>

int var = 100;

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

int main(int argc, char *argv[])
{
    int *p; 
    pid_t pid;
    
    int fd; 

    fd = open("temp", O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (fd < 0) {
        sys_err("open error");
    }   
    ftruncate(fd, 4); 

    p = (int *)mmap(NULL, 4, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); 
    if (p == MAP_FAILED) {
        sys_err("mmap error");
    }   
    close(fd);  // 映射区建立完毕,即可关闭文件

    pid = fork();  // 创建子进程
    if (pid == 0) {
        *p = 2000;  // 写共享内存
        var = 1000;
        printf("child, *p = %d, var = %d\n", *p, var);
    }   
    else {
        sleep(1);
        printf("parent, *p = %d, var = %d\n", *p, var);  // 读共享内存
        wait(NULL);

        int ret = munmap(p, 4);  // 释放映射区
        if (ret == -1) {
            sys_err("munmap error");
        }
    }

    return 0;
}

把 p = (int *)mmap(NULL, 4, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);  中的 MAP_SHARED 改为 MAP_PRIVATE

4.6 进程间通信

 1、写进程

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <errno.h>

struct student {
    int id; 
    char name[256];
    int age;
};

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

int main(int argc, char *argv[])
{
    struct student stu = {1, "xiaoming", 18};
    struct student *p; 
    
    int fd; 

    fd = open("temp", O_RDWR | O_CREAT | O_TRUNC, 0664);
    if (fd == -1) {
        sys_err("open error");
    }   

    ftruncate(fd, sizeof(stu));

    p = mmap(NULL, sizeof(stu), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); 
    if (p == MAP_FAILED) {
        sys_err("mmap error");
    }   
    close(fd);

    while (stu.id < 100) {
        memcpy(p, &stu, sizeof(stu));
        stu.id++;
        sleep(1);
    }

    munmap(p, sizeof(stu));

    return 0;
}

2、读进程

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <errno.h>

struct student {
    int id; 
    char name[256];
    int age;
};

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

int main(int argc, char *argv[])
{
    struct student stu = {1, "xiaoming", 18};
    struct student *p; 
    
    int fd; 

    fd = open("temp", O_RDONLY);
    if (fd == -1) {
        sys_err("open error");
    }   

    p = mmap(NULL, sizeof(stu), PROT_READ, MAP_SHARED, fd, 0); 
    if (p == MAP_FAILED) {
        sys_err("mmap error");
    }   
    close(fd);

    while (stu.id < 100) {
        printf("id = %d, name = %s, age = %d\n", p->id, p->name, p->age);
        sleep(1);
    }

    munmap(p, sizeof(stu));

    return 0;
}

3、执行

4.7 匿名映射

只能用于血缘关系进程间通讯

写法一

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>

int var = 100;

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

int main(int argc, char *argv[])
{
    int *p; 
    pid_t pid;

    p = (int *)mmap(NULL, 40, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); 
    if (p == MAP_FAILED) {
        sys_err("mmap error");
    }   

    pid = fork();  // 创建子进程
    if (pid == 0) {
        *p = 2000;  // 写共享内存
        var = 1000;
        printf("child, *p = %d, var = %d\n", *p, var);
    }   
    else {
        sleep(1);
        printf("parent, *p = %d, var = %d\n", *p, var);  // 读共享内存
        wait(NULL); 
    
        int ret = munmap(p, 4);  // 释放映射区
        if (ret == -1) {
            sys_err("munmap error");
        }   
    }

    return 0;
}

写法二

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

herb.dr

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

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

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

打赏作者

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

抵扣说明:

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

余额充值