Windows、Linux下檔案操作(寫、刪除)錯誤的產生原因、及解決方法

Andrew.Hann發表於2015-06-18

catalog

0. 引言
1. Linux平臺上涉及的File IO操作
2. Windows平臺上涉及的File IO操作

 

0. 引言

本文試圖討論在windows、linux作業系統上基於C庫進行檔案IO操作時,可能遇到的錯誤,及其解決方法,主機安全攻防產品除了需要將安全攻防上的領域知識固化到程式實現上之外,還極度依賴關聯絡統本身、程式語言庫的特性,原則上,並不是所有的安全需求都能100%地落實到程式設計中,這需要我們對作業系統、程式語言本身具有較深的理解

Relevant Link:

http://www.cnblogs.com/LittleHann/p/3905608.html
http://www.cnblogs.com/LittleHann/p/4305892.html
//搜尋:0x3: 許可權檢查 

 

1. Linux平臺上涉及的File IO操作

0x1: 開啟目錄: opendir()

全磁碟遍歷、檔案IO操作的第一步是開啟檔案目錄本身,在Linux中,檔案和目錄都統一用inode進行抽象

#include <sys/types.h>  
#include <dirent.h>

DIR * opendir(const char * name);
//返回值:成功則返回DIR* 型態的目錄流, 開啟失敗則返回NULL

錯誤程式碼

1. EACCESS: 許可權不足
2. EMFILE: 已達到程式可同時開啟的檔案數上限 
3. ENFILE: 已達到系統可同時開啟的檔案數上限 
4. ENOTDIR: 引數name非真正的目錄
5. ENOENT: 引數name指定的目錄不存在,或是引數name為一空字串
6. ENOMEM: 核心記憶體不足 

其中EACCESS(許可權不足)是最經常遇到的問題

1. Each directory in the path name preceding the directory to be opened
    1) 呼叫opendir,如果傳入路徑引數的各個分量中,有一個分量沒有X許可權,則返回Permission denied(EACCES)錯誤
    2) 沒有X許可權,ll命令雖然可以列出該目錄下的檔案及目錄,但是同樣因為可執行(x)許可權無法檢視屬性
    3) cd命令需要可執行(x)許可權,因此無法通過cd命令切換到該目錄下,子目錄也是不可切換的,即便對子目錄有完整許可權
    4) 同樣對於cat命令,由於缺少可執行(x)許可權,該目錄下的檔案也是不可讀的
    5) 對於stat系統呼叫和cat命令是一樣的,如果缺少x許可權,則stat將執行失敗
    6) 沒有x許可權,目錄下的檔案也不能cp
    7) 父目錄沒有x許可權,即使該目錄下的檔案可讀可執行,也無法執行該目錄下的檔案
//需要明白的是,Linux的目錄定址定位是逐個路徑分量逐段進行的,如果父目錄因為沒有x許可權受阻了,則即使目錄下的子檔案許可權足夠,也無法正常操作

2. The directory to be opened
如果傳入的目錄路徑沒有R許可權, 則呼叫opendir返回Permission denied(EACCES)錯誤,沒有R許可權導致無法讀取目錄inode的元資訊

我們梳理一下概念

1. 對於檔案許可權: 可讀許可權(r)是可執行(x)的基礎,因為執行的前提是讀取二進位制資料
2. 對於目錄許可權: 可執行(x)許可權是關鍵,沒有可執行許可權意味著所有命令都不能在該目錄及子目錄下執行,該目錄及子目錄下的檔案也不能被執行。這意味著我們常用的命令cd、ls、mkdir、cp、mv、rm等等在該目錄下全部失效

Relevant Link:

http://c.biancheng.net/cpp/html/319.html
http://pubs.opengroup.org/onlinepubs/009695399/functions/opendir.html
http://lxr.free-electrons.com/source/include/uapi/asm-generic/errno-base.h#L16
http://axisray.me/2015/01/04/linux-dir-and-file-permission/
http://os.51cto.com/art/201003/186949.htm

0x2: 讀取目錄: readdir()

#include <sys/types.h>   
#include <dirent.h>

struct dirent * readdir(DIR * dir);
/*
返回值
1. 成功則返回下個目錄進入點
2. 有錯誤發生或讀取到目錄檔案尾則返回NULL
*/

No authorization is required. Authorization is verified during opendir()
錯誤程式碼

1. EOVERFLOW
One of the values in the structure to be returned cannot be represented correctly.
The readdir() function may fail if:

2. EBADF
The dirp argument does not refer to an open directory stream.

3. ENOENT
The current position of the directory stream is invalid.

Relevant Link:

http://c.biancheng.net/cpp/html/320.html
http://pubs.opengroup.org/onlinepubs/7908799/xsh/readdir.html
https://www-01.ibm.com/support/knowledgecenter/ssw_ibm_i_61/apis/readdir.htm

