天天看點

Linux學習之旅(14)-----檔案I/O

C中檔案I/O操作和系統檔案I/O的關系?

在C/C++中我們也學過關于檔案的操作函數,例如:fopen(),fclose()等。其實Linux中的檔案I/O和這些也差不多,那為什麼還要學習Linux的檔案I/O?那這兩者有什麼聯系和關系那?為什麼我們使用printf()函數就可以将一句話列印在顯示器上?

當我們使用printf()函數時,實際調用的是作業系統的printf()函數(應用層,這實際就是我們要學習的檔案I/O)。然後由作業系統的printf()函數取調用作業系統的sys_printf()(核心層)(假如叫這個名字,這裡隻講述原理,不過于深究)。然後核心的函數負責取調用驅動裝置,由驅動裝置負責将函數的内容列印到顯示器上。是以,我們在無形中已經使用過了系統的檔案I/O。

我們知道受用c或者c++編寫的程式,假如你是在windows系統下生成的exe檔案,那麼這個exe檔案直接拿到Linux系統是不能運作的,我們需要将源代碼在linux系統下重新編譯連結再一次生成可執行檔案才可以。這就是因為上面的說的,C中的函數并不是去直接操作作業系統的底部,而隻是去調用應用層的函數,而兩個系統下的函數又設計的不相同,是以就不能直接使用。

那既然c提供的函數擁有這些功能(雖然跨平台需要重新編譯,但編譯也不是很麻煩)那為什麼還要學習Liunx系統的檔案I/O?這是因為系統提供的檔案I/O遠比c語言提供的函數功能更強大。舉一個列子:

我們知道是使用c語言提供的函數操作檔案,比如将一段檔案寫入檔案中,它并不會直接将檔案的内容寫入磁盤,而是将内容寫入檔案緩沖區中(每個檔案指針都有一個檔案緩沖區,大小為8192B,這樣做的主要目的是為了減少檔案在磁盤上的操作次數,進而提高速度。)等到重新整理檔案緩沖區的時候再将檔案寫入磁盤中。也就是如果使用c标準函數将檔案寫入磁盤中,如果緩沖區沒有重新整理,那我們是無法讀到檔案的内容的,雖然你寫了,這時你唯一的方法就是等待系統重新整理檔案緩沖區(也可以手動的重新整理)。但是如果使用的系統的函數,就不會出現這樣的問題。如果使用系統的AP将檔案存入磁盤中,系統的API會去排程核心的層API,将檔案内容寫在核心的緩沖(守護程序控制)中,雖然也是寫在緩沖中,但檔案讀取是同樣也是調用核心的API,就會直接在核心的緩沖中找到檔案内容。

Linux學習之旅(14)-----檔案I/O

剛才提到檔案緩沖區的重新整理,那麼檔案重新整理都有什麼方法那?

(1)緩沖區滿。

(2)在内容中遇到了"\n",即換行。作用是将光标移動到輸出裝置中下一行開頭處,并且清空緩沖區。

(3)使用flush手動重新整理。将内容輸出到裝置。

(4)關閉檔案。

PCB(Process Control Block)程序控制塊

一段代碼沒有運作之前被成為程式,一旦運作它就是程序(一個程式可以有多個程序)。而PCB是程序存在的唯一标志,系統利用PCB來描述程序的基本情況和活動情況,進而控制和管理程序。一般一個程序(程序實體,程序映像)由三部分組分程式段、資料、PCB。建立程序就是建立了一個PCB,撤銷程序也是撤銷了一個程序的PCB。對于程序的定義有很多,我們選取了一種比較典型的:

(1)程序是程式的一次執行。

(2)程序是一個程式以及資料在處理機上順序執行時所發生的活動。

(3)程序是具有獨立功能的程式在一個資料集合上運作的過程,它是系統進行資源配置設定和排程的一個獨立機關。

程序具有:動态性、并發行、獨立性、異步性。

為什麼要在這裡談一下程序那?

因為在系統的檔案I/O中會使用到程序。一個程式系統會給它配置設定4G的空間,用來存儲資訊。在這4G的存儲空間中。其中0-3G是程式單獨使用,會不幹擾的,而3-4G是核心擁有的,是共享的。系統會在這裡為每個程序建立PCB,PCB中儲存這這個程式的檔案資訊。我們都知道在使用檔案操作打開一個檔案時,打開成功時,就是傳回一個檔案描述符(關閉時也傳回檔案描述符,隻有打開錯誤時傳回-1),我們可以通過這個檔案描述符對檔案程序操作。那這個檔案描述符是如何找到對應的檔案的?就是使用函數傳回的檔案描述符和PCB中存儲的檔案資訊的檔案描述符做比較。其實PCB的檔案資訊儲存在一個名為file struct的結構體中,那時如何存儲的那,其實就是一個整形數組(相當于指針的作用,索引)。一個程式在運作時會預設打開三個檔案(無論你是否進行檔案操作)他們分别是:标準輸入、标準輸出、标準出錯,分别對應的整形數組的0,1,2号位置。如果我們需要打開檔案,那該檔案的索引就會被存儲在3号的位置(配置設定原則為沒有使用的最小的号碼。)

Linux學習之旅(14)-----檔案I/O

打開、關閉檔案

在Linux系統中的打開檔案的函數為open()函數。

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

可以看到open函數有倆中重載形式。

參數說明:

必選項:必須選擇且隻能選擇一個。

(1)O_RDONLY:隻讀打開

(2)O_WRONLY:隻寫打開

(3)O_RDWR:可讀可寫打開

