第七章 硬盘和文件系统

作者:Maxwell Li
日期:2017/12/09
未经作者允许,禁止转载本文任何内容。如需转载请留言。


[TOC]

7.1 GPT 硬盘

7.1.1 基于 MBR 分区的传统硬盘

MBR(Master Boot Record)是硬盘的首个扇区(0号扇区)。0号扇区用于存放启动代码和主分区表。0 ~ 439字节为启动代码。446 ~ 509为主分区表,主分区表区域最多容纳4个分区表项。最后2字节是结束标志。

7.1.2 GPT 硬盘详解

MBR 分区和 GPT 分区对比

要素 MBR 分区 GPT 分区
块地址位数 32位 64位
分区数量 4个主分区 目前 UEFI 版本支持128个分区
可扩展性 有版本号,可扩展
分区表安全性 无备份 有备份分区表
分区表完整性 无完整性检查 分区表有 CRC32 校验

GPT 硬盘仍然划分为扇区,所有扇区统一编址,从0开始编号,扇区地址称为 LBA。

  • 0号扇区:保护性 MBR。
  • 1号扇区:GPT 表头。
  • 2 ~ 33号扇区:GPT 硬盘分区表。
  • 末尾33个扇区:备份分区表和 GPT 头。

保护性 MBR

保护性 MBR 主要作用是兼容 MBR 硬盘时代的工具,防止这些工具不识别 GPT 格式而破坏硬盘。对各个域有不同的要求:

  1. 启动代码域(0 ~ 439字节):对 UEFI 系统无效,将被忽略。
  2. 分区表项的第一项:必须包含整个硬盘,并且 BootIndicator 必须为0,OSType 标志为 0xEE。
  3. 其他分区表项:必须为0。
  4. 标志位(510 ~ 511字节):必须为 0xAA55。
  5. 其它字节:必须为0。

GPT 头

1号扇区称为 GPT 头,存储了硬盘的结构信息。

第七章 硬盘和文件系统_第1张图片
7-1.png

硬盘最后一个扇区用于备份 GPT 头,当计算机系统读取硬盘 GPT 头时,会计算表头的 CRC32 校验码,如果计算出的校验码与存储的不一致,则会尝试从备份表头恢复 GPT 头。

GPT 分区表

分区表项结构体:

typedef struct {
  EFI_GUID PartitionTypeGUID;    // 分区类型 GUID,0表示此项空
  EFI_GUID UniquePartitionGUID;  // 分区标志 GUID
  EFI_LBA StartingLBA;           // 分区的首扇区
  EFI_LBA EndingLBA;             // 分区的尾扇区
  UINT64 Attributes;             // 分区属性
  CHAR16 PartitionName[36];      // 分区名字字符串,必须以 0x0000 结尾
} EFI_PARTITION_ENTRY

分区属性 Attributes

名称 属性
0 必须分区 若此标志位为1,表明该分区对系统至关重要。本分区被破坏或删除将导致系统不能正常工作
1 无 BlockIo 分区 若此标志位为1,UEFI 不为该分区生成块设备
2 传统 BIOS 引导 仅供传统 BIOS 系统使用,UEFI 系统忽略该位
3 ~ 37 保留区1 必须为0
48 ~ 63 保留区2 保留给 PartitionTypeGUID 的拥有者使用

7.2 设备路径

设备路径中的节点称为设备节点。设备路径由设备节点组成的列表构成,列表由“结束设备节点”结束。

设备节点以 EFI_DEVICE_PATH_PROTOCOL 开始,它是设备节点的第一个域。从面向对象的角度来说,它是所有设备节点的基类。结构体如下:

typedef struct _EFI_DEVICE_PATH_PROTOCOL {
  UINT8 Type;       // 类型
  UINT8 SubType;    // 次类型
  UINT8 Length[2];  // 节点字节数
} EFI_DEVICE_PATH_PROTOCOL

UEFI 支持的设备类型和次类型

设备路径类型 类型 Type 值 设备次类型
硬件设备路径 0x01 1、PCI 设备;2、PCCARD;3、内存映射设备;4、vendor 自定义设备;5、控制器设备
ACPI 设备路径 0x02 1、ACPI 设备;2、扩展 ACPI 设备;3、_ADR 设备
Messaging 设备路径 0x03 1、ATAPI;2、SCSI;3、光纤;4、1394;5、USB;12、IPv4;13、IPv6;15、USB;18、SATA;20、802.1q
介质设备路径 0x04 1、硬盘;2、光驱;3、vendor 自定义设备;4、文件路径;5、介质 Protocol;6、固件文件路径;7、固件卷
BIOS 启动设备路径 0x05 用于启动不支持 UEFI 的操作系统
结束节点设备路径 0x7F