0x3: 獲取檔案屬性: stat()

在進行檔案IO操作的時候,常常遇到目標檔案設定了ACL許可權而導致操作失敗的情況,這就要求我們在進行檔案IO操作之前對檔案的屬性進行檢測,並設定相應的許可權以支援讀寫

#include <sys/stat.h>  
#include <unistd.h>
 
int stat(const char * file_name, struct stat *buf);

返回值: 執行成功則返回0,失敗返回-1,錯誤程式碼存於errno
錯誤程式碼

1. ENOENT: 引數file_name指定的檔案不存在
2. ENOTDIR: 路徑中的目錄存在但卻非真正的目錄
3. ELOOP: 欲開啟的檔案有過多符號連線問題,上限為16符號連線
4. EFAULT: 引數buf為無效指標,指向無法存在的記憶體空間
5. EACCESS: 存取檔案時被拒絕
6. ENOMEM: 核心記憶體不足
7. ENAMETOOLONG: 引數file_name的路徑名稱太長

如果需要修改檔案屬性,可以通過如下方法

1. chown設定檔案屬性
/*
#include <sys/types.h>
#include <unistd.h>

int chown(const char *path,uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group)’
int lchown(const char *path, uid_t owner,gid_t group);
檔案的所有者只能改變檔案的組id為其所屬組中的一個,超級使用者才能修改檔案的所有者id,並且超級使用者可以任意修改檔案的使用者組id
*/

2. truncate改變檔案大小
/*
#include <sys/types.h>
#include <unistd.h>

int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
將指定檔案大小改為引數length指定的大小,如果原來的檔案比引數length大,則超過的部分會被刪除;如果原來的檔案大小比引數length小,則檔案將被擴充套件,擴充套件部分用0填充
*/
      
3. utime改變檔案的st_mtime域和st_ctime域,即存取時間和修改時間
/*
#include <sys/types.h>
#include <utime.h>
int utime(const char *filename,struct utimbuf *buf);

#include <sys/time.h>
int utime(char *filename,struct timeval *tvp);
如果buf是一個空指標,則存取時間和修改時間都為當前時間
*/

Relevant Link:

http://blog.csdn.net/dlutbrucezhang/article/details/8627387
http://c.biancheng.net/cpp/html/326.html
http://www.cnblogs.com/LittleHann/p/3905608.html
//搜尋:3. int stat(const char * file_name, struct stat *buf);

0x4: 開啟檔案: 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);
//引數pathname指向欲開啟的檔案路徑字串 

錯誤程式碼

1. EEXIST: 引數pathname所指的檔案已存在,卻使用了O_CREAT和O_EXCL旗標 
2. EACCESS: 引數pathname所指的檔案不符合所要求測試的許可權
3. EROFS: 欲測試寫入許可權的檔案存在於只讀檔案系統內
4. EFAULT: 引數pathname指標超出可存取記憶體空間
5. EINVAL: 引數mode不正確 
6. ENAMETOOLONG: 引數pathname太長
7. ENOTDIR: 引數pathname不是目錄
8. ENOMEM: 核心記憶體不足
9. ELOOP: 引數pathname有過多符號連線問題
10. EIO: I/O存取錯誤

受到POSIX ACL規則的影響,open函式經常返回的是EACCESS許可權錯誤

Relevant Link:

http://c.biancheng.net/cpp/html/238.html
http://www.cnblogs.com/LittleHann/p/3905608.html
//搜尋:1. int open(const char *pathname, int flags);

0x5: 向檔案寫入內容: write()

#include <unistd.h>

ssize_t write (int fd, const void * buf, size_t count);
//返回值: 如果順利write()會返回實際寫入的位元組數. 當有錯誤發生時則返回-1, 錯誤程式碼存入errno中

錯誤程式碼

1. EINTR: 此呼叫被訊號所中斷.
2. EAGAIN: 當使用不可阻斷I/O 時(O_NONBLOCK),若無資料可讀取則返回此值
3. EADF: 引數fd非有效的檔案描述詞,或該檔案已關閉 

Relevant Link:

http://c.biancheng.net/cpp/html/241.html

0x6: Linux檔案佔用狀態下: 開啟檔案

檔案的操作的獨佔性是通過檔案系統flag指定,而作業系統提供底層的強制保證的,但是Linux並不像windows那樣提供強制的讀寫互斥保護,即

1. 兩個程式可以同時以寫模式開啟一個檔案,同時向其中寫入資料,這可能導致不一致情況的發生
2. 兩個程式可以分別以讀模式、寫模式開啟一個檔案,同時進行讀/寫的操作,這可能導致錯誤的資料讀取,即髒讀、幻讀

