Linux-IO操作之fcntl 和 ioctl

fcntl函数,也就是file control,提供了对文件描述符的各种操作。另一个常见的控制文件描述符的属性和行为的系统调用是ioctl,而且ioctlfcntl能够执行更多的控制。但是,对于控制文件描述符常见的属性和行为,fcntl函数是由POSIX规范指定的首选方法

  • ioctl()是底层的系统调用(system call),所以跨平台特性不好。

  • 而fcntl则是被封装的函数,各个OS都是支持的。

可参考:

Unix/Linux编程:fcntl函数总结-CSDN博客

【Linux C | 文件I/O】fcntl函数详解 | 设置描述符非阻塞、文件(记录)锁-CSDN博客

fcntl函数可以改变一个已打开的文件的属性,可以重新设置读、写、追加、非阻塞等标志(这些标志称为File Status Flag),而不必重新open文件。

通过fcntl设置的都是当前进程如何访问设备或文件的访问控制属性,例如读、写、追加、非阻塞、加锁等,但并不设置文件或设备本身的属性,例如文件的读写权限、串口波特率等。

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设置。

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 函数详解

参考:

linux 内核 - ioctl 函数详解-CSDN博客

(笔记)Linux下的ioctl()函数详解 - tdyizhen1314 - 博客园 (cnblogs.com)

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)

更多实际应用待补充。

  • 18
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值