일반적으로 유저 프로그램이 실행하기 전에, 커널은 레지스터에 맨 처음 function의 argument를 저장 해야한다. process_exec()은 유저가 입력한 명령어를 수행할 수 있도록, 프로그램을 메모리에 적재하고 실행하는 함수이다. Pintos는 f_name에 문자열로 이름이 저장되어있으나, 새로운 프로세스에 대한 인자 passing을 제공하지 않습니다. 이 부분을 구현하는 것이 목표이다.
int
process_exec (void *f_name) { // start_process 함수
char *file_name = f_name; // f_name를 (void *)로 넘겨받았고, 해당 부분을 문자열로 인식하기 위해서 char * 로 변환함
bool success;
/* ----------- Project2 ----------- */
char *fn_copy[128]; // 스택에 저장, 다른 함수에서 원본 문자열을 사용할 수 있기 때문에 복사본을 이용하여 파싱함
memcpy(fn_copy, file_name, strlen(file_name) + 1); // 문자열에는 \0이 들어가는데 strlen에서는 \0 앞까지만 읽고 끝냄, 그래서 전체를 들고오기 위해 + 1
/* ----------- Project2 ----------- */
// 아래 intr_frame 기존에 스택에 있던 다른 쓰레기 값들이 들어 있을 수 있어서 초기화
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 (); // 현재 프로세스에 할당된 page directory를 지워준다.
/* ----------- Project2 ----------- */
/* And then load the binary */
success = load (fn_copy, &_if); // file_name의 _if를 현재 프로세스에 load
/* ----------- Project2 ----------- */
/* If load failed, quit. */
palloc_free_page (file_name);
if (!success)
return -1;
/* ----------- Project2 ----------- */
hex_dump(_if.rsp, _if.rsp, USER_STACK - _if.rsp, true);
/* ----------- Project2 ----------- */
/* Start switched process. */
do_iret (&_if);
NOT_REACHED ();
}
현재 실행 중인 스레드의 context를 f_name에 해당하는 명령을 실행
하기 위해
context switching
하는 역할, 입력해주는 명령을 실행하기전에 실행중인 스레드가 돌고 있을 수 있다.
그래서 process_exec()에 context switching 역할이 있어야 한다.
fn_copy
라는 복사한 문자열을 담을 배열을 만들었다.다른 함수에서 원본 문자열을 사용할 수 있기 때문
에 복사본을 사용하여 파싱한 것이다.strlen(file_name) + 1
과 같이 +1을 해주었다.문자열이 끝났다는 것을 표시
하기 위해 \0(sentinel)
이 들어가는데 strlen()함수
내부를 보면 for문을 돌면서 한 글자씩 count하다가 NULL을 만나는 순간 for문을 종료
한다. 그래서 원본 문자열과 동일한 크기로 복사
할 수 있도록 하기위해 + 1을 해준 것이다.intr_frame
은 인터럽트와 같은 요청이 들어와서기존까지 실행 중이던 context
(레지스터 값 포함)를 스택에 저장
하기 위한 구조체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;
/* ----------- Project2 ----------- */
char *save_file, *token;
char *arg_list[128];
int cnt = 0;
token = strtok_r(file_name, " ", &save_file); // 첫번째 이름
while(token != NULL) {
arg_list[cnt++] = token; // arg_list 배열에 인자들 추가
token = strtok_r(NULL, " ", &save_file);
}
/* ----------- Project2 ----------- */
...
/* Set up stack. */
if (!setup_stack (if_))
goto done;
/* Start address. */
if_->rip = ehdr.e_entry;
/* ----------- Project2 ----------- */
argument_stack(arg_list, cnt, if_);
/* ----------- Project2 ----------- */
success = true;
...
load()는 실행파일의 file name을 적재해서 실행하는 함수이다.
load()를 인자는 현재 process_exec()에서 입력한 커맨드(f_name의 복사본)가 file_name 인자로 넘어온다.
arg_list[0]
에 저장하고argument_stack()
도 추가해준다유저 프로그램은 %rdi
, %rsi
, %rdx
, %rcx
, %r8
, %r9
순서를 전달하기 위한 정수 레지스터를 사용
합니다.
예를들어 argument_stack을 사용
하여 /bin/ls -l foo bar
를 커맨드라인의 입력값을 넘겨준다면 아래의 Gitbook의 내용과 같이 스택에 올려주게 됩니다. 이것을 구현하는 것입니다.
/* ----------- Project2 ----------- */
void argument_stack(char **arg_list, int cnt, struct intr_frame *if_) {
char *arg_address[128];
/* 맨 끝을 알려주기 위한 arg의 마지막 요소인 NULL 값을 제외하고 스택에 저장 */
for (int i = cnt-1; i >= 0; i--) {
int argv_len = strlen(arg_list[i]);
// if_->rsp 는 현재 user_stack에서 현재 위치를 가리키는 stack pointer
if_->rsp = if_->rsp - (argv_len + 1); // 문자열크기 + sentinel(1) 값까지 포함하여 공간만큼 rsp를 내려주고
memcpy(if_->rsp, arg_list[i], argv_len + 1); // 인자로 받은 arg_list의 값을 하나씩 스택에 추가
arg_address[i] = if_->rsp; // arg_address 배열에 저장한 값들의 문자열 시작 주소를 저장
}
// 성능을 위해 스택포인터가 8의 배수 될때까지 rsp를 1씩 내려서 나머지 영역을 padding
while (if_->rsp % 8 != 0) {
if_->rsp--;
*(uint8_t *)if_->rsp = 0;
}
// arg_address[] 배열을 따로 만들어 주소값을 저장했던 것을 활용해서 주소값 자체를 삽입
for (int i = cnt; i >= 0; i--) {
if_->rsp = if_->rsp - 8;
if (i == cnt) { // arg_address에 담긴 주소의 마지막을 표시하고, 다른 프로세스가 해당 메모리를 전에 사용했을 수도 있기 때문에 0으로 초기화
memset(if_->rsp, 0, sizeof(char **));
} else { // stack에 arg_address의 값을 저장
memcpy(if_->rsp, &arg_address[i], sizeof(char **));
}
}
if_->rsp = if_->rsp - 8; // rsp를 fake address까지 이동시키고
memset(if_->rsp, 0, sizeof(void *)); // return address에 0으로 초기화
if_->R.rdi = cnt;
if_->R.rsi = if_->rsp + 8; // arg_address의 맨처음 가리키는 주소값, fake address의 바로 위
}
/* ----------- Project2 ----------- */
argument_stack()
에 들어가는 인자값을 스택에 올려주는 함수
위에서 parsing한 다음 한 문자씩 넣어준 배열 arg_list
와 배열내에 담긴 갯수인 cnt
, intr_frame
을 인자로 넣는다.
이 함수 자체에서 intr_frame을 스택에 올리는 것은 아니고, 인터럽트 프레임 내 구조체 중 특정값(rsp)에 인자를 넣어주기 위함이다. 이후에 do_iret()에서 이 인터럽트 프레임을 스택에 올린다.
배열 argaddress[128]은 아래 for문에서 스택에 담을텍스트 각 인자의 주소값을 저장하는 배열
for문을 돌면서 process_exec()에서 넣어주는 arg_list
로부터 값을 하나씩 꺼내서 if->rsp에 삽입,
이때 if_->rsp
는 user stack에서 현재 위치
를 가리키는 스택 포인터
이자 intr_frame 내의 멤버
이다.
각 인자에서 인자 크기(argv_len)을 읽는데 이때 각 인자마다는 문자열이라 끝에 \0
(sentinel)이 포함되어 있다. strlen은 sentinel을 읽지 않아서 +1을 추가해줘야 한다.
첫번째 for문이 동작하는 방식
1. 먼저 스택 포인터를 넣어줄 공간만큼 쭉 내린다.(if->rsp = if->rsp - (argvlen +1)
2. 그다음, 해당 공간에 인자값을 복붙한다(memcpy(if->rsp, argv[i], argvlen+1))
3. arg_address 배열에 인자값이 위치한 주소를 저장한다. (arg_address[i] = if->rsp)
while문을 돌면서 패딩을 만들어준다
8배수 정렬이 되면 정렬되지 않은 것보다 속도가 빠르기 때문에 최상의 성능을 위해 스택 포인터를 첫 번째 푸시 전에 8의 배수로 반올림 해줍니다.
성능을 위해 스택포인터가 8의 배수 될때까지 rsp를 1씩 내려서 나머지 영역을 padding 해준다.
두번째 for문이 동작하는 방식
위에 arg_address[]
배열을 따로 만들어 주소값을 저장했던 것을 활용해서 주소값 자체를 삽입
64bit의 주소값
은 8byte
이기 때문에 rsp - 8
하여 그 공간에 값을 넣어준다.
for문에서는(int i = cnt-1)로 선언된 반면 여기 for문에서는 (int i = cnt)로 선언된다.
이는 앞서 process_exec()에서 while문을 돌며 token을 받아오는 것을 보면,
맨 마지막 인자값으로 NULL을 받아 arg_list에 저장하게 된다. 따라서 argv[-1]= \0
이 된다.
근데 이 인자 값을 스택에 저장할 때는 맨 끝에 있는 NULL 값을 저장하지 않은 반면
여기서는 NULL값 가리키는 포인터를 저장
한다.
처음 if문을 통과해서 arg_address에 담긴 주소의 마지막을 표시하고, 다른 프로세스가 해당 메모리를 전에 사용했을 수도 있기 때문에 0으로 초기화해주고
두번째부터는 인자값에 대한 주소를 스택포인터가 낮은 순서대로 차례대로 저장해준다
for문을 돌고 나면 fake address를 넣어준다.
rsp
를 fake address까지 이동시키고 메모리는 0으로 초기화
해준다.
마지막으로 intr_frame if_
의 멤버로 있는 레지스터 구조체
의 rdi
에 인자 count값인 cnt
를 넣고 rsi
에는 rsp + 8byte
크기만큼 스택포인터를 증가시켜 arg_address
의 맨 앞의 주소값
을 넣는다.
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. */
/* ----------- Project2 ----------- */
while (1){} // 자식 프로세스 종료될 때까지 무한 대기
/* ----------- Project2 ----------- */
return -1;
}
프로세스 생성 함수를 자세히 뜯어보면 process_wait
에 종료 구현
이 안 되어있는 부분이 있다. Hint라고 친절하게 적어뒀는데 이 부분을 수정하지 않아서 테스트 케이스를 통과하는데 꽤나 애를 먹었다. 핀토스는 유저 프로세스를 생성한 후 프로세스 종료를 대기해야 하는데 자식 프로세스가 종료될 때까지 무한 대기한다. 그래서 while (1){}
추가가 필요하다.
#ifndef USERPROG_PROCESS_H
#define USERPROG_PROCESS_H
...
void process_exit (void);
void process_activate (struct thread *next);
/* ----------- Project2 ----------- */
void argument_stack(char **arg_list, int cnt, struct intr_frame *if_);
/* ----------- Project2 ----------- */
#endif /* userprog/process.h */
process.c에서 사용하기 위해 추가되는 함수를 헤더에 선언
all: os.dsk
disk:
pintos-mkdisk filesys.dsk 10
pintos --fs-disk filesys.dsk -p tests/userprog/args-single:args-single -- -q -f run 'args-single onearg'
include ../../Make.config
include ../Make.vars
include ../../tests/Make.tests
$(Make) -C build disk: 이건 현재 위치에서 build 디렉터리로 이동해서 Makefile로 이동 disk명령을 수행하라
include ../Makefile.kernel
disk:
$(MAKE) -C build disk
/userprog 의 경로
에서 아래의 순서로 실행합니다.
1. make
2. make disk
system call을 호출하면서 무한대기
정리 잘 하셨네요!!
덕분에 큰 도움이 되었습니다 :)