• Linux进程间通信


    进程间通信的概念

    进程间通信(IPC)全称 Interprocess communication ,进程通信就是在不同进程间传播或者交换信息
     

    进程间通信的目的

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


    进程间通信的本质

    让不同进程看到同一块内存,(同一块资源)

    在之前的 Linux进程概念中我们谈到 :进程具有独立性,多进程运行,需要独享各种资源,多进程运行期间互不干扰。,这在一定程度上加大了进程通信的难度。所以,我们通常借助第三方资源来进行数据通信。

    进程间通信的分类

    管道

    匿名管道pipe
    命名管道

    System V IPC

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

    POSIX IPC

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

    管道

    什么是管道

    我们把从一个进程连接到另一个进程的一个数据流称为管道,我们比较常见的  " | "就是典型的管道。

    示例:

    who | wc -l# 作用 统计我们当前使用云服务器上的登录用户个数。


     匿名管道

    匿名管道的原理

     让两个父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信。


            pipe函数

    参数解释:

    pipefd[0] :表示管道读端的文件描述符;pipefd[1] : 表示管道写端的文件描述符       

     匿名管道使用步骤

    1. 创建管道(父进程);

    2. 创建子进程;

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

     管道读写规则

    1. 管道只能够进行单向通信,因此当父进程创建完子进程后,需要明确父子进程谁读谁写,然后关闭相应的读写端。
    2. 从管道写端写入的数据会被内核缓冲,直到从管道的读端被读取,(往往需要调用fflush函数刷新缓冲区来观察效果)。

    管道的特点

    1、管道内部自带同步与互斥机制。(不用担心安全问题)

    • 同步: 两个或两个以上的进程在运行过程中协同步调,按预定的先后次序运行。比如,A任务的运行依赖于B任务产生的数据。
    • 互斥: 一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源。

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

    2、管道的生命周期随进程。

    管道依赖文件系统,只要当打开管道的所有进程退出后,管道文件才会退出

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

    • 流式服务: 数据没有明确的分割,不分一定的报文段。
    • 数据报服务: 数据有明确的分割,拿数据按报文段拿。

    管道提供的服务是管道文件中有多少就获取多少,(写多少读多少),这种服务属于流式服务

    4、管道是半双工通信的。

    我们之前提到,管道只能提供单向服务,只能单独“ 读 ” 或者单独 “ 写 ” ,不能同时 “ 读写 ” 。这种通信叫做 半双工通信,与之相对的是全双工通信,即 可以同时读写,这样虽然效率好像高了,但是也要求对临界资源有其他的保护手段。

    管道的四种特殊情况

      1.  写端进程不写,读端进程一直读,那么此时会因为管道里面没有数据可读,对应的读端进程会被挂起,直到管道里面有数据后,读端进程才会被唤醒。
      2. 读端进程不读,写端进程一直写,那么当管道被写满后,对应的写端进程会被挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。

    上述两种情况内核中可以通过调节同步与互斥来调节这种读写不均的问题。
      3. 写端进程将数据写完后将写端关闭,那么读端进程将管道当中的数据读完后,就会继续执行该进程之后的代码逻辑,而不会被挂起。

    没有数据可以读了,写段关闭后,读端结束读写逻辑去做之后的代码
      4. 读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么操作系统会将写端进程杀掉。
     为什么这种情况不会像第二种一样呢? ---读端已经关闭了,没有操作可以读了,os只能停止写端了,这种很明显是程序异常退出,那这种异常是什么呢。我们通过这个实验来观察

    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. //child
    17. close(fd[0]); //子进程关闭读端
    18. //子进程向管道写入数据
    19. const char* msg = "hello father, I am child...";
    20. int count = 10;
    21. while (count--){
    22. write(fd[1], msg, strlen(msg));
    23. sleep(1);
    24. }
    25. close(fd[1]); //子进程写入完毕,关闭文件
    26. exit(0);
    27. }
    28. //father
    29. close(fd[1]); //父进程关闭写端
    30. close(fd[0]); //父进程直接关闭读端(导致子进程被操作系统杀掉)
    31. int status = 0;
    32. waitpid(id, &status, 0);
    33. printf("child get signal:%d\n", status & 0x7F); //打印子进程收到的信号
    34. return 0;
    35. }

    13号信号对应的是 SIGPIPE  信号

    我们可以通过  man 7 signal  去查看信号的具体解释

    管道的大小

    通过上面几种情况的分析,我们不难得出管道是有大小限制的,我们可以通过以下两种方式查看管道大小:

    1. ulimit  -a       

    2.  man  7 pipe

    命名管道

    命名管道的原理

         如果要实现两个毫不相关进程之间的通信,可以使用命名管道来做到。命名管道就是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。   

    使用命令创建命名管道

    mkfifo fifo

      

    使用这个命名管道文件,就能实现两个进程之间的通信了。我们在一个进程(进程A)中用shell脚本每秒向命名管道写入一个字符串,在另一个进程(进程B)当中用cat命令从命名管道当中进行读取。

    1. while : ; do echo "hello";sleep 1;done > fifo
    2. cat < fifo


     这就证明了这两个毫不相关的进程可以通过命名管道进行数据传输,即通信。

    之前我们说过,当管道的读端进程退出后,写端进程再向管道写入数据就没有意义了,此时写端进程会被操作系统杀掉,

    在这里就可以很好的得到验证:当我们终止掉读端进程后,因为写端执行的循环脚本是由命令行解释器bash执行的,所以此时bash就会被操作系统杀掉,我们的云服务器也就退出了。

     创建一个命名管道

    我们通过使用函数 mkfifo函数创建命名管道

    函数介绍:

    参数解释:

    path name:要创建的命名管道文件。

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

    mode:     建命名管道文件的默认权限 

    示例:

    1. #include
    2. #include
    3. #include
    4. #define FILE_NAME "myfifo"
    5. int main()
    6. {
    7. umask(0); //将文件默认掩码设置为0
    8. if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
    9. perror("mkfifo");
    10. return 1;
    11. }
    12. //create success...
    13. return 0;
    14. }

        命名管道的打开规则

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

                    O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO。
                    O_NONBLOCK enable:立刻返回成功。
            2、如果当前打开操作是为写而打开FIFO时。

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

     

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

    server端

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

    client端

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #define FILE_NAME "myfifo" //让客户端和服务端使用同一个命名管道
    8. int main()
    9. {
    10. int fd = open(FILE_NAME, O_WRONLY); //以写的方式打开命名管道文件
    11. if (fd < 0){
    12. perror("open");
    13. return 1;
    14. }
    15. char msg[128];
    16. while (1){
    17. msg[0] = '\0'; //每次读之前将msg清空
    18. printf("Please Enter# "); //提示客户端输入
    19. fflush(stdout);
    20. //从客户端的标准输入流读取信息
    21. ssize_t s = read(0, msg, sizeof(msg)-1);
    22. if (s > 0){
    23. msg[s - 1] = '\0';
    24. //将信息写入命名管道
    25. write(fd, msg, strlen(msg));
    26. }
    27. }
    28. close(fd); //通信完毕,关闭命名管道文件
    29. return 0;
    30. }

     

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

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

    共享内存

    共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会称为一个进程用户空间的一部分,因此这种IPC机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。
    与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种IPC技术的速度更快。


     共享内存使用步骤 

    1. 调用shmget ()创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。

    2. 使用shmat ()来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分。

    3. 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存,程序需要使用由shmat ()调用返回的addr值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。
    4. 调用shmdt ()来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。
    5. 调用shmctl ()来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存段才会销毁。只有一个进程需要执行这一步。

    使用示例:

    服务端

    1. //server.c
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #define PATHNAME "/home/cl/Linuxcode/IPC/shm/server.c" //路径名
    9. #define PROJ_ID 0x6666 //整数标识符
    10. #define SIZE 4096 //共享内存的大小
    11. int main()
    12. {
    13. key_t key = ftok(PATHNAME, PROJ_ID); //获取key值
    14. if (key < 0){
    15. perror("ftok");
    16. return 1;
    17. }
    18. int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建新的共享内存
    19. if (shm < 0){
    20. perror("shmget");
    21. return 2;
    22. }
    23. printf("key: %x\n", key); //打印key值
    24. printf("shm: %d\n", shm); //打印共享内存用户层id
    25. char* mem = shmat(shm, NULL, 0); //关联共享内存
    26. while (1){
    27. //不进行操作
    28. }
    29. shmdt(mem); //共享内存去关联
    30. shmctl(shm, IPC_RMID, NULL); //释放共享内存
    31. return 0;
    32. }

    客户端:

    1. //client.c
    2. //comm.h
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #define PATHNAME "/home/cl/Linuxcode/IPC/shm/server.c" //路径名
    10. #define PROJ_ID 0x6666 //整数标识符
    11. #define SIZE 4096 //共享内存的大小
    12. int main()
    13. {
    14. key_t key = ftok(PATHNAME, PROJ_ID); //获取与server进程相同的key值
    15. if (key < 0){
    16. perror("ftok");
    17. return 1;
    18. }
    19. int shm = shmget(key, SIZE, IPC_CREAT); //获取server进程创建的共享内存的用户层id
    20. if (shm < 0){
    21. perror("shmget");
    22. return 2;
    23. }
    24. printf("key: %x\n", key); //打印key值
    25. printf("shm: %d\n", shm); //打印共享内存用户层id
    26. char* mem = shmat(shm, NULL, 0); //关联共享内存
    27. int i = 0;
    28. while (1){
    29. //不进行操作
    30. }
    31. shmdt(mem); //共享内存去关联
    32. return 0;
    33. }


     

  • 相关阅读:
    Shell sed编辑器
    (游戏:三个数的加法)编写程序,随机产生三个一位整数,并提示用户输入这三个整数的和,判断用户输入的和是否正确。
    git 工具学习笔记
    嵌入式物联网在医疗行业中的应用——案例分析
    【深度学习】模型过拟合的原因以及解决办法
    Excel VLOOKUP实用教程之 02 vlookup如何双向查找,两个字段查询数据?(教程含数据excel)
    python大学生生活信息交互平台的设计与实现毕业设计-附源码031315
    正点原子《嵌入式linux c应用编程》视频教程学习笔记,持续更新中
    elementUI加springboot实现上传excel文件给后端并读取excel
    力扣 146. LRU 缓存
  • 原文地址:https://blog.csdn.net/qq_59293418/article/details/132729614