精华内容
下载资源
问答
  • 一般PG DBA会规划一个存索引表空间、存归档表空间,一个很频繁使用索引可以被放在非常快并且非常可靠的磁盘上,一个很少使用或者对性能要求不高存储归档数据表可以存储一个便宜但比较慢的磁盘系统上。...

    概述

    PostgreSQL中的表空间实际上就是文件系统位置的一个目录,可以保存所有其他对象的容器,如表、索引等,有默认表空间、共享系统表表空间、自定义表空间。

    一般PG DBA会规划一个存索引的表空间、存归档的表空间,一个很频繁使用的索引可以被放在非常快并且非常可靠的磁盘上,一个很少使用的或者对性能要求不高的存储归档数据的表可以存储在一个便宜但比较慢的磁盘系统上。

    DBA规划表空间就是合理利用磁盘性能和空间,制定最优的物理存储方式来管理数据库表和索引。

    fa8f5f3a2bc59ef1a75bfb275b1ddd21.png

    一、Oracle VS PG 数据库的物理布局设计

    在Oracle数据库中;一个表空间只属于一个数据库使用;而一个数据库可以拥有多个表空间。属于"一对多"的关系

    在PostgreSQL集群中;每个数据库都会在$PGDATA/base下面生成一个子目录,文件名与数据库oid一一对应,一个表空间可以让多个数据库使用;数据库目录可以分布于多个表空间,也就是同一个数据库oid可以存在于不同表空间。属于"多对多"的关系。


    二、Oracle VS PG系统自带表空间

    1、Oracle自带系统表空间

    1)system表空间

    每个Oracle数据库都会有SYSTEM表空间,而且SYSTEM表空间总是要保持在联机模式下,因为其包含了数据库运行所要求的基本信息,如:数据字典、联机求助机制、所有回退段、临时段、所有的用户数据库实体、其它ORACLE软件产品要求的表等等。

    2)SYSAUX表空间

    作为SYSTEM表空间的辅助表空间,主要存放一些其他的 metadata 组件,如 OEM,Streams 等会默认存放在 SYSAUX 表空间里。

    3)临时表空间

    临时表空间是用来管理数据库排序操作以及用于存储临时表、中间排序结果等临时对象,当ORACLE里需要用到SORT的时候,并且当PGA中sort_area_size大小不够时,将会把数据放入临时表空间里进行排序。像数据库中一些操作: CREATE INDEX、 ANALYZE、SELECT DISTINCT、ORDER BY、GROUP BY、 UNION ALL、 INTERSECT、MINUS、SORT-MERGE JOINS、HASH JOIN等都可能会用到临时表空间。当操作完成后,系统会自动清理临时表空间中的临时对象,自动释放临时段。

    4)UNDO表空间

    undo表空间中会自动分配undo段,这些undo段用来保存事务中的DML语句的undo信息,也就是来保存数据在被修改之前的值。在rollback,实例恢复(前滚),一致性读CR块的构造时会使用到undo信息。由于undo的引入,从而Oracle的select语句实现一致性读时,不需要任何锁。

    5)USERS表空间

    创建用户必须为其指定表空间,如果没有显性指定默认表空间,则指定为users表空间,此用户所有信息都会放入到users表空间中。

    2、PG自带系统表空间

    注意PG默认没有temp表空间(或者说临时段默认是存放在pg_default表空间)和undo表空间。

    1)pg_default表空间

    表空间pg_default是用来存储系统目录对象、用户表、用户表index、和临时表、临时表index、内部临时表的默认空间。对应存储目录$PADATA/base/

    (1)目录名“1”是系统数据库template1各relation的存储目录

    (2)目录数字“倒数第二”是系统数据库template0各relation的存储目录

    (3)其他数字目录是用户建立的数据库目录

    (4)目录的命名方法就是数据库的oid值

    (5)FSM的文件记录每个数据块的空闲空间

    (6)VM文件让vacuum高效工作,被标为无效的记录需要用vacuum来进行清理,需要一个文件来标识哪些数据块中存在无效记录来达到高效的清理。

    2)pg_global表空间

    表空间pg_global用来存放系统字典表;对应存储目录$PADATA/global/


    三、pg表空间物理布局图

    a02e7145ab3ffe8b87c1478be4e3300d.png

    四、PG表空间管理相关命令

    1、查看表空间

    dbselect * from pg_tablespace;
    6dc0ffc11d79a51924fc52d64e739636.png

    2、创建表空间

    语法:CREATE TABLESPACE tablespace_name [ OWNER { new_owner | CURRENT_USER | SESSION_USER } ] LOCATION 'directory'

    注意目录"directory"必须是一个已有的空目录,并且属于PostgreSQL操作系统用户

    这里把建用户、表空间、建库一起做一个实例介绍了。

    su - postgrespsql--建用户CREATE USER dbmt WITH PASSWORD 'jkc800$uTxt$9K20';--创建表空间CREATE TABLESPACE dbmt OWNER dbmt LOCATION '/data/pgdata/dbmt';--建库CREATE DATABASE dbmt with OWNER=dbmt ENCODING='UTF-8' TABLESPACE=dbmt CONNECTION LIMIT=-1;--将dbmt数据库的所有权限都赋予dbmtGRANT ALL PRIVILEGES ON DATABASE dbmt TO dbmt;q
    413a362a861936395bc09836753ea2f3.png

    3、删除表空间

     drop tablespace tablespace_name;

    4、为数据库指定默认表空间

    注意:

    1)执行该操作不能连着对应数据库操作

    2)对应的数据库不能存在表或者索引已经指定默认的表空间

    3)执行该操作必须是没有人连着对应的数据库

     ALTER DATABASE name SET TABLESPACE new_tablespace;

    5、将表从一个表空间移到另一个表空间

    注意:在移动表的时候会锁表,此时对该表的所有操作都将被阻塞,包括select操作,所以请考虑在合适的实际做这个操作。

     ALTER TABLE name SET TABLESPACE new_tablespace;

    6、临时表空间管理

    PG的临时表空间用来存储临时表或临时表的索引,以及执行SQL时可能产生的临时文件例如排序,聚合,哈希等。为了提高性能,一般建议将临时表空间放在SSD或者IOPS,以及吞吐量较高的分区中。

    PG临时表空间主要是通过参数temp_tablespaces进行配置,PostgreSQL允许用户配置多个临时表空间。配置多个临时表空间时,使用逗号隔开。如果没有配置temp_tablespaces 参数,临时表空间对应的是默认的表空间pg_default。

    6.1、创建临时表空间

    CREATE TABLESPACE temp01 LOCATION '/data/pgdata/temp';--查看临时表空间show temp_tablespaces;

    6.2、设置临时表空间

    --会话级生效set temp_tablespaces = 'temp01';--永久生效1)修改参数文件postgresql.conf2)执行pg_ctl reload

    觉得有用的朋友多帮忙转发哦!后面会分享更多devops和DBA方面的内容,感兴趣的朋友可以关注下~

    65d2e4d139fb251e21f474416fc20256.gif
    展开全文
  • 目前,对于上述这种大文件的操作一般是以内存映射文件的方式来加以处理的,本文下面将针对这种Windows核心编程技术展开讨论。内存映射文件 内存映射文件与虚拟内存有些类似,通过内存映射文件可以保留一个地址空间...

    殊应用领域所需要的动辄几十GB、几百GB、乃至几TB的海量存储,再以通常的文件处理方法进行处理显然是行不通的。目前,对于上述这种大文件的操作一般是以内存映射文件的方式来加以处理的,本文下面将针对这种Windows核心编程技术展开讨论。

      内存映射文件

      内存映射文件与虚拟内存有些类似,通过内存映射文件可以保留一个地址空间的区域,同时将物理存储器提交给此区域,只是内存文件映射的物理存储器来自一个已经存在于磁盘上的文件,而非系统的页文件,而且在对该文件进行操作之前必须首先对文件进行映射,就如同将整个文件从磁盘加载到内存。由此可以看出,使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,这意味着在对文件进行处理时将不必再为文件申请并分配缓存,所有的文件缓存操作均由系统直接管理,由于取消了将文件数据加载到内存、数据从内存到文件的回写以及释放内存块等步骤,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。另外,实际工程中的系统往往需要在多个进程之间共享数据,如果数据量小,处理方法是灵活多变的,如果共享数据容量巨大,那么就需要借助于内存映射文件来进行。实际上,内存映射文件正是解决本地多个进程间数据共享的最有效方法。

      内存映射文件并不是简单的文件I/O操作,实际用到了Windows的核心编程技术--内存管理。所以,如果想对内存映射文件有更深刻的认识,必须对Windows操作系统的内存管理机制有清楚的认识,内存管理的相关知识非常复杂,超出了本文的讨论范畴,在此就不再赘述,感兴趣的读者可以参阅其他相关书籍。下面给出使用内存映射文件的一般方法:

      首先要通过CreateFile()函数来创建或打开一个文件内核对象,这个对象标识了磁盘上将要用作内存映射文件的文件。在用CreateFile()将文件映像在物理存储器的位置通告给操作系统后,只指定了映像文件的路径,映像的长度还没有指定。为了指定文件映射对象需要多大的物理存储空间还需要通过CreateFileMapping()函数来创建一个文件映射内核对象以告诉系统文件的尺寸以及访问文件的方式。在创建了文件映射对象后,还必须为文件数据保留一个地址空间区域,并把文件数据作为映射到该区域的物理存储器进行提交。由MapViewOfFile()函数负责通过系统的管理而将文件映射对象的全部或部分映射到进程地址空间。此时,对内存映射文件的使用和处理同通常加载到内存中的文件数据的处理方式基本一样,在完成了对内存映射文件的使用时,还要通过一系列的操作完成对其的清除和使用过资源的释放。这部分相对比较简单,可以通过UnmapViewOfFile()完成从进程的地址空间撤消文件数据的映像、通过CloseHandle()关闭前面创建的文件映射对象和文件对象。

      内存映射文件相关函数

      在使用内存映射文件时,所使用的API函数主要就是前面提到过的那几个函数,下面分别对其进行介绍:

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


      函数CreateFile()即使是在普通的文件操作时也经常用来创建、打开文件,在处理内存映射文件时,该函数来创建/打开一个文件内核对象,并将其句柄返回,在调用该函数时需要根据是否需要数据读写和文件的共享方式来设置参数dwDesiredAccessdwShareMode,错误的参数设置将会导致相应操作时的失败。

    HANDLE CreateFileMapping(HANDLE hFile,
    LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
    DWORD flProtect,
    DWORD dwMaximumSizeHigh,
    DWORD dwMaximumSizeLow,
    LPCTSTR lpName);


      CreateFileMapping()函数创建一个文件映射内核对象,通过参数hFile指定待映射到进程地址空间的文件句柄(该句柄由CreateFile()函数的返回值获取)。由于内存映射文件的物理存储器实际是存储于磁盘上的一个文件,而不是从系统的页文件中分配的内存,所以系统不会主动为其保留地址空间区域,也不会自动将文件的存储空间映射到该区域,为了让系统能够确定对页面采取何种保护属性,需要通过参数flProtect来设定,保护属性PAGE_READONLYPAGE_READWRITEPAGE_WRITECOPY分别表示文件映射对象被映射后,可以读取、读写文件数据。在使用PAGE_READONLY时,必须确保CreateFile()采用的是GENERIC_READ参数;PAGE_READWRITE则要求CreateFile()采用的是GENERIC_READ|GENERIC_WRITE参数;至于属性PAGE_WRITECOPY则只需要确保CreateFile()采用了GENERIC_READGENERIC_WRITE其中之一即可。DWORD型的参数dwMaximumSizeHighdwMaximumSizeLow也是相当重要的,指定了文件的最大字节数,由于这两个参数共64位,因此所支持的最大文件长度为16EB,几乎可以满足任何大数据量文件处理场合的要求。

    LPVOID MapViewOfFile(HANDLE hFileMappingObject,
    DWORD dwDesiredAccess,
    DWORD dwFileOffsetHigh,
    DWORD dwFileOffsetLow,
    DWORD dwNumberOfBytesToMap);


      MapViewOfFile()函数负责把文件数据映射到进程的地址空间,参数hFileMappingObjectCreateFileMapping()返回的文件映像对象句柄。参数dwDesiredAccess则再次指定了对文件数据的访问方式,而且同样要与CreateFileMapping()函数所设置的保护属性相匹配。虽然这里一再对保护属性进行重复设置看似多余,但却可以使应用程序能更多的对数据的保护属性实行有效控制。MapViewOfFile()函数允许全部或部分映射文件,在映射时,需要指定数据文件的偏移地址以及待映射的长度。其中,文件的偏移地址由DWORD型的参数dwFileOffsetHighdwFileOffsetLow组成的64位值来指定,而且必须是操作系统的分配粒度的整数倍,对于Windows操作系统,分配粒度固定为64KB。当然,也可以通过如下代码来动态获取当前操作系统的分配粒度:

    SYSTEM_INFO sinf;
    GetSystemInfo(&sinf);
    DWORD dwAllocationGranularity = sinf.dwAllocationGranularity;


      参数dwNumberOfBytesToMap指定了数据文件的映射长度,这里需要特别指出的是,对于Windows 9x操作系统,如果MapViewOfFile()无法找到足够大的区域来存放整个文件映射对象,将返回空值(NULL);但是在Windows 2000下,MapViewOfFile()只需要为必要的视图找到足够大的一个区域即可,而无须考虑整个文件映射对象的大小。

      在完成对映射到进程地址空间区域的文件处理后,需要通过函数UnmapViewOfFile()完成对文件数据映像的释放,该函数原型声明如下:

    BOOL UnmapViewOfFile(LPCVOID lpBaseAddress);


      唯一的参数lpBaseAddress指定了返回区域的基地址,必须将其设定为MapViewOfFile()的返回值。在使用了函数MapViewOfFile()之后,必须要有对应的UnmapViewOfFile()调用,否则在进程终止之前,保留的区域将无法释放。除此之外,前面还曾由CreateFile()CreateFileMapping()函数创建过文件内核对象和文件映射内核对象,在进程终止之前有必要通过CloseHandle()将其释放,否则将会出现资源泄漏的问题。

      除了前面这些必须的API函数之外,在使用内存映射文件时还要根据情况来选用其他一些辅助函数。例如,在使用内存映射文件时,为了提高速度,系统将文件的数据页面进行高速缓存,而且在处理文件映射视图时不立即更新文件的磁盘映像。为解决这个问题可以考虑使用FlushViewOfFile()函数,该函数强制系统将修改过的数据部分或全部重新写入磁盘映像,从而可以确保所有的数据更新能及时保存到磁盘。

    使用内存映射文件处理大文件应用示例

      下面结合一个具体的实例来进一步讲述内存映射文件的使用方法。该实例从端口接收数据,并实时将其存放于磁盘,由于数据量大(几十GB),在此选用内存映射文件进行处理。下面给出的是位于工作线程MainProc中的部分主要代码,该线程自程序运行时启动,当端口有数据到达时将会发出事件hEvent[0]WaitForMultipleObjects()函数等待到该事件发生后将接收到的数据保存到磁盘,如果终止接收将发出事件hEvent[1],事件处理过程将负责完成资源的释放和文件的关闭等工作。下面给出此线程处理函数的具体实现过程:

    ……
    //
    创建文件内核对象,其句柄保存于hFile
    HANDLE hFile = CreateFile("Recv1.zip",
    GENERIC_WRITE | GENERIC_READ,
    FILE_SHARE_READ,
    NULL,
    CREATE_ALWAYS,
    FILE_FLAG_SEQUENTIAL_SCAN,
    NULL);

    //
    创建文件映射内核对象,句柄保存于hFileMapping
    HANDLE hFileMapping = CreateFileMapping(hFile,NULL,PAGE_READWRITE,
    0, 0x4000000, NULL);
    //
    释放文件内核对象
    CloseHandle(hFile);

    //
    设定大小、偏移量等参数
    __int64 qwFileSize = 0x4000000;
    __int64 qwFileOffset = 0;
    __int64 T = 600 * sinf.dwAllocationGranularity;
    DWORD dwBytesInBlock = 1000 * sinf.dwAllocationGranularity;

    //
    将文件数据映射到进程的地址空间
    PBYTE pbFile = (PBYTE)MapViewOfFile(hFileMapping,
    FILE_MAP_ALL_ACCESS,
    (DWORD)(qwFileOffset>>32), (DWORD)(qwFileOffset&0xFFFFFFFF), dwBytesInBlock);
    while(bLoop)
    {
    //
    捕获事件hEvent[0]和事件hEvent[1]
    DWORD ret = WaitForMultipleObjects(2, hEvent, FALSE, INFINITE);
    ret -= WAIT_OBJECT_0;
    switch (ret)
    {
    //
    接收数据事件触发
    case 0:
    //
    从端口接收数据并保存到内存映射文件
    nReadLen=syio_Read(port[1], pbFile + qwFileOffset, QueueLen);
    qwFileOffset += nReadLen;

    //
    当数据写满60%时,为防数据溢出,需要在其后开辟一新的映射视图
    if (qwFileOffset > T)
    {
    T = qwFileOffset + 600 * sinf.dwAllocationGranularity;
    UnmapViewOfFile(pbFile);
    pbFile = (PBYTE)MapViewOfFile(hFileMapping,
    FILE_MAP_ALL_ACCESS,
    (DWORD)(qwFileOffset>>32), (DWORD)(qwFileOffset&0xFFFFFFFF), dwBytesInBlock);
    }
    break;

    //
    终止事件触发
    case 1:
    bLoop = FALSE;

    //
    从进程的地址空间撤消文件数据映像
    UnmapViewOfFile(pbFile);

    //
    关闭文件映射对象
    CloseHandle(hFileMapping);
    break;
    }
    }


      在终止事件触发处理过程中如果只简单的执行UnmapViewOfFile()CloseHandle()函数将无法正确标识文件的实际大小,即如果开辟的内存映射文件为30GB,而接收的数据只有14GB,那么上述程序执行完后,保存的文件长度仍是30GB。也就是说,在处理完成后还要再次通过内存映射文件的形式将文件恢复到实际大小,下面是实现此要求的主要代码:

    // 创建另外一个文件内核对象
    hFile2 = CreateFile("Recv.zip",
    GENERIC_WRITE | GENERIC_READ,
    FILE_SHARE_READ,
    NULL,
    CREATE_ALWAYS,
    FILE_FLAG_SEQUENTIAL_SCAN,
    NULL);

    //
    以实际数据长度创建另外一个文件映射内核对象
    hFileMapping2 = CreateFileMapping(hFile2,
    NULL,
    PAGE_READWRITE,
    0,
    (DWORD)(qwFileOffset&0xFFFFFFFF),
    NULL);

    //
    关闭文件内核对象
    CloseHandle(hFile2);

    //
    将文件数据映射到进程的地址空间
    pbFile2 = (PBYTE)MapViewOfFile(hFileMapping2,
    FILE_MAP_ALL_ACCESS,
    0, 0, qwFileOffset);

    //
    将数据从原来的内存映射文件复制到此内存映射文件
    memcpy(pbFile2, pbFile, qwFileOffset);

    file://
    从进程的地址空间撤消文件数据映像
    UnmapViewOfFile(pbFile);
    UnmapViewOfFile(pbFile2);

    //
    关闭文件映射对象
    CloseHandle(hFileMapping);
    CloseHandle(hFileMapping2);

    //
    删除临时文件
    DeleteFile("Recv1.zip");


      结论

      经实际测试,内存映射文件在处理大数据量文件时表现出了良好的性能,比通常使用CFile类和ReadFile()WriteFile()等函数的文件处理方式具有明显的优势。本文所述代码在Windows 98下由Microsoft Visual C++ 6.0编译通过。

    展开全文
  • windows 下文件的高级操作

    千次阅读 2017-06-11 19:02:15
    本文主要说明Windows下操作文件...Windows并没有专门提供判断文件是否存在的API,替代的解决方案是使用函数GetFileAttributes,传入一个路径,如果文件不存在,函数会返回INVALID_FILE_ATTRIBUTES,这个时候一般

    本文主要说明在Windows下操作文件的高级方法,比如直接读写磁盘,文件的异步操作,而文件普通的读写方式在网上可以找到一大堆资料,在这也就不再进行专门的说明。

    判断文件是否存在

    在Windows中并没有专门提供判断文件是否存在的API,替代的解决方案是使用函数GetFileAttributes,传入一个路径,如果文件不存在,函数会返回INVALID_FILE_ATTRIBUTES,这个时候一般就可以认为文件不存在。更严格一点的,可以在返回INVALID_FILE_ATTRIBUTES之后调用GetLastError函数,判断返回值是否为ERROR_FILE_NOT_FOUND或者ERROR_PATH_NOT_FOUND(这个值适用于判断目录)
    下面是它的实例代码

    BOOL IsFileExist(LPCTSTR pFilePath)
    {
        DWORD dwRet = GetFileAttributes(pFilePath);
        if(INVALID_FILE_ATTRIBUTES == dwRet)
        {
            dwRet = GetLastError();
            if (ERROR_FILE_NOT_FOUND == dwRet || ERROR_PATH_NOT_FOUND == dwRet)
            {
                return FALSE;
            }
        }
    
        return TRUE;
    }

    文件查找和目录遍历

    这个操作主要使用到了下面几个API函数:
    1. FindFirstFile:建立一个指定搜索条件的搜索句柄,函数原型如下:

    HANDLE FindFirstFile(
      LPCTSTR lpFileName, 
      LPWIN32_FIND_DATA lpFindFileData 
    ); 

    第一个参数是一个搜索起始位置路劲的字符串,但是这个字符串的格式为“路径+特定文件的通配符”这样它会以这个路径作为起始路径,依次查找到目录中文件名符合通配符的文件,比如”c:\*.“会返回c盘下的所有文件,而”c:\”直接返回错误,”c:\a.txt”会返回c盘中以a开头的txt文件
    2. FindNextFile:搜索符合条件的下一项,在循环中调用它的话,它会依次返回符合FindFirstFile要求的文件信息和所有子目录新消息
    3. FindClose:关闭搜索句柄
    FindFirstFile和FindNextFile返回的文件信息结构为WIN32_FIND_DATA,它的定义如下:

    typedef struct _WIN32_FIND_DATA {   
        DWORD dwFileAttributes; //文件属性
        FILETIME ftCreationTime;   //创建时间
        FILETIME ftLastAccessTime; //最后访问时间
        FILETIME ftLastWriteTime;   //最后修改时间
        DWORD nFileSizeHigh;   
        DWORD nFileSizeLow; //这两个值是一个64位的文件大小的高32位和低32位
        DWORD dwOID;  
        TCHAR cFileName[MAX_PATH]; //文件名称 
    } WIN32_FIND_DATA; 

    一般在遍历的时候首先判断文件属性,如果为FILE_ATTRIBUTE_DIRECTORY(是个目录),并且文件名称不为”.”,”..”则递归调用遍历函数遍历它的子目录,但是一定要记得进行文件路径的拼接,如果不为目录,这个时候一般就是普通文件,这个时候可以选择进行打印(遍历文件目录)或者比较文件名称与需要查找的名称是否相同(查找文件)。下面是一个全盘搜索特定文件名的实例代码:

    void FindFileByPath(LPCTSTR pszSearchEntry, LPCTSTR pszFileName)
    {
        WIN32_FIND_DATA fd = {0};
        TCHAR szFilePath[MAX_PATH] = _T("");
        StringCchCat(szFilePath, MAX_PATH, pszSearchEntry);
        StringCchCat(szFilePath, MAX_PATH, _T("*.*"));
    
        HANDLE hSearch = FindFirstFile(szFilePath, &fd);
        if (INVALID_HANDLE_VALUE == hSearch)
        {
            return;
        }
    
        do
        {
            if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && _tcscmp(fd.cFileName, _T(".")) != 0 && _tcscmp(fd.cFileName, _T("..")) != 0)
            {
                TCHAR szSubDir[MAX_PATH] = _T("");
                StringCchCat(szSubDir, MAX_PATH, pszSearchEntry);
                StringCchCat(szSubDir, MAX_PATH, fd.cFileName);
                StringCchCat(szSubDir, MAX_PATH, _T("\\"));
                FindFileByPath(szSubDir, pszFileName);
            }else
            {
                if (_tcscmp(fd.cFileName, pszFileName) == 0)
                {
                    TCHAR szFullPath[MAX_PATH] = _T("");
                    StringCchCat(szFullPath, MAX_PATH, pszSearchEntry);
                    StringCchCat(szFullPath, MAX_PATH, _T("\\"));
                    StringCchCat(szFullPath, MAX_PATH, fd.cFileName);
                    printf("full path:%ws\n", szFullPath);
                    return;
                }
            }
            ZeroMemory(&fd, sizeof(fd));
        } while (FindNextFile(hSearch, &fd));
    }
    
    void FindFile(LPCTSTR pFileName)
    {
        TCHAR szVolumn[MAX_PATH] = _T("");
        GetLogicalDriveStrings(MAX_PATH, szVolumn);
    
        LPCTSTR pVolumnName = szVolumn;
        while (_tcscmp(pVolumnName, _T("")) != 0)
        {
            FindFileByPath(pVolumnName, pFileName);
    
            //偏移到下一个盘符的字符串位置
            size_t nLen = 0;
            StringCchLength(pVolumnName, MAX_PATH, &nLen);
            nLen++;
            pVolumnName += nLen;
        }
    }

    由于这段代码会遍历整个磁盘,查找所有具有相同文件名称的文件,所以当某个逻辑分区的文件结构比较复杂的时候,可能执行效果比较慢。
    这段代码出现了两个函数,第一个函数是真正遍历文件的函数,由于FindFirst函数需要传入一个入口点,所以在需要进行全盘遍历的时候提供了另外一个函数来获取所有磁盘的逻辑分区名。
    获取所有逻辑分区名调用函数GetLogicalDriveStrings,这个函数会返回一个含有所有分区名称的字符串,每个分区名称之间以”\0”分割,所以在获取所有名称的时候需要自己进行字符串指针的偏移操作
    在遍历的时候为了要遍历所有文件及目录搜索的统配符应该匹配所有文件名称。另外FindFirst也会返回一个文件信息的结构,这个结构是当前目录中符合条件的第一个文件信息,在遍历的时候不要忘记也取一下它返回的文件信息。最后当文件为目录的时候需要判断它是否为当前目录或者当前目录的父目录,也就是是否为”.”和”..”,这段代码有一点不足就是不支持通配符,必须输入文件名的全称

    目录变更监视

    一般像notepad++等文本编辑器都会提供一个功能,就是在它们打开了一个文本之后,如果文本被其他程序更改,那么它们会提示用户是否需要重新载入,这个功能的实现需要对文件进行监控,windows中提供了一套API用于监控目录变更
    使用函数FindFirstChangeNotification创建一个监控句柄,该函数原型如下:

    HANDLE FindFirstChangeNotification(  
        LPCTSTR lpPathName,  
        BOOL bWatchSubtree,
        DWORD dwNotifyFilter);

    第一个参数是一个目录的字符串,表示将要监控哪个目录,注意这里必须穿入一个目录,不能穿文件路径
    第二个参数是一个bool类型,表示是否监控目录中的整个目录树
    第三个参数是监控的时间类型,如果要监控目录中的文件的改动,可以使用FILE_NOTIFY_CHANGE_LAST_WRITE 标记,该标记会监控文件的最后一次写入,其他类型请查阅MSDN
    创建监控句柄后使用Wait函数循环等待监控句柄,如果目录中发生对应的事件,wait函数返回,这个时候可以对比上次目录结构得出哪个文件被修改,做相应的处理后调用FindNextChangeNotification函数传入监控句柄,继续监控下一次变更。
    最后当我们不需要进行监控的时候调用FindCloseChangeNotification关闭监控句柄

    void WatchDirectoryChange(LPCTSTR lpDir)
    {
        HANDLE hChangNotify = FindFirstChangeNotification(lpDir, FALSE, FILE_NOTIFY_CHANGE_LAST_WRITE );
        if (hChangNotify == INVALID_HANDLE_VALUE)
        {
            printf("FindFirstChangeNotification function faild!\n");
            return ExitProcess(GetLastError());
        }
    
        while (TRUE)
        {
            printf("wait for change notify.......\n");
            if(WAIT_OBJECT_0 == WaitForSingleObject(hChangNotify, INFINITE))
            {
                printf("some file be changed in this directory\n");
            }
            FindNextChangeNotification(hChangNotify);
        }
    
        FindCloseChangeNotification(hChangNotify);
    }

    如果嫌这个方法比较麻烦的话,为了实现这个功能,Windows专门提供了一个函数ReadDirectoryChangesW,就跟他的名字一样他只能用于UNICODE平台,这个函数不存在ANSI版本,所以在ANSI版本时需要进行字符串的转化操作。
    函数原型如下:
    BOOL WINAPI ReadDirectoryChangesW(
    __in HANDLE hDirectory, //需要监控的目录的句柄,这个句柄可以用CreateFile打开
    __out LPVOID lpBuffer, //函数返回信息的缓冲
    __in DWORD nBufferLength, //缓冲区的长度
    __in BOOL bWatchSubtree, //是否监控它的子目录
    __in DWORD dwNotifyFilter, //监控的事件
    __out_opt LPDWORD lpBytesReturned, //实际返回数据长度
    __inout_opt LPOVERLAPPED lpOverlapped, //异步调用时的OVERLAPPED结构
    __in_opt LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine //异步调用时的APC函数
    );

    这个函数它的原理就类似于上面的三个函数,如果是同步操作,当需要监控的目录发生指定的事件时函数返回,并将监控得到的信息填充到结构体中,它会将数据以FILE_NOTIFY_INFORMATION结构的形式返回。该结构的定义如下:

    typedef struct _FILE_NOTIFY_INFORMATION {  
        DWORD NextEntryOffset;  
        DWORD Action;  
        DWORD FileNameLength;  
        WCHAR FileName[1];
    } FILE_NOTIFY_INFORMATION,  *PFILE_NOTIFY_INFORMATION;

    这个结构体中存储文件名称的成员为FileName,这个成员只是起到一个变量名称标识的作用,在存储文件名称时用到了越界访问的方式,所以定义缓冲的大小一定要大于这个结构,让其有足够的空间容纳FileName这个字符串。结构体中的Action表示当前发生了何种操作,具体的类型可以参考MSDN,它的意思根据字面的单词很容易理解
    下面是使用它的具体代码:

    void WatchFileChange(LPCTSTR lpFilePath)
    {
        DWORD cbBytes;
        char notify[1024];
    
        HANDLE dirHandle = CreateFile(lpFilePath,GENERIC_READ | GENERIC_WRITE | FILE_LIST_DIRECTORY,
            FILE_SHARE_READ | FILE_SHARE_WRITE,
            NULL,
            OPEN_EXISTING,
            FILE_FLAG_BACKUP_SEMANTICS,
            NULL);
    
        if(dirHandle == INVALID_HANDLE_VALUE) //若网络重定向或目标文件系统不支持该操作,函数失败,同时调用GetLastError()返回ERROR_INVALID_FUNCTION
        {
            cout<<"error"+GetLastError()<<endl;
        }
    
    
        memset(notify,0,strlen(notify));
    
        FILE_NOTIFY_INFORMATION *pnotify = (FILE_NOTIFY_INFORMATION*)notify; 
        cout<<"start...."<<endl;   
        while(true)
        {   
    
            if(ReadDirectoryChangesW(dirHandle,&notify,1024,true,
                FILE_NOTIFY_CHANGE_FILE_NAME |
                FILE_NOTIFY_CHANGE_DIR_NAME
                | FILE_NOTIFY_CHANGE_SIZE,
                &cbBytes,NULL,NULL))
            {
                //设置类型过滤器,监听文件创建、更改、删除、重命名等
                switch(pnotify->Action)
                {
                case FILE_ACTION_ADDED:
                    _tprintf(_T("add file: %s\n"), pnotify->FileName);
                    break;
                case FILE_ACTION_MODIFIED:
                    _tprintf(_T("modify file:%s\n"), pnotify->FileName);
                    break;
                case FILE_ACTION_REMOVED:
                    _tprintf(_T("file removed %s\n"), pnotify->FileName);
                    break;
                case FILE_ACTION_RENAMED_OLD_NAME:
                    _tprintf(_T("file renamed:%s\n"), pnotify->FileName);
                    break;
    
                default:
                    cout<<"unknow command!"<<endl;
    
                }
    
            }   
    
        }
        CloseHandle(dirHandle);
    }

    这段代码很容易理解,但是需要注意几点:
    1. 之前说过的分配的缓冲一定要大于FILE_NOTIFY_INFORMATION 结构
    2. 这个函数也是用来监控目录的,所以这里要传入一个目录路径,不能传入文件路径
    3. 在使用CreateFile来打开目录的时候这个函数要求传入的文件句柄必须要以FILE_LIST_DIRECTORY标识打开,否则在调用的时候会报“参数错误”这个错

    文件映射

    Windows中,文件映射是文件内容到进程的虚拟地址空间的映射,这个映射称之为File Mapping,文件内容的拷贝就是文件视图(File View),从内存管理的角度来看,文件映射只是将磁盘的真实地址通过页表映射到进程的虚拟地址空间中,读写这段虚拟地址空间其实就是在读写磁盘。而文件视图就是将文件中的内容整个读到内存中,并将这段虚拟地址空间与真实物理内存对应。最终在关闭整个文件映射的时候如果存在文件视图,操作系统会将视图中的内容写会到磁盘,其实也就是简单的进行了下物理内存到磁盘的页面交换,从内存管理的角度来看,文件映射其实就是操作系统将磁盘上的数据与物理内存之间的页面交换,操作系统在二者之间来回倒腾数据而已
    文件映射本身是一个内核对象,操作系统在内核中维护了一个相关的数据结构,这个结构中记录了被映射到虚拟地址空间中的起始地址和被映射的数据的大小。
    由于内核对象的数据结构是在内核中被维护,而内核被所有进程共享,所以从理论上将不同的进程是可以共享同一个内核对象的,虽然它们的对象句柄会在不同进程中呈现不同的值,但是在内核中,却是指向同一个结构,那么虽然不同进程的文件映射对象不同,但是通过寻址得到的物理内存肯定是同一个,所以这就提供了另一种进程间共享内存的方法——文件映射。
    创建文件映射主要使用函数CreateFileMapping,这个函数第一个参数是一个文件句柄,这个句柄可以是一个真实存在在磁盘上的文件,这样创建的文件映射最终就是将磁盘中的数据映射到进程的虚拟地址空间,也可以传入一个INVALID_HANDLE_VALUE,这个时候也会返回成功,传入INVALID_HANDLE_VALUE一般是用来在进程间共享内存的。注意:这个函数只是创建了一个内核对象并返回它的句柄,并没有进行内存映射的相关操作。同时由于它第一个句柄参数可以填INVALID_HANDLE_VALUE,在使用CreateFile函数后一定要注意校验,不然可能看到CreateFileMapping函数返回的是一个有效句柄,但是并没有成功创建这个文件的映射
    然后调用MappingViewOfFile函数,将对应文件与一段进程的虚拟地址空间关联并将文件映射到内存,也就是将磁盘文件中的数据交换到物理内存中
    当我们不使用这块真实内存的时候,调用UnMapViewOfFile将内存中的数据交换到磁盘,最终使用文件映射完毕后,调用CloseHandle关闭所有句柄
    使用文件映射一般有几个好处:
    1. 针对文件来说,文件映射本质上是磁盘到物理内存之间的页面交换,由操作系统的内存管理机制统一调度,效率比一般的文件读写要高,而且在使用完毕后,操作系统会自动的将内存中的数据写到磁盘中,不用手动的更新文件
    2. 针对不同进程来说,使用文件映射来共享内存本质上是在使用同样一块内存,相比于管道油槽等方式传输数据来说显得更为高效
    下面通过几个例子来说明在这两种情况下使用文件映射

    void GetFileNameByHandle(HANDLE hFile)
    {
        HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
        if (INVALID_HANDLE_VALUE == hMapping)
        {
            _tprintf(_T("create file mapping error\n"));
            return;
        }
    
        LPVOID lpMappingMemeory = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 1);
        if (NULL == lpMappingMemeory)
        {
            _tprintf(_T("MapViewOfFile error\n"));
            return;
        }
    
        TCHAR szFileName[MAX_PATH] = _T("");
        if(0 == GetMappedFileName(GetCurrentProcess(), lpMappingMemeory, szFileName, MAX_PATH))
        {
            _tprintf(_T("GetMappedFileName error\n"));
            return;
        }
    
        TCHAR szTemp[MAX_PATH] = _T("");
        GetLogicalDriveStrings(MAX_PATH, szTemp);
        TCHAR szDriver[4] = _T(" :");
        LPCTSTR p = szTemp;
        while (*p != _T('\0'))
        {
            *szDriver = *p;
            TCHAR szName[MAX_PATH] = _T("");
            QueryDosDevice(szDriver, szName, MAX_PATH);
            size_t nPathLen = 0;
            StringCchLength(szName, MAX_PATH, &nPathLen);
            if(CSTR_EQUAL == CompareString(LOCALE_USER_DEFAULT, NORM_IGNORECASE, szName, nPathLen, szFileName, nPathLen))
            {
                TCHAR szFullPath[MAX_PATH] = _T("");
                StringCchCopy(szFullPath, MAX_PATH, p);
                //在这使用文件带卷名的字符串首地址 + 卷名长度 + 1(+1是为了偏移到卷名后面的"\"的下一个字符,因为这个盘符中自己带了"/"字符)
                StringCchCat(szFullPath, MAX_PATH, szFileName + nPathLen + 1);
                _tprintf(_T("文件全路径:%s"), szFullPath);
                break;
            }
    
            size_t dwLen = 0;
            StringCchLength(p, MAX_PATH, &dwLen);
            p = p + dwLen + 1;
        }
    
        UnmapViewOfFile(lpMappingMemeory);
        CloseHandle(hMapping);
        return;
    }

    该函数利用文件映射的方式,通过一个文件的句柄获取它的绝对路径。
    该函数首先根据文件句柄创建一个文件映射并调用GetMappedFileName获取文件的全路径,但是获取到的是类似于“\Device\HarddiskVolume6\Program\FileDemo\FileMapping\FileMapping.cpp”这样的卷名加上文件的相对路径,而不是我们常见的类似于C D E这样的盘符名称,所以为了获取对应的盘符,使用的方式是利用GetLogicalDriverString函数来获取系统所有逻辑卷的盘符,然后调用QueryDosDevice函数将盘符转化为卷名,再与之前获取到的路径中的卷名进行比较,在这使用了一个技巧,就是首先获取卷名对应的长度,然后调用比较函数时传入卷名的长度让其只比较卷名对应的字符,如果相同,就找到了卷名对应的盘符名称,最后将卷名与在卷中的相对路径进行拼接就得到了它的文件全路径。
    下面来看一个使用文件映射在不同进程间共享内存的例子

    //Process A
    #define BUFF_SIZE 1024
    
    int _tmain(int argc, _TCHAR* argv[])
    {
        TCHAR szHandleName[] = _T("Global\\ShareMemMapping");
        HANDLE hMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, BUFF_SIZE, szHandleName);
        if (INVALID_HANDLE_VALUE == hMapping)
        {
            printf("create file mapping error\n");
            return GetLastError();
        }
    
        LPVOID pMem = MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS, 0, 0, BUFF_SIZE);
        if (NULL == pMem)
        {
            printf("MapViewOfFile Error\n");
            return GetLastError();
        }
    
        ZeroMemory(pMem, BUFF_SIZE);
        TCHAR pszData[] = _T("this is written by process A");
        CopyMemory(pMem, pszData, sizeof(pszData));
        _tsystem(_T("PAUSE"));
    
        UnmapViewOfFile(pMem);
        CloseHandle(hMapping);
    
        return 0;
    }
    #define BUFF_SIZE 1024
    
    int _tmain(int argc, _TCHAR* argv[])
    {
        TCHAR szHandleName[] = _T("Global\\ShareMemMapping");
        HANDLE  hMapping = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, szHandleName);
        if (INVALID_HANDLE_VALUE == hMapping)
        {
            printf("OpenFileMapping");
            return GetLastError();
        }
    
        LPCTSTR pMem = (LPCTSTR)MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS, 0, 0, BUFF_SIZE);
        if (NULL == pMem)
        {
            printf("MapViewOfFile Error\n");
            return GetLastError();
        }
    
        printf("read date: %ws\n", pMem);
    
        _tsystem(_T("PAUSE"));
        UnmapViewOfFile(pMem);
        CloseHandle(hMapping);
        return 0;
    }
    
    

    在上面的例子中,进程A做了如下工作:
    1. 创建一个命名的文件映射对象
    2. 构建文件映射的视口,并写入一段内存
    3. 等待
    4. 关闭相关句柄
    在进程B中做了如下工作:
    1. 打开之前A创建的文件映射对象
    2. 构建文件映射的视口,读取内存
    3. 关闭相关句柄
    在使用文件映射共享内存时需要注意:
    1. 使用命名对象的时候,对象前面必须要加上“Global//”表示该对象是一个全局的对象
    2. 不同进程在使用文件映射共享内存时调用函数MapViewOfFile填写内存的起始偏移,视口大小必须完全一样
    这个例子中只是简单的一个进程写,另一个进程读,如果想要两个进程同时读写共享内存,可以使用Event等方式进行同步。

    直接读写磁盘扇区

    CreateFile可以打开许多设备,一般来说,它可以打开所有的字符设备,向串口,管道,油槽等等,在编写某些硬件的驱动程序时如果将其以字符设备的方式来操作,那么理论上在应用层是可以用CreateFile打开这个硬件设备的句柄,并操作它的,这里介绍下如何使用CreateFile来直接读取物理磁盘。
    读写物理磁盘只需要改变一下CreateFile中代表文件名称的第一个参数,将这个参数改为\.\PhysicalDrive0,后面的数字代表的是第几块物理硬盘,如果有多块硬盘,后面还可以是1、2等等
    注意这是在直接读写物理磁盘,当你不了解文件系统的时候,不要随意往里面写数据,以免造成磁盘损坏
    下面是一个简单的例子

        DWORD dwSectorsPerCluster = 0;
        DWORD dwBytesPerSector = 0;
        DWORD dwNumberOfFreeClusters = 0;
        DWORD dwTotalNumberOfClusters = 0;
        TCHAR pDiskName[] = _T("\\\\.\\PhysicalDrive0");
    
        //get disk info
        if(GetDiskFreeSpace(_T("c:\\"), &dwSectorsPerCluster, &dwBytesPerSector, &dwNumberOfFreeClusters, &dwTotalNumberOfClusters))
        {
            printf("磁盘信息:\n");
            LARGE_INTEGER size_disk = {0};
            size_disk.QuadPart = (LONGLONG)dwTotalNumberOfClusters * (LONGLONG)dwSectorsPerCluster * (LONGLONG)dwBytesPerSector;
            printf("\t总大小 %dG", size_disk.QuadPart / (1024 * 1024 * 1024));
            printf("\t簇总数%d, 簇中扇区总数:%d, 扇区大小:%d\n", dwTotalNumberOfClusters, dwSectorsPerCluster, dwBytesPerSector);
        }
        else
        {
            dwBytesPerSector = 512;
        }
    
        HANDLE hDisk = CreateFile(pDiskName,GENERIC_READ,FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,NULL,OPEN_EXISTING,0,NULL);
        if(hDisk == INVALID_HANDLE_VALUE)
        {
            printf("create file error\n");
            return GetLastError();
        }
    
        char* pMem = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwBytesPerSector * 8);
        DWORD dwRead = 0;
        if(!ReadFile(hDisk, pMem, dwBytesPerSector * 8, &dwRead, NULL))
        {
            printf("read file error\n");
            return GetLastError();
        }
    
        for(int i = 0; i < dwBytesPerSector * 8; i++)
        {
            if(i % 16 == 0 && i != 0)
            {
                printf("\n");
            }
    
            printf("0x%02x ", pMem[i]);
        }
        CloseHandle(hDisk);

    上面的例子调用了GetDiskFreeSpace函数获取了逻辑卷的相关信息,它需要传入一个盘符,表示要获取哪个盘的数据,它会通过输出参数返回多个逻辑卷的信息,它们分别是:每个簇有多少个扇区,每个扇区的大小,有多少个空闲的簇,卷中簇的个数。根据这些信息就可以计算出逻辑卷的大小哦,在计算的时候由于磁盘空间一定是大于4G的,所以在这要用64位整数保存。
    知道了扇区大小后,直接调用文件操作函数,读取8个扇区的数据,然后输出。

    文件的异步操作

    在常规文件读写方式中,是严格串行化的,只有当读写操作完全完成时才会返回,由于磁盘读写相对于CPU的运行效率来说实在是太慢的,这就造成了程序长时间处理等待状态,这种读写方式称之为阻塞方式,早期的磁盘在进行读写时是需要CPU来控制,这样CPU必须来配合慢速的硬盘,造成了效率低下,于是硬件工程师在在磁盘中加入了一个控制设备,专门用来控制磁盘的读写,这个设备被称之为DMA,由于DMA的存在,使得CPU从漫长的磁盘操作中解放出来,一般在进行磁盘读写时,CPU主要向DMA发出一个读写命令,然后就继续执行后面的工作,当读写完成后DMA向CPU发出完成的指令,这个时候CPU会停下手上的工作,来处理这个通知,程序此时会陷入中断,直到CPU完成对应的操作。由于DMA的出现使得CPU从慢速的磁盘操作中解放出来,但是在同步的读写方式中,CPU发出磁盘的读写指令后什么都不做,一直等待磁盘的读写玩成,使CPU长时间陷入等待状态,浪费了宝贵的CPU的资源。所以为了程序效率,在读写磁盘时一般使用异步的方式,在发出读写命令后立即返回,然后执行后面的操作,这样就在一定程度上利用了闲置的CPU资源。

    重叠IO

    在Windows中默认使用同步的方式进行读写操作,如果要使用异步的方式,在创建文件句柄的时候,需要在CreateFile函数的dwFlagsAndAttributes参数中加上FILE_FLAG_OVERLAPPED标识,然后可以设置一个完成函数,并在对应线程中调用waitex函数或者使用SleepEx函数使线程陷入可警告状态,当读写操作完成时会将完成函数插入线程的APC队列,当线程进入可警告状态的时候会调用APC函数,这样就可以知道读写操作已经完成。这是一种方式,还可以使用一个OVERLAPPED结构,并给这个结构中填上一个事件对象,在需要进行同步的地方等待这个事件对象,在磁盘操作完成的时候会将其设置为有信号,上面的两种方式都利用的Windows提供的重叠IO模型
    不管使用哪种方式,在进行文件的异步操作时都需要自己维护并偏移文件指针。在同步的方式时Windows是完成之后返回,它一次只会写入一条数据到磁盘,而且它也知道具体写入了多少数据,这时候系统帮助我们完成了文件指针的偏移,但是在进行异步操作的时候可能会同时有多条数据写入,并且系统不知道具体会成功写入多少数据,所以它不可能帮我们进行文件指针的偏移,这个时候就需要自己进行偏移操作

    完成函数

    使用完成函数主要需要如下步骤:
    1. 调用CreateFile在dwFlagsAndAttributes参数中加上FILE_FLAG_OVERLAPPED标识表示我们需要使用异步的方式来进行磁盘操作
    2. 准备一个完成函数,函数的原型为:VOID CALLBACK FileIOCompletionRoutine(DWORD dwErrorCode,DWORD dwNumberOfBytesTransfered,LPOVERLAPPED lpOverlapped);函数的最后一个参数是一个OVERLAPPED结构,该结构的定义如下:

    typedef struct _OVERLAPPED {  
        ULONG_PTR Internal;  
        ULONG_PTR InternalHigh;  
        union {    
            struct {      
                DWORD Offset;      
                DWORD OffsetHigh;    
            };
            PVOID Pointer;  
        };  
        HANDLE hEvent;
    } OVERLAPPED,  *LPOVERLAPPED;

    这个结构中有一个共用体,其实这个共用体都可以用来操作文件指针,如果用其中的结构体,那么需要分别给其中的高32位和低32位赋值,如果使用指针,这个时候指针变量不指向任何内存,这个指针变量仅仅是作为一个变量名罢了,使用时也是将其作为正常变量来使用,虽然它是一个指针占4个字节,但是由于是一个共用体,它后面还有4个字节的剩余空间可以使用,所以使用它来存储文件指针的偏移没有任何问题。
    3. 调用ReadFileEx或者WriteFileEx函数(ReadFile WriteFile不支持完成函数的方式)并将完成函数作为最后一个参数传入
    4. 调用WaitEx族的等待函数或者SleepEx函数使线程陷入可警告状态,这个时候会执行完成函数
    下面是一个演示的例子

    LARGE_INTEGER g_FilePointer = {0}; //全局的文件指针
    struct  ST_EXT_OVERLAPPED
    {
        OVERLAPPED m_ol; //后面的代码在使用的时候后
        HANDLE m_hFile; //操作的文件句柄
        LPVOID m_pData; //操作的内存
        DWORD m_dwLen;  //操作的数据长度
    };
    
    VOID CALLBACK FileIOCompletionRoutine(DWORD dwErrorCode,DWORD dwNumberOfBytesTransfered,LPOVERLAPPED lpOverlapped)
    {
        ST_EXT_OVERLAPPED* pExOl = (ST_EXT_OVERLAPPED*)lpOverlapped;
        printf("线程[%04x]完成写入操作\n", GetCurrentThreadId());
        HeapFree(GetProcessHeap(), 0, pExOl->m_pData);
        HeapFree(GetProcessHeap(), 0, pExOl);
        pExOl = NULL;
    }
    
    DWORD WriteThreadProc(LPVOID lpParameter)
    {
        HANDLE hFile = *(HANDLE*)(lpParameter);
        ST_EXT_OVERLAPPED* pExOl = NULL;
        TCHAR szBuf[256] = _T("");
        StringCchPrintf(szBuf, 256, _T("这是一条模拟日志写入信息,由线程[%04x]写入\r\n"), GetCurrentThreadId());
        size_t dwLen = 0;
        StringCchLength(szBuf, 256, &dwLen);
        dwLen += 1; //保存字符串结尾的\0
    
        for (int i = 0; i < 100; i++)
        {
            pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED));
            pExOl->m_dwLen = dwLen * sizeof(TCHAR);
            pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwLen * sizeof(TCHAR));
            StringCchCopy((TCHAR*)pExOl->m_pData, 256, szBuf);
            pExOl->m_hFile = hFile;
    
            //使用锁无关的方式进行同步操作
            *((LONGLONG*)&pExOl->m_ol.Pointer) = InterlockedCompareExchange64(&g_FilePointer.QuadPart, g_FilePointer.QuadPart + pExOl->m_dwLen, g_FilePointer.QuadPart);
    
            WriteFileEx(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, (OVERLAPPED*)&pExOl->m_ol, FileIOCompletionRoutine);
    
            //do something
    
            if(WAIT_IO_COMPLETION == SleepEx(INFINITE, TRUE))
            {
    
            }
    
        }
    
        return 0;
    }
    
    int _tmain(int argc, _TCHAR* argv[])
    {
        HANDLE hFile = CreateFile(_T("log.txt"), GENERIC_ALL, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);//让其支持异步操作
        if (hFile == INVALID_HANDLE_VALUE)
        {
            printf("CreateFile error\n");
            return GetLastError();
        }
    
        ST_EXT_OVERLAPPED* pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED));
        pExOl->m_hFile = hFile;
        pExOl->m_dwLen = sizeof(WORD);
        pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WORD));
        *((WORD*)pExOl->m_pData) = MAKEWORD(0xff,0xfe);
    
        //文件指针的偏移
        pExOl->m_ol.Offset = g_FilePointer.LowPart;
        pExOl->m_ol.OffsetHigh = g_FilePointer.HighPart;
        g_FilePointer.QuadPart += pExOl->m_dwLen;
        WriteFileEx(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, (LPOVERLAPPED)&pExOl->m_ol, FileIOCompletionRoutine);
    
        HANDLE hThreads[20] = {NULL};
        for (int i = 0; i < 20; i++)   //创建20个写线程
        {
            hThreads[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)WriteThreadProc, &hFile, 0, NULL);
        }
    
        while(WAIT_IO_COMPLETION == WaitForMultipleObjectsEx(20, hThreads, TRUE, INFINITE, TRUE)) //函数返回WAIT_IO_COMPLETION 表示执行了完成函数
        {
            printf("有一个读写操作完成\n");
        }
    
        for (int i = 0; i < 20; i++)
        {
            CloseHandle(hThreads[i]);
        }
    
        CloseHandle(hFile);
    
        _tsystem(_T("PAUSE"));
        return 0;
    }

    在上面的例子中,我们首先向文件中写入0xff, 0xfe这两个值,在Windows中存储Unicode字符串的文件都是以0xff 0xfe开头,所以在写入Unicode字符串之前需要写入这两个值
    然后创建了20个线程,每个线程负责往文件中写入100条数据。线程先创建了一个包含OVERLAPPED结构的数据类型,然后再使用InterlockedCompareExchange64同步文件指针,这句话的意思是,向将高速缓存中的数据与内存中的数据进行比较,如果二者的值相同,那么久更改全局的文件指针,否则就不进行变化。实际上在Intel架构的机器上存在大量的高速缓存,为了效率,有的时候会将一些数据放置到高速缓存中,这样造成高速缓存中一份,内存中也有一份,有的时候在进行值得更改时它只会改变内存中的值,而高速缓存中的值不会更新,在调用这个函数的时候第一个参数传入的是一个指针,取值操作会强制CPU到内存中进行访问,这样这句话实质上是比较高速缓存与内存中的值是否一致,如果不一致,那么说明它被其他的线程进行过修改,将新的文件指针进行了替换,那么这个时候不需要进行任何操作,在之前写入文件的末尾进行追加即可,如果没有发生修改,那么其他线程可能会在当前位置写入,本线程也在当前位置写的话会造成覆盖,所以往后偏移文件指针,使其他线程使用新偏移的位置,本线程使用当前的位置,这样就不会发生覆盖
    在完成历程中完成清理内存的任务。每个WriteFileEx都对应着内存的分配,完成后都会调用这个完成历程清理对应的内存,这样就不会造成内存泄露。
    最后在主线程中等待子线程的完成,然后关闭句柄并结束进程

    事件模型

    事件模型与之前的完成历程相似,只是它不需要设置完成函数,需要在OVERLAPPED结构中设置一个事件,当IO操作完成时会将这个事件设置为有信号,然后在需要进行同步的位置等待这个事件即可
    下面是它的具体的例子

    LARGE_INTEGER g_FilePointer = {0}; //全局的文件指针
    struct  ST_EXT_OVERLAPPED
    {
        OVERLAPPED m_ol; //后面的代码在使用的时候后
        HANDLE m_hFile; //操作的文件句柄
        LPVOID m_pData; //操作的内存
        DWORD m_dwLen;  //操作的数据长度
    };
    
    DWORD WriteThreadProc(LPVOID lpParameter)
    {
        HANDLE hFile = *(HANDLE*)(lpParameter);
        ST_EXT_OVERLAPPED* pExOl = NULL;
        TCHAR szBuf[256] = _T("");
        StringCchPrintf(szBuf, 256, _T("这是一条模拟日志写入信息,由线程[%04x]写入\r\n"), GetCurrentThreadId());
        size_t dwLen = 0;
        StringCchLength(szBuf, 256, &dwLen);
        dwLen += 1; //保存字符串结尾的\0
    
        for (int i = 0; i < 100; i++)
        {
            pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED));
            pExOl->m_dwLen = dwLen * sizeof(TCHAR);
            pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwLen * sizeof(TCHAR));
            StringCchCopy((TCHAR*)pExOl->m_pData, 256, szBuf);
            pExOl->m_hFile = hFile;
            pExOl->m_ol.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
    
            //使用锁无关的方式进行同步操作
            *((LONGLONG*)&pExOl->m_ol.Pointer) = InterlockedCompareExchange64(&g_FilePointer.QuadPart, g_FilePointer.QuadPart + pExOl->m_dwLen, g_FilePointer.QuadPart);
            DWORD dwWritten = 0;
            WriteFile(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, &dwWritten, (OVERLAPPED*)&pExOl->m_ol);
    
            //do something
    
            if(WAIT_OBJECT_0 == WaitForSingleObject(pExOl->m_ol.hEvent, INFINITE))
            {
                printf("线程[%04x],写入操作完成一次,继续等待写入.....\n", GetCurrentThreadId());
                HeapFree(GetProcessHeap(), 0, pExOl->m_pData);
                HeapFree(GetProcessHeap(), 0, pExOl);
            }
        }
    
        return 0;
    }
    
    int _tmain(int argc, _TCHAR* argv[])
    {
        HANDLE hFile = CreateFile(_T("log.txt"), GENERIC_ALL, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);//让其支持异步操作
        if (hFile == INVALID_HANDLE_VALUE)
        {
            printf("CreateFile error\n");
            return GetLastError();
        }
    
        ST_EXT_OVERLAPPED* pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED));
        pExOl->m_hFile = hFile;
        pExOl->m_dwLen = sizeof(WORD);
        pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WORD));
        *((WORD*)pExOl->m_pData) = MAKEWORD(0xff,0xfe);
        pExOl->m_ol.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
    
        //文件指针的偏移
        pExOl->m_ol.Offset = g_FilePointer.LowPart;
        pExOl->m_ol.OffsetHigh = g_FilePointer.HighPart;
        g_FilePointer.QuadPart += pExOl->m_dwLen;
    
        DWORD dwWritten = 0;
        WriteFile(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, &dwWritten, (LPOVERLAPPED)&pExOl->m_ol);
    
        HANDLE hThreads[20] = {NULL};
    
        //等待当前写入完成
        if (WAIT_OBJECT_0 == WaitForSingleObject(pExOl->m_ol.hEvent, INFINITE))
        {
            printf("写入头部操作完成\n");
            HeapFree(GetProcessHeap(), 0, pExOl->m_pData);
            HeapFree(GetProcessHeap(), 0, pExOl);
        }
    
        for (int i = 0; i < 20; i++)   //创建20个写线程
        {
            hThreads[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)WriteThreadProc, &hFile, 0, NULL);
        }
    
        WaitForMultipleObjects(20, hThreads, TRUE, INFINITE);
        for (int i = 0; i < 20; i++)
        {
            CloseHandle(hThreads[i]);
        }
    
        CloseHandle(hFile);
    
        _tsystem(_T("PAUSE"));
        return 0;
    }

    上面的例子与之前的完成历程的例子基本上一样,只是在OVERLAPPED结构中加入EVENT对象,并且没有完成历程,内存的清理工作需要在本线程中进行清理

    完成端口

    上述重叠IO在一定程度上解决的线程陷入等待的问题,但是从上面的代码上来看,仍然需要在本线程中进行等待操作,也就是说,如果在IO函数返回后进行某项操作,但是这项操作完成后而IO操作并没有完成,那么仍然要陷入等待,现在有一个想法,就是同步操作不在本线程中完成,另外开辟一个线程,将所有的等待操作都放到新线程中,而本线程就不必进行等待,同步线程只需要在操作完成的时候启动执行,这样几乎就不存在CPU等待IO设备的问题。主要的问题是,怎么向新线程传递同步对象,就像上面的例子来说,等待IO操作完成就是为了清理内存而已,这个时候如果创建新线程进行等待的话,总共有2000个写入操作,为了清理每块内存,需要定义一个2000O包含VERLAPPED结构的数组,然后当所有线程启动后将数组指针传入,如果为每个如果动态添加新的写入线程,那就必须修改数组大小。这给编程造成了很大的麻烦,为了解决这个问题,VC中引入了完成端口模型
    本质上完成端口利用了线程池机制并结合了重叠IO的优势,在Windows下这种IO模型是最高效的一种。
    完成端口首先创建对应数量的线程的线程池,然后将相关的文件句柄与完成端口对象绑定,并传入一个OVERLAPPED结构的指针,然后进行等待,一旦有IO操作完成,就会启动完成端口中的线程,完成后续的操作。
    完成端口的使用一般经过下面几个步骤:
    1. 调用CreateIoCompletionPort创建完成端口对象,并制定最大并发线程数(一般制定CPU核数或者核数的两倍)
    2. 创建用于完成端口的线程,一般大于等于最大并发数
    3. 调用函数CreateIoCompletionPort,将文件句柄与完成端口绑定
    4. 在IO操作中传入一个OVERLAPPED结构
    5. 在完成端口的线程中调用GetQueuedCompletionStatus进行等待,当有IO操作完成时函数会返回,对应的线程就可以启动执行
    函数CreateIoCompletionPort原型如下

    HANDLE WINAPI CreateIoCompletionPort(
      __in      HANDLE FileHandle,
      __in_opt  HANDLE ExistingCompletionPort,
      __in      ULONG_PTR CompletionKey,
      __in      DWORD NumberOfConcurrentThreads
    );

    第一个参数是文件句柄,第二参数是完成端口句柄,第三个参数是一个完成的标识。一般给NULL,第四个是最大线程数。一般在操作的时候如果是创建完成端口句柄,那么只需要指定最大并发线程数,如果是将文件句柄和完成端口对象进行绑定,只需要提供前连个参数。在下面的例子中可以很清楚的看到它的用法
    下面是一个使用完成端口的例子:

    LARGE_INTEGER g_FilePointer = {0}; //全局的文件指针
    struct  ST_EXT_OVERLAPPED
    {
        OVERLAPPED m_ol; //后面的代码在使用的时候后
        HANDLE m_hFile; //操作的文件句柄
        LPVOID m_pData; //操作的内存
        DWORD m_dwLen;  //操作的数据长度
        BOOL bExit;
    };
    
    DWORD WriteThreadProc(LPVOID lpParameter)
    {
        HANDLE hFile = *(HANDLE*)(lpParameter);
        ST_EXT_OVERLAPPED* pExOl = NULL;
        TCHAR szBuf[256] = _T("");
        StringCchPrintf(szBuf, 256, _T("这是一条模拟日志写入信息,由线程[%04x]写入\r\n"), GetCurrentThreadId());
        size_t dwLen = 0;
        StringCchLength(szBuf, 256, &dwLen);
        dwLen += 1; //保存字符串结尾的\0
    
        for (int i = 0; i < 100; i++)
        {
            pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED));
            pExOl->m_dwLen = dwLen * sizeof(TCHAR);
            pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwLen * sizeof(TCHAR));
            StringCchCopy((TCHAR*)pExOl->m_pData, 256, szBuf);
            pExOl->m_hFile = hFile;
            pExOl->bExit = FALSE;
    
            //使用锁无关的方式进行同步操作
            *((LONGLONG*)&pExOl->m_ol.Pointer) = InterlockedCompareExchange64(&g_FilePointer.QuadPart, g_FilePointer.QuadPart + pExOl->m_dwLen, g_FilePointer.QuadPart);
            DWORD dwWritten = 0;
            WriteFile(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, &dwWritten, (OVERLAPPED*)&pExOl->m_ol);
        }
    
        return 0;
    }
    
    DWORD IocpThreadProc(LPVOID lpParameter)
    {
        HANDLE hIocp = *(HANDLE*)lpParameter;
        DWORD dwBytesTransfered = 0;
        DWORD dwFlags = 0;
        LPOVERLAPPED pOl = NULL;
        while (TRUE)
        {
            ST_EXT_OVERLAPPED* pExOl = NULL;
            BOOL bRet = GetQueuedCompletionStatus(hIocp, 0, 0, &pOl, INFINITE);//MSDN上说如果完成端口队列为空,那么函数会返回FLASE,并且pOl为NUULL, 所以在这进行判断,如果为FLASE,就不往下执行,否则程序会崩溃
            if (!bRet)
            {
                continue;
            }
            pExOl = (ST_EXT_OVERLAPPED*)pOl;
            if (pExOl->bExit)
            {
                printf("收到退出消息,IOCP线程[%04x]退出", GetCurrentThreadId());
                HeapFree(GetProcessHeap(), 0, pExOl);
                return 0;
            }
    
            printf("有一个线程的写入操作完成\n");
            HeapFree(GetProcessHeap(), 0, pExOl->m_pData);
            HeapFree(GetProcessHeap(), 0, pExOl);
        }
    }
    
    int _tmain(int argc, _TCHAR* argv[])
    {
        HANDLE hFile = CreateFile(_T("log.txt"), GENERIC_ALL, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);//让其支持异步操作
        if (hFile == INVALID_HANDLE_VALUE)
        {
            printf("CreateFile error\n");
            return GetLastError();
        }
    
        //创建IOCP内核对象并制定最大并发线程数
        SYSTEM_INFO si = {0};
        GetSystemInfo(&si);
        HANDLE hIocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, 2 * si.dwNumberOfProcessors);
        //创建IOCP线程
        HANDLE* hIocpThreads = (HANDLE*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 2 * si.dwNumberOfProcessors * sizeof(HANDLE));
        for (int i = 0; i < 2 * si.dwNumberOfProcessors; i++)
        {
            hIocpThreads[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)IocpThreadProc, &hIocp, 0, NULL);
        }
    
        //将文件句柄与IOCP句柄绑定
        CreateIoCompletionPort(hFile, hIocp, NULL, 0);
    
        ST_EXT_OVERLAPPED* pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED));
        pExOl->m_hFile = hFile;
        pExOl->m_dwLen = sizeof(WORD);
        pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WORD));
        *((WORD*)pExOl->m_pData) = MAKEWORD(0xff,0xfe);
        pExOl->bExit = FALSE;
    
        //文件指针的偏移
        pExOl->m_ol.Offset = g_FilePointer.LowPart;
        pExOl->m_ol.OffsetHigh = g_FilePointer.HighPart;
        g_FilePointer.QuadPart += pExOl->m_dwLen;
    
        DWORD dwWritten = 0;
        WriteFile(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, &dwWritten, (LPOVERLAPPED)&pExOl->m_ol);
    
        HANDLE hThreads[20] = {NULL};
    
    
        for (int i = 0; i < 20; i++)   //创建20个写线程
        {
            hThreads[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)WriteThreadProc, &hFile, 0, NULL);
        }
    
        //等待写入线程的完成
        WaitForMultipleObjects(20, hThreads, TRUE, INFINITE);
    
        for (int i = 0; i < 20; i++)
        {
            CloseHandle(hThreads[i]);
        }
    
        //关闭IOCP线程
        for (int i = 0; i < 2 * si.dwNumberOfProcessors; i++)
        {
            ST_EXT_OVERLAPPED* pExitMsg = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED));
            pExitMsg->bExit = TRUE;
            PostQueuedCompletionStatus(hIocp, 0, 0, &pExitMsg->m_ol);
        }
    
        //关闭IOCP线程句柄
        for (int i = 0; i < 2 * si.dwNumberOfProcessors; i++)
        {
            CloseHandle(hIocpThreads[i]);
        }
    
        CloseHandle(hFile);
    
        _tsystem(_T("PAUSE"));
        return 0;
    }
    展开全文
  • 在Linux的文件系统,保存在磁盘分区文件不管是什么类型都给它分配一个编号,称为索引节点号(Inode Index)。在Linux,多个文件名指向同一索引节点是存在的一般这种连接就是硬连接。硬连接...

    Linux链接分两种,一种被称为硬链接(Hard Link),另一种被称为符号链接(Symbolic Link)。默认情况下,ln命令产生硬链接。

    【硬连接】
    硬连接指通过索引节点来进行连接。在Linux的文件系统中,保存在磁盘分区中的文件不管是什么类型都给它分配一个编号,称为索引节点号(Inode Index)。在Linux中,多个文件名指向同一索引节点是存在的。一般这种连接就是硬连接。硬连接的作用是允许一个文件拥有多个有效路径名,这样用户就可以建立硬连接到重要文件,以防止“误删”的功能。其原因如上所述,因为对应该目录的索引节点有一个以上的连接。只删除一个连接并不影响索引节点本身和其它的连接,只有当最后一个连接被删除后,文件的数据块及目录的连接才会被释放。也就是说,文件真正删除的条件是与之相关的所有硬连接文件均被删除。

    【软连接】
    另外一种连接称之为符号连接(Symbolic Link),也叫软连接。软链接文件有类似于Windows的快捷方式。它实际上是一个特殊的文件。在符号连接中,文件实际上是一个文本文件,其中包含的有另一文件的位置信息。

    2.通过实验加深理解

    touch f1          #创建一个测试文件f1 注意:touch更多时候是被用来修改文件时间 Hacker修改别人文件后喜欢用来隐藏自己的操作时间记录
    ln f1 f2          #创建f1的一个硬连接文件f2
    ln -s f1 f3       #创建f1的一个符号连接文件f3

    ls -li            # -i参数显示文件的inode节点信息
    total 0
    9797648 -rw-r--r--  2 oracle oinstall 0 Apr 21 08:11 f1
    9797648 -rw-r--r--  2 oracle oinstall 0 Apr 21 08:11 f2
    9797649 lrwxrwxrwx  1 oracle oinstall 2 Apr 21 08:11 f3 -> f1

     

    从上面的结果中可以看出,硬连接文件f2与原文件f1的inode节点相同,均为9797648,然而符号连接文件的inode节点不同。
    使用操作来讲解软硬链接之间的关系

    echo "I am f1 file" >>f1
    cat f1
    I am f1 file
    cat f2
    I am f1 file
    cat f3
    I am f1 file
    rm -f f1
    cat f2
    I am f1 file
    cat f3
    cat: f3: No such file or directory

    通过上面的测试可以看出:当删除原始文件f1后,硬连接f2不受影响,但是符号连接f1文件无效

    3.总结
    依此您可以做一些相关的测试,可以得到以下全部结论:
    1).删除符号连接f3,对f1,f2无影响;
    2).删除硬连接f2,对f1,f3也无影响;
    3).删除原文件f1,对硬连接f2没有影响,导致符号连接f3失效;
    4).同时删除原文件f1,硬连接f2,整个文件会真正的被删除。

    展开全文
  • C语言文件操作

    2020-12-20 22:16:39
    C语言文件操作文件文件名文件类型文件缓冲区什么是文件缓冲区为什么存在文件缓冲区缓冲区刷新方式缓冲区刷新时机文件指针文件的打开与关闭C语言默认会打开的文件(设备)文件的顺序读写文件的随机读写...
  • C语言之文件

    2020-02-15 15:08:24
    所谓的文件并不是存在该项目中的一般指存储在磁盘的文件 我们在写c文件的时候经常引用stdio文件,该文件具有一个FILE 结构体类型。用数组存放若干文件。所以,利用这一特点,FILE t[5]表示存放5 个文件的信息...
  • 在Linux的文件系统,保存在磁盘分区文件不管是什么类型都给它分配一个编号,称为索引节点号。在Linux多个文件名指向同一索引节点是存在的一般这种连接就是硬连接。与硬连接相对应,Lnux系统还存在另一种...
  • 进程间通信的方式

    2016-11-29 21:47:34
    进程间通信就是不同进程之间传播或交换信息,那么不同进程之间存在着什么双方都...这个意义上,两个进程当然也可以通过磁盘普通文件交换信息,或者通过“注册表”或其它数据库中的某些表项和记录交换信息。广义
  • 进程间通信方式

    2016-09-22 22:30:38
    进程间通信就是不同进程之间传播或交换信息,那么不同进程之间存在着什么双方都...这个意义上,两个进程当然也可以通过磁盘普通文件交换信息,或者通过“注册表”或其它数据库中的某些表项和记录交换信息。广义
  • 实际应用我们会把这条命令和重定向符号(也称为管道符号,一般用> >> ^)结合来实现输入一些命令到特定格式的文件中.这将以后例子体现出来。 2.@ 命令 表示不显示@后面命令,入侵过程(例如使用...
  • 13.第十三章 文件.txt

    2019-11-08 16:33:27
    在程序运行时,常常需要将一些数据(运行最终结果或中间数据)输出到磁盘上保存,以后要用时再从磁盘中输入到计算机内存,这就要用到磁盘文件。 操作系统文件标识包括三部分: (1)文件路径:表示文件在...
  • 进程之间通信方式

    2016-03-20 19:05:00
    进程间通信就是不同进程之间传播或交换信息,那么不同进程之间存在着什么双方都...这个意义上,两个进程当然也可以通过磁盘普通文件交换信息,或者通过“注册表”或其它数据库中的某些表项和记录交换信息。...
  • 进程间通信就是不同进程之间传播或交换信息,那么不同进程之间存在着什么双方...这个意义上,两个进程当然也可以通过磁盘普通文件交换信息,或者通过“注册表”或其它数据库中的某些表项和记录交换信息。广...
  • 进程间通信 就是不同进程之间传播或交换信息,那么不同进程之间存在着什么双方...这个意义上,两个进程当然也可以通过磁盘普通文件交换信息,或者通过“注册表”或其它数据库中的某些表项和记录交换信息。广...
  •  企业数据量庞大,但难免也存在大量冗余文件,进行全部备份,也将占用不必要的磁盘空间,使用FileGee设置备份任务时,可以设置文件过滤,选择需要备份或同步部分文件,如是否包含子目录、跳过空目录、根据...
  • 进程间通信就是不同进程之间传播或交换信息,那么不同进程之间存在着什么...这个意义上,两个进程当然也可以通过磁盘普通文件交换信息,或者通过“注册表”或其它数据库中的某些表项和记录交换信息。广义...
  • 进程间通信就是不同进程之间传播或交换信息,那么不同进程之间存在着什么双方都...这个意义上,两个进程当然也可以通过磁盘普通文件交换信息,或者通过“注册表”或其它数据库中的某些表项和记录交换信息。广义
  • 进程间通信就是不同进程之间传播或交换信息,那么不同进程之间存在着什么双方都...这个意义上,两个进程当然也可以通过磁盘普通文件交换信息,或者通过“注册表”或其它数据库中的某些表项和记录交换信息。广义
  • 进程间通信方式与区别

    千次阅读 2015-12-23 02:53:07
    进程间通信就是不同进程之间传播或交换信息,那么不同进程之间存在着什么双方都...这个意义上,两个进程当然也可以通过磁盘普通文件交换信息,或者通过“注册表”或其它数据库中的某些表项和记录交换信息。广义
  • 这个意义上,两个进程当然也可以通过磁盘普通文件交换信息,或者通过“注册表”或其它数据库中的某些表项和记录交换信息。广义上这也是进程间通信手段,但是一般都不把这算作“进程间通信”。 二 、管道...
  • 进程间通信就是不同进程之间传播或交换信息,那么不同进程之间存在着什么双方都可以访问介质呢?...这个意义上,两个进程当然也可以通过磁盘普通文件交换信息,或者通过“注册表”或其它数据库中的某些表...
  • IPC进程之间通信几种方式

    千次阅读 2016-09-22 16:41:06
    进程间通信就是不同进程之间传播或交换信息,那么不同进程之间存在着什么双方都...这个意义上,两个进程当然也可以通过磁盘普通文件交换信息,或者通过“注册表”或其它数据库中的某些表项和记录交换信息。广义

空空如也

空空如也

1 2 3 4 5 ... 9
收藏数 174
精华内容 69
关键字:

一般文件在磁盘中存在的方式