pintOS (project_2)

이승우·2023년 6월 11일
0

크래프톤 정글

목록 보기
11/14
post-thumbnail

Project 2: USER-PROGRAMS

키워드

  • Argument Passing

  • System Call

    • System Calls - Ⅰ
    • System Calls - II

Argument Passing

과제 목표

process_exec() 내에 사용자 프로그램을 위한 인자를 셋업해라.

  • 유저 프로그램을 실행하기 전에, 커널은 레지스터에다가 맨 처음 function의 argument를 저장해야 한다. process_exec()은 유저가 입력한 명령어를 수행할 수 있도록 프로그램(=process)을 메모리에 적재하고 실행하는 함수이다. 해당 프로그램은 f_name에 문자열로 저장되어 있으나 현재 상태에서 process_exec() 은 새로운 프로세스에 대한 인자 passing을 제공하지 않는다. 이 기능을 구현하는 것이 이번 과제이다. process_exec() 에 코드를 추가해서 간단히 프로그램 파일 이름을 인자로 넣는것 대신에, space가 올 때마다 단어를 parsing하도록 만들어야 한다. 이때, 첫 번째 단어는 프로그램 이름이고 두세 번째 단어는 각각 첫 번째, 두 번째 인자이다.

  • 명령어 라인과 함께, 여러 개 space는 하나의 space와 동일하게 취급해야 한다. 이때 명령어 인자의 길이에 제한을 둘 수 있다.

구현

process_create_initd() 수정
원본 코드

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);

    /* 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;
}

현재 process_create_initd() 함수는 커맨드 라인의 첫 번째 토큰을 thread_create() 함수의 첫 인자로 전달 되도록 프로그램을 수정해야 한다. 현재는 커맨드 라인 전체가 thread_create() 에 전달되고 있다.

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);

    /* Create a new thread to execute FILE_NAME. */
    char *save_ptr;
    strtok_r(file_name, " ", &save_ptr);

    tid = thread_create (file_name, PRI_DEFAULT, initd, fn_copy);
    if (tid == TID_ERROR)
        palloc_free_page (fn_copy);
    return tid;
}

strtok_r() : strtok_r 함수는 지정된 문자(이를 delimiters라고 한다)를 기준으로 문자열을 자른다.

예를 들어 위 코드에서 token = strtok_r(file_name, " ", &save_ptr);이라고 하면 file_name의 가장 첫번째 문자열이 나온다. 긴 말 할 것 없이 아래 예시를 보자. "The little prince"를 공백을 기준으로 잘라서 출력한다고 하자. 그러면 출력값은 "The", "Little", "Prince"일 것이다. strtok_r은 한글자씩 이동하다가(T, h, e, ...) 공백을 만나면 그곳에 NULL 값을 넣은 다음, 그 앞까지의 문자열(=The)을 반환한다. 여기서 &the_last는 그 뒤의 문자열("\nLittie Prince\n")에서 가장 첫번째 문자의 주소값을 나타낸다. 이렇게 한 글자씩 잘라서 반환하는 개념이기에 while문을 돌리면서 한글자씩 뽑아내는 것으로 이해하면 된다.

load() 수정
원본 코드

