友情提示:您需要一個 kernel 3.15.6,下載地址:https://www.kernel.org/pub/linux/kernel/v3.0/linux-3.15.6.tar.xz
我們將以 Linux 系統調用 open 為主線,參觀游覽 Kernel 的文件系統,一窺 Kernel 文件系統精妙的設計和嚴謹的實現。因受篇幅限制,我們此次觀光只涉足 Kernel 的虛擬文件系統(vfs),對於具體的文件系統就不深入進去了。
各位,准備好了嗎?我們已經迫不及待要開始這次奇幻之旅了!
“前往 Kernel 的旅客請注意,您所乘坐的 OPEN1024 航班已經開始登機......”
好了,我們的 OPEN1024 航班已經通過 Linux 系統調用穿越了中斷門來到了內核空間,並且通過系統調用表(sys_call_table)到達了系統調用 open 的主程序 sys_open,這就開始我們的旅程吧。
【fs/open.c】sys_open()
點擊(此處)折疊或打開
SYSCALL_DEFINE3(open,
const char __user
*, filename,
int, flags, umode_t, mode)
{
if (force_o_largefile())
flags |= O_LARGEFILE;
return do_sys_open(AT_FDCWD, filename, flags, mode);
}
SYSCALL_DEFINE3 是用來定義系統調用的宏,展開後類似於這樣:
long sys_open(const char __user
*filename,
int flags, umode_t mode)
形參 filename 實際上就是路徑名;flags 表示打開模式,諸如只讀、新建等等;mode 代表新建文件的權限,所以僅僅在創建新文件時(flags 為 O_CREAT 或 O_TMPFILE)才使用,具體還有哪些標志位請參考 Linux man 手冊(http://man7.org/linux/man-pages/man2/open.2.html)。接下來,除了
flags 會在 64 位 Kernel 的情況下強制加上 O_LARGEFILE 標志位,其余的參數都原封不動的傳遞給 open 的主函數 do_sys_open。唯一需要注意的是 AT_FDCWD,其定義在 include/uapi/linux/fcntl.h,是一個特殊值(-100),該值表明當 filename 為相對路徑的情況下將當前進程的工作目錄設置為起始路徑。相對而言,你可以在另一個系統調用 openat 中為這個起始路徑指定一個目錄,此時 AT_FDCWD 就會被該目錄的描述符所替代。
現在來看 open 的主函數 do_sys_open。
【fs/open.c】sys_open()->do_sys_open()
點擊(此處)折疊或打開
long do_sys_open(int dfd,
const char __user
*filename,
int flags, umode_t mode)
{
struct open_flags op;
int fd = build_open_flags(flags, mode,
&op);
struct filename *tmp;
if (fd)
return fd;
tmp = getname(filename);
if (IS_ERR(tmp))
return PTR_ERR(tmp);
fd = get_unused_fd_flags(flags);
if (fd
>= 0)
{
struct file *f
= do_filp_open(dfd, tmp,
&op);
if (IS_ERR(f))
{
put_unused_fd(fd);
fd = PTR_ERR(f);
} else
{
fsnotify_open(f);
fd_install(fd, f);
}
}
putname(tmp);
return fd;
}
首先檢查並包裝傳遞進來的標志位(962),隨後將用戶空間的路徑名復制到內核空間(968),在順利取得空閒的文件表述符的情況下調用 do_filp_open 完成對路徑的搜尋和文件的打開(974),如果一切順利,就為這個文件描述符安裝文件(980),然後大功告成並將文件描述符返回用戶空間。在此之前還不忘使用 fsnotify 機制來喚醒文件系統中的監控進程(979)。
build_open_flags 就是對標志位進行檢查,然後包裝成 struct open_flags 結構以供接下來的函數使用。因為這些標志位大多涉及到對最終目標文件的操作,所以這個函數也等到我們用到這些標志位的時候再回過頭來看。
接下來就是 getname,這個函數定義在 fs/namei.c,主體是 getname_flags,我們撿重點的分析,無關緊要的代碼以 ... 略過:
【fs/namei.c】sys_open()->do_sys_open()->getname()->getname_flags()
點擊(此處)折疊或打開
static struct filename
*
getname_flags(const char __user
*filename,
int flags,
int *empty)
{
struct filename *result,
*err;
...
result = __getname();
...
kname = (char
*)result
+ sizeof(*result);
result->name
= kname;
result->separate
= false;
max = EMBEDDED_NAME_MAX;
recopy:
len = strncpy_from_user(kname, filename, max);
...
if (len
== EMBEDDED_NAME_MAX
&& max
== EMBEDDED_NAME_MAX)
{
kname =
(char *)result;
result = kzalloc(sizeof(*result), GFP_KERNEL);
...
result->name
= kname;
result->separate
= true;
max = PATH_MAX;
goto recopy;
}
...
}
首先通過第 144 行 __getname 在內核緩沖區專用隊列裡申請一塊內存用來放置路徑名,其實這塊內存就是一個 4KB 的內存頁。這塊內存頁是這樣分配的,在開始的一小塊空間放置結構體 struct filename,之後的空間放置字符串。152 行初始化字符串指針 kname,使其指向這個字符串的首地址,相當於 kname = (char *)((struct filename *)result + 1)。然後就是拷貝字符串(158),返回值
len 代表了已經拷貝的字符串長度。如果這個字符串已經填滿了內存頁剩余空間(170),就說明該字符串的長度已經大於 4KB - sizeof(struct filename) 了,這時就需要將結構體 struct filename 從這個內存頁中分離(180)並單獨分配空間(173),然後用整個內存頁保存該字符串(171)。
回到 do_sys_open,現在需要為新打開的文件分配空閒文件描述符。get_unused_fd_flags 主要作用就是在當前進程的 files 結構中找到空閒的文件描述符,並初始化該描述符對應的 file 結構。
一切准備就緒,就進入 do_filp_open 了。
【fs/namei.c】sys_open->do_sys_open->do_filp_open
點擊(此處)折疊或打開
struct file *do_filp_open(int dfd, struct filename
*pathname,
const struct open_flags
*op)
{
struct nameidata nd;
int flags
= op->lookup_flags;
struct file *filp;
filp = path_openat(dfd, pathname,
&nd, op, flags
| LOOKUP_RCU);
if (unlikely(filp
== ERR_PTR(-ECHILD)))
filp = path_openat(dfd, pathname,
&nd, op, flags);
if (unlikely(filp
== ERR_PTR(-ESTALE)))
filp = path_openat(dfd, pathname,
&nd, op, flags
| LOOKUP_REVAL);
return filp;
}
主角是 path_openat,在這裡 Kernel 向我們展示了“路徑行走(path walk)”的兩種策略:rcu-walk(3242)和 ref-walk(3244)。在 rcu-walk 期間將會禁止搶占,也決不能出現進程阻塞,所以其效率很高;ref-walk 會在 rcu-walk 失敗、進程需要隨眠或者需要取得某結構的引用計數(reference count)的情況下切換進來,很明顯它的效率大大低於 rcu-walk。最後 REVAL(3246)其實也是 ref-walk,在以後我們會看到,該模式是在已經完成了路徑查找,打開具體文件時,如果該文件已經過期(stale)才啟動的,所以
REVAL 是給具體文件系統自己去解釋的。其實 REVAL 幾乎不會用到,在內核的文件系統中只有 nfs 用到了這個模式。
path_openat 主要作用是首先為 struct file 申請內存空間,設置遍歷路徑的初始狀態,然後遍歷路徑並找到最終目標的父節點,最後根據目標的類型和標志位完成 open 操作,最終返回一個新的 file 結構。我們分段來看:
【fs/namei.c】sys_open->do_sys_open->do_filp_open->path_openat
點擊(此處)折疊或打開
static struct file *path_openat(int dfd, struct filename
*pathname,
struct nameidata *nd,
const struct open_flags
*op, int flags)
{
...
file = get_empty_filp();
if (IS_ERR(file))
return file;
file->f_flags
= op->open_flag;
...
error = path_init(dfd, pathname->name,
flags | LOOKUP_PARENT, nd,
&base);
if (unlikely(error))
goto out;
...
首先需要分配一個 file 結構,成功的話 get_empty_filp 會返回一個指向該結構的指針,在這個函數裡會對權限、最大文件數進行檢查。我們忽略有關 tempfile 的處理,直接來看 path_init。path_init 是對真正遍歷路徑環境的初始化,主要就是設置變量 nd。這個 nd 是 do_filp_open 裡定義的局部變量,是一個臨時性的數據結構,用來存儲遍歷路徑的中間結果,其結構體定義如下:
【include/linux/namei.h】
點擊(此處)折疊或打開
struct nameidata {
struct path path;
struct qstr last;
struct path root;
struct inode *inode;
/* path.dentry.d_inode
*/
unsigned int flags;
unsigned seq, m_seq;
int last_type;
unsigned depth;
char *saved_names[MAX_NESTED_LINKS
+ 1];
};
其中,path 保存當前搜索到的路徑;last 保存當前子路徑名及其散列值;root 用來保存根目錄的信息;inode 指向當前找到的目錄項的 inode 結構;flags 是一些和查找(lookup)相關的標志位;seq 是相關目錄項的順序鎖序號; m_seq 是相關文件系統(其實是 mount)的順序鎖序號; last_type 表示當前節點類型;depth 用來記錄在解析符號鏈接過程中的遞歸深度;saved_names 用來記錄相應遞歸深度的符號鏈接的路徑。我們結合 path_init
的代碼來看這些成員的初始化。
【fs/namei.c】sys_open->do_sys_open->do_filp_open->path_openat->path_init
點擊(此處)折疊或打開
static int path_init(int dfd,
const char *name, unsigned
int flags,
struct nameidata *nd, struct file
**fp)
{
int retval
= 0;
nd->last_type
= LAST_ROOT;
/*
if there are only slashes...
*/
nd->flags
= flags | LOOKUP_JUMPED;
nd->depth
= 0;
if (flags
& LOOKUP_ROOT)
{
...
}
nd->root.mnt
= NULL;
nd->m_seq
= read_seqbegin(&mount_lock);
if (*name=='/')
{
...
set_root(nd);
...
nd->path
= nd->root;
} else
if (dfd
== AT_FDCWD)
{
...
get_fs_pwd(current->fs,
&nd->path);
...
} else
{
...
}
nd->inode
= nd->path.dentry->d_inode;
return 0;
}
首先將 last_type 設置成 LAST_ROOT,意思就是在路徑名中只有“/”。為方便敘述,我們把一個路徑名分成三部分:起點(根目錄或工作目錄)、子路徑(以“/”分隔的一系列子字符串)和最終目標(最後一個子路徑),Kernel 會一個子路徑一個子路徑的遍歷整個路徑。所以 last_type 表示的是當前子路徑(不是 dentry 或 inode)的類型。last_type 一共有五種類型:
【include/linux/namei.h】
點擊(此處)折疊或打開
/*
* Type of the last component
on LOOKUP_PARENT
*/
enum {LAST_NORM, LAST_ROOT, LAST_DOT, LAST_DOTDOT, LAST_BIND};
LAST_NORM 就是普通的路徑名;LAST_ROOT 是 “/”;LAST_DOT 和 LAST_DOTDOT 分別代表了 “.” 和 “..”;LAST_BIND 就是符號鏈接。
下面接著來看 path_init,LOOKUP_ROOT 可以提供一個路徑作為根路徑,主要用於兩個系統調用 open_by_handle_at 和 sysctl,我們就不關注了。然後是根據路徑名設置起始位置,如果路徑是絕對路徑(以“/”開頭)的話,就把起始路徑指向進程的根目錄(1849);如果路徑是相對路徑,並且 dfd 是一個特殊值(AT_FDCWD),那就說明起始路徑需要指向當前工作目錄,也就是 pwd(1856);如果給了一個有效的 dfd,那就需要吧起始路徑指向這個給定的目錄(1871)。
path_init 返回之後 nd 中的 path 就已經設定為起始路徑了,現在可以開始遍歷路徑了。在此之前我們先探討一下 Kernel 的文件系統,特別是 vfs 的組織結構。vfs 是具體文件系統(如 ext4、nfs、fat)和 Kernel 之間的橋梁,它將各個文件系統抽象出來並提供一個統一的機制來組織和管理各個文件系統,但具體的實現策略則由各個文件系統來實現,這很好的屏蔽的各個文件系統的差異,也非常容易擴展,這就是
Linux 著名格言“提供機制而不是策略”的具體實踐。vfs 中有兩個個很重要的數據結構 dentry 和 inode,dentry 就是“目錄項”保存著諸如文件名、路徑等信息;inode 是索引節點,保存具體文件的數據,比如權限、修改日期、設備號(如果是設備文件的話)等等。文件系統中的所有的文件(目錄也是一種特殊的文件)都必有一個 inode 與之對應,而每個 inode 也至少有一個 dentry 與之對應(也有可能有多個,比如硬鏈接)。結合下圖我們可以更清晰的理解這個架構:
【dentry-inode 結構圖】
首先有這樣一個文件:/home/user1/file1,它的目錄項中 d_parent 指針指向它所在目錄的目錄項 /home/user1(1),而這個目錄項中有一個雙向鏈表 d_subdirs,裡面鏈接著該目錄的子目錄項(2),所以 /home/user1/file1 目錄項裡的 d_u 也加入到了這個鏈表(3),這樣一個文件上下關系就建立起來了。同樣,/home/user1 的 d_parent 將指向它的父目錄 /home,並且將自己的 d_u 鏈接到 /home 的 d_subdirs。file1
的目錄項中有一個 d_inode 指針,指向一個 inode 結構(4),這個就是該文件的索引節點了,並且 file1 目錄項裡的 d_alias 也加入到了 inode 的鏈表 i_dentry 中(5),這樣 dentry 和 inode 的關系也建立起來了。前面講過,如果一個文件的硬連接不止一個的話就會有多個 dentry 與 inode 相關聯,請看圖中 /home/user2/file2,它和 file1 互為硬鏈接。和 file1 一樣,file2 也把自己的 d_inode 指向這個 inode
結構(6)並且把 d_alias 加入到了 inode 的鏈表 i_dentry 裡(7)。這樣無論是通過 /home/user1/file1 還是 /home/user2/file2,訪問的都是同一個文件。還有,目錄也是允許硬鏈接的,只不過不允許普通用戶創建目錄的硬鏈接。
但是 Kernel 並不直接使用這樣的結構來進行路徑的遍歷,為了提高效率 Kernel 使用散列數組來組織這些 dentry 和 inode,這已經超出我們的討論范圍了,所以知道有這麼個東西就好了。現在我們可以回到 path_openat 接著我們的旅行了。
【fs/namei.c】sys_open->do_sys_open->do_filp_open->path_openat
點擊(此處)折疊或打開
...
current->total_link_count
= 0;
error = link_path_walk(pathname->name,
nd);
if (unlikely(error))
goto out;
...
total_link_count 是用來記錄符號鏈接的深度,每穿越一次符號鏈接這個值就加一,最大允許 40 層符號鏈接。接下來 link_path_walk 會帶領我們走向目標,並在到達最終目標所在目錄的時候停下來(最終目標需要交給另一個函數 do_last 單獨處理)。下面我們就來看看這個函數是怎樣一步一步接近目標的。
【fs/namei.c】sys_open->do_sys_open->do_filp_open->path_openat->link_path_walk
點擊(此處)折疊或打開
static int link_path_walk(const char
*name, struct nameidata
*nd)
{
...
while (*name=='/')
name++;
if (!*name)
return 0;
...
首先略過連續的“/”(可以試試這個命令“ls /////dev/”,看看有什麼效果),如果此時路徑就結束了那就相當於整個路徑只有一個“/”(1741),還記得在 init_path() 裡 nd->last_type 的初始值是什麼麼?沒錯這就是 LAST_ROOT。那麼如果路徑沒有結束呢,那就說明我們至少擁有了一個真正的子路徑,這就需要進入 1745 行這個大大的循環體來一步一步的走下去,所以連函數名都叫做“路徑行走”嘛。
【fs/namei.c】sys_open->do_sys_open->do_filp_open->path_openat->link_path_walk
點擊(此處)折疊或打開
...
for(;;)
{
...
err = may_lookup(nd);
...
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;
...
首先是例行安全檢查(1750),然後就看子路徑名是否是“.”或“..”並做好標記(1759-1768)。如果不是“.”或“..”那麼這就是一個普通的路徑名,此時還要看看這個當前目錄項是否需要重新計算一下散列值(1772)。現在可以先把子路徑名更新一下(1779),如果此時已經到達了最終目標,那麼“路徑行走”的任務就完成了(1782 和 1791)。如果路徑還沒到頭,那麼現在就一定是一個“/”,再次略過連續的“/”(1790)並讓 name 指向下一個子路徑(1794),為下一次循環做好了准備。
到現在為止我們第一天的行程已經結束了,好好休息准備明天的旅程吧。