Персистентная ОС: ніщо не блокується

Це — стаття-питання. У мене немає ідеального відповіді на те, що тут буде описано. Якийсь є, але наскільки він вдалий — неочевидно.

Стаття стосується однієї з концептуальних проблем ОС Фантом, ну або будь-якої іншої системи, в якій є персистентная і «волатильна» складові.

Для розуміння суті проблеми варто прочитати одну з попередніх статей — про персистентную оперативну пам'ять.

Коротка постановка проблеми: В силу того, що прикладна програма в ОС Фантом персистентна (не запускається при перезавантаженні), а ядро — ні (перезавантаження перезавантаження і може бути змінено між запусками), в такій системі не можна робити блокуючі системні виклики. Звичайним способом.

Дійсно: якщо прикладна програма зробить виклик в ядро і в такому стані ми зробимо снапшот, то абсолютно незрозуміло, як такий снапшот відновлювати. Ядро снапшотом не фотографується, записується і відновлюється тільки пам'ять прикладних програм. Незрозуміло, де вона там в ядрі була. Незрозуміло, правильне чи, взагалі, місце в ядрі вказує фактична точка входу. Незрозуміло, які об'єкти прикладного рівня ядро зворушувало і створило для себе.

Окремо незрозуміло, наскільки в такому стані можна робити снапшот — не чіпає ядро об'єкти як раз коли ми їх записуємо на диск.

Для початку опишемо інтерфейси для доступу за даними між ядром і об'єктної середовищем.

Об'єктна середовище має інтерфейс в ядро у вигляді вбудованих класів — аналог native в Java. Ці класи реалізовані в ядрі у вигляді функцій Сі, які відповідають методам. Блокуватися такі функції не можуть — зобов'язані повернутися ASAP, і поки вони виконуються — снапшоти неможливі. Цього достатньо для простих методів типу window.paintLine() або string.concat(), але не більше того.

Банальний приклад (исходник):

static int si_string_8_substring(struct pvm_object me, struct data_area_4_thread *tc )
{
DEBUG_INFO;
ASSERT_STRING(me);
struct data_area_4_string *meda = pvm_object_da( me, string );

int n_param = POP_ISTACK;
CHECK_PARAM_COUNT(n_param, 2);

int parmlen = POP_INT();
int index = POP_INT();

if( index < 0 || index >= meda->length )
SYSCALL_THROW_STRING( "string.substring index is out of bounds" );

int len = meda->length - index;
if( parmlen < len ) len = parmlen;

if( len < 0 )
SYSCALL_THROW_STRING( "string.substring length is negative" );

SYSCALL_RETURN(pvm_create_string_object_binary( (char *)meda->data + index, len ));
}


Якщо вміст звичайного об'єкта — тільки посилання, internal class object містить довільну структуру даних, недоступну віртуальній машині через звичайні інструкції, але доступну всередині методів, написаних на сі — struct data_area_4_string, в даному прикладі.

Очевидно, що такі методи можуть працювати зі структурами даних ядра, якщо здобудуть доступ до них. Але зворотне невірно — посилання на себе вони залишити в ядрі не можуть. Вірніше, може, але з деякими «але».

Їх два.

По-перше, потрібно, щоб збирач сміття знав, що деякий об'єкт доступний з ядра і не «зібрав» його, навіть якщо посилання з об'єктного світу скінчилися.

По-друге, потрібно, щоб ядро не обходило дані такого об'єкта, поки йде ключова операція формування снапшота. Що небанально і, здається, може бути реалізовано тільки глобальної зупинкою всіх ниток крім нитки формування снапшота. Що, слава богу, потрібно всього на кілька мсек.

Що стосується збирача сміття, то це реалізується ось яким способом. У кореневому об'єкті об'єктної середовища присутній, серед інших об'єктів, так званий restart list — список об'єктів. Будь-який об'єкт internal класу може в нього додатися:

void pvm_add_object_to_restart_list( pvm_object_t o );
void pvm_remove_object_from_restart_list( pvm_object_t o );


Знаходження в такому списку (а будь-який об'єкт, з яким працює ядро, повинен в ньому бути) обслуговує два завдання. По-перше, гарантує наявність посилання на об'єкт «від імені ядра» — це посилання завадить GC вбити об'єкт, навіть якщо про нього всі інші об'єкти забудуть.

По-друге, вирішується ось яка проблема. Припустимо, ми зробили об'єкт «пристрій», попрацювали з ним, він потрапив у снапшот, після чого систему перезавантажили через reset. При рестарті ядра воно має якось дізнатися про проблему і відновити зв'язок такого об'єкта з ядром, або повідомити йому, що все — оживити його не виходить. (Наприклад, якщо відповідний пристрій вийняли з комп'ютера.)

Для цього ядро після рестарту, але до перезапуску об'єктної середовища, відкладає рестарт лист в бік, створює новий порожній, і обходить всі об'єкти в старому рестарт-листі. Для кожного викликається функція restart, яка повинна відновити зв'язок об'єкта з ядром, або повідомити об'єкту, що він помер. По закінченні роботи функції об'єкт викидається зі старого списку рестарту. Якщо функція restart знову підключила об'єкт до ядра, вона ж поставить його в новий список рестарту. Якщо немає — посилання на об'єкт «від імені ядра» зникне. Якщо вона була останньою — об'єкт буде видалений, оскільки не потрібен нікому.