因為對於Linux VFS檔案系統來說,開啟一個檔案,open系統呼叫僅僅是向作業系統發起一個inode操作請求,每個程式都持有自己獨有的fd控制程式碼,並通過引用共享核心記憶體中的inode向底層的儲存資料的超級塊發起資料操作請求(inode的引用計數加1),VFS本身並沒有處理互斥的問題

0x7: Linux檔案佔用狀態下: 刪除檔案

和開啟檔案一樣,Linux VFS並沒有對檔案的刪除採用獨佔保護。Linux是通過link的數量來控制檔案刪除的,只有當一個檔案不存在任何link的時候,這個檔案才會被刪除。一般來說,每個檔案都有2個link計數器

1. i_count: i_count的意義是當前檔案使用者(或被呼叫)的數量
2. i_nlink: i_nlink的意義是介質連線的數量(硬連結的數量)
//可以理解為i_count是記憶體引用計數器,i_nlink是磁碟的引用計數器,當一個檔案被某一個程式引用時,對應i_count數就會增加;當建立檔案的硬連結的時候,對應i_nlink數就會增加 

對於刪除命令rm而言,實際就是減少磁碟引用計數i_nlink,如果一個檔案正在被某個程式呼叫,而使用者卻執行rm操作把檔案刪除了,因為rm操作只是將檔案的i_nlink減少了,如果沒其它的連結i_nlink就為0了;但由於該檔案依然被程式引用,因此,此時檔案對應的i_count並不為0,所以即使執行rm操作,但系統並沒有真正刪除這個檔案,當只有i_nlink及i_count都為0的時候,這個檔案才會真正被刪除。也就是說,還需要解除該程式的對該檔案的呼叫才行
從原則上來說,Linux並不提供檔案獨佔的保護,也就是從根本上說,即使一個檔案正在被程式使用,那麼其他程式也可以直接無條件地刪除這個檔案,雖然真實的刪除要等到原始程式釋放對該檔案的持有時才進行

0x7: Linux中的檔案鎖

從應用程式的角度來說,需要由程式設計師自己保證檔案的獨佔使用,即在開啟之前,先檢測當前檔案是否被其他程式佔用,但是這樣增加了開發的成本,而且也不具有相容性,一個更優雅的解決方案是利用Linux作業系統自身的檔案鎖機制,從而更好地解決多個程式讀取同一個檔案的互斥問題
早期的UNIX系統只支援對整個檔案進行加鎖,因此無法執行資料庫之類的程式,因為此類程式需要實現記錄級的加鎖。在System V Release 3中,通過fcntl提供了記錄級的加鎖,此後發展成為 POSIX標準的一部分
Linux 支援的檔案鎖技術主要包括

1. 勸告鎖(advisory lock)
2. 強制鎖(mandatory lock)
/*
在Linux中,不論程式是在使用勸告鎖還是強制鎖,它都可以同時使用共享鎖(S鎖 讀鎖)和排他鎖(X鎖 寫鎖)
1. 多個共享鎖之間不會相互干擾,多個程式在同一時刻可以對同一個檔案加共享鎖
2. 如果一個程式對該檔案加了排他鎖,那麼其他程式則無權再對該檔案加共享鎖或者排他鎖,直到該排他鎖被釋放
*/

3. 共享模式強制鎖(share-mode mandatory lock)
4. 租借鎖(lease)

對於同一個檔案來說,它可以同時擁有很多讀者,但是在某一特定時刻,它只能擁有一個寫者,它們之間的相容關係如表所示

 是否滿足請求
當前加上的鎖 共享鎖 排他鎖
共享鎖
排他鎖

1. 勸告鎖

勸告鎖是一種協同工作的鎖。對於這一種鎖來說,核心只提供加鎖以及檢測檔案是否已經加鎖的手段,但是核心並不參與鎖的控制和協調。也就是說,如果有程式不遵守"遊戲規則",不檢查目標檔案是否已經由別的程式加了鎖就往其中寫入資料,那麼核心是不會加以阻攔的。因此,勸告鎖並不能阻止程式對檔案的訪問,而只能依靠各個程式在訪問檔案之前檢查該檔案是否已經被其他程式加鎖來實現併發控制
程式需要事先對鎖的狀態做一個約定,並根據鎖的當前狀態和相互關係來確定其他程式是否能對檔案執行指定的操作。從這點上來說,勸告鎖的工作方式與使用訊號量保護臨界區的方式非常類似。
勸告鎖可以對檔案的任意一個部分進行加鎖,也可以對整個檔案進行加鎖,甚至可以對檔案將來增大的部分也進行加鎖。由於程式可以選擇對檔案的某個部分進行加鎖,所以一個程式可以獲得關於某個檔案不同部分的多個鎖
2. 強制鎖

