Pthreads和不透明类型

时间:2014-06-05 18:10:44

标签: c struct pthreads unions

我正在读取pthreads库的头文件,并在bits / pthreadtypes.h中找到了互斥体(和其他类型)的这个特定定义:

typedef union
{
  struct __pthread_mutex_s
  {
    int __lock;
    unsigned int __count;
    int __owner;
    /* KIND must stay at this position in the structure to maintain
       binary compatibility.  */
    int __kind;
    unsigned int __nusers;
    __extension__ union
    {
      int __spins;
      __pthread_slist_t __list;
    };
  } __data;
  char __size[__SIZEOF_PTHREAD_MUTEX_T];
  long int __align;
} pthread_mutex_t;

这不完全是这样,但为了清晰起见,我简化了它。在头文件和实现文件中创建一个具有两个不同定义的结构,实现真正的结构定义,标题只是真实结构大小的字符缓冲区,用作隐藏实现的技术(opaque类型) )但在调用malloc或在堆栈中分配对象时仍然分配正确的内存量。

这个特定的实现使用了一个union并且仍然暴露了struct的结构和字符缓冲区,但是在隐藏结构方面似乎没有提供任何好处,因为结构仍然暴露,并且二进制兼容性是仍然依赖于结构不变。

  1. 为什么pthreads中定义的类型遵循此模式?
  2. 如果您不提供二进制兼容性(如不透明指针模式),拥有opaque类型有什么好处?我理解安全性就是其中之一,因为你不允许用户篡改结构的字段,但是还有什么吗?
  3. pthread类型是否主要暴露于允许静态初始化,还是有其他特定原因?
  4. 在不透明指针模式之后的pthreads实现是否可行(即根本不暴露任何类型并且不允许静态初始化)?或者更具体地说,是否存在只能通过静态初始化来解决问题的情况?
  5. 完全没有关系,是否有“C之前的主线”?

4 个答案:

答案 0 :(得分:5)

我的看法是__size__align字段指定(猜测:-))结构的大小和对齐方式,与__data结构无关。因此,数据可以是更小的尺寸并且具有更少的对准要求,它可以自由地修改而不会破坏关于它的这些基本假设。反之亦然,这些基本特征可以在不改变数据结构的情况下进行更改,例如here

重要的是要注意,如果__data的大小变得大于__SIZEOF_PTHREAD_MUTEX_T指定的大小,则__pthread_mutex_init()中的断言失败:

assert (sizeof (pthread_mutex_t) <= __SIZEOF_PTHREAD_MUTEX_T);

将此断言视为此方法的重要组成部分。

因此,结论是这样做不是为了隐藏实现细节,而是为了使数据结构更具可预测性和可管理性。对于一个广泛使用的库来说非常重要,它应该关注可以对这个结构进行更改的其他代码的向后兼容性和性能影响。

答案 1 :(得分:0)

标准委员会(如IEEE和POSIX)通过迭代开发和发展标准,这些迭代可提供更多功能或纠正先前版本标准的问题。此过程由具有问题域软件需求的人员以及支持这些人员的软件产品供应商的需求驱动。通常,标准的实施在某种程度上会因供应商而异。与任何其他软件一样,不同的人根据目标环境以及他们自己的技能和知识提供实施方面的差异。然而,随着标准的成熟,有一种达尔文选择,其中就最佳实践达成一致,各种实现开始趋同。

pthreads POSIX库的第一个版本是在20世纪90年代针对UNIX风格的操作系统环境,例如参见POSIX. 4: Programming for the Real World,另见PThreads Primer: A guide to Multithreaded Programming。该库的思想和概念源于之前完成的工作,旨在提供一种协同例程或线程类型的功能,这些功能在比操作系统进程级别更精细的级别上工作,以减少创建,管理和销毁进程的开销。参与其中。有两种主要的线程方法,用户级别,内核支持很少,内核级别取决于操作系统,以提供线程管理,具有一些不同的功能,如先发制人的线程切换或不可用。

此外,工具制造商(如调试器)也需要为在多线程环境中工作提供支持,并能够查看线程状态并识别特定线程。

在API中为API使用opaque类型有几个原因。主要原因是允许库的开发人员灵活地修改类型,而不会给库的用户带来问题。有几种方法可以在C中创建不透明类型。

一种方法是要求API的用户使用指向由API库管理的某个内存区域的指针。您可以在标准C库中查看此方法的示例,其中包含fopen()等文件访问函数,它返回指向FILE类型的指针。

虽然这实现了创建opaque类型的目标,但它需要API库来管理内存分配。由于它是指针,因此可能会遇到分配内存并且从未释放或尝试使用已释放内存的指针的问题。这也意味着专用硬件上的专用应用程序可能很难将功能移植到具有裸支持的专用传感器,而不包括内存分配器。这种隐藏的开销也会影响资源有限的专用应用程序,并能够预测或建模应用程序使用的资源。

第二种方法是向API的用户提供一个数据结构,该结构与API使用的实际数据结构大小相同,但使用char缓冲区来分配内存。这种方法隐藏了内存布局的细节,因为API看到的所有用户都是一个char缓冲区或数组,但它也分配了API使用的正确内存量。然后,API有自己的结构,用于说明内存的实际使用方式,API在内部进行指针转换,以更改用于访问内存的结构。

