【Linux】17. 进程间通信 --- 管道

1. 什么是进程间通信(进程间通信的目的)

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

在之前的学习过程中我们了解到进程是具有独立性的,也就是说要实现通信成本一定不低
那我们为啥要实现通信呢?
是因为在使用操作系统的过程中是存在多进程协同的应用场景滴!完成某种业务需求 (例如:cat file | grep ’ hello ')

  • 那么该如何理解通信的本质问题呢?
  • 要实现通信,首先数据需要存放位置,如果将数据存放在某个进程当中,因为进程的独立性,直接由进程创建的资源其他进程无法看见
    所以要由操作系统直接或者间接的给通信双方的进程提供"内存空间"
  • 要让不同的进程看到同一份公共的资源

通过资源是OS中的不同模块提供所得到不同的通信种类:如果是文件模块 – 管道通信 如果是SystemV通信模块 – System V通信…

2. 进程间通信发展

管道

  • 匿名管道pipe
  • 命名管道

System V IPC

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

POSIX IPC

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

其实在进程间通信的研究过程当中产生了一大堆的标准,但是主流的还是以上三种
POSIX — 让通信过程可以跨越主机
System V — 主要是聚焦在本地通信当中
管道 — 基于文件系统形成的

3. 管道

3.1 什么是管道

管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道
在这里插入图片描述
在这里插入图片描述
文件存储在磁盘当中,当数据写入到内核缓冲区时,会刷新到磁盘上
进程间通信是否会采取数据写入到磁盘上再从磁盘上读取到进程的上下文环境呢? – 不会
因为这样太慢了,我们要实现的是两进程的通信(从内存到内存) ,而将数据写入磁盘(内存 -> 磁盘 -> 内存)效率非常低
对于管道文件而言,需不需要在磁盘上占据内存空间(在磁盘上打开文件) – 不需要
操作系统非常强大,可以直接在内存当中创建文件(管道文件 – 内存级文件)
内存级文件不需要进行磁盘刷新,大大的提高了进程间通信的效率

3.2 匿名管道

上述的实现过程中是如何让两个进程看到同一个管道文件? 通过fork创建子进程完成
子进程会继承父进程的文件描述符表 父子进程会指向同一文件,但是该文件没有名字(内存级文件) 我们将其称之为匿名管道
在这里插入图片描述
为啥父子进程要以读写的方式打开同一文件呢?
因为子进程会继承父进程的文件描述符表和文件的打开方式,若是父进程只以读/写的方式打开文件,那么子进程就会继承对应的读写方式,无法构成管道的需求。
在这里插入图片描述
这里的pipefd [ 2 ] 是输出型参数。
在之前的学习过程中我们了解到只要打开一个文件,操作系统会对应打开0,1,2(标准输入,标准输出,标准错误)
那么文件描述符就是要从3开始

创建管道文件,操作系统以读写的方式打开文件,将进程的文件描述符表填写到数组当中,再将数组输出返回调用该函数就可以创建管道文件

写管道文件的描述符必须通过fd数组的下标访问,不能直接使用具体的fd:3 / 4
(因为我们并不清楚现在文件描述符当中是否还存在其他描述符)
fd【0】读取 – 0 像嘴巴
fd【1】写入 – 1 像钢笔

在这里插入图片描述

基本框架如下:
#include <iostream>
#include <unistd.h>
// 当我们在进行C语言、C++混编的时候
// 推荐将C语言的头文件引用为<c..>的格式
#include <cassert>
#include <cstdlib>
#include <sys/types.h>
#include <sys/wait.h>using namespace std;int main()
{// 第一步:创建管道文件,打开读写端int fds[2];int n = pipe(fds);// 管道创建成功,n返回0assert(n == 0);// 第二步:fork(创建子进程)pid_t id = fork();// 创建子进程成功assert(id >= 0);if (id == 0){// 子进程模块// 关闭读接口close(fds[0]);// 进行父子进程通信close(fds[1]);exit(0);}// 父进程模块// 进行读取// 关闭写接口close(fds[1]);// 父进程进行等待n = waitpid(id, nullptr, 0);assert(n == id);close(fds[0]);
}

运行结果如下:
在这里插入图片描述
在这里插入图片描述
此时父进程完成读取,进入等待状态(R ——> S),在等待管道文件就会将父进程的PCB放入文件的等待队列当中
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.3 管道读写规则

  • 当没有数据可读时
  • 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将不再保证写入的原子性。

3.4 管道特点

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
  • 管道提供流式服务
  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程
  • 一般而言,内核会对管道操作进行同步与互斥
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
    在这里插入图片描述
    在这里插入图片描述
    sleep10000 | sleep 20000 bash命令行解释器,会将其划分成为两个进程, 竖划线创建的就是匿名管道

3.5 匿名管道进程池(重点)

1) 理清思路

