malloc 将 errno 设置为 EAGAIN

malloc setting errno to EAGAIN

考虑以下程序:

#include <sys/mman.h>                                                           
#include <stdlib.h>                                                             
#include <errno.h>                                                              

int                                                                             
main()                                                                          
{                                                                              
  errno = 0;
  mlockall(MCL_FUTURE);                                           
  char *a = malloc(1);                                                      
  if (!a)                                                                       
    exit(errno);                                                                
  munlockall();                                                                 
  exit(0);                                                                      
}

当 运行 作为普通用户时,我得到:

~ ./a.out                                                             
~ echo $?                                                             
11

来自/usr/include/asm-generic/errno-base.h

#define EAGAIN    11  /* Try again */                                     

当 运行 它作为 root 或当传递 MCL_FUTURE | MCL_CURRENT 它成功运行。我假设权限不足或标志错误,但 EPERM 和 EINVAL 均未返回。

这两个函数的手册页和 mlockall 的 POSIX 规范中都没有指定该错误。在 mlockall 之后放置一个 printf 表明是 malloc 在设置 errno。

更奇怪的是,malloc 似乎没有设置 EAGAIN(或者我找错地方了):

/usr/src/glibc/glibc-2.19/malloc grep -r . -e EAGAIN

那怎么回事?

~ uname -r                                                                                                                                                                                                 18:15:04 
3.16-2-486
~ gcc --version                                                                                                                                                                                            18:15:05 
gcc (Debian 4.9.2-10) 4.9.2
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

~ ldd --version                                                                                                                                                                                            18:15:11 
ldd (Debian GLIBC 2.19-18+deb8u1) 2.19
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.
~                                                                                                                                                                                                          18:15:15
  1. mlockall is setting error number, reason Some or all of the specified address range could not be locked.
  2. Standard malloc() does not set errno to EAGAIN on failure.

手册页

mlockall

malloc

mlockall() 的 return 值是多少?

根据 the POSIX standard:

[EAGAIN]

Some or all of the memory identified by the operation could not be locked when the call was made.

根据 Linux man page:

   EAGAIN Some or all of the specified address range could not be
          locked.

您的 mlockall() 调用要求锁定所有未来的内存分配。但是,OS 设置了任何一个非特权进程可以锁定的最大内存量。您可以使用 getrlimit(RLIMIT_MEMLOCK,...) 查询此金额。在我的系统上它是 65536 字节。

现在,当我 运行 你的程序在我的系统上时,使用 strace(1) 查看进行了哪些系统调用,我得到以下信息:

mlockall(MCL_FUTURE)                    = 0
brk(0)                                  = 0x2318000
brk(0x2339000)                          = 0x2318000
mmap(NULL, 1048576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = -1 EAGAIN (Resource temporarily unavailable)
mmap(NULL, 134217728, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0) = -1 EAGAIN (Resource temporarily unavailable)
mmap(NULL, 67108864, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0) = -1 EAGAIN (Resource temporarily unavailable)
mmap(NULL, 134217728, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0) = -1 EAGAIN (Resource temporarily unavailable)
mmap(NULL, 67108864, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0) = -1 EAGAIN (Resource temporarily unavailable)
exit_group(11)                          = ?

所以malloc首先使用brk尝试分配135168字节(0x2339000-0x2318000)。这失败了,因为它超过了锁定限制,因此 brk 保持 "break point"(进程数据段的顶部)不变。 (请参阅 brk(2) 手册页中有关 brk() 的 C 库和内核版本之间的不同约定的注释。)

malloc 然后尝试使用 mmap 分配 1048576 字节。这也失败了(因为它超过了 65536 字节),在这里我们看到返回了 EAGAIN 错误代码。 mmap(2) 的手册页记录了 errno 设置为 EAGAIN if "The file has been locked, or too much memory has been locked",后者正是此处的情况。 malloc,像许多库函数一样,将传递它进行的系统调用留下的 errno 值,所以 EAGAIN 就是你在 malloc returns 时看到的.

(带有 PROT_NONE 的额外 mmap 调用似乎是为了保留一些地址 space 以供将来使用,并有助于确保将来的分配以适当的方式对齐。请参阅 glibc 源代码中的 malloc/arena.c 以了解血淋淋的细节。在这种情况下它们也失败了,但这不是那么相关。)

简而言之,问题是 malloc 试图向 OS 请求比您(用户)请求的内存量大得多的内存。这是为了提高效率,因为在大多数情况下,您将继续分配更多的小内存块,并且您不想为每个内存块进行系统调用。但是这个数量超过了锁定内存的限制,所以它失败了。 EAGAIN是本例中mmap系统调用设置的错误码。

也许 malloc 手册页应该提到这种可能的 errno 设置,但更常见的是高级库函数没有描述 errno 可能的所有可能方式由底层系统调用设置。 (例如,fprintf(3) 调用 write(2),如果磁盘已满,它可以将 errno 设置为 ENOSPC,但您不会在 fprintf(3) 手册页。)你应该知道。

如果你想使用mlockall(MCL_FUTURE),那么你可能无法计划使用malloc(3)分配内存。您必须从 sbrk(2)mmap(2) 手动获取它,当然,计划将其保持在适当的限制或优雅地失败。这是非常不方便和限制性的,所以如果你需要一些锁定的内存,而你不是 root,你可能只想在足够小的对象上使用 mlock(2)