저번 게시글에서는 시스템콜의 동작에 대해서 알아보았다.
이번 게시글에서 다룰 내용은 Process.c 에서 동작하는 함수들이다.
본인도 블로그를 많이 뒤져보았지만 무작정 가져다 쓴다고 코드가 동작하지 않는다.
작동 코드와 현재 자신의 코드를 비교해보며, 수정해나가면 동작하게 될 것임 .
그럼 알아보자
static void process_init(void)현재 실행중인 스레드(=프로세스)에 대해 파일 디스크립터 테이블(FDT)를 초기화 하는 함수
즉, 프로세스가 실행되기 위해 필요한 파일 시스템 관련 리소스를 준비하는 초기 설정 단계
static void
process_init(void)
{
struct thread *current = thread_current();
current->FDT = palloc_get_multiple(PAL_ZERO, FDT_PAGES);
current->running_file = NULL;
current->next_FD = 2;
}
- 현재 실행중인 스레드(즉, 유저 프로세스)를 가져온다
- 프로세스의 FDT(File Disciptor Table)를 할당한다.
palloc_get_multiple()은FDT_PAGES만큼의 페이지를 확보
PAL_ZERO플래그를 통해 메모리를 0으로 초기화
- 현재 실행중인 파일은 없으므로
NULL로 설정 해주고
- FD는 :
- 0 : stdin(표준 입력), 1 : stdout(표준 출력), 2 : stderr(에러) 가 기본적으로 예약되어 있음
- 새로 열리는 파일은 원래 3번부터 시작해야 하는데,
- PintOS에서는 stdin(0)과 stdout(1)만 쓰므로 2부터 시작
tid_t process_create_initd(const char *file_name)새 유저 프로세스를 위한 초기 커널 스레드(initd)를 생성하고, 해당 스레드에서 file_name으로 지정된 실행 파일을 유저 프로그램으로 실행할 준비를 시키는 함수
즉, 전체 유저 프로세스 실행 흐름의 시작점이자 진입점
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);
/* 이 코드를 넣어줘야 thread_name이 file name이 됩니다 */
char *save_ptr;
strtok_r(file_name, " ", &save_ptr);
/* 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;
}
palloc_get_page(0)
→ 인자로 받은file_name을 복사할 메모리 할당. (동기화 문제 방지 목적)
→ 이후load()에서 파싱할 때 원본이 손상되지 않도록 안전하게 복사해둠.
strlcpy(fn_copy, file_name, PGSIZE)
→ 복사본에 실제 file_name 문자열 저장.
strtok_r(file_name, " ", &save_ptr)
→ 원래의file_name문자열에서 실행 파일 이름만 추출
→thread_create()는 이 이름을 스레드 이름으로 사용함.
thread_create(...)
→initd함수를 entry로 하는 새 스레드 생성
→ 인자로fn_copy전달. 최종적으로 이 스레드는 유저 프로그램을 로딩하게 됨.
- 실패 시
palloc_free_page()호출로 할당한 페이지 회수.
tid_t process_fork(const char *name, struct intr_frame *if_)현재 실행 중인 프로세스를 복제(fork)하여, 자식 프로세스를 생성하는 함수
POSIX 시스템의 fork()와 유사하게, 부모 프로세스의 메모리/레지스터 상태를 복사해서 동일한 실행 흐름을 가진 새 프로세스를 만들기 위한 엔트리 포인트
tid_t process_fork(const char *name, struct intr_frame *if_)
{
memcpy(&thread_current()->intr_frame, if_, sizeof(struct intr_frame));
tid_t fork_tid = thread_create(name, PRI_DEFAULT, __do_fork, thread_current());
if(fork_tid == TID_ERROR)
return TID_ERROR;
struct thread *child = get_child_by_tid(fork_tid);
if (child != NULL) {
sema_down(&child->fork_sema); // 자식의 초기화가 끝날 때까지 대기
}
return fork_tid;
}
memcpy(&thread_current()->intr_frame, if_, sizeof(struct intr_frame))
→ 부모 스레드의 CPU 레지스터 상태(intr_frame)를 현재 스레드에 복사
→ 이 값은 이후 __do_fork()에서 자식 프로세스의 레지스터 상태로 재사용됨.
thread_create(...)
→ 자식 프로세스를 생성하며, __do_fork()가 엔트리 포인트가 됨.
→ thread_current()를 인자로 넘김 → 자식은 부모의 정보를 참고하여 복사 수행.
- 생성 실패 시 TID_ERROR 반환.
get_child_by_tid(fork_tid)
→ 생성된 자식 프로세스를 부모 입장에서 확인.
sema_down(&child->fork_sema)
→ 자식이 __do_fork() 내부에서 초기화와 복제를 마칠 때까지 부모는 대기(Synchronization)
→ 자식이 sema_up()을 호출할 때까지 block.
static void __do_fork()
process_fork()에서 생성된 자식 스레드가 진입하는 함수.
부모 프로세스의 실행 상태, 메모리 구조, 파일 디스크립터 등을 복제해서 자식 프로세스를 완전히 독립된 실행 단위로 초기화한다.
최종적으로do_iret()을 호출하여 자식 프로세스가 유저 모드로 진입하도록 한다.
static void
__do_fork(void *aux)
{
struct intr_frame if_;
struct thread *parent = (struct thread *)aux;
struct thread *current = thread_current();
struct intr_frame *parent_if = &parent->intr_frame;
bool succ = true;
process_init(); // FDT 초기화
// 1. 부모의 레지스터 상태 복사
memcpy(&if_, parent_if, sizeof(struct intr_frame));
// 2. 페이지 테이블 복제
current->pml4 = pml4_create();
if (current->pml4 == NULL) goto error;
process_activate(current);
#ifdef VM
supplemental_page_table_init(¤t->spt);
if (!supplemental_page_table_copy(¤t->spt, &parent->spt))
goto error;
#else
if (!pml4_for_each(parent->pml4, duplicate_pte, parent))
goto error;
#endif
// 3. 파일 디스크립터 복제
int fd_end = parent->next_FD;
for (int fd = 0; fd < fd_end; fd++) {
if (fd <= 2)
current->FDT[fd] = parent->FDT[fd]; // stdin, stdout
else if (parent->FDT[fd] != NULL)
current->FDT[fd] = file_duplicate(parent->FDT[fd]); // 그 외는 복제
}
current->next_FD = fd_end;
// 4. 자식 프로세스의 리턴값 0으로 설정 (fork의 특징)
if_.R.rax = 0;
// 5. 유저 모드 전환용 세그먼트/플래그 설정
if_.ds = if_.es = if_.ss = SEL_UDSEG;
if_.cs = SEL_UCSEG;
if_.eflags = FLAG_IF;
// 6. 부모에게 복제가 완료되었음을 알림
sema_up(¤t->fork_sema);
// 7. 유저 모드 진입
if (succ)
do_iret(&if_);
error:
current->exit_status = -1;
sema_up(¤t->fork_sema);
thread_exit();
}
1️⃣ 부모의 레지스터 상태를 자식 스레드의 지역 intr_frame에 복사
2️⃣ 페이지 테이블(pml4) 생성 + 부모의 SPT or PTE를 복사
3️⃣ 부모의 파일 디스크립터 테이블(FDT) 복사, 파일 객체는
file_duplicate()4️⃣ 자식 프로세스의 fork 리턴값은 항상 0으로 설정 (R.rax = 0)
5️⃣ 유저 모드 진입을 위한 인터럽트 프레임 설정
6️⃣
sema_up()으로 부모 프로세스를 깨움7️⃣ 자식은
do_iret()으로 유저 모드 진입🔁 실패 시 오류 처리 및 종료
이 함수가 끝나기 전까지는 부모는 process_fork()에서 대기 상태 (→ sema_down)
file_duplicate()는 include/filesys/file.h에 구현되어 있음. 복사 안 하면 FD 충돌/동기화 문제 발생 가능
do_iret() 이후의 코드는 절대 도달하지 않음 (자식은 유저 모드로 전환되므로)
int process_exec(void *f_name)이 함수는 현재 커널 스레드를 새로운 유저 프로그램으로 완전히 교체한다.
기존 프로세스 컨텍스트를 제거하고, 새로운 실행 파일을 로딩한 뒤 유저 스택까지 셋업하고 유저 모드로 진입시킨다.
→exec()시스템 콜의 핵심 구현 함수.
int process_exec(void *f_name)
{
char *argv[MAX_ARGS];
int argc = parse_args(f_name, argv);
bool success;
struct intr_frame _if;
_if.ds = _if.es = _if.ss = SEL_UDSEG;
_if.cs = SEL_UCSEG;
_if.eflags = FLAG_IF | FLAG_MBS;
/* 현재 실행 중인 프로세스 정보를 모두 정리 (페이지 테이블, FDT 등) */
process_cleanup();
/* 실행 파일 로드 */
ASSERT(argv[0] != NULL);
success = load(argv[0], &_if);
if (!success) {
palloc_free_page(f_name);
return -1;
}
/* 유저 스택에 인자 push (argv, argc 포함) */
argument_stack(argv, argc, &_if);
palloc_free_page(f_name);
// 유저 모드 진입
do_iret(&_if);
NOT_REACHED(); // 절대 도달하지 않음
}
1️⃣ 인자로 받은 문자열
f_name을 파싱해서argv[],argc로 변환2️⃣ 새로 사용할 인터럽트 프레임
_if생성 및 세그먼트 설정3️⃣ 기존의 프로세스 리소스를 정리 (
process_cleanup)4️⃣
load(argv[0], &_if)로 새로운 실행 파일을 메모리에 적재5️⃣ 실패하면
-1반환6️⃣ 성공 시
argument_stack()으로 유저 스택 초기화 (인자 push)7️⃣
do_iret(&_if)를 호출해서 유저 모드 진입8️⃣ 유저 모드 진입 이후 커널 코드는 더 이상 실행되지 않음 (
NOT_REACHED)
_if는 로컬 변수로만 선언되어야 함 → 커널 스레드 구조체 내 intr_frame을 덮어쓰면 안 됨 (재스케줄링 시 쓰임)
palloc_free_page(f_name)은 process_create_initd()에서 넘긴 복사본 정리를 위한 것
argument_stack() 함수는 argv 배열, argc 값을 기준으로 스택에 인자들을 포맷에 맞춰 push
static int parse_args(char *target, char *argv[])문자열로 주어진 실행 명령어(ex:
"echo hello world")를 공백 기준으로 잘라서
argv[]배열에 각 토큰의 포인터를 저장하고, 인자의 개수(argc)를 반환하는 파서 함수
→ 유저 스택에 인자를push하기 위해 필수적인 전처리 단계
static int parse_args(char *target, char *argv[])
{
int argc = 0;
char *token;
char *save_ptr; // 파싱 상태를 저장할 변수!
for (token = strtok_r(target, " ", &save_ptr);
token != NULL;
token = strtok_r(NULL, " ", &save_ptr))
{
argv[argc++] = token; // 각 인자의 포인터 저장
}
argv[argc] = NULL; // 마지막에 NULL로 끝맺기(C 관례)
return argc;
}
1️⃣
strtok_r()를 사용하여 target 문자열을 공백(" ") 기준으로 파싱2️⃣ 각 토큰의 포인터를
argv[]배열에 차례대로 저장3️⃣
argc는 인자의 개수를 세는 카운터4️⃣
argv[argc] = NULL로 마지막 인자 뒤에 NULL 포인터 추가 (exec()의 argv 규약)
5️⃣ 최종적으로argc리턴
strtok_r()는 재진입 가능한 strtok: 멀티스레드 환경에서 안전하게 사용 가능
argv[]는 포인터 배열이기 때문에 실제 문자열을 복사하지 않고 포인터만 저장 → 성능 효율적
target 문자열은 이 과정에서 파괴됨 (공백이 \0으로 바뀜), 따라서 사본을 사용하는 것이 필수
struct thread *get_child_by_tid(tid_t child_tid)현재 스레드(=부모 프로세스)의 자식 리스트에서 특정 TID를 가진 자식 스레드를 탐색해서 반환한다.
fork나 wait 구현 시, 특정 자식 스레드에 대한 참조를 얻기 위해 사용되는 기본 도우미 함수.
struct thread *get_child_by_tid(tid_t child_tid){
struct thread *cur = thread_current();
struct thread *v = NULL;
for (struct list_elem *i = list_begin(&cur->children);
i != list_end(&cur->children);
i = i->next)
{
struct thread *t = list_entry(i, struct thread, child_elem);
if (t->tid == child_tid) {
v = t;
break;
}
}
return v;
}
1️⃣ 현재 실행 중인 스레드(=부모 프로세스)의 포인터를 가져옴
2️⃣
cur->children리스트 순회 (모든 자식 프로세스들)3️⃣ 각 자식은
child_elem리스트 노드를 통해 접근4️⃣ 자식의
tid가 인자로 받은child_tid와 일치하면 포인터 저장5️⃣ 찾았으면 바로
break, 마지막에 결과 반환 (못 찾으면NULL)
struct thread는 각 스레드가 자신을 child_elem을 통해 부모의 children 리스트에 연결함.
리스트에서 원하는 자식을 찾지 못했을 경우 NULL을 반환하므로, 이후 코드에서 체크 필수
성능상 자식 수가 많을 경우 linear search이므로 비용은 있음 (하지만 일반적으로 자식 수는 적음)
int process_wait(tid_t child_tid)현재 프로세스(부모)가
child_tid를 가진 자식 프로세스의 종료를 기다리고,
해당 자식의exit_status를 반환하는 함수.
즉,wait()시스템 콜의 내부 구현 함수이자 부모-자식 동기화의 핵심.
int process_wait (tid_t child_tid) {
enum intr_level old_level = intr_disable();
struct thread *cur = thread_current();
struct thread *search_cur = get_child_by_tid(child_tid);
intr_set_level(old_level);
if (search_cur == NULL)
return -1;
sema_down(&search_cur->wait_sema);
int stat = search_cur->exit_status;
list_remove(&search_cur->child_elem);
sema_up(&search_cur->exit_sema);
return stat;
}
1️⃣ 현재 스레드(=부모)의 자식 리스트에서
child_tid를 가진 자식을 탐색2️⃣ 없으면 -1 반환 (유효하지 않은 자식)
3️⃣ 찾았다면 해당 자식의
wait_sema를sema_down()→ 자식이 종료될 때까지 대기(block)4️⃣ 자식이 종료되면, 자식이 남긴
exit_status를 읽어서 저장5️⃣ 자식 프로세스를
children리스트에서 제거6️⃣
exit_sema를sema_up()하여 자식이 진짜 종료할 수 있도록 허용7️⃣ 최종적으로 자식의 종료 상태
stat반환
intr_disable()과 intr_set_level()은 리스트 탐색 중 인터럽트 중단 → 데이터 경쟁 방지용
get_child_by_tid()로 자식의 존재와 유효성 검사 수행
자식 리스트에서 직접 제거해야 다시 wait하지 않게 됨 (POSIX wait()의 한 번만 대기 규칙 반영)
static bool load(const char *file_name, struct intr_frame *if_)ELF 형식의 유저 실행 파일을 열고, 유효성 검증 및 섹션 로딩을 수행하여,
유저 프로그램이 메모리에서 정상적으로 시작할 수 있도록 준비한다.
→process_exec()나initd()에서 유저 모드 진입 직전에 반드시 호출되는 함수.
load 함수의 경우에는 코드가 굉장히 길기 때문에 쪼개서 알아보자
t->pml4 = pml4_create();
process_activate(thread_current());
새로운 유저 프로세스를 위한 주소 공간 생성 및 활성화
실패 시 종료
file = filesys_open(file_name);
파일 시스템에서 실행 파일 Open
실패하면 load 실패
file_read(...); memcmp(...); 조건 체크
\177ELF), 타입, 머신, 버전, 프로그램 헤더 개수 등 유효성 확인for (int i = 0; i < ehdr.e_phnum; i++) {
// PT_LOAD 타입만 처리
}
PT_LOAD 타입인 segment만 메모리에 매핑
읽기/제로 초기화 여부 계산 후 load_segment()로 로딩 수행
setup_stack(if_);
유저 스택을 커널 힙이 아닌 유저 영역에 설정
이후 인자 전달용 스택 초기화(argument_stack())를 위한 기반
if_->rip = ehdr.e_entry;
file_deny_write(file);
t->running_file = file;
if (!success && file != NULL) file_close(file);
1️⃣ 페이지 디렉터리(pml4) 생성 및 활성화
2️⃣ 파일 열기: 주어진 file_name 실행파일 open
3️⃣ ELF 헤더 유효성 검사
4️⃣ 프로그램 헤더(PHdr) 순회하며, 필요한 segment를 메모리에 load
5️⃣ 유저 스택 설정 (setup_stack())
6️⃣ 유저 진입 지점 설정 (rip = entry)
7️⃣ 성공 시 실행 중인 파일 포인터 등록 및 write 금지 설정
8️⃣ 실패 시 리소스 정리
ELF 헤더는 리눅스 바이너리 포맷이며, e_phoff, e_entry, e_phnum 등을 기반으로 메모리 로딩
프로그램 헤더가 PT_LOAD인 경우만 실제로 메모리에 load됨
유저 스택은 반드시 이 함수에서 세팅되어야 이후 do_iret() 호출이 가능
file_deny_write() 호출은 file 시스템 write 동기화에 매우 중요함
static void argument_stack(char *argv[], int argc, struct intr_frame *if_)유저 프로그램 실행 전에 유저 스택에 인자(argv)와 argc 값을 포맷에 맞게 push하는 함수.
즉, 유저 프로그램이main(int argc, char *argv[])형태로 실행될 수 있도록 스택 포맷을 세팅해준다.
static void argument_stack(char *argv[], int argc, struct intr_frame *if_) {
uint64_t rsp_arr[argc];
// 1. 문자열을 역순으로 스택에 복사
for (int i = argc - 1; i >= 0; i--) {
size_t len = strlen(argv[i]) + 1;
if_->rsp -= len;
rsp_arr[i] = if_->rsp;
memcpy((void *)if_->rsp, argv[i], len);
}
// 2. 16바이트 정렬 (x86-64 ABI 규약)
if_->rsp = if_->rsp & ~0xF;
// 3. NULL sentinel
if_->rsp -= 8;
memset(if_->rsp, 0, 8);
// 4. argv[i] 포인터들을 다시 역순 push
for (int i = argc - 1; i >= 0; i--) {
if_->rsp -= 8;
memcpy(if_->rsp, &rsp_arr[i], sizeof(char *));
}
// 5. fake return address
if_->rsp -= 8;
memset(if_->rsp, 0, 8);
// 6. 레지스터 설정 (유저 main 전달용)
if_->R.rdi = argc;
if_->R.rsi = if_->rsp + 8;
}
1️⃣ 문자열들을 유저 스택에 역순 복사
2️⃣ 16바이트 정렬 수행
3️⃣ NULL 포인터 sentinel 추가
4️⃣
argv[i]포인터 주소들을 역순으로 스택에 push5️⃣ fake return address (
0) 추가6️⃣ intr_frame에
rdi=argc,rsi=argv주소 설정 (유저 모드 전달 준비)
유저 스택은 top-down 방향으로 성장하므로 역순 push 필수
16바이트 정렬은 System V AMD64 ABI에서 요구하는 표준 규약
rsp_arr[]에 각 문자열이 복사된 위치를 저장했다가 → 이후 포인터만 push
fake return address는 유저 main에서 ret 호출 대비용. 일반적으로 0
PintOS의 시스템 콜에서 파일을 열거나 닫을 때 내부적으로 사용하는 유틸리티 함수들을 소개하겠다. 이 함수들은 이전 게시글 System Call 구현에서도 사용하는 함수들이다.
모두 현재 스레드의 File Descriptor Table (FDT)을 기반으로 동작한다.
process_add_file(struct file *file)파일을 열 때 FDT의 빈 슬롯에 등록하고 FD를 반환
int process_add_file(struct file *file) {
struct thread *curr = thread_current();
// fd는 0(stdin), 1(stdout), 2(stderr)을 건너뛰고 3부터 시작
for (int fd = 3; fd < MAX_FD; fd++) {
// 비어 있는 슬롯 찾기
if (curr->FDT[fd] == NULL) {
curr->FDT[fd] = file; // 파일 등록
if (curr->next_FD <= fd)
curr->next_FD = fd + 1;
return fd; // 해당 fd 반환
}
}
return -1; // 여유 공간 없음 → 실패
}
process_get_file(int fd)주어진 FD에 연결된 파일을 가져오기. 유효하지 않으면 NULL
struct file *process_get_file(int fd) {
struct thread *curr = thread_current();
// stdin(0), stdout(1), stderr(2)은 시스템 콜에서 직접 처리하므로 제외
// 유효한 범위가 아니면 NULL
if (fd < 3 || fd >= MAX_FD) {
return NULL;
}
// FDT에서 해당 fd 위치의 파일 반환
return curr->FDT[fd];
}
process_close_file(int fd)FD에 연결된 열린 파일을 닫고 테이블에서 제거
void process_close_file(int fd) {
struct thread *curr = thread_current();
// stdin, stdout, stderr 제외 + 유효한 범위인지 확인
if (fd >= 3 && fd < MAX_FD) {
// 실제로 열려 있는 파일이 있으면 닫기
if (curr->FDT[fd] != NULL) {
file_close(curr->FDT[fd]); // 파일 자원 해제
curr->FDT[fd] = NULL; // FDT에서 제거
}
}
}
FDT는 프로세스가 시작될 때 process_init()에서 초기화되며, 각 프로세스는 자신만의 파일 테이블을 가진다.
FD 번호 0, 1, 2는 각각 stdin, stdout, stderr로 예약되어 있어 일반 파일은 3번부터 사용된다.
MAX_FD는 FDT의 최대 크기를 정의한 상수로, 동시에 열 수 있는 파일 수를 제한함으로써 시스템 자원을 보호한다.
이로써 process.c의 함수 로직들을 알아보앗다 !
전체 코드는 아래 링크에 들어가면 확인할 수 있으니 많관부
📍GitHub