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 中,分别为:
#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 中指定本条打印信息的级别,一般格式如下:
printk(KERN_INFO "Message: %s\n", arg);直接在格式字符串前指定打印等级即可。
2. 控制台级别
2.1 有哪些?
上边提到了控制台级别,控制台级别定义在哪?它们定义在 printk.h - include/linux/printk.h 文件中:
/* 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 中):
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 系统中通过以下命令查看:
cat /proc/sys/kernel/printk如上图,从左到右依次对应当前控制台日志级别、默认消息日志级别、 最小的控制台级别、默认控制台日志级别。
- 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 怎么控制?
我们直接在终端输入以下指令即可:
echo 8 4 1 7 > /proc/sys/kernel/printk
# 也可以用下边的命令
dmesg -n 5 # 这种只能修改 控制台日志级别 也就是 console_printk [0]中间的数字分别就代表各个等级,可以直接这样修改。例如:

三、使用实例
1. 打印等级实例
代码可以看这里:01_module_load/printk_eg。操作的时候主要是看加载驱动的时候的打印信息,主要是以下信息:
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);然后通过下边的命令修改各个默认值来看内核日志打印情况:
echo 8 4 1 7 > /proc/sys/kernel/printk
echo 0 4 1 7 > /proc/sys/kernel/printk2. 打印出 git 版本和编译时间
在内核模块中不支持 __DATE__ 选项,另外对于内核源码外编译的内核模块而言,我们不是很容易在执行 make 的时候为编译器添加-D 选项,至少我在学习到这里的时候没有发现什么好办法,然后我去参考了 uboot,发现 uboot 是生成了两个 .h 头文件,于是仿照它来进行,具体的实现过程可以看这里:LV120-编译时间与版本解析 | Linux
使用实例可以看这里:03_module_basic/03_printk_prj_info。实现 makefile 后,我们就可以包含对应的头文件,打印编译时间和 git 版本信息了。
2.1 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
endif2.2 Kbuild.include
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