User Program _ main부터

핀토스 2번째 프로젝트가 시작되었다

발제 중 코치님께서 Pinos의 메인 프로그램이 어디에 위치하는지 파악하고, run 파라미터를 처리하는 부분을 찾아보라 하셨다

  • run 명령어는 사용자 프로그램을 실행시키기 위해 사용된다.
  • run 명령어를 해석하는 부분에서 스레드를 생성하고 유저 프로그램을 로드하는 과정이 시작된다.
  • 이 과정에서 부모 스레드는 자식 스레드를 분기시키고, 해당 프로그램을 메모리에 로드하여 실행시킨다.
    • pintos의 메인 프로그램은 threads/init.c 의 main에서 시작됨

그래서 시작된 main부터의 여행~.,~

/* Pintos main program. */
int
main (void) {
	uint64_t mem_end;
	char **argv;

	/* 1. BSS 영역 초기화 (.bss에 있는 전역 변수들을 0으로 설정) */
	bss_init ();

	/* 2. 커맨드라인 문자열을 argv 배열로 파싱하고 옵션을 처리 */
	argv = read_command_line ();
	argv = parse_options (argv);

	/* 3. 스레드 시스템 초기화 (lock, ready list 등) */
	thread_init ();
	/* 4. 콘솔 초기화 (동기화된 printf 지원용) */
	console_init ();

	/* 5. 메모리 시스템 초기화 */
	mem_end = palloc_init ();	// 페이지 할당자 초기화 (유저/커널 페이지 풀)
	malloc_init ();	// 커널 heap 초기화
	paging_init (mem_end);	// 페이지 테이블 설정 (커널 초기 매핑 포함)

#ifdef USERPROG
	/* 6. 사용자 프로그램 실행을 위한 환경 설정 */
	tss_init ();	// Task State Segment 초기화 (시스템 콜용 커널 스택 설정)
	gdt_init ();	// GDT(Global Descriptor Table) 초기화 (세그먼트 셀렉터용)
#endif

	/* 7. 인터럽트 및 장치 초기화 */
	intr_init ();	// 인터럽트 디스크립터 테이블(IDT) 설정
	timer_init ();	// 하드웨어 타이머 초기화
	kbd_init ();	// 키보드 장치 초기화
	input_init ();	// 키보드 입력 버퍼 초기화
#ifdef USERPROG
	exception_init ();
	syscall_init ();
#endif

	/* 8. 커널 스케줄러 시작 + 인터럽트 허용 */
	thread_start ();	// 초기 thread → idle thread로 교체, 인터럽트 on
	serial_init_queue ();  // 시리얼 포트 초기화 (test용)
	timer_calibrate ();	// 타이머 정확도 보정

#ifdef FILESYS
	/* 9. 파일 시스템 초기화 */
	disk_init ();	// 디스크 하드웨어 초기화
	filesys_init (format_filesys);	// 파일 시스템 구조체 및 루트 초기화
#endif

#ifdef VM
	/* 10. 가상 메모리 초기화 (SPL 등) */
	vm_init ();	// supplemental page table 및 swap 공간 설정
#endif
	/* 11. 부팅 완료 메시지 출력 */
	printf ("Boot complete.\n");

	/* 12. 커맨드라인 인자로 받은 실행 명령 수행 (예: run alarm-multiple) */
	run_actions (argv);

	/* 13. 옵션에 따라 자동 종료 */
	if (power_off_when_done)
		power_off ();
	/* 14. main()은 NO_RETURN → 종료 시 명시적으로 thread_exit 호출 */
	thread_exit ();
}
  1. main()은 커널이 부팅되며 가장 먼저 실행되는 함수
  2. run_actions(argv) 호출이 유저 프로그램을 실행하는 진짜 출발점
  3. 그 뒤에야 비로소 "prog a b c" 같은 유저 프로그램이 메모리에 올라가고, 실행됨

앞서 초기화들을 진행하고 실제로 테스트를 진행하는 run_actions로 넘어갑니다

run_actions(argv);
  • 여기서 argv는 커맨드라인 인자들을 담고 있음 (예: -q run args-single onearg)
  • run_actions()는 이 인자들을 보고:
    • -q: 조용히 실행
    • run <testname>: 실제로 사용자 프로그램 실행
    • fsdump: 파일 시스템 상태 출력
    • ls: 파일 목록 출력
    • 등등의 동작을 선택해서 실행함


/* Executes all of the actions specified in ARGV[]
   up to the null pointer sentinel. */
