• 进程间的通信


    目录

    一.进程间通信的介绍

    1.1通信的目的

    1.2进程间通信的分类

    二.匿名管道

    2.1概念

    2.1.2使用步骤

    2.2管道读写规则

    2.3特点

    2.4多进程派发任务的实现

    2.5管道大小

    三.命名管道

    3.1概念

    3.2命名管道的打开规则

    3.3命名管道演示

    3.3.1用命名管道实现serve&client通信 

     3.3.2用命名管道实现进程控制

    3.4命名管道和匿名管道的区别

    3.5命令行中的管道

    四.system V进程间通信

    4.1system V共享内存

    4.1.1共享内存的建立与释放

    4.1.2创建

     4.1.3释放

    4.1.4关联

     4.1.5去关联

    4.1.6用共享内存实现serve&client通信

     4.1.7共享内存与管道进行对比

    五.消息队列

    六.System V信号量


    一.进程间通信的介绍

    概念:进程间通信简称IPC(Interprocess communication),进程间通信就是在不同进程之间传播或交换信息。

    1.1通信的目的

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

    本质就是让不同的进程可以看到同一份资源。

    每个进程都是独立的,各个进程之间若想实现通信,一定要借助第三方资源,这些进程就可以通过向这个第三方资源写入或是读取数据,进而实现进程之间的通信,这个第三方资源实际上就是操作系统提供的一段内存区域。

    b740fa5b5ac240a99478ab4490e7f181.png

    1.2进程间通信的分类

    管道

    • 匿名管道
    • 命名管道

    System V IPC

    • System V 消息队列
    • System V 共享内存
    • System V 信号量

    POSIX IPC

    • 消息队列
    • 共享内存
    • 信号量
    • 互斥量
    • 条件变量
    • 读写锁

    二.匿名管道

    管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的数据流称为一个“管道”。

    183ed0dceaeb443e94b055f568affb56.png

    2.1概念

    匿名管道用于进程间通信,但仅限于本地父子进程之间的通信。使用匿名管道通信,可以让父子进程看到同一份资源,父子进程可以对其进行读写操作,从而实现通信。

    如图:

    5d59616c9628458aaec82742c08683f2.png

     补充:

    1.这里父子进程看到的同一份资源是由操作系统维护的,父子进程对其进行读写操作时不会发生写时拷贝。

    2.管道虽然用的是文件的方案,但操作系统不会把进程进行通信的数据刷新到磁盘当中,因为这样做有IO参与会降低效率。也就是说,这种文件是一批不会把数据写到磁盘当中的文件,换句话说,磁盘文件和内存文件不一定是一一对应的,有些文件只会在内存当中存在,而不会在磁盘当中存在。

    创建匿名管道的函数:

    int pipe(int pipefd[2]);

    解释:pipe函数的参数是一个输出型参数,数组pipefd用于返回两个指向管道读端和写端的文件描述符。函数调用成功返回0,失败返回-1。

    数组元素含义
    pipefd[0]管道读端的文件描述符
    pipefd[1]管道写端的文件描述符

    2.1.2使用步骤

    创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用.

    例如:

    1.先由父进程调用pipe函数,创建管道。

    2.父进程在创建子进程。

    3.父进程关闭写端,子进程关闭读端。

    也可站在文件描述的角度来看待:

    c698198d6ef0410cbe53a7e4d0bf9b30.png

    755e488b7b1a4a10a498a36afddae27d.png

    c87a9caf65bd49a5954c1a74f723cadb.png

     用代码进行演示:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. int main()
    8. {
    9. int fd[2] = { 0 };
    10. if (pipe(fd) < 0){ //使用pipe创建匿名管道
    11. perror("pipe");
    12. return 1;
    13. }
    14. pid_t id = fork(); //使用fork创建子进程
    15. if (id == 0)
    16. {
    17. close(fd[0]); //子进程关闭读端
    18. char arr[20]="hello world";
    19. for(int i=0;i<5;i++)
    20. { write(fd[1], arr, strlen(arr)); //子进程向管道写入数据
    21. sleep(1);
    22. }
    23. close(fd[1]); //子进程写入完毕,关闭文件
    24. exit(0);
    25. }
    26. close(fd[1]); //父进程关闭写端
    27. char buff[64];
    28. while (true)
    29. {
    30. ssize_t s = read(fd[0], buff, sizeof(buff));//父进程从管道读取数据
    31. if (s > 0){
    32. buff[s] = '\0';
    33. printf("child send to father:%s\n", buff);
    34. buff[0]='\0'
    35. }
    36. else if (s == 0){
    37. printf("read file end\n");
    38. break;
    39. }
    40. else{
    41. printf("read error\n");
    42. break;
    43. }
    44. }
    45. int status=0;
    46. close(fd[0]); //父进程读取完毕,关闭文件
    47. pid_t ret=waitpid(id,&status,0);
    48. if(ret>0)
    49. {
    50. printf("wait success, ret : %d, 我所等待的子进程的退出码: %d, 退出信号是: %d\n",
    51. ret, (status>>8)&0xFF, status&0x7F);
    52. }
    53. return 0;
    54. }

    结果:

    80eb9f4fbc0e44638a1faad882b5d904.png

    2.2管道读写规则

    pipe2函数:int pipe2(int pipefd[2], int flags);

     解释:

    pipe2函数的第二个参数用于设置选项。

    1、当没有数据可读时:

    O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来为止。
    O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
    2、当管道满的时候:

    O_NONBLOCK disable:write调用阻塞,直到有进程读走数据。
    O_NONBLOCK enable:write调用返回-1,errno值为EAGAIN。
    3、如果所有管道写端对应的文件描述符被关闭,则read返回0。
    4、如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出。
    5、当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。
    6、当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。

    2.3特点

    1.管道内部自带同步与互斥机制。

    我们将一次只允许一个进程使用的资源,称为临界资源。管道在同一时刻只允许一个进程对其进行写入或是读取操作,因此管道也就是一种临界资源。

    2.管道生命周期随进程结束而结束。

    管道本质上是通过文件进行通信的,也就是说管道依赖于文件系统,那么当所有打开该文件的进程都退出后,该文件也就会被释放掉,所以说管道的生命周期随进程。

    3.管道提供的是流式服务。

    一个进程从管道取走的数据是任意的,这种被称为流式服务。

    数据报服务:数据有明确的分割,拿数据按报文段。

    4.管道是半双工通信

    在数据通信中,数据在线路上的传送方式可以分为以下三种:

    单工通信(Simplex Communication):单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定为接收端。
    半双工通信(Half Duplex):半双工数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。
    全双工通信(Full Duplex):全双工通信允许数据在两个方向上同时传输,它的能力相当于两个单工通信方式的结合。全双工可以同时(瞬时)进行信号的双向传输。例如:

    f7e77b75f62c477888a319ef1579d2d5.png

     管道的四种特殊情况:

    1.写端进程不写,读端进程一直读,那么此时会因为管道里面没有数据可读,对应的读端进程会被挂起,直到管道里面有数据后,读端进程才会被唤醒。
    2.读端进程不读,写端进程一直写,那么当管道被写满后,对应的写端进程会被挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。
    3.写端进程将数据写完后将写端关闭,那么读端进程将管道当中的数据读完后,就会继续执行该进程之后的代码逻辑,而不会被挂起。
    4.读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么操作系统会将写端进程杀掉。

    这里对第四种情况代码演示:

    1. int main()
    2. {
    3. int fd[2] = { 0 };
    4. if (pipe(fd) < 0){ //使用pipe创建匿名管道
    5. perror("pipe");
    6. return 1;
    7. }
    8. pid_t id = fork(); //使用fork创建子进程
    9. if (id == 0)
    10. {
    11. close(fd[0]); //子进程关闭读端
    12. char arr[20]="hello world";
    13. for(int i=0;i<5;i++)
    14. { write(fd[1], arr, strlen(arr)); //子进程向管道写入数据
    15. sleep(1);
    16. }
    17. close(fd[1]); //子进程写入完毕,关闭文件
    18. exit(0);
    19. }
    20. close(fd[1]); //父进程关闭写端
    21. char buff[64];
    22. // while (true)
    23. // {
    24. // ssize_t s = read(fd[0], buff, sizeof(buff));//父进程从管道读取数据
    25. // if (s > 0){
    26. // buff[s] = '\0';
    27. // printf("child send to father:%s\n", buff);
    28. // buff[0]='\0';
    29. // }
    30. // else if (s == 0){
    31. // printf("read file end\n");
    32. // break;
    33. // }
    34. // else{
    35. // printf("read error\n");
    36. // break;
    37. // }
    38. // }
    39. int status=0;
    40. close(fd[0]); //父进程读取完毕,关闭文件
    41. pid_t ret=waitpid(id,&status,0);
    42. if(ret>0)
    43. {
    44. printf("wait success, ret : %d, 我所等待的子进程的退出码: %d, 退出信号是: %d\n",
    45. ret, (status>>8)&0xFF, status&0x7F);
    46. }
    47. return 0;
    48. }

    与上面的代码一样,只是将父进程读写都关闭了。

    看结果:

    9efac95e8ec44023938c01e550969812.png

     可知:当情况4发生时,操作系统会向子进程发送第13号信号,将其终止掉。

    2.4多进程派发任务的实现

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    12. using namespace std;
    13. typedef void (*functor)();
    14. vector functors; // 方法集合
    15. unordered_map<uint32_t, string> info;
    16. void f1()
    17. {
    18. cout << "这是一个处理日志的任务, 执行的进程 ID [" << getpid() << "]"
    19. << "执行时间是[" << time(nullptr) << "]\n" << endl;
    20. }
    21. void f2()
    22. {
    23. cout << "这是一个备份数据任务, 执行的进程 ID [" << getpid() << "]"
    24. << "执行时间是[" << time(nullptr) << "]\n" << endl;
    25. }
    26. void f3()
    27. {
    28. cout << "这是一个处理网络连接的任务, 执行的进程 ID [" << getpid() << "]"
    29. << "执行时间是[" << time(nullptr) << "]\n" << endl;
    30. }
    31. void loadFunctor()
    32. {
    33. info.insert({functors.size(), "处理日志的任务"});
    34. functors.push_back(f1);
    35. info.insert({functors.size(), "备份数据任务"});
    36. functors.push_back(f2);
    37. info.insert({functors.size(), "处理网络连接的任务"});
    38. functors.push_back(f3);
    39. }
    40. typedef std::pair<int32_t, int32_t> elem;
    41. int processNum = 5;
    42. void work(int blockFd)//blockFd:子进程对应管道的fd,从fd读取任务序号(0,1,2)
    43. {
    44. cout << "进程[" << getpid() << "]" << " 开始工作" << endl;
    45. while (true)
    46. {
    47. uint32_t operatorCode = 0;
    48. ssize_t s = read(blockFd, &operatorCode, sizeof(uint32_t));
    49. if(s == 0)
    50. break;
    51. assert(s == sizeof(uint32_t));
    52. (void)s;
    53. if(operatorCode < functors.size())
    54. functors[operatorCode]();//子进程执行任务
    55. }
    56. cout << "进程[" << getpid() << "]" << " 结束工作" << endl;
    57. }
    58. // [子进程的pid, 子进程的管道fd]
    59. void blanceSendTask(const vector &processFds)
    60. {
    61. srand((long long)time(nullptr));
    62. int n=5;
    63. while(n--)
    64. {
    65. sleep(1);
    66. uint32_t pick = rand() % processFds.size();//随机选取一个子进程
    67. uint32_t task = rand() % functors.size();//随机选取一个任务
    68. write(processFds[pick].second, &task, sizeof(task));//向对应子进程的管道fd写入任务序号
    69. cout << "父进程指派任务->" << info[task] << "给进程: " << processFds[pick].first << " 编号: " << pick << endl;
    70. }
    71. }
    72. int main()
    73. {
    74. loadFunctor();
    75. vector assignMap;
    76. for (int i = 0; i < processNum; i++)
    77. {
    78. int pipefd[2] = {0};
    79. pipe(pipefd);//创建processNum个管道
    80. printf("创建管道成功,%d %d\n",pipefd[0],pipefd[1]);
    81. pid_t id = fork();//创建processNum个子进程
    82. if (id == 0)
    83. {
    84. close(pipefd[1]);//子进程关闭管道的写端
    85. work(pipefd[0]);//子进程工作代码
    86. close(pipefd[0]);//子进程关闭对应管道的读端
    87. exit(0);
    88. }
    89. close(pipefd[0]);//父进程关闭管道的读端
    90. elem e(id, pipefd[1]);//将子进程id和对应管道的写端描述符保存
    91. assignMap.push_back(e);
    92. }
    93. cout << "create all process success!" << std::endl;
    94. blanceSendTask(assignMap);//分派任务
    95. // 回收资源
    96. for(int i=0;i
    97. {
    98. close(assignMap[i].second);
    99. cout<<"关闭父进程对应文件描述符"<"成功"<
    100. }
    101. for(int i=0;i
    102. {
    103. if (waitpid(assignMap[i].first, nullptr, 0) > 0)
    104. cout << "wait for: pid=" << assignMap[i].first << " wait success!"
    105. << "number: " << i << "\n";
    106. }
    107. }

    结果:

    1. [LF@ecs-100710 lesson1]$ ./test
    2. 创建管道成功,3 4
    3. 创建管道成功,3 5
    4. 创建管道成功,3 6
    5. 创建管道成功,3 7
    6. 创建管道成功,3 8
    7. create all process success!
    8. 进程[24382] 开始工作
    9. 进程[24383] 开始工作
    10. 进程[24379] 开始工作
    11. 进程[24380] 开始工作
    12. 进程[24381] 开始工作
    13. 父进程指派任务->处理网络连接的任务给进程: 24383 编号: 4
    14. 这是一个处理网络连接的任务, 执行的进程 ID [24383]执行时间是[1667992038]
    15. 父进程指派任务->备份数据任务给进程: 24381 编号: 2
    16. 这是一个备份数据任务, 执行的进程 ID [24381]执行时间是[1667992039]
    17. 父进程指派任务->处理网络连接的任务给进程: 24382 编号: 3
    18. 这是一个处理网络连接的任务, 执行的进程 ID [24382]执行时间是[1667992040]
    19. 父进程指派任务->处理网络连接的任务给进程: 24382 编号: 3
    20. 这是一个处理网络连接的任务, 执行的进程 ID [24382]执行时间是[1667992041]
    21. 父进程指派任务->备份数据任务给进程: 24383 编号: 4
    22. 关闭父进程对应文件描述符4成功
    23. 关闭父进程对应文件描述符5成功
    24. 关闭父进程对应文件描述符6成功
    25. 关闭父进程对应文件描述符7成功
    26. 关闭父进程对应文件描述符8成功
    27. 这是一个备份数据任务, 执行的进程 ID [24383]执行时间是[1667992042]
    28. 进程[24383] 结束工作
    29. 进程[24382] 结束工作
    30. 进程[24381] 结束工作
    31. 进程[24380] 结束工作
    32. 进程[24379] 结束工作
    33. wait for: pid=24379 wait success!number: 0
    34. wait for: pid=24380 wait success!number: 1
    35. wait for: pid=24381 wait success!number: 2
    36. wait for: pid=24382 wait success!number: 3
    37. wait for: pid=24383 wait success!number: 4

    2.5管道大小

    管道的容量是有限的,如果管道满了。那么写端将阻塞或失败,那么管道的最大容量是多少?

    代码测试:

    e72861c518ab43a3bf727ba3d893a707.png

     结果:

    62d50089807a4cab8eeb4774b8ee5ea5.png

    在读端进程不读取的情况下,写端进程最多写65536字节的数据就被操作系统挂起,意思就是当前Linux版本中管道的最大容量为65536字节。也可用man手册去查看。

    三.命名管道

    3.1概念

    概念:匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间的通信,但若两个不相关之间的进程通信,可以使用命名管道来完成。命名管道就是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像,但这个映像的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中。

    int mkfifo(const char *pathname, mode_t mode);

    67d1593515a642d59f4eb7037c20100a.png

     参数解释:

    mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件。

    • 若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。
    • 若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。(注意当前路径的含义)

    mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限。会受到umask(文件默认掩码)的影响 。

    3.2命名管道的打开规则

    1、如果当前打开操作是为读而打开FIFO时。

    O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO。
    O_NONBLOCK enable:立刻返回成功。


    2、如果当前打开操作是为写而打开FIFO时。

    O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO。
    O_NONBLOCK enable:立刻返回失败,错误码为ENXIO。

    3.3命名管道演示

    3.3.1用命名管道实现serve&client通信 

    解释:实现服务端(server)和客户端(client)之间的通信之前,我们需要先让服务端运行起来,我们需要让服务端运行后创建一个命名管道文件,然后再以读的方式打开该命名管道文件,之后服务端就可以从该命名管道当中读取客户端发来的通信信息了。

    代码示例:

    共用头文件代码:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #define FILE_NAME "myfifo" //让客户端和服务端使用同一个命名管道

    服务端代码:

    1. #include "comm.h"
    2. int main()
    3. {
    4. umask(0); //将文件默认掩码设置为0
    5. if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
    6. perror("mkfifo");
    7. return 1;
    8. }
    9. int fd = open(FILE_NAME, O_RDONLY); //以读的方式打开命名管道文件
    10. if (fd < 0){
    11. perror("open");
    12. return 2;
    13. }
    14. char msg[128];
    15. while (1){
    16. //从命名管道当中读取信息
    17. ssize_t s = read(fd, msg, sizeof(msg)-1);
    18. if (s > 0){
    19. msg[s] = '\0'; //手动设置'\0',便于输出
    20. printf("client# %s\n", msg); //输出客户端发来的信息
    21. }
    22. else if (s == 0){
    23. printf("client quit!\n");
    24. break;
    25. }
    26. else{
    27. printf("read error!\n");
    28. break;
    29. }
    30. }
    31. close(fd); //通信完毕,关闭命名管道文件
    32. printf("客户端退出了,我服务端也退出了\n");
    33. unlink(FILE_NAME);
    34. return 0;
    35. }

    客户端代码:

    1. #include "comm.h"
    2. int main()
    3. {
    4. int fd = open(FILE_NAME, O_WRONLY); //以写的方式打开命名管道文件
    5. if (fd < 0){
    6. perror("open");
    7. return 1;
    8. }
    9. char msg[128];
    10. while (1){
    11. printf("Please Enter# "); //提示客户端输入
    12. fflush(stdout);
    13. //从客户端的标准输入流读取信息
    14. ssize_t s = read(0, msg, sizeof(msg)-1);
    15. if (s > 0){
    16. msg[s - 1] = '\0';
    17. //将信息写入命名管道
    18. write(fd, msg, strlen(msg));
    19. }
    20. }
    21. close(fd); //通信完毕,关闭命名管道文件
    22. return 0;
    23. }

    结果:

    3a7562ce3d93419084370fdd7d45ab1a.png

     3.3.2用命名管道实现进程控制

    只需要更改下客户端的代码即可:

    1. while (1){
    2. //从命名管道当中读取信息
    3. ssize_t s = read(fd, msg, sizeof(msg)-1);
    4. if (s > 0){
    5. msg[s] = '\0'; //手动设置'\0',便于输出
    6. printf("client# %s\n", msg); //输出客户端发来的信息
    7. if (fork() == 0)
    8. {
    9. //child
    10. execlp(msg, msg, NULL); //进程程序替换
    11. exit(1);
    12. }
    13. waitpid(-1, NULL, 0); //等待子进程
    14. }

    结果:

    eb769ed37a074d9d947ec00248b8a5fd.png

    3.4命名管道和匿名管道的区别

    • 匿名管道由pipe函数创建并打开。
    • 命名管道由mkfifo函数创建,由open函数打开。
    • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在于它们创建与打开的方式不同,一旦这些工作完成之后,它们具有相同的语义

    3.5命令行中的管道

    匿名管道只能用于有亲缘关系的进程之间的通信,而命名管道可以用于两个毫不相关的进程之间的通信,所以我们可以先看看命令行当中用管道(“|”)连接起来的各个进程之间是否具有亲缘关系。

     a9c0c3b25c8e49768b259c2922ac3bfa.png

    978714c920e24260a2e758e9b9f91237.png

     它们的父进程都是25187,可知 | 是匿名管道。

    四.system V进程间通信

    概念:而system V IPC是操作系统特地设计的一种通信方式,本质也是让不同进程看到同一份资源。

    system V IPC提供的通信方式有以下三种:

    system V共享内存
    system V消息队列
    system V信号量

    其中,system V共享内存和system V消息队列是以传送数据为目的的,而system V信号量是为了保证进程间的同步与互斥而设计的,虽然system V信号量和通信好像没有直接关系,但属于通信范畴。

    4.1system V共享内存

    原理:共享内存让不同进程看到同一份资源的方式就是,在物理内存当中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫做共享内存。
    d707cb55a10a459bb57e5a54fd234d14.png

    创建共享内存,操作系统一定要对其进行管理,所以共享内存除了在内存当中真正开辟空间之外,系统一定还要为共享内存维护相关的内核数据结构。

    其数据结构:

    1. struct shmid_ds {
    2. struct ipc_perm shm_perm; /* operation perms */
    3. int shm_segsz; /* size of segment (bytes) */
    4. __kernel_time_t shm_atime; /* last attach time */
    5. __kernel_time_t shm_dtime; /* last detach time */
    6. __kernel_time_t shm_ctime; /* last change time */
    7. __kernel_ipc_pid_t shm_cpid; /* pid of creator */
    8. __kernel_ipc_pid_t shm_lpid; /* pid of last operator */
    9. unsigned short shm_nattch; /* no. of current attaches */
    10. unsigned short shm_unused; /* compatibility */
    11. void *shm_unused2; /* ditto - used by DIPC */
    12. void *shm_unused3; /* unused */
    13. };

    当在内存中创建一块共享内存后,为了让要实现通信的进程能够看到同一个共享内存,因此每一个共享内存被申请时都有一个key值,这个key值用于标识系统中共享内存的唯一性。

    struct ipc_perm  shm_perm结构体中存有key值,其对应数据结构:

    1. struct ipc_perm{
    2. __kernel_key_t key;
    3. __kernel_uid_t uid;
    4. __kernel_gid_t gid;
    5. __kernel_uid_t cuid;
    6. __kernel_gid_t cgid;
    7. __kernel_mode_t mode;
    8. unsigned short seq;
    9. };

    4.1.1共享内存的建立与释放

    共享内存的建立大致包括以下两个过程:

    1. 在物理内存当中申请共享内存空间。
    2. 将申请到的共享内存挂接到地址空间,即建立映射关系。

    共享内存的释放大致包括以下两个过程:

    1. 将共享内存与地址空间去关联,即取消映射关系。
    2. 释放共享内存空间,即将物理内存归还给系统。

    4.1.2创建

    创建共享内存我们需要用shmget函数:

    int shmget(key_t key, size_t size, int shmflg);

    函数参数说明:

    • 第一个参数key,表示待创建共享内存在系统当中的唯一标识。
    • 第二个参数size,表示待创建共享内存的大小。
    • 第三个参数shmflg,表示创建共享内存的方式。

    函数返回值说明

    • shmget调用成功,返回一个有效的共享内存标识符(用户层标识符)。
    • shmget调用失败,返回-1。

    我们把具有标定某种资源能力的东西叫做句柄,而这里shmget函数的返回值实际上就是共享内存的句柄,这个句柄可以在用户层标识共享内存,当共享内存被创建后,我们在后续使用共享内存的相关接口时,可以通过这个句柄对指定共享内存进行各种操作。

     key值的获取

    可通过函数key_t ftok(const char *pathname, int proj_id)进行获取。

    ftok函数的作用就是,将一个已存在的路径名pathname和一个整数标识符proj_id转换成一个key值,称为IPC键值

    参数解释:1.pathname:是已经存在的文件路径名。2.一个指定的整数。

    注意:

    1. 使用ftok函数生成key值可能会产生冲突,此时可以对传入ftok函数的参数进行修改。
    2. 需要进行通信的各个进程,在使用ftok函数获取key值时,都需要采用同样的路径名和和整数标识符,进而生成同一种key值,然后才能找到同一个共享资源。

    shmflg的组合方式有两种

    6901da9cb58e4ffd9c2d05692575e4d9.png

     若使用第一种组合方式,会返回共享内存中的标识符,但不能确定该共享内存是已存在,还是新创建的。使用第二种方式,返回共享内存中的标识符,说明创建成功,且该共享内存是新创建的。若已存在,则会出错返回。

    代码演示:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #define PATHNAME "/home/LF/if/Linux/lesson2/tt.c" //路径名
    7. #define PROJ_ID 0x22//整数标识符
    8. #define SIZE 4096 //共享内存的大小
    9. int main()
    10. {
    11. key_t key = ftok(PATHNAME, PROJ_ID); //获取key值
    12. if (key < 0){
    13. printf("key失败\n");
    14. perror("ftok");
    15. return 1;
    16. }
    17. int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL); //创建新的共享内存
    18. if (shm < 0){
    19. perror("shmget");
    20. return 2;
    21. }
    22. printf("key: %x\n", key); //打印key值
    23. printf("shm: %d\n", shm); //打印句柄
    24. return 0;
    25. }

    结果查看:

    a12679fb6a9845d79374dd083070c79f.png

     解释:

    ipcs命令:

    会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看它们之间某一个的相关信息,可以选择携带以下选项:

    • -q:列出消息队列相关信息。
    • -m:列出共享内存相关信息。
    • -s:列出信号量相关信息

    ipcs命令输出的每列信息的含义如下:

    fa2f363ccdb043b49ba522965bcadef3.png

    注意: key是在内核层面上保证共享内存唯一性的方式,而shmid是在用户层面上保证共享内存的唯一性,key和shmid之间的关系类似于fd和FILE*之间的的关系.

     4.1.3释放

    我们的进程运行完毕后,申请的共享内存依旧存在,并没有被操作系统释放。实际上,管道的生命周期是随进程的,而共享内存的生命周期是随内核的。即使对应的进程退出了,但是曾经创建的共享内存不会随着进程的退出而释放。

    如果进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启(system V IPC都是如此),同时也说明了IPC资源是由内核提供并维护的。

    释放共享内存的方法:

    1.可以使用ipcrm -m shmid命令释放指定id的共享内存资源。

    例如:

    5617ead5dcd2435ea18ca98858669fe3.png

    2.使用程序释放共享内存;

    int shmctl(int shmid, int cmd, struct shmid_ds *buf);

    函数的参数说明:

    • 第一个参数shmid,表示所控制共享内存的用户级标识符。
    • 第二个参数cmd,表示具体的控制动作。
    • 第三个参数buf,用于获取或设置所控制共享内存的数据结构。

    shmctl函数的返回值说明:

    • shmctl调用成功,返回0。
    • shmctl调用失败,返回-1。

    shmctl函数的第二个参数传入的常用的选项有以下三个:

    3803ae371d884b20a2b4a5d9172f94c2.png

    代码演示 :

    1. int main()
    2. {
    3. key_t key = ftok(PATHNAME, PROJ_ID); //获取key值
    4. if (key < 0){
    5. printf("key失败\n");
    6. perror("ftok");
    7. return 1;
    8. }
    9. int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL); //创建新的共享内存
    10. if (shm < 0){
    11. perror("shmget");
    12. return 2;
    13. }
    14. printf("key: %x\n", key); //打印key值
    15. printf("shm: %d\n", shm); //打印句柄
    16. sleep(3);
    17. shmctl(shm, IPC_RMID, NULL); //释放共享内存
    18. sleep(3);
    19. return 0;
    20. }

    结果:用一段脚本监控查看过程:while :; do ipcs -m;echo "###################################";sleep 1;done

    5127cceace08452cb8f7562fe55ec4d9.png

    4.1.4关联

    共享内存连接到进程地址空间我们需要用shmat函数:

    void *shmat(int shmid, const void *shmaddr, int shmflg);

     函数的参数说明:

    • 第一个参数shmid,表示待关联共享内存的用户级标识符。
    • 第二个参数shmaddr,指定共享内存映射到进程地址空间的某一地址,通常设置为NULL,表示让内核自己决定一个合适的地址位置。
    • 第三个参数shmflg,表示关联共享内存时设置的某些属性。

    函数的返回值说明:

    • shmat调用成功,返回共享内存映射到进程地址空间中的起始地址。
    • shmat调用失败,返回(void*)-1。

    shmat函数的第三个参数传入的常用的选项有以下三个。

    fd8a09a0f1d2493581ed7590abb9915a.png

     4.1.5去关联

    取消共享内存与进程地址空间之间的关联我们需要用shmdt函数:

    int shmdt(const void *shmaddr); 

    函数的参数说明:

    • shmaddr:待去关联共享内存的起始地址,即调用shmat函数时得到的起始地址。

    函数的返回值说明:

    • shmdt调用成功,返回0。
    • shmdt调用失败,返回-1。

    代码演示:

    1. #define PATHNAME "/home/LF/if/Linux/lesson2/tt.c" //路径名
    2. #define PROJ_ID 0x22//整数标识符
    3. #define SIZE 4096 //共享内存的大小
    4. int main()
    5. {
    6. key_t key = ftok(PATHNAME, PROJ_ID); //获取key值
    7. if (key < 0){
    8. printf("key失败\n");
    9. perror("ftok");
    10. return 1;
    11. }
    12. int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL); //创建新的共享内存
    13. if (shm < 0){
    14. perror("shmget");
    15. return 2;
    16. }
    17. printf("创建共享内存成功\n");
    18. printf("key: %x\n", key); //打印key值
    19. printf("shm: %d\n", shm); //打印句柄
    20. sleep(2);
    21. char* p=shmat(shm,NULL,0);//关联共享内存
    22. if(p==(void*)-1)
    23. {
    24. perror("shmat");
    25. return 1;
    26. }
    27. printf("关联成功\n");
    28. sleep(2);
    29. int k= shmdt(p);//去关联
    30. if(k==-1)
    31. return 1;
    32. printf("去关联成功\n");
    33. sleep(2);
    34. int u= shmctl(shm, IPC_RMID, NULL); //释放共享内存
    35. if(u==-1)
    36. return 1;
    37. printf("释放共享内存成功\n");
    38. sleep(2);
    39. return 0;
    40. }

    结果:

    7d5f3db1e5e54953bddfd55d5c86ddce.png

     shmat函数出现了问题:它是去将进程与关联共享起来的。原因是我们使用shmget函数创建共享内存时,并没有对创建的共享内存设置权限,所以创建出来的共享内存的默认权限为0,即什么权限都没有,因此进程没有权限关联该共享内存。

    perm为权限

    43518d2cb184479b8d3bbc5696b375ff.png

     int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建权限为0666的共享内存

    将代码调整后:

    3dfcd1519c774835a29ceacc5083b59d.png

     用脚本查看:

    1. ------ Shared Memory Segments --------
    2. key shmid owner perms bytes nattch status
    3. ###################################
    4. ------ Shared Memory Segments --------
    5. key shmid owner perms bytes nattch status
    6. 0x220123c5 8 LF 666 4096 0
    7. ###################################
    8. ------ Shared Memory Segments --------
    9. key shmid owner perms bytes nattch status
    10. 0x220123c5 8 LF 666 4096 0
    11. ###################################
    12. ------ Shared Memory Segments --------
    13. key shmid owner perms bytes nattch status
    14. 0x220123c5 8 LF 666 4096 1
    15. ###################################
    16. ------ Shared Memory Segments --------
    17. key shmid owner perms bytes nattch status
    18. 0x220123c5 8 LF 666 4096 1
    19. ###################################
    20. ------ Shared Memory Segments --------
    21. key shmid owner perms bytes nattch status
    22. 0x220123c5 8 LF 666 4096 0
    23. ###################################
    24. ------ Shared Memory Segments --------
    25. key shmid owner perms bytes nattch status
    26. 0x220123c5 8 LF 666 4096 0
    27. ###################################
    28. ------ Shared Memory Segments --------
    29. key shmid owner perms bytes nattch status
    30. ###################################

    4.1.6用共享内存实现serve&client通信

    服务端负责创建共享内存,创建好后将共享内存和服务端进行关联.后让其死循环,查看是否成功。

    客户端只需要直接和服务端创建的共享内存进行关联即可,之后也进入死循环,便于观察客户端是否挂接成功。

    代码演示:

    serve.c代码:

    1. #include"comm.h"
    2. int main()
    3. {
    4. key_t key = ftok(PATHNAME, PROJ_ID); //获取key值
    5. if (key < 0){
    6. printf("key失败\n");
    7. perror("ftok");
    8. return 1;
    9. }
    10. int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL|0666); //创建新的共享内存
    11. if (shm < 0){
    12. perror("shmget");
    13. return 2;
    14. }
    15. printf("创建共享内存成功\n");
    16. printf("key: %x\n", key); //打印key值
    17. printf("shm: %d\n", shm); //打印句柄
    18. sleep(2);
    19. char* p=shmat(shm,NULL,0);//关联共享内存
    20. if(p==(void*)-1)
    21. {
    22. perror("shmat");
    23. return 1;
    24. }
    25. printf("关联成功\n");
    26. sleep(2);
    27. while(1){
    28. printf("%s\n", p);
    29. sleep(2);
    30. }
    31. int k= shmdt(p);//去关联
    32. if(k==-1)
    33. return 1;
    34. printf("去关联成功\n");
    35. sleep(2);
    36. int u= shmctl(shm, IPC_RMID, NULL); //释放共享内存
    37. if(u==-1)
    38. return 1;
    39. printf("释放共享内存成功\n");
    40. sleep(2);
    41. return 0;
    42. }

    client代码:

    1. #include"comm.h"
    2. int main()
    3. {
    4. key_t key = ftok(PATHNAME, PROJ_ID); //获取key值
    5. if (key < 0){
    6. printf("key失败\n");
    7. perror("ftok");
    8. return 1;
    9. }
    10. int shm = shmget(key, SIZE, IPC_CREAT); //创建新的共享内存
    11. if (shm < 0){
    12. perror("shmget");
    13. return 2;
    14. }
    15. printf("创建共享内存成功\n");
    16. printf("key: %x\n", key); //打印key值
    17. printf("shm: %d\n", shm); //打印句柄
    18. sleep(2);
    19. char* p=shmat(shm,NULL,0);//关联共享内存
    20. if(p==(void*)-1)
    21. {
    22. perror("shmat");
    23. return 1;
    24. }
    25. printf("关联成功\n");
    26. sleep(2);
    27. while(1)
    28. {
    29. printf("Please Enter# ");
    30. fflush(stdout);
    31. ssize_t s = read(0, p, SIZE);
    32. if(s > 0)
    33. {
    34. p[s] = '\0';
    35. }
    36. }
    37. int k= shmdt(p);//去关联
    38. if(k==-1)
    39. return 1;
    40. printf("去关联成功\n");
    41. sleep(2);
    42. int u= shmctl(shm, IPC_RMID, NULL); //释放共享内存
    43. if(u==-1)
    44. return 1;
    45. printf("释放共享内存成功\n");
    46. sleep(2);
    47. return 0;
    48. }

    头文件代码:

    1. [LF@ecs-100710 lesson2]$ cat comm.h
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #define PATHNAME "/home/LF/if/Linux/lesson2/tt.c" //路径名
    9. #define PROJ_ID 0x22//整数标识符
    10. #define SIZE 4096 //共享内存的大小

    bf7d9fe87d8340f98f385ecfb3082351.png

    c9b4600490fa4e9a8bb989681dcaf316.png

     4.1.7共享内存与管道进行对比

    共享内存创建好后,通信就可以不用到系统调用的接口。而管道创建好后仍需要read、write等系统接口进行通信。实际上,共享内存是所有进程间通信方式中最快的一种通信方式。

    7e275902881f4cd9b86ddba14b99029b.png

     再看共享内存的通信:

    277c3ef14b3a426b98c7146e7d05f13e.png

     注意:管道是自带同步与互斥机制的,但是共享内存并没有提供任何的保护机制,包括同步与互斥。

    五.消息队列

    概念:

    消息队列实际上就是在系统当中创建了一个队列,队列当中的每个成员都是一个数据块,这些数据块是由类型和信息组成,两个互相通信的进程可以通过某种方式看到同一个消息队列,这两个进程向对方发数据时,都在消息队列的队尾添加数据块,这两个进程获取数据块时,都在消息队列的队头取数据块。

    六.System V信号量

    信号量相关概念:
    由于进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系叫做进程互斥。
    系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。
    在进程中涉及到临界资源的程序段叫临界区。
    IPC资源必须删除,否则不会自动删除,因为system V IPC的生命周期随内核。

    保护临界资源的本质是保护临界区,我们把进程代码中访问临界资源的代码称之为临界区,信号量就是用来保护临界区的,信号量分为二元信号量和多元信号量。

  • 相关阅读:
    使用Ubuntu虚拟机离线部署RKE2高可用集群
    语法练习:front_back
    json的数据类型有哪些?json数据类型介绍
    docker上面部署nginx-waf 防火墙“modsecurity”,使用CRS规则,搭建WEB应用防火墙
    Py之yellowbrick:yellowbrick的简介、安装、使用方法之详细攻略
    扬帆际海:shopee店铺关键词广告怎么获得流量?
    如何为微服务选择正确的消息队列
    29.5.2 备份数据
    Linux 编译链接那些事儿(02)C++链接库std::__cxx11::basic_string和std::__1::basic_string链接问题总结
    流浪动物救助小程序|基于微信小程序的流浪动物救助系统设计与实现(源码+数据库+文档)
  • 原文地址:https://blog.csdn.net/m0_64397669/article/details/127628201