create_module系统调用及示例

关于 create_module 和 delete_module 的内核版本历史

create_module 废弃时间

  • Linux 2.6 内核(2003年左右)开始逐步废弃
  • Linux 2.6.8 版本后完全移除
  • 最后支持的内核版本:Linux 2.4.x

delete_module 变化时间

  • Linux 2.6 内核开始改变行为
  • 从系统调用转变为更安全的模块管理机制
  • 现代系统中仍然存在,但行为更加受限

详细历史说明

/*
 * Linux 内核模块管理演进历史:
 * 
 * Linux 2.0-2.4 (1996-2003):
 * - 使用 create_module() 分配内核内存
 * - 使用 delete_module() 卸载模块
 * - 相对简单的模块加载机制
 * 
 * Linux 2.6+ (2003年至今):
 * - 引入 init_module() 替代 create_module()
 * - delete_module() 仍然可用但更加安全
 * - 模块签名验证机制
 * - 更严格的权限控制
 * 
 * 现代 Linux (3.0+):
 * - 模块加载通过 finit_module() 系统调用
 * - delete_module() 保留但增加安全检查
 * - 强制模块签名(某些发行版)
 */

现代替代方案

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>

int main() {
    printf("=== 现代内核模块管理 ===\n");
    printf("Linux内核版本演进:\n");
    printf("- 2.4及以前: create_module/delete_module\n");
    printf("- 2.6开始: init_module/delete_module\n");
    printf("- 3.0+: fini_module/delete_module\n\n");
    
    printf("create_module废弃时间: Linux 2.6.8 (2004年)\n");
    printf("最后支持版本: Linux 2.4.37 (2009年停止维护)\n\n");
    
    printf("现代替代方案:\n");
    printf("1. 用户空间工具:\n");
    printf("   - insmod: 加载模块\n");
    printf("   - rmmod: 卸载模块\n");
    printf("   - lsmod: 列出模块\n\n");
    
    printf("2. 系统调用:\n");
    printf("   - init_module(): 加载模块\n");
    printf("   - finit_module(): 文件描述符版本的init_module\n");
    printf("   - delete_module(): 卸载模块(仍在使用)\n\n");
    
    printf("3. 程序化使用:\n");
    printf("   - libkmod库提供高级API\n");
    printf("   - modprobe命令处理依赖关系\n");
    
    return 0;
}

总结

  • create_module: Linux 2.6.8 (2004年) 后完全废弃
  • delete_module: 仍在使用,但在现代内核中有更多安全限制
  • 对于现代Linux系统编程,应该使用用户空间工具或libkmod库来管理内核模块
发表在 linux文章 | 留下评论

dup3系统调用及示例

1. 函数介绍

dup3 是 Linux 系统调用,是 dup2 的扩展版本。它用于将一个已存在的文件描述符复制到指定的目标文件描述符,类似于 dup2,但提供了额外的标志参数来控制复制行为。

这个函数的主要优势是可以设置文件描述符标志,最常用的是 O_CLOEXEC 标志,该标志使得复制的文件描述符在执行 exec 系列函数时自动关闭,避免了文件描述符泄漏到新程序中。


2. 函数原型

#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>

int dup3(int oldfd, int newfd, int flags);

3. 功能

  • 将文件描述符 oldfd 复制到指定的文件描述符 newfd
  • 如果 newfd 已经打开,会先将其关闭
  • 可以设置额外的文件描述符标志
  • 如果 oldfd 等于 newfd,则返回错误(与 dup2 不同)

4. 参数

  • int oldfd: 要被复制的原始文件描述符
  • int newfd: 目标文件描述符编号
  • int flags: 控制标志,可以是以下值的按位或组合:
    • O_CLOEXEC: 设置执行时关闭标志(FD_CLOEXEC)
    • 0: 不设置任何特殊标志(等同于 dup2 的行为)

5. 返回值

  • 成功时: 返回 newfd
  • 失败时: 返回 -1,并设置 errno
    • EBADFoldfd 或 newfd 不是有效的文件描述符
    • EINVALflags 参数无效,或 oldfd 等于 newfd
    • EMFILE: 进程打开的文件描述符数量达到上限

6. 相似函数

  • dup(): 复制文件描述符到最小可用编号
  • dup2(): 复制文件描述符到指定编号(不支持标志)
  • fcntl(): 更通用的文件描述符控制函数

7. 示例代码

示例 1:基本的 dup3 使用

#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main() {
    int fd1, fd2, fd3;
    
    printf("=== Dup3 基本使用演示 ===\n");
    
    // 1. 打开测试文件
    fd1 = open("test_dup3.txt", O_CREAT | O_RDWR | O_TRUNC, 0644);
    if (fd1 == -1) {
        perror("打开文件失败");
        exit(EXIT_FAILURE);
    }
    printf("打开文件获得描述符: %d\n", fd1);
    
    // 2. 使用 dup3 复制文件描述符(无特殊标志)
    fd2 = dup3(fd1, 10, 0);
    if (fd2 == -1) {
        perror("dup3 失败");
        close(fd1);
        exit(EXIT_FAILURE);
    }
    printf("使用 dup3(%d, 10, 0) 复制,获得描述符: %d\n", fd1, fd2);
    
    // 3. 使用 dup3 复制并设置 O_CLOEXEC 标志
    fd3 = dup3(fd1, 15, O_CLOEXEC);
    if (fd3 == -1) {
        perror("dup3 带 O_CLOEXEC 失败");
        close(fd1);
        close(fd2);
        exit(EXIT_FAILURE);
    }
    printf("使用 dup3(%d, 15, O_CLOEXEC) 复制,获得描述符: %d\n", fd1, fd3);
    
    // 4. 验证 O_CLOEXEC 标志是否设置
    int flags = fcntl(fd3, F_GETFD);
    if (flags != -1) {
        if (flags & FD_CLOEXEC) {
            printf("描述符 %d 已设置 FD_CLOEXEC 标志\n", fd3);
        } else {
            printf("描述符 %d 未设置 FD_CLOEXEC 标志\n", fd3);
        }
    }
    
    // 5. 验证文件描述符共享性
    const char *message1 = "通过 fd1 写入\n";
    const char *message2 = "通过 fd2 写入\n";
    const char *message3 = "通过 fd3 写入\n";
    
    write(fd1, message1, strlen(message1));
    write(fd2, message2, strlen(message2));
    write(fd3, message3, strlen(message3));
    
    // 6. 读取验证
    lseek(fd1, 0, SEEK_SET);
    char buffer[256];
    ssize_t bytes_read = read(fd1, buffer, sizeof(buffer) - 1);
    if (bytes_read > 0) {
        buffer[bytes_read] = '\0';
        printf("\n读取到的数据:\n%s", buffer);
    }
    
    // 7. 清理资源
    close(fd1);
    close(fd2);
    close(fd3);
    unlink("test_dup3.txt");
    
    return 0;
}

示例 2:O_CLOEXEC 标志的重要性

#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>

void demonstrate_cloexec_flag() {
    printf("=== O_CLOEXEC 标志演示 ===\n");
    
    // 创建测试文件
    int fd = open("cloexec_test.txt", O_CREAT | O_RDWR | O_TRUNC, 0644);
    if (fd == -1) {
        perror("创建测试文件失败");
        return;
    }
    
    write(fd, "测试数据", 8);
    
    // 使用 dup3 设置 O_CLOEXEC 标志
    int fd_with_cloexec = dup3(fd, 10, O_CLOEXEC);
    if (fd_with_cloexec == -1) {
        perror("dup3 设置 O_CLOEXEC 失败");
        close(fd);
        return;
    }
    
    // 使用 dup2 不设置 O_CLOEXEC 标志
    int fd_without_cloexec = dup2(fd, 15);
    if (fd_without_cloexec == -1) {
        perror("dup2 失败");
        close(fd);
        close(fd_with_cloexec);
        return;
    }
    
    printf("创建了两个描述符:\n");
    printf("  %d: 带 O_CLOEXEC 标志\n", fd_with_cloexec);
    printf("  %d: 不带 O_CLOEXEC 标志\n", fd_without_cloexec);
    
    // 创建子进程执行新程序
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        printf("子进程 PID: %d\n", getpid());
        
        // 检查文件描述符是否仍然打开
        if (fcntl(fd_with_cloexec, F_GETFD) == -1) {
            printf("带 O_CLOEXEC 的描述符 %d 已自动关闭\n", fd_with_cloexec);
        } else {
            printf("带 O_CLOEXEC 的描述符 %d 仍然打开\n", fd_with_cloexec);
        }
        
        if (fcntl(fd_without_cloexec, F_GETFD) == -1) {
            printf("不带 O_CLOEXEC 的描述符 %d 已关闭\n", fd_without_cloexec);
        } else {
            printf("不带 O_CLOEXEC 的描述符 %d 仍然打开\n", fd_without_cloexec);
        }
        
        // 执行新程序(这里用 ls 作为示例)
        execl("/bin/ls", "ls", "-l", "cloexec_test.txt", NULL);
        perror("execl 失败");
        exit(EXIT_FAILURE);
    } else if (pid > 0) {
        // 父进程
        wait(NULL);
        printf("父进程继续执行\n");
    } else {
        perror("fork 失败");
    }
    
    // 清理
    close(fd);
    close(fd_with_cloexec);
    close(fd_without_cloexec);
    unlink("cloexec_test.txt");
}

int main() {
    printf("Dup3 函数演示\n\n");
    
    // 基本使用演示
    system("gcc -o basic_dup3 basic_dup3.c");
    system("./basic_dup3");
    
    printf("\n");
    
    // O_CLOEXEC 标志演示
    demonstrate_cloexec_flag();
    
    printf("\n=== 总结 ===\n");
    printf("dup3 的优势:\n");
    printf("1. 支持设置 O_CLOEXEC 标志,防止文件描述符泄漏\n");
    printf("2. 原子性操作,避免了 dup2 + fcntl 的竞态条件\n");
    printf("3. 当 oldfd == newfd 时返回错误,行为更明确\n\n");
    
    printf("使用建议:\n");
    printf("- 优先使用 dup3 而不是 dup2\n");
    printf("- 在可能执行 exec 的场景中使用 O_CLOEXEC\n");
    printf("- 注意检查返回值和错误处理\n");
    
    return 0;
}

示例 3:错误处理演示

#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

void demonstrate_dup3_errors() {
    printf("=== Dup3 错误处理演示 ===\n");
    
    // 1. 无效的文件描述符
    printf("1. 使用无效的文件描述符:\n");
    int result = dup3(999, 10, 0);
    if (result == -1) {
        printf("   错误: %s\n", strerror(errno));
        if (errno == EBADF) {
            printf("   说明: 文件描述符 999 无效\n");
        }
    }
    
    // 2. 无效的标志
    printf("\n2. 使用无效的标志:\n");
    int fd = open("/dev/null", O_RDWR);
    if (fd != -1) {
        result = dup3(fd, 10, 0x1000); // 无效标志
        if (result == -1) {
            printf("   错误: %s\n", strerror(errno));
            if (errno == EINVAL) {
                printf("   说明: 标志参数无效\n");
            }
        }
        close(fd);
    }
    
    // 3. oldfd 等于 newfd
    printf("\n3. oldfd 等于 newfd:\n");
    fd = open("/dev/null", O_RDWR);
    if (fd != -1) {
        result = dup3(fd, fd, 0);
        if (result == -1) {
            printf("   错误: %s\n", strerror(errno));
            if (errno == EINVAL) {
                printf("   说明: dup3 不允许 oldfd 等于 newfd\n");
                printf("   对比: dup2 在这种情况下会返回 newfd\n");
            }
        } else {
            printf("   意外成功: %d\n", result);
        }
        close(fd);
    }
    
    // 4. 文件描述符数量达到上限
    printf("\n4. 文件描述符数量达到上限的模拟:\n");
    printf("   这种情况很难模拟,但会返回 EMFILE 错误\n");
}

