はじめに
malloc関連のパラメータはmallopt(3)
で変更することができます。
例えば、下記を実行することで、malloc内部でmmapを使うしきい値を1MBに変更することができます。
mallopt(M_MMAP_THRESHOLD, 1024*1024);
malloptで設定可能なパラメータのうちいくつかは、環境変数でも変更できます。
例えば、先述のM_MMAP_THRESHOLD
は、MALLOC_MMAP_THRESHOLD_
環境変数でも設定できます。
$ export MALLOC_MMAP_THRESHOLD_=1048576
$ ./a.out
環境変数であればプログラムの変更なしに適用できるので、システムのデフォルトを変更するときなどに有用です。
この環境変数によるmalloptの変更は、glibcのTunablesという仕組みを使っています。
本記事ではglibcのtunablesが設定される流れを
MALLOC_MMAP_THRESHOLD_
環境変数を例に備忘メモとして残しておきます。
誤りなどありましたら、ご指摘いただければ幸いです。
環境は以下のとおりです。
$ arch
x86_64
$ uname -r
5.4.0-42-generic
$ lsb_release -d
Description: Ubuntu 18.04.4 LTS
$ dpkg -l | grep libc-bin
ii libc-bin 2.27-3ubuntu1.2 amd64 GNU C Library: Binaries
MALLOC_MMAP_THRESHOLD_
環境変数が反映される流れ
1. ローダ(ld-linux.so)でauxiliary vectorを確認する
auxiliary vectorとは、getauxval(3)
で取得することができる、
kernelのELF loaderから渡される実行ファイルの補助情報です。
ローダで、auxiliary vectorからAT_SECURE
情報を取得し、
AT_SECURE
が真の場合、global変数の__libc_enable_secure
が立ちます。
ここでAT_SECURE
は、少し勘違いしやすい名前ですが、
今のプロセスがセキュアに実行される必要があるということを意味します。
より具体的に言えば、下記のいずれかの場合にAT_SECURE
が立ちます。
- set-user-ID もしくは set-group-ID が有効なプログラム
- capabilitiesを付与されたプログラム
ElfW(Addr)
_dl_sysdep_start (void **start_argptr,
void (*dl_main) (const ElfW(Phdr) *phdr, ElfW(Word) phnum,
ElfW(Addr) *user_entry, ElfW(auxv_t) *auxv))
{
...
for (av = GLRO(dl_auxv); av->a_type != AT_NULL; set_seen (av++))
switch (av->a_type)
{
...
case AT_SECURE:
__libc_enable_secure = av->a_un.a_val;
break;
...
}
__tunables_init (_environ);
...
(*dl_main) (phdr, phnum, &user_entry, GLRO(dl_auxv));
return user_entry;
}
2. ローダで、環境変数の値からtunable_list[]
を更新
__tunables_init
では、tunable_list[]
の中にある項目が環境変数に含まれている場合、
tunable_initialize()
で環境変数の値が反映されます。
ただし、__libc_enable_secure
が立っている場合(=AT_SECURE
が立っている場合)は、
以下の3種類のsecurity_level
に応じた処理が行われます。
-
SXID_ERASE
:AT_SECURE
の場合、子プロセスが読まないように環境変数から削除する -
SXID_IGNORE
:AT_SECURE
の場合、無視する(環境変数には残る) -
NONE
: 常に反映する
void
__tunables_init (char **envp)
{
...
while ((envp = get_next_env (envp, &envname, &len, &envval,
&prev_envp)) != NULL)
{
...
for (int i = 0; i < sizeof (tunable_list) / sizeof (tunable_t); i++)
{
tunable_t *cur = &tunable_list[i];
...
if (__libc_enable_secure)
{
if (cur->security_level == TUNABLE_SECLEVEL_SXID_ERASE)
{
/* Erase the environment variable. */
...
}
if (cur->security_level != TUNABLE_SECLEVEL_NONE)
continue;
}
tunable_initialize (cur, envval);
break;
}
}
}
}
tunableの各項目のsecurity_level
は、環境変数名や型と併せて、.list
ファイルで定義されています。
glibc {
malloc {
...
mmap_threshold {
type: SIZE_T
env_alias: MALLOC_MMAP_THRESHOLD_
security_level: SXID_IGNORE
}
...
}
}
scripts/gen-tunables.awk
によってdl-tunables.list
からdl-tunable-list.h
を自動生成し、
そこにtunable_list[]
が定義されます。
static tunable_t tunable_list[] attribute_relro = {
...
{TUNABLE_NAME_S(glibc, malloc, mmap_threshold), {TUNABLE_TYPE_SIZE_T, 0, SIZE_MAX}, {}, NULL, TUNABLE_SECLEVEL_SXID_IGNORE, "MALLOC_MMAP_THRESHOLD_"},
...
};
3. 初回malloc時に、tunable_list[]
からmp_
に反映
ここからは、tunableの話ではなく、mallocパラメータ固有の話になります。
また、実行タイミングもローダではなく、普通のmain関数に入ってから初めてmallocが呼び出されたときです。
プログラムで初めてメモリ確保関数を呼び出したときには、malloc_hook_ini
が呼び出され、
そこからptmalloc_init
を呼び出します。
static void *
malloc_hook_ini (size_t sz, const void *caller)
{
__malloc_hook = NULL;
ptmalloc_init ();
return __libc_malloc (sz);
}
ptmalloc_init
では、2.で設定したtunable_listから、mmap_threshold
を取得し、
mallocパラメータをまとめて保持する構造体mp_
に設定します。
static void
ptmalloc_init (void)
{
...
TUNABLE_GET (mmap_threshold, size_t, TUNABLE_CALLBACK (set_mmap_threshold));
...
}
void
__tunable_get_val (tunable_id_t id, void *valp, tunable_callback_t callback)
{
tunable_t *cur = &tunable_list[id];
switch (cur->type.type_code)
{
...
case TUNABLE_TYPE_SIZE_T:
{
*((size_t *) valp) = (size_t) cur->val.numval;
break;
}
...
if (cur->initialized && callback != NULL)
callback (&cur->val);
}
実際に設定するのはdo_set_mmap_threshold
関数です。
ここを見ると実は設定値の上限があったり、
mmap_threshold
を設定するときには動的なしきい値制御を無効にしていることがわかります。
static inline int
__always_inline
do_set_mmap_threshold (size_t value)
{
/* Forbid setting the threshold too high. */
if (value <= HEAP_MAX_SIZE / 2)
{
LIBC_PROBE (memory_mallopt_mmap_threshold, 3, value, mp_.mmap_threshold,
mp_.no_dyn_threshold);
mp_.mmap_threshold = value;
mp_.no_dyn_threshold = 1;
return 1;
}
return 0;
}
これで晴れて、mmap
するかどうかの判断に、MALLOC_MMAP_THRESHOLD_
環境変数の値が使われるようになります。
めでたしめでたし。
static void *
sysmalloc (INTERNAL_SIZE_T nb, mstate av)
{
...
if (av == NULL
|| ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
&& (mp_.n_mmaps < mp_.n_mmaps_max)))
{
...
mm = (char *) (MMAP (0, size, PROT_READ | PROT_WRITE, 0));
}
}
おわりに
mallocパラメータを環境変数で設定する場合、デバッガなりでちゃんと設定が反映されているか確認したほうが良いですね。
参考
mallocの動作を追いかける(環境変数編) - Qiita
mallopt(3) - Linux manual page
The GNU C Library
getauxval(3) - Linux manual page