11. fcntl - 文件控制 Link to heading

函数介绍 Link to heading

fcntl系统调用用于对已打开的文件描述符执行各种控制操作。它是文件I/O操作的重要补充,可以用来复制文件描述符、设置文件状态标志、管理文件锁等。

函数原型 Link to heading

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

int fcntl(int fd, int cmd, ... /* arg */ );

功能 Link to heading

对文件描述符执行各种控制操作,包括复制、设置标志、管理锁等。

参数 Link to heading

  • int fd: 文件描述符
  • int cmd: 控制命令
    • F_DUPFD: 复制文件描述符(返回大于等于arg的最小可用fd)
    • F_DUPFD_CLOEXEC: 复制文件描述符并设置FD_CLOEXEC标志
    • F_GETFD: 获取文件描述符标志
    • F_SETFD: 设置文件描述符标志
    • F_GETFL: 获取文件状态标志
    • F_SETFL: 设置文件状态标志
    • F_GETLK: 获取文件锁信息
    • F_SETLK: 设置文件锁(非阻塞)
    • F_SETLKW: 设置文件锁(阻塞)
  • ...: 可选参数,根据命令不同而不同

返回值 Link to heading

  • 根据命令不同返回不同值:
    • F_DUPFD, F_DUPFD_CLOEXEC: 成功返回新的文件描述符,失败返回-1
    • F_GETFD, F_GETFL: 成功返回标志值,失败返回-1
    • 其他命令:成功返回0,失败返回-1
  • 失败时设置errno

相似函数 Link to heading

  • dup(), dup2(): 复制文件描述符
  • open(): 打开文件
  • flock(): 文件锁操作
  • lockf(): 另一种文件锁接口

示例代码 Link to heading

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

