Linux 文件描述符與文件系統

  • 列表內容

文件描述符

Linux下一切皆文件 文件描述符是爲了高效的管理已經被打開的文件而設計的 文件描述符作爲操作文件的句柄 在Linux 下一切I/O操作的系統調用都是通過文件描述符操作的。文件描述符是一個非負整數,Linux下進程要訪問一個文件就必須拿到該文件的文件描述符。

每一個進程控制塊結構體(PCB)中中都有一個指向 file_struct結構體的指針。
file_struct結構體中有一個文件描述符表 這個文件描述符表實際上是一個指針數組 而這個指針數組中存放的是file結構體 的指針

每個進程有進程控制塊PCB
file結構體實際上相當於 每個文件的控制塊 描述着一個文件的屬性信息。

文件描述符是連續的非負整數 本質上是file_struct結構體中只想file結構體指針的數組的下標。

這裏寫圖片描述

這裏寫圖片描述

如何獲取文件描述符

1.子進程從父進程繼承文件描述符 文件描述符對於每一個進程是唯一的,每個進程都有一張文件描述符表,用於管理文件描述符。當使用fork創建子進程的話,子進程會獲得父進程所有文件描述符的副本,這些文件描述符在執行fork時打開。 父子進程擁有同樣的文件描述符表 意味着可以訪問同一個文件 每個進程是在自己的地址空間內運行 他們如果要相互實現通信 就得有在各自的文件描述符表的同一個文件描述符表示同一個文件 (公共資源) 通過這個中介你讀我寫 你寫我讀來事現通信。

  1. open() create() 系統調用

文件描述符與打開的文件之間的關係

每一個文件描述符對應一個打開的文件 同一個文件可以被多個文件描述符表示 相同的文件可以被不同的進程打開,也可以在同一個進程中被打開多次。系統爲每一個進程維護了一個文件描述符表,該表的值從0開始,所以在不同的進程中會看到數字相同的文件描述符,這種情況下的相同的文件描述符有可能指向同一個文件,也有可能指向不同的文件。

與文件描述符有關的部分系統調用。

open函數

       #include <sys/types.h>
       #include <sys/stat.h>
       #include <fcntl.h>

       int open(const char *pathname, int flags);
       int open(const char *pathname, int flags, mode_t mode);

open函數的參數 :
第一個參數是要打開的文件的路徑
第二個參數是標誌位 標識出打開文件的方式
O_RDONLY 以只讀方式打開
O_WRONLY 以只寫方式打開
O_RDWR 以可讀可寫方式打開
O_APPEND 以追加寫方式打開

O_CREAT 如果該文件不存在則創建它 這是需要填入第三個參數mode

第三個參數mode是文件的權限 用一個數字表式文件的擁有者 所屬組 和其他 三種‘人’ 對文件的讀寫 執行權限。

open函數的返回值是所打開文件的文件描述符 失敗返回-1 errno自動被置。

close函數

 #include <unistd.h
 int close(int fd);

close用來關閉一個文件 參數是要關閉的文件的文件描述符
成功返回值 0 失敗返回 -1 並自動置errno。

read write函數

 #include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
 #include <unistd.h>

 ssize_t read(int fd, void *buf, size_t count);

read write函數 三個參數分別是

  1. 從哪個文件描述符標識的文件 讀 寫。
  2. 寫 讀 到哪個文件描述符中。
  3. 寫 讀 幾個字節。

返回值是:
成功返回讀寫的字節數 失敗返回-1 並置 errno

以上幾個函數都是系統調用 fwrite fopen fread 這些C的庫函數 封裝了這些系統調用接口。

C庫中的文件操作函數中的返回值和參數有 FILE*
C庫中的FILE結構體有什麼信息呢?

dup 和 dup2 系統調用

dup:複製文件描述符,返回沒有使用的文件描述符中的最小編號。
dup2:由用戶指定返回的文件描述符的值,用來重新打開或重定向一個文件描述符。

#include<stdio.h>
#include<string.h>
#include<sys/types.h>

#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>


