VC++的文件描述符和內核文件句柄HANDLE

VC++的文件描述符和內核文件句柄HANDLE

本文描述VC++中的C語言使用代碼文件描述符(file descriptor),和內核文件句柄HANDLE之間關係,以及兩者之間的轉換函數_get_osfhandle,_open_osfhandle以及使用他們的風險。在Windows代碼中代碼中間文件描述符號和內核句柄HANDLE千萬不要共用。

在文章的開頭,要聲明這是我寫的bug,但是是被兩個小夥子derickhu,Sasukeliu發現了在此再次對它們表示感謝。

去年在寫一些跨平臺代碼的時候,我們在Windows下儘量模擬一下POSIX(Linux),的函數,結果發現一個問題,如果希望使用到Windows下使用更多的功能,你必須使用內核的文件句柄HANDLE,而不能VC++中C語言函數open等使用的文件描述符(file descriptor),也就是int。到爲了方便跨平臺,我儘量希望接口是統一的,我們實現的如下:

//我們的垮平臺代碼
ZEN_HANDLE ZEN_OS::open (const char *filename,
                         int open_mode,
                         mode_t perms)

ZEN_HANDLE在多個WINDOWS平臺下被定義爲Windows的內核句柄HANDLE,而在LINUX下被定義爲LINUX的內核句柄int。

而VC++默認的確實現了一個符合POSIX標準的open函數,返回值也是標準的文件描述符int

//WINDOWS下實現的open函數
int _open(
   const char *filename,
   int oflag [,
   int pmode] 
);

那麼如果有一個方法能從C語言的文件描述符轉換到Windows內核句柄,那麼我就可以直接利用_open這個函數了。Google發現了這兩個函數_get_osfhandle,_open_osfhandle。當時的理解是_get_osfhandle將文件描述符號轉換成HANDLE,_open_osfhandle將HANDLE轉換文件描述符。


//MSDN
Retrieves the operating-system file handle that is associated with the specified file descriptor.
intptr_t _get_osfhandle( 
   int fd );

Associates a C run-time file descriptor with an existing operating-system file handle.
int _open_osfhandle (
   intptr_t osfhandle,
   int flags 
);

 

於是我的代碼寫成了。


//在open的時候,調用_get_osfhandle得到內核HANDLE,
//省略了很多其他代碼
//open函數用_sopen_s打開文件後,用_get_osfhandle轉換爲句柄提供給其他地方使用
ZEN_HANDLE ZEN_OS::open (const char *filename,
                         int open_mode,
                         mode_t perms)
{
    int file_id = -1;
    errno_t open_error =::_sopen_s (&file_id,
                                    filename,
                                    open_mode,
                                    _SH_DENYNO,
                                    nt_perms);
    HANDLE openfile_handle =  (HANDLE)::_get_osfhandle(file_id);
    return openfile_handle;
}

//關閉的時候,用_open_osfhandle將HANDLE轉換爲文件描述符,再用_close關閉
int ZEN_OS::close (ZEN_HANDLE handle)
{
    int file_des = ::_open_osfhandle((intptr_t)handle, _O_RDONLY);
    if (-1 == file_des )
    {
        return -1;
    }
    return ::_close(file_des);
}

 

代碼最開始的實現是利用open函數(我們的代碼定義了_CRT_NONSTDC_NO_DEPRECATE,不用在POSIX函數前面加_),一開始的測試也基本OK。但後來發現在部分同事的機器::close函數會出現斷言錯誤,當時有點莫名奇妙,通過將代碼open函數換成了sopen_s,close函數替換_close暫時規避了問題(注意_close函數和close函數的確是兩個實現)。

最近重構上線,一個服務程序會不斷open,close文件的,在運行3天后會就會崩潰,錯誤是文件描述符號不夠用。兩位同事將上面的代碼改寫提取出來,發現了問題。他們的測試代碼大致如下:


