Linux open系統呼叫(二)
注:本文分析基於3.10.0-693.el7核心版本,即CentOS 7.4
上回說到根據使用者給的路徑,通過path_init函式設定起始目錄nd->path,那接下來就要遍歷目錄了,我們從link_path_walk函式開始分析。
static int link_path_walk(const char *name, struct nameidata *nd) { struct path next; int err; while (*name=='/') name++;//過濾前導"/",即使ls /////home 正常執行 if (!*name)//只有根目錄,沒有子目錄,直接返回 return 0; /* At this point we know we have a real path component. */ //開始遍歷子目錄 for(;;) { struct qstr this; long len; int type; err = may_lookup(nd);//許可權校驗 if (err) break; //計算路徑名的雜湊值,並返回路徑長度 len = hash_name(name, &this.hash); this.name = name; this.len = len; type = LAST_NORM; //處理路徑為.和..的情況,設定相應標記 if (name[0] == '.') switch (len) { case 2: if (name[1] == '.') { type = LAST_DOTDOT;//..的情況 nd->flags |= LOOKUP_JUMPED; } break; case 1: type = LAST_DOT; } //如果是普通路徑名 if (likely(type == LAST_NORM)) { struct dentry *parent = nd->path.dentry; nd->flags &= ~LOOKUP_JUMPED; //檢查是否需要重新計算雜湊值 if (unlikely(parent->d_flags & DCACHE_OP_HASH)) { err = parent->d_op->d_hash(parent, &this); if (err < 0) break; } } //更新子路徑名 nd->last = this; nd->last_type = type; if (!name[len]) return 0; /* * If it wasn't NUL, we know it was '/'. Skip that * slash, and continue until no more slashes. */ do { len++; } while (unlikely(name[len] == '/'));//過濾掉中間連續的/ if (!name[len]) return 0; name += len; //開始處理子路徑 err = walk_component(nd, &next, LOOKUP_FOLLOW); if (err < 0) return err; if (err) { err = nested_symlink(&next, nd); if (err) return err; } if (!d_can_lookup(nd->path.dentry)) { err = -ENOTDIR; break; } } terminate_walk(nd); return err; }
處理好“.”和“..”的情況,同時過濾掉多餘的/之後,此時路徑名肯定就是一個目錄或者連結了。這是就要進入walk_component函式處理這些子路徑。
static inline int walk_component(struct nameidata *nd, struct path *path, int follow) { struct inode *inode; int err; /* * "." and ".." are special - ".." especially so because it has * to be able to know about the current root directory and * parent relationships. */ if (unlikely(nd->last_type != LAST_NORM))//處理.和..的情況 return handle_dots(nd, nd->last_type); ... }
對於這個子路徑,有三種情況,分別是“.”和“..” ,普通目錄以及符號連結。我們先看看“.”和“..”的處理。
static inline int handle_dots(struct nameidata *nd, int type) { //..——上級目錄 if (type == LAST_DOTDOT) { if (nd->flags & LOOKUP_RCU) { //處理..,往上一級目錄查詢 if (follow_dotdot_rcu(nd)) return -ECHILD; } else return follow_dotdot(nd); } //如果是.,那就是當前目錄,不需要處理 return 0; }
平時我們認為“..”很簡單,就是上一級目錄而已,但是在核心中並沒有表現出來的這麼簡單。因為往上一級就有可能走到另一個檔案系統中,而且由於涉及到掛載的問題,處理起來還是略顯複雜的。
static int follow_dotdot_rcu(struct nameidata *nd)
{
set_root_rcu(nd);//設定nd->root為根檔案系統
while (1) {
//如果當前目錄已經是預設的根目錄,那到頂了,直接返回
if (nd->path.dentry == nd->root.dentry && nd->path.mnt == nd->root.mnt) {
break;
}
//如果當前目錄不是預設的根目錄,且不是當前檔案系統的根目錄,那就向上走一級
if (nd->path.dentry != nd->path.mnt->mnt_root) {
struct dentry *old = nd->path.dentry;
struct dentry *parent = old->d_parent;//獲取父目錄
unsigned seq;
seq = read_seqcount_begin(&parent->d_seq);
if (read_seqcount_retry(&old->d_seq, nd->seq))
goto failed;
nd->path.dentry = parent;//nd跨越到上一級目錄
nd->seq = seq;
if (unlikely(!path_connected(&nd->path)))
goto failed;
break;
}
//判斷父mount結構是否在另一個檔案系統中,返回0表示在同一個檔案系統中
//在不同檔案系統時,需要一直往上走,因為可能是多個檔案系統掛載同一個目錄
if (!follow_up_rcu(&nd->path))
break;
nd->seq = read_seqcount_begin(&nd->path.dentry->d_seq);
}
//如果此時找到的父目錄也是一個掛載點,需要往上繼續找(核心空間)
//雖然從路徑名上我們往上一層就可以了,但是在核心裡,
//當前這個路徑名同樣可能是經過多次掛載呈現出來的,因此需要找到最新的那個掛載點
while (d_mountpoint(nd->path.dentry)) {
struct mount *mounted;
//在散列表裡查詢對應的掛載點
mounted = __lookup_mnt(nd->path.mnt, nd->path.dentry);
if (!mounted)
break;//找到的目錄不是掛載點就可以退出了
//找到的目錄仍然是掛載點,需要繼續找
//因為有可能多個檔案系統掛載到同一個目錄,因此需要在連結串列中找到最新的那個目錄
nd->path.mnt = &mounted->mnt;
nd->path.dentry = mounted->mnt.mnt_root;
nd->seq = read_seqcount_begin(&nd->path.dentry->d_seq);
if (read_seqretry(&mount_lock, nd->m_seq))
goto failed;
}
nd->inode = nd->path.dentry->d_inode;//更新inode
return 0;
failed:
nd->flags &= ~LOOKUP_RCU;
if (!(nd->flags & LOOKUP_ROOT))
nd->root.mnt = NULL;
rcu_read_unlock();
return -ECHILD;//返回使用ref-walk方式查詢
}
可見,如果當前目錄不是根目錄,也不是當前檔案系統的根目錄,也就是誰就是簡簡單單的一個普通目錄,那處理“..”也就是獲取父目錄的索引而已。
但是如果是該目錄掛載了多個檔案系統,那麼跨越到父目錄的時候需要先找到最初掛載的目錄結構,否則獲取到的檔案系統狀態仍然是該目錄層級。
static int follow_up_rcu(struct path *path)
{
struct mount *mnt = real_mount(path->mnt);
struct mount *parent;
struct dentry *mountpoint;
//獲取上一級mount結構
parent = mnt->mnt_parent;
//當前檔案系統的掛載點就是自己,即跨越到根檔案系統(rootfs)的時候
//在根檔案系統裡,其上一級mount結構指向的就是自己,因此他們的vfsmount結構相同
if (&parent->mnt == path->mnt)
return 0;
//走到這說明上一級mount結構是在另一個檔案系統中
mountpoint = mnt->mnt_mountpoint;
path->dentry = mountpoint;//更新掛載點,掛載點的本質也是目錄
path->mnt = &parent->mnt;//更新vfsmount結構,也就跨越到上一級目錄,完成..的操作
return 1;
}
如果當前檔案系統的掛載點就是自己,即跨越到根檔案系統(rootfs)的時候。此時follow_up_rcu函式將返回0,之後退出while(1)迴圈。
我們舉個例子說明這種情況,考慮以下場景,當前目錄pwd=/home/a/,檔案路徑path=…/log.txt(即絕對路徑為/home/log.txt),其中目錄home和a都是普通目錄,沒有掛載任何檔案系統。因此我們可以知道此時傳入follow_up_rcu函式的入參nd->path->mnt指向的是根檔案系統(rootfs),因此其上一級mount結構指向的還是自己,follow_up_rcu函式返回0,退出while(1)迴圈。
而需要while(1)迴圈則是因為某個目錄可能被重複掛載多個檔案系統。考慮另一種場景,當前目錄pwd=/home/a/,檔案路徑path=…/log.txt(即絕對路徑為/home/log.txt)。但是在此之前先將某個分割槽,如/dev/sda1,檔案系統為fs1,掛載至/home目錄,原先/home目錄下的檔案將被隱藏。然後再將另一個分割槽,如/dev/sda2,檔案系統為fs2,掛載至/home目錄,此時在/home目錄下再建立目錄a和檔案log.txt,形成開始時的場景。這個時候因為/home目錄被重複掛載,因此在a目錄訪問上級目錄下的log.txt檔案,我們需要一個迴圈體來順著mount結構的連結串列從fs2->fs1找到最初的檔案系統。
如果退出迴圈體,至此已經獲取到了當前目錄項的上一級目錄項(即“…”所代表的父目錄項)。
接下來又是一個while迴圈,這是考慮到這個父目錄項有可能也是一個掛載點,也可能被重複掛載,所以要獲取到最新的那個掛載系統。通過__lookup_mnt()檢查父目錄下掛載的檔案系統是否為最新的檔案系統,如果是則檢查結束;否則,將繼續檢查;
走完上述流程,我們也就處理完“.”和“…”的情況,接下來我們來看下如果是普通目錄的情況,這就是下次要說的了。