static void
run_actions (char **argv) {
    
    /* An action represents a possible kernel command
       like 'run', 'ls', 'rm', etc. */
    struct action {
        char *name;                       /* 액션 이름: run, ls, cat 등 */
        int argc;                         /* 요구되는 인자 수 (이름 포함) */
        void (*function) (char **argv);   /* 이 액션이 실행할 함수 포인터 */
    };

    /* 액션 테이블: argv[0]에 있는 명령어에 따라 호출할 함수 매핑 */
    static const struct action actions[] = {
        {"run", 2, run_task},     // ex) run 'prog a b c' → run_task 호출
#ifdef FILESYS
        {"ls", 1, fsutil_ls},     // ex) ls
        {"cat", 2, fsutil_cat},   // ex) cat filename
        {"rm", 2, fsutil_rm},
        {"put", 2, fsutil_put},
        {"get", 2, fsutil_get},
#endif
        {NULL, 0, NULL},          // 테이블 종료용 NULL 엔트리
    };

    /* ARGV 배열 끝(NULL)까지 루프를 돌면서 명령어를 하나씩 처리 */
    while (*argv != NULL) {
        const struct action *a;
        int i;

        /* 현재 argv[0]에 해당하는 명령어 문자열과 일치하는 액션 찾기 */
        for (a = actions; ; a++) {
            if (a->name == NULL)
                PANIC ("unknown action `%s' (use -h for help)", *argv); // 일치하는 명령 없으면 종료
            else if (!strcmp (*argv, a->name))
                break; // 일치하면 해당 액션을 찾았으므로 탈출
        }

        /* 지정된 액션이 요구하는 인자 개수만큼 실제로 존재하는지 확인 */
        for (i = 1; i < a->argc; i++)
            if (argv[i] == NULL)
                PANIC ("action `%s' requires %d argument(s)", *argv, a->argc - 1);

        /* 찾은 액션의 함수 실행: 예를 들어 run_task(argv) */
        a->function (argv);

        /* argv 포인터를 다음 명령어 위치로 이동시킴 */
        argv += a->argc;
    }
}
  • run_actions(argv)는 "run" 명령어를 찾아서 그에 해당하는 함수인 run_task()를 호출함으로써, 유저 프로그램 실행을 출발시키는 스위치 역할

여기서 일단 run_task로 넘어감


/* ARGV[1]에 명시된 유저 프로그램 또는 테스트를 실행합니다.
 *
 * ARGV[0] = "run"
 * ARGV[1] = 실행할 프로그램명 또는 테스트명 (예: "prog a b c")
 *
 * - thread_tests가 true일 경우 스레드 테스트 실행
 * - 아니라면 유저 프로그램을 실행하고, 종료할 때까지 기다립니다.
 */
static void
run_task (char **argv) {
    /* 실행할 작업 이름(프로그램 or 테스트 전체 문자열)을 task에 저장 */
    const char *task = argv[1];

    /* 실행 시작 메시지 출력 */
    printf ("Executing '%s':\n", task);

#ifdef USERPROG
    /* 스레드 테스트 플래그가 켜져 있을 경우 (스레드만 테스트할 때) */
    if (thread_tests){
        run_test (task);  // 스레드 테스트 실행
    } else {
        /* 유저 프로그램 실행을 위한 흐름
         * 1. process_create_initd(): 유저 프로세스 생성 및 실행
         * 2. process_wait(): 해당 프로세스가 종료될 때까지 대기 */
        process_wait (process_create_initd (task));
    }
#else
    /* USERPROG 설정이 꺼져 있으면 무조건 스레드 테스트 실행 */
    run_test (task);
#endif

    /* 실행 완료 메시지 출력 */
    printf ("Execution of '%s' complete.\n", task);
}

우린 유저 프로그램이니 else문 타서 process_wait으로 가야하는데 일단 process_create_initd로 감


/* 첫 번째 사용자 프로그램인 "initd"를 FILE_NAME에서 로드하여 시작합니다.
 * 새 스레드는 스케줄링 될 수 있으며 (그리고 심지어 종료될 수도 있음)
 * process_create_initd()가 반환되기 전에.
 *
 * 이 함수는 반드시 한 번만 호출되어야 합니다.
 * 유저 프로그램을 실행하기 위해 커널 스레드를 생성하며,
 * initd() 함수가 실행 entry point가 됩니다.
 *
 * 생성된 스레드의 TID(thread ID)를 반환하고,
 * 생성에 실패하면 TID_ERROR를 반환합니다.
 */
