정글사관학교 개발일지/운영체제-PintOS

PintOS Project 2 - User Program (2) Argument Passing(정글사관학교 63일차 TIL)

Woonys 2022. 1. 3. 00:43
반응형

1. 과제 목표: Command Line Parsing

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

 

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

 

ex) process_exec("grep foo bar"): process_exec()에서 두 인자 foo, bar로 parsing되어야 한다.

 

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

 

2. 과제 구현

먼저 process.c 내에 있는 process_exec()원본 코드부터 보자.

 

@/userprog/process.c

/* Switch the current execution context to the f_name.
 * Returns -1 on fail. */
int
process_exec (void *f_name) { // 유저가 입력한 명령어를 수행하도록 프로그램을 메모리에 적재하고 실행하는 함수. 여기에 파일 네임 인자로 받아서 저장(문자열) => 근데 실행 프로그램 파일과 옵션이 분리되지 않은 상황.
	char *file_name = f_name; // f_name은 문자열인데 위에서 (void *)로 넘겨받음! -> 문자열로 인식하기 위해서 char * 로 변환해줘야.

	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; // intr_frame 내 구조체 멤버에 필요한 정보를 담는다.
	_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 ();
	// 새로운 실행 파일을 현재 스레드에 담기 전에 먼저 현재 process에 담긴 context를 지워준다.
	// 지운다? => 현재 프로세스에 할당된 page directory를 지운다는 뜻.

	/* And then load the binary */
	success = load (file_name, &_if); // file_name, _if를 현재 프로세스에 load.
	// success는 bool type이니까 load에 성공하면 1, 실패하면 0 반환.
	// 이때 file_name: f_name의 첫 문자열을 parsing하여 넘겨줘야 한다!

	/* If load failed, quit. */
	palloc_free_page (file_name); // file_name: 프로그램 파일 받기 위해 만든 임시변수. 따라서 load 끝나면 메모리 반환.
	if (!success)
		return -1;

	/* Start switched process. */
	do_iret (&_if);
	NOT_REACHED ();
}

 

char *file_name = f_name; 위에서 인자로 받은 문자열은 void 형인데 우리가 받은 명령어는 문자열이니 문자열 포인터로 형을 변경한다.
 
struct intr_frame _if; intr_frame 내 구조체 멤버에 필요한 정보를 담는다. 여기서 intr_frame은 인터럽트 스택 프레임이다. 
 
process_cleanup (); 새로운 실행 파일을 현재 스레드에 담기 전에 먼저 현재 process에 담긴 context를 지워준다. 이때 지운다는 말은 현재 프로세스에 할당된 page directory를 지운다는 뜻.
 
success = load (file_name, &_if); file_name, _if를 현재 프로세스에 적재(load)한다. success는 bool type이니 적재에 성공하면 1, 실패하면 0을 반환한다.
 
palloc_free_page (file_name); file_name은 프로그램 파일을 받기 위해 만든 임시 변수이다. load가 끝나면 메모리를 반환한다.
 
* 어라, 근데 page를 할당해준 적이 없는데 왜 free를 하는 거지? => palloc()은 load() 함수 내에서 file_name을 메모리에 올리는 과정에서 page allocation을 해준다. 이때, 페이지를 할당해주는 걸 임시로 해주는 것.
 
do_iret (&_if); 만약 load가 실행됐다면 context switching을 진행한다.
 
 
 
현재 process_exec() 코드에는 parsing 기능이 구현되어 있지 않다. 이 부분을 추가하도록 하자.
 
먼저, 위 코드에서 인자로 받아오는 f_name 원본은 다른 곳에서도 작업할 여지가 있으니, 이를 똑같이 복사해서 새로운 변수에 저장하도록 하자.
 
 
process_exec()
@/userprog/process.c

int
process_exec (void *f_name) { // 유저가 입력한 명령어를 수행하도록 프로그램을 메모리에 적재하고 실행하는 함수. 여기에 파일 네임 인자로 받아서 저장(문자열) => 근데 실행 프로그램 파일과 옵션이 분리되지 않은 상황.
	char *file_name = f_name; // f_name은 문자열인데 위에서 (void *)로 넘겨받음! -> 문자열로 인식하기 위해서 char * 로 변환해줘야.
	bool success;
	/* --- Project 2: Command_line_parsing ---*/
	/* 원본 file name을 copy해오기 */
	char file_name_copy[128]; // 스택에 저장
	// file_name_copy = palloc_get_page(PAL_USER); // 이렇게는 가능 but 비효율적.
	memcpy(file_name_copy, file_name, strlen(file_name)+1); // strlen에 +1? => 원래 문자열에는 \n이 들어가는데 strlen에서는 \n 앞까지만 읽고 끝내기 때문. 전체를 들고오기 위해 +1
	/* --- Project 2: Command_line_parsing ---*/
    
    ...
    
    /* And then load the binary */
	success = load (file_name_copy, &_if); // file_name, _if를 현재 프로세스에 load.
   }
 
 

