IPC之一:使用匿名管道进行父子进程间通信的例子

news2024/12/29 10:20:04

IPC 是 Linux 编程中一个重要的概念,IPC 有多种方式,本文主要介绍匿名管道(又称管道、半双工管道),尽管很多人在编程中使用过管道,但一些特殊的用法还是鲜有文章涉及,本文给出了多个具体的实例,每个实例均附有完整的源代码;本文所有实例在 Ubuntu 20.04 上编译测试通过,gcc版本号为:9.4.0;本文适合 Linux 编程的初学者阅读

1 概述

  • IPC(Inter-Process Communication) - 进程间通信,提供了各种进程间通信的方法;
  • 在 Linux C 编程中,IPC 通常有如下几种方式
    1. 半双工管道(Unix Pipe),简称管道,又称为匿名管道
    2. FIFOs - 又称为命名管道
    3. 消息队列(Message Queues)
    4. 信号量集(Semaphore Sets)
    5. 共享内存(Shared Memory Segments)
    6. 网络 socket(AF_INET Address Family)
    7. 全双工管道(AF_UNIX Address family)
  • 本文主要介绍匿名管道(Unix Pipe)的应用场景及使用方法,并给出多个附有完整源代码的实例;
  • 管道又被称为 匿名管道,是相对于命名管道而言的;
  • 匿名管道的通信模式是半双工的,所谓半双工指的是在管道中数据流是单方向的,当 A 进程和 B 进程之间使用管道进行通信时,数据要么从 A 发向 B,要么从 B 发向 A,在一个管道上,不能既有 A 向 B 的数据流,又有 B 向 A 的数据流;
  • 管道还有一个特性就是只能在有亲缘关系的进程间传递消息,换句话说,只有当两个进程有相同的祖先时,才有可能使用管道进行通信。

2. 管道的基本概念

  • 简单地说,管道是一种将一个进程的输出连接到另一个进程的输入的方法;

  • 管道是最古老的 IPC 工具,从最早的 UNIX 操作系统开始就存在了;它们提供了进程间单向通信的方法(因此称为半双工);

  • 实际上,管道的这个特性广泛应用在 Linux 的命令行上,比如下面的命令:

    ls | sort | lp
    
  • 这条命令实际上就建立了一个管道,将 ls 的输出作为 sort 的输入,将 sort 的输出作为 lp 的输入;数据在匿名管道中运行,看上去,数据在管道中从左向右单方向流动;

  • 管道是在 Linux 内核中实现的,很多程序员在 shell 脚本编程中都会频繁使用管道,但很少有人会去想管道在 Linux 内核中是如何实现的;

    当一个进程创建管道时,内核会创建两个文件描述符(fd[0]fd[1])供管道使用;一个描述符(fd[1])用于将数据写入管道,另一个描述符(fd[0])用于从管道中读取数据;此时,管道的实际用处不大,因为创建管道的进程只能使用管道与自身进行通信,毫无意义;

  • 下图展示了一个进程创建管道后,进程与内核的关系:

    process and kernel

  • 从上图中,可以看出以下几点:

    • 文件描述符是如何连接在一起的;进程通过文件描述符(fd[1])向管道写入数据,也能够从文件描述符(fd[0])从管道中读取该数据;
    • 通过管道传输数据时,数据是通过内核流动的;在 Linux 下,管道在内核内部使用 inode 表示,innode 驻留在内核中,并不属于一个物理文件系统。
  • 这样建立的管道毫无用处,一个进程要自言自语,没有必要建立一个管道;但是,如果创建管道的进程再 fork 出一个子进程,由于子进程会从父进程继承管道的描述符,这样父子进程之间有可以通过这个管道进行通信了;

  • 下图描述了父进程、子进程和内核的关系

    parent-child processes and kernel

  • 从上图中,我们可以看到,父进程和子进程都可以访问管道的两个文件描述符,但是很显然,如果父进程和子进程同时向 fd[1] 写入数据,一定会造成混乱,而且如果父、子进程均向 fd[1] 写入数据,当从 fd[0] 读出数据时,并无法区分读到的数据是那个进程写入的;所以必须要做出抉择,这个建立的管道的数据是向那个方向流动,从父进程流向子进程?还是从子进程流向父进程?两个进程必须达成一致,否则会出现混乱;

  • 为了讨论方便,我们假定子进程要做一些事务,然后把结果通过管道发送给父进程,如下面图示:

    Data flows from child to parent process

  • 至此,管道已经建立完毕,下面就是如何使用管道;前面提到过,管道的文件描述符使用 inode,所以可以使用低级文件 I/O 的系统调用来直接访问管道;

  • 向管道中写入数据,使用 write() 系统调用;从管道中读出数据,使用 read() 系统调用;

  • 特别提醒:系统调用 lseek() 不能在管道中使用。