與勸告鎖不同,強制鎖是一種核心強制採用的檔案鎖,它是從System V Release 3開始引入的。每當有系統呼叫open()、read()以及write()發生的時候,核心都要檢查並確保這些系統呼叫不會違反在所訪問檔案上加的強制鎖約束。也就是說,如果有程式不遵守遊戲規則,硬要往加了鎖的檔案中寫入內容,核心就會加以阻攔

1. 如果一個檔案已經被加上了讀鎖或者共享鎖,那麼其他程式再對這個檔案進行寫操作就會被核心阻止 
2. 如果一個檔案已經被加上了寫鎖或者排他鎖,那麼其他程式再對這個檔案進行讀取或者寫操作就會被核心阻止 

如果其他程式試圖訪問一個已經加有強制鎖的檔案,程式行為取決於所執行的操作模式和檔案鎖的型別,歸納如表所示

當前鎖型別阻塞讀阻塞寫非阻塞讀非阻塞寫
讀鎖 正常讀取資料 阻塞 正常讀取資料 EAGAIN
寫鎖 阻塞 阻塞 EAGAIN EAGAIN

需要注意的是,如果要訪問的檔案的鎖型別與要執行的操作存在衝突,那麼採用阻塞讀/寫操作的程式會被阻塞,而採用非阻塞讀/寫操作的程式則不會阻塞,而是立即返回EAGAIN
然而,在有些應用中並不適合使用強制鎖,所以索引節點結構中的i_flags欄位中定義了一個標誌位MS_MANDLOCK用於有選擇地允許或者不允許對一個檔案使用強制鎖。在super_block結構中,也可以將s_flags這個標誌為設定為1或者0,用以表示整個裝置上的檔案是否允許使用強制鎖
要想對一個檔案採用強制鎖,必須按照以下步驟執行

1. 使用 -o mand 選項來掛載檔案系統。這樣在執行 mount() 系統呼叫時,會傳入 MS_MANDLOCK 標記,從而將 super_block 結構中的 s_flags 設定為 1,用來表示在這個檔案系統上可以採用強制鎖
*
# mount -o mand /dev/sdb7 /mnt
# mount | grep mnt
/dev/sdb7 on /mnt type ext3 (rw,mand)
*/

2. 修改要加強制鎖的檔案的許可權: 設定SGID位,並清除組可執行位。這種組合通常來說是毫無意義的,系統用來表示該檔案被加了強制鎖。例如:
# touch /mnt/testfile
# ls -l /mnt/testfile 
-rw-r--r-- 1 root root 0 Jun 22 14:43 /mnt/testfile
# chmod g+s /mnt/testfile  
# chmod g-x /mnt/testfile
# ls -l /mnt/testfile    
-rw-r-Sr-- 1 root root 0 Jun 22 14:43 /mnt/testfile

3. 使用 fcntl() 系統呼叫對該檔案進行加鎖或解鎖操作 

3. 共享模式鎖

共享模式強制鎖可以用於某些私有網路檔案系統,如果某個檔案被加上了共享模式強制鎖,那麼其他程式開啟該檔案的時候不能與該檔案的共享模式強制鎖所設定的訪問模式相沖突。但是由於可移植性不好,因此並不建議使用這種鎖
4. 租借鎖

採用強制鎖之後,如果一個程式對某個檔案擁有寫鎖,只要它不釋放這個鎖,就會導致訪問該檔案的其他程式全部被阻塞或不斷失敗重試;即使該程式只擁有讀鎖,也會造成後續更新該檔案的程式的阻塞。為了解決這個問題,Linux 中採用了一種新型的租借鎖。
當程式嘗試開啟一個被租借鎖保護的檔案時,該程式會被阻塞,同時,在一定時間內擁有該檔案租借鎖的程式會收到一個訊號。收到訊號之後,擁有該檔案租借鎖的程式會首先更新檔案,從而保證了檔案內容的一致性,接著,該程式釋放這個租借鎖。如果擁有租借鎖的程式在一定的時間間隔內沒有完成工作,核心就會自動刪除這個租借鎖或者將該鎖進行降級,從而允許被阻塞的程式繼續工作

//系統預設的這段間隔時間是45秒鐘
int lease_break_time = 45;

這個引數可以通過修改"/proc/sys/fs/lease-break-time"進行調節,前提是"/proc/sys/fs/leases-enable" = 1

0x8: 鎖的使用方法示例

# cat -n mandlock.c

#include <errno.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/stat.h>

