首页
/ Python fcntl 模块完全指南:fcntl() 与 ioctl() 系统调用的文件控制与文件锁实践

Python fcntl 模块完全指南:fcntl() 与 ioctl() 系统调用的文件控制与文件锁实践

2026-09-07 11:24:53作者:毕习沙Eudora

fcntl 是 CPython 标准库中面向 Unix 的底层模块,负责在文件描述符上执行文件控制(fcntl(2))与 I/O 控制(ioctl(2)),可用于设置非阻塞标志、查询终端进程组、实现跨进程文件锁、管理管道缓冲区乃至 Linux memfd 密封等操作。读完本文,你将掌握 fcntl/ioctl/flock/lockf 四大函数的参数语义与返回规则、缓冲区传参的坑点,以及如何结合 structtermios 写出可移植、可上线的锁与终端控制代码。

本文主体内容来自 CPython 官方文档 Doc/library/fcntl.rst,源码级行为均可在 Modules/fcntlmodule.c 及标准库测试 Lib/test/test_fcntl.py 中查证。

模块定位与可用性

本模块对文件描述符执行文件控制与 I/O 控制,是 Unix 例程 fcntl()ioctl() 的 Python 接口,完整行为细节以 fcntl(2)ioctl(2) 手册为准。

  • 可用性:仅 Unix 平台可用,WASI 不可用
  • 第一参数约定:模块中所有函数都以文件描述符 fd 作为第一个参数,它可以是:
    • 整数文件描述符,例如 sys.stdin.fileno() 的返回值;
    • 实现了 fileno() 方法并返回真实文件描述符的 io.IOBase 对象(如 sys.stdin 本身)。此时 CPython 会通过 fildes 类型转换器取出底层整数 fd。

在 CPython 官方文档的术语中,该模块属于"Unix 专属服务"类标准库,需在支持 fcntl/ioctl 的系统调用(Linux、macOS、BSD、AIX、Solaris 等)上运行,Windows 没有该模块。

错误语义的演变

文档明确记录了两处关键的历史行为变化:

  • 3.3 版本起:模块内的操作不再抛 IOError,而是统一抛出 OSErrorOSErrorerrno 属性,可用于判断锁冲突等具体失败原因。
  • 所有函数的实现都使用 PyErr_SetFromErrno(PyExc_OSError) 将 C 层 errno 直接映射为 Python 异常(见 Modules/fcntlmodule.c 中各实现函数的错误分支)。

审计钩子

四个公开函数都会触发审计事件(audit events),供 sys.addaudithook 监听:

  • fcntl.fcntl,事件参数为 (fd, cmd, arg)
  • fcntl.ioctl,事件参数为 (fd, request, arg)
  • fcntl.flock,事件参数为 (fd, operation)
  • fcntl.lockf,事件参数为 (fd, cmd, len, start, whence)

在源码中对应 Modules/fcntlmodule.c 各函数开头的 PySys_Audit(...) 调用,例如 fcntl_fcntl_implPySys_Audit("fcntl.fcntl", "iiO", fd, code, arg)

fcntl(fd, cmd, arg=0, /):最通用的文件控制入口

fcntl.fcntl(fd, cmd, arg=0, /)

对文件描述符 fd 执行 cmd 指定的操作。cmd 的取值依赖操作系统,全部以与 C 头文件相同的名字作为常量暴露在 fcntl 模块中。arg 可以是整数bytes-like 对象字符串,其类型与大小必须与该操作在 C 文档中要求的参数类型和大小一致。

三种 arg 形态的返回规则

arg 形态 数据流 返回值
整数 作为第三参数原样传给 C 的 fcntl(fd, cmd, int_arg) C 调用返回的整数
bytes-like 对象 表示二进制结构(如 struct.pack 的产物),复制到缓冲区后把缓冲区地址传给 C 调用 成功后返回调用后的缓冲区内容(bytes 对象),长度与传入的 arg 相同
字符串 UTF-8 编码为二进制后再进入上述缓冲区流程 同上,返回 bytes

调用失败时抛 OSError

缓冲区溢出防护(源码细节)

Modules/fcntlmodule.c 的实现可以看出,Python 不会限制操作系统向缓冲区写回的数据量,但做了主动防护:

  • 长度 ≤ 1024 字节的缓冲区会先复制到栈上 char buf[FCNTL_BUFSZ+GUARDSZ]FCNTL_BUFSZ 为 1024),并在数据尾部追加 8 字节的随机哨兵 guard
  • 调用后若哨兵被改写,说明系统调用发生了缓冲区溢出,会抛出 SystemError("Possible stack corruption in fcntl() due to buffer overflow..."),提示提供足够大的参数缓冲;
  • 长度超过 1024 字节的输入在 3.15 起不再受 1024 字节上限约束,而是改用堆上的 PyBytesWriter 路径并检查尾部 NUL 是否被覆盖(见 fcntl_fcntl_impllen <= FCNTL_BUFSZ 的 if/else 分支)。

