🔥fcntl
函数是一个在 UNIX 和类 UNIX 系统(如 Linux)上用来操作文件描述符的系统调用
函数原型
#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
命令,可以传递新的状态标志返回值
errno
以指示错误类型常见的命令包括:
F_DUPFD
:复制文件描述符F_DUPFD_CLOEXEC
:复制文件描述符,设置FD_CLOEXEC标志F_GETFL
:获取文件描述符的当前状态标志F_SETFL
:设置文件描述符的状态标志F_GETFD
:获取文件描述符的内部标志
F_SETFD
:设置文件描述符的内部标志F_GETOWN
:获得异步I/O所有权F_SETOWN
:设置异步I/O所有权F_GETLK
:获取文件锁定信息
F_SETLK
:设置文件锁定信息
F_SETLKW
:以阻塞方式设置文件锁定信息。
注意:以下某些操作仅在特定的
Linux
内核版本之后才受支持。检查主机内核是否支持特定操作的首选方法是使用所需的cmd
值调用fcntl()
,然后使用EINVAL测试调用是否失败,这表明内核是否能够识别该值
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工作模式 或 实时信号
使用 fcntl
函数时,如果返回值为 -1,可以通过 errno 获取具体错误信息。常见的错误如下:
EBADF
:提供的文件描述符无效EINTR
:操作被信号中断EINVAL
:指定的命令无效或者参数不符合规范#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;
}
int fcntl(int fd, F_DUPFD, int n);
/* 参数说明 */
fd: 要复制的原始文件描述符。
n: 分配的新文件描述符的起始值。如果可用,新的文件描述符将返回一个大于或等于 n 的最小的文件描述符。
功能:
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;
}
int fcntl(int fd, F_DUPFD_CLOEXEC, int n);
功能:
F_DUPFD
,区别在于F_DUPFD_CLOEXEC
在复制的同时会设置文件描述符标志 FD_CLOEXEC
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;
}
♐️ 文件描述符标志FD_CLOEXEC
,用来表示该描述符在执行完 fork+exec
系列函数创建子进程时会自动关闭,以防止它们被传递给子进程。那么为什么要这样做呢?
原因:因为当一个进程调用
exec
系列函数(比如execve
)来创建子进程时,所有打开的文件描述符都会被传递给子进程
FD_CLOEXEC
标志,这些文件将保持打开状态并继续对子进程可见🏖 fcntl
函数中的 F_GETFD
和 F_SETFD
命令用于获取和设置文件描述符的标志,这在操作文件描述符的行为时非常重要
int fcntl(int fd, F_GETFD);
// 参数说明:
fd: 要查询状态的文件描述符
功能:获取指定文件描述符的标志
返回值:返回文件描述符的标志,如果失败则返回 -1,并设置 errno。主要的标志如下:
FD_CLOEXEC
:文件描述符在 exec 系列调用时会被关闭。如果该标志被设置,返回值包含该标志。FD_CLOEXEC
持平,表示标志已设置;如果返回值包含 0,表示没有设置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
命令用于获取和设置文件的状态标志,这些标志控制文件的行为和特性。
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(十六进制)#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) { /* 只读 */ }
#include <fcntl.h>
int fcntl(int fd, F_SETFL, int flags);
返回值:
注意: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
select()
、poll()
、epoll()
等配合,实现高性能 I/O 多路复用。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;
}
文件描述符标志(File Descriptor Flags)
fcntl(F_SETFD)
修改。FD_CLOEXEC
:在 execve()
时自动关闭描述符文件状态标志(File Status Flags)
fcntl(F_SETFL)
修改。O_APPEND
:写入时自动定位到文件末尾O_NONBLOCK
:非阻塞模式⛵️ 在 Unix 和类 Unix 系统中,fcntl 函数提供了用于记录锁(record locks)的功能,通过命令 F_GETLK、F_SETLK 和 F_SETLKW 来实现。这些命令用于获取、设置和管理文件的记录锁,帮助实现进程间的同步。
Linux实现了 POSIX
标准化的传统(“进程相关”)UNIX记录锁
record locking
)的功能是:当一个进程正在读或修改文件的某个部分时,它可以阻止其他进程修改同一文件区。UNIX
系统内核根本 没有使用 文件记录 这种概念byte-rangelocking
),因为它锁定的只是文件中的一个区域(也可能是整个文件)100
到 200
字节的区域,其他进程不能修改该区域。F_RDLCK
) :共享锁,允许多个进程同时读取锁定区域,但阻止写操作。F_WRLCK
) :独占锁,阻止其他进程读写锁定区域。F_UNLCK
) :释放已持有的锁。fork()
子进程不会继承父进程的锁。execve()
后,若文件描述符设置了 FD_CLOEXEC
,锁会被释放。
F_SETLK
、F_SETLKW
和F_GETLK
用于获取、释放和测试记录锁(也称为字节范围、文件段或文件区域锁)的存在。使用记录锁时,第三个参数是指向 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
= 0l_type
有三种状态 : F_RDLCK
建立一个供读取用的锁定,允许其他进程读该文件,但不允许其他进程写该文件F_WRLCK
建立一个供写入用的锁定,不允许其他进程读、写该文件F_UNLCK
删除之前建立的锁定l_whence
也有三种方式: SEEK_SET
以文件开头为锁定的起始位置SEEK_CUR
以目前文件读写位置为锁定的起始位置SEEK_END
以文件结尾为锁定的起始位置。功能 :测试是否可以对指定区域加锁,不会实际加锁
行为 :
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);
}
}
功能 :尝试对指定区域加锁或解锁。
行为 :
-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");
}
功能 :类似于 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
的功能
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有