int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len)
{
    struct flock    lock;

    lock.l_type = type;     /* F_RDLCK, F_WRLCK, F_UNLCK */
    lock.l_start = offset;  /* byte offset, relative to l_whence */
    lock.l_whence = whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
    lock.l_len = len;       /* #bytes (0 means to EOF) */
    
    return( fcntl(fd, cmd, &lock) );
}
#define read_lock(fd, offset, whence, len) lock_reg(fd, F_SETLK, F_RDLCK, offset, whence, len)
#define write_lock(fd, offset, whence, len) lock_reg(fd, F_SETLK, F_WRLCK, offset, whence, len)
#define err_sys(x) { perror(x); exit(1); }

int main(int argc, char *argv[])
{
    int             fd, val;
    pid_t           pid;
    char            buf[5];
    struct stat     statbuf;
    if (argc != 2) 
    {
        fprintf(stderr, "usage: %s filename\n", argv[0]);
        exit(1);
    }
    if ((fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC )) < 0)
        err_sys("open error");
    if (write(fd, "hello world", 11) != 11)
        err_sys("write error");

    /* turn on set-group-ID and turn off group-execute */
    if (fstat(fd, &statbuf) < 0)
        err_sys("fstat error");
    if (fchmod(fd, (statbuf.st_mode & ~S_IXGRP) | S_ISGID) < 0)
        err_sys("fchmod error");
    
    sleep(2);
    
    if ((pid = fork()) < 0) 
    {
        err_sys("fork error");
    } 
    else if (pid > 0) 
    {   /* parent */
        /* write lock entire file */
        if (write_lock(fd, 0, SEEK_SET, 0) < 0)
            err_sys("write_lock error");
        sleep(20);      /* wait for child to set lock and read data */
        
        if (waitpid(pid, NULL, 0) < 0)
            err_sys("waitpid error");
    } 
    else 
    {            /* child */
        sleep(10);      /* wait for parent to set lock */
    
        if ( (val = fcntl(fd, F_GETFL, 0)) < 0)
            err_sys("fcntl F_GETFL error");
        
        val |= O_NONBLOCK;           /* turn on O_NONBLOCK flag */
        if (fcntl(fd, F_SETFL, val) < 0)
            err_sys("fcntl F_SETFL error");
        /* first let's see what error we get if region is locked */
        if (read_lock(fd, 0, SEEK_SET, 0) != -1)    /* no wait */
            err_sys("child: read_lock succeeded");
        printf("read_lock of already-locked region returns %d: %s\n", errno, strerror(errno));
        
        /* now try to read the mandatory locked file */
        if (lseek(fd, 0, SEEK_SET) == -1)
            err_sys("lseek error");
        if (read(fd, buf, 5) < 0)
            printf("read failed (mandatory locking works)\n");
        else
            printf("read OK (no mandatory locking), buf = %5.5s\n", buf);
    }
    exit(0);
} 
/*
# mount | grep mnt
/dev/sdb7 on /mnt type ext3 (rw,mand)
/dev/sdb6 on /tmp/mnt type ext3 (rw)
# ./mandlock /mnt/testfile    
read_lock of already-locked region returns 11: Resource temporarily unavailable
read failed (mandatory locking works)
# ./mandlock /tmp/mnt/testfile
read_lock of already-locked region returns 11: Resource temporarily unavailable
read OK (no mandatory locking), buf = hello
*/

Relevant Link:

http://blog.sina.com.cn/s/blog_4b3c1f950102uz74.html 
http://www.ibm.com/developerworks/cn/linux/l-cn-filelock/ 

 

2. Windows平臺上涉及的File IO操作

因為C庫是跨作業系統平臺的,對於Linux、Windows系統來說,C庫封裝了對檔案的IO操作,但是在底層,C庫分別實現了Win32 API的封裝(windows)、系統呼叫的封裝(linux),在Linux平臺上,C庫和系統呼叫幾乎是平行的,在Windows上,C庫和Win32 API差別較大,並且最終還是要通過Win32 API得以實現,因此,我們接下來全部以原生的Win32 API進行討論

0x1: 遍歷目錄: FindFirstFile、FindNextFile

#include <windows.h>
#include <tchar.h> 
#include <stdio.h>
#include <strsafe.h>
#pragma comment(lib, "User32.lib")

void DisplayErrorBox(LPTSTR lpszFunction);

