PintOS #1 : 진행 흐름도 파악하기
정글의 커리큘럼으로 진행으로 진행 중인 PintOS의 테스트 데이터가 어떻게 입력되고 작동되는지에 대해 논해보겠다. 우리는 64bit의 KAIST PintOS에 대해 다루고 있다. 이 논함은 정규 커리큘럼은 아니고 개인 호기심에 의한다.
2025.05.10 - [구현하기] - PintOS #0 : 서론
PintOS #0 : 서론
넋이 다 나가는 와중에 최악의 영역에 진입했다. 농담으로 퇴소언급을 했지만 이정도로 막막한 정도가 차원이 다르다.예전에는 뭘 하면서 임하다보면 끝이 없을 것 같다는 분량이었지만 이젠
hyeonistic.tistory.com
계속 보고 있으니 이 내용은 테스트 케이스 분야를 비롯한 다양한 부분에 영향을 받는듯하다. 다시 말해 앞으로 이어질 커리큘럼도 일관된 내용으로 테스트 케이스가 작동하리라는 보장은 없다는 것.
pintos-kaist/threads/init.c
/* Pintos main program. */
int main (void) {
uint64_t mem_end;
char **argv;
/* Clear BSS and get machine's RAM size. */
bss_init ();
/* Break command line into arguments and parse options. */
argv = read_command_line ();
argv = parse_options (argv);
/* Initialize ourselves as a thread so we can use locks,
then enable console locking. */
thread_init ();
console_init ();
/* Initialize memory system. */
mem_end = palloc_init ();
malloc_init ();
paging_init (mem_end);
/* Initialize interrupt handlers. */
intr_init ();
timer_init ();
kbd_init ();
input_init ();
thread_start ();
serial_init_queue ();
timer_calibrate ();
printf ("Boot complete.\n");
/* Run actions specified on kernel command line. */
run_actions (argv);
/* Finish up. */
if (power_off_when_done)
power_off ();
thread_exit ();
}
크게 세 가지로 나눌 수 있다 :
_init()을 비롯한 초기화 함수
각기 앞에 필요한 주제에 따른 초기화를 진행한다. 예를 들어 intr_init(); 은 인터럽트에 관한 초기화이다. 이런식으로 초기화하는 것은 쓰레드, 콘솔, palloc, malloc, 페이징, 인터럽트, 타이머, kbd, 기본 입력이 수행된다.
이어 thread_start(), serial_init_queue(), timer_calibrate() 를 연달아 호출하며 쓰레드에 대한 스케줄러와 인터럽트를 허용한다.
power_off
그냥 뭐 정직하게 컴퓨터를 끈다. 이 함수에서는 QEMU에 대한 Power off command를 보내고 종료하게 된다 :
void power_off (void) {
print_stats (); // PintOS 실행에 대한 정보 출력 후 종료.
printf ("Powering off...\n");
outw (0x604, 0x2000); /* QEMU 종료 커맨드*/
for (;;);
}
run_actions
static void run_actions (char **argv) {
/* An action. */
struct action {
char *name; /* Action name. */
int argc; /* # of args, including action name. */
void (*function) (char **argv); /* Function to execute action. */
};
/* Table of supported actions. */
static const struct action actions[] = {
{"run", 2, run_task},
{NULL, 0, NULL},
};
while (*argv != NULL) {
const struct action *a;
int i;
/* Find action name. */
for (a = actions; ; a++)
if (a->name == NULL)
PANIC ("unknown action `%s' (use -h for help)", *argv);
else if (!strcmp (*argv, a->name))
break;
/* Check for required arguments. */
for (i = 1; i < a->argc; i++)
if (argv[i] == NULL)
PANIC ("action `%s' requires %d argument(s)", *argv, a->argc - 1);
/* Invoke action and advance. */
a->function (argv);
argv += a->argc;
}
}
진행도가 있을 만한 것만 보자. 사실 이 내용은 실제 pintos 실행 시 입력 받는 매개변수를 구분하고 그에 따른 커맨드를 구분짓는다.
그래서 매개변수 내용으로 실행할 테스트 케이스를 입력하게 되고 그에 따라 테스트 케이스가 작동한다.
우리가 봐야 할 내용은 run_task 이다.
static void run_task (char **argv) {
const char *task = argv[1];
printf ("Executing '%s':\n", task);
if (thread_tests){
run_test (task);
} else {
process_wait (process_create_initd (task));
}
run_test (task);
printf ("Execution of '%s' complete.\n", task);
}
그냥 입력받는 내용에 대해 바로 run_test 를 진행하고 있다. 이제보니 run_test(tasks); 구문이 반복되는데, 이에 대한 이유 분석을 생각해 봐야 할 것 같다.. 이제 tests.c에 넘어와서 이어서 내용을 진행한다.
pintos-kaist/testgs/threads/tests.c
struct test
{
const char *name;
test_func *function;
};
static const struct test tests[] =
{
{"alarm-single", test_alarm_single},
{"alarm-multiple", test_alarm_multiple},
{"alarm-simultaneous", test_alarm_simultaneous},
{"alarm-priority", test_alarm_priority},
{"alarm-zero", test_alarm_zero},
{"alarm-negative", test_alarm_negative},
{"priority-change", test_priority_change},
{"priority-donate-one", test_priority_donate_one},
{"priority-donate-multiple", test_priority_donate_multiple},
{"priority-donate-multiple2", test_priority_donate_multiple2},
{"priority-donate-nest", test_priority_donate_nest},
{"priority-donate-sema", test_priority_donate_sema},
{"priority-donate-lower", test_priority_donate_lower},
{"priority-donate-chain", test_priority_donate_chain},
{"priority-fifo", test_priority_fifo},
{"priority-preempt", test_priority_preempt},
{"priority-sema", test_priority_sema},
{"priority-condvar", test_priority_condvar},
{"mlfqs-load-1", test_mlfqs_load_1},
{"mlfqs-load-60", test_mlfqs_load_60},
{"mlfqs-load-avg", test_mlfqs_load_avg},
{"mlfqs-recent-1", test_mlfqs_recent_1},
{"mlfqs-fair-2", test_mlfqs_fair_2},
{"mlfqs-fair-20", test_mlfqs_fair_20},
{"mlfqs-nice-2", test_mlfqs_nice_2},
{"mlfqs-nice-10", test_mlfqs_nice_10},
{"mlfqs-block", test_mlfqs_block},
};
static const char *test_name;
이러한 테스트 이름 매핑을 우선 사용한다.
void run_test (const char *name)
{
const struct test *t;
for (t = tests; t < tests + sizeof tests / sizeof *tests; t++)
if (!strcmp (name, t->name))
{
test_name = name;
msg ("begin");
t->function ();
msg ("end");
return;
}
PANIC ("no test named \"%s\"", name);
}
t->function(); 에서 실행하고자 하는 function()이 제대로 시작된다. 을 예로 들어보자.
pintos --gdb -- -q run alarm-single
매핑된 함수는 유의미한 함수가 존재하는 파일로 연결된다. 그리고 실질적인 내용이 진행된다.
우린 위에 있는 tests 에서 alarm-single 이 test_alarm_single 로 연결 되는 것을 보았다.
pintos-kaist/tests/threads/alarm-wait.c
void test_alarm_single (void)
{
test_sleep (5, 1);
}
이제 실제 코드가 이루어진다. 자세한 내용은 진짜 PintOS 코드에서 확인해보길 기대한다.