위 코드를 보면, file_name을 복사해온다. 원본 문자열을 parsing하면 다른 함수에서 원본 문자열을 쓸 여지가 있으니 따로 복사본을 만들어준다. 이때, 복사한 문자열을 담을 file_name_copy 배열은 128자로 설정했다. Gitbook을 읽으면 핀토스가 커널에 통과시킬 수 있는 커맨드 라인의 인자 길이 제한은 128바이트라고 하여서 이렇게 적긴 했는데, 64로 해도 통과하고 현재 테스트 케이스에만 한정하면(args-single) 그보다 더 적은 길이로도 가능하다.

 

이를 동적으로 받아오면 더 좋지 않을까 싶어 malloc을 사용할까 했는데, 어차피 임시적으로 process_exec() 안에서만 사용할 것이다보니 굳이 스택이 아닌 힙에다가 저장할 이유도 없을 뿐더러 문자열 size만큼 받아오도록 해보니 그것도 되지 않더라. 위처럼 palloc을 쓰는 식으로 정적 변수를 이용해 힙에 할당해주면 가능. 하지만 임시 값으로 쓰는 것이기에 굳이 이렇게 할 이유가 없다. 따라서 위와 같이 128자 길이로 file_name_copy를 만들어주고 거기에 memcpy로 file_name 문자열을 저장한다. (참고로 malloc에서는 할당하려는 인자의 크기가 크면 palloc으로 전환한다)

 

이때, strlen+1을 하는 이유는 무엇일까? 원래 문자열 file_name에는 이 문자열이 끝났다는 것을 읽기 위해 문자열 끝에 '\n'이 들어간다.(이를 sentinel이라고 한다.) 그런데 strlen() 함수를 보면, for문을 돌면서 한글자씩 count하다가 NULL을 만나는 순간 for문을 종료한다. 즉, \n 까지 count하지 않는 것. 원본 문자열의 길이는 이 센티넬까지 포함이므로 한 글자를 더 더해서 NULL까지 복사할 수 있도록 해주는 작업이다.

 

이어서 load() 함수가 나올텐데, 그전에 원본 코드에서 보면 나오는 intr_frame 구조체를 선언하는 내용이 있다. 이 구조체를 선언하는 시점에서 기존에 스택에 있던 다른 쓰레기 값들이 들어있을 수 있으니, 여기를 초기화해준다. 이 intr_frame을 조금 더 살펴보자.

 

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(레지스터 값 포함)를 스택에 저장하기 위한 구조체이다!(1주차에 답답했던 고민이 하나 쓱 풀리는 기분! 짱이다!) 아니, 근데 지금 실행하는 건 인자를 parsing해서 명령어를 실행하는 건데 왜 인터럽트가 나오지? 싶을텐데, 이를 실행하는 함수 process_exec()의 설명을 다시 보자.

 

Switch the current execution context to the f_name.

현재 실행 중인 스레드의 context를 f_name에 해당하는 명령을 실행하기 위해 context switching하는 것이 process_exec()의 역할이다. 즉, 우리가 입력해주는 명령을 받기 직전에 어떤 스레드가 돌고 있었을 테니(그게 idle이든 실제로 실행 중이던) process_exec()에 context switching 역할도 같이 넣어줘야 한다. 

 

이제 parsing 작업을 해주는 코드를 load() 안에서 추가한다.

 

 

load() - Parsing 코드 추가

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;

	/* --- Project 2: Command_line_parsing ---*/
	char *arg_list[128];
	char *token, *save_ptr;
	int token_count = 0;
 
	token = strtok_r(file_name, " ", &save_ptr); // 첫번째 이름
	//token = strtok_r(file_name_total, " ", &save_ptr); // 첫번째 이름을 받아온다. save_ptr: 앞에 애 자르고 남은 문자열의 가장 맨 앞을 가리키는 포인터 주소값!
	arg_list[token_count] = token; //arg_list[0] = file_name_first
	
	while (token != NULL) {
		token = strtok_r (NULL, " ", &save_ptr);
		token_count++;
		arg_list[token_count] = token;
	}
	/* --- Project 2: Command_line_parsing ---*/

 

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

 

위에서 새롭게 선언해주는 변수 token과 save_ptr은 문자열을 자르는 함수 strtok_r()에서 쓰기 위한 변수이다. strtok_r()을 먼저 살펴보자. strtok_r 함수는 지정된 문자(이를 delimiters라고 한다)를 기준으로 문자열을 자른다.

 

