进程间通信:传递 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::vector
和 std::pair
及其值(没有指针或任何东西),以及这些的 getter变量。 没有虚函数,除了虚析构函数,它的存在是因为我从一个基本消息类继承了所有消息,因为当时我在想可能有一些通用的基本功能。 (我现在可以很容易地摆脱这个基类,因为到目前为止还没有什么真正常见的东西!谢天谢地,出于某种原因,我将消息的解析和格式化保留在一个单独的类中。运气还是远见?:D)
我不是为我的案例寻求解决方案。我可以将这些对象的结果包装到结构中并通过 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 传递它们对我来说听起来不错,就我个人而言,我会这样做。另一种解决方案也不错,没有上下文很难判断哪个更好。
【讨论】:
@JoeyMallonestd::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++ 中一样。 int
、long
、float
、double
等类型不能保证在不同的编译器中具有相同的大小。有些编译器的设置会改变部分或所有基本类型的大小(例如,具有不同浮点选项的编译器、具有影响 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++ 对象的主要内容,如果未能解决你的问题,请参考以下文章