Windows系统:解析文件句柄Handle的详细机制

1. 用户程序和运行库层面的文件”句柄” FILE*

IO初始化时讨论了在运行库层面的文件句柄管理机制,工作流程如下FILE *file ->ininfo* pioinfo\[\]\[\] -> ioinfo

  • 1.在程序中提供给IO函数的文件句柄是封装在FILE*指针里面的;
  • 2.FILE结构体总共包括两方面内容:a.该文件配备的缓冲大小、地址等信息;b.该文件在进程打开文件列表中的下标_file;
   typedef struct _iobuf{
    char *_ptr;//指向缓冲的第一个未使用的字节
    int   _cnt;//记录文件缓冲区的剩余未读字节数,一般为了提高效率,减少I/O次数,会
        //为文件对象配备提前的文件缓存,这些缓存都是提前存储了数据对象的
    char *_base;//指向一个字符数组,即这个文件对应的缓冲
    int   _flag;//记录FILE结构所代表的打开文件的一些属性
    /*
        这一位的标志总共有3个标志
        #define  _IOYOURBUF 0x0100 //代表是用户通过setbuf手动为该FILE绑定的buffer
        #define  _IOMYBUF   0x0008 //代表这个文件使用内部的缓冲
        #define  _IONBF     0x0004 //代表文件使用一个单字节的缓冲,在这种情况下,
            将不启用base指向的字符数组,而直接用下面_charbuf作为单字节
    */
    int   _file; //指向打开文件列表pioinfo二维数组中的下标
    int   _charbuf;
    int   _bufsiz;//记录着这个文件缓存的大小
    char *_tmpfname;
}FILE;
  • 3.找到打开文件列表中指向目标ioinfo的指针项

typedef struct{
    intptr_t osfhnd;//打开文件的句柄,使用8字节整数类型intptr_t来存储,很有可能是指向文件的索引节点FCB这类的
    char osfile;//文件的打开属性
    /*osfile的值可以通过一系列值按位或的方式得出:
        FOPEN(0x01)句柄被打开
        FEOFLAG(0x02)已到达文件末尾
        FCRLF(0x04)在文本模式中,行缓冲已遇到回车符
        FPIPE(0x08)管道文件
        FNOINHERIT(0x10)句柄打开时具有属性_O_NOINHERIT(不遗传给子进程)
        FAPPEND(0x20)句柄打开时具有属性O_APPEND(在文件末尾追加数据)
        FDEV(0x40)设备文件
        FTEXT(0x80)文件以文本模式打开
    */
    char pipch;//用于管道的单字符缓冲,这里先忽略
}ioinfo;//MSVC CRT中,已经打开的文件句柄信息使用数据结构ioinfo来表示
  • 4.根据ioinfo结构体中句柄值来交给内核来进行相应的文件搜索和操作。

到运行库层面可以看到关于文件句柄详细内容依旧被隐藏着,所以需要进一步深入内核去了解文件句柄到底如何运行的。

2. 内核层面的HANDLE实现

1. HANDLE指针的文件寻址跳转过程
HANDLE句柄是归属于进程的。当一个进程创建时,系统会在进程的内存空间中生成一个表,用于记录各个对象(窗口、位图、打开的文件)等在内存中的地址,这个表在虚拟内存空间的位置是固定的,不妨称这个指向各个对象的指针数组叫做对象地址表。

Fig.1 ”句柄“寻址跳转示意图

这个直接指向各对象的指针数组显然不能直接暴露给用户层,因为这块区域是可写的,无论是别有用心或者无意,都有可能改变各对象指针的指向位置,导致执行别的区域代码或者出错。

所以我的猜想是:要在这个各对象的指针数组基础上再封装一层,一块专门用来指向各对象指针的指针数组,但是这块区域将被设置为只读(软件+硬件保护:并且只有系统提供的API可以有权限读取和修改这些“指向指针的指针”的内容)。每一个“指向对象指针的指针”便是对象的HANDLE,相当于各对象在进程中ID,全局唯一且固定。考虑到句柄的数量应该是有上限的,故而显然句柄应该可以被重定位的。所以设计成上图中的方式是合理的。底层的句柄表相当于对象ID索引表,中间的对象地址表应该是被阉割后的结构体指针数组。虽然这个猜想有可能不对,但我觉得这一想法是个不错的安全机制,可以用在别处。

