【Linux 编程】:深入解析 fcntl 函数

一、序言
🔥fcntl 函数是一个在 UNIX 和类 UNIX 系统(如 Linux)上用来操作文件描述符的系统调用
作用:可以用于改变文件描述符的属性或状态,或者执行基本的控制操作
场景:fcntl 函数非常强大且灵活,常用于实现各种文件和进程间通信的功能
二、认识 fcntl 函数
1. 函数介绍
函数原型
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
// arg表示可变参数,由cmd决定
参数说明
fd:文件描述符,指定要操作的文件或套接字的描述符
cmd:控制命令,指示要执行的操作类型
fcntl()的第三个参数是可选,是否需要此参数由 cmd 决定
所需的参数类型在每个 cmd 名称后面的括号中指示(在大多数情况下,所需的类型是int,我们使用名称arg来标识参数),如果不需要参数,则指定 void
例如:对于 F_SETFL 命令,可以传递新的状态标志
返回值
成功时返回命令的结果,通常是状态标志或锁信息
失败时返回 -1,并设置 errno 以指示错误类型
常见的命令包括:
复制一个现有的描述符
F_DUPFD:复制文件描述符
F_DUPFD_CLOEXEC:复制文件描述符,设置FD_CLOEXEC标志
获得/设置文件状态标记
F_GETFL:获取文件描述符的当前状态标志
F_SETFL:设置文件描述符的状态标志
获得/设置文件描述符标记
F_GETFD:获取文件描述符的内部标志
F_SETFD:设置文件描述符的内部标志
获得/设置异步I/O所有权
F_GETOWN:获得异步I/O所有权
F_SETOWN:设置异步I/O所有权
获得/设置记录锁
F_GETLK:获取文件锁定信息
F_SETLK:设置文件锁定信息
F_SETLKW:以阻塞方式设置文件锁定信息。
注意:以下某些操作仅在特定的 Linux 内核版本之后才受支持。检查主机内核是否支持特定操作的首选方法是使用所需的 cmd 值调用 fcntl() ,然后使用EINVAL测试调用是否失败,这表明内核是否能够识别该值
2. 函数用途
1️⃣获取和设置文件状态标志
作用:设置文件为非阻塞模式
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);  // 将这个描述符设置为非阻塞模式
2️⃣文件锁定
作用:防止多个进程同时写入同一文件
struct flock lock;
lock.l_type = F_WRLCK;    // 请求写锁
lock.l_whence = SEEK_SET; // 从文件开始处锁定
lock.l_start = 0;        // 从文件开始位置
lock.l_len = 0;          // 锁定整个文件
fcntl(fd, F_SETLK, &lock); // 设置锁定
3️⃣更改文件描述符的属性
作用:动态更改文件描述符的功能,例如 将其设置为 异步I/O工作模式 或 实时信号
3. 错误处理
使用 fcntl 函数时,如果返回值为 -1,可以通过 errno 获取具体错误信息。常见的错误如下:
EBADF:提供的文件描述符无效
EINTR:操作被信号中断
EINVAL:指定的命令无效或者参数不符合规范
4. 示例代码
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
    int fd = open("example.txt", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
    if (fd == -1) {
        perror("open");
        return 1;
    }
    // 获取当前的文件状态标志
    int flags = fcntl(fd, F_GETFL);
    if (flags == -1) {
        perror("fcntl get");
        return 1;
    }
    // 设置为非阻塞模式
    if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
        perror("fcntl set");
        return 1;
    }
    // 关闭文件描述符
    close(fd);
    return 0;
}
三、复制文件描述符
1. F_DUPFD
int fcntl(int fd, F_DUPFD, int n);
/* 参数说明 */
  fd: 要复制的原始文件描述符。
  n: 分配的新文件描述符的起始值。如果可用,新的文件描述符将返回一个大于或等于 n 的最小的文件描述符。
