进程间通信的方式?
- - 管道
- - 匿名管道
- - 有名管道
- - 内存映射
- - 本地套接字
- - 网络套接字
- - 消息队列
- - 共享内存
- 父子进程始终共享什么东西?
- - 文件描述符
- - 内存映射区
目录
一、管道
管道的本质:
- 是内核缓冲区
- 拥有文件的特质(读操作、写操作)
- 匿名管道 -> 没有文件的实体
- 有名管道 -> 有文件实体, 不存储数据
- 可以文件操作的方式对管道进行处理
在创建子进程之前, 父进程通过文件操作打开了两个文件 a, b, 得到了两个文件描述符: fd3 fd4
父进程通过fork 创建子进程
子进程对应一个虚拟地址空间
- 文件描述符表: fd3, fd4 -> 从父进程拷贝过来的
- 在子进程中通过得到的fd3 fd4 来操作 A, B文件
1.1匿名管道
创建匿名管道
#include <unistd.h>
int pipe(int pipefd[2]); char*
参数:
pipefd: 传出参数
- pipefd[0] -> 管道的读端
- pipefd[1] -> 管道的写端
返回值:
0: 调用成功
-1: 失败
匿名管道的原理:
- 没有名字, 在磁盘上没有实体, 是内存中的一块缓冲区
- 这个缓冲区, 由父进程在fork()子进程之前创建得到的
- 内核缓冲区有两部分
- 读端 -> 可以进行读操作的文件描述符
- 写端 -> 可以进行写操作的文件描述符 - 父进程被销毁, 管道自动释放
- 默认阻塞
匿名管道数据结构是一个环形队列,默认容量4k
实现过程:
- - 读操作
- - 如果管道中有数据
- - read读, read返回值是读到的字节数
- - 管道中没有数据
- - 写端没有关闭, 但是写的慢,read会阻塞
- - 写端关闭了,read解除阻塞, 返回值为0
- - 如果管道中有数据
- - 写数据
- - 管道有空间。接着写, 写满之后阻塞
- - 没有空间。直接阻塞
- - 写数据的时候, 读端关闭了
- - 管道破裂
- - 进程被 SIGPIPE 信号杀死
管道的读写两端都默认阻塞,如何设置为非阻塞呢?
// 使用 fcntl 函数
// 设置读端为非阻塞 -> fd[0]
int flag = fcntl(fd[0], F_GETFL)
flag |= O_NONBLOCK;
fcntl(fd[0], F_SETFL, flag);
匿名管道的局限性:
- 管道中的数据只能被读一次
- 半双工,数据是单向流动的
- 只能进程有血缘关系的进程通信
- 在磁盘上没有实体
- 被一个进程创建出来的
- 得到两个fd(一读一写)
栗子:(使用匿名管道实现进程间通信)
流程:
父子进程间通信, 实现 ps aux
子进程 -> ps aux
子进程得到的结果, 给到父进程
涉及到有血缘关系的进程通信 -> pipe
创建进程 -> fork()
子进程做的事儿: execlp(), 执行命令数据会默认写到终端
- 重定向: stdout_fileno -> 管道的写端 dup2
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
int fd[2];
// 创建匿名管道
int ret = pipe(fd);
if(ret == -1)
{
perror("pipe");
exit(0);
}
// 创建子进程
pid_t pid = fork();
if(pid > 0)
{
// 父进程, 读管道
char buf[1024];
while(1)
{
read(fd[0], buf, sizeof(buf));
printf("%s", buf);
}
wait(NULL);
}
else if(pid == 0)
{
// 子进程, 写管道
// 文件描述符重定向 stdout_fileno -> fd[1]
dup2(fd[1], STDOUT_FILENO);
// 执行shell命令 ps aux
execlp("ps", "ps", "aux", NULL);
perror("execlp");
exit(0);
}
else
{
perror("fork");
exit(0);
}
return 0;
2.2 有名管道
有名管道的原理:
- - 在磁盘上有一个文件 -> 伪文件
- - 通过这个伪文件给不同 的进程搭建一个桥梁 -> 找到同一块内核缓冲区
- - 这个磁盘文件大小, 永远为0
- - 内核缓冲区 -> 环形队列实现的
- - 数据只能被读一次
- - 默认也是阻塞的
- - 实现没有血缘关系的进程通信
创建方式:
# 通过命令创建
mkfifo 名字
# 通过函数创建
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数:
- pathname: 创建的管道文件对应路径和名字
- mode: 用户对管道文件的操作权限, 八进制的数, 最终权限: (mode & ~umask)
创建栗子:(2个无关的进程通信)
wirte文件
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <string.h>
int main()
{
// 创建有名管道
int ret = mkfifo("test", 0664);
if(ret == -1)
{
perror("mkfifo");
exit(0);
}
// 打开管道文件
int fd = open("test", O_WRONLY);
// 写数据
for(int i=0; i<100; ++i)
{
char buf[1024];
sprintf(buf, "hello, %d\n", i);
write(fd, buf, strlen(buf)+1);
sleep(2);
}
// 关闭文件
close(fd);
return 0;
}
read文件
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <string.h>
int main()
{
// 打开管道文件
int fd = open("test", O_RDONLY);
// 写数据
while(1)
{
char buf[1024];
read(fd, buf, sizeof(buf));
printf("recv buf: %s\n", buf);
}
// 关闭文件
close(fd);
return 0;
}
二、内存映射
将磁盘文件的数据映射到内存, 用户通过修改内存就能修改磁盘文件
函数原型:
- mmap 得到映射内存在共享库加载的区域(虚拟地址空间)
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数:
- addr: NULL, 由内核指定
- length: 要映射的数据的长度, 这个值不能为0
- prot: 对申请的内存映射区的操作权限
- PROT_READ: 读权限, 这个权限必须要有
- PROT_WRITE: 写权限
- PROT_READ | PROT_WRITE: 读写权限
- flags:
- MAP_SHARED: 映射区数据会自动和磁盘文件进行同步, 进程间通信必须设置这个选项
- MAP_PRIVATE: 不同步
- fd: 文件描述符
- 通过open得到的, open的是一个磁盘文件
- 文件大小不能为0 (新文件要用lseek扩展)
- open的时候需要指定flags
- 这个权限要和prot参数值对应
- prot:PROT_READ , flag=只读/读写
- prot:PROT_READ | PROT_WRITE , flag=读写
- offset: 偏移量, 一定得的4k的整数倍, 0是可以的
返回值:
成功: 指向映射区起始位置的指针
失败: MAP_FAILED (that is,(void *) -1)
- munmap 释放申请的内存映射区
int munmap(void *addr, size_t length);
参数:
- addr: mmap的返回值
- length: 和mmap函数的第二个参数相同即可
父子进程使用内存映射通信的栗子:
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
int main()
{
int fd = open("test.txt", O_RDWR);
int size = lseek(fd, 0, SEEK_END);
// 创建内存映射区
void* ptr = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(-1);
}
// 创建子进程
pid_t pid = fork();
if(pid > 0)
{
// 父进程
// 写内存
strcpy((char*)ptr, "你是我儿子吗?");
}
else if(pid == 0)
{
// 子进程
// 读内存
char buf[64];
strcpy(buf, (char*)ptr);
printf("read data: %s\n", buf);
}
// 关闭内存映射区
munmap(ptr, size);
return 0;
}
无关系进程使用内存映射通信的栗子:
wirte文件
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
int main()
{
int fd = open("test.txt", O_RDWR);
int size = lseek(fd, 0, SEEK_END);
// 创建内存映射区
void* ptr = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(-1);
}
strcpy((char*)ptr, "你是我儿子吗===================xxxxxxxxxxxxxx?");
// 关闭内存映射区
munmap(ptr, size);
return 0;
}
read文件
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
int main()
{
int fd = open("test.txt", O_RDWR);
int size = lseek(fd, 0, SEEK_END);
// 创建内存映射区
void* ptr = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(-1);
}
// 读内存
char buf[64];
strcpy(buf, (char*)ptr);
printf("read data: %s\n", buf);
// 关闭内存映射区
munmap(ptr, size);
return 0;
}
三、总结
3.1 进程间通信
- 有血缘关系的进程通信
- 还没有子进程的时候。通过唯一的父进程, 先创建内存映射区,内存映射区有了之后, 创建子进程,父子进程共享创建的内存映射区
- 无血缘关系的进程通信
- 准备一个大小非0的磁盘文件
- 进程1 通过磁盘文件创建内存映射区得到一个操作这块内存的指针(读或者写)
- 进程2 通过磁盘文件创建内存映射区得到一个操作这块内存的指针(读或者写)
- 使用内存映射区通信, 不阻塞
3.2 面试问题
1. 如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?
void* ptr = mmap();
ptr++;
munmap(ptr, len); // 错误
映射区的起始地址必须要保留下来, 用于映射区的释放
2. 如果open时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样?
mmap返回 MAP_FAILED
prot参数指定PROT_READ | PROT_WRITE
int fd = open("xxx", O_RDWR);
3. 如果文件偏移量为1000会怎样?
mmap调用失败, 返回: MAP_FAILED
4. mmap什么情况下会调用失败?
- 第二个参数: length == 0
- 第三个参数: prot
- 指定了写权限
- prot:PROT_READ | PROT_WRITE,
第5个参数fd通open打开文件的时候指定了 O_RDONY/O_WRONLY
5. 可以open的时候O_CREAT一个新文件来创建映射区吗?
- 创建的新文件大小肯定为0, 这是不行的
- 可以对新得到的文件进行拓展
- lseek(fd, 拓展的长度, SEEK_END);然后对文件进行一次写操作: write(fd, " ", 1);
- int truncate(const char *path, off_t length);
- int ftruncate(int fd, off_t length);
6. mmap后关闭文件描述符,对mmap映射有没有影响?
int fd = open("xxx");
mmap(,,,,fd,0);
close(fd);
映射区还存在, 创建映射区使用的fd被关闭了
7. 对ptr越界操作会怎样?
void* ptr = mmap(NULL, 100,,,,);
映射区的最新单位是4k
越界 == 操作非法内存 -> 段错误