int main() {
    demonstrate_dup3_errors();
    
    printf("\n=== Dup 系列函数对比 ===\n");
    printf("函数     | 目标描述符 | 支持标志 | oldfd==newfd 行为\n");
    printf("---------|------------|----------|------------------\n");
    printf("dup      | 自动分配   | 否       | N/A\n");
    printf("dup2     | 指定       | 否       | 返回 newfd\n");
    printf("dup3     | 指定       | 是       | 返回错误\n");
    
    return 0;
}

编译和运行说明

# 编译示例
gcc -o dup3_basic dup3_basic.c
gcc -o dup3_cloexec dup3_cloexec.c
gcc -o dup3_errors dup3_errors.c

# 运行示例
./dup3_basic
./dup3_cloexec
./dup3_errors

重要注意事项

  1. Linux 特定dup3 是 Linux 特定的系统调用,在其他 Unix 系统上可能不可用
  2. 标志支持: 主要优势是支持 O_CLOEXEC 标志,提高程序安全性
  3. 原子操作dup3 是原子操作,避免了 dup2 + fcntl 组合可能的竞态条件
  4. 错误处理: 当 oldfd 等于 newfd 时,dup3 返回错误,而 dup2 返回 newfd
  5. 兼容性: 如果需要跨平台兼容性,应该使用 dup2 或 fcntl
发表在 linux文章 | 留下评论

dup-dup2系统调用及示例

我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 dup 和 dup2 函数,它们用于复制一个已存在的文件描述符 (file descriptor)。


1. 函数介绍

dup 和 dup2 是 Linux 系统调用,它们的功能是创建一个指向同一文件表项 (open file description) 的新文件描述符

简单来说,当你调用 dup 或 dup2 时,你得到的是一个别名副本,这个新文件描述符和原始文件描述符指向同一个打开的文件,共享文件的:

  • 文件偏移量 (file offset): 通过一个描述符读写会改变文件位置,通过另一个描述符读写会从新的位置开始。
  • 状态标志 (status flags): 如 O_APPENDO_NONBLOCK 等。
  • 文件锁 (file locks): 通过任何一个描述符获取的锁,对另一个描述符也有效。

它们最常见的用途是重定向标准输入、标准输出或标准错误。例如,将一个程序的输出重定向到文件,而不是终端。

你可以把文件描述符想象成一个指向文件的“把手”。dup 就像是给这个“把手”又做了一个一模一样的复制品。你拿着任何一个“把手”都能操作同一个文件,而且它们的状态是同步的。


2. 函数原型

#include <unistd.h> // 必需

// 复制文件描述符 (返回新的最小可用 fd)
int dup(int oldfd);

// 复制文件描述符到指定的新 fd
int dup2(int oldfd, int newfd);

3. 功能

  • dup(int oldfd):
    • 复制一个已存在的文件描述符 oldfd
    • 内核会在当前进程中选择最小的未使用的文件描述符号码作为新的描述符。
    • 新的文件描述符和 oldfd 指向同一个文件表项。
  • dup2(int oldfd, int newfd):
    • 复制文件描述符 oldfd,并强制使复制得到的新文件描述符的号码为 newfd
    • 如果 newfd 已经打开(指向另一个文件),dup2 会在复制前先关闭 newfd(相当于先调用 close(newfd))。
    • 如果 oldfd 和 newfd 相同,dup2 什么都不做,直接返回 newfd

4. 参数

  • dup:
    • int oldfd: 要被复制的现有有效文件描述符。
  • dup2:
    • int oldfd: 要被复制的现有有效文件描述符。
    • int newfd: 请求的新文件描述符号码。
      • 如果 newfd 已经打开,它会被关闭。
      • 如果 newfd 等于 oldfd,则不执行任何操作。

5. 返回值

  • 成功时:
    • 返回新的文件描述符号码。
    • 对于 dup,这个号码是当前进程中最小的可用号码。
    • 对于 dup2,这个号码就是请求的 newfd
  • 失败时:
    • 返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 EBADF oldfd 或 newfd 无效,EMFILE 进程打开的文件描述符已达上限等)。

6. 相似函数,或关联函数

  • fcntldup(oldfd) 等价于 fcntl(oldfd, F_DUPFD, 0)fcntl 提供了更灵活的复制方式,例如 F_DUPFD_CLOEXEC 可以在复制时设置 FD_CLOEXEC 标志。
  • closedup2 在复制前如果 newfd 已打开,会隐式调用 close(newfd)
  • open: 用于获取最初的文件描述符。
  • readwrite: 对复制后的文件描述符进行操作。

7. 示例代码

示例 1:使用 dup 和 dup2 重定向标准输出

这个例子演示了如何使用 dup 和 dup2 来保存原始标准输出,然后将标准输出重定向到一个文件,最后再恢复标准输出。

#include <unistd.h>  // dup, dup2, close, write
#include <fcntl.h>   // open, O_WRONLY, O_CREAT, O_TRUNC
#include <stdio.h>   // printf, perror
#include <stdlib.h>  // exit

int main() {
    int saved_stdout;   // 用于保存原始标准输出的文件描述符
    int file_fd;        // 用于重定向输出的文件描述符

    // 1. 保存原始的标准输出 (stdout, 文件描述符 1)
    saved_stdout = dup(STDOUT_FILENO);
    if (saved_stdout == -1) {
        perror("dup saved_stdout");
        exit(EXIT_FAILURE);
    }
    printf("Saved original stdout to fd %d\n", saved_stdout);

    // 2. 打开一个文件用于重定向
    file_fd = open("output_redirection.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (file_fd == -1) {
        perror("open file for redirection");
        close(saved_stdout); // 清理
        exit(EXIT_FAILURE);
    }
    printf("Opened file 'output_redirection.txt' with fd %d\n", file_fd);

    // 3. 将标准输出重定向到文件
    // 方法一:使用 dup2
    if (dup2(file_fd, STDOUT_FILENO) == -1) {
        perror("dup2 redirect stdout");
        close(file_fd);
        close(saved_stdout);
        exit(EXIT_FAILURE);
    }
    printf("Standard output redirected to file.\n");
    // 注意:从现在开始,printf 的输出将写入到文件中,而不是终端!

    // 4. 输出一些内容到文件
    printf("This line goes to the file.\n");
    printf("This line also goes to the file.\n");

    // 5. 恢复标准输出
    // 使用 dup2 将保存的原始 stdout 描述符复制回 STDOUT_FILENO
    if (dup2(saved_stdout, STDOUT_FILENO) == -1) {
        perror("dup2 restore stdout");
        // 清理
        close(file_fd);
        close(saved_stdout);
        exit(EXIT_FAILURE);
    }
    printf("Standard output restored to terminal.\n");
    // 注意:从现在开始,printf 的输出将重新显示在终端上!

    // 6. 再输出一些内容到终端
    printf("This line goes back to the terminal.\n");

    // 7. 关闭所有打开的文件描述符
    // close(file_fd) 实际上关闭了文件表项的一个引用
    // dup2 在重定向时已经关闭了原来的 STDOUT_FILENO 对应的文件表项引用
    // 所以这里只需要关闭 file_fd 和我们自己保存的 saved_stdout
    if (close(file_fd) == -1) {
        perror("close file_fd");
    }
    // 关闭 saved_stdout 也会关闭它指向的文件表项(即原始的终端 stdout)
    if (close(saved_stdout) == -1) {
        perror("close saved_stdout");
    }

    printf("All file descriptors closed.\n");
    return 0;
}

代码解释:

  1. 调用 dup(STDOUT_FILENO) 创建原始标准输出(文件描述符 1)的一个副本,并将其保存在 saved_stdout 中。这个副本让我们可以稍后恢复标准输出。
  2. 使用 open 创建或打开一个名为 output_redirection.txt 的文件,用于接收重定向的输出。
  3. 调用 dup2(file_fd, STDOUT_FILENO)。这会:
    • 关闭当前的 STDOUT_FILENO(文件描述符 1)。
    • 将 file_fd 复制一份,并使这个新副本的号码为 1 (即 STDOUT_FILENO)。
    • 现在,文件描述符 1 和 file_fd 都指向 output_redirection.txt 文件。
  4. 执行 printf。因为标准输出已经被重定向,所以这些内容会写入到 output_redirection.txt 文件中。
  5. 调用 dup2(saved_stdout, STDOUT_FILENO) 来恢复标准输出。这会将之前保存的原始终端输出描述符复制回文件描述符 1。
  6. 再次执行 printf。现在标准输出已经恢复,内容会显示在终端上。
  7. 最后,使用 close 关闭所有打开的文件描述符。

示例 2:dup2 自动关闭目标文件描述符

这个例子重点演示 dup2 在目标文件描述符已打开时自动关闭它的行为。

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    int fd1, fd2;
    char buffer[100];
    ssize_t bytes_read;

    // 1. 打开两个不同的文件
    fd1 = open("file1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd1 == -1) {
        perror("open file1.txt");
        exit(EXIT_FAILURE);
    }
    write(fd1, "Content of file 1\n", 18);

    fd2 = open("file2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd2 == -1) {
        perror("open file2.txt");
        close(fd1);
        exit(EXIT_FAILURE);
    }
    write(fd2, "Content of file 2\n", 18);

    printf("Initially:\n");
    printf("  fd1 points to 'file1.txt' (fd=%d)\n", fd1);
    printf("  fd2 points to 'file2.txt' (fd=%d)\n", fd2);

    // 2. 使用 dup2 将 fd1 复制到 fd2
    // 这会自动关闭 fd2 当前指向的 'file2.txt'
    printf("\nCalling dup2(fd1, fd2)...\n");
    if (dup2(fd1, fd2) == -1) {
        perror("dup2(fd1, fd2)");
        close(fd1);
        close(fd2);
        exit(EXIT_FAILURE);
    }

    printf("After dup2(fd1, fd2):\n");
    printf("  fd1 still points to 'file1.txt' (fd=%d)\n", fd1);
    printf("  fd2 now ALSO points to 'file1.txt' (fd=%d)\n", fd2);
    printf("  'file2.txt' has been closed automatically.\n");

    // 3. 验证:向 fd2 写入数据,应该出现在 'file1.txt' 中
    write(fd2, "Data written via fd2 after dup2\n", 32);

    // 4. 关闭文件描述符
    // 关闭 fd1 和 fd2 实际上是关闭同一个文件表项的两个引用
    // 内核会在最后一个引用关闭时才真正关闭文件
    close(fd1);
    close(fd2); // 这个 close 实际上是关闭 fd1/fd2 共同指向的文件表项

    // 5. 读取 file1.txt 来验证内容
    printf("\nContents of 'file1.txt' after operations:\n");
    int read_fd = open("file1.txt", O_RDONLY);
    if (read_fd != -1) {
        while ((bytes_read = read(read_fd, buffer, sizeof(buffer) - 1)) > 0) {
            buffer[bytes_read] = '\0';
            printf("%s", buffer);
        }
        close(read_fd);
    }

    // 6. 检查 file2.txt 是否为空或被截断 (因为它被 dup2 自动关闭了)
    printf("\nContents of 'file2.txt' (should be empty or just initial content if not truncated):\n");
    read_fd = open("file2.txt", O_RDONLY);
    if (read_fd != -1) {
        while ((bytes_read = read(read_fd, buffer, sizeof(buffer) - 1)) > 0) {
            buffer[bytes_read] = '\0';
            printf("%s", buffer);
        }
        close(read_fd);
    }
    if (bytes_read == 0) {
         printf("(file2.txt is empty)\n");
    }

    return 0;
}

