File System · Linux v6.6 · source + diagram note

path lookup

RCU/ref walk의 전환, 중간·마지막 성분, mount·symlink 경계, 성공 path 소유권을 고정 소스와 함께 읽습니다.

전체 흐름

pathname 해석은 파일 내용을 읽는 과정이 아니다. filename_lookup()이 먼저 RCU-walk를 시도하고 path_lookupat()이 시작 경로, 부모 성분, 마지막 성분, 최종 검증을 연결한다. 성공하면 dentry와 vfsmount를 묶은 struct path를 호출자에게 넘긴다.

부모와 마지막 성분

link_path_walk()은 마지막 성분을 nd->last에 남기며, lookup_last()가 별도로 처리한다.

RCU와 참조

RCU-walk 중 참조가 필요한 시점에는 try_to_unlazy()가 전환을 시도한다. 실패한 -ECHILD는 바깥의 ref-walk 재시도로 이어질 수 있다.

그림 1. pathname에서 호출자 소유 path까지

그림을 가로로 밀어 전체 흐름 보기 ↔

filename_lookupname + flags우선 LOOKUP_RCU실패 시 조건부 재시도path_lookupatpath_init시작점: root / pwd / fdLOOKUP_DOWN 조건parent 경로link_path_walk중간 componentlast 정보 보류마지막 경로lookup_lastWALK_TRAILINGsymlink면 반복마무리complete_walkRCU면 ref 확보제약 검사성공*path = nd->pathnd->path 비움terminate_walk호출자struct pathdentry + vfsmount참조를 보유이 함수는 경로를 해석한다. path_lookupat() 자체를 file open이나 page-cache I/O로 해석하지 않는다.

화살표는 함수 경계다. 단일 next dentry 또는 file open 한 번으로 축약되지 않는다.

소스 코드 위치

먼저 확인할 Linux v6.6 소스 파일

원본 코드:
Linux v6.6: fs/namei.c
Linux v6.6: fs/dcache.c
Linux v6.6: fs/mount.h

설명: 함수 순서와 소유권 이전은 fs/namei.c를 기준으로 확인한다. dcache와 mount 세부 구현은 연결된 고정 버전 파일에서 더 추적할 수 있다.

대표 코드

Linux v6.6의 fs/namei.c에서 함수 시작과 끝이 일치하는 연속 발췌다. 아래 두 버전은 서로 다른 줄 번호와 이름 해싱 구현을 갖는다.

path_lookupat() — fs/namei.c L2471–2502

원본 코드: Linux v6.6, fs/namei.c L2471–2502
static int path_lookupat(struct nameidata *nd, unsigned flags, struct path *path)
{
	const char *s = path_init(nd, flags);
	int err;

	if (unlikely(flags & LOOKUP_DOWN) && !IS_ERR(s)) {
		err = handle_lookup_down(nd);
		if (unlikely(err < 0))
			s = ERR_PTR(err);
	}

	while (!(err = link_path_walk(s, nd)) &&
	       (s = lookup_last(nd)) != NULL)
		;
	if (!err && unlikely(nd->flags & LOOKUP_MOUNTPOINT)) {
		err = handle_lookup_down(nd);
		nd->state &= ~ND_JUMPED; // no d_weak_revalidate(), please...
	}
	if (!err)
		err = complete_walk(nd);

	if (!err && nd->flags & LOOKUP_DIRECTORY)
		if (!d_can_lookup(nd->path.dentry))
			err = -ENOTDIR;
	if (!err) {
		*path = nd->path;
		nd->path.mnt = NULL;
		nd->path.dentry = NULL;
	}
	terminate_walk(nd);
	return err;
}

읽는 법: link_path_walk()과 lookup_last()는 반복될 수 있다. 성공 시 nd->path를 결과로 옮긴 뒤 내부 경로 포인터를 비우고 terminate_walk()을 호출한다.

link_path_walk() — fs/namei.c L2247–2347