2. HANDLE指针的定义

//Winnt.h中关于HANDLE句柄的定义
typedef void* PVOID;//将PVOID定义为无类型指针

#ifdef STRICT
    typedef void* HANDLE; 
    #define DECLARE_HANDLE(name) struct name##_{ int unused;}; typedef struct name##_ * name 
//定义一个指针name,指向结构体name##_
/*##粘贴符号,表示将左右两边的内容连接起来,
        #define STR(s) #s //使用#把一个宏参数变成一个字符串,printf(STR(macroPara))将打印出“macroPara”
        #define Conjugate(a, b) int(a##b) //使用##将两个宏参数结合在一起,printf(Conjugate(2,3))则将打印出“23”
*/

#else
    typedef  PVOID HANDLE;//HANDLE也被定义为无类型指针

    #define DECLARE_HANDLE(name) typedef HANDLE name  //将HANDLE无类型指针变成name指针,至此                 //HANDLE正式成为我们常说的句柄,即通用句柄
#endif
typedef HANDLE* PHANDLE

在Winnt.h中通过

typedef struct name_{int unused;}* name;

来给出了句柄指向的结构体形式,这里进行了一步封装,并且明面上这个结构体只有一个整数unused,看到这里总觉得有点奇怪,按理来说句柄不仅仅是文件的唯一ID这么简单,在操作系统的文件,显然还有该文件是否可共享,当前正常使用的进程数目,还应该指出该文件对应的内存是否可调整等属性,因此,有可能完整的Windows句柄指向的结构体应该如下

struct {
    int pointer; //指向文件的内存地址;
    int count;   //内核计数段;
    int attribute; //文件属性段:SHARED等
    int memAttribute; //内存属性段:MOVEBLE和FIXED等
    ...
};

然后通过强制性的结构指针转换,将暴露给用户的句柄人为地截断到第一个字段,而把文件其他信息给隐藏起来。这样一种解释可以比较合理地解释上图的句柄寻址的三层跳转,否则,如果真的是中间结构体只有一个整数unused,那么即便我上面提出的抽象层保护是一种解释,但其实我依旧觉得蛮多余的。

3. 句柄类型的STRICT和非STRICT模式

/*windef.h定义的句柄HWND便是通过宏
  DECLARE_HANDLE(HWND);
  根据上面宏定义的可知typedef struct name##_ * name知道最后HWND是指向HWND_结构体的指针
*/

#ifndef NO_STRICT
#ifndef STRICT
#define STRICT_1
#endif
#endif

Windows官网对启用STRICT机制说明:Enabling SRICT redefines certain data types so that the compiler does not permit assignment from one type to another withoout an explicit cast。

即启用STRICT后,Windows应用程序在使用句柄的过程中将进行严格的句柄类型检查,即下面的HICON和HINSTANCE句柄类型不通用;若是未启用STRICT,则根据上面Winnt.h中的逻辑可以看到,所有的句柄是不分种类的,全部被定义成HANDLE即void*无类型指针类型,则要求HICON的地方传递一个HINSTANCE类型句柄也是可以通过参数类型检查的。


#if !defined(_MAC) || !defined(GDI_INTERNAL)
DECLARE_HANDLE(HFONT);
#endif
DECLARE_HANDLE (HICON);

#if !defined(_MAC) || !defined(GDI_INTERNAL)
DECLARE_HANDLE(HMENU);
#endif
DECLARE_HANDLE (HMETAFILE);
DECLARE_HANDLE (HINSTANCE);
typedef HINSTANCE HMODULE;

#if !defined(_MAC) || !defined(GDI_INTERNAL)
DECLARE_HANDLE(HPALETTE);
DECLARE_HANDLE(HPEN);
#endif
DECLARE_HANDLE (HRGN);
DECLARE_HANDLE (HRSRC);
DECLARE_HANDLE (HSTR);
DECLARE_HANDLE (HTASK);
DECLARE_HANDLE (HWINSTA);
DECLARE_HANDLE (HKL);

上面可以看到,虽然在软件层面很多对象都可以被泛化为”文件“,但操作系统内核依旧为不同的文件对象定义了不同的句柄类型,如果操作系统定义了STRICT严格模式,则句柄类型是需要严格区分的。