3 如何用C语言创建管道

  • 使用 pipe() 系统调用可以创建一个管道,这个调用需要一个由两个整数组成的数组作为参数,调用成功后,该数组将包含管道的两个文件描述符;
  • 系统调用:pipe()
    原型:#include <unistd.h>
         int pipe(int fd[2]);
    返回:调用成功返回 0
         调用失败返回 -1
         error = EMFILE (no free descriptors)
                 EMFILE (system file table is full)
                 EFAULT (fd array is not valid)
    
    备注: fd[0] 用于从管道中读取数据, fd[1] 用于向管道中写入数据
    
  • 调用成功后,不仅两个管道描述符被建立,而且处于打开状态,可以直接进行读、写操作;
  • 再次重申,所有通过管道传输的数据都要通过内核;下面是使用 pipe() 建立管道的代码:
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    
    main() {
        int     fd[2];
        
        pipe(fd);
        .
        .
    }
    
  • 前面说过,这样建立的管道毫无用处,进程自言自语并不需要使用管道;要使管道有意义,在建立管道后要 fork() 一个子进程;
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    
    main() {
        int   fd[2];
        pid_t childpid;
    
        pipe(fd);
    
        if ((childpid = fork()) == -1) {
            perror("fork");
            exit(1);
        }
        .
        .
    }
    
  • 如果父进程要从子进程接收数据,父进程应关闭向管道写入的描述符 fd[1],而子进程应该关闭从管道读出的描述符 fd[0];如果父进程要向子进程发送数据,则父进程应关闭从管道读出的描述符 fd[0],而子进程应该关闭向管道写入的描述符 fd[1]
  • 由于管道描述符在父进程和子进程之间是共享的,所以我们要确保关闭掉我们不需要的管道末端,从技术上讲,如果不需要的管道末端没有关闭,则永远不会返回 EOF
  • 下面代码假定父进程要从子进程接收数据:
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    
    main() {
        int   fd[2];
        pid_t childpid;
    
        pipe(fd);
    
        if ((childpid = fork()) == -1) {
            perror("fork");
            exit(1);
        }
    
        if (childpid == 0) {
            /* Child process closes up input side of pipe */
            close(fd[0]);
        } else {
            /* Parent process closes up output side of pipe */
            close(fd[1]);
        }
        .
        .
    }
    
  • 如前所述,建立了管道以后,就可以像对待普通文件描述符一样对待管道描述符;
  • 源程序:pipe.c(点击文件名下载源程序)演示了子进程向父进程发送信息:Hello, world!