int main()
{
   const char* msgg = "fwrite, running!\n";
   printf("printf running ...I\n");
   const char* msgg1 = "write, running!\n";
   fwrite(msgg, 1, strlen(msgg), stdout);
   write(1,msgg1, strlen(msgg1));
   pid_t id = fork();
    if(id == 0 ){
        printf("I am child!\n");
    }else{
        printf("I am father!\n");
    }
   return 0;
}

這裏寫圖片描述

請注意 myfile.c 代碼輸出到標準輸出 和重定向到文件中 有不一樣的效果
在重定向後 fwrite函數的寫操作在文件中寫了兩遍 而write函數在文件中只寫了一邊 這是爲什麼?

C的庫函數寫到顯示器是行緩衝的 寫到文件中是行緩衝的
fwrite函數是自帶緩衝區的 當重定向到文件時 緩衝方式變成了全緩衝
fork()後子進程寫時拷貝拷貝父進程內存數據 包括緩衝區數據 當父進程退出時要刷新緩衝區 這時子進程拷貝了一份新的數據 子進程退出時這部分數據再次被刷新到了文件中

write系統調用不提供緩衝區 調用結束後 所以父進程沒有write的數據 創建出的子進程也沒有 故只輸出到文件一次。

(這裏所說的緩衝區是用戶級緩衝區)

從這個例子可以看書 C庫中的 FILE結構體中至少有兩個東西
1. 文件描述符
2. 緩衝區

FILE 結構體:

struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
//以下是封裝的緩衝區
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

int _fileno;
  //這裏就是文件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */

#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;

signed char _vtable_offset;
char _shortbuf[1];

/* char* _save_gptr; char* _save_egptr; */

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

文件描述符的分配規則 與 重定向

Linux進程 在默認情況下 再一開始就會默認打開三個文件描述符 0 1 2
也就是 file結構體中文件描述符表file_array[] 的頭三個下標

這三個文件描述符 0 1 2 對應的三個文件 鍵盤 顯示器 顯示器 (Linux下一切皆文件 ) 對應C語言中的三個流 stdin stdout stderror 三個流(FILE* 類型)
正常輸出到顯示器上 叫stdout標準輸出 。
出錯信息顯示到顯示器上交 stderror標準錯誤。

#include<stdio.h>
#include<string.h>
#include<sys/types.h>

#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>


int main()
{
   close(1);
   int fd = open("./print_file",O_RDWR|O_CREAT,0755);
   printf("fd = %d\n",fd);
   printf("helloworld\n");
   fflush(stdout);
   return 0;
}

這段代碼一上來關閉了 默認打開的標準輸出文件描述符 1 然後打開了文件print_file 之後調用了printf函數 這是我們發現 ./myfile後顯示屏上無顯示 再查看printf_file文件 裏面顯示fd = 1 和helloword
請注意這裏調用了fflush函數刷新了stdout 的輸出緩衝區 才使得字符寫進了 printf_file

這裏說明
1. printf函數是向文件描述符爲 1 的文件寫的
2. 文件描述符的分配規則是從零開始找第一個未被使用的文件描述符來分配的。

這裏實現了printf函數的輸出重定向。

這裏寫圖片描述

自己模擬一個有重定向功能的命令行解釋器myshell

根據文件描述符的分配規則 和exec系列函數的概念模擬一個有重定向功能的shell命令行解釋器

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdio.h>
#include<sys/stat.h>
#include<sys/fcntl.h>
#include<string.h>
#include<ctype.h>