代码解释:

  1. 打开两个文件 file1.txt 和 file2.txt,分别得到文件描述符 fd1 和 fd2
  2. 向两个文件写入不同的初始内容。
  3. 调用 dup2(fd1, fd2)。关键点在于:
    • fd2 当前是打开的,指向 file2.txt
    • dup2 会自动关闭 fd2
    • 然后,它将 fd1(指向 file1.txt)复制一份,并使这个副本的号码为 fd2
  4. 现在,fd1 和 fd2 都指向 file1.txt
  5. 向 fd2 写入数据,数据会出现在 file1.txt 中,证明了 fd2 现在确实指向 file1.txt
  6. 关闭 fd1 和 fd2。由于它们指向同一个文件表项,文件只会在最后一个引用关闭时才真正关闭。
  7. 读取 file1.txt 和 file2.txt 的内容来验证操作结果。file1.txt 应该包含所有写入的内容,而 file2.txt 可能为空(因为它在 dup2 时被关闭了,如果它之前的内容没有被 O_TRUNC 重新截断,则可能还保留着)。

示例 3:dup 选择最小可用文件描述符

这个例子演示 dup 如何选择最小的可用文件描述符。

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    int fd, new_fd1, new_fd2;

    // 1. 打开一个文件
    fd = open("test_dup.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }
    printf("Original file descriptor: %d\n", fd);

    // 2. 复制文件描述符 (使用 dup)
    new_fd1 = dup(fd);
    if (new_fd1 == -1) {
        perror("dup 1");
        close(fd);
        exit(EXIT_FAILURE);
    }
    printf("First dup() returned new fd: %d\n", new_fd1);

    // 3. 再次复制
    new_fd2 = dup(fd);
    if (new_fd2 == -1) {
        perror("dup 2");
        close(fd);
        close(new_fd1);
        exit(EXIT_FAILURE);
    }
    printf("Second dup() returned new fd: %d\n", new_fd2);

    // 4. 写入数据到所有描述符,验证它们指向同一文件
    write(fd, "Written via fd\n", 15);
    write(new_fd1, "Written via new_fd1\n", 20);
    write(new_fd2, "Written via new_fd2\n", 20);

    printf("Data written via all three file descriptors.\n");

    // 5. 关闭所有描述符
    close(fd);
    close(new_fd1);
    close(new_fd2);

    printf("All file descriptors closed.\n");
    return 0;
}

代码解释:

  1. 打开一个文件,得到文件描述符 fd(假设为 3)。
  2. 调用 dup(fd)。因为 0, 1, 2 (stdin, stdout, stderr) 通常已被占用,所以 dup 会返回下一个最小的可用描述符,比如 4。
  3. 再次调用 dup(fd)。现在 3, 4 已被占用,所以会返回 5。
  4. 向 fdnew_fd1new_fd2 写入数据,验证它们都写入了同一个文件。
  5. 关闭所有文件描述符。

总结:

dup 和 dup2 是用于复制文件描述符的强大工具,它们在实现输入/输出重定向、保存和恢复标准流、以及在进程管理(如 fork 后)中非常有用。理解它们的关键在于掌握新旧描述符共享文件表项的特性,以及 dup2 自动关闭目标描述符的行为。

发表在 linux文章 | 留下评论

epoll_create1系统调用及示例

epoll_create1 – 创建epoll实例(扩展版)

函数介绍

epoll_create1epoll_create的扩展版本,支持额外的标志位参数,提供了更多的控制选项。

函数原型

#include <sys/epoll.h>

int epoll_create1(int flags);

功能

创建一个新的epoll实例,支持额外的标志位控制。

参数

  • int flags: 控制标志
    • 0: 与epoll_create(size)相同
    • EPOLL_CLOEXEC: 设置文件描述符在exec时自动关闭

返回值

  • 成功时返回epoll文件描述符
  • 失败时返回-1,并设置errno

特殊限制

  • 需要Linux 2.6.27以上内核支持

相似函数

  • epoll_create(): 基础版本
  • poll()select(): 传统多路复用函数

示例代码

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>

int main() {
    int epfd1, epfd2;
    
    printf("=== Epoll_create1 函数示例 ===\n");
    
    // 示例1: 基本使用
    printf("\n示例1: 基本使用\n");
    
    epfd1 = epoll_create1(0);
    if (epfd1 == -1) {
        perror("epoll_create1(0) 失败");
    } else {
        printf("成功创建epoll实例(无标志): %d\n", epfd1);
        close(epfd1);
    }
    
    // 示例2: 使用EPOLL_CLOEXEC标志
    printf("\n示例2: 使用EPOLL_CLOEXEC标志\n");
    
    epfd2 = epoll_create1(EPOLL_CLOEXEC);
    if (epfd2 == -1) {
        perror("epoll_create1(EPOLL_CLOEXEC) 失败");
    } else {
        printf("成功创建epoll实例(带CLOEXEC): %d\n", epfd2);
        
        // 验证CLOEXEC标志是否设置
        int flags = fcntl(epfd2, F_GETFD);
        if (flags != -1) {
            if (flags & FD_CLOEXEC) {
                printf("FD_CLOEXEC标志已正确设置\n");
            } else {
                printf("FD_CLOEXEC标志未设置\n");
            }
        }
        
        close(epfd2);
    }
    
    // 示例3: 错误处理
    printf("\n示例3: 错误处理\n");
    
    int invalid_fd = epoll_create1(-1);
    if (invalid_fd == -1) {
        if (errno == EINVAL) {
            printf("无效标志错误处理正确: %s\n", strerror(errno));
        }
    }
    
    // 示例4: 与epoll_create对比
    printf("\n示例4: 与epoll_create对比\n");
    
    int fd1 = epoll_create(10);
    int fd2 = epoll_create1(0);
    
    if (fd1 != -1 && fd2 != -1) {
        printf("epoll_create返回: %d\n", fd1);
        printf("epoll_create1(0)返回: %d\n", fd2);
        printf("两者功能基本相同\n");
        
        close(fd1);
        close(fd2);
    }
    
    // 示例5: 实际应用演示
    printf("\n示例5: 实际应用演示\n");
    
    // 推荐的现代用法
    int epfd = epoll_create1(EPOLL_CLOEXEC);
    if (epfd != -1) {
        printf("推荐用法: epoll_create1(EPOLL_CLOEXEC) = %d\n", epfd);
        printf("优点: 避免文件描述符泄漏到子进程\n");
        close(epfd);
    }
    
    printf("\nEPOLL_CLOEXEC的优势:\n");
    printf("1. 原子性设置标志,避免竞态条件\n");
    printf("2. 防止文件描述符泄漏到exec的程序\n");
    printf("3. 提高程序安全性\n");
    printf("4. 减少系统调用次数\n\n");
    
    printf("总结:\n");
    printf("epoll_create1是现代Linux编程推荐的epoll创建函数\n");
    printf("EPOLL_CLOEXEC标志提供了更好的安全性\n");
    printf("在支持的系统上应优先使用epoll_create1\n");
    
    return 0;
}
发表在 linux文章 | 留下评论

epoll_create系统调用及示例

epoll_create – 创建epoll实例

函数介绍

epoll_create系统调用用于创建一个epoll实例,返回一个文件描述符,用于后续的epoll操作。epoll是Linux特有的I/O多路复用机制,比传统的select和poll更高效。

函数原型

#include <sys/epoll.h>
#include <sys/syscall.h>
#include <unistd.h>

int epoll_create(int size);

功能

创建一个新的epoll实例,用于监控多个文件描述符的I/O事件。

参数

  • int size: 建议的内核为该epoll实例分配的事件数(Linux 2.6.8后被忽略)

返回值

  • 成功时返回epoll文件描述符(非负整数)
  • 失败时返回-1,并设置errno

特殊限制

  • 需要Linux 2.5.44以上内核支持
  • 每个进程可创建的epoll实例数量受系统限制

相似函数

  • epoll_create1(): 现代版本,支持标志位
  • poll(): 传统的轮询机制
  • select(): 传统的多路复用机制

示例代码

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/syscall.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>

// 系统调用包装
static int epoll_create_wrapper(int size) {
    return syscall(__NR_epoll_create, size);
}

int main() {
    int epfd;
    
    printf("=== Epoll_create 函数示例 ===\n");
    
    // 示例1: 基本的epoll实例创建
    printf("\n示例1: 基本的epoll实例创建\n");
    
    epfd = epoll_create_wrapper(10); // size参数在现代内核中被忽略
    if (epfd == -1) {
        perror("epoll_create 失败");
        exit(EXIT_FAILURE);
    }
    
    printf("成功创建epoll实例,文件描述符: %d\n", epfd);
    
    // 检查epoll文件描述符是否有效
    if (fcntl(epfd, F_GETFD) != -1) {
        printf("epoll文件描述符验证成功\n");
    }
    
    // 关闭epoll实例
    if (close(epfd) == -1) {
        perror("关闭epoll实例失败");
    } else {
        printf("成功关闭epoll实例\n");
    }
    
    // 示例2: 多个epoll实例创建
    printf("\n示例2: 多个epoll实例创建\n");
    
    int epfds[5];
    int created_count = 0;
    
    for (int i = 0; i < 5; i++) {
        epfds[i] = epoll_create_wrapper(1);
        if (epfds[i] != -1) {
            printf("创建第%d个epoll实例: %d\n", i+1, epfds[i]);
            created_count++;
        } else {
            printf("创建第%d个epoll实例失败: %s\n", i+1, strerror(errno));
            break;
        }
    }
    
    // 关闭所有创建的epoll实例
    for (int i = 0; i < created_count; i++) {
        if (close(epfds[i]) == -1) {
            printf("关闭epoll实例%d失败: %s\n", epfds[i], strerror(errno));
        } else {
            printf("关闭epoll实例%d成功\n", epfds[i]);
        }
    }
    
    // 示例3: 错误处理演示
    printf("\n示例3: 错误处理演示\n");
    
    // 使用负数作为size参数
    epfd = epoll_create_wrapper(-1);
    if (epfd == -1) {
        if (errno == EINVAL) {
            printf("负数size参数错误处理正确: %s\n", strerror(errno));
        }
    }
    
    // 检查系统资源限制
    printf("\n系统epoll相关限制:\n");
    FILE *fp = fopen("/proc/sys/fs/epoll/max_user_watches", "r");
    if (fp != NULL) {
        char line[256];
        if (fgets(line, sizeof(line), fp)) {
            printf("最大用户监视数量: %s", line);
        }
        fclose(fp);
    }
    
    // 示例4: epoll vs select/poll对比说明
    printf("\n示例4: epoll优势说明\n");
    printf("epoll相比select/poll的优势:\n");
    printf("1. 文件描述符数量无限制(受系统资源限制)\n");
    printf("2. O(1)时间复杂度的事件通知\n");
    printf("3. 内存使用更高效\n");
    printf("4. 支持边缘触发和水平触发模式\n");
    printf("5. 更好的可扩展性\n\n");
    
    // 示例5: 实际使用场景
    printf("示例5: 实际使用场景\n");
    printf("epoll的典型应用场景:\n");
    printf("1. 高并发网络服务器\n");
    printf("2. 实时数据处理系统\n");
    printf("3. 聊天服务器和即时通讯\n");
    printf("4. 代理服务器和负载均衡\n");
    printf("5. 监控和日志收集系统\n\n");
    
    // 示例6: 性能考虑
    printf("示例6: 性能考虑\n");
    printf("epoll性能优化建议:\n");
    printf("1. 合理设置epoll_wait的maxevents参数\n");
    printf("2. 避免频繁添加/删除监视的文件描述符\n");
    printf("3. 使用边缘触发模式提高效率\n");
    printf("4. 批量处理事件\n");
    printf("5. 及时关闭不需要的epoll实例\n\n");
    
    printf("总结:\n");
    printf("epoll_create是创建epoll实例的基础函数\n");
    printf("虽然现代推荐使用epoll_create1,但epoll_create仍然广泛支持\n");
    printf("返回的文件描述符需要妥善管理\n");
    printf("epoll是Linux高性能网络编程的重要工具\n");
    
    return 0;
}
发表在 linux文章 | 留下评论

