学习视频链接
黑马程序员-Linux系统编程_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1KE411q7ee?p=100
目录
一、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;
}
写法二