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加价一倍】

原文地址:https://www.cnblogs.com/fullsail/p/2732873.html