硬盘设备(HARDDRIVE_DEVICE_PATH)节点

偏移 域长度 域类型 域的含义
Type 0 1 UINT8 介质设备路径,值为 0x4
SubType 1 1 UINT8 硬盘,值为1
Length 2 2 UINT16 长度,大小为42字节
PartitionNumber 4 4 UINT32 该分区在分区表中的位置,从1开始编号。
0表示整个硬盘设备。
对 MBR 硬盘来说,有效值是1、2、3、4中的一个;
对 GPT 硬盘来说,有效值的范围是[1,分区个数]。
PartitionStart 8 8 UINT64 该分区第一个扇区的 LBA 地址
PartitionSize 16 8 UINT64 该分区的扇区数
Signature 24 16 UINT8[16] 该分区的标识符。
如果为0,本域必须为0;
如果为1,本域前4字节有效,后12字节为0;
如果为2,本域必须是一个有效的 GUID
MBRType 40 1 UINT8 1表示 MBR 硬盘,2表示 GPT 硬盘
SignatureType 41 1 UINT8 分区标识符的类。
0表示无分区标识符;
1表示 MBR 分区(32位)标识符;
2表示标识符为 GUID(16位)

UEFI 提供 EFI_DEVICE_PATH_TO_EXIT_PROTOCOL 将设备路径转换为字符串。函数原型如下:

typedef CHAR16*(EFIAPI *EFI_DEVICE_PATH_TO_EXIT_PROTOCOL)(
  IN CONST EFI_DEVICE_PATH_TO_EXIT_PROTOCOL *DevicePath,
  IN BOOLEAN DisplayOnly,
  IN BOOLEAN AllowShortcuts
  );
typedef struct {
  EFI_DEVICE_PATH_TO_EXIT_NODE ConvertDeviceNodeToText;  // 将设备节点转换为字符串
  EFI_DEVICE_PATH_TO_EXIT_PATH ConvertDevicePateToText;  // 将设备路径转换为字符串
} EFI_DEVICE_PATH_TO_EXIT_PROTOCOL;
  • 若 DisplayOnly 为 TRUE,则生成的字符串为短格式,不能用于被分析后生成设备路径;
  • 若 DisplayOnly 为 FALSE,则生成的字符串为长格式,可以被分析后反向生成设备路径。

7.3 硬盘相关的 Protocol

每个硬盘设备(硬盘设备包括分区设备)控制器都安装有一个 BlockIo 实例、一个 BlockIo2 实例。 BlockIo 提供访问设备的阻塞函数;BlockIo2 提供访问设备的异步函数。

并且都安装有一个 DiskIo 实例、一个 DiskIo2 实例。 DiskIo 提供访问硬盘的阻塞函数;DiskIo2 提供访问硬盘的异步函数。

BlockIo 与 DiskIo 的区别:

  • BlockIo 只能按块(扇区)读写设备。
  • DiskIo 可以从任意偏移处(以字节为单位)读写磁盘,并可以读取任意字节数。

7.3.1 BlockIo 解析

BlockIo 是 EFI_BLOCK_IO_PROTOCOL 的缩写,包含两个属性:Revision、Media 以及四个成员函数:Reset、ReadBlocks、WriteBlocks、FlushBlocks。结构体如下:

typedef struct _EFI_BLOCK_IO_PROTOCOL {
  UINT64 Revision;              // Protocol 版本号
  EFI_BLOCK_IO_MEDIA *Media;    // 设备信息
  EFI_BLOCK_RESET Reset;        // 重置设备
  EFI_BLOCK_READ ReadBlocks;    // 读扇区
  EFI_BLOCK_WRITE WriteBlocks;  // 写扇区
  EFI_BLOCK_FLUSH FlushBlocks;  // 将缓冲中的数据写入扇区
} EFI_BLOCK_IO_PROTOCOL;

设备信息 Media

Media 指向该设备的 EFI_BLOCK_IO_MEDIA 结构体,结构体如下:

typedef struct {
  UINT32 MediaId;
  BOOLEAN RemovableMedia;
  BOOLEAN MediaPresent;      // 设备中是否有介质
  BOOLEAN LogicalPartition;  // 该介质是分区(TRUE);或整个设备(FALSE)
  BOOLEAN ReadOnly;
  BOOLEAN WriteCaching;      // 是否用 cache 方式写硬盘
  UINT32 BlockSize;
  UINT32 IoAlign;            // 读写硬盘时缓冲区地址对齐字节数,0或2^n。0和1表示可以是任意地址
  EFI_LBA LastBlock;         // 设备最后一个块的 LBA 地址
  // 以下三项只出现在 EFI_BLOCK_IO_PROTOCOL_REVISION2 及以上版本
  // 当该 Protocol 安装在分区设备上时,数值全为0
  EFI_LBA LowestAlignedLba;
  UINT32 LogicalBlocksPerPhysicalBlock;
  UINT32 OptimalTransferLengthGranularity;
} EFI_BLOCK_IO_MEDIA;