int main(){
    while(1){
        printf("[ym@localhost process myshell]$ ");
        fflush(stdout);//刷新輸出緩衝區  打印命令行解釋器的頭部信息
        char buf[1024];//定義一個緩衝區  用來存儲從鍵盤輸入的命令
        ssize_t s = read(0,buf,sizeof(buf)-1);
        if(s > 0){
            buf[s-1] = 0;//確保緩衝區最後的‘\0’
            //printf("%s\n", buf);
        }
        char* start = buf;
        char* _argv[32] = {NULL};
        int i = 0;//將存儲命了的緩衝區中的命令參數存入_argv[]  空格填上 ‘\0’
        while(*start){
            while(*start && isspace(*start)){
                *start = 0;
                start++;
            }
            _argv[i++] = start;
            while(*start && !isspace(*start)){
                start++;
            }
        }
        _argv[i] = NULL;//_argv[] 要作爲execv的參數 所以末尾必須加NULL
        pid_t id = fork();//依照shell的原理創建子進程
        if(id == 0){
            int flag = 0;
            int i = 0;
            for( ; _argv[i] != NULL; ++i ){//尋找 命令行參數中的 ‘>’ 重定向符號
                if(strcmp(">", _argv[i]) == 0){
                    flag = 1;//flag設置爲1 表示有重定向符號
                    break;
                }
            }
            int copyfd;
            _argv[i] = NULL;//要作爲execv的參數 _argv[i]要設置爲 NULL
            if(flag){
                close(1);//關閉標準輸出緩衝區
                int fd = open(_argv[i + 1], O_RDWR | O_CREAT, 0755); //此時fd爲1
               //copyfd = dup2(1, fd); 
           }
           execvp(_argv[0],_argv);//被替換的程序 如果有重定向 因爲PCB沒有變 file結構體沒有變  文件描述符沒有變
           //此時原本默認的輸出到文件描述符爲1的文件 已經不是顯示屏  而是新打開的一個文件
           //if(flag){
           //   close(1);
           //   dup2(copyfd, 1);
           //}
           exit(1);//進程退出時會對自己打開的文件描述符關閉   下一次while(1)的循環開始時 重新創建子進程 默認打開 0 1 2 stdin stdout stderror
       }else{
           pid_t ret = waitpid(id, NULL, 0);
           if(ret > 0){
              // printf("wait child success!\n");
           }
       }
   }
   return 0;
}

這裏寫圖片描述

內核的文件操作結構體關係圖   (轉來的):
這裏寫圖片描述

文件描述相關聯的各個用戶級和系統級結構體關係和作用
http://blog.csdn.net/captain_mxd/article/details/52153233
http://blog.csdn.net/lf_2016/article/details/54605651

文件系統

除了ls -l 命令可以爲我們從磁盤中獲取 文件夾中文件的信息
還有一個命令 stat 命令

這裏寫圖片描述

如何stat命令顯示的信息 ?
首先理解一下上面兩個鏈接中提到的inode結點
這裏寫圖片描述

文件系統將磁盤分成這幾塊?
超級塊 :存放文件系統本身的結構信息
inode: 存放文件屬性 如 文件大小 所有者 最近修改時間 存在哪個磁盤區塊
數據區:一般被分爲512k一塊的磁盤區域 用來存儲數據

touch命令創建一個文件都幹了些什麼呢?

  1. 內核先找到一個空閒的i節點 把要新建的文件初始信息填進去
  2. 存儲文件數據 計算發現該文件存儲需要 3 個盤塊 比如這三個盤塊是 100 200 300 則將內核緩衝區中的 數據一塊一塊地輸出到磁盤對應的盤塊上
  3. 記錄分配情況 inode上的磁盤分佈記錄了上述 塊列表
  4. 內核將 inode節點號和文件名 一起作爲文件的入口 存儲到目錄文件中
    文件名和 inode節點的對應關係 把文件名和 文件的數據和屬性對應起來

硬鏈接與軟連接

至此 我們知道真正找到磁盤上文件的是inode節點 而不是文件名
其實 我們可以讓多個文件名對應一個inode節點。
這就是硬鏈接

ln abc test.c ln命令爲test.c文件定義一個硬鏈接
對於硬鏈接來說 其實是兩個文件名公用一個inode節點 在目錄文件中保存了這兩個文件名和inode節點的關係
刪除時 在目錄文件中將inode 與一個文件名的記錄刪除 inode結點中保存的硬鏈接數減一 如果減到了零 則刪除該文件

ln -s abc test.c 對test.c 文件搞一個abc這樣的軟連接
軟連接實際上自己有另一個 inode節點 這個inode節點中對應的磁盤塊保存的是test.c文件的路徑。

這裏寫圖片描述
軟鏈接常用於大型工程中文件路徑的指代。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章