1편에서 FAT와 파일 확장을 만들었다면, 이번에는 파일 이름을 찾는 방식 자체를 바꾼다. Project 4의 디렉터리 테스트는 더 이상 모든 파일이 root 아래에 있다고 봐주지 않는다.
기존 파일 시스템은 파일 이름 하나를 root directory에서만 찾으면 됐다. Project 4에서는 /a/b/c, ../x, 현재 작업 디렉터리, ., ..까지 처리해야 한다. 그래서 파일 생성과 삭제도 “부모 디렉터리 찾기 + 마지막 이름 처리”로 나누었다.
../tmp/a.., tmp, a1. thread가 현재 작업 디렉터리를 가져야 한다
상대 경로를 지원하려면 process마다 기준 디렉터리가 있어야 한다. 그래서 thread에 cwd를 추가하고, user process 초기화 시 root를 기본값으로 잡았다.
#ifdef USERPROG
struct dir;
#endif
struct thread {
#ifdef USERPROG
struct dir *cwd; /* Current working directory. */
#endif
};
void
process_user_init (struct thread *t) {
ASSERT (t != NULL);
if (t->cwd == NULL)
t->cwd = dir_open_root ();
}
bool
process_duplicate_fds (struct thread *dst, struct thread *src) {
process_user_init (dst);
process_close_files (dst);
if (src->cwd != NULL)
dst->cwd = dir_reopen (src->cwd);
else
dst->cwd = dir_open_root ();
/* fd table 복제 로직은 기존 흐름을 이어간다. */
return true;
}
이전에는 모든 경로가 root 기준인 것처럼 동작했다. 이제 각 process는 자기 cwd를 가진다. fork에서는 부모 cwd를 reopen해서 자식에게 넘긴다. 같은 inode를 가리키되, dir 객체는 따로 열어두는 방식이다.
2. 디렉터리는 생성될 때 . 과 .. 를 가져야 한다
하위 디렉터리는 단순히 inode flag만 directory로 표시한다고 끝나지 않는다. 현재 디렉터리를 가리키는 .와 부모 디렉터리를 가리키는 ..가 있어야 경로 순회가 자연스럽게 된다.
bool
dir_create (disk_sector_t sector, size_t entry_cnt, disk_sector_t parent_sector) {
struct dir *dir;
bool success;
success = inode_create_dir (sector, entry_cnt * sizeof (struct dir_entry));
if (!success)
return false;
dir = dir_open (inode_open (sector));
if (dir == NULL)
return false;
success = dir_add (dir, ".", sector) && dir_add (dir, "..", parent_sector);
dir_close (dir);
return success;
}
사용자에게 보여주는 readdir()에서는 .와 ..를 숨긴다. 내부 경로 해석에는 필요하지만, 테스트가 기대하는 디렉터리 목록에는 일반 파일처럼 나오면 안 된다.
bool
dir_readdir (struct dir *dir, char name[NAME_MAX + 1]) {
struct dir_entry e;
while (inode_read_at (dir->inode, &e, sizeof e, dir->pos) == sizeof e) {
dir->pos += sizeof e;
if (e.in_use && strcmp (e.name, ".") && strcmp (e.name, "..")) {
strlcpy (name, e.name, NAME_MAX + 1);
return true;
}
}
return false;
}
3. open은 전체 경로를 따라가고, create는 부모와 이름을 나눈다
여기서 가장 많이 헷갈린 지점은 open("/a/b/c")와 create("/a/b/c")의 차이다. open은 마지막 c까지 이미 있어야 한다. create는 /a/b라는 부모 디렉터리를 찾은 뒤, 그 안에 c라는 이름을 새로 추가해야 한다.
static struct dir *
starting_dir (struct dir *base, const char *path) {
struct thread *cur = thread_current ();
if (path[0] == '/')
return dir_open_root ();
if (base != NULL)
return dir_reopen (base);
if (cur != NULL && cur->cwd != NULL)
return dir_reopen (cur->cwd);
return dir_open_root ();
}
bool
filesys_mkdir (const char *name) {
disk_sector_t inode_sector = 0;
char dir_name[NAME_MAX + 1];
struct dir *parent = open_parent_dir (name, dir_name);
bool success = false;
if (parent != NULL
&& allocate_inode_sector (&inode_sector)
&& dir_create (inode_sector, 2,
inode_get_inumber (dir_get_inode (parent)))
&& dir_add (parent, dir_name, inode_sector))
success = true;
if (!success && inode_sector != 0)
release_inode_sector (inode_sector);
dir_close (parent);
return success;
}
4. 삭제는 비어 있는 디렉터리만 허용한다
파일 삭제와 디렉터리 삭제는 위험도가 다르다. 디렉터리를 지울 때는 내부 항목이 비어 있는지 확인하고, .와 ..는 삭제 대상에서 막아야 한다.
bool
dir_is_empty (struct dir *dir) {
struct dir_entry e;
off_t ofs;
for (ofs = 0; inode_read_at (dir->inode, &e, sizeof e, ofs) == sizeof e;
ofs += sizeof e) {
if (e.in_use && strcmp (e.name, ".") && strcmp (e.name, ".."))
return false;
}
return true;
}
5. 이 단계에서 의미 있는 테스트
마무리
이 단계에서 파일 시스템은 “이름 하나를 root에서 찾는 구조”를 벗어난다. 각 process가 cwd를 갖고, path resolver가 token을 따라가며, create와 mkdir은 부모 디렉터리와 마지막 이름을 분리한다. 이 기준이 잡히면 다음 글의 디렉터리 fd syscall도 훨씬 자연스럽게 연결된다.