天天看點

linux path_lookup,Linux虛拟檔案系統(4)-- 路徑名查找

前言

幾乎所有Linux的檔案操作,例,read、write、mkdir等都會涉及到路徑名查找操作。而檔案查找對Linux核心來說,主要指的是找到檔案路徑對應的Dentry節點。其主要過程就是對路徑字元串進行一級級解析(以路徑名中的.. , . , /等字元作為解析依據),找到路徑的最後一級目錄。若傳入的路徑字元串是以/開始的,那麼查找會從系統根目錄開始。否則,從目前工作目錄開始查找。

linux path_lookup,Linux虛拟檔案系統(4)-- 路徑名查找

然而,查找過程并非僅僅是對路徑名一級級解析和比對,其中還要考慮到如下細節:

使用者可能對要查找的路徑沒有通路權限

路徑名查找是系統中頻繁且常見的操作,如何保證查找的效率

多個程序 or 使用者會同時操作到同一個路徑,查找過程中需要做必要的保護

查找過程中可能經過符号連結,同時還要考慮避免符号連結的循環引用,造成無限查找

查找過程可能會跨越多個檔案系統類别

…….

引用核心文檔對VFS路徑查找的介紹:

The most obvious aspect of pathname lookup, which very little exploration is needed to discover, is that it is complex. There are many rules, special cases, and implementation alternatives that all combine to confuse the unwary reader.

上篇文章中介紹了Mount系統調用,其中do_mount函數會進行檔案系統挂載點的查找,代碼如下:

long do_mount(const char dev_name, const char __user *dir_name,

const char *type_page, unsigned long flags, void *data_page)

{

……..

/ … and get the mountpoint */

retval = user_path(dir_name, &path)

……..

}

user_path()函數傳入要查找的路徑名,傳回struct path結構體類型供後續使用。user_path()最終會調用到filename_lookup(),執行檔案查找工作。本章就對filename_lookup()函數進行深入剖析(以Linux 4.4核心為基礎)。

資料結構

與路徑查找相關的資料結構是struct nameidata,其具體字段及主要作用如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22struct nameidata {

struct path path; //記錄路徑查找的結果

struct qstr last; //路徑名最後一個分量

struct path root; //路徑查找的根目錄資訊,可能會在查找開始時由調用者傳入

struct inode    *inode;

unsigned int    flags; //路徑名查找标志

unsigned    seq, m_seq; //

int     last_type; //記錄目前查找到目錄的類别Normal/Dot/DotDot/Root/Bind

unsigned    depth; //查找過程中跨越的符号連結深度

int     total_link_count; //查找過程中經過的符号連結總數

struct saved {

struct path link;

void *cookie;

const char *name;

struct inode *inode;

unsigned seq;

} *stack, internal[EMBEDDED_LEVELS]; //用來記錄查找過程中碰到的符号連結

struct filename *name; //

struct nameidata *saved;//

unsigned    root_seq; //

int     dfd; //

};

filename_lookup

函數filename_lookup解析如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24static 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);

if (unlikely(root)) {

nd.root = *root;

flags |= LOOKUP_ROOT;

}

set_nameidata(&nd, dfd, name);

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_PARENT);

restore_nameidata();

putname(name);

return retval;

}

Kernel路徑名查找的方式有兩種:

REF-Walk方式:指的是路徑查找過程中,使用Spinlock(dentryàd_lock)并發使用或者修改目錄項,來保證系統最終目錄項内容的正确性。但是Spinlock因為會引發阻塞,是以效率會低于RCU-Walk.Kernel路徑名查找的方式有兩種:

RCU-Walk方式:采用RCU鎖的方式進行查找,并發查找過程中并不會因為等待spinlock而阻塞,是以速度相對更快。但是它并不能保證所有情況下都能查找成功

filename_lookup函數首先初始化上文介紹的nameidata結構體,接着進行RCU方式查找指定路徑。若RCU查找失敗,則退回傳統的查找方式(REF-Walk)。正因為Linux路徑查找穿插了兩種查找方式的代碼,是以讀起來比較困難。本文接下來試圖将兩種查找方式分開進行介紹。

REF-Walk

path_lookupat主要代碼如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26static int path_lookupat(struct nameidata *nd, unsigned flags, struct path *path)

{

const char *s = path_init(nd, flags);

while (!(err = link_path_walk(s, nd))

&& ((err = lookup_last(nd)) > 0)) {

s = trailing_symlink(nd);

if (IS_ERR(s)) {

err = PTR_ERR(s);

break;

}

}

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;

}

首先path_init對nameidata做必要的初始指派并傳回路徑字元串:

如果執行路徑查找函數的函數傳入了root參數,那麼會置起LOOKUP_ROOT标志,此時會對目前使用者的對應root目錄通路權限進行檢查,若不允許,則傳回錯誤。

如果未置起LOOKUP_ROOT,則判斷路徑名查找從根目錄,目前目錄或者對應檔案描述符對應的目錄開始查找,并修改nameidata的path字段,最後修改nameidata的inode字段作為查找起始點的inode。

其次循環執行link_path_walk(),做真正的路徑查找,直到碰到查找錯誤或者查找結束。接下來一節深入剖析。 之後執行complete_walk(),主要為再次确認要通路的dentry是否仍然有效(這取決于對應dentry的檔案系統的d_weak_revalidate函數,一般情況下為NULL,且對REF-WALK模式來講不會被調用到)。 最後執行terminate_walk()前,将nameidata的查找結果(即path)指派給調用者傳入的參數。而terminate_walk()則會将對路徑的引用釋放,同時将查找過程中跨越的符号連結引用釋放掉并将nameidata的深度置為0。

link_path_walk

link_path_walk()首先跳過路徑名中的斜杠/,接下來執行其核心:一個for循環。主要做如下事情:

檢查目前要查找目錄是否有查找的權限(目前目錄對應inode是否有EXEC權限),若沒有則退出查找。

對目前查找目錄計算其Hash長度

若目前查找目錄為”..”,則置起來LOOKUP_JUMP标記,表示查找跳過該目錄。否則開始路徑查找walk_component()

若第三步查找傳回為0,則進行下一路徑分量的查找(Walk componet過程中會修改nameidata的path以及last等字段)

若第三步查找傳回不為0且不為負數,則表示查找過程中碰到符号連結,修改符号連結對應節點的通路時間等資訊,并将相關資訊記錄在nameidata的stack字段。

重新開始執行for循環,直到路徑周遊結束(關鍵變量name為NULL)

walk_component

walk_component主要做如下事情:

處理點符号(即.或者..),這裡主要處理的是..傳回上一級目錄,這裡要特别處理的是上級目錄可能與目前在兩個不同的系統挂載點上。

若目前要查找的路徑是普通路徑,則進行路徑的快查找lookup_fast,其會執行dentry = __d_lookup(parent, &nd->last),即從dentry高速緩存中查找。

若第2步查找失敗則進行慢查找lookup_slow(),其調用__lookup_hash進行查找,這會執行lookup_real到對應檔案系統的i_op到磁盤上去進行查找。

判斷查找到的目錄分量是否是符号連結,若是符号連結,則看目前周遊的符号連結數量是否超過系統中允許的最大數量(避免循環周遊),如超過則傳回錯誤。否則,在nameidata中的stack字段為目前周遊到的符号連結配置設定空間并做相應指派。注意,nameidata字段預設已經預留了儲存符号連結的stack空間,隻有當周遊過程經過的符号連結超過數量時,才需要重新配置設定。

将查找的目錄分量資訊記錄在nameidata裡,供下個查找循環使用

RCU-Walk

待添加