06_进程间通信

代码: 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. 内存映射

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的整数倍
    越界 == 操作的非法内存(大概率事件 段错误)
    段错误 程序崩溃

猜你喜欢

转载自blog.csdn.net/liangwenhao1108/article/details/107581197