回顾:
1.回顾嵌入式linux系统相关内容
2.linux系统分为用户空间和内核空间
用户空间:
CPU的工作模式为:USR(用户)模式,也是非特权模式
运行:应用程序,C库等
地址空间:4G虚拟地址空间,占前3G
地址范围:0x00000000~0xBFFFFFFF
不能直接访问硬件资源,不能访问内核空间地址
内核空间:
CPU工作模式:SVC(管理)模式,也是特权模式
设备驱动,网络协议栈,文件系统,进程管理,内存管理,平台相关,系统调用 (open,close,read,write,mmap,fork...)
地址范围:0xC0000000 ~ 0xFFFFFFFF
能够访问任何资源
3.用户空间和内核空间进行通信必须通过系统调用~!
4.用户进程一旦陷入内核空间运行,内核会给进程分配一个内核栈(8K)?
5.了解和掌握linux内核的配置编译
----------------------------------------------------------------------------------------------------------------------------------------
内核代码:helloworld.c
静态编译:将helloworld.c和zImage编译在一起
动态编译,模块化编译:将helloworld.c和zImage分开编译,将helloworld.c编译成对应的二进制文件,默认以ko结尾,例如helloworld.ko
**********************************************************************************************
Day02
内核模块的动态编译:
内核编译借助Kconfig和Makefile,Kconfig和Makefile的选择关键看helloworld.c放置在哪个目录下,例如将helloworld.c放置在内核源码的drivers/char/
修改Kconfig:
vimdrivers/char/Kconfig,添加:
configHELLOWORLD
tristate "hello,world"
--help---
"this is my first kernel module"
注意:config HELLOWORLD最终生成的给Makefile使用的配置项:CONFIG_HELLOWORLD(才是Kconfig和Makefile连接的桥梁)
修改Makefile文件:
obj-$(COFNIG_HELLOWORLD)+= helloworld.o
每当make menuconfig进行配置时,如果选择为*
obj-y +=helloworld.o //静态编译
如果选择为M
obj-m +=helloworld.o //动态编译-》helloworld.ko
*********************************************************************************************
一,内核编程
1.入口函数
如果需要驱动有一个入口函数,那么通过内核提供的宏函数进行修饰,告知内核驱动的入口函数
module_init(hellokernel_init);
入口函数的定义要求:
int hellokernel_init(void)
返回值:int型,如果入口函数正确执行,给内核返回0,否则返回负值;
入口函数正确执行了,内核模块才能加载到内核,否则加载失败!
参数:void
函数功能:向内核注册操作硬件的方法或者申请资源,例如分配内存
注意:一个驱动(.c)只能有一个入口函数
必须通过module_init来告诉内核这是入口函数!
什么时候被内核执行?
每当内核初始化(内核模块采用静态编译进zImage)或者动态加载驱动模块到内核时,内核会执行入口函数!
2.出口函数
如果驱动有必要添加一个出口函数,那么通过内核提供的宏函数进行修饰,告知内核驱动的出口函数
module_exit(hellokernel_exit);
出口函数的定义:
void hellokernel_exit(void)
参数,返回值:void
函数功能:一般和入口函数相对应,做些释放操作硬件方法和释放资源的工作,例如释放内存
注意:一个驱动(.c)只有一个出口函数
什么时候被内核执行?
系统复位或者动态将模块从内核卸载时,内核调用出口函数。
案例:编写第一个内核模块代码helloworld.c
驱动存放目录:/opt/drivers/day01,/opt/drivers/day02...
mkdir/opt/drivers/day01/1.0 -p
cd/opt/drivers/day01/1.0
vim helloworld.c
********************************************************************************************
3.内核模块代码编译(采用模块化编译)
明确:内核模块代码的编译必须使用内核源码,不能使用C库!
编写Makefile实现将helloworld.c的目录,源码和内核源码进行关联,就是告诉内核源码,还有一个内核模块的目录,不是在内核源码中,而是在/opt/drivers/day01
vim Makefile 内容如下:
obj-m +=helloworld.o
KDIR=/opt/kernel
all:
(TAB键)make -C $(KDIR)SUBDIRS=$(PWD) modules
#说明:-C表示指定到某个目录下,SUBDIRS表示子目录 modules表示模块化编译-》helloworld.ko
clean:
(TAB键) rm -fr .*.cmd *.o *.mod.c *.ko .tmp_versions module* Module*
编译:
make //结果生成一个目标文件helloworld.ko
filehelloworld.ko //查看是否是针对ARM架构
注意:编译模块之前一定要先编译内核源码,还要在内核源码中进行对内核源码进行模块编译
实施步骤:
1.cd /opt/kernel
2.cpconfig_cw210...V1.0 .config
3.makemenuconfig //3个检查
4.make zImage
5.make modules//把内核源码中但凡选择为M的代码进行模块化编译
总结:每当进行模块化编译内核代码是,最后前期做好以上5步骤,只需做一次即可!
**********************************************************
内核模块的使用(helloworld.ko):
linux系统提供了模块操作的相关命令:
insmod:模块加载命令,例如insmod helloworld.ko,就是将模块helloworld.ko动态加载到内核中,如果模块有入口函数,内核此时执行模块的入口函数,如果没有入口函数,没关系,不影响模块的加载!
lsmod:查看当前运行中的内核已经加载的模块,例如,lsmod列出当前已经加载了哪些模块
rmmod:从内核中动态删除指定的模块,例如rmmod helloworld,就是将模块helloworld从内核动态删除掉,如果模块有出口函数,内核执行此模块的出口函数。
modinfo:查看模块本身的信息,例如modinfo helloworld.ko
实验步骤:
1.cp/opt/drivers/day01/1.0/hellooworld.ko /opt/rootfs
在开发板上:
2.lsmod查看
2.加载insmod helloworld.ko
3.查看lsmod
4.卸载rmmod helloworld
5.modinfohelloworld.ko
问题:rmmod卸载模块失败,提示:
rmmod:chdir(/lib/modules):Nosuch file or dir...
问题的原因是制作rootfs时,busybox配置时,模块操作的命令都是选择的精简版本,最好使用完成版本,如果使用精简版本的模块命令,解决以上的问题的方法:
mkdir/lib/modules/2.6.35.7-Concenwit -p
如何将busybox配置添加完整版的模块操作命令:
cdbusybox-1.19.4
make menuconfig
Linux Module Utilities --->
[*] Simplified modutils //精简版本,如果选用完整版本,这个选项去掉!然后将一下信息全部选中!
[*] insmod [*] rmmod
[*] lsmod
[*] Pretty output
[*] modprobe [*] Blacklist support
[*] depmod
make
make install
cp _install/*/opt/rootfs -frd
问题:
~ # modinfohelloworld.ko
modinfo: can'topen '/lib/modules/2.6.35.7-Concenwit/modules.dep': No such file or directory
后续讲解!
********************************************************************************
给内核模块添加相关信息:
1.给内核模块必须添加许可声明
MODULE_LICENSE(“GPL”);//告诉内核此模块遵循GPL协议
注意:以后写任何一个.c内核代码,都必须添加这么一句话!
2.给内核模块添加可选信息:
MODULE_AUTHOR(“tarena<40902560@qq.com>”);
MODULE_DESCRIPTION(“Hello Kernel module”);
MODULE_VERSION(“V1.0.0”);
3.查看以上模块信息通过modinfo来查看
*********************************************************************************************
内核模块参数声明:
作用:像应用程序命令行传参一样,给程序传递参数信息./a.out 100 200.内核如果也需要给程序传递参数,需要使用内核模块参数声明来解决。
如果要对内核模块的某一个变量进行修改,必须要声明:
module_param(name,type,perm);
name:要修改的变量名
type:变量的数据类型,没有浮点数
perm:对变量的访问权限,如果权限非0,每当加载完模块以后,会在/sys/module(虚拟文件系统的目录,存在于内存中,不存在nfs的主机上或者flash上,并且与其他的根文件系统一样,在内存中还有一份)目录下生成一个跟模块名同名的目录,在这个目录下有一个parameters,在这个目录下会生成一个跟变量名同名的文件,以后通过修改这个文件,来修改这个变量的值;如果权限为0,不会有同名的文件生成,只能在insmod模块时才能对这个变量进行赋值!
实验步骤:
1.不传参
insmodhelloworld.ko
lsmod
rmmod helloworld
2.传参
insmod helloworld.ko irq=100pstr=tarena
lsmod
rmmod helloworld
3.修改文件来修改变量的内容
insmodhelloworld.ko irq=100 pstr=tarena
lsmod
ls /sys/module/helloworld/parameters/
cat /sys/module/helloworld/parameters/irq //读文件
echo 2000 > /sys/module/helloworld/parameters/irq //修改文件内容
rmmod helloworld//查看irq是否修改为2000
案例:把pstr的权限也给0664
数组的模块参数声明:
module_param_array(name,type,nump,perm);
作用:对数组进行模块参数声明,一旦声明,以后可以进行对数组传递参数
name:数组名
type:数据元素的数据类型
nump:记录有效的数组元素个数的指针
perm:访问权限
进行模块定义、实现的时候,只需定义一个变量,将它的地址给module_param_array的第三个参数即可!然后内核会自动根据有效的数组元素的个数给其赋值!
实验步骤:
insmodhelloworld.ko fish=10,20,30,40,50 //nr_fish=5
cat/sys/module/helloworld/parameters/fish
echo1,2,3,4,5,6,7,8 > /sys/module/helloworld/parameters/fish
rmmod helloworld
总结:虽然在进行模块参数声明的时候,如果给定了权限,那么就会在指定的目录下生成一个跟变量名同名的文件,修改文件即可对变量进行修改,这种方式虽然比较方便,灵活,但是这个文件存在于内存中,如果驱动有大量的这样代码,会造成内存的大量使用!如果没有需要在模块运行期间改变变量内容的需求,权限一律给0!如果权限为0,只能在insmod时修改!
**********************************************************************************************
内核符号导出:
作用:将函数或者变量进行符号导出,让其他模块能够访问使用。
EXPORT_SYMBOL(函数名或者变量名);
EXPORT_SYMBOL_GPL(函数名或者变量名);
前者导出的符号能够被任何模块使用,但是后者导出的符号只能被遵循GPL协议的模块使用!
案例:helloworld.c调用test_module.c的某个函数
调试宏:
__FUNCTION__,__func__,%s
__LINE__,%d
__FILE__,%s
__DATE__,%s
__TIME__,%s
内核多文件编译:
Makefile:
分别编译:
obj-m += A.o B.o
编译在一起:
obj-m += C.o //不能是A.o或者B.o
C-objs = A.o B.o
多文件模块加载:一定首先先加载依赖模块,例如A.ko调用B.ko的某个函数,那么A依赖B。先加载insmod B.ko,后加载insmod A.ko.卸载时,先卸载A,最后卸载依赖模块B。
总结:写任何.c文件,都要加MODULE_LICENSE("GPL");
*******************************************************************************
如果模块之间的依赖过于复杂,可以利用modprobe命令来加载或者卸载模块。例如.A.ko依赖B.ko,每当modprobe A.ko时,此命令根据modules.dep,先帮你加载B.ko,后加载A.ko.
modprobe命令使用步骤:
1.进入内核源码 cd /opt/kernel
make modules
2.回到驱动模块所在目录/opt/drivers/day02/3.0
2.修改Makefile,添加如下信息:
install:
make -C $(KDIR) SUBDIRS=$(PWD)modules_install INSTALL_MOD_PATH=/opt/
#modules_install表示安装模块,主要是产生modules.dep依赖关系文件,然后将.ko拷贝到extra目录。
#INSTALL_MOD_PATH表示安装到那个目录下
#一旦执行make install安装模块时,最后在/opt目录生成一个lib目录,然后将lib目录的内容拷贝到/opt/rootfs/lib下
3.make //编译模块
4.make install//安装模块
5. cp /opt/lib/*/opt/rootfs/lib/ -frd
6.在开发板上进行加载:
modprobe helloworld.ko / modprobe helloworld
说明:modprobe默认到/lib/modules/2.6..../下找依赖文件modules.dep,根据依赖文件,决定先加载哪个模块!模块在/lib/modules/2.6..../extra
卸载:
modprobe -r helloworld
问题:
~ # modinfohelloworld.ko
modinfo: can'topen '/lib/modules/2.6.35.7-Concenwit/modules.dep': No such file or directory
通过以上步骤也能够解决掉!
**************************************************************************************
二:内核打印函数printk用法
1.printk能够指定输出打印级别(0~7)
2.printk打印输出信息要依赖默认的输出级别,printk函数在使用时,指定的输出级别如果小于默认的输出级别,信息一律打印,否则(大于等于)不打印!
3.设置默认的输出级别的方法:
方法1:通过修改配置文件
cat /proc/sys/kernel/printk
7(关注第一个,默认的输出级别为7) 4 1 7
echo 8 >/proc/sys/kernel/printk
缺点:无法设置内核启动时的打印信息
方法2:通过修改uboot的bootargs来实现,这种设置方法能够解决内核启动时的打印输出
setenv bootargsroot=/dev/nfs nfsroot=... quiet//设置默认的输出级别为4
boot //启动或者res(复位)
在u-boot下,boot为直接启动内核的命令,res复位从u-boot开始启动!
在内核下,reboot为重启系统命令,直接从u-boot开始启动,相当于复位!
(1)setenv bootargsroot=/dev/nfs nfsroot=... debug//设置的默认输出级别为10
save然后boot启动内核
setenv bootargsroot=/dev/nfs nfsroot=... loglevel=数字
如果不加这三个条件,则默认的输出级别为7,都可通过查看/proc/sys/kernel/printk来检查
Boot
案例:
static intprintall_init(void)
{
printk("<0>" "level 0\n");
printk("<1>" "level 1\n");
printk("<2>" "level2\n");
printk("<3>" "level3\n");
printk("<4>" "level4\n");
printk("<5>" "level5\n");
printk("<6>" "level6\n");
printk("<7>" "level7\n");
return 0;
}
输出级别与格式字符串之间,没有逗号,空格或者直接相连即可!
未指定优先级的默认级别定义在内核根目录下的/kernel/printk.c下,
本系统中在/opt/kernel/kernel/printk.c下,
#defineDEFAULT_MESSAGE_LOGLEVEL 4
总结:产品最终发布时,打印信息一律屏蔽,使用loglevle=0!产品软件研发阶段,使用debug来查看程序的运行状态
quiet 为4,debug为10,loglevel=数字(自己设置输出级别)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
总结:
1.内核打印函数printk
默认的输出级别的设置,两种方法:(1)内核启动以后设置 /proc/sys/kernel/printk(2)设置u-boot的环境变量bootargs 添加quiet/debug/loglevel=数字
未指定输出级别的默认级别的设置在/kernel/printk.c下的宏 #defineDEFAULT_MESSAGE_LOGLEVEL 4