18

由于一些与此问题无关的晦涩原因,我需要使用 MAP_FIXED 来获取靠近 libc 的文本部分在内存中的位置的页面。

在阅读 mmap(2) 之前(我应该首先完成),如果我使用 MAP_FIXED 调用 mmap 并且基地址与已经映射的区域重叠,我预计会出现错误。

然而事实并非如此。例如,这里是某些进程的 /proc/maps 的一部分

7ffff7299000-7ffff744c000 r-xp 00000000 08:05 654098                     /lib/x86_64-linux-gnu/libc-2.15.so

其中,在进行以下 mmap 调用后...

  mmap(0x7ffff731b000,
       getpagesize(),
       PROT_READ | PROT_WRITE | PROT_EXEC,
       MAP_ANONYMOUS | MAP_PRIVATE | MAP_FIXED,
       0,
       0);

... 变成:

7ffff7299000-7ffff731b000 r-xp 00000000 08:05 654098                     /lib/x86_64-linux-gnu/libc-2.15.so
7ffff731b000-7ffff731c000 rwxp 00000000 00:00 0 
7ffff731c000-7ffff744c000 r-xp 00083000 08:05 654098                     /lib/x86_64-linux-gnu/libc-2.15.so

这意味着我已经用我自己的页面覆盖了专用于 libc 的部分虚拟地址空间。显然不是我想要的...

在 mmap(2) 手册的 MAP_FIXED 部分中,它明确指出:

如果 addr 和 len 指定的内存区域与任何现有映射的页面重叠,则现有映射的重叠部分将被丢弃

这解释了我所看到的,但我有几个问题:

  1. 有没有办法检测某些东西是否已经映射到某个地址?没有访问/proc/maps?
  2. 在查找重叠页面的情况下,有没有办法强制 mmap 失败?
4

3 回答 3

