内核构建 Caching/Nondeterminism
Kernel Build Caching/Nondeterminism
我 运行 一个 CI 服务器,我用它来构建自定义 linux 内核。 CI 服务器功能不强大,每次构建的时间限制为 3 小时。为了在这个限制内工作,我想到了使用 ccache 来缓存内核构建。我希望我可以在每个次要版本发布后创建一个缓存,并将其重新用于补丁发布,例如我有一个为 4.18 制作的缓存,我想将其用于所有 4.18.x 内核。
删除构建时间戳后,这非常适合我正在构建的确切内核版本。对于上面引用的 4.18 内核,在 CI 上构建它会给出以下统计信息:
$ ccache -s
cache directory
primary config
secondary config (readonly) /etc/ccache.conf
stats zero time Thu Aug 16 14:36:22 2018
cache hit (direct) 17812
cache hit (preprocessed) 38
cache miss 0
cache hit rate 100.00 %
called for link 3
called for preprocessing 29039
unsupported code directive 4
no input file 2207
cleanups performed 0
files in cache 53652
cache size 1.4 GB
max cache size 5.0 GB
100% 的缓存命中率和 1 小时完成构建,出色的统计数据,符合预期。
不幸的是,当我尝试构建 4.18.1 时,我得到
cache directory
primary config
secondary config (readonly) /etc/ccache.conf
stats zero time Thu Aug 16 10:36:22 2018
cache hit (direct) 0
cache hit (preprocessed) 233
cache miss 17658
cache hit rate 1.30 %
called for link 3
called for preprocessing 29039
unsupported code directive 4
no input file 2207
cleanups performed 0
files in cache 90418
cache size 2.4 GB
max cache size 5.0 GB
这是 1.30% 的命中率,构建时间反映了这种糟糕的性能。即仅从单个补丁版本更改。
我原以为缓存性能会随着时间的推移而下降,但不会下降到这种程度,所以我唯一的想法是,除了时间戳之外,还有更多的不确定性。例如,most/all 的源文件是否包括完整的内核版本字符串?我的理解是,类似的东西会完全破坏缓存。有没有办法让缓存按照我的意愿工作,或者这不可能?
有include/generated/uapi/linux/version.h
header(在顶层Makefile中生成https://elixir.bootlin.com/linux/v4.16.18/source/Makefile)
其中包含确切的内核版本作为宏:
version_h := include/generated/uapi/linux/version.h
old_version_h := include/linux/version.h
define filechk_version.h
(echo \#define LINUX_VERSION_CODE $(shell \
expr $(VERSION) \* 65536 + 0$(PATCHLEVEL) \* 256 + 0$(SUBLEVEL)); \
echo '#define KERNEL_VERSION(a,b,c) (((a) << 16) + ((b) << 8) + (c))';)
endef
$(version_h): $(srctree)/Makefile FORCE
$(call filechk,version.h)
$(Q)rm -f $(old_version_h)
因此,linux 4.16.18 的 version.h 将生成为(266258 是 (4 << 16) + (16 << 8) + 18 = 0x41012)
#define LINUX_VERSION_CODE 266258
#define KERNEL_VERSION(a,b,c) (((a) << 16) + ((b) << 8) + (c))
稍后,例如在模块构建中应该有读取LINUX_VERSION_CODE宏值https://www.tldp.org/LDP/lkmpg/2.4/html/lkmpg.html的方法(4.1.6. 为多个内核版本编写模块)
The way to do this to compare the macro LINUX_VERSION_CODE
to the macro KERNEL_VERSION
. In version a.b.c
of the kernel, the value of this macro would be 2^{16}a+2^{8}b+c
. Be aware that this macro is not defined for kernel 2.0.35 and earlier, so if you want to write modules that support really old kernels
如何包含version.h?示例模块包括 <linux/kernel.h>
<linux/module.h>
和 <linux/modversions.h>
,这些文件之一 可能 间接包含全局 version.h
。大多数甚至所有内核源代码都将包含 version.h.
比较构建时间戳时,version.h 可能会重新生成并禁用 ccache。当忽略时间戳时,LINUX_VERSION_CODE
仅对完全相同的 linux 内核版本相同,并在下一个补丁级别更改。
更新:检查gcc -H
一些内核object编译的输出,将有另一个header具有完整内核版本宏定义。例如:include/generated/utsrelease.h
(UTS_RELEASE
宏),include/generated/autoconf.h
(CONFIG_VERSION_SIGNATURE
)。
或者甚至在两个补丁级别之间对同一内核进行 gcc -E
预处理 object 编译并比较生成的文本。使用最简单的 linux 模块,我直接在 gcc 命令行中有 -include ./include/linux/kconfig.h
,它包含 include/generated/autoconf.h
(但这在 -H
输出中不可见,是 gcc 的错误还是特性?)。
https://patchwork.kernel.org/patch/9326051/
... because the top Makefile forces to include it with:
-include $(srctree)/include/linux/kconfig.h
它实际上是:https://elixir.bootlin.com/linux/v4.16.18/source/Makefile
# Use USERINCLUDE when you must reference the UAPI directories only.
USERINCLUDE := \
-I$(srctree)/arch/$(SRCARCH)/include/uapi \
-I$(objtree)/arch/$(SRCARCH)/include/generated/uapi \
-I$(srctree)/include/uapi \
-I$(objtree)/include/generated/uapi \
-include $(srctree)/include/linux/kconfig.h
# Use LINUXINCLUDE when you must reference the include/ directory.
# Needed to be compatible with the O= option
LINUXINCLUDE := \
-I$(srctree)/arch/$(SRCARCH)/include \
-I$(objtree)/arch/$(SRCARCH)/include/generated \
$(if $(KBUILD_SRC), -I$(srctree)/include) \
-I$(objtree)/include \
$(USERINCLUDE)
LINUXINCLUDE 导出到 env 并在 source/scripts/Makefile.lib
中用于定义编译器标志 https://elixir.bootlin.com/linux/v4.16.18/source/scripts/Makefile.lib
c_flags = -Wp,-MD,$(depfile) $(NOSTDINC_FLAGS) $(LINUXINCLUDE)
我 运行 一个 CI 服务器,我用它来构建自定义 linux 内核。 CI 服务器功能不强大,每次构建的时间限制为 3 小时。为了在这个限制内工作,我想到了使用 ccache 来缓存内核构建。我希望我可以在每个次要版本发布后创建一个缓存,并将其重新用于补丁发布,例如我有一个为 4.18 制作的缓存,我想将其用于所有 4.18.x 内核。
删除构建时间戳后,这非常适合我正在构建的确切内核版本。对于上面引用的 4.18 内核,在 CI 上构建它会给出以下统计信息:
$ ccache -s
cache directory
primary config
secondary config (readonly) /etc/ccache.conf
stats zero time Thu Aug 16 14:36:22 2018
cache hit (direct) 17812
cache hit (preprocessed) 38
cache miss 0
cache hit rate 100.00 %
called for link 3
called for preprocessing 29039
unsupported code directive 4
no input file 2207
cleanups performed 0
files in cache 53652
cache size 1.4 GB
max cache size 5.0 GB
100% 的缓存命中率和 1 小时完成构建,出色的统计数据,符合预期。
不幸的是,当我尝试构建 4.18.1 时,我得到
cache directory
primary config
secondary config (readonly) /etc/ccache.conf
stats zero time Thu Aug 16 10:36:22 2018
cache hit (direct) 0
cache hit (preprocessed) 233
cache miss 17658
cache hit rate 1.30 %
called for link 3
called for preprocessing 29039
unsupported code directive 4
no input file 2207
cleanups performed 0
files in cache 90418
cache size 2.4 GB
max cache size 5.0 GB
这是 1.30% 的命中率,构建时间反映了这种糟糕的性能。即仅从单个补丁版本更改。
我原以为缓存性能会随着时间的推移而下降,但不会下降到这种程度,所以我唯一的想法是,除了时间戳之外,还有更多的不确定性。例如,most/all 的源文件是否包括完整的内核版本字符串?我的理解是,类似的东西会完全破坏缓存。有没有办法让缓存按照我的意愿工作,或者这不可能?
有include/generated/uapi/linux/version.h
header(在顶层Makefile中生成https://elixir.bootlin.com/linux/v4.16.18/source/Makefile)
其中包含确切的内核版本作为宏:
version_h := include/generated/uapi/linux/version.h
old_version_h := include/linux/version.h
define filechk_version.h
(echo \#define LINUX_VERSION_CODE $(shell \
expr $(VERSION) \* 65536 + 0$(PATCHLEVEL) \* 256 + 0$(SUBLEVEL)); \
echo '#define KERNEL_VERSION(a,b,c) (((a) << 16) + ((b) << 8) + (c))';)
endef
$(version_h): $(srctree)/Makefile FORCE
$(call filechk,version.h)
$(Q)rm -f $(old_version_h)
因此,linux 4.16.18 的 version.h 将生成为(266258 是 (4 << 16) + (16 << 8) + 18 = 0x41012)
#define LINUX_VERSION_CODE 266258
#define KERNEL_VERSION(a,b,c) (((a) << 16) + ((b) << 8) + (c))
稍后,例如在模块构建中应该有读取LINUX_VERSION_CODE宏值https://www.tldp.org/LDP/lkmpg/2.4/html/lkmpg.html的方法(4.1.6. 为多个内核版本编写模块)
The way to do this to compare the macro
LINUX_VERSION_CODE
to the macroKERNEL_VERSION
. In versiona.b.c
of the kernel, the value of this macro would be2^{16}a+2^{8}b+c
. Be aware that this macro is not defined for kernel 2.0.35 and earlier, so if you want to write modules that support really old kernels
如何包含version.h?示例模块包括 <linux/kernel.h>
<linux/module.h>
和 <linux/modversions.h>
,这些文件之一 可能 间接包含全局 version.h
。大多数甚至所有内核源代码都将包含 version.h.
比较构建时间戳时,version.h 可能会重新生成并禁用 ccache。当忽略时间戳时,LINUX_VERSION_CODE
仅对完全相同的 linux 内核版本相同,并在下一个补丁级别更改。
更新:检查gcc -H
一些内核object编译的输出,将有另一个header具有完整内核版本宏定义。例如:include/generated/utsrelease.h
(UTS_RELEASE
宏),include/generated/autoconf.h
(CONFIG_VERSION_SIGNATURE
)。
或者甚至在两个补丁级别之间对同一内核进行 gcc -E
预处理 object 编译并比较生成的文本。使用最简单的 linux 模块,我直接在 gcc 命令行中有 -include ./include/linux/kconfig.h
,它包含 include/generated/autoconf.h
(但这在 -H
输出中不可见,是 gcc 的错误还是特性?)。
https://patchwork.kernel.org/patch/9326051/
... because the top Makefile forces to include it with:
-include $(srctree)/include/linux/kconfig.h
它实际上是:https://elixir.bootlin.com/linux/v4.16.18/source/Makefile
# Use USERINCLUDE when you must reference the UAPI directories only.
USERINCLUDE := \
-I$(srctree)/arch/$(SRCARCH)/include/uapi \
-I$(objtree)/arch/$(SRCARCH)/include/generated/uapi \
-I$(srctree)/include/uapi \
-I$(objtree)/include/generated/uapi \
-include $(srctree)/include/linux/kconfig.h
# Use LINUXINCLUDE when you must reference the include/ directory.
# Needed to be compatible with the O= option
LINUXINCLUDE := \
-I$(srctree)/arch/$(SRCARCH)/include \
-I$(objtree)/arch/$(SRCARCH)/include/generated \
$(if $(KBUILD_SRC), -I$(srctree)/include) \
-I$(objtree)/include \
$(USERINCLUDE)
LINUXINCLUDE 导出到 env 并在 source/scripts/Makefile.lib
中用于定义编译器标志 https://elixir.bootlin.com/linux/v4.16.18/source/scripts/Makefile.lib
c_flags = -Wp,-MD,$(depfile) $(NOSTDINC_FLAGS) $(LINUXINCLUDE)