원본 코드: Linux v6.6, fs/namei.c L2247–2347
static int link_path_walk(const char *name, struct nameidata *nd)
{
	int depth = 0; // depth <= nd->depth
	int err;

	nd->last_type = LAST_ROOT;
	nd->flags |= LOOKUP_PARENT;
	if (IS_ERR(name))
		return PTR_ERR(name);
	while (*name=='/')
		name++;
	if (!*name) {
		nd->dir_mode = 0; // short-circuit the 'hardening' idiocy
		return 0;
	}

	/* At this point we know we have a real path component. */
	for(;;) {
		struct mnt_idmap *idmap;
		const char *link;
		u64 hash_len;
		int type;

		idmap = mnt_idmap(nd->path.mnt);
		err = may_lookup(idmap, nd);
		if (err)
			return err;

		hash_len = hash_name(nd->path.dentry, name);

		type = LAST_NORM;
		if (name[0] == '.') switch (hashlen_len(hash_len)) {
			case 2:
				if (name[1] == '.') {
					type = LAST_DOTDOT;
					nd->state |= ND_JUMPED;
				}
				break;
			case 1:
				type = LAST_DOT;
		}
		if (likely(type == LAST_NORM)) {
			struct dentry *parent = nd->path.dentry;
			nd->state &= ~ND_JUMPED;
			if (unlikely(parent->d_flags & DCACHE_OP_HASH)) {
				struct qstr this = { { .hash_len = hash_len }, .name = name };
				err = parent->d_op->d_hash(parent, &this);
				if (err < 0)
					return err;
				hash_len = this.hash_len;
				name = this.name;
			}
		}

		nd->last.hash_len = hash_len;
		nd->last.name = name;
		nd->last_type = type;

		name += hashlen_len(hash_len);
		if (!*name)
			goto OK;
		/*
		 * If it wasn't NUL, we know it was '/'. Skip that
		 * slash, and continue until no more slashes.
		 */
		do {
			name++;
		} while (unlikely(*name == '/'));
		if (unlikely(!*name)) {
OK:
			/* pathname or trailing symlink, done */
			if (!depth) {
				nd->dir_vfsuid = i_uid_into_vfsuid(idmap, nd->inode);
				nd->dir_mode = nd->inode->i_mode;
				nd->flags &= ~LOOKUP_PARENT;
				return 0;
			}
			/* last component of nested symlink */
			name = nd->stack[--depth].name;
			link = walk_component(nd, 0);
		} else {
			/* not the last component */
			link = walk_component(nd, WALK_MORE);
		}
		if (unlikely(link)) {
			if (IS_ERR(link))
				return PTR_ERR(link);
			/* a symlink to follow */
			nd->stack[depth++].name = name;
			name = link;
			continue;
		}
		if (unlikely(!d_can_lookup(nd->path.dentry))) {
			if (nd->flags & LOOKUP_RCU) {
				if (!try_to_unlazy(nd))
					return -ECHILD;
			}
			return -ENOTDIR;
		}
	}
}

읽는 법: 중간 성분은 WALK_MORE로 전진하고, 마지막 성분은 nd->last에 남긴다. 이 버전은 hash_name(parent, name) → hash_len를 사용한다.

filename_lookup() — fs/namei.c L2504–2523

원본 코드: Linux v6.6, fs/namei.c L2504–2523
int filename_lookup(int dfd, struct filename *name, unsigned flags,
		    struct path *path, struct path *root)
{
	int retval;
	struct nameidata nd;
	if (IS_ERR(name))
		return PTR_ERR(name);
	set_nameidata(&nd, dfd, name, root);
	retval = path_lookupat(&nd, flags | LOOKUP_RCU, path);
	if (unlikely(retval == -ECHILD))
		retval = path_lookupat(&nd, flags, path);
	if (unlikely(retval == -ESTALE))
		retval = path_lookupat(&nd, flags | LOOKUP_REVAL, path);

	if (likely(!retval))
		audit_inode(name, path->dentry,
			    flags & LOOKUP_MOUNTPOINT ? AUDIT_INODE_NOEVAL : 0);
	restore_nameidata();
	return retval;
}

읽는 법: 첫 RCU 시도가 -ECHILD면 일반 플래그로 재시도하고, -ESTALE이면 LOOKUP_REVAL로 다시 시도한다. 재시도는 조건부다.

함수별 분석

문자열에서 component를 꺼내는 일과 실제 dentry·mount를 전진시키는 일은 다르다. 마지막 성분은 trailing slash와 symlink follow 정책 때문에 별도 단계로 다룬다.

그림 2. 중간 component와 마지막 component의 분기

그림을 가로로 밀어 전체 흐름 보기 ↔