4 在管道上使用 dup()

  • 大多数已有的 Linux 命令或者自己编写的程序,其默认的输入设备往往是 STDIN,而输出设备是 STDOUT,当我们希望在程序中用某个 Linux 命令处理数据时,往往不太好获得命令的输出,或者不好把数据传送给这个程序,这时候管道可以发挥作用;

  • 比如 Linux 命令 sort,在没有其它参数时,其默认的输入设备就是 STDIN,当我们在程序中希望使用 sort 处理一组数据时,我们可以设法把 STDIN 连接到管道的输出端,这样,我们向管道中的一端写入数据时,管道的另一端已经启动的 sort 就可以从 STDIN 读到数据并进行处理;

  • 系统调用 dup()dup2() 可以帮助我们实现这个想法;先看一下这两个系统调用的说明;

  • 系统调用: dup();

    原型:#include <unistd.h>
         int dup(int oldfd);
    说明:dup() 系统调用创建文件描述符 oldfd 的副本,使用编号最小的未使用的文件描述符作为新描述符。
    返回:调用成功则返回新描述符
         调用失败则返回 -1
         errno = EBADF (oldfd is not a valid descriptor)       
                 EBADF (newfd is out of range)
                 EMFILE (too many descriptors for the process) 
    
    备注:oldfd 不会被关闭,新描述符和 oldfd 都可以使用。
    
  • 系统调用:dup2();

    原型:#include <unistd.h>
         int dup2(int oldfd, int newfd);
    说明:dup2() 系统调用与 dup() 相似,创建文件描述符 oldfd 的副本,但它不使用编号最小的未使用文件描述符,而是使用 newfd 中指定的文件描述符;
         如果文件描述符 newfd 先前已打开,该调用会首先将其关闭然后再使用。
    返回:调用成功则返回新描述符
         调用失败则返回 -1
         errno = EBADF (oldfd is not a valid descriptor)
                 EBADF (newfd is out of range)
                 EMFILE (too many descriptors for the process)
    
    备注:使用 dup2(),oldfd 会被关闭
    
  • 在子进程中,使用 dup2() 将管道的输出(fd[0])复制到 STDIN 上,并关闭 STDIN,然后用 exec() 启动 sort 时,当 sortSTDIN 读入数据时,实际上是从管道中读出数据,当我们从父进程向管道中写入数据时,这个数据将被 sort 读取并处理;

  • 为了搞清楚这种用法,请自行学习 Linux 命令 sort,可以用在线手册 man sort 了解该命令的详细信息;

  • 下图或许可以更直观地描述这种使用方法:

    pipe with dup()

  • 源程序:pipe-dup-stdin.c(点击文件名下载源程序)演示了在管道中使用 dup2() 将 fd[0] 复制到 STDIN 的方法:

    子进程中把 fd[0] 复制到 STDIN,然后启动 sort,父进程向管道中写入若干个单词,每个单词以 \n 结尾,sortSTDIN 读入数据,实际上是从管道中读入数据,所以 sort 程序会对这些单词进行排序,并把结果写入文件 sort.log 中,程序运行完毕后,使用 cat sort.log 可以看到经过排序的单词;

  • 同样道理,也可以在子进程中把管道的输入端(fd[1])复制到 STDOUT 上,这样,当子进程中启动的程序向 STDOUT 输出时,实际上是在向管道上写入数据;

  • 源程序:pipe-dup-stdout.c(点击文件名下载源程序)演示了在管道中使用 dup2() 将 fd[1] 复制到 STDOUT 的方法:

    子进程中把 fd[1] 复制到 STDOUT,然后启动 unameuname -r 会输出一个字符串到 STDOUT,实际上是写入到了管道中,父进程从管道中收到了这个字符串并显示出来;

    子进程中把管道的输入端复制到 STDOUT 后,在子进程中启动任何程序,在主进程中通过读取管道都可以轻易地获得这个程序的输出,比如我们要知道当前系统的是不是 64 位系统,那我们在子进程中启动命令 uname -m,如果主进程在管道上读出的内容是 x86_64,则系统无疑是64位的。

