进程间通信:传递 C 风格的结构与 C++ 对象

Posted

技术标签:

【中文标题】进程间通信:传递 C 风格的结构与 C++ 对象【英文标题】:Interprocess communication: passing C-style structs vs C++-objects 【发布时间】:2018-12-20 10:06:06 【问题描述】:

警告/免责声明:

这个问题包含异端,但在我过去半小时左右所做的小研究中,我找不到以下声明的答案。我只是好奇这里是否有人已经知道这件事。

这个问题没有代码。只是技术问题。

背景:

我有一个遗留应用程序,它使用在进程之间传递的 C 样式结构进行进程间通信。这工作得很好,并且已经工作了很多年,甚至在我来到这个星球之前很久:P。

我应该编写一个新进程,它将成为这个应用程序的一部分。不知不觉中,我用 C++ 编写了它,假设我们使用的任何 IPC 都可以处理这个问题。不幸的是,后来我(从同事那里)发现现有的基础设施只能传递 C 风格的结构。

“未经证实”的声明/声明:

此外,其中一位同事列出了在这种情况下 C++ 不是一个糟糕选择的以下原因。

    C++ 对象具有 vtables。 C 风格的结构只是变量和值。因此,C 风格的结构可以在进程中传递,而 C++ 对象不能。

    1234563 vtable 可能会有所不同'。

    '如果我们改变编译器,那就更糟了。对于 C++ 对象,我们会处理更多的排列。'

调查索赔:

不用说,这位同事对C有点偏见,但他比我经验丰富得多,大概知道他在说什么。 我与语言无关。但这立刻让我想到了。为什么我们不能用 C++ 进行进程间通信?我用谷歌搜索,第一次点击总是来自 ***,就像这个:

Inter-Process Communication Recommendation

我查看了此处列出的 IPC 的不同方法。 https://en.wikipedia.org/wiki/Inter-process_communication#Approaches

我的意思是,我跟进了列表中的每个方法,例如管道或共享内存等,每个人都不断指出的唯一警告是,指针(当然!当然)不能像这样传递并且同步的一些问题可能会蔓延 - je nachdem。

但是我找不到可以反驳或证实他的“主张”的东西。 (当然,我可以在剩下的时间里继续挖掘。:P)

问题:

    他的三个主张真的如此还是只是 FUD?考虑到这一点,我想要传递的那些对象中的所有内容也只有 POD 变量和一些 STL 容器,如 std::vectorstd::pair 及其值(没有指针或任何东西),以及这些的 getter变量。 没有虚函数,除了虚析构函数,它的存在是因为我从一个基本消息类继承了所有消息,因为当时我在想可能有一些通用的基本功能。 (我现在可以很容易地摆脱这个基类,因为到目前为止还没有什么真正常见的东西!谢天谢地,出于某种原因,我将消息的解析和格式化保留在一个单独的类中。运气还是远见?:D)

    1234563是否使用了“虚拟”关键字?

    我不是为我的案例寻求解决方案。我可以将这些对象的结果包装到结构中并通过 IPC 传递它们,或者我可以摆脱基类和虚拟析构函数,如上面“我的”第 1 点所述。

不需要 Boost 或任何 C++11 的东西或任何处理此问题的库。这方面的任何建议都与手头的问题无关。

(p.s. 现在我发布并重新阅读了我发布的内容,我想将可能在任何阅读此、那个的读者脑海中蔓延的想法扼杀在萌芽状态......我问这个是为了我的知识,而不是与那位同事争论。怀疑是好的,但如果我们都假设其他人有良好的意图,这对社区来说会很好。:))

【问题讨论】:

即使您不使用 Boost,您也可以查看 Boost.Interprocess 的文档,了解哪些是可能的,哪些不是,以及有哪些问题和解决方法。 【参考方案1】:

每个人都不断指出的唯一警告是,指针(当然!当然)不能像这样传递

指针值(以及其他对内存和资源的引用)在进程间确实毫无意义。这显然是虚拟内存的结果。

另一个警告是,虽然 C 标准为结构指定了精确的(特定于平台的)内存布局,但 C++ 标准通常不保证类的特定内存布局。例如,一个进程不一定与另一个进程在成员之间的填充量上达成一致——即使在同一系统中也是如此。 C++ 只保证标准布局类型的内存布局——这种保证布局与 C 结构匹配。


