linux環境:C編程文件操作


C語言標準函數庫文件操作函數

  1. 打開文件:foen(文件名,打開格式)
  2. 關閉文件:fclose(文件名)
  3. 無格式讀文件:fread(目標地址,讀取單位大小,讀取個數,源文件指針)
  4. 無格式寫文件:fwrite(源地址,讀取單位大小,讀取個數,目標文件指針)
  5. 格式化讀文件:fscanf(源文件指針,帶格式控制的字符串,目的變量地址…)
  6. 格式化寫文件:fprintf(目標文件指針,帶格式控制的字符串,源變量)
  7. 讀文件字符:fgetc(源文件指針)
  8. 寫文件字符:fputc(字符,目標文件指針)
  9. 讀文件字符串:fgets(源文件指針)
  10. 寫文件字符串:fputs(字符串,目標文件指針)
  11. 文件結束指示:feof(文件指針)
  12. 文件指針定位:ftell(文件指針)返回當前文件指針距離文件開始處的字節數
  13. 文件指針重定位:fseek(文件指針,偏移量,起始點)起始點可以是文件頭0,文件尾2,文件指針當前位置1
  14. 文件指針復原rewind(文件指針)
  15. 格式化寫字符串函數:sprintf(字符串指針,格式控制,變量)
  16. 格式化讀字符串函數:fscanf(字符串指針,帶格式控制的字符串,目的變量地址…)

使用方法和在windows環境下一致


Linux目錄操作

目錄文件查找

首先需要理解:linux系統中的目錄也是以文件的方式存儲,在目錄文件中存儲了該目錄下的文件的信息,包括文件名,文件inode編號,文件類型等等,姑且把這些信息稱爲一個文件的索引信息。如果想要找到某個文件,首先需要找到這個文件所在目錄的目錄文件。

接下來就可以理解linux下打開目錄查找文件的流程了:

  1. 函數DIR *opendir(目錄路徑)可以根據路徑查找打開對應的目錄文件,返回指向該目錄文件流的結構體指針DIR*,DIR數據結構存儲了當前打開的目錄文件的相關信息,目錄文件的大小,起始地址等等。
  2. 獲得目錄流的結構體指針後,我們就可以使用dirent * readdir(目錄流指針)讀取目錄文件中的內容,依次獲取每個文件的索引信息。
  3. 爲了保證目錄文件不至於過大,文件的索引信息中只存了少量必要信息,所以想要獲得文件的詳細信息,在通過dirent結構體知道了文件名只後,我們需要通過int stat(包含路徑的完整文件名, stat結構體指針);來獲取文件的詳細信息,
  4. 可以通過void rewinddir(DIR *dir)重定位目錄流指針來重新讀取當前目錄下的文件索引信息
  5. 和文件流指針一樣,使用完之後應使用 closedir(DIR *dir)關閉目錄流指針DIR*
其他目錄操作
  • 修改權限int chmod(const char* path, mode_t mode);//path:文件路徑 mode 數字權限
  • char *getcwd(char *buf, size_t size); //獲取當前目錄,相當於 pwd 命令
    將當前的工作目錄絕對路徑複製到參數 buf 所指的內存空間,參數 size 爲 buf
    的空間大小,空間不足返回NULL,buf爲NULL時系統使用malloc自動分配內存
  • int chdir(const char *path); //修改當前目錄,即切換目錄,相當於 cd 命令

基於文件描述符的Linux文件操作

基本文件操作-打開、關閉與讀寫

除了C標準庫中的文件操作函數,linux環境下提供了另外一套直接根據文件描述符進行文件操作的函數。直接使用文件描述符對文件操作,可以提高程序的運行效率。

  1. 打開文件int open(const char *pathname, int flags); //文件名 打開方式
  2. 打開文件int open(const char *pathname, int flags, mode_t mode);
  3. 關閉文件int close(int fd);
  4. 讀文件ssize_t read(int fd, void *buf, size_t count);//文件描述詞 緩衝區 長度
  5. 寫文件ssize_t write(int fd, const void buf, size_t count);
    打開和關閉函數與標準函數基本類似,區別在於由FILE類型的文件指針變成了int類型的文件描述符,文件權限表示也不一樣。
    讀寫文件類似fread fwrite,不同之處在於不再區分單位長度和單位個數,而是直接以總字節長度作爲參數。