int _tmain(int argc, TCHAR *argv[])
{
   WIN32_FIND_DATA ffd;
   LARGE_INTEGER filesize;
   TCHAR szDir[MAX_PATH];
   size_t length_of_arg;
   HANDLE hFind = INVALID_HANDLE_VALUE;
   DWORD dwError=0;
   
   // If the directory is not specified as a command-line argument,
   // print usage.

   if(argc != 2)
   {
      _tprintf(TEXT("\nUsage: %s <directory name>\n"), argv[0]);
      return (-1);
   }

   // Check that the input path plus 3 is not longer than MAX_PATH.
   // Three characters are for the "\*" plus NULL appended below.

   StringCchLength(argv[1], MAX_PATH, &length_of_arg);

   if (length_of_arg > (MAX_PATH - 3))
   {
      _tprintf(TEXT("\nDirectory path is too long.\n"));
      return (-1);
   }

   _tprintf(TEXT("\nTarget directory is %s\n\n"), argv[1]);

   // Prepare string for use with FindFile functions.  First, copy the
   // string to a buffer, then append '\*' to the directory name.

   StringCchCopy(szDir, MAX_PATH, argv[1]);
   StringCchCat(szDir, MAX_PATH, TEXT("\\*"));

   // Find the first file in the directory.

   hFind = FindFirstFile(szDir, &ffd);

   if (INVALID_HANDLE_VALUE == hFind) 
   {
      DisplayErrorBox(TEXT("FindFirstFile"));
      return dwError;
   } 
   
   // List all the files in the directory with some info about them.

   do
   {
      if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
      {
         _tprintf(TEXT("  %s   <DIR>\n"), ffd.cFileName);
      }
      else
      {
         filesize.LowPart = ffd.nFileSizeLow;
         filesize.HighPart = ffd.nFileSizeHigh;
         _tprintf(TEXT("  %s   %ld bytes\n"), ffd.cFileName, filesize.QuadPart);
      }
   }
   while (FindNextFile(hFind, &ffd) != 0);
 
   dwError = GetLastError();
   if (dwError != ERROR_NO_MORE_FILES) 
   {
      DisplayErrorBox(TEXT("FindFirstFile"));
   }

   FindClose(hFind);
   return dwError;
}


void DisplayErrorBox(LPTSTR lpszFunction) 
{ 
    // Retrieve the system error message for the last-error code

    LPVOID lpMsgBuf;
    LPVOID lpDisplayBuf;
    DWORD dw = GetLastError(); 

    FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | 
        FORMAT_MESSAGE_FROM_SYSTEM |
        FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL,
        dw,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPTSTR) &lpMsgBuf,
        0, NULL );

    // Display the error message and clean up

    lpDisplayBuf = (LPVOID)LocalAlloc(LMEM_ZEROINIT, 
        (lstrlen((LPCTSTR)lpMsgBuf)+lstrlen((LPCTSTR)lpszFunction)+40)*sizeof(TCHAR)); 
    StringCchPrintf((LPTSTR)lpDisplayBuf, 
        LocalSize(lpDisplayBuf) / sizeof(TCHAR),
        TEXT("%s failed with error %d: %s"), 
        lpszFunction, dw, lpMsgBuf); 
    MessageBox(NULL, (LPCTSTR)lpDisplayBuf, TEXT("Error"), MB_OK); 

    LocalFree(lpMsgBuf);
    LocalFree(lpDisplayBuf);
}

If the function fails or fails to locate files from the search string in the lpFileName parameter, the return value is INVALID_HANDLE_VALUE
If the function fails because no matching files can be found, the GetLastError function returns ERROR_FILE_NOT_FOUND.

Relevant Link:

https://msdn.microsoft.com/en-us/library/aa365200(VS.85).aspx
http://www.cnblogs.com/lanxuezaipiao/p/3420025.html 
https://msdn.microsoft.com/en-us/library/aa364418(v=vs.85).aspx

0x2: 獲取、更改檔案屬性

可以在對檔案進行IO操作之前對檔案的屬性進行檢測,並進行相對應的修改,例如關閉只讀屬性,使檔案可寫

SetFileAttributes function
GetFileAttributes function

0x2: 開啟檔案: _open、_wopen C API

int _open(
   const char *filename,
   int oflag [,
   int pmode] 
);
int _wopen(
   const wchar_t *filename,
   int oflag [,
   int pmode] 
);
  
//Opens a file. These functions are deprecated because more-secure versions are available; see _sopen_s, _wsopen_s

對於windows系統來說,系統核心對檔案獨佔、互斥提供的底層支援,並通過檔案IO API將能力提供給程式設計師,我們來看open API涉及的引數

