我们知道当Linux系统或者Android系统运行起来之后,可以通过cat /proc/version来获取对应的版本信息.如下所示:
xiehaocheng@xiehaocheng:~$ cat /proc/version
Linux version 3.13.0-19-generic (buildd@akateko) (gcc version 4.8.2 (Ubuntu 4.8.
2-17ubuntu1) ) #40-Ubuntu SMP Mon Mar 24 02:36:06 UTC 2014
那么我们今天就来探讨一下这个信息是怎么编译到内核中的。首先从这个接口来入手,顺藤摸瓜来看清楚具体是如何实现的。
/proc/version接口是在内核中的一个标准接口,它的实现在kernel-4.4/fs/proc/version.c中:
#define BUILDER_INFO_LEN (24)
static char simu_builder_info[BUILDER_INFO_LEN] = "NONE";
static char simu_linux_proc_banner[] =
"%s version %s"
" (%s)"
" (" LINUX_COMPILER ") %s\n";
static int version_proc_show(struct seq_file *m, void *v)
{
if (strncmp(simu_builder_info, "NONE", strlen("NONE")) == 0) {
seq_printf(m, linux_proc_banner,
utsname()->sysname,
utsname()->release,
utsname()->version);
} else {
seq_printf(m, simu_linux_proc_banner,
utsname()->sysname,
utsname()->release,
simu_builder_info,
utsname()->version);
}
return 0;
}
static int version_proc_open(struct inode *inode, struct file *file)
{
return single_open(file, version_proc_show, NULL);
}
static const struct file_operations version_proc_fops = {
.open = version_proc_open,
.read = seq_read,
.llseek = seq_lseek,
.release = single_release,
};
static int __init proc_version_init(void)
{
proc_create("version", 0, NULL, &version_proc_fops);
return 0;
}
fs_initcall(proc_version_init);
接口的实现也是比较简单的,主要就是从utsname这个结构体变量中获取对应的信息并输出,输出的格式在linux_proc_banner中。utsname它的全称应该叫做unix time share namespace,这个和各个进程的命名空间有关了,看下它的实现:
include/linux/utsname.h:
static inline struct new_utsname *utsname(void)
{
return ¤t->nsproxy->uts_ns->name;
}
static inline struct new_utsname *init_utsname(void)
{
return &init_uts_ns.name;
}
可以看到其实最终这个utsname就是当前current进程的uts namespace。对于kernel更老的版本实现中,utsname并不会按照进程做区分,而是所有进程共享同一个utsname,笔者也不是很清楚从哪个版本开始,utsname的实现是按照进程做了区分,我想这么做自然也有这么做的道理,比如可以把不同进程按照不同uts namespace进行隔离开。
如果没有修改过当前进程的utsname的话,默认utsname是和init_utsname保持一致的,因为init进程是所有进程的父进程,所有进程在初始化创建时都会直接从init进程中把utsname克隆过来。那么我们就跟踪到关键的数据结构了:init_uts_ns,它的定义如下:
kernel-4.4/init/version.c:
#include //UST_XXX的宏定义头文件
struct uts_namespace init_uts_ns = {
.kref = {
.refcount = ATOMIC_INIT(2),
},
.name = {
.sysname = UTS_SYSNAME,
.nodename = UTS_NODENAME,
.release = UTS_RELEASE,
.version = UTS_VERSION,
.machine = UTS_MACHINE,
.domainname = UTS_DOMAINNAME,
},
.user_ns = &init_user_ns,
.ns.inum = PROC_UTS_INIT_INO,
#ifdef CONFIG_UTS_NS
.ns.ops = &utsns_operations,
#endif
};
EXPORT_SYMBOL_GPL(init_uts_ns);
/* FIXED STRINGS! Don't touch! */
const char linux_banner[] =
"Linux version " UTS_RELEASE " (" LINUX_COMPILE_BY "@"
LINUX_COMPILE_HOST ") (" LINUX_COMPILER ") " UTS_VERSION "\n";
const char linux_proc_banner[] = //proc/version接口的数据打印格式
"%s version %s"
" (" LINUX_COMPILE_BY "@" LINUX_COMPILE_HOST ")"
" (" LINUX_COMPILER ") %s\n";
通过上面的梳理,我们可以知道当我们cat /proc/version时最终数据是取得:
UTS_SYSNAME/UTS_RELEASE/UTS_VERSION/LINUX_COMPILE_HOST/LINUX_COMPILER/LINUX_COMPILE_BY。
这些宏定义是在include/generated/compile.h中定义的:
/* This file is auto generated, version 171 */
/* SMP PREEMPT */
#define UTS_MACHINE "arm64"
#define UTS_VERSION "#171 SMP PREEMPT Mon Apr 23 14:30:26 CST 2018"
#define LINUX_COMPILE_BY "xiehaocheng"
#define LINUX_COMPILE_HOST "xiehaocheng"
#define LINUX_COMPILER "gcc version 4.9 20150123 (prerelease) (GCC) "
#define ANDROID_VERSION "NGI77B test-keys"
不过有一点需要注意,就是上述头文件并不是固定不变的,你从github上拉取的kernel代码是不包含此头文件的,从它所在的目录generated我们也能猜到这个头文件是编译过程中生成的。
我们下面进一步来看是怎么生成的。
kernel-4.4/init/Makefile:
$(obj)/version.o: include/generated/compile.h
include/generated/compile.h: FORCE
@$($(quiet)chk_compile.h)
$(Q)$(CONFIG_SHELL) $(srctree)/scripts/mkcompile_h $@ \ //mkcompile_h是一个shell脚本,用来生成compile.h头文件,这里执行它来生成对应的target
"$(UTS_MACHINE)" "$(CONFIG_SMP)" "$(CONFIG_PREEMPT)" "$(CC) $(KBUILD_CFLAGS)"
看一下脚本实现:
kernel-4.4/scripts/mkcompile_h:
#!/bin/sh
TARGET=$1 //第一个参数为生成的目标文件名称
ARCH=$2 //arch类型,UTS_MACHINE
SMP=$3 //SMP,CONFIG_SMP
PREEMPT=$4 //CONFIG_PREEMPT
CC=$5 //$(CC)
......
# Fix the language to get consistent output
LC_ALL=C
export LC_ALL
if [ -z "$KBUILD_BUILD_VERSION" ]; then
if [ -r .version ]; then
VERSION=`cat .version`
else
VERSION=0
echo 0 > .version
fi
else
VERSION=$KBUILD_BUILD_VERSION
fi
if [ -z "$KBUILD_BUILD_TIMESTAMP" ]; then
TIMESTAMP=`date`
else
TIMESTAMP=$KBUILD_BUILD_TIMESTAMP
fi
if test -z "$KBUILD_BUILD_USER"; then
LINUX_COMPILE_BY=$(whoami | sed 's/\\/\\\\/')
else
LINUX_COMPILE_BY=$KBUILD_BUILD_USER
fi
if test -z "$KBUILD_BUILD_HOST"; then
LINUX_COMPILE_HOST=`hostname`
else
LINUX_COMPILE_HOST=$KBUILD_BUILD_HOST
fi
UTS_VERSION="#$VERSION"
CONFIG_FLAGS=""
if [ -n "$SMP" ] ; then CONFIG_FLAGS="SMP"; fi
if [ -n "$PREEMPT" ] ; then CONFIG_FLAGS="$CONFIG_FLAGS PREEMPT"; fi
UTS_VERSION="$UTS_VERSION $CONFIG_FLAGS $TIMESTAMP"
# Truncate to maximum length
UTS_LEN=64
UTS_TRUNCATE="cut -b -$UTS_LEN"
# Generate a temporary compile.h //生成一个临时文件 .tmpcompile
( echo /\* This file is auto generated, version $VERSION \*/
if [ -n "$CONFIG_FLAGS" ] ; then echo "/* $CONFIG_FLAGS */"; fi
echo \#define UTS_MACHINE \"$ARCH\"
echo \#define UTS_VERSION \"`echo $UTS_VERSION | $UTS_TRUNCATE`\"
echo \#define LINUX_COMPILE_BY \"`echo $LINUX_COMPILE_BY | $UTS_TRUNCATE`\"
echo \#define LINUX_COMPILE_HOST \"`echo $LINUX_COMPILE_HOST | $UTS_TRUNCATE`\"
echo \#define LINUX_COMPILER \"`$CC -v 2>&1 | grep ' version '`\"
echo \#define ANDROID_VERSION \"`echo $BUILD_DISPLAY_ID`\"
) > .tmpcompile
......
if [ -r $TARGET ] && \
grep -v 'UTS_VERSION' $TARGET > .tmpver.1 && \
grep -v 'UTS_VERSION' .tmpcompile > .tmpver.2 && \
cmp -s .tmpver.1 .tmpver.2; then
rm -f .tmpcompile
else
vecho " UPD $TARGET"
mv -f .tmpcompile $TARGET //移动.tmpcompile为TARGET,也就是include/generated/compile.h
fi
rm -f .tmpver.1 .tmpver.2 //删除其余不需要的临时文件
综上所述,我们从入口函数一步一步跟踪,到后来窥探到整个的实现流程。