전체 흐름
pathname 해석은 파일 내용을 읽는 과정이 아니다. filename_lookup()이 먼저 RCU-walk를 시도하고 path_lookupat()이 시작 경로, 부모 성분, 마지막 성분, 최종 검증을 연결한다. 성공하면 dentry와 vfsmount를 묶은 struct path를 호출자에게 넘긴다.
link_path_walk()은 마지막 성분을 nd->last에 남기며, lookup_last()가 별도로 처리한다.
RCU-walk 중 참조가 필요한 시점에는 try_to_unlazy()가 전환을 시도한다. 실패한 -ECHILD는 바깥의 ref-walk 재시도로 이어질 수 있다.
그림을 가로로 밀어 전체 흐름 보기 ↔
화살표는 함수 경계다. 단일 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 세부 구현은 연결된 고정 버전 파일에서 더 추적할 수 있다.
GitHub 원본:
대표 코드
Linux v6.6의 fs/namei.c에서 함수 시작과 끝이 일치하는 연속 발췌다. 아래 두 버전은 서로 다른 줄 번호와 이름 해싱 구현을 갖는다.
path_lookupat() — 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
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
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 정책 때문에 별도 단계로 다룬다.
그림을 가로로 밀어 전체 흐름 보기 ↔
link_path_walk()이 마지막 성분을 건네면 lookup_last()가 WALK_TRAILING으로 처리한다.
| 함수·지점 | 실제 동작 | 읽을 때의 경계 |
|---|---|---|
path_init() | 절대·상대 경로의 시작점과 RCU/ref 문맥을 설정 | 시작점 선택 자체는 파일 열기가 아니다. |
link_path_walk() | 검색 권한 검사, 이름 분류, 중간 성분 이동·symlink target 재진입 | 마지막 성분을 즉시 최종 파일로 확정하지 않는다. |
walk_component() | 특수 dot 처리 또는 fast lookup, 필요 시 slow lookup, step_into | lookup_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을 바꾸는 흐름을 놓친다.
그림을 가로로 밀어 전체 흐름 보기 ↔
step_into()는 handle_mounts() 뒤 symlink 여부를 보고 target을 계속 걷거나 현재 path를 갱신한다.
| 대상 | 이 글에서의 의미 |
|---|---|
nd->path | 현재 dentry와 vfsmount의 쌍. mount crossing에서 둘을 함께 추적한다. |
nd->last / last_type | 마지막 이름과 LAST_NORM/DOT/DOTDOT 분류. lookup_last에 전달된다. |
nd->flags | LOOKUP_RCU, LOOKUP_PARENT, LOOKUP_FOLLOW, LOOKUP_DIRECTORY 등 경로 정책. |
nd->seq / next_seq | RCU-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를 추가하지 않은 플래그로 처음부터 재시도한다.
그림을 가로로 밀어 전체 흐름 보기 ↔
complete_walk()의 참조 전환과 성공 시 nd->path 이전을 구분한다.
| 경계 | 조건·동작 | 결과가 뜻하지 않는 것 |
|---|---|---|
| 첫 RCU-walk | filename_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_walk | path가 반환 전 해제된 것은 아니다. 호출자가 수명 관리한다. |
| 오류 | terminate_walk()이 내부 보유 경로·link/RCU를 정리 | 성공 path를 호출자에게 건넨 상태와 구분한다. |
불변 조건과 실패 사례
RCU-walk를 일반 ref-walk로 읽고 임의로 잠드는 작업을 넣을 수 없다. 필요하면 try_to_unlazy()의 참조 전환·검증을 거친다.
경로는 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()으로 놓는가?