epoll_ctl系统调用及示例

epoll_ctl – 控制epoll实例

函数介绍

epoll_ctl系统调用用于控制epoll实例,可以添加、修改或删除要监视的文件描述符。

函数原型

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

功能

对epoll实例进行控制操作,管理要监视的文件描述符。

参数

  • int epfd: epoll实例的文件描述符
  • int op: 操作类型
    • EPOLL_CTL_ADD: 添加文件描述符到监视集合
    • EPOLL_CTL_MOD: 修改已监视文件描述符的设置
    • EPOLL_CTL_DEL: 从监视集合中删除文件描述符
  • int fd: 要控制的文件描述符
  • struct epoll_event *event: 指向epoll_event结构体的指针

返回值

  • 成功时返回0
  • 失败时返回-1,并设置errno

特殊限制

  • 需要有效的epoll文件描述符
  • 文件描述符必须是有效的
  • 某些操作需要文件描述符已在监视集合中

相似函数

  • epoll_wait(): 等待epoll事件
  • poll(): 传统轮询控制

示例代码

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>

int main() {
    int epfd, sockfd;
    struct epoll_event ev;
    
    printf("=== Epoll_ctl 函数示例 ===\n");
    
    // 创建epoll实例
    epfd = epoll_create1(EPOLL_CLOEXEC);
    if (epfd == -1) {
        perror("epoll_create1 失败");
        exit(EXIT_FAILURE);
    }
    printf("创建epoll实例: %d\n", epfd);
    
    // 创建测试用的socket
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("创建socket失败");
        close(epfd);
        exit(EXIT_FAILURE);
    }
    printf("创建测试socket: %d\n", sockfd);
    
    // 设置socket为非阻塞模式
    int flags = fcntl(sockfd, F_GETFL, 0);
    fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
    printf("设置socket为非阻塞模式\n");
    
    // 示例1: 添加文件描述符到epoll监视集合
    printf("\n示例1: 添加文件描述符到epoll\n");
    
    ev.events = EPOLLIN | EPOLLOUT | EPOLLET; // 读事件、写事件、边缘触发
    ev.data.fd = sockfd;
    
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev) == -1) {
        perror("EPOLL_CTL_ADD 失败");
    } else {
        printf("成功添加socket %d 到epoll监视集合\n", sockfd);
        printf("监视事件: EPOLLIN | EPOLLOUT | EPOLLET\n");
    }
    
    // 示例2: 修改已监视的文件描述符
    printf("\n示例2: 修改已监视的文件描述符\n");
    
    ev.events = EPOLLIN | EPOLLET; // 只监视读事件和边缘触发
    ev.data.fd = sockfd;
    
    if (epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev) == -1) {
        if (errno == ENOENT) {
            printf("修改失败,文件描述符未在监视集合中: %s\n", strerror(errno));
        } else {
            perror("EPOLL_CTL_MOD 失败");
        }
    } else {
        printf("成功修改socket %d 的监视设置\n", sockfd);
        printf("新监视事件: EPOLLIN | EPOLLET\n");
    }
    
    // 示例3: 删除文件描述符
    printf("\n示例3: 删除文件描述符\n");
    
    if (epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL) == -1) {
        if (errno == ENOENT) {
            printf("删除失败,文件描述符不在监视集合中: %s\n", strerror(errno));
        } else {
            perror("EPOLL_CTL_DEL 失败");
        }
    } else {
        printf("成功从epoll监视集合中删除socket %d\n", sockfd);
    }
    
    // 示例4: 错误处理演示
    printf("\n示例4: 错误处理演示\n");
    
    // 使用无效的epoll文件描述符
    if (epoll_ctl(-1, EPOLL_CTL_ADD, sockfd, &ev) == -1) {
        if (errno == EBADF) {
            printf("无效epoll文件描述符错误处理正确: %s\n", strerror(errno));
        }
    }
    
    // 使用无效的操作类型
    if (epoll_ctl(epfd, 999, sockfd, &ev) == -1) {
        if (errno == EINVAL) {
            printf("无效操作类型错误处理正确: %s\n", strerror(errno));
        }
    }
    
    // 使用无效的文件描述符
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, -1, &ev) == -1) {
        if (errno == EBADF) {
            printf("无效文件描述符错误处理正确: %s\n", strerror(errno));
        }
    }
    
    // 示例5: epoll_event结构说明
    printf("\n示例5: epoll_event结构说明\n");
    printf("struct epoll_event 结构体:\n");
    printf("  uint32_t events;  // 事件类型\n");
    printf("  epoll_data_t data; // 用户数据\n\n");
    
    printf("常用事件类型:\n");
    printf("  EPOLLIN:    可读事件\n");
    printf("  EPOLLOUT:   可写事件\n");
    printf("  EPOLLPRI:   紧急数据可读\n");
    printf("  EPOLLERR:   错误条件\n");
    printf("  EPOLLHUP:   挂起事件\n");
    printf("  EPOLLET:    边缘触发模式\n");
    printf("  EPOLLONESHOT: 一次性事件\n");
    printf("  EPOLLRDHUP: 对端关闭连接\n\n");
    
    // 示例6: epoll_data_t联合体说明
    printf("epoll_data_t联合体(可存储不同类型的数据):\n");
    printf("  void *ptr;    // 指针\n");
    printf("  int fd;       // 文件描述符\n");
    printf("  uint32_t u32; // 32位无符号整数\n");
    printf("  uint64_t u64; // 64位无符号整数\n\n");
    
    // 演示使用用户数据
    printf("示例6: 使用用户数据\n");
    
    struct epoll_event event_with_data;
    event_with_data.events = EPOLLIN;
    event_with_data.data.fd = sockfd;
    
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event_with_data) == 0) {
        printf("使用文件描述符作为用户数据: %d\n", event_with_data.data.fd);
        
        // 删除文件描述符
        epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
    }
    
    // 示例7: 实际应用场景
    printf("\n示例7: 实际应用场景\n");
    
    printf("服务器程序中的典型使用流程:\n");
    printf("1. 创建epoll实例\n");
    printf("2. 添加监听socket到epoll\n");
    printf("3. 在事件循环中:\n");
    printf("   a. 调用epoll_wait等待事件\n");
    printf("   b. 处理就绪事件\n");
    printf("   c. 根据需要添加/修改/删除监视的文件描述符\n\n");
    
    printf("常见操作模式:\n");
    printf("监听socket: EPOLLIN | EPOLLET\n");
    printf("客户端socket: EPOLLIN | EPOLLOUT | EPOLLET\n");
    printf("一次性事件: EPOLLIN | EPOLLONESHOT\n\n");
    
    // 清理资源
    close(sockfd);
    close(epfd);
    
    printf("总结:\n");
    printf("epoll_ctl是管理epoll监视集合的核心函数\n");
    printf("支持添加、修改、删除三种基本操作\n");
    printf("正确使用事件类型和用户数据很重要\n");
    printf("需要妥善处理各种错误情况\n");
    
    return 0;
}
发表在 linux文章 | 留下评论

epoll_pwait系统调用及示例

epoll_pwait – 带信号掩码的epoll等待

函数介绍

epoll_pwaitepoll_wait的扩展版本,支持在等待期间设置临时的信号掩码,提供了更精确的信号控制能力。

函数原型

#include <sys/epoll.h>
#include <signal.h>

int epoll_pwait(int epfd, struct epoll_event *events,
                int maxevents, int timeout,
                const sigset_t *sigmask);

功能

等待epoll事件,同时可以指定在等待期间临时应用的信号掩码。

参数

  • int epfd: epoll实例的文件描述符
  • struct epoll_event *events: 用于存储就绪事件的数组
  • int maxevents: events数组的最大元素数
  • int timeout: 超时时间(毫秒)
  • const sigset_t *sigmask: 临时信号掩码(NULL表示不改变)

返回值

  • 成功时返回就绪事件的数量
  • 失败时返回-1,并设置errno

特殊限制

  • 需要Linux 2.6.19以上内核支持
  • 原子性地设置信号掩码和等待事件

相似函数

  • epoll_wait(): 基础版本
  • pselect(): 带信号掩码的select版本
  • ppoll(): 带信号掩码的poll版本

示例代码

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <signal.h>