1. filename: File name.
2. oflag: The kind of operations allowed.
oflag is an integer expression formed from one or more of the following manifest constants or constant combinations, which are defined in <fcntl.h>.
    1) _O_APPEND: Moves the file pointer to the end of the file before every write operation.
    2) _O_BINARY: Opens the file in binary (untranslated) mode. (See fopen for a description of binary mode.)
    3) _O_CREAT: Creates a file and opens it for writing. Has no effect if the file specified by filename exists. The pmode argument is required when _O_CREAT is specified.
    4) _O_CREAT | _O_SHORT_LIVED: Creates a file as temporary and if possible does not flush to disk. The pmode argument is required when _O_CREAT is specified.
    5) _O_CREAT | _O_TEMPORARY: Creates a file as temporary; the file is deleted when the last file descriptor is closed. The pmode argument is required when _O_CREAT is specified.
    6) _O_CREAT | _O_EXCL: Returns an error value if the file specified by filename exists. Applies only when used with _O_CREAT.
    7) _O_NOINHERIT: Prevents creation of a shared file descriptor.
    8) _O_RANDOM: Specifies that caching is optimized for, but not restricted to, random access from disk.
    9) _O_RDONLY: Opens a file for reading only. Cannot be specified with _O_RDWR or _O_WRONLY.
    10) _O_RDWR: Opens file for both reading and writing. Cannot be specified with _O_RDONLY or _O_WRONLY.
    11) _O_SEQUENTIAL: Specifies that caching is optimized for, but not restricted to, sequential access from disk.
    12) _O_TEXT: Opens a file in text (translated) mode. (For more information, see Text and Binary Mode File I/O and fopen.)
    13) _O_TRUNC: Opens a file and truncates it to zero length; the file must have write permission. Cannot be specified with _O_RDONLY. _O_TRUNC used with _O_CREAT opens an existing file or creates a file.
    14) _O_WRONLY: Opens the file for writing only. Cannot be specified with _O_RDONLY or _O_RDWR.
    15) _O_U16TEXT: Opens the file in Unicode UTF-16 mode.
    16) _O_U8TEXT: Opens the file in Unicode UTF-8 mode.
    17) _O_WTEXT: Opens the file in Unicode mode.

3. pmode: Permission mode.
The pmode argument is required only when _O_CREAT is specified. If the file already exists, pmode is ignored, Otherwise, pmode specifies the file permission setting
    1) _S_IREAD: Only reading permitted.
    2) _S_IWRITE: Writing permitted. (In effect, permits reading and writing.)
    3) _S_IREAD | _S_IWRITE: Reading and writing permitted.

程式碼示例

// crt_open.c
// compile with: /W3
/* This program uses _open to open a file
 * named CRT_OPEN.C for input and a file named CRT_OPEN.OUT
 * for output. The files are then closed.
 */
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <io.h>
#include <stdio.h>

int main( void )
{
   int fh1, fh2;

   fh1 = _open( "CRT_OPEN.C", _O_RDONLY ); // C4996
   // Note: _open is deprecated; consider using _sopen_s instead
   if( fh1 == -1 )
      perror( "Open failed on input file" );
   else
   {
      printf( "Open succeeded on input file\n" );
      _close( fh1 );
   }

   fh2 = _open( "CRT_OPEN.OUT", _O_WRONLY | _O_CREAT, _S_IREAD | 
                            _S_IWRITE ); // C4996
   if( fh2 == -1 )
      perror( "Open failed on output file" );
   else
   {
      printf( "Open succeeded on output file\n" );
      _close( fh2 );
   }
}

即當某個檔案被一個程式以寫模式開啟的時候,其他程式無法以讀/寫模式開啟,當某個程式以讀模式開啟的時候,其他程式最多隻能以讀模式開啟,否則作業系統會強制返回控制程式碼錯誤

Relevant Link:

https://msdn.microsoft.com/en-us/library/z0kc8e3z.aspx
https://msdn.microsoft.com/en-us/library/w64k0ytk.aspx

0x3: 開啟檔案: CreateFile Win32 API

在windows下,建立新檔案、開啟檔案都使用同一個Win32 API: createfile

HANDLE CreateFile(
  LPCTSTR lpFileName,
  DWORD dwDesiredAccess,
  DWORD dwShareMode,
  LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  DWORD dwCreationDisposition,
  DWORD dwFlagsAndAttributes,
  HANDLE hTemplateFile
);

Parametes引數列表

1. lpFileName: 要開啟的檔案的名字
2. dwDesiredAccess
    1) GENERIC_READ: 表示允許對裝置進行讀訪問
    2) GENERIC_WRITE: 表示允許對裝置進行寫訪問(可組合使用)
    3) GENERIC_EXECUTE: 只允許執行
    4) 如果為零: 表示只允許獲取與一個裝置有關的資訊
3. dwShareMode
    1) 零: 表示不共享
    2) FILE_SHARE_READ: 表示其他程式允許對這個檔案發起"讀請求"共享訪問
    3) FILE_SHARE_WRITE: 表示其他程式允許對這個檔案發起"寫請求"共享訪問
