当前位置:   article > 正文

【Linux】进程间通信_父子进程通信

父子进程通信

目录

一、进程间通信背景

1、进程间通信的理解

2、进程间通信的目的

3、进程间的必要性

二、管道

1、什么是管道

2、匿名管道

3、命名管道

4、管道通信的特点

三、System V IPC

1、共享内存

2、进程互斥

总结


一、进程间通信背景

1、进程间通信的理解

进程运行具有独立性,进程想要直接进行进行通信难度比较大,进程间通信的前提是让不同的资源能够看到同一块资源。

2、进程间通信的目的

数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变。

3、进程间的必要性

单进程无法使用并发能力,无法实现多进程协同,传输数据,同步执行流,消息通知等。

二、管道

1、什么是管道

想象现实中的管道,它一定是有一个出口有一个入口,用来传输资源,资源只能从一端流向另一端

进程间通信的管道也同理,一个进程发送数据另一个进程接收数据。

管道是由操作系统所提供的最古老的进程间通信的形式。

2、匿名管道

匿名管道只适用于具有亲缘关系的进程间通信——通常用于父子进程间的通信

一个进程创建子进程,父进程打开的文件也会被子进程所继承,因为是同一个文件,所以父子进程打开的同一个文件具有相同的struct file,因此父子进程就同时看到了同一份内存资源,也就可以借助这个文件来进行父子进程间的通信。

匿名管道创建步骤

1、分别以读写方式打开同一个文件

2、fork()创建子进程

3、双方进程各自关闭自己不需要的文件描述符

管道文件是不需要刷新到磁盘的,它是专门用来进行进程间通信的文件,刷新到磁盘,相当于进行IO使进程间通信的效率降低

我们可以使用pipe来创建匿名管道,这里要传入一个数组,它是输出线参数

下标为0的代表读端,1代表写端

 这样前置工作就准备好了

接下来以一个比较复杂的例子来演示匿名管道

写一个简单的单机版负载均衡