ReadBlocks 函数

ReadBlocks 只能按块读取设备,函数原型如下:

typedef EFI_STATUS(EFIAPI *EFI_BLOCK_READ) (
  IN EFI_BLOCK_IO_PROTOCOL *This,
  IN UINT32 MediaId,    // 设备中的介质号
  IN EFI_LBA LBA,       // 读取设备(或分区)起始块的 LBA 地址
  IN UINTN BufferSize,  // 读取的字节数,必须是块大小的整数倍
  OUT VOID *Buffer      // 读取的数据存到此缓冲区,调用者负责管理此内存
  );

WriteBlocks 函数

WriteBlocks 用于按块写设备,函数原型如下:

typedef EFI_STATUS(EFIAPI *EFI_BLOCK_WRITE) (
  IN EFI_BLOCK_IO_PROTOCOL *This,
  IN UINT32 MediaId,    // 设备中的介质号
  IN EFI_LBA LBA,       // 写入设备(或分区)起始块的 LBA 地址
  IN UINTN BufferSize,  // 写入的字节数,必须是块大小的整数倍
  IN VOID *Buffer       // 从缓冲区写数据到设备,调用者负责管理此内存
  );

FlushBlock 函数

FlushBlocks 用于将设备缓存中修改过的数据全部更新到介质中。

7.3.2 BlockIo2 解析

为了提高性能,UEFI 提供异步读写块设备的 Protocol:BlockIo2(EFI_BLOCK_IO2_PROTOCOL),结构体如下:

typedef struct _EFI_BLOCK_IO2_PROTOCOL {
  EFI_BLOCK_IO_MEDIA *Media;         // 设备信息
  EFI_BLOCK_RESET_EX Reset;          // 重置设备(阻塞函数)
  EFI_BLOCK_READ_EX ReadBlocksEx;    // 异步读设备
  EFI_BLOCK_WRITE_EX WriteBlocksEx;  // 异步写设备
  EFI_BLOCK_FLUSH_EX FlushBlocksEx;  // 异步 Flush 设备
} EFI_BLOCK_IO2_PROTOCOL;

ReadBlocksEx 函数

通常异步操作需要事件的支持,还需要上下文用于传递数据。在 UEFI 规范中,异步操作的事件和上下文封装起来称为令牌(Token),不同的异步操作有不同类型的令牌。一个拥有令牌的异步操作也称为一个事务。ReadBlocksEx 的令牌为 EFI_BLOCK_IO2_TOKEN。

ReadBlocksEx 函数向设备发出读指令后立刻返回。设备完成操作后会触发 Token 中的事件。

  • 如果函数返回错误,则表示指令没有发送到设备,事件不会触发。
  • 如果 Token 为 NULL 或 Token->Event 为 NULL,则该函数将退化为阻塞函数,功能与 ReadBlocks 相同。

Token 由调用者负责创建和删除。函数原型如下:

typedef EFI_STATUS(EFIAPI *EFI_BLOCK_READ_EX) (
  IN EFI_BLOCK_IO2_PROTOCOL *This,
  IN UINT32 MediaId,                  // 设备中的介质号
  IN EFI_LBA LBA,                     // 读取设备(或分区)起始块的 LBA 地址
  IN OUT EFI_BLOCK_IO2_TOKEN *Token,  // 此事务对应的 Token,调用者负责生成和关闭
  IN  UINTN BufferSize,               // 读取的字节数,必须是块大小的整数倍
  OUT VOID *Buffer                    // 读取的数据存到此缓冲区,调用者负责管理此内存
  ); 

有两种方式等待异步读硬盘完成:

  • 使用 WaitForEvent 等待异步读结束后再执行后续任务。流程如下:
    1. 生成 EFI_BLOCK_IO2_TOKEN 对象,主要是生成 Token 中的事件对象。
    2. 通过 ReadBlocksEx 函数向设备发出读指令。
    3. 执行其他任务。
    4. 通过 WaitForEvent 服务等待 Token 中的事件,然后处理读回的数据。
  • 在事件的 Notification 函数中完成后续任务。
    1. 生成 EFI_BLOCK_IO2_TOKEN 对象,主要是生成 Token 中的事件对象,定义事件的 Notification 函数,该函数的主要作用是处理读到的数据。
    2. 通过 ReadBlocksEx 函数向设备发出读指令。
    3. 执行其他任务。
    4. 通过 WaitForEvent 服务等待 Token 中的事件。WaitForEvent 返回时,Notification 函数已经由系统执行完毕。

