preadv2系统调用及示例
我们继续按照您的要求学习 Linux 系统编程中的重要函数。这次我们介绍 preadv2、pwritev2 和 pkey_mprotect。
函数 1: preadv2
1. 函数介绍
preadv2 (pread vector 2) 是 preadv 系统调用的扩展版本。它结合了 pread(带偏移量读取)和 readv(分散读取)的优点,并引入了一个新的 flags 参数,提供了更灵活的 I/O 控制选项。
简单来说,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 则是将这个编号应用到具体的内存区域上。
一旦内存区域通过 pkey_mprotect 与一个 pkey 关联,对该区域的访问权限就不仅受传统的 PROT_READ/PROT_WRITE/PROT_EXEC 控制,还受该 pkey 在 CPU 的 PKRU(Protection Key Rights User)寄存器中设置的权限控制。
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;
}