5 使用管道的简单方法

  • 上面介绍的在程序中使用管道获取一个外部程序的输出(或者向一个外部程序输入数据)的方法看上去不仅繁琐,而且绕的弯也比较多,其实使用管道还有更为简单的方法;

  • 使用标准库函数 popen() 可以很容易地使用管道;

    库函数:popen();
    
    原型:#include <stdio.h>
         FILE *popen (char *command, char *type);
    说明:popen() 函数通过创建一个管道,调用 fork 产生一个子进程,执行 shell 运行命令来开启一个进程。
    返回:调用成功则返回一个标准 I/O 流
         调用 fork() 或 pipe() 失败则返回 NULL
    
  • 该标准库函数通过内部调用 pipe() 创建匿名管道,然后 fork() 一个子进程,执行 shell,并在 shell 中执行 “command” 参数;数据流的方向由第二个参数 type 确定,type 可以是 “r” 或 “w”,表示读或写,不可能两者兼而有之!在 Linux 下,管道将以 type 参数的第一个字符指定的模式打开,如果您将 type 设置为 “rw”,该函数会以 “r” (读)模式打开管道。

  • 与直接使用 pipe() 系统调用相比,这个库函数为我们做了很多繁琐的工作,但却让我们失去了对整个过程的精细控制;

  • 该函数直接使用了 Bourne shell(bash), 所以在 command 参数中可以使用 shell 元字符以及元字符扩展(包括通配符);

  • 使用 popen() 创建的管道必须使用 pclose() 关闭;popen()/pclose() 与标准文件流I/O函数 fopen()/fclose() 非常相似。

    库函数:pclose();
    
    原型:#include <stdio.h>
         int pclose(FILE *stream);
    说明:pclose()函数等待相关进程终止,并返回由 wait4() 返回的命令退出状态。
    返回:返回 wait4() 调用的退出状态码
         如果 stream 不合法,或者 wait() 执行失败,则返回 -1
    
    备注:等待管道进程退出,然后关闭文件 I/O 流
    
  • pclose() 函数对由 popen() 派生的进程执行 wait4(),当 wait4() 返回时,它会销毁管道和文件流;

  • 源程序:pipe-popen.c(点击文件名下载源程序)完成与前面的例子 pipe-dup-stdin.c 一样的功能,但看上去要简单的多;

  • 由于 popen() 使用 shell 来执行命令,因此 shell 扩展字符和元字符都可以使用,此外,使用 popen() 打开管道时,可以使用一些高级技术来执行命令,例如重定向,甚至输出管道;以下调用示例分别使用了扩展字符、重定向和输出管道:

    popen("ls ~scottb", "r");
    popen("sort > /tmp/foo", "w");
    popen("sort | uniq | more", "w");
    
  • 源程序:pipe-popen2.c(点击文件名下载源程序)打开了两个管道,一个用于 ls 命令,另一个用于 sort 命令;

  • 下面这个例子试图编写一个通用的管道程序,源程序文件:pipe-popen3.c(点击文件名下载源程序)

    • 使用方法为:./pipe-popen3 [command] [filename]
    • 该程序会首先打开文件 filename
    • 然后使用 popen() 以写方式打开 command 管道
    • filename 中读出内容并写入管道
    • 可以尝试用以下方式测试这个例子
      ./pipe-popen3 sort pipe-popen3.c
      ./pipe-popen3 cat pipe-popen3.c
      ./pipe-popen3 more pipe-popen3.c
      ./pipe-popen3 cat pipe-popen3.c | grep main
      

6 管道的原子操作

  • 所谓“原子操作”,是指一个或一系列不可中断的操作,就是说一个原子操作一旦开始执行就不能被中断,直至执行完毕;
  • POSIX 标准规定了管道上原子操作的最大缓冲区大小是 512 字节,定义在头文件:bits/posix1_lim.h 中:
    #define _POSIX_PIPE_BUF         512
    
  • 根据这一定义,如果一次写入/读出管道的操作大于 512 字节,操作将是非“原子操作”,也就是写入/读出的数据可能会被分割;
  • 在 Linux 下,定义的管道上的原子操作的最大缓冲区大小为:4096 字节,定义在头文件:linux/limits.h 中:
    #define PIPE_BUF        4096	/* # bytes in atomic write to a pipe */
    
  • 显然,在我们目前的环境下,在管道上进行不大于 4096 字节的读/写操作是原子操作;
  • 在多进程环境下,原子操作对管道的读/写操作非常重要,当一个进程写入管道的数据大于阈值时,其写入过程中间会中断,操作系统会产生进程调度,如果这时其它进程也向这个管道写入数据,那么写入管道的数据会产生混乱。

