amarao (amarao_san) wrote,
amarao
amarao_san

The Kernel (part 7)

Краткое содержание предыдущей части.
Обнаружилось, что вызов memblock_reserve первый раз показывает с самого начала ненулевой объём резервирования, приводя к тому, что сумма переданных в memblock_reserve величин отличается от размера результирующего резервирования.

Вот как выглядит первое резервирование:

[ 0.000000] memblock_reserve: [0x00000001a50000-0x00000001a5d000] setup_arch+0x5e4/0xb80, memblock.reserved.total_size=33673216

(при этом debug печатается до выполнения фактического резервирования).

В связи с этим, перед тем, как мы начнём отлаживать этот баг, у меня есть ещё один вопрос: а сколько вызовов memblock_reserve было на момент вывода первой строчки. Может быть, часть резервирования происходила до момента, когда возможен отладочный вывод?

Модифицируем функцию:

int __init_memblock memblock_reserve(phys_addr_t base, phys_addr_t size)
{
    struct memblock_type *_rgn = &memblock.reserved;
    static int call_count=0;
    call_count++;
    memblock_dbg("memblock_reserve: [%#016llx-%#016llx] %pF, memblock.reserved.total_size=%lu, call=%i\n",
             (unsigned long long)base,
             (unsigned long long)base + size,
             (void *)_RET_IP_,
             (unsigned long) memblock.reserved.total_size,
             call_count);

    return memblock_add_region(_rgn, base, size, MAX_NUMNODES);
}


Увы, первая же строчка подтверждает подозрения:

[ 0.000000] memblock_reserve: [0x00000001a50000-0x00000001a5d000] setup_arch+0x5e4/0xb80, memblock.reserved.total_size=33673216, call=5
[ 0.000000] memblock_reserve: [0x0000000009a000-0x000000000a0000] setup_real_mode+0x65/0x186, memblock.reserved.total_size=33726464, call=6
[ 0.000000] memblock_reserve: [0x00000001deb000-0x00000001e4f000]
....
[ 0.000000] memblock_reserve: [0x0000000fee88c0-0x0000000ff088c0] __alloc_memory_core_early+0x56/0x70, memblock.reserved.total_size=38974946, call=43



Где-то тайно происходят 4 вызова... Впрочем, мы можем точно говорить, что цифры всё равно не сходятся.

И начнём мы с простого - с фиксации первых вызовов.

Поскольку мы знаем их количество, то мы можем заняться грязными хаками.

Вот как будет выглядеть one-and-unique функция для отладки (мне даже не стыдно):

int __init_memblock memblock_reserve(phys_addr_t base, phys_addr_t size)
{
    struct memblock_type *_rgn = &memblock.reserved;
    static int call_count=0;
    int i;
    static unsigned long long debug_5_base[5] ={ 0, 0, 0, 0, 0};
    static unsigned long long debug_5_size[5] = { 0, 0, 0, 0, 0};
    static void* debug_5_ip[5] = { 0, 0, 0, 0, 0};
    static unsigned long debug_5_total [5] = {0,0,0,0,0};
    if (call_count<5){
        debug_5_base[call_count]=base;
        debug_5_size[call_count]=size;
        debug_5_ip[call_count]=(void*) _RET_IP_;
        debug_5_total[call_count]=memblock.reserved.total_size;
    }
    call_count++;

    if (call_count==5){
        for (i=0;i<5;i++){
            memblock_dbg("memblock_reserve_history5: base=%#016llx,size=%ul, caller=%pF,memblock.reserved.total_size=%lu, iter=%i\n",
                debug_5_base[i],
                debug_5_size[i],
                debug_5_ip[i],
                debug_5_total[i],
                i);
        }
    }
    memblock_dbg("memblock_reserve: [%#016llx-%#016llx] %pF, memblock.reserved.total_size=%lu, call=%i\n",
             (unsigned long long)base,
             (unsigned long long)base + size,
             (void *)_RET_IP_,
             (unsigned long) memblock.reserved.total_size,
             call_count);

    return memblock_add_region(_rgn, base, size, MAX_NUMNODES);
}


Итак, момент истины... (приятная новость - я написал цикл с кучей static переменных и не огрёб зависания или чего похуже. С первой попытки!).

[ 0.000000] memblock_reserve_history5: base=0x00000003421000,size=114688l, caller=xen_setup_kernel_pagetable+0x27b/0x2a8,memblock.reserved.total_size=0, iter=0
[ 0.000000] memblock_reserve_history5: base=0x00000001000000,size=10813440l, caller=x86_64_start_reservations+0x72/0xad,memblock.reserved.total_size=114688, iter=1
[ 0.000000] memblock_reserve_history5: base=0x00000001e6d000,size=22208512l, caller=x86_64_start_reservations+0xa2/0xad,memblock.reserved.total_size=10928128, iter=2
[ 0.000000] memblock_reserve_history5: base=0x0000000339b000,size=536576l, caller=xen_memory_setup+0x5a6/0x5d4,memblock.reserved.total_size=33136640, iter=3
[ 0.000000] memblock_reserve_history5: base=0x00000001a50000,size=53248l, caller=setup_arch+0x5e4/0xb80,memblock.reserved.total_size=33673216, iter=4
[ 0.000000] memblock_reserve: [0x00000001a50000-0x00000001a5d000] setup_arch+0x5e4/0xb80, memblock.reserved.total_size=33673216, call=5