int main() {
    int epfd, sockfd, nfds;
    struct epoll_event ev, events[10];
    sigset_t mask, orig_mask;
    
    printf("=== Epoll_pwait 函数示例 ===\n");
    
    // 创建epoll实例
    epfd = epoll_create1(EPOLL_CLOEXEC);
    if (epfd == -1) {
        perror("epoll_create1 失败");
        exit(EXIT_FAILURE);
    }
    printf("创建epoll实例: %d\n", epfd);
    
    // 创建测试用的socket
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("创建socket失败");
        close(epfd);
        exit(EXIT_FAILURE);
    }
    printf("创建测试socket: %d\n", sockfd);
    
    // 设置socket为非阻塞模式
    int flags = fcntl(sockfd, F_GETFL, 0);
    fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
    
    // 添加socket到epoll监视集合
    ev.events = EPOLLIN | EPOLLOUT;
    ev.data.fd = sockfd;
    
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev) == -1) {
        perror("epoll_ctl 添加失败");
        close(sockfd);
        close(epfd);
        exit(EXIT_FAILURE);
    }
    printf("添加socket到epoll监视集合\n");
    
    // 示例1: 基本使用(不改变信号掩码)
    printf("\n示例1: 基本使用(不改变信号掩码)\n");
    
    nfds = epoll_pwait(epfd, events, 10, 1000, NULL);
    if (nfds == -1) {
        if (errno == EINTR) {
            printf("等待被信号中断\n");
        } else {
            perror("epoll_pwait 失败");
        }
    } else {
        printf("等待完成,就绪事件数: %d\n", nfds);
    }
    
    // 示例2: 设置临时信号掩码
    printf("\n示例2: 设置临时信号掩码\n");
    
    // 初始化信号掩码
    sigemptyset(&mask);
    sigaddset(&mask, SIGUSR1);
    sigaddset(&mask, SIGUSR2);
    
    printf("设置临时阻塞SIGUSR1和SIGUSR2信号\n");
    
    nfds = epoll_pwait(epfd, events, 10, 2000, &mask);
    if (nfds == -1) {
        if (errno == EINTR) {
            printf("等待被未阻塞的信号中断\n");
        } else {
            perror("epoll_pwait 失败");
        }
    } else {
        printf("等待完成,就绪事件数: %d\n", nfds);
    }
    
    // 示例3: 与普通epoll_wait对比
    printf("\n示例3: 与epoll_wait对比\n");
    
    printf("epoll_wait vs epoll_pwait的区别:\n");
    printf("epoll_wait:\n");
    printf("  - 使用当前进程的信号掩码\n");
    printf("  - 无法在调用中临时改变信号掩码\n\n");
    
    printf("epoll_pwait:\n");
    printf("  - 可以指定临时信号掩码\n");
    printf("  - 原子性地设置掩码和等待\n");
    printf("  - 避免竞态条件\n\n");
    
    // 示例4: 原子性操作演示
    printf("示例4: 原子性操作演示\n");
    
    printf("说明epoll_pwait的原子性优势:\n");
    printf("传统方式(非原子性):\n");
    printf("  1. sigprocmask(SIG_BLOCK, &mask, &orig_mask);\n");
    printf("  2. epoll_wait(epfd, events, 10, -1);\n");
    printf("  3. sigprocmask(SIG_SETMASK, &orig_mask, NULL);\n");
    printf("  问题: 在步骤1和2之间可能收到信号\n\n");
    
    printf("epoll_pwait方式(原子性):\n");
    printf("  epoll_pwait(epfd, events, 10, -1, &mask);\n");
    printf("  优势: 设置掩码和等待是原子操作\n\n");
    
    // 示例5: 错误处理演示
    printf("示例5: 错误处理演示\n");
    
    // 使用无效的信号掩码
    nfds = epoll_pwait(epfd, events, 10, 1000, (sigset_t*)-1);
    if (nfds == -1) {
        if (errno == EINVAL) {
            printf("无效信号掩码错误处理正确: %s\n", strerror(errno));
        }
    }
    
    // 其他错误与epoll_wait相同
    nfds = epoll_pwait(-1, events, 10, 1000, &mask);
    if (nfds == -1) {
        if (errno == EBADF) {
            printf("无效epoll文件描述符错误处理正确\n");
        }
    }
    
    // 示例6: 实际应用场景
    printf("\n示例6: 实际应用场景\n");
    
    printf("多线程服务器中的信号处理:\n");
    printf("场景: 主线程处理网络事件,信号处理线程处理信号\n");
    printf("要求: 主线程在epoll_wait期间不被某些信号中断\n\n");
    
    printf("实现方案:\n");
    printf("sigset_t mask;\n");
    printf("sigemptyset(&mask);\n");
    printf("sigaddset(&mask, SIGUSR1);  // 阻塞用户信号\n");
    printf("sigaddset(&mask, SIGALRM);  // 阻塞定时器信号\n");
    printf("epoll_pwait(epfd, events, MAX_EVENTS, -1, &mask);\n\n");
    
    // 示例7: 信号掩码操作演示
    printf("示例7: 信号掩码操作演示\n");
    
    // 创建复杂的信号掩码
    sigset_t complex_mask;
    sigemptyset(&complex_mask);
    
    // 添加多个信号到掩码
    int signals[] = {SIGINT, SIGTERM, SIGUSR1, SIGUSR2, SIGALRM};
    const char *signal_names[] = {"SIGINT", "SIGTERM", "SIGUSR1", "SIGUSR2", "SIGALRM"};
    
    printf("创建包含以下信号的掩码:\n");
    for (int i = 0; i < 5; i++) {
        sigaddset(&complex_mask, signals[i]);
        printf("  %s\n", signal_names[i]);
    }
    
    printf("使用复杂信号掩码进行epoll_pwait\n");
    nfds = epoll_pwait(epfd, events, 10, 1000, &complex_mask);
    if (nfds != -1) {
        printf("epoll_pwait成功完成\n");
    }
    
    // 示例8: 线程安全考虑
    printf("\n示例8: 线程安全考虑\n");
    
    printf("在多线程环境中的使用:\n");
    printf("1. 每个线程可以有自己的epoll实例\n");
    printf("2. 每个线程可以设置不同的信号掩码\n");
    printf("3. 避免线程间的信号处理冲突\n");
    printf("4. 提高信号处理的精确性\n\n");
    
    printf("线程特定的信号掩码设置:\n");
    printf("线程1: 阻塞SIGUSR1, SIGUSR2\n");
    printf("线程2: 阻塞SIGALRM, SIGVTALRM\n");
    printf("线程3: 不阻塞任何信号\n\n");
    
    // 示例9: 性能和安全性
    printf("示例9: 性能和安全性\n");
    
    printf("epoll_pwait的优势:\n");
    printf("1. 原子性操作,避免竞态条件\n");
    printf("2. 更精确的信号控制\n");
    printf("3. 提高程序的可靠性\n");
    printf("4. 简化信号处理逻辑\n\n");
    
    printf("使用建议:\n");
    printf("1. 在需要精确信号控制时使用epoll_pwait\n");
    printf("2. 合理设计信号掩码\n");
    printf("3. 注意信号处理的线程安全性\n");
    printf("4. 在多线程环境中谨慎使用\n\n");
    
    // 清理资源
    epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
    close(sockfd);
    close(epfd);
    
    printf("总结:\n");
    printf("epoll_pwait是epoll_wait的增强版本\n");
    printf("支持临时信号掩码设置\n");
    printf("提供原子性的信号控制\n");
    printf("适用于需要精确信号处理的场景\n");
    printf("在现代Linux系统中推荐使用\n");
    
    return 0;
}
发表在 linux文章 | 留下评论

epoll_wait系统调用及示例

epoll_wait – 等待epoll事件

函数介绍

epoll_wait系统调用用于等待epoll实例中的文件描述符就绪事件。它是epoll机制的核心函数,用于高效地等待多个文件描述符的I/O事件。

函数原型

#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events,
               int maxevents, int timeout);

功能

等待epoll实例中的事件就绪,返回就绪的事件数组。

参数

  • int epfd: epoll实例的文件描述符
  • struct epoll_event *events: 用于存储就绪事件的数组
  • int maxevents: events数组的最大元素数(必须大于0)
  • int timeout: 超时时间(毫秒)
    • -1: 永久等待
    • 0: 立即返回(轮询)
    • >0: 等待指定毫秒数

返回值

  • 成功时返回就绪事件的数量(0表示超时)
  • 失败时返回-1,并设置errno

特殊限制

  • maxevents必须大于0
  • 需要有效的epoll文件描述符
  • 可能被信号中断

相似函数

  • epoll_pwait(): 支持信号掩码的版本
  • poll(): 传统轮询等待
  • select(): 传统多路复用等待

示例代码

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <signal.h>

// 信号处理标志
volatile sig_atomic_t signal_received = 0;

void signal_handler(int sig) {
    signal_received = 1;
    printf("接收到信号 %d\n", sig);
}

int main() {
    int epfd, sockfd, nfds;
    struct epoll_event ev, events[10];
    
    printf("=== Epoll_wait 函数示例 ===\n");
    
    // 设置信号处理
    signal(SIGINT, signal_handler);
    signal(SIGTERM, signal_handler);
    
    // 创建epoll实例
    epfd = epoll_create1(EPOLL_CLOEXEC);
    if (epfd == -1) {
        perror("epoll_create1 失败");
        exit(EXIT_FAILURE);
    }
    printf("创建epoll实例: %d\n", epfd);
    
    // 创建测试用的socket
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("创建socket失败");
        close(epfd);
        exit(EXIT_FAILURE);
    }
    printf("创建测试socket: %d\n", sockfd);
    
    // 设置socket为非阻塞模式
    int flags = fcntl(sockfd, F_GETFL, 0);
    fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
    
    // 添加socket到epoll监视集合
    ev.events = EPOLLIN | EPOLLOUT;
    ev.data.fd = sockfd;
    
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev) == -1) {
        perror("epoll_ctl 添加失败");
        close(sockfd);
        close(epfd);
        exit(EXIT_FAILURE);
    }
    printf("添加socket到epoll监视集合\n");
    
    // 示例1: 带超时的等待
    printf("\n示例1: 带超时的等待\n");
    
    printf("等待1秒...\n");
    nfds = epoll_wait(epfd, events, 10, 1000); // 1秒超时
    
    if (nfds == -1) {
        if (errno == EINTR) {
            printf("等待被信号中断\n");
        } else {
            perror("epoll_wait 失败");
        }
    } else if (nfds == 0) {
        printf("等待超时(1秒内无事件)\n");
    } else {
        printf("就绪事件数量: %d\n", nfds);
        for (int i = 0; i < nfds; i++) {
            printf("  事件 %d: fd=%d, events=0x%x\n", 
                   i, events[i].data.fd, events[i].events);
        }
    }
    
    // 示例2: 立即返回的轮询
    printf("\n示例2: 立即返回的轮询\n");
    
    nfds = epoll_wait(epfd, events, 10, 0); // 立即返回
    
    if (nfds == -1) {
        perror("epoll_wait 轮询失败");
    } else {
        printf("轮询结果: %d 个事件就绪\n", nfds);
    }
    
    // 示例3: 永久等待(演示信号中断)
    printf("\n示例3: 永久等待(演示信号中断)\n");
    printf("请在5秒内按Ctrl+C发送SIGINT信号\n");
    
    alarm(5); // 5秒后发送SIGALRM
    nfds = epoll_wait(epfd, events, 10, -1); // 永久等待
    
    if (nfds == -1) {
        if (errno == EINTR) {
            printf("永久等待被信号中断\n");
        } else {
            perror("epoll_wait 失败");
        }
    } else {
        printf("永久等待返回: %d 个事件\n", nfds);
    }
    alarm(0); // 取消alarm
    
    // 示例4: 错误处理演示
    printf("\n示例4: 错误处理演示\n");
    
    // 使用无效的epoll文件描述符
    nfds = epoll_wait(-1, events, 10, 1000);
    if (nfds == -1) {
        if (errno == EBADF) {
            printf("无效epoll文件描述符错误处理正确: %s\n", strerror(errno));
        }
    }
    
    // 使用无效的maxevents
    nfds = epoll_wait(epfd, events, 0, 1000);
    if (nfds == -1) {
        if (errno == EINVAL) {
            printf("无效maxevents错误处理正确: %s\n", strerror(errno));
        }
    }
    
    // 使用NULL事件数组
    nfds = epoll_wait(epfd, NULL, 10, 1000);
    if (nfds == -1) {
        if (errno == EFAULT) {
            printf("NULL事件数组错误处理正确: %s\n", strerror(errno));
        }
    }
    
    // 示例5: 事件处理演示
    printf("\n示例5: 事件处理演示\n");
    
    // 模拟不同类型的事件
    printf("常见事件类型处理:\n");
    
    for (int i = 0; i < 10; i++) {
        events[i].events = 0;
        events[i].data.fd = i;
    }
    
    // 设置一些模拟事件
    events[0].events = EPOLLIN;
    events[1].events = EPOLLOUT;
    events[2].events = EPOLLIN | EPOLLOUT;
    events[3].events = EPOLLERR;
    events[4].events = EPOLLHUP;
    
    printf("模拟事件处理:\n");
    for (int i = 0; i < 5; i++) {
        printf("  fd %d: ", events[i].data.fd);
        if (events[i].events & EPOLLIN) printf("可读 ");
        if (events[i].events & EPOLLOUT) printf("可写 ");
        if (events[i].events & EPOLLERR) printf("错误 ");
        if (events[i].events & EPOLLHUP) printf("挂起 ");
        printf("\n");
    }
    
    // 示例6: 性能考虑
    printf("\n示例6: 性能考虑\n");
    
    printf("epoll_wait性能优化建议:\n");
    printf("1. 合理设置maxevents参数\n");
    printf("   - 不要过大浪费内存\n");
    printf("   - 不要过小频繁调用\n");
    printf("2. 使用适当的超时时间\n");
    printf("   - 根据应用需求选择\n");
    printf("3. 批量处理就绪事件\n");
    printf("   - 减少系统调用次数\n");
    printf("4. 避免在事件处理中阻塞\n");
    printf("   - 保持事件循环响应性\n\n");
    
    // 示例7: 实际服务器循环演示
    printf("示例7: 实际服务器循环演示\n");
    
    printf("典型的服务器事件循环:\n");
    printf("while (running) {\n");
    printf("    int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);\n");
    printf("    if (nfds == -1) {\n");
    printf("        if (errno == EINTR) continue;  // 被信号中断\n");
    printf("        else break;  // 真正的错误\n");
    printf("    }\n");
    printf("    \n");
    printf("    for (int i = 0; i < nfds; i++) {\n");
    printf("        if (events[i].data.fd == listen_sock) {\n");
    printf("            // 处理新连接\n");
    printf("        } else {\n");
    printf("            // 处理已连接socket的数据\n");
    printf("        }\n");
    printf("    }\n");
    printf("}\n\n");
    
    // 示例8: 超时时间说明
    printf("示例8: 超时时间说明\n");
    printf("timeout参数说明:\n");
    printf("  -1: 永久等待,直到有事件或被信号中断\n");
    printf("   0: 立即返回,不阻塞(轮询模式)\n");
    printf("  >0: 等待指定毫秒数\n\n");
    
    printf("超时时间选择建议:\n");
    printf("实时应用: 较小的超时值(1-100ms)\n");
    printf("批处理应用: 较大的超时值(1000ms以上)\n");
    printf("交互应用: 中等超时值(100-500ms)\n\n");
    
    // 清理资源
    epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
    close(sockfd);
    close(epfd);
    
    printf("总结:\n");
    printf("epoll_wait是epoll机制的核心等待函数\n");
    printf("支持灵活的超时控制\n");
    printf("正确处理信号中断很重要\n");
    printf("合理的maxevents和timeout设置影响性能\n");
    printf("是构建高性能网络服务器的基础\n");
    
    return 0;
}
发表在 linux文章 | 留下评论