WriteBlocksEx 函数

WriteBlocksEx 用于异步写数据到硬盘扇区,与 ReadBlocksEx 相似。函数原型如下:

typedef EFI_STATUS(EFIAPI *EFI_BLOCK_WRITE_EX) (
  IN EFI_BLOCK_IO2_PROTOCOL *This,
  IN UINT32 MediaId,                  // 设备中的介质号
  IN EFI_LBA LBA,                     // 写入设备(或分区)起始块的 LBA 地址
  IN OUT EFI_BLOCK_IO2_TOKEN *Token,  // 此事务对应的 Token,调用者负责生成和关闭
  IN UINTN BufferSize,                // 写入的字节数,必须是块大小的整数倍
  IN VOID *Buffer                     // 从缓冲区写数据到设备,调用者负责管理此内存
  ); 

7.3.3 DiskIo 解析

利用 DiskIo(EFI_DISK_IO_PROTOCOL) 可以从磁盘任意地址读写任意长度的数据。其结构体如下:

typedef struct _EFI_DISK_IO_PROTOCOL {
  UINT64 Revision;           // 版本号
  EFI_DISK_READ ReadDisk;    // 读磁盘
  EFI_DISK_WRITE WriteDisk;  // 写磁盘
} EFI_DISK_IO_PROTOCOL;

ReadDisk 函数

ReadDisk 函数原型:

typedef EFI_STATUS(EFIAPI *EFI_DISK_READ) (
  IN EFI_DISK_IO_PROTOCOL *This,
  IN UINT32 MediaId,    // 介质号
  IN UINT64 Offset,     // 从偏移 Offset(以字节为单位)处开始读数据
  IN UINTN BufferSize,  // 读取数据的长度
  OUT VOID *Buffer      // 数据读取到此缓冲区,调用者负责管理此内存
  );

WriteDisk 函数

WriteDisk 函数原型:

typedef EFI_STATUS(EFIAPI *EFI_DISK_WRITE) (
  IN EFI_DISK_IO_PROTOCOL *This,
  IN UINT32 MediaId,
  IN UINT64 Offset,
  IN UINTN BufferSize,
  IN VOID *Buffer
  );

7.3.4 DiskIo2 解析

DiskIo2(EFI_DISK_IO2_PROTOCOL)建立在 BlockIo2 基础上,是异步的磁盘操作协议。结构体如下:

typedef struct _EFI_DISK_IO2_PROTOCOL {
  UINT64 Revision;
  EFI_DISK_CANCEL_EX Cancel;      // 取消磁盘设备上处于等待状态的事件
  EFI_DISK_READ_EX ReadDiskEx;    // 异步读磁盘
  EFI_DISK_WRITE_EX WriteDiskEx;  // 异步写磁盘
  EFI_DISK_FLUSH_EX FlushDiskEx;  // 异步 Flush 磁盘
} EFI_DISK_IO2_PROTOCOL;

ReadDiskEx 函数

ReadDiskEx 是 ReadDisk 的异步版,用于异步读硬盘。函数原型如下:

typedef EFI_STATUS(EFIAPI *EFI_DISK_READ_EX) (
  IN EFI_DISK_IO2_PROTOCOL *This,
  IN UINT32 MediaId,                 // 介质号
  IN UINT64 Offset,                  // 从 Offset(以字节为单位)处开始读数据
  IN OUT EFI_DISK_IO2_TOKEN *Token,
  IN UINTN BufferSize,               // 读取数据的长度
  OUT VOID *Buffer                   // 数据读取到此缓冲区,调用者负责管理此内存
  );  
  • 如果 Token 为 NULL 或 Token 中的事件无效,则该函数退化为阻塞操作,相当于 ReadDisk 函数。
  • 如果 Token 中的事件有效,则该函数想设备发出读指令后立即返回,系统在该事务完成后自动触发 Token 中的事件。

WriteDiskEx 函数

WriteDiskEx 函数原型:

typedef EFI_STATUS(EFIAPI *EFI_DISK_WRITE_EX) (
  IN EFI_DISK_IO2_PROTOCOL *This,
  IN UINT32 MediaId,
  IN UINT64 Offset,
  IN OUT EFI_DISK_IO2_TOKEN *Token,
  IN UINTN BufferSize,
  IN VOID *Buffer
  );

7.3.5 PassThrough 解析