功能:
创建一个新的文件描述符,该描述符是现有文件描述符的副本
新的文件描述符将从指定的最小文件描述符值开始分配,使用大于或等于arg参数的编号最低的可用文件描述符
新描述符与旧 fd 共享同一文件表项
但是:新描述符有它自己的一套文件描述符标志,其 FD_CLOEXEC 文件描述符标志被清除〈这表示该描述符在 exec 时仍保持打开状态)
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main()
{
int fd = open("./fcntl_F_DUPFD.txt", O_RDWR | O_CREAT | O_TRUNC, 0775);
int fcntlFd = fcntl(fd, F_DUPFD, 0); // 指定从 0 开始分配最小的可用描述符作为新描述符
int dupFd = dup(fd); // 等效于 fcntl(fd, F_DUPFD, 0);
close(fd);
close(fcntlFd);
close(dupFd);
return 0;
}
2. F_DUPFD_CLOEXEC
int fcntl(int fd, F_DUPFD_CLOEXEC, int n);
1
功能:
类似于 F_DUPFD,区别在于F_DUPFD_CLOEXEC在复制的同时会设置文件描述符标志 FD_CLOEXEC
这意味着如果当前进程调用 exec 系列函数时,将自动关闭这个文件描述符
3. 二者区别
dup 和 F_DUPFD :新描述符的 FD_CLOEXEC 默认为 0,无论原描述符是否设置了该标志(都不会保留 FD_CLOEXEC 标志)
F_DUPFD_CLOEXEC :新描述符的 FD_CLOEXEC 强制为 1,覆盖原描述符的设置
F_GETFD:仅读取文件描述符标志(如 FD_CLOEXEC),不会修改标志状态
int flags = fcntl(fd, F_GETFD); // 获取标志
if (flags & FD_CLOEXEC) { /* 检查 FD_CLOEXEC 是否设置 */ }
示例代码
样例一
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
    int fd = open("testfile", O_RDWR | O_CREAT, 0644);
    if (fd == -1) { perror("open"); return 1; }
    // 设置原描述符的 FD_CLOEXEC
    fcntl(fd, F_SETFD, FD_CLOEXEC);
    // 测试 dup
    int dup_fd = dup(fd);
    int dup_flags = fcntl(dup_fd, F_GETFD);
    printf("dup_fd FD_CLOEXEC: %d\n", (dup_flags & FD_CLOEXEC) ? 1 : 0); // 输出 0
    // 测试 F_DUPFD_CLOEXEC
    int cloexec_fd = fcntl(fd, F_DUPFD_CLOEXEC, 0);
    int cloexec_flags = fcntl(cloexec_fd, F_GETFD);
    printf("cloexec_fd FD_CLOEXEC: %d\n", (cloexec_flags & FD_CLOEXEC) ? 1 : 0); // 输出 1
    close(fd);
    close(dup_fd);
    close(cloexec_fd);
    return 0;
}
样例二
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main()
{
//fd 设置标记 fcntlFd 不设置 fcntlCloFd 设置 dupFd 不设置
int fd = open("./fcntl_F_DUPFD_CLOEXEC", O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC, 0775);
int fcntlFd = fcntl(fd, F_DUPFD, 0);
int fcntlCloFd = fcntl(fd, F_DUPFD_CLOEXEC, 0);
int dupFd = dup(fd);
//通过fcntl(fd, F_GETFD, 0);获取状态标记
int fdFlag = fcntl(fd, F_GETFD, 0);
int fcntlFdFlag = fcntl(fcntlFd, F_GETFD, 0);
int fcntlCloFdFlag = fcntl(fcntlCloFd, F_GETFD, 0);
int dupFdFlag = fcntl(dupFd, F_GETFD, 0);
// 结果是:fdFlag=1, fcntlFdFlag=0, fcntlCloFdFlag=1, dupFdFlag=0
printf("fdFlag=%d, fcntlFdFlag=%d, fcntlCloFdFlag=%d, dupFdFlag=%d\n",
fdFlag,fcntlFdFlag,fcntlCloFdFlag,dupFdFlag);
close(fd);
close(fcntlFd);
close(fcntlCloFd);
close(dupFd);
return 0;
}
4. 补充 – FD_CLOEXEC 意义
♐️ 文件描述符标志FD_CLOEXEC,用来表示该描述符在执行完 fork+exec 系列函数创建子进程时会自动关闭,以防止它们被传递给子进程。那么为什么要这样做呢?
原因:因为当一个进程调用 exec 系列函数(比如 execve )来创建子进程时,所有打开的文件描述符都会被传递给子进程
如果文件描述符没有设置 FD_CLOEXEC 标志,这些文件将保持打开状态并继续对子进程可见
这样就可能导致潜在的安全风险或者意外行为
四、获取/设置文件描述符标志
🏖 fcntl 函数中的 F_GETFD 和 F_SETFD 命令用于获取和设置文件描述符的标志,这在操作文件描述符的行为时非常重要
1. F_GETFD
int fcntl(int fd, F_GETFD);
// 参数说明:
 fd: 要查询状态的文件描述符