중간 component · LOOKUP_PARENT가 설정된 구간마지막 component · 별도 lookup_last 단계link_path_walkmay_lookup부모 검색 권한 검사이름 해석hash_nameparent/name → hash_lenLAST_DOT · DOTDOT중간 경로WALK_MOREwalk_componentnext path 진행symlink 대상name 교체stack에 나머지 저장문자열 끝last 보존LOOKUP_PARENT 해제부모 메타데이터path_lookupatlookup_lastWALK_TRAILING후행 / 조건 처리symlink 후속반복 가능반환 target으로link_path_walk 재진입완료 단계complete_walk검증·참조 전환중간 경로와 마지막 성분은 같은 즉시 dentry 결정 한 단계가 아니다.

link_path_walk()이 마지막 성분을 건네면 lookup_last()가 WALK_TRAILING으로 처리한다.

함수·지점실제 동작읽을 때의 경계
path_init()절대·상대 경로의 시작점과 RCU/ref 문맥을 설정시작점 선택 자체는 파일 열기가 아니다.
link_path_walk()검색 권한 검사, 이름 분류, 중간 성분 이동·symlink target 재진입마지막 성분을 즉시 최종 파일로 확정하지 않는다.
walk_component()특수 dot 처리 또는 fast lookup, 필요 시 slow lookup, step_intolookup_slow는 cache miss 등의 조건부 경로다.
lookup_last()WALK_TRAILING으로 마지막 성분을 걷고 필요한 follow/directory 플래그 설정후행 slash와 symlink에 따라 다시 link_path_walk가 돌 수 있다.
complete_walk()남은 RCU 상태를 참조 보유 경로로 전환하고 검증성공 시 소유된 path로 넘어갈 준비다.

구조체와 필드

nameidata는 현재 path, 시작 root, 마지막 이름, sequence와 flags를 함께 들고 간다. dentry만 따라가면 다른 mount로 건너가는 변화와 symlink가 pathname을 바꾸는 흐름을 놓친다.

그림 3. dentry 조회 뒤 mount와 symlink 경계

그림을 가로로 밀어 전체 흐름 보기 ↔

nameidata현재 pathdentry + vfsmountroot / seq 보유일반 이름walk_componentlookup_fastmiss면 lookup_slow경계step_intohandle_mounts 먼저그 뒤 inode·symlinkmount 변화path.mnt 변경LOOKUP_NO_XDEV제약 검사symlinkpick_linktarget namestack에 잔여 경로특수 이름 . / .. 는 handle_dots 경로로 간다.마운트 경계는 dentry 해시 조회와 별도다.negative dentry는 일반 이름 캐시에 있을 수 있으나 성공 path의 positive inode로 곧장 간주하지 않는다.

step_into()는 handle_mounts() 뒤 symlink 여부를 보고 target을 계속 걷거나 현재 path를 갱신한다.

대상이 글에서의 의미
nd->path현재 dentry와 vfsmount의 쌍. mount crossing에서 둘을 함께 추적한다.
nd->last / last_type마지막 이름과 LAST_NORM/DOT/DOTDOT 분류. lookup_last에 전달된다.
nd->flagsLOOKUP_RCU, LOOKUP_PARENT, LOOKUP_FOLLOW, LOOKUP_DIRECTORY 등 경로 정책.
nd->seq / next_seqRCU-walk 중 dentry sequence 검증에 사용. 단순 참조 카운트가 아니다.
nd->stack따라가는 symlink 뒤에 남은 pathname을 보관·복귀한다.

실행 단계와 상태 변화

RCU-walk가 먼저 시도되지만 모든 성분을 참조 없이 끝낼 수 있는 것은 아니다. try_to_unlazy()가 경로·link·root 참조를 정당화하면 현재 위치에서 ref-walk로 이어진다. 이 전환의 실패뿐 아니라 lookup_fast()의 부모 seqcount 불일치 등도 -ECHILD를 반환할 수 있다. 첫 path_lookupat()이 -ECHILD를 반환하면 filename_lookup()이 RCU를 추가하지 않은 플래그로 처음부터 재시도한다.

그림 4. RCU/ref 재시도와 성공 path 소유권

그림을 가로로 밀어 전체 흐름 보기 ↔

시도와 재시도 · filename_lookup()마무리와 소유권 · path_lookupat()첫 시도LOOKUP_RCU참조 없는 조회전환try_to_unlazy검증·참조 확보실패 시 -ECHILD-ECHILDref-walk 재시도flags에서 RCU 제외-ESTALELOOKUP_REVAL별도 재검증 시도complete_walkRCU → ref 전환성공 시 소유 참조성공*path = nd->pathnd->path.mnt/dentryNULL로 이전항상terminate_walknd에 남은 참조또는 RCU 해제호출자path_put 필요성공 path 수명호출자가 관리-ESTALE 재검증은 첫 시도나 ref 재시도 뒤에도 필요할 수 있다. 성공 path는 nd에서 호출자로 이동한다.