UEFI 标准中定义了两种 PassThrough:一种用于 ATA(Advanced Technology Attachment)硬盘,另一种用于 SCSI 硬盘和 ATAPI(AT Attachment Program Interface)硬盘。ATA 设备的 PassThrough Protocol 数据结构如下:

struct _EFI_ATA_PASS_THRU_PROTOCOL {
  EFI_ATA_PASS_THRU_MODE *Mode;                         // 设备模式
  EFI_ATA_PASS_THRU_PASSTHRU PassThru;                  // 向 ATA 设备发送 ATA 命令
  EFI_ATA_PASS_THRU_GET_NEXT_PORT GetNextPort;          // 列出 ATA 设备的端口号
  EFI_ATA_PASS_THRU_GET_NEXT_DEVICE GetNextDevice;      // 列出指定端口上的 Port Multiplier
  EFI_ATA_PASS_THRU_BUILD_DEVICE_PATH BuildDevicePath;  // 设备生成 DevicePath 节点
  EFI_ATA_PASS_THRU_GET_DEVICE GetDevice;               // 获得 DevicePath 对应的端口和 Port Multiplier
  EFI_ATA_PASS_THRU_RESET_PORT ResetPort;               // 重置指定端口及其端口上的设备
  EFI_ATA_PASS_THRU_RESET_DEVICE ResetDevice;           // 重置指定的设备
};

PassThru 的作用是向设备发送命令包,设备由 Port 和 PortMultiplierPort 决定。

  • 若指定参数 Event,则 PassThru 采用异步方式。
  • 若参数 Event 设为 NULL,则 PassThru 采用阻塞模式。

BlockIo 建立在 PassThrough 基础之上,用于读写硬盘扇区;DiskIo 建立在 BlockIo 基础之上,提供了读写硬盘任意地址处数据的能力。

7.4 文件系统

每个 UEFI 系统至少有一个 ESP(EFI System Partition)分区,用于存放启动文件。操作系统加载器以文件的形式存放在 ESP 分区,UEFI 通过文件系统来完成读写操作。UEFI 内置了 EFI_SIMPLE_FILE_SYSTEM_PROTOCOL(FileSyetemIo)来操作 FAT 文件系统,FileSystemIo 建立在 DiskIo 基础上,结构体如下:

typedef struct _EFI_SIMPLE_FILE_SYSTEM_PROTOCOL {
  UINT64 Revision;
  EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_OPEN_VOLUME OpenVolume; // 打开卷并获得根目录句柄
} EFI_SIMPLE_FILE_SYSTEM_PROTOCOL;
// 打开卷,获得该卷上的根目录句柄,即根目录文件操作接口 EFI_FILE_PROTOCOL
typedef EFI_STATUS(EFIAPI *EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_OPEN_VOLUME)(
  IN EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *This,
  OUT EFI_FILE_PROTOCOL **Root  // 返回根目录句柄
  );

7.5 文件操作

EFI_FILE_HANDLE 是指向 EFI_FILE_PROTOCOL(FileIo)的指针。这里文件视为一种特殊的设备,EFI_FILE_PROTOCOL 被安装到文件设备上,其指针被当作文件句柄。EFI_FILE_PROTOCOL 用于操作设备上的文件或目录,结构体如下:

typedef struct _EFI_FILE_PROTOCOL {
  UINT64                Revision;
  EFI_FILE_OPEN         Open;
  EFI_FILE_CLOSE        Close;
  EFI_FILE_DELETE       Delete;
  EFI_FILE_READ         Read;
  EFI_FILE_WRITE        Write;
  EFI_FILE_GET_POSITION GetPosition;
  EFI_FILE_SET_POSITION SetPosition;
  EFI_FILE_GET_INFO     GetInfo;  // 获得文件属性
  EFI_FILE_SET_INFO     SetInfo;  // 设置文件属性
  EFI_FILE_FLUSH        Flush;    // 将文件中修改过的内容全部更新到设备
  EFI_FILE_OPEN_EX      OpenEx;
  EFI_FILE_READ_EX      ReadEx;
  EFI_FILE_WRITE_EX     WriteEx;
  EFI_FILE_FLUSH_EX     FlushEx;
} EFI_FILE_PROTOCOL;

7.5.1 打开文件

EFI_FILE_OPEN 函数用于打开文件或目录,函数原型如下:

typedef EFI_STATUS(EFIAPI *EFI_FILE_OPEN)(
  IN EFI_FILE_PROTOCOL  *This,        // EFI_FILE_PROTOCOL 实例,通常是一个目录的句柄
  OUT EFI_FILE_PROTOCOL **NewHandle,  // 返回打开文件的句柄
  IN CHAR16             *FileName,    // 文件名,以 NULL 结尾的 Unicode 字符串
  IN UINT64             OpenMode,     // 打开模式,表明打开文件用于读、写、创建
  IN UINT64             Attributes    // 文件属性,仅在产生文件时有效
  );
  • 如果 FileName 指定的文件路径是相对路径(非 \ 开头),该相对路径起始于 This 对应的目录。
  • 如果 FileName 指定的文件路径是绝对路径(以 \ 开头),该文件路径起始于 This 所在文件系统的根目录。