功能:获取指定文件描述符的标志
返回值:返回文件描述符的标志,如果失败则返回 -1,并设置 errno。主要的标志如下:
FD_CLOEXEC:文件描述符在 exec 系列调用时会被关闭。如果该标志被设置,返回值包含该标志。
如果返回值与 FD_CLOEXEC 持平,表示标志已设置;如果返回值包含 0,表示没有设置
2. F_SETFD
int fcntl(int fd, F_SETFD, int flags);
// 参数说明:
 fd: 要设置状态的文件描述符。
 flags: 新的状态标志,可以是以下值之一,或者是它们的组合:
 FD_CLOEXEC: 设置该标志,表示在调用 exec 系列函数时关闭文件描述符
功能:设置指定文件描述符的标志
上面我们讲了 FD_CLOEXEC,下面来演示一下如何获取
文件描述符的 FD_CLOEXEC 标志可以通过三个方法得到:
调用 open 函数是,指定 O_CLOEXEC
通过 fcntl 函数使用 F_DUPFD_CLOEXEC 复制文件描述符,新的描述符就是 FD_CLOEXEC
通过 fcntl 函数使用 F_SETFD 直接设置 FD_CLOEXEC
代码示例
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main()
{
int fd = open("./fcntl_F_GETFD", O_RDWR | O_CREAT | O_TRUNC, 0775);
int fdCloExec = open("./fcntl_F_GETFD2", O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC, 0775);
int fdCloExecDup = fcntl(fd, F_DUPFD_CLOEXEC, 0);
int fdSetFd = dup(fd);
fcntl(fdSetFd, F_SETFD, FD_CLOEXEC);
int flagFd = fcntl(fd, F_GETFD);
int flagFdCloExec = fcntl(fdCloExec, F_GETFD);
int flagFdCloExecDup = fcntl(fdCloExecDup, F_GETFD);
int flagFdSetFd = fcntl(fdSetFd, F_GETFD);
// 打印结果:flagFd=0, flagFdCloExec=1, flagFdCloExecDup=1 flagFdSetFd=1
printf("flagFd=%d, flagFdCloExec=%d, flagFdCloExecDup=%d flagFdSetFd=%d\n",
flagFd,flagFdCloExec,flagFdCloExecDup,flagFdSetFd);
close(fd);
close(fdCloExec);
close(fdCloExecDup);
close(fdSetFd);
return 0;
}
分析
flagFd=0 :fd 通过 open(...) 创建时未设置 O_CLOEXEC,标志未启用
flagFdCloExec=1 :fdCloExec 在 open 时指定了 O_CLOEXEC,标志已设置
flagFdCloExecDup=1 :F_DUPFD_CLOEXEC 自动设置 FD_CLOEXEC
flagFdSetFd=1 :dup 后通过 F_SETFD 显式设置标志
五、获取/设置文件状态标志
🛠 fcntl 函数中的 F_GETFL 和 F_SETFL 命令用于获取和设置文件的状态标志,这些标志控制文件的行为和特性。
1. 文件状态标志(File Status Flags)
作用 :控制文件的行为,如读写模式、追加、非阻塞、同步等。
常用标志 :
访问模式 :O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写)。
操作模式 :O_APPEND(追加)、O_NONBLOCK(非阻塞)、O_SYNC(同步写入)、O_ASYNC(异步 I/O)。
兼容性标志 :O_DIRECT(直接 I/O)、O_NOATIME(不更新访问时间)。
标志值与系统差异
访问模式 :
O_RDONLY:0(二进制 0b00)。
O_WRONLY:1(0b01)。
O_RDWR:2(0b10)。
这些值属于位掩码的一部分,需通过 O_ACCMODE 提取。
其他标志 :
O_APPEND:0x400(十六进制)。
O_NONBLOCK:0x800(Linux 中对应八进制 04000)。
O_ASYNC:0x2000(十六进制)
一些其他标志具体值因系统而异
2. fcntl 命令详解
1. F_GETFL: 获取文件状态标志
#include <fcntl.h>
int fcntl(int fd, F_GETFL);
返回值 :
成功:返回标志值(如 O_RDWR | O_APPEND)
失败:返回 -1,并设置 errno
访问方式标志:O_RDONLY 、O_WRONLY、O_RDWR。这3个值是互斥的,因此首先必须用屏蔽 O_ACCMODE 取得访问方式位,然后将结果与这3个值中的每一个相比较。
示例:访问模式处理
int flags = fcntl(fd, F_GETFL);
int access_mode = flags & O_ACCMODE; // 提取访问模式
if (access_mode == O_RDONLY) { /* 只读 */ }
2. F_SETFL: 设置文件状态标志
#include <fcntl.h>
int fcntl(int fd, F_SETFL, int flags);
返回值:
成功返回 0
失败返回 -1
注意:fcntl(fd, F_SETFL, flags) 并非所有标志都可通过此命令修改 。根据 Linux 手册页和 POSIX 标准,F_SETFL 可以设置的标志包括以下常见选项:
标志名 用途说明 应用场景
O_APPEND 强制每次写操作前将文件偏移量设置到文件末尾(追加模式)。 日志文件(确保多进程写入时自动追加到文件末尾)
O_NONBLOCK 设置非阻塞模式(对设备、管道、套接字等有意义) 网络编程( I/O 多路复用)
O_ASYNC 异步 I/O 通知(当数据可读/写时发送信号,需特定系统支持) 异步信号驱动 I/O(如通过SIGIO信号触发处理逻辑)
O_DIRECT 绕过内核缓冲区(直接 I/O,减少内存拷贝,需硬件/文件系统支持) 高性能数据库(绕过内核缓冲区,减少内存拷贝)
O_NOATIME 读取文件时不更新文件的访问时间(atime),用于优化性能 文件系统优化(避免频繁更新atime,减少磁盘 I/O)
注意 :
O_TRUNC (截断文件)和 O_CREAT (创建文件)等标志只能通过 open() 设置 ,不能通过 fcntl(F_SETFL) 修改
O_SYNC (同步写入,确保数据落盘)等标志在某些系统(如 Linux)中也属于 F_SETFL 的可修改范围,但需注意其行为可能与 O_DSYNC 等标志有差异
最常用标志:O_NONBLOCK
非阻塞 I/O :当读写套接字、管道或设备时,若没有数据可读或缓冲区满,操作会立即返回而非阻塞等待。
多路复用结合使用 :与 select()、poll()、epoll() 等配合,实现高性能 I/O 多路复用。
避免阻塞线程 :在事件驱动模型(如 Reactor 模式)中,非阻塞模式是实现高并发的基础
int flags = fcntl(sockfd, F_GETFL);  // 获取当前标志
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);  // 设置非阻塞模式
示例代码:设置非阻塞
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <cerrno>
#include <string>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/select.h>
const int MAX = 1024;
void SetNonBlock(int fd)
{
    int flags = fcntl(fd, F_GETFL, 0);
    if(flags < 0){
        std::cout << "Get flags error" << std::endl;
        return;
    }
    flags |= O_NONBLOCK; // O_NONBLOCK = 04000 :让 fd 以非阻塞的方式工作
    if(fcntl(fd, F_SETFL, flags) < 0){
        std::cout << "Set flags error" << std::endl;
    }
}
int main()
{
    std::string tips = "Please Enter# ";
    char buffer[MAX];
    SetNonBlock(0);
    while(true)
    {
        write(0, tips.c_str(), tips.size());
        // 非阻塞,如果我们不输入,数据就不会读取(就绪),所以会一直循环,以出错形式返回
        // read 不是有读取失败(-1),失败 vs 底层数据没就绪 -> 底层数据没就绪其实不算失败
        // 如果是 -1,失败 vs 底层数据没就绪 后续的做法不同的,需要区分的必要性
        // errno:更详细的出错原因,最近一次调用出错的错误码
        int n = read(0, buffer, sizeof(buffer));
        if(n > 0){
            buffer[n] = 0;
            std::cout << "Read " << n << " echo# " << buffer << std::endl;
        }
        else if(n == 0){ // 在 标准输入中,Ctrl + d 退出
            std::cout << "Read over" << std::endl;
            break;
        }
        else{
            if(errno == EAGAIN || errno == EWOULDBLOCK){ // 11(try again) || (Operation would block)
                std::cout << "Data not ready" << std::endl; // 底层数据没就绪
            }
            else if(errno == EINTR)
            {
                std::cout << "Interrupted system call" << std::endl;   // 被中断,重新来过
                sleep(1);
                continue;
            }
            else std::cout << "Read error: " << n << ", errno " << errno << std::endl;
        }
        sleep(1);
    }
    return 0;
}
3. 文件描述符标志 vs 文件状态标志
文件描述符标志(File Descriptor Flags)
通过 fcntl(F_SETFD) 修改。
主要控制描述符的生命周期和继承性,例如:
FD_CLOEXEC:在 execve() 时自动关闭描述符
文件状态标志(File Status Flags)
通过 fcntl(F_SETFL) 修改。
影响文件描述符(File Descriptor)的行为,例如:
O_APPEND:写入时自动定位到文件末尾
O_NONBLOCK:非阻塞模式
六、获取/设置记录锁
⛵️ 在 Unix 和类 Unix 系统中,fcntl 函数提供了用于记录锁(record locks)的功能,通过命令 F_GETLK、F_SETLK 和 F_SETLKW 来实现。这些命令用于获取、设置和管理文件的记录锁,帮助实现进程间的同步。
1. 记录锁的基本概念
Linux实现了 POSIX 标准化的传统(“进程相关”)UNIX记录锁
记录锁 (record locking)的功能是:当一个进程正在读或修改文件的某个部分时,它可以阻止其他进程修改同一文件区。
对于UNIX系统而言,“记录”这个词是一种误用,因为 UNIX 系统内核根本 没有使用 文件记录 这种概念
更适合的术语可能是字节范围锁 (byte-rangelocking),因为它锁定的只是文件中的一个区域(也可能是整个文件)
字节范围锁(Byte-range Locking) :
UNIX/Linux 的记录锁本质是对文件的某个字节范围加锁 ,而非传统意义上的“记录”。
例如:锁定文件从偏移 100 到 200 字节的区域,其他进程不能修改该区域。
锁的类型 :
读锁(F_RDLCK) :共享锁,允许多个进程同时读取锁定区域,但阻止写操作。
写锁(F_WRLCK) :独占锁,阻止其他进程读写锁定区域。
解锁(F_UNLCK) :释放已持有的锁。
锁的继承与释放 :
进程终止时,所有锁自动释放。
fork() 子进程不会继承父进程的锁。
execve() 后,若文件描述符设置了 FD_CLOEXEC,锁会被释放。
F_SETLK、F_SETLKW 和 F_GETLK 用于获取、释放和测试记录锁(也称为字节范围、文件段或文件区域锁)的存在。使用记录锁时,第三个参数是指向 struct flock 结构的指针
2. struct flock 结构体
strucy flock 结构体定义如下:
struct flock {
    short l_type;   // 锁类型: F_RDLCK, F_WRLCK, F_UNLCK
    short l_whence; // 偏移起点: SEEK_SET, SEEK_CUR, SEEK_END
    off_t l_start;  // 偏移量
    off_t l_len;    // 锁定区域长度 (0 表示到文件末尾)
    pid_t l_pid;    // 持有锁的进程 ID (仅用于 F_GETLK)
};
l_whence、l_start、l_len这三个参数用于分段对文件加锁,若对整个文件加锁,则:l_whence= SEEK_SET,l_start= 0,l_len= 0
l_type 有三种状态 :
F_RDLCK 建立一个供读取用的锁定,允许其他进程读该文件,但不允许其他进程写该文件
F_WRLCK 建立一个供写入用的锁定,不允许其他进程读、写该文件
F_UNLCK 删除之前建立的锁定
l_whence 也有三种方式:
SEEK_SET 以文件开头为锁定的起始位置
SEEK_CUR 以目前文件读写位置为锁定的起始位置
SEEK_END 以文件结尾为锁定的起始位置。
2. fcntl 命令详解
2.1 F_GETLK: 检测锁冲突
功能 :测试是否可以对指定区域加锁,不会实际加锁
行为 :
如果存在冲突锁,fcntl 返回 0,并将冲突锁的信息填充到 flock 结构体中(如 l_type 为冲突锁类型,l_pid 为持有锁的进程 ID)。
如果没有冲突锁,l_type 会被设置为 F_UNLCK。
错误处理 :此命令不会设置 errno,返回值始终为 0
示例:
struct flock lock = {0};
lock.l_type = F_WRLCK;        // 测试写锁
lock.l_whence = SEEK_SET;
lock.l_start = 0;             // 从文件开头
lock.l_len = 1024;            // 锁定前 1KB
if (fcntl(fd, F_GETLK, &lock) == 0) {
    if (lock.l_type == F_UNLCK) {
        printf("No conflicting lock.\n");
    } else {
        printf("Conflicting lock by PID %d\n", lock.l_pid);
    }
}
2.2 F_SETTLK: 尝试加锁/解锁
功能 :尝试对指定区域加锁或解锁。
行为 :
加锁 :若无冲突,立即加锁;若有冲突,返回 -1,errno 设置为 EACCES 或 EAGAIN
解锁 :直接释放指定区域的锁
错误处理 :
冲突锁存在时返回 -1,errno = EACCES/EAGAIN
其他错误(如无效参数)返回 -1,errno 设置为具体错误码
示例:
struct flock lock = {0};
lock.l_type = F_WRLCK;        // 加写锁
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;               // 锁定整个文件
if (fcntl(fd, F_SETLK, &lock) == -1) {
    perror("fcntl F_SETLK failed");
}
2.3 F_SETLKW: 阻塞等待加锁
功能 :类似于 F_SETLK,但若存在冲突锁,会阻塞等待 直到锁被释放或被信号中断。
行为 :
成功加锁后返回 0。
被信号中断时返回 -1,errno = EINTR
示例 :
struct flock lock = {0};
lock.l_type = F_RDLCK;        // 加读锁
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;               // 锁定整个文件
if (fcntl(fd, F_SETLKW, &lock) == -1) {
    perror("fcntl F_SETLKW failed");
}
注意:F_SETLKW 不会填充 flock 结构体 ,它只是阻塞等待锁可用。填充结构体是 F_GETLK 的功能
————————————————
                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/island1314/article/details/147687881
阅读剩余
THE END