fcntl VS ioctl
fcntl函数,也就是
file control
,提供了对文件描述符的各种操作。另一个常见的控制文件描述符的属性和行为的系统调用是ioctl
,而且ioctl
比fcntl
能够执行更多的控制。但是,对于控制文件描述符常见的属性和行为,fcntl
函数是由POSIX规范指定的首选方法
- ioctl()是底层的系统调用(system call),所以跨平台特性不好。
- 而fcntl则是被封装的函数,各个OS都是支持的。
fcntl
NAME
fcntl - 对一个打开的文件描述符执行一系列控制操作。
SYNOPSIS
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
DESCRIPTION
fcntl() 对打开的文件描述符fd执行下面操作之一。操作由cmd决定
fcntl() 可以接受可选的第三个参数。是否需要此参数由cmd确定。所需要的
参数类型在每个cmd名称后面的括号中表示(在大多数情况下,所需
的类型是int,我们使用名称arg标识参数),如果参数不是必需的,
则指定void。
第三个参数以省略号来表示,这意味着可以将其设置为不同的类型,
或者加以省略。内核会依据 cmd 参数(如果有的话)的值来确定该
参数的数据类型
仅从特定的Linux内核版本开始,才支持以下某些操作。 检查主机内核是否
支持特定操作的首选方法是使用所需的cmd值调用fcntl(),然后使用EINVAL
测试调用是否失败,这表明内核无法识别该值。
。。。(待补)
返回值: 失败返回-1并设置errno
/*
* 功能: 复制一个已经有的描述符(cmd=F_DUPFD或者F_DUPFD_CLOEXEC)
* 获取/设置文件描述符标志(cmd=F_GETFD或者F_SETFD)
* 获取/设置文件状态标志(cmd=F_GETFL或者F_SETFL)
* 获取/设置异步IO所有权(cmd=F_GETOWN或者F_SETOWN)
* 获取/设置记录锁(cmd=F_GETLK或者F_SETLK或者F_SETLKW)
*/
获取文件标志
fcntl()的用途之一是针对一个打开的文件,获取或者修改器访问模式和状态标志(这些值是通过指定open()调用的flag参数来设置的)。要获取这些设置,应将 fcntl()的 cmd 参数设置为F_GETFL:
if ((flags = fcntl(atoi(argv[1]), F_GETFL)) == -1){
printf("fcntl error for fd %d", atoi(argv[1]));
exit(EXIT_FAILURE);
}
在上述代码之后,可以以如下代码测试文件是否以同步写方式打开:
if (flags & O_SYNC)
printf(", synchronous writes");
SUSv3 规定:针对一个打开的文件,只有通过 open()或后续 fcntl()的 F_SETFL 操作,才能对该文件的状态标志进行设置。然而在如下方面,Linux 实现与标准有所偏离:如果一个程序编译时采用了 5.10 节所提及的打开大文件技术,那么当使用 F_GETFL 命令获取文件状态标志时,标志中将总是包含 O_LARGEFILE 标志。
判定文件的访问模式有一点复杂,这是因为 O_RDONLY(0)、O_WRONLY(1)和 O_RDWR(2)这 3 个常量并不与打开文件状态标志中的单个比特位对应。因此,要判定访问模式,需使用掩码 O_ACCMODE 与 flag 相与,将结果与 3 个常量进行比对:
int accessMode ;
accessMode = flags & O_ACCMODE;
if(accessMode == O_RDONLY){
printf("read only");
}
综上所示,完整代码如下:
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int flags;
if (argc != 2){
printf("usage: a.out <descriptor#>");
exit(EXIT_FAILURE);
}
// 第一个参数指定文件描述符
if ((flags = fcntl(atoi(argv[1]), F_GETFL)) < 0){
printf("fcntl error for fd %d", atoi(argv[1]));
exit(EXIT_FAILURE);
}
switch (flags & O_ACCMODE) {
case O_RDONLY:
printf("read only");
break;
case O_WRONLY:
printf("write only");
break;
case O_RDWR:
printf("read write");
break;
default:
printf("unknown access mode");
exit(EXIT_SUCCESS);
}
if (flags & O_APPEND)
printf(", append");
if (flags & O_NONBLOCK)
printf(", nonblocking");
if (flags & O_SYNC)
printf(", synchronous writes");
#if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC) && (O_FSYNC != O_SYNC)
if (val & O_FSYNC)
printf(", synchronous writes");
#endif
putchar('\n');
exit(0);
}
测试
[root@localhost build]$ ./apud 0 < /dev/tty
read only
[root@localhost build]$ ./apud 1 > temp.foo
[root@localhost build]$ ./apud 2 > temp.foo
[root@localhost build]$ ./apud 2 2>>temp.foo
write only, append
[root@localhost build]$ ./apud 5 5<>temp.foo #5<>temp.foo表示再文件描述符5上以读写的方式打开文件temp.foo
read write
修改文件的属性
可以使用 fcntl()的 F_SETFL 命令来修改打开文件的某些状态标志。允许更改的标志有
O_APPEND、O_NONBLOCK、O_NOATIME、O_ASYNC 和 O_DIRECT。系统将忽略对其他标志的修改操作。(有些其他的 UNIX 实现允许 fcntl()修改其他标志,如 O_SYNC。
使用fcntl()修改文件状态标志,尤其适用于如下场景
- 文件不是由调用程序打开的,所以程序也无法使用open()调用来控制文件的状态标志(比如,文件时3个标准输入输出描述符中的一员,这些描述符在程序启动之前就被打开)
- 文件描述符的获取是通过 open()之外的系统调用
- 比如 pipe()调用,该调用创建一个管道,并返回两个文件描述符分别对应管道的两端。
- 比如 socket()调用,该调用创建一个套接字并返回指向该套接字的文件描述符
为了修改打开文件的状态标志,可以使用 fcntl()的 F_GETFL 命令来获取当前标志的副本,然后修改需要变更的比特位,最后再次调用 fcntl()函数的 F_SETFL 命令来更新此状态标志。
设置文件属性
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
void set_fl(int fd, int flags) /* flags are file status flags to turn on */
{
int val;
if ((val = fcntl(fd, F_GETFL, 0)) < 0){
perror("fcntl F_GETFL error");
exit(EXIT_FAILURE);
}
val |= flags; /* turn on flags */
if (fcntl(fd, F_SETFL, val) < 0){
perror("fcntl F_GETFL error");
exit(EXIT_FAILURE);
}
}
清除文件属性
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
void clr_fl(int fd, int flags)
{
int val;
if ((val = fcntl(fd, F_GETFL, 0)) == -1) {
printf("fcntl F_GETFL error\n");
exit(EXIT_FAILURE);
}
val &= ~flags;
if (fcntl(fd, F_SETFL, val) == -1) {
printf("fcntl F_SETFL error\n");
exit(EXIT_FAILURE);
}
}
设置/清除文件描述符为非阻塞的
int setnonblocking(int fd){
int old_option = fcntl(fd, F_GETFL); // 获取文件描述符旧的状态标志
int new_option = old_option | O_NONBLOCK; //设置非阻塞标志
fcntl(fd, F_SETFL, new_option);
return old_option;
}
int clrnonblocking(int fd){
int old_option = fcntl(fd, F_GETFL); // 获取文件描述符旧的状态标志
int new_option = old_option & ~O_NONBLOCK; //设置非阻塞标志
fcntl(fd, F_SETFL, new_option);
return old_option;
}
O_NONBLOCK和O_NDELAY所产生的结果都是使I/O变成非阻塞模式(non-blocking),在读取不到数据或是写入缓冲区已满会马上return,而不会阻塞等待。
它们的差别在于:在读操作时,如果读不到数据,O_NDELAY会使I/O函数马上返回0,但这又衍生出一个问题,因为读取到文件末尾(EOF)时返回的也是0,这样无法区分是哪种情况。因此,O_NONBLOCK就产生出来,它在读取不到数据时会回传-1,并且设置errno为EAGAIN。
O_NDELAY是在System V的早期版本中引入的,在编码时,还是推荐POSIX规定的O_NONBLOCK,O_NONBLOCK可以在open和fcntl时设置。
- acl_non_blocking.c
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#define ACL_SOCKET int
#define acl_msg_error printf
/**
* 设置套接口为阻塞或非阻塞
* @param fd {ACL_SOCKET} SOCKET 套接字
* @param on {int} 是否设置该套接字为非阻塞
* @return {int} >= 0: 成功, 返回值 > 0 表示设置之前的标志位; -1: 失败
*/
int acl_non_blocking(ACL_SOCKET fd, int on){
int flags;
int nonb = O_NONBLOCK;
if ((flags = fcntl(fd, F_GETFL)) == -1) {
acl_msg_error("%s(%d), %s: fcntl(%d, F_GETFL) error: %s",
__FILE__, __LINE__, __FUNCTION__,
fd, strerror(errno));
return -1;
}
if (fcntl(fd, F_SETFL, on ? flags | nonb : flags & ~nonb) < 0) {
acl_msg_error("%s(%d), %s: fcntl(%d, F_SETL, nonb) error: %s",
__FILE__, __LINE__, __FUNCTION__,
fd, strerror(errno));
return -1;
}
return flags;
}
/**
* 判断套接口为阻塞或非阻塞
* @param fd {ACL_SOCKET} SOCKET 套接字
* @return {int} == 1, 非阻塞; 0阻塞
*/
int acl_is_blocking(ACL_SOCKET fd)
{
int flags;
if ((flags = fcntl(fd, F_GETFL, 0)) == -1) {
acl_msg_error("fcntl(fd, F_GETFL) failed");
return -1;
}
return (flags & O_NONBLOCK) == 0 ? 1 : 0;
}
- acl_non_blocking.h
#ifndef ACL_LEARING_ACL_NON_BLOCKING_H
#define ACL_LEARING_ACL_NON_BLOCKING_H
# define ACL_SOCKET int
#define ACL_BLOCKING 0 /**< 阻塞读写标志位 */
#define ACL_NON_BLOCKING 1 /**< 非阻塞读写标志位 */
/**
* 设置套接口为阻塞或非阻塞
* @param fd {ACL_SOCKET} SOCKET 套接字
* @param on {int} 是否设置该套接字为非阻塞, ACL_BLOCKING 或 ACL_NON_BLOCKING
* @return {int} >= 0: 成功, 返回值 > 0 表示设置之前的标志位; -1: 失败
*/
int acl_non_blocking(ACL_SOCKET fd, int on);
/**
* 判断所给套按口是否为阻塞模式
* @param fd {ACL_SOCKET} SOCKET 套接字
* @return {int} -1 表示出错或所给参数有误或该平台不支持,1 表示所给套接字为阻塞模式
* 0 表示所给套接字为非阻塞模式
*/
int acl_is_blocking(ACL_SOCKET fd);
#endif //ACL_LEARING_ACL_NON_BLOCKING_H
总结
fcntl函数支持的常用操作以其参数如表所示:
SIGIO
和SIGURG
这两个信号与其他Linux信号不同,它们必须和某个文件描述符相关联方可使用:
- 当被关联的文件描述符可读或者可写时,系统将触发
SIGIO
信号 - 当被关联的文件描述符(必须是一个socket)上有带外数据可读时,系统将触发
SIGURG
信号
将信号和文件描述符关联的方法,就是使用fcntl
函数为目标文件描述符指定宿主进程或者进程组,那么被指定的宿主进程或者进程组将会捕获这两个信号
使用SIGIO
时,还需要利用fcntl
设置其O_ASYNC
标志(异步IO标志)
F_GETFL
:
- 返回fd对应的文件状态标志