7 匿名管道的其它说明

  • 尽管管道是半双工的,但是,打开两个管道,并在子进程中合理地重新分配描述符,可以构建出一个类似全双工的管道;
  • pipe() 调用必须在 fork() 调用之前进行,否则描述符将不会被子进程继承;
  • 使用匿名管道进行通信的进程都必须有一个共同的祖先,而且这个祖先必须是管道的创建者,由于管道位于内核中,不在管道创建者祖先中的进程都无法对其进行寻址,这与命名管道(FIFO)是不同;
  • 由于使用管道的一些限制,在进程间进行通讯时,管道实际上并不是一个常用的方法,但是,如果需要使用已有的 Linux 命令处理数据,或者从 Linux 命令获得结果数据,管道不失为一个好的选择;
  • 匿名管道的生命周期与创建它的进程的生命周期一致,当进程结束时,其创建的匿名管道也将被销毁。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/848696.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Leetcode.1316 不同的循环子字符串

题目链接 Leetcode.1316 不同的循环子字符串 rating : 1837 题目描述 给你一个字符串 text &#xff0c;请你返回满足下述条件的 不同 非空子字符串的数目&#xff1a; 可以写成某个字符串与其自身相连接的形式&#xff08;即&#xff0c;可以写为 a a&#xff0c;其中 a 是…

服务器感染了LockBit 3.0勒索病毒,如何确保数据文件完整恢复?

引言&#xff1a; 在数字时代&#xff0c;恶意软件的威胁变得愈发严峻&#xff0c;而LockBit 3.0勒索病毒则是其中的顶尖恶势力之一。其先进的加密技术和毫不留情的勒索手段&#xff0c;使无数人蒙受损失。然而&#xff0c;我们不应束手无策。本文91数据恢复将带您深入了解Loc…

AndroidStudio通过Profiler查找内存泄漏

Fragment内存泄漏&#xff1a; AndroidStudio --> Profiler --> 勾选 show nearest Gc root only&#xff0c;然后查看非weakreference的引用&#xff08;weakreference是不会导致内存泄漏的&#xff09;&#xff0c;往下就能找自己项目里写的代码&#xff0c;一般此处…

旷视科技AIoT软硬一体化走向深处,生态和大模型成为“两翼”?

齐奏AI交响曲的当下&#xff0c;赛道玩家各自精彩。其中&#xff0c;被称作AI四小龙的商汤科技、云从科技、依图科技、旷视科技已成长为业内标杆&#xff0c;并积极追赶新浪潮。无论是涌向二级市场还是布局最新风口大模型&#xff0c;AI四小龙谁都不甘其后。 以深耕AIoT软硬一…

ASCP系列电气防火限流式保护器在养老院的应用-安科瑞黄安南

摘要&#xff1a;2020年&#xff0c;我国65岁及以上老年人口数量为1.91亿&#xff0c;老龄化率达到13.5%。总体来看&#xff0c;大部分省市的养老机构数量还较少。养老设施的建设与民生息息相关&#xff0c;养老院的电气安全也非常重要。如果发生电气火灾&#xff0c;对于行动不…

【多模态】24、开放词汇学习到底是什么?

文章目录 一、什么是开放词汇学习二、开放词汇学习的测评和数据集三、开放词汇目标检测3.1 Region-Aware Training3.2 Pseudo-Labeling3.3 Knowledge Distillation-Based3.4 Transfer Learning-Based3.5 总结3.6 效果 参考论文&#xff1a;A Survey on Open-Vocabulary Detecti…

Vue3 事件处理简单应用

去官网学习→事件处理 | Vue.js 运行示例&#xff1a; 代码&#xff1a;HelloWorld.vue <template><div class"hello"><h1>Vue 事件处理</h1><button v-on:click"numb 1">点击加1-----{{ numb }}</button><br/&…

独家揭秘Linux内核栈:内核态的奇妙之处和与用户态的差异

理解Linux内核栈可以从以下几个方面来考虑&#xff1a;内核态与用户态&#xff1a;在阅读Linux内核及相关资料时&#xff0c;需要明确它所描述的是内核态还是用户态的内容。这有助于理解所讨论的是在哪个执行环境下进行的操作。进程与线程的描述&#xff1a;用户态的进程和线程…

Yield Guild Games:社区更新 — 2023 年第二季度

本文重点介绍了 Yield Guild Games (YGG) 2023 年第二季度社区更新中涵盖的关键主题&#xff0c;包括公会发展计划 (GAP) 第 3 季的总结、YGG 领导团队的新成员以及 YGG 的最新消息地区公会网络和广泛的游戏合作伙伴生态系统。 在 YGG 品牌焕然一新的基础上&#xff0c;第二季…