... 和一些 STL 容器,例如 std::vector ...(没有指针或任何东西)

std::array 之外的所有标准容器都在内部使用指针。它们必须这样做,因为它们的大小是动态的,因此必须动态分配数据结构。此外,这些都不是标准布局类。此外,不能保证一个标准库实现的类定义与另一个实现匹配,并且两个进程可以使用不同的标准库——这在 Linux 上并不少见,其中一些进程可能使用 libstdc++(来自 GNU)而其他进程可能使用 libc++(来自 Clang)。

没有虚函数除了虚析构函数

换句话说:至少有一个虚函数(析构函数),因此有一个指向vtable的指针。而且也没有保证的内存布局,因为具有虚函数的类从来都不是标准的布局类。


所以回答问题:

    大多数情况下没有 FUD,尽管有些说法在技术上有点不准确:

      C++ 对象可能有 vtables;并非所有人都这样做。 C 结构可以有指针,所以也不是所有的 C 结构都可以共享。一些 C++ 对象可以跨进程共享。具体来说,可以共享标准布局类(假设没有指针)。 实际上无法共享具有 vtable 的对象。 标准布局类有保证的内存布局。只要您将自己限制在标准布局类中,更改编译器就不是问题。如果运气不好,尝试共享其他类可能会奏效,但在开始混合编译器时可能会遇到问题。

    C++ 标准定义了类是标准布局的确切条件。所有 C 结构定义都是 C++ 中的标准布局类。编译器知道这些规则。

    这不是问题。


结论:您可以将 C++ 用于 IPC,但您仅限于该接口中的标准布局类。这将您排除在许多 C++ 功能之外,例如虚函数、访问说明符等。但不是全部:例如,您仍然可以拥有成员函数。

但请注意,使用 C++ 功能可能会导致进程间接口仅适用于 C++。许多语言可以与 C 接口,但几乎没有任何语言可以与 C++ 接口。

此外:如果您的“进程间”通信超出了系统的边界——即跨网络——即使是 C 结构或标准布局类也不是一个好的表示。在这种情况下,您需要序列化。

【讨论】:

【参考方案2】:
    他的三个主张真的如此还是只是 FUD?考虑到这一点,我想要传递的那些对象中的所有内容也只有 POD 变量和一些 STL 容器,如 std::vector 和 std::pair 及其值(没有指针或任何东西),以及 getter对于那些变量。除了虚析构函数之外没有虚函数,因为我从一个基本消息类继承了所有消息,所以它存在,因为当时我在想可能有一些通用的基本功能。 (我现在可以很容易地摆脱这个基类,因为到目前为止还没有什么真正常见的东西!谢天谢地,由于某种原因,我将消息的解析和格式化保留在一个单独的类中。运气还是远见?:D)

不,只要 stl 容器在您的结构中,您就不能像 POD 数据那样传递它们。未指定 stl 容器的实现,它们可能(并且在大多数情况下确实)包含用于内部目的的指针。

    这实际上也让我想知道,既然我们在整个项目中都使用 g++ 编译器,那么编译器如何知道一个结构何时是 C 风格的结构?是否使用了“虚拟”关键字?