创建一个进程池,父进程通过匿名管道随机向子进程派发任务

  1. //task.hpp
  2. #pragma once
  3. #include <iostream>
  4. #include <vector>
  5. #include <string>
  6. #include <functional>
  7. #include <unordered_map>
  8. #include <cassert>
  9. #include <ctime>
  10. #include <sys/wait.h>
  11. #include <unistd.h>
  12. typedef std::function<void()> func;
  13. std::vector<func> callbacks;
  14. std::unordered_map<int, std::string> desc;
  15. void readMySQL()
  16. {
  17. std::cout << "sub process [ " << getpid() << " ] 执行访问数据库任务\n" << std::endl;
  18. }
  19. void execuleUrl()
  20. {
  21. std::cout << "sub process [ " << getpid() << " ] 执行URL解析\n" << std::endl;
  22. }
  23. void cal()
  24. {
  25. std::cout << "sub process [ " << getpid() << " ] 执行加密任务\n" << std::endl;
  26. }
  27. void save()
  28. {
  29. std::cout << "sub process [ " << getpid() << " ] 执行持久化任务\n" << std::endl;
  30. }
  31. void Load()
  32. {
  33. desc.emplace(callbacks.size(), "readSQL: 读取数据库");
  34. callbacks.emplace_back(readMySQL);
  35. desc.emplace(callbacks.size(), "execuleURL: 进行url解析");
  36. callbacks.emplace_back(execuleUrl);
  37. desc.emplace(callbacks.size(), "cal: 进行加密计算");
  38. callbacks.emplace_back(cal);
  39. desc.emplace(callbacks.size(), "save: 进行数据的文件保存");
  40. callbacks.emplace_back(save);
  41. }
  42. void showHandler()
  43. {
  44. for(const auto& iter : desc)
  45. {
  46. std::cout << iter.first << "\t" << iter.second << std::endl;
  47. }
  48. }
  49. int handlerSize()
  50. {
  51. return callbacks.size();
  52. }
  53. //main.cpp
  54. #include "task.hpp"
  55. #define PROCESS_NUM 5
  56. // 父进程给子进程派发任务
  57. int waitCommind(int waitFd, bool &quit)
  58. {
  59. uint32_t command = 0;
  60. ssize_t s = read(waitFd, &command, sizeof(command));
  61. if (s == 0)
  62. {
  63. quit = true;
  64. return -1;
  65. }
  66. assert(s == sizeof(uint32_t));
  67. return command;
  68. }
  69. void sendAndWakeUp(pid_t who, int fd, uint32_t command)
  70. {
  71. write(fd, &command, sizeof(command));
  72. std::cout << "main process: call process " << who << "execute" << desc[command] << "through" << fd << std::endl;
  73. }
  74. int main()
  75. {
  76. Load();
  77. // 简单的线程池
  78. // 进程pid 及 文件描述符
  79. std::vector<std::pair<pid_t, int>> slots;
  80. // 创建子进程
  81. for (size_t i = 0; i < PROCESS_NUM; i++)
  82. {
  83. // 1、以读写方式打开文件
  84. int fd[2] = {0};
  85. int n = pipe(fd);
  86. assert(n == 0);
  87. pid_t id = fork();
  88. if (id < 0)
  89. {
  90. std::cerr << "fork" << std::endl;
  91. }
  92. else if (id == 0)
  93. {
  94. // child
  95. // 关闭写端
  96. close(fd[1]);
  97. while (true)
  98. {
  99. bool quit = false;
  100. int command = waitCommind(fd[0], quit);
  101. if (quit == true)
  102. {
  103. break;
  104. }
  105. if (command >= 0 && command < handlerSize())
  106. {
  107. callbacks[command];
  108. }
  109. else
  110. {
  111. std::cout << "非法command" << std::endl;
  112. }
  113. }
  114. exit(1);
  115. }
  116. else
  117. {
  118. // father
  119. // 关闭读端
  120. close(fd[0]);
  121. slots.emplace_back(id, fd[1]);
  122. }
  123. }
  124. // 派发任务
  125. size_t count = 0;
  126. srand(time(0));
  127. while (true)
  128. {
  129. int command = rand() % handlerSize();
  130. int choice = rand() % slots.size();
  131. sendAndWakeUp(slots[choice].first, slots[choice].second, command);
  132. sleep(1);
  133. count++;
  134. if (count == 10)
  135. {
  136. break;
  137. }
  138. }
  139. // 关闭文件描述符
  140. for (const auto &slot : slots)
  141. {
  142. close(slot.second);
  143. }
  144. // 回收子进程
  145. for (const auto &slot : slots)
  146. {
  147. waitpid(slot.first, nullptr, 0);
  148. }
  149. return 0;
  150. }

3、命名管道

命名管道与匿名管道类似,不过命名管道可以让不具有亲缘关系的进程通信

首先也是要让不同的进程看到同一份资源,双方进程就可以通过管道文件的路径看到同一份资源

管道文件首先是一个文件,它是有名字的可以被打开,但是不会将内存数据进行刷新到磁盘

mkfifo命名可以创建一个管道文件

我们通过echo命令将hello通过管道文件发送过去,echo命令就进入了阻塞状态

这时通过管道文件就可以读取资源了

这样就完成了echo和cat的通信

删除管道文件可以使用unlink或者rm

 接下来看代码的方式创建管道文件

mkfifo可以创建管道文件

成功返回0,失败返回-1

 接下来还是举一个例子来演示命名管道进程间通信

server创建管道文件,从client读取资源,结束通信后,client关闭管道文件并且删除管道文件