tid_t
process_create_initd (const char *file_name) {
    char *fn_copy;
    tid_t tid;

    /* file_name 문자열을 복사합니다.
     * 이유: file_name은 caller 함수의 지역 변수일 수 있으므로
     * 스레드 생성 이후에도 안전하게 사용하려면 복사본이 필요합니다.
     */
    fn_copy = palloc_get_page (0);  // 페이지 단위로 복사 공간 확보
    if (fn_copy == NULL)
        return TID_ERROR;
    strlcpy (fn_copy, file_name, PGSIZE);  // 안전하게 문자열 복사
    
    /* 이 코드를 넣어줘야 thread_name이 file name이 됩니다  */
    char *save_ptr;
    strtok_r(file_name, " ", &save_ptr);
    
    /* 새로운 커널 스레드를 생성하여, initd() 함수를 실행합니다.
     * 이 때 인자로 fn_copy를 넘겨줍니다 ("prog a b c" 등)
     */
    tid = thread_create (file_name, PRI_DEFAULT, initd, fn_copy);
    if (tid == TID_ERROR)
        palloc_free_page (fn_copy);  // 스레드 생성 실패 시 메모리 해제

    return tid;  // 생성된 스레드의 TID 반환
}
  • 유저 프로그램을 실행하기 위해 새로운 커널 스레드를 생성하는 함수
  • 저 파싱 부분을 넣어준 이유는 :
    • 사용자 프로세스가 종료될 때마다, exit()을 호출했든 다른 이유든 간에, 다음 형식에 맞춰 프로세스 이름과 종료 코드를 출력해야 테스트가 통과됩니다

저initd보이시나요

스레드 생성할때 들어가는 함수인데 저기로 넘어갑니다

/* 첫 번째 사용자 프로세스를 시작하는 스레드 함수입니다.
 * process_create_initd()에서 thread_create()를 통해 생성된 스레드가 실행하는 함수로,
 * 전달받은 인자 문자열(f_name)을 기반으로 실제 유저 프로그램을 메모리에 적재하고 실행합니다.
 */
static void
initd(void *f_name)
{
#ifdef VM
	/* (1) 가상 메모리 기능이 켜져 있는 경우, 현재 스레드의 보조 페이지 테이블 초기화
	 * - 페이지 폴트 처리 시 사용할 supplemental page table을 생성합니다.
	 * - 이를 통해 lazy loading, stack growth, mmap 등을 구현할 수 있습니다.
	 */
	supplemental_page_table_init(&thread_current()->spt);
#endif

	/* (2) 현재 스레드의 process 관련 구조 초기화
	 * - 파일 디스크립터 테이블, 자식 리스트, lock 등 프로세스 관리에 필요한 구조를 설정합니다.
	 * - process_exec()에서 사용할 수 있도록 준비하는 과정입니다.
	 */
	process_init();

	/* (3) 전달받은 f_name("args-single onearg" 등)을 실행합니다.
	 * - ELF 실행 파일을 메모리에 로딩하고, 유저 스택 구성 및 인자 설정을 수행합니다.
	 * - 성공하면 do_iret()를 통해 유저 모드로 진입하며, 이후 이 함수는 더 이상 실행되지 않습니다.
	 * - 실패 시 커널 패닉을 발생시켜 시스템을 중단합니다.
	 */
	if (process_exec(f_name) < 0)
		PANIC("Fail to launch initd\n");

	/* (4) 위에서 유저 모드로 넘어갔기 때문에, 여기에 도달하면 논리적 오류입니다.
	 * - 정상적으로 동작한다면 이 줄은 절대 실행되지 않아야 하므로,
	 * - ASSERT(false)와 동일한 역할을 합니다.
	 */
	NOT_REACHED();
}

 

자 여기서

if (process_exec(f_name) < 0)
	PANIC("Fail to launch initd\n");
  • 여기가 핵심입니다
  • f_name == "prog a b c" 같은 인자 문자열임
  • 이 문자열을 기반으로 실행 파일을 메모리에 로드하고
  • 인자 세팅 + 스택 구성 + do_iret()으로 유저 모드로 점프
  • 실패하면 커널 패닉 발생

 


이제 이 다음은 process_exec에 가서 파싱하고 스택에 넣고 뭐...등등 아무튼 그 부분은 인자 파싱 부분이라 따로 다루도록 하겠습니다

 

일단 흐름을 잡아야 코드에서 문제가 생겨도 어디서 문제가 생기고 어디에서 막히고를 파악하기가 훨씬 낫더라구요

 

전체적인 흐름입니다

argv = ["run", "prog a b c", NULL]      // 커맨드라인 인자 파싱 결과
↓
run_actions(argv)                       // 명령어 파서
↓
match "run"                             // run 명령 발견
↓
run_task(argv[1])                       // ⇒ run_task("prog a b c")
↓
process_create_initd("prog a b c")      // 유저 프로그램 이름 + 인자 문자열 전달
↓
thread_create("prog", initd, "prog a b c") // 이름은 첫 단어로 파싱, 인자는 전체 전달
↓
→ initd("prog a b c")                   // 새 스레드 시작 함수
↓
→ process_exec("prog a b c")            // 현재 스레드의 유저 실행 컨텍스트 교체
↓
→ load() + 스택에 인자 셋팅 + do_iret  // ELF 로딩, 인자 전달, 유저 모드 진입