4. 从GlobalAlloc()内存分配函数的参数GMEM_FIXED和GMEM_MOVABLE

众所周知,操作系统采用段页映射来进行物理内存管理,以减少实际物理内存碎片;进程空间抽象出的虚拟空间中的堆也需要相应的堆算法来管理虚拟堆空间碎片。管理通常意味着数据迁移以达到碎片整理,这里的参数GMEM_MOVABLE是说允许操作系统实施堆管理,在需要的时候,操作系统可以移动通过对堆里数据进行迁移整合,合并空闲的虚拟页,以达到垃圾回收;GMEM_FIXED参数是说允许数据在物理内存中移动数据存储的内存块,但是要保证数据所在的逻辑地址是不变的。

我们知道,操作系统在虚拟内存和物理内存间是通过段页表来达到一一映射的,如果更改了数据在物理内存中的位置,只需要修改段页表的对应关系即可;但是如果修改了数据在虚拟空间的逻辑地址,则怎么办?这便是句柄的作用,只需要修改句柄表中的指针和中间层的对象地址表的指针即可。

当GlobalAlloc指定参数GMEM_MOVABLE,显然这时对进程虚拟空间的堆在进行整合,故而显然需要对相应文件的逻辑管理结构体显然加锁信息,即我们猜想的真实的中间层结构体的字段memAttribute应该加1,标示该文件对象的逻辑块正在被移动。
int memAttribute; //内存属性段:MOVEBLE和FIXED等
GMEM_MOVEABLE是允许操作系统(或者应用程序)实施对内存堆的管理,在必要时,操作系统可以移动内存块获取更大的块,或者合并一些空闲的内存块,也称“垃圾回收”,它可以提高内存的利用率。一般情况下,内存堆空间是由用户来管理的,Windows操作系统不干预。如果存在下列情况,即堆中有10个1K的空闲块,这时如果直接申请一个5K的内存空间,会得到不成功的信息。但如果其它已经被占用的内存块是movable,这时系统就可以移动这些内存块,合并出一个5k的内存块,并成功分配给用户使用。它的空间效率是以运行时的时间效率为代价的。

HGLOBAL WINAPI GlobalAlloc(
  _In_ UINT   uFlags,
  _In_ SIZE_T dwBytes
);

可以使用GlobalAlloca()函数测试一下两种参数的效果

#include <windows.h>
#include <stdio.h>
#include <tchar.h>

void main()
{
    /*
    PSECURITY_DESCRIPTOR pSD;

    pSD = (PSECURITY_DESCRIPTOR) GlobalAlloc(
           GMEM_FIXED,
           sizeof(PSECURITY_DESCRIPTOR));

    if (pSD == NULL) {
        _tprintf(TEXT("GlobalAlloc 分配内存失败 (%d)\n"), GetLastError());
        return;
    }
    */

    TCHAR szBuffer[] = _T("one Piece");
    _tprintf(TEXT("使用句柄当做指针访问的数据为:%s\n"), szBuffer); 

    PSECURITY_DESCRIPTOR hSD;
    PSECURITY_DESCRIPTOR pGlobal; 

    //***************GMEM_FIXED固定逻辑地址*************//
    _tprintf(TEXT("\n**************使用GMEM_FIXED固定逻辑地址***********\n"));

    hSD = (PSECURITY_DESCRIPTOR) GlobalAlloc(GMEM_FIXED, (lstrlen(szBuffer)+1) * sizeof(TCHAR)); 
    pGlobal =  (PSECURITY_DESCRIPTOR) GlobalLock(hSD);
    lstrcpy((TCHAR*)pGlobal, szBuffer);  
    _tprintf(TEXT("pGlobal和hSD%s\n"), pGlobal==hSD ? TEXT("相等") : TEXT("不相等"));  
    GlobalUnlock(hSD);  
    _tprintf(TEXT("使用句柄当做指针访问的数据为:%s\n"), hSD);  

    GlobalFree(hSD); 

    //***************GMEM_MOVEABLE可移动虚拟地址*************//

    _tprintf(TEXT("\n**************使用GMEM_MOVEABLE可移动逻辑地址***********\n"));  
    hSD = (PSECURITY_DESCRIPTOR) GlobalAlloc(GMEM_MOVEABLE, (lstrlen(szBuffer)+1) * sizeof(TCHAR)); 
    pGlobal =  (PSECURITY_DESCRIPTOR) GlobalLock(hSD);
    lstrcpy((TCHAR*)pGlobal, szBuffer);  
    _tprintf(TEXT("pGlobal和hSD%s\n"), pGlobal==hSD ? TEXT("相等") : TEXT("不相等"));  
    GlobalUnlock(hSD);  
    _tprintf(TEXT("使用句柄当做指针访问的数据为:%s\n"), hSD);  
    GlobalFree(hSD); 
}


