0

我正在处理一些我想记忆映射一些包含数字数据的大文件的东西。问题是数据可以是多种格式,包括实数字节/短/整数/长/浮点/双精度和复数字节/短/整数/长/浮点/双精度。自然而然地处理所有这些类型很快就会变得笨拙,所以我正在考虑实现一个内存映射接口,可以为用户进行实时类型转换。

我真的很喜欢映射文件的想法,这样你就可以在内存中得到一个指针,做你需要的任何事情,然后取消映射它。不需要缓冲学或其他任何东西。因此,读取数据并为我进行类型转换的函数将大大减少这一点。

我在想我可以内存映射正在操作的文件,然后同时映射一个匿名文件,并以某种方式捕获页面获取/存储并按需进行类型转换。我将在 64 位上工作,所以在这些情况下这会给你一个 63 位的地址空间,但是哦,好吧。

有谁知道这种 mmap 挂钩是否可行,如果可以,如何实现?

4

3 回答 3

1

阅读部分对我来说听起来有些可行。我没有这方面的经验,但原则上让信号处理程序获取您的数据并在您访问用户呈现的缓冲区中尚不存在的页面时立即转换它应该是可能的。但可能这样的事情效率很低,你会在每个页面上都有一个上下文切换。

我猜反过来会更难。默认情况下,写入是异步的,因此很难捕获它们。

因此,您想要的“一半”可能是可能的:始终以用户想要的格式将数据写入新文件,但在读取此类文件时会即时自动翻译。

但我认为,对您来说更重要的是,您对不同的存储表示具有清晰的语义,并且正确地封装了数据项的读取或写入。如果您有这样的界面(例如“将元素存储在具有类型E的位置”),您可以轻松触发与目标格式相关的转换。iT

于 2012-07-25T06:57:16.170 回答
1

是的(-ish)。您可以创建不可访问mmap的区域。每当有人试图触摸一个时,SIGSEGV通过修复其权限、填充和恢复来处理引发的问题。

long *long_view =
   mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
double *double_view =
   mmap(NULL, 4096, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

static void on_segv(int signum, siginfo_t *info, void *data) {
    void *addr = info->si_addr;
    if ((uintptr_t)addr - (uintptr_t)long_view < 4096) {
        mprotect(long_view, 4096, PROT_READ|PROT_WRITE);
        /* translate from double_view to long_view */
        mprotect(double_view, 4096, PROT_NONE);
    } else if ((uintptr_t)addr - (uintptr_t)double_view < 4096) {
        mprotect(double_view, 4096, PROT_READ|PROT_WRITE);
        /* translate from long_view to long_view */
        mprotect(double_view, 4096, PROT_NONE);
    } else {
        abort();
    }
}

struct sigaction segv_action = {
    .sa_sigaction = on_segv,
    .sa_flags = SA_RESTART | SA_SIGINFO,
};
sigaction(SIGSEGV, &segv_action, NULL);

long_view[0] = 42;
/* hopefully, this will trigger the code to fixup double_view and resume */
printf("%g\n", double_view[0]);

(未经测试,但这些方面的东西应该可以工作......)

如果您不想一次填满整个页面,我认为这仍然可行...第三个参数可以转换为 a ucontext_t *,您可以使用它来解码正在执行的指令并将其修复,就好像它已经执行了预期的操作,同时留下内存PROT_NONE以捕获进一步的访问......但它会慢很多,因为你要捕获每个访问,而不仅仅是第一个。

于 2012-07-25T07:09:01.723 回答
1

您是否有理由不使用访问器功能?

有两种基本情况:结构化数据和纯数据。结构化数据具有混合数据类型,纯数据只有您列出的一种格式。如果您可以为每种使用的类型(列出的所有类型总共 28 或 30 个字节)存储一个原型值(具有不同的字节组件),您还可以支持透明的字节序校正——复杂类型只是对,并且具有相同的字节顺序作为基本组件)。我已经使用这种方法来存储和访问来自分子动力学模拟的原子数据,并且在实践中非常有效——这是我测试过的最快的便携式方法。

我会使用一个结构来描述“文件”(支持文件、内存映射、字节顺序和数据格式,如果是纯数据):

struct file {
    int            descriptor;
    size_t         size;
    unsigned char *data;
    unsigned int   endian;   /* Relative to current architecture */
    unsigned int   format;   /* endian | format, for unstructured files */
};

#define  ENDIAN_I16_MASK  0x0001
#define  ENDIAN_I16_12    0x0000
#define  ENDIAN_I16_21    0x0001

#define  ENDIAN_I32_MASK  0x0006
#define  ENDIAN_I32_1234  0x0000
#define  ENDIAN_I32_4321  0x0002
#define  ENDIAN_I32_2143  0x0004
#define  ENDIAN_I32_3412  0x0006

#define  ENDIAN_I64_MASK  0x0018
#define  ENDIAN_I64_1234  0x0000
#define  ENDIAN_I64_4321  0x0008
#define  ENDIAN_I64_2143  0x0010
#define  ENDIAN_I64_3412  0x0018

#define  ENDIAN_F16_MASK  0x0020
#define  ENDIAN_F16_12    0x0000
#define  ENDIAN_F16_21    0x0020

#define  ENDIAN_F32_MASK  0x00C0
#define  ENDIAN_F32_1234  0x0000
#define  ENDIAN_F32_4321  0x0040
#define  ENDIAN_F32_2143  0x0080
#define  ENDIAN_F32_3412  0x00C0

#define  ENDIAN_F64_MASK  0x0300
#define  ENDIAN_F64_1234  0x0000
#define  ENDIAN_F64_4321  0x0100
#define  ENDIAN_F64_2143  0x0200
#define  ENDIAN_F64_3412  0x0300

#define  FORMAT_MASK      0xF000