附加選項(可以選擇0個或者多個,需要和必選項按位或):

(1)O_APPEND:追加

(2)O_CREAT:如果檔案不存在則建立它,使用時需要提供mode(第三個參數)來表示檔案的通路權限。

(3)O_EXCL:如果同時提供了O_CREAT,并且檔案存在,則出錯傳回。

(4)O_TRUNC:如果檔案存在,并且以隻寫或可讀可寫的方式打開,則将其檔案長度截斷為0,即重新寫的意思。

(5)O_NONBLOCK:對于裝置檔案,以本方式打開可以作為非阻塞I/O。

read(int fd,void* buf,size_t size);
//成功傳回讀取到的位元組數,讀到檔案末尾傳回0,出錯傳回-1.
write(int fd,coid* buf,size_t size);
//成傳回寫入的位元組數,出錯傳回-1并設定errno
           

建立一個不存在的檔案并向其中寫入“白日依山盡”。

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

int main()
{
    int fileDes;
    //建立一個使用權限為774的名為test1的檔案。
    fileDes=open("test1",O_WRONLY|O_CREAT,0774);
    char buf[]="白日依山盡\n";
    write(fileDes,buf,sizeof(buf)/sizeof(buf[0]));
    close(fileDes);
    return 0;
}
           
Linux學習之旅(14)-----檔案I/O

編寫程式在剛才的test1檔案内加入“黃河入海流”,追加在“白日依山盡”之後。

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

int main()
{
    int fileDes;
    fileDes=open("test1",O_WRONLY|O_APPEND);
    char buf[]="黃河入海流\n";
    write(fileDes,buf,sizeof(buf)/sizeof(buf[0]));
    close(fileDes);
    return 0;
}
           
Linux學習之旅(14)-----檔案I/O

建立一個名為test1的檔案并且使用O_EXCL參數。

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
    int fileDes;
    fileDes=open("test1",O_WRONLY|O_CREAT|O_EXCL,0777);
    //沒打開就輸出
    if(fileDes==-1)
    {
        printf("對應檔案以存在\n");
    }   
    //打開就寫入
    else
    {   
        char buf[]="欲窮千裡目\n";
        write(fileDes,buf,sizeof(buf)/sizeof(buf[0]));
        close(fileDes);
    }   
    return 0;
}
           
Linux學習之旅(14)-----檔案I/O

使用O_TRUNC将“更上一層樓”寫入檔案中(加入這個參數,會将原來的檔案内容全部删除,然後重新開始寫)。

#include <fcntl.h>
#include <unistd.h>

int main()
{
    int fileDes;
    fileDes=open("test1",O_WRONLY|O_TRUNC);
    char buf[]="更上一層樓\n";
    write(fileDes,buf,sizeof(buf)/sizeof(buf[0]));
    close(fileDes);
    return 0;
}
           
Linux學習之旅(14)-----檔案I/O

如果不加O_TRUNC會如何那,我們直接在test1檔案中寫入“孟浩然”。

#include <fcntl.h>
#include <unistd.h>

int main()
{
    int fileDes;
    fileDes=open("test1",O_WRONLY);
    char buf[]="孟浩然";
    write(fileDes,buf,sizeof(buf)/sizeof(buf[0]));
    close(fileDes);
    return 0;
}
           

我們可以到如果不加O_TRUNC,會從檔案開頭開始寫,覆寫之前的内容。如果新寫入的内容的位元組數大于原檔案,那不會産生什麼影響,一旦新輸入的内容的位元組數小于原檔案就會出現下面的情況。

Linux學習之旅(14)-----檔案I/O

亂碼的原因:我們知道一個漢字占兩個字元,而字元串會在末尾自動加上'\0',會占用一個位元組,這樣就會把“一”字的一半覆寫,那一半就會顯示亂碼。

使用系統API簡單實作cp指令功能。

我們知道cp指令的作用為複制檔案或目錄。這裡我們隻實作複制檔案。

include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main(int args,char* argv[])
{
    //說明缺少參數
    if(args<3)
    {
        printf("請檢查檔案名個數\n");
        exit(1);
    }
    //要求已經存在
    int sourceFile;
    //要求不能存在
    int targetFile;
    sourceFile=open(argv[1],O_RDONLY);
    if(sourceFile==-1)
    {
        printf("源檔案不存在。\n");
        exit(1);
    }
    targetFile=open(argv[2],O_WRONLY|O_CREAT|O_EXCL,0644);
    if(targetFile==-1)
    {
        printf("目标檔案以存在。\n");
        exit(1);
    }
    //将緩沖區大小定為101個位元組
    char buf[100];
    //記錄讀到檔案的真實大小
    int len;
    while(len=(read(sourceFile,buf,100)))
    {
        write(targetFile,buf,len);
    }
    close(sourceFile);
    close(targetFile);
    printf("檔案以拷貝完成,程式退出。\n");
    return 0;
}
           
Linux學習之旅(14)-----檔案I/O

打開檔案一定要記得關閉,雖然在一般在程式結束時作業系統會自動關閉,但這是一個非常不好的習慣。

這裡需要強調一點一個程式預設最大打開1024個檔案(可以修改)。我們通過ulimit -a指令可以檢視。

Linux學習之旅(14)-----檔案I/O

可以通過ulimit -n來修改。

Linux學習之旅(14)-----檔案I/O

但是這個也是有上限的。可以通過cat /proc/sys/fs/file-max可以檢視(和記憶體大小有關)。

Linux學習之旅(14)-----檔案I/O