Fig.2程序运行结果

从上文可以看到HANDLE这一常见的句柄类型,实际被定义为void *,即无类型指针,而在定义了STRICT模式下的,各文件对象又分门别类地拥有自己的具体句柄类型,如HICON(实际是指向具体结构体的指针)。根据上面程序的运行结果,我们可以判断Windows在使用GMEM_FIXED的时候返回的应该是直接指向对象的数据指针(没有经过中间结构体跳转),所以才能锁住对象在逻辑空间中的存放位置,这时因为对象类型未知,所以应该使用HANDLE通用句柄;使用GMEM_MOVEABLE的时候应该是返回了中间结构体的指针,这是就是指向具体的结构体指针,所以看到即便加了锁,锁住的也只是中间结构体还不是具体的文件对象。这也是为什么在各种文件对象都专门定义了结构体指针类型的同时,通用句柄HANDLE依旧需要存在的原因。

至此,关于Windows下的句柄Handle的机制详细介绍结束。

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Windows 平台下,可以使用以下步骤存放结构体数据到共享内存中: 1. 定义需要存储的结构体数据类型。 2. 创建共享内存对象,可以使用 Windows API 函数 CreateFileMapping()。 3. 映射共享内存到当前进程的地址空间中,可以使用 Windows API 函数 MapViewOfFile()。 4. 在共享内存中存储结构体数据,可以使用 memcpy() 函数将结构体数据复制到共享内存的地址空间中。 5. 当需要访问共享内存中的结构体数据时,可以使用相应的指针类型进行访问。 下面是一个简单的示例代码: ```c++ #include <Windows.h> #include <iostream> // 定义需要存储的结构体数据类型 struct MyData { int i; double d; char str[20]; }; int main() { // 创建共享内存对象 HANDLE hMapFile = CreateFileMapping( INVALID_HANDLE_VALUE, // 使用无效的句柄创建 NULL, // 默认安全特性 PAGE_READWRITE, // 共享内存的读写权限 0, // 大小为0表示文件映射到整个文件 sizeof(MyData), // 共享内存的大小 L"MySharedMemory"); // 共享内存对象的名称 if (hMapFile == NULL) { std::cerr << "CreateFileMapping failed: " << GetLastError() << std::endl; return 1; } // 映射共享内存到当前进程的地址空间中 LPVOID lpMapAddress = MapViewOfFile( hMapFile, // 共享内存的句柄 FILE_MAP_ALL_ACCESS, // 共享内存的访问权限 0, // 偏移量为0表示从文件的开头开始映射 0, // 映射整个文件 sizeof(MyData)); // 映射的大小 if (lpMapAddress == NULL) { std::cerr << "MapViewOfFile failed: " << GetLastError() << std::endl; CloseHandle(hMapFile); return 1; } // 在共享内存中存储结构体数据 MyData data = { 123, 3.14, "Hello shared memory!" }; memcpy(lpMapAddress, &data, sizeof(MyData)); // 访问共享内存中的结构体数据 MyData* pData = reinterpret_cast<MyData*>(lpMapAddress); std::cout << "i = " << pData->i << std::endl; std::cout << "d = " << pData->d << std::endl; std::cout << "str = " << pData->str << std::endl; // 解除映射并关闭共享内存对象 UnmapViewOfFile(lpMapAddress); CloseHandle(hMapFile); return 0; } ``` 需要注意的是,由于共享内存是多个进程共享的,因此在访问共享内存时需要进行同步操作,以避免数据的竞争和冲突。例如,可以使用互斥量或信号量等同步机制来保护共享内存的数据。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值