
정신없다.
내가 CS를 하는 건지, 구현을 하는 건지, 노션을 하는 건지, 코드를 읽는 건지
EOY로 0 리턴이다. End Of Yuljeong이다.
핀토스를 할 때 조심해야할 게 있다.
배열 사이즈를 정할 때, 크기를 잘 정해야 한다.
나는 항상 메모리를 넉넉하게 쓴다.
백준할 때부터 지켜왔던 유구한 전통이다.
그래서 이번에도 인심을 좀 후하게 썼다.
기왕 받을거 한 페이지씩 받을 수 있도록 만들었다.

그랬더니 레지스터가 무너졌다.
할당받았는데 내가 후하게 받았어
컴파일러가 한순간에 돌변하더라
너무 힘들어
보고싶다 백준 메모리

디버깅하는 것도 좀 슴슴하니까
system call!도 커스텀 좀 해봤다.
system wow call!이라고 바꿨다.
유저 프로세스가 짬처리할 생각에 신나서 시스템 콜을 부르는 거 아닐까
...
위 사진은 아르그크 싱글을 구현할 때 하던 디버깅인데
문제점을 찾고있다...
어떻게 해결은 했다.
구현을 하기 전, 프로그램이 제대로 돌아가는지 보려면
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. */
while(1){
}
return -1;
}
위와 같이 바꿔야한다고 한다.
pass tests/userprog/args-none
pass tests/userprog/args-single
pass tests/userprog/args-multiple
pass tests/userprog/args-many
pass tests/userprog/args-dbl-space
pass tests/userprog/halt
pass tests/userprog/exit
pass tests/userprog/create-normal
pass tests/userprog/create-empty
pass tests/userprog/create-null
pass tests/userprog/create-bad-ptr
pass tests/userprog/create-long
pass tests/userprog/create-exists
pass tests/userprog/create-bound
pass tests/userprog/open-normal
pass tests/userprog/open-missing
pass tests/userprog/open-boundary
pass tests/userprog/open-empty
pass tests/userprog/open-null
pass tests/userprog/open-bad-ptr
pass tests/userprog/open-twice
pass tests/userprog/close-normal
pass tests/userprog/close-twice
pass tests/userprog/close-bad-fd
pass tests/userprog/read-normal
pass tests/userprog/read-bad-ptr
pass tests/userprog/read-boundary
pass tests/userprog/read-zero
pass tests/userprog/read-stdout
pass tests/userprog/read-bad-fd
pass tests/userprog/write-normal
pass tests/userprog/write-bad-ptr
pass tests/userprog/write-boundary
pass tests/userprog/write-zero
pass tests/userprog/write-stdin
pass tests/userprog/write-bad-fd
pass tests/userprog/fork-once
pass tests/userprog/fork-multiple
pass tests/userprog/fork-recursive
pass tests/userprog/fork-read
pass tests/userprog/fork-close
pass tests/userprog/fork-boundary
pass tests/userprog/exec-once
pass tests/userprog/exec-arg
pass tests/userprog/exec-boundary
pass tests/userprog/exec-missing
pass tests/userprog/exec-bad-ptr
pass tests/userprog/exec-read
pass tests/userprog/wait-simple
pass tests/userprog/wait-twice
pass tests/userprog/wait-killed
pass tests/userprog/wait-bad-pid
pass tests/userprog/multi-recurse
pass tests/userprog/multi-child-fd
pass tests/userprog/rox-simple
pass tests/userprog/rox-child
pass tests/userprog/rox-multichild
pass tests/userprog/bad-read
pass tests/userprog/bad-write
pass tests/userprog/bad-read2
pass tests/userprog/bad-write2
pass tests/userprog/bad-jump
pass tests/userprog/bad-jump2
pass tests/filesys/base/lg-create
pass tests/filesys/base/lg-full
pass tests/filesys/base/lg-random
pass tests/filesys/base/lg-seq-block
pass tests/filesys/base/lg-seq-random
pass tests/filesys/base/sm-create
pass tests/filesys/base/sm-full
pass tests/filesys/base/sm-random
pass tests/filesys/base/sm-seq-block
pass tests/filesys/base/sm-seq-random
pass tests/filesys/base/syn-read
pass tests/filesys/base/syn-remove
pass tests/filesys/base/syn-write
pass tests/userprog/no-vm/multi-oom
pass tests/threads/alarm-single
pass tests/threads/alarm-multiple
pass tests/threads/alarm-simultaneous
pass tests/threads/alarm-priority
pass tests/threads/alarm-zero
pass tests/threads/alarm-negative
FAIL tests/threads/priority-change
FAIL tests/threads/priority-donate-one
FAIL tests/threads/priority-donate-multiple
FAIL tests/threads/priority-donate-multiple2
FAIL tests/threads/priority-donate-nest
FAIL tests/threads/priority-donate-sema
FAIL tests/threads/priority-donate-lower
FAIL tests/threads/priority-fifo
pass tests/threads/priority-preempt
FAIL tests/threads/priority-sema
FAIL tests/threads/priority-condvar
FAIL tests/threads/priority-donate-chain
프로젝트 2... 패...스!
신뢰도를 위해 마패부터 등장시켰다.
process.c에서 load 함수를 손보고 있다.
유저 스택에 아르그브와 이를 가리키는 주소를 야무지게 넣어야한다.
커널 영역
------
유저 영역
이렇게 있으면
유저 영역에서 유저 스택 영역에 아르그브를
------
(유저 스택 영역)
argv[0]
argv[1]
padding
null
argv[1] 주소
argv[0] 주소
fake 주소
이렇게 반듯하게 넣어줘야한다.
그래야 나중에 메인 함수가 시작할 때
------
(유저 스택 영역)
argv[0]
argv[1]
padding
null
argv[1] 주소
argv[0] 주소
fake 주소
(메인 함수 시작)
이렇게 야무지게 들어간다고 한다.
그래서 진짜 최대한 혼자 구현하려 노력해봤다.
그러나 아직 레지스터나 형변환에 대한 이해가 부족한 것 같다.
다시 말해 그 부분은 이래저래 도움을 받았다.
/* Start address. */
if_->rip = ehdr.e_entry;
char* stack_pointer = if_->rip;
pointer_addr[0] = NULL;
pointer_addr[argc + 1] = NULL;
char* argv = NULL;
msg("pointer: %p", stack_pointer);
msg("this");
for (int i = 0; i < argc; i++) {
stack_pointer -= 1;
*stack_pointer = '\0';
stack_pointer -= strlen(token_list[i]);
memcpy(stack_pointer, token_list[i], strlen(token_list[i]));
pointer_addr[i+1] = stack_pointer;
msg("token pointer: %p", stack_pointer);
}
int padding_size = (int)(stack_pointer)%8;
if (padding_size != 0) {
stack_pointer -= padding_size;
uint8_t padding[padding_size];
*stack_pointer = padding;
msg("padding pointer: %p", stack_pointer);
}
msg("argc: %d", argc);
for (int i = 0; i < argc + 2; i++) {
stack_pointer -= sizeof(char*);
*stack_pointer = pointer_addr[argc + 1 - i];
msg("argc pointer: %p", stack_pointer);
if (i == argc) {
msg("assign");
argv = stack_pointer;
}
}
msg("argv: %p", argv);
if_->R.rsi = argv;
if_->R.rdi = argc;
이런 식으로 만들긴 했는데, 잘 작동하지 않는 거보면 아쉬운 부분이 있는 것 같다.
그 부분을 다시 찾으러 갈 예정이다.
코딩에서 제일 어려운게 뭐냐고 물으면
나는 자신있게 형변환이라고 대답할 수 있다.
내가 봤을 때는 똑같이 생긴 놈인데 컴퓨터는 절대 아니라고 한다.
이상한 마술을 부릴 수밖에 없다. GPT가 대신 부려주는 거긴 하다.
아무튼 디버깅 끝에 아래와 같은 결과를 만날 수 있었다.
특별한 기술이 있는게 아니라 더 설명해주기가 어렵다.
그냥 빡구현과 C언어에 대한 지식이 필요한 것 같다.
성공은 성공인 것 같다.
hex_dump는 우리반 행님 덕분에 사용할 수 있었다.
다만 사용법에 대해서는 공부가 필요할 것 같다.
사실 프로젝트 2에서 두 번째 어려운 구현이라 생각한다.
제일 어려운 구현은 fork-exec-wait 종합세트이다.
원래 해당 부분에 대한 구현을 쓰려했으나 쓰지 않는 이유에 대해서는 아래에 서술하겠다.
다른 시스템콜을 구현하기 위해서는 syscall_handler 구현이 필수이다.
https://velog.io/@mogiyoon/CS-의문-정리와-고찰#전체-흐름은-어떻게-될까
여기서 2번 목록을 보면 시스템 콜 번호를 꺼내서 해석하는 건 syscall handler의 역할이기 때문이다.
구현해야하는 함수들은
include/lib/user/syscall.h에 있고
각 시스템 콜 번호에 대한 정보는
include/lib/syscall-nr.h에 잘 정리되어 있다.
따라서 syscall.c 아래에 함수들 잘 정리해놓고
레지스터에서 매개변수 잘 꺼내서
switch case문을 잘쓰면 되지 않을까 생각한다.
벨로그에 정리하고 싶었던 것이 무엇이었을까
살짝 고민을 하게 되었다.
시행착오를 했던 과정을 다 정리하고 싶긴하지만, 코드를 몇 번이나 갈아엎었는지 잘 모르겠다.
그래서 그 과정들을 모두 쓰기에는 시간과 노력이 너무 많이 들어가고..
그게 얼마나 의미가 있는지도 잘 모르겠다.
적어도 처음에 생각했던 방향성과 그게 틀린 이유 및 수정된 방향성 정도는 적을 예정이다.
그리고 테스트 케이스별로 정리를 할지 함수별로 정리를 할지 고민을 하다가 함수 위주로 정리를 하자는 생각이 들었다.
그래서 앞서 구현했던 argument passing에 대한 파트를 따로 적진 않을 예정이다.
그래도 argment passing과 write는 구현이 돼 있어야 본격적인 코딩이 가능하기 때문에 따로 명시해둘 예정이다.
char* call_name[] = {"halt\0", "exit\0", "fork\0", "exec\0", "wait\0", "create\0", "remove\0", "open\0", "file size\0", "read\0", "write\0", "seek\0", "tell\0", "close\0"};
디버깅용 전역 변수이다.
디버깅에 진심이기 때문에 이런 식으로 선언해두면 디버깅할 떄 눈에 잘 들어와서 편하다.
void
syscall_handler (struct intr_frame *f UNUSED) {
uint64_t arg0 = f->R.rax;
uint64_t arg1 = f->R.rdi;
uint64_t arg2 = f->R.rsi;
uint64_t arg3 = f->R.rdx;
uint64_t arg4 = f->R.r10;
uint64_t arg5 = f->R.r8;
uint64_t arg6 = f->R.r9;
uint64_t result = 0;
// printf("\nsys call = %s\n", call_name[arg0]);
// printf("sys cur name: %s\n", thread_current()->name);
// printf("sys cur pid: %d\n\n", thread_current()->tid);
// printf ("system wow call!\n");
switch (arg0)
{
case SYS_HALT:
halt();
break;
case SYS_EXIT:
exit((int)arg1);
break;
case SYS_FORK:
result = fork(f);
break;
case SYS_EXEC:
result = exec((char*)arg1);
break;
case SYS_WAIT:
result = wait((pid_t)arg1);
break;
case SYS_CREATE:
result = create((char*)arg1, (unsigned)arg2);
break;
case SYS_REMOVE:
result = remove((char*)arg1);
break;
case SYS_OPEN:
result = open((char*)arg1);
break;
case SYS_FILESIZE:
result = filesize((int)arg1);
break;
case SYS_READ:
result = read((int)arg1, (void*)arg2, (unsigned)arg3);
break;
case SYS_WRITE:
result = write((int)arg1, (void*)arg2, (unsigned)arg3);
break;
case SYS_SEEK:
seek((int)arg1, (unsigned)arg2);
break;
case SYS_TELL:
result = tell((int)arg1);
break;
case SYS_CLOSE:
close((int)arg1);
break;
default:
/* code for error not sys call */
break;
}
f->R.rax = result;
}
사실 별거 없는 함수라 예쁘게 만드는 데 신경을 썼던 것 같다.
uint64_t arg0 = f->R.rax;
uint64_t arg1 = f->R.rdi;
uint64_t arg2 = f->R.rsi;
uint64_t arg3 = f->R.rdx;
uint64_t arg4 = f->R.r10;
uint64_t arg5 = f->R.r8;
uint64_t arg6 = f->R.r9;
시스템콜을 호출할 때, 유저 모드에서 커널 모드로 변경이 되면 레지스터를 통해서 시스템 콜의 매개변수가 전달이 된다.
기본적으로 rax는 시스템콜 번호이고
rdi부터는 차례대로 시스템콜의 매개변수가 들어가는 레지스터들이다.
rdi는 매개변수가 1개일 때, rdi, rsi는 매개변수가 2개일 때,
r9까지 사용되면 매개변수를 총 6개를 사용하는 것이라고 생각하면 되겠다.
uint64_t result = 0;
// printf("\nsys call = %s\n", call_name[arg0]);
// printf("sys cur name: %s\n", thread_current()->name);
// printf("sys cur pid: %d\n\n", thread_current()->tid);
// printf ("system wow call!\n");
result와 디버깅 코드이다.
result는 시스템 콜의 결과를 '깔끔하게' 반환하기 위해 설정해둔 변수이다.
그 아래로는 시스템 콜 이름과 해당 시스템 콜을 부른 스레드의 이름 및 번호가 있다.
switch (arg0)
{
case SYS_HALT:
halt();
break;
case SYS_EXIT:
exit((int)arg1);
break;
case SYS_FORK:
result = fork(f);
break;
case SYS_EXEC:
result = exec((char*)arg1);
break;
case SYS_WAIT:
result = wait((pid_t)arg1);
break;
case SYS_CREATE:
result = create((char*)arg1, (unsigned)arg2);
break;
case SYS_REMOVE:
result = remove((char*)arg1);
break;
case SYS_OPEN:
result = open((char*)arg1);
break;
case SYS_FILESIZE:
result = filesize((int)arg1);
break;
case SYS_READ:
result = read((int)arg1, (void*)arg2, (unsigned)arg3);
break;
case SYS_WRITE:
result = write((int)arg1, (void*)arg2, (unsigned)arg3);
break;
case SYS_SEEK:
seek((int)arg1, (unsigned)arg2);
break;
case SYS_TELL:
result = tell((int)arg1);
break;
case SYS_CLOSE:
close((int)arg1);
break;
default:
/* code for error not sys call */
break;
}
switch case가 컴파일하면 더 메모리 효율적이라는 것을 알 것이다.
각 매개변수들을 형변환해서 넣어주고, 반환값이 있다면 result에 담는다.
default문에도 뭔가를 넣으면 좋을 것 같은데, 잘 모르겠다.
아마 지워버린 thread_exit()을 넣는게 베스트일 것 같긴하다.
그리고 함수명이 syscall.h와 있는 함수명과 똑같은데
들어보니 sys_close와 같은 형태로 쓰는게 일반적이라고 한다...ㅎㅎ..
근데 나는 주변 사람들 디버깅해줄 때, 위와 같은 형태로 고치라고 아는 척을 해버렸다... 하하..
f->R.rax = result;
프로세스가 유저 모드로 변경되면 그 결과를 알 수 있어야한다.
그리고 그 결과는 rax 레지스터를 통해서 전달된다.
void halt (void) {
power_off();
}
홀트다.
void exit (int status) {
struct thread *curr = thread_current();
if (curr != NULL) {
curr->self_status->exit_status = status;
printf("%s: exit(%d)\n", curr->name, curr->self_status->exit_status);
thread_exit();
}
}
종료 상태(status)를 매개변수로 받고, 만약 부모 스레드가 wait하고 있다면 부모 스레드에게 그 값을 전달해야 한다.
이게 뭔 - 소린가 싶을거다.
아무튼 부모는 자식이 어떻게 종료됐는지 알고 싶어한다. 라고만 알고있자.
그럼 curr->self_status->exit_status가 뭐냐
우선 curr은 현재 스레드이다. 보다시피
그리고 self_status는 자식 스레드의 이모저모를 담고 있는 구조체이다.
왜 이런 구조체를 만드느냐?
자식 스레드가 종료되면 당연히 free시키고, 그 내부의 데이터는 영영 사라진다.
거자필반 없는 회자정리이다.
그러면 자식이 먼저 종료되면 부모는 자식이 어떻게 종료됐는지 어떻게 알지?
그래서 필요한 게 저 구조체이다.
솔직히 말해서 저 구조체없이 oom까지 해결하긴 했다.
근데 그렇게 해결을 해버리면 어떤 일이 발생하느냐?
테스트 케이스의 pass와 fail이 중첩되는 상태가 발생한다.
https://velog.io/@mogiyoon/CS-의문-정리와-고찰-with-Pintos#종-잡을-수-없는-커널-패닉
특히 syn-read, syn-write에서 그게 더 심해졌다.
진짜 미쳐버리는 줄 알았다.
사실 글을 읽고 있는 사람이라면 '이 사람 이미 미쳐있는데 무슨 소리지?'할 수도 있다.
뭐가 됐든
저 구조체에 대해서는 추후 소개를 하겠다.
그리고 그 밑에 있는 printf문은 사실 여기서 소환하면 안되긴 하지만
alarm 테스트 케이스를 통과하기 위해서는 여기가 제일 만만한 위치이긴하다.
스레드가 종료되면 스레드가 어떻게 종료됐는지 출력하는 게 필수이다.
진짜 필수다. 없으면 테스트 통과 못한다.
pid_t fork (const struct intr_frame* thread_frame) {
char* thread_name = thread_frame->R.rdi;
return process_fork(thread_name, thread_frame);
}
무시무시한 악명에 비해 그렇지 못한 코드
사실 위 코드도 무시못하는 코드이다.
아마 syscall.h에는 char*로 돼 있을거다.
fork에서 가장 애먹는 디버깅이 위 부분과 관련이 있다.
시스템콜 호출 당시의 인터럽트 프레임을 그~대~로~ fork에 사용해야한다.
때문에 위와 같은 괴랄한 코드가 된다.
프레임을 가져와서 다시 rdi에서 추출하고 이름과 frame을 전달한다.
int exec (const char *file) {
if (ptr_error(file, UADDR)) {
exit(-1);
}
char* fn_copy = palloc_get_page (0);
if (fn_copy == NULL) {
exit(-1);
}
strlcpy (fn_copy, file, PGSIZE);
process_exec(fn_copy);
}
일명 '익섹-버'라고 불리는 함수이다.
ptr에러에 대해서는 추후 소개하겠지만, 간단하게 말하자면 포인터가 오류일 때 에러/예외처리 해주는 함수이다.
fn_copy는 추후 코드의 일관성을 위해서 넣었다. process_create_initd라는 함수에서 fn_copy를 페이지 단위로 할당받고 이를 initd 함수를 가진 스레드로 전달한다. initd 함수가 호출하는 process_exec 함수에서 fn_copy를 사용하고 할당해제하기 때문에 여기서도 똑같은 것을 만들어둔다.
int wait (pid_t child_pid) {
return process_wait(child_pid);
}
wait이다. process_wait을 실행시킨다. process_wait 역시 어마무시한 함수이다.
bool create (const char *file, unsigned initial_size) {
if (ptr_error(file, UADDR)) {
exit(-1);
}
if (strlen(file) == 0) {
exit(-1);
}
struct file* tmp_file = filesys_open(file);
if (tmp_file != NULL) {
file_close(tmp_file);
return false;
}
if (filesys_create(file, initial_size)) {
return true;
} else {
return false;
}
}
create함수다. file이 잘못된 주소거나 file 이름의 길이가 0이면 -1 종료한다.
같은 이름의 파일을 열었을 때, 파일이 열린다면 이미 파일이 있다는 의미이므로 false를 리턴하고 파일을 닫는다.
만약 없다면 filesys_create 함수를 통해 파일을 생성한다.
bool remove (const char *file) {
if (ptr_error(file, UADDR)) {
exit(-1);
}
bool result = filesys_remove(file);
return result;
}
에러잡고 파일 제거하고 결과를 리턴한다.
syn_remove 테스트에 나오겠지만 파일은 열었던 파일들이 전부 닫힐 때 제거된다.
즉, 제거한다고 마킹만 해두고 inode의 open cnt가 0이면 제거한다.
inode의 자세한 설명은 프로젝트 2의 구조 고찰에 업로드할 예정이다.
int open (const char *file) {
if (ptr_error(file, UADDR)) {
exit(-1);
}
struct file* new_file = filesys_open(file);
struct thread* curr = thread_current();
int fd_num;
if (curr->next_fd >= FD_MAX) {
file_close(new_file);
return -1;
}
if (new_file != NULL) {
while (curr->next_fd < FD_MAX && curr->file_dt[curr->next_fd] != NULL) {
curr->next_fd++;
}
if (curr->next_fd < FD_MAX && curr->file_dt[curr->next_fd] == NULL) {
curr->file_dt[curr->next_fd] = new_file;
fd_num = curr->next_fd;
curr->next_fd++;
return fd_num;
}
}
return -1;
}
파일을 열어서 해당 파일에 대한 파일 구조체를 생성해서 스레드의 fd테이블에 연결한다.
만약 할당할 수 있는 fd 수를 초과했을 경우, 파일을 다시 닫는다.
새로운 파일이 NULL이 아닌 경우, 스레드의 next_fd를 활용하여 fd를 할당할 위치를 찾는다.
지극히 사견이지만 나는 아래와 같은 문법을 별로 좋아하지 않는다.
fd_num = curr->next_fd++;
한 줄에는 하나의 일?만 적혀있으면 좋겠다.
그래서 if문 안에 함수가 들어가는 것도 별로 좋아하지 않는다.
int filesize (int fd) {
struct thread* curr = thread_current();
if (fd >= FD_MAX || fd < 0 || curr->file_dt[fd] == NULL) {
return -1;
}
return file_length(curr->file_dt[fd]);
}
fd만 오류없으면 file_length로 쉽게 해결가능하다.
int read (int fd, void *buffer, unsigned length) {
if (ptr_error(buffer, UADDR)) {
exit(-1);
}
if (fd == 0) {
for (unsigned i = 0; i < length; i++) {
((uint8_t *)buffer)[i] = input_getc();
}
return length;
}
struct thread* curr = thread_current();
if (fd >= FD_MAX || fd < 0 || curr->file_dt[fd] == NULL) {
return -1;
}
unsigned int read_len;
read_len = file_read(curr->file_dt[fd], buffer, length);
return read_len;
}
할당된 fd를 사용하여 파일을 읽는 함수이다. fd가 0이면 표준입력이다. 따라서 이에 해당하는 코드를 작성하면 된다. 솔직히 말하면 얘는 그냥 gpt 돌렸다.
이후 fd 검사를 해주고,
(fd 0과 fd 1은 표준 입출력이기 때문에 NULL로 뒀다. 따라서 fd 검사보다 표준입출력 관련 코드가 먼저 실행돼야한다.)
file_read 함수를 불러와서 사용해준다.
unsigned int read_len;
read_len = file_read(curr->file_dt[fd], buffer, length);
return read_len;
이런 코드가 내가 좋아하는 코드다.
각자 할일이 명확하게 보이는 코드이다.
그리고 코드를 이렇게 써둬야 나중에 유지보수가 편하다.
int write (int fd, const void *buffer, unsigned length) {
if (ptr_error(buffer, UADDR)) {
exit(-1);
}
if (fd == 1) {
putbuf(buffer, length);
return length;
}
struct thread* curr = thread_current();
if (fd >= FD_MAX || fd < 0 || curr->file_dt[fd] == NULL) {
return -1;
}
if (inode_get_deny(file_get_inode(curr->file_dt[fd]))) {
return 0;
}
unsigned int write_len;
write_len = file_write(curr->file_dt[fd], buffer, length);
return write_len;
}
할당된 fd를 사용하여 파일에 쓰는 함수이다. fd가 1이면 표준출력이다. 따라서 이에 해당하는 코드를 작성하면 된다. 솔직히 말해서 얘도 그냥 gpt 돌렸다.
read와 write에서 gpt를 돌렸다는 건 표준입출력에 한해서이다.
이후 fd 검사를 해주고,
(fd 0과 fd 1은 표준 입출력이기 때문에 NULL로 뒀다. 따라서 fd 검사보다 표준입출력 관련 코드가 먼저 실행돼야한다.)
file_write 함수를 불러와서 사용해준다.
read와 다른 부분이 하나 있는데
if (inode_get_deny(file_get_inode(curr->file_dt[fd]))) {
return 0;
}
이 부분이다.
해당 코드는 추후 rox 테스트나 syn 테스트와 관련이 있는데
개념만 간단하게 설명하자면, 실행중인 파일이거나 다른 fd가 해당 파일을 읽거나 쓰고 있을 때, 파일 동기화를 위해 쓰지 못하게 막아둔다고 생각하면 된다.
void seek (int fd, unsigned position) {
struct file* tmp_file = thread_current()->file_dt[fd];
if (tmp_file == NULL) {
exit(-1);
}
file_seek(tmp_file, position);
}
ckwdkfk
unsigned tell (int fd) {
struct file* tmp_file = thread_current()->file_dt[fd];
if (ptr_error(tmp_file, UADDR)) {
exit(-1);
}
return file_tell(tmp_file);
}
akfgofk
void close (int fd) {
if (fd >= FD_MAX) {
exit(-1);
}
struct file* tmp_file = thread_current()->file_dt[fd];
if (tmp_file == NULL) {
return;
}
file_close(thread_current()->file_dt[fd]);
thread_current()->file_dt[fd] = NULL;
if (thread_current()->next_fd > fd) {
thread_current()->next_fd = fd;
}
}
파일을 닫는다는 것은 중요하다. 파일을 닫아야 쓰기 권한을 설정할 수 있고 삭제할 수 있다.
파일을 닫고 해당 fd 테이블이 가리키는 파일을 NULL로 만들어준다.
그리고 만약 할당 해제한 fd 값이 next fd보다 작을 경우, next fd를 갱신해준다.
static bool ptr_error (char* input_ptr, void* aux) {
if (input_ptr == NULL) {
return true;
}
//address is user area
if ((enum waddr)aux == UADDR) {
if (!is_user_vaddr(input_ptr)) {
return true;
}
if (pml4_get_page(thread_current()->pml4, input_ptr) == NULL) {
return true;
}
}
if ((enum waddr)aux == KADDR) {
if (!is_kernel_vaddr(input_ptr)) {
return true;
}
}
return false;
}
aux라는 보조인자를 통해서 해당 포인터가 어디를 가리켜야 하는지 확실하게 만들어줬다.
포인터가 NULL인 경우 리턴하고
이 모든 경우를 통과하면 에러가 아닌 것이다.
struct status_tag {
tid_t tid;
int exit_status;
struct thread* thread;
struct list_elem tag_elem;
struct semaphore load_sema;
struct semaphore fork_sema;
struct semaphore wait_sema;
};
status_tag라는 구조체이다.
사실 위에 있던 정보들은 모두 스레드 구조체 안에 있던 정보들이었는데,
스레드가 먼저 죽어버리면 그 안에 있는 정보들을 쓸 수 없는 상황이 생길 수 있기 때문에 따로 정보를 저장해두는 것이다.
모든 정보들이 해당 tag가 가리키는 스레드를 위한 것들이라고 생각하면 된다.
tid는 스레드 tid, exit_status는 종료코드, thread는 해당 스레드이다.
tag_elem이 있다는 것은 이 tag 역시 리스트로 관리된다는 말이다.
부모 스레드의 자식 status tag 리스트에서 관리된다.
그리고 load sema, fork sema, wait sema는 부모가 자녀 스레드를 load, fork, wait할 때
부모가 다른 행동을 하지 않게 만들기 위해 잠깐 들어가있는 곳이다. 부모를 감시하는 자녀다.
struct thread {
/* Owned by thread.c. */
tid_t tid; /* Thread identifier. */
enum thread_status status; /* Thread state. */
char name[16]; /* Name (for debugging purposes). */
int priority; /* Priority. */
int original_priority; /* Priority Donate. */ //ADD
int64_t sleep_time; /* Sleep_time */ //ADD
/* Shared between thread.c and synch.c. */
struct list_elem elem; /* List element. */
/* Donate */ //ADD
struct list donate_list;
struct list_elem donator_elem;
/* File Descriptor */
struct file* file_dt[FD_MAX];
int next_fd;
struct file* running_file;
/* Parent thread */
struct thread* parent_thread;
/* Self status */
struct status_tag* self_status;
/* Child thread */
struct list child_status_tags;
#ifdef USERPROG
/* Owned by userprog/process.c. */
uint64_t *pml4; /* Page map level 4 */
#endif
#ifdef VM
/* Table for whole virtual memory owned by thread. */
struct supplemental_page_table spt;
#endif
/* Owned by thread.c. */
struct intr_frame tf; /* Information for switching */
unsigned magic; /* Detects stack overflow. */
};
추가된 부분은 아래와 같다.
/* File Descriptor */
struct file* file_dt[FD_MAX];
int next_fd;
struct file* running_file;
/* Parent thread */
struct thread* parent_thread;
/* Self status */
struct status_tag* self_status;
/* Child thread */
struct list child_status_tags;
먼저
struct file* file_dt[FD_MAX];
int next_fd;
이건 파일 디스크립터를 관리하는 테이블과 할당 번호이다.
FD_MAX값은 매크로로 관리하면 편한데, 나는 이 값을 32로 주었다.
60을 넘기면 다른 스레드 구조체 영역을 침범해서 오류를 일으킬 수 있다고 한다.
다른 부분들은 쉽게쉽게 읽힐 것이다.
부모 스레드 정보를 담는 포인터, 자기 자신의 상태를 가리키는 포인터, 자식들의 정보를 관리하는 리스트
여기서 유심히 볼 부분은 자기 자신의 상태(status_tag)를 가리키고(*) 있다는 것이다.
즉, 커널 힙 영역에 해당 구조체를 할당하고 그걸 가리키고 있다.
왜냐하면 스레드 구조체 안에 구조체 그 자체를 넣어버리면, 스레드가 죽었을 때 상태 정보도 같이 사라지기 때문이다.
사실 구조체를 따로 만들기 전에는 스레드 안에 다양한 정보들을 저장했었다.
child status[]
child_status 배열을 선언해서 자식이 종료되면 부모 스레드의 child status를 업데이트 하는 방식으로 진행했다.
current_thread->parrent_thread->child_status[current_thread->pid]
이런 식으로 업데이트를 했는데, 마지막 테스트인 oom 테스트에서 pid값이 2500개 언저리까지 가기 때문에 불가능하다고 봐야한다.
sema 삼총사
load, fork, wait sema 삼총사를 스레드 안에서 관리했었다. 근데 역시 자식 스레드가 종료되면 부모 스레드가 미아가 돼버리기 때문에 status_tag 구조체를 만들게 되었다.
thread.c 파일에서 특별히 바뀐 것은 대략 네 부분 정도 된다.
tid_t
thread_create (const char *name, int priority,
thread_func *function, void *aux) {
struct thread *t;
tid_t tid;
ASSERT (function != NULL);
/* Allocate thread. */
t = palloc_get_multiple (PAL_ZERO, THREAD_PAGE);
if (t == NULL)
return TID_ERROR;
/* Initialize thread. */
init_thread (t, name, priority);
tid = t->tid = allocate_tid ();
//ADD parent & children
if (thread_current() != NULL && name != "idle") {
t->parent_thread = thread_current();
struct status_tag* t_status = make_child_status(t->tid);
if (t_status == NULL) {
palloc_free_multiple (t, THREAD_PAGE);
return TID_ERROR;
}
t_status->thread = t;
t->self_status = t_status;
list_push_back(&thread_current()->child_status_tags, &t_status->tag_elem);
}
/* Call the kernel_thread if it scheduled.
* Note) rdi is 1st argument, and rsi is 2nd argument. */
t->tf.rip = (uintptr_t) kernel_thread;
t->tf.R.rdi = (uint64_t) function;
t->tf.R.rsi = (uint64_t) aux;
t->tf.ds = SEL_KDSEG;
t->tf.es = SEL_KDSEG;
t->tf.ss = SEL_KDSEG;
t->tf.cs = SEL_KCSEG;
t->tf.eflags = FLAG_IF;
/* Add to run queue. */
thread_unblock (t);
return tid;
}
thread create에서는 부모와 자식 관계를 설정하는 코드를 추가했다.
메인 스레드가 테스트 케이스용 스레드를 만드는 것도 자식으로 취급하고
fork한 스레드는 당연히 자식 취급이기 때문에
그냥 thread create에서 부모/자식 스레드 관계를 처리했다.
//ADD parent & children
if (thread_current() != NULL && name != "idle") {
t->parent_thread = thread_current();
struct status_tag* t_status = make_child_status(t->tid);
if (t_status == NULL) {
palloc_free_multiple (t, THREAD_PAGE);
return TID_ERROR;
}
t_status->thread = t;
t->self_status = t_status;
list_push_back(&thread_current()->child_status_tags, &t_status->tag_elem);
}
사실 if문은 필요없을 거 같긴하다. 그냥 있으면 보기 좋을 것 같아서 넣었다.
새로 생성된 스레드(t)의 부모 스레드를 현재 스레드로 설정하고
(스레드 만드는 함수를 실행 시키는 것은 부모 스레드이다.)
child status를 만들어서 할당한다.
만약 자리가 부족해서 만들지 못했을 경우에는 스레드 페이지를 해제하고 에러를 반환한다.
status tag가 가리키는 스레드(t)와 스레드(t)가 가리키는 status tag를 각각 설정하고, status tag를 현재 스레드의 자식 스레드 tag리스트에 넣어준다.
void
thread_exit (void) {
ASSERT (!intr_context ());
struct thread* cur = thread_current();
enum intr_level old_level = intr_disable ();
cur->self_status->thread = NULL;
sema_up(&cur->self_status->wait_sema);
#ifdef USERPROG
process_exit ();
#endif
/* Just set our status to dying and schedule another process.
We will be destroyed during the call to schedule_tail(). */
do_schedule (THREAD_DYING);
intr_set_level(old_level);
msg("end");
NOT_REACHED ();
}
thread exit에서는 현재 스레드 status tag에 있는 wait sema의 부모 스레드를 풀어주고
(부모가 wait 하고 있지 않아도 괜찮다. sema value가 +1이 되면 추후 부모가 wait sema down할 때 이 값을 다시 0으로 만들어버린다. 그리고 부모는 잠기지 않는다.)
현재 status tag가 가리키는 값을 NULL로 만들어서 thread가 죽었다는 것을 기록한다.
나머지 코드는 똑같다.
static void
init_thread (struct thread *t, const char *name, int priority) {
ASSERT (t != NULL);
ASSERT (PRI_MIN <= priority && priority <= PRI_MAX);
ASSERT (name != NULL);
memset (t, 0, sizeof *t);
t->status = THREAD_BLOCKED;
t->sleep_time = 0; //ADD
strlcpy (t->name, name, sizeof t->name);
t->tf.rsp = (uint64_t) t + PGSIZE - sizeof (void *);
t->priority = priority;
t->original_priority = priority;
t->magic = THREAD_MAGIC;
//donate
list_init(&t->donate_list);
//init file descriptor table
t->next_fd = 2;
//init child
list_init(&t->child_status_tags);
}
init thread에서는
//init file descriptor table
t->next_fd = 2;
//init child
list_init(&t->child_status_tags);
이 코드만 보면 될 거라고 생각한다.
다음에 할당될 파일 디스크립터를 2번으로 설정하고 (0: 표준입력, 1: 표준출력)
자식들의 status_tag가 들어갈 리스트를 초기화시켜준다.
struct status_tag*
make_child_status (tid_t child_tid) {
struct status_tag* child_status = calloc(1, sizeof(struct status_tag));
if (child_status == NULL) {
return NULL;
}
child_status->tid = child_tid;
child_status->exit_status = -1;
sema_init(&child_status->load_sema, 0);
sema_init(&child_status->fork_sema, 0);
sema_init(&child_status->wait_sema, 0);
return child_status;
}
새로 추가된 커스텀 함수이다.
calloc으로 스택 힙에서 status tag를 넣을 자리를 받아온다.
tid는 자식의 tid로, 종료 코드는 -1로 설정해두고 sema를 모두 초기화시켜준다.
이렇게 하면 스레드는 끝이다.
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);
char* save_ptr;
file_name = strtok_r (file_name, " ", &save_ptr);
/* Create a new thread to execute FILE_NAME. */
enum intr_level old_level = intr_disable();
tid = thread_create (file_name, PRI_DEFAULT, initd, fn_copy);
if (tid == TID_ERROR) {
palloc_free_page (fn_copy);
return tid;
}
struct list_elem* load_elem;
struct status_tag* tmp_child_status;
if (!list_empty(&thread_current()->child_status_tags)) {
load_elem = list_front(&thread_current()->child_status_tags);
tmp_child_status = list_entry(load_elem, struct status_tag, tag_elem);
while (tmp_child_status->tid != tid)
{
if (load_elem->next != list_end(&thread_current()->child_status_tags)) {
load_elem = load_elem->next;
tmp_child_status = list_entry(load_elem, struct status_tag, tag_elem);
} else {
return TID_ERROR;
}
}
}
intr_set_level(old_level);
// printf("cur th: %s load sema down\n", thread_current()->name);
sema_down(&tmp_child_status->load_sema);
return tid;
}
조금 길다.
char* save_ptr;
file_name = strtok_r (file_name, " ", &save_ptr);
이건 스레드 이름을 설정하기 위해 만든 코드다. 이렇게 하지 않으면
tid = thread_create (file_name, PRI_DEFAULT, initd, fn_copy);
여기서 file_name이 single_arg_test single_arg라면
스레드 이름 역시 single_arg_test single_arg와 같이 돼버린다.
따라서 여기서 스레드 이름 부분만 자른다.
이렇게 자르지 않으면 exit할 때 이상한 스레드 이름이 나와서 통과하지 못한다.
struct list_elem* load_elem;
struct status_tag* tmp_child_status;
if (!list_empty(&thread_current()->child_status_tags)) {
load_elem = list_front(&thread_current()->child_status_tags);
tmp_child_status = list_entry(load_elem, struct status_tag, tag_elem);
while (tmp_child_status->tid != tid)
{
if (load_elem->next != list_end(&thread_current()->child_status_tags)) {
load_elem = load_elem->next;
tmp_child_status = list_entry(load_elem, struct status_tag, tag_elem);
} else {
return TID_ERROR;
}
}
}
이 부분은 만들어진 자식 스레드를 찾는 부분이라고 생각하면 된다.
status tag 중에 tid가 같은 녀석을 찾는다.
왜 찾냐고?
sema_down(&tmp_child_status->load_sema);
sema down하기 위해서다. child 스레드가 load하는 동안 부모가 꺼지면 안된다.
inid는 딱시 별 거 없어서 패스하겠다.
struct th_w_if {
struct thread* the_thread;
struct intr_frame* the_if;
};
thread with if라는 커스텀 구조체이다.
fork할 때는 시스템콜 호출 시점의 인터럽트 프레임이 필요하다. 따라서 이 값을 전달해줘야하는데, do fork할 때 넘겨주는 값은 기본적으로 스레드만 있다. 그래서 두 값을 다 넘겨주기 위해 구조체 형태로 만들었다.
근데 다른 사람들 얘기를 들어보니 스레드 내부에 부모 if 공간을 따로어서 fork 문제를 해결했다고도 한다.
tid_t
process_fork (const char *name, struct intr_frame *if_ UNUSED) {
enum intr_level old_level = intr_disable();
struct th_w_if tmp_twi;
tmp_twi.the_thread = thread_current();
tmp_twi.the_if = if_;
tid_t child_tid = thread_create (name, PRI_DEFAULT, __do_fork, &tmp_twi);
if (child_tid == TID_ERROR) {
return TID_ERROR;
}
struct status_tag* tmp_child_status;
struct list_elem* fork_elem;
if (!list_empty(&thread_current()->child_status_tags)) {
fork_elem = list_front(&thread_current()->child_status_tags);
tmp_child_status = list_entry(fork_elem, struct status_tag, tag_elem);
while (tmp_child_status->tid != child_tid)
{
if (fork_elem->next != list_end(&thread_current()->child_status_tags)) {
fork_elem = fork_elem->next;
tmp_child_status = list_entry(fork_elem, struct status_tag, tag_elem);
} else {
return -1;
}
}
}
intr_set_level(old_level);
sema_down(&tmp_child_status->fork_sema);
/* Clone current thread to new thread.*/
return tmp_child_status->tid;
}
th_w_if 구조체에 스레드와 if값을 저장하고, 새로운 스레드를 생성한다. fork하는 도중에 부모가 종료되면 안되므로, status tag의 fork sema를 down한다.
앞서 status 구조체를 따로 만든 이유가 여기서 다시 등장한다.
intr_set_level(old_level);
sema_down(&tmp_child_status->fork_sema);
여기서 인터럽트가 풀림과 동시에 sema down 전에 자식의 do fork가 끝나고 exit이 호출될 수가 있다.
실제로 있었던 일이다.
이 때문에 status_tag를 만드는 귀찮은 짓을 했다.
process fork는 부모스레드가 실행하는 함수이므로 fork한 자식 tid를 반환한다.
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;
ASSERT (intr_get_level () == INTR_OFF);
/* 1. TODO: If the parent_page is kernel page, then return immediately. */
if (!is_user_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 true;
}
/* 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 = *pte & PTE_W;
/* 5. Add new page to child's page table at address VA with WRITABLE
* permission. */
if (!pml4_set_page (current->pml4, va, newpage, writable)) {
palloc_free_page(newpage);
return false;
}
return true;
}s
TODO만 따라가면 어려움없이 해결할 수 있다. 아마 TODO가 없었다면 난이도 상 내지 최상에 가까웠을 것이라 생각한다.
if (!is_user_vaddr(va)) {
return true;
}
유저 가상 주소가 아니면 true(에러)를 반환한다.
parent_page = pml4_get_page (parent->pml4, va);
if (parent_page == NULL) {
return true;
}
페이지가 존재하는지 확인한다. 부모 스레드는 PTE 전부를 쓰는 것이 아니기 때문에 NULL 페이지를 얻을 수도 있다.
newpage = palloc_get_page(PAL_USER);
if (newpage == NULL) {
return false;
}
부모 페이지를 복사하고 부모페이지의 쓰기 권한을 가져온다.
memcpy(newpage, parent_page, PGSIZE);
writable = *pte & PTE_W;
페이지를 세팅하고, 실패할 경우 에러를 반환한다.
static void
__do_fork (void *aux) {
struct intr_frame if_;
struct th_w_if *parent_w_if = (struct th_w_if *) aux;
struct thread *current = thread_current ();
/* TODO: somehow pass the parent_if. (i.e. process_fork()'s if_) */
enum intr_level old_level = intr_disable();
//ADD
struct thread* parent = parent_w_if->the_thread;
//ADD
bool succ = true;
/* 1. Read the cpu context to local stack. */
memcpy (&if_, parent_w_if->the_if, sizeof (struct intr_frame));
/* 2. Duplicate PT */
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
/* TODO: Your code goes here.
* TODO: Hint) To duplicate the file object, use `file_duplicate`
* TODO: in include/filesys/file.h. Note that parent should not return
* TODO: from the fork() until this function successfully duplicates
* TODO: the resources of parent.*/
current->next_fd = parent->next_fd;
for (int i = 0; i < FD_MAX; i++) {
if (parent->file_dt[i] != NULL) {
current->file_dt[i] = file_duplicate(parent->file_dt[i]);
if (current->file_dt[i] == NULL) {
goto error;
}
}
}
process_init ();
/* Finally, switch to the newly created process. */
if (succ) {
if_.R.rax = 0;
sema_up(&thread_current()->self_status->fork_sema);
do_iret (&if_);
intr_set_level(old_level);
NOT_REACHED();
}
error:
thread_current()->self_status->tid = TID_ERROR; //
sema_up(&thread_current()->self_status->fork_sema);
intr_set_level(old_level);
thread_exit ();
}
aux로 전달받은 th_w_if의 포장을 뜯어서 부모 스레드를 설정하고 if에 복사한다.
페이지 테이블을 생성하고 부모 스레드의 페이지를 duplicate_pte를 사용하여 복사한다.
current->next_fd = parent->next_fd;
for (int i = 0; i < FD_MAX; i++) {
if (parent->file_dt[i] != NULL) {
current->file_dt[i] = file_duplicate(parent->file_dt[i]);
if (current->file_dt[i] == NULL) {
goto error;
}
}
}
file duplicate를 사용하여 부모의 파일 디스크립터를 복사한다.
만약 부모가 가리키고 있는 파일이 NULL일 경우 복사하지 않는다.
이 모든 과정이 성공할 경우
if (succ) {
if_.R.rax = 0;
sema_up(&thread_current()->self_status->fork_sema);
do_iret (&if_);
intr_set_level(old_level);
NOT_REACHED();
}
현재 스레드(자식 스레드)의 statugtag에서 fork sema up해서 부모 스레드를 풀어주고
if의 rax 레지스터를 0으로 만들어서 pid0을 반환한다.
그리고 do iret을 실행한다.
만약 실패할 경우
error:
thread_current()->self_status->tid = TID_ERROR; //
sema_up(&thread_current()->self_status->fork_sema);
intr_set_level(old_level);
thread_exit ();
똑같이 sema up 하고 현재 스레드의 tid를 error로 만든 뒤 종료시킨다.
int
process_exec (void *f_name) {
// printf("exec\n");
char *file_name = f_name;
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;
_if.ds = _if.es = _if.ss = SEL_UDSEG;
_if.cs = SEL_UCSEG;
_if.eflags = FLAG_IF | FLAG_MBS;
// enum intr_level old_level = intr_disable();
/* We first kill the current context */
process_cleanup ();
/* And then load the binary */
success = load (file_name, &_if);
/* If load failed, quit. */
if (!success) {
return -1;
}
char* save_ptr;
char* running_file_name = strtok_r (file_name, " ", &save_ptr);
struct file* running_file = filesys_open(running_file_name);
thread_current()->running_file = running_file;
file_deny_write(running_file);
// printf("exec running file inode: %p\n", running_file->inode);
palloc_free_page (file_name);
/* Start switched process. */
do_iret (&_if);
NOT_REACHED ();
}
exec는 현재 프로세스를 실행파일로 덮어씌우는 작업을 수행한다.
프로세스를 깨끗하게 지우고 file을 load한다.
char* save_ptr;
char* running_file_name = strtok_r (file_name, " ", &save_ptr);
struct file* running_file = filesys_open(running_file_name);
thread_current()->running_file = running_file;
file_deny_write(running_file);
이 부분은 rox 테스트와 관련이 있다.
실행 중인 파일은 쓰기가 불가능하므로 어떤 파일이 실행중인지 알 수 있어야 한다.
그리고 해당 파일을 쓰기 금지한다.
palloc_free_page (file_name);
앞서 exec 시스템 콜에서 페이지에 할당한 이유가 얘 때문이다.
그리고 두 아이렛으로 마무리
int
process_wait (tid_t child_tid UNUSED) {
struct thread* curr = thread_current();
struct status_tag* tmp_child_status;
struct list_elem* child_elem;
if (!list_empty(&curr->child_status_tags)) {
enum intr_level old_level = intr_disable();
child_elem = list_front(&curr->child_status_tags);
tmp_child_status = list_entry(child_elem, struct status_tag, tag_elem);
while (tmp_child_status->tid != child_tid)
{
if (child_elem->next != list_end(&curr->child_status_tags)) {
child_elem = child_elem->next;
tmp_child_status = list_entry(child_elem, struct status_tag, tag_elem);
} else {
break;
}
}
if (tmp_child_status->tid != child_tid) {
return -1;
}
intr_set_level(old_level);
if (tmp_child_status->thread != NULL) {
sema_down(&tmp_child_status->wait_sema);
}
int return_value = tmp_child_status->exit_status;
list_remove(&tmp_child_status->tag_elem);
free(tmp_child_status);
return return_value;
} else {
return -1;
}
}
만만한 녀석이 아니라고 했다.
크게 if문인 곳과 if문이 아닌 곳으로 나뉘는데,
if (!list_empty(&curr->child_status_tags))
이 if문의 의미는 자식 status tag가 비어있는지 확인하는 용도이다.
자식이 없는데 wait을 한다면 당연히 -1로 에러처리를 한다.
만약 자식이 있다면 해당 tid를 가진 자식의 status를 찾는다.
while (tmp_child_status->tid != child_tid)
{
if (child_elem->next != list_end(&curr->child_status_tags)) {
child_elem = child_elem->next;
tmp_child_status = list_entry(child_elem, struct status_tag, tag_elem);
} else {
break;
}
}
찾으면 해당 status의 wait sema에 down한다.
if (tmp_child_status->thread != NULL) {
sema_down(&tmp_child_status->wait_sema);
}
만약 자식이 먼저 종료된 상태라면 세마의 value는 이미 1이므로 세마에 잠기지 않고 바로 통과된다.
wait이 끝났다는 것은 child가 종료됐다는 것이므로 list에서 제거하고 메모리 할당도 해제한다.
list_remove(&tmp_child_status->tag_elem);
free(tmp_child_status);
void
process_exit (void) {
struct thread *curr = thread_current ();
for (int i = 0; i < FD_MAX; i++) {
if (curr->file_dt[i]) {
file_close(curr->file_dt[i]);
curr->file_dt[i] = NULL;
}
}
if (curr->running_file != NULL) {
file_close(curr->running_file);
}
/* TODO: Your code goes here.
* TODO: Implement process termination message (see
* TODO: project2/process_termination.html).
* TODO: We recommend you to implement process resource cleanup here. */
// printf("process exit work\n");
process_cleanup ();
}
여기서는 파일 디스크립터를 모두 닫고 할당도 해제한다.
for (int i = 0; i < FD_MAX; i++) {
if (curr->file_dt[i]) {
file_close(curr->file_dt[i]);
curr->file_dt[i] = NULL;
}
}
그리고 실행 중인 파일이 있다면 닫는다.
if (curr->running_file != NULL) {
file_close(curr->running_file);
}
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;
char *token, *save_ptr;
char** token_list[64];
char** pointer_addr[64];
int argc = 0;
/* Adding parsed arguments */
for (token = strtok_r (file_name, " ", &save_ptr); token != NULL; token = strtok_r (NULL, " ", &save_ptr)) {
token_list[argc++] = token;
}
/* 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 (token_list[0]);
if (file == NULL) {
printf ("load: %s: open failed\n", token_list[0]);
sema_up(&thread_current()->self_status->load_sema);
exit(-1);
}
/* Read and verify executable header. */
if (file_read (file, &ehdr, sizeof ehdr) != sizeof ehdr
|| memcmp (ehdr.e_ident, "\177ELF\2\1\1", 7)
|| ehdr.e_type != 2
|| ehdr.e_machine != 0x3E // amd64
|| ehdr.e_version != 1
|| ehdr.e_phentsize != sizeof (struct Phdr)
|| ehdr.e_phnum > 1024) {
printf ("load: %s: error loading executable\n", token_list[0]);
goto done;
}
// intr_set_level(old_level);
/* Read program headers. */
file_ofs = ehdr.e_phoff;
for (i = 0; i < ehdr.e_phnum; i++) {
struct Phdr phdr;
if (file_ofs < 0 || file_ofs > file_length (file))
goto done;
file_seek (file, file_ofs);
if (file_read (file, &phdr, sizeof phdr) != sizeof phdr)
goto done;
file_ofs += sizeof phdr;
switch (phdr.p_type) {
case PT_NULL:
case PT_NOTE:
case PT_PHDR:
case PT_STACK:
default:
/* Ignore this segment. */
break;
case PT_DYNAMIC:
case PT_INTERP:
case PT_SHLIB:
goto done;
case PT_LOAD:
if (validate_segment (&phdr, file)) {
bool writable = (phdr.p_flags & PF_W) != 0;
uint64_t file_page = phdr.p_offset & ~PGMASK;
uint64_t mem_page = phdr.p_vaddr & ~PGMASK;
uint64_t page_offset = phdr.p_vaddr & PGMASK;
uint32_t read_bytes, zero_bytes;
if (phdr.p_filesz > 0) {
/* Normal segment.
* Read initial part from disk and zero the rest. */
read_bytes = page_offset + phdr.p_filesz;
zero_bytes = (ROUND_UP (page_offset + phdr.p_memsz, PGSIZE)
- read_bytes);
} else {
/* Entirely zero.
* Don't read anything from disk. */
read_bytes = 0;
zero_bytes = ROUND_UP (page_offset + phdr.p_memsz, PGSIZE);
}
if (!load_segment (file, file_page, (void *) mem_page,
read_bytes, zero_bytes, writable))
goto done;
}
else
goto done;
break;
}
}
/* Set up stack. */
if (!setup_stack (if_))
goto done;
/* Start address. */
if_->rip = ehdr.e_entry;
char* stack_pointer = if_->rsp;
pointer_addr[0] = NULL;
pointer_addr[argc + 1] = NULL;
char** argv = NULL;
for (int index = 0; index < argc; index++) {
stack_pointer -= 1;
*stack_pointer = '\0';
stack_pointer -= strlen(token_list[index]);
memcpy(stack_pointer, token_list[index], strlen(token_list[index]));
pointer_addr[index+1] = stack_pointer;
}
int padding_size = (int)(stack_pointer)%8;
if (padding_size != 0) {
stack_pointer -= padding_size;
memset(stack_pointer, 0, padding_size);
}
for (int index = 0; index < argc + 2; index++) {
stack_pointer -= sizeof(char*);
*((char**)stack_pointer) = pointer_addr[argc + 1 - index];
if (index == argc) {
argv = (char**)stack_pointer;
}
}
if_->R.rsi = argv;
if_->R.rdi = argc;
if_->rsp = argv;
/* stack_pointer: Your code goes here.
* TODO: Implement argument passing (see project2/argument_passing.html). */
success = true;
done:
printf("load end\n");
/* We arrive here whether the load is successful or not. */
sema_up(&thread_current()->self_status->load_sema);
file_close(file);
return success;
}
구현한 부분만 설명을 하겠다.
char *token, *save_ptr;
char** token_list[64];
char** pointer_addr[64];
int argc = 0;
/* Adding parsed arguments */
for (token = strtok_r (file_name, " ", &save_ptr); token != NULL; token = strtok_r (NULL, " ", &save_ptr)) {
token_list[argc++] = token;
}
token list는 말 그대로 token이 들어가는 공간이다.
pointer addr은 추후 스택에 들어갈 인자들의 주소를 가리킬 예정인 녀석이다.
받은 인자로부터 파일이름과 arg들을 떼어내고 argc를 활용하여 token list에 바로 저장함과 동시에 인자들을 카운트한다.
file = filesys_open (token_list[0]);
if (file == NULL) {
printf ("load: %s: open failed\n", token_list[0]);
sema_up(&thread_current()->self_status->load_sema);
exit(-1);
}
이 부분이 사실 가장 애매한 부분이긴 하다.
syn write 부분에서 계속 fail과 pass가 번갈아나오고 있다.
진짜 딥하게 팠는데 답이 안보인다. 사실 딥하게 파지 않은 것이 아닐까? 아무튼 핀토스가 끝날 때까지 확실하게 원인을 파악하고 해결할 예정이다.
이 부분에 대해서 설명하자면 token_list[0]으로 파일을 열고 만약 파일 열기에 실패했다면 load sema를 up해준 뒤 스레드를 종료한다.
참고로 이 부분은 인터럽트를 잠글 수 없다. 왜냐하면 인터럽트가 필요한 부분이기 때문이다.
그래서 나는 문제의 원인이 이게 아닐까 생각한다.
load하고 있는 도중에 인터럽트가 발생해서 문제가 생겼다고 생각한다.
아래는 유저 스택으로 인자를 전달하는 것과 관련있는 부분이다.
char* stack_pointer = if_->rsp;
pointer_addr[0] = NULL;
pointer_addr[argc + 1] = NULL;
char** argv = NULL;
for (int index = 0; index < argc; index++) {
stack_pointer -= 1;
*stack_pointer = '\0';
stack_pointer -= strlen(token_list[index]);
memcpy(stack_pointer, token_list[index], strlen(token_list[index]));
pointer_addr[index+1] = stack_pointer;
}
int padding_size = (int)(stack_pointer)%8;
if (padding_size != 0) {
stack_pointer -= padding_size;
memset(stack_pointer, 0, padding_size);
}
for (int index = 0; index < argc + 2; index++) {
stack_pointer -= sizeof(char*);
*((char**)stack_pointer) = pointer_addr[argc + 1 - index];
if (index == argc) {
argv = (char**)stack_pointer;
}
}
if_->R.rsi = argv;
if_->R.rdi = argc;
if_->rsp = argv;
시작 주소(메인 함수의 가짜 리턴 주소)와 마지막 주소(인자가 끝나는 부분)를 NULL로 만든다.
pointer_addr[0] = NULL;
pointer_addr[argc + 1] = NULL;
스택 포인터를 잘 조정해야되는게 넣을 글자만큼 먼저 이동하고 값을 넣어야 한다.
아래는 그 과정이다.
그리고 마자막에 그 주소를 저장한다.
for (int index = 0; index < argc; index++) {
stack_pointer -= 1;
*stack_pointer = '\0';
stack_pointer -= strlen(token_list[index]);
memcpy(stack_pointer, token_list[index], strlen(token_list[index]));
pointer_addr[index+1] = stack_pointer;
}
즉, 이건 \0을 넣기 위해 먼저 1을 이동한 것이다.
stack_pointer -= 1;
*stack_pointer = '\0';
그리고 이건 token list에 있는 값을 넣기 위해 먼저 이동하고 메모리 카피를 한 것이다.
stack_pointer -= strlen(token_list[index]);
memcpy(stack_pointer, token_list[index], strlen(token_list[index]));
그리고 그 주소를 저장한다.
pointer_addr[index+1] = stack_pointer;
8바이트 정렬하기 위해 모드 연산으로 나머지 값만큼 패딩을 넣는다.
memset이라는 아이디어는 gpt의 도움을 받았다..
int padding_size = (int)(stack_pointer)%8;
if (padding_size != 0) {
stack_pointer -= padding_size;
memset(stack_pointer, 0, padding_size);
}
그리고 주소값만큼 내려오면서 주소를 저장해준다.
for (int index = 0; index < argc + 2; index++) {
stack_pointer -= sizeof(char*);
*((char**)stack_pointer) = pointer_addr[argc + 1 - index];
if (index == argc) {
argv = (char**)stack_pointer;
}
}
끔찍한 형변환의 현장이다.
그리고 rsp를 갱신해준 뒤에 레지스터에 argv와 argc 값을 넣는다.
이것도 gpt의 힘을 빌렸다.
if_->R.rsi = argv;
if_->R.rdi = argc;
if_->rsp = argv;
load sema up으로 마무리하고 파일을 닫는다.
done:
/* We arrive here whether the load is successful or not. */
sema_up(&thread_current()->self_status->load_sema);
file_close(file);
return success;
file.c, filesys.c, inode.c를 조금씩 건드리긴 했는데
큰 차이는 없는 걸로 안다.