改變文件大小
  • 直接改變文件大小:使用函數int ftruncate(int fd, off_t length);
    函數 ftruncate 會將參數 fd 指定的文件大小改爲參數 length 指定的大小。參數 fd 爲已打開的文件描述詞,而且必須是以寫入模式打開的文件。如果原來的文件大小比參數 length 大,則超過的部分會被刪去。執行成功則返回 0,失敗返回-1

  • 間接擴大文件:使用文件指針定位函數lseek
    off_t lseek(int fd, off_t offset, int whence);//fd 文件描述詞,offset爲偏移量,whence爲基準地址,0代表文件頭,1代表文件指針所在,2代表文件尾

    利用該函數可以實現文件空洞(對一個新建的空文件,可以定位到偏移文件開頭 1024 個字節的地方,在寫入一個字符,則相當於給該文件分配了 1025 個字節的空間,形成文件空洞)通常用於多進程間通信的時候的共享內存。

文件描述符與文件指針的轉換
  • FILE* -> int :int fileno(FILE * file);

  • int ->FILE*:FILE *fdopen(int fd);

獲取文件信息
  • 通過文件指針獲取:
    int stat(const char *file_name, struct stat *buf); //文件名 stat 結構體指針
  • 通過文件描述符獲取:
    int fstat(int fd, struct stat *buf); //文件描述符stat 結構體指針
文件描述符複製

原理解析

  • linux系統中,包括設備在內所有資源均是通過文件格式進行管理的。
  • 系統訪問相應的文件的時候,需要首先打開文件。
  • 所有對已打開的文件的操作,均需要通過文件描述符進行,FILE*指針最後也是由系統轉爲文件描述符進行操作的。
  • 同一文件往往會有不同線程同時訪問,Linux採用引用計數的方式進行管理,即文件只打開一份,此後每有一個新的打開請求,只返回一個新的文件描述符,同時該文件的引用計數加一。
  • 關閉文件同理,僅僅釋放文件描述符,即更新進程的文件描述符表中該描述符的狀態,當文件的引用計數爲0時才真正執行關閉文件的操作。
  • 文件描述符,僅僅只是一個整數序號,和變量名無關,只要序號相同,就是同一個文件描述符。

理解上述原理後,我們來看文件描述符的複製

使用 = 直接複製
int fd0=open("test.c",O_RDWR | O_CREAT);
int fd1=fd0;

這種情況下,fd1fd0的值相同,是同一個整數,也就是說兩者是同一個文件描述符,test.c文件的引用計數不增加,雖然都可以對文件進行訪問,但是隻要對任意一個執行close()操作,這個整數代表的描述符就和該文件脫離聯繫,通過fd0,fd1兩個變量均不能再訪問test.c文件。

使用dup()函數複製

函數原理分析

dup函數接收一個文件描述符作爲參數,查找對應的文件。
如果該文件描述符沒有對應打開的文件則返回-1
有對應的文件,則返回當前未被使用的最小文件描述符,同時該文件的引用計數加1

樣例程序:

	int main()
{
    int fd1 = open("dup.doc", O_RDWR | O_CREAT);
    int fd2 = dup(fd1);
    
    printf("fd1=%d\nfd2=dup(fd1)\nfd2==%d\n",fd1,fd2);
    
    close(fd1);
    printf("fd1 closed!\n");
    
    write(fd2,"HELLO WORLD!",13);
    printf("write dup.doc by fd2!\n");
}

執行效果如下:

fd1=3
fd2=dup(fd1)
fd2==4
fd1 closed!
write dup.doc by fd2!

使用cat命令查看dup.doc文件:

[jzk@ubuntu ~/code/dup]$ cat dup.doc
HELLO WORLD![jzk@ubuntu ~/code/dup]$ 

可以看到在關閉fd1後,我們通過fd2仍然可以對文件進行讀寫。

使用dup2()函數複製

函數原理分析