client获取管道文件,进行正常通信,向server发送数据

  1. //comm.hpp
  2. #pragma once
  3. #include <iostream>
  4. #include <string>
  5. #include <cassert>
  6. #include <ctime>
  7. #include <unistd.h>
  8. #include <sys/types.h>
  9. #include <sys/stat.h>
  10. #include <sys/wait.h>
  11. #include <fcntl.h>
  12. #define MODE 0666
  13. #define SIZE 128
  14. #define PROCESS_NUM 3
  15. std::string ipcPath = "./fifo.ipc";
  16. //log.hpp
  17. #include <iostream>
  18. #include <string>
  19. #include <ctime>
  20. #define Debug 0
  21. #define Notice 1
  22. #define Warning 2
  23. #define Error 3
  24. const std::string msg[] = {
  25. "Debug",
  26. "Notice",
  27. "Warning",
  28. "Error"};
  29. std::ostream &Log(std::string message, int level)
  30. {
  31. std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
  32. return std::cout;
  33. }
  34. //server.cpp
  35. #include "log.hpp"
  36. #include "comm.hpp"
  37. static void GetMessage(int fd)
  38. {
  39. char buffer[SIZE];
  40. while (true)
  41. {
  42. ssize_t s = read(fd, buffer, sizeof(buffer));
  43. if (s > 0)
  44. {
  45. buffer[s] = '\0';
  46. std::cout << "client call" << buffer << std::endl;
  47. }
  48. else if (s == 0)
  49. {
  50. std::cerr << "[ " << getpid() << " ]"
  51. << "read end of file server quit" << std::endl;
  52. break;
  53. }
  54. else
  55. {
  56. std::cerr << "read" << std::endl;
  57. break;
  58. }
  59. }
  60. }
  61. int main()
  62. {
  63. // 1、创建管道文件
  64. if (mkfifo(ipcPath.c_str(), MODE) < 0)
  65. {
  66. std::cerr << "mkfifo" << std::endl;
  67. exit(1);
  68. }
  69. Log("创建管道文件成功", Debug) << "step 1" << std::endl;
  70. // 2、正常文件操作
  71. int fd = open(ipcPath.c_str(), O_RDONLY);
  72. if (fd < 0)
  73. {
  74. std::cerr << "open" << std::endl;
  75. }
  76. Log("打开文件成功", Debug) << "step 2" << std::endl;
  77. // 3、正常通信代码
  78. // 使用进程池
  79. Log("进程间开始通信", Debug) << "step 3" << std::endl;
  80. for (size_t i = 0; i < PROCESS_NUM; i++)
  81. {
  82. pid_t id = fork();
  83. if (id < 0)
  84. {
  85. std::cerr << "fork" << std::endl;
  86. }
  87. else if (id == 0)
  88. {
  89. GetMessage(fd);
  90. exit(1);
  91. }
  92. }
  93. Log("进程间通信完成", Debug) << "step 4" << std::endl;
  94. // 4、回收子进程
  95. for (size_t i = 0; i < PROCESS_NUM; i++)
  96. {
  97. waitpid(-1, nullptr, 0);
  98. }
  99. Log("等待子进程成功", Debug) << "step 5" << std::endl;
  100. // 5、关闭文件
  101. close(fd);
  102. Log("关闭文件成功", Debug) << "step 6" << std::endl;
  103. // 5、删除文件
  104. unlink(ipcPath.c_str());
  105. Log("删除管道文件成功", Debug) << "step 7" << std::endl;
  106. return 0;
  107. }
  108. //clinet.cpp
  109. #include "comm.hpp"
  110. int main()
  111. {
  112. //1、获取管道文件
  113. int fd = open(ipcPath.c_str(), O_WRONLY);
  114. if(fd < 0)
  115. {
  116. std::cerr << "open" << std::endl;
  117. exit(1);
  118. }
  119. //2、IPC过程
  120. std::string buffer;
  121. while(true)
  122. {
  123. std::cout << "Please Enter Mseeage Line:> ";
  124. std::getline(std::cin, buffer);
  125. if(buffer == "quit")
  126. {
  127. break;
  128. }
  129. write(fd, buffer.c_str(), buffer.size());
  130. }
  131. //3、关闭管道文件,server自动停止读取
  132. close(fd);
  133. return 0;
  134. }

命名管道多个进程竞争读取,没有任何问题,发送数据大小是小于4096字节就是原子的

4、管道通信的特点

1、管道具有通过让进程间协同,提供了访问控制

2、管道提供的是面向流式的通信服务——面向字节流——协议

3、管道是基于文件的,文件的声明周期是随进程的,管道的声明周期是随进程的

4、管道是单向通信的,就是半双工通信的一种特殊情况

管道的访问控制

1、写快,读慢,写满就不能再写了

2、写慢,读快,管道没有数据的时候,读必须等待

3、写关,读0,标识读到了文件结尾

4、读关,写继续,OS终止写进程

三、System V IPC

1、共享内存

还要说一下通信的前提:让不同的进程能够看到同一份资源共享内存也是同样道理

不过它是直接向OS申请一块空间,然后映射到进程的共享区,这样就使得进程间通信的效率提升,因为不用使用系统调用多次拷贝资源了,进程可以直接访问那块共享的内存资源