strtok_r()

@/lib/string.c

char * strtok_r (char *s, const char *delimiters, char **save_ptr) {
	char *token;

	ASSERT (delimiters != NULL);
	ASSERT (save_ptr != NULL);

	/* If S is nonnull, start from it.
	   If S is null, start from saved position. */
	if (s == NULL)
		s = *save_ptr;
	ASSERT (s != NULL);

	/* Skip any DELIMITERS at our current position. */
	while (strchr (delimiters, *s) != NULL) {
		/* strchr() will always return nonnull if we're searching
		   for a null byte, because every string contains a null
		   byte (at the end). */
		if (*s == '\0') {
			*save_ptr = s;
			return NULL;
		}

		s++;
	}

	/* Skip any non-DELIMITERS up to the end of the string. */
	token = s;
	while (strchr (delimiters, *s) == NULL)
		s++;
	if (*s != '\0') {
		*s = '\0';
		*save_ptr = s + 1;
	} else
		*save_ptr = s;
	return token;
}

 

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

 

(*링크에서 소개하는 함수는 strtok인데, strtok_r과의 차이는 strtok는 한글자를 자르고 뒤에 남은 문자열을 지역변수를 저장하는 stack에 넣는다. 그런데 이렇게 되면 멀티 스레드 환경에서 오류가 뜰 위험이 생긴다. 따라서 strtok_r은 전역 변수를 선언하고 거기에 저장하는 식이다.) 

#include <stdio.h>
#include <string.h>    // strtok 함수가 선언된 헤더 파일

char *ptr, *the_rest
int main()
{
    char s1[30] = "The Little Prince";  // 크기가 30인 char형 배열을 선언하고 문자열 할당
    char *ptr = strtok_r(s1, " ", &the_rest);      // " " 공백 문자를 기준으로 문자열을 자름, 포인터 반환
    while (ptr != NULL)               // 자른 문자열이 나오지 않을 때까지 반복
    {
        printf("%s\n", ptr);          // 자른 문자열 출력
        ptr = strtok_r(NULL, " ", &the_rest);      // 다음 문자열을 잘라서 포인터를 반환
    }

    return 0;
}
 

 

 

여기까지 마치면 arg_list에 공백을 기준으로 자른 값이 하나씩 들어간다. 다시 load()로 돌아오자.

 

@/userprog/process.c

