Планировщик задач HaikuOS. Часть 2. Переделываем.
Содержание:
- Место, где зарыта собака.
- Наполеоновские планы.
- Гадание на кофейной гуще.
----NewOS
----Haiku
----BeOS
----Zeta
- Остапа понесло.
- Расхлёбываем.
- SOS Emulator к вашим услугам.
Место, где зарыта собака.
Вот как планировщик Haiku выбирает очередной поток, которому отдать квант (сужу по завалявшемуся у меня куску исходника планировщика NewOS дремучего 2003 года).
Есть отсортированная по приоритетам очередь потоков жаждущих процессорного времени. В одном конце очереди 1 или более IDLE-потоков с приоритетами 0 (таких потоков в системе всегда есть как минимум столько же, сколько доступных процессоров, и хотя бы один из этих потоков гарантированно сидит в очереди в тот момент когда планировщик по ней шарит). В другом конце - поток с самым высоким приоритетом.
Итак двигаемся по упорядоченной очереди потоков. От бОльших приоритетов к меньшим, до тех пор пока выполняется некий критерий. Как только критерий выполняться перестал останавливаемся, последний рассмотренный поток и будет тем счастливчиком, которому достанется квант процессорного времени.
Собственно критерий:
// always extract real time threads
if (nextThread->priority >= B_FIRST_REAL_TIME_PRIORITY)
break;
// never skip last non-idle normal thread
if(nextThread->queue_next && (nextThread->queue_next->priority == B_IDLE_PRIORITY))
break;
// skip normal threads sometimes
if (_rand() > 0x3000) break;
где:
static int _rand(void)
{
static int next = 0;
if (next == 0) next = system_time();
next = next * 1103515245 + 12345;
return((next >> 16) & 0x7FFF);
}
Гусары молчать! :) Оставаясь серьёзными разберём код.
Потокам реального времени всегда у нас дорога. Это понятно. Остановить пробег если далее только IDLE-потоки - тоже понятно. А последнее "правило-лотеря" конечно на первый взгляд вызывает недоумение, но не мешает системе делить процессорное время поровну между потоками одинакового приоритета. Всё дело в структуре очереди.
Когда поток вытаскивают из очереди и он отрабатывает свой квант, то возвращают его после этого в очередь в место между потоками более низких приоритетов чем он сам и такими же как он и старше. Т.е. среди потоков такого же приоритета он в следующей итерации решедуленга будет иметь шанс удостоиться кванта меньше всего.
Очередь как бы состоит из под-очередей потоков одинакового приоритета и эти под-очереди самоперемешиваются. Вот код вставки в очередь:
void scheduler_enqueue_in_run_queue(struct thread *thread)
{
struct thread *curr, *prev;
for(curr = gRunQueue.head, prev = NULL;
curr && (curr->priority >= thread->priority);
curr = curr->queue_next)
{
if (prev)
prev = prev->queue_next;
else
prev = gRunQueue.head;
}
thread->queue_next = curr;
if(prev)
prev->queue_next = thread;
else
gRunQueue.head = thread;
}
Нигде не видно ничего что как-то бы опиралось на абсолютное значение номера приоритета. Не важно какие там разрывы по приоритетам между потоками в очереди, идут они с промежутком в 10 или в 1, результат выборки будет одинаковым.
Почему я так уверенно гребу NewOS и Haiku в одну кучу? Да потому что запуская SOS Tester'а на Haiku имею на выходе картинку замечательно согласующуюся с данным кодом из NewOS, т.е. разработчики Haiku ничего радикально там не меняли, хотя кое-что всё-таки изменили чтобы свести ситуацию к ещё более некрасивой.
Наполеоновские планы.
В голову стали приходить всякие идеи улучшения планировщика, например:
1) Давайте не тупо выбирать по всей очереди, а так чтобы в "кольцах" (так я обозвал под-очереди, в данном случае аналогия верная) рассматривать только по одному кандидату, если он не подошёл то прыгаем сразу на следующее кольцо.
2) На границах колец сравним приоритеты потоков и вероятность того состоится ли прыжок в следующее кольцо будет зависеть от ширины пропасти между ними.
3) Потвикаем условия ширины пропасти так чтобы прыжки в определённых диапазонах были равновероятностными. Не знаю правда насколько это будет хорошо, но можно побаловаться...
И много других идей. Правда для их проверки опять потребовался какой-то инструмент. У меня снова зачесались руки и вышла из под них примитивнейшая программка SOS Emulator. О её назначении думаю все уже догадались по названию :)
Программа до безобразия простая и немного по-своему делает всё тоже самое что SOS Tester.
Вместо пачки рабочих потоков фактически порождается лишь один поток нового класса - поток эмулятора планировщика.
В этом потоке в бесконечном цикле последовательно:
- запускается функция scheduler_reschedule(), которая выбирает из очереди фиктивных рабочих потоков (особого вида структур данных) один и помечает его текущим
- увеличивается на 1 счётчик текущего фиктивного потока (поток как бы отрабатывает свой квант процессорного времени)
А главный поток всё также проснувшись однажды по таймеру прекращает работу эмулятора планировщика и распечатывает статистику.
Исполняемый потоком эмулятора планировщика код - местами закомментированный и чуточку модифицированный для удобства внесения дальнейших изменений код планировщика задач NewOS.
Гадание на кофейной гуще.
Если руки чешутся - это надолго :) Написав программку довольно много с ней игрался опробуя различные модификации планировщика. Отбросив неудачные предлагаю к рассмотрению несколько наиболее интересных. От простого к сложному. Весь код не привожу, только места модификаций:
1) NewOS без модификаций, используем его как базу:
// без изменений
void scheduler_enqueue_in_run_queue(struct thread *thread)
{
struct thread *curr, *prev;
for(curr = gRunQueue.head, prev = NULL;
curr && (curr->priority >= thread->priority);
curr = curr->queue_next)
{
if (prev)
prev = prev->queue_next;
else
prev = gRunQueue.head;
}
thread->queue_next = curr;
if(prev)
prev->queue_next = thread;
else
gRunQueue.head = thread;
}
// этой функции в оригинале нет, я её сделал разбив более крупную конструкцию
// на несколько функций, остальной код одинаков для всех случаев
struct thread* select_next_thread_to_run()
{
struct thread *nextThread;
nextThread = gRunQueue.head;
while (nextThread && (nextThread->priority > B_IDLE_PRIORITY))
{
// always extract real time threads
if (nextThread->priority >= B_FIRST_REAL_TIME_PRIORITY)
break;
// never skip last non-idle normal thread
if(nextThread->queue_next
&& (nextThread->queue_next->priority == B_IDLE_PRIORITY))
break;
// skip normal threads sometimes
if (_rand() > 0x3000) break;
nextThread = nextThread->queue_next;
}
return nextThread;
}
// без изменений
static int _rand(void)
{
static int next = 0;
if (next == 0) next = system_time();
next = next * 1103515245 + 12345;
return((next >> 16) & 0x7FFF);
}
Settings: work time 1000000 microseconds, 20 working threads.
Using NewOS scheduler add-on.
Number Priority Value Share (%)
--------------------------------------------------
0 30 0 0.000000
1 33 0 0.000000
2 37 0 0.000000
3 40 0 0.000000
4 44 2 0.000047
5 48 5 0.000117
6 51 6 0.000140
7 55 19 0.000444
8 59 65 0.001518
9 62 137 0.003200
10 66 361 0.008432
11 69 999 0.023335
12 73 2850 0.066570
13 77 7466 0.174390
14 80 19795 0.462370
15 84 52923 1.236170
16 88 141490 3.304909
17 91 375453 8.769793
18 95 1003684 23.443949
19 99 2675952 62.504616
Settings: work time 1000000 microseconds, 20 working threads.
Using NewOS scheduler add-on.
Number Priority Value Share (%)
--------------------------------------------------
0 80 0 0.000000
1 81 0 0.000000
2 82 0 0.000000
3 83 0 0.000000
4 84 1 0.000023
5 85 5 0.000117
6 86 8 0.000187
7 87 21 0.000490
8 88 51 0.001190
9 89 133 0.003102
10 90 432 0.010076
11 91 1027 0.023953
12 92 2812 0.065586
13 93 7503 0.174996
14 94 19735 0.460290
15 95 52956 1.235121
16 96 141705 3.305061
17 97 376948 8.791759
18 98 1006840 23.483067
19 99 2677338 62.444983
2) А-ля Haiku:
// skip normal threads sometimes
if (_rand() > 0x1999) break;
Вообще-то я не знаю точной константы которая в Haiku а эту подобрал прогоняя SOS Tester и SOS Emulator подставляя разные значения и ища закономерность. Но то что вы тут видете очень сильно похоже на реальное положение дел на моей машине в последних альфа-сборках Haiku.
Settings: work time 1000000 microseconds, 20 working threads.
Using Haiku scheduler add-on.
Number Priority Value Share (%)
--------------------------------------------------
0 30 0 0.000000
1 33 0 0.000000
2 37 0 0.000000
3 40 0 0.000000
4 44 0 0.000000
5 48 0 0.000000
6 51 0 0.000000
7 55 0 0.000000
8 59 0 0.000000
9 62 0 0.000000
10 66 1 0.000020
11 69 12 0.000240
12 73 53 0.001062
13 77 270 0.005409
14 80 1215 0.024342
15 84 6442 0.129065
16 88 31624 0.633582
17 91 159905 3.203674
18 95 799013 16.008114
19 99 3992765 79.994490
Лично мне картинка для NewOS нравилась больше.
3) По канонам Be Newsletter и Be Book, т.е.
- каждый следующий приоритет в 2 раза больше шансов на исполнение:
struct thread* select_next_thread_to_run()
{
struct thread *nextThread, *nextLower;
uint32 n;
nextThread = gRunQueue.head;
// always extract real time threads
if (nextThread->priority >= B_FIRST_REAL_TIME_PRIORITY) {
return nextThread;
}
if (nextThread->priority == B_IDLE_PRIORITY) {
return nextThread;
}
while (1) {
// find thread with next lower priority
nextLower = nextThread->queue_next;
while (nextLower->priority >= nextThread->priority)
nextLower = nextLower->queue_next;
// never skip last non-idle normal subqueue of threads
if (nextLower->priority == B_IDLE_PRIORITY)
break;
// skip subqueue of normal threads sometimes
n = (uint32)(nextThread->priority - nextLower->priority);
if(rand()%(int)(pow(2,n))) break;
nextThread = nextLower;
}
return nextThread;
}
Settings: work time 1000000 microseconds, 20 working threads.
Using BeOS scheduler add-on.
Number Priority Value Share (%)
--------------------------------------------------
0 30 0 0.000000
1 33 0 0.000000
2 37 0 0.000000
3 40 0 0.000000
4 44 0 0.000000
5 48 0 0.000000
6 51 0 0.000000
7 55 0 0.000000
8 59 0 0.000000
9 62 0 0.000000
10 66 0 0.000000
11 69 0 0.000000
12 73 0 0.000000
13 77 1 0.000036
14 80 8 0.000291
15 84 72 0.002619
16 88 1256 0.045680
17 91 9424 0.342744
18 95 160241 5.827842
19 99 2578575 93.780789
Settings: work time 1000000 microseconds, 20 working threads.
Using BeOS scheduler add-on.
Number Priority Value Share (%)
--------------------------------------------------
0 80 2 0.000111
1 81 3 0.000166
2 82 10 0.000553
3 83 10 0.000553
4 84 33 0.001826
5 85 52 0.002877
6 86 96 0.005312
7 87 234 0.012947
8 88 482 0.026669
9 89 903 0.049963
10 90 1768 0.097823
11 91 3503 0.193820
12 92 6970 0.385647
13 93 14157 0.783302
14 94 28208 1.560738
15 95 56373 3.119097
16 96 113241 6.265582
17 97 225863 12.496915
18 98 451578 24.985642
19 99 903864 50.010457
4) Или такая модификация (3) варианта если надо чтобы правило было без привязки к абсолютным значениям приоритетов:
// skip subqueue of normal threads sometimes
if(rand()%2) break;
Settings: work time 1000000 microseconds, 20 working threads.
Using BeOS2 scheduler add-on.
Number Priority Value Share (%)
--------------------------------------------------
0 30 4 0.000127
1 33 6 0.000191
2 37 17 0.000540
3 40 25 0.000794
4 44 59 0.001873
5 48 97 0.003080
6 51 177 0.005620
7 55 410 0.013018
8 59 841 0.026703
9 62 1585 0.050326
10 66 3069 0.097444
11 69 6194 0.196667
12 73 12280 0.389905
13 77 24661 0.783016
14 80 49173 1.561302
15 84 98059 3.113491
16 88 197068 6.257146
17 91 393759 12.502322
18 95 786623 24.976226
19 99 1575380 50.020210
5) А-ля Zeta 1.51, где
- каждые седующие ДВА приоритета имеют в 2 раза больше шансов на исполнение (т.е. фактически подочереди "склеиваются" парами) и жёсткая привязка к номерам приоритетов:
void scheduler_enqueue_in_run_queue(struct thread *thread)
{
struct thread *curr, *prev;
for(curr = gRunQueue.head, prev = NULL;
curr && (curr->priority >= thread->priority - thread->priority%2);
curr = curr->queue_next)
{
if (prev)
prev = prev->queue_next;
else
prev = gRunQueue.head;
}
thread->queue_next = curr;
if(prev)
prev->queue_next = thread;
else
gRunQueue.head = thread;
}
struct thread* select_next_thread_to_run()
{
struct thread *nextThread, *nextLower;
uint32 n;
nextThread = gRunQueue.head;
// always extract real time threads
if (nextThread->priority >= B_FIRST_REAL_TIME_PRIORITY) {
return nextThread;
}
// in case we don't have any non-idle threads to run
if (nextThread->priority == B_IDLE_PRIORITY) {
return nextThread;
}
while (1) {
// find thread with next lower priority
nextLower = nextThread->queue_next;
while (nextLower->priority >=
nextThread->priority - nextThread->priority%2)
nextLower = nextLower->queue_next;
// never skip last non-idle normal subqueue of threads
if (nextLower->priority == B_IDLE_PRIORITY)
break;
// skip subqueue of normal threads sometimes
n = (uint32)(nextThread->priority - nextLower->priority);
if(rand()%n) break;
nextThread = nextLower;
}
return nextThread;
}
Settings: work time 1000000 microseconds, 20 working threads.
Using Zeta scheduler add-on.
Number Priority Value Share (%)
--------------------------------------------------
0 30 0 0.000000
1 33 0 0.000000
2 37 0 0.000000
3 40 0 0.000000
4 44 0 0.000000
5 48 0 0.000000
6 51 0 0.000000
7 55 2 0.000060
8 59 2 0.000060
9 62 6 0.000179
10 66 18 0.000537
11 69 60 0.001792
12 73 273 0.008152
13 77 1091 0.032577
14 80 2853 0.085191
15 84 13076 0.390452
16 88 52442 1.565929
17 91 139728 4.172308
18 95 626695 18.713246
19 99 2512692 75.029517
Settings: work time 1000000 microseconds, 20 working threads.
Using Zeta scheduler add-on.
Number Priority Value Share (%)
--------------------------------------------------
0 80 4288 0.188195
1 81 4287 0.188151
2 82 2997 0.131535
3 83 2996 0.131491
4 84 6612 0.290193
5 85 6611 0.290149
6 86 12131 0.532415
7 87 12131 0.532415
8 88 22660 0.994520
9 89 22660 0.994520
10 90 42744 1.875983
11 91 42744 1.875983
12 92 80298 3.524184
13 93 80298 3.524184
14 94 150685 6.613386
15 95 150685 6.613386
16 96 283300 12.433700
17 97 283300 12.433700
18 98 533529 23.415954
19 99 533529 23.415954
6) И аналогично если нам хочется чтобы в (5) правило не было абсолютно привязано к номерам:
// skip subqueue of normal threads sometimes
if(rand()%2) break;
Settings: work time 1000000 microseconds, 20 working threads.
Using Zeta2 scheduler add-on.
Number Priority Value Share (%)
--------------------------------------------------
0 30 3 0.000100
1 33 6 0.000200
2 37 16 0.000532
3 40 22 0.000732
4 44 58 0.001929
5 48 87 0.002894
6 51 165 0.005488
7 55 394 0.013106
8 59 814 0.027076
9 62 1512 0.050293
10 66 2933 0.097560
11 69 5926 0.197116
12 73 11703 0.389275
13 77 23565 0.783840
14 80 46914 1.560494
15 84 93709 3.117030
16 88 188112 6.257145
17 91 375602 12.493601
18 95 751049 24.982046
19 99 1503765 50.019542
Правда у вариантов (5) и (6) в моём исполнении есть баг - несовсем корректно отрабатываются случаи когда потоков всего 2-3 и их приоритеты соседние. Хех, а ведь у настоящей неэмулированной Zeta похожая проблема.
И ещё косяк - когда всего 1 не IDLE-поток и он разделяемого времени, то ему вообще не выдаются кванты. Есть подобная проблема в самой Zeta или нет - не знаю, но очень даже может быть.
Исправление этих проблемок оставляю читателям в качестве домашнего задания ;)
Остапа понесло.
Ну а самым удачным и в меру продвинутым планировщиком на мой взгляд является примерно такой:
/* Skip gaps -- Begin */
#define REAL_TIME_SKIP_GAP 20
#define MIN_REAL_TIME_SKIP_GAP 0
#define MAX_REAL_TIME_SKIP_GAP 20
#define SKIP_GAP 99
#define MIN_SKIP_GAP 0
#define MAX_SKIP_GAP 99
#define IS_IN_RANGE(i,min,max) (i >= min && i <= max)
typedef struct scheduler_skip_gap_info {
uint32 real_time;
uint32 normal;
} scheduler_skip_gap_info;
static scheduler_skip_gap_info gSkipGaps = { REAL_TIME_SKIP_GAP, SKIP_GAP };
/* Skip gaps -- End */
scheduler_skip_gap_info get_skip_gaps()
{
return gSkipGaps;
}
status_t set_skip_gaps(scheduler_skip_gap_info skip_gaps)
{
if (IS_IN_RANGE(skip_gaps.real_time, MIN_REAL_TIME_SKIP_GAP, MAX_REAL_TIME_SKIP_GAP) &&
IS_IN_RANGE(skip_gaps.normal, MIN_SKIP_GAP, MAX_SKIP_GAP))
{
gSkipGaps = skip_gaps;
return B_OK;
}
return B_ERROR;
}
struct thread* select_next_thread_to_run()
{
struct thread *nextThread, *nextLower;
uint32 n, n_candidate;
nextThread = gRunQueue.head;
// in case we don't have any non-idle threads to run
if (nextThread->priority == B_IDLE_PRIORITY) {
return nextThread;
}
// select only from real-time threads if any
if (nextThread->priority >= B_FIRST_REAL_TIME_PRIORITY) {
while (1) {
// find thread with next lower priority
nextLower = nextThread->queue_next;
while (nextLower->priority >= nextThread->priority)
nextLower = nextLower->queue_next;
// never skip last and only real-time subqueue of threads
if (nextLower->priority < B_FIRST_REAL_TIME_PRIORITY
&& nextThread == gRunQueue.head)
break;
if (nextLower->priority < B_FIRST_REAL_TIME_PRIORITY) {
n_candidate = (uint32)(nextThread->priority - nextLower->priority);
if (n_candidate < n) n = n_candidate;
} else {
n = (uint32)(nextThread->priority - nextLower->priority);
}
// never skip too much at once
if (n > gSkipGaps.real_time) break;
// skip subqueue of real-time threads sometimes
if (rand()%((int)pow(2,n))) break;
// return to head if no more real-time subqueues of threads
if (nextLower->priority < B_FIRST_REAL_TIME_PRIORITY) {
nextThread = gRunQueue.head;
} else {
// othrewise continue
nextThread = nextLower;
}
}
return nextThread;
}
// select from normal threads
while (1) {
// find thread with next lower priority
nextLower = nextThread->queue_next;
while (nextLower->priority >= nextThread->priority)
nextLower = nextLower->queue_next;
// never skip last and only non-idle normal subqueue of threads
if (nextLower->priority == B_IDLE_PRIORITY
&& nextThread == gRunQueue.head)
break;
if (nextLower->priority == B_IDLE_PRIORITY) {
n_candidate = (uint32)(nextThread->priority - nextLower->priority);
if (n_candidate < n) n = n_candidate;
} else {
n = (uint32)(nextThread->priority - nextLower->priority);
}
// never skip too much at once
if (n > gSkipGaps.normal) break;
// skip subqueue of normal threads sometimes
if (rand()%((int)pow(2,n))) break;
// return to head if no more non-idle normal subqueues of threads
if (nextLower->priority == B_IDLE_PRIORITY) {
nextThread = gRunQueue.head;
} else {
// othrewise continue
nextThread = nextLower;
}
}
return nextThread;
}
И вот какие у меня на то основания:
- Имитирует оригинала (BeOS) соблюдением на мой взгляд весьма разумного правила со степенями двойки.
- Расширяет этот принцип на потоки реального времени. Они по-прежнему вытесняют обычные потоки, но при этом становятся более учтивыми по отношению друг к другу и делятся процессором.
- Имеет настраиваемый пользователем режим "форсажа" приоритетных задач.
Для тех кто хочет дополнительно обезопасить высокоприоритетные задачи, включить "форсаж" в ущерб всему прочему, есть т.н. "ограничители прыжка". По-умолчанию их значения таковы что они ни на что не влияют, но уменьшая их можно регулировать степень игнорирования системой менее приоритетных задач когда есть что-то более приоритетное также в очереди на выполнение.
Как я это себе вижу - файл настроек и/или особое приложение-настройщик с двумя бегунками, которые продвинутый пользователь может двигать регулируя степень "форсажа" в той или иной зоне, разделяемого и реального времени, или кнопочка-чекбоксик которая переключает между некими наперёд заданными установками. Желательно иметь возможность переключения "на лету", но и необходимость перезагрузки не так сильно страшна.
"Слишком сложно, BeOS - ОС с философией Best Defaults!", - скажете вы и будете правы. Но позвольте, есть же приложение настройки виртуальной памяти. Да оно только для продвинутых, но оно есть и даже иногда требует перезагрузки для того чтобы изменения вступили в силу!
А если возможность регулировки планировщика окажется очень полезной, то почему бы и нет? По-умолчанию будет стоять что-то оптимальное для большинства случаев и многие пользователи так никогда возможно и не узнают о сей фиче.
Как пример настройки по-умолчанию поведение аналогичное планировщику BeOS/Zeta:
REAL_TIME_SKIP_GAP = 0
SKIP_GAP = 99
Проиллюстрирую:
Zeta 1.51. Данные SOS Tester'а:
Settings: work time 10000000 microseconds, 20 working threads.
Real work time 10000019 microseconds, 20 working threads.
Inaccuracy if SMP and more than one CPU enabled: 0.000460%.
Number Priority Value Share (%)
--------------------------------------------------
0 90 0 0.000000
1 91 0 0.000000
2 92 0 0.000000
3 93 0 0.000000
4 94 0 0.000000
5 95 0 0.000000
6 96 0 0.000000
7 97 0 0.000000
8 98 0 0.000000
9 99 0 0.000000
10 100 0 0.000000
11 101 0 0.000000
12 102 0 0.000000
13 103 0 0.000000
14 104 0 0.000000
15 105 0 0.000000
16 106 0 0.000000
17 107 0 0.000000
18 108 0 0.000000
19 109 3398375425 100.000000
BeOSrt (так я обозвал свою задумку) с полным "форсажем" в зоне реального времени ведёт себя аналогично. А вот другой пример -
BeOSrt с отключенным "форсажем" в зоне реального времени. Данные SOS Emulator'а:
Settings: work time 1000000 microseconds, 20 working threads.
Using BeOSrt2 scheduler add-on.
Number Priority Value Share (%)
--------------------------------------------------
0 90 0 0.000000
1 91 0 0.000000
2 92 0 0.000000
3 93 0 0.000000
4 94 0 0.000000
5 95 0 0.000000
6 96 0 0.000000
7 97 0 0.000000
8 98 0 0.000000
9 99 0 0.000000
10 100 17655 0.097968
11 101 35227 0.195476
12 102 70369 0.390481
13 103 141102 0.782981
14 104 281507 1.562094
15 105 563610 3.127496
16 106 1128570 6.262482
17 107 2252514 12.499295
18 108 4507710 25.013472
19 109 9022865 50.068256
Расхлёбываем.
Ну что, уважаемые, ни у кого больше руки не чешутся? (а хочется верить что это заразно, хе-хе)
Скачивайте исходники Haiku, вносите изменения в планировщик задач, собирайте и сравнивайте на реальном железе какой из вышеприведённых или ваших собственных вариантов лучше.
Не гарантирую того что всё получится прямо так сходу, возможно что-то где-то ещё придётся поправить, но направление я вам показал. Остальное - дело техники. Дерзайте.
Много планировщиков для одной ОС конечно плохо. Принцип Best Defaults никто не отменял. Но хорошо когда этот самый "best" есть из чего выбрать. Для Haiku R1 (ярко выраженная BeOS-совместимость) я бы остановился на своём самом лучшем варианте, но вот после... Кто знает.
SOS Emulator к вашим услугам.
Вам наверное уже не терпится наложить руки на эту программу. Не смею препятствовать:
http://otoko.narod.ru/files/haiku/sos_emulator.zip
Планировщики задач выполнены в качестве add-on'ов. Чтобы получить ещё один собственный просто сделайте копию какого-нибудь из готовых и модифицируйте. Не забудьте поправить строковую константу add_on_name, лучше ей быть отличной от тех что уже есть.
Для удобства сборки и запуска всего этого хозяйства как обычно прилагаются скрипты.
Но как обычно начать работу можно и с команды: sos_emulator -h
Программа не менее сырая чем SOS Tester и есть куда рости. Исходники вам в руки!
Спасибо за внимание!
(c) Михаил Панасюк aka BeAR, 2009. E-mail: otoko yandex ru