好的,我们继续按照您的要求学习 Linux 系统编程中的重要函数。这次我们介绍 preadv2
、pwritev2
和 pkey_mprotect
。
函数 1: preadv2
1. 函数介绍
简单来说,preadv2
允许你从文件的指定偏移量开始,将数据分散读入到多个不连续的缓冲区中,同时还能指定一些高级 I/O 行为(通过 flags
)。
2. 函数原型
#define _GNU_SOURCE // 必须定义以使用 preadv2
#include <sys/uio.h> // struct iovec
#include <unistd.h> // ssize_t
ssize_t preadv2(int fd, const struct iovec *iov, int iovcnt, off_t offset, int flags);
3. 功能
- 从文件描述符
fd
指定的文件中,从绝对偏移量offset
开始读取数据。 - 将读取的数据分散存储到由
iov
和iovcnt
指定的多个缓冲区中。 - 不修改文件的当前读写位置指针(
lseek
位置)。 - 根据
flags
参数执行特定的 I/O 操作。
4. 参数
int fd
: 有效的文件描述符。const struct iovec *iov
: 指向struct iovec
数组的指针,描述了多个分散的缓冲区。int iovcnt
:iov
数组中元素的个数。off_t offset
: 在文件中开始读取的绝对偏移量(以字节为单位)。必须是非负数。int flags
: 控制 I/O 行为的标志。可以是以下值的按位或组合:0
: 默认行为,等同于preadv
。RWF_HIPRI
: 尝试使用高优先级/实时 I/O(如果内核和设备支持)。RWF_DSYNC
: 要求 I/O 操作具有数据同步持久性(类似于O_DSYNC
)。RWF_SYNC
: 要求 I/O 操作具有文件同步持久性(类似于O_SYNC
)。RWF_NOWAIT
: 非阻塞。如果 I/O 无法立即完成(例如,需要从磁盘读取而数据不在页缓存中),则不等待,立即返回错误EAGAIN
。这需要内核和文件系统支持。RWF_APPEND
: 强制将写入追加到文件末尾(仅对pwritev2
有效)。
5. 返回值
- 成功时: 返回实际读取的总字节数(0 表示 EOF)。
- 失败时: 返回 -1,并设置
errno
。
函数 2: pwritev2
1. 函数介绍
pwritev2
(pwrite vector 2) 是 pwritev
系统调用的扩展版本。它结合了 pwrite
(带偏移量写入)和 writev
(集中写入)的优点,并同样引入了 flags
参数。
简单来说,pwritev2
允许你从多个不连续的缓冲区中收集数据,并将其写入到文件的指定偏移量处,同时还能指定一些高级 I/O 行为(通过 flags
)。
2. 函数原型
#define _GNU_SOURCE
#include <sys/uio.h>
#include <unistd.h>
ssize_t pwritev2(int fd, const struct iovec *iov, int iovcnt, off_t offset, int flags);
3. 功能
- 从由
iov
和iovcnt
指定的多个缓冲区中收集数据。 - 将收集到的数据写入到文件描述符
fd
指定的文件中,从绝对偏移量offset
开始写入。 - 不修改文件的当前读写位置指针(
lseek
位置)。 - 根据
flags
参数执行特定的 I/O 操作。
4. 参数
int fd
: 有效的文件描述符。const struct iovec *iov
: 指向struct iovec
数组的指针,描述了多个包含数据的缓冲区。int iovcnt
:iov
数组中元素的个数。off_t offset
: 在文件中开始写入的绝对偏移量(以字节为单位)。必须是非负数。- 如果文件以
O_APPEND
模式打开,或者flags
中设置了RWF_APPEND
,则offset
参数会被忽略,数据总是被写入到文件末尾。
- 如果文件以
int flags
: 控制 I/O 行为的标志。可以是以下值的按位或组合:0
: 默认行为,等同于pwritev
。RWF_HIPRI
: 尝试使用高优先级/实时 I/O。RWF_DSYNC
: 要求数据同步持久性。RWF_SYNC
: 要求文件同步持久性。RWF_NOWAIT
: 非阻塞。如果 I/O 无法立即完成,立即返回错误EAGAIN
。RWF_APPEND
: 强制将写入追加到文件末尾,即使文件没有以O_APPEND
打开。
5. 返回值
- 成功时: 返回实际写入的总字节数。
- 失败时: 返回 -1,并设置
errno
。
函数 3: pkey_mprotect
1. 函数介绍
pkey_mprotect
是 mprotect
系统调用的扩展,用于将一个内存区域与一个特定的内存保护键(Protection Key, pkey)相关联。
回忆一下 pkey_alloc
/free
:它们用于获取和释放 pkey 编号。pkey_mprotect
则是将这个编号应用到具体的内存区域上。
2. 函数原型
#define _GNU_SOURCE
#include <sys/mman.h> // 包含 MPK 相关常量
int pkey_mprotect(void *addr, size_t len, int prot, int pkey);
3. 功能
- 修改从地址
addr
开始、长度为len
字节的内存区域的访问权限。 - 将该内存区域与保护键
pkey
(由pkey_alloc
获得)进行关联。 - 设置该区域的基本权限为
prot
(PROT_READ
,PROT_WRITE
,PROT_EXEC
的组合)。
4. 参数
void *addr
: 要修改的内存区域的起始地址。必须是页对齐的。size_t len
: 内存区域的长度(以字节为单位)。会向上舍入到最近的页边界。int prot
: 新的内存保护标志。可以是PROT_NONE
,PROT_READ
,PROT_WRITE
,PROT_EXEC
及其按位或组合。int pkey
: 通过pkey_alloc
获得的保护键编号(0-15)。
5. 返回值
- 成功时: 返回 0。
- 失败时: 返回 -1,并设置
errno
。
示例代码
示例 1:preadv2
和 pwritev2
的基本使用
// preadv2_pwritev2_example.c
#define _GNU_SOURCE
#include <sys/uio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#define FILENAME "test_piov2.txt"
int main() {
int fd;
char buf1[20], buf2[30], buf3[50];
struct iovec iov_w[2], iov_r[3];
ssize_t bytes_written, bytes_read;
// 1. 创建并写入测试文件 (使用传统 write)
fd = open(FILENAME, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open for write");
exit(EXIT_FAILURE);
}
const char *data1 = "Part One: Hello, ";
const char *data2 = "preadv2 and pwritev2 World!\n";
iov_w[0].iov_base = (void*)data1;
iov_w[0].iov_len = strlen(data1);
iov_w[1].iov_base = (void*)data2;
iov_w[1].iov_len = strlen(data2);
bytes_written = writev(fd, iov_w, 2);
if (bytes_written == -1) {
perror("writev");
close(fd);
exit(EXIT_FAILURE);
}
printf("Written %zd bytes using writev.\n", bytes_written);
close(fd);
// 2. 使用 preadv2 读取
fd = open(FILENAME, O_RDONLY);
if (fd == -1) {
perror("open for read");
exit(EXIT_FAILURE);
}
// 初始化读取缓冲区
memset(buf1, '.', sizeof(buf1) - 1); buf1[sizeof(buf1)-1] = '\0';
memset(buf2, '.', sizeof(buf2) - 1); buf2[sizeof(buf2)-1] = '\0';
memset(buf3, '.', sizeof(buf3) - 1); buf3[sizeof(buf3)-1] = '\0';
iov_r[0].iov_base = buf1;
iov_r[0].iov_len = sizeof(buf1) - 1;
iov_r[1].iov_base = buf2;
iov_r[1].iov_len = sizeof(buf2) - 1;
iov_r[2].iov_base = buf3;
iov_r[2].iov_len = sizeof(buf3) - 1;
// 从偏移量 0 开始读取,使用默认标志
bytes_read = preadv2(fd, iov_r, 3, 0, 0);
if (bytes_read == -1) {
perror("preadv2");
close(fd);
exit(EXIT_FAILURE);
}
printf("\nRead %zd bytes using preadv2 from offset 0:\n", bytes_read);
printf("Buffer 1: '%s'\n", buf1);
printf("Buffer 2: '%s'\n", buf2);
printf("Buffer 3: '%s'\n", buf3);
close(fd);
// 3. 使用 pwritev2 追加写入
fd = open(FILENAME, O_WRONLY); // 不用 O_APPEND
if (fd == -1) {
perror("open for write (again)");
exit(EXIT_FAILURE);
}
const char *append1 = "Appended via ";
const char *append2 = "pwritev2 with RWF_APPEND flag.\n";
struct iovec iov_a[2];
iov_a[0].iov_base = (void*)append1;
iov_a[0].iov_len = strlen(append1);
iov_a[1].iov_base = (void*)append2;
iov_a[1].iov_len = strlen(append2);
// 使用 RWF_APPEND 标志强制追加,忽略 offset
bytes_written = pwritev2(fd, iov_a, 2, 0, RWF_APPEND);
if (bytes_written == -1) {
perror("pwritev2 with RWF_APPEND");
close(fd);
exit(EXIT_FAILURE);
}
printf("\nAppended %zd bytes using pwritev2 with RWF_APPEND.\n", bytes_written);
close(fd);
// 4. 验证文件内容
printf("\n--- Final file content ---\n");
fd = open(FILENAME, O_RDONLY);
if (fd != -1) {
char final_buf[200];
ssize_t n = read(fd, final_buf, sizeof(final_buf) - 1);
if (n > 0) {
final_buf[n] = '\0';
printf("%s", final_buf);
}
close(fd);
}
// unlink(FILENAME); // 可选:清理文件
return 0;
}
代码解释:
- 创建一个测试文件,并使用
writev
写入一些初始内容。 - 重新打开文件进行读取。
- 使用
preadv2(fd, iov_r, 3, 0, 0)
从文件偏移量 0 开始,将数据分散读入三个缓冲区。flags
为 0,表示默认行为。 - 打开文件进行写入(非
O_APPEND
模式)。 - 使用
pwritev2(fd, iov_a, 2, 0, RWF_APPEND)
将数据写入文件。尽管offset
是 0,但由于使用了RWF_APPEND
标志,数据被追加到了文件末尾。 - 重新读取并打印文件内容以验证操作结果。
示例 2:pkey_mprotect
结合 pkey_alloc
/free
使用
// pkey_mprotect_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);
}
// Conceptual PKRU manipulation (requires inline assembly in real code)
// For demonstration, we'll just print what would happen.
void set_pkey_access(int pkey, int disable_access) {
printf(" [Concept] Modifying PKRU for pkey %d: %s\n",
pkey, disable_access ? "DISABLE access" : "ENABLE access");
// Real code would involve inline assembly to write to PKRU register
}
int main() {
// Check for MPK support conceptually
if (sysconf(_SC_MPKEY) <= 0) {
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 page_size = getpagesize();
size_t len = page_size;
void *addr;
// 1. Allocate memory
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 %zu bytes at %p\n", len, addr);
// 2. Write some data
strcpy((char*)addr, "This memory is protected by a pkey.");
printf("Written data: %s\n", (char*)addr);
// 3. Allocate a protection key
int pkey = pkey_alloc(0, 0);
if (pkey == -1) {
if (errno == EOPNOTSUPP) {
printf("MPK not supported on this hardware/kernel.\n");
munmap(addr, len);
exit(EXIT_FAILURE);
} else {
perror("pkey_alloc");
munmap(addr, len);
exit(EXIT_FAILURE);
}
}
printf("Allocated pkey: %d\n", pkey);
// 4. Associate memory with the pkey using pkey_mprotect
printf("\n--- Associating memory with pkey %d ---\n", pkey);
if (pkey_mprotect(addr, len, PROT_READ | PROT_WRITE, pkey) == -1) {
perror("pkey_mprotect");
pkey_free(pkey);
munmap(addr, len);
exit(EXIT_FAILURE);
}
printf("Memory successfully associated with pkey %d.\n", pkey);
// 5. Disable access via PKRU (conceptual)
printf("\n--- Disabling access to pkey %d via PKRU ---\n", pkey);
set_pkey_access(pkey, 1); // Conceptual call
// 6. Try to access protected memory (should trigger SIGSEGV)
printf("\n--- Attempting to READ from protected memory ---\n");
sigsegv_caught = 0;
if (setjmp(jmp_env) == 0) {
printf(" Trying to read from %p...\n", addr);
volatile char first_char = *((char*)addr);
printf(" ERROR: Read succeeded (first char: %c). This should not happen!\n", first_char);
} else {
if (sigsegv_caught) {
printf(" SUCCESS: SIGSEGV caught. Access correctly denied by pkey.\n");
} else {
printf(" Unexpected longjmp.\n");
}
}
// 7. Re-enable access
printf("\n--- Re-enabling access to pkey %d via PKRU ---\n", pkey);
set_pkey_access(pkey, 0); // Conceptual call
// 8. Try to access memory again (should succeed)
printf("\n--- Attempting to access memory again (should succeed now) ---\n");
printf(" Reading from %p: %.50s\n", addr, (char*)addr);
// 9. Cleanup
if (pkey_free(pkey) == -1) {
perror("pkey_free");
}
if (munmap(addr, len) == -1) {
perror("munmap");
}
printf("\nPkey_mprotect example finished.\n");
return 0;
}
**代码解释 **(概念性):
- 设置信号处理和
setjmp
/longjmp
用于捕获SIGSEGV
。 - 使用
mmap
分配一页内存。 - 写入一些测试数据。
- 调用
pkey_alloc(0, 0)
获取一个 pkey。 - 关键步骤: 调用
pkey_mprotect(addr, len, PROT_READ | PROT_WRITE, pkey)
将分配的内存区域与获取的 pkey 关联起来。 - 概念性操作: 模拟通过修改
PKRU
寄存器来禁用对这个 pkey 的访问。 - 尝试读取受保护的内存,预期会触发
SIGSEGV
。 - 概念性操作: 模拟重新启用对这个 pkey 的访问。
- 再次尝试读取,这次应该成功。
- 清理资源(释放 pkey 和内存)。
重要提示与注意事项:
- 内核版本:
preadv2
/pwritev2
: Linux 内核 4.6+。pkey_mprotect
/pkey_alloc
/pkey_free
: Linux 内核 4.9+ (MPK)。
- glibc 版本: 需要 glibc 2.27+ 才能直接使用这些函数。
- 硬件支持:
pkey_*
函数需要 CPU 支持(如 Intel x86_64 Skylake 及更新架构)。 _GNU_SOURCE
: 必须定义此宏才能使用这些扩展函数。flags
参数:preadv2
/pwritev2
的flags
提供了强大的 I/O 控制能力,特别是RWF_NOWAIT
(非阻塞)和RWF_APPEND
。pkey_mprotect
是核心: 它是将 pkey 机制应用到实际内存区域的关键步骤。仅仅pkey_alloc
是不够的。PKRU
操作: 真正控制 pkey 权限需要直接操作 CPU 的PKRU
寄存器,这通常需要内联汇编,比较复杂。- 错误处理: 始终检查返回值,特别是
pkey_*
函数可能返回EOPNOTSUPP
。
总结:
preadv2
和 pwritev2
是对现有 I/O 系统调用的有力增强,通过引入 flags
参数,提供了更细粒度的控制,如非阻塞 I/O 和强制追加写入。
pkey_mprotect
是内存保护键(MPK)技术的核心 API 之一,它允许将特定的内存区域与一个 pkey 绑定,从而实现比传统 mprotect
更快速、更灵活的内存访问控制。结合 pkey_alloc
/free
和对 PKRU
寄存器的操作,可以构建出高性能的内存安全机制。
这三个函数都代表了 Linux 系统编程向更高性能、更细粒度控制发展的趋势。
preadv2 函数
1. 函数介绍
preadv2
是 preadv
的增强版本,支持额外的标志参数,提供更多的控制选项。它是Linux 4.6引入的新特性。
2. 函数原型
#define _GNU_SOURCE
#include <sys/uio.h>
ssize_t preadv2(int fd, const struct iovec *iov, int iovcnt, off_t offset, int flags);
3. 功能
与 preadv
类似,但从指定位置读取数据到多个缓冲区,并支持额外的控制标志。
4. 参数
- int fd: 文件描述符
- *const struct iovec iov: iovec结构体数组
- int iovcnt: iov数组元素个数
- off_t offset: 文件偏移量
- int flags: 控制标志(如RWF_HIPRI, RWF_DSYNC等)
5. 返回值
- 成功: 返回实际读取的总字节数
- 失败: 返回-1,并设置errno
6. 相似函数,或关联函数
- preadv: 基本版本
- pwritev2: 对应的写入函数
- read: 基本读取函数
7. 示例代码
#define _GNU_SOURCE
#include <sys/uio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
/**
* 演示preadv2的基本使用
* 注意:需要Linux 4.6+内核支持
*/
int demo_preadv2_basic() {
int fd;
struct iovec iov[2];
char buf1[30], buf2[20];
ssize_t bytes_read;
printf("=== preadv2 基本使用示例 ===\n");
// 创建测试文件
fd = open("test_preadv2.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644);
if (fd == -1) {
perror("创建测试文件失败");
return -1;
}
const char *test_data = "This is test data for preadv2 function demonstration.";
write(fd, test_data, strlen(test_data));
close(fd);
// 打开文件进行读取
fd = open("test_preadv2.txt", O_RDONLY);
if (fd == -1) {
perror("打开文件失败");
return -1;
}
// 设置iovec数组
iov[0].iov_base = buf1;
iov[0].iov_len = sizeof(buf1) - 1;
iov[1].iov_base = buf2;
iov[1].iov_len = sizeof(buf2) - 1;
// 使用preadv2读取数据(flags设为0表示默认行为)
bytes_read = preadv2(fd, iov, 2, 0, 0);
if (bytes_read == -1) {
if (errno == ENOSYS) {
printf("系统不支持 preadv2 函数\n");
close(fd);
unlink("test_preadv2.txt");
return 0;
}
perror("preadv2 失败");
close(fd);
unlink("test_preadv2.txt");
return -1;
}
printf("preadv2 成功读取 %zd 字节\n", bytes_read);
// 添加字符串结束符并显示结果
buf1[iov[0].iov_len] = '\0';
buf2[iov[1].iov_len] = '\0';
printf("缓冲区1: %s\n", buf1);
printf("缓冲区2: %s\n", buf2);
close(fd);
unlink("test_preadv2.txt");
return 0;
}
/**
* 演示preadv2的高级特性(如果系统支持)
*/
int demo_preadv2_advanced() {
int fd;
struct iovec iov[1];
char buffer[100];
ssize_t bytes_read;
printf("\n=== preadv2 高级特性示例 ===\n");
printf("preadv2 支持的标志包括:\n");
printf(" RWF_HIPRI: 高优先级I/O\n");
printf(" RWF_DSYNC: 数据同步写入\n");
printf(" RWF_SYNC: 同步写入\n");
printf(" RWF_NOWAIT: 非阻塞操作\n");
printf(" RWF_APPEND: 追加模式写入\n");
// 创建测试文件
fd = open("advanced_test.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644);
if (fd == -1) {
perror("创建测试文件失败");
return -1;
}
const char *test_data = "Advanced preadv2 test data for feature demonstration.";
write(fd, test_data, strlen(test_data));
close(fd);
fd = open("advanced_test.txt", O_RDONLY);
if (fd == -1) {
perror("打开文件失败");
return -1;
}
// 设置iovec
iov[0].iov_base = buffer;
iov[0].iov_len = sizeof(buffer) - 1;
// 尝试使用RWF_NOWAIT标志(非阻塞读取)
bytes_read = preadv2(fd, iov, 1, 0, RWF_NOWAIT);
if (bytes_read == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
printf("非阻塞操作:数据暂时不可用\n");
} else if (errno == ENOSYS) {
printf("系统不支持 RWF_NOWAIT 标志\n");
} else {
printf("preadv2 with RWF_NOWAIT 失败: %s\n", strerror(errno));
}
} else {
buffer[bytes_read] = '\0';
printf("非阻塞读取成功: %s\n", buffer);
}
close(fd);
unlink("advanced_test.txt");
return 0;
}
int main() {
printf("preadv2 需要 Linux 4.6+ 内核支持\n");
if (demo_preadv2_basic() == 0) {
demo_preadv2_advanced();
printf("\n=== preadv2 使用总结 ===\n");
printf("优点:支持额外控制标志,更灵活的I/O控制\n");
printf("注意:需要较新内核版本支持\n");
}
return 0;
}