回到 path_openat:
【fs/namei.c】sys_open
> do_sys_open >
do_filp_open >
path_openat
點擊(此處)折疊或打開
...
error = do_last(nd,
&path, file, op,
&opened, pathname);
while (unlikely(error
> 0))
{ /* trailing symlink
*/
struct path link
= path;
void *cookie;
if (!(nd->flags
& LOOKUP_FOLLOW))
{
path_put_conditional(&path, nd);
path_put(&nd->path);
error
= -ELOOP;
break;
}
error
= may_follow_link(&link, nd);
if (unlikely(error))
break;
nd->flags
|= LOOKUP_PARENT;
nd->flags
&=
~(LOOKUP_OPEN|LOOKUP_CREATE|LOOKUP_EXCL);
error
= follow_link(&link, nd,
&cookie);
if (unlikely(error))
break;
error
= do_last(nd,
&path, file, op,
&opened, pathname);
put_link(nd,
&link, cookie);
}
...
其實到現在為止我們的旅程才進行了一半,還記得嗎,我們現在只是順著路徑走到了最終目標所在的目錄,對最終目標還沒有進行任何處理,甚至連這個最終目標到底存不存在我們都不知道。這一切都會交給 do_last 來完成,如果一切順利它會填充 file 結構並返回 0;如果返回值大於 0(其實還是 1,為什麼是“還是”?回頭看看 walk_component 你就明白了),那就表明這個最終目標是一個符號鏈接且需要跟隨這個符號鏈接,那麼就要進入 3194 行這個循環來順著符號鏈接找到真正目標(還有一種情況是雖然最終目標是一個符號鏈接但我們只想得到這個符號鏈接本身,也不會進入這個循環)。符號鏈接的處理我們應該很熟悉了,只不過因為是最終目標所以增加了一些對標志位的判斷和處理,大家理解這段代碼應該不會有什麼問題。
接下來我們就要進入 do_last 了,首先快速浏覽一遍這個函數,它有 228 行,而且遍地是 if,到處是 goto,誰第一次看到它都會發懵,根本就無從下手。但是只要我們抓住本質,一切事物都有規律可循,比如這個 do_last,可以說它是既復雜又簡單。說它復雜是因為這裡面進行了大量的標志位的檢查,這些標志位是用戶傳進來指示 Kernel 需要打開什麼樣的文件、打開這些文件的模式以及怎麼打開這些文件的,這些標志位總共有二十幾個而且還相互影響相互制約,盤根錯節的能不復雜嗎?說它簡單,那是和 link_path_walk
相比,在邏輯上和結構上相對簡單,雖然這裡也會遇到諸如跟隨“..”、跟隨符號鏈接、跟隨掛載點,但這都是我們熟悉的老朋友了,沒有什麼新奇的東西。
既然 do_last 的復雜之處是在於對各個標志位的判斷,那麼我們至少需要了解設置這些標志位的目的,這可以參考 Linux 最權威的文檔——man 手冊有關 open 的內容(http://man7.org/linux/man-pages/man2/open.2.html)。我們把這個手冊當成指路牌,它可以引導我們理解
do_last 裡對這些標志位的操作。其實用戶設置的標志位並不是原封不動的傳遞進來,大家應該還記得在 do_sys_open 裡會調用 build_open_flags 對這些標志位進行預處理和分裝,所以我們可能還需要參考這個函數。
事情是這樣的話我們的 do_last 之行就不能像之前一樣沿著代碼一步一步邊走邊看了,如果那樣的話我們很快就會迷失在 flag 的海洋之中,而且也會使我們本來愉快的旅行變得枯燥乏味。現在讓我們用一種全新的方式游覽 do_last 吧,我稱之為“情景模式”。在情景模式中我們假設了幾個 open 應用的場景,然後在我們想像的場景之中看看都有哪些標志位起到了怎樣的作用。
【場景一】open(pathname, O_RDONLY)
使用只讀方式打開一個文件,這應該是最簡單的 open 應用了。我們先來看看 build_open_flags 是怎麼包裝 O_RDONLY 的:
【fs/open.c】sys_open
> do_sys_open > build_open_flags
點擊(此處)折疊或打開
static inline int build_open_flags(int flags, umode_t mode,
struct open_flags *op)
{
...
op->mode
= 0;
...
acc_mode = MAY_OPEN
| ACC_MODE(flags);
...
op->intent
= flags & O_PATH
? 0 : LOOKUP_OPEN;
...
}
經過我們的簡化,build_open_flags 和 O_RDONLY 相關的就剩這麼三行了。我記得第一天說過,mode 代表文件權限只有在新建文件的時候才會用到,咱們當前的情景顯然不是創建文件,所以先將 mode 置零(847)。接下來設置訪問模式 acc_mode 這個東西主要用來進行權限檢查,把宏 ACC_MODE 展開後這一行其實就是這樣:acc_mode = MAY_OPEN | MAY_READ,其意思也很明白,那就是對於目標文件我們至少需要打開和讀取的權限。最後是判斷標志位中 O_PATH
有沒有被設置,如果沒有就將 intent 加上 LOOKUP_OPEN,intent 用來標記我們對最終目標想要做什麼操作(intent 字面意思就是“意圖”),所以在以後我們會看到這裡暫存的 op->intent 會在 do_last 裡重出江湖。
build_open_flags 之後我們直接進入 do_last:
【fs/namei.c】sys_open
> do_sys_open >
do_filp_open >
path_openat > do_last
點擊(此處)折疊或打開
static int do_last(struct nameidata
*nd, struct path
*path,
struct file *file,
const struct open_flags
*op,
int *opened, struct filename
*name)
{
...
nd->flags
&=
~LOOKUP_PARENT;
nd->flags
|= op->intent;
...
if (!(open_flag
& O_CREAT))
{
...
error
= lookup_fast(nd, path,
&inode);
if (likely(!error))
goto finish_lookup;
...
}
retry_lookup:
...
LOOKUP_PARENT 實在 patn_init 的時候設置的,當時我們的目標是找到最終文件的父目錄,但現在我們要找的就是最終文件,所以需要將這個標志位清除(2888),緊接著 intent 重現江湖(2889)。很明顯我們當前的情況不是創建文件,所以會進入 2898 這個 if,這裡有個熟面孔 lookup_fast,看到老朋友就是高興。還記得 lookup_fast 執行結構有幾種情況麼?如果返回 0,則表示成功找到;返回 1,表示內存中沒有,需要 lookup_real;返回小於 0,則表示需要從當前
rcu-walk 轉到 ref-walk。那現在我們先看看返回 1 的情況:
【fs/namei.c】sys_open
> do_sys_open >
do_filp_open >
path_openat > do_last
點擊(此處)折疊或打開
...
retry_lookup:
...
mutex_lock(&dir->d_inode->i_mutex);
error = lookup_open(nd, path, file, op,
got_write, opened);
mutex_unlock(&dir->d_inode->i_mutex);
...
error = follow_managed(path, nd->flags);
...
inode = path->dentry->d_inode;
lookup_fast 返回 1 的話程序會直接走到標號 retry_lookup 處。現在的程序已經肯定不在 rcu-walk 模式裡了(為什麼?),所以可以使用各種有可能引起進程阻塞的鎖來占有相應的資源了(2941)。接下來是一個新朋友 lookup_open,說是新朋友其實是新瓶裝舊酒,因為它和 lookup_slow 很像,都是先使用 lookup_dcache 在內存中找,如果不行就啟動 lookup_real 在具體文件系統裡面去找,當它成功返回時會將 path 指向找到的目標。接下來是
follow_managed 它也算是老朋友吧,之前我們簡單介紹過的。再往下走,我們來到了 finish_lookup:
【fs/namei.c】sys_open
> do_sys_open >
do_filp_open >
path_openat > do_last
點擊(此處)折疊或打開
...
finish_lookup:
...
if ((nd->flags
& LOOKUP_RCU)
|| nd->path.mnt
!= path->mnt)
{
path_to_nameidata(path, nd);
} else
{
save_parent.dentry
= nd->path.dentry;
save_parent.mnt
= mntget(path->mnt);
nd->path.dentry
= path->dentry;
}
nd->inode
= inode;
這裡是 lookup_fast 返回 1 和返回 0 的交匯點。這時就需要更新 nd 了,但這個交匯點有兩個來源也就是說現在有可能還在 rcu-walk 模式當中,所以還需要分情況處理一下(3014)。請注意這個 if 的第二個條件“nd->path.mnt != path->mnt”,什麼情況下會出現這兩個 mnt 不相等呢?還記得 nd 的脾氣嗎,當遇到掛載點的時候 nd 會原地踏步,只有 path 才大無畏的向前走。既然兩個 mnt 不一樣了,那麼更新 nd 前也許要放棄原先占有的結構,這就是
path_to_nameidata 所做。接下來就要徹底告別 rcu-walk 了:
【fs/namei.c】sys_open
> do_sys_open >
do_filp_open >
path_openat > do_last
點擊(此處)折疊或打開
...
finish_open:
error = complete_walk(nd);
...
我們到 complete_walk 裡面看看 rcu-walk 告別儀式:
【fs/namei.c】sys_open
> do_sys_open >
do_filp_open >
path_openat > do_last > complete_walk
點擊(此處)折疊或打開
static int complete_walk(struct nameidata
*nd)
{
struct dentry *dentry
= nd->path.dentry;
int status;
if (nd->flags
& LOOKUP_RCU)
{
nd->flags
&=
~LOOKUP_RCU;
...
rcu_read_unlock();
}
if (likely(!(nd->flags
& LOOKUP_JUMPED)))
return 0;
...
}
告別 rcu-walk 其實很簡單,最主要的就是 605 和 624 這兩行,rcu_read_unlock 之後就會重新啟動進程搶占,本進程就有可能被切換出去,這樣的話當前 CPU 就會經過 quiescent state,當所有 CPU 都經過了自己的 quiescent state 之後,在 rcu-walk 期間的變更才會被更新。接著如果當前不是 LOOKUP_JUMPED,就會直接返回。這個 LOOKUP_JUMPED 在哪裡會被設置呢?其實我們之前遇到了很多次設置 LOOKUP_JUMPED
的地方,比方說遇到“..”的時候、遇到掛載點的時候、符號鏈接是絕對路徑的時候,它們的共同點就是有可能會跨越(jump)文件系統。如果的確跨越了文件系統也很簡單,檢查一下當前 dentry 需不需要驗證(DCACHE_OP_WEAK_REVALIDATE),大部分情況是不需要,所以這段代碼咱們也省略了,有興趣的同學請查閱 Kernel 源代碼。
接下來終於要真正“打開”這個文件了:
【fs/namei.c】sys_open
> do_sys_open >
do_filp_open >
path_openat > do_last
點擊(此處)折疊或打開
...
finish_open_created:
error = may_open(&nd->path,
acc_mode, open_flag);
if (error)
goto out;
file->f_path.mnt
= nd->path.mnt;
error = finish_open(file, nd->path.dentry,
NULL, opened);
if (error)
{
if (error
==
-EOPENSTALE)
goto stale_open;
goto out;
}
opened:
...
out:
if (got_write)
mnt_drop_write(nd->path.mnt);
path_put(&save_parent);
terminate_walk(nd);
return error;
...
}
may_open 就是權限和標志位的檢查,咱們就懶得進去看了。finish_open 會真正打開這個我們期待已久的目標文件,裡面主要是利用該文件系統自己的 file_operations.open 來填充 file 結構。如果一切順利 finish_open 返回 0,然後釋放占用的資源之後就大功告成可以返回了。
這樣,我們的情景一就結束了,休息一下,咱們再看看稍微復雜一點的情景。