文件打开模式(OpenMode)

模式 用途
EFI_FILE_MODE_READ 文件用于读
EFI_FILE_MODE_WRITE 文件用于写
EFI_FILE_MODE_CHEATE 若文件不存在,则创建

创建文件时,可指定文件属性。

文件属性

属性 功能
EFI_FILE_READ_ONLY 只读文件
EFI_FILE_HIDDEN 隐藏文件
EFI_FILE_SYSTEM 系统文件
EFI_FILE_RESERVED 保留
EFI_FILE_DIRECTORY 目录
EFI_FILE_ARCHIVE 归档文件
EFI_FILE_VALID_ATTR 有效属性位

打开成功后,系统为文件(或目录)生成文件对象 FAT_IFILE,并返回文件对象中的 EFI_FILE_PROTOCOL 指针作为文件句柄。

7.5.2 读文件

EFI_FILE_READ 函数用于读文件或目录,函数原型如下:

typedef EFI_STATUS(EFIAPI *EFI_FILE_READ)(
  IN EFI_FILE_PROTOCOL *This,        // 文件句柄
  IN OUT UINTN         *BufferSize,  // 输入:要读出的的数据长度;输出:实际读出的数据长度
  OUT VOID             *Buffer       // 读缓冲区
  );
  • This 指向文件。Read 函数将从文件当前位置处读 BufferSize 个字节到 Buffer 缓冲区,如果文件当前位置到文件末尾小于 BufferSize,则读至文件末尾。返回后 BufferSize 存放实际读取的字节数。
  • This 指向目录。Read 函数将读取该目录下的文件及目录(不包含子目录下的文件和目录)的目录项 EFI_FILE_INFO。
    • 如果给定的缓冲区小于目录项大小,返回 EFI_BUFFER_TOO_SMALL,同时 BufferSize 返回需要的缓冲区大小。
    • 如果当前位置到达该目录下目录项的末尾,返回 EFI_SUCCESS,BufferSize 返回0。

EFI_FILE_INFO 结构体如下:

typedef struct {
  UINT64    Size;             // EFI_FILE_INFO 结构的大小,包括 FileName 字符串(及串尾的0)的长度
  UINT64    FileSize;         // 文件的长度
  UINT64    PhysicalSize;     // 文件在文件系统卷上占用的实际字节数
  EFI_TIME  CreateTime;
  EFI_TIME  LastAccessTime;
  EFI_TIME  ModificationTime;
  UINT64    Attribute;
  CHAR16    FileName[1];
} EFI_FILE_INFO;

7.5.3 写文件

EFI_FILE_WRITE 用于写数据到文件,函数原型如下:

typedef EFI_STATUS (EFIAPI *EFI_FILE_WRITE)(
  IN EFI_FILE_PROTOCOL *This,        // 文件句柄
  IN OUT UINTN         *BufferSize,  // 输入:要写入的数据长度;输出:实际写入的数据长度
  IN VOID              *Buffer       // 待写入数据
  ); 

Write 只能写数据到文件,不能写数据到目录。通常 Write 函数会写 BufferSize 指定的字节数到文件中(实际写的字节数等于指定要写的字节数),仅在遇到错误时,只写入部分数据,BufferSize 返回实际写的字节数。

7.5.4 关闭文件(句柄)

文件操作完毕后,要关闭文件句柄。Close 函数只有一个参数,即要关闭的文件句柄。

7.5.5 其他文件操作

读写文件位置

EFI_FILE_PROTOCOL 提供了 GetPosition 和 SetPosition 用于获取和设置文件的当前位置,函数原型如下:

/**
  @retval EFI_SUCCESS      成功获取当前位置
  @retval EFI_UNSUPPORTED  This 指向目录句柄,无法获得目录的当前位置
  @retval EFI_DEVICE_ERROR This 指向的文件已删除或其他设备错误
**/
typedef EFI_STATUS(EFIAPI *EFI_FILE_GET_POSITION)(
  IN EFI_FILE_PROTOCOL *This,     // 文件句柄
  OUT UINT64           *Position  // 返回文件的当前位置(从文件开头算起)
  );