ArcGIS Pro基础:【按顺序编号】工具实现属性字段的编号自动赋值

本次介绍一个字段的自动排序编号赋值工具&#xff0c;基于arcgis 的字段计算器工具也可以实现类似功能&#xff0c;但是需要自己写一段代码实现&#xff0c; 相对而言不是很方便。 如下所示&#xff0c;该工具就是【编辑】下的【属性】下的【按顺序编号】工具。 其操作方法是…

Openlayers实战:右键点击,弹出feature信息

鼠标作为一个重要的交互触发手段,不但有左点击,还有右点击。 Openlayers开发的项目中,我们取消鼠标右键默认菜单,右击后获取到的feature的信息值。 效果图 源代码 /* * @Author: 大剑师兰特(xiaozhuanlan),还是大剑师兰特(CSDN) * @此源代码版权归大剑师兰特所有,可…

Spring系列四:AOP切面编程

文章目录 &#x1f497;AOP-官方文档&#x1f35d;AOP 讲解&#x1f35d;AOP APIs &#x1f497;动态代理&#x1f35d;初始动态代理&#x1f35d;动态代理深入&#x1f35d;AOP问题提出&#x1f4d7;使用土方法解决&#x1f4d7; 对土方法解耦-开发最简单的AOP类&#x1f4d7;…

美国探亲签证怎样预约?

近年来&#xff0c;越来越多的人都对前往美国探亲感兴趣&#xff0c;然而在计划之初&#xff0c;签证预约却可能成为一个让人头疼的问题。那么&#xff0c;究竟如何预约美国探亲签证呢&#xff1f;下面知识人网小编就为大家详细介绍一下预约的流程和注意事项。 首先&#xff0c…

spring boot 集成mqtt

spring boot 集成mqtt 1.到官网下载软件 MQTT linux版本&#xff08;使用apt方式下载安装&#xff09; 执行 curl -s https://assets.emqx.com/scripts/install-emqx-deb.sh | sudo bash再执行 sudo apt-get install emqx最后启动 emqx startWindows版 下载解压后进入bin…

【从零学习python 】05. Python中的输出和输入

文章目录 输出一、普通的输出二、格式化输出格式化操作的目的什么是格式化 三、换行输出四、练习五、python2与python3里的区别 输入input进阶案例 输出 简单来说&#xff0c;就是将程序的运行结果显示出来。 一、普通的输出 生活中的“输出” 软件中的图形化界面输出 py…

C语言----字符串操作函数汇总

在C的库函数中&#xff0c;有丰富的字符串操作函数&#xff0c;在平时的coding中灵活运用这些库函数会达到事半功倍的效果 一&#xff1a;str系列 char *strcpy(s, ct)将字符串ct(包括\0)复制到字符串s中&#xff0c;并返回s&#xff0c;需要注意s的长度是否容纳ct。char *st…

TikTok推出PrivacyGo,品牌可与平台共享部分用户数据

1.TikTok宣布允许用户关闭内容自动显示功能 TikTok近日宣布修改运营方式&#xff0c;即允许用户关闭内容自动显示功能&#xff0c;以遵守将于8月底生效的欧盟新规定&#xff08;欧盟数字服务法案DSA&#xff09;&#xff0c;该法案对平台提出了新的要求&#xff0c;以更好地保…

Idea2023之热部署插件JRebel+XRebel激活及使用

使用的目的就是不用因为改动一点Java代码重复启动服务 JRable会自动检测代码变动重启服务,这也会变向增加计算机内存和性能消耗! 激活教程 1-生成guid guid 复制后下载exe文件 ReverseProxy_windows_amd64.exe 下载完毕后双击运行exe 初始只有第一行后面是激活过程中跑出…

Unity游戏源码分享-有意思的科普游戏3D-Electronic-Blocks-master

Unity游戏源码分享-有意思的科普游戏3D-Electronic-Blocks-master 下载地址&#xff1a;https://download.csdn.net/download/Highning0007/88190169

Spring Boot 简介与入门

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…