eventfd系统调用及示例

eventfd – 创建事件文件描述符

函数介绍

eventfd系统调用用于创建一个事件文件描述符,用于用户空间程序之间的事件通知。它提供了一个简单的计数器机制,可以用于线程间或进程间的同步。

函数原型

#include <sys/eventfd.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdint.h>

int eventfd(unsigned int initval, int flags);

功能

创建一个事件文件描述符,内部维护一个64位无符号整数计数器,用于事件通知和同步。

参数

  • unsigned int initval: 计数器的初始值
  • int flags: 控制标志
    • 0: 基本模式
    • EFD_CLOEXEC: 设置执行时关闭标志
    • EFD_NONBLOCK: 设置非阻塞模式
    • EFD_SEMAPHORE: 信号量模式(每次读取递减1而不是重置为0)

返回值

  • 成功时返回事件文件描述符(非负整数)
  • 失败时返回-1,并设置errno

特殊限制

  • 需要Linux 2.6.22以上内核支持
  • 计数器值有最大限制(0xfffffffffffffffeULL)

相似函数

  • eventfd2(): 现代版本,更好的标志支持
  • pipe(): 管道机制
  • signalfd(): 信号文件描述符

示例代码

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/eventfd.h>
#include <sys/syscall.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <pthread.h>

// 系统调用包装(如果glibc不支持)
static int eventfd_wrapper(unsigned int initval, int flags) {
    return syscall(__NR_eventfd2, initval, flags);
}

// 线程函数
void* thread_function(void* arg) {
    int efd = *(int*)arg;
    uint64_t value = 1;
    
    printf("  子线程: 准备发送事件通知\n");
    
    // 发送事件通知
    if (write(efd, &value, sizeof(value)) != sizeof(value)) {
        perror("  子线程: 写入eventfd失败");
    } else {
        printf("  子线程: 成功发送事件通知\n");
    }
    
    return NULL;
}

int main() {
    int efd;
    uint64_t value;
    pthread_t thread;
    
    printf("=== Eventfd 函数示例 ===\n");
    
    // 示例1: 基本使用
    printf("\n示例1: 基本使用\n");
    
    efd = eventfd_wrapper(0, 0);
    if (efd == -1) {
        perror("eventfd 创建失败");
        exit(EXIT_FAILURE);
    }
    printf("成功创建eventfd,文件描述符: %d\n", efd);
    
    // 检查文件描述符属性
    int flags = fcntl(efd, F_GETFD);
    if (flags != -1) {
        printf("eventfd文件描述符验证成功\n");
    }
    
    // 关闭eventfd
    close(efd);
    printf("关闭eventfd\n");
    
    // 示例2: 基本的事件通知
    printf("\n示例2: 基本的事件通知\n");
    
    efd = eventfd_wrapper(0, 0);
    if (efd == -1) {
        perror("eventfd 创建失败");
        exit(EXIT_FAILURE);
    }
    printf("创建eventfd: %d\n", efd);
    
    // 启动线程发送事件
    if (pthread_create(&thread, NULL, thread_function, &efd) != 0) {
        perror("创建线程失败");
        close(efd);
        exit(EXIT_FAILURE);
    }
    
    printf("主线程: 等待事件通知...\n");
    
    // 等待事件通知
    ssize_t bytes_read = read(efd, &value, sizeof(value));
    if (bytes_read == sizeof(value)) {
        printf("主线程: 收到事件通知,计数器值: %lu\n", value);
    } else {
        perror("主线程: 读取eventfd失败");
    }
    
    // 等待线程结束
    pthread_join(thread, NULL);
    
    close(efd);
    
    // 示例3: 使用标志位
    printf("\n示例3: 使用标志位\n");
    
    // 使用EFD_CLOEXEC标志
    efd = eventfd_wrapper(0, EFD_CLOEXEC);
    if (efd != -1) {
        printf("创建带EFD_CLOEXEC标志的eventfd: %d\n", efd);
        
        // 验证标志是否设置
        flags = fcntl(efd, F_GETFD);
        if (flags != -1 && (flags & FD_CLOEXEC)) {
            printf("EFD_CLOEXEC标志已正确设置\n");
        }
        close(efd);
    }
    
    // 使用EFD_NONBLOCK标志
    efd = eventfd_wrapper(0, EFD_NONBLOCK);
    if (efd != -1) {
        printf("创建带EFD_NONBLOCK标志的eventfd: %d\n", efd);
        
        // 尝试非阻塞读取(应该失败)
        bytes_read = read(efd, &value, sizeof(value));
        if (bytes_read == -1) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                printf("非阻塞读取正确返回EAGAIN: %s\n", strerror(errno));
            }
        }
        close(efd);
    }
    
    // 示例4: 信号量模式
    printf("\n示例4: 信号量模式\n");
    
    efd = eventfd_wrapper(3, EFD_SEMAPHORE);
    if (efd != -1) {
        printf("创建信号量模式eventfd,初始值: 3\n");
        
        // 多次读取,每次递减1
        for (int i = 0; i < 5; i++) {
            bytes_read = read(efd, &value, sizeof(value));
            if (bytes_read == sizeof(value)) {
                printf("第%d次读取,获得值: %lu\n", i+1, value);
            } else {
                if (errno == EAGAIN) {
                    printf("第%d次读取,无可用事件: %s\n", i+1, strerror(errno));
                    break;
                }
            }
        }
        
        close(efd);
    }
    
    // 示例5: 错误处理演示
    printf("\n示例5: 错误处理演示\n");
    
    // 使用无效的初始值(虽然eventfd允许大值,但有上限)
    efd = eventfd_wrapper(0xffffffff, 0);
    if (efd != -1) {
        printf("使用大初始值创建eventfd成功: %d\n", efd);
        close(efd);
    }
    
    // 尝试写入无效值
    efd = eventfd_wrapper(0, 0);
    if (efd != -1) {
        uint64_t invalid_value = 0xffffffffffffffffULL; // 最大值
        ssize_t result = write(efd, &invalid_value, sizeof(invalid_value));
        if (result == -1) {
            printf("写入最大值失败: %s\n", strerror(errno));
        } else {
            printf("写入最大值成功\n");
        }
        close(efd);
    }
    
    // 示例6: 计数器溢出处理
    printf("\n示例6: 计数器溢出处理\n");
    
    efd = eventfd_wrapper(0, 0);
    if (efd != -1) {
        // 写入接近最大值的数据
        uint64_t large_value = 0xfffffffffffffffeULL; // 接近最大值
        if (write(efd, &large_value, sizeof(large_value)) == sizeof(large_value)) {
            printf("写入大值成功\n");
            
            // 再次写入会导致溢出
            uint64_t add_value = 2;
            ssize_t result = write(efd, &add_value, sizeof(add_value));
            if (result == -1) {
                if (errno == EAGAIN) {
                    printf("计数器溢出,写入失败: %s\n", strerror(errno));
                }
            }
        }
        close(efd);
    }
    
    // 示例7: 实际应用场景
    printf("\n示例7: 实际应用场景\n");
    
    printf("eventfd的典型应用场景:\n");
    printf("1. 线程池任务通知\n");
    printf("2. 异步I/O完成通知\n");
    printf("3. 事件驱动编程\n");
    printf("4. 进程间简单通信\n");
    printf("5. 与epoll配合使用\n\n");
    
    // 演示与epoll配合使用
    printf("与epoll配合使用的示例:\n");
    printf("int epfd = epoll_create1(EPOLL_CLOEXEC);\n");
    printf("int efd = eventfd(0, EFD_CLOEXEC);\n");
    printf("struct epoll_event ev;\n");
    printf("ev.events = EPOLLIN;\n");
    printf("ev.data.fd = efd;\n");
    printf("epoll_ctl(epfd, EPOLL_CTL_ADD, efd, &ev);\n");
    printf("// 在其他线程中: write(efd, &value, sizeof(value));\n");
    printf("// 在事件循环中: epoll_wait(epfd, events, maxevents, timeout);\n\n");
    
    // 示例8: 性能优势
    printf("示例8: 性能优势\n");
    printf("eventfd相比传统机制的优势:\n");
    printf("1. 更少的系统调用\n");
    printf("2. 更小的内存占用\n");
    printf("3. 更快的通知速度\n");
    printf("4. 更简单的API\n");
    printf("5. 更好的可扩展性\n\n");
    
    printf("与pipe的对比:\n");
    printf("pipe: 需要两个文件描述符,缓冲区较大\n");
    printf("eventfd: 只需要一个文件描述符,固定8字节计数器\n\n");
    
    printf("总结:\n");
    printf("eventfd是Linux提供的轻量级事件通知机制\n");
    printf("适用于简单的同步和通知场景\n");
    printf("支持多种模式和标志位\n");
    printf("与epoll等机制配合使用效果更佳\n");
    printf("是现代Linux编程的重要工具\n");
    
    return 0;
}

