1.为什么使用内核模块
宏内核:简单来说,就是把很多东西都集成进内核,例如linux内核,除了最基本的进程、线程管理、内存管理外,文件系统,驱动,网络协议等等都在内核里面。优点是效率高。缺点是稳定性差,开发过程中的bug经常会导致整个系统挂掉。做驱动开发的应该经常有按电源键强行关机的经历。
微内核:内核中只有最基本的调度、内存管理。驱动、文件系统等都是用户态的守护进程去实现的。优点是超级稳定,驱动等的错误只会导致相应进程死掉,不会导致整个系统都崩溃,做驱动开发时,发现错误,只需要kill掉进程,修正后重启进程就行了,比较方便。缺点是效率低。
Linux是一个宏内核,也就是说,Linux内核运行在单独的内核地址空间。不过,Linux汲取了微内核的精华:其引以为豪的是模块化设计、抢占式内核、支持内核线程以及动态装载内核模块的能力。不仅如此,Linux还避免其微内核设计上性能损失的缺陷,让所有事情都运行在内核态,直接调用函数,无需消息传递。至今,Linux是模块化的、多线程的以及内核本身可调度的操作系统,实用主义再次占了上风。
同时内核模块的这一特点也有助于减小内核镜像文件的体积,自然也就减少了内核所占的内存空间(因为整个内核镜像将会被加载到内存中运行)。不必把所有的驱动都编译内核,而是以模块的形式单独编译驱动程序,这是基于不是所有的驱动都会同时工作原理。因为不是所有的硬件都要同时接入系统,比如一个USB无线网卡。讨论完内核模块的这些特性后,我们正式开始编写模块程序。
2.内核模块是什么?
驱动程序在内核中,都是独立的模块,例如led驱动、蜂鸣器驱动,它们驱动之间没有相互的联系,可以通过应用程序将两个驱动联系在一起,例如以下的代码,led驱动和蜂鸣器驱动各自都是一个独立的模块(module)。
int main(int argc, char **argv)
{
fd_led = open(“/dev/led_drv”,O_WRONLY);
fd_beep = open(“/dev/beep_drv”,O_WRONLY);
.......
}
内核模块编译成功之后,会输出*.ko(kernel object)文件。
加载内核模块到内核:insmod led_drv.ko
从内核卸载内核模块:rmmod led_drv
若查看当前的内核模块是否加载成功,使用lsmod命令。
注意:
驱动是安装在内存中正在运行的内核上。
3.设计一个简单的内核模块
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
//入口函数
static int __init imx6ull_led_init(void)
{
printk("imx6ull led init\n");
return 0;
}
//出口函数
static void __exit imx6ull_led_exit(void)
{
printk("imx6ull led exit\n");
}
//驱动程序的入口:insmod led_drv.ko调用module_init,module_init又会去调用 imx6ull_led_init。
module_init( imx6ull_led_init);
//驱动程序的出口:rmmod led_drv调用module_exit,module_exit又会去调用imx6ull_led_exit。
module_exit(imx6ull_led_exit)
//模块描述
MODULE_AUTHOR("xx@163.com"); //作者信息
MODULE_DESCRIPTION("imx6ull_led_exit led driver"); //模块功能说明
MODULE_LICENSE("GPL"); //许可证:驱动遵循GPL协议
linux内核是节约内存操作系统的典范,任何可能节约下来的内存操作都不会放过它。关于__init的初始化函数,往往只是被调用一次,在调用完成后,该函数不应该再被调用。所以它占用的内存必须得释放,只要该函数加上__init关键字就能够达到这个目的。
__exit也是用于修饰清除退出函数,和__init的效果也是一样,被调用之后,就会释放内存。
4.内核模块与应用程序区别
- 内核模块有入口也有出口,入口是由module_init指定,出口是由module_exit指定。应用程序只有入口,该入口为main函数。
- 内核模块运行在内核空间,因此得到很高的权限,但是稍有不当,直接导致内核崩溃。应用程序运行在用户空间,权限是比较低,不能够直接访问硬件,但是应用程序崩溃只影响到自己,不会影响到内核。
- 内核模块只能使用内核源码提供的函数(路径:内核源码/include),不能使用标准C的函数,例如只能使用printk来打印信息,不能使用printf来打印。
- 内核模块是一个独立的模块,各个模块之间没有直接的联系,符合软件工程的设计——高内聚低耦合。
- 编译的时候,使用Makefile,因为要指定内核源码路径、交叉编译器路径、内核模块路径等信息。
- 内核模块指定源码的版本(IMX6ULL内核源码)必须跟目标平台的内核版本(例如IMX6ULL开发板)必须一致,否则无法加载该驱动。
如何去查看目标平台的内核版本
root@ATK-IMX6U:~# uname -r
4.1.15-gb8ddbbc
- 虽然printk和printf函数非常相似,但是通常printk函数不支持浮点数,虽然能够编译成功,但是最终运行却得不到我们想要的结果。
static int __init imx6ull_led_init(void)
{
float pi=3.14;
float a=2.14;
printk("imx6ull led init,%f\n",pi/a);
return 0;
}
[root@ATK-IMX6U]#insmod led_drv.ko
[ 2731.460000] imx6ull led init,%f
- 整个内核空间的调用链上只有4KB或8KB的栈,相对于应用程序来说是非常小,如果需要大内存的空间,需要使用专门的函数进行动态分配——kmalloc、zmalloc、vmalloc。
5.Makefile
1.关于Makefile的编写,可以阅读Documentation/kbuild/modules.txt,包含很多编译内核模块的操作步骤:
......................
--- 2.1 Command Syntax
The command to build an external module is:
$ make -C <path_to_kernel_src> M=$PWD
......................
2.内容如下:
KERNELDIR := /home/gec/IMX6ULL
CURRENT_PATH := $(shell pwd)
obj-m :=led_drv.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
obj-m+=led_drv.o
make的第一阶段将源程序编译为目标文件led_drv.o
make的第二阶段将led_drv.o编译成一个模块,即led_drv.ko。
KERNELDIR :=/home/gec/IMX6ULL
内核源码路径:查找编译所需的头文件、函数原型、Makefile…
交叉编译工具,内核使用什么编译器版本进行编译,内核模块最好也是跟内核使用相同的编译工具(这里已提前设置,就不需要再指定)
PWD:=$(shell pwd)
当前内核模块源码路径
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
使用make命令的时候,传递多个参数,并调用内核源码下的Makefie文件,使用该Makefile文件中的工具,将led_drv.o文件编译为一个内核模块led_drv.ko。
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
相当于:rm *.o .order ..cmd *.mod.c *.symvers .tmp_versions -rf
删除过程文件及其他文件。
注:+=,?=, := 区别
= 是最基本的赋值
:= 是覆盖之前的值
?= 是如果没有被赋值过就赋予等号后面的值
+= 是添加等号后面的值
3.编译:
gec@ubuntu:~/1_chrdevbase$ make
make -C /home/gec/IMX6ULL M=/home/gec/1_chrdevbase modules
make[1]: Entering directory '/home/gec/IMX6ULL'
CC [M] /home/gec/1_chrdevbase/chrdevbase.o
Building modules, stage 2.
MODPOST 1 modules
CC /home/gec/1_chrdevbase/chrdevbase.mod.o
LD [M] /home/gec/1_chrdevbase/chrdevbase.ko
make[1]: Leaving directory '/home/gec/IMX6ULL'
4. 查看模块信息
使用modinfo命令,可以查看到模块详细信息。
root@ATK-IMX6U:/mnt# modinfo led.ko
filename: /mnt/led.ko
author: zuozhongkai
license: GPL
srcversion: 597E1DDC8A372707B8FD0DE
depends:
vermagic: 4.1.15 SMP preempt mod_unload modversions ARMv7 p2v8
注意:
通过modinfo命令也能够知道内核模块的内核源码版本——4.1.15,可以发现跟IMX6ULL开发板运行的内核版本是一致,只要是一致,该内核才能进行加载。
5. 演示操作
加载内核模块
root@ATK-IMX6U:/mnt# insmod led.ko
使用lsmod命令显示当前内核已经加载的内核模块,该命令等同于cat /proc/modules操作
root@ATK-IMX6U:/mnt# lsmod
Module Size Used by
led 1791 0
mx6s_capture 14876 0
ov5640_camera 19965 0
root@ATK-IMX6U:/mnt# cat /proc/modules
led 1791 0 - Live 0x7f011000 (O)
mx6s_capture 14876 0 - Live 0x7f009000
ov5640_camera 19965 0 - Live 0x7f000000
关键内容
第一列:模块的名称,led
第二列:模块占用内存的大小,1791
第三列:表示依赖该模块的个数,当前为0;若有一个程序调用会自加1;若有一个模块依赖该模块,也会自加1。
第四列:模块的运行状态,live。
第五列:模块的加载地址,0x7f011000
5.卸载的内核模块
root@ATK-IMX6U:/mnt# rmmod led.ko
注意:
如果加载内核模块或者卸载内核模块没有打印信息,有可能是printk函数的优先级是低于控制台的优先级无法进行打印,这个时候可以使用dmesg来检查打印信息,该打印信息是直接输出到日志文件。
6.printk函数
1.在驱动开发当中,我们不能使用printf函数,只能使用printk函数,使用方法会跟printf函数相像,但是也有点不同。
参考内核的一些驱动源码
#include <linux/kernel.h>
使用方法:
printk(KERN_INFO "## delay: %d\n", readl(base_addr + S3C_ADCDLY));
printk(KERN_WARNING" %d is already reserved for TouchScreen\n",
adc_port);
printk(KERN_ERR "cannot register miscdev on minor=%d (%d)\n",
ADC_MINOR, ret);
2.printk函数打印优先级的宏定义,在<linux/printk.h>当中能够找到
#define KERN_EMERG "<0>" /* system is unusable */
#define KERN_ALERT "<1>" /* action must be taken immediately */
#define KERN_CRIT "<2>" /* critical conditions */
#define KERN_ERR "<3>" /* error conditions */
#define KERN_WARNING "<4>" /* warning conditions */
#define KERN_NOTICE "<5>" /* normal but significant condition */
#define KERN_INFO "<6>" /* informational */
#define KERN_DEBUG "<7>" /* debug-level messages
中文版:
#define KERN_EMERG "<0>" /* 致命级:紧急事件消息,系统崩溃之前提示,表示系统不可用 */
#define KERN_ALERT "<1>" /* 警戒级:报告消息,表示必须采取措施 */
#define KERN_CRIT "<2>" /* 临界级:临界条件,通常涉及严重的硬件或软件操作失败 */
#define KERN_ERR "<3>" /* 错误级:错误条件,驱动程序常用KERN_ERR来报告硬件错误 */
#define KERN_WARNING "<4>" /* 警告级:警告条件,对可能出现问题的情况进行警告 */
#define KERN_NOTICE "<5>" /* 注意级:正常但又重要的条件,用于提醒 */
#define KERN_INFO "<6>" /* 通知级:提示信息,如驱动程序启动时,打印硬件信息 */
#define KERN_DEBUG "<7>" /* 调试级:调试级别的信息
3.查看printk的打印优先级
root@ATK-IMX6U:/mnt# cat /proc/sys/kernel/printk
7 4 1 7
该printk文件总共有4个值。
7:控制台打印级别,默认的消息日志级别优先级高于该值,消息就能够被打印到控制台。
4:默认的消息日志级别,例如printk(“imx6ull led drvier init\n”); == printk("<4>"“imx6ull led drvier init\n”);
1:最低的控制台日志级别:控制台日志级别可被设置的最小值(最高优先级)
7:缺省的控制台日志级别:控制台日志级别的缺省值
4.修改printk的打印优先级
1)第一种方法:直接修改printk文件
root@ATK-IMX6U:/mnt# echo 7 3 1 7 > /proc/sys/kernel/printk
root@ATK-IMX6U:/mnt# cat /proc/sys/kernel/printk
7 3 1 7
2)第二种方法:在printk函数添加优先级
printk("<3>"“imx6ull led drvier init\n”);
也可以写为
printk(KERN_ERR"imx6ull led drvier init\n");
7.内核模块参数
1.概述
比如编写一个串口驱动,想要在串口驱动加载的时候,波特率能够由命令行参数设定,就像运行普通的应用程序的时候,通过命令行参数来传递信息一样,应用程序示例如下:
#./udp 192.168.1.100
int main(int argc,char **argv)
{
argv[0]
argv[1]
............
}
模块参数允许用户在加载模块的时候,通过命令行指令参数值,内核支持的参数:bool、invbool(反转bool值)、charp(字符串指针)、short、int、long、ushort、uint、ulong类型,这些类型可以对应于整型、数组、字符串。
2.module_param与module_param_array宏定义的使用
module_param(name,type,perm)
module_param_array(name,type,nump,perm)
name:变量的名字
type:变量或数组元素的类型
nump:保存数组元素个数的指针,可选。默认写NULL。
perm:在sysfs文件系统中对应的文件的权限属性,决定哪些用户能够传递哪些参数,如果该用户权限过低,则无法通过命令行传递参数给该内核模块。
3.示例源码
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
static int baud = 9600; //baud变量
static int port[4]={0,1,2,3}; //port数组
static int port_cnt=0; //记录module_param_array传递数组的长度
static char *name="vcom"; //name变量
//通过以下宏定义来接收命令行的参数
module_param(baud,int,0644); //rw- r-- r--
module_param_array(port,int,&port_cnt,0644); //rw- r-- r--
module_param(name,charp,0644); //rw- r-- r--
//入口函数
static int __init imx6ull_led_init(void)
{
printk("imx6ull led init\n");
printk("baud=%d\n",baud);
printk("port=%d %d %d %d ,port_cnt=%d\n",port[0],port[1],port[2],port[3],port_cnt);
printk("name=%s\n",name);
return 0;
}
//出口函数
static void __exit imx6ull_led_exit(void)
{
printk("imx6ull led exit\n");
}
//驱动程序的入口:insmod led_drv.ko调用module_init,module_init又会去调用gec6818_led_init。
module_init(imx6ull_led_init);
//驱动程序的出口:rmmod led_drv调用module_exit,module_exit又会去调用gec6818_led_exit。
module_exit(imx6ull_led_exit)
//模块描述
MODULE_AUTHOR("stephenwen88@163.com"); //作者信息
MODULE_DESCRIPTION("imx6ull led driver"); //模块功能说明
MODULE_LICENSE("GPL"); //许可证:驱动遵循GPL协议
演示效果:
1)加载内核模块的时候,填写相应的参数
root@ATK-IMX6U:/mnt# insmod led.ko baud=115200 port=1,2,3,4 name="tcom"
[ 3201.074680] imx6ull led init
[ 3201.077594] baud=115200
[ 3201.080048] port=1 2 3 4 ,port_cnt=4
[ 3201.087332] name=tcom
2)加载内核模块后,能够在/sys/module/led/parameters/目录下看到对参数的访问权限。
root@ATK-IMX6U:/mnt# ls -l /sys/module/led/parameters/
total 0
-rw-r--r-- 1 root root 4096 Aug 14 13:38 baud
-rw-r--r-- 1 root root 4096 Aug 14 13:38 name
-rw-r--r-- 1 root root 4096 Aug 14 13:38 port
root@ATK-IMX6U:/mnt#
8.编译多个内核模块
在Makefile添加对应的语句就能能够实现多个模块的编译,例如当前demo包含了led_drv.c和sum.c文件,对应的Makefile修改如下:
KERNELDIR := /home/gec/IMX6ULL
CURRENT_PATH := $(shell pwd)
//注意是+= 不是:=
obj-m +=led_drv.o
obj-m +=sum.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
9.内核符号表——全局共享函数接口与变量
1.内核符号表
内核符号表是记录了内核中所有的符号(函数、全局变量)的地址及名字,这个符号表被嵌入到内核镜像中,使得内核可以在运行过程中随时获得一个符号地址对应的符号名。
注:
符号:指的是函数、全局变量的地址及名字。
2.用到的宏定义
EXPORT_SYMBOL(符号名):导出的符号可以给其他模块使用。
EXPORT_SYMBOL_GPL(符号名):导出的符号只能让符合GPL协议的模块才能使用。
3.示例代码
sum.c重点代码
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
static unsigned int g_count=0x100000;
static int myadd(int a,int b)
{
return (a+b);
}
EXPORT_SYMBOL_GPL(myadd);
EXPORT_SYMBOL_GPL(g_count);
.............
led.drv.c重点代码
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
extern int myadd(int a,int b);
extern unsigned int g_count;
//入口函数
static int __init imx6ull_led_init(void)
{
printk("imx6ull led init\n");
printk("myadd(10+110)=%d",myadd(10,110));
return 0;
}
.........
加载与卸载模块
加载模块的时候要分先后顺序,由于led_drv.ko依赖于sum.ko文件,因此先加载sum.ko文件,再加载led_drv.ko。
[root@GEC6818 /]#insmod sum.ko
[ 1109.544000] sum init
[root@GEC6818 /]#insmod led_drv.ko
[ 1116.872000] myadd(10+110)=120
[ 1117.022000] gec6818 led init
加载成功后,能够在内核符号表找到myadd函数,说明了该函数已经嵌入了内核符号表。
[root@GEC6818 /]#grep -r myadd /proc/kallsyms
bf018000 t myadd [sum]
卸载模块的时候要分先后顺序,由于led_drv.ko依赖于sum.ko文件,因此先卸载led_drv.ko文件,再卸载sum.ko。
[root@GEC6818 /]#rmmod led_drv
gec6818 led exit
[root@GEC6818 /]#rmmod sum
[ 1212.821000] sum exit
两个模块存在依赖关系,sum内核模块依赖led_drv,显示被1个内核模块依赖,显示如下
[root@GEC6818 /]#lsmod
led_drv 793 0 - Live 0xbf03c000 (O)
sum 926 1 led_drv, Live 0xbf034000 (O)
5.错误整理
1)如果在sum.c中删除”EXPORT_SYMBOL_GPL(myadd)”该代码,然后led_drv.c直接调用myadd函数,编译错误如下:
make ARCH=arm CROSS_COMPILE=/home/gec/6818GEC/prebuilts/gcc/linux-x86/arm/arm-eabi-4.8/bin/arm-eabi- -C /home/gec/6818GEC/kernel M=/mnt/hgfs/share/driver/module/demo4 modules
make[1]: Entering directory '/home/gec/6818GEC/kernel'
CC [M] /mnt/hgfs/share/driver/module/demo4/led_drv.o
CC [M] /mnt/hgfs/share/driver/module/demo4/sum.o
Building modules, stage 2.
MODPOST 2 modules
WARNING: "myadd" [/mnt/hgfs/share/driver/module/demo4/led_drv.ko] undefined!
CC /mnt/hgfs/share/driver/module/demo4/led_drv.mod.o
LD [M] /mnt/hgfs/share/driver/module/demo4/led_drv.ko
CC /mnt/hgfs/share/driver/module/demo4/sum.mod.o
LD [M] /mnt/hgfs/share/driver/module/demo4/sum.ko
make[1]: Leaving directory '/home/gec/6818GEC/kernel'
2)在加载驱动的时候,先加载led_drv.ko,会出现以下错误,原因在于led_drv.ko依赖sum.ko的符号,所以无法加载成功,只能先加载sum.ko,再加载led_drv.ko。
[ 1219.032000] led_drv: Unknown symbol myadd (err 0)
insmod: can't insert 'led_drv.ko': unknown symbol in module or invalid parameter
3)在卸载驱动的时候,先卸载sum.ko,会出现以下错误,因为led_drv.ko引用了sum.ko提供的符号,所以无法被卸载成功,只能先卸载led_drv.ko,再卸载sum.ko。
[root@GEC6818 /]#rmmod sum
rmmod: remove 'sum': Resource temporarily unavailable
4)如果当前内核模块没有添加许可证,那么无论是编译还是加载驱动的时候,都会有警告信息。
警告信息如下:
root@ubuntu:/mnt/hgfs/share/driver/module/demo1# make
make ARCH=arm CROSS_COMPILE=/home/gec/6818GEC/prebuilts/gcc/linux-x86/arm/arm-eabi-4.8/bin/arm-eabi- -C /home/gec/6818GEC/kernel M=/mnt/hgfs/share/driver/module/demo1 modules
make[1]: Entering directory '/home/gec/6818GEC/kernel'
CC [M] /mnt/hgfs/share/driver/module/demo1/led_drv.o
Building modules, stage 2.
MODPOST 1 modules
WARNING: modpost: missing MODULE_LICENSE() in /mnt/hgfs/share/driver/module/demo1/led_drv.o
see include/linux/module.h for more information
CC /mnt/hgfs/share/driver/module/demo1/led_drv.mod.o
LD [M] /mnt/hgfs/share/driver/module/demo1/led_drv.ko
make[1]: Leaving directory '/home/gec/6818GEC/kernel'
加载内核模块的警告信息:
[root@GEC6818 /]#insmod led_drv.ko
[ 87.372000] led_drv: module license 'unspecified' taints kernel. //模块许可证“未指定”污染内核。
[ 87.372000] Disabling lock debugging due to kernel taint //由于内核污染而禁用调试
[ 87.379000] gec6818 led init
10.多个源文件组合为内核模块
有些时候不想通过符号表全局共享函数与全局变量,可以使用以下方法实现多个文件编译,实现局部访问。
led.c关键代码
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
extern int myadd(int a,int b);
extern unsigned int g_count;
//入口函数
static int __init gec6818_led_init(void)
{
printk("gec6818 led init\n");
printk("gec6818 led init\n");
printk("myadd(10+110)=%d",myadd(10,110));
printk("g_count=%d",g_count);
return 0;
}
//模块描述
MODULE_AUTHOR("stephenwen88@163.com"); //作者信息
MODULE_DESCRIPTION("gec6818 led driver"); //模块功能说明
MODULE_LICENSE("GPL"); //许可证:驱动遵循GPL协议
sum.c关键代码
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
unsigned int g_count=0x100000;
int myadd(int a,int b)
{
return (a+b);
}
Makefile
在这个Makefile中,obj-m说明最终生成的内核模块目标文件是led_drv.ko,并且它必须依赖两个文件led.c和sum.c。因此,通过模块名加-objs的形式可以定义整个模块所包含的文件。
注意,这里定义的都是与原文件同名的目标文件,详细如下:
obj-m +=led_drv.o
led_drv-objs=led.o sum.o
KERNEL_DIR :=/home/gec/6818GEC/kernel
CROSS_COMPILE :=/home/gec/6818GEC/prebuilts/gcc/linux-x86/arm/arm-eabi-4.8/bin/arm-eabi-
PWD:=$(shell pwd)
default:
$(MAKE) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNEL_DIR) M=$(PWD) modules
clean:
rm *.o *.order .*.cmd *.mod.c *.symvers .tmp_versions -rf
编译输出
gec@ubuntu:/mnt/hgfs/share/module/demo5$ make
make ARCH=arm CROSS_COMPILE=/home/gec/6818GEC/prebuilts/gcc/linux-x86/arm/arm-eabi-4.8/bin/arm-eabi- -C /home/gec/6818GEC/kernel M=/mnt/hgfs/share/module/demo5 modules
make[1]: Entering directory '/home/gec/6818GEC/kernel'
CC [M] /mnt/hgfs/share/module/demo5/led.o //编译led.c输出led.o目标文件
CC [M] /mnt/hgfs/share/module/demo5/sum.o //编译sum.c输出sum.o目标文件
LD [M] /mnt/hgfs/share/module/demo5/led_drv.o //将led.o sum.o链接为led_drv.o文件
Building modules, stage 2.
MODPOST 1 modules
CC /mnt/hgfs/share/module/demo5/led_drv.mod.o
LD [M] /mnt/hgfs/share/module/demo5/led_drv.ko
make[1]: Leaving directory '/home/gec/6818GEC/kernel'
操作演示
[root@GEC6818 /]#insmod led.ko
[ 6840.604000] gec6818 led init
[ 6840.604000] gec6818 led init
[ 6840.604000] myadd(10+110)=120
[ 6840.604000] g_count=104857