Skip to content

LV055-printk简介

一、概述

大部分常用的 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 中,分别为:

c
#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 中指定本条打印信息的级别,一般格式如下:

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

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

2. 控制台级别

2.1 有哪些?

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

c
/* 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 中):

c
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 系统中通过以下命令查看:

shell
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 怎么控制?

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

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

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

image-20241120231958158

三、使用实例

1. 打印等级实例

代码可以看这里:01_module_load/printk_eg。操作的时候主要是看加载驱动的时候的打印信息,主要是以下信息:

c
  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);

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

shell
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 头文件,于是仿照它来进行,具体的实现过程可以看这里:LV120-编译时间与版本解析 | Linux

使用实例可以看这里:03_module_basic/03_printk_prj_info。实现 makefile 后,我们就可以包含对应的头文件,打印编译时间和 git 版本信息了。

2.1 makefile 文件

makefile
# 模块名和模块测试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

makefile
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