int dup2(int oldfd, int newfd)接收兩個文件描述符作爲參數

  • 第一個參數oldfd是源文件描述符,應該對應一個打開的文件,如果該文件描述符沒有對應打開的文件則報錯返回-1
  • 第二個參數newfd是目的文件描述符,如果newfd等於oldfd則返回這個值。如果newfd已經關聯其他文件,則執行close(newfd)操作,然後把newfdoldfd對應的文件進行關聯,該文件的引用計數加1,返回newfd
重定向標準輸入輸出到文件

預備知識:Linux系統中輸入輸出設備同樣是通過文件描述符管理,其中標準輸入stdin,標準輸出stdout,標準錯誤輸出stderr默認的文件描述符是0,1,2,分別用STDIN_FILENOSTDOUT_FILENOSTDERR_FILENO 三個宏表示
值得注意的是,對於所有調用標準輸入輸出的函數,比如printf(),puts,scanf(),perror()等等,它們只認0,1,2這三個描述符,而不關注這三個描述符關聯的對象。這樣我們就可以通過改變0,1,2這三個文件描述符關聯的文件,從而實現對標準輸入輸出的重定向。
同時要注意,一旦關閉標準輸入輸出之後,就手動不能打開了,所以,如果在重定向之後需要再使用標準輸入輸出,需要先將其複製給其文件描述符來維持標準輸入輸出文件的打開。

樣例程序


int main()
{
    //備份標準輸入
    int ind = dup(0);
    //備份標準輸出
    int outd = dup(1);
    
    
    int fd = open("test.doc",O_RDWR|O_CREAT);
 
    //將原本標準輸出的文件描述符與fd關聯起來
    dup2(fd,1);
    printf("you will see me in test.doc.\n");

	//重新把標準輸出文件和1聯繫起來
    dup2(outd,1);
    printf("you will see me on the screen.\n");
    
    //將文件指針重定位到文件頭
    lseek(fd,0,0);
   
    //將原本標準輸入的文件描述符與fd關聯起來
    dup2(fd,0);

    char str[30]={0};
   //從文件中讀取字符串
    gets(str);
    printf("this is a string from the file:\t%s\n",str);
   
    //重新把標準輸入的文件描述符與0聯繫起來
    dup2(ind,0);

    //測試是否重新回到了標準輸入輸出流
    printf("input test:input a string\n");
    scanf("%s",str);
    printf("%s\n",str);
    return 0;
}

執行效果如下:

you will see me on the screen.
this is a string from the file:	you will see me in test.doc.
input test:input a string
success!
success!

查看test.doc中的內容:

[jzk@ubuntu ~/code/stdio]$ cat test.doc 
you will see me in test.doc.
[jzk@ubuntu ~/code/stdio]$ 


MMAP文件映射

MMP文件映射的原理是使用DMA通道技術,在地址A與地址B之間構造映射。
當我們在地址A爲起始的映射區讀寫操作時,DMA控制器可以將該讀寫操作同步到地址B起始的映射區,從而實現了數據高效傳輸。
其中地址A和B可以一個在內存,一個在外存,一個在用戶空間,一個在內核空間,靈活性很強。

調用形式如下:
void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )

參數解釋:

  1. 第一個參數addr指定文件應被映射到進程空間的起始地址,一般被指定一個空指針,此時選擇起始地址的任務留給內核來完成.
  2. 第二個參數len指定映射區的大小,從被映射文件開頭offset個字節開始算起,注意該值加上offset值後不能超過映射文件的大小
  3. 第三個參數prot指定映射的內存段的訪問權限,可讀PROT_READ可寫PROT_WRITE、可執行PROT_EXEC或不可訪問PROT_NONE,可以使用或運算進行組合
  4. 第四個參數flags指定映射對象的類型,映射選項和映射頁是否可以共享,一般爲MAP_SHARED
  5. 第五個參數fd爲文件描述符
  6. 第六個參數offset爲文件中的映射區從文件開始處算起的偏移量,必須是物理頁面大小4K的整數倍

注意:數據傳輸完畢後,需要使用int munmap(映射文件描述符,映射區大小)解除映射,解除成功返回0,否則返回-1


管道文件通信