static bool
load (const char *file_name, struct intr_frame *if_) {
    struct thread *t = thread_current ();
    struct ELF ehdr;
    struct file *file = NULL;
    off_t file_ofs;
    bool success = false;
    int i;

    /* Allocate and activate page directory. */
    t->pml4 = pml4_create ();
    if (t->pml4 == NULL)
        goto done;
    process_activate (thread_current ());

    /* Open executable file. */
    file = filesys_open (file_name);
    if (file == NULL) {
        printf ("load: %s: open failed\n", file_name);
        goto done;
    }

        •••

원래 코드 이후에는 load()를 실행하는 코드가 나온다. load()함수는 실행파일의 file_name을 적재해 실행하는 함수이다. load()를 부른 caller인 process_exec()에서 입력한 커맨드 전체가 file_name인자로 넘어온다.

static bool
load (const char *file_name, struct intr_frame *if_) {
    struct thread *t = thread_current ();
    struct ELF ehdr;
    struct file *file = NULL;
    off_t file_ofs;
    bool success = false;
    int i;

    /* Allocate and activate page directory. */
    t->pml4 = pml4_create ();
    if (t->pml4 == NULL)
        goto done;
    process_activate (thread_current ());

    char *token, *save_ptr;
    char *argv[64];
    uint64_t cnt = 0;

    for (token = strtok_r(file_name, " ", &save_ptr); token != NULL; token = strtok_r(NULL, " ", &save_ptr)) {
        argv[cnt++] = token;
    }

    /* Open executable file. */
    file = filesys_open (argv[0]);
    if (file == NULL) {
        printf ("load: %s: open failed\n", file_name);
        goto done;
    }

        •••

for 문을 보면 매개변수로 받은 인자 file_name을 NULL을 기준으로 문자열을 잘라서 argv 배열에 하나씩 넣어준다.

        •••

    /* Set up stack. */
    if (!setup_stack (if_))
        goto done;

    /* Start address. */
    if_->rip = ehdr.e_entry;

    /* TODO: Your code goes here.
     * TODO: Implement argument passing (see project2/argument_passing.html). */

    argument_stack(argv, cnt, &if_->rsp);
    if_->R.rdi = cnt;
    if_->R.rsi = if_->rsp + 8;

    success = true;

done:
    /* We arrive here whether the load is successful or not. */
    file_close (file);
    return success;
}

다음은 인자값을 스택에 올리는 함수 argument_stack()이 필요하다. 위에서 parsing한 다음 한 문자씩 넣어준 배열 arg_list와 count값인 token_count, 그리고 인터럽트 프레임도 인자로 넣는다. 이 함수 자체에서 인터럽트 프레임을 스택에 올리는 것은 아니고, 인터럽트 프레임 내 구조체 중 특정값(rsp)에 인자를 넣어주기 위함이다. 이후에 do_iret()에서 이 인터럽트 프레임을 스택에 올린다.

argument_stack() 추가

argument_stack() 함수는 유저 스택에 프로그램 이름과 인자들을 저장하는 함수이다.
parse : 프로그램 이름과 인자가 저장되어 있는 메모리 공간, count : 인자의 개수, esp : 스택 포인터를 가리키는 주소

void argument_stack(char **parse, int count, void **esp) {

  char *argv_address[count];
  uint8_t size = 0;

    // * argv[i] 문자열
    for (int i = count - 1; -1 < i; i--) {
        *esp -= (strlen(parse[i]) + 1);
        memcpy(*esp, parse[i], strlen(parse[i]) + 1);
        size += strlen(parse[i]) + 1;
        argv_address[i] = *esp;
    }

    if (size % 8) {
        for (int i = (8 - (size % 8)); 0 < i; i--) {
            *esp -= 1;
        **(char **)esp = 0;
    }
  }

  *esp -= 8;
  **(char **)esp = 0;

  // * argv[i] 주소
    for (int i = count - 1; -1 < i; i--) {
        *esp = *esp - 8;
        memcpy(*esp, &argv_address[i], strlen(&argv_address[i]));
    }

    // * return address(fake)
    *esp = *esp - 8;
    **(char **)esp = 0;

}

argument_stack() 코드를 뜯어보자. 배열 argaddress[128]은 아래 for문에서 스택에 담을 각 인자의 주소값을 저장하는 배열이다. 이후 for문을 돌면서 process_exec() 에서 넣어주는 arg_list로부터 값을 하나하나씩 꺼내서 if->rsp에 하나씩 넣어준다. 이때 if->rsp는 user stack에서 현재 위치를 가리키는 스택 포인터이자 인터럽트 프레임 내 멤버이다. 여기서 작업이 Gitbook에 나오는 테이블에 값을 채워넣는 것과 같다.

이때, 각 인자에서 인자 크기(argv_len)을 읽는데 이때 각 인자마다는 실제로 sentinel(\n)이 포함되어 있는데 여기서 역시 strlen은 sentinel을 읽지 않으니 +1을 해주는 것이다. for문 순서를 자세히 보면

  1. 먼저 스택 포인터를 넣어줄 공간만큼 쭉 내린다.(if->rsp = if->rsp - (argv_len +1)
  2. 그다음, 해당 공간에 인자값을 복붙한다(memcpy(if_->rsp, argv[i], argv_len+1))
  3. arg_address 배열에 인자값이 위치한 주소를 저장한다. (arg_address[i] = if->rsp)

이후에는 while문을 돌면서 패딩을 삽입한다.

이번에는 주소값 자체를 삽입한다. 이래서 위에 arg_address[] 배열을 따로 만들어 여기에 주소값을 저장한 이유이다. 똑같이 for문들 돌면서 넣는데, 처음 for문에서는(int i = argc-1)로 선언된 반면 여기 for문에서는 (int i = argc)로 선언된다. 이는 앞서 process_exec()에서 while문을 돌며 token을 받아오는 것을 보면, 맨 마지막 인자값으로 NULL을 받아 arg_list에 저장하게 된다. 따라서 argv[-1]= '\n'이 된다. 근데 이 인자 값을 스택에 저장할 때는 맨 끝에 있는 NULL 값을 저장하지 않은 반면 여기서는 NULL값 가리키는 포인터를 저장한다.

for문을 돌고 나면 fake address를 넣어준다. 해당 영역의 메모리는 0으로 초기화해준다.
마지막으로 인터럽트 프레임 if_의 멤버로 있는 레지스터 구조체의 rdi에 인자 count값인 argc, 그리고 rsi에는 fake address바로 위인 arg_address의 맨 앞을 가리키는 주소값을 넣는다.

Interrupt Frame 인터럽트 프레임 (struct Intr_frame)

인터럽트 프레임은 인터럽트가 들어왔을 때, 이전에 레지스터에 작업하던 context를 switching하기 위해 이 정보를 담아놓는 구조체이다. 그래서 구조체 intr_frame에 가보면 엄청 복잡하게 나와 있고, 그안에 멤버 구조체 gp_registers R을 들고 있다. 이 R은 기존 스레드가 작업하고 있을 때의 레지스터 값을 인터럽트가 들어오면 switching하기 위해 이 구조체에다가 정보를 담는다. 그래서 1주차에 do_schedule() 을 보면do_iret()이 나오고, 이 do_iret()는 어셈블리어로 되어 있는데, 여기가 기존까지 작업했던 context를 intr_frame에 담는 과정이라고 보면 되겠다.

즉, 인터럽트 프레임은 인터럽트와 같은 요청이 들어와서 기존까지 실행 중이던 context(레지스터 값 포함)를 스택에 저장하기 위한 구조체이다.

결과

결과(중간 결과)를 확인하기 위해서는 process_exec()함수에 hex_dump() 함수를 추가해줘야 한다.

hex_dump(_if.rsp, _if.rsp, KERN_BASE - _if.rsp, true);

process_exec()함수 중간에 load가 끝난 후 다음 위에 함수를 추가해준다.

또한 process_wait() 함수에 무한루프를 추가해줘야 결과를 제대로 볼 수 있다.

while (1){}

다음 구문을 함수 내 return 전에 추가해준다. 밑에 사진은 현재 pintos의 상태를 나타내는 사진이다.

결과


System Call

System Calls - Ⅰ

과제 목표

  • 이번 과제에서는 시스템 콜 핸들러 및 시스템 콜을 구현하는 것이다. 여기서 시스템 콜이란 사용자가 커널 영역에 접근하고 싶을 때, 원하는 목적을 대신해서 작업하는 프로그래밍 인터페이스이다. 그렇기 때문에 시스템 콜은 커널 모드에서 실행되고, 작업 후 사용자 모드로 복귀한다. pintos에서는 이를 시스템 콜 핸드러를 통해 시스템 콜을 호출한다.

    시스템 콜을 호출할 때, 원하는 기능에 해당하는 시스템 콜 번호를 rax에 담는다. 그리고 시스템 콜 핸들러는 rax의 숫자로 시스템 콜을 호출하고, 해당 콜의 반환값을 다시 rax에 담아서 intr frame(인터럽트 프레임)에 저장한다.

구현

halt();
case SYS_HALT:
	halt();
	break;
void halt(void) {
	// * power_off()를 사용하여 pintos 종료
	power_off();
}
  • 주석에서 설명하고 있듯이 power_off()함수를 호출하면서 pintos를 종료시키는 시스템 콜이다
exit();
case SYS_EXIT:
	exit(f->R.rdi);
	break;
void exit(int status) {
	/*
	* 실행중인 스레드 구조체를 가져옴
	* 프로세스 종료 메시지 출력
	* 출력 양식: "프로세스 이름: exit(종료상태)"
	* thread 종료
	*/ 
	struct thread *cur = thread_current();
	cur->exit_status = status;
	printf("%s: exit(%d)\n", cur->name, status);
	thread_exit();
}
  • 현재 실행 중인 프로세스를 종료시키는 시스템 콜이다. 프로세스를 완전히 종료시키기 전에 thread 구조체에 int형인 exit_status에 인자로 받은 status를 저장한다. 그리고 thread_exit()를 호출하여 thread를 완전히 종료시킨다.

※ 곧 사라질 thread의 exit_status에 status를 저장하는 것은 만약 현재 프로세스가 자식 프로세스여서 부모 프로세스가 wait 상태일 경우, 자식 프로세스가 사라지면서 종료 상태(exit_status)를 부모에게 알려주기 위함이다. 정상적 종료라면 0을 저장한다.

create();
case SYS_CREATE:
	f->R.rax = create(f->R.rdi, f->R.rsi);
	break;
bool create (const char *file, unsigned initial_size) {
	/* 
	* 파일 이름과 크기에 해당하는 파일 생성
	* 파일 생성 성공 시 true 반환, 실패 시 false 반환
	*/
	check_address(file);
	return filesys_create(file, initial_size);
}
  • file을 만드는 시스템 콜이다. 인자로 받은 file 이름과 크기(initial_size)에 해당하는 파일을 생선한다. 파일 생성에 성공하면 true를 반환하고 실패하면 false를 반환한다. filesys_create가 실질적으로 작업을 수행하므로 크게 볼 것은 없다. (파일을 만들고 바로 열지 않는다. 여는 것은 open() 시트템 콜을 사용해서 따로 실행한다.)

  • create를 실행하기 전에 해당 file이 유저 영역에 있는 file인지 확인해야한다. 이는 check_address()로 확인할 수 있다.

remove();
case SYS_REMOVE:
	f->R.rax = remove(f->R.rdi);
	break;
bool remove (const char *file) {
	/* 
	* 파일 이름에 해당하는 파일을 제거
	* 파일 제거 성공 시 true 반환, 실패 시 false 반환
	*/
	check_address(file);
	return filesys_remove(file);
}
  • 파일 이름에 해당하는 파일을 제거한다. 파일 제거 성공 시 true 반환, 실패 시 false 반환한다.
open();
case SYS_OPEN:
	f->R.rax = open(f->R.rdi);
	break;
int open (const char *file) {
	check_address(file);
	struct thread *cur = thread_current();
	struct file *fd = filesys_open(file);
	if (fd) {
		for (int i = 2; i < 128; i++) {
			if (!cur->fdt[i]) {
				cur->fdt[i] = fd;
				cur->next_fd = i + 1;
				return i;
			}
		}
		file_close(fd);
	}
	return -1;
}
  • open()함수는 파일을 열 때 사용하는 시스템 콜이다. 성공 시 fd를 생성하고 반환, 실패 시 -1을 반환한다.
filesize();
case SYS_FILESIZE:
	f->R.rax = filesize(f->R.rdi);
	break;
int filesize (int fd) {
	struct file *file = thread_current()->fdt[fd];
	if (file)
		return file_length(file);
	return -1;
}
  • filesize()함수는 파일의 크기를 알려주는 시스템 콜이다
read();
case SYS_READ:
	f->R.rax = read(f->R.rdi, f->R.rsi, f->R.rdx);
	break; 
int read (int fd, void *buffer, unsigned size) {
	check_address(buffer);
	if (fd == 1) {
		return -1;
	}

	if (fd == 0) {
		lock_acquire(&filesys_lock);
		int byte = input_getc();
		lock_release(&filesys_lock);
		return byte;
	}
	struct file *file = thread_current()->fdt[fd];
	if (file) {
		lock_acquire(&filesys_lock);
		int read_byte = file_read(file, buffer, size);
		lock_release(&filesys_lock);
		return read_byte;
	}
	return -1;
}
  • buffer 파라미터는 읽은 데이터를 저장할 버퍼의 주소값이고, size는 읽을 데이터의 크기이다.
  • fd값이 0일 때는 표준입력이기 때문에 input_getc() 함수를 이용하여 키보드의 데이터를 읽어 byte에 저장한다.
write();
case SYS_WRITE:      
	f->R.rax = write(f->R.rdi, f->R.rsi, f->R.rdx);
	break;
int write (int fd UNUSED, const void *buffer, unsigned size) {
	check_address(buffer);

	if (fd == 0) // STDIN일때 -1
		return -1;

	if (fd == 1) {
		lock_acquire(&filesys_lock);
		putbuf(buffer, size);
		lock_release(&filesys_lock);
		return size;
	}

	struct file *file = thread_current()->fdt[fd];
	if (file) {
		lock_acquire(&filesys_lock);
		int write_byte = file_write(file, buffer, size);
		lock_release(&filesys_lock);
		return write_byte;
	}
}
  • write() 함수는 열린 파일의 데이터를 기록하는 시스템 콜이다. 역시 fd값이 1일 때는 표준 출력이기 때문에 1일 시 putbuf() 함수를 사용하여 버퍼에 저장된 데이터를 화면에 출력한다.
seek();
case SYS_TELL:
	f->R.rax = tell(f->R.rdi);
	break;
unsigned tell (int fd) {
	struct file *curfile = thread_current()->fdt[fd];
	if (curfile)
		return file_tell(curfile);
}
  • tell()함수는 열린 파일의 위치를 알려주는 시스템 콜이다.
close();
case SYS_CLOSE:
	close(f->R.rdi);
	break;
void close (int fd) {
	struct file * file = thread_current()->fdt[fd];
	if (file) {
		lock_acquire(&filesys_lock);
		thread_current()->fdt[fd] = NULL;
		file_close(file);
		lock_release(&filesys_lock);
	}
}
  • close()함수는 열린 파일을 닫는 시스템 콜이다. 파일을 닫고 fd를 제거한다.
check_addres();
void check_address(void *addr) {
	struct thread *cur = thread_current();
	if (addr == NULL || is_kernel_vaddr(addr) || pml4_get_page(cur->pml4, addr) == NULL)
		exit(-1);
}
  • Check_Address() 는 해당 주소 값이 주소 영역에 있는 주소 값인지 확인하는 함수이다. PintOS에서는 시스템 콜이 접근할 수 있는 주소를 0cx0000000 ~ 0x8048000 (이 이상은 커널 영억이다.) 으로 제한하기 때문에 유저 영억을 벗어난 영역일 경우 비정상 접근이라고 판단해 exit(-1)로서 프로세스를 종료한다.

System Calls - II

구현

exec();
case SYS_EXEC:
	exec(f->R.rdi);
	break;
int exec (const char *file_name) {
	check_address(file_name);

	int file_size = strlen(file_name) + 1;
	char *fn_copy = palloc_get_page(PAL_ZERO);
	if (!fn_copy) {
		exit(-1);
		return -1;
	}
	strlcpy(fn_copy, file_name, file_size);
	if (process_exec(fn_copy) == -1) {
		exit(-1);
		return -1;
	}
}
  • 인자로 받은 실행 파일을 실행시킨다. 현재 실행 중인 프로세스의 이미지를 이 실행 파일 프로세스의 이미지로 바꿔치기한다. 새로운 프로세스를 생성하는 것은 아니다. fork()가 자신의 복사본을 생성해 실행한다면, 자신의 복사본이 아닌 아예 다른 프로그램을 실행해야 하는 경우에 exec()을 사용한다.

  • file_name을 copy를 해야 하는가?

    • copy를 하지 않으면, process_exec()에서 process_cleanup()을 할 때 해당 file_name의 문자열의 물리적 메모리와의 매핑 정보를 담은 Page Table도 같이 지워지기 때문이다.

    • 여기서 strlcpy()하여 복사된 파일 이름 문자열은 커널 스택에 저장될 것이다. 따라서 process_cleanup() 후에도 커널 스택과 연결된 페이지 테이블을 통해 물리적 메모리와 매핑되어 활용될 수 있으므로, 애초에 fn_copy 문자열을 process_exec()의 인자로 넣어준다.

    • 실제로 파일 이름을 복사하지 않고 그대로 사용해서 process_cleanup() 후에 참조하려 시도하면 page fault가 뜬다.

wait();
case SYS_WAIT:
	f->R.rax = wait(f->R.rdi);
	break;
int wait (tid_t pid) {
  	return process_wait(pid);
}
  • 자식 프로세스가 올바르게 종료됐는지 확인한 후, 모두 종료될 때까지 대기(SLEEP)한다. 자식 프로세스가 종료되면 자식 프로세스의 종료 상태를 반환한다.
userprog/process.c

int process_wait (tid_t child_tid UNUSED) {
	/* XXX: Hint) The pintos exit if process_wait (initd), we recommend you
	 * XXX:       to add infinite loop here before
	 * XXX:       implementing the process_wait. */

	struct thread *child = get_child_process(child_tid);

	if (child == NULL)
		return -1;

	sema_down(&child->sema_wait);
	int exit_status = child->exit_status;
	list_remove(&child->child_elem);
	sema_up(&child->sema_exit);
	return exit_status;
}
  • 기다려야 하는 자식 프로세스의 tid를 인자로 입력받는다.
  • 해당 자식 프로세스의 wait_sema를 DOWN해주고 BLOCK된다. 이제 자식 프로세스가 sema_up()으로 명시적으로 풀어주지 않는 이상 부모 프로세스는 계속 잠들어 있는다.
  • 자식 프로세스가 실행되어 작업을 한 후, 종료하기 직전에 부모 프로세스를 깨우고 자기 자신을 BLOCK한다. 이를 통해 부모 프로세스는 자식 프로세스한테서 필요로 하는 정보들을 얻어올 수 있다. 예를 들어 자식 프로세스의 exit_status 등.
  • 부모 프로세스는 자식 프로세스에게서 exit_status를 얻어온다. 그리고 자신의 child_list에서 자식 프로세스를 지운다.
  • 그 후 BLOCK되어있던 자식 프로세스를 깨워 제대로 종료될 수 있도록 한다.
  • 종료된 자식 프로세스의 exit_status를 반환한다.
userprog/process.c

void process_exit (void) {
    	
        •••
        
	sema_up(&curr->sema_wait);
	sema_down(&curr->sema_exit);

	palloc_free_page(table);
	process_cleanup ();
}
  • 위에 코드 처럼 process_exit()함수에 세마포어를 추가해 자식 프로세스가 종료되었을 때, 부모를 깨울수 있도록 다음과 같이 코드를 추가해준다.
fork();
case SYS_FORK:
	memcpy(&thread_current()->ptf, f, sizeof(struct intr_frame));
	f->R.rax = fork(f->R.rdi);
	break;
int fork (const char *thread_name) {
	check_address(thread_name);
	return process_fork(thread_name, &thread_current()->ptf);
}
  • 현재 프로세스를 복사한 새 자식 프로세스를 만든다. 자식 프로세스는 부모 프로세스와 다른 PID를 갖는다. fork()의 반환값은 부모 프로세스는 자식 프로세스의 PID, 자식 프로세스는 0을 반환받는다. fork() 후 부모와 자식 중 어느 프로세스가 먼저 실행되는지는 스케줄링 방식에 따라 다르다.
userprog/process.c

tid_t process_fork (const char *name, struct intr_frame *if_ UNUSED) {
	/* Clone current thread to new thread.*/
	struct thread *cur = thread_current();
	tid_t ctid = thread_create (name, PRI_DEFAULT, __do_fork, cur);
	if (ctid == TID_ERROR)
		return TID_ERROR;
	struct thread *child = get_child_process(ctid);
	sema_down(&cur->sema_fork);
	return ctid;
}
  • 현재 실행되고 있는 부모 프로세스를 자식 프로세스에게 복제한다 __do_fork(). 자식이 fork를 완료할 때까지 BLOCK해 있다가, 자식이 fork를 완료하면 새롭게 생성된 자식 프로세스의 pid를 반환한다.

  • 이 때 인자로 받는 if_는 시스템 콜 핸들러 함수에 인자로 들어가는, 시스템 콜을 부른 부모 프로세스의 인터럽트 프레임이다.

  • 어차피 부모 프로세스의 인터럽트 프레임이면 그냥 parent->tf를 하면 되지 뭐하러 parent->parent_ifif를 복사해서 쓰냐?

    • 인터럽트 프레임이 바뀌었을 것이다. 우리에게 필요한 건 시스템 콜을 부르기 전의 부모 프로세스의 레지스터 값들이고, 그게 인자로 받은 if_ = parent->parent_if이다.
    • 지금 부모 프로세스의 tf의 값은 시스템 콜을 부르면서 값이 바뀌어져 있을 것이다. 시스템 콜 엔트리에서 RSP의 값을 Ring0에 맞춰줬기 때문이다.
userprog/process.c

static void
__do_fork (void *aux) {
	struct intr_frame if_;
	struct thread *parent = (struct thread *) aux;
	struct thread *current = thread_current ();
	/* TODO: somehow pass the parent_if. (i.e. process_fork()'s if_) */
	struct intr_frame *parent_if = &parent->ptf;
	bool succ = true;

	/* 1. Read the cpu context to local stack. */
	memcpy (&if_, parent_if, sizeof (struct intr_frame));
  	if_.R.rax = 0;
	
        •••
  • 부모의 CPU 문맥, 즉 인터럽트 프레임 값을 복사하고 RAX에 0을 넣는다.
    • 자식 프로세스는 RAX의 값을 0으로, 부모 프로세스는 후에 return tid;로 RAX의 값을 자식의 PID로 설정한다.
     •••
	int cnt = 2;
	struct file **table = parent->fdt;
	while (cnt < 128) {
		if (table[cnt]) {
			current->fdt[cnt] = file_duplicate(table[cnt]);
		} else {
			current->fdt[cnt] = NULL;
		}
		cnt++;
	}
	current->next_fd = parent->next_fd;

 	sema_up(&parent->sema_fork);

  	process_init ();
	/* Finally, switch to the newly created process. */
	if (succ)
		do_iret (&if_);
error:
	sema_up(&parent->sema_fork);
	exit(TID_ERROR);
}  
  • 파일을 복사한다.

    • 부모 프로세스의 FDT를 하나하나 훑으면서 자신의 FDT로 파일을 복사해간다. fdt와 그에 매핑되는 파일도 모조리 똑같이 복사한다.
  • 자식이 fork가 끝날 때까지 잠들어있던 부모 프로세스를 깨운다.

  • if_의 값들을 모두 레지스터에 넣음으로서 자식 프로세스를 실행시킨다.

userprog/process.c

static bool
duplicate_pte (uint64_t *pte, void *va, void *aux) {
	struct thread *current = thread_current ();
	struct thread *parent = (struct thread *) aux;
	void *parent_page;
	void *newpage;
	bool writable;

	/* 1. TODO: If the parent_page is kernel page, then return immediately. */
	if (is_kernel_vaddr(va))
		return true;

	/* 2. Resolve VA from the parent's page map level 4. */
	parent_page = pml4_get_page (parent->pml4, va);
	if (parent_page == NULL)
		return false;

	/* 3. TODO: Allocate new PAL_USER page for the child and set result to
	 *    TODO: NEWPAGE. */
	newpage = palloc_get_page(PAL_USER);
	if (newpage == NULL)
		return false;  

	/* 4. TODO: Duplicate parent's page to the new page and
	 *    TODO: check whether parent's page is writable or not (set WRITABLE
	 *    TODO: according to the result). */
	memcpy(newpage, parent_page, PGSIZE);
	writable = is_writable(pte);

	/* 5. Add new page to child's page table at address VA with WRITABLE
	 *    permission. */
	if (!pml4_set_page (current->pml4, va, newpage, writable)) {
		/* 6. TODO: if fail to insert page, do error handling. */
    	return false;
	}
	return true;
}
  • 자식 프로세스는 부모 프로세스와 별도의 물리 메모리 공간을 가진다. 그 대신, 부모와 자식 프로세스의 가상 메모리 주소가 같으면 같은 물리 메모리 정보에 매핑된다.

결과


0개의 댓글