9
  1. 用于page = sysconf(SC_PAGE_SIZE)找出页面大小,然后扫描您希望使用的每个页面大小的块msync(addr, page, 0)(使用(unsigned long)addr % page == 0,即addr与页面对齐)。如果它返回-1with errno == ENOMEM,则该页面未映射。

    编辑:正如下面评论的那样,mincore(addr,page,&dummy)优于msync(). (系统调用的实现mm/mincore.c在 Linux 内核源代码中,C 库通常提供更新的包装器errno。由于系统调用在确保页面对齐后立即进行映射检查addr,因此在未映射的情况下是最佳的 ( ENOMEM) . 如果页面已经被映射,它会做一些工作,所以如果性能是最重要的,尽量避免检查你知道被映射的页面。

    您必须单独执行此操作,每个页面单独执行,因为对于大于单个页面的区域,ENOMEM意味着该区域未完全映射;它可能仍被部分映射。映射总是细化到页面大小的单元。

  2. 据我所知,mmap()如果该区域已映射或包含已映射的页面,则无法判断失败。(同样适用于mremap(),因此您无法创建映射,然后将其移动到所需区域。)

    这意味着您有竞争条件的风险。最好自己执行实际的系统调用,而不是 C 库包装器,以防它们在内部进行内存分配或更改内存映射:

    #define _GNU_SOURCE
    #include <unistd.h>
    #include <sys/syscall.h>
    
    static size_t page = 0;
    static inline size_t page_size(void)
    {
        if (!page)
            page = (size_t)sysconf(_SC_PAGESIZE);
        return page;
    }
    
    
    static inline int raw_msync(void *addr, size_t length, int flags)
    {
        return syscall(SYS_msync, addr, length, flags);
    }
    
    static inline void *raw_mmap(void *addr, size_t length, int prot, int flags)
    {
        return (void *)syscall(SYS_mmap, addr, length, prot, flags, -1, (off_t)0);
    }
    

但是,我怀疑无论您尝试做什么,您最终都需要解析/proc/self/maps

  • 我建议完全避免使用标准 I/O stdio.h(因为各种操作将动态分配内存,从而更改映射),而是使用unistd.h不太可能影响映射的较低级别的接口。这是一组简单粗暴的函数,可用于找出每个映射区域以及该区域中启用的保护(并丢弃其他信息)。在实践中,它使用大约一千字节的代码,比堆栈中的代码少,因此即使在有限的架构(例如嵌入式设备)上也非常有用。

    #include <unistd.h>
    #include <fcntl.h>
    #include <errno.h>
    #include <string.h>
    
    #ifndef   INPUT_BUFFER
    #define   INPUT_BUFFER   512
    #endif /* INPUT_BUFFER */
    
    #ifndef   INPUT_EOF
    #define   INPUT_EOF     -256
    #endif /* INPUT_EOF */
    
    #define   PERM_PRIVATE  16
    #define   PERM_SHARED    8
    #define   PERM_READ      4
    #define   PERM_WRITE     2
    #define   PERM_EXEC      1
    
    typedef struct {
        int            descriptor;
        int            status;
        unsigned char *next;
        unsigned char *ends;
        unsigned char  buffer[INPUT_BUFFER + 16];
    } input_buffer;
    
    /* Refill input buffer. Returns the number of new bytes.
     * Sets status to ENODATA at EOF.
    */
    static size_t input_refill(input_buffer *const input)
    {
        ssize_t n;
    
        if (input->status)
            return (size_t)0;
    
        if (input->next > input->buffer) {
            if (input->ends > input->next) {
                memmove(input->buffer, input->next,
                        (size_t)(input->ends - input->next));
                input->ends = input->buffer + (size_t)(input->ends - input->next);
                input->next = input->buffer;
            } else {
                input->ends = input->buffer;
                input->next = input->buffer;
            }
        }
    
        do {
            n = read(input->descriptor, input->ends,
                     INPUT_BUFFER - (size_t)(input->ends - input->buffer));
        } while (n == (ssize_t)-1 && errno == EINTR);
        if (n > (ssize_t)0) {
            input->ends += n;
            return (size_t)n;
    
        } else
        if (n == (ssize_t)0) {
            input->status = ENODATA;
            return (size_t)0;
        }
    
        if (n == (ssize_t)-1)
            input->status = errno;
        else
            input->status = EIO;
    
        return (size_t)0;
    }
    
    /* Low-lever getchar() equivalent.
    */
    static inline int input_next(input_buffer *const input)
    {
        if (input->next < input->ends)
            return *(input->next++);
        else
        if (input_refill(input) > 0)
            return *(input->next++);
        else
            return INPUT_EOF;
    }
    
    /* Low-level ungetc() equivalent.
    */
    static inline int input_back(input_buffer *const input, const int c)
    {
        if (c < 0 || c > 255)
            return INPUT_EOF;
        else
        if (input->next > input->buffer)
            return *(--input->next) = c;
        else
        if (input->ends >= input->buffer + sizeof input->buffer)
            return INPUT_EOF;
    
        memmove(input->next + 1, input->next, (size_t)(input->ends - input->next));
        input->ends++;
        return *(input->next) = c;
    }
    
    /* Low-level fopen() equivalent.
    */
    static int input_open(input_buffer *const input, const char *const filename)
    {
        if (!input)
            return errno = EINVAL;
    
        input->descriptor = -1;
        input->status = 0;
        input->next = input->buffer;
        input->ends = input->buffer;
    
        if (!filename || !*filename)
            return errno = input->status = EINVAL;
    
        do {
            input->descriptor = open(filename, O_RDONLY | O_NOCTTY);
        } while (input->descriptor == -1 && errno == EINTR);
        if (input->descriptor == -1)
            return input->status = errno;
    
        return 0;
    }
    
    /* Low-level fclose() equivalent.
    */
    static int input_close(input_buffer *const input)
    {
        int result;
    
        if (!input)
            return errno = EINVAL;
    
        /* EOF is not an error; we use ENODATA for that. */
        if (input->status == ENODATA)
            input->status = 0;
    
        if (input->descriptor != -1) {
            do {
                result = close(input->descriptor);
            } while (result == -1 && errno == EINTR);
            if (result == -1 && !input->status)
                input->status = errno;
        }
    
        input->descriptor = -1;
        input->next = input->buffer;
        input->ends = input->buffer;
    
        return errno = input->status;
    }
    
    /* Read /proc/self/maps, and fill in the arrays corresponding to the fields.
     * The function will return the number of mappings, even if not all are saved.
    */
    size_t read_maps(size_t const n,
                     void **const ptr, size_t *const len,
                     unsigned char *const mode)
    {
        input_buffer    input;
        size_t          i = 0;
        unsigned long   curr_start, curr_end;
        unsigned char   curr_mode;
        int             c;
    
        errno = 0;
    
        if (input_open(&input, "/proc/self/maps"))
            return (size_t)0; /* errno already set. */
    
        c = input_next(&input);
        while (c >= 0) {
    
            /* Skip leading controls and whitespace */
            while (c >= 0 && c <= 32)
                c = input_next(&input);
    
            /* EOF? */
            if (c < 0)
                break;
    
            curr_start = 0UL;
            curr_end = 0UL;
            curr_mode = 0U;
    
            /* Start of address range. */
            while (1)
                if (c >= '0' && c <= '9') {
                    curr_start = (16UL * curr_start) + c - '0';
                    c = input_next(&input);
                } else
                if (c >= 'A' && c <= 'F') {
                    curr_start = (16UL * curr_start) + c - 'A' + 10;
                    c = input_next(&input);
                } else
                if (c >= 'a' && c <= 'f') {
                    curr_start = (16UL * curr_start) + c - 'a' + 10;
                    c = input_next(&input);
                } else
                    break;
            if (c == '-')
                c = input_next(&input);
            else {
                errno = EIO;
                return (size_t)0;
            }
    
            /* End of address range. */
            while (1)
                if (c >= '0' && c <= '9') {
                    curr_end = (16UL * curr_end) + c - '0';
                    c = input_next(&input);
                } else
                if (c >= 'A' && c <= 'F') {
                    curr_end = (16UL * curr_end) + c - 'A' + 10;
                    c = input_next(&input);
                } else
                if (c >= 'a' && c <= 'f') {
                    curr_end = (16UL * curr_end) + c - 'a' + 10;
                    c = input_next(&input);
                } else
                    break;
            if (c == ' ')
                c = input_next(&input);
            else {
                errno = EIO;
                return (size_t)0;
            }
    
            /* Permissions. */
            while (1)
                if (c == 'r') {
                    curr_mode |= PERM_READ;
                    c = input_next(&input);
                } else
                if (c == 'w') {
                    curr_mode |= PERM_WRITE;
                    c = input_next(&input);
                } else
                if (c == 'x') {
                    curr_mode |= PERM_EXEC;
                    c = input_next(&input);
                } else
                if (c == 's') {
                    curr_mode |= PERM_SHARED;
                    c = input_next(&input);
                } else
                if (c == 'p') {
                    curr_mode |= PERM_PRIVATE;
                    c = input_next(&input);
                } else
                if (c == '-') {
                    c = input_next(&input);
                } else
                    break;
            if (c == ' ')
                c = input_next(&input);
            else {
                errno = EIO;
                return (size_t)0;
            }
    
            /* Skip the rest of the line. */
            while (c >= 0 && c != '\n')
                c = input_next(&input);
    
            /* Add to arrays, if possible. */
            if (i < n) {
                if (ptr) ptr[i] = (void *)curr_start;
                if (len) len[i] = (size_t)(curr_end - curr_start);
                if (mode) mode[i] = curr_mode;
            }
            i++;
        }
    
        if (input_close(&input))
            return (size_t)0; /* errno already set. */
    
        errno = 0;
        return i;
    }
    

    read_maps()函数最多读取n区域、数组的起始地址、void *数组ptr的长度len和数组的权限mode,返回映射的总数(可能大于),如果发生错误,则返回n零并设置。errno

    很可能将系统调用用于上面的低级 I/O,这样您就不会使用任何 C 库功能,但我认为根本没有必要。(据我所知,C 库在这些实际系统调用周围使用了非常简单的包装器。)

希望这个对你有帮助。

于 2013-02-19T03:30:01.480 回答
7

“这解释了我所看到的,但我有几个问题:”

“有没有办法检测某些东西是否已经映射到某个地址?无需访问 /proc/maps?”

是的,使用不带 MAP_FIXED 的 mmap。

“有没有办法在找到重叠页面的情况下强制mmap失败?”

显然不是,但如果 mmap 返回所请求地址以外的映射,则只需在 mmap 之后使用 munmap。

当在没有MAP_FIXED 的情况下使用时,Linux 和 Mac OS X 上的 mmap(我也怀疑在其他地方)服从地址参数,如果在 [地址,地址 + 长度)范围内不存在现有映射。因此,如果 mmap 回答与您提供的地址不同的地址的映射,您可以推断该范围内已经存在映射,您需要使用不同的范围。由于 mmap 通常会在忽略地址参数时回答位于非常高地址的映射,因此只需使用 munmap 取消映射该区域,然后在不同的地址重试。

使用 mincore 检查地址范围的使用不仅浪费时间(必须一次探测一个页面),它可能不起作用。较旧的 linux 内核只会使 mincore 因文件映射而失败。对于 MAP_ANON 映射,他们根本不会回答任何问题。但正如我所指出的,您所需要的只是 mmap 和 munmap。

我刚刚完成了这个为 Smalltalk VM 实现内存管理器的练习。我使用 sbrk(0) 找出可以映射第一个段的第一个地址,然后使用 mmap 和 1Mb 的增量为后续段搜索空间:

static long          pageSize = 0;
static unsigned long pageMask = 0;

#define roundDownToPage(v) ((v)&pageMask)
#define roundUpToPage(v) (((v)+pageSize-1)&pageMask)

void *
sqAllocateMemory(usqInt minHeapSize, usqInt desiredHeapSize)
{
    char *hint, *address, *alloc;
    unsigned long alignment, allocBytes;

    if (pageSize) {
        fprintf(stderr, "sqAllocateMemory: already called\n");
        exit(1);
    }
    pageSize = getpagesize();
    pageMask = ~(pageSize - 1);

    hint = sbrk(0); /* the first unmapped address above existing data */

    alignment = max(pageSize,1024*1024);
    address = (char *)(((usqInt)hint + alignment - 1) & ~(alignment - 1));

    alloc = sqAllocateMemorySegmentOfSizeAboveAllocatedSizeInto
                (roundUpToPage(desiredHeapSize), address, &allocBytes);
    if (!alloc) {
        fprintf(stderr, "sqAllocateMemory: initial alloc failed!\n");
        exit(errno);
    }
    return (usqInt)alloc;
}

/* Allocate a region of memory of at least size bytes, at or above minAddress.
 *  If the attempt fails, answer null.  If the attempt succeeds, answer the
 * start of the region and assign its size through allocatedSizePointer.
 */
void *
sqAllocateMemorySegmentOfSizeAboveAllocatedSizeInto(sqInt size, void *minAddress, sqInt *allocatedSizePointer)
{
    char *address, *alloc;
    long bytes, delta;

    address = (char *)roundUpToPage((unsigned long)minAddress);
    bytes = roundUpToPage(size);
    delta = max(pageSize,1024*1024);

    while ((unsigned long)(address + bytes) > (unsigned long)address) {
        alloc = mmap(address, bytes, PROT_READ | PROT_WRITE,
                     MAP_ANON | MAP_PRIVATE, -1, 0);
        if (alloc == MAP_FAILED) {
            perror("sqAllocateMemorySegmentOfSizeAboveAllocatedSizeInto mmap");
            return 0;
        }
        /* is the mapping both at or above address and not too far above address? */
        if (alloc >= address && alloc <= address + delta) {
            *allocatedSizePointer = bytes;
            return alloc;
        }
        /* mmap answered a mapping well away from where Spur prefers.  Discard
         * the mapping and try again delta higher.
         */
        if (munmap(alloc, bytes) != 0)
            perror("sqAllocateMemorySegment... munmap");
        address += delta;
    }
    return 0;
}

这似乎运作良好,在跳过任何现有映射的同时在升序地址分配内存。

高温高压

于 2014-06-24T17:56:19.857 回答
4

看来这posix_mem_offset()就是我一直在寻找的。

它不仅告诉您一个地址是否被映射,而且如果它恰好被映射,它会隐含地为您提供它所属的映射区域的边界(通过在len参数中提供 SIZE_MAX)。

因此,在执行之前 MAP_FIXED,我可以使用posix_mem_offset()来验证我正在使用的地址是否尚未映射。

我也可以使用msync()or mincore() (检查ENOMEM错误会告诉您地址已被映射),但那样我会更加盲目(没有关于地址映射区域的信息)。此外,msync()具有可能对性能产生影响的副作用,并且mincore()仅适用于 BSD(不是 POSIX)。

于 2013-02-19T11:48:11.407 回答