原理
管道通信爲半雙工通信,通信雙方通過交替讀寫管道文件,實現數據傳遞。通過兩個不同進程中的各自文件描述符指向同一個文件,來實現讀寫同一對象。

管道文件基本操作
  1. 創建管道文件的命令:mkfifo filename
  2. 刪除管道文件的命令:unlink filename
  3. 管道文件的創建函數:int mkfifo(const char *pathname, mode_t mode);
    參數 pathname 爲要創建的 管道文件的全路徑名;
    參數 mode 爲文件訪問權限
  4. 管道文件的刪除函數:int unlink(const char *pathname);
  5. 管道文件的讀寫打開關閉函數和其他類型的文件相同
管道文件的性質
  1. 打開管道文件時分讀端和寫端,分別用O_RDONLYO_WRONLY選項打開。
  2. 一個管道文件可以同時被多個進程打開
  3. 如果以讀取方式打開 FIFO,並且還沒有其它進程以寫入方式打開 FIFO,open 函數將被阻塞,即不能繼續運行,因爲沒有數據寫入文件,無法進行讀取
  4. 如果以寫入方式打開 FIFO,並且還沒其它進程以讀取方式 FIFO,open 函數也將被阻塞。因爲沒有進程讀取數據,寫入的數據可能無法正確送到目的地。
  5. 關閉 FIFO 時,如果先關讀端,將導致繼續往 FIFO 中寫數據的進程接收 SIGPIPE 的信號,如果寫進程不對該信號進行處理,將導致寫進程終止。
  6. 如果先關閉寫端則從另一端讀數據時,read 函數將返回 0,表示管道已經關閉
使用管道實現簡單的進程通信
//寫進程
int main(int argc,char* argv[])
{
    int fdw;
    fdw=open(argv[1],O_WRONLY);
    printf("I am writer fdw=%d\n",fdw);
    write(fdw,"hello",5);
    return 0;
}
//讀進程
int main(int argc,char *argv[])
{
    int fdr;
    fdr=open(argv[1],O_RDONLY);
    printf("I am reader,fdr=%d\n",fdr);
    char buf[128]={0};
    int ret=read(fdr,buf,sizeof(buf));
    printf("ret=%d,buf=%s\n",ret,buf);
    return 0;
}

注意當管道文件只有讀端或寫端時,進程均會在open函數處阻塞
運行效果:

在這裏插入圖片描述

select()——I/O 多路轉接模型

原理:在多進程操作時,如果進程請求的 I/O 操作阻塞,不是真正阻塞 I/O,而是讓其中的一個函數等待,則可以用select函數對一組套接字進行監控,如果有就緒信號時發出通知,從而實現多路複用輸入/輸出模型。

函數原型如下:
int select(int maxfd,fd_set *rdset,fd_set *wrset,fd_set *exset,struct timeval *timeout);

各個參數含義如下:

  • 參數maxfd是需要監視的最大的文件描述符值+1
  • rdset,wrset,exset分別對應於需要檢測的可讀文件描述符的集合,可寫文件描述符的集 合及異常文件描述符的集合
  • struct timeval結構用於描述一段時間長度,如果在這個時間內,需要監視的描述符沒有事件發生則函數返回,返回值爲0

需要重點注意的時fd_set數據類型,該數據類型是一個文件描述符數組,保存了一系列需要進行監控的文件描述符。

對於fd_set類型通過下面四個宏來操作:

  • FD_ZERO(fd_set *fdset) 將指定的文件描述符集清空,在對文件描述符集合進行設置前,必須對其進行初始化,如果不清空,由於在系統分配內存空間後,通常並不作清空處理,所以結果是不可知的。
  • FD_SET(fd_set *fdset) 用於在文件描述符集合中增加一個新的文件描述符。
  • FD_CLR(fd_set *fdset) 用於在文件描述符集合中刪除一個文件描述符。
  • FD_ISSET(int fd,fd_set *fdset) 用於測試指定的文件描述符是否就緒。

