fcntl函数,也就是
file control
,提供了对文件描述符的各种操作。另一个常见的控制文件描述符的属性和行为的系统调用是ioctl
,而且ioctl
比fcntl
能够执行更多的控制。但是,对于控制文件描述符常见的属性和行为,fcntl
函数是由POSIX规范指定的首选方法
ioctl()是底层的系统调用(system call),所以跨平台特性不好。
而fcntl则是被封装的函数,各个OS都是支持的。
可参考:
Unix/Linux编程:fcntl函数总结-CSDN博客
【Linux C | 文件I/O】fcntl函数详解 | 设置描述符非阻塞、文件(记录)锁-CSDN博客
fcntl函数可以改变一个已打开的文件的属性,可以重新设置读、写、追加、非阻塞等标志(这些标志称为File Status Flag),而不必重新open文件。
通过fcntl设置的都是当前进程如何访问设备或文件的访问控制属性,例如读、写、追加、非阻塞、加锁等,但并不设置文件或设备本身的属性,例如文件的读写权限、串口波特率等。
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
ioctl函数用于设置某些设备本身的属性,例如串口波特率、终端窗口大小,注意区分这两个函数的作用。
ioctl用于向设备发控制和配置命令,有些命令也需要读写一些数据,但这些数据是不能用read/write读写的,称为Out-of-band数据。也就是说,read/write读写的数据是in-band数据,是I/O操作的主体,而ioctl命令传送的是控制信息,其中的数据是辅助的数据。例如,在串口线上收发数据通过ead/write操作,而串口的波特率、校验位、停止位通过ioctl设置,A/D转换的结果通过read读取,而A/D转换精度和工作频率通过ioctl设置。
int ioctl(int d, int request, ...);
fcntl 函数介绍
fcntl - 对一个打开的文件描述符执行一系列控制操作。
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
fcntl() 对打开的文件描述符fd执行下面操作之一。操作由cmd决定 fcntl() 可以接受可选的第三个参数。是否需要此参数由cmd确定。所需要的 参数类型在每个cmd名称后面的括号中表示(在大多数情况下,所需 的类型是int,我们使用名称arg标识参数),如果参数不是必需的, 则指定void。 第三个参数以省略号来表示,这意味着可以将其设置为不同的类型, 或者加以省略。内核会依据 cmd 参数(如果有的话)的值来确定该 参数的数据类型
返回值: 失败返回-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)
本文主要记录前三个功能。
复制文件描述符(F_DUPFD、F_DUPFD_CLOEXEC)
F_DUPFD(int)
F_DUPFD(int) 表示使用 F_DUPFD 作为cmd时,第三个参数需要传入int型数据。
cmd为F_DUPFD表示复制文件描述符fd。调用成功会返回新的描述符。新描述符使用大于或等于arg参数的编号最低的可用文件描述符复制文件描述符fd。
// fcntl_F_DUPFD.c #include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <string.h> 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(int) 表示使用 F_DUPFD_CLOEXEC 作为cmd时,第三个参数需要传入int型数据。
cmd为F_DUPFD_CLOEXEC的功能与F_DUPFD类似,区别在于F_DUPFD_CLOEXEC在复制的同时会设置文件描述符标志FD_CLOEXEC,用来表示该描述符在执行完fork+exec系列函数创建子进程时会自动关闭,以防止它们被传递给子进程。
获取/设置文件描述符标志(F_GETFD、F_SETFD)
当前只定义了一个文件描述符标志
FD_CLOEXEC
。注意,是文件描述符标志,不是文件描述符本身。
FD_CLOEXEC
用来表示该描述符在执行完fork+exec系列函数创建子进程时会自动关闭,以防止它们被传递给子进程。为什么要这样做呢?因为当一个进程调用exec系列函数(比如execve)来创建子进程时,所有打开的文件描述符都会被传递给子进程。如果文件描述符没有设置FD_CLOEXEC标志,这些文件将保持打开状态并继续对子进程可见。这可能导致潜在的安全风险或者意外行为。
文件描述符的FD_CLOEXEC标志可以通过三个方法得到:
1、调用open函数是,指定 O_CLOEXEC
2、通过fcntl函数使用F_DUPFD_CLOEXEC复制文件描述符,新的描述符就是FD_CLOEXEC
3、通过fcntl函数使用F_SETFD直接设置FD_CLOEXEC。
F_GETFD(void) :表示使用 F_GETFD 作为cmd时,不需要传入第三个参数。
功能:获取文件描述符标志。
返回值:
成功返回文件描述符标志
失败返回 -1.
F_SETFD(int):表示使用 F_SETFD 作为cmd时,传入第三个参数是int型的。
功能:设置文件描述符标志,第三个参数传入新的标志值。
返回值:
成功返回 0
失败返回 -1.
获取/设置文件状态标志(F_GETFL、F_SETFL)
文件状态标志就是open时指定的flags标志。
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 <stdio.h> #include <unistd.h> #include <fcntl.h> #include <string.h> 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; }
更多待补充。
ioctl 函数详解
参考:
https://blog.csdn.net/qq_19923217/article/details/82698787
https://www.cnblogs.com/tdyizhen1314/p/4896689.html
ioctl 是设备驱动程序中设备控制接口函数,一个字符设备驱动通常会实现设备打开、关闭、读、写等功能,在一些需要细分的情境下,如果需要扩展新的功能,通常以增设 ioctl() 命令的方式实现。
用户空间 ioctl
#include <sys/ioctl.h>
int ioctl(int fd, int cmd, ...) ;
参数描述
fd 文件描述符
cmd 交互协议,设备驱动将根据 cmd 执行对应操作
… 可变参数 arg,依赖 cmd 指定长度以及类型
ioctl() 函数执行成功时返回 0,失败则返回 -1 并设置全局变量 errno 值,如下:
EBADF d is not a valid descriptor.
EFAULT argp references an inaccessible memory area.
EINVAL Request or argp is not valid.
ENOTTY d is not associated with a character special device.
ENOTTY The specified request does not apply to the kind of object that the descriptor d references.
因此,在用户空间使用 ioctl 时,可以做如下的出错判断以及处理:
int ret;
ret = ioctl(fd, MYCMD);
if (ret == -1) {
printf("ioctl: %s\n", strerror(errno));
}
在实际应用中,ioctl 最常见的 errorno 值为 ENOTTY(error not a typewriter),顾名思义,即第一个参数 fd 指向的不是一个字符设备,不支持 ioctl 操作,这时候应该检查前面的 open 函数是否出错或者设备路径是否正确
要记住,用户程序所作的只是通过命令码(cmd)告诉驱动程序它想做什么,至于怎么解释这些命令和怎么实现这些命令,这都是驱动程序要做的事情。
驱动程序中,对应的ioctl
struct file_operations {
struct module *owner;
int (*ioctl) (struct inode *inode, struct file *filep, unsigned int cmd, unsigned long args);
long (*unlocked_ioctl) (struct file *filep, unsigned int cmd, unsigned long args); ......
}
在2.6.36以后
linux
的内核中,只支持unlocked_ioctl()
,不支持ioctl()
。2.6.35.7
内核中,两个函数都可以使用。compat 全称 compatible(兼容的),主要目的是为 64 位系统提供 32 位 ioctl 的兼容方法。在字符设备驱动开发中,一般情况下只要实现 unlocked_ioctl 函数即可,因为在 vfs 层的代码是直接调用 unlocked_ioctl 函数
ioctl 用户与驱动之间的协议
前文提到 ioctl 方法第二个参数 cmd 为用户与驱动的 “协议”,理论上可以为任意 int 型数据,可以为 0、1、2、3……,但是为了确保该 “协议” 的唯一性,ioctl 命令应该使用更科学严谨的方法赋值,在linux中,提供了一种 ioctl 命令的统一格式,将 32 位 int 型数据划分为四个位段,从高到低分别占据2bits、14bits、8bits、8bits,如下图所示:
在驱动程序中实现的ioctl函数体内,实际上是有一个switch{case}结构,每一个case对应一个命令码,做出一些相应的操作。怎么实现这些操作,那就具体设备具体对待了。
这个32位的数据如果由我们来自己生成也可以,就是会比较麻烦,因此,在内核中,提供了宏接口以生成上述格式的 ioctl 命令:
// include/uapi/asm-generic/ioctl.h
#define _IOC(dir,type,nr,size) \
(((dir) << _IOC_DIRSHIFT) | \
((type) << _IOC_TYPESHIFT) | \
((nr) << _IOC_NRSHIFT) | \
((size) << _IOC_SIZESHIFT))
dir(direction),ioctl 命令访问模式(数据传输方向),占据 2 bit,可以为 _IOC_NONE、_IOC_READ、_IOC_WRITE、_IOC_READ | _IOC_WRITE,分别指示了四种访问模式:无数据、读数据、写数据、读写数据;
type(device type),设备类型,占据 8 bit,在一些文献中翻译为 “幻数” 或者 “魔数”,可以为任意 char 型字符,例如
‘a’、’b’、’c’ 等等,其主要作用是使 ioctl 命令有唯一的设备标识;
nr(number),命令编号/序数,占据 8 bit,可以为任意 unsigned char 型数据,取值范围 0~255,如果定义了多个 ioctl 命令,通常从 0 开始编号递增;
size,涉及到 ioctl 函数 第三个参数 arg ,占据 13bit 或者 14bit(体系相关,arm 架构一般为 14 位),指定了 arg 的数据类型及长度,如果在驱动的 ioctl 实现中不检查,通常可以忽略该参数;
通常而言,为了方便会使用宏 _IOC() 衍生的接口来直接定义 ioctl 命令:
// include/uapi/asm-generic/ioctl.h
/* used to create numbers */
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
_IO: 定义不带参数的 ioctl 命令
_IOW: 定义带写参数的 ioctl 命令(copy_from_user)
_IOR: 定义带读参数的ioctl命令(copy_to_user)
_IOWR: 定义带读写参数的 ioctl 命令
同时,内核还提供了反向解析 ioctl 命令的宏接口:
// include/uapi/asm-generic/ioctl.h
/* used to decode ioctl numbers */
#define _IOC_DIR(nr) (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
#define _IOC_TYPE(nr) (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
#define _IOC_NR(nr) (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
#define _IOC_SIZE(nr) (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)
更多实际应用待补充。