1. 程式人生 > >Linux程式設計——檔案 IO操作

Linux程式設計——檔案 IO操作

   Linux檔案 I\O 介紹

    1. Linux系統呼叫

    Linux系統呼叫(system call)是指作業系統提供給使用者程式的一組“特殊介面”,使用者程式可以通過這組“特殊”介面來獲得作業系統提供的特殊服務。

    為了更好的保護核心空間,將程式的執行空間分為核心空間和使用者空間,他們執行在不同的級別上,在邏輯上是相互隔離的。在Linux中,使用者程式不能直接訪問核心提供的服務,必須通過系統呼叫來使用核心提供的服務。

    Linux中的使用者程式設計介面(API)遵循了UNIX中最流行的應用程式設計介面標準——POSIX。這些系統呼叫程式設計介面主要是通過C庫(libc)實現的。

    2. 檔案描述符

    對核心而言,所有開啟檔案都由檔案描述符引用。檔案描述符是一個非負整數。當開啟一個現存檔案或建立一個新檔案時,核心向程序返回一個檔案描述符。當寫一個檔案時,用open或create返回的檔案描述符標識該檔案,將其作為引數傳送給read或write。

    在POSIX應用程式中,整數0、1、2應被代換成符號常數:

  1.     STDIN_FILENO(標準輸入,預設是鍵盤)
  2.     STDOUT_FILENO(標準輸出,預設是螢幕)
  3.     STDERR_FILENO(標準錯誤輸出,預設是螢幕)

    這些常數都定義在標頭檔案<unistd.h>中,檔案描述符的範圍是0~OPEN_MAX。早期的UNIX版本採用的上限值是19(允許每個程序開啟20個檔案), 現在很多系統則將其增加至256。

    可用的檔案I\O函式很多,包括:開啟檔案,讀檔案,寫檔案等。大多數Linux檔案I\O只需要用到5個函式:open,read,write,lseek以及close。


   基本API學習

    1. open

    需要包含的標頭檔案:<sys/types.h>,<sys/stat.h>,<fcntl.h> 函式原型:  

int open(const str * pathname, int oflag, [..., mode_t mode])

    功能:開啟檔案  返回值:成功則返回檔案描述符,出錯返回-1  引數:

    pathname: 開啟或建立的檔案的全路徑名   oflag:可用來說明此函式的多個選擇項, 詳見後。 mode:對於open函式而言,僅當建立新檔案時才使用第三個引數,表示新建檔案的許可權設定。

    詳解oflag引數:oflag 引數由O_RDONLY(只讀開啟)、O_WRONLY(只寫開啟)、O_RDWR(讀寫開啟)中的一個於下列一個或多個常數 O_APPEND: 追加到檔案尾 O_CREAT: 若檔案不存在則建立它。使用此選擇項時,需同時說明第三個引數mode,用其說明新聞件的訪問許可權 O_EXCL: 如果同時指定O_CREAT,而該檔案又是存在的,報錯;也可以測試一個檔案是否存在,不存在則建立。O_TRUNC: 如果次檔案存在,而且為讀寫或只寫成功開啟,則將其長度截短為0 O_SYNC: 使每次write都等到物理I\O操作完成。

    用open建立一個檔案:open.c 

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

#define FILE_PATH   "./test.txt"

int main(void)
{
    int fd;
    if ((fd = open(FILE_PATH, O_RDWR | O_CREAT | O_EXCL, 0666)) < 0) {
        printf("open error\n");
        exit(-1);
    } else {
        printf("open success\n");
    }
    return 0;
}

    如果當前目錄下以存在test.txt,螢幕上就會列印“open error”;不存在則建立該檔案,並列印“open success”。

    2. read

    需要包含的標頭檔案:<unistd.h>

    函式原型:

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

    功能:從開啟的檔案中讀取資料。  返回值:實際讀到的位元組數;已讀到檔案尾返回0,出錯的話返回-1,ssize_t是系統標頭檔案中用typedef定義的資料型別相當於signed int 引數:fd:要讀取的檔案的描述符buf:得到的資料在記憶體中的位置的首地址count:期望本次能讀取到的最大位元組數。size_t是系統標頭檔案中用typedef定義的資料型別,相當於unsigned int。

    3. write

   需要包含的標頭檔案:<unistd.h> 函式原型:

