1.系统开发相关的内容
uboot
kernel
rootfs
2.linux系统的划分
用户空间
内核空间
3.linux内核子系统
4.linux模块开发的特点
七大注意事项
5.加载函数,卸载函数
insmod/modprobe rmmod
lsmod modinfo
6.编译模块
Makefile
7.模块信息
MODULE_LICENSE("GPL");
8.设置内核默认的输出级别的方法:
方法1:
修改/proc/sys/kernel/printk文件
echo 8 > /proc/sys/kernel/printk 打印所有的信息
echo 4 > /proc/sys/kernel/printk 打印输出小于4的级别的信息
方法2:方法1无法解决设置内核启动时候的输出信息
在uboot的bootargs中设置默认的输出级别
debug/quiet/loglevel=数字(级别)
setenv bootargs root=/dev/nfs nfsroot=192.168.1.8:/opt/rootfs
ip=192.168.1.6:192.168.1.8:192.168.1.1:255.255.255.0::eth0:on
init=/linuxrc console=ttySAC0,115200 debug //级别为10
setenv bootargs root=/dev/nfs nfsroot=192.168.1.8:/opt/rootfs
ip=192.168.1.6:192.168.1.8:192.168.1.1:255.255.255.0::eth0:on
init=/linuxrc console=ttySAC0,115200 quiet //级别为4
setenv bootargs root=/dev/nfs nfsroot=192.168.1.8:/opt/rootfs
ip=192.168.1.6:192.168.1.8:192.168.1.1:255.255.255.0::eth0:on
init=/linuxrc console=ttySAC0,115200 loglevel = 4
案例:要求在加载驱动模块时,点亮所有的灯
要求在卸载驱动模块时,关闭所有的灯
mount -t vfat /dev/sda1 /mnt
cd /mnt
ls 查看u盘的信息
总结:
1.内核模块参数
作用:在加载模块和加载模块以后,能够
给模块传递相应的参数信息
module_param
module_param_array
权限问题:
非0:在/sys/module/模块名/paramters/文件
通过修改这个文件完成对变量的内容修改
问题:会占用内存的资源
权限为0:就不会有一个文件存在,只能在模块
加载的时候才能修改
2.模块的符号导出
作用:将函数和变量导出,供其他模块使用
EXPORT_SYMBOL
EXPORT_SYMBOL_GPL
前者任何模块都能使用,后者只能给遵循GPL协议的
模块使用,所有要求模块编程时,一律添加:
MODULE_LICENSE("GPL");
3.printk
能够指定打印输出的级别:8级
数字越小,级别越高
一般linux系统都有一个默认的输出级别:
如何配置默认的输出级别:
修改/proc/sys/kernel/printk
在uboot中指定:debug quiet loglevel=数字
4.linux系统调用的原理和实现SCI
作用:
1.为用户提供统一的硬件抽象层
操作一个文件,无需关注这个文件存在
硬盘上,SD卡,U盘,只需调用open,read,
write等函数操作即可
2.安全保护
原理:
1.应用程序调用open
2.进程会调用C库的open函数的实现
3.C库的open实现会将open对应的系统调用号
保存在寄存器中
4.C库的open实现会调用swi(svc)触发一个软中断
异常
5.这时进程就会跳转到内核预先指定的一个位置
6.对应的位置就在内核定义好的异常向量的入口
vector_swi
7.这个函数会根据系统调用号,在内核预先
定义好的一个系统调用表中找到对应的open的
内核实现(sys_open)
8.找到这个函数以后,执行这个函数
9.执行完毕以后,原路返回给用户空间
如何添加一个系统调用:
1.在内核代码中arch/arm/kernel/sys_arm.c
添加一个系统调用的内核实现sys_add
2.在内核代码中arch/arm/include/asm/unistd.h
中添加一个新的系统调用号__NR_add
3.在内核代码中arch/arm/kernel/calls.S
中的系统调用表sys_call_table中添加
一个项:CALL(sys_add)
4.在用户空间调用syscall完成调用新添加的
系统调用实现sys_add
注意:syscall这个函数本身会帮你实现调用
swi(svc),你只需传递一个系统调用号
总结:一个进程从用户空间到内核空间的转换靠软中断!
5.linux内核提供的GPIO操作的库函数
CPU的GPIO对于内核来说是一种资源,
这种资源新式是以软件编号的新式存在
S5PV210_GPC0(3).....
gpio_request
gpio_direction_output
gpio_direction_input
gpio_set_value
gpio_get_value
gpio_free
1.linux设备驱动分类
按管理的设备硬件来分:
字符设备
按字节流来访问,能够顺序访问,也能够
指定位置的访问
按键,串口,终端,触摸屏,LCD等
块设备
在unix系统下,块设备按一定的数据块进行
访问,数据块为512字节,或者1K等
在linux系统下,块设备即可按数据块进行
访问,也可以按字节流访问,那么他和字符
设备本质的区别在于linux系统描述块设备和
字符设备的数据结构和操作方法是不一样的。
硬盘,U盘,SD卡,TF卡,nandflash,norflash
网络设备
网卡,网络设备一般都要结合TCP/IP协议栈
来实现。
2.字符设备驱动
驱动程序的作用:
1.管理操作硬件
2.给用户提供访问硬件操作的方法(接口)
led_on
led_off
read_uart
write_uart
....
unix、linux:一切皆文件!
问:应用程序如何访问硬件呢?
答:硬件设备在linux系统下,会以设备文件的形式
存在,设备文件(字符和块)在/dev/,那么应用程序
要访问硬件其实就是对设备文件的访问。
问:应用程序如何访问设备文件呢?
答:通过调用系统调用函数来实现对其访问,访问
设备文件和访问普通的文件的方式是一样的
open,read,write,ioctl,mmap,close...
问:应用程序通过设备文件如何在茫茫的内核驱动
代码中找到自己对应的驱动程序呢?
答:设备文件本身包含了一些属性:设备文件是字符
设备文件(c)还是块设备文件(b),
还包括了主设备号和次设备号这两个重要的属性。
应用程序就是根据主设备号找到对应的驱动程序。
一个驱动程序只有一个主设备号(进行绑定)。
问:次设备号的作用是什么?
答:主设备号用于应用程序找到驱动程序;
次设备号用于应用程序找到具体要操作访问的
设备个体,比如:
S5PV210处理器有4个串口,只需一个串口驱动
来管理即可,四个串口共享一个主设备号,
应用程序通过次设备号来分区要访问的串口
设备号:主设备号和次设备号
数据类型:
dev_t (unsigned int)
高12位:主设备号
低20位:次设备号
设备操作宏
MAJOR
MINOR
MKDEV
如果要实现一个驱动和设备号的绑定,首先需要
向内核申请设备号资源,只有完成申请以后,才能
进行后续的绑定工作!
问:如何向内核申请设备号呢?
答:静态分配和动态分配
静态分配
1.首先通过cat /proc/devices查看linux系统中哪个主设备号没有
被占用
Character devices: 字符设备
主设备号 设备名称
1 mem
2 pty
3 ttyp
4 /dev/vc/0
...
Block devices: 块设备
259 blkext
7 loop
8 sd
31 mtdblock
2.然后根据你的设备个数分配次设备号,
如果设备个数只有一个,一般次设备号从0开始
dev_t dev_id = MKDEV(主设备号,次设备号);
3.调用register_chrdev_region;向内核
申请即可
4.如果主设备号为0,静态分配失败。
动态分配
1.调用alloc_chrdev_region直接向内核去
申请设备号,也就是让操作系统内核帮你分配
设备号。
释放设备号:
unregister_chrdev_region将设备号资源
归还操作系统内核
案例:要求驱动程序能够动态选择静态分配和动态分配
两种方法实现设备号的申请
提示:采用模块参数的方法
<<linux设备驱动程序>>第三版
<<linux内核设计与实现>>第三版
四个重要的数据结构:
1.struct file
作用:描述文件打开以后的状态属性
生命周期:从open打开成功由内核创建
到close关闭文件时进行销毁
重要的成员:
struct file_operations *f_op;//指向驱动
程序中实现的各个硬件操作方法
f_op = &led_fops;
unsigned int f_flags;//文件的操作属性
loff_t f_ops; //文件操作位置
void *private_data; //??
总结:设备文件的操作方法最终来源于f_op
他指向驱动中的操作集合。
struct inode
作用:描述一个文件的物理属性
生命周期:文件存在,内核创建
文件销毁,内核销毁对应的inode
重要成员:
dev_t i_rdev; //存放设备号
struct cdev *i_cdev; //指向一个字符设备
一个文件只有一个inode,可以有多个file
问:struct file和struct file_operations如何关联:
答:1.当应用程序调用open时,最终调用sys_open
2.sys_open创建struct file结构体内存,描述
打开的文件信息
3.通过某种机制<?>获取到驱动的struct file_operations
然后将驱动的file_operations的地址赋值给
struct file的f_op
4.sys_open最后再调用驱动的file_operations里的
open函数,这个open指针指向驱动的led_open
问:应用程序如何read,write设备呢?
答:由于对设备的访问总是先open,一旦先open,就需要
做上面的过程,一旦过程执行,file和底层驱动的
file_operations就进行了关联,
以后read,write最终:
read->sys_read->file->f_op->read = led_read
write->sys_write->file->f_op->write = led_write
问:如何将驱动的file_operations注册到内核中?
问:应用程序如何通过设备号找到驱动程序的?
一旦找到驱动程序,就等于找到file_operations
答:struct cdev
问:应用程序如何通过系统调用函数完成对硬件设备的访问?
答:
1.编写安装字符设备驱动程序
1.1 分配初始化struct file_operations
struct file_operations led_fops = {
.open = led_open,
.release = led_close,
.read = led_read,
...
};
1.2 分配初始化struct cdev
struct cdev led_cdev;
cdev_init(&led_cdev, &led_fops);
结果就是led_cdev.ops = &led_fops
cdev_add(&led_cdev, 设备号,设备个数);
结果就是将led_cdev添加到内核的cdev的数组之中
下标是以设备号为索引!
一旦完成对cdev的注册,就等于有了一个真实
的字符设备,关键这个驱动有了对应的操作
集合=>led_fops
2.应用程序首先要open打开设备
2.1 应用程序调用open打开设备文件(打开设备)
2.2 调用C库的open实现
2.3 C库的open实现会保存open的系统调用号
2.4 C库的open实现调用swi触发一个软中断,跳转到
内核态
2.5 根据系统调用号,在系统调用表中找到open对应的
内核系统调用实现sys_open
2.6 调用sys_open,sys_open会做如下工作:
1.通过inode.i_rdev获取设备号
2.根据设备号在内核cdev数组中找到对应的
字符设备驱动cdev(led_cdev)
3.然后将找到的cdev的地址赋值给inode.i_cdev指针
用于缓存和别的用途(?)
4.创建struct file结构体内存用于描述打开的
设备文件信息
5.根据已经获得的cdev,从而获取其中的驱动
操作集合ops(led_fops)
6.将字符设备驱动的操作接口ops在赋值
给file->f_op = &led_fops
7.最后在调用一次file->f_op->open = led_open
3.后续就可以对设备进行read,write等操作
app:read(fd, buf, size);
kernel:
sys_read:
获取struct file
获取上一次操作的偏移file->f_pos
直接调用:
file->f_ops->read(file,buf,size,file->f_pos)
结果:最终完成对底层read函数的调用
总结:完成一个字符设备驱动主要对cdev和struct file_operations进行操作
案例:要求应用程序open设备时,打开所有的灯
要求应用程序close设备时,关闭所有的灯
1.添加头文件
2.添加加载函数
申请设备号
分配初始化struct file_operations
.open = led_open
.release = led_close
分配初始化注册cdev
申请GPIO资源
3.添加卸载函数
释放GPIO资源
卸载cdev
释放设备号
实验步骤:
1.编译驱动模块
2.交叉编译应用程序
arm-linux-gcc -o led_test led_test.c
3.insmod led_drv.ko
4.cat /proc/devices //查看动态申请的主设备号
5.创建设备节点
mknod /dev/myled c 250 0
6../led_test //查看打印信息和灯的开关状态
7.优化代码
app:read
kernel:
sys_read->led_read:
函数原型:
ssize_t (*read)(struct file *filp,
char __user *buf,
size_t count,
loff_t *f_pos);
切记:buf在内核空间不能直接使用,buf指向用户空间
*buf = 1(不允许)
必须利用copy_to_user完成数据由内核空间
到用户空间的转移
案例:要求用户写1,开灯
要求用户写0,关灯
int cmd;
int stat;
cmd = 1;
write(fd, &cmd, 4);
read(fd, &stat, 4);
cmd = 0;
write(fd, &cmd, 4);
读取灯的状态:1表示灯亮,0表示灯灭
案例:指定其中一盏的操作
提示:在用户和内核定义一个结构体
struct led_cmd {
int cmd; //指定开关命令
int index;//指定灯的编号
};
int (*ioctl)(struct inode *inode,
struct file *filp,
unsigned int cmd,
unsigned long arg);
app:
ioctl(fd, LED_ON);//仅仅发送命令
或者
int index = 2;
ioctl(fd, LED_ON, &index);//发送命令和写入输入到设备
driver:
cmd:对应的就是应用程序的LED_ON
arg:存放的就是用户空间的缓冲区地址&index
所以在内核空间不能直接对arg访问,
需要使用copy_from_user....
回顾:
1.设备节点、设备文件
问:设备文件干什么用?
答:用于表示设备,用户访问设备文件,就是在访问设备
问:设备文件在哪里?
答:在/dev
问:设备文件如何创建?
答:手工创建mknod /dev/设备文件名 <c|b> 主设备号 次设备号
自动创建
2.设备号
问:设备号包含什么内容?
答:包含主,次设备号
问:主,次设备号的作用?
答:应用程序通过主设备号找到驱动程序
应用程序通过次设备号找到同类型设备的不同个体,比如串口
问:由于设备号对于内核来说是一种资源,如何向内核申请?
答:静态申请和动态申请
静态申请:事先查看当前系统里哪个主设备号是空闲的
优点是可以提前创建好设备文件,缺点不便于驱动的推广
动态申请:让内核帮你申请一个设备号
优点是便于驱动的推广,缺点不能提前创建好设备节点
问:设备号数据类型是什么?
答:dev_t 本质上是一个unsigned int
12-》主
20-》次
MAJOR
MINOR
MKDEV
3.字符设备四个重要数据结构
struct inode:
问:这个结构描述什么内容?
答:用于描述一个文件的物理特性,一个文件有一个对应的inode
问:生命周期?谁来创建?
答:文件存在,它存在,文件销毁,它销毁;
内核创建
问:有哪些重要的成员?
答:i_rdev(存放设备文件对应的设备号信息)
i_cdev(如果这个设备文件是字符设备文件,
它存放的就是字符设备驱动对应的数据结构cdev)
用于缓存
struct file
问:描述什么属性?
答:描述设备文件被成功打开以后的状态属性
问:生命周期?谁来创建?
答:文件打开以后,内核创建
文件关闭以后,内核进行销毁
问:有哪些重要的成员?
答:f_op(存放底层驱动的硬件操作集合)
f_flags(存放应用程序调用open时指定的操作属性,O_RDWR|O_NONBLOCK)
f_pos(存放文件操作的位置信息)
private_data文件私有数据?
struct cdev
问:描述什么内容?
答:描述一个字符设备
问:生命周期?谁来创建?
答:由驱动程序在加载函数中完成cdev的分配,初始化和注册工作
由驱动程序在卸载函数中完成cdev的删除操作,一旦删除
,内核就不再存在一个字符设备设备驱动
问:有哪些重要成员?
答:ops(存放底层驱动的硬件操作集合)
dev(存放设备号)
count(存放设备的个数)
问:cdev涉及的相关操作函数
答:cdev_init
cdev_add
cdev_del
问:如何完成cdev的注册过程?
答:当调用cdev_add时,根据设备号,将cdev
添加到内核事先定义好一个cdev的数组中,以后
应用程序就可以通过设备号来访问这个数组,获取
对应的cdev,也就是获取到对应的字符设备驱动
struct file_operations
问:描述什么内容?
答:它仅仅就是包含了一堆的硬件操作的接口
问:应用程序如何和这些接口对应的呢?
答:一一对应
open->sys_open->.open = x_open
close->sys_close->.release = x_close
read->sys_read->.read = x_read
write->sys_write->.write = x_write
ioctl->sys_ioctl->.ioctl = x_ioctl
问:应用程序到sys_系统调用函数这个过程
是内核已经帮你实现,sys_open->.open = x_open
是如何关联的呢?
答:答案参看day03.txt笔记
4.如何实现一个字符设备驱动
案例:按键驱动的一个版本
要求:能够获取按键值
按键按下,获取的键值为0x50
按键松开,获取的键值为0x51
1.了解硬件原理
2.驱动编写过程
1.分配初始驱动操作集合
struct file_operations btn_fops = {
.owner = THIS_MODULE,
.open = btn_open,
.release = btn_close,
.read = btn_read
};
2.分配cdev
3.在加载函数中
分配设备号
初始化cdev
注册cdev
4.在卸载函数中
删除cdev
释放设备号
5.填充btn_open
申请GPIO资源GPH0_0
配置为输入口
6.填充btn_close
释放GPIO资源
7.填充btn_read
读取GPIO的管脚状态
判断管脚状态
如果是1:把0x51 copy_to_user到用户空间
如果是0:把0x50 copy_to_user到用户空间
实验步骤:
1.去除官方按键驱动
make menuconfig //在内核源代码根目录
Device Drivers->
Input device support --->
[*] Keyboards --->
[*] S3C gpio keypad support //去掉
make zImage
cp arch/arm/boot/zImage /tftpboot
重启开发板,用新内核引导
2.insmod btn_drv.ko
3.cat /proc/devices //查看申请主设备号
4.mknod /dev/buttons c 主设备号 0
5../btn_test 操作按键查看信息
6.去掉应用程序的打印信息
7../btn_test & //让应用程序后台运行
8.top //查看CPU的使用情况
案例:用ioctl实现读取按键信息
中断相关内容:
问:为什么有中断
答:由于外设的处理速度相对处理器比较慢,
如果CPU采用轮训或者定期检查设备,浪费很多
CPU的资源,做很多无用功。采用中断,可以最大
的利用CPU
问:中断在硬件的连接方式是怎么样的?
答:外设->中断控制器->CPU
首先外设操作,产生电信号,发送给中断控制器
中断控制器能够检测和处理电信号,决定是否
将信号发送CPU,如果发送给CPU,CPU相应这个
电信号,做后续的中断处理
问:CPU进行中断的处理流程
中断有优先级
中断可以打断中断,当然也可以打断执行的进程
中断异常向量表(内核启动时创建)
保存现场
处理中断(执行中断服务程序)
恢复现场
问:内核如何实现中断编程的?
答:request_irq
free_irq
问:内核要求中断处理程序应该注意哪些事项?
答:1.要求中断处理过程越快越好
2.中断处理程序在内核空间,是随机执行,
不隶属于任何进程,不参与进程的调度
3.中断不能和用户空间进行数据的交互
如果要交互,需要通过系统调用
4.中断处理程序不能调用引起阻塞的函数
copy_*
问:虽然理想状态是要求中断处理函数执行的越快越好,
但某些场合是无法满足这个要去的,比如网卡接收数据的过程,
如果网卡对应的中断处理程序长时间的占有CPU的资源,
就会影响系统的并发和响应能力,怎么办呢?
答:如果对于这种情况,可以采用顶半部+底半部的实现方法
其实就是将以前的中断处理程序分为两部分:
顶半部:就是中断处理程序,做一些比较紧急的事情,
比如将网卡数据包从网卡硬件缓冲区拷贝到主存中,
这个过程不可中断,然后一定要在顶半部中登记底半部
要做的事,CPU会在空闲,适当的时候再去执行底半部剩余的工作。
底半部:做一些不紧急,相对耗时的工作,比如将数据包
提交给协议层的过程,这个过程可以被别的新的中断所打断
问:底半部是如何的实现呢?
问:顶半部和底半部如何关联的呢?
答:底半部的实现机制如下:
tasklet
工作队列
软中断
问:tasklet怎么玩?
答:
按键中断测试步骤:
1.insmod btn_drv.ko
2.cat /proc/interrupts //查看中断的注册信息
中断号 中断触发的次数 中断类型 中断名称
CPU0
16: 137 s3c-uart s5pv210-uart
18: 273 s3c-uart s5pv210-uart
32: 0 s5p_vic_eint KEY_UP
1.设备节点、设备文件
问:设备文件干什么用?
答:用于表示设备,用户访问设备文件,就是在访问设备
问:设备文件在哪里?
答:在/dev
问:设备文件如何创建?
答:手工创建mknod /dev/设备文件名 <c|b> 主设备号 次设备号
自动创建
2.设备号
问:设备号包含什么内容?
答:包含主,次设备号
问:主,次设备号的作用?
答:应用程序通过主设备号找到驱动程序
应用程序通过次设备号找到同类型设备的不同个体,比如串口
问:由于设备号对于内核来说是一种资源,如何向内核申请?
答:静态申请和动态申请
静态申请:事先查看当前系统里哪个主设备号是空闲的
优点是可以提前创建好设备文件,缺点不便于驱动的推广
动态申请:让内核帮你申请一个设备号
优点是便于驱动的推广,缺点不能提前创建好设备节点
问:设备号数据类型是什么?
答:dev_t 本质上是一个unsigned int
12-》主
20-》次
MAJOR
MINOR
MKDEV
3.字符设备四个重要数据结构
struct inode:
问:这个结构描述什么内容?
答:用于描述一个文件的物理特性,一个文件有一个对应的inode
问:生命周期?谁来创建?
答:文件存在,它存在,文件销毁,它销毁;
内核创建
问:有哪些重要的成员?
答:i_rdev(存放设备文件对应的设备号信息)
i_cdev(如果这个设备文件是字符设备文件,
它存放的就是字符设备驱动对应的数据结构cdev)
用于缓存
struct file
问:描述什么属性?
答:描述设备文件被成功打开以后的状态属性
问:生命周期?谁来创建?
答:文件打开以后,内核创建
文件关闭以后,内核进行销毁
问:有哪些重要的成员?
答:f_op(存放底层驱动的硬件操作集合)
f_flags(存放应用程序调用open时指定的操作属性,O_RDWR|O_NONBLOCK)
f_pos(存放文件操作的位置信息)
private_data文件私有数据?
struct cdev
问:描述什么内容?
答:描述一个字符设备
问:生命周期?谁来创建?
答:由驱动程序在加载函数中完成cdev的分配,初始化和注册工作
由驱动程序在卸载函数中完成cdev的删除操作,一旦删除
,内核就不再存在一个字符设备设备驱动
问:有哪些重要成员?
答:ops(存放底层驱动的硬件操作集合)
dev(存放设备号)
count(存放设备的个数)
问:cdev涉及的相关操作函数
答:cdev_init
cdev_add
cdev_del
问:如何完成cdev的注册过程?
答:当调用cdev_add时,根据设备号,将cdev
添加到内核事先定义好一个cdev的数组中,以后
应用程序就可以通过设备号来访问这个数组,获取
对应的cdev,也就是获取到对应的字符设备驱动
struct file_operations
问:描述什么内容?
答:它仅仅就是包含了一堆的硬件操作的接口
问:应用程序如何和这些接口对应的呢?
答:一一对应
open->sys_open->.open = x_open
close->sys_close->.release = x_close
read->sys_read->.read = x_read
write->sys_write->.write = x_write
ioctl->sys_ioctl->.ioctl = x_ioctl
问:应用程序到sys_系统调用函数这个过程
是内核已经帮你实现,sys_open->.open = x_open
是如何关联的呢?
答:答案参看day03.txt笔记
4.如何实现一个字符设备驱动
案例:按键驱动的一个版本
要求:能够获取按键值
按键按下,获取的键值为0x50
按键松开,获取的键值为0x51
1.了解硬件原理
2.驱动编写过程
1.分配初始驱动操作集合
struct file_operations btn_fops = {
.owner = THIS_MODULE,
.open = btn_open,
.release = btn_close,
.read = btn_read
};
2.分配cdev
3.在加载函数中
分配设备号
初始化cdev
注册cdev
4.在卸载函数中
删除cdev
释放设备号
5.填充btn_open
申请GPIO资源GPH0_0
配置为输入口
6.填充btn_close
释放GPIO资源
7.填充btn_read
读取GPIO的管脚状态
判断管脚状态
如果是1:把0x51 copy_to_user到用户空间
如果是0:把0x50 copy_to_user到用户空间
实验步骤:
1.去除官方按键驱动
make menuconfig //在内核源代码根目录
Device Drivers->
Input device support --->
[*] Keyboards --->
[*] S3C gpio keypad support //去掉
make zImage
cp arch/arm/boot/zImage /tftpboot
重启开发板,用新内核引导
2.insmod btn_drv.ko
3.cat /proc/devices //查看申请主设备号
4.mknod /dev/buttons
uboot
kernel
rootfs
2.linux系统的划分
用户空间
内核空间
3.linux内核子系统
4.linux模块开发的特点
七大注意事项
5.加载函数,卸载函数
insmod/modprobe rmmod
lsmod modinfo
6.编译模块
Makefile
7.模块信息
MODULE_LICENSE("GPL");
8.设置内核默认的输出级别的方法:
方法1:
修改/proc/sys/kernel/printk文件
echo 8 > /proc/sys/kernel/printk 打印所有的信息
echo 4 > /proc/sys/kernel/printk 打印输出小于4的级别的信息
方法2:方法1无法解决设置内核启动时候的输出信息
在uboot的bootargs中设置默认的输出级别
debug/quiet/loglevel=数字(级别)
setenv bootargs root=/dev/nfs nfsroot=192.168.1.8:/opt/rootfs
ip=192.168.1.6:192.168.1.8:192.168.1.1:255.255.255.0::eth0:on
init=/linuxrc console=ttySAC0,115200 debug //级别为10
setenv bootargs root=/dev/nfs nfsroot=192.168.1.8:/opt/rootfs
ip=192.168.1.6:192.168.1.8:192.168.1.1:255.255.255.0::eth0:on
init=/linuxrc console=ttySAC0,115200 quiet //级别为4
setenv bootargs root=/dev/nfs nfsroot=192.168.1.8:/opt/rootfs
ip=192.168.1.6:192.168.1.8:192.168.1.1:255.255.255.0::eth0:on
init=/linuxrc console=ttySAC0,115200 loglevel = 4
案例:要求在加载驱动模块时,点亮所有的灯
要求在卸载驱动模块时,关闭所有的灯
mount -t vfat /dev/sda1 /mnt
cd /mnt
ls 查看u盘的信息
总结:
1.内核模块参数
作用:在加载模块和加载模块以后,能够
给模块传递相应的参数信息
module_param
module_param_array
权限问题:
非0:在/sys/module/模块名/paramters/文件
通过修改这个文件完成对变量的内容修改
问题:会占用内存的资源
权限为0:就不会有一个文件存在,只能在模块
加载的时候才能修改
2.模块的符号导出
作用:将函数和变量导出,供其他模块使用
EXPORT_SYMBOL
EXPORT_SYMBOL_GPL
前者任何模块都能使用,后者只能给遵循GPL协议的
模块使用,所有要求模块编程时,一律添加:
MODULE_LICENSE("GPL");
3.printk
能够指定打印输出的级别:8级
数字越小,级别越高
一般linux系统都有一个默认的输出级别:
如何配置默认的输出级别:
修改/proc/sys/kernel/printk
在uboot中指定:debug quiet loglevel=数字
4.linux系统调用的原理和实现SCI
作用:
1.为用户提供统一的硬件抽象层
操作一个文件,无需关注这个文件存在
硬盘上,SD卡,U盘,只需调用open,read,
write等函数操作即可
2.安全保护
原理:
1.应用程序调用open
2.进程会调用C库的open函数的实现
3.C库的open实现会将open对应的系统调用号
保存在寄存器中
4.C库的open实现会调用swi(svc)触发一个软中断
异常
5.这时进程就会跳转到内核预先指定的一个位置
6.对应的位置就在内核定义好的异常向量的入口
vector_swi
7.这个函数会根据系统调用号,在内核预先
定义好的一个系统调用表中找到对应的open的
内核实现(sys_open)
8.找到这个函数以后,执行这个函数
9.执行完毕以后,原路返回给用户空间
如何添加一个系统调用:
1.在内核代码中arch/arm/kernel/sys_arm.c
添加一个系统调用的内核实现sys_add
2.在内核代码中arch/arm/include/asm/unistd.h
中添加一个新的系统调用号__NR_add
3.在内核代码中arch/arm/kernel/calls.S
中的系统调用表sys_call_table中添加
一个项:CALL(sys_add)
4.在用户空间调用syscall完成调用新添加的
系统调用实现sys_add
注意:syscall这个函数本身会帮你实现调用
swi(svc),你只需传递一个系统调用号
总结:一个进程从用户空间到内核空间的转换靠软中断!
5.linux内核提供的GPIO操作的库函数
CPU的GPIO对于内核来说是一种资源,
这种资源新式是以软件编号的新式存在
S5PV210_GPC0(3).....
gpio_request
gpio_direction_output
gpio_direction_input
gpio_set_value
gpio_get_value
gpio_free
1.linux设备驱动分类
按管理的设备硬件来分:
字符设备
按字节流来访问,能够顺序访问,也能够
指定位置的访问
按键,串口,终端,触摸屏,LCD等
块设备
在unix系统下,块设备按一定的数据块进行
访问,数据块为512字节,或者1K等
在linux系统下,块设备即可按数据块进行
访问,也可以按字节流访问,那么他和字符
设备本质的区别在于linux系统描述块设备和
字符设备的数据结构和操作方法是不一样的。
硬盘,U盘,SD卡,TF卡,nandflash,norflash
网络设备
网卡,网络设备一般都要结合TCP/IP协议栈
来实现。
2.字符设备驱动
驱动程序的作用:
1.管理操作硬件
2.给用户提供访问硬件操作的方法(接口)
led_on
led_off
read_uart
write_uart
....
unix、linux:一切皆文件!
问:应用程序如何访问硬件呢?
答:硬件设备在linux系统下,会以设备文件的形式
存在,设备文件(字符和块)在/dev/,那么应用程序
要访问硬件其实就是对设备文件的访问。
问:应用程序如何访问设备文件呢?
答:通过调用系统调用函数来实现对其访问,访问
设备文件和访问普通的文件的方式是一样的
open,read,write,ioctl,mmap,close...
问:应用程序通过设备文件如何在茫茫的内核驱动
代码中找到自己对应的驱动程序呢?
答:设备文件本身包含了一些属性:设备文件是字符
设备文件(c)还是块设备文件(b),
还包括了主设备号和次设备号这两个重要的属性。
应用程序就是根据主设备号找到对应的驱动程序。
一个驱动程序只有一个主设备号(进行绑定)。
问:次设备号的作用是什么?
答:主设备号用于应用程序找到驱动程序;
次设备号用于应用程序找到具体要操作访问的
设备个体,比如:
S5PV210处理器有4个串口,只需一个串口驱动
来管理即可,四个串口共享一个主设备号,
应用程序通过次设备号来分区要访问的串口
设备号:主设备号和次设备号
数据类型:
dev_t (unsigned int)
高12位:主设备号
低20位:次设备号
设备操作宏
MAJOR
MINOR
MKDEV
如果要实现一个驱动和设备号的绑定,首先需要
向内核申请设备号资源,只有完成申请以后,才能
进行后续的绑定工作!
问:如何向内核申请设备号呢?
答:静态分配和动态分配
静态分配
1.首先通过cat /proc/devices查看linux系统中哪个主设备号没有
被占用
Character devices: 字符设备
主设备号 设备名称
1 mem
2 pty
3 ttyp
4 /dev/vc/0
...
Block devices: 块设备
259 blkext
7 loop
8 sd
31 mtdblock
2.然后根据你的设备个数分配次设备号,
如果设备个数只有一个,一般次设备号从0开始
dev_t dev_id = MKDEV(主设备号,次设备号);
3.调用register_chrdev_region;向内核
申请即可
4.如果主设备号为0,静态分配失败。
动态分配
1.调用alloc_chrdev_region直接向内核去
申请设备号,也就是让操作系统内核帮你分配
设备号。
释放设备号:
unregister_chrdev_region将设备号资源
归还操作系统内核
案例:要求驱动程序能够动态选择静态分配和动态分配
两种方法实现设备号的申请
提示:采用模块参数的方法
<<linux设备驱动程序>>第三版
<<linux内核设计与实现>>第三版
四个重要的数据结构:
1.struct file
作用:描述文件打开以后的状态属性
生命周期:从open打开成功由内核创建
到close关闭文件时进行销毁
重要的成员:
struct file_operations *f_op;//指向驱动
程序中实现的各个硬件操作方法
f_op = &led_fops;
unsigned int f_flags;//文件的操作属性
loff_t f_ops; //文件操作位置
void *private_data; //??
总结:设备文件的操作方法最终来源于f_op
他指向驱动中的操作集合。
struct inode
作用:描述一个文件的物理属性
生命周期:文件存在,内核创建
文件销毁,内核销毁对应的inode
重要成员:
dev_t i_rdev; //存放设备号
struct cdev *i_cdev; //指向一个字符设备
一个文件只有一个inode,可以有多个file
问:struct file和struct file_operations如何关联:
答:1.当应用程序调用open时,最终调用sys_open
2.sys_open创建struct file结构体内存,描述
打开的文件信息
3.通过某种机制<?>获取到驱动的struct file_operations
然后将驱动的file_operations的地址赋值给
struct file的f_op
4.sys_open最后再调用驱动的file_operations里的
open函数,这个open指针指向驱动的led_open
问:应用程序如何read,write设备呢?
答:由于对设备的访问总是先open,一旦先open,就需要
做上面的过程,一旦过程执行,file和底层驱动的
file_operations就进行了关联,
以后read,write最终:
read->sys_read->file->f_op->read = led_read
write->sys_write->file->f_op->write = led_write
问:如何将驱动的file_operations注册到内核中?
问:应用程序如何通过设备号找到驱动程序的?
一旦找到驱动程序,就等于找到file_operations
答:struct cdev
问:应用程序如何通过系统调用函数完成对硬件设备的访问?
答:
1.编写安装字符设备驱动程序
1.1 分配初始化struct file_operations
struct file_operations led_fops = {
.open = led_open,
.release = led_close,
.read = led_read,
...
};
1.2 分配初始化struct cdev
struct cdev led_cdev;
cdev_init(&led_cdev, &led_fops);
结果就是led_cdev.ops = &led_fops
cdev_add(&led_cdev, 设备号,设备个数);
结果就是将led_cdev添加到内核的cdev的数组之中
下标是以设备号为索引!
一旦完成对cdev的注册,就等于有了一个真实
的字符设备,关键这个驱动有了对应的操作
集合=>led_fops
2.应用程序首先要open打开设备
2.1 应用程序调用open打开设备文件(打开设备)
2.2 调用C库的open实现
2.3 C库的open实现会保存open的系统调用号
2.4 C库的open实现调用swi触发一个软中断,跳转到
内核态
2.5 根据系统调用号,在系统调用表中找到open对应的
内核系统调用实现sys_open
2.6 调用sys_open,sys_open会做如下工作:
1.通过inode.i_rdev获取设备号
2.根据设备号在内核cdev数组中找到对应的
字符设备驱动cdev(led_cdev)
3.然后将找到的cdev的地址赋值给inode.i_cdev指针
用于缓存和别的用途(?)
4.创建struct file结构体内存用于描述打开的
设备文件信息
5.根据已经获得的cdev,从而获取其中的驱动
操作集合ops(led_fops)
6.将字符设备驱动的操作接口ops在赋值
给file->f_op = &led_fops
7.最后在调用一次file->f_op->open = led_open
3.后续就可以对设备进行read,write等操作
app:read(fd, buf, size);
kernel:
sys_read:
获取struct file
获取上一次操作的偏移file->f_pos
直接调用:
file->f_ops->read(file,buf,size,file->f_pos)
结果:最终完成对底层read函数的调用
总结:完成一个字符设备驱动主要对cdev和struct file_operations进行操作
案例:要求应用程序open设备时,打开所有的灯
要求应用程序close设备时,关闭所有的灯
1.添加头文件
2.添加加载函数
申请设备号
分配初始化struct file_operations
.open = led_open
.release = led_close
分配初始化注册cdev
申请GPIO资源
3.添加卸载函数
释放GPIO资源
卸载cdev
释放设备号
实验步骤:
1.编译驱动模块
2.交叉编译应用程序
arm-linux-gcc -o led_test led_test.c
3.insmod led_drv.ko
4.cat /proc/devices //查看动态申请的主设备号
5.创建设备节点
mknod /dev/myled c 250 0
6../led_test //查看打印信息和灯的开关状态
7.优化代码
app:read
kernel:
sys_read->led_read:
函数原型:
ssize_t (*read)(struct file *filp,
char __user *buf,
size_t count,
loff_t *f_pos);
切记:buf在内核空间不能直接使用,buf指向用户空间
*buf = 1(不允许)
必须利用copy_to_user完成数据由内核空间
到用户空间的转移
案例:要求用户写1,开灯
要求用户写0,关灯
int cmd;
int stat;
cmd = 1;
write(fd, &cmd, 4);
read(fd, &stat, 4);
cmd = 0;
write(fd, &cmd, 4);
读取灯的状态:1表示灯亮,0表示灯灭
案例:指定其中一盏的操作
提示:在用户和内核定义一个结构体
struct led_cmd {
int cmd; //指定开关命令
int index;//指定灯的编号
};
int (*ioctl)(struct inode *inode,
struct file *filp,
unsigned int cmd,
unsigned long arg);
app:
ioctl(fd, LED_ON);//仅仅发送命令
或者
int index = 2;
ioctl(fd, LED_ON, &index);//发送命令和写入输入到设备
driver:
cmd:对应的就是应用程序的LED_ON
arg:存放的就是用户空间的缓冲区地址&index
所以在内核空间不能直接对arg访问,
需要使用copy_from_user....
回顾:
1.设备节点、设备文件
问:设备文件干什么用?
答:用于表示设备,用户访问设备文件,就是在访问设备
问:设备文件在哪里?
答:在/dev
问:设备文件如何创建?
答:手工创建mknod /dev/设备文件名 <c|b> 主设备号 次设备号
自动创建
2.设备号
问:设备号包含什么内容?
答:包含主,次设备号
问:主,次设备号的作用?
答:应用程序通过主设备号找到驱动程序
应用程序通过次设备号找到同类型设备的不同个体,比如串口
问:由于设备号对于内核来说是一种资源,如何向内核申请?
答:静态申请和动态申请
静态申请:事先查看当前系统里哪个主设备号是空闲的
优点是可以提前创建好设备文件,缺点不便于驱动的推广
动态申请:让内核帮你申请一个设备号
优点是便于驱动的推广,缺点不能提前创建好设备节点
问:设备号数据类型是什么?
答:dev_t 本质上是一个unsigned int
12-》主
20-》次
MAJOR
MINOR
MKDEV
3.字符设备四个重要数据结构
struct inode:
问:这个结构描述什么内容?
答:用于描述一个文件的物理特性,一个文件有一个对应的inode
问:生命周期?谁来创建?
答:文件存在,它存在,文件销毁,它销毁;
内核创建
问:有哪些重要的成员?
答:i_rdev(存放设备文件对应的设备号信息)
i_cdev(如果这个设备文件是字符设备文件,
它存放的就是字符设备驱动对应的数据结构cdev)
用于缓存
struct file
问:描述什么属性?
答:描述设备文件被成功打开以后的状态属性
问:生命周期?谁来创建?
答:文件打开以后,内核创建
文件关闭以后,内核进行销毁
问:有哪些重要的成员?
答:f_op(存放底层驱动的硬件操作集合)
f_flags(存放应用程序调用open时指定的操作属性,O_RDWR|O_NONBLOCK)
f_pos(存放文件操作的位置信息)
private_data文件私有数据?
struct cdev
问:描述什么内容?
答:描述一个字符设备
问:生命周期?谁来创建?
答:由驱动程序在加载函数中完成cdev的分配,初始化和注册工作
由驱动程序在卸载函数中完成cdev的删除操作,一旦删除
,内核就不再存在一个字符设备设备驱动
问:有哪些重要成员?
答:ops(存放底层驱动的硬件操作集合)
dev(存放设备号)
count(存放设备的个数)
问:cdev涉及的相关操作函数
答:cdev_init
cdev_add
cdev_del
问:如何完成cdev的注册过程?
答:当调用cdev_add时,根据设备号,将cdev
添加到内核事先定义好一个cdev的数组中,以后
应用程序就可以通过设备号来访问这个数组,获取
对应的cdev,也就是获取到对应的字符设备驱动
struct file_operations
问:描述什么内容?
答:它仅仅就是包含了一堆的硬件操作的接口
问:应用程序如何和这些接口对应的呢?
答:一一对应
open->sys_open->.open = x_open
close->sys_close->.release = x_close
read->sys_read->.read = x_read
write->sys_write->.write = x_write
ioctl->sys_ioctl->.ioctl = x_ioctl
问:应用程序到sys_系统调用函数这个过程
是内核已经帮你实现,sys_open->.open = x_open
是如何关联的呢?
答:答案参看day03.txt笔记
4.如何实现一个字符设备驱动
案例:按键驱动的一个版本
要求:能够获取按键值
按键按下,获取的键值为0x50
按键松开,获取的键值为0x51
1.了解硬件原理
2.驱动编写过程
1.分配初始驱动操作集合
struct file_operations btn_fops = {
.owner = THIS_MODULE,
.open = btn_open,
.release = btn_close,
.read = btn_read
};
2.分配cdev
3.在加载函数中
分配设备号
初始化cdev
注册cdev
4.在卸载函数中
删除cdev
释放设备号
5.填充btn_open
申请GPIO资源GPH0_0
配置为输入口
6.填充btn_close
释放GPIO资源
7.填充btn_read
读取GPIO的管脚状态
判断管脚状态
如果是1:把0x51 copy_to_user到用户空间
如果是0:把0x50 copy_to_user到用户空间
实验步骤:
1.去除官方按键驱动
make menuconfig //在内核源代码根目录
Device Drivers->
Input device support --->
[*] Keyboards --->
[*] S3C gpio keypad support //去掉
make zImage
cp arch/arm/boot/zImage /tftpboot
重启开发板,用新内核引导
2.insmod btn_drv.ko
3.cat /proc/devices //查看申请主设备号
4.mknod /dev/buttons c 主设备号 0
5../btn_test 操作按键查看信息
6.去掉应用程序的打印信息
7../btn_test & //让应用程序后台运行
8.top //查看CPU的使用情况
案例:用ioctl实现读取按键信息
中断相关内容:
问:为什么有中断
答:由于外设的处理速度相对处理器比较慢,
如果CPU采用轮训或者定期检查设备,浪费很多
CPU的资源,做很多无用功。采用中断,可以最大
的利用CPU
问:中断在硬件的连接方式是怎么样的?
答:外设->中断控制器->CPU
首先外设操作,产生电信号,发送给中断控制器
中断控制器能够检测和处理电信号,决定是否
将信号发送CPU,如果发送给CPU,CPU相应这个
电信号,做后续的中断处理
问:CPU进行中断的处理流程
中断有优先级
中断可以打断中断,当然也可以打断执行的进程
中断异常向量表(内核启动时创建)
保存现场
处理中断(执行中断服务程序)
恢复现场
问:内核如何实现中断编程的?
答:request_irq
free_irq
问:内核要求中断处理程序应该注意哪些事项?
答:1.要求中断处理过程越快越好
2.中断处理程序在内核空间,是随机执行,
不隶属于任何进程,不参与进程的调度
3.中断不能和用户空间进行数据的交互
如果要交互,需要通过系统调用
4.中断处理程序不能调用引起阻塞的函数
copy_*
问:虽然理想状态是要求中断处理函数执行的越快越好,
但某些场合是无法满足这个要去的,比如网卡接收数据的过程,
如果网卡对应的中断处理程序长时间的占有CPU的资源,
就会影响系统的并发和响应能力,怎么办呢?
答:如果对于这种情况,可以采用顶半部+底半部的实现方法
其实就是将以前的中断处理程序分为两部分:
顶半部:就是中断处理程序,做一些比较紧急的事情,
比如将网卡数据包从网卡硬件缓冲区拷贝到主存中,
这个过程不可中断,然后一定要在顶半部中登记底半部
要做的事,CPU会在空闲,适当的时候再去执行底半部剩余的工作。
底半部:做一些不紧急,相对耗时的工作,比如将数据包
提交给协议层的过程,这个过程可以被别的新的中断所打断
问:底半部是如何的实现呢?
问:顶半部和底半部如何关联的呢?
答:底半部的实现机制如下:
tasklet
工作队列
软中断
问:tasklet怎么玩?
答:
按键中断测试步骤:
1.insmod btn_drv.ko
2.cat /proc/interrupts //查看中断的注册信息
中断号 中断触发的次数 中断类型 中断名称
CPU0
16: 137 s3c-uart s5pv210-uart
18: 273 s3c-uart s5pv210-uart
32: 0 s5p_vic_eint KEY_UP
1.设备节点、设备文件
问:设备文件干什么用?
答:用于表示设备,用户访问设备文件,就是在访问设备
问:设备文件在哪里?
答:在/dev
问:设备文件如何创建?
答:手工创建mknod /dev/设备文件名 <c|b> 主设备号 次设备号
自动创建
2.设备号
问:设备号包含什么内容?
答:包含主,次设备号
问:主,次设备号的作用?
答:应用程序通过主设备号找到驱动程序
应用程序通过次设备号找到同类型设备的不同个体,比如串口
问:由于设备号对于内核来说是一种资源,如何向内核申请?
答:静态申请和动态申请
静态申请:事先查看当前系统里哪个主设备号是空闲的
优点是可以提前创建好设备文件,缺点不便于驱动的推广
动态申请:让内核帮你申请一个设备号
优点是便于驱动的推广,缺点不能提前创建好设备节点
问:设备号数据类型是什么?
答:dev_t 本质上是一个unsigned int
12-》主
20-》次
MAJOR
MINOR
MKDEV
3.字符设备四个重要数据结构
struct inode:
问:这个结构描述什么内容?
答:用于描述一个文件的物理特性,一个文件有一个对应的inode
问:生命周期?谁来创建?
答:文件存在,它存在,文件销毁,它销毁;
内核创建
问:有哪些重要的成员?
答:i_rdev(存放设备文件对应的设备号信息)
i_cdev(如果这个设备文件是字符设备文件,
它存放的就是字符设备驱动对应的数据结构cdev)
用于缓存
struct file
问:描述什么属性?
答:描述设备文件被成功打开以后的状态属性
问:生命周期?谁来创建?
答:文件打开以后,内核创建
文件关闭以后,内核进行销毁
问:有哪些重要的成员?
答:f_op(存放底层驱动的硬件操作集合)
f_flags(存放应用程序调用open时指定的操作属性,O_RDWR|O_NONBLOCK)
f_pos(存放文件操作的位置信息)
private_data文件私有数据?
struct cdev
问:描述什么内容?
答:描述一个字符设备
问:生命周期?谁来创建?
答:由驱动程序在加载函数中完成cdev的分配,初始化和注册工作
由驱动程序在卸载函数中完成cdev的删除操作,一旦删除
,内核就不再存在一个字符设备设备驱动
问:有哪些重要成员?
答:ops(存放底层驱动的硬件操作集合)
dev(存放设备号)
count(存放设备的个数)
问:cdev涉及的相关操作函数
答:cdev_init
cdev_add
cdev_del
问:如何完成cdev的注册过程?
答:当调用cdev_add时,根据设备号,将cdev
添加到内核事先定义好一个cdev的数组中,以后
应用程序就可以通过设备号来访问这个数组,获取
对应的cdev,也就是获取到对应的字符设备驱动
struct file_operations
问:描述什么内容?
答:它仅仅就是包含了一堆的硬件操作的接口
问:应用程序如何和这些接口对应的呢?
答:一一对应
open->sys_open->.open = x_open
close->sys_close->.release = x_close
read->sys_read->.read = x_read
write->sys_write->.write = x_write
ioctl->sys_ioctl->.ioctl = x_ioctl
问:应用程序到sys_系统调用函数这个过程
是内核已经帮你实现,sys_open->.open = x_open
是如何关联的呢?
答:答案参看day03.txt笔记
4.如何实现一个字符设备驱动
案例:按键驱动的一个版本
要求:能够获取按键值
按键按下,获取的键值为0x50
按键松开,获取的键值为0x51
1.了解硬件原理
2.驱动编写过程
1.分配初始驱动操作集合
struct file_operations btn_fops = {
.owner = THIS_MODULE,
.open = btn_open,
.release = btn_close,
.read = btn_read
};
2.分配cdev
3.在加载函数中
分配设备号
初始化cdev
注册cdev
4.在卸载函数中
删除cdev
释放设备号
5.填充btn_open
申请GPIO资源GPH0_0
配置为输入口
6.填充btn_close
释放GPIO资源
7.填充btn_read
读取GPIO的管脚状态
判断管脚状态
如果是1:把0x51 copy_to_user到用户空间
如果是0:把0x50 copy_to_user到用户空间
实验步骤:
1.去除官方按键驱动
make menuconfig //在内核源代码根目录
Device Drivers->
Input device support --->
[*] Keyboards --->
[*] S3C gpio keypad support //去掉
make zImage
cp arch/arm/boot/zImage /tftpboot
重启开发板,用新内核引导
2.insmod btn_drv.ko
3.cat /proc/devices //查看申请主设备号
4.mknod /dev/buttons