1)申请空间

2)建立映射(多个进程映射同一块共享内存)

3)通信

4)去掉映射

5)释放空间


共享内存的提供者是操作系统,操作系统也要管理共享内存,通过先描述在组织

共享内存 = 共享内存块 + 对应的共享内存的内核数据结构

我们可以使用shmget函数来申请共享内存

它的返回值shmid是共享内存的用户标识符,类似于文件描述符fd

它的最后一个参数有两个选项IPC_CREAT and IPC_EXCL 

一般两者是组合使用的,单独使用IPC_EXCL是没有意义的

单独使用IPC_CREAT:如果创建共享内存,底层已经存在,那么就获取它,不存在就创建它

两者组合使用:如果底层不存在就创建它,并且返回新创建的shmid,如果底层存在,出错返回

接下来就是shmget的第一个参数key

key可以保证与我们进程通信的进程就是要通信的进程,并且能够看到我创建的共享内存

我们可以使用ftok函数来创建key,ftok是一种加密算法,使用同样的算法规则,传入的参数相同就能够形成唯一值,ftok的第一个参数pathname最好设定我们有访问权限的路径

shmid vs key

只有创建的时候使用key,大部分情况用户访问共享内存,都是使用的shmid

当我们的程序运行结束,我们的共享内存,还存在

System V IPC资源的生命周期随内核

解决办法

1、手动删除

ipcs -m可以获取共享内存相关属性

使用命令 ipcrm -m + shmid  可以删除共享内存 

2、代码删除

shmctl + 选项IPC_RMID可以删除共享内存 

共享内存链接方法

使用shmat可以将共享内存与进程建立映射,这个函数类似于malloc,返回值是void*可以强制转换为char*当作数组来使用共享内存

共享内存去关联方法

去除关联的方式是使用shmdt

注意:创建共享内存的大小最好是页(4096bytes)的整数倍

如果你申请4097个字节,操作系统会给你申请4096 * 2bytes,剩下的4095字节就浪费了

总结:

shmget返回值是用户层标识符

key是内核层面标定共享内存的标识符

想要自己挂接共享内存等对共享内存的操作要使用shmid,同时共享内存的生命周期是随内核的

shmat:挂接共享内存

shmdt:去关联

shmctl :删除共享内存

共享内存是在堆栈之间的共享区,它是属于用户的

用户空间:不用经过系统调用就可以访问的空间

管道的通信方式是文件,它是在内核空间中,它是内核的一种特定的数据结构,是操作系统维护的在[3G, 4G]的内核空间中,用户访问需要使用系统调用