Итак, ситуация становится лучше. Во-первых memblock не виноват. Во-вторых мы нашли скрытые итерации и можем на них вволю пофапать (вывод немного хитрый - memblock.reserved.total_size показывает нам значение ДО вызова).

Итак, в человеческом выводе:

xen_setup_kernel_pagetable+0x27b/0x2a8 попросил нас о 114688 байтах (114к)
x86_64_start_reservations+0x72/0xad - 10813440 (10Мб, не жирно ли?)
x86_64_start_reservations+0xa2/0xad - 22208512 (ещё 22 Мб)
xen_memory_setup+0x5a6/0x5d4 - 536576 (500к)
setup_arch+0x5e4/0xb80 - 53248 (50к)

Таким образом (вместе с остальными резервациями вот топ тех, с кем надо плотно поговорить):

x86_64_start_reservations+0x72/0xad - 10813440
x86_64_start_reservations+0xa2/0xad - 22208512
_alloc_memory_core_early+0x56/0x70, (в цикле съедает ещё под полтора десятка).

Начнём с x86_64_start_reservations (http://lxr.free-electrons.com/source/arch/x86/kernel/head64.c#L96)

Сама функция тривиальна и не интересна. Интереснее те, кто её вызывают. А их три штуки:

Два: arch/x86/kernel/head64.c, один arch/x86/xen/enlighten.c.

Что, кстати, интересно, потому что memblock_reserve вызывают из x86_64_start_reservations дважды, а не трижды. Впрочем, смотрим код.

Один вызов из __init x86_64_start_kernel. И у меня есть гипотеза, что этот кусочек кода просто резервирует память самого ядра. (хотя это большой-большой вопрос, что это за "real_mode_data" для PV-виртуалки без real mode в принципе).

Второй раз - это оказалась, сама функция. Подводит сайтик...

Ок, куда интереснее второй случай:

xen/enlighten.c: x86_64_start_reservations((char *)__pa_symbol(&boot_params));

Отвратительная функция на три страницы asmlinkage void __init xen_start_kernel(void) с кучей дефайнов.

Итак, у нас почему-то происходит двойное резервирование (это показано в логе memblock_reserve) из x86_64_start_reservations.

Почему оно двойное? Моя гипотеза - кто-то делает сначала x86_64_start_kernel, а потом xen_start_kernel (или наоборот).

Нам надо определить кто сколько чего резервирует и, по мере возможности, понять "что это".

Начнём с head64.c

x86_64_start_reservations(real_mode_data);

Что такое real_mode_data?

... И мы тут же утыкаемся в место вызова функции x86_64_start_kernel. Это arch/x86/kernel/head_64.S.

Да-да, то самое страшное 'S', которое означает, что мы перестаём читать плебейский Си и переходим к конкретному ассемблеру.

... И у меня появляется стойкое подозрение, что этот код не выполняется. По крайней мере судя по комментариям. Никто не даст PV-гостю переключаться из 32 в 64 и что-то там хитрить с cr'ом...

Если мы посмотрим в xen-head.S, то увидим вот такой кусочек кода:

__INIT
ENTRY(startup_xen)
cld
#ifdef CONFIG_X86_32
mov %esi,xen_start_info
mov $init_thread_union+THREAD_SIZE,%esp
#else
mov %rsi,xen_start_info
mov $init_thread_union+THREAD_SIZE,%rsp
#endif
jmp xen_start_kernel

__FINIT

Можете мне что угодно рассказывать, но я вижу xen_start_kernel. И, теоретически, это должен быть первый кусок кода, который выполняется. Потому что %rsi, %esi - их заполняет гипервизор перед запуском. Точнее domain_builder (но вот эта тонкая грань - я её пока ещё не читал со стороны зена/domain_builder'а).

Итак, xen_start_kernel

И нам тут же бонусная подсказка:

/* First C function to be called on Xen boot */
asmlinkage void __init xen_start_kernel(void)

Кстати, на будущее:

хen_domain_type = XEN_PV_DOMAIN;

Там же мы видим крайне любопытную функцию:

x86_init.resources.memory_setup = xen_memory_setup;

Собственно, из этой же функции у нас и происходит первый (?) вызов
x86_64_start_reservations((char *)__pa_symbol(&boot_params));

И дальше выполняется код x86_64_start_reservations. Кстати, я лошок, и пропустил, что функция-то длинее, чем казалась.

void __init x86_64_start_reservations(char *real_mode_data)
{
    copy_bootdata(__va(real_mode_data));

    memblock_reserve(__pa_symbol(&_text),
             __pa_symbol(&__bss_stop) - __pa_symbol(&_text));

#ifdef CONFIG_BLK_DEV_INITRD
    /* Reserve INITRD */
    if (boot_params.hdr.type_of_loader && boot_params.hdr.ramdisk_image) {
        /* Assume only end is not page aligned */
        unsigned long ramdisk_image = boot_params.hdr.ramdisk_image;
        unsigned long ramdisk_size  = boot_params.hdr.ramdisk_size;
        unsigned long ramdisk_end   = PAGE_ALIGN(ramdisk_image + ramdisk_size);
        memblock_reserve(ramdisk_image, ramdisk_end - ramdisk_image);
    }
#endif

    reserve_ebda_region();

    /*
     * At this point everything still needed from the boot loader
     * or BIOS or kernel text should be early reserved or marked not
     * RAM in e820. All other memory is free game.
     */

    start_kernel();
}

И memblock_reserve вызывается дважды (!). Причём, как мы знаем, первый раз на 10Мб. 10Мб на boot params? Они с дуба рухнули.

Мне кажется, я начинаю понимать происходящее. Ок, сфокусируемся на происхождении boot_params в xen_start_kernel. Я не уверен, но кажется, это оно: http://lxr.free-electrons.com/source/arch/x86/include/asm/bootparam.h#L97

Может быть, я плохо умею читать, но... 0xeec=3820 байт это не 10Мб никаким образом.

Впрочем, я погорячился, подумав, что резервируется boot_params. На самом деле вызов выглядит как
memblock_reserve(__pa_symbol(&_text),
__pa_symbol(&__bss_stop) - __pa_symbol(&_text));

То есть мы резервируем всю память от text до bss_stop. (__pa_symbol - это всего лишь приведение типов и всякая возня вокруг компилятора).

То есть вопрос в том, что такое __bss_stop. Вообще, BSS - это статические переменные, а text - это код программы (читай, код ядра). Другими словами, мы резервируем всю память от начала _text до конца __bss_stop (само объявление __bss_stop и _text мы ща найдём). То есть в резерв однозначно попадает всё ядро "как оно есть".

Второй момент резервирования - это memblock_reserve(ramdisk_image, ramdisk_end - ramdisk_image);

... И я хочу сказать, с учётом, что всю эту трахомудию я отлаживаю с initrd в 30Мб размером ситуация становится очень даже ясной. В этой части.

Мы просто резервируем память ядра и initrd. На самом деле немного странно (почему память ядра не попадает в memTotal), но вот вывод для моего ноутбука:

Memory: 3995204k/4691968k available (6311k kernel code, 79472k reserved, 3039k data, 784k init, 3163784k highmem)

3Мб reserve для 4Гб...

Теперь посмотрим на загрузку -xen ядра, а именно, 3.1-xen.


[ 0.000000] Memory: 222600k/262144k available (5186k kernel code, 0k absent, 39544k reserved, 4119k data, 420k init)

А вот ванильный 3.2

Memory: 808020k/8388608k available (3409k kernel code, 448k absent, 7580140k reserved, 3324k data, 572k init)

Очевидно, что большой reserve в моём случае объясняется большим ramdisk (хм, там полный комплект софта для выживания - от ssh-сервера до testdisk'а).



Первая загадка стала понятна: initrd и память ядра теперь помечаются как reserved и не попадают в totalram_pages (то есть не показываются в free).

Вторая загадка, которую, я думаю, мы оставим на вкусное, выглядит вот так:

Зависимость free от static-max:
static  target  mem_kb   TotalMem
2048M	256M	262144	187316	 
2560M	256M	262144	177052	 
2680M	256M	262144	174504	 
2750M	256M	262144	172056
2790M	256M	262144	171976
2800M	256M	262144	171956
2810M	256M	262144	171936
2812M	256M	262144	171932
2813M	256M	262144	171928
2816M	256M	262144	171924	 
2820M	256M	262144	171924	 
3072M	256M	262144	171924	 
4096M	256M	262144	171924	 


Вопрос: что меняется с памятью при работе balloon'а?

Промежуточный вердикт по поводу pv-ops ядер: видимо, мы запустим их, но с пометкой, что, мол, по поводу различий в аккаунтинге памяти - к торвальдсу. Точнее, я из вот этого всего сделаю компиляцию, с объяснением, куда эта память девается.

Впрочем, для этого сначала надо разобраться. Впереди романтическая возня с __alloc_memory_core_early, который тихой сапой догоняет с 33 до 38.
Tags: linux kernel, memory on demand
Subscribe

  • План действий

    AAA при логине ведёт себя по разному в зависимости от того A это или AAAA.

  • Админский гольф

    Вам выдали шелл на сервер, на котором кто-то удалил все симлинки (т.е. файлы типа "симлинк"). Ваша задача починить сервер. Починенным сервер…

  • продолжая leetcode

    Первый раз я ощутил Силу. Задача - roman numerals, с обещанием, что на входе нет мусора. pub fn roman_to_int(s: String) -> i32 { let mut acc =…

  • Post a new comment

    Error

    default userpic

    Your IP address will be recorded 

    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
  • 3 comments