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 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怎么玩?
答:
数据结构
struct tasklet_struct
{
//tasklet的处理函数,底半部要完成的任务
就在这个函数中来实现
void (*func)(unsigned long);
//给处理函数传递的参数,一般传递指针
unsigned long data;
};
问:如何使用?
答:1.分配
2.初始化
方法1:
DECLARE_TASKLET( taskletname,
tasklet_func,
data);
方法2:
struct tasklet_struct mytasklet;
tasklet_init(&mytasklet, tasklet_func, data);
3.在中断处理函数中(顶半部)进行登记,切记登记
而不是调用,CPU会在适当的时候会调用tasklet的处理函数
tasklet_schedule(&mytasklet); //完成登记
4.tasklet的处理函数还是工作在中断上下文中
问:如果在底半部的处理过程中,有睡眠的情况怎么办?
答:这个时候考虑使用工作队列。
问:工作队列使用?
答:工作数据结构:
struct work_struct:关心其中的处理函数这个字段
延时的工作数据结构
struct delayed_work:能够指定在哪个时刻去执行对应的处理函数
问:如何使用呢?
答:1.分配工作或者延时工作
struct work_struct mywork;
struct delayed_work mydwork;
2.初始化工作和延时工作
INIT_WORK(&mywork, my_work_func);
INIT_DELAYED_WORK(&mydwork, my_dwork_func);
3.在中断处理函数(顶半部)登记工作或延时工作
schedule_work(&mywork);//CPU会在适当的时候
会调用执行对应的处理函数
或者
schedule_delayed_work(&mydwork, 5*HZ);
//CPU会在5s以后执行对应的处理函数
问:调用schedule_work或者schedule_delayed_work
都会将工作和延时工作交给内核默认的工作队列和内核
线程去执行,默认的内核线程(events/0) ,如果
都将工作和延时工作交给内核默认的线程去处理,无形
会增大它的负载。对于这种情况怎么办呢?
答:自己创建自己的内核线程和工作队列,然后将
自己的工作交给自己的工作队列和内核线程来处理即可,
减轻了内核默认的线程和默认的工作队列的负担。
问:如何实现?
答:1.分配工作和延时工作
2.初始化工作和延时工作
3.创建自己的工作队列和内核线程
工作队列指针 = create_workqueue(线程名);
4.在中断处理函数中(顶半部)进行关联和登记工作
queue_work(自己的工作队列,自己的工作);
或者
queue_delayed_work(自己的工作队列,自己的延时工作,时间间隔);
按键中断测试步骤:
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.系统定时器硬件
通过软件可以设置定时器硬件的工作频率
周期性的发生时钟中断
2.既然是时钟中断,在内核里必然有对应的
中断处理程序,这个程序做什么事呢?
1.更新系统的运行时间
2.更新实际时间
3.检查进程的时间片信息
4.执行超时的定时器
5.做相关的统计信息
...
3.时间处理相关的概念:
HZ:用于设置硬件定时器的工作频率
ARM:HZ=100,表明一秒钟能发生100时钟中断
tick:1/HZ,发生一次时钟中断所需的时间间隔
ARM:HZ=100,1tick= 10ms
jiffies:内核的32全局变量,用来记录自开机以来
发生了多少次时钟中断,一般内核用它来表示时间
unsigned long timeout = jiffies + HZ/2;//500ms以后的时间
jiffies注意事项:回绕问题
time_after/time_before
内核定时器:
1.数据结构:
struct timer_list {
unsigned long expires; //超时时候jiffies的值
function; //超时处理函数
data; //给超时处理函数传递的参数,一般传递指针
} ;
2.如何使用定时器呢?
1.分配定时器
struct timer_list mytimer;
2.初始化定时器
init_timer(&mytimer);//驱动关心的三个字段需要另外指定
//指定超时时候的时间
mytimer.expires = jiffies + 5*HZ;
//指定超时处理函数
mytimer.function = mytimer_func;
//指定给超时处理函数传递的参数
mytimer.data = (unsigned long)&mydata;
3.向内核添加启动定时器
add_timer(&mytimer);//后续关于定时器的处理
都是内核来做(时钟中断处理程序)
一旦定时器到期,内核执行超时处理函数
4.如果要修改定时器
mod_timer(&mytimer, jiffies + 2*HZ);
设置新的超时时间为2s以后
mod_timer = del_timer +
expires + jiffies + 2*HZ +
add_timer
5.删除定时器 del_timer(&mytimer);
案例:要求加载驱动程序以后,每隔2s
打印一句话
案例:要求加载驱动程序以后,每隔2s开关灯
案例:要求加载驱动程序以后,能够动态修改
灯的闪烁频率 1000ms,2000ms,5000ms,500ms,200ms
提示:不能使用字符设备驱动框架,
利用模块传参的方式实现频率的修改
提示ms的处理用如下宏:
msecs_to_jiffies(6000);6000表示的6000ms
linux内核并发和竞态:
1.概念
并发
竞态
共享资源:硬件和软件上的全局变量
互斥访问
临界区
2.哪些情况会产生竞态
1.SMP
2.进程和进程
3.进程和中断
4.中断和中断
3.linux内核提供互斥访问的机制
中断屏蔽
问:中断屏蔽能够解决哪些情况的竞态问题?
答:进程和进程,中断和进程,中断和中断
内核提供了一组操作方法:
unsigned long flags;//用于保存中断状态信息
local_irq_save(flags);
临界区
local_irq_restore(flags);
使用注意事项:
由于中断对于操作系统的运行至关重要,所以
长时间的屏蔽中断是危险的,所以要求屏蔽
中断以后,内核执行的路径的速度要快,尽快的
恢复中断。
原子操作
问:能够解决哪些场合的竞态
答:所有场合的竞态情况
位原子操作
以后在驱动开发中,如果涉及到对共享资源
进行位运算,要考虑使用位原子操作的方法
set_bit
clear_bit
change_bit
test_bit
案例:要求将数值0x5555->0xaaaa
不允许使用change_bit
请自己实现set_bit,clear_bit,change_bit,test_bit
整型原子操作
自旋
锁
信号量
并发和竞态 1.概念 并发 竞态 共享资源 互斥访问 临界区 2.linux系统,哪些情况会产生竞态 SMP 进程和进程之间的抢占 中断和进程 中断和中断 3.linux系统,提供了哪些互斥机制 中断屏蔽 原子操作 位原子操作 set_bit(nr, void *addr); clear_bit change_bit test_bit 整型原子操作 数据类型:atomic_t 如何使用: 1.分配初始化 atomic_t v = ATOMIC_INIT(1);//int open_cnt = 1 或者: atomic_t v; atomic_set(&v, 1); 2.内核提供的整型原子操作的方法: atomic_set atomic_read atomic_add atomic_sub atomic_inc atomic_dec ... --open_cnt:不是原子的 案例:要求LED灯设备只能被一个应用软件所打开 驱动设计: 1.分配整型原子变量v 2.分配初始化struct file_operations; 3.分配struct cdev 4.在加载函数中完成: 申请设备号 初始化注册cdev 初始化整型原子变量v=1 创建设备类(树枝) 创建设备节点 5.在卸载函数中完成: 删除设备节点 设备设备类 卸载cdev 释放设备号 6.led_open: 操作v并判断其值, 根据值来判断设备的打开状态 7.led_close:
标签: a2传感器二线交流传感器d10轴位移传感器lh系列高精度位移传感器