文档给出的两个重要提醒:

  1. 如果 arg 的类型/大小与操作不匹配(例如需要指针时传了整数,或系统写回的信息大于缓冲区长度),极可能引发段错误或更隐蔽的数据损坏——这是由 C 层直接传指针的机制决定的,Python 无法在越界发生后挽回。
  2. cmd 被解析为 C int,调用方需自行保证该值能放进平台相关的命令码范围。

各版本暴露的平台常量

CPython 根据平台头文件按需导出常量(见 Modules/fcntlmodule.call_ins() 大段的 #ifdef 保护式 PyModule_AddIntMacro),文档按版本记录了新增能力:

  • 3.8:新增 F_ADD_SEALSF_GET_SEALSF_SEAL_* 系列,用于 os.memfd_create 创建的文件描述符的密封(sealing)操作。
  • 3.9:macOS 上暴露 F_GETPATH(由 fd 反查文件路径);Linux ≥ 3.15 上暴露 F_OFD_GETLKF_OFD_SETLKF_OFD_SETLKW(open file description 锁)。
  • 3.10:Linux ≥ 2.6.11 上暴露 F_GETPIPE_SZF_SETPIPE_SZ(查询/修改管道大小)。
  • 3.11:FreeBSD 上暴露 F_DUP2FDF_DUP2FD_CLOEXEC(复制描述符,后者额外设置 FD_CLOEXEC)。
  • 3.12:Linux ≥ 4.5 上暴露 FICLONEFICLONERANGE,用于在 btrfs、OCFS2、XFS 等文件系统上通过 reflink 实现"写时复制"(copy-on-write)的数据共享。注意源码中对 Android 做了排除(#ifndef __ANDROID__,注释说明 SELinux 会阻止 FICLONE)。
  • 3.13
    • Linux ≥ 2.6.32 暴露 F_GETOWN_EXF_SETOWN_EXF_OWNER_TIDF_OWNER_PIDF_OWNER_PGRP,可将 I/O 可用性信号定向到指定线程/进程/进程组;
    • Linux ≥ 4.13 暴露 F_GET_RW_HINTF_SET_RW_HINTF_GET_FILE_RW_HINTF_SET_FILE_RW_HINTRWH_WRITE_LIFE_*,用于告知内核 inode 或某个打开文件描述上写入的预期存活期(write life time hints);
    • Linux ≥ 5.1 与 NetBSD 暴露 F_SEAL_FUTURE_WRITE
    • FreeBSD 暴露 F_READAHEADF_ISUNIONSTACKF_KINFO;macOS 与 FreeBSD 暴露 F_RDAHEAD;NetBSD 与 AIX 暴露 F_CLOSEM;NetBSD 暴露 F_MAXFD;macOS 与 NetBSD 暴露 F_GETNOSIGPIPEF_SETNOSIGPIPE
  • 3.14:Linux ≥ 6.1 暴露 F_DUPFD_QUERY(查询是否指向同一文件的 fd)。

同一函数在 all_ins() 中还按平台惯例导出了 F_DUPFD/F_DUPFD_CLOEXECF_GETFD/F_SETFDF_GETFL/F_SETFLF_GETLK/F_SETLK/F_SETLKW(含 64 位 LFS 变体 F_*64)、F_GETOWN/F_SETOWNFD_CLOEXECF_NOTIFY 相关 DN_*F_SETLEASE/F_GETLEASEF_FULLFSYNC/F_NOCACHE(macOS)等常量,使用时建议先 hasattr(fcntl, name) 探测再访问以保证可移植性。

ioctl(fd, request, arg=0, mutate_flag=True, /):可原地改写的设备控制

fcntl.ioctl(fd, request, arg=0, mutate_flag=True, /)

该函数与 fcntl() 逻辑相同,但对参数的缓冲处理"更加复杂"。request(ioctl 请求码)被限定在平台相关的 32 位或 64 位范围内;很多常用的 request 常量并不在 fcntl 模块而在 termios 模块中,名字与相关 C 头文件一致。源码将 request 解析为 unsigned_long(bitwise=True),正是为了承载 ioctl 请求码中按位编码的"方向+大小+编号"(参考 _IOR/_IOW/_IOWR 宏的编码习惯)。

mutate_flag 决定缓冲区的命运

arg 同样可以是整数、bytes-like 或字符串。区别在于 mutate_flag(默认 True):

  • arg 支持读写缓冲接口,或 mutate_flag 为假:行为与 fcntl() 相同——传入只读副本,返回一个 bytes(内容为系统调用后缓冲区中的数据)。
  • arg 支持读写缓冲接口(如 bytearray)且 mutate_flag 为真:缓冲区(相当于)直接传给底层 ioctl 系统调用,系统调用的返回值传给 Python,同时系统调用对缓冲区内容的修改会反映回原对象

文档中的"简化说明"(历史细节)指出:长度小于 1024 字节的缓冲区会先复制到 1024 字节的静态缓冲区再传给 ioctl,调用后拷回原缓冲区。从当前源码看,这一机制在 Modules/fcntlmodule.c 中已实现为:mutate_arg && !PyBytes_Check(arg) && !PyUnicode_Check(arg) 时先以 PyBUF_WRITABLE 请求可写缓冲,若对象确实是可变缓冲(如 bytearrayarray.array),≤1024 字节经栈缓冲中转、>1024 字节直接传原始指针,调用成功后 memcpy(view.buf, buf, len) 拷回;若对象是不可变的(如 bytes 或只读视图),则走与 fcntl() 一致的只读拷贝路径,返回新建的 bytes 对象。

3.14 起实现层面还有一个重要行为:系统调用期间始终释放 GILPy_BEGIN_ALLOW_THREADS/Py_END_ALLOW_THREADS),并且EINTR 失败会自动重试do...while (ret == -1 && errno == EINTR && !(async_err = PyErr_CheckSignals()))),重试间隙还会检查信号,异步信号触发时返回 NULL 而不是死循环。3.15 起未被修改的 bytes-like 对象同样取消了 1024 字节上限。

