개발/프로젝트

Pintos Project 4-4: symlink는 파일을 복사하지 않고 경로를 다시 해석한다

cedis 2026. 5. 22. 21:52
Project 4 File System 시리즈 4편

FAT, 하위 디렉터리, 디렉터리 fd syscall까지 연결한 뒤 마지막으로 붙인 기능은 symbolic link였다. symlink는 대상 파일을 복사하지 않는다. 작은 inode 하나에 target path를 저장하고, open 과정에서 그 path를 다시 해석한다.

핵심부터 말하면

이번 구현에서 symlink inode는 실제 파일 데이터를 갖는 대신 target 문자열을 metadata로 저장한다. open("link")가 들어오면 path resolver가 link inode를 발견하고, target path로 이어서 다시 resolve한다. 순환 링크를 막기 위해 최대 깊이는 8로 제한했다.

symlink 해석 흐름
입력/a/link/file
link 발견link → ../b
경로 재조립../b/file
다시 resolve기준 dir에서 이어서 탐색

1. inode에 symlink 표시와 target을 저장한다

symlink는 일반 파일도 아니고 디렉터리도 아니다. 그래서 inode에 flag를 두고, target path를 저장할 공간을 on-disk inode 안에 넣었다.

#define INODE_SYMLINK_MAX 255
#define INODE_FLAG_DIR 0x1
#define INODE_FLAG_SYMLINK 0x2

struct inode_disk {
	cluster_t start;
	off_t length;
	unsigned magic;
	uint32_t flags;
	char symlink_target[INODE_SYMLINK_MAX + 1];
	uint32_t unused[60];
};
bool
inode_create_symlink (disk_sector_t sector, const char *target) {
	if (target == NULL || strlen (target) > INODE_SYMLINK_MAX)
		return false;
	return inode_create_internal (sector, strlen (target),
			INODE_FLAG_SYMLINK, target);
}

bool
inode_is_symlink (const struct inode *inode) {
	return inode != NULL && (inode->data.flags & INODE_FLAG_SYMLINK);
}

const char *
inode_symlink_target (const struct inode *inode) {
	return inode->data.symlink_target;
}
이 블록이 바꾼 것

이전 inode는 파일 길이와 데이터 위치 중심이었다. 이제 inode는 “이 항목이 directory인지 symlink인지”도 설명한다. path resolver가 이 flag를 보고 동작을 바꾼다.

2. symlink syscall은 target과 linkpath를 분리한다

symlink(target, linkpath)는 target 파일을 열 필요가 없다. target이 지금 존재하지 않아도 link 자체는 만들 수 있다. 중요한 것은 linkpath의 부모 디렉터리를 찾아, 그 안에 symlink inode를 추가하는 것이다.

int
filesys_symlink (const char *target, const char *linkpath) {
	disk_sector_t inode_sector = 0;
	char link_name[NAME_MAX + 1];
	struct dir *parent;
	bool success = false;

	if (target == NULL || target[0] == '\0')
		return -1;

	parent = open_parent_dir (linkpath, link_name);
	if (parent != NULL
			&& allocate_inode_sector (&inode_sector)
			&& inode_create_symlink (inode_sector, target)
			&& dir_add (parent, link_name, inode_sector))
		success = true;

	if (!success && inode_sector != 0)
		release_inode_sector (inode_sector);
	dir_close (parent);
	return success ? 0 : -1;
}
case SYS_SYMLINK:
{
	const char *target = (const char *) f->R.rdi;
	const char *linkpath = (const char *) f->R.rsi;

	user_check_string (target);
	user_check_string (linkpath);
	lock_acquire (&filesys_lock);
	f->R.rax = filesys_symlink (target, linkpath);
	lock_release (&filesys_lock);
	break;
}

3. resolver는 symlink를 만나면 target으로 경로를 다시 만든다

중간 경로에 symlink가 있을 수도 있고, 마지막 경로가 symlink일 수도 있다. open처럼 실제 대상이 필요한 경우에는 마지막 symlink도 따라가야 하지만, remove처럼 link 자체를 지워야 하는 경우에는 마지막 symlink를 따라가면 안 된다. 그래서 resolver에 follow_final 인자가 있다.

#define SYMLINK_DEPTH_LIMIT 8

static struct inode *
resolve_path_from (struct dir *base, const char *path, bool follow_final,
		int depth) {
	/* token 순회 중 일부 */
	if (path == NULL || path[0] == '\0' || depth > SYMLINK_DEPTH_LIMIT)
		return NULL;

	if (inode_is_symlink (inode) && (follow_final || next != NULL)) {
		char *joined = join_symlink_path (inode_symlink_target (inode),
				next, save);
		struct inode *resolved = NULL;

		if (joined != NULL) {
			resolved = resolve_path_from (dir, joined, follow_final,
					depth + 1);
			free (joined);
		}
		inode_close (inode);
		free (copy);
		dir_close (dir);
		return resolved;
	}
}
여기서 depth limit이 필요한 이유

링크 A가 B를 가리키고 B가 다시 A를 가리키면 resolver가 끝없이 재귀할 수 있다. 그래서 구현은 symlink follow 깊이를 8로 제한했다. 실패 처리는 파일이 없는 것처럼 NULL을 반환하는 쪽으로 모은다.

4. 최종 검증 범위

Project 3 완료 코드 복사본 위에 Project 4 구현을 얹은 뒤, filesys build 기준으로 전체 테스트를 실행했다. 검증 결과는 다음과 같다.

검증 결과

make check 기준 All 146 tests passed.

검증 범위에 대한 메모

이번 시리즈의 기준은 filesys 프로젝트의 기본/확장 테스트다. FAT 기반 파일 확장, 하위 디렉터리, 디렉터리 syscall, symlink, persistence까지 통과한 상태다. buffer cache는 별도 가산점 영역이라 이 글의 구현 범위에는 넣지 않았다.

5. 이 단계에서 의미 있는 테스트

테스트 의미 실패 시 먼저 볼 곳
symlink-file 파일을 가리키는 symlink가 open/read로 따라가지는가 resolve_path_from(), follow_final
symlink-dir 디렉터리를 가리키는 symlink가 중간 경로에서 동작하는가 join_symlink_path(), base dir 처리
symlink-link symlink가 다른 symlink를 가리킬 때도 제한 안에서 따라가는가 depth limit, 재귀 resolve
persistence 계열 FAT, 디렉터리, symlink metadata가 재부팅 후에도 남는가 fat_close(), inode disk write

Project 4 전체를 한 문장으로 정리하면

Project 4는 파일 시스템을 “파일 이름 하나를 열고 고정 길이 파일을 읽는 코드”에서 “경로를 해석하고, inode 종류를 구분하고, FAT chain으로 파일을 늘리고, 디렉터리와 symlink까지 일관되게 다루는 코드”로 바꾸는 작업이었다.