pkey_alloc pkey_free系统调用及示例
我们继续按照您的要求学习 Linux 系统编程中的重要函数。这次我们介绍 pkey_alloc
和 pkey_free
。
1. 函数介绍
pkey_alloc
和 pkey_free
是一组与 内存保护键(Memory Protection Keys, MPK)相关的 Linux 系统调用(内核版本 >= 4.9, x86 架构)。它们是 Intel MPK (Memory Protection Keys) 特性的用户态接口。
**核心概念:内存保护键 **(Protection Keys)
想象你的内存是一间大房子,里面有各种不同的房间(内存页)。传统上,你只有一把总钥匙(页表项中的权限位 RWX
)来控制进入这间房子的所有门(内存访问)。如果这把钥匙丢了或者被复制,坏人就能进入所有房间。
内存保护键(MPK)就像是给这间大房子加装了多把独立的锁(保护键):
- 获取钥匙 (
pkey_alloc
) 你向操作系统申请一把新的、独立的钥匙(保护键)。操作系统给你一个钥匙编号(pkey)。 - **给房间上锁 **(
pkey_mprotect
) 你可以使用pkey_mprotect
系统调用,将特定的房间(内存区域)与你刚申请到的那把钥匙(pkey)关联起来。这相当于给这些房间的门加上了这把新锁。 - **控制钥匙 **(特殊寄存器) CPU 内部有一个特殊的寄存器(x86 上是
PKRU
– Protection Key Rights User register)。这个寄存器里有 16 个插槽(对应 16 个可能的 pkey),每个插槽可以设置为允许或禁止访问。 - 尝试进入房间: 当程序试图访问一个与特定 pkey 关联的内存页时:
- CPU 会检查页表项中的 pkey 编号(例如 3)。
- 然后检查
PKRU
寄存器中对应插槽(第 3 个插槽)的权限。 - 如果
PKRU
允许访问(例如,插槽 3 是 0b00),访问成功。 - 如果
PKRU
禁止访问(例如,插槽 3 是 0b01 或 0b10),CPU 会立即产生一个SIGSEGV
(段错误) 信号,而无需进行昂贵的页表遍历。
pkey_alloc
和 pkey_free
的作用:
pkey_alloc
: 申请一个可用的内存保护键(pkey)。成功时返回一个唯一的 pkey 编号(0-15)。pkey_free
: 释放一个之前通过pkey_alloc
申请的 pkey,使其可以被其他部分的程序再次申请使用。
优势:
- 快速权限切换: 通过修改
PKRU
寄存器(一个非常快的操作),可以瞬间改变对大量内存区域的访问权限,而无需修改每个内存页的页表项。 - 细粒度保护: 可以将不同的内存区域分配给不同的 pkey,实现更细粒度的内存访问控制。
- 硬件加速: 权限检查由 CPU 硬件直接完成,性能极高。
相关文章:pkey_alloc系统调用及示例-CSDN博客 Linux 3.0 内核系统调用 preadv2系统调用及示例
2. 函数原型
#include <sys/mman.h> // 包含 MPK 相关常量和函数声明 (需要较新的 glibc)
// 分配一个保护键
int pkey_alloc(unsigned int flags, unsigned int access_rights);
// 释放一个保护键
int pkey_free(int pkey);
注意: 这些函数需要 glibc 2.27 或更高版本。在较旧的系统上,可能需要直接使用 syscall
。
3. 功能
pkey_alloc
:- 向内核请求一个当前未被使用的内存保护键。
- 内核会返回一个唯一的 pkey 编号(通常在 0 到 15 之间,具体取决于 CPU 实现)。
- 这个 pkey 可以用于后续的
pkey_mprotect
调用。
pkey_free
:- 将一个之前分配的 pkey 返还给内核。
- 释放后,该 pkey 可以被后续的
pkey_alloc
调用再次分配。 - 重要: 在调用
pkey_free
之前,应该确保没有内存区域仍然通过pkey_mprotect
与该 pkey 关联,否则这些区域的访问权限可能会变得不可预测。
4. 参数
pkey_alloc
unsigned int flags
: 目前必须设置为 0。保留供将来扩展。unsigned int access_rights
: 指定该 pkey 的初始访问权限。这是一个位掩码,定义在<sys/mman.h>
中。PKEY_DISABLE_ACCESS
: 禁止所有访问(读、写、执行)。这是最严格的权限。PKEY_DISABLE_WRITE
: 禁止写入,但允许读取和执行。0
: 允许所有访问(读、写、执行)。这是最宽松的权限。- 注意: 这个初始权限是设置在内核内部的,与
PKRU
寄存器中的权限是分开的。pkey_alloc
返回的 pkey,其在PKRU
中的初始状态通常是允许访问的。这个access_rights
参数更多是作为一种内核层面的标记或备用机制。
pkey_free
int pkey
: 这是之前通过成功的pkey_alloc
调用返回的保护键编号。
5. 返回值
pkey_alloc
:- 成功时: 返回一个非负整数,即新分配的保护键编号(pkey)。
- 失败时: 返回 -1,并设置
errno
(例如ENOSPC
没有可用的 pkey,EINVAL
flags
或access_rights
无效,EOPNOTSUPP
硬件不支持)。
pkey_free
:- 成功时: 返回 0。
- 失败时: 返回 -1,并设置
errno
(例如EINVAL
pkey
无效,EOPNOTSUPP
硬件不支持)。
6. 相似函数,或关联函数
pkey_mprotect
: 用于将一个内存区域与特定的 pkey 关联起来,是使用 pkey 进行内存保护的核心函数。mprotect
: 传统的内存保护函数,修改内存区域的RWX
权限。pkey_mprotect
是其增强版,增加了 pkey 功能。mmap
: 用于分配和映射内存区域,这些区域后续可以用pkey_mprotect
来关联 pkey。syscall(SYS_pkey_alloc, ...)
/syscall(SYS_pkey_free, ...)
: 在 glibc 不支持时,直接调用系统调用的方式。
7. 示例代码
重要提示:
- 硬件和内核支持: MPK 仅在支持该特性的 CPU(如 Intel x86_64 Skylake 及更新架构)和 Linux 内核(>= 4.9)上可用。
- glibc 版本: 需要 glibc 2.27 或更高版本。
- 复杂性: 使用 MPK 需要结合
pkey_mprotect
和对PKRU
寄存器的操作(通常通过内联汇编或专用库函数),下面的示例主要演示pkey_alloc
/free
的基本用法。
示例 1:检查支持并基本使用 pkey_alloc
/free
这个例子演示了如何检查系统是否支持 MPK,然后分配和释放保护键。
// pkey_basic_example.c
#define _GNU_SOURCE // For pkey functions
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
// 如果 glibc 版本过低,可能需要手动定义系统调用号
#ifndef SYS_pkey_alloc
#define SYS_pkey_alloc 330
#endif
#ifndef SYS_pkey_free
#define SYS_pkey_free 331
#endif
// 用于手动调用系统调用的包装函数 (如果需要)
// #include <sys/syscall.h>
// static inline int my_pkey_alloc(unsigned int flags, unsigned int access_rights) {
// return syscall(SYS_pkey_alloc, flags, access_rights);
// }
// static inline int my_pkey_free(int pkey) {
// return syscall(SYS_pkey_free, pkey);
// }
// 检查系统是否支持内存保护键
int check_pkey_support() {
int pkey;
// 尝试分配一个 pkey 来检查支持
pkey = pkey_alloc(0, 0);
if (pkey == -1) {
if (errno == EOPNOTSUPP) {
return 0; // Not supported
} else {
// Other error, but might still support it in general
// Let's assume it does and let the main code handle specific errors
return 1;
}
}
// Allocation succeeded, support confirmed. Free it.
if (pkey_free(pkey) == -1) {
perror("pkey_free after check");
}
return 1;
}
int main() {
printf("Checking for Memory Protection Key (MPK) support...\n");
if (!check_pkey_support()) {
fprintf(stderr, "Memory Protection Keys (MPK) are NOT supported on this system/CPU.\n");
fprintf(stderr, "This might be because:\n");
fprintf(stderr, " 1. The CPU does not support MPK (e.g., older than Intel Skylake).\n");
fprintf(stderr, " 2. The Linux kernel version is older than 4.9.\n");
fprintf(stderr, " 3. The feature is disabled.\n");
exit(EXIT_FAILURE);
}
printf("MPK support detected.\n");
// --- 分配保护键 ---
printf("\n--- Allocating Protection Keys ---\n");
int pkey1, pkey2;
// 分配第一个 pkey,初始权限为允许所有访问
pkey1 = pkey_alloc(0, 0);
if (pkey1 == -1) {
perror("pkey_alloc 1 failed");
exit(EXIT_FAILURE);
}
printf("Successfully allocated pkey 1: %d\n", pkey1);
// 分配第二个 pkey,初始权限为禁止写入
pkey2 = pkey_alloc(0, PKEY_DISABLE_WRITE);
if (pkey2 == -1) {
perror("pkey_alloc 2 failed");
// Cleanup previously allocated pkey
pkey_free(pkey1);
exit(EXIT_FAILURE);
}
printf("Successfully allocated pkey 2: %d (initially write-disabled)\n", pkey2);
// 尝试分配更多 pkey (系统通常限制为 16 个, 0-15)
printf("\nAttempting to allocate more pkeys...\n");
int pkeys[20];
int allocated_count = 0;
for (int i = 0; i < 20; i++) {
pkeys[i] = pkey_alloc(0, 0);
if (pkeys[i] == -1) {
if (errno == ENOSPC) {
printf(" Allocation %d failed: No space left (ENOSPC). Max pkeys reached.\n", i);
break;
} else {
printf(" Allocation %d failed with errno %d (%s)\n", i, errno, strerror(errno));
break;
}
} else {
printf(" Allocated pkey %d: %d\n", i, pkeys[i]);
allocated_count++;
}
}
// --- 释放保护键 ---
printf("\n--- Freeing Protection Keys ---\n");
// 释放前两个
if (pkey_free(pkey1) == -1) {
perror("pkey_free pkey1 failed");
} else {
printf("Successfully freed pkey 1 (%d)\n", pkey1);
}
if (pkey_free(pkey2) == -1) {
perror("pkey_free pkey2 failed");
} else {
printf("Successfully freed pkey 2 (%d)\n", pkey2);
}
// 释放之前批量分配的
for (int i = 0; i < allocated_count; i++) {
if (pkey_free(pkeys[i]) == -1) {
printf("Failed to free pkey %d (%d)\n", i, pkeys[i]);
} else {
printf("Successfully freed pkey %d (%d)\n", i, pkeys[i]);
}
}
printf("\nAll pkey operations completed.\n");
return 0;
}
如何编译和测试:
# 需要较新的 glibc (>= 2.27)
gcc -o pkey_basic_example pkey_basic_example.c
./pkey_basic_example
代码解释:
- 定义了必要的头文件。
check_pkey_support
: 通过尝试调用pkey_alloc
来粗略检查系统是否支持 MPK。如果返回EOPNOTSUPP
,则说明不支持。- 在
main
函数中,首先调用check_pkey_support
。 - 分配 pkey:
- 调用
pkey_alloc(0, 0)
分配第一个 pkey,初始权限为允许所有访问。 - 调用
pkey_alloc(0, PKEY_DISABLE_WRITE)
分配第二个 pkey,初始权限为禁止写入。 - 通过一个循环尝试分配更多 pkey,直到系统报告
ENOSPC
(没有空间,即达到上限)。
- 调用
- 释放 pkey:
- 调用
pkey_free
释放之前分配的所有 pkey。
- 调用
- 打印相关信息。
示例 2:结合 mmap
, pkey_mprotect
使用 pkey_alloc
/free
(概念性)
这个例子展示了一个更完整的、但概念性的用法,结合了分配内存、分配 pkey、关联内存与 pkey 以及通过 PKRU
控制访问。请注意:直接操作 PKRU
寄存器需要内联汇编,这比较复杂且依赖于架构。
// pkey_conceptual_example.c
#define _GNU_SOURCE
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <setjmp.h>
static jmp_buf jmp_env;
static volatile sig_atomic_t sigsegv_caught = 0;
// 信号处理函数
void sigsegv_handler(int sig) {
sigsegv_caught = 1;
longjmp(jmp_env, 1);
}
// 概念性的 PKRU 操作函数 (实际需要内联汇编)
// 这里用伪代码表示
void write_pkru(unsigned int pkru_value) {
// In real code, this would be inline assembly like:
// asm volatile(".byte 0x0f,0x01,0xef\n\t" : : "a" (pkru_value), "d" (0), "c" (0) : "memory");
printf(" [Concept] Writing PKRU with value: 0x%08x\n", pkru_value);
// WARNING: This is NOT real code, just for illustration.
// Real code needs inline assembly.
}
unsigned int read_pkru() {
// In real code:
// unsigned int pkru;
// asm volatile(".byte 0x0f,0x01,0xee\n\t" : "=a" (pkru) : "c" (0) : "rdx", "memory");
// return pkru;
printf(" [Concept] Reading PKRU\n");
return 0; // Dummy return
}
int main() {
if (sysconf(_SC_MPKEY) <= 0) { // Check if supported
fprintf(stderr, "MPK not supported by sysconf.\n");
exit(EXIT_FAILURE);
}
struct sigaction sa;
sa.sa_handler = sigsegv_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if (sigaction(SIGSEGV, &sa, NULL) == -1) {
perror("sigaction");
exit(EXIT_FAILURE);
}
size_t len = 4096; // 1 page
void *addr;
// 1. 分配内存
addr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}
printf("Allocated memory at %p\n", addr);
// 2. 写入一些数据
strcpy((char*)addr, "Initial data in protected memory.");
printf("Written initial data.\n");
// 3. 分配一个 pkey
int pkey = pkey_alloc(0, 0);
if (pkey == -1) {
perror("pkey_alloc");
munmap(addr, len);
exit(EXIT_FAILURE);
}
printf("Allocated pkey: %d\n", pkey);
// 4. 将内存区域与 pkey 关联 (概念性)
// 实际需要调用 pkey_mprotect(addr, len, PROT_READ|PROT_WRITE, pkey);
printf("--- Conceptually associating memory with pkey %d ---\n", pkey);
// printf("Would call: pkey_mprotect(%p, %zu, PROT_READ|PROT_WRITE, %d)\n", addr, len, pkey);
// 5. 通过修改 PKRU 来禁止访问 pkey (概念性)
printf("\n--- Disabling access to pkey %d via PKRU ---\n", pkey);
// 计算新的 PKRU 值以禁用 pkey 的访问
// 每个 pkey 在 PKRU 中占 2 位: 00(allow), 01(deny access), 10(deny write), 11(deny access)
// 假设 pkey 是 1, 那么它在 PKRU 的 bit 2-3
// unsigned int current_pkru = read_pkru();
unsigned int new_pkru = 0; // Start with allowing all
// Set bits for pkey to 01 (deny access)
// new_pkru |= (1 << (pkey * 2));
// write_pkru(new_pkru);
printf(" [Concept] Would set PKRU bits for pkey %d to deny access.\n", pkey);
// 6. 尝试访问受保护的内存 (应该触发 SIGSEGV)
printf("\n--- Attempting to access protected memory ---\n");
sigsegv_caught = 0;
if (setjmp(jmp_env) == 0) {
// This block will be executed first
printf(" Trying to read from %p...\n", addr);
char first_char = *((char*)addr); // This should trigger SIGSEGV
printf(" ERROR: Read succeeded (first char: %c). This should not happen!\n", first_char);
} else {
// This block will be executed if longjmp is called from signal handler
if (sigsegv_caught) {
printf(" SUCCESS: SIGSEGV caught as expected. Access denied by pkey.\n");
} else {
printf(" Unexpected longjmp.\n");
}
}
// 7. 重新允许访问
printf("\n--- Re-enabling access to pkey %d via PKRU ---\n", pkey);
// write_pkru(0); // Allow all access again
printf(" [Concept] Would reset PKRU to allow all access.\n");
// 8. 再次尝试访问 (应该成功)
printf("\n--- Attempting to access memory again (should succeed now) ---\n");
printf(" Reading from %p: %s\n", addr, (char*)addr);
// 9. 清理
if (pkey_free(pkey) == -1) {
perror("pkey_free");
}
if (munmap(addr, len) == -1) {
perror("munmap");
}
printf("\nConceptual pkey example finished.\n");
return 0;
}
**代码解释 **(概念性):
- 设置了
SIGSEGV
信号处理函数和setjmp
/longjmp
机制来捕获预期的段错误。 - 使用
mmap
分配了一块匿名内存。 - 向内存写入了初始数据。
- 调用
pkey_alloc(0, 0)
分配一个 pkey。 - 概念性步骤: 描述了将内存与 pkey 关联(实际需要
pkey_mprotect
)和通过修改PKRU
寄存器禁止访问的操作。这部分用printf
和伪函数write_pkru
/read_pkru
代替,因为真实的实现需要内联汇编。 - 尝试读取受保护的内存。在真实场景下,这会触发
SIGSEGV
,信号处理函数会设置标志并longjmp
回来。 - 概念性步骤: 描述了重新允许访问(重置
PKRU
)。 - 再次尝试访问,这次应该成功。
- 释放 pkey 和内存。
重要提示与注意事项:
- 硬件和内核依赖: MPK 是 x86_64 架构(Intel Skylake 及更新)的特性,需要 Linux 内核 4.9+。
- glibc 版本: 需要 glibc 2.27+ 才有原生支持。
- 复杂性: 真正使用 MPK 需要结合
pkey_mprotect
和对PKRU
寄存器的精确控制(通常通过内联汇编),这比示例中展示的要复杂得多。 pkey_mprotect
是关键:pkey_alloc
/free
只是管理 pkey 编号,真正将内存和权限联系起来的是pkey_mprotect
。PKRU
操作: 直接读写PKRU
寄存器是使用 MPK 功能的核心,但需要内联汇编知识。- 错误处理: 始终检查
pkey_alloc
是否返回EOPNOTSUPP
(不支持)或ENOSPC
(pkey 耗尽)。 - 性能优势: MPK 的主要优势在于权限切换的极低延迟,因为它避免了修改页表的开销。
- 应用场景: 适用于需要快速、动态地改变大量内存区域访问权限的场景,如沙箱、内存安全库、调试器等。
总结:
pkey_alloc
和 pkey_free
是 Linux 内存保护键(MPK)机制的一部分,用于分配和回收独立的内存访问控制键。它们本身只是 pkey 生命周期管理的第一步。要真正利用 MPK 提供的快速、细粒度内存保护能力,还需要结合 pkey_mprotect
来关联内存区域,以及通过直接操作 PKRU
寄存器来动态启用或禁用访问权限。虽然使用起来比较底层和复杂,但对于需要极致内存安全和性能控制的应用来说,MPK 是一个强大的工具。