49. eventfd2 – 创建事件文件描述符(扩展版)

函数介绍

eventfd2eventfd的扩展版本,提供了更好的标志位支持和错误处理。它是现代Linux系统推荐使用的eventfd创建函数。

函数原型

#include <sys/eventfd.h>

int eventfd2(unsigned int initval, int flags);

功能

创建一个事件文件描述符,功能与eventfd相同但接口更现代。

参数

  • unsigned int initval: 计数器的初始值
  • int flags: 控制标志(支持更多标志位)

返回值

  • 成功时返回事件文件描述符
  • 失败时返回-1,并设置errno

特殊限制

  • 需要Linux 2.6.27以上内核支持
  • 某些旧系统可能不支持

相似函数

  • eventfd(): 基础版本
  • pipe(): 管道机制

示例代码

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/eventfd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <pthread.h>

int main() {
    int efd1, efd2;
    
    printf("=== Eventfd2 函数示例 ===\n");
    
    // 示例1: 基本使用对比
    printf("\n示例1: 基本使用对比\n");
    
    // 使用eventfd
    efd1 = eventfd(0, 0);
    if (efd1 != -1) {
        printf("eventfd创建成功: %d\n", efd1);
        close(efd1);
    } else {
        printf("eventfd创建失败: %s\n", strerror(errno));
    }
    
    // 使用eventfd2
    efd2 = eventfd2(0, 0);
    if (efd2 != -1) {
        printf("eventfd2创建成功: %d\n", efd2);
        close(efd2);
    } else {
        printf("eventfd2创建失败: %s\n", strerror(errno));
        printf("说明: 系统可能不支持eventfd2\n");
    }
    
    // 示例2: 标志位支持
    printf("\n示例2: 标志位支持\n");
    
    // EFD_CLOEXEC标志
    efd2 = eventfd2(0, EFD_CLOEXEC);
    if (efd2 != -1) {
        printf("使用EFD_CLOEXEC标志创建成功: %d\n", efd2);
        
        // 验证标志设置
        int flags = fcntl(efd2, F_GETFD);
        if (flags != -1 && (flags & FD_CLOEXEC)) {
            printf("EFD_CLOEXEC标志验证成功\n");
        }
        close(efd2);
    }
    
    // EFD_NONBLOCK标志
    efd2 = eventfd2(0, EFD_NONBLOCK);
    if (efd2 != -1) {
        printf("使用EFD_NONBLOCK标志创建成功: %d\n", efd2);
        
        // 测试非阻塞特性
        uint64_t value;
        ssize_t result = read(efd2, &value, sizeof(value));
        if (result == -1) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                printf("非阻塞读取正确返回EAGAIN\n");
            }
        }
        close(efd2);
    }
    
    // EFD_SEMAPHORE标志
    efd2 = eventfd2(5, EFD_SEMAPHORE);
    if (efd2 != -1) {
        printf("使用EFD_SEMAPHORE标志创建成功,初始值: 5\n");
        
        // 测试信号量模式
        for (int i = 0; i < 7; i++) {
            uint64_t read_value;
            ssize_t result = read(efd2, &read_value, sizeof(read_value));
            if (result == sizeof(read_value)) {
                printf("第%d次读取成功,值: %lu\n", i+1, read_value);
            } else {
                if (errno == EAGAIN) {
                    printf("第%d次读取失败,无可用资源: %s\n", i+1, strerror(errno));
                    break;
                }
            }
        }
        close(efd2);
    }
    
    // 示例3: 组合标志
    printf("\n示例3: 组合标志\n");
    
    // 组合多个标志
    efd2 = eventfd2(0, EFD_CLOEXEC | EFD_NONBLOCK);
    if (efd2 != -1) {
        printf("组合标志创建成功: %d\n", efd2);
        
        // 验证所有标志
        int flags = fcntl(efd2, F_GETFD);
        if (flags != -1) {
            if (flags & FD_CLOEXEC) {
                printf("EFD_CLOEXEC标志已设置\n");
            }
        }
        
        // 测试非阻塞特性
        uint64_t value;
        if (read(efd2, &value, sizeof(value)) == -1) {
            if (errno == EAGAIN) {
                printf("EFD_NONBLOCK标志生效\n");
            }
        }
        
        close(efd2);
    }
    
    // 示例4: 错误处理
    printf("\n示例4: 错误处理\n");
    
    // 使用无效标志
    efd2 = eventfd2(0, 0x1000); // 无效标志
    if (efd2 == -1) {
        if (errno == EINVAL) {
            printf("无效标志错误处理正确: %s\n", strerror(errno));
        }
    }
    
    // 在不支持的系统上
    printf("在不支持eventfd2的系统上会返回ENOSYS错误\n");
    
    // 示例5: 与eventfd的差异
    printf("\n示例5: 与eventfd的差异\n");
    
    printf("eventfd vs eventfd2:\n");
    printf("eventfd:\n");
    printf("  - 较老的接口\n");
    printf("  - 标志位支持有限\n");
    printf("  - 在所有支持eventfd的系统上可用\n\n");
    
    printf("eventfd2:\n");
    printf("  - 现代接口\n");
    printf("  - 更好的标志位支持\n");
    printf("  - 原子性设置标志\n");
    printf("  - 需要Linux 2.6.27+\n\n");
    
    // 示例6: 实际应用推荐
    printf("示例6: 实际应用推荐\n");
    
    printf("现代应用推荐使用模式:\n");
    printf("#ifdef EFD_CLOEXEC\n");
    printf("    int efd = eventfd2(0, EFD_CLOEXEC | EFD_NONBLOCK);\n");
    printf("#else\n");
    printf("    int efd = eventfd(0, 0);\n");
    printf("    fcntl(efd, F_SETFD, FD_CLOEXEC);\n");
    printf("    fcntl(efd, F_SETFL, fcntl(efd, F_GETFL) | O_NONBLOCK);\n");
    printf("#endif\n\n");
    
    // 示例7: 兼容性处理
    printf("示例7: 兼容性处理\n");
    
    // 兼容性包装函数
    printf("兼容性处理示例:\n");
    printf("int create_eventfd(unsigned int initval, int flags) {\n");
    printf("#ifdef __NR_eventfd2\n");
    printf("    int fd = eventfd2(initval, flags);\n");
    printf("    if (fd >= 0 || errno != ENOSYS)\n");
    printf("        return fd;\n");
    printf("#endif\n");
    printf("    // 回退到eventfd\n");
    printf("    fd = eventfd(initval, 0);\n");
    printf("    if (fd >= 0) {\n");
    printf("        // 手动设置标志\n");
    printf("        if (flags & EFD_CLOEXEC)\n");
    printf("            fcntl(fd, F_SETFD, FD_CLOEXEC);\n");
    printf("        if (flags & EFD_NONBLOCK)\n");
    printf("            fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);\n");
    printf("    }\n");
    printf("    return fd;\n");
    printf("}\n\n");
    
    // 示例8: 性能考虑
    printf("示例8: 性能考虑\n");
    printf("eventfd2性能优势:\n");
    printf("1. 原子性标志设置,避免竞态条件\n");
    printf("2. 减少系统调用次数\n");
    printf("3. 更好的错误处理\n");
    printf("4. 现代内核优化\n\n");
    
    printf("使用建议:\n");
    printf("1. 优先使用eventfd2\n");
    printf("2. 提供eventfd回退方案\n");
    printf("3. 合理使用标志位\n");
    printf("4. 注意资源清理\n\n");
    
    printf("总结:\n");
    printf("eventfd2是eventfd的现代替代品\n");
    printf("提供了更好的标志位支持\n");
    printf("在支持的系统上应优先使用\n");
    printf("需要考虑向后兼容性\n");
    printf("是构建高性能应用的重要工具\n");
    
    return 0;
}
发表在 linux文章 | 留下评论

execveat系统调用及示例

这次我们介绍 execveat


1. 函数介绍

execveat 是一个 Linux 系统调用(内核版本 >= 3.19),它是 execve 函数族的一员。它的核心功能是在当前进程中执行一个新的程序,从而替换当前进程的镜像(代码、数据、堆栈等)。

简单来说,execveat 就像用一个新灵魂替换旧灵魂

  • 你的身体(进程)还在,但里面的思想、记忆、行为(程序代码和数据)被完全替换成另一个人(新程序)的。
  • 旧程序的所有状态(局部变量、堆栈)都消失了。
  • 新程序从它的 main 函数开始执行。

execveat 相比于 execve 的独特之处在于它引入了目录文件描述符dirfd)和路径解析标志flags),使得程序执行可以相对于一个已打开的目录进行,或者直接执行一个已打开的文件描述符所指向的文件。这提供了更灵活和安全的程序执行方式,尤其是在处理复杂路径或受限环境(如容器)时。


2. 函数原型

#include <unistd.h> // 必需

int execveat(int dirfd, const char *pathname,
             char *const argv[], char *const envp[], int flags);

3. 功能

  • 执行新程序: 终止调用进程的当前程序,并使用由 pathname(结合 dirfd 和 flags)指定的可执行文件来替换当前进程的内存镜像。
  • 传递参数和环境: 将新的命令行参数 (argv) 和环境变量 (envp) 传递给新程序。
  • 灵活的路径解析: 通过 dirfd 和 flags 参数,提供了比 execve 更灵活的路径解析方式。

4. 参数

  • int dirfd: 一个目录文件描述符,用作解析 pathname 的起始点
    • 如果 pathname 是相对路径(例如 "subdir/myprogram"),则相对于 dirfd 指向的目录进行查找。
    • 如果 pathname 是绝对路径(例如 "/usr/bin/ls"),则 dirfd 被忽略。
    • 可以传入特殊的值 AT_FDCWD,表示使用当前工作目录作为起始点(此时行为类似于 execve)。
  • const char *pathname: 指向要执行的可执行文件的路径名。这个路径名会根据 dirfd 和 flags 进行解析。
  • char *const argv[]: 一个字符串数组(向量),用于传递给新程序的命令行参数
    • 数组的每个元素都是一个指向以空字符 (\0) 结尾的字符串的指针。
    • 数组必须以 NULL 指针结束
    • argv[0] 通常是程序的名字(惯例,但不是强制)。
    • 例如:char *argv[] = {"myprogram", "--verbose", "input.txt", NULL};
  • char *const envp[]: 一个字符串数组(向量),用于设置新程序的环境变量
    • 数组的每个元素都是一个形如 "NAME=VALUE" 的字符串。
    • 数组必须以 NULL 指针结束
    • 例如:char *envp[] = {"PATH=/usr/bin:/bin", "HOME=/home/user", NULL};
    • 可以使用全局变量 environ 来传递当前进程的环境。
  • int flags: 控制路径解析行为的标志位。可以是以下值的按位或组合:
    • 0: 默认行为。pathname 被当作普通路径名处理,相对于 dirfd 解析。
    • AT_EMPTY_PATH: 如果 pathname 是一个空字符串 (""),则 execveat 会尝试执行 dirfd 本身所引用的文件。dirfd 必须是一个有效的、指向可执行文件的文件描述符
    • AT_SYMLINK_NOFOLLOW: 如果 pathname 解析过程中遇到符号链接(symbolic link),则不跟随该链接,而是尝试执行符号链接文件本身。注意:并非所有文件系统都支持执行符号链接文件本身,通常会失败。

