! 주의. project2는 all pass가 되었으나
project3에 에러가 걸린게 이 원인 일 수 있으니 플로우를 참고만 할것.
저번 프로젝트랑 스케일 똑같은데
왜 이번 프로젝트가 더 정리하기 막막한 것일까....
절친한 thread와 이별하고
(엄밀히 따지자면 이별은 덜 했는데)
process에게 놀러갔습니다
분명 process 안에 thread가 여러개 있는거라 배웠지만
PintOS는 process 마다 하나의 thread를 갖게 구현이 되어있어서
thread 구조체를 process 구조체 겸 쓴답니다...
pid-t라는 타입은 있지만
그냥 int를 다른 이름으로 붙였을 뿐
찾을 때도 tid 로 비교한답니다...
그래서 모두가 처음에는 힘들어했습니다
process? load?
argument passing?
아니 프로그램이 실행될때
넘어오는 인자를 내가 한땀한땀
스택 포인터에 쌓아야한다고???
reg에 직접 쌓게 하진 않는 것에 감사해야할 때입니다
(나중에 인자로 레지스터 쓸 땐
엥? 진짜로요? 내가 그런 이론적 개념을 쓴다고요?
를 체감할 수 있음.)
참고로 저는 너무 못 쌓아서
남의 코드 봤습니다 여러분은 포인터 천재되세요
...
그러니까 뭐하는거냐?
foo(3, 4);
에서 3, 4를 직접 인식할 수 있도록
foo의 유저 스택에 받은 argument, 인자를
push 해주는 겁니다.
와!
배운 개념 다 쓴다!
약간 뭔소리지 싶을겁니다.
처음부터 얘기해봅시다.
OS는 어떤 코드를 받아 실행할 때,
사실 우리는 그 코드가 어떤 파일 내에 있고,
그 파일을 실행하잖아요?
파일을 실행할 때 무슨 일이 일어납니까?
메모리에 적당히 저장하고 레지스터로 샤바샤바 연산해서
결과값을 콘솔에 출력해야하지 않겠습니까?
그런데 그거 있지않았습니까?
가상 주소.
예전에 기계로는 메모리가 짱 커도
프로그램을 하나씩 밖에 실행이 못해서
비운의 남은 메모리가 많았죠.
(반대로 짱 큰걸 실행하려고 해도
메모리가 없어서 모자랐을 때도 있었고요.)
그래서 우리는 묘책을 생각했습니다...
있는 것처럼 뻥치자.
프로그램이 처음 접근할 때
보통 어느 주소로 가야하는지
주소가 적힌 테이블 같은거.. 차트 같은 거보고
찾아가잖아요?
그 주소가 적힌 테이블에 구라치자.
실제로 대도시인 것처럼 주소 개많이 주고 찾아오면 그때그때 있던 장소 재단장해서 새로운 장소인걸로 뻥치자.
참 대컴퓨터사기극이 아닐수 없습니다.
그래서 이 얘길 왜했냐,
파일이 뭐냐면
파일 < 앞으로 이러이러한거 할건데 공간 있나용?)
OS < 넹 있습니다(없어도 일단 있다고함 먹고살아야하니까))
하고 OS 수준에서 공간을 배정해주거든요.
(아까 만든 그 주소 테이블)
방문할 장소 외에도 걔가 가져온 물건(인자)도 잘 쓸 수 있게 OS가 관리해줘야해요.
파일끼리 물건섞이면 큰일나니까 전용 사물함을 줍니다.
(전용 사물함 = 유저 스택)
OS < 허용된 공간만 돌아다니실수 있구 가져오신건 제가 파일님 전용 사물함에 넣어놓을게용)
파일 < 헤헤 넹)
이제 파일은 자기가 진짜 일할때
사물함에서 물건을 꺼내다 쓰겠죠?
근데 저희는 파일이 이제 아니잖아요? OS잖아요?
Quest : 전용 사물함에 물건을 순서에 맞춰 넣으시오. 잘 찾아가시도록.
OS < ㅠㅠ )
뭐 이걸 해야하는 거죠.
순서를 봅시다.
태초에
->tid_t process_create_initd 에 들어갑니다.
여기서 뭘하냐?
쓰레드 만들기. (initd라고 시작용을 만든당.)
쓰레드 만든거 tid 번호 받기.
->initd
process exec하기.
->process exec.
여태까지의 context를 process_cleanup으로 삭제하고
process님이 가져오신 파일과 새 컨텍스트를 load하고
process에게 컨텍스트 스위칭을 하기 위해
do_iret을 한다.
헉!
찾았다.
가져오신 파일과 새 컨텍스트를 load 한다니
이 부분이 사물함이겠죠?
그래서 총 세 가지를 합니다.
(1번은 load 바깥에서 process 단계에 thread 생성시에 합니다.)
들어온 값은 (파일명) (인자) 가 한 줄로 이어져있는 string인데,
이걸 가공해야합니다.
1번과 2번은 들어온 string을
파일 이름만 추출해야하니 쉽겠죠?
그럼 3번은 어떤 순서로 넣어야할까요?
정답 : 가이드가(깃북이) 시키는대로 한다.
다 자르고,
데이터를 순서대로 쭉쭉쭉 넣고,
그 데이터를 넣는 순간! 그 데이터가 들어간 주소를 저장해놨다가!
데이터 다음으로 주소를 쭉쭉 넣고
(주소가 끝났다는걸 구분하기 위해 빈칸도 넣고,
데이터 갯수에 따라 8배수로 padding도 합니당.)
맨 마지막엔 return address를 넣지만
(아무튼 확인했다가 돌아가긴 해야할거아냐)
우리는 스레드가 하나니까 구색 맞추기용 페이크로 빈칸을 넣습니다.
...
OK, 알았어.
그럼 어떻게 자르고
어떻게 넣는건데?
여기서 쓰라고 한 내장 함수를 쓴다!
넣는건, 넣어야하는 스택 포인터를 이동하고!
포인터 안에 값을 집어넣고!
하면 된다!
관건은 1. 내장 함수 이해하기 2. 포인터 안에 값을 어케넣어요(심지어 주소는 어케넣는데요) 인데, 2번은 malloc의 프로라면 할수있을 것이다.
나는 프로가 아니라서 패배했습니다
static bool
load (const char *file_name, struct intr_frame *if_) {
...
char f_name_buf[128];
// file naem 전달시 parse
char *name_ptr;
strlcpy(f_name_buf,file_name, sizeof(f_name_buf));
char *f_name = strtok_r(f_name_buf, " ", &name_ptr);
...
힘내서 기존 함수 strtok_r을 쓰는 모습.
왜 128이냐고요?
그건....글자수 제한이 128자거든요.
(다음날 적어서 느낌이 좀 바뀜.)
다들 strtok_r의 동작 방식자체를 이해하려했음.
하지만 사실 나는 그냥 쓰라고하는대로 쓰면 쓰기만하는 사람이라...
주워듣다 대충 알게됨...
아무튼...
이 짓을 thread 생성 시에도 하고,
load에서 file open 시에도 하고,
마지막에는 정성스레 자르는 작업을
argument passing에서 함.
...
/* And then load the binary */
success = load (file_name, &_if);
/* If load failed, quit. */
palloc_free_page (file_name);
if (!success)
return -1;
argument_passing(full_name_buf, count,(void **) &_if.rsp);
_if.R.rdi = count;
_if.R.rsi = _if.rsp+8;
...
load 이후 시점에서 하는거임.
실제로 load 안에 Todo가 있는데,
이상하게 load 하단에서 하면 페이지폴트가 많이 뜨니까
(진짜 이유를 알았었는데 까먹었음)
그냥 모두가 하는대로 success 한 이후로 하는게 좋습니다...
나는 count가 따로 있는데, 굳이 한번 copy해서
띄어쓰기 나오는 순간 전부 잘라서
count만 썼다 버리는 개 사치스러운 일을 하기때문에
여러분은 좀더 효율적으로 사시길 바랍니다
char *save_ptr;
char full_f_name_buf[128];
char full_name_buf[128];
int count = 0;
strlcpy(full_f_name_buf, file_name,sizeof(full_f_name_buf)); // file_name 가져오기
strlcpy(full_name_buf, file_name, sizeof(full_name_buf)); //strtok에서 잘리길래 두개 복사해놓음..
char f_name_arg[128];
char *f_buf;
for(f_buf = strtok_r(full_f_name_buf," ", &save_ptr); f_buf != NULL;f_buf = strtok_r(NULL, " ", &save_ptr)){
strlcpy(f_name_arg, f_buf, sizeof(f_name_arg));
// printf("process exec f_name_arg :%s\n", f_name_arg);
count++;
}// arg 갯수 세기
메모리의 부를 누리며 개사치스럽게 코드를 짜는 모습(위)
부끄러워서 다른 사람에겐
이렇게 짰다고 말하지 않았습니다
아무튼..
사실 다른 사람의 argument passing을 보고 짠거라
함수명도 같아...
코드 내용도 같아...
니가 한게뭐야...싶긴 한데 여기까지 읽었는데
남의 코드라도 좀 보셔야겠죠
맨처음에 리스트 구조체로 token이라는걸 만들어서
arg자체를 리스트로 관리했는데 나중에
리스트 두번째 노드부터 생김새가 이상하더라고요?
스스로의 느린 진도에 멘탈이 털려 다른 사람 코드 보고
다 끝내고 보니 strtok_r을 잘못 이해해서 생긴 실수...
뭐 어때요 개성은 제 몫을 할줄 알때야 개성이지
안 돌아가는 코드는 그냥 개에 가깝겠죠
//포인터의 주소 자체를 수정해야해서, 포인터로 받아온게 아니라 이중 포인터로 받아옴.(포인터의 포인터로.)
void
argument_passing(char *file_name,int count,void **rsp){
char full_f_name_buf[128];
strlcpy(full_f_name_buf, file_name,sizeof(full_f_name_buf)); // file_name 가져오기
char *f_buf, *save_ptr;
int total_size = 0;
char *parse[64];
int count_parse = 0;
// parse에 나눠서 arg별로 저장해두고
for(f_buf = strtok_r(full_f_name_buf, " ", &save_ptr); f_buf != NULL; f_buf = strtok_r(NULL," ", &save_ptr)){
parse[count_parse++] = f_buf;
}
// 한글자씩 1씩 낮춰가며 이동.
for(int i = count-1; i > -1; i--){
for(int j = strlen(parse[i]);j > -1; j--){
(*rsp)--;
**(char**)rsp=parse[i][j]; // char 형태로 넣어야해서 이중 포인터 안에 있는 값을 char 만큼 움직여 값 넣기.
total_size += 1;
}
parse[i] = *(char**)rsp; // 한번 카운트 다 돌때마다 맨 앞글자일테니 본래 자리를 주소로 갱신.
// 주소라서 앞의 *갯수가 하나임.
}
if(total_size%8 != 0){
size_t padding = 8 - (total_size%8);
for(unsigned int k = 0; k<padding;k++){
(*rsp)--;
**(uint8_t**)rsp = 0;
} // 패딩용, uin8_t만큼 움직이자.
// printf("argument passing padding : %d\n", padding);
}
// align 용으로 8칸을 비워놓기 위함.
(*rsp)-=8;
**(char***)rsp = 0;
// 근데 8칸 내내 0으로 채워놔야하는거아닌가? 왜 그 지점만 채워넣지?
for (int i = count-1; i> -1; i--){
(*rsp)-=8;
**(char***)rsp = parse[i]; // char*만큼 움직이며 parse[i]를 넣기.
}
(*rsp)-=8;
**(void***)rsp = 0;
후....
저는 그게 어려웠는데요
포인터를 어케 정적 배열에 넣어
포인터를 정적 배열에 넣는다면
주소 자체는 어케 정적 배열에 넣어
rsp자체를 지금 이중 포인터로 가져와서
현란한 포인터의 마술 중인데
사실 저렇게 일일이 수공예로 넣지 않아도
그냥 if_rsp 형태로 가져와
memcpy를 해도 되는 문제이니 편하게 삽시다
대충 앞에 캐스팅, (char **)같은건 내가 움직여야하는 포인터의... 사이즈고
앞에 **은 값에 넣는지 주소에 넣는지 그런 거겠죠
현재 char ** -> char * -> char 이니까
char **시점으로 char을 꺼내려면 **(char **)고
(역참조해서**로 꺼내되, 포인터는 char**단위로 이동.)
char *를 꺼낸다면 *(char *)인겁니다.
만약 헷갈린다면
char ** -> char * -> char
(이중 포인터, rsp) -> (주소, rsp 위치) -> (rsp위치에 있는 data)로
이해하면 편하실 겁니다.
**(char ***)는?
char *** -> char ** -> char * -> char
이니까
char ***시점으로 char *를 가리키는 거겠죠?
(*rsp)는?
rsp가 char **시점에 있으니까
char *... rsp의 주소값을 변경하는 걸거고?
아무튼 그렇습니다.
쓰면서 저도 이해했네요.
(몇번이나 역참조할거야)(누구 시점에서 이동하는거야)
**(char ***) 식으로 이해하면 좋을지도요...
-> char *** 시점에서 두번 타고 들어간 값을 가져올거임!
이런 식으로!
별개로는
0으로 채우는 부분에서
한꺼번에 휙! 이동하고 일일이 0으로 채우지 않는데요
memset으로 수공예로 하나하나 0으로 채우는 분들도 있으니
저도 그게 맞다고 생각하지만
뭐 한꺼번에 휙 이동해도 test는 잘 통과하며
원래 C의 관례가 안 쓰는 자리는 없는거나 마찬가지로 살기때문에
상관이 없을듯 싶네요...
그리고 argument passing시
rsi나 rdi를 채워주는 것도 꼭! 잊지 맙시다.
OS! 퇴근!
...
옛날옛날에
유저가 있었습니다...
유저는..
자기가 맡길 물건에
자동해킹나노로봇을 심는다는 소문이 돌았습니다...
철통같은 보안 검사로 나노 로봇을 찾아봅시다...
....
그래서 단순히
check address라며
register로 넘어오는 인자가
이상한 주소를 가리키는 건 아닌지,(유저 스택 내인지)
null은 아닌지,
유저 소유 page 출신은 맞는지 확인하는데요,
함수 자체는 왕 간단하지만
이 부분이 학습에 의의가 있는 건
저희가 레지스터 단위로 인자를 받는건 처음이라는 점입니다.
유저 프로그램이 시스템 콜을 호출하면
대충 컨텍스트 스위칭도 일어나고 그래서
메모리 단위로 주고받으면 속도도 느리고 손상 염려도 있으니까
그냥 냅다 레지스터에 싣거든요.
그치만 말로만 듣던 레지스터를 쓴다는건
뭐가 뭔지 막막해지는 작업입니다...
그래서 다들 본래의 syscall 함수를 뜯어보면서
실제로 어떤 과정에서 타고 넘어오는지 확인해야
어떤 레지스터로 인자를 받아야하는지,
어떤 레지스터에 리턴값을 반환해야하는지 확인하지요.
intr_frame이라든가
컨텍스트 스위칭 시 int N으로 커널 모드로 왔다가
do_iret으로 유저 모드로 돌아간다는데,
이상하게 int N이라는 명령어는 없고말이죠...
(나중에 알게되었지만,
syscall이라는 인스트럭션 안에
그 인스트럭션으로 접근을 하면 접근이 허용된
커널 스택의 syscall handler로 넘어오는 주소가
int형으로 적혀있는 겁니다. 내용은 16진수겠죠. 주소니깐.)
그리고 intr_frame 자체도
레지스터에 관한 정보 나 컨텍스트에 관한 정보를 담는다는데
출력해도 영 모를 것이라 모두가 궁금해하고 있습니다
아무튼
user/syscall.c라고 완전히 똑같은게 있는데
내용은 영 달라서 뭐지 이미 구현한건가 싶었는데
userprogram이 호출시 매크로로 시스템콜 핸들러로 넘기는
arg받아 넘기는 것 뿐이었고(정확히는,
여기에서 커널에서 수행하도록 레지스터에 적재합니다.)
userprog/sycall.c로
실제 기능을 수행하는 거였지요...
...
그리고 심지어
기능을 수행하는 함수명을 다 적는 것도 적는건데
받는것도 intr_frame의 f로
레지스터로 받아..!!!
반환도 레지스터로 해...!!!
레지스터는 실존하는걸까요?
아니면 일종의 관념적인 또다른 구라일까요?
아무튼 인자 주고 받는걸 실수하진 않았는지
컨닝 욕구가 제일 많이 느껴지던 부분이었습니다.
저의 check address와
syscall 주고 받기를 공개합니다.
/* The main system call interface */
void
syscall_handler (struct intr_frame *f) {
switch (f->R.rax)
{
case SYS_HALT:{
halt();
break;
}
case SYS_EXIT:{
exit((int)f->R.rdi);
break;
}
case SYS_FORK:{
pid_t ppid = ffork ((const char *)f->R.rdi, f); //이름이 내장함수에 충돌된다고 컴파일이 울어서 고쳐줌.
f->R.rax = ppid;
break;
}
case SYS_EXEC:{
int success = exec((const char *)f->R.rdi);
f->R.rax = success;
break;
}
case SYS_WAIT:{
int status = wait((pid_t)f->R.rdi);
f->R.rax = status;
break;
}
case SYS_CREATE:{
bool is_create = create((const char *)f->R.rdi, (unsigned int) f->R.rsi);
f->R.rax = is_create;
break;
}
case SYS_REMOVE:{
bool is_remove = remove((const char *)f->R.rdi);
f->R.rax = is_remove;
break;
}
case SYS_OPEN:{
int fd = open((const char *)f->R.rdi);
f->R.rax = fd;
break;
}
case SYS_FILESIZE:{
int size = filesize((int)f->R.rdi);
f->R.rax = size;
break;
}
case SYS_READ:{
int byte_read = read((int)f->R.rdi, (void *)f->R.rsi, (unsigned int) f->R.rdx);
f->R.rax = byte_read;
break;
}
case SYS_WRITE:{
int byte_write = write((int)f->R.rdi, (const void *)f->R.rsi, (unsigned int)f->R.rdx);
f->R.rax = byte_write;
break;
}
case SYS_SEEK:{
seek((int)f->R.rdi, (unsigned int)f->R.rsi);
break;
}
case SYS_TELL:{
unsigned int position = tell((int)f->R.rdi);
f->R.rax = position;
break;
}
case SYS_CLOSE:{
close((int)f->R.rdi);
break;
}
default:
exit(-1);
break;
}
컴파일이 캐스팅 안됏어 이거맞아?? 하고 백번 물어봐서
저야 캐스팅 해줬지만 안해줘도 무방하다네요
void
check_address(const uint64_t *addr){
if(addr == NULL){
exit(-1);
}
if(!is_user_vaddr(addr)){
exit(-1);
}
if(pml4_get_page(thread_current()->pml4, addr) == NULL){
exit(-1);
}
}
다른 사람은 쿨하고 시크하게 || 연산자로 처리했다고합니다.
흠... 맞는말이야...
이 함수 자체를 어느 시점에 쓰는지도 헷갈렸는데요
int 는 값 자체라 이걸로는 안하고
포인터로 buffer등이 넘어오는 systemcall 케이스에서
그 포인터 자체를 check address 자체에 넣어주면 된답니다.
먼 옛날..
다들.. check address는 함수를 만들면서
addr로 뭘 받는건지.. 어딘가 addr이 실존은 하는건지
만들어는 놨지만 울면서 이거모임 ㅠㅠ 하던게 엊그제같군요...
다들 당연히 알거라고 생각하고
또는 생각할 거리를 못 주고 다 적는게 문제일까
하지만 이 글을 읽는 시점에선 답이 필요하실 거라 믿습니다
한시간 반 동안
argument passing과 user memory를 적었다니 거짓말
여기가..
이제....시작인데.....
...
PintOS kaist 강의가 유튜브에 있다는 것 아실겁니다.
교수님 감사합니다. 제가 덕분에 이론 이해하고 코딩합니다.
systemcall 이라는 강의도 있지만
file manipulation이라는 강의도 있습니다.
근데 둘다 systemcall에 구현하라는 함수가 나와서
뭘 봐야하는 건지 헷갈리실 겁니다.
정답은.....둘 다.
그래서 이론 정리 하루하고
다음날 이론 이해 못한거 찾아보기 반나절 하고
이론만 이틀 걸린 거같은데 아깝게 생각하진마세요
어차피 다 써요...
systemcall은
아마 exec, wait, halt, exit을 다룰거고
크래프톤 사람들은 아시다시피 fork를 받으실텐데
강의는 exec이 fork와 유사하다 어쩌고하지만
아무튼 fork는 강의에서 안 다루니깐
블로그를 찾으며 은혜를 얻으시길 바랍니다.
이 PintOS를 제공해주신 교수님이
이거 내고 그악하다는 평을 받았다는데(못했다기보단 난이도 헬로)
좀 동감합니다...
fork가 구현 량이 많은데
그거 구현 못하면 wait, exec 테스트 케이스가 fork 써서 디버깅 못함.
근데 사실 process wait 자체를 구현하지 않으면
아무것도 디버깅 못함.....
while(1){} 이나 timer sleep같은걸
process wait 자체에 꼼수로 끼워넣고
open, close등을 먼저 디버깅 하고
나머지 케이스를 디버깅 했다고 합니다.
물론 process wait를 먼저 하는게 정규겠지만
쉬운거 먼저해서 자신감 찾는것도 방법이죠...
wait 뭔지도몰겟고...
...
그리고 fork를 하면
멀쩡한 project1도 죽기때문에
다들 멘탈이 펑펑 터집니다.
심지어 fork는 블로그 코드를 긁어와도 바로 안되는 그악한 친구입니다.
그렇담 가봅시다.
void
halt (void) {
power_off();
}
...짱!
끝. 헤더 잘 포함해줍시다.
void
exit (int status) {
// pid에 exit status 저장
// 딱히 pid 가 없어서 thread... 안에 저장할까싶다
struct thread *curr = thread_current();
curr->exit_status = status;
printf("%s: exit(%d)\n", curr->name, status);
thread_exit();
}
Process Termination Messages가 여기있네요.
얘 pid 에 저장하라고해서
제가 pid를 찾아 떠나는 모험을 했지만
딱히 누구도 갖고 있지 않고
찾아보니 tid= pid 식으로 씁니다.
지금 현재 쓰레드가
듣도보도 못한 exit status를 가지고 있죠?
예. 만듭니다.
이개 뭐냐면
스레드 종료시 대체 무슨 상태로 종료되었는지
다른 스레드는 파악할 필요가 있기때문에
스레드 자체에 exit_status라는 Int값을 저장해두고
나중에 wait해서 회수할때 저 값을 가져옵니다.
대강 이건 -1이니 재수없게 뒤졌겠군... 하는거죠.
처음에 exit_status 개념 자체가
대체 뭔지 몰랐던 기억이 새록새록 하네요.
상태 코드...니가 처음이야...
제가 지금 쓰는 순서대로 구현한건 아닙니다.
그건 어차피 TIL에 녹아있습니다.
int
exec (const char *file) {
check_address(file);
tid_t tid;
char * f_name_copy;
f_name_copy = palloc_get_page(PAL_ZERO);
if(f_name_copy == NULL){
exit(-1);
}
strlcpy(f_name_copy, file, strlen(file)+1);
tid = process_exec((void *)f_name_copy);
if(tid == -1){
exit(-1);
}
}
오우! check address가 여기있네요. 잘 지내니?
그러겠지...
file 저거,
내가 실행할 파일 이름입니다 파일이지만 진짜 아직 파일은 아니고요.
지금 굳이 process_exec을 호출하는데
f_name_copy니 뭐니
palloc이니 뭐니를 하고있죠.
블로그발로는 (const로 받아서 안 잘려서 그렇당..ㅠ)
이라는데 실제로는 const가 상수라 안 잘리는게 맞고
맞는데 잘렸던거같은데 가물하고 확인하기 귀찮네요
아무튼 palloc을 하는 이유라 하면
지금은 시스템 콜, 즉 커널 모드에서 실행하는거라
애시당초 유저 프로그램 컨텍스트라면
맨처음 load 시에 이미 다 자기가 쓸 page를 배정받았겠지만
시스템 콜의 f_name_copy는 신입이지 않습니까?
새로 page 주셔야죠. 얘 괴롭히면 안됩니다.
아무튼 이걸 안하면 exec에서 터지는데
또 fork 할때만터진다나 뭐라나.
뭔가 안될때 천지신명님에게 빌며 해봤을때 나름 효과가 있는 요법입니다.
tid가 -1일때 바로 exit(-1)로 종료를 해주는것도
테스트 케이스에 효험이 있는거니 믿어보셔도 됩니다.
(-1자체를 리턴하면 bad...관련 뭔가 들어간 부분에서 케이스 이슈가 있나봅니다.)
하긴 안되면 끝내주는게 맞긴 하겠죠?
아예 page fault 레벨에서 exit(-1)을 해도 된다고는 하네요.
int
wait (pid_t pid) {
return process_wait(pid);
}
^^
끝!
근데 process_wait가 구현 안되어있죠?
고통받죠? 괴롭죠?
wait 먼저 구현했다가
process_wait와 로직이 같으며
wait와 exit sema 를 맞추느라 피보는 사람이 많으므로
깃북이 하라는대로 wait를 process wait로 넘기고
그걸 구현하는게 좋습니다.
int
process_wait (tid_t child_tid) {
struct thread *child = get_child_process(child_tid);
if(child == NULL){
return -1;
}
sema_down(&child->wait);
int exit_status = child->exit_status;
list_remove(&child->child_elem);
sema_up(&child->free_wait);
return exit_status;
}
코드 짧네요 쉽네!
무슨소리에요 눈물의 디버깅 printf가 사라진 흔적입니다
저는 get_child_process니 뭐니
새로 함수를 만드는 일을 탐탁치않게 보지만
모듈화는 원래 그런거겠죠. 조금이라도 다르고 싶었지만
디버깅에 걸려있으면 그냥 울면서 만들게 됩니다.
struct thread*
get_child_process(int pid){
struct thread *cur = thread_current();
struct list_elem *e;
for(e = list_begin(&cur->child_list); e != list_end(&cur->child_list); e= list_next(e)){
struct thread *t;
t = list_entry(e, struct thread, child_elem);
if(t->tid == pid){
return t;
}
}
return NULL;
}
그러니까...
그렇습니다
그냥 child_list를 돌면서
child_elem을 꺼내어
tid와 pid가 일치하면 반환하죠?
그렇다니까요? tid랑 pid 같이 쓴다니깐요?
딱히 타입이 pid_t인건 아니지만...^^
근데 왜 갑자기 child_list며
child_elem이냐고요?
그건....
슬픈 과거는...
없고요
결국 어느 프로세스 내에서
새로운 프로세스를 create하면
PID를 물려받는게 추상화의 정통이거든요.
그래서 저는 tid자체를 똑같이 물려받나 고민했는데
그러면 손자랑 증손자랑 자식이 구분이 되지않아....
그냥 tid를 따로 배분하되
강의가 하라는 대로 child list와 child elem을
thread 구조체 내에 만들어
thread 별로 각자 자식을 족보로 관리하고 있답니다...
그래서 내가 wait할때 내가 기다리는애가 진짜 친자식인지 남의 자식인지 알수있는거죠...
list_init 시점과 list_push_back으로 넣는시점은
잘 아실거라 생각합니다
(후자는 create시점에 넣으면 되겠죠?)
왜 가이드가 일일이 안 알려주는지 알겠어요
쓰기 귀찮아..
아무튼 process wait를 다시 봅시다
int
process_wait (tid_t child_tid) {
struct thread *child = get_child_process(child_tid);
if(child == NULL){
return -1;
}
sema_down(&child->wait);
int exit_status = child->exit_status;
list_remove(&child->child_elem);
sema_up(&child->free_wait);
return exit_status;
}
좋아 자식 가져왔어
좋아 자식 null이면 반환시켜
좋아 wait를 sema down.....
.......
...그렇습니다.
얘를 대기 시키는전략은 앞서 sleep thread 에서는 block 시켜
sleep list로 관리했으나
여기서는 sema로 무한 대기 시켜버립니다.
그래서
child < 압바 나 exit 했어!)
father < 어어 아빠 wait 하고 있었어)
할때까지 기다리는거죠...
오케이 알겠어
그리고 자식이 왔으면
(사실 저 wait는 child것을 기다리든 부모 것을 기다리든 상관없습니다. 같은 sema기만 하면 어차피 down 을 원하는 놈은 걔가 기다려야함...)
exit_status를 받아놓고?
fre....뭐? 를 up하고?
exit staus를 리턴해?
왜 세마가 두 개인거죠?
어서오세요 메모리누수 방지 exit_status 세계에...
저는 exit status를 수집하는게 유의미한건지
죽은 자식프로세스가 진짜 죽었는지 확인하는게 유의미한건지
잘 모르겠지만 아무튼 thread가 언제 wait하든
자식이 언제 exit 되어도 exit_status를 받아야한다는 구절이 있거든요
그래서 한번 더 sema를 겁니다.
exit_staus를 부모가 완전히 받을때까진
자식 시체를 보관하는거죠.........
그래서 process_exit에는
sema-down으로 자식 시체를 보관하는 구절이 있답니다...
부모가 자식 시체 데려가면 sema up됨...
자식이 먼저 죽는건 큰 불효인 유교사회에서
정말 무시무시한 개념이 아닐...이쯤하고
void
process_exit (void) {
struct thread *cur = thread_current();
sema_up(&cur->wait);
file_close (thread_current()->running);
for(int i = 2; i <63; i++){
file_close(cur->fdt[i]);
}
struct list_elem *e;
while(!list_empty(&cur->child_list)){
for(e = list_begin(&cur->child_list);e != list_end(&cur->child_list); e= list_next(e)){
struct thread *t = list_entry(e, struct thread, child_elem);
process_wait(t->tid);
}
}
sema_down(&cur->free_wait);
process_cleanup ();
}
음 뭔가 많아
차근차근 봅시다
처음은 wait한 애보고 잘기다렸어 올리는 wait고,
file_close는 이따 봅시다.
(deny write 개념입니다.)
for로 file 전부 close해주는건
95 케이스 중최종보스인 multo-oom 용이고요.
(그 아래 while도 포함입니다.)
free_wait semadown... 있다!
아 설명할게 없네
아무튼 close는 상식적으로 해줘야하는
메모리 누수 케이스지만,
구현이 덜 된상태에서는 페이지 폴트의 주범이니
마지막 케이스까지는 뭔 일 생기면 그냥 주석처리 해둡시다.
bool
create (const char *file, unsigned initial_size) {
check_address(file);
lock_acquire(&filesys_lock);
bool is_create = filesys_create(file, initial_size);
lock_release(&filesys_lock);
return is_create;
}
오케이...
create니 혹시 이상한 파일 이름 준건 아닌지 확인 먼저 하고?
lock....
filesys_lock? 언제 이런게 있었어요?
없었습니다
만듭니다.
file create는 상관있나없나 잘 모르는데
아무튼 filesys는 동시에하면 곤란한 케이스가 좀 있습니다.
읽고 있는데 옆에 얘가 아래 쓰고 있으면
저의 아기돼지 삼형제가
아기늑대삼남매가 되어있을지 어떻게 압니까.
그래서 syscall 파일 단위에서
handler 에서 lock을 init하고
filesys 관련을 쓰면 그때그때마다 lock을 얻었다 쓰는 거죠.
쓰라는 함수로 create를 합니다. 끝!
bool
remove (const char *file) {
check_address(file);
lock_acquire(&filesys_lock);
bool is_remove = filesys_remove(file);
lock_release(&filesys_lock);
return is_remove;
}
쓰라는 함수를 쓴다. 끝!
int
open (const char *file) {
check_address(file);
lock_acquire(&filesys_lock);
struct file *open_n = filesys_open(file);
lock_release(&filesys_lock);
if(open_n == NULL){
return -1;
}
int new_fd;
new_fd = thread_current()->next_fd;
if(new_fd == 63){
return -1;
}
thread_current()->next_fd += 1;
if(new_fd <2){
new_fd = 2; // 0이하가 인식 안되어 꼼수.
thread_current()->next_fd = 3;
}
thread_current()->fdt[new_fd] = open_n;
return new_fd;
}
왜 일까요?
next_fd는 thread 구조체에 속해있으며,
저는 thread create 시점에 init할때
2로 미리 해놓는데(0과 1은 stdin과 stdout이 쓰니까...)
syscall handler로 넘어오면
언제 init 했냐는듯 새초롬하게 0을 출력하더군요
누가 알아냈나 싶었는데
다들 당연하지 않냐는듯 그냥 여기서 2를 주더군요
알게되면 댓글주세요
아무튼... 앞부분까지는다른 함수와 똑같지만
null인 경우를 제외했고요,
next_fd는 원래 fd에 null이 자리가 있는곳을
while로 돌아가며 찾아야하지만
간단히 이전에 2에 열었으면 다음은 3이겠지..하는 전략입니다
그래서 열때마다 다음에 열 fd를 갖다쓰지요
물론 단점은
중간에 뭐 닫힌 파일이 있어도 못 찾아가고
걍 fd 자리없어ㅠ 하고 우는
있는 메모리도 못쓰는 바보같은코드지만
테케는 다 잘됩니다.
여기서 페이지 폴트가 자주 뜬다면
없는 file을 open하려고했을때에 가까울겁니다.
int
filesize (int fd) {
struct file *target_file = thread_current()->fdt[fd];
lock_acquire(&filesys_lock);
off_t size = file_length(target_file);
lock_release(&filesys_lock);
return size;
}
...짱!
int
read (int fd, void *buffer, unsigned size) {
check_address(buffer);
lock_acquire(&filesys_lock);
if(fd == 0){
unsigned count = size;
while(count--)
*((char *)buffer++)= input_getc();
lock_release(&filesys_lock);
return size;
}
else if(fd == 1){
lock_release(&filesys_lock);
return -1;
}
if(fd <64){
struct file *target_file = thread_current()->fdt[fd];
off_t byte_read = file_read(target_file,buffer,size);
lock_release(&filesys_lock);
return byte_read;
}
else{
exit(-1);
// 64 이상이나 이하의 fd를 가져왔다면 리턴.
// 또 target_file 자체가 존재하는지도 확인해야함.
}
흠
네
fd가 0이면 stdin이기때문에
키보드에게 한줄한줄 받아와 넣고 있는 모습입니다
하지만 깃북은 이러면 여러 스레드가 하나씩 출력하면
왕못생겨지니 큰 buf로 한꺼번에 전달하라고했으나 개무시하는 모습입니다
fd가 1이면 stdout인데
애초에 stdout을 열 일은 없겠지만
user는 호기심대마왕이니 그럴수도 있죠? 바로 죽여줍니다.
그리고 들어오는게 fd 자체라
실제 넘기는 fd자체가 불온한 놈은 아닌지
64 이하인지 확인해야합니다.(0이상인지도 확인해야겠죠 정규적으로는. 뭐야 나 왜 통과했지?)
용케 file 없어 이자식아, 즉
targer file이 null일 케이스를 안 빼고도
all 통과했군요.
역시 점수 받는 비법은 세세하게 잘한다는게 아니라
큰걸 맞는다 이겁니다.
(나름 구현하려고 주석은 써놨네요.)
int
write (int fd, const void *buffer, unsigned size) {
check_address(buffer);
lock_acquire(&filesys_lock);
if(fd >64 || fd <0){
lock_release(&filesys_lock);
exit(-1);
}
if(fd == 1){
putbuf(buffer, size);
lock_release(&filesys_lock);
return size;
}
else if(fd == 0){
lock_release(&filesys_lock);
return -1;
}
else if(!is_user_vaddr(buffer+size)){
lock_release(&filesys_lock);
exit(-1);
}
else{
struct file *target_file = thread_current()->fdt[fd];
if(target_file == NULL){
lock_release(&filesys_lock);
return -1;
}
off_t byte_write = file_write(target_file,buffer,size);
lock_release(&filesys_lock);
return byte_write;
}
}
fd 이상해?
죽인다...
fd가 1이야?
콘솔에 putbuf 해준다...
fd가 0이야?
죽인다...
저 두번째 else if는 쓸모없는 짓거리인데 안지웠군요
혹시 저의 수호부적일지도 모르니 전 안지우겠습니다
다 아니면 file을 찾아준다..
(null이면 죽인다..)
쓴다..
끝!
참 쉬워보이지만 다들 많이들 죽습니다
고려해줘야할 Null처리도 좀 있고 여러개를 연계해서 해서
write 문제인지 다른 문제인지 모른다는게 이 디버깅이 망할 묘미입니다
void
seek (int fd, unsigned position) {
struct file *target_file = thread_current()->fdt[fd];
if(position > (unsigned int) filesize(fd)){
return ;
}
else{
lock_acquire(&filesys_lock);
file_seek(target_file, position);
lock_release(&filesys_lock);
}
}
seek한다...
근데 네가 찾는 그 위치가 내 파일 사이즈보다 크면
없는 거니까 돌아가....
끝!
unsigned
tell (int fd) {
struct file *target_file = thread_current()->fdt[fd];
lock_acquire(&filesys_lock);
off_t position = file_tell(target_file);
lock_release(&filesys_lock);
return (unsigned)position;
}
어디인지 tell 해준다...
끝!
void
close (int fd) {
struct file *target_file;
if(fd >64 || fd <0){
exit(-1);
}
if(thread_current()->fdt[fd] == NULL){
exit(-1);
}
target_file = thread_current()->fdt[fd];
if(target_file == NULL){
return 0;
}
thread_current()->fdt[fd] = NULL; // 닫았으니 null로 초기화
lock_acquire(&filesys_lock);
file_close(target_file);
lock_release(&filesys_lock);
}
fd 이상해?
죽인다...
파일 비었어?
죽인다...
이자식은 왜 같은말을 두번이나 if로 한건지
아주 요지경 코드로군요
그냥 수호부적 민간신앙이라 칩시다
닫았으니 fdt를 지워준다...
close해준다...
끝!
pid_t
ffork (const char *thread_name, struct intr_frame *f){
return process_fork(thread_name, f);
}
왜 이름이 이모양이죠?
포크 떠서 늘어난다고 f도 늘어난걸까요?
아닙니다..
우리 친구가 intr_frame을 추가 인자로 받으면
하면 안 되는 짓인지 conflict type이라고 합니다.
이름을 바꾸라는거죠. 검색해보면 어느곳에도 fork가 없지만
컴파일은 그냥 달래줍니다.
사실 이름을 바꿔도 안될겁니다.
intr_frame 구조체를 포함한 헤더파일을 include 해줍니다.
도의적으로는 그럼 못 찾는 구조체라고 해야할거같은데
컴파일의 마음은 프로그래머가 알수없습니다.
(실제로는 함수 인자를 바꾸는게 찝찝하다면
시스템콜 핸들러 안쪽에서 Intr_frame을 memcpy해주면
fork는 원래의 모습으로 행복하게 살아갈 수 있습니다.)
아무튼 process_fork를 해준다! 끝!
일리없지...
이제 그걸 구현합니다...
process_fork (const char *name, struct intr_frame *if_ UNUSED) {
struct thread *cur = thread_current();
memcpy(&cur->parent_if, if_, sizeof(struct intr_frame));
/* Clone current thread to new thread.*/
tid_t tid = thread_create (name, PRI_DEFAULT, __do_fork, cur);
if(tid == TID_ERROR){
return TID_ERROR;
}
struct thread *child = get_child_process(tid);
sema_down(&child->fork_wait);
if(child->exit_status == -1){
return TID_ERROR;
}
return tid;
}
UNUSED?
use할 겁니다.
자자 보십쇼.
맨처음에는 받아온 intr_frame을
우리 부모 쓰레드 옆구리에 놓고갔다고 끼워줍니다.
(왜? 나중에 나옵니다.)
그리고 당근 우리 자식을 thread create하지요.
걔는 do fork라는 화끈한 루틴으로 들어가네요.
그리고 제가 아까 만든 그 child를 기용해야하는데
아는게 tid 뿐이지만
우리에게는 든든한 child list가 있습니다.
thread create 시점에 들어갔겠죠?
그걸로 족보 조사해서 잡아옵시다.
그래서 우리 자식이 진짜 fork를 다 끝낼때까지
아빠 먼저 가버리면 안되니깐
자식 락 붙잡고 sema down으로 멍때리다가
우리애가 재수없게 죽었으면 실패했다고 해주구
잘 fork하고 오면 tid를 우리 유저에게 우리 애 이름이라고 알려줍니다.
그렇습니다. fork도 대기타야해요.
sema만 세개인 프로젝트에요. 헷갈리지 않게 조심합시다.
실제로 헷갈린 어느 팀원은 디버깅으로 실려갔습니다.
child process를 돌아 tid로 가져오는거야
그게 그거일테니 넘어가고요.
관건은 그거입니다
fork는 하는순간
컨텍스트를 그대로 받으므로 자식도 이 process fork를 탑니다.
근데 자식이 벌써 손자를 보면 곤란하잖아요?
저흰 계획 자식 어쩌고라고요?
그래서 자식은 자기 애를 만들지 않도록 tid를 0을 반환합니다.
그걸 어떻게 하냐?
부모 프로세스의 컨텍스트를 받아와
rax에 0으로 리턴...
갑자기 납득이 안되네 쓰다보니까
아 알겠습니다
부모는 thread create는 해도 do fork는 타지 않습니다.
그래서 그대로 받아온 tid를 가져오겠죠.
그렇지만 자식은 do fork를 타는데요.
걔도 do fork가 대충 끝나고 돌아올텐데
똑같이 return tid로 귀결하는게 정통이겠지만
do fork 내에 리턴 값 반환하는 부분 rax에 0을 넣어버립니다
시스템 콜이 원래 리턴 받는 부분을 똑같이 나오지 못하도록
0으로 덮어 씌워버려 완벽 범죄를 하는겁니다..
그래서 자식과 부모를 다른 일을 시키고 싶으면 보통
Pid == 0인지 확인합니다.
그럼 정말 그런 일을 하는지 do fork를 봅시다.
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;
bool succ = true;
parent_if = &parent->parent_if;
memcpy (&if_, parent_if, sizeof (struct intr_frame));
if_.R.rax = 0;
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
for (int i = 0; i < 64; i++)
{
struct file *file = parent->fdt[i];
if (file == NULL)
continue;
struct file *new_file;
if( i == 63){//맨끝에 정체불명 주소가 생겨서 스루...근데대체왜생기는거임?
continue;
}
if (file > 2){
new_file = file_duplicate(file);
}
else{
new_file = file;
}
current->fdt[i] = new_file;
}
current->next_fd = parent->next_fd;
sema_up(¤t->fork_wait);
process_init ();
/* Finally, switch to the newly created process. */
if (succ)
do_iret (&if_);
error:
succ = false;
sema_up(¤t->fork_wait);
exit(-1);
}
길다..
원래 원본 자체가 긴 코드입니다.
지금 보시면
부모 컨텍스트 물려받은
parent_if라는 것에 parent_if를 memcpy를 하는데
이게 뭐냐면
스레드 구조체 자체에 parent_if라고(이름은아무렇게나 지으십쇼)
intr_frame을 애초에 달아줬던거
아까 챙겨준걸 지금 살뜰하게 if에 넣어서 쓰고있죠.
부모 레벨에서 리턴값이 두개일텐데,
(그야 부모 컨텍스트에서 fork 를 불렀으니깐...)
여기선 레지스터를 빌려와 부모가 먼저 돌아갔을테니
자기가 나중에 rax를 0으로 차지하는 모습입니다..
모두가 저 parent_if가 왜 부모 컨텍스트를 유지하는지 궁금해하는데
아마 syscall 당시 intr_frame은 그 넘어오는 순간에
참고하라고 호출한 프로세스의 컨텍스트가 담겨있어서
thread 구조체 자체에 있는 tf와는 다른 intr_frame이라더군요.
tf자체는 자기 자신보단 이전에 컨텍스트 스위칭한 걔라나 뭐라나.
이후 구현해야할 부분은
else 안, pml4_for_each 안에 있는
duplicate pte,
endif 이후
for문부터 process init 전까지 일겁니다.
(error로 빠지는 부분도 적당히 잘 추가해줘야겠죠.)
pml4_for_each부분은
부모의 페이지맵을 가져오는 거고요,
for문 부분은
부모의 파일 디스크럽터 테이블을 가져오는 겁니다.
전자는 자기 자신, 코드 자체의 데이터라면
후자는 자기가 열었던 관련 파일을 가져오는 거죠.
아마 fork를 하나라도 오케이하고싶다면
file duplicate보단 앞의 pte가 더 중요하겠죠?
저는 납득할수 없는 에러를 마주하여
pml4가 어떻게 pte와 va를 만나는지 보고말았습니다...
(duplicate_pte에 들어가면
인자로 갑자기 pte와 va를 받는데
사실 pml4 for each에서 넣는 값은
pml4에 대한 포인터와 parent라는 thread 부가 정보밖에 없지않습니까)
dulpliate에 가면
기다렸다는듯 친절하게 todo가 다 적혀있는데요
저는 그걸 믿고 썼지만
당연히 함수가 다 틀렸습니다
당연합니다 전 pml4와 제법 서먹합니다
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;
if(is_kernel_vaddr(va)){
return true;
}
parent_page = pml4_get_page (parent->pml4, va);
if(parent_page == NULL){
return false;
}
newpage = palloc_get_page(PAL_USER | PAL_ZERO);
if(newpage == NULL){
return false;
}
memcpy(newpage, parent_page, PGSIZE);
writable = is_writable(pte);
if(!pml4_set_page(current->pml4,va, newpage,writable)){
return false;
}
return true;
}
1 맨처음은 커널 출신인지 확인한다
2 부모의 페이지를 가져온당
2 자식 페이지를 새로 할당해준당
3 자식페이지에 부모 페이지를 복사해준당
4 쓰기 가능 여부를 writable에 저장해준당
5 자식 pml4에 페이지와 writable을 추가하고 실패했다면 에러처리를 해준당
간단히 말하면
부모 페이지로 자식페이지 만들어서 페이지맵에 풀칠하기
정도가 되겠군요
저는 부모 페이지를 set으로 가져오는지 get으로 가져오는지 헷갈렸고
자식에게 새 페이지를 할당한다는게 get인가 헷갈렸고(palloc이 아니라 pml4로)
복붙한다는건 대체뭐야했고
...
아마 마지막 정도는 맞았을걸요..
첫번째 커널 출신이면 바로 리턴한다도
대체 무엇 기준으로 커널 출신인지 판단하는지 괴로워했죠
지금 보면 너무나도 명확하고 간단한 코드입니다
PAL USER나 PAL ZERO가 뭔지 모르고
일단 넣었던 때가 새록새록하군요.
뭐냐면 유저 영역에 만듦 | 0으로 초기화함 입니다.
그렇겠지......
그리고 은근 커널출신인지 판단한다..를 알려면
애초에 저 pte와 va는 어디서 굴러들어왔는지 알아야했죠
대충 모르고 블로그 코드 참고했는데
저는 저기서 무수히 에러가 걸리더라고요
납득할수없었습니다
다들 이런 에러를 겪어서 조치를 취했다면
블로그에 적었을거 아녜요??
근데 내 코드만 아프다??
1 project1을 잘못짰다
2 아무튼 니코드는 안됨
휴.. 그래도 물어보려면 합리적으로 물어봐야해서
실제로 어디까지 유추를 했는지 말해야하므로
치열하게 알아내고 질문 올린 순간 이후부터 되었던 때가
(답변도 안들었는데) 새록새록하군요.
(뭘했냐고요?
어디서 에러가 빠지는지 궁금해서
printf를 찍었었는데
그거 지우니까 됩니다 안되어서 printf를 찍은건데
말이됩니까? 결론은 저도모릅니다)
pml4를 받아서 pml4 for each가 하는건
말 그대로 페이지 맵을 4번 타고 들어가
pte를 받아오고, 4번 타고 들어가서 얻은 각자의 offset으로 합체하여
va를 만듭니다.
pml4 자체는 페이지 맵이니 당근 커널 스택이겠지만
va와 pte는 어느 출신 스레드냐에 따라 커널인지 유저인지가 갈리겠죠.
(어떤 분의 발표로는 우리 애들은 전부 커널 스택에 있고
tick이 user tick이 올라가느냐 kernel tick이 올라가느냐로
갈린다는 군요...)
(조교님은 커널 스택에 있는건 당연히 OS가 접근 가능하니
굳이 두개나 만들 필요가 없어 바로 true로 리턴한다고 합니다.)
여하튼 완전 머리싸맨 에러인데
해결도 에러같지않게 해결하고
집에나 가고싶어도 이해해줍시다.
file duplicate 파트를 봅시다.
for (int i = 0; i < 64; i++)
{
struct file *file = parent->fdt[i];
if (file == NULL)
continue;
struct file *new_file;
if( i == 63){//맨끝에 정체불명 주소가 생겨서 스루...근데대체왜생기는거임?
continue;
}
if (file > 2){
new_file = file_duplicate(file);
}
else{
new_file = file;
}
current->fdt[i] = new_file;
}
current->next_fd = parent->next_fd;
간단합니다
부모 fdt를 돌면서
파일을 duplicate해가지구
자식 fdt에 추가해준당
끝!
이상하게 에러가 걸리실겁니다
안 걸리면 축하드립니다 축복받은 컴퓨터입니다
저는 이상하게 에러가 걸려서 디버깅을 했습니다
정적 배열로 구조한 자료가 가뜩이나 없는데
안되어서 동적 배열을 할지 고뇌했습니다
근데 사실 처음부터 동적배열이 좋겠죠
file share 까지 구현한다면요...
extra file descriptro도 그렇고..
슬픔..
아무튼
그래서 저는 fdt[i]에 있는 file의
주소를 전부 찍어보고...
했는데 이상하게 맨 마지막에 요상한 값이 늘 들어있더라고요?
대체 왜?
왜?
이걸 어떻게 디버깅하라고?
갑자기 제 맨마지막 인덱스에 3이라는값이 들어잇어용 ㅠㅠ
아무도 해결하지 못할 개인적인 에러군요 탈락
심지어 정적배열 칸을 늘려도 항상 맨끝에 있고
줄여도 맨끝에만 있고
항상 3만 들어있는것도 아닙니다 듣도보도못한 꽤 심플한 쓰레기값이 삽니다
웃긴건 저는 init 시점에 모두 null로 이미 초기화해주고있어서
그냥 눈물만 흐른다는 점이죠
그래서 원래 3이라는 주소값이라면 스루하게 했는데
그걸로 하면 fork를 여러개할시 3말고 다른 이상한 값이 들어있는지
여러개 fork가 안되는 개무능한 코드가 되었는데
혹시나해서 맨 마지막 인덱스를 스루하니
우리애가 좀 아프지만 일은 할줄아는 코드가 되었습니다
참 낯부끄러운 코드입니다
아무튼 이후
fork wait가 잘 들어있는건 보셨을거고
이만 syscall은 하산하셔도됩니다..
...
대체 뭘 denying 하라는건지
정답은
현재 실행중인 스레드가 연 파일을
누가 갑자기 옆에서 써버리면 안되니까
load에서 열었던 file을 deny_write 같은 함수를 쓰라는 얘기입니다.
그리고 file close를 한다면 어차피 allow_write가 포함되어있으니
close나 적절히 하라는 얘기죠.
그래서 저는 사람들이 running이니 self니
thread 구조체에 자기 파일을 넣고 다니는 이유를 알게되었습니다.
나중에 닫을라고....
그래서 thread 구조체에 잘 추가하고
load 시점 file open 이후에 자기 옆구리에 챙겨놓고
process exit 시점에 옆구리에 챙겨놓은걸 file close하면 됩니다.
구조체에 포인터 형태로 파일을 추가하지않게 조심합시다.
누군가가 또 여기서 슬프게 디버깅을 했습니다..
음...
아마 뭐 기술적인 이슈가 없다면
fork는 되는데 fork 횟수가 부족할겁니다
메모리 누수 확인용이죠
그래서
1. fdt를 돌아 파일을 다 닫는당
2. 자식 리스트를 돌아 자식을 다 wait해서 자식을 완전히 없앤당
을 하면되는데
이거야 뭐 process exit 파트 보시면 될거고
이상하게 저는 그때쯤 1회정도 모자랐는데
(개열받죠? 포기하기엔 아깝고 할수있는건없죠?)
free wait를 process cleanup 위로 올렸더니
완치됐더라고요
아무튼 잘 회수되는지 확인합시다...
하지만 이 케이스만큼은
여태까지의 구현에 따라 디버깅 이슈가 완전히 다르므로
적당히 모든 블로그를 참고하시길 바랍니다..

전 사실 테스트가 95개일때
(음... all pass야 하고싶지만 못해도 괜찬을듯)
했는데 어떻게 되긴 했네요
블로그의 계시를 많이 받았습니다
비리가 아닐까 고민했지만 할수있는건 다해보고 비리한거니까 괜찮습니다 저자신은.
건승하시길.
끝!