preadv2系统调用及示例

好的,我们继续按照您的要求学习 Linux 系统编程中的重要函数。这次我们介绍 preadv2pwritev2 和 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 iovcntiov 数组中元素的个数。
  • 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 iovcntiov 数组中元素的个数。
  • 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 获得)进行关联
  • 设置该区域的基本权限为 protPROT_READPROT_WRITEPROT_EXEC 的组合)。

4. 参数

  • void *addr: 要修改的内存区域的起始地址。必须是页对齐的
  • size_t len: 内存区域的长度(以字节为单位)。会向上舍入到最近的页边界。
  • int prot: 新的内存保护标志。可以是 PROT_NONEPROT_READPROT_WRITEPROT_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;
}

代码解释:

  1. 创建一个测试文件,并使用 writev 写入一些初始内容。
  2. 重新打开文件进行读取。
  3. 使用 preadv2(fd, iov_r, 3, 0, 0) 从文件偏移量 0 开始,将数据分散读入三个缓冲区。flags 为 0,表示默认行为。
  4. 打开文件进行写入(非 O_APPEND 模式)。
  5. 使用 pwritev2(fd, iov_a, 2, 0, RWF_APPEND) 将数据写入文件。尽管 offset 是 0,但由于使用了 RWF_APPEND 标志,数据被追加到了文件末尾。
  6. 重新读取并打印文件内容以验证操作结果。

示例 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;
}

**代码解释 **(概念性):

  1. 设置信号处理和 setjmp/longjmp 用于捕获 SIGSEGV
  2. 使用 mmap 分配一页内存。
  3. 写入一些测试数据。
  4. 调用 pkey_alloc(0, 0) 获取一个 pkey。
  5. 关键步骤: 调用 pkey_mprotect(addr, len, PROT_READ | PROT_WRITE, pkey) 将分配的内存区域与获取的 pkey 关联起来。
  6. 概念性操作: 模拟通过修改 PKRU 寄存器来禁用对这个 pkey 的访问。
  7. 尝试读取受保护的内存,预期会触发 SIGSEGV
  8. 概念性操作: 模拟重新启用对这个 pkey 的访问。
  9. 再次尝试读取,这次应该成功。
  10. 清理资源(释放 pkey 和内存)。

重要提示与注意事项:

  1. 内核版本:
    • preadv2/pwritev2: Linux 内核 4.6+。
    • pkey_mprotect/pkey_alloc/pkey_free: Linux 内核 4.9+ (MPK)。
  2. glibc 版本: 需要 glibc 2.27+ 才能直接使用这些函数。
  3. 硬件支持pkey_* 函数需要 CPU 支持(如 Intel x86_64 Skylake 及更新架构)。
  4. _GNU_SOURCE: 必须定义此宏才能使用这些扩展函数。
  5. flags 参数preadv2/pwritev2 的 flags 提供了强大的 I/O 控制能力,特别是 RWF_NOWAIT(非阻塞)和 RWF_APPEND
  6. pkey_mprotect 是核心: 它是将 pkey 机制应用到实际内存区域的关键步骤。仅仅 pkey_alloc 是不够的。
  7. PKRU 操作: 真正控制 pkey 权限需要直接操作 CPU 的 PKRU 寄存器,这通常需要内联汇编,比较复杂。
  8. 错误处理: 始终检查返回值,特别是 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;
}
此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注