select函數功能總結:在給定時間對給定的文件集合不斷進行測試,返回就緒文件個數,即滿足可讀,可寫或有異常條件待處理的文件。如果time爲NULL在一直進行測試,如果time爲0則測一次後返回,如果time爲定值,在時間耗盡之前持續測試。
然後我們通過FD_ISSET(int fd,fd_set *fdset) 可以測試指定的文件描述符是否就緒,從而應對不同情況進行IO操作。

樣例:實現簡單的聊天程序:

//chatter1.c
int main(int args,char *argv[])
{
    ARG_CHECK(args,3);
    int fdr,fdw;
    fdr = open(argv[1],O_RDONLY);//存放要接收的信息的文件
    fdw = open(argv[2],O_WRONLY);//存放要發送的信息的文件
    printf("小明,fdr=%d,fdw=%d\n",fdr,fdw);
    char sentence[1024] = {0};

    fd_set rdset;//文件描述符數組,數組中的文件描述符需要手動添加
    int fdReadyNum;//計數
    int ret;    //返回值
    struct timeval tc;//等待時間

    while(1)
    {
        FD_ZERO(&rdset);//清空描述符集合
        FD_SET(STDIN_FILENO,&rdset);//將標準輸入流放入集合中
        FD_SET(fdr,&rdset);     //將收到的消息存放文件放入集合中
        tc.tv_sec =999;
        tc.tv_usec = 0;

        fdReadyNum = select(fdr+1,&rdset,NULL,NULL,&tc);

        //監控到就緒描述符
        if(fdReadyNum>0)
        {
            //標準輸入就緒,則接收字符串寫入fdw中
            if(FD_ISSET(STDIN_FILENO,&rdset))
            {
                memset(sentence,0,sizeof(sentence));

                ret = read(0,sentence,sizeof(sentence)-1);

                //標準輸入沒有輸入內容
                if(0==ret)
                {
                    printf("下線\n");
                    break;
                }
                write(fdw,sentence,strlen(sentence)-1);
            }
            //fdr文件可讀,代表管道內有新消息
            if(FD_ISSET(fdr,&rdset))
            {
                memset(sentence,0,sizeof(sentence));
                ret = read(fdr,sentence,sizeof(sentence));
                if(0==ret)
                {
                    printf("系統提示:對方已下線\n");
                    break;
                }
                printf("小紅:\n%s\n小明:\n",sentence);
            }
        }
        else
        {
            printf("超時無應答,連接已斷開!\n");
        }
    }
    return 0;
}

//chatter2.c
int main(int args,char *argv[])
{
    ARG_CHECK(args,3);

    int fdr,fdw;
    fdw = open(argv[1],O_WRONLY);//存放發送信息的文件
    fdr = open(argv[2],O_RDONLY);//存放接收信息的文件
    printf("小紅,fdr=%d,fdw=%d\n",fdr,fdw);     
    char sentence[1024] = {0};

    fd_set rdset;//文件描述符數組,數組中的文件描述符需要手動添加
    int fdReadyNum;//計數
    int ret;    //返回值
     printf("小紅:\n");   
    while(1)
    {
        FD_ZERO(&rdset);//清空描述符集合
        FD_SET(STDIN_FILENO,&rdset);//將標準輸入流放入集合中
        FD_SET(fdr,&rdset);     //將收到的消息存放文件放入集合中

        fdReadyNum = select(fdr+1,&rdset,NULL,NULL,NULL);
        //標準輸入就緒,則接收字符串寫入fdw中
        if(FD_ISSET(STDIN_FILENO,&rdset))
        {
            memset(sentence,0,sizeof(sentence));
            
            ret = read(0,sentence,sizeof(sentence)-1);

            //標準輸入沒有輸入內容
            if(0==ret)
            {
                printf("下線\n");
                break;
            }
            write(fdw,sentence,strlen(sentence)-1);
        }
        //fdr文件可讀,代表管道內有新消息
        if(FD_ISSET(fdr,&rdset))
        {
            memset(sentence,0,sizeof(sentence));
            ret = read(fdr,sentence,sizeof(sentence));
            if(0==ret)
            {
                printf("系統提示:對方已下線\n");
                break;
            }
            printf("小明:\n%s\n小紅:\n",sentence);
        }
    }
    return 0;
}


運行效果:

在這裏插入圖片描述

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