complete_walk()의 참조 전환과 성공 시 nd->path 이전을 구분한다.

경계조건·동작결과가 뜻하지 않는 것
첫 RCU-walkfilename_lookup()이 LOOKUP_RCU를 더해 호출항상 lockless로 끝난다는 보장은 없다.
ref-walk 전환try_to_unlazy()가 link/current/root를 정당화하면 LOOKUP_RCU 해제무조건 전체 path를 처음부터 다시 걷는 것은 아니다.
-ECHILD전환·검증 실패 시 RCU 없이 path_lookupat() 재시도일반적인 파일 없음(-ENOENT)과 다르다.
-ESTALE필요 시 LOOKUP_REVAL로 재시도모든 조회에서 실행되는 단계가 아니다.
성공*path = nd->path, nd 포인터 NULL, terminate_walkpath가 반환 전 해제된 것은 아니다. 호출자가 수명 관리한다.
오류terminate_walk()이 내부 보유 경로·link/RCU를 정리성공 path를 호출자에게 건넨 상태와 구분한다.
성공 소유권: nd->path → *path; 내부 정리: terminate_walk(nd); 호출자는 path_put() 책임

불변 조건과 실패 사례

RCU 중 수면 금지

RCU-walk를 일반 ref-walk로 읽고 임의로 잠드는 작업을 넣을 수 없다. 필요하면 try_to_unlazy()의 참조 전환·검증을 거친다.

mount와 dentry

경로는 dentry 단독이 아니라 (vfsmount, dentry) 쌍이다. step_into()의 handle_mounts() 이후 두 값을 확인한다.

마지막 이름

중간 성분의 WALK_MORE와 마지막 성분의 WALK_TRAILING은 다르다. trailing slash와 symlink 조건이 마지막 lookup에 영향을 준다.

참조 소유권

성공 시 결과 path에 참조를 넘기고 nd의 path를 비운다. 실패는 내부 정리이며 호출자에게 성공 path를 넘긴 경로와 구분한다.

주의

link_path_walk()를 언제나 참조를 잡는 slow path로 부르지 않는다.

주의

mount crossing을 단순 directory lookup과 동일시하지 않는다.

주의

negative dentry 캐시와 성공적으로 반환되는 positive path를 혼동하지 않는다.

계측과 검증

특정 pathname의 결과와 errno를 먼저 읽고, 고정 소스의 LOOKUP_RCU·-ECHILD·LOOKUP_REVAL 분기를 대조한다. 일반 부팅 로그만으로 개별 component의 RCU/ref 경로를 확정할 수 없고, 여기서는 공유 tracefs 설정을 바꾸지 않는다.

읽기 전용 자료확인할 수 있는 범위한계
stat -L -- PATH / readlink -f -- PATH특정 경로의 결과 또는 오류를 확인내부 RCU/ref 전환 횟수를 드러내지 않는다.
findmnt --target PATH현재 mount tree에서 해당 경로의 mount 정보를 확인실제 lookup 중 일어난 경쟁·재시도는 증명하지 않는다.
고정 fs/namei.c함수 순서·분기·path 소유권의 코드 근거특정 실행에서 선택된 모든 분기는 별도 증거가 필요하다.
읽기 전용 예:
stat -L -- /etc/passwd
readlink -f -- /etc/passwd
findmnt --target /etc/passwd

도구가 없거나 경로가 달라질 수 있으므로 대상 보드의 실제 pathname으로 바꿔 읽는다. tracefs 전역 상태는 변경하지 않는다.

추가 확인 사항

  • 어느 component에서 lookup_fast()가 결과를 주지 못해 lookup_slow()가 필요한가?
  • 마지막 성분에 trailing slash나 symlink가 있을 때 lookup_last()의 follow/directory 조건은 무엇인가?
  • mount 경계 뒤에도 nd->path.mnt와 nd->path.dentry를 하나의 path로 추적했는가?
  • RCU에서 try_to_unlazy()가 실패한 -ECHILD와 stale 재검증 -ESTALE을 구분했는가?
  • 성공 반환 뒤 결과 struct path의 참조는 누가 path_put()으로 놓는가?