一、改回nfs方式挂载根文件系统
在产品将要上线之前,需要制作不同类型格式的根文件系统
在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统
优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命
【1】重启上位机nfs服务
sudo service nfs-kernel-server restart
【2】关闭上位机防火墙
sudo service ufw stop
【3】修改下位机的环境变量
setenv bootargs root=/dev/nfs nfsroot=192.168.1.8:/nfs_share/_install ip=192.168.1.6:192.168.1.8:192.168.1.1:255.255.255.0 init=/linuxrc console=ttySAC0,115200 maxcpus=1 lcd=wy070ml tp=gslx680-linux
【4】保存环境变量
saveenv
【5】重启
re
二、Linux内核驱动开发的基础知识
1、裸板驱动和内核驱动的区别
1)裸板驱动
主观性较强,相当于英语考试中的作文模块
while (1) {
// 代码逻辑
...
}
2)Linux下的驱动开发
客观性较强,相当于英语考试中的完形填空
需要的知识:
【1】硬件的知识
读懂硬件原理图、读懂读写时序图、各种接口协议
【2】读取cpu数据手册
各种寄存器如何配置、各种外设如何配置
【3】驱动的编码框架
字符设备驱动(按字节访问,顺序固定)
块设备驱动(按块访问,顺序不固定)
网络设备驱动(按字节访问,顺序固定)
【4】内核态的编程规则
用户态和内核态的数据交互
内核模块的编程框架
解决竞态和并发
。。。
2、Linux内核代码的特点
1)介绍
Linux内核本质上就是一个巨大的裸板程序,所有的函数都是自身实现的
|----------|----------|------------------|------|
| 标准C库 | 系统调用 | Linux Kernel | 说明 |
| fopen | open | sys_open | 打开文件 |
| fclose | close | sys_close | 关闭文件 |
| fread | read | sys_read | 读文件 |
| fwrite | write | sys_write | 写文件 |
学习Linux内核最好的老师就是内核源码,遇到不会用的函数,去找内核源码。
2)推荐书籍
内核:<Linux内核的设计与实现>
驱动:<LDD3>、<Linux设备驱动第三版>
3、Linux内核需要注意的地方
1)Linux内核不允许做浮点运算
2)Linux内核中使用的是GUN C,不是标准C(GNU C是标准C的扩展版)
3)Linux内核中每个线程都有两个栈
【1】用户态的栈
【2】内核态的栈
4)Linux内核使用的内存空间是3G - 4G
5)Linux内核更加注重代码的执行效率和可移植性
4、搭建Linu内核的开发环境
1)安装交叉编译器
2)获取一份x6818上使用的Linux内核源码
3)编译Linux内核源码
4)制作并移植根文件系统
5、使用source insight建立一个内核源码的项目工程
source insight是一个阅读项目工程源码非常好用的工具
官网:Source Insight Programming Editor and Code Browser
1)安装source insight
序列号位置:sourcesightSN.txt
2)添加Linux Kernel源码
【1】在window下某盘符中创建一个目录
【2】将Linux内核源码拷贝并解压到该目录
【3】打开kernel文件夹,新建文件夹,用来存储项目工程文件
3)在source insight中添加项目工程
6、编写内核的模块文件(*.ko)
1)上位机编写模块文件
【1】在虚拟机创建新目录,存放工程目录
mkdir drivers
【2】进入drivers目录
cd drivers/
【3】创建第一个工程目录
mkdir hello_pro
【4】进入hello_pro目录
cd hello_pro/
【5】编写工程文件hello.c
vim hello.c
cpp#include <linux/init.h> #include <linux/module.h> MODULE_LICENSE("GPL"); // 声明开源, // 如果不添加该声明,内核会报受到污染,因为没有遵守开源规则 MODULE_AUTHOR("Zjd"); // 声明作者 int __init hello_init(void) { printk("<0>" "Hello, my owner kernel!\n"); return 0; } void __exit hello_exit(void) { printk("<0>" "bye, my owner kernel!\n"); return ; } module_init (hello_init); module_exit (hello_exit);
【6】编写Makefile
vim Makefile
cppobj-m += hello.o KERNEL_PATH=/home/zjd/s5p6818/KERNEL/kernel all: make -C $(KERNEL_PATH) M=$(PWD) modules clean: make -C $(KERNEL_PATH) M=$(PWD) clean
【7】编译工程文件
make
【8】将工程文件拷贝到共享的根文件系统中
cp hello.ko /nfs_share/_install/
2)下位机验证模块文件
【1】查看内核中的模块
lsmod
【2】安装模块
insmod hello.ko
【3】卸载模块
rmmod hello
提示缺少 /lib/modules/ 目录
mkdir /lib/modules
提示缺少 3.4.39-embTwoGroup 目录
mkdir /lib/modules/3.4.39-embTwoGroup
卸载成功
7、导出符号
在C语言中,使用extern关键字修饰的符号可以跨模块访问
进行内核态开发时,需要额外添加一个宏去修饰
EXPORT_SYMBOL:它所修饰的符号,在内核中,所有的模块都可以访问到
EXPORT_SYMBOL_GPL:它所修饰的符号,在内核中,只有遵循GPL规则的模块才能访问到
1)上位机编写试验文件
【1】创建新的项目工程
mkdir export
【2】进入实验目录
cd extern_pro
【3】创建文件
touch export.c import.c export.h
【4】编写程序
vim export.h
cpp#ifndef __EXPORT_H #define __EXPORT_H extern int add(int a, int b); #endif
vim export.c
cpp#include <linux/init.h> #include <linux/module.h> MODULE_LICENSE("GPL"); MODULE_AUTHOR("Zjd"); int add(int a, int b) { return a + b; } EXPORT_SYMBOL(add);
vim import.c
cpp#include <linux/init.h> #include <linux/module.h> #include "export.h" MODULE_LICENSE("GPL"); MODULE_AUTHOR("Zjd"); int __init import_init(void) { int ret = add(20, 6); printk("<0>" "ret = %d\n", ret); return 0; } void __exit import_exit(void) { printk("<0>" "I'm going.\n"); return ; } module_init(import_init); module_exit(import_exit);
【5】编写Makefile
vim Makefile
bashobj-m += export.o import.o KERNEL_PATH=/home/zjd/s5p6818/KERNEL/kernel ROOTFS_PATH=/nfs_share/_install all: make -C $(KERNEL_PATH) M=$(PWD) modules cp *.ko $(ROOTFS_PATH) clean: make -C $(KERNEL_PATH) M=$(PWD) clean
2)下位机验证
【1】挂载export.ko
insmod export.ko
【2】挂载import.ko
insmod import.ko
【3】卸载import
rmmod import
【4】挂载export
rmmod export
【5】注意事项
1】安装和卸载模块的顺序是相反的
2】安装时要填写 *.ko 文件,卸载时直接填写文件名 *
8、printk
1)简介
printk是内核中的打印函数,它输出到内核自己维护的缓冲区。
printk的使用方法:
printk("<0/1/2/3.../7>" "info");
注释:
<0/1/2/3.../7>:代表该条消息的打印优先级,越小优先级越高
info:代表要打印的消息
它的用法与printf相同,只不过前面多了一个消息优先级的配置,且不需要用 ',' 分开
2)特殊情况
printk("ret = %d\n", ret); // 使用的是默认优先级
Linux内核将打印优先级设定为8个等级(0~7),值越小,优先级越高
printk输出的信息先到内核维护的缓冲区
缓冲区的内容能不能输出到控制终端是有限制的
3)限制
在uboot中设置的bootargs环境变量中的loglevel代表Linux内核设置的优先级阈值
当我们设定printk的优先级大于loglevel所设置的优先级,则可以打到终端,相反则不可以打印到终端。
4)验证
【1】创建实验目录
mkdir printk_pro
【2】进入实验目录
cd printk_pro/
【3】编写程序
vim myprintk.c
cpp#include <linux/init.h> #include <linux/module.h> MODULE_LICENSE("GPL"); MODULE_AUTHOR("Zjd"); int __init printk_init(void) { printk("<0>" "Level 0!\n"); printk("<1>" "Level 1!\n"); printk("<2>" "Level 2!\n"); printk("<3>" "Level 3!\n"); printk("<4>" "Level 4!\n"); printk("<5>" "Level 5!\n"); printk("<6>" "Level 6!\n"); printk("<7>" "Level 7!\n"); return 0; } void __exit printk_exit(void) { return ; } module_init(printk_init); module_exit(printk_exit);
【4】查看内核源码
在Source Insight中查找printk
【5】优先级说明
|--------------|---------|--------|
| 宏名 | 宏值 | 说明 |
| KERN_EMERG | "<0>" | 系统不可用 |
| KERN_ALERT | "<1>" | 立即操作 |
| KERN_CRIT | "<2>" | 临界条件 |
| KERN_ERR | "<3>" | 错误 |
| KERN_WARNING | "<4>" | 警告 |
| KERN_NOTICE | "<5>" | 正常但重要 |
| KERN_INFO | "<6>" | 消息 |
| KERN_DEBUG | "<7>" | 调试 |【6】更改程序内容
vim myprintk.c
cpp#include <linux/init.h> #include <linux/module.h> MODULE_LICENSE("GPL"); MODULE_AUTHOR("Zjd"); int __init printk_init(void) { printk(KERN_EMERG "Level 0!\n"); printk(KERN_ALERT "Level 1!\n"); printk(KERN_CRIT "Level 2!\n"); printk(KERN_ERR "Level 3!\n"); printk(KERN_WARNING "Level 4!\n"); printk(KERN_NOTICE "Level 5!\n"); printk(KERN_INFO "Level 6!\n"); printk(KERN_DEBUG "Level 7!\n"); return 0; } void __exit printk_exit(void) { return ; } module_init(printk_init); module_exit(printk_exit);
【7】编写Makefile
vim Makefile
bashobj-m += myprintk.o KERNEL_PATH=/home/zjd/s5p6818/KERNEL/kernel ROOTFS_PATH=/nfs_share/_install all: make -C $(KERNEL_PATH) M=$(PWD) modules cp *.ko $(ROOTFS_PATH) clean: make -C $(KERNEL_PATH) M=$(PWD) clean
【8】下位机验证
查看内核优先级设置
cat /proc/sys/kernel/printk
第一个值:优先级阈值(这里是2)
第二个值:内核默认优先级(这里是4)
9、模块参数(三步法)
在C语言中,我们使用argc、argv给程序传参
./a.out xxx yyy zzz
// int main(int argc, char **argv)
int main(int argc, char *argv[]){
...
};
1)定义全局变量
cppint irq = 0; char *str = "Hello World!"; int fish[10] = {0}; int num = 10; int __init module_param_init(void) { ...; return 0; }
2)通过特定的宏
module_param(name, type, perm)
name:要声明为模块参数的变量名
type:变量的类型
perm:权限
module_param_array(name, type, nump, perm)
name:要声明为模块参数的数组名
type:数组元素的类型
nump:数组元素个数指针
perm:权限
3)使用模块参数
insmod module_param.ko
insmod module_param.ko irq=10 str="easthome" fish=1,2,3,4,5,6
4)验证
【1】创建工程目录
mkdir module_param
【2】进入工程目录
cd module_param
【3】编写程序
vim module_param.c
cpp#include <linux/init.h> #include <linux/module.h> MODULE_LICENSE("GPL"); MODULE_AUTHOR("Zjd"); int irq = 0; char *str = "hello world!"; int len = 10; int arr[10] = {0}; module_param(irq, int, 0644); module_param(str, charp, 0); module_param_array(arr, int, &len, 0644); int __init module_param_init(void) { int i = 0; printk(KERN_EMERG "irq = %d\n", irq); printk(KERN_EMERG "str = %s\n", str); // Linux Kernel apply the define of ARRAY_SIZE // to count the number of members // #define ARRAY_SIZE(x) sizeof(x) / sizeof(x[0]) for (i = 0; i < ARRAY_SIZE(arr); i++) { printk(KERN_EMERG "arr[%d] = %d\n", i, arr[i]); } return 0; } void __exit module_param_exit(void) { printk(KERN_EMERG "bye ~\n"); return ; } module_init(module_param_init); module_exit(module_param_exit);
【4】编写Makefile
vim Makefile
bashobj-m += module_param.o KERNEL_PATH=/home/zjd/s5p6818/KERNEL/kernel ROOTFS_PATH=/nfs_share/_install all: make -C $(KERNEL_PATH) M=$(PWD) modules cp *.ko $(ROOTFS_PATH) clean: make -C $(KERNEL_PATH) M=$(PWD) clean
【5】下位机验证
模板参数在系统中有负责维护的节点
/sys/module/module_param/parameters/
【6】思考
我们在驱动代码里面定义了3个全局变量,这里只出来两个,arr、irq
因为我们将str全局变量的权限设定为0,所以没有显示
我们给irq与arr全局变量设定的权限就是该节点文件的权限
【7】意义
我们可以在内核中验证寄存器的功能,将寄存器声明为模块参数,在安装模块的时候就可以使用模块参数(REG=VAL)
a、系统调用
系统调用是用户进入内核空间的一种方式(还可以通过中断进入内核空间)
1)意义
【1】用户空间到内核空间
是用户空间调用内核空间函数的一种方式
【2】安全
系统调用保证了内核的安全,允许应用程序调用内核中的函数(以安全的方式)
2)系统调用的实现
1】应用程序首先使用适当的值,填充寄存器
2】调用特殊的指令
3】执行指令,跳转到某个位置
4】在该位置,根据填充到寄存器的值,找到内核中对应的函数
5】调用该函数
6】函数执行完毕后,原路返回到用户空间
3)验证
【1】应用程序选取适当的值
vim kernel/arch/arm/include/asm/unistd.h
【2】填充寄存器
vim kernel/arch/arm/kernel/entry-common.S
寄存器:r7
【3】调用特殊指令
vim kernel/arch/arm/kernel/entry-common.S
检查系统调用号
去系统调用表中的宏进行匹配
输出调用号,跳转到asm_syscall()
vim kernel/arch/arm/kernel/traps.c
vim arch/arm/kernel/calls.S
4)演示
【1】新增一个系统调用号
vim kernel/arch/arm/include/asm/unistd.h
【2】新增一个内核中的API(接口)函数
vim kernel/arch/arm/kernel/sys_arm.c
【3】修改系统调用表
vim kernel/arch/arm/kernel/call.S
【4】重新编译内核
make uImage
【5】重新烧录内核
cp arch/arm/boot/uImage /tftpboot
tftp 48000000 uImage
mmc write 48000000 2000 3000
re
【6】系统调用syscall
【7】编写测试程序
mkdir sys_call
vim sys_add.c
cpp#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/syscall.h> #define SYS_ADD_NUM 378 int main(void) { int ret = 0; ret = syscall(SYS_ADD_NUM, 20, 6); printf("ret = %d\n", ret); return 0; }
【8】编译sys_add.c文件
arm-cortex_a9-linux-gnueabi-gcc sys_add.c -o sys_add
【9】下位机测试
cp sys_add /nfs_share/_install/
【a】调试
1】查找交叉编译链工具位置
which arm-cortex_a9-linux-gnueabi-gcc
2】进入gcc的库目录
cd /opt/toolchains/arm-cortex_a9-eabi-4.7-eglibc-2.18/arm-cortex_a9-linux-gnueabi/lib
3】拷贝需要的库文件根文件系统
cp libgcc_s.so.1 /nfs_share/_install/lib/
【b】再次测试