ssize_t write(int fd, const void * buf, size_t count)

    功能:向開啟的檔案寫資料 返回值:寫入成功返回實際寫入的位元組數,出錯返回-1。

    不得不提的是,返回-1的常見原因是:磁碟空間已滿,超過了一個給定程序的檔案長度。

    引數: fd:要寫入檔案的檔案描述符buf:要寫入檔案的資料在記憶體中存放位置的首地址count:期望寫入的資料的最大位元組數。

    read和write使用範例

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

int main(void)
{
    char buf[100];
    int num = 0;

    // 獲取鍵盤輸入,還記得POSIX的檔案描述符嗎?
    if ((num = read(STDIN_FILENO, buf, 10)) == -1) {
        printf ("read error");
        error(-1);
    } else {
    // 將鍵盤輸入又輸出到螢幕上
        write(STDOUT_FILENO, buf, num);
    }

    return 0;
}

    4. close

    需要包含的標頭檔案:<unistd.h> 函式原型:int close(int filedes) 功能:關閉一個開啟的檔案 引數:需要關閉檔案的檔案描述符。

    當一個程序終止的時候,它所有的開啟檔案都是由核心自動關閉。很多程式都使用這一功能而不顯式地調close關閉一個已開啟的檔案。 但是,作為一名優秀的程式設計師,應該顯式的呼叫close來關閉已不再使用的檔案。

    5. lseek

    每個開啟的檔案都有一個“當前檔案偏移量”,是一個非負整數,用以度量從檔案開始處計算的位元組數。通常,讀寫操作都是從當前檔案偏移量處開始,並使偏移量增加所讀或寫的位元組數。預設情況下,你開啟一個檔案(open),除非指定O_APPEND引數,不然位移量被設為0。

    需要包含的標頭檔案:<sys/types.h>,<unistd.h> 函式原型:

off_t lseek(int filesdes, off_t offset, int whence)

    功能:設定檔案內容讀寫位置 返回值:成功返回新的檔案位移,出錯返回-1;同樣off_t是系統標頭檔案定義的資料型別,相當於signed int 引數:

  1.     whence是SEEK_SET, 那麼該檔案的位移量設定為據檔案開始處offset個位元組
  2.     whence是SEEK_CUR, 那麼該檔案的位移量設定為當前值加offset。offset可為正或負
  3.     whence是SEEK_END, 那麼該檔案的位移量設定為檔案長度加offset。offset可為正或負
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char * argv[])
{
    int fd;
    char buf[100];
    if ((fd = open(argv[1], O_RDONLY)) < 0) {
        perror("open");
        exit(-1);
    }
    read(fd, buf, 1);
    write(STDOUT_FILENO, buf, 1);
    lseek(fd, 2, SEEK_CUR);

    read(fd, buf, 1);
    write(STDOUT_FILENO, buf, 1);
    lseek(fd, -1, SEEK_END);

    read(fd, buf, 1);
    write(STDOUT_FILENO, buf, 1);
    lseek(fd, 0, SEEK_SET);

    read(fd, buf, 1);
    write(STDOUT_FILENO, buf, 1);
    close(fd);
    printf("\n");

    return 0;
}

    6. select

    之前的read函式可以監控一個檔案描述符(eg:鍵盤)是否有輸入,當鍵盤沒有輸入,read將會阻塞,直到使用者從鍵盤輸入為止。用相同的方法可以監控滑鼠是否有輸入。但想同時監控滑鼠和鍵盤是否有輸入,這個方法就不行的了。

