핀토스 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 ();
}
- main()은 커널이 부팅되며 가장 먼저 실행되는 함수
- run_actions(argv) 호출이 유저 프로그램을 실행하는 진짜 출발점
- 그 뒤에야 비로소 "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 로딩, 인자 전달, 유저 모드 진입
'크래프톤정글 > Pintos' 카테고리의 다른 글
| User Program _System Call(filesize, read, seek, tell) (0) | 2025.05.19 |
|---|---|
| User Program _ Argument Passing (1) | 2025.05.17 |
| PintOS_Priority_Scheduling_part_3_donation (1) | 2025.05.13 |
| 지금까지의 개념 정리_Alram Clock & Priority Scheduling (0) | 2025.05.12 |
| PintOS_Priority_Scheduling_part_2 (0) | 2025.05.11 |