(Див. root.h

Добре, але мало. Все ж, напевно, ми хочемо якимось чином з об'єктного середовища зробити read() і дочекатися результату. Без блокування прямого виклику з нитки віртуальної машини всередині інструкції.

Я розглядав три варіанти реалізації.

  1. Проміжна зупинка: блокуючий системний виклик складається з пари: ініціюючого і зчитує викликів. Між ними, на кордоні інструкції, віртуальна машина блокується. Якщо трапляється снапшот і рестарт — машина рестартует з другої інструкції і отримує явну відмову.
  2. Коллбек: по закінченні виконання довгої операції об'єктна середовище отримує зворотний дзвінок з ядра.
  3. Псевдо-закінчення операції: Блокуючий виклик працює саме як блокуючий виклик — йде в ядро і там чекає як завгодно довго. Але перед цим виклик робить вигляд, що завершився — кладе на стек нульову посилання, як якщо б у реалізації було написано return null;, а по закінченні роботи знімає цей null і замінює фактичним результатом.


Зараз реалізований останній спосіб. Інші два я вважав вкрай незручними у використанні.

Він теж не ідеальний — краще б, звичайно, рестартовать запит при рестарті ядра, а не видавати відмову. В принципі, це теж нереально.

Поясню більш детально, чому все це важливо.

Віртуальна машина (інтерпретатор) працює в циклі, виконуючи інструкцію за інструкцією. При рестарті ядра віртуальні машини рестартуют — ядро пробігає всі об'єкти типу .internal.thread і запускає їх. У тому стані, в якому вони були на момент запуску ядра.

Що це за стан? Це — стан, в якому вони були на момент формування снапшота. Очевидно, цей момент повинен бути таким, щоб, умовно, longjmp з цієї точки в точку входу функції інтерпретатора не завдав системі смертельних ран.

Куди прилітає управління після рестарту

Відповідно, якщо ми блокуємо нитка інтерпретатора (або JIT-коду, неважливо), потрібно, щоб стан був цілісним — обов'язково на кордоні інструкції, і все, що важливо для об'єктної середовища — лежить в персистентной пам'яті.

(Повний код: Функция, яка реалізує блокуючий виклик

Що для цього робиться.

Для початку, зробимо вигляд, що інструкція віртуальної машини здійснилася. Тобто — прочитала зі стека параметри і поклала на стек «помилковий» код повернення, null. Якщо ми потрапимо в снапшот і потім будемо вбиті, саме це буде підсумком роботи інструкції в збереженому стані віртуальної машини.

int n_param = POP_ISTACK;
CHECK_PARAM_COUNT(n_param, 2); // Кидає exception, якщо не 2 параметра

int nmethod = POP_INT();
pvm_object_t arg = POP_ARG;

// push to zero obj stack
pvm_ostack_push( tc->_ostack, pvm_create_null_object() ); 


Далі ми майже вільні робити що хочемо. Майже.

Справа в тому, що для формування снапшота код підсистеми віртуальної пам'яті зупиняє всі нитки віртуальних машин, і перевіряє, що вони зупинилися. Якщо він як раз зараз це робить або буде робити пізніше — скажемо йому, що ми ніби зупинилися — ми ж дійсно перестали виконувати код віртуальної машини. Так — і перед всім цим скажімо інтерпретатору, щоб всі свої закешовані змінні поклав назад в об'єкти, в яких вони повинні лежати (save_fast_acc). Заодно перевіримо, чи немає, взагалі, запиту на останов віртуальних машин (шатдаун) — якщо є, то виконаємо його.

pvm_exec_save_fast_acc(tc); // Before snap

if(phantom_virtual_machine_stop_request)
hal_exit_kernel_thread();

hal_mutex_lock( &interlock_mutex );

phantom_virtual_machine_threads_stopped++;
phantom_virtual_machine_threads_blocked++;
hal_cond_broadcast( &phantom_snap_wait_4_vm_enter );

hal_mutex_unlock( &interlock_mutex );


Виконаємо сам запит. По закінченні звільнимо змінну (посилання на аргумент).

// now do syscall - can block
pvm_object_t ret = syscall_worker( this, tc, nmethod, arg );
ref_dec_o( arg );


Повідомимо підсистемі снапшотов, що ми закінчили свої справи і хочемо знову піти у інтерпретатор. Якщо вона заперечує (йде снапшот) — поспим, поки вона нас не розбудить.

hal_mutex_lock( &interlock_mutex );

if(phantom_virtual_machine_snap_request)
hal_cond_wait( &phantom_vm_wait_4_snap, &interlock_mutex );

phantom_virtual_machine_threads_stopped--;
phantom_virtual_machine_threads_blocked--;

hal_cond_broadcast( &phantom_snap_wait_4_vm_leave );

hal_mutex_unlock( &interlock_mutex );


Все зроблено, знімемо з стека віртуальної машини фейковое значення, що повертається, запишемо реальне.

// electro from zero obj stack
pvm_ostack_pop( tc->_ostack );
// push ret val to obj stack
pvm_ostack_push( tc->_ostack, ret );


В цілому, ця реалізація працює. Але в ній є тонка помилка. Система генерації снапшотов перевіряє не тільки всі нитки поснули перед формуванням снапшота, але і що всі вони прокинулися. Неважко бачити, що якщо якась нитка заблокується навічно, то вона ж навічно зупинить і снапшоти (тому що не «прокинеться»).

Спроба вирішити цю проблему в лоб (порахувати кількість заблокованих ниток і врахувати при підрахунку зупинених/запущених) не привела до успіху: цілісність об'єктного стейта порушується.

Можливо, у рішенні є підводні камені, які я поки не бачу.

На цьому я поки закриваю дозволені промови і йду пити чай. :)

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.