// /dev/input/mice 是滑鼠的裝置檔案
fd = open("/dev/input/mice", O_RDONLY);
read(0, buf, 100);
read(fd, buf, 100);

    在上面的程式中,當read鍵盤的時候,若無鍵盤輸入則程式阻塞在第2行,此時即使滑鼠有輸入,程式也沒有機會執行第3行獲得滑鼠的輸入。這種情況就需要select同時監控多個檔案描述符。

    需要包含的標頭檔案:<sys/select.h> 函式原型:

int select(int maxfd, fd_set \* readset, fd_set \* writeset, fd_set \* exceptset, const struct timeval \* timeout)

    返回值:失敗返回-1,成功返回readset,writeset,exceptset中所有,有指定變化的檔案描述符的數目(若超時返回0)

    引數: maxfd:要檢測的描述符個數, 因此值應為最大描述符+1 readset:被監控是否有輸入的檔案描述符集。不監控時,設為NULL writeset:被監控是否可以輸入的檔案描述符集。不監控時,設為NULL exceptset:被監控是否有錯誤產生的檔案描述符集。不監控時,設為NULL timeval:監控超時時間。設定為NULL表示一直阻塞到有檔案描述符被監控到有指定變化。

    readset,writeset,exceptset這三個描述符集指標均是值—結果引數,呼叫的時候,被監控描述符相應位需要置1;返回時,未就緒的描數字相應位會被清0,而就緒的會被置1。下面的系統定義的巨集,和select配套使用

    FD_ZERO(&rset):將檔案描述符集rset的所有位清0

    FD_SET(4, &reset):設定檔案描述符集rset的bit 4

    FD_CLR(fileno(stdin), &rset):將檔案描述符集rset的bit 0清0

    FD_ISSET(socketfd, &rset):若檔案描述符集rset中的socketfd位置1   

#include <stdio.h>
#include <sys/select.h>
#include <fcntl.h>
#include <unistd.h>

#define MAXNUM      100
#define OPEN_DEV    "/dev/input/mice"

int main(void)
{
    fd_set rfds;
    struct timeval tv;
    int retval, fd;
    char buf[MAXNUM];

    fd = open(OPEN_DEV, O_RDONLY);
    while (1) {
        FD_ZERO(&rfds);
        FD_SET(0, &rfds);
        FD_SET(fd, &rfds);
        tv.tv_sec = 5;
        tv.tv_usec = 0;

        retval = select(fd+1, &rfds, NULL, NULL, &tv);
        if (retval < 0)
            printf ("error\n");
        if (retval == 0)
            printf ("No data within 5 seconds\n");
        if (retval > 0) {
            if (FD_ISSET(0, &rfds)) {
                printf ("Data is available from keyboard now\n");
                read(0, buf, MAXNUM);
            }
            if (FD_ISSET(fd, &rfds)) {
                printf ("Data is available from mouse now\n");
                read(fd, buf, MAXNUM);
            }
        }
    }
    return 0;
}

   

   stat 的使用

    Linux有個命令,ls -l,效果如下:

   

    這個命令能顯示檔案的型別、操作許可權、硬連結數量、屬主、所屬組、大小、修改時間、檔名。它是怎麼獲得這些資訊的呢,請看下面的講解。

    1. stat 的基本使用

    系統呼叫stat的作用是獲取檔案的各個屬性。

    需要包含的標頭檔案: <sys/types.h><sys/stat.h><unistd.h> 函式原型:   

int stat(const char \* path, struct stat \* buf)

     功能:檢視檔案或目錄屬性。將引數path所指的檔案的屬性,複製到引數buf所指的結構中。引數:path:要檢視屬性的檔案或目錄的全路徑名稱。buf:指向用於存放屬性的結構體。stat成功呼叫後,buf的各個欄位將存放各個屬性。struct stat是系統標頭檔案中定義的結構體,定義如下: 

