代码: https://github.com/WHaoL/study/tree/master/00_06_Linux_SystemCode_and_SocketCode
代码: https://gitee.com/liangwenhao/study/tree/master/00_06_Linux_SystemCode_and_SocketCode
1. 概述
1.IPC - InterProcess Communication:进程间通信
2.进程间通信的目的?
2.1.目的:实现两个进程之间的数据传递和数据共享
2.2.进程和进程之间是相互独立的
- 不共享全局变量, 堆区数据
进程间通信的方式?
- 1、管道 -> 最简单的
- 匿名管道
- 有名管道
- 2、内存映射区
- 3、套接字
- 网络套接字
- 本地套接字
- 4、信号(信号优先级太高,容易把执行优先级顺序打乱)
- 不推荐
- 5、共享内存 -> 效率最高 !!!
父子进程始终共享什么东西 ?(有血缘关系的进程间通信)
- 1、父进程拷贝给子进程的有效的文件描述符
- 父子进程都可以使用这些文件描述符打开相同的文件
- 2、内存映射区
2. 管道
管道的本质? :内核中的一块缓冲区 -> 内核中的一块内存
1.操作系统使用队列这种数据结构来维护这块内存
2.所以,对这块内存操作的方式 ==等价于== 操作一个队列
3.队头队尾分别对应`两个文件描述符`
4.一端读, 一端写
4.为什么可以使用管道进行进程间通信?
有血缘关系的父子进程间
父进程拷贝给子进程的有效的文件描述符
父子进程都可以使用这些文件描述符打开相同的文件
- 对管道操作 == 操作文件
1.假设在父进程中创建匿名管道, 得到了两个文件描述符
使用这两个文件描述符对管道进行读写操作
2.在父进程中创建子进程
这两个文件描述符被复制到子进程中
因此在子进程中同样可以使用这两个文件描述符对管道进行读写操作
- 上边说的管道 == 图中的磁盘文件
- 操作方式是一样的
- 数据的载体不同
2.1 匿名管道
1.匿名管道的特点?
1.在磁盘上没有实体, 没有名字
2.管道中的数据只能读一次(管道是使用队列来维护的)
3.管道默认是阻塞的
- 读: 没数据的时候阻塞
- 写: 管道被写满了之后阻塞
2.管道的原理?
-
环形队列
-
默认大小是4k
$ ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 3746
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8 # 管道默认大小
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 3746
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
3.匿名管道的局限性?
1、数据只能读一次
2、只能实现有血缘关系的进程间通信
- 父子、爷孙、叔侄、兄弟...
4.如何创建匿名管道?
#include <unistd.h>
// 创建匿名管道
int pipe(int pipefd[2]);
参数:
管道创建成功, 会得到两个有效的文件描述符
- pipefd[0]: 管道的读端
- pipefd[1]: 管道的写端
返回值:
成功: 0
失败: -1
5.使用匿名管道实现进程间通信
/*
练习题
父子进程间通信, 实现 ps aux
1.子进程执行shell命令: ps aux
2.父进程中输出命令结果
*/
处理思路:
- 1. 在父进程中创建匿名管道 -> 得到到了两个fd
- 2. 在父进程中创建子进程 -> 子进程中也拥有了这两个fd
- 3. 在子进程中执行shell指令:
- 调用函数: execlp(); -> stdout -> stdout_fileno
- 结果默认输出到终端, 不能直接输出到终端否则数据不好操作
- 可以对数据重定向: dup2(oldfd, newfd); // 从终端 -> 管道写端
- 4. 父进程从管道中读数据
6.管道的读写行为?
管道的读写行为?
1.读管道
1.1.管道中有数据
通过`read`读数据, read不阻塞, 返回读到的字节数
1.2.管道中没有数据
1.2.1写端还打开着
`read阻塞`, 等待写端写数据
1.2.2写端已经关闭了 -> 所有的写端(父进程、子进程)
`read不阻塞`, read的返回值 == 0, 相当于读文件读完了
2.写管道
2.1.读端没有关闭
当管道数据满了 --> `write阻塞`
管道没有满 --> 写数据`write不阻塞`
2.2.读端关闭
往管道中写数据, 管道破裂, 当前进程直接退出
当管道的读写两端全部被关闭, 匿名管道也就被销毁了
7.如何设置管道非阻塞?
1.管道的读写两端都默认阻塞
2.一般不设置为非阻塞,因为
使用起来考虑的事情更多
非阻塞时,read会空轮循,占用CPU
...
举例:分别设置读端和写端非阻塞
// 比如:设置读端为非阻塞
int fd[2];
int ret = pipe(fd);
int flag = fcntl(fd[0], F_GETFL);//获取当前属性
flag |= O_NONBLOCK;// 添加非阻塞属性
fcntl(fd[0], F_SETFL, flag);// 新的属性设置给fd
2.2 有名管道
fifo : frist in frist out 先进先出(队列模式)
1.特点
1.内核中的一块缓冲区, 有名字, 在磁盘上对应的是一个文件
1.1是一个伪文件(看起来是个文件,其实不是),
这个文件大小永远为0, 不存储数据
1.2进程可以通过这个文件找到内核缓冲区地址
进程里的数据通过这个文件直接流向内核缓冲区
2.数据只能读一次(这个内核缓冲区也是一个队列维护的)
3.读写默认也是阻塞的
4.使用场景(一般用有名管道)
有血缘关系的进程间通信
没有血缘关系的进程间通信
2.创建方式 -> 两种
# 1. 通过shell命令
mkfifo 管道名字
$ mkfifo test
robin@OS:~$ ll test
prw-rw-r-- 1 robin robin 0 Jul 28 12:25 test|
// 使用函数创建有名管道
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数:
- pathname: 文件名
- mode: 创建出的新文件的权限, (mode & ~umask).
返回值:
成功: 0
失败: -1
使用有名管道进行没有血缘关系的进程间通信
-
进程A -> 读操作
// 1. 创建有名管道, 文件名叫: test.fifo mkfifo("test.fifo", 0664); // 2. 在进程A中打开管道文件, 给只读权限 int fd = open("test.fifo", O_RDONLY); // 3. 通过fd读数据到文件 read(fd, buf, sizeof(buf)); // 4. 关闭文件 close(fd);
-
进程B -> 写操作
// 1. 在进程B中打开管道文件, 给只写权限 int fd = open("test.fifo", O_WRONLY); // 3. 通过fd写数据到文件 write(fd, data, strlen(data)); // 4. 关闭文件 close(fd);
3. 内存映射(mmap munmap)
3.1 概念
1.内存映射区:有一个磁盘文件, 某个进程通过调用一个函数 `mmap`可以在当前进程的虚拟地址空间中申请一块内存, 将磁盘文件中的内容映射到这块内存中. 这块内存就叫内存映射区
2.两个进程通过内存映射区实现进程间通信, 每个进程中都有一块属于自己的内存映射区
3.内存映射区的位置: 动态库加载区
3.2 mmap、munmap函数原型
#include <sys/mman.h>
// 创建内存映射区
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数:
- addr: 指定要申请的内存映射区的地址, 指定为NULL, 代表这个地址由的内核指定
- length: 创建的内存映射区的大小(实际分配的大小是4k的整数倍)
- 比如: 100字节, 或者1024字节等等(小于4k的数) --> 实际大小是4k
- 注意: 这个参数值不能为0, 应该指定一个>0的值
- prot: 对内存映射区的操作权限
- PROT_READ: 读内存映射区数据
- PROT_WRITE: 往内存映射区中写数据
- 注意: 创建出的内存映射区, 当前进程必须对其拥有读的操作权限
- PROT_READ
- PROT_READ | PROT_WRITE
- flags: 指定映射区和磁盘文件的关系
- MAP_SHARED: 映射区数据和磁盘文件中的数据可以同步
- 进程间通信需要这个条件的支持
- MAP_PRIVATE: 修改了内存映射区数据, 数据不会自动同步到磁盘文件
- fd: 磁盘文件打开之后对应的文件描述符
- 映射区中的数据来自于这个fd对应的磁盘文件
- fd需要通过open得到
int open(const char *pathname, int flags);
- 需要指定文件的打开权限
- 因为文件的数据最终要被映射到内存映射区, 必须要对文件有读权限
- 如果要内存映射区中的数据被同步到磁盘中, 必须要对文件有写权限
- 打开文件的时候, 指定的对文件的操作权限应该和第三个参数对应(权限要相互对应)
- 如果对磁盘文件有读/写权限, 那么对内存映射区也得有读/写权限
- 如果对磁盘文件有读写权限, 对映射区有只有读权限 -> 错误的
- 改正: prot参数指定的权限也应该是读写
- offset: 磁盘文件的偏移量
- 要求: 必须是4k的整数倍, 否则函数调用失败
- 0: 不偏移
返回值:
成功: 得到创建的内存映射区的首地址
失败: MAP_FAILED (that is, (void *) -1)
// 释放内存映射区
int munmap(void *addr, size_t length);
参数:
- addr: mmap函数的返回值, 内存映射区的首地址
- length: 这个值和mmap的第二个参数值相同
返回值:
成功: 0
失败: -1
mmap进程间通信 -> 读 / 写都不不阻塞
重要的条件:
创建内存映射区的时候, 这些进程操作的磁盘文件必须是同一个
3.3 步骤:有血缘关系的–进程间通信
// 1. 准备一个磁盘文件, 非空文件(文件大小不能为0) -> test.txt
// 2. 在父进程中打开磁盘文件test.txt -> 得到了文件描述符fd
// 3. 创建内存映射区 -> mmap
// 4. 创建子进程, fork()
// - fd被复制到了子进程中, 因此也可以操作磁盘文件test.txt
// - 内存映射区也被复制了
// 5. 对内存映射区中的数据进行读写操作 -> 对内存操作
// - 内存首地址mmap的的返回值
// - 读/写内存(从内存中取数据,存数据)
void *memcpy(void *dest, const void *src, size_t n);
int printf(const char *format, ...);
...
3.4 步骤:没有血缘关系的–进程间通信
// 进程A
// 1. 准备一个磁盘文件, 非空文件(文件大小不能为0!!!) -> test.txt
// 2. 在进程中打开磁盘文件test.txt -> 得到了文件描述符fd
// 3. 创建内存映射区 -> mmap
// 4. 对内存映射区中的数据进行读写操作 -> 对内存操作
// - 内存首地址mmap的的返回值
// - 读/写内存(从内存中取数据,存数据)
void *memcpy(void *dest, const void *src, size_t n);
int printf(const char *format, ...);
...
// 进程B
// 1. 和进程A打开同一个磁盘文件 -> test.txt
// 2. 在进程中打开磁盘文件test.txt -> 得到了文件描述符fd
// 3. 创建内存映射区 -> mmap
// 4. 对内存映射区中的数据进行读写操作 -> 对内存操作
// - 内存首地址mmap的的返回值
// - 读/写内存(从内存中取数据,存数据)
void *memcpy(void *dest, const void *src, size_t n);
int printf(const char *format, ...);
...
思考问题
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(void *addr, size_t length);
1. 如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?
不能成功
地址指向不是首地址 变成了首地址++
- munmap函数调用失败
- void* ptr = mmap();
- void* pt1 = ptr;
- ptr++;
2. 如果open时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样?
两个权限必须一样 映射时是一样的
- open: 指定读写权限
3. 如果文件偏移量为1000会怎样?
- 必须是4K的整数倍
4. mmap什么情况下会调用失败?
-第二个参数length不能为0,
-第三个参数prot和第5个参数fd
这两个参数对应 内存映射区和文件描述符的权限
这两个对应的权限必须相同
-如果进行进程间通信,第四个参数必须是 MAP_SHARED
-最后一个参数:offset它必须是4K的整数倍
必须同时满足条件 否则就会失败
5. 可以open的时候O_CREAT一个新文件来创建映射区吗?
直接创建的话,没有指定文件大小,则新文件大小为0
mmap要求 创建内存映射区的时候,磁盘文件大小不能为0
6. mmap后关闭文件描述符,对mmap映射有没有影响?
没有影响
7. 对mmap的返回值ptr越界操作会怎样?
分区的内存是4k的整数倍
越界 == 操作的非法内存(大概率事件 段错误)
段错误 程序崩溃
代码
07read_mmap.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
#include <fcntl.h>
int main()
{
//1、准备磁盘文件
int fd = open("07test.txt", O_RDWR);
if (fd == -1)
{
perror("open");
exit(0);
}
//2、求磁盘文件大小
int length = lseek(fd, 0, SEEK_END);
//3、创建内存映射区
void *ptr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED)
{
perror("mmap");
exit(0);
}
//4、当前进程,读数据
char buf2[1024] = {0};
memcpy(buf2, ptr, strlen((char *)ptr));
printf("%s\n", buf2);
// printf("%s\n",(char *)ptr);
//5、释放内存映射区
munmap(ptr, length);
close(fd);
return 0;
}
07write_mmap.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
#include <fcntl.h>
int main()
{
//1、准备磁盘文件
int fd = open("07test.txt", O_RDWR);
if (fd == -1)
{
perror("open");
exit(0);
}
//2、求磁盘文件大小
int length = lseek(fd, 0, SEEK_END);
//3、创建内存映射区
void *ptr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED)
{
perror("mmap");
exit(0);
}
//4、当前进程,写数据
int num = 0;
char basebuf[] = "你是我儿子吗,我是爹!!!";
char buf1[1024] = {0};
sprintf(buf1, "%s-----%d\n", basebuf, num++);
memcpy(ptr, buf1, strlen(buf1) + 1);
//5、释放内存映射区
munmap(ptr, length);
close(fd);
return 0;
}
4. 共享内存(shared memory)
C programming in the UNIX environment的编程手册,一般都会为进程间用共享内存的方法通信提供两组方法:
- POSIX定义的:(用这个)
int shm_open(const char *name, int oflag, mode_t mode);
int ftruncate(int fd, off_t length);
int shm_unlink(const char *name);
代码参考我的博客
《C++标准库第2版》侯捷译 - 第5章 通用工具 - 5.2 Smart Pointer(智能指针) - 读书笔记 4. shared_ptr处理共享内存
链接:https://blog.csdn.net/liangwenhao1108/article/details/109829745
由于POSIX标准比较通用,一般建议使用该标准定义的方法集。
但是在使用shm_open和shm_unlink两个函数时,可能提示你函数找不到。如下:
[root@lwh testcpp]# make
Compiling out test4.cpp
g++ -I /usr/include/ -I /usr/local/include -w -g -ggdb -fshort-wchar -std=c++11 -pthread -c test4.cpp -o test4.o
creating out start...
g++ -L /usr/local/lib/ -L /usr/lib/ -l pthread ./test4.o -o out
./test4.o: In function `getSharedIntMemory(int)':
/root/testcpp/test4.cpp:36: undefined reference to `shm_open'
./test4.o: In function `SharedMemoryDetacher::operator()(int*)':
/root/testcpp/test4.cpp:26: undefined reference to `shm_unlink'
collect2: error: ld returned 1 exit status
make: *** [out] Error 1
[root@lwh testcpp]#
注意:链接代码时要加上 -lrt 参数 即可解决上述问题
我们man 3 shm_open时就可以看到Link with -lrt
[root@lwh testcpp]# man 3 shm_open
SYNOPSIS
#include <sys/mman.h>
#include <sys/stat.h> /* For mode constants */
#include <fcntl.h> /* For O_* constants */
int shm_open(const char *name, int oflag, mode_t mode);
int shm_unlink(const char *name);
Link with -lrt.
- SYSTEM V定义的
int shmget(key_t key, int size, int shmflg);
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
(1)通过int shmget(key_t key, size_t size, int shmflg);在物理内存创建一个共享内存,返回共享内存的编号。
(2)通过void *shmat(int shmid, constvoid shmaddr,int shmflg);连接成功后把共享内存区对象映射到调用进程的地址空间
(3)通过void *shmdt(constvoid* shmaddr);断开用户级页表到共享内存的那根箭头。
(4)通过int shmctl(int shmid, int cmd, struct shmid_ds* buf);释放物理内存中的那块共享内存
5. mmap内存映射区 和 shm共享内存的区别
https://blog.csdn.net/hj605635529/article/details/73163513
https://blog.csdn.net/woaiclh13/article/details/106409361
linux中的两种共享内存。
一种是我们的IPC通信System V版本的共享内存(shm),
另外的一种就是内存映射I/O(mmap函数)
1.mmap内存映射
1.mmap内存映射:
内存映射是通过操作内存来实现对文件的操作,这样可以加快执行速度,,不是专门用来进行数据通信的(但它也可以用于进程间的通信)
2.shm共享内存
2.shm共享内存:
共享内存,顾名思义,就是预留出的内存区域,它允许一组进程对其访问
共享内存是system vIPC中三种通信机制最快的一种,也是最简单的一种;!!!!!!
对于进程来说,获得共享内存后,他对内存的使用和其他的内存是一样的。
由一个进程对共享内存所进行的操作对其他进程来说都是立即可见的,因为每个进程只需要通过一个指向共享内存空间的指针就可以来读取共享内存中的内容(说白了就好比申请了一块内存,每个需要的进程都有一个指针指向这个内存)就可以轻松获得结果。使用共享内存要注意的问题:共享内存不能确保对内存操作的互斥性。
一个进程可以向共享内存中的给定地址写入,而同时另一个进程从相同的地址读出,这将会导致不一致的数据。
因此使用共享内存的进程必须自己保证读操作和写操作的的严格互斥。
可使用锁和原子操作解决这一问题。也可使用信号量保证互斥访问共享内存区域。
共享内存在一些情况下可以代替消息队列,而且共享内存的读/写比使用消息队列要快!
3.mmap和shm的区别:
普通的读写文件的原理
在说mmap之前我们先说一下普通的读写文件的原理,进程调用read或是write后会陷入内核,因为这两个函数都是系统调用,进入系统调用后,内核开始读写文件,假设内核在读取文件,内核首先把文件读入自己的内核空间,读完之后进程在内核回归用户态,内核把读入内核内存的数据再copy进入进程的用户态内存空间。实际上我们同一份文件内容相当于读了两次,先读入内核空间,再从内核空间读入用户空间。
内存映射函数mmap
Linux提供了内存映射函数mmap, 它把文件内容映射到一段内存上(准确说是虚拟内存上), 通过对这段内存的读取和修改, 实现对文件的读取和修改,mmap()系统调用使得进程之间可以通过映射一个普通的文件实现共享内存。普通文件映射到进程地址空间后,进程可以向访问内存的方式对文件进行访问,不需要其他系统调用(read,write)去操作。
1、mmap是在磁盘上建立一个文件,每个进程地址空间中都会开辟出一块空间进行文件-内存的映射。
而对于shm而言,shm每个进程最终会映射到同一块物理内存。shm保存在物理内存,这样读写的速度要比磁盘要快,但是存储量不是特别大。
(mmap每个进程都会有自己的内存映射区,shm是映射到同一物理内存)
2、相对于shm来说,mmap更加简单,调用更加方便,所以这也是大家都喜欢用的原因。
3、另外mmap有一个好处是当机器重启,因为mmap把文件保存在磁盘上,这个文件还保存了操作系统同步的映像,所以mmap不会丢失,但是shmget就会丢失。