static bool
load (const char *file_name, struct intr_frame *if_) {

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


	/* --- Project 2: Command_line_parsing ---*/
	argument_stack(arg_list, token_count, if_);
	/* --- Project 2: Command_line_parsing ---*/



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

/* --- Project 2: Command_line_parsing ---*/
/* 인자를 stack에 올린다. */
void argument_stack(char **argv, int argc, struct intr_frame *if_) { // if_는 인터럽트 스택 프레임 => 여기에다가 쌓는다.

	/* insert arguments' address */
	char *arg_address[128];

	// 거꾸로 삽입 => 스택은 반대 방향으로 확장하기 떄문!
	
	/* 맨 끝 NULL 값(arg[4]) 제외하고 스택에 저장(arg[0] ~ arg[3]) */
	for (int i = argc-1; i>=0; i--) { 
		int argv_len = strlen(argv[i]);
		/* 
		if_->rsp: 현재 user stack에서 현재 위치를 가리키는 스택 포인터.
		각 인자에서 인자 크기(argv_len)를 읽고 (이때 각 인자에 sentinel이 포함되어 있으니 +1 - strlen에서는 sentinel 빼고 읽음)
		그 크기만큼 rsp를 내려준다. 그 다음 빈 공간만큼 memcpy를 해준다.
		 */
		if_->rsp = if_->rsp - (argv_len + 1);
		memcpy(if_->rsp, argv[i], argv_len+1);
		arg_address[i] = if_->rsp; // arg_address 배열에 현재 문자열 시작 주소 위치를 저장한다.
	}

	/* word-align: 8의 배수 맞추기 위해 padding 삽입*/
	while (if_->rsp % 8 != 0) 
	{
		if_->rsp--; // 주소값을 1 내리고
		*(uint8_t *) if_->rsp = 0; //데이터에 0 삽입 => 8바이트 저장
	}

	/* 이제는 주소값 자체를 삽입! 이때 센티넬 포함해서 넣기*/
	
	for (int i = argc; i >=0; i--) 
	{ // 여기서는 NULL 값 포인터도 같이 넣는다.
		if_->rsp = if_->rsp - 8; // 8바이트만큼 내리고
		if (i == argc) { // 가장 위에는 NULL이 아닌 0을 넣어야지
			memset(if_->rsp, 0, sizeof(char **));
		} else { // 나머지에는 arg_address 안에 들어있는 값 가져오기
			memcpy(if_->rsp, &arg_address[i], sizeof(char **)); // char 포인터 크기: 8바이트
		}	
	}
	

	/* fake return address */
	if_->rsp = if_->rsp - 8; // void 포인터도 8바이트 크기
	memset(if_->rsp, 0, sizeof(void *));

	if_->R.rdi  = argc;
	if_->R.rsi = if_->rsp + 8; // fake_address 바로 위: arg_address 맨 앞 가리키는 주소값!
}

 

argument_stack() 코드를 뜯어보자. 배열 arg_address[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값 가리키는 포인터를 저장한다. (정확히 왜 여기서는 NULL 포인터를 저장하고 위는 저장하지 않는지는 좀더 파봐야 알듯..)

 

무튼 for문을 돌고 나면  fake address를 넣어준다. 해당 영역의 메모리는 0으로 초기화해준다.

 

마지막으로 인터럽트 프레임 if_의 멤버로 있는 레지스터 구조체의 rdi에 인자 count값인 argc, 그리고 rsi에는 fake address바로 위인 arg_address의 맨 앞을 가리키는 주소값을 넣는다.

 

기타 함수 수정

 

거의다 끝났는데, 테스트 케이스를 돌리기 위해 몇가지 기타 함수 수정을 추가해준다.

 

hex_dump(): 디버깅용 툴

테스트 케이스에서 parsing이 제대로 동작하는지 확인하려면 hex_dump를 마지막 부근에 추가해줘야 한다. 흐름을 이해하기 좋도록 위에 추가로 코드를 넣어놨다. 위에서 설명했던 argument_stack()도 아래쪽에 선언해준다.

 

@/userprog/process.c
int
process_exec (void *f_name) {

	...
    
	/* 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; // intr_frame 내 구조체 멤버에 필요한 정보를 담는다.
	_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 ();
	// 새로운 실행 파일을 현재 스레드에 담기 전에 먼저 현재 process에 담긴 context를 지워준다.
	// 지운다? => 현재 프로세스에 할당된 page directory를 지운다는 뜻.

	
	/* --- Project 2: Command_line_parsing ---*/
	memset(&_if, 0, sizeof _if);
	/* --- Project 2: Command_line_parsing ---*/


	/* And then load the binary */
	success = load (file_name_first, &_if); // file_name, _if를 현재 프로세스에 load.
	// success는 bool type이니까 load에 성공하면 1, 실패하면 0 반환.
	// 이때 file_name: f_name의 첫 문자열을 parsing하여 넘겨줘야 한다!
	// _if: context switching에 필요한 정보.
	
	/* If load failed, quit. */
	if (!success)
	{
		return -1;
	}

	hex_dump(_if.rsp, _if.rsp, KERN_BASE - _if.rsp, true);
	/* --- Project 2: Command_line_parsing ---*/
	//palloc_free_page (file_name); // file_name: 프로그램 파일 받기 위해 만든 임시변수. 따라서 load 끝나면 메모리 반환.
	/* Start switched process. */
	
	do_iret (&_if);
	NOT_REACHED ();
}

 

 

 

process_wait(): 무한 루프 추가

@/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. */
	
	/* --- Project 2: Command_line_parsing ---*/
	while (1){}
	/* --- Project 2: Command_line_parsing ---*/
	return -1;
}

힌트라고 적혀있듯, 무한 루프를 돌아야 한다. 핀토스는 유저 프로세스를 생성한 후 프로세스 종료를 대기해야 하는데 자식 프로세스가 종료될 때까지 무한 대기한다.

 

 

 

process.h: 선언하는 것 잊지 말기!

@/include/userprog/process.h

#ifndef USERPROG_PROCESS_H
#define USERPROG_PROCESS_H

#include "threads/thread.h"

tid_t process_create_initd (const char *file_name);
tid_t process_fork (const char *name, struct intr_frame *if_);
int process_exec (void *f_name);
int process_wait (tid_t);
void process_exit (void);
void process_activate (struct thread *next);
/* --- project 2: argument passing --- */
void argument_stack(char **argv, int argc, struct intr_frame *if_);
/* --- project 2: argument passing --- */
#endif /* userprog/process.h */

 

 

결과

 

1. 먼저 입력한 값인 args-single onearg를 실행한다. 아래와 같이 문자가 parsing되는 것을 볼 수 있다.

2. 이어서 system call을 호출하면서 무한대기한다. 이렇게 되면 성공! 혹시 page fault가 뜬다면 잘못한 것이다..!

반응형