这第二种方法提供了一些很好的好处。首先,API使用的内存现在由API的用户管理,而不是库本身。 API的用户可以决定是否要使用堆栈分配或全局静态分配或其他一些内存分配,例如malloc()。 API的用户可以决定是否要将内存分配包装在某种资源跟踪中,例如引用计数或用户想要做的其他管理(尽管这也可以通过指针不透明类型来完成)以及)。这种方法还允许API的用户更好地了解内存消耗,并为专用硬件上的专用应用程序建模内存消耗。

API设计者还可以向API的用户提供某些类型的数据,这些数据可能很方便,例如状态信息。此状态信息的目标是允许API的用户查询等于仅直接读取结构成员的内容,而不是为了提高效率而经历某种辅助函数的开销。虽然成员未指定为const(为了鼓励C编译器引用实际成员而不是在某个时间点缓存该值,具体取决于它不会更改),API可能会在操作期间更新字段向API的用户提供信息,而不依赖于这些字段的值供自己使用。

但是,任何此类数据字段都存在引入向后兼容性问题以及引入内存布局问题的更改的风险。 AC编译器可以在结构的成员之间引入填充,以便在将数据加载和存储到这些成员中时提供有效的机器指令,或者由于CPU架构需要某种类型的指令的某种起始存储器地址边界。

特别是对于pthreads库,我们受到了20世纪80年代和90年代的UNIX风格C编程的影响,这些编程往往具有开放和可见的数据结构和头文件,允许程序员通过注释读取结构定义和定义的常量。可用的文档是源。

不透明结构的简要示例如下。有一个include文件,thing.h,它包含opaque类型,任何使用API​​的人都包含它。然后有一个库,其源文件thing.c包含使用的实际结构。

thing.h可能看起来像

#define MY_THING_SIZE  256

typedef struct {
    char  array[MY_THING_SIZE];
} MyThing;

int DoMyThing (MyThing *pMyThing, int stuff);

然后在实现文件thing.c中,您可能拥有以下

之类的源代码
typedef struct {
    int   thingyone;
    int   thingytwo;
    char  aszName[32];
} RealMyThing;

int DoMyThing (MyThing *pMyThing, int stuff)
{
    RealMyThing *pReal = (RealMyThing *)pMyThing;

    // do stuff with the real memory layout of MyThing
    return 0;
}

关注&#34;在主要&#34;之前线程

当启动使用C运行时的应用程序时,加载程序使用C运行时的入口点作为应用程序起始位置。然后,C运行时执行它需要执行的初始化和环境设置,然后调用实际应用程序的指定入口点。历史上,此指定的入口点是函数main(),但C运行时使用的内容可能因操作系统和开发环境而异。例如,对于Windows GUI应用程序,指定的入口点为WinMain()(请参阅WinMain entry point)而不是main()

由C运行时决定调用应用程序的指定入口点的条件。是否有&#34; pre-main&#34;运行的线程将取决于C运行时和目标环境。

使用带有自己的消息泵的Active-X控件的Windows应用程序,很可能是&#34; pre-main&#34;线程。我使用一个大型Windows应用程序,它使用几个控件提供各种设备接口,当我查看调试器时,我可以看到许多线程,我的应用程序的源不是通过特定的创建线程调用创建的。这些线程在运行时启动,因为使用的Active-X控件已加载并启动。

答案 2 :(得分:0)

是的,通常一个实现会隐藏这样一个结构的大部分细节,或者像这样(在某些先前包含的系统头文件中可能会定义__SIZEOF_PTHREAD_MUTEX_T):

typedef union
{
    char      __size[__SIZEOF_PTHREAD_MUTEX_T];
    long int  __align;
} pthread_mutex_t;

或者像这样:

typedef union
{
#if __COMPILE_FOR_SYSTEM
    struct __pthread_mutex_s
    {
        ...internal struct member declarations...
    } __data;
#endif
    char __size[__SIZEOF_PTHREAD_MUTEX_T];
    long int __align;
} pthread_mutex_t;

第一个表单完全隔离了结构声明的内部与客户端代码。然后,访问结构的实际内部将需要包含具有完整结构声明的系统内核头文件,这是常规客户端代码通常无法访问的内容。由于客户端代码应仅处理指向此struct / union类型的指针,因此实际成员可以对所有客户端代码保持隐藏。

第二种形式将struct内部暴露给程序员,但不暴露给编译器(可能是__COMPILE_FOR_SYSTEM在一些其他系统头文件中定义,只能在编译内核代码时使用。

那么,问题仍然存在,为什么这个库的实现者选择将内部细节留给编译器?毕竟,似乎第二种解决方案很容易提供。

我的猜测是,在这种特殊情况下,要么实施者都忘了它。或许他们的源代码和头文件代码排列不完美,因此他们需要保持成员暴露,以便他们的编译工作(但这是值得怀疑的。)

很抱歉,这并没有真正回答你的问题。

答案 3 :(得分:0)

  1. 我之前看过结构和数据缓冲区的联合,以支持双字比较和交换指令(具有特定的对齐要求);该指令可能是他们用来实现互斥锁功能的。

  2. 允许实施者更自由地在快速高效的pthreads库中实现他们的愿景,同时仍然为最终用户提供统一的推断。

  3. Main是一个内在概念,通常在main之前调用一个函数来设置标准文件描述符等。在GCC中,您可以将属性'__attribute __((constructor))'添加到函数中,然后在main之前调用它(然后它可以启动一堆线程然后退出)。但是,生成其他进程或线程的根进程/线程必须首先出现(如果这是您的问题)。