/**
  @retval EFI_SUCCESS      成功设置文件位置
  @retval EFI_UNSUPPORTED  This 指向目录句柄,无法设置目录的当前位置
  @retval EFI_DEVICE_ERROR This 指向的文件已删除或其他设备错误
**/
typedef EFI_STATUS(EFIAPI *EFI_FILE_SET_POSITION)(
  IN EFI_FILE_PROTOCOL *This,    // 文件句柄
  IN UINT64            Position  // 要设定的文件位置
  );

读写文件信息

EFI_FILE_PROTOCOL 提供了 GetInfo 和 SetInfo 用于读取和设置文件信息,函数原型如下:

typedef EFI_STATUS(EFIAPI *EFI_FILE_GET_INFO)(
  IN EFI_FILE_PROTOCOL *This,             // 文件(或目录)句柄
  IN EFI_GUID          *InformationType,  // 要获取信息的类型标识符
  IN OUT UINTN         *BufferSize,       // 输入:缓冲区大小;输出:返回数据的长度
  OUT VOID             *Buffer            // 读缓冲区,数据类型由 InformationType 决定
  );
typedef EFI_STATUS (EFIAPI *EFI_FILE_SET_INFO)(
  IN EFI_FILE_PROTOCOL *This,             // 文件(或目录)句柄
  IN EFI_GUID          *InformationType,  // 要设置信息的类型标识符
  IN UINTN             BufferSize,        // InformationType 指定的数据的字节数
  IN VOID              *Buffer            // InformationType 指定的数据
  );

InformationType 有三种类型:

  • EFI_FILE_INFO(标识符 EFI_FILE_INFO_ID),结构体见 Read 函数。
  • EFI_FILE_SYSTEM_VOLUME_LABEL(标识符 EFI_FILE_SYSTEM_VOLUME_LABEL_ID)包含了用于表示卷标的字符串。
  • EFI_FILE_SYSTEM_INFO(标识符 EFI_FILE_SYSTEM_INFO_ID)包含了文件系统的相关信息,其中只有 VolumeLabel 可写,其他成员只读。结构体如下:
typedef struct {
  UINT64  Size;            // EFI_FILE_SYSTEM_INFO 结构的大小,包括 VolumeLabel(及串尾的0)的长度
  BOOLEAN ReadOnly;        // 只读卷
  UINT64  VolumeSize;      // 卷大小
  UINT64  FreeSpace;       // 文件系统剩余空间
  UINT32  BlockSize;       // 文件以此大小的块增加长度
  CHAR16  VolumeLabel[1];  // 卷名字符串(以 NULL 结尾)
} EFI_FILE_SYSTEM_INFO;

7.5.6 异步文件操作

异步文件操作需要提供一个 EFI_FILE_IO_TOKEN 类型的令牌,数据结构如下:

typedef struct {
  EFI_EVENT   Event;       // 此事务对应的事件
  EFI_STATUS  Status;      // Event 触发后,事务的状态
  UINTN       BufferSize;  // 作为输入:表示请求操作的字节数;作为输出:表示实际操作的字节数
  VOID        *Buffer;
} EFI_FILE_IO_TOKEN;

异步操作文件的步骤:

  1. 初始化 Token,包括 Token 中的 Event,如果时 ReadEx 或 WriteEx,还需要初始化 Buffer 和 BufferSize。
  2. 发出异步命令。
  3. 等待 Token 中的事件触发。
  4. 检查 Token 中的状态。
  5. 清理资源。

异步打开

OpenEx 用于异步打开文件或目录,当 Token 无效时,退化为阻塞操作。函数原型如下:

typedef EFI_STATUS(EFIAPI *EFI_FILE_OPEN_EX)(
  IN EFI_FILE_PROTOCOL      *This,        // EFI_FILE_PROTOCOL 实例,通常是一个目录的句柄
  OUT EFI_FILE_PROTOCOL     **NewHandle,  // 返回打开文件的句柄
  IN CHAR16                 *FileName,    // 文件名,以 NULL 结尾的 Unicode 字符串
  IN UINT64                 OpenMode,     // 打开模式,表明打开文件用于读、写、创建
  IN UINT64                 Attributes,   // 文件属性,仅在产生文件时有效
  IN OUT EFI_FILE_IO_TOKEN  *Token        // 每个事务都需要一个有效的 Token
  );

异步读/写文件

ReadEx 用于异步读文件,WriteEx 用于异步写文件,函数原型如下:

typedef EFI_STATUS(EFIAPI *EFI_FILE_READ_EX) (
  IN EFI_FILE_PROTOCOL      *This,
  IN OUT EFI_FILE_IO_TOKEN  *Token
);
typedef EFI_STATUS (EFIAPI *EFI_FILE_WRITE_EX) (
  IN EFI_FILE_PROTOCOL      *This,
  IN OUT EFI_FILE_IO_TOKEN  *Token 
);

