1 思维导图
进程通信分为共享内存、管道通信、消息传递
(1)共享内存:要互斥地访问共享空间;
(2)管道通信:
一个管道只能实现半双工通信;
写满时,不能再写,读空时,不能再读;
没写满,不能读,没读空,不能写。
(3)消息传递
2 进程通信
- 图中我们可以知道什么是进程通信,以及进程通信的低级和高级方式;
- 我们还可以知道为什么要引入进程通信方式,以及它的意义
进程通信就是进程之间的信息交换。
2.1 共享存储
共享一块大家都可以访问的空间,一次只能有一个进程进行读或写操作
两个进程对共享空间的访问必须是互斥的(互斥访问通过操作系统的工具实现)
2.2 管道通信
(1)管道只能采用半双工通信,某一时间段只能单向传输;如果要实现双向同时通信,则要设置两个管道;
(2)各进程要互斥地访问管道;
(3)数据以字符流的方式写入管道,当管道写满时,写进程的write系统调用将被阻塞,等待读进程将数据取走。当写进程将数据全部取走后,管道为空,此时,读进程read系统调用将被阻塞;
(4)如果没有写满,就不允许读。如果没读空,就不允许写;
(5)数据一旦被读出,就从管道中被抛弃,意味着读进程最多只能有一个。
2.3 消息传递
发送信息的进程将消息头写好,接受信息进程根据消息头读取信息或寻找信封是哪一个
3 linux下常见进程通信
管道:简单
信号:开销小
mmap映射:非血缘关系进程间
socket(本地套接字):稳定
3.1 管道
管道:实现原理: 内核借助环形队列机制,使用内核缓冲区实现。
特质
1. 伪文件
2. 管道中的数据只能一次读取。
3. 数据在管道中,只能单向流动。
局限性
1. 自己写,不能自己读。
2. 数据不可以反复读。
3. 半双工通信。
4. 血缘关系进程间可用。
3.1.1 管道的基本用法
pipe函数
// pipe函数: 创建,并打开管道。
int pipe(int fd[2]);
// 参数: fd[0]: 读端。
// fd[1]: 写端。
// 返回值: 成功: 0
// 失败: -1 errno
管道通信原理
创建一个管道,可读可写,再创建子进程,同样可读可写。
关闭父进程从管道的读操作,关闭子进程对管道的操作,即对管道的操作是单向的。
一个管道通信的示例,父进程往管道里写,子进程从管道读,然后打印读取的内容
/*************************************************************************
> File Name: pipe.c
> Author: Winter
> Created Time: 2021年10月14日 星期四 21时18分54秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<error.h>
int main(int argc, char* argv[])
{
int res;
int fd[2];
char* str = "hello pipe\n";
char buff[1024];
res = pipe(fd);
if (res == -1) {
perror("pipe error\n");
exit(1);
}
// 有管道了,父进程写,子进程读
pid_t pid = fork();
if (pid > 0 ){
// 父进程
close(fd[0]); // 关闭读端
write(fd[1], str, strlen(str));
sleep(3);
close(fd[1]);
} else if (pid == 0) {
// 子进程
close(fd[1]); // 关闭写端
res = read(fd[0], buff, sizeof(buff));
printf("child read res = %d\n",res);
write(STDOUT_FILENO, buff, res);
close(fd[0]);
} else {
perror("fork error");
exit(1);
}
return 0;
}
执行
3.1.2 管道读写行为
读管道
读管道:
1. 管道有数据,read返回实际读到的字节数。
2. 管道无数据:
1)无写端,read返回0 (类似读到文件尾)
2)有写端,read阻塞等待。
写管道
1. 无读端, 异常终止。 (SIGPIPE导致的)
2. 有读端:
1) 管道已满, 阻塞等待
2) 管道未满, 返回写出的字节个数。
3.1.3 父子进程通信练习
使用管道实现父子进程间通信,完成:ls | wc -l 假定父进程实现ls,子进程实现wc ls命令正常会将结果集写到stdout,但现在会写入管道写端wc -l命令正常应该从stdin读取数据,但此时会从管道的读端读。
/*************************************************************************
> File Name: pipe.c
> Author: Winter
> Created Time: 2021年10月14日 星期四 21时18分54秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<error.h>
#include<fcntl.h>
void sys_err(const char* str) {
perror(str);
exit(1);
}
int main(int argc, char* argv[])
{
pid_t pid; // 进程用
int res = 0; // 返回结果
int fd[2]; // 管道读写
// 创建管道
if (pipe(fd) == -1) {
sys_err("pipe error\n");
}
pid = fork();
if (pid < 0) {
sys_err("fork error\n");
} else if (pid > 0) {
// 父进程执行
close(fd[1]); // 关闭读端
dup2(fd[0], STDIN_FILENO); // 将 参数2 重定向 参数1
execlp("wc", "wc", "-l", NULL);
sys_err("execlp wc error\n");
} else {
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp("ls", "ls", NULL);
sys_err("execlp error\n");
}
return 0;
}
执行
3.1.4 兄弟间进程通信
练习题:兄弟进程间通信
兄:ls
弟:wc -l
父:等待回收子进程
要求,使用循环创建N个子进程模型创建兄弟进程,使用循环因子i标识,注意管道读写行为
/*************************************************************************
> File Name: pipe.c
> Author: Winter
> Created Time: 2021年10月14日 星期四 21时18分54秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<error.h>
#include<sys/wait.h>
#include<fcntl.h>
void sys_err(const char* str) {
perror(str);
exit(1);
}
int main(int argc, char* argv[])
{
pid_t pid; // 进程用
int res = 0; // 返回结果
int fd[2]; // 管道读写
int i;
// 创建管道
if (pipe(fd) == -1) {
sys_err("pipe error\n");
}
// 循环创建2个子进程
for (i = 0; i < 2; i++) {
pid = fork(); // 创建进程
if (pid < 0) {
sys_err("fork error\n");
}
// 子进程退出
if (pid == 0) {
break;
}
}
// 用i来标识子进程
// 父进程
if (i == 2) {
// 父进程不使用进程,关掉
close(fd[0]);
close(fd[1]);
wait(NULL);
wait(NULL);
} else if (i == 0) {
// 兄进程
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp("ls", "ls", NULL);
sys_err("execlp error\n");
} else if (i == 1) {
// 弟进程
close(fd[1]); // 关闭读端
dup2(fd[0], STDIN_FILENO); // 将 参数2 重定向 参数1
execlp("wc", "wc", "-l", NULL);
sys_err("execlp wc error\n");
}
return 0;
}
执行
测试:
是否允许,一个pipe有一个写端多个读端 可以
是否允许,一个pipe有多个写端一个读端 可以
管道默认大小4096
3.1.5 多个写端操作管道
允许pipe中有一个写端,多个读端。
下面是一个父进程读,俩子进程写的例子,也就是一个读端多个写端。需要调控写入顺序才行。
/*************************************************************************
> File Name: pipe.c
> Author: Winter
> Created Time: 2021年10月14日 星期四 21时18分54秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<error.h>
#include<sys/wait.h>
#include<fcntl.h>
void sys_err(const char* str) {
perror(str);
exit(1);
}
int main(int argc, char* argv[])
{
pid_t pid; // 进程用
int res = 0; // 返回结果
int fd[2]; // 管道读写
int i;
char* buff[1024];
// 创建管道
if (pipe(fd) == -1) {
sys_err("pipe error\n");
}
// 循环创建2个子进程
for (i = 0; i < 2; i++) {
pid = fork(); // 创建进程
if (pid < 0) {
sys_err("fork error\n");
}
// 子进程退出
if (pid == 0) {
break;
}
}
// 用i来标识子进程
// 父进程
if (i == 2) {
// 父进程关闭写端,保留读端
close(fd[1]);
sleep(1);
int n = 0;
n = read(fd[0], buff, 1024);
write(STDOUT_FILENO, buff, n);
for (int k = 0; k < 2; k++) {
wait(NULL);
}
} else if (i == 0) {
// 兄进程
close(fd[0]); // 关闭读端
write(fd[1], "0 hello\n", strlen("0 hello\n"));
} else if (i == 1) {
// 弟进程
close(fd[0]); // 关闭读端
write(fd[1], "1 hello\n", strlen("1 hello\n"));
}
return 0;
}
执行
3.2 fifo
有名管道
优点:简单,相比信号,套接字实现进程通信,简单很多。
缺点:
1.只能单向通信,双向通信需建立两个管道
2.只能用于有血缘关系(父子,兄弟)的进程间通信。该问题后来使用fifo命名管道解决
fifo管道:可以用于无血缘关系的进程间通信。
命名管道: mkfifo
无血缘关系进程间通信:
读端,open fifo O_RDONLY
写端,open fifo O_WRONLY
fifo操作起来像文件
下面的代码创建一个fifo:
/*************************************************************************
> File Name: testfifl.c
> Author: Winter
> Created Time: 2021年10月17日 星期日 21时02分59秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/stat.h>
int main(int argc, char* argv[])
{
int res = mkfifo("myTestFifo", 0664);
if (res == -1) {
perror("mkfifo error\n");
}
return 0;
}
执行
如图,管道就通过程序创建出来了。
3.2.1 fifo实现非血缘关系进程间通信
下面这个例子,一个写fifo,一个读fifo,操作起来就像文件一样的:
fifo_w.c
/**************************************************************************
> File Name: fifo_w.c
> Author: Winter
> Created Time: 2021年10月17日 星期日 21时14分49秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
int main(int argc, char* argv[])
{
int fd;
char buff[4096];
if (argc < 2) {
printf("Enter like this: ./a.out fifoname\n");
}
fd = open(argv[1], O_WRONLY);
if (fd < 0) {
perror("fifo error\n");
exit(1);
}
int i = 0;
while (1) {
sprintf(buff, "hello %d\n",i++);
write(fd, buff, strlen(buff));
sleep(1);
}
close(fd);
return 0;
}
fifo_r.c
/*************************************************************************
> File Name: fifo_r.c
> Author: Winter
> Created Time: 2021年10月17日 星期日 21时19分15秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
int main(int argc, char* argv[])
{
int fd, len;
char buff[4096];
if (argc < 2) {
printf("Enter like this: ./a.out fifoname\n");
}
fd = open(argv[1], O_RDONLY);
if (fd < 0) {
perror("fifo error\n");
exit(1);
}
while (1) {
len = read(fd, buff, sizeof(buff));
write(STDOUT_FILENO, buff, len);
sleep(1);
}
close(fd);
return 0;
}
如图
编译执行,如图:
测试一个写端多个读端的时候,由于数据一旦被读走就没了,所以多个读端的并集才是写端的写入数据。
3.2.2 文件用于进程间通信
打开的文件是内核中的一块缓冲区。多个无血缘关系的进程,可以同时访问该文件。
文件通信这个,有没有血缘关系都行,只是有血缘关系的进程对于同一个文件,使用的同一个文件描述符,没有血缘关系的进程,对同一个文件使用的文件描述符可能不同。这些都不是问题,打开的是同一个文件就行。
3.3 mmap
3.3.1 函数原型
存储映射I/O(Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是从缓冲区中取数据,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不使用read和write函数的情况下,使地址指针完成I/O操作。
使用这种方法,首先应该通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。
mmap函数原型
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
// 创建共享内存映射
// 参数:
// addr: 指定映射区的首地址。通常传NULL,表示让系统自动分配
// length:共享内存映射区的大小。(<= 文件的实际大小)
// prot: 共享内存映射区的读写属性。PROT_READ、PROT_WRITE、
PROT_READ|PROT_WRITE
// flags: 标注共享内存的共享属性。MAP_SHARED、MAP_PRIVATE
// fd: 用于创建共享内存映射区的那个文件的 文件描述符。
// offset:默认0,表示映射文件全部。偏移位置。需是 4k 的整数倍。
// 返回值:
// 成功:映射区的首地址。
// 失败:MAP_FAILED (void*(-1)), errno
// flags里面的shared意思是修改会反映到磁盘上,private表示修改不反映到磁盘上
munmap函数原型
int munmap(void *addr, size_t length); // 释放映射区。
// addr:mmap 的返回值
// length:大小
3.3.2 mmap建立映射区
下面这个示例代码,使用mmap创建一个映射区(共享内存),并往映射区里写入内容:
/*************************************************************************
> File Name: mmap.c
> Author: Winter
> Created Time: 2021年10月18日 星期一 20时30分00秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/mman.h>
int main(int argc, char* argv[])
{
char* p = NULL;
// 打开一个文件
int fd = open("testmap", O_RDWR|O_CREAT|O_TRUNC, 0644);
if (fd == -1) {
perror("open error\n");
exit(1);
}
/*
// 此时上面文件大小为0,拓展文件大小
lseek(fd, 10, SEEK_END);
write(fd, "1", 1); // 文件大小11
上面这两个函数等于ftruncate()函数
*/
ftruncate(fd, 20);
int len = lseek(fd, 0, SEEK_END); // 有IO操作
// mmap在这里
p = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) {
perror("mmap error\n");
exit(1);
}
// 使用p对文件进行读写操作
strcpy(p, "hello mmap");
// 读操作
printf("-----%s\n",p);
// 释放内存
int res = munmap(p, len);
if (res == -1) {
perror("munmap error\n");
exit(1);
}
return 0;
}
执行
查看testmap
1 od -tcx testmap
3.3.3 mmap使用注意事项
使用注意事项:
1 用于创建映射区的文件大小为 0,实际指定非0大小创建映射区,出 “总线错误”。
int len = 20;
p = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
2 用于创建映射区的文件大小为 0,实际指定0大小创建映射区, 出 “无效参数”。
int len = 0;
p = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
3 用于创建映射区的文件读写属性为,只读(只写)。映射区属性为 读、写。 出 “无效参数(权限不允许)”。
4 创建映射区,需要read权限。当访问权限指定为 “共享”MAP_SHARED时, mmap的读写权限,应该<=文件的open权限。 只写不]行。两个都是只读【段错误】
5 文件描述符fd,在mmap创建映射区完成即可关闭。后续访问文件,用 地址访问。
6 offset 必须是 4096的整数倍。(MMU 映射的最小单位 4k )
7 对申请的映射区内存,不能越界访问。
8 munmap用于释放的 地址,必须是mmap申请返回的地址。
9 映射区访问权限为 “私有”MAP_PRIVATE, 对内存所做的所有修改,只在内存有效,不会反应到物理磁盘上。
10 映射区访问权限为 “私有”MAP_PRIVATE, 只需要open文件时,有读权限,用于创建映射区即可。
mmap函数的保险调用方式:
fd = open("文件名", O_RDWR);
mmap(NULL, 有效文件大小, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
总结
1. 创建映射区的过程中,隐含着一次对映射文件的读操作
2. 当MAP_SHARED时,要求:映射区的权限应该<=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制
3. 映射区的释放与文件关闭无关。只要映射建立成功,文件可以立即关闭
4. 特别注意,当映射文件大小为0时,不能创建映射区。所以:用于映射的文件必须要有实际大
小!!mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。如,400字节大小的文件,在建立映射区时,offset4096字节,则会报出总线错误
5. munmap传入的地址一定是mmap返回的地址。坚决杜绝指针++操作
6. 文件偏移量必须为4K的整数倍
7. mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。
3.3.4 父子进程间通信
父子进程使用 mmap 进程间通信:
父进程 先 创建映射区。 open( O_RDWR) mmap( MAP_SHARED );
指定 MAP_SHARED 权限
fork() 创建子进程。
一个进程读, 另外一个进程写。
下面这段代码,父子进程mmap通信,共享内存是一个int变量:
/*************************************************************************
> File Name: fork_mmap.c
> Author: Winter
> Created Time: 2021年10月20日 星期三 21时07分53秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/mman.h>
#include<sys/wait.h>
#include<fcntl.h>
int var = 10;
int main(int argc, char* argv[])
{
int fd = open("temp", O_RDWR|O_CREAT|O_TRUNC, 0644);
int* p = NULL;
pid_t pid;
if (fd < 0) {
perror("open error\n");
exit(1);
}
ftruncate(fd, 4); // 将文件大小拓展为4
p = (int*) mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) {
perror("mmap error\n");
exit(1);
}
close(fd); // 映射区创建完毕,关闭文件
pid = fork(); // 创建子进程
if (pid == 0) {
// 子进程处理
*p = 11000; // 写共享内存
var = 20; // 修改全局变量
printf("child *p = %d, var = %d\n",*p, var);
} else if (pid > 0) {
// 父进程处理
sleep(1);
printf("parent *p = %d, var = %d\n",*p, var); // 读共享内存
wait(NULL); // 回收子进程
// 回收
int res = munmap(p, 4);
if (res == -1) {
perror("munmmap error\n");
exit(1);
}
} else {
perror("fork error\n");
exit(1);
}
return 0;
}
执行【读时共享,写时复制】
如图,子进程修改p的值,也反映到了父进程上,这是因为共享内存定义为shared的。
如果将共享内存定义为private,运行结果如下:
父子进程使用mmap进程间通信。
父进程先创建映射区,O_RDWR,指定MAP_SHARED,fork创建子进程,一个读一个写。
3.3.5 无血缘关系进程间mmap通信
两个进程 打开同一个文件,创建映射区。
指定flags 为 MAP_SHARED。
一个进程写入,另外一个进程读出。
【注意】:无血缘关系进程间通信。
mmap:数据可以重复读取。
fifo:数据只能一次读取。
下面是两个无血缘关系的通信代码,先是写进程:
/*************************************************************************
> File Name: mmap_w.c
> Author: Winter
> Created Time: 2021年10月22日 星期五 20时43分01秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/mman.h>
struct Stu{
int id;
char name[20];
char gender;
};
int main(int argc, char* argv[])
{
struct Stu student = {1, "xiaoming", 'M'};
struct Stu* p = NULL;
/* if (argc < 2) {
printf("please input a.out sharedFile......\n");
exit(1);
}*/
int fd = open("stuFile", O_RDWR|O_CREAT|O_TRUNC, 0664);
if (fd == -1) {
perror("open error\n");
exit(1);
}
// 扩大内存
ftruncate(fd, sizeof(student));
p = mmap(NULL, sizeof(student), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) {
perror("mmap error\n");
exit(1);
}
close(fd);
while (1) {
memcpy(p, &student, sizeof(student));
student.id++;
sleep(1);
}
munmap(p, sizeof(student));
return 0;
}
然后是读进程:
/*************************************************************************
> File Name: mmap_w.c
> Author: Winter
> Created Time: 2021年10月22日 星期五 20时43分01秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/mman.h>
struct Stu{
int id;
char name[20];
char gender;
};
int main(int argc, char* argv[])
{
struct Stu student;
struct Stu* p = NULL;
/* if (argc < 2) {
printf("please input a.out sharedFile......\n");
exit(1);
}
*/
int fd = open("stuFile", O_RDONLY);
if (fd == -1) {
perror("open error\n");
exit(1);
}
p= mmap(NULL, sizeof(student), PROT_READ, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) {
perror("mmap error\n");
exit(1);
}
close(fd);
while (1) {
printf("id = %d, name = %s, gender = %c\n",p->id, p->name, p->gender);
sleep(1);
}
munmap(p, sizeof(student));
return 0;
}
执行
多个写端一个读端也没问题,打开多个写进程即可,完事儿读进程会读到所有写进程写入的内容。
这里要注意一个,内容被读走之后不会消失,所以如果读进程的读取时间间隔短,它会读到很多重复内容,就是因为写进程没来得及写入新内容
3.3.6 mmap匿名映射区
匿名映射:只能用于 血缘关系进程间通信。
p = (int *)mmap(NULL, 40, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
较老的系统 类unix
/dev/zero------->聚宝盆
/dev/null------->文件黑洞
参考:https://blog.csdn.net/weixin_43914604/article/details/104882398