官方示例:用 ioctl 读取前台进程组

文档给出了 SVR4 兼容系统上的经典示例,分别用不可变字符串缓冲与可变的 array.array 缓冲读取终端 fd 0 的前台进程组:

>>> import array, fcntl, struct, termios, os
>>> os.getpgrp()
13341
>>> struct.unpack('h', fcntl.ioctl(0, termios.TIOCGPGRP, "  "))[0]
13341
>>> buf = array.array('h', [0])
>>> fcntl.ioctl(0, termios.TIOCGPGRP, buf, 1)
0
>>> buf
array('h', [13341])

解读:

  • 第一种写法传两个空格的字符串(UTF-8 编码后 2 字节,对应 C 的 pid_t),ioctl 会把内核写回的两字节内容作为 bytes 返回,再用 struct.unpack('h', ...) 解析出进程组号;
  • 第二种写法传入 array.array('h', [0]) 这种可写缓冲,并显式让 mutate_flag 为真,调用返回 0(ioctl 成功码),进程组号被直接写进 buf[0]

两种风格分别展示了"只读缓冲取回值"与"可写缓冲原地填充"的惯用法。注意 TIOCGPGRP 是一个读方向的请求码,若缓冲区太小放不下内核返回的数据,就会触发文档反复警告的段错误或数据损坏风险。

flock(fd, operation, /):简单易用的建议性文件锁

fcntl.flock(fd, operation, /)