只要您的结构/类只有 POD 数据而没有虚函数,它将被存储为 POD,但是如果您的 IPC 的另一端已使用另一个编译器和/或不同的编译器编译,则对齐差异可能是一个问题设置或不同的对齐指令(例如#pragma pack 等)。

    我不是为我的案例寻求解决方案。我可以将这些对象的结果包装到结构中并通过 IPC 传递它们,或者我可以摆脱基类和虚拟析构函数,如上面“我的”第 1 点所述。

将这些对象的结果包装到结构中并通过 IPC 传递它们对我来说听起来不错,就我个人而言,我会这样做。另一种解决方案也不错,没有上下文很难判断哪个更好。

【讨论】:

@JoeyMallone std::vector 绝对使用指针(几乎是“根据定义”)。 @JoeyMallone 看看Standard Layout(并将“其他编程语言”读作“C 或会说 C 的东西”) 作为补充,虚拟析构函数足以使结构不是 POD,即使不涉及 C++ 库容器。 @Jabberwocky:我坚持这样做是因为 OP 写了 除了虚拟析构函数之外没有虚拟函数。也许我对他的问题的评论比对你的回答更好...... @JoeyMallone 存在虚拟析构函数 => 存在虚拟函数 => 不是 POD,因此存在虚拟析构函数 => 不是 POD。 qed【参考方案3】:

“未经证实”的声明/声明:

C++ 对象有 vtables。 C 风格的结构只是变量和值。因此,C 风格的结构可以在进程中传递,而 C++ 对象不能。

这句话部分正确,但被误导了。

并非所有 C++ 对象都有 vtables(从技术上讲,C++ 标准根本不需要 vtables,尽管它是一种用于支持虚函数分派的常见实现技术,因为它提供了各种优势)。

如果您查看this SO question and various answers,您会发现有关 C++ 中聚合和 POD 类型的讨论。问题是定义在 C++ 标准之间演变(正如对该问题的各种答案所反映的那样)。在 C++11 中,POD 类型的概念发生了变化,并有效地替换为普通和标准布局类型的概念。

POD 类型(C++11 之前)和标准布局类型(C++11 和更高版本)可以在 C++ 和 C 之间互换(即从一种语言编写的代码传递到另一种语言编写的代码,因为内存布局兼容)。

确实,具有任何虚函数的 C++ 对象都属于不能与 C 互换的对象。指针通常不能被复制,这妨碍了(大多数)C++ 标准容器的使用。

使用 C 风格的结构,我们可以嵌入信息,例如 struct,以便双方都知道期望什么以及发送什么,但是 对于 C++ 对象,这是不可能的,因为 'vtable 的大小 可能会有所不同'。

这个说法是错误的,因为有些类型没有 vtable,并且类型可以在 C++ 和 C 之间互换。

如果我们改变编译器,那就更糟了。对于 C++ 对象,我们会处理更多的排列。

同样,只要在 C++ 代码中正确选择了类型,就可以将它们与 C 互换。

这句话——它适用于 C 与 C++ 的互操作——也适用于 C。C 类型的大小是在 C 中正式定义的实现,就像在 C++ 中一样。 intlongfloatdouble 等类型不能保证在不同的编译器中具有相同的大小。有些编译器的设置会改变部分或所有基本类型的大小(例如,具有不同浮点选项的编译器、具有影响 int 是 16 位还是 32 位的设置的编译器等)。

C 中的struct 类型在成员之间也可能有填充,并且填充在 C 编译器之间可能会有所不同。许多编译器具有影响填充的编译选项,这会影响struct 类型的大小。这可能会导致 C 中相同 struct 类型的布局不兼容。

那么这里可能发生了什么?

进程间通信的设计可能假设它总是在 C 代码之间,使用相同(或兼容)编译器构建。 IPC 机制可能非常简单:例如,一个进程将指定内存位置的一定数量的数据沿管道喷出,而接收方将在该管道另一端接收到的数据复制到等效的数据结构中。

隐含的假设是可以通过这种方式直接复制数据。这依赖于两个程序中兼容的数据类型的布局。

问题在于,由于 IPC 机制是在假设兼容 C 编译器的情况下设计的,现在您被告知这是因为 C 优于 C++(或其他语言)的优势。它不是。这是如何完成 IPC 的人工制品。

IPC 方法可能非常有限,但您的 C++ 代码可以通过 IPC 机制发送和接收数据,只要您在 C++ 代码中以适当的类型(例如标准布局)打包数据。其他进程是用 C 还是 C++ 编写的也没有关系。在 C++ 中可能需要更多的工作(例如,将 C++ 类中的数据打包到标准布局结构中,然后将该结构喷射到其他进程 - 或者如果接收数据则相反),但这当然是可能的。

无论如何,您都需要使用兼容的编译器。

这假设您无法更改进程间通信的方式(例如,设计一个用于在进程之间进行通信的协议,而不是盲目地将数据从内存位置复制到另一个进程,然后接收进程复制数据返回到兼容的数据结构中)。有一些做 IPC 的方法,如果需要的话,可以更好地支持一系列编程语言——尽管有不同的权衡(例如,通信带宽、转换数据以便发送数据的代码,以及接收数据和转换数据的代码)回到数据结构中)。

【讨论】:

以上是关于进程间通信:传递 C 风格的结构与 C++ 对象的主要内容,如果未能解决你的问题,请参考以下文章

进程间通信的问题(C++高手进)

Linux-进程间通信

Linux-进程间通信

Linux-进程间通信

Linux C与C++一线开发实践之四 Linux进程间的通信

Linux C与C++一线开发实践之四 Linux进程间的通信