LV06-02-内核模块-06-printk

本文主要是内核模块——printk函数的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
https://elixir.bootlin.com/u-boot/latest/source uboot源码

一、概述

大部分常用的C库函数在Linux内核中都已经得到了实现。在所有没有实现的函数中,最著名的就数printf()函数了。内核代码虽然无法调用 printf()函数,但它可以调用printk()函数。printk()函数负责把格式化好的字符串拷贝到内核日志缓冲上,这样syslog程序就可 以通过读取该缓冲区来获取内核信息。

printk()函数是直接使用了向终端写函数tty_write()。而printf()函数是调用write()系统调用函数向标准输出设备写。所以 在用户态(如进程0)不能够直接使用printk()函数,而在内核态由于他已是特权级,所以无需系统调用来改变特权级,因而能够直接使用 printk()函数。printf是使用了标准的C库函数的时候才能使用的,而内核中无法使用标准的C库函数,所以就连最常见的printf都不能使用。

二、两个级别

1. 日志级别

1.1 有哪些?

printk相比printf来说还多了个日志级别的设置,用来控制printk打印的这条信息是否在终端上显示的,当日志级别的数值小于控制台级别时,printk要打印的信息才会在控制台打印出来,否则不会显示在控制台。在我们内核中一共有8种级别(数字越小级别越高),他们定义在 kern_levels.h - include/linux/kern_levels.h 中,分别为:

1
2
3
4
5
6
7
8
#define KERN_EMERG      KERN_SOH "0"    /* system is unusable */
#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */
#define KERN_CRIT KERN_SOH "2" /* critical conditions */
#define KERN_ERR KERN_SOH "3" /* error conditions */
#define KERN_WARNING KERN_SOH "4" /* warning conditions */
#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */
#define KERN_INFO KERN_SOH "6" /* informational */
#define KERN_DEBUG KERN_SOH "7" /* debug-level messages */

所有的printk()消息都会被打印到内核日志缓冲区,这是一个通过/dev/kmsg输出到用户空间的环 形缓冲区。读取它的通常方法是使用 dmesg 。日志级别指定了一条消息的重要性。内核根据日志级别和当前 console_loglevel (一个内核变量)决定是否立即显示消息(将其打印到当前控制台)。如果消息的优先级比 console_loglevel 高(日志级 别值较低),消息将被打印到控制台。如果省略了日志级别,则以 KERN_DEFAULT 级别打印消息。格式字符串虽然与C99基本兼容,但并不遵循完全相同的规范。它有一些扩展和一些限制(没 有 %n 或浮点转换指定符)。

1.2 怎么控制?

我们可以直接在printk中指定本条打印信息的级别,一般格式如下:

1
printk(KERN_INFO "Message: %s\n", arg);

直接在格式字符串前指定打印等级即可。

2. 控制台级别

2.1 有哪些?

上边提到了控制台级别,控制台级别定义在哪?它们定义在 printk.h - include/linux/printk.h 文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* printk's without a loglevel use this.. */
#define MESSAGE_LOGLEVEL_DEFAULT CONFIG_MESSAGE_LOGLEVEL_DEFAULT

/* We show everything that is MORE important than this.. */
#define CONSOLE_LOGLEVEL_SILENT 0 /* Mum's the word */
#define CONSOLE_LOGLEVEL_MIN 1 /* Minimum loglevel we let people use */
#define CONSOLE_LOGLEVEL_QUIET 4 /* Shhh ..., when booted with "quiet" */
#define CONSOLE_LOGLEVEL_DEFAULT 7 /* anything MORE serious than KERN_DEBUG */
#define CONSOLE_LOGLEVEL_DEBUG 10 /* issue debug messages */
#define CONSOLE_LOGLEVEL_MOTORMOUTH 15 /* You can't shut this one up */

extern int console_printk[];

#define console_loglevel (console_printk[0])
#define default_message_loglevel (console_printk[1])
#define minimum_console_loglevel (console_printk[2])
#define default_console_loglevel (console_printk[3])

我们看一下console_printk这个数组(定义在printk.c - kernel/printk/printk.c中):