struct stat {
    dev_t       st_dev;
    ino_t       st_ino;
    mode_t      st_mode;
    nlink_t     st_nlink;
    uid_t       st_uid;
    gid_t       st_gid;
    dev_t       st_rdev;
    off_t       st_size;
    blksize_t   st_blksize;
    blkcnt_t    st_blocks;
    time_t      st_atime;
    time_t      st_mtime;
    time_t      st_ctime;
};

    st_ino:節點號 st_mode:檔案型別和檔案訪問許可權被編碼在該欄位中 st_nlink:硬連線數  st_uid:屬主的使用者ID  st_gid:所屬組的組ID st_rdev:裝置檔案的主、次裝置號編碼在該欄位中 st_size:檔案的大小  st_mtime:檔案最後被修改時間

    返回值:成功返回0;失敗返回-1

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    struct stat     buf;
    if(argc != 2) { 
        printf("Usage: stat <pathname>"); 
        exit(-1); 
    }
    if(stat(argv[1], &buf) != 0) { 
        printf("stat error."); 
        exit(-1); 
    }
    printf("#i-node:    %ld\n", buf.st_ino);
    printf("#link:      %d\n", buf.st_nlink);
    printf("UID:        %d\n", buf.st_uid);
    printf("GID:        %d\n", buf.st_gid);
    printf("Size        %ld\n", buf.st_size);
    exit(0);
}

     2. 檔案型別的判定

    struct stat中有個欄位為st_mode,可用來獲取檔案型別和檔案訪問許可權,我們將陸續學到從該欄位解碼我們需要的檔案資訊。st_mode中檔案型別巨集定義

   

    我們修改上面的例子:

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    struct stat buf;
    char * file_mode;
    if(argc != 2) {
        printf("Usage: stat <pathname>\n"); 
        exit(-1); 
    }
    if(stat(argv[1], &buf) != 0) {
        printf("stat error.\n"); 
        exit(-1); 
    }
    if (S_ISREG(buf.st_mode))
        file_mode = "-";
    else if (S_ISDIR(buf.st_mode))
        file_mode = "d";
    else if (S_ISCHR(buf.st_mode))
        file_mode = "c";
    else if(S_ISBLK(buf.st_mode))
        file_mode = "b";
    printf("#i-node:    %ld\n", buf.st_ino);
    printf("#link:      %d\n", buf.st_nlink);
    printf("UID:        %d\n", buf.st_uid);
    printf("GID:        %d\n", buf.st_gid);
    printf("Size        %ld\n", buf.st_size);
    printf("mode: %s\n", file_mode);
    exit(0);
}

     

    目錄操作

    當目標是目錄而不是檔案的時候,ls -l的結果會顯示目錄下所有子條目的資訊,怎麼去遍歷整個目錄呢?答案馬上揭曉!

    1. 開啟目錄

    需要包含的標頭檔案:<sys/types.h><dirent.h>

    函式原型:DIR * opendir(const char * name)

    功能:opendir()用來開啟引數name指定的目錄,並返回DIR *形態的目錄流 返回值:成功返回目錄流;失敗返回NULL

    2. 讀取目錄

    函式原型:struct dirent * readdir(DIR * dir)

    功能:readdir()返回引數dir目錄流的下一個子條目(子目錄或子檔案)

    返回值: 成功返回結構體指向的指標,錯誤或以讀完目錄,返回NULL

    函式執行成功返回的結構體原型如下:  

struct dirent {
   ino_t   d_ino;
   off_t   d_off;
   unsigned short  d_reclen;
   unsigned char   d_type;
   char    d_name[256];
};

    其中 d_name欄位,是存放子條目的名稱

    3. 關閉目錄

    函式原型:int closedir(DIR * dir)

    功能:closedir()關閉dir所指的目錄流

    返回值:成功返回0;失敗返回-1,錯誤原因在errno中

    我們來學習一個綜合的例子吧: 

#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
int main(int argc, char *argv[])
{
    DIR *dp;
    struct dirent *entp;
    if (argc != 2) {
        printf("usage: showdir dirname\n");
        exit(0);
    }
    if ((dp = opendir(argv[1])) == NULL) {
        perror("opendir");
        exit(-1);
    }
    while ((entp = readdir(dp)) != NULL)
        printf("%s\n", entp->d_name);

    closedir(dp);
    return 0;
}