지난 Project1 thread를 활용한 과제부터 System call이라는 Project2를 진행하면서 코드들이 점차 많아지면서 전반적인 코드의 흐름을 짚고 가야 할 필요를 느껴서 이번에는 전체적인 코드의 흐름을 다시 잡고 가는 과정을 추가했습니다.
코드의 흐름
init.c → int main(void) → run_actions(argv) → run_task(char **argv) → process_create_initd(task) → thread_create (file_name, PRI_DEFAULT, initd, fn_copy) → initd→process_exec → load, do_iret -> syscall_handler()
전반적인 코드의 흐름은 위와 같고 위 순서대로 분석을 해봅시다.
1. init.c
qemu 라는 가상화 환경을 제공하는 에뮬레이터가 실행되면서 Pintos는 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);
#ifdef USERPROG
tss_init ();
gdt_init ();
#endif
/* Initialize interrupt handlers. */
intr_init ();
timer_init ();
kbd_init ();
input_init ();
#ifdef USERPROG
exception_init ();
syscall_init ();
#endif
/* Start thread scheduler and enable interrupts. */
thread_start ();
serial_init_queue ();
timer_calibrate ();
#ifdef FILESYS
/* Initialize file system. */
disk_init ();
filesys_init (format_filesys);
#endif
#ifdef VM
vm_init ();
#endif
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.c의 main()함수입니다. read_command_line() 함수로 우리가 입력한 명령어를 읽어들이고 init.c 코드인만큼 사용하는 자원들을 초기화해주는 함수들이 실행됩니다. read_command_line() 함수에는 ptov() 라는 물리 주소를 가상주소로 바꿔주는 함수가 있습니다. 이 함수는 아래와 같이 물리주소에 KERN_BASE만큼 더해서 가상주소로 변경해줍니다. 이와 반대로 vtop() 함수도 존재하는데 이 함수는 변경된 가상주소에서 KERN_BASE를 빼주면 물리주소가 됩니다.
/* Returns kernel virtual address at which physical address PADDR
* is mapped. */
#define ptov(paddr) ((void *) (((uint64_t) paddr) + KERN_BASE))
/* Returns physical address at which kernel virtual address VADDR
* is mapped. */
#define vtop(vaddr) \
({ \
ASSERT(is_kernel_vaddr(vaddr)); \
((uint64_t) (vaddr) - (uint64_t) KERN_BASE);\
})
2. run_actions(argv) → run_task(char **argv)
run_actions() 함수가 실행되면서 run task()함수가 실행됩니다.
/* Runs the task specified in ARGV[1]. */
static void
run_task (char **argv) {
const char *task = argv[1];
printf ("Executing '%s':\n", task);
#ifdef USERPROG
if (thread_tests){
run_test (task);
} else {
process_wait (process_create_initd (task));
}
#else
run_test (task);
#endif
printf ("Execution of '%s' complete.\n", task);
}
이 run_task함수에서 task는 argv[1]을 입력받게되고, 여기서 argv[1]에는 'args-single onearg'가 들어갑니다. 그리고 process_wait() 함수를 통해서 무한 대기 루프에 스레드가 진입하게되는데 앞서 구현한 타이머 인터럽트를 통해 타이머 인터럽트가 발생되면 운영 체제는 인터럽트 핸들러를 실행하고, 인터럽트 핸들러 내에서 현재 스레드와 다른 스레드 사이의 컨텍스트 스위치가 이루어집니다. 이로 인해 우리가 실행하고자하는 사용자 프로세스에 대응하는 커널스레드로 변경됩니다.
3. process_create_initd(task)
tid_t process_create_initd (const char *file_name) {
char *fn_copy;
tid_t tid;
/* Make a copy of FILE_NAME.
* Otherwise there's a race between the caller and load(). */
fn_copy = palloc_get_page (0);
if (fn_copy == NULL)
return TID_ERROR;
strlcpy (fn_copy, file_name, PGSIZE);
char *parsing;
/*file_name을 받아와서 null 기준으로 문자열 파싱*/
strtok_r(file_name," ", &parsing);
/* Create a new thread to execute FILE_NAME. */
tid = thread_create (file_name, PRI_DEFAULT, initd, fn_copy);
if (tid == TID_ERROR)
palloc_free_page (fn_copy);
return tid;
}
여기서 전달받은 'args-single onearg'에 대한 프로세스를 생성합니다. 그리고 thread_create()에서 initd()를 실행하고 해당 파일 이름으로 스레드를 생성합니다.
4. initd (void *f_name)
/* A thread function that launches first user process. */
static void
initd (void *f_name) {
#ifdef VM
supplemental_page_table_init (&thread_current ()->spt);
#endif
process_init ();
if (process_exec (f_name) < 0)
PANIC("Fail to launch initd\n");
NOT_REACHED ();
}
initd() 함수는 첫 프로세스를 생성할 때만 사용합니다. 그 이유는 다음부터는 fork()를 통해 프로세스를 생성하면 되기 때문입니다. initd()함수에서 첫 프로세스를 생성하고 이어서 process_init() -> process_exec()함수를 실행합니다.
5. process_exec
/* Switch the current execution context to the f_name.
* Returns -1 on fail. */
int process_exec (void *f_name) {
char *file_name = f_name;
bool success;
/* We cannot use the intr_frame in the thread structure.
* This is because when current thread rescheduled,
* it stores the execution information to the member. */
struct intr_frame _if;
_if.ds = _if.es = _if.ss = SEL_UDSEG;
_if.cs = SEL_UCSEG;
_if.eflags = FLAG_IF | FLAG_MBS;
/* We first kill the current context */
process_cleanup ();
/*parsing한 인자를 담을 argu배열의 길이는 pintos제한 128바이트*/
char *argu[128];
char *token, *parsing;
int cnt = 0;
/* strtok_r 함수를 통해서 첫 번째로 얻은 값을 token에 저장, 나머지를 parsing에 저장 */
/* token이 NULL일때까지 반복 */
/* strtok_r 함수에 NULL값을 받아온다면 이전 호출 이후의 남은 문자열에서 토큰을 찾음, 따라서 token에는 다음 문자 저장*/
for(token=strtok_r(file_name," ",&parsing);token!=NULL; token=strtok_r(NULL," ",&parsing))
{
argu[cnt++]=token;
}
/* And then load the binary */
success = load (file_name, &_if);
argument_stack(argu,cnt,&_if.rsp);
/*if 구조체의 필드값 갱신*/
_if.R.rdi=cnt;
_if.R.rsi=(char*)_if.rsp+8;
/*_if.rsp를 시작 주소로하여 메모리 덤프를 생성. 메모리 덤프의 크기는 16진수로*/
hex_dump(_if.rsp, _if.rsp, USER_STACK - (uint64_t)_if.rsp, true);
/* If load failed, quit. */
palloc_free_page (file_name);
if (!success)
return -1;
/* Start switched process. */
do_iret (&_if);
NOT_REACHED ();
}
위 함수는 유저가 입력한 명령어를 수행하도록 프로그램을 메모리에 적재하고 실행하는 함수입니다. 이전 Argument parsing을 통해 받은 명령어를 argument_stack함수를 통해 유저 스택에 정보를 올리는 작업(load)을 수행합니다. 스택에 정보를 다 올리고나면 hex_dump()를 통해 우리가 입력한 값이 parsing되어서 16진수로 나타나는 것을 확인할 수 있습니다.
그리고 나서 do_iret()함수가 실행되면 CPU는 인터럽트 프레임에 저장된 상태를 복원하고, 현재 실행 중인 스레드의 컨텍스트를 전환합니다. 이로써 새로운 스레드의 코드가 실행되며, 새로운 스레드의 컨텍스트가 활성화됩니다.
6. do_iret(struct intr_frame *tf)
/* Use iretq to launch the thread */
void do_iret (struct intr_frame *tf) {
__asm __volatile(
"movq %0, %%rsp\n"
"movq 0(%%rsp),%%r15\n"
"movq 8(%%rsp),%%r14\n"
"movq 16(%%rsp),%%r13\n"
"movq 24(%%rsp),%%r12\n"
"movq 32(%%rsp),%%r11\n"
"movq 40(%%rsp),%%r10\n"
"movq 48(%%rsp),%%r9\n"
"movq 56(%%rsp),%%r8\n"
"movq 64(%%rsp),%%rsi\n"
"movq 72(%%rsp),%%rdi\n"
"movq 80(%%rsp),%%rbp\n"
"movq 88(%%rsp),%%rdx\n"
"movq 96(%%rsp),%%rcx\n"
"movq 104(%%rsp),%%rbx\n"
"movq 112(%%rsp),%%rax\n"
"addq $120,%%rsp\n"
"movw 8(%%rsp),%%ds\n"
"movw (%%rsp),%%es\n"
"addq $32, %%rsp\n"
"iretq"
: : "g" ((uint64_t) tf) : "memory");
}
do_iret() 함수를 실행하면 위와 같은 어셈블리어 명령을 수행합니다. 처음 부분의 'movq %0, %%rsp' 부분은 tf(인터럽트 프레임)변수의 값을 %rsp에 입력해줍니다. 이러면 스택 포인터가 새로운 스레드의 스택으로 설정됩니다. 그 이후 movq 명령을 통해 스택에 저장되어있는 레지스터 값들을 올바른 오프셋에 해당 레지스터를 복사합니다. 그리고 addq를 통해 스택 포인터를 조정하여 스택에서 복원한 컨텍스트 값들을 제거합니다.
따라서 do_iret()함수는 인터럽트 프레임의 값을 로드하고, 스택에서 레지스터 값을 복원하여 컨텍스트 스위치를 수행하는 역할을 합니다. 이를 통해 새로운 스레드의 코드가 실행되고, 프로세스의 실행이 스위칭됩니다.
7. msg() -> vmsg() -> write()
@/lib/user/syscall.c
int write (int fd, const void *buffer, unsigned size) {
return syscall3 (SYS_WRITE, fd, buffer, size);
}
#define syscall3(NUMBER, ARG0, ARG1, ARG2) ( \
syscall(((uint64_t) NUMBER), \
((uint64_t) ARG0), \
((uint64_t) ARG1), \
((uint64_t) ARG2), 0, 0, 0))
do_iret() 함수가 실행된이후 msg() -> vmsg() -> write()를 따라가다보면 우리가 구현하고있는 시스템 콜인 wirte()함수가 나오게됩니다. 이 코드는 시스템 콜을 요청하는 유저 코드이므로 userprog/syscall.c에 구현한 write()함수와는 다른 것입니다.
write()함수의 인자를 보니 syscall3, 즉 인자가 3개 필요한 함수입니다. 따라서 인자 세개를 넣고 나머지 값은 0으로 레지스터에 넣습니다.
그다음에는 syscall()함수로 진입하고나서 최종적으로 syscall_handler() 함수가 실행됩니다.
8. syscall_handler(struct intr_frame *f UNUSED)
/* The main system call interface */
void syscall_handler (struct intr_frame *f UNUSED) {
/* rax = 시스템 콜 넘버 */
int sys_number = f->R.rax;
// TODO: Your implementation goes here.
switch (sys_number)
{
case SYS_HALT:
halt();
break;
case SYS_EXIT:
exit(f->R.rdi);
break;
case SYS_FORK:
fork(f->R.rdi);
break;
case SYS_WAIT:
wait(f->R.rdi);
break;
case SYS_CREATE:
create(f->R.rdi, f->R.rsi);
break;
case SYS_REMOVE:
remove(f->R.rdi);
break;
case SYS_OPEN:
open(f->R.rdi);
break;
case SYS_FILESIZE:
filesize(f->R.rdi);
break;
case SYS_READ:
read(f->R.rdi,f->R.rsi, f->R.rdx);
break;
case SYS_WRITE:
write(f->R.rdi,f->R.rsi, f->R.rdx);
break;
case SYS_SEEK:
seek(f->R.rdi, f->R.rsi);
break;
case SYS_TELL:
tell(f->R.rdi);
break;
case SYS_CLOSE:
close(f->R.rdi);
break;
default:
thread_exit();
}
printf ("system call!\n");
}
위의 전반적인 과정이 끝나면 syscall_handler() 함수가 실행되고 그 안에 요청한 시스템 콜넘버를 보고 해당하는 함수들이실행됩니다.
그리고나서 "system call!"이 출력되는 과정이 나오게 됩니다.
출력값