博客主页:https://blog.csdn.net/wkd_007
博客内容:嵌入式开发、Linux、C语言、C++、数据结构、音视频
本文内容:介绍
金句分享:你不能选择最好的,但最好的会来选择你——泰戈尔
本文未经允许,不得转发!!!
函数原型:
#include
#include
int fcntl(int fd, int cmd, ... /* arg */ );
// arg表示可变参数,由cmd决定
fcntl()
对打开的文件描述符fd
执行下面描述的操作之一。操作由cmd
决定。
fcntl()
的第三个参数是可选。是否需要此参数由cmd
决定。所需的参数类型在每个cmd名称后面的括号中指示(在大多数情况下,所需的类型是int,我们使用名称arg来标识参数),如果不需要参数,则指定void。
以下某些操作仅在特定的Linux内核版本之后才受支持。检查主机内核是否支持特定操作的首选方法是使用所需的cmd值调用fcntl()
,然后使用EINVAL
测试调用是否失败,这表明内核无法识别该值。
本文主要介绍下面4个功能:
- 1、复制文件描述符(F_DUPFD、F_DUPFD_CLOEXEC);
- 2、获取/设置文件描述符标志(F_GETFD、F_SETFD);
- 3、获取/设置文件状态标志(F_GETFL、F_SETFL);
- 4、获取/设置记录锁(F_GETLK、F_SETLK、F_SETLKW);
F_DUPFD(int) 表示使用 F_DUPFD
作为cmd
时,第三个参数需要传入int
型数据。
cmd为F_DUPFD
表示复制文件描述符fd。调用成功会返回新的描述符。新描述符使用大于或等于arg参数的编号最低的可用文件描述符复制文件描述符fd。新描述符与f似共享同一文件表项。但是,新描述符有它自己的一套文件描述符标志,其FD_CLOEXEC文件描述符标志被清除〈这表示该描述符在 exec 时仍保持打开状态)。
// fcntl_F_DUPFD.c
#include
#include
#include
#include
int main()
{
int fd = open("./fcntl_F_DUPFD", 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;
}
F_DUPFD_CLOEXEC(int) 表示使用 F_DUPFD_CLOEXEC
作为cmd
时,第三个参数需要传入int
型数据。
cmd为F_DUPFD_CLOEXEC
的功能与F_DUPFD
类似,区别在于F_DUPFD_CLOEXEC
在复制的同时会设置文件描述符标志FD_CLOEXEC,表示在执行exec系列函数后,该描述符会关闭。
看例子:F_GETFD
和dup函数
都会清除新描述符的FD_CLOEXEC标志,F_DUPFD_CLOEXEC
会复制并设置FD_CLOEXEC标志。
#include
#include
#include
#include
int main()
{
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);
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
标志,这些文件将保持打开状态并继续对子进程可见。这可能导致潜在的安全风险或者意外行为。
F_GETFD(void) :表示使用
F_GETFD
作为cmd
时,不需要传入第三个参数。
功能:获取文件描述符标志。
返回值:
- 成功返回
文件描述符标志
- 失败返回 -1.
F_SETFD(int):表示使用
F_SETFD
作为cmd
时,传入第三个参数是int型的。
功能:设置文件描述符标志,第三个参数传入新的标志值。
返回值:
- 成功返回 0
- 失败返回 -1.
文件描述符的FD_CLOEXEC
标志可以通过三个方法得到:
F_DUPFD_CLOEXEC
复制文件描述符,新的描述符就是FD_CLOEXEC
F_SETFD
直接设置FD_CLOEXEC
。看例子:
#include
#include
#include
#include
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;
}
文件状态标志如下表:
文件状态标志 | 说明 | 十六进制值 |
---|---|---|
O_RDONLY | 只读打开 | 0x0 |
O_WRONLY | 只写打开 | 0x1 |
O_RDWR | 读、写打开 | 0x2 |
O_APPEND | 追加写 | 0x400 |
O_NONBLOCK | 非阻塞模式 | 0x800 |
O_SYNC | 等待写完成(数据和属性) | 0x |
O_DSYNC | 等待写完成(仅数据) | |
O_RSYNC | 同步读和写 | |
O_FSYNC | 等待写完成 | |
O_ASYNC | 异步IO | 0x2000 |
F_GETFL(void) :表示使用
F_GETFL
作为cmd
时,不需要传入第三个参数。
功能:获取文件状态标志。
返回值:
- 成功返回
文件状态标志
- 失败返回 -1.
访问方式标志:O_RDONLY 、O_WRONLY、O_RDWR。这3个值是互斥的,因此首先必须用屏蔽O_ACCMODE取得访问方式位,然后将结果与这3个值中的每一个相比较。
F_SETFL(int):表示使用
F_SETFL
作为cmd
时,传入第三个参数是int型的。
功能:设置文件状态标志
,第三个参数传入新的文件状态标志值。
返回值:
- 成功返回 0
- 失败返回 -1.
在Linux上,只能设置这5个文件状态标志:O_APPEND、 O_ASYNC、 O_DIRECT、 O_NOATIME、O_NONBLOCK,其中最常用的是将文件描述符设置成非阻塞(
O_NONBLOCK
),特别是在网络编程中很常见。
看例子,设置文件状态标志在日常使用中,就用来设置非阻塞,其他的可以先不关注。
#include
#include
#include
#include
int main()
{
int fd = open("./fcntl_F_GETFL", O_RDWR | O_CREAT | O_TRUNC | O_NONBLOCK, 0775);
int flag = fcntl(fd, F_GETFL);
if(flag<0)
return ;
printf("flag=%x, O_ACCMODE=%x [%x %x %x] [%x %x %x %x %x %x %x][%x %x]\n",
flag & O_ACCMODE,O_ACCMODE,O_RDONLY,O_WRONLY,O_RDWR,O_APPEND,O_NONBLOCK,O_ASYNC,O_DSYNC,O_RSYNC,O_FSYNC,O_SYNC,__O_DIRECT,__O_NOATIME);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
close(fd);
return 0;
}
Linux实现了POSIX标准化的传统(“进程相关”)UNIX记录锁。
记录锁(record locking)的功能是:当一个进程正在读或修改文件的某个部分时,它可以阻止其他进程修改同一文件区。对于UNIX系统而言,“记录”这个词是一种误用,因为UNIX系统内核根本没有使用文件记录这种概念。更适合的术语可能是字节范围锁(byte-rangelocking),因为它锁定的只是文件中的一个区域(也可能是整个文件)。
F_SETLK、F_SETLKW和F_GETLK用于获取、释放和测试记录锁(也称为字节范围、文件段或文件区域锁)的存在。使用记录锁时,第三个参数是指向struct 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; //锁区偏移,从l_whence
off_t l_len; //锁区长度(字节)
pid_t l_pid; //阻塞我们加锁的进程ID
...
}
F_SETLK(struct flock *):表示使用
F_SETLK
作为cmd
时,传入第三个参数是struct flock *
型的。
在锁的l_where、l_start和l_len字段指定的字节上设置锁(当l_type为F_RDLCK或F_WRLCK时)或释放锁(当l_type为F_UNLOCK时)。如果另一个进程持有冲突锁,则此调用返回-1并将errno设置为EACCES或EAGAIN。(本例中返回的错误因实现而异,因此POSIX需要一个可移植的应用程序来检查这两个错误。)
F_SETLKW(struct flock *):表示使用
F_SETLKW
作为cmd
时,传入第三个参数是struct flock *
型的。
类似于F_SETLK(命名中的W表示等待(wait)),但如果文件上持有冲突的锁,则等待该锁释放。如果在等待时捕获到信号,则调用将中断,并且(在信号处理程序返回后)立即返回(返回值为-1,errno设置为EINTR;)。
F_GETLK(struct flock *):表示使用
F_GETLK
作为cmd
时,传入第三个参数是struct flock *
型的。
判断由struct flock *
(第三个参数)所描述的锁是否会被另外一把锁所排斥(阻塞)。如果存在一把锁,它阻止创建由第三个参数(struct flock *
)所描述的锁,则把该现存锁的信息写到第三个参数(struct flock *
)指向的结构中。如果不存在这种情况,则除了将l_type设置为F_UNLCK之外,第三个参数所指向结构中的其他信息保持不变。
读锁(F_RDLCK
):允许其他进程读该文件,但不允许其他进程写该文件;
写锁(F_WRLCK
):不允许其他进程读、写该文件;
看例子,下面两个.c保存后编译成2个执行文件,分别在两个窗口允许:
fcntl_lock1.c:
// 进程1,加了读锁,允许其他进程读该文件,但不允许其他进程写该文件;
// gcc fcntl_lock1.c -o fcntl_lock1
#include
#include
#include
#include
int makeFlock(struct flock* pLock,int type, int whence, off_t start, off_t len)
{
if (pLock == NULL)
return -1;
pLock->l_type=type;
pLock->l_whence=whence;
pLock->l_start=start;
pLock->l_len=len;
pLock->l_pid = -1;
return 0;
}
int main()
{
int fd = open("./fcntl_lock", O_RDWR | O_CREAT | O_TRUNC, 0664);
int i=0;
for(i=0; i<30; i++)
{
char num = i+1;
write(fd, &num, 1);
}
// 加读锁
struct flock rlock;
makeFlock(&rlock, F_RDLCK, SEEK_SET, 11, 10); // 读锁,锁第11~20个字节
int ret = fcntl(fd, F_SETLK, &rlock);
if(ret<0)
{
return -1;
}
// 读取数据
printf("reading 11~20 byte:\n");
lseek(fd, 10, SEEK_SET);
for(i=0; i<10; i++)
{
char num;
read(fd, &num, 1);
printf("reading byte: %d\n",num);
sleep(1);
}
sleep(30);
// 释放锁
rlock.l_type = F_UNLCK;
ret = fcntl(fd, F_SETLK, &rlock);
if(ret == -1)
{
perror("rlock release failed");
return -1;
}
sleep(30);
close(fd);
return 0;
}
fcntl_lock2.c
// 进程2,加了读锁,允许其他进程读该文件,但不允许其他进程写该文件;
// gcc fcntl_lock2.c -o fcntl_lock2
#include
#include
#include
#include
int makeFlock(struct flock* pLock,int type, int whence, off_t start, off_t len)
{
if (pLock == NULL)
return -1;
pLock->l_type=type;
pLock->l_whence=whence;
pLock->l_start=start;
pLock->l_len=len;
pLock->l_pid = -1;
return 0;
}
int main()
{
int i=0;
int fd = open("./fcntl_lock", O_RDWR);
// 加读锁,与进程1重叠,仍然可以读
struct flock rlock;
makeFlock(&rlock, F_RDLCK, SEEK_SET, 6, 10); // 读锁,锁第6~15个字节,与进程1的11~20有重叠
int ret = fcntl(fd, F_SETLK, &rlock);
if(ret == -1)
{
return -1;
}
// 读取数据
printf("reading 6~15 byte:\n");
lseek(fd, 5, SEEK_SET);
for(i=0; i<10; i++)
{
char num;
read(fd, &num, 1);
printf("reading byte: %d\n",num);
}
// 释放读锁
rlock.l_type = F_UNLCK;
ret = fcntl(fd, F_SETLK, &rlock);
if(ret == -1)
{
perror("rlock release failed");
return -1;
}
// 加写锁,此时会失败,因为进程1加了读锁没释放
struct flock wlock;
makeFlock(&wlock, F_WRLCK, SEEK_SET, 6, 10); // 写锁,锁第6~15个字节,与进程1的11~20有重叠
ret = fcntl(fd, F_SETLK, &wlock);
if(ret == -1)
{
perror("rlock release failed");
}
sleep(30);
close(fd);
return 0;
}
本文详细介绍fcntl函数,并举例介绍常用功能如:设置非阻塞文件描述符、文件锁等
如果文章有帮助的话,点赞、收藏⭐,支持一波,谢谢
参考资料:
《Unix环境高级编程》
Linux系统调用–fcntl函数详解:https://blog.csdn.net/bailyzheng/article/details/7463775