4. lpSecurityAttributes: SECURITY_ATTRIBUTES
指向一個SECURITY_ATTRIBUTES結構的指標,定義了檔案的安全特性(如果作業系統支援的話)
5. dwCreationDisposition
    1) CREATE_NEW: 建立檔案,如檔案存在則會出錯
    2) CREATE_ALWAYS: 建立檔案,會改寫前一個檔案
    3) OPEN_EXISTING: 檔案必須已經存在。由裝置提出要求
    4) OPEN_ALWAYS: 如檔案不存在則建立它
    5) TRUNCATE_EXISTING: 講現有檔案縮短為零長度
6. dwFlagsAndAttributes
    1) FILE_ATTRIBUTE_ARCHIVE: 標記歸檔屬性
    2) FILE_ATTRIBUTE_COMPRESSED: 將檔案標記為已壓縮,或者標記為檔案在目錄中的預設壓縮方式
    3) FILE_ATTRIBUTE_NORMAL: 預設屬性
    4) FILE_ATTRIBUTE_HIDDEN: 隱藏檔案或目錄
    5) FILE_ATTRIBUTE_READONLY: 檔案為只讀
    6) FILE_ATTRIBUTE_SYSTEM: 檔案為系統檔案
    7) FILE_FLAG_WRITE_THROUGH: 作業系統不得推遲對檔案的寫操作
    8) FILE_FLAG_OVERLAPPED: 允許對檔案進行重疊操作
    9) FILE_FLAG_NO_BUFFERING: 禁止對檔案進行緩衝處理。檔案只能寫入磁碟卷的扇區塊
    10) FILE_FLAG_RANDOM_ACCESS: 針對隨機訪問對檔案緩衝進行優化
    11) FILE_FLAG_SEQUENTIAL_SCAN: 針對連續訪問對檔案緩衝進行優化
    12) FILE_FLAG_DELETE_ON_CLOSE: 關閉了上一次開啟的控制程式碼後,將檔案刪除。特別適合臨時檔案
//也可在 Windows NT 下組合使用下述常數標記:
    13) SECURITY_ANONYMOUS
    14) SECURITY_IDENTIFICATION
    15) SECURITY_IMPERSONATION
    16) SECURITY_DELEGATION
    17) SECURITY_CONTEXT_TRACKING
    18) SECURITY_EFFECTIVE_ONLY
7. hTemplateFile: 如果不為零,則指定一個檔案控制程式碼。新檔案將從這個檔案中複製擴充套件屬性

在createfile的引數中,和檔案獨佔相關較大的是dwShareMode這個引數,如果程式在開啟檔案的時候,通過dwShareMode引數指定了FILE_SHARE_WRITE,相當於加上了x互斥鎖,則其他程式無法再開啟這個檔案,如果通過dwShareMode引數指定了FILE_SHARE_READ,相當於加上了s共享鎖,其他程式最多隻能以讀模式開啟

Relevant Link:

https://msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx
https://msdn.microsoft.com/zh-cn/library/aa914735.aspx
http://www.cppblog.com/yishanhante/articles/19545.html 

0x4: 刪除檔案: DeleteFile()

BOOL WINAPI DeleteFile(
  _In_ LPCTSTR lpFileName
);

Return value

1. If the function succeeds, the return value is nonzero.
2. If an application attempts to delete a file that does not exist, the DeleteFile function fails with ERROR_FILE_NOT_FOUND. 
3. If the file is a read-only file, the function fails with ERROR_ACCESS_DENIED.

The following list identifies some tips for deleting, removing, or closing files:

1. To delete a read-only file, first you must remove the read-only attribute.
2. To delete or rename a file, you must have either delete permission on the file, or delete child permission in the parent directory.
3. To recursively delete the files in a directory, use the SHFileOperation function.
4. To remove an empty directory, use the RemoveDirectory function.
5. To close an open file, use the CloseHandle function.
6. If you set up a directory with all access except delete and delete child, and the access control lists (ACL) of new files are inherited, then you can create a file without being able to delete it. However, then you can create a file, and then get all the access you request on the handle that is returned to you at the time you create the file.
7. If you request delete permission at the time you create a file, you can delete or rename the file with that handle, but not with any other handle. For more information, see File Security and Access Rights.
8. The DeleteFile function fails if an application attempts to delete a file that has other handles open for normal I/O or as a memory-mapped file (FILE_SHARE_DELETE must have been specified when other handles were opened).
9. The DeleteFile function marks a file for deletion on close. Therefore, the file deletion does not occur until the last handle to the file is closed. Subsequent calls to CreateFile to open the file fail with ERROR_ACCESS_DENIED.

windows作業系統有嚴格的handler控制程式碼的概念,並且作業系統對進行對檔案控制程式碼的獨佔保護提供的底層支援

Relevant Link:

https://msdn.microsoft.com/zh-cn/library/windows/desktop/aa363915(v=vs.85).aspx

 

Copyright (c) 2015 Little5ann All rights reserved

 

相關文章