[sw정글 12주차] parse_path()를 만들면서 마주하는 문제들과 그 해결책
카테고리: swjungle survive
🤩이번주는
FAT 파일시스템 구현과, Sub-directory 구조 구현을 했다. FAT파일시스템을 만들면서 배운 내용도 많지만, 이번 글에서는 parse_path()를 만들면서 고려했던 디자인 사항들에 대한 이야기를 해보도록하겠다.
그럼에도 불구하고 FAT파일시스템 관련 정리한 자료들이 아까우니 아래 토글에 간단히 첨부하도록 하겠다
FAT파일시스템 구현의 핵심 함수들
FAT파일시스템을 만들면서 그 역할이 헷갈렸던 함수 두가지를 설명하도록 하겠다. 처음에는 offset만큼 읽어내는 함수인줄 알았지만 이해를 완전히 잘못하고 있었다. 따라서 함수들과 그 역할을 설명해보도록 하겠다.
첫번째로 inode_read_at() 함수이다.
해당 함수는 4가지 인자를 입력으로 받는다. byte_to_sector()함수를 통해 어떤 섹터를 읽으면 되는지를 찾아내고, offset부터 읽어나간다. 그리고 읽은 내용을 버퍼에 담아주는 함수이다. inode_write_at()함수도 완전히 동일한 흐름으로 진행된다.
두번째로 inode_create() 함수이다
어떻게 보면 해당 함수는 그 역할이 많이 추상화 되어있다. inode_create()함수는 두가지 인자를 받는다. 몇번섹터에 넣을지와, 파일길이가 얼마나 되는지에 대한 인자를 받는다. 그리고 해당 길이만큼 "알아서" 할당한다. 위의 이미지에도 하단에도 나와있지만, 4번섹터부터 해당 파일을 작성하긴 하지만, 6,8,9번 섹터에 그 데이터가 들어간다는 사실을 함수 바깥에서는 알기 어렵다.
굳이 inode를 타고타고 들어가며 몇번섹터에 데이터가 있는지 알수도 있겠지만… 기본적으로 함수 바깥에서는 "모른다@! 알아서 해라"라고 생각하면 편할 것 같다. 생각해보면 디스크에 내가 원하는 글을 적었는데 내가 그걸 몇번 섹터에 넣었는지까지 알 필요가 없다. 그저 컴퓨터에게 “그정도 쯤은 알아서 처리해줘” 라는 식의 부탁을 하게 되는 것이다. 이러한 요소 하나하나가 OS의 추상화를 일구어 낸다.
parse_path()와의 고군분투
sub-directory 구조
를 구현하며, parse_path()와 정말 많이 싸웠다. 팀원들과 TC를 고려해나가며 디자인을 해 나갔다. 태초마을로 돌아가야 하나, 현재 디자인대로 쭉 가야하나… 등 많은 고민 이 있었다.
고민의 흔적
parse_path()를 구현하며 생각했던 부분들과 우리팀이 부딪혔던 문제사항에 대해서 이야기 해보도록 하겠다.
🤔sub-dir이란 무엇인가
Sub-dir이란 무엇일까? 결국 루트안에 루트안에 루트를 만들어 내는 것이다. 우리가 알고있는 파일의 경로, 즉, “/user/sunny/documnet/핀토스망해라.pdf” 와 같은 하위 디렉토리들을 구성해나가는 작업이다. 이번 작업을 해나가면서 모든 작업의 중심은 parse_path로부터 시작됨을 알게 되었다. 컴퓨터가 어떻게 경로를 분석하고 이해하는지가, 결국, 어떻게 sub-dir을 디자인하는지와 동일한 이야기이기 때문이다. 그렇다면 parse_path를 디자인하면서 고려했던 요소들과 그 결과값에 대해 설명해 보도록 하겠다.
👾parse_path()함수
parse_path()함수는 어떤 역할을 할까? 결국 directory 구조체를 리턴하고, file_name을 구해다준다.
parse_path()함수는 다음과 같은 3단계로 흘러간다
- 상대경로인지 절대경로인지 분석
- 디렉토리 walking
- 예외처리
1번
과 2번
은 크게 고려할 사항이 없다. 하지만 3번에서 예외처리를 어떻게 할지 고민이 시작된다. 그리고 다음과 같은 4가지 고려사항이 생긴다.
🎨parse_path()함수 디자인 고려사항
🔥1번
1번
parse_path()를 통해 나온 값들(dir
, file_name
)이 어떤 함수에서 쓰이는지에 따라 다른 처리들이 필요하다. 파일을 생성
하기 위해서는 해당경로의 파일이 존재하지 않아야 하고, 파일을 제거
하기 위해서는 해당 경로의 파일이 존재해야 한다. 이처럼…
create
, create_dir
, remove
, open
, change_dir
등 다앙한 상황에 따라 parth_path()의 결과값들을 처리해야 하므로, 이럴바에는 parth_path()함수를 2개만들어서 용도별로 사용하는게 편하지 않을까? 라는 고려사항이 생겼다.
🔥2번
2번
디렉토리를 어디까지 읽을 것인가에 대한 문제이다. 인자로 들어온 path가 모두 디렉토리로 구성해있는 경우 어떤 선택을 해야할까?
(방법1) 끝까지 디렉토리를 연다.
(방법2)마지막 경로는 항상 file_name에 넣어서 리턴시킨다.
다음 두 그림은 1번과 2번의 디자인별 결과값이다.
방법1
방법2
parth_path()는 요리에 필요한 재료를 준비해주는 과정이고, 실제 요리는 filesys_create()
, fiesys_remove()
, filesys_open()
등의 함수에서 입맛대로 요리를 한다. 따라서, 경로설정에 필요한 재료만 준비해준다는 관점에서 방법2
가 더 적절한 디자인이라고 생각한다. 우리팀은 방법1
로 주욱 달렸다가 조금 고생하긴 했지만, 그래도 여러 처리를 해준 끝에 끝내 all PASS를 띄울 수 있었다.
🔥3번
3번
해당 파일이 존재하는지, 존재하지만 이미 삭제된(메모리에만 표시) 파일인지, file_name이 파일인지, file_name이 디렉토리인지 등 다양한 경우가 있다. 그리고 경우에 따라서, parse_path()를 호출한 곳에서는 정말 다양한 처리를 해주어야 한다. 이럴바에는 차라리 플래그를 세워서 인자로 받는게 좋지 않을까? 라는 생각을 했다.
따라서 위처럼, 경우에 따라 플래그를 세워주면, filesys_create()
, fiesys_remove()
, filesys_open()
등의 함수에서 입맛대로 요리할 때, 훨씬 편하게 할 수 있지 않을까 싶다.
🔥4번
4번
위 그림은 우리조의 예외처리 방법이다. 함수 내부에서도 조금 처리를 해주고, 함수 바깥에서도 처리를 해준다. 위의 2번에서 말한, 요리재료를 준비해주는 관점에서, 함수내부의 예외처리를 최대한 많이 줄이는것이 좋을 것 같다. 그저 파싱만 해주고, 그 선택은 parse_path()를 호출한 함수들에게 맡기는것이 좋을 것 같다.
조금 더 추상적으로 한번 더, 이야기 해보자면.. callee
는 가장 날것의 형태를 리턴해주고, 그 판단은 요리사(caller
)에게 맡기는 디자인이 더 좋다고 생각한다.
( 다음은 우리조의 if문 7개가 들어간 parse_path()함수이다 )
함수 보러가기
아래의 코드는 simons팀/S-J브랜치/filesys.c 에서도 확인하실 수 있습니다.
struct dir* parse_path (char *path_name, char *file_name){
struct dir *dir = dir_open_root();
char *token, *next_token, *save_ptr;
char *path = malloc(strlen(path_name) + 1);
strlcpy(path, path_name, strlen(path_name) + 1);
/**************1단계: 절대경로인지 확인 ******************/
if(path[0] == '/'){ //절대경로인 경우
}
else{
dir_close(dir);
dir = dir_reopen(thread_current()->cwd);
}
token = strtok_r(path, "/", &save_ptr);
next_token = strtok_r(NULL, "/", &save_ptr);
if (token == NULL){ // path_name = "/" 로 입력받은 상태, root directory를 return (case 9)
return dir_open_root();
}
/**************2단계: walking ******************/
while(next_token != NULL){
struct inode *inode = NULL;
if(!dir_lookup(dir, token, &inode)){
dir_close(dir);
return NULL;
}
if (inode_is_dir(inode)){
dir_close(dir);
dir = dir_open(inode);
}
else{
dir_close(dir);
return NULL;
}
token = next_token;
next_token = strtok_r(NULL, "/", &save_ptr);
/*walking done!*/
}
/**************3단계: 예외처리 ******************/
if (token == NULL){
dir_close(dir);
return NULL;
}
else{
struct inode *inode = NULL;
dir_lookup(dir, token, &inode);
if (inode == NULL || inode_is_removed(inode)){
strlcpy(file_name, token, strlen(token) + 1);
return dir;
}
if (inode_is_dir(inode)){ //마지막이 디렉토리인 경우
dir_close(dir);
dir = dir_open(inode);
}
else{//마지막이 파일인 경우
strlcpy(file_name, token, strlen(token) + 1);
}
}
free(path);
return dir;
}
중간정리
parse_path()를 디자인하며 고려된 4가지 사항에 대해 알아보았다. 그렇다면, 이렇게 공을 들여 만들었음에도 sub-dir을 구현하며 수많은 문제들을 마주했다.
( jaimie형과 함께한 무수히 많은 버그해결 과정들…)
그렇다면, 그 중, 이야기해봄직한 문제 2가지를 적어나가볼까 한다.
↔️dir_readdir()처리
readdir()함수를 처리시, 파일을 오픈할 때 마다 offset이 초기화 되는 문제가 있었다. 그리고 특정 디렉토리 내에 파일이 몇개가 있을지 모르는 상황에서, 어떻게 dir구조를 닫아줄지에 대한 고민이 있었다.
따라서 위처럼 유사형변환을 진행시켰다.
struct file과 struct dir의 멤버 순서는 위와 같다. 따라서 struct파일의 위치를 dir이라고 속이고 dir_readdir()함수내부로 넣어주게 된다면, dir의 offset을 변경할 때 마다 파일의 offset이 변하게 된다. (선언된 구조체 멤버의 순서를 바꾸면 안돌아간다)
따라서, file을 변경하고 있지만 마치 dir을 변경하는 것과 같은 효과를 줄 수 있는 것이다.
(+추가사항)
해당내용을 발표했는데, 코치님 께서는 위와같은 방법이 C 프로젝트를 망치는 주범중 하나라고 하셨다. 무엇을 바꾸는지에 대한 불명확성을 지적해 주셨다. struct *dir
을 변경하는 코드지만 실제로는 struct *file
을 변경하는 위와 같은 코드는, “나만 알고 있기 때문”이라고 하셨다.
아무래도 저러한 implicit한 표현이 팀원(다른 개발자)들에게 혼동을 줄 수 있기 때문이라는 뜻으로 받아들였다. 그렇다면 어떻게 문제를 해결할 수 있을지에 대해 고민해 보았다. 친구들과 토론결과, pintos과제에서 짜준 틀을 깨고 코드를 다시짜야 할 것 같다. 선언된 readdir에 추가적인 인자를 받고, readdir이 false를 리턴하는 순간 해당구조체를 닫아준다던지… 그런 추가적인 엔지니어링(a.k.a 🚧대공사)를 치루어야 할 것으로 추측한다.
🔍자식이름찾기
filesys_remove()함수에서 다음과 같은 문제를 마주했다.
자식 디렉토리 내부로 들어온 상황이다. 여기서 엄마디렉토리를 찾고, 자식디렉토리의 이름도 찾아야 하는 상황이다. 따라서 다음과 같은 방법을 사용했다. 로직은 다음과 같다.
..
을 통해 엄마 dir을 찾는다.- 엄마 dir을 walking한다.
- walking중 자식 디렉토리와 같은 섹터에 있는 디렉토리엔트리를 찾는다.
- 디렉토리엔트리의 이름을 반환한다.
위와 같은 방법으로 코드를 작성했고 성공적으로 자식의 이름을 찾을 수 있었다. 위에서말한, 디자인 고려사항2번에서 방법2
로 구현을 했다면 위와같은 문제가 생기지 않았으리라 생각된다. 방법2
를 택해도 다른 예외처리가 불가피하지만, 훨씬 머리가 덜아팠지 않을까 싶다. (우리팀의 parse_path는 디렉토리 끝까지 들어와버리는것이 문제였던 것)
🚀금주 발표영상
😃금주 회고
똑똑이들과 팀을 맺어서 정말 편했다. 똑똑이들이라고 귀엽게 표현했지만, 나보다 사회경험이 훨씬 많은 형들이다. 저번주의 몸살감기 여파가 미약하게 남아있었지만 나도 쫓아가기 위해 열심히 했고, 팀원들은 옆자리에서 본인들이 알게된 내용을 나에게 쉽게 설명해 주었다. 나만무 준비 등으로 여러가지 어수선했지만, 우리조는 중꺾마를 실천해 나갔고 결국 sub-dir을 모두 PASS시킬 수 있었다.
프로젝트 이해도가 조금 떨어져서 중간에 브랜치를 분리시켜서 작업해나갔다. 이 글에서 설명한 내용과 그 코드는 여기브랜치에 있으며, sub-dir까지 allpass를 이루었다.
중간에 분기를 해서 끝까지 달린 코드는 여기브랜치에 정리되어 있다. soft-link까지 포함하여, all-pass를 이룬 코드이다. (145/146 PASS)
😃핀토스를 마치며
핀토스에 프로젝트에 끝까지 집중했다. 프로젝트 종료 하루 전날까지도, 코드를 완성시키기 위해 심혈을 기울였고 그 결과 많은것을 얻을 수 있었다. 첫번째로 강해졌다. 그냥 엄청 강해졌다. 웬만한 문제는 이제 문제로도 느껴지지 않는다. 어떤 특정 TC가 실패하면, 이를 고치는데 얼마나 걸릴지 대충 짐작이 가기도 한다.
주변에서 사실, 핀토스를 배워서 어디에 써먹냐는 이야기를 종종 듣는다. 나도 공감하는 부분이 없지않아 있다. 하지만 많은 시니어들이 “필요한 때가 온다”라는 말을 하는 것으로 보아, 결국 필요한 근본 CS가 되지 않을까 싶다. 하지만 지식
과는 별개로 다른 것들을 얻었다고 생각한다. 코치님이 “책과 말로는 설명할 수 없는 지식”이라는 말을 하셨다. 나는 원래 ‘말로 설명할 수 없는것은 모르는것’이라는 생각을 했었다. 따라서 처음에는 공감이 되지 않았었다.
하지만 이는 상충되는 개념이 아니라는 것 을 알게 되었다. 지식습득 측면에 있어서 경험
으로 얻을 수 있는 부분이 있고, 이를 잘 습득하였다면 말로 표현될 수도 있는 것이다. (물론 100% 전달이 되지 않겠지만…)
핀토스를 끝내고 나니, 많은 의심들이 확신으로 바뀌었다. 핀토스는 그런 프로젝트이다. 우리에게 확신과, 자신감을 얻어갈 수 있게해주는 그런 프로젝트인 것이다. 무기력하게 만들고, 피곤하게 만드는 시간들이였지만, 나를 담금질하는 시간이였던 것 같다. 이제 다시 파이썬, 자바스크립트로 넘어가게 된다면, 허허허 웃으면서 코딩을 할 수 있을 것 같은 기분이다.
내가 아직 모든 문제를 해결할수는 없겠지만, 어떤 문제든 풀어낼 수 있을 것 같은 기분이 든다. 이 기분과 자신감을 레버리지 삼되, 겸손한 태도로 실력을 쌓아나가는것이, 향후 1~3년간 가져야할 태도이지 않을까 싶다. 우리는 그 씨앗을 가지게 되었다. 그런 프로젝트였던 것 같다.
(핀토스 끝났다!!!!)
😵배우면서 깨달은 내용을 정리해 보았습니다. 틀린 것 같은 개념을 아래 댓글에 달아주시면 감사합니다😵
🌜 Thank you for reading it. Please leave your comments below😄
댓글 남기기