5. 返回值

  • 成功时永不返回。因为 execveat 成功执行后,当前进程的代码和数据已经被新程序完全替换,从新程序的 main 函数开始执行。
  • 失败时: 返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 EACCES 权限不足,ENOENT 文件未找到,EINVAL 参数无效,ENOMEM 内存不足,ELIBBAD 动态链接库损坏等)。

重要只有在 execveat 调用失败时,它才会返回 -1。如果成功,控制流就不会回到调用 execveat 的代码处。


6. 相似函数,或关联函数

  • execve: 最基础的 exec 函数,功能与 execveat 相同,但不支持 dirfd 和 flags 参数。
  • execlexeclpexecleexecvexecvpexecve 的各种包装函数,提供了不同的参数传递方式(如使用可变参数列表 ... 或自动搜索 PATH 环境变量)。
  • fork: 通常与 execveat/execve 配合使用。fork 创建子进程,然后在子进程中调用 execveat/execve 来执行新程序。
  • system: 一个更高级的库函数,内部通过 fork + execve (/bin/sh -c command) 来执行 shell 命令。
  • posix_spawn: POSIX 标准定义的、更现代和可移植的创建和执行子进程的方式,功能强大且避免了 fork 的开销(在某些系统上)。

7. 示例代码

示例 1:基本的 execveat 使用

这个例子演示了如何使用 execveat 来执行一个简单的程序(/bin/echo)。

// execveat_basic.c
#define _GNU_SOURCE // For AT_FDCWD
#include <unistd.h>   // execveat
#include <fcntl.h>    // open, O_RDONLY, AT_FDCWD
#include <stdio.h>    // perror, printf
#include <stdlib.h>   // exit
#include <errno.h>    // errno

int main() {
    // 1. 准备 execveat 的参数
    const char *pathname = "/bin/echo";
    char *const argv[] = {"echo", "Hello,", "from", "execveat!", NULL};
    // 使用当前进程的环境变量
    extern char **environ;
    char *const envp[] = {NULL}; // 或者直接传递 environ

    printf("About to execute '/bin/echo' using execveat...\n");

    // 2. 调用 execveat
    // dirfd = AT_FDCWD: 使用当前工作目录解析绝对路径
    // pathname = "/bin/echo": 要执行的程序
    // argv: 传递给新程序的参数
    // environ: 传递给新程序的环境变量
    // flags = 0: 默认行为
    if (execveat(AT_FDCWD, pathname, argv, environ, 0) == -1) {
        // 3. 如果 execveat 返回,说明执行失败
        perror("execveat failed");
        exit(EXIT_FAILURE);
    }

    // 4. 这行代码永远不会被执行,因为 execveat 成功后不会返回
    printf("This line will never be printed if execveat succeeds.\n");

    return 0; // 这行也不会被执行
}

代码解释:

  1. 定义要执行的程序路径 pathname ("/bin/echo")。
  2. 准备传递给新程序的命令行参数 argv 数组。argv[0] 通常是程序名 "echo",后面跟着要传递的参数 "Hello,""from""execveat!"非常重要:数组必须以 NULL 结尾。
  3. 准备环境变量 envp。这里为了简化,传递一个只包含 NULL 的数组,表示不传递任何环境变量。实际应用中通常传递 extern char **environ 来继承当前进程的环境。
  4. 调用 execveat(AT_FDCWD, pathname, argv, environ, 0)
    • AT_FDCWD: 使用当前工作目录作为路径解析起点。
    • pathname: 要执行的程序的绝对路径。
    • argv: 参数向量。
    • environ: 环境变量向量。
    • 0: 默认标志。
  5. 关键: 检查 execveat 的返回值。如果它返回 -1,说明执行失败,打印错误信息并退出。
  6. 如果 execveat 成功,当前进程的代码被 /bin/echo 程序替换,从 echo 的 main 函数开始执行,打印 "Hello, from execveat!",然后 echo 程序退出,整个进程也随之结束。
  7. main 函数中 execveat 之后的代码(包括 return 0;永远不会被执行

示例 2:使用 execveat 和 AT_EMPTY_PATH 执行已打开的文件

这个例子演示了如何使用 AT_EMPTY_PATH 标志来执行一个已经通过文件描述符打开的可执行文件。

// execveat_empty_path.c
#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main() {
    const char *binary_path = "/bin/ls"; // 我们将执行 ls 命令
    int binary_fd;
    char *const argv[] = {"ls", "-l", "/tmp", NULL}; // ls -l /tmp
    extern char **environ;

    printf("Opening executable file '%s'...\n", binary_path);

    // 1. 打开可执行文件,获取文件描述符
    binary_fd = open(binary_path, O_RDONLY);
    if (binary_fd == -1) {
        perror("open executable file");
        exit(EXIT_FAILURE);
    }
    printf("Executable file opened successfully. File descriptor: %d\n", binary_fd);

    // 2. 准备 execveat 的参数
    // 注意:pathname 是空字符串 ""
    const char *pathname = "";

    printf("About to execute using execveat with AT_EMPTY_PATH...\n");
    printf("Command will be: ls -l /tmp\n");

    // 3. 调用 execveat
    // dirfd = binary_fd: 指向已打开的可执行文件的文件描述符
    // pathname = "": 空字符串
    // argv: 传递给 ls 的参数
    // environ: 环境变量
    // flags = AT_EMPTY_PATH: 告诉 execveat 执行 dirfd 指向的文件
    if (execveat(binary_fd, pathname, argv, environ, AT_EMPTY_PATH) == -1) {
        // 4. 如果 execveat 返回,说明执行失败
        perror("execveat with AT_EMPTY_PATH failed");
        // 可能的原因:内核版本 < 3.19, binary_fd 无效, 文件不可执行等
        close(binary_fd); // 关闭文件描述符
        exit(EXIT_FAILURE);
    }

    // 5. 这行代码永远不会被执行
    printf("This line will never be printed if execveat succeeds.\n");

    // close(binary_fd); // 这行也不会被执行,因为 execveat 成功后进程已替换
    return 0;
}

代码解释:

  1. 定义要执行的二进制文件路径 binary_path ("/bin/ls")。
  2. 使用 open(binary_path, O_RDONLY) 以只读方式打开该可执行文件,并获得文件描述符 binary_fd
  3. 准备 execveat 的参数:
    • pathname 被设置为空字符串 ""
    • argv 设置为执行 ls -l /tmp 所需的参数。
    • envp 传递 environ
  4. 关键步骤: 调用 execveat(binary_fd, "", argv, environ, AT_EMPTY_PATH)
    • binary_fd: 之前打开的可执行文件的文件描述符。
    • "": 空字符串 pathname
    • AT_EMPTY_PATH: 这个标志告诉 execveat:忽略 pathname,直接执行 dirfd(即 binary_fd)所指向的文件。
  5. 检查返回值。如果成功,当前进程被 /bin/ls 程序替换,并执行 ls -l /tmp 命令。
  6. 如果失败,打印错误信息,关闭 binary_fd 并退出。

示例 3:execveat 与 fork 结合使用

这个例子演示了 execveat 如何与 fork 结合,创建一个子进程并在其中执行新程序。

// execveat_with_fork.c
#define _GNU_SOURCE
#include <unistd.h>
#include <sys/wait.h> // waitpid
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main() {
    pid_t pid;
    int status;
    const char *pathname = "/bin/date"; // 执行 date 命令
    char *const argv[] = {"date", "+%Y-%m-%d %H:%M:%S", NULL};
    extern char **environ;

    printf("Parent process (PID: %d) is about to fork.\n", getpid());

    // 1. 创建子进程
    pid = fork();
    if (pid == -1) {
        // fork 失败
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        // --- 子进程 ---
        printf("Child process (PID: %d) created.\n", getpid());

        // 2. 在子进程中调用 execveat 执行新程序
        printf("Child process executing '/bin/date'...\n");

        if (execveat(AT_FDCWD, pathname, argv, environ, 0) == -1) {
            // 3. execveat 失败
            perror("execveat in child failed");
            _exit(EXIT_FAILURE); // 子进程使用 _exit 退出
        }

        // 4. 这行代码在子进程中永远不会被执行
        printf("This line in child will never be printed.\n");

    } else {
        // --- 父进程 ---
        printf("Parent process (PID: %d) forked child (PID: %d).\n", getpid(), pid);

        // 5. 父进程等待子进程结束
        printf("Parent process waiting for child to finish...\n");
        if (waitpid(pid, &status, 0) == -1) {
            perror("waitpid");
            exit(EXIT_FAILURE);
        }

        // 6. 检查子进程退出状态
        if (WIFEXITED(status)) {
            printf("Child process (PID: %d) exited normally with status %d.\n",
                   pid, WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("Child process (PID: %d) was killed by signal %d.\n",
                   pid, WTERMSIG(status));
        } else {
            printf("Child process (PID: %d) exited abnormally.\n", pid);
        }

        printf("Parent process (PID: %d) finished.\n", getpid());
    }

    return 0;
}

代码解释:

  1. 定义要执行的程序 "/bin/date" 及其参数。
  2. 调用 fork() 创建子进程。
  3. fork 返回后
    • 子进程 (pid == 0):
      • 调用 execveat(AT_FDCWD, pathname, argv, environ, 0) 执行 /bin/date 命令。
      • 如果 execveat 成功,子进程的代码被替换,date 命令开始执行并打印当前日期时间。
      • date 命令执行完毕后,子进程退出。
      • 如果 execveat 失败,打印错误信息并使用 _exit(EXIT_FAILURE) 退出(在子进程中推荐使用 _exit 而非 exit)。
    • 父进程 (pid > 0):
      • 打印子进程 PID。
      • 调用 waitpid(pid, &status, 0) 等待子进程结束。
      • waitpid 返回后,检查子进程的退出状态 status
      • 使用 WIFEXITEDWEXITSTATUSWIFSIGNALEDWTERMSIG 等宏来判断子进程是正常退出还是被信号终止,并获取退出码或信号号。
      • 父进程打印相关信息并退出。

重要提示与注意事项:

  1. 永不返回execveat 成功时永远不会返回到调用者。这是 exec 系列函数的根本特性。
  2. 失败处理必须检查 execveat 的返回值。如果返回 -1,表示执行失败,需要进行错误处理(如打印错误信息、退出等)。
  3. argv 和 envp 必须以 NULL 结尾: 这是 execve 系列函数的严格要求。
  4. AT_EMPTY_PATH: 这是 execveat 相比 execve 的关键新增功能,允许通过文件描述符直接执行文件,这在容器技术和安全沙箱中非常有用。
  5. AT_SYMLINK_NOFOLLOW: 尝试执行符号链接本身,但支持有限。
  6. dirfd 的使用: 提供了更灵活的路径解析能力,尤其是在受限或复杂目录结构中。
  7. 与 fork 结合: 这是创建新进程并执行新程序的经典模式(fork-and-exec 模式)。
  8. 内核版本: 需要 Linux 内核 3.19 或更高版本。
  9. 权限: 执行文件需要有可执行权限 (x)。
  10. environ: 使用 extern char **environ; 可以方便地传递当前进程的环境变量给新程序。

总结:

execveat 是 execve 的强大扩展,通过引入 dirfd 和 flags 参数,提供了更灵活、更安全的程序执行方式。它允许程序基于已打开的目录或文件描述符来定位和执行可执行文件,这在现代 Linux 系统编程,特别是容器和沙箱技术中具有重要意义。理解其参数和与 fork 的结合使用是掌握 Linux 进程控制的基础。

发表在 linux文章 | 留下评论