1
2
3
4
5
6
int console_printk[4] = {
CONSOLE_LOGLEVEL_DEFAULT, /* console_loglevel */
MESSAGE_LOGLEVEL_DEFAULT, /* default_message_loglevel */
CONSOLE_LOGLEVEL_MIN, /* minimum_console_loglevel */
CONSOLE_LOGLEVEL_DEFAULT, /* default_console_loglevel */
};
  • console_printk[0]:CONSOLE_LOGLEVEL_DEFAULT,控制台日志级别,优先级高于该值的消息将在控制台显示(也就是终端)。
  • console_printk[1]:MESSAGE_LOGLEVEL_DEFAULT,默认消息日志级别,printk没定义优先级时,打印这个优先级以上的消息。
  • console_printk[2]:CONSOLE_LOGLEVEL_MIN,最小控制台日志级别,控制台日志级别可被设置的最小值(最高优先级
  • console_printk[3]:CONSOLE_LOGLEVEL_DEFAULT,默认的控制台日志级别。

我们可以在linux系统中通过以下命令查看:

1
cat /proc/sys/kernel/printk
image-20241120231849696

如上图,从左到右依次对应当前控制台日志级别、默认消息日志级别、 最小的控制台级别、默认控制台日志级别。

  • console_printk[0]:为CONSOLE_LOGLEVEL_DEFAULT:默认为7,所有优先级高于7的log等级(0~6),都会打印在终端上。
  • console_printk[1]:为MESSAGE_LOGLEVEL_DEFAULT:默认为4,printk打印消息时的默认等级。
  • console_printk[2]:为CONSOLE_LOGLEVEL_MIN:控制台日志级别可被设置的最小值(最高优先级),这里默认为1。
  • console_printk[3]:为CONSOLE_LOGLEVEL_DEFAULT:默认的控制台日志级别,默认为7。

打印内核所有打印信息:dmesg,注意内核log缓冲区大小有限制,缓冲区数据可能被覆盖掉。

2.2 怎么控制?

我们直接在终端输入以下指令即可:

1
2
3
echo 8 4 1 7 > /proc/sys/kernel/printk
# 也可以用下边的命令
dmesg -n 5 # 这种只能修改 控制台日志级别 也就是 console_printk[0]

中间的数字分别就代表各个等级,可以直接这样修改。例如:

image-20241120231958158

三、使用实例

1. 打印等级实例

代码可以看这里:01_module_load/printk_eg · sumumm/imx6ull-linuxdriver-eg - 码云 - 开源中国 (gitee.com)。操作的时候主要是看加载驱动的时候的打印信息,主要是以下信息:

1
2
3
4
5
6
7
8
printk(KERN_EMERG"KERN_EMERG:%s\r\n", KERN_EMERG);
printk(KERN_ALERT"KERN_ALERT:%s\r\n", KERN_ALERT);
printk(KERN_CRIT"KERN_CRIT:%s\r\n", KERN_CRIT);
printk(KERN_ERR"KERN_ERR:%s\r\n", KERN_ERR);
printk(KERN_WARNING"KERN_WARNING:%s\r\n", KERN_WARNING);
printk(KERN_NOTICE"KERN_NOTICE:%s\r\n", KERN_NOTICE);
printk(KERN_INFO"KERN_INFO:%s\r\n", KERN_INFO);
printk(KERN_DEBUG"KERN_DEBUG:%s\r\n", KERN_DEBUG);

然后通过下边的命令修改各个默认值来看内核日志打印情况:

1
2
echo 8 4 1 7 > /proc/sys/kernel/printk
echo 0 4 1 7 > /proc/sys/kernel/printk

2. 打印出git版本和编译时间

在内核模块中不支持__DATE__选项,另外对于内核源码外编译的内核模块而言,我们不是很容易在执行make的时候为编译器添加-D选项,至少我在学习到这里的时候没有发现什么好办法,然后我去参考了uboot,发现uboot是生成了两个.h头文件,于是仿照它来进行,具体的实现过程可以看这里:LV05-02-U-Boot-07-uboot编译时间与版本 | 苏木

使用实例可以看这里:imx6ull-driver-demo: i.mx6ull驱动开发demo - Gitee.com。实现makefile后,我们就可以包含对应的头文件,打印编译时间和git版本信息了。

2.1 makefile文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# 模块名和模块测试APP名称
MODULE_NAME := printk_prj_info

ARCH ?= arm
MAKE_PARAM :=
CURRENT_PATH := $(shell pwd)
KERNEL_KO_RELEASE = $(shell git rev-parse --verify --short HEAD)
ifeq ("$(origin V)", "command line")
KBUILD_VERBOSE = $(V)
endif
ifndef KBUILD_VERBOSE
KBUILD_VERBOSE = 0
endif

ifeq ($(KBUILD_VERBOSE),1)
quiet =
Q =
else
quiet=quiet_
Q = @
endif

#=====================================================
INCDIRS := ./
SRCDIRS := ./

INCLUDE := $(patsubst %, -I %, $(INCDIRS))
#=====================================================
# NFS 共享目录
TFTP_SERVER ?= ~/3tftp
NFS_SERVER ?= ~/4nfs

TFTP_DIR ?= $(TFTP_SERVER)
ROOTFS_ROOT_DIR ?= $(NFS_SERVER)/imx6ull_rootfs
#ROOTFS_MODULE_DIR ?= $(ROOTFS_ROOT_DIR)/lib/modules/4.19.71-00007-g51f3cd8ec-dirty
ROOTFS_MODULE_DIR ?= $(ROOTFS_ROOT_DIR)/drivers_demo
#=====================================================
ifeq ($(KERNELRELEASE),)

# 选择可执行文件运行的平台
ifeq ($(ARCH), arm)
KERNELDIR ?= ~/7Linux/imx6ull-kernel
MAKE_PARAM += ARCH=arm
MAKE_PARAM += CROSS_COMPILE=arm-linux-gnueabihf-
CROSS_COMPILE_PREFIX ?= arm-linux-gnueabihf-
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
CROSS_COMPILE_PREFIX ?=
endif

CC := $(CROSS_COMPILE_PREFIX)gcc
LD := $(CROSS_COMPILE_PREFIX)ld

include Kbuild.include

srctree := .
timestamp_h := timestamp_autogenerated.h
version_h := version_autogenerated.h

export CC LD srctree

# 编译模块和测试程序
all: $(timestamp_h) $(version_h) modules

modules:
$(MAKE) $(MAKE_PARAM) -C $(KERNELDIR) M=$(CURRENT_PATH) modules $(INCLUDE)

modules_install:
$(MAKE) $(MAKE_PARAM) -C $(KERNELDIR) M=$(CURRENT_PATH) modules INSTALL_MOD_PATH=$(ROOTFS_MODULE_DIR) modules_install

$(timestamp_h): $(srctree)/Makefile FORCE
$(call filechk,timestamp_autogenerated.h)

$(version_h): $(srctree)/Makefile FORCE
$(call filechk,version_autogenerated.h)

# 拷贝相关文件到nfs共享目录
install:
@sudo cp -v $(MODULE_NAME).ko $(ROOTFS_MODULE_DIR)

uninstall:
@sudo rm -vf $(ROOTFS_MODULE_DIR)/$(MODULE_NAME).ko

PHONY += FORCE
FORCE:

PHONY += clean
clean:
rm -rf *.o *.ko *.o.d
rm -rf .*.cmd *.mod.* *.mod modules.order Module.symvers .tmp_versions .cache.mk
rm -rf $(timestamp_h)
rm -rf $(version_h)

.PHONY: $(PHONY)

help:
@echo "\033[1;32m================================ Help ================================\033[0m"
@echo "Ubuntu may need to add sudo:"
@echo "insmod <module_name>.ko # Load module"
@echo "rmmod <module_name> # Uninstall the module"
@echo "dmesg -C # Clear the kernel print information"
@echo "lsmod # Check the kernel modules that have been inserted"
@echo "dmesg # View information printed by the kernel"
@echo "file <module_name>.ko # View \".Ko\" file information"
@echo ""
@echo "make ARCH=x86_64 # x86_64 platform"
@echo "make # arm platform"
@echo "\033[1;32m======================================================================\033[0m"

print:
@echo "KERNELDIR = $(KERNELDIR)"
@echo "INCLUDE = $(INCLUDE)"
else
CONFIG_MODULE_SIG = n
obj-m += $(MODULE_NAME).o
endif

2.2 Kbuild.include

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
define filechk_timestamp_autogenerated.h
(if test -n "$${SOURCE_DATE_EPOCH}"; then \
SOURCE_DATE="@$${SOURCE_DATE_EPOCH}"; \
DATE=""; \
for date in gdate date.gnu date; do \
$${date} -u -d "$${SOURCE_DATE}" >/dev/null 2>&1 && DATE="$${date}"; \
done; \
if test -n "$${DATE}"; then \
LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define KERNEL_KO_DATE "%b %d %C%y"'; \
LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define KERNEL_KO_TIME "%T"'; \
LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define KERNEL_KO_TZ "%z"'; \
LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define KERNEL_KO_DMI_DATE "%m/%d/%Y"'; \
LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define KERNEL_KO_BUILD_DATE 0x%Y%m%d'; \
else \
return 42; \
fi; \
else \
LC_ALL=C date +'#define KERNEL_KO_DATE "%b %d %C%y"'; \
LC_ALL=C date +'#define KERNEL_KO_TIME "%T"'; \
LC_ALL=C date +'#define KERNEL_KO_TZ "%z"'; \
LC_ALL=C date +'#define KERNEL_KO_DMI_DATE "%m/%d/%Y"'; \
LC_ALL=C date +'#define KERNEL_KO_BUILD_DATE 0x%Y%m%d'; \
fi)
endef

define filechk_version_autogenerated.h
(echo \#define PLAIN_VERSION \"$(KERNEL_KO_RELEASE)\"; \
echo \#define KERNEL_KO_VERSION \"\" PLAIN_VERSION; \
echo \#define CC_VERSION_STRING \"$$(LC_ALL=C $(CC) --version | head -n 1)\"; \
echo \#define LD_VERSION_STRING \"$$(LC_ALL=C $(LD) --version | head -n 1)\"; )
endef

define filechk
$(Q)set -e; \
: ' CHK $@'; \
mkdir -p $(dir $@); \
$(filechk_$(1)) < $< > $@.tmp; \
if [ -r $@ ] && cmp -s $@ $@.tmp; then \
rm -f $@.tmp; \
else \
: ' UPD $@'; \
mv -f $@.tmp $@; \
fi
endef