本文主要是内核模块——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官方提供)
点击查看本文参考资料
点击查看相关文件下载
一、概述 大部分常用的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" #define KERN_ALERT KERN_SOH "1" #define KERN_CRIT KERN_SOH "2" #define KERN_ERR KERN_SOH "3" #define KERN_WARNING KERN_SOH "4" #define KERN_NOTICE KERN_SOH "5" #define KERN_INFO KERN_SOH "6" #define KERN_DEBUG KERN_SOH "7"
所有的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 #define MESSAGE_LOGLEVEL_DEFAULT CONFIG_MESSAGE_LOGLEVEL_DEFAULT #define CONSOLE_LOGLEVEL_SILENT 0 #define CONSOLE_LOGLEVEL_MIN 1 #define CONSOLE_LOGLEVEL_QUIET 4 #define CONSOLE_LOGLEVEL_DEFAULT 7 #define CONSOLE_LOGLEVEL_DEBUG 10 #define CONSOLE_LOGLEVEL_MOTORMOUTH 15 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, MESSAGE_LOGLEVEL_DEFAULT, CONSOLE_LOGLEVEL_MIN, CONSOLE_LOGLEVEL_DEFAULT, };
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
如上图,从左到右依次对应当前控制台日志级别、默认消息日志级别、 最小的控制台级别、默认控制台日志级别。
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]
中间的数字分别就代表各个等级,可以直接这样修改。例如:
三、使用实例 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 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) ) TFTP_SERVER ?= ~/3tftp NFS_SERVER ?= ~/4nfs TFTP_DIR ?= $(TFTP_SERVER) ROOTFS_ROOT_DIR ?= $(NFS_SERVER) /imx6ull_rootfs 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 srctreeall: $(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) 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}" +' LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +' LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +' LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +' LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +' else \ return 42; \ fi; \ else \ LC_ALL=C date +' LC_ALL=C date +' LC_ALL=C date +' LC_ALL=C date +' LC_ALL=C date +' fi) endef define filechk_version_autogenerated.h (echo \ echo \ echo \ echo \ 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