int main() {
    int fd, new_fd;
    int flags;
    
    printf("=== Fcntl函数示例 ===\n");
    
    // 创建测试文件
    fd = open("test_fcntl.txt", O_CREAT | O_RDWR | O_TRUNC, 0644);
    if (fd == -1) {
        perror("创建测试文件失败");
        exit(EXIT_FAILURE);
    }
    printf("成功创建测试文件,文件描述符: %d\n", fd);
    
    // 写入测试数据
    const char *test_data = "Hello, fcntl test data!";
    if (write(fd, test_data, strlen(test_data)) == -1) {
        perror("写入测试数据失败");
        close(fd);
        exit(EXIT_FAILURE);
    }
    
    // 示例1: 复制文件描述符(F_DUPFD)
    printf("\n示例1: 复制文件描述符\n");
    
    // 复制文件描述符,要求新fd >= 10
    new_fd = fcntl(fd, F_DUPFD, 10);
    if (new_fd == -1) {
        perror("复制文件描述符失败");
    } else {
        printf("  成功复制文件描述符: %d -> %d\n", fd, new_fd);
        
        // 验证两个fd指向同一文件
        char buffer[100];
        lseek(new_fd, 0, SEEK_SET);  // 移动new_fd的位置
        ssize_t bytes_read = read(fd, buffer, sizeof(buffer) - 1);
        if (bytes_read > 0) {
            buffer[bytes_read] = '\0';
            printf("  从原fd读取数据: %s\n", buffer);
        }
        
        close(new_fd);
    }
    
    // 示例2: 获取和设置文件描述符标志(F_GETFD, F_SETFD)
    printf("\n示例2: 文件描述符标志操作\n");
    
    // 获取当前文件描述符标志
    flags = fcntl(fd, F_GETFD);
    if (flags == -1) {
        perror("获取文件描述符标志失败");
    } else {
        printf("  当前文件描述符标志: 0x%x\n", flags);
        if (flags & FD_CLOEXEC) {
            printf("    包含FD_CLOEXEC标志(exec时关闭)\n");
        } else {
            printf("    不包含FD_CLOEXEC标志\n");
        }
    }
    
    // 设置FD_CLOEXEC标志
    if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1) {
        perror("设置FD_CLOEXEC标志失败");
    } else {
        printf("  成功设置FD_CLOEXEC标志\n");
        
        // 验证设置结果
        flags = fcntl(fd, F_GETFD);
        if (flags != -1 && (flags & FD_CLOEXEC)) {
            printf("  验证: FD_CLOEXEC标志已设置\n");
        }
    }
    
    // 示例3: 获取和设置文件状态标志(F_GETFL, F_SETFL)
    printf("\n示例3: 文件状态标志操作\n");
    
    // 获取当前文件状态标志
    flags = fcntl(fd, F_GETFL);
    if (flags == -1) {
        perror("获取文件状态标志失败");
    } else {
        printf("  当前文件状态标志: 0x%x\n", flags);
        
        // 分析标志位
        if (flags & O_RDONLY) printf("    只读模式\n");
        if (flags & O_WRONLY) printf("    只写模式\n");
        if (flags & O_RDWR) printf("    读写模式\n");
        if (flags & O_APPEND) printf("    追加模式\n");
        if (flags & O_NONBLOCK) printf("    非阻塞模式\n");
    }
    
    // 设置追加模式
    if (fcntl(fd, F_SETFL, flags | O_APPEND) == -1) {
        perror("设置追加模式失败");
    } else {
        printf("  成功设置追加模式\n");
        
        // 验证设置结果
        flags = fcntl(fd, F_GETFL);
        if (flags != -1 && (flags & O_APPEND)) {
            printf("  验证: 追加模式已设置\n");
        }
    }
    
    // 示例4: 非阻塞模式演示
    printf("\n示例4: 非阻塞模式演示\n");
    
    // 创建管道用于演示
    int pipefd[2];
    if (pipe(pipefd) == -1) {
        perror("创建管道失败");
    } else {
        printf("  创建管道成功: 读端=%d, 写端=%d\n", pipefd[0], pipefd[1]);
        
        // 设置读端为非阻塞模式
        flags = fcntl(pipefd[0], F_GETFL);
        if (fcntl(pipefd[0], F_SETFL, flags | O_NONBLOCK) == -1) {
            perror("  设置非阻塞模式失败");
        } else {
            printf("  成功设置读端为非阻塞模式\n");
            
            // 尝试从空管道读取(应该立即返回错误)
            char pipe_buffer[10];
            ssize_t bytes_read = read(pipefd[0], pipe_buffer, sizeof(pipe_buffer));
            if (bytes_read == -1) {
                if (errno == EAGAIN || errno == EWOULDBLOCK) {
                    printf("  非阻塞读取: 管道为空,立即返回EAGAIN\n");
                } else {
                    perror("  非阻塞读取失败");
                }
            }
        }
        
        close(pipefd[0]);
        close(pipefd[1]);
    }
    
    // 示例5: 文件描述符复制对比
    printf("\n示例5: 文件描述符复制对比\n");
    
    // 使用fcntl复制
    int fcntl_dup = fcntl(fd, F_DUPFD, 0);
    if (fcntl_dup != -1) {
        printf("  fcntl F_DUPFD复制: %d -> %d\n", fd, fcntl_dup);
        close(fcntl_dup);
    }
    
    // 使用fcntl复制并设置CLOEXEC
    int fcntl_dup_cloexec = fcntl(fd, F_DUPFD_CLOEXEC, 0);
    if (fcntl_dup_cloexec != -1) {
        printf("  fcntl F_DUPFD_CLOEXEC复制: %d -> %d\n", fd, fcntl_dup_cloexec);
        
        // 检查CLOEXEC标志
        int cloexec_flags = fcntl(fcntl_dup_cloexec, F_GETFD);
        if (cloexec_flags != -1 && (cloexec_flags & FD_CLOEXEC)) {
            printf("    验证: FD_CLOEXEC标志已设置\n");
        }
        close(fcntl_dup_cloexec);
    }
    
    // 使用dup复制
    int dup_fd = dup(fd);
    if (dup_fd != -1) {
        printf("  dup复制: %d -> %d\n", fd, dup_fd);
        close(dup_fd);
    }
    
    // 使用dup2复制到指定fd
    int target_fd = 100;
    int dup2_fd = dup2(fd, target_fd);
    if (dup2_fd != -1) {
        printf("  dup2复制到fd %d: %d -> %d\n", target_fd, fd, dup2_fd);
        close(dup2_fd);
    }
    
    // 示例6: 多进程文件描述符继承演示
    printf("\n示例6: 多进程文件描述符继承演示\n");
    
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork失败");
    } else if (pid == 0) {
        // 子进程
        printf("  子进程 %d 继承了文件描述符 %d\n", getpid(), fd);
        
        // 子进程可以使用继承的文件描述符
        char child_buffer[100];
        lseek(fd, 0, SEEK_SET);
        ssize_t bytes_read = read(fd, child_buffer, sizeof(child_buffer) - 1);
        if (bytes_read > 0) {
            child_buffer[bytes_read] = '\0';
            printf("  子进程读取数据: %s\n", child_buffer);
        }
        
        exit(EXIT_SUCCESS);
    } else {
        // 父进程
        printf("  父进程 %d 等待子进程结束\n", getpid());
        wait(NULL);
    }
    
    // 示例7: 错误处理演示
    printf("\n示例7: 错误处理演示\n");
    
    // 尝试对无效文件描述符操作
    if (fcntl(999, F_GETFD) == -1) {
        printf("  操作无效文件描述符: %s\n", strerror(errno));
    }
    
    // 尝试使用无效命令
    if (fcntl(fd, 999, 0) == -1) {
        printf("  使用无效命令: %s\n", strerror(errno));
    }
    
    // 清理资源
    printf("\n清理资源...\n");
    if (close(fd) == -1) {
        perror("关闭文件失败");
    } else {
        printf("成功关闭文件描述符 %d\n", fd);
    }
    
    // 删除测试文件
    if (unlink("test_fcntl.txt") == -1) {
        perror("删除测试文件失败");
    } else {
        printf("成功删除测试文件\n");
    }
    
    return 0;
}