#define  FORMAT_I8        0x1000
#define  FORMAT_I16       0x2000
#define  FORMAT_I32       0x3000
#define  FORMAT_I64       0x4000

#define  FORMAT_P8        0x5000   /* I8 pair ("complex I8") */
#define  FORMAT_P16       0x6000   /* I16 pair ("complex I16") */
#define  FORMAT_P32       0x7000   /* I32 pair ("complex I32") */
#define  FORMAT_P64       0x8000   /* I64 pair ("complex I64") */

#define  FORMAT_R16       0x9000   /* BINARY16 IEEE-754 floating-point */
#define  FORMAT_R32       0xA000   /* BINARY32 IEEE-754 floating-point */
#define  FORMAT_R64       0xB000   /* BINARY64 IEEE-754 floating-point */

#define  FORMAT_C16       0xC000   /* BINARY16 IEEE-754 complex */
#define  FORMAT_C32       0xD000   /* BINARY32 IEEE-754 complex */
#define  FORMAT_C64       0xE000   /* BINARY64 IEEE-754 complex */

访问器功能可以以各种方式实现。在 Linux 中,标记static inline的函数也与宏一样快。

由于该double类型不完全涵盖 64 位整数类型(因为尾数只有 52 位),我将定义一个数字结构,

#include <stdint.h>

struct number {
    int64_t   ireal;
    int64_t   iimag;
    double    freal;
    double    fimag;
};

并让访问器函数始终填写四个字段。使用 GCC,您还可以创建一个宏来使用自动类型检测定义结构编号:

#define  Number(x) \
    ( __builtin_types_compatible_p(__typeof__ (x), double)          ? number_double(x) : \
      __builtin_types_compatible_p(__typeof__ (x), _Complex double) ? number_complex_double(x) : \
      __builtin_types_compatible_p(__typeof__ (x), _Complex long)   ? number_complex_long(x) : \
                                                                      number_int64(x) )

static inline struct number number_int64(const int64_t  x)
{
    return (struct number){ .ireal = (int64_t)x,
                            .iimag = 0,
                            .freal = (double)x,
                            .fimag = 0.0 };
}

static inline struct number number_double(const double  x)
{
    return (struct number){ .ireal = (int64_t)x,
                            .iimag = 0,
                            .freal = x,
                            .fimag = 0.0 };
}

static inline struct number number_complex_long(const _Complex long  x)
{
    return (struct number){ .ireal = (int64_t)(__real__ (x)),
                            .iimag = (int64_t)(__imag__ (x)),
                            .freal = (double)(__real__ (x)),
                            .fimag = (double)(__imag__ (x)) };
}


static inline struct number number_complex_double(const _Complex double  x)
{
    return (struct number){ .ireal = (int64_t)(__real__ (x)),
                            .iimag = (int64_t)(__imag__ (x)),
                            .freal = __real__ (x),
                            .fimag = __imag__ (x) };
}

这意味着只要是整数或浮点实数或复数类型,就可以Number(value)构造一个正确的类型。struct numbervalue

请注意整数和浮点组件如何设置为相同的值,只要类型转换允许。(对于数量级非常大的整数,浮点值将是一个近似值。(int64_t)round(...)在设置整数分量时,您还可以使用舍入而不是截断浮点参数。

您将需要四个访问器函数:两个用于结构化数据,两个用于非结构化数据。对于非结构化(纯)数据:

static inline struct number file_get_number(const struct file *const  file,
                                            const size_t              offset)
{
    ...
}

static inline void file_set_number(const struct file *const  file,
                                   const size_t              offset,
                                   const struct number       number)
{
    ...
}

注意offset上面不是字节偏移,而是数字的索引。对于结构化文件,您需要使用字节偏移量,并添加一个指定文件中使用的数字格式的参数:

static inline struct number file_get(const struct file *const  file,
                                     const size_t              byteoffset,
                                     const unsigned int        format)
{
    ...
}

static inline void file_set(const struct file *const  file,
                            const size_t              byteoffset,
                            const unsigned int        format,
                            const struct number       number)
{
    ...
}

我省略的函数体中所需的转换 (...) 非常简单。您也可以采取一些技巧进行优化。例如,我喜欢调整字节序常数,使低位始终是字节交换(ab -> ba,abcd -> badc,abcdefgh -> badcfehg),高位是短交换(abcd -> cdab , abcdefgh->cdabghef)。如果您想完全确定,您可能需要第三位来表示 64 位值(abcdefgh -> efghabcd)。

函数体中的 if 或 case 语句确实会导致很小的访问开销,但它应该足够小,可以在实践中忽略。避免它的所有方法都会导致更复杂的代码。(为了获得最大吞吐量,您需要对所有访问变体进行开放编码,并__builtin_types_compatible_p()在函数或宏中使用以确定要使用的正确方法。如果考虑字节序转换,这意味着很多函数。我相信非常小的访问开销——每次访问最多几个时钟——更可取。(无论如何,我所有的测试都是 I/O 绑定的,即使是 200 Mb/s,所以对我来说开销完全无关紧要。)

通常,对于使用原型值的自动字节顺序转换,您只需测试该类型的每个可能的转换。只要原型值的每个字节分量都是唯一的,那么只有一次转换会产生正确的预期值。在某些架构上,整数和浮点值具有不同的字节序;这就是为什么ENDIAN_常量分别用于每种类型和大小的原因。

假设您已经实现了上述所有内容,那么在您的应用程序代码中,数据访问将类似于

struct file    f;

/* Set first number to zero. */
file_set_number(&f, 0, Number(0));

/* Set second number according to variable v,
 * which can be just about any numeric type. */
file_set_number(&f, 1, Number(v));

希望这个对你有帮助。

于 2012-07-25T11:55:17.393 回答