在这里插入图片描述
我们想要实现的功能是首先父进程跟多个子进程之间建立管道,创建任务集,随机挑选子进程完成随机任务。
子进程根据父进程传入管道中的commandCode完成对应的任务。

2) 代码实现

① 加载任务集
#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>
#include <cassert>
#include <ctime>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>/子进程要完成的某种任务  --- 模拟实现// 函数指针 类型
typedef void (*func_t)();// 这里就模拟实现了三种任务
void downLoadTask()
{// 执行任务的同时获取一下子进程的pid -- 便于观察std::cout << getpid() << ": 下载任务\n"<< std::endl;
}void ioTask()
{std::cout << getpid() << ": IO任务\n"<< std::endl;
}void flushTask()
{std::cout << getpid() << ": 刷新任务\n"<< std::endl;
}// 往out中插入任务集
void loadTaskFunc(std::vector<func_t> *out)
{assert(out);out->push_back(downLoadTask);out->push_back(ioTask);out->push_back(flushTask);
}int main()
{// 1. 加载任务集std::vector<func_t> funcMap;loadTaskFunc(&funcMap);return 0;
}
② 创建子进程
    // 2. 创建子进程std::vector<subEp> subs;createSubProcess(&subs, funcMap);
#define PROCESS_NUM 4/子进程要完成的某种任务  --- 模拟实现// 函数指针 类型
typedef void (*func_t)();// 这里就模拟实现了三种任务
void downLoadTask()
{// 执行任务的同时获取一下子进程的pid -- 便于观察std::cout << getpid() << ": 下载任务\n"<< std::endl;
}void ioTask()
{std::cout << getpid() << ": IO任务\n"<< std::endl;
}void flushTask()
{std::cout << getpid() << ": 刷新任务\n"<< std::endl;
}// 往out中插入任务集
void loadTaskFunc(std::vector<func_t> *out)
{assert(out);out->push_back(downLoadTask);out->push_back(ioTask);out->push_back(flushTask);
}///下面的代码模拟实现多进程程序
class subEp
{
public:subEp(pid_t subId, int writeFd): subId_(subId), writeFd_(writeFd){char nameBuffer[1024];snprintf(nameBuffer, sizeof nameBuffer, "process-%d[pid(%d)-fd(%d)]", num++, subId_, writeFd_);name_ = nameBuffer;}public:static int num;std::string name_;pid_t subId_;int writeFd_;
};// 因为num为static类型 不属于任何一个对象(实例) 所以必须在这里进行初始化
int subEp::num = 0;int recvTask(int readFd)
{int code = 0;// 判断是否读取到管道中的数据ssize_t s = read(readFd, &code, sizeof code);// 读取到 4个字节if (s == 4)return code;// 父进程退出else if (s <= 0)return -1;elsereturn 0;
}void createSubProcess(std::vector<subEp> *subs, std::vector<func_t> &funcMap)
{for (int i = 0; i < PROCESS_NUM; i++){int fds[2];int n = pipe(fds);assert(n == 0);(void)n;pid_t id = fork();if (id == 0){close(fds[1]);while (true){// 获取管道中的任务编号,如果没有发送,子进程应该进行阻塞等待int commandCode = recvTask(fds[0]);if (commandCode >= 0 && commandCode < funcMap.size()){// 执行任务funcMap[commandCode];}else if (commandCode == -1){// 检测到父进程退出 跳出循环break;}}// 子进程退出exit(0);}// 关闭读文件描述符 fds[0]close(fds[0]);subEp sub(id, fds[1]);// 将创建出的子进程插入subEp对象当中subs->push_back(sub);}
}

这里构造的subEp(先描述再组织)用来管理子进程

③ 父进程控制子进程
    // 3. 父进程控制子进程,负载均衡的向子进程发生任务码int taskCnt = 3;loadBlanceControl(subs, funcMap, taskCnt);
void sendTask(const subEp &process, int taskNum)
{std::cout << "send task num: " << taskNum << " send to -> " << process.name_ << std::endl;int n = write(process.writeFd_, &taskNum, sizeof(taskNum));assert(n == sizeof(int));(void)n;
}void loadBlanceControl(const std::vector<subEp> &subs, const std::vector<func_t> &funcMap, int count)
{int processnum = subs.size();int tasknum = funcMap.size();while (count){// 这里就是负载均衡的体现// 1. 选择一个子进程 --> std::vector<subEp> -> index - 随机数int subIdx = rand() % processnum;// 2. 选择一个任务 --> std::vector<func_t> -> indexint taskIdx = rand() % tasknum;// 3. 将任务发送给选择的进程sendTask(subs[subIdx], taskIdx);// 每次发送完休息1秒sleep(1);count--;}// 当write写入退出 说明读到0了 不再需要给子进程发送任务for (int i = 0; i < processnum; i++){close(subs[i].writeFd_); // waitpid();}
}
④ 回收子进程信息
// 4. 回收子进程信息
waitProcess(subs);
void waitProcess(std::vector<subEp> processes)
{int processnum = processes.size();for(int i =0;i<processnum;i++){waitpid(processes[i].subId_,nullptr,0);std::cout<<"wait sub process success ...: " << processes[i].subId_ << std::endl;}
}

在这里插入图片描述
assert断言
意料之中用assert 意料之外用if判断
(void)n的作用 assert的作用只是在debug下,在release版本下该行代码会删除
那么之前定义的n变量就没有进行使用可能就会出现warning报错:定义了变量但未使用。

3.6 命名管道

  • 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
  • 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
  • 命名管道是一种特殊类型的文件

创建一个命名管道

  • 命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
$ mkfifo filename
  • 命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);

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

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

命名管道的打开规则

  • 如果当前打开操作是为读而打开FIFO时

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

  • 如果当前打开操作是为写而打开FIFO时

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

命名管道通信

逻辑分析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
即便两个文件在进行通信,管道文件的大小仍是0
在这里插入图片描述
在这里插入图片描述

代码分析
// 头文件 + 创建/删除管道
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>// 定义宏 将命名管道放在当前目录下(便于观察)
#define NAMED_PIPE "./mypipe"
// 创建管道
bool createFifo(const std::string &path)
{// 将文件掩码设置为0umask(0);int n = mkfifo(path.c_str(), 0600);if (n == 0)return true;else{std::cout << "errno: " << errno << " err stirng: " << strerror(errno) << std::endl;return false;}
}// 删除管道
void removeFifo(const std::string &path)
{int n = unlink(path.c_str());assert(n == 0);(void)n;
}

mkfifo 创建管道文件,unlink 关闭文件

client端 – 客户端 往管道中写入数据

#include "comm.hpp"int main()
{std::cout << "client begin" << std::endl;// 将管道文件以只写入的方式打开int wfd = open(NAMED_PIPE, O_WRONLY);std::cout << "client end" << std::endl;// 管道文件打开失败if (wfd < 0)exit(1);// writechar buffer[1024];while (true){std::cout << "Please say# ";fgets(buffer, sizeof(buffer), stdin);// 当管道中有数据 将最后一位置为0// 这里的if判断buffer>0虽然没必要 因为输入数据至少要按下回车键 就一定有数据// 但是逻辑正确if (strlen(buffer) > 0)buffer[strlen(buffer) - 1] = 0;ssize_t n = write(wfd, buffer, strlen(buffer));assert(n == strlen(buffer));(void)n;}close(wfd);return 0;
}

server端 — 服务端 从管道当中读出数据

#include "comm.hpp"// sever作为主控制
int main()
{// 创建命名管道bool r = createFifo(NAMED_PIPE);assert(r);(void)r;std::cout << "sever begin" << std::endl;int rfd = open(NAMED_PIPE, O_RDONLY);std::cout << "sever end" << std::endl;if (rfd < 0)exit(1);// readchar buffer[1024];while (true){ssize_t s = read(rfd, buffer, sizeof(buffer) - 1);if (s > 0){buffer[s] = 0;std::cout << "client->server#" << buffer << std::endl;}else if (s == 0){std::cout << "client quit,me too!" << std::endl;break;}else{std::cout << "err string: " << strerror(errno) << std::endl;break;}}close(rfd);// 等待10秒 观察当前路径下的命名管道是否删除// sleep(10);removeFifo(NAMED_PIPE);return 0;
}

细节1:
默认情况下 读取进程先运行,只有当写入打开后,读取进程才会继续往后进行
在这里插入图片描述
read进程会卡在open函数这, 因为此时管道文件只有读描述符,没有写入描述符(读没有意义,所以阻塞)

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

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

相关文章

团队执行力差,多半都是管理的问题

在日常管理中&#xff0c;我们习惯用“执行力好不好”来评价一个团队的表现&#xff0c;但实际上&#xff0c;执行力更应该是一个管理者需要思考和解决的问题&#xff0c;而非单纯归咎于团队。 我们需要明确一点&#xff1a;执行力不是团队的问题&#xff0c;而是管理者的问题…

比亚迪CAN数据实时监控分析应用数字化差异化的决策价值洞察

在当今这个信息化飞速发展的时代&#xff0c;汽车数字化转型已成为企业持续竞争力的关键。中国新能源汽车行业的领军企业——比亚迪&#xff0c;其数字化之旅充分展现了企业的创新精神和对未来的深远洞察。 比亚迪的数字化战略不是简单的技术应用&#xff0c;而是一场深刻的商…

C++奇迹之旅:string类对象的容量操作

文章目录 &#x1f4dd; string类的常用接口&#x1f309; string类对象的容量操作&#x1f320;size&#x1f320;length&#x1f320;capacity&#x1f320;clear&#x1f320;empty&#x1f320;reserve&#x1f309;resize &#x1f6a9;总结 &#x1f4dd; string类的常用…

大数据集成平台建设方案-word原件资料

基础支撑平台主要承担系统总体架构与各个应用子系统的交互&#xff0c;第三方系统与总体架构的交互。需要满足内部业务在该平台的基础上&#xff0c;实现平台对于子系统的可扩展性。基于以上分析对基础支撑平台&#xff0c;提出了以下要求&#xff1a; (1) 基于平台的基础架构&…

【优选算法】——Leetcode——611. 有效三角形的个数

目录 ​编辑 1.题目 2 .补充知识 3.解法⼀&#xff08;暴⼒求解&#xff09;&#xff08;可能会超时&#xff09;&#xff1a; 算法思路&#xff1a; 算法代码&#xff1a; 4.解法⼆&#xff08;排序双指针&#xff09;&#xff1a; 算法思路&#xff1a; 以输入: nums …

多个glibc库存在时如何查看ldd调用的哪个

但是发现存在多个版本的glibc版本&#xff0c;需要查看具体的库的信息&#xff0c;和相应的关键函数的信息&#xff0c;但是并不知道具体的libc.so.6的路径信息 rootalg-dev04:~/xingqiao# ldd --version ldd (GNU libc) 2.29 rootalg-dev04:/opt# which ldd /usr/local/bin/…

硬件基础——晶振(复试被问到)

1.什么是晶振 石英晶体振荡器&#xff0c;是芯片的心脏&#xff0c;主要用于提供给芯片稳定、精确的时钟频率信号。其主要利用石英晶体的压电效应&#xff0c;从而实现振荡。 一般晶振会在芯片的旁边&#xff0c;不能远离晶振&#xff0c;因为振荡时会受外界电磁干扰的影响。 我…

LLM——大语言模型完整微调策略指南

1、 概述 GPT-4、LaMDA、PaLM等大型语言模型&#xff08;LLMs&#xff09;以其在广泛主题上的深入理解和生成高度类人文本的能力而闻名遐迩&#xff0c;它们在全球范围内引起了广泛关注。这些模型的预训练过程涉及对来自互联网、书籍和其他来源的数十亿词汇的海量数据集进行学…

如何阅读:一个已被证实的低投入高回报的学习方法的笔记

系列文章目录 如何有效阅读一本书笔记 如何阅读&#xff1a;一个已被证实的低投入高回报的学习方法 麦肯锡精英高效阅读法笔记 读懂一本书笔记 文章目录 系列文章目录第一章 扫清阅读障碍破解读不快、读不进去的谜题一切为了阅读小学教师让你做&#xff0c;但中学老师阻止你做的…

ffmpeg ubuntu18.04编译报错fcntl64

fcntl&#xff0c;fcntl64均是系统的api提供的文件操作&#xff0c;fcntl64本来是用来解决操作大文件的问题&#xff0c;后面fcntl本身已经解决了这个问题&#xff0c;fcntl64就被舍弃了 系统环境信息&#xff1a; ubuntu 18.04 root# cat /etc/issue Ubuntu 18.04.6 LTS \n…

DLP数据防泄密软件推荐盘点:防泄密软件厂商

数据防泄密软件在当今的数字化时代扮演着至关重要的角色。随着信息技术的迅猛发展&#xff0c;企业、组织乃至个人面临着日益严峻的数据安全挑战。数据防泄密软件应运而生&#xff0c;为信息安全领域筑起了一道坚实的防线。以下是五款备受推崇的数据防泄密软件&#xff0c;它们…

【工具】Office/WPS 插件|AI 赋能自动化生成 PPT 插件测评 —— 必优科技 ChatPPT

本文参加百度的有奖征文活动&#xff0c;更主要的也是借此机会去体验一下 AI 生成 PPT 的产品的现状&#xff0c;因此本文是设身处地从用户的角度去体验、使用这个产品&#xff0c;并反馈最真实的建议和意见&#xff0c;除了明确该产品的优点之外&#xff0c;也发现了不少缺陷和…

[C++基础编程]----预处理指令简介、typedef关键字和#define预处理指令之间的区别

目录 引言 正文 01-预处理指令简介 02-typedef关键字简介 03-#define预处理指令简介 04-#define预处理指令和typedef关键字的区别 &#xff08;1&#xff09;原理不同 &#xff08;2&#xff09;功能不同 &#xf…

IP协议全解析:网络层通信的基石

⭐小白苦学IT的博客主页⭐ ⭐初学者必看&#xff1a;Linux操作系统入门⭐ ⭐代码仓库&#xff1a;Linux代码仓库⭐ ❤关注我一起讨论和学习Linux系统❤ 前言 在数字化时代的浪潮中&#xff0c;网络通信无处不在&#xff0c;它连接着世界的每一个角落&#xff0c;承载着信息的高…

FileLink跨网文件交换的交换方式:满足不同场景下的文件交换需求

FileLink&#xff0c;作为一款创新的文件交换工具&#xff0c;不仅满足了用户在日常生活中对文件传输的需求&#xff0c;更在技术上实现了跨网文件交换的突破。其独特之处在于支持邮件方式投递、文件中转站、网盘模式共享三种交换方式&#xff0c;这使得FileLink能够适应不同场…

3D 交互展示该怎么做?

在博维数孪&#xff08;Bowell&#xff09;平台制作3D交互展示的流程相对简单&#xff0c;主要分为以下几个步骤&#xff1a; 1、准备3D模型&#xff1a;首先&#xff0c;你需要有一个3D模型。如果你有3D建模的经验&#xff0c;可以使用3ds Max或Blender等软件自行创建。如果没…

MySQL中的ON DUPLICATE KEY UPDATE和REPLACE

在 MySQL 中&#xff0c;ON DUPLICATE KEY UPDATE 和 REPLACE 语句都可以用来处理插入数据时主键或唯一键冲突的情况&#xff0c;但它们在处理冲突的方式上有所不同。它们有以下区别&#xff1a; 行为方式&#xff1a; ON DUPLICATE KEY UPDATE&#xff1a;当插入的数据行存在冲…

【智能安防监控补光灯调光芯片方案】单节锂电降压恒流驱动芯片FP8013 最大输出3A体积小/静态功耗低/效率高/支持无频闪调光

文章目录 文章目录 前言 一、pandas是什么&#xff1f; 二、使用步骤 1.引入库 2.读入数据 总结 前言 随着智能安防监控技术的不断发展&#xff0c;补光灯的关键性能也日益受到重视。为了提供更好的夜间监控效果&#xff0c;我们需要一种高效、稳定的调光芯片来驱动补光灯的亮…

vue 文本中的\n 、<br>换行显示

一、背景&#xff1a; 后端接口返回数据以\n 作为换行符&#xff0c;前端显示时候需要换行显示&#xff1b; demo&#xff1a; <p style"white-space: pre-wrap;">{{ info }}</p>data() {return {info: 1、优化图片\n 2、 优化时间\n}},项目上&#…

嘎嘎好用的虚拟键盘第二弹之中文输入法

之前还在为不用研究输入中文而暗自窃喜 这不新需求就来了&#xff08;新需求不会迟到 它只是在路上飞一会儿&#xff09; 找到了个博主分享的代码 是好使的 前端-xyq 已经和原作者申请转载了 感谢~~ 原作者地址&#xff1a;https://www.cnblogs.com/linjiangxian/p/16223681.h…