在文件描述符 fd 上执行 flock(2) 风格的锁操作 operation。部分系统上该函数内部会用 fcntl 模拟实现(见源码中 #ifdef HAVE_FLOCK / #else 两个分支:无原生 flock 时,LOCK_UN/LOCK_SH/LOCK_EX 分别被映射为 F_UNLCK/F_RDLCK/F_WRLCK 并调用 F_SETLK/F_SETLKW)。失败时抛 OSError,成功时返回 None。它不触发任何缓冲区操作,因此没有 fcntl() 的段错误风险,非常适合"进程间互斥"这种粗粒度加锁。

测试 Lib/test/test_fcntl.py 中的 test_flock 展示了标准用法:在 'wb+' 打开的文件上依次 LOCK_SHLOCK_UNLOCK_EX | LOCK_NB,并验证向非法 fd 传 LOCK_SH 会抛 ValueError、向字符串传参抛 TypeError。Solaris 上获取共享锁要求文件可读,因此测试用 wb+ 打开以保证同时可读可写。

lockf(fd, cmd, len=0, start=0, whence=0, /):POSIX 记录锁(带偏移区间)

fcntl.lockf(fd, cmd, len=0, start=0, whence=0, /)

本质是 fcntl() 加锁调用的包装器,内部构建 struct flock 并通过 F_SETLK(配合 LOCK_NB)或 F_SETLKW 执行(Modules/fcntlmodule.cfcntl_lockf_impl),支持按字节区间锁定,适合"锁文件中某一段数据"的细粒度场景。

cmd 取值

常量 含义 对应 flock 类型字段
LOCK_UN 释放已有锁 F_UNLCK
LOCK_SH 获取共享锁(读锁,允许多个读者共存) F_RDLCK
LOCK_EX 获取排他锁(写锁) F_WRLCK
LOCK_NB 与上述三者按位或,使请求非阻塞 使内核走 F_SETLK 而非 F_SETLKW

要点:

  • 使用 LOCK_NB 且锁无法获取时抛 OSError,其 errno 属性为 EACCESEAGAIN(因系统而异,可移植写法是两种都检查);
  • 至少在部分系统上,LOCK_EX 只能用于以写方式打开的文件描述符;
  • 识别不出合法 cmd 时抛 ValueError("unrecognized lockf argument"),与 flock 的错误行为一致。

len / start / whence 区间语义

  • len:锁定字节数,默认 0 表示锁到文件末尾
  • start:锁起始字节偏移,相对 whence 而言,默认 0 即从文件开头起;
  • whence:与 io.IOBase.seek 一致——0 相对文件开头(os.SEEK_SET)、1 相对当前位置(os.SEEK_CUR)、2 相对文件末尾(os.SEEK_END),默认 0。

源码中还考虑了 64 位大文件支持(HAVE_LARGEFILE_SUPPORTstart/lenPyLong_AsLongLong 转换,并传给 struct flockl_start/l_len),因此可以锁定大于 2GB 的文件区间。

综合示例与移植性建议

文档在讲解完 lockf 后给出了 SVR4 系统上的两个示例,并点出关键差异:

import struct, fcntl, os

f = open(...)
rv = fcntl.fcntl(f, fcntl.F_SETFL, os.O_NDELAY)          # rv 是整数

lockdata = struct.pack('hhllhh', fcntl.F_WRLCK, 0, 0, 0, 0, 0)
rv = fcntl.fcntl(f, fcntl.F_SETLKW, lockdata)             # rv 是 bytes

注意:

  1. 第一个例子把 F_SETFLos.O_NDELAY(非阻塞)组合,返回整数;
  2. 第二个例子用 struct.pack 手工构造 struct flock 布局传给 F_SETLKW,成功后返回等长 bytes
  3. lockdata 的结构布局是系统相关的——32/64 位平台、不同 Unix 对 off_t/pid_t 的宽度与对齐要求不同。这正是测试 Lib/test/test_fcntl.pyget_lockdata() 要为 NetBSD/FreeBSD/OpenBSD、GNU/kFreeBSD、HP-UX/unixware7 分别用不同 struct 格式串的原因。因此文档明确建议:纯加锁需求优先用 flock,可免去手工拼装 struct flock 的平台差异。

与 os 模块锁接口的取舍

文档的 seealso 部分指出:若 os 模块中存在 O_SHLOCKO_EXLOCK 标志(仅 BSD 系),os.open() 也提供了 lockf/flock 之外的替代——在打开文件的同时直接获取共享/排他锁,省去"打开后再加锁"两步间的竞态窗口。

实操建议:编写可移植的 fcntl 代码

综合官方文档与 CPython 实现,编写 fcntl 代码时建议遵循以下清单:

  1. 先探测后使用:平台相关常量未必存在,用 hasattr(fcntl, 'F_GETPATH')hasattr(fcntl, 'F_OFD_SETLK') 等做特性探测;Linux 特有功能还要核对内核版本(如 reflink 需 ≥ 4.5、F_DUPFD_QUERY 需 ≥ 6.1)。
  2. 按返回类型分支:传整数 arg 得到整数返回值;传 bytes-like/字符串得到 bytes 返回值,且长度与入参相等。
  3. 缓冲区宁大勿小:ioctl/fcntl 的写回缓冲区大小不足会直接造成内存破坏,Python 只能用哨兵在调用之后发现栈上溢出并抛 SystemError,无法阻止越界写本身。
  4. 优先高层的 flock:锁文件整体用 flock,避免 struct flock 的平台布局问题;需要区间锁再用 lockf
  5. 非阻塞锁记得吞掉 EAGAIN/EACCESLOCK_NB 加锁失败是正常业务分支而非崩溃,按 e.errno in (errno.EACCES, errno.EAGAIN) 处理。
  6. 配合 GIL 释放理解并发:3.14 起系统调用期间不持 GIL 且自动重试 EINTR,长阻塞的加锁/ioctl 不会再拖住其它 Python 线程,但调用本身是同步的,仍建议放入线程或使用 LOCK_NB 轮询避免阻塞事件循环。

如需深入实现细节,可继续研读 Modules/fcntlmodule.c(函数实现、缓冲区哨兵、常量导出与模块多解释器/GIL 槽位)与 Lib/test/test_fcntl.py(跨平台 struct flock 布局、flock/lockf/F_GETPATH/管道大小等测试用例)。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389