下面看一个简单的例子

  1. //comm.hpp
  2. #pragma once
  3. #include <iostream>
  4. #include <string>
  5. #include <cassert>
  6. #include <cstring>
  7. #include "../named_pipe/log.hpp"
  8. #include <unistd.h>
  9. #include <sys/stat.h>
  10. #include <sys/types.h>
  11. #include <sys/ipc.h>
  12. #include <sys/shm.h>
  13. #include <fcntl.h>
  14. #define FIFO_NAME "./fifo"
  15. #define PROJ_ID 0x66
  16. #define SHM_SIZE 4096
  17. #define PATH_NAME "/home/ww"
  18. class Init
  19. {
  20. public:
  21. Init()
  22. {
  23. umask(0);
  24. int n = mkfifo(FIFO_NAME, 0666);
  25. assert(n == 0);
  26. Log("创建管道文件成功", Notice) << "\n";
  27. }
  28. ~Init()
  29. {
  30. unlink(FIFO_NAME);
  31. Log("删除管道文件成功", Notice) << "\n";
  32. }
  33. };
  34. #define READ O_RDONLY
  35. #define WRITE O_WRONLY
  36. int OpenFIFO(std::string pathname, int falgs)
  37. {
  38. int fd = open(pathname.c_str(), falgs);
  39. assert(fd >= 0);
  40. return fd;
  41. }
  42. void Wait(int fd)
  43. {
  44. Log("等待中", Notice) << "\n";
  45. uint32_t tmp = 0;
  46. ssize_t s = read(fd, &tmp, sizeof(uint32_t));
  47. assert(s == sizeof(uint32_t));
  48. }
  49. void Signal(int fd)
  50. {
  51. uint32_t tmp = 0;
  52. ssize_t s = write(fd, &tmp, sizeof(tmp));
  53. assert(s == sizeof(uint32_t));
  54. Log("唤醒中", Notice) << "\n";
  55. }
  56. void CloseFIFO(int fd)
  57. {
  58. close(fd);
  59. }
  60. //server.cpp
  61. #include "comm.hpp"
  62. Init init;
  63. int main()
  64. {
  65. // 1、生成相同的key
  66. key_t key = ftok(PATH_NAME, PROJ_ID);
  67. Log("生成key成功", Notice) << "key: " << key << std::endl;
  68. // 2、申请共享内存
  69. int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
  70. if (shmid < 0)
  71. {
  72. Log("申请共享内存失败", Error) << "shmid: " << shmid << std::endl;
  73. exit(2);
  74. }
  75. Log("申请共享内存成功", Notice) << "shmid: " << shmid << std::endl;
  76. // 3、共享内存与进程建立映射
  77. char *adder = (char *)shmat(shmid, nullptr, 0);
  78. Log("链接共享内存成功", Notice) << "shmAdder: " << adder << std::endl;
  79. // 4、正常通信
  80. Log("开始通信", Notice) << std::endl;
  81. // 共享内存是没有访问控制的,使用管道进行管道控制
  82. int fd = OpenFIFO(FIFO_NAME, READ);
  83. while (true)
  84. {
  85. Wait(fd);
  86. printf("%s\n", adder);
  87. if (strcmp(adder, "quit") == 0)
  88. break;
  89. }
  90. // 5、取消映射
  91. int ret = shmdt(adder);
  92. if (ret < 0)
  93. {
  94. Log("共享内存去关联失败", Error) << std::endl;
  95. }
  96. // 6、删除共享内存
  97. ret = shmctl(shmid, IPC_RMID, nullptr);
  98. if (ret < 0)
  99. {
  100. Log("删除共享内存失败", Error) << std::endl;
  101. exit(3);
  102. }
  103. return 0;
  104. }
  105. //client.cpp
  106. #include "comm.hpp"
  107. int main()
  108. {
  109. // 1、生成相同的key
  110. key_t key = ftok(PATH_NAME, PROJ_ID);
  111. Log("生成key成功", Notice) << "key: " << key << std::endl;
  112. // 2、获取共享内存
  113. int shmid = shmget(key, SHM_SIZE, 0);
  114. if (shmid < 0)
  115. {
  116. Log("获取共享内存失败", Error) << "shmid: " << shmid << std::endl;
  117. exit(2);
  118. }
  119. // 3、建立映射
  120. char *adder = (char *)shmat(shmid, nullptr, 0);
  121. Log("链接共享内存成功", Notice) << "shmAdder: " << adder << std::endl;
  122. // 4、通信
  123. Log("开始通信", Notice) << std::endl;
  124. int fd = OpenFIFO(FIFO_NAME, WRITE);
  125. while (true)
  126. {
  127. ssize_t s = read(0, adder, SHM_SIZE - 1);
  128. if (s > 0)
  129. {
  130. adder[s - 1] = 0;
  131. Signal(fd);
  132. if (strcmp(adder, "quit") == 0)
  133. break;
  134. }
  135. }
  136. // 5、取消映射
  137. int ret = shmdt(adder);
  138. if (ret < 0)
  139. {
  140. Log("取消共享内存失败", Error) << std::endl;
  141. }
  142. return 0;
  143. }

2、进程互斥

为了让进程间通信,让不同的进程之间看到同一份资源,之前所说的所有通信方式,本质都是优先解决一个问题,让不同的进程看到同一份资源

这样就会带来一些时序问题,造成数据不一致问题

1、多个进程看到的公共的一份资源叫做临界资源

2、把自己的进程访问临界资源的代码叫做临界区

3、为了更好的保护临界区,可以让在任何时刻都只能有一个进程进入临界区——互斥

4、原子性:要么不做,要么做完,没有中间状态


总结


以上就是今天要讲的内容,本文仅仅简单介绍了进程间通信的管道和共享内存

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/256291
推荐阅读
相关标签
  

闽ICP备14008679号