int test_osadapt_file(int  /*argc*/,char * /*argv*/[])
{
    int file_desc = open("C:\\123.txt",O_CREAT|O_APPEND);
    if (file_desc == 0)
    {
        return 0;
    }
    //fh_1 == fh_2 內核句柄一致
    HANDLE fh_1 = (HANDLE)_get_osfhandle(file_desc);
    HANDLE fh_2 = (HANDLE)_get_osfhandle(file_desc);
    

    std::cout << (int) fh_1 << std::endl;
    std::cout << (int) fh_2 << std::endl;

    //file_desc==3  filedesc_1==4 filedesc_2==5,3個文件描述符不一樣
    int filedesc_1 = _open_osfhandle((intptr_t)fh_1,O_RDONLY);
    int filedesc_2 = _open_osfhandle((intptr_t)fh_1,O_RDONLY);

    std::cout << (int) filedesc_1 << std::endl;
    std::cout << (int) filedesc_2 << std::endl;

    //fh_1 == fh_2 == fh_3,內核句柄一致
    HANDLE fh_3 = (HANDLE)_get_osfhandle(filedesc_1);
    std::cout << (int) fh_3 << std::endl;

    return 0;
}

 

輸出的結果在上面的註釋都有,結果發現每次調用_open_osfhandle得到的文件描述符號並不是原來最初的文件描述符。

看來完全錯誤理解_open_osfhandle函數的意義。_open_osfhandle根本就不是關聯原來的文件描述符,而是對於HANDLE重新分配一個相關的C語言描述符,並不是找回這個HANDLE對應的(MSDN這個地方的描述居然用了Associates)。

image

 

從下面這個圖可以很清楚的說明爲什麼會有C函數的文件描述符泄漏。(這很可能也是原來導致斷言的原因)

當然仔細看完MSDN,可以看出MSDN _open_osfhandle函數的remark還是有相關的蛛絲馬跡的(E文爛實在影響編程能力)。

The _open_osfhandle function allocates a C run-time file descriptor and associates it with the operating-system file handle specified by osfhandle.……
To close a file opened with _open_osfhandle, call _close. The underlying handle is also closed by a call to _close, so it is not necessary to call the Win32 function CloseHandle on the original handle. 

其實轉念想想,寫這段代碼的時候過於大意了,Windows內部肯定是使用自己的API,內核自己內部的句柄管理可定是HANDLE,C語言運行時庫的函數封裝肯定只是上層的一套封裝。所以C函數庫內部保存的映射關係,肯定只是是文件描述符到內核句柄的,不可能存在內核句柄到文件描述符的映射,所以不要指望通過內核句柄得到相關文件描述符。

混用C運行時庫和API不是一個好方法,其實如果沒有記錯,C語言封裝線程的函數(_beginthread,_endthread)和API(CreateThread,CloseHandle)兩者之間也有類似的問題。

總結:

_open_osfhandle和_get_osfhandle都是要慎重使用的函數,_open_osfhandle是根據HANDLE分配一個文件描述符,這種情況必須用_close關閉文件描述符,_get_osfhandle只是根據文件描述符得到將對應文件句柄HANDLE,你仍然要管理原來的文件描述符。這2個函數試圖連接起來C運行時庫文件描述符和內核文件句柄HANDLE,但其實會導致很多陷阱,不如不用。

最好的方案還是不要混用文件描述符和內核句柄HANDLE,該用HANDLE的時候,老老實實使用Windows的API。當年寫這段代碼的時候看過ACE的實現,發現他的內部實現是老老實實用API,::CreateFile(用::CreateFile實現類似open的接口要寫好多代碼),我還無比嘲笑,結果發現土鱉的還是自己。

推薦音樂:黑撒的《西安事變》和《流川楓和蒼井空》,derickhu,Sasukeliu他們都是西安畢業的。送給他們。

【本文作者是雁渡寒潭,本着自由的精神,你可以在無盈利的情況完整轉載此文檔,轉載時請附上BLOG鏈接:http://www.cnblogs.com/fullsail/ 或者http://blog.csdn.net/fullsail,否則每字一元,每圖一百不講價。對Baidu文庫。360doc加價一倍】


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