有两种方式可以完成异步文件读写:

  • 使用 WaitForEvent 等待异步读结束后再执行后续任务;
  • 在事件的 Notification 函数中完成后续任务。

7.5.7 EFI_SHELL_PROTOCOL 中的文件操作

EFI_SHELL_PROTOCOL 中的文件操作接口对 EFI_FILE_PROTOCOL 进行了封装和扩充,除了提供基本的文件访问函数之外,还提供了相对目录的操作方式,以及 stdin、stdout、stderr 及文件查找函数。只有 Shell 下的应用程序才能使用 EFI_SHELL_PROTOCOL 中的文件操作函数。结构体如下:

typedef struct _EFI_SHELL_PROTOCOL {
  EFI_SHELL_EXECUTE                         Execute;
  EFI_SHELL_GET_ENV                         GetEnv;
  EFI_SHELL_SET_ENV                         SetEnv;
  EFI_SHELL_GET_ALIAS                       GetAlias;
  EFI_SHELL_SET_ALIAS                       SetAlias;
  EFI_SHELL_GET_HELP_TEXT                   GetHelpText;
  EFI_SHELL_GET_DEVICE_PATH_FROM_MAP        GetDevicePathFromMap;
  EFI_SHELL_GET_MAP_FROM_DEVICE_PATH        GetMapFromDevicePath;
  EFI_SHELL_GET_DEVICE_PATH_FROM_FILE_PATH  GetDevicePathFromFilePath;
  EFI_SHELL_GET_FILE_PATH_FROM_DEVICE_PATH  GetFilePathFromDevicePath;
  EFI_SHELL_SET_MAP                         SetMap;
  EFI_SHELL_GET_CUR_DIR                     GetCurDir;
  EFI_SHELL_SET_CUR_DIR                     SetCurDir;
  EFI_SHELL_OPEN_FILE_LIST                  OpenFileList;
  EFI_SHELL_FREE_FILE_LIST                  FreeFileList;
  EFI_SHELL_REMOVE_DUP_IN_FILE_LIST         RemoveDupInFileList;
  EFI_SHELL_BATCH_IS_ACTIVE                 BatchIsActive;
  EFI_SHELL_IS_ROOT_SHELL                   IsRootShell;
  EFI_SHELL_ENABLE_PAGE_BREAK               EnablePageBreak;
  EFI_SHELL_DISABLE_PAGE_BREAK              DisablePageBreak;
  EFI_SHELL_GET_PAGE_BREAK                  GetPageBreak;
  EFI_SHELL_GET_DEVICE_NAME                 GetDeviceName;
  EFI_SHELL_GET_FILE_INFO                   GetFileInfo;
  EFI_SHELL_SET_FILE_INFO                   SetFileInfo;
  EFI_SHELL_OPEN_FILE_BY_NAME               OpenFileByName;
  EFI_SHELL_CLOSE_FILE                      CloseFile;
  EFI_SHELL_CREATE_FILE                     CreateFile;
  EFI_SHELL_READ_FILE                       ReadFile;
  EFI_SHELL_WRITE_FILE                      WriteFile;
  EFI_SHELL_DELETE_FILE                     DeleteFile;
  EFI_SHELL_DELETE_FILE_BY_NAME             DeleteFileByName;
  EFI_SHELL_GET_FILE_POSITION               GetFilePosition;
  EFI_SHELL_SET_FILE_POSITION               SetFilePosition;
  EFI_SHELL_FLUSH_FILE                      FlushFile;
  EFI_SHELL_FIND_FILES                      FindFiles;
  EFI_SHELL_FIND_FILES_IN_DIR               FindFilesInDir;
  EFI_SHELL_GET_FILE_SIZE                   GetFileSize;
  EFI_SHELL_OPEN_ROOT                       OpenRoot;
  EFI_SHELL_OPEN_ROOT_BY_HANDLE             OpenRootByHandle;
  EFI_EVENT                                 ExecutionBreak;
  UINT32                                    MajorVersion;
  UINT32                                    MinorVersion;
  // Added for Shell 2.1
  EFI_SHELL_REGISTER_GUID_NAME              RegisterGuidName;
  EFI_SHELL_GET_GUID_NAME                   GetGuidName;
  EFI_SHELL_GET_GUID_FROM_NAME              GetGuidFromName;
  EFI_SHELL_GET_ENV_EX                      GetEnvEx;
} EFI_SHELL_PROTOCOL;

EFI_SHELL_PROTOCOL 中文件操作相关函数的第一个参数不再是 This 指针,通常是 Shell 文件句柄。

你可能感兴趣的:(第七章 硬盘和文件系统)