• Linux 进程间通信


    目录

    进程间通信目的

    进程间通信发展

    进程间通信分类

    管道

    什么是管道

    管道的原理

     用fork来共享管道原理

    站在文件描述符角度-深度理解管道

    匿名管道

    匿名管道代码

    管道读写规则

    管道特点(是管道并不止是匿名管道)

    命名管道

    创建一个命名管道

    命名管道的代码

    代码(进程池)

    makefile

    comm.h

    clientFifo.cpp

    serverFifo.cpp

    命名管道的打开规则

    匿名管道与命名管道的区别

    system V共享内存

    共享内存示意图

    共享内存函数

    shmget函数

    ​编辑

    ftok函数

    关于共享内存的更多细节

    shmctl 函数

    ​编辑

    shmat(a -- attach)关联共享内存

    shmdt函数

    shmctl函数

    使用

    Comm.hpp

    log.hpp

    Makefile

    IpcShmSer.cc

    IpcShmCli.cc

    结果

    分析

    上面的使用也可以再改一下

    client 去写

    server端

    ​编辑现象 

    分析(重要)

    进程互斥 


    C语言总结在这常见八大排序在这

    作者和朋友建立的社区:非科班转码社区-CSDN社区云💖💛💙

    期待hxd的支持哈🎉 🎉 🎉

    最后是打鸡血环节:想多了都是问题,做多了都是答案🚀 🚀 🚀

    最近作者和好友建立了一个公众号

    公众号介绍:

    专注于自学编程领域。由USTC、WHU、SDU等高校学生、ACM竞赛选手、CSDN万粉博主、双非上岸BAT学长原创。分享业内资讯、硬核原创资源、职业规划等,和大家一起努力、成长。(二维码在文章底部哈!

    进程间通信目的

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

    进程间通信发展

    管道
    System V 进程间通信
    POSIX 进程间通信
    本文主要讲述管道(System V 后面会讲,POSIX因为太老就不讲了)

    进程间通信分类

    管道

    匿名管道 pipe
    命名管道
    System V IPC
    System V 消息队列
    System V 共享内存
    System V 信号量
    POSIX IPC
    消息队列
    共享内存
    信号量
    互斥量
    条件变量
    读写锁

    管道

    什么是管道

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

     (这个 | 就是匿名管道,把who的输出 输入到 wc -l 进程 。同时我们也可以知道,管道是在      内核里面的)


    管道的原理

     用fork来共享管道原理

    站在文件描述符角度-深度理解管道

    所以管道也是内存级文件
    而且传输也是单向
    看看源码,这个里面就可以知道这个文件是管道文件还是磁盘文件还是字符设备
    如果是管道文件就可以看到

    所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想

    匿名管道

    #include
    功能 : 创建一无名管道
    原型
    int pipe(int fd[2]);
    参数
    fd :文件描述符数组 , 其中 fd[0] 表示读端 , fd[1] 表示写端
    返回值 : 成功返回 0 ,失败返回错误代码

    记忆技巧:

    0 - 》 嘴巴 -》 读
    1 -》 笔 -》写 

    匿名管道代码

    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. //演示pipe通信的基本过程 -- 匿名管道
    14. int main()
    15. {
    16. // 1. 创建管道
    17. int pipefd[2] = {0};
    18. if(pipe(pipefd) != 0)
    19. {
    20. cerr << "pipe error" << endl;
    21. return 1;
    22. }
    23. // 2. 创建子进程
    24. pid_t id = fork();
    25. if(id < 0)
    26. {
    27. cerr << "fork error" << endl;
    28. return 2;
    29. }
    30. else if (id == 0)
    31. {
    32. // child
    33. // 子进程来进行读取, 子进程就应该关掉写端
    34. close(pipefd[1]);
    35. #define NUM 1024
    36. char buffer[NUM];
    37. while(true)
    38. {
    39. cout<<"进入了子进程"<
    40. cout << "时间戳: " << (uint64_t)time(nullptr) << endl;
    41. // 子进程没有带sleep,为什么子进程你也会休眠呢??
    42. memset(buffer, 0, sizeof(buffer));
    43. //sleep(100);
    44. ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
    45. if(s > 0)
    46. {
    47. //读取成功
    48. buffer[s] = '\0';
    49. cout << "子进程收到消息,内容是: " << buffer << endl;
    50. }
    51. else if(s == 0)
    52. {
    53. cout << "父进程写完了,我也退出啦" << endl;
    54. break;
    55. }
    56. else{
    57. //Do Nothing
    58. }
    59. }
    60. close(pipefd[0]);
    61. exit(0);
    62. }
    63. else
    64. {
    65. // parent
    66. // 父进程来进行写入,就应该关掉读端
    67. close(pipefd[0]);
    68. const char *msg = "你好子进程,我是父进程, 这次发送的信息编号是: ";
    69. int cnt = 0;
    70. // while(cnt < 5)
    71. while(cnt<5)
    72. {
    73. char sendBuffer[1024];
    74. sprintf(sendBuffer, "%s : %d", msg, cnt);
    75. //sleep(30); // 这里是为了一会看现象明显
    76. sleep(1);
    77. write(pipefd[1], sendBuffer, strlen(sendBuffer)); //要不要+1 1,0
    78. cnt++;
    79. cout << "cnt: " << cnt << endl;
    80. }
    81. close(pipefd[1]);
    82. cout << "父进程写完了" << endl;
    83. }
    84. pid_t res = waitpid(id, nullptr, 0);
    85. if(res > 0)
    86. {
    87. cout << "等待子进程成功" << endl;
    88. }
    89. // 0 -> 嘴巴 -> 读(读书)
    90. // 1 -> 笔 -> 写的
    91. // cout << "fd[0]: " << pipefd[0] << endl;
    92. // cout << "fd[1]: " << pipefd[1] << endl;
    93. return 0;
    94. }

    运行结果

     

    但是我们运行的时候会大学子进程没有带sleep但是子进程也会随着父进程的休眠而休眠,是因为父进程和子进程读写是有顺序的!管道内部,没有数据,reader就必须阻塞等待(等待就是讲当前进程的PCB放入到等待队列中)(read),管道内部,如果数据被写满,writer就必须阻塞等待(write)。所以说,pipe内部自带访问控制机制-》同步和互斥机制(后面详细讲!)

    管道读写规则

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

    管道特点(是管道并不止是匿名管道)

    • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
    • 管道提供流式服务
    • 一般而言,进程退出,管道释放,所以管道的生命周期随进程
    • 一般而言,内核会对管道操作进行同步与互斥
    • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
    • 管道的生命周期

    PS:两个人在交互的时候一个人说一个人听就是半双工,就是一下只要一个进行读写

    命名管道

    创建一个命名管道

    命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
    $ mkfifo filename
    命名管道也可以从程序里创建,相关函数有:
    int mkfifo(const char *filename,mode_t mode);
    创建命名管道:
    1. int main(int argc, char *argv[])
    2. {
    3. mkfifo("p2", 0644);
    4. return 0;
    5. }

    命名管道的代码

    效果图

    代码(进程池)

    makefile

    1. .PHONY:all
    2. all: clientFifo serverFifo
    3. clientFifo:clientFifo.cpp
    4. g++ -Wall -o $@ $^ -std=c++11
    5. serverFifo:serverFifo.cpp
    6. g++ -Wall -o $@ $^ -std=c++11
    7. .PHONY:clean
    8. clean:
    9. rm -rf clientFifo serverFifo .fifo

    comm.h

    1. #pragma once
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #define IPC_PATH "./.fifo"

    clientFifo.cpp

    1. //写入
    2. #include "comm.h"
    3. using namespace std;
    4. int main()
    5. {
    6. int pipeFd = open(IPC_PATH, O_WRONLY);
    7. if(pipeFd < 0)
    8. {
    9. cerr << "open: " << strerror(errno) << endl;
    10. return 1;
    11. }
    12. #define NUM 1024
    13. char line[NUM];
    14. while(true)
    15. {
    16. printf("请输入你的消息# ");
    17. fflush(stdout);
    18. memset(line, 0, sizeof(line));
    19. // fgets -> C -> line结尾自动添加\0
    20. if(fgets(line, sizeof(line), stdin) != nullptr)
    21. {
    22. //abcd\n\0
    23. line[strlen(line) - 1] = '\0';
    24. write(pipeFd, line, strlen(line));
    25. }
    26. else
    27. {
    28. break;
    29. }
    30. }
    31. close(pipeFd);
    32. cout << "客户端退出啦" << endl;
    33. return 0;
    34. }

    serverFifo.cpp

    1. //读取
    2. #include "comm.h"
    3. using namespace std;
    4. int main()
    5. {
    6. umask(0);
    7. if(mkfifo(IPC_PATH, 0600) != 0)
    8. {
    9. cerr << "mkfifo error" << endl;
    10. return 1;
    11. }
    12. int pipeFd = open(IPC_PATH, O_RDONLY);
    13. if(pipeFd < 0)
    14. {
    15. cerr << "open fifo error" << endl;
    16. return 2;
    17. }
    18. #define NUM 1024
    19. //正常的通信过程
    20. char buffer[NUM];
    21. while(true)
    22. {
    23. ssize_t s = read(pipeFd, buffer, sizeof(buffer)-1);
    24. if(s > 0)
    25. {
    26. buffer[s] = '\0';
    27. cout << "客户端->服务器# " << buffer << endl;
    28. }
    29. else if(s == 0)
    30. {
    31. cout << "客户退出啦,我也退出把";
    32. break;
    33. }
    34. else
    35. {
    36. //do nothing
    37. cout << "read: " << strerror(errno) << endl;
    38. break;
    39. }
    40. }
    41. close(pipeFd);
    42. cout << "服务端退出啦" << endl;
    43. unlink(IPC_PATH);
    44. return 0;
    45. }

    命名管道的打开规则

    如果当前打开操作是为读而打开 FIFO
            O_NONBLOCK disable:阻塞直到有相应进程为写而打开该 FIFO
            O_NONBLOCK enable:立刻返回成功
    如果当前打开操作是为写而打开 FIFO
            O_NONBLOCK disable:阻塞直到有相应进程为读而打开该 FIFO
            O_NONBLOCK enable:立刻返回失败,错误码为 ENXIO

    匿名管道与命名管道的区别

    匿名管道由 pipe 函数创建并打开。
    命名管道由 mkfififo 函数创建,打开用 open
    FIFO (命名管道)与 pipe (匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。
    匿名管道:子进程继承父进程
    命名管道:通过一个fifo文件-》有路径-》具有唯一性-》通过路径,找到同一份资源

    system V共享内存

    共享内存区是最快 IPC 形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。

    共享内存示意图

    就是内存中的一块 空间,通过页表分别映射到不同的进程中,使多个进程看到同一份资源,而且不需要涉及内核,所以还很快。

    共享内存函数

    shmget函数

    功能:用来创建共享内存
    原型
    int shmget(key_t key, size_t size, int shmflg);
    参数
    key: 这个共享内存段名字,
    size: 共享内存大小(建议设置成4KB的整数倍(操作系统是以4KB来分配空间的))
    shmflg: 由九个权限标志构成,它们的用法和创建文件时使用的 mode 模式标志是一样的
    关于shmflg(重要):里面有两个宏,一个是IPC_CREAT一个是IPC_EXCL
    IPC_CREAT:创建共享内存,如果已经存在,就获取之,不存在,就创建。
    IPC_EXCL:不单独使用,必须和IPC_CREAT配合,如果不存在指定的共享内存,就创建,如果存在,出错返回。
    一起使用(使用 | ) ---》 可以保证如果shmget函数调用成功,那么就一定是一个全新的share memory!
    返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1。
    那么共享内存存在哪里?and 我怎么知道他存不存在?
    1. 内核中,内核会给我们维护共享内存的结构!共享内存也要被管理起来(先描述,后组织)
    2. 我怎么知道他存不存在。那就一定是有方法去标识共享内存的唯一性!
    共享内存要被管理 -> struct shmid_ds{} -> struct ipc_perm ->key(shmget)( 共享内存的唯一值)。而这个 key 一般是由 用户提供的!(就像前面命名管道约定一定用同一个管道文件一样,这里让他们使用同一个key就可以了!)

     这个里面的key就是上面shmget要传的key

    PS:创建共享内存时prem(权限)可以直接 |

    ftok函数

    ftok就是生成一个为一值,保证key的唯一性

    就是传一个路径和一个整形,然后就可以通过算法去转化生成一个有唯一性的整形(key_t本质就是整形,可以自己去看哈) 

    但是如果有使用过的小伙伴就会发现问题

    当我们运行完毕创建新的共享内存的代码后(进程退出了),但是当第二(n)次再去运行的时候,就会报错,告诉我们file是存在的即共享内存是存在的!那是因为 system V 下的共享内存,生命周期是随内核的!如果不显示去删除,那么只能通过kernel(OS)重启来解决了!

    那问题又来了,我们怎么知道有哪些IPC资源呢?

    ipcs -m(查看共享内存)/-q(消息队列)/-s(看信号量)

    如何去删除这些

    1. 命令删除共享内存 ipcrm -m  shmid(q/s)

    2. 代码删除 chmctl(系统命令)(共享内存控制)(不止可以删除)

    (你怎么使用的malloc的空间,就怎么使用共享内存的空间(也是一样的先申请,然后得到返回的指针,然后直接使用即可))

    关于共享内存的更多细节

    进程间通信的前提是先让不同的进程看到同一份资源,我们把共享内存实际上是映射到了我们进程地址空间的用户空间( 堆-》栈 之间),对于每一个进程而言,挂接到自己的上下文中的共享内存,属于自己的空间,类似于堆空间或栈空间,可以直接被用户使用。

    共享内存,因为他自身的特性,他没有任何访问控制,共享内存被双方之间看到,属于双方的用户空间。

    shmctl 函数

    shmat(a -- attach)关联共享内存

    功能:将共享内存段连接到进程地址空间
    原型
    void *shmat(int shmid, const void *shmaddr, int shmflg);
    参数
    shmid: 共享内存标识
    shmaddr: 指定连接的地址
    shmflg: 它的两个可能取值是 SHM_RND SHM_RDONLY
    返回值:成功返回一个指针,指向共享内存第一个节;失败返回 -1
    说明:
    shmaddr NULL ,核心自动选择一个地址
    shmaddr 不为 NULL shmflg SHM_RND 标记,则以 shmaddr 为连接地址。
    shmaddr 不为 NULL shmflg 设置了 SHM_RND 标记,则连接的地址会自动向下调整为 SHMLBA 的整数倍。公式: shmaddr -
    (shmaddr % SHMLBA)
    shmflg=SHM_RDONLY ,表示连接操作用来只读共享内存
    使用
    第 2 3 个参数直接默认就可以了

    shmdt函数

    功能:将共享内存段与当前进程脱离
    原型
    int shmdt(const void *shmaddr);
    参数
    shmaddr: shmat 所返回的指针
    返回值:成功返回 0 ;失败返回 -1
    注意:将共享内存段与当前进程脱离不等于删除共享内存段
    shmdt 的参数就是 shmat 的返回值
    ​​​​​​​

    shmctl函数

    功能:用于控制共享内存
    原型
    int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    参数
    shmid: shmget 返回的共享内存标识码
    cmd: 将要采取的动作(有三个可取值)
    buf: 指向一个保存着共享内存的模式状态和访问权限的数据结构
    返回值:成功返回 0 ;失败返回 -1

    ​​​​​​​

    使用

    Comm.hpp

    1. #pragma once
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #define PATH_NAME "/home/lml/linux"
    10. #define PROJ_ID 0x14
    11. #define MEM_SIZE 4096
    12. key_t CreateKey()
    13. {
    14. key_t key = ftok(PATH_NAME, PROJ_ID);
    15. if(key < 0)
    16. {
    17. std::cerr <<"ftok: "<< strerror(errno) << std::endl;
    18. exit(1);
    19. }
    20. return key;
    21. }

    log.hpp

    1. #pragma once
    2. #include
    3. #include
    4. std::ostream &Log()
    5. {
    6. std::cout << "Fot Debug |" << " timestamp: " << (uint64_t)time(nullptr) << " | ";
    7. return std::cout;
    8. }

    Makefile

    1. .PHONY:all
    2. all: IpcShmCli IpcShmSer
    3. IpcShmCli:IpcShmCli.cc
    4. g++ -o $@ $^ -std=c++11
    5. IpcShmSer:IpcShmSer.cc
    6. g++ -o $@ $^ -std=c++11
    7. .PHONY:clean
    8. clean:
    9. rm -f IpcShmCli IpcShmSer

    IpcShmSer.cc

    1. #include "Comm.hpp"
    2. #include "Log.hpp"
    3. #include
    4. using namespace std;
    5. // 我想创建全新的共享内存
    6. const int flags = IPC_CREAT | IPC_EXCL;
    7. // 充当使用共享内存的角色
    8. int main()
    9. {
    10. key_t key = CreateKey();
    11. Log() << "key: " << key << "\n";
    12. Log() << "create share memory begin\n";
    13. int shmid = shmget(key, MEM_SIZE, flags | 0666);
    14. if (shmid < 0)
    15. {
    16. Log() << "shmget: " << strerror(errno) << "\n";
    17. return 2;
    18. }
    19. Log() << "create shm success, shmid: " << shmid << "\n";
    20. sleep(5);
    21. // 1. 将共享内存和自己的进程产生关联attach
    22. char *str = (char *)shmat(shmid, nullptr, 0);
    23. Log() << "attach shm : " << shmid << " success\n";
    24. sleep(5);
    25. // 用它(提供数据给Cli去使用)
    26. str[0]='1';
    27. str[1]='2';
    28. str[2]='3';
    29. str[3]='4';
    30. str[4]='\0';
    31. int cnt=10;
    32. while(cnt--)
    33. {
    34. sleep(1);
    35. }
    36. // 2. 去关联
    37. shmdt(str);
    38. Log() << "detach shm : " << shmid << " success\n";
    39. sleep(5);
    40. // 删它
    41. shmctl(shmid, IPC_RMID, nullptr);
    42. Log() << "delete shm : " << shmid << " success\n";
    43. sleep(5);
    44. return 0;
    45. }

    IpcShmCli.cc

    1. #include "Comm.hpp"
    2. #include "Log.hpp"
    3. #include
    4. using namespace std;
    5. // 充当使用共享内存的角色
    6. int main()
    7. {
    8. // 创建相同的key值
    9. key_t key = CreateKey();
    10. Log() << "key: " << key << "\n";
    11. // 获取共享内存
    12. int shmid = shmget(key, MEM_SIZE, IPC_CREAT);
    13. if (shmid < 0)
    14. {
    15. Log() << "shmget: " << strerror(errno) << "\n";
    16. return 2;
    17. }
    18. // 挂接
    19. char *str = (char*)shmat(shmid, nullptr, 0);
    20. // 用它
    21. //sleep(5);
    22. int cnt=5;
    23. while(cnt--)
    24. {
    25. //使用Ser提供的数据
    26. cout<
    27. sleep(1);
    28. }
    29. // 去关联
    30. shmdt(str);
    31. return 0;
    32. }

    结果

    分析

    这里可以看出三种情况,当Ser启动之后

    第一种情况就是Ser还没有提供数据,Cli打印没有东西

    第二种情况就是提供了数据,然后Cli可以一直拿到并打印(数据不会因为你拿了就消失或者改变)

    第三种情况就是 Ser delete之后,Cli再去拿数据会发生段错误!

    PS:

    创建共享内存时prem(权限)可以直接 |

    nattch -- 挂接共享内存
    shmat(a -- attach)关联共享内存
    shmdt 去关联共享内存 

    上面的使用也可以再改一下

    client 去写

    server端​​​​​​​

    现象 

    分析(重要):

    但是我们发现就算client端没有写的时候,server端也在打印,也就是说没有访问控制!属于双方的内存空间,但是不安全(与管道相比较)
    对于共享内存是非常快的:
    管道:先是外设写入到进程,然后进程写到内核的管道,再管道再到进程,进程再输出。(四次拷贝)
    共享内存:是外设直接写入到进程的时候,其实就是写入到了共享内存,然后另一个进程之间就能看到,之间输出就可以了。所以说共享内存快!(两次拷贝) 

    进程互斥 

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

    最后的最后,创作不易,希望读者三连支持💖

    赠人玫瑰,手有余香💖​​​​​​​

  • 相关阅读:
    Word控件Spire.Doc 【文本】教程(19) ;如何在 C#、VB.NET 中通过 Word 中的正则表达式查找和替换文本
    ubuntu 配置 vino-server
    Jmeter实现在请求param和body里面加入随机参数
    【微信小程序】6天精准入门(第1天:小程序入门)
    看我如何连夜自建网站背刺我的求职对手们
    如何写好一篇学术论文
    Nuxt 3组件开发与管理
    activiti命令模式与责任链模式
    关于Ajax的深入学习
    软件测试人员如何提升自己?写给职场中迷茫的你。
  • 原文地址:https://blog.csdn.net/weixin_62700590/article/details/127739719