
上周在一台老旧的开发机上编译 liburing 时撞上了一个相当典型的报错fatal error: linux/time_types.h: No such file or directory。机器是 CentOS 7内核 3.10目标产物是给高内核版本设备用的 io_uring 用户态库。这个问题表面看只是缺一个头文件但背后牵扯到内核 UAPI 头文件机制、liburing 版本与内核版本的匹配关系甚至编译环境和运行环境的界限。如果没想清楚就乱补文件很容易陷入“补了 A 又缺 B、换了版本又缺特性”的连环坑里。这篇文章我按照实际排查顺序把根因、方案选型、完整操作步骤和后续容易踩的坑一次讲清楚。适合遇到同样编译报错的人直接照着操作也适合想理解 liburing 编译原理的读者当参考。1. 从一次编译报错说起liburing 和缺失的头文件1.1 这个报错发生在什么场景liburing 是 io_uring 的官方用户态封装库负责帮你绕开那些繁琐的系统调用参数和内存管理细节提供类似io_uring_queue_init、io_uring_submit这样的一层友好 API。很多高性能 IO 框架、存储引擎、数据库中间件都在用它比如 RocksDB 的某些版本、ceph 的异步模块等。编译 liburing 本身不算复杂官方仓库下载源码后执行make就能得到静态库和动态库。但在我这次场景里执行make后编译器直接抛出In file included from src/io_uring.c:36: src/include/liburing/io_uring.h:12:10: fatal error: linux/time_types.h: No such file or directory当时第一反应是“系统缺包”于是分别试了yum install kernel-headers和从包仓库装linux-libc-dev发现 CentOS 7 的默认源里这个头文件压根就不存在。这是因为linux/time_types.h是内核 5.6 才引入的 UAPI 头文件而 CentOS 7 的内核头文件停留在 3.10。换句话说你装的不是“缺一个文件”而是整个头文件版本落后了 liburing 要求的时代。1.2 为什么一个头文件能卡住整个构建很多人不理解一个小头文件而已为什么会导致整个编译失败这其实和 C/C 的头文件依赖机制有关。io_uring.h里声明了不少与时间相关的结构体和接口例如io_uring_prep_timeout、io_uring_prep_timeout_update这些接口需要用到struct __kernel_timespec。这个结构体为了让用户态和内核态保持一致的 ABI 布局被单独剥出来定义在linux/time_types.h中而不是直接放在linux/time.h里。更关键的是liburing 源码里用的是尖括号形式#include linux/time_types.h这意味着编译器只会去系统的头文件搜索路径里找。老系统的/usr/include/linux/目录下根本没有这个文件于是编译直接终止。它不像普通业务代码缺个头文件那样好糊弄因为它不是一个你能随便在项目里新建的同名文件——它依赖的内核类型定义必须和系统其他 UAPI 头文件保持兼容否则就算编译过了生成的二进制也可能埋着结构体布局不一致的雷。2. 根因拆解linux/time_types.h到底是谁要的2.1 内核 UAPI 头文件与 liburing 的依赖链要理解这个报错得先搞清楚 Linux 头文件的分层。内核源码里有include/uapi/目录专门存放向用户态暴露的接口定义编译安装后会被复制到系统的/usr/include/linux、/usr/include/asm等位置。发行版会把这些 UAPI 头文件打包成kernel-headersRHEL 系或linux-libc-devDebian 系。liburing 自己不是一个独立的系统库它需要依赖这些 UAPI 头文件来定义 io_uring 相关系统调用的数据结构。具体依赖链大致是liburing 源码 - src/include/liburing/io_uring.h - #include linux/time_types.h - 依赖 linux/types.h 中的 __kernel_time64_t其中linux/types.h老系统也有但linux/time_types.h只在内核 5.6 之后的 UAPI 头文件里出现。liburing 之所以在新版本中引入这个依赖是因为 io_uring 的 timeout 相关操作需要一种不随 32/64 位体系结构变化的固定时间结构体。这件事在内核 5.6 之前一直用struct timespec将就但它受 glibc 的__USE_TIME_BITS64这类宏影响容易在用户态和内核态之间产生不一致。所以才专门分离出一个linux/time_types.h统一放__kernel_timespec、__kernel_itimerspec、__kernel_old_timeval等类型。2.2 版本错位的核心矛盾现在问题的主线已经清晰了liburing 新版本期望内核头文件至少是 5.6 之后的版本而你的系统头文件还停留在上古时代。具体到版本关系上组件最低版本要求说明内核运行期5.1io_uring 首次引入内核推荐运行期5.4liburing 官方建议IO 性能与稳定性提升明显UAPI 头文件编译期5.6出现linux/time_types.hliburing 版本2.x2.x 起大量使用__kernel_timespec我这次在 CentOS 7 上编译 liburing 2.2系统自带的内核头文件是 3.10 版本两者跨度实在太大。这也是为什么网上很多教程说“编译 liburing 建议升级系统”的原因——单纯补一个time_types.h能解决编译但如果其他 UAPI 头文件也版本过低后面还会冒出linux/io_uring.h缺失、__kernel_rwf_t未定义之类的问题。2.3 必须确认的一个事实编译环境与运行环境是两回事先说一个很多人容易忽略的点编译 liburing 的机器内核版本和最终运行 liburing 的目标机器内核版本可以是不同的。很多 CI 机、编译服务器为了稳定性操作系统本身停留在老版本比如 CentOS 7、Ubuntu 16.04内核也从不升级。但它在编译产物可能是要拷贝到另一台新内核服务器上运行的。这种情况下编译机缺新内核头文件不代表目标机不能运行 io_uring。编译机和运行机各管各的编译机只负责生成二进制运行机才负责执行。所以我当时的目标很明确不动编译机内核只是通过补头文件的方式让编译环境具备产出兼容新内核库的能力。这一点想清楚了后面方案选择就非常清晰。3. 方案选型四种修复思路对比3.1 思路一补齐头文件推荐优先尝试既然编译环境缺少linux/time_types.h最直接的办法是把对应内核版本的头文件补到编译器的搜索路径中。这个方案的好处是不需要动系统内核不影响编译机稳定性不限制 liburing 版本新版本随便编译风险较低补一个文件基本能解决这一层报错。具体实现有两种子路径一是把文件放到系统/usr/include/linux/下需要 root且影响所有编译任务二是放到 liburing 项目的自定义 include 目录只影响这个项目的编译。我更推荐后者干净无副作用。3.2 思路二换用旧版 liburing如果你的目标环境本身内核版本就很低比如内核 5.4 以下那其实根本没必要用 liburing 2.x 系列旧版本同样能提供完整的 io_uring 封装。liburing 0.7 版本对内核头文件的要求低很多不需要linux/time_types.h在老系统上可以直接编译通过。这个方案的代价是把新版本的功能改进、bug 修复都放弃了比如某些新内核特性支持、内存分配优化等。如果你的需求只是“有个 liburing 能用”那版本降级是成本最低的如果后续想跟随社区更新迟早还是要回头处理头文件问题。建议根据实际需要判断不必盲目追求最新版。3.3 思路三升级系统内核头文件包对 RHEL/CentOS 系统尝试安装新版本kernel-headers对 Debian/Ubuntu尝试更新linux-libc-dev。比如 Ubuntu 18.04 可以通过 HWE 仓库拿到更高版本的内核头文件CentOS 7 则可能要借助 ELRepo 或其他第三方源。这个方案的问题是老发行版的依赖关系错综复杂直接升级核心头文件有可能影响系统里其他依赖旧头文件编译的软件。而且 CentOS 7 的官方源里 kernel-headers 最高就是 3.10想通过 yum 正常升级基本无望。如果你用的是较新的 Ubuntu/Debian 且有可用 backports 源这个方案倒是可以考虑成功率较高。3.4 思路四回退修改 liburing 源码不推荐网上有教程让你直接改io_uring.h把#include linux/time_types.h删掉或者把它改成系统老版本能用的#include linux/time.h。这个做法我强烈不建议。首先liburing 的头文件是一个统一的 API 层删掉一个 include 之后如果后续代码里真的用到了struct __kernel_timespec马上会报类型未定义。其次就算你临时定义一个结构体蒙混过关结构体布局和内核实际期望的 ABI 也未必一致这种隐蔽问题在运行时非常难排查。改源码是最后的手段只有当你对代码路径和 ABI 都有十足把握时才考虑否则别给自己埋雷。我把这四种方案整理成一张对比表方便你按自己的情况快速选型方案操作成本风险适用场景推荐度补齐头文件低低编译机系统旧但目标机内核新强烈推荐降级 liburing低中目标内核本来就低不需要新版特性看情况升级系统头文件包中中高系统较新可获取更高版本头文件看系统修改 liburing 源码中高几乎不推荐不推荐4. 实操记录一步步补全linux/time_types.h4.1 先确认真实报错与环境信息不要看到报错就直接搜解决方案先花一分钟把环境信息摸清楚。我当时依次执行了uname -r cat /etc/os-release ls /usr/include/linux/time_types.h make V1 21 | head -50输出显示内核版本是 3.10系统是 CentOS 7.9/usr/include/linux/time_types.h不存在编译命令中可以看到-I src/include等搜索路径。同时我还用gcc -H看了一下真的是哪个头文件引入了time_types.h确认源头后心里就有底了。这一步的意义在于你要修复的是“头文件缺失”而不是“编译命令错误”或“源码版本不兼容”方向不能搞错。如果报错信息里还伴随其他文件缺失那要先理清完整的依赖链再动手。4.2 获取正确的头文件内容获取linux/time_types.h最稳妥的方法是直接从内核官方仓库下载对应版本的文件。内核 UAPI 头文件在源码中的路径是include/uapi/linux/time_types.h。我选择下载 5.10 版本因为这个版本比较中庸既覆盖了 5.6 之后新增的内容又没有被后续频繁改动影响稳定性兼容性足够。mkdir -p extra_headers/linux wget -O extra_headers/linux/time_types.h \ https://raw.githubusercontent.com/torvalds/linux/v5.10/include/uapi/linux/time_types.h下载后建议先打开看一眼内容正常情况下文件内容大致是这样的/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */ #ifndef _UAPI_LINUX_TIME_TYPES_H #define _UAPI_LINUX_TIME_TYPES_H #include linux/types.h struct __kernel_timespec { __kernel_time64_t tv_sec; long long tv_nsec; }; struct __kernel_itimerspec { struct __kernel_timespec it_interval; struct __kernel_timespec it_value; }; struct __kernel_old_timeval { __kernel_time_t tv_sec; __kernel_suseconds_t tv_usec; }; struct __kernel_sock_timeval { __s64 tv_sec; __s64 tv_usec; }; #endif要特别留意第一行和最后一行必须有 include guard防止和其他头文件重复包含时出现重定义。另外linux/time_types.h依赖linux/types.h中的__kernel_time64_t、__kernel_suseconds_t等类型老系统如果这些类型定义不完整编译时会爆出第二个错误后面我会专门讲。如果你不方便直接访问 GitHub也可以从内核源码 tar 包里解压或者访问 elixir.bootlin.com 这类内核源码浏览器网站复制内容。注意不要在下载时混入非 UAPI 目录下的同名文件内核源码里还有include/linux/time_types.h那是内核内部头文件不能拿来做用户态编译。4.3 放置头文件的三种路径策略拿到文件后把它放哪里决定了影响范围。我给你梳理三种典型做法。第一种放到系统目录sudo cp extra_headers/linux/time_types.h /usr/include/linux/time_types.h这种方法一劳永逸之后编译任何依赖这个头文件的项目都不会报错。缺点是已经污染了系统环境如果系统里原来存在旧版本的这个文件概率较低可能覆盖成不兼容版本。第二种放到 liburing 源码目录下仅对该项目生效cd liburing mkdir -p extra_headers/linux cp time_types.h extra_headers/linux/ make CFLAGS-I$PWD/extra_headers CPPFLAGS-I$PWD/extra_headers因为io_uring.h里写的是#include linux/time_types.h编译器会按顺序在-I指定的目录中查找linux/time_types.h所以目录层级必须严格是extra_headers/linux/time_types.h。这种做法的好处是干净整个项目独立自洽删除extra_headers目录后系统环境不受任何影响。第三种放到交叉编译工具链的 sysroot 目录中。如果你不是本机编译而是用交叉工具链给 ARM 或其他平台编译那要找到工具链的 sysroot 路径arm-linux-gnueabihf-gcc -print-sysroot然后把头文件放到该路径下的usr/include/linux/time_types.h。这个方法和第一种类似作用于整个工具链比较适合固定目标平台的嵌入式项目。我当时用的是第二种理由是编译机和目标机环境分离我想尽量保持编译机的系统目录干净。如果你只是为了快速解决问题第一种也不是不行。4.4 重新编译与验证放置完成后执行清理和重新编译make clean make CFLAGS-I$PWD/extra_headers CPPFLAGS-I$PWD/extra_headers正常情况下编译会顺利通过并在src/目录下生成liburing.a和liburing.so。如果想额外验证头文件的正确性可以在项目目录下手动编译一个最简单的测试程序#include liburing.h int main() { struct io_uring ring; io_uring_queue_init(8, ring, 0); io_uring_queue_exit(ring); return 0; }用类似下面的命令编译gcc -I./src/include -I./extra_headers test.c -o test -L./src -luring如果这一步也能通过基本可以确信头文件补得没问题。这里要再提醒一次编译通过不代表运行也能通过如果当前内核版本低于 5.1运行这个测试程序会直接报io_uring_setup系统调用不存在。我是在目标机器上执行验证的编译机上的验证只到“能编译出正确的库”这个环节为止。5. 编译通过之后常见问题与避坑经验5.1 常见问题速查表在实际处理过程中除了linux/time_types.h缺失本身还会遇到几个关联问题。我把它们整理成表格排查时可以直接对照报错特征根本原因解决办法linux/time_types.h: No such file or directory内核头文件版本过低按上文补齐对应头文件linux/io_uring.h: No such file or directory缺少 io_uring 相关 UAPI 头文件从同版本内核源码复制io_uring.h或升级头文件包__kernel_time64_t未定义linux/types.h版本过旧确认包含linux/types.h必要时补全相关类型定义__kernel_timespec重复定义多个头文件定义了同名字结构体检查 include guard避免同时引入多个版本的time_types.hFunction not implemented或ENOSYS当前运行内核低于 5.1换到 5.4 内核环境运行或者只为交叉编译使用undefined reference to io_uring_queue_init链接时没指定库路径编译命令加-L./src -luring特别说一下__kernel_time64_t未定义这个问题。在老系统的linux/types.h中__kernel_time64_t可能没有定义需要你在自己补充的time_types.h中手动加一行兼容定义例如#ifndef __kernel_time64_t typedef long long __kernel_time64_t; #endif这个宏防护可以避免重复定义而且只在老系统上触发新系统上不影响。5.2 交叉编译时的额外注意事项交叉编译场景比本机编译多一层坑。因为工具链的 sysroot 里有一套独立的头文件树你补头文件不能补到编译机的/usr/include/linux必须补到 sysroot 里。而且交叉编译时make默认可能不会用你指定的交叉编译器你需要同时传递工具链相关变量make CROSS_COMPILEarm-linux-gnueabihf- \ CCarm-linux-gnueabihf-gcc \ CFLAGS-I$PWD/extra_headers \ CPPFLAGS-I$PWD/extra_headers另外目标平台的位数也要注意。time_types.h里的__kernel_timespec是 64 位对齐的如果目标平台是 32 位 ARM那long long的对齐规则和 64 位平台不同。下载头文件时最好确认目标平台对应的内核版本并在目标板上做一次冒烟测试。条件允许的话用 qemu-user 模拟运行最简单。5.3 关于运行时 io_uring 支持的真实情况我看到不少人在编译通过后直接在老内核机器上运行测试程序然后一脸懵地问我为什么运行报错。这里再强调一遍io_uring 要求内核 5.1 以上并且要确认内核配置里开启了CONFIG_IO_URING。很多云服务器、虚拟机的内核虽然版本号够新但发行版编译时可能裁剪了相关配置同样跑不起来。在目标机上可以用下面这条命令快速检查grep io_uring /proc/kallsyms | head -3如果没有任何输出说明当前内核根本没有加载 io_uring 相关符号。或者直接调用一个最简单的io_uring_setup系统调用用 strace 观察返回值。我在生产环境中一般要求内核版本不低于 5.4主要原因是 5.4 之后的 io_uring 在性能稳定性和特性完整度上才有保障。5.4 一点个人习惯与建议最后分享几个我自己的习惯。第一编译 require 新内核特性的库之前先花几十秒检查系统头文件版本能省掉后面的连环排查。第二尽量不要直接改系统/usr/include下的文件除非你很清楚自己在做什么。项目级补头文件的方式虽然看起来不够“彻底”但维护起来非常清爽——哪天不需要了直接一个rm -rf extra_headers就清理干净了。第三补头文件时优先从内核官方仓库下载而不是手写或从不明来源复制因为 UAPI 头文件对 ABI 一致性要求极高一个字段对不上生成的库可能在某些极端场景下出现诡异崩溃。我后来把extra_headers目录直接保留在 liburing 构建目录里并在 README 里写清楚了它的用途和来源版本。这样同事再遇到同样问题不用重新查一遍内核版本对应关系直接看文档就能解决。这个细节看起来不值一提但在一台多人共用的编译机上它真的能帮大家省下不少时间。