Linux Open系統調用篇一html
Linux Open系統調用篇二linux
內核源碼:linux-4.4 目標平臺:ARM體系結構 源碼工具:source insight 4數組
說明: 文中因爲 md 語法問題,沒法在代碼高亮的同時而忽略因爲
__
或者*
形成斜體的 問題,因此相似__user
改爲__ user
,或者char *filename
改爲char* filename
。 經過在中間添加空格進行避免。註釋統一使用了\\
。緩存
應用層的 open
函數是 glibc 庫封裝了系統調用以比較友好的方式提供給開發者。 那麼爲何要這麼作? 這主要是從安全以及性能這兩大方面進行了考慮:安全
在用戶空間和內核空間之間,有一個叫作Syscall(系統調用, system call)的中間層,是鏈接用 戶態和內核態的橋樑。這樣即提升了內核的安全型,也便於移植, 只需實現同一套接口便可。Linux系統,用戶空間經過向內核空間發出Syscall,產生軟中斷, 從而讓程序陷入內核態,執行相應的操做。對於每一個系統調用都會有一個對應的系統調用號 ,比不少操做系統要少不少。markdown
安全性與穩定性:內核駐留在受保護的地址空間,用戶空間程序沒法直接執行內核代碼 ,也沒法訪問內核數據,經過系統調用數據結構
性能:Linux上下文切換時間很短,以及系統調用處理過程很是精簡,內核優化得好,因此性能上 每每比不少其餘操做系統執行要好。app
在應用層對於 open
操做主要使用的是如下兩個函數:
(1) int open(const char *pathname, int flags, mode_t mode); (2) int openat(int dirfd, const char *pathname, int flags, mode_t mode); 複製代碼
若是打開文件成功,那麼返回文件描述符,值大於或等於0;若是打開文件失敗,返 回負的錯誤號。
下面是該函數參數的說明:
openat
中解釋爲相對文件描述符 dirfd
引用的目錄,open
函數中解釋爲相對 調用進程的當前工做目錄。若是文件路徑是絕對路徑, openat
忽略參數 dirfd
。O_RDONLY
(只讀)、O_ WRONLY
(只寫)或 O_RDWR
(讀寫)。參數 flags
能夠包含多個文件建立標誌和文件狀態標誌。 兩組標誌的區別是: 文件建立標誌隻影響打開操做, 文件狀態標誌影響後面的讀寫操做。文件建立標誌包括以下:
O_CLOEXEC
:開啓 close-on-exc標誌,使用系統調用 execve() 裝載程序的時候關閉文件。CREAT
:若是文件不存在,建立文件。ODIRECTORY
:參數 pathname 必須是一個日錄。EXCL
:一般和標誌位 CREAT 聯合使用,用來建立文件。若是文件已經存在,那麼 open() 失敗,返回錯誤號 EEXIST。NOFOLLOW
:不容許參數 pathname 是符號連接,即最後一個份量不能是符號 連接,其餘份量能夠是符號連接。若是參數 pathname 是符號連接,那麼打開失敗,返回錯誤號 ELOOP。O_TMPFILE
:建立沒有名字的臨時普通文件,參數 pathname 指定目錄關閉文件的時候,自動刪除文件。O_TRUNC
:若是文件已經存在,是普通文件而且訪問模式容許寫,那麼把文件截斷到長度爲0。文件狀態標誌包括以下:
APPEND
:使用追加模式打開文件,每次調用 write 寫文件的時候寫到文件的末尾。O_ASYNC
:啓用信號驅動的輸入輸出,當輸入或輸出可用的時候,發送信號通知進程,默認的信號是 SIGIO。O_DIRECT
:直接讀寫存儲設備,不使用內核的頁緩存。雖然會下降讀寫速度, 可是在某些狀況下有用處,例如應用程序使用本身的緩衝區,不須要使用內核的頁緩存文件。DSYNC
:調用 write 寫文件時,把數據和檢索數據所須要的元數據寫回到存儲設備LARGEFILE
:容許打開長度超過 4 GB 的大文件。NOATIME
:調用 read 讀文件時,不要更新文件的訪問時間。O_NONBLOCK
:使用非阻塞模式打開文件, open 和之後的操做不會致使調用進程阻塞。PATH
:得到文件描述符有兩個用處,指示在目錄樹中的位置以及執行文件描述符層次的操做。 不會真正打開文件,不能執行讀操做和寫操做。O_SYNC
:調用 write 寫文件時,把數據和相關的元數據寫回到存儲設備。參數 mode: 參數 mode 指定建立新文件時的文件模式。當參數 flags 指定標誌位 O_CREAT
或 O_TMPFILE
的時候,必須指定參數 mode,其餘狀況下忽略參數 mode。 參數 mode 能夠是下面這些標準的文件模式位的組合。
S_IRWXU
(0700,以0開頭表示八進制):用戶(即文件擁有者)有讀、寫和執行權限。S_IRUSR
(00400):用戶有讀權限。S_IWUSR
(00200):用戶有寫權限S_IXUSR
(00100):用戶有執行權限。S_IRWXG
(00070):文件擁有者所在組的其餘用戶有讀、寫和執行權限S_IRGRP
(00040):文件擁有者所在組的其餘用戶有讀權限。S_IWGRP
(00020):文件擁有者所在組的其餘用戶有寫權限。S_IXGRP
(0010):文件擁有者所在組的其餘用戶有執行權限。S_IRWXO
(0007):其餘組的用戶有讀、寫和執行權限。S_IROTH
(0004):其餘組的用戶有讀權限。S_IWOTH
(00002):其餘組的用戶有寫權限。S_IXOTH
(00001):其餘組的用戶有執行權限。參數 mode 能夠包含下面這些 Linux 私有的文件模式位:
S_ISUID
(0004000):set-user-ID 位。S_ISGID
(0002000):set-group-iD位。S_ISVTX
(0001000):粘滯(sticky)位。以上內容能夠參考:open man7手冊
那麼咱們該如何找到對應的 syscall
? 有幾個小技巧能夠用來幫助咱們:
細節能夠參考下面給出的連接:Linux系統調用(syscall)原理。
根據第一個小技巧,咱們知道咱們須要找的函數爲:sys_open
。 具體代碼流程比較複雜,這裏使用取巧的方式,找到對應的內核函數,前面提到須要找的的函數 爲 sys_open
。 這種函數在內核中是經過宏定義 SYSCALL_DEFINEx
展開後獲得的。那麼能夠 利用 source insight
的搜索功能。應用層 open
函數的參數的個數爲 3,能夠假想先從 SYSCALL_DEFINE3
進行全局搜索。隨便選擇一個搜索結果,這裏假設選擇的是 SYSCALL_DEFINE3(mknod
,這步主要是爲了獲取代碼格式,把 mknod
改爲 open
,而後搜索 SYSCALL_DEFINE3(open
。 很快咱們就在 kernel\fs\open.c
文件中找到惟一的搜索結果,代碼以下:
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); } 複製代碼
if (force_o_largefile()) flags |= O_LARGEFILE; 複製代碼
表示 flags 會在 64 位 Kernel 的狀況下強制麼設置 O_LARGEFILE
來表示支持大文件。 接着跳轉到 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 文件描述符 fd = get_unused_fd_flags(flags); if (fd >= 0) { //調用 do_filp_open 完成對路徑的搜尋和文件的打開 struct file * f = do_filp_open(dfd, tmp, &op); if (IS_ERR(f)) { //若是發生了錯誤,釋放已分配的 fd 文件描述符 put_unused_fd(fd); //釋放已分配的 struct file 數據 fd = PTR_ERR(f); } else { fsnotify_open(f); //綁定 fd 與 f。 fd_install(fd, f); } } //釋放已分配的 filename 結構體。 putname(tmp); return fd; } 複製代碼
fd 是一個整數,它實際上是一個數組的下標,用來獲取指向 file 描述符的指針, 每一個進程都有個 task_struct 描述符用來描述進程相關的信息,其中有個 files_struct 類型的 files
字段,裏面有個保存了當前進程全部已打開文件 描述符的數組,而經過 fd 就能夠找到具體的文件描述符,之間的關係能夠參考下圖:
這裏的參數已經在上面提到過了,惟一須要注意的是 AT_FDCWD
,其定義在 include/uapi/linux/fcntl.h
,是一個特殊值(** -100 **), 該值代表當 filename 爲相對路徑的狀況下將當前進程的工做目錄設置爲起始路徑。相對而言, 你能夠在另外一個系統調用 openat 中爲這個起始路徑指定一個目錄, 此時 AT_FDCWD
就會被該目錄的描述符所替代。
static inline int build_open_flags(int flags, umode_t mode, struct open_flags *op) { int lookup_flags = 0; //O_CREAT 或者 `__O_TMPFILE*` 設置了,acc_mode 纔有效。 int acc_mode; // Clear out all open flags we don't know about so that we don't report // them in fcntl(F_GETFD) or similar interfaces. // 只保留當前內核支持且已被設置的標誌,防止用戶空間亂設置不支持的標誌。 flags &= VALID_OPEN_FLAGS; if (flags & (O_CREAT | __ O_TMPFILE)) op->mode = (mode & S_IALLUGO) | S_IFREG; else //若是 O_CREAT | __ O_TMPFILE 標誌都沒有設置,那麼忽略 mode。 op->mode = 0; // Must never be set by userspace flags &= ~FMODE_NONOTIFY & ~O_CLOEXEC; // O_SYNC is implemented as __ O_SYNC|O_DSYNC. As many places only // check for O_DSYNC if the need any syncing at all we enforce it's // always set instead of having to deal with possibly weird behaviour // for malicious applications setting only __ O_SYNC. if (flags & __ O_SYNC) flags |= O_DSYNC; //若是是建立一個沒有名字的臨時文件,參數 pathname 用來表示一個目錄, //會在該目錄的文件系統中建立一個沒有名字的 iNode。 if (flags & __ O_TMPFILE) { if ((flags & O_TMPFILE_MASK) != O_TMPFILE) return -EINVAL; acc_mode = MAY_OPEN | ACC_MODE(flags); if (!(acc_mode & MAY_WRITE)) return -EINVAL; } else if (flags & O_PATH) { // If we have O_PATH in the open flag. Then we // cannot have anything other than the below set of flags // 若是設置了 O_PATH 標誌,那麼 flags 只能設置如下 3 個標誌。 flags &= O_DIRECTORY | O_NOFOLLOW | O_PATH; acc_mode = 0; } else { acc_mode = MAY_OPEN | ACC_MODE(flags); } op->open_flag = flags; // O_TRUNC implies we need access checks for write permissions // 若是設置了,那麼寫以前可能須要清空內容。 if (flags & O_TRUNC) acc_mode |= MAY_WRITE; // Allow the LSM permission hook to distinguish append // access from general write access. // 讓 LSM 有能力區分 追加訪問和普通訪問。 if (flags & O_APPEND) acc_mode |= MAY_APPEND; op->acc_mode = acc_mode; //設置意圖,若是沒有設置 O_PATH,表示這次調用有打開文件的意圖。 op->intent = flags & O_PATH ? 0 : LOOKUP_OPEN; if (flags & O_CREAT) { //是否有建立文件的意圖 op->intent |= LOOKUP_CREATE; if (flags & O_EXCL) op->intent |= LOOKUP_EXCL; } //判斷查找的目標是不是目錄。 if (flags & O_DIRECTORY) lookup_flags |= LOOKUP_DIRECTORY; //判斷當發現符號連接時是否繼續跟下去 if (!(flags & O_NOFOLLOW)) lookup_flags |= LOOKUP_FOLLOW; //查找標誌設置了 LOOKUP_FOLLOW 表示會繼續跟下去。 //設置查找標誌,lookup_flags 在路徑查找時會用到 op->lookup_flags = lookup_flags; return 0; } 複製代碼
上面的函數主要是根據用戶傳遞進來的 flags 進一步設置具體的標誌,而後把這些標誌封裝到 open_flags 結構體中。以便後續使用。
接下來就是函數 getname()
,這個函數定義在 fs/namei.c
,主體是 getname_flags
, 咱們撿重點的分析,可有可無的代碼以 ... 略過。
struct filename * getname(const char __ user *filename) { return getname_flags(filename, 0, NULL); } 複製代碼
struct filename { const char* name; // pointer to actual string ---指向真實的字符串 const __ user char* uptr; // original userland pointer -- 指向原來用戶空間的 filename struct audit_names* aname; int refcnt; const char iname[]; //用來保存 pathname }; 複製代碼
struct filename * getname_flags(const char __ user *filename, int flags, int* empty) { struct filename* result; char* kname; int len; // 這裏通常來講賦值爲 NULL。這裏主要是針對Linux 審計工具 audit,咱們無論。 result = audit_reusename(filename); // 若是不爲空直接返回。 if (result) return result; // 經過__getname 在內核緩衝區專用隊列裏申請一塊內存用來放置路徑名(filemname 結構體) result = __getname(); if (unlikely(!result)) return ERR_PTR(-ENOMEM); //First, try to embed the struct filename inside the names_cache //allocation //kname 指向 struct filename 的 iname 數組。 kname = (char*)result->iname; // 把 filename->name 指向 iname[0],待會 iname 用來保存用戶空間傳遞過來的路徑名(filemname 結構體)。 result->name = kname; //該函數把用戶空間的 filename 複製到 iname len = strncpy_from_user(kname, filename, EMBEDDED_NAME_MAX); //若是複製失敗,釋放已分配的 result 並返回錯誤。 if (unlikely(len < 0)) { __putname(result); return ERR_PTR(len); } // Uh-oh. We have a name that's approaching PATH_MAX. Allocate a // separate struct filename so we can dedicate the entire // names_cache allocation for the pathname, and re-do the copy from // userland. // 這裏判斷用戶空間傳遞過來的路徑名的長度接近了 PATH_MAX,因此須要分配一個獨立的空間 // 用來保存 struct filename 前面的字段,並把 name_cache 所有空間用來保存路徑名 (filename->iname)。 // // #define PATH_MAX 4096 // 4 kb 大小。 // #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE * )0)->MEMBER) // #define EMBEDDED_NAME_MAX (PATH_MAX - offsetof(struct filename, iname)) // EMBEDDED_NAME_MAX 指的就是:字段 iname 在 filename 結構體中的偏移。 if (unlikely(len == EMBEDDED_NAME_MAX)) { // 注意,這裏是把 iname[1] 的偏移賦值給了 size。這樣 size 的大小包含了 inaem[0] // 能夠用來保存 iname 數組的首地址。 const size_t size = offsetof(struct filename, iname[1]); // 把舊 result 的首地址賦值給了 kanme。 kname = (char * )result; // size is chosen that way we to guarantee that // result->iname[0] is within the same object and that // kname can't be equal to result->iname, no matter what. // 分配一個獨立空間用來保存 filename,這樣就能夠把 filename 分離出來。 result = kzalloc(size, GFP_KERNEL); //分配失敗,釋放資源並返回錯誤。 if (unlikely(!result)) { __putname(kname); return ERR_PTR(-ENOMEM); } // 把原來的 filename 的首地址賦值給新分配的 result。這樣就實現了分離。 result->name = kname; // 把用戶空間的 filename 複製到 kname(name_cache 起始地址)。 len = strncpy_from_user(kname, filename, PATH_MAX); // 原來: // filename struct(內核空間,用 name_cach 來保存) // result ---> name_cache-----> name // uptr // aname // .... 複製操做(strncpy_from_user()) // iname <--------------> filename struct(用戶空間) // // 如今: // filename struct(內核空間,注意這裏是新開獨立的空間。) // result ---> name ------> name_cache <---------------> filename struct(用戶空間) // uptr 複製操做(strncpy_from_user()) // aname // .... // iname // 新分配的 filename 的首地址指向 name_cach,而 name_cach 又保存了用戶 // 空間的 filename,因此新的 filename(result) 能間接訪問到用戶空間的 filename。 // 複製失敗,釋放資源,返回。 if (unlikely(len < 0)) { __putname(kname); kfree(result); return ERR_PTR(len); } // 路徑過長,一樣返回錯誤(從這裏也能夠看出,在 Linux 中路徑名的長度不能超過 4096 字節)。 if (unlikely(len == PATH_MAX)) { __putname(kname); kfree(result); return ERR_PTR(-ENAMETOOLONG); } } // 引用計數爲 1 result->refcnt = 1; // The empty path is special.空路徑的處理。 if (unlikely(!len)) { if (empty) * empty = 1; // 若是 LOOKUP_EMPTY 沒有設置,也就是本次 open 操做的目標不是空路徑,可是傳遞了一個 // 空路徑,因此返回錯誤。 if (! (flags & LOOKUP_EMPTY)) { //回收資源 putname(result); return ERR_PTR(-ENOENT); } } // 指向用戶空間的 filename result->uptr = filename; result->aname = NULL; audit_getname(result); return result; } 複製代碼
struct filename { const char* name; //pointer to actual string ---指向真實的字符串 const __ user char* uptr; //original userland pointer --- 指向原來用戶空間 struct audit_names* aname; int refcnt; const char iname[]; //用來保存 pathname }; 複製代碼
首先經過 __getname
在內核緩衝區專用隊列裏申請一塊內存用來放置路徑名,其實這塊內存就是 一個 4KB 的內存頁。這塊內存頁是這樣分配的,在開始的一小塊空間放置結構體 struct filename 結構體前面字段的信息,這裏咱們假設 iname 字段以前的結構使用 struct filename-iname 表示, 以後的空間放置字符串(保存在 iname)。初始化字符串指針 kname,使其指向這個字符串 (iname[]
)的首地址。而後就是拷貝字符串,返回值 len 表明了 已經 拷貝的字符串長度。若是這個字符串已經填滿了內存頁剩餘空間,就說明該字符串的長度已經大於 4KB - (sizeof(struct filename-iname)了,這時就須要將結構體 struct filename-iname 從這個內存頁中分離並單獨分配空間,而後用整個內存頁保存該字符串。
get_unused_fd_flags()
函數用來查找一個可用的 fd(文件描述符)。
int get_unused_fd_flags(unsigned flags) { return __alloc_fd(current->files, 0, rlimit(RLIMIT_NOFILE), flags); } 複製代碼
/* * allocate a file descriptor, mark it busy. */ int __alloc_fd(struct files_struct *files, unsigned start, unsigned end, unsigned flags) { unsigned int fd; int error; struct fdtable * fdt; spin_lock(&files->file_lock); repeat: // 經過 files 字段獲取 fdt 字段。(該函數考慮了線程競爭,較複雜不展開了。) fdt = files_fdtable(files); //從 start 開始搜索 fd = start; // 進程上一次獲取的 fd 的下一個號(fd + 1)保存在 next_fd 中。因此從 next_fd 開始進行查找。 if (fd < files->next_fd) fd = files->next_fd; if (fd < fdt->max_fds) //獲取下一個 fd fd = find_next_fd(fdt, fd); // N.B. For clone tasks sharing a files structure, this test // will limit the total number of files that can be opened. error = -EMFILE; if (fd >= end) goto out; // 獲取 fd 後,判斷是否須要擴展用來保存 file struct 描述符的數組(fdtable->fd)的容量。 // 返回 0 表示不須要,<0 表示錯誤,1 表示成功。 error = expand_files(files, fd); if (error < 0) goto out; // If we needed to expand the fs array we // might have blocked - try again. // 1,擴容成功,而且從新嘗試獲取fd // 由於擴容過程可能會發生阻塞,這期間就有可能其餘線程也在獲取 fd,因此前面獲取的 fd // 可能被其餘線程搶先佔用了,由於 Linux 的喚醒是不保證順序的。 if (error) goto repeat; if (start <= files->next_fd) files->next_fd = fd + 1; // 在 fdtable->open_fds 位圖中置位表示當前獲取的 fd 處於使用狀態。 // 也就是說當釋放該 fd 位圖中對應的位清除,從而達到重複使用的的目的。 __set_open_fd(fd, fdt); // 若是設置了 O_CLOEXEC 標誌,那麼在 fdtable->close_on_exec 位圖對應的位置位。 // 前面提到過開啓 close-on-exc 標誌,使用系統調用 execve() 裝載程序的時候會關閉設置過該標誌的文件。 // Linux 中使用 fork() 產生子進程的時候回繼承父進程已打開的文件描述符集。execve() 通常就是在子進程 // 裏用來運行新程序。 if (flags & O_CLOEXEC) __set_close_on_exec(fd, fdt); else __clear_close_on_exec(fd, fdt); // 設置返回值。 error = fd; #if 1 // Sanity check 一些合法性檢查。 if (rcu_access_pointer(fdt->fd[fd]) != NULL) { printk(KERN_WARNING "alloc_fd: slot %d not NULL!\n", fd); rcu_assign_pointer(fdt->fd[fd], NULL); } #endif out: spin_unlock(&files->file_lock); return error; } 複製代碼
struct fdtable { unsigned int max_fds; struct file __ rcu ** fd; // current fd array unsigned long * close_on_exec; unsigned long * open_fds; unsigned long * full_fds_bits; struct rcu_head rcu; }; 複製代碼
#ifdef CONFIG_64BIT #define BITS_PER_LONG 64 #else #define BITS_PER_LONG 32 #endif /* CONFIG_64BIT */ static inline void __set_open_fd(unsigned int fd, struct fdtable *fdt) { __set_bit(fd, fdt->open_fds); fd /= BITS_PER_LONG; if (!~fdt->open_fds[fd]) __set_bit(fd, fdt->full_fds_bits); } 複製代碼
這裏以 32 位 arm 芯片爲例。其中函數 __set_bit
表示以某個地址開始在某個位置 1。 假設咱們目前數組的容量爲 128 ,那麼以下表:共有 4 行,一行 32 列,fd = 32 * row + column。 每一個格子中 0 表示當前 fd 沒有被佔用,1 表示佔用了。其中 ...
表示全部的列爲 1。 假設咱們如今獲取的 fd 爲 66 也就是第 3 行第 3 列,此時咱們能夠看到該格子爲 0。 調用 __set_bit(fd, fdt->open_fds);
把該位(66)置1,fd /= BITS_PER_LONG;
獲取行號 66 / 32 = 2(行號從 0 開始),!~fdt->open_fds[fd]
, open_fds
爲 long 類型 指針,也就是說步長爲 32 位,至關於取第 3 個 long 數據的值,而後位取反,由於此時該 long 數據 的每一位都置 1 了,因此取反後的值爲 0,!0 就爲 true 了。此時咱們能夠肯定第 3 行全部的 列都被使用了,因此咱們能夠把 full_fds_bits
的第 2 位置 1,表示該行已所有被使用。
0 | 1 | 2 | ... | 30 | 31 |
---|---|---|---|---|---|
1 | 1 | 1 | ... | 1 | 1 |
1 | 1 | 1 | ... | 1 | 1 |
1 | 1 | 0 | ... | 1 | 1 |
1 | 1 | 1 | ... | 0 | 0 |
接下來看找 fd 函數 find_next_fd()
就很簡單了。
static unsigned long find_next_fd(struct fdtable *fdt, unsigned long start) { unsigned long maxfd = fdt->max_fds; // 當前容量最後的一行 unsigned long maxbit = maxfd / BITS_PER_LONG; // 開始行 unsigned long bitbit = start / BITS_PER_LONG; // 先找到一個空行(有空閒位的某一行) bitbit = find_next_zero_bit(fdt->full_fds_bits, maxbit, bitbit) * BITS_PER_LONG; if (bitbit > maxfd) return maxfd; if (bitbit > start) start = bitbit; // 在該行上找到一個具體的空位。 return find_next_zero_bit(fdt->open_fds, maxfd, start); } 複製代碼
儘可能以本身的能力對每行代碼進行了註釋,同時只是爲了學習內核大神是如何玩轉指針以及數據結構。 能夠從 __set_open_fd()
函數看出對指針熟練的使用方式,以及快速定位的思想。