驱动第一天
一、驱动编程
[1] 虚拟地址空间分配
内核空间: 3G - 4G
应用程序空间: 0-3G
[2] 应用程序栈 VS 内核线程栈
应用程序栈大,内核线程栈小
[3] 内核中3G开始,物理地址映射区
物理地址映射区就是直接把一块物理内存(低地址位置),固定映射到3G开始的地方(768M)
[4] 驱动工作方式
应用程序不能完成的功能会请求内核完成,内核不能完成的功能请求驱动完成
【驱动模块管理】
[1] 加载函数(初始化函数)
模块加载的时候调用的
[2] 卸载函数(退出函数)
模块卸载的时候调用的
[3]模块的动态加载和卸载命令
sudo insmod hello.ko 加载命令
sudo rmmod hello 卸载命令
sudo lsmod | grep hello 查询模块加载情况
[4] 源代码跟踪(利用tags工具)(VI必须配置好)
1. 到达内核的源代码目录
2. 用vim打开任意的源代码文件
3. 按F9来生成tags文件
4. 用vim打开模块源代码,然后用下面命令设置tags文件的位置
:set tags=内核源代码目录/tags
5. 在模块源代码里面按ctrl + ] 组合键来查找对用的源代码定义
[5] 理解module_init和module_exit这个两个宏的用途,无需理解原理
[6] 模块信息查询
modinfo 模块名.ko
[7] 模块参数
1. 模块参数定义
module_param(参数名, 类型, 权限)
module_param_array(数据名, 成员类型, 接收数据参数个数, 权限)
2. 模块参数传参
sudo insmod hello.ko myint=100
sudo insmod hello.ko array=5,6,7,8
3. 模块参数描述
MODULE_PARM_DESC(参数名, 参数描述)
4. 系统中参数查看
权限:ls -l /sys/module/模块名/parameters/参数名 (参数是用文件来表示的)
值:cat /sys/module/模块名/parameters/参数名
【字符设备驱动功能】
[1] 分类
1.字符设备
2.块设备
3.网络设备
[2] 驱动功能
[字符设备驱动]
实现字符设备初始化、接收发送数据、释放设备需要的资源
[块设备驱动]
从块设备上读取一块或者多块数据到buffer cache
从buffer cache获得数据,写到块设备
[网络设备驱动]
发送网络协议栈产生的协议包到网络
接收网络协议包,交给网络协议栈解析
[3] 设备号
[作用]
帮助我们找到要操作设备
[申请]
操作是否成功 = register_chrdev_region(起始设备号, 设备的数量, “设备号名字”)
if (操作是否成功 < 0) {
打印错误
}
[查看设备号是否申请成功]
cat /proc/devices
主设备号 名字
[释放]
unregister_chrdev_region(起始设备号, 设备的数量);
二、驱动源代码结构
// 所有模块都必须加的头文件
#include <linux/module.h>
#include <linux/kernel.h>
// 模块有定义参数需要加这个头文件
#include <linux/moduleparam.h>
// 模块参数定义(可选)
// myshort为参数名称
short int myshort = 1;
int array[10];
// module_param(参数名, 类型, 权限)
module_param(myshort, short, 0400);
// MODULE_PARM_DESC(参数名, 参数描述)
MODULE_PARM_DESC(myshort, "A short integer");
// module_param_array(数组名, 成员类型, 数组传参个数, 权限)
module_param_array(array, int, &arr_argc, 0400);
MODULE_PARM_DESC(array, "An array of integers");
// 主次设备号变量定义
int kernel_major = 250;
int kernel_minor = 0;
// __init 告诉内核这个函数在模块加载之后,没有用了
int __init kernel_init(void)
{
设备号的合成
申请设备号
return 0;
}
// __exit 告诉内核这个函数式模块的卸载函数,在模块被静态编译到内核时,无需这个函数
void __exit kernel_exit(void)
{
释放设备号
}
// 指定模块初始化函数
module_init(kernel_init);
// 指定模块的卸载函数
module_exit(kernel_exit);
MODULE_LISENCE("GPL"); // 必须的
// 可选
MODULE_AUTHOR(作者);
MODULE_DESCRIPTION(模块描述);
MODULE_SUPPORTED_DEVICE(模块设备描述);
三、字符设备
【inode结构】
[头文件]
#include <linux/fs.h>
struct inode {
umode_t i_mode; // 确认文件的类型
dev_t i_rdev; // 设备文件节点的设备编号
union {
struct cdev *i_cdev; // 字符设备内部结构
};
};
【文件结构】
[头文件]
#include <linux/fs.h>
注意:这个结构跟用户空间的FILE没有任何关系,FILE是在C库中定义的
struct file {
// 文件模式:文件的权限内核在调用file_operations接口之前就已经验证,如果不允许,内核直接拒绝
// FMODE_READ 可读
// FMODE_WRITE 可写
mode_t f_mode;
// 当前读写位置,64位的(long long),驱动可以读取它来获得文件的当前位置,但是不要改变它,只有在llseek函数中才修改,他是用来改变文件位置指针
loff_t f_pos;
// 文件标志: 所有标志在linux/fcntl.h头文件中定义
// O_NONBLOCK 非阻塞(最常用)
// O_ACCMODE(3) 取出访问模式位
// O_RDONLY(00) 只读方式打开
// O_WRONLY(01) 只写方式打开
// O_RDWR(02) 读写方式打开
unsigned int f_flags;
// 驱动操作接口,为文件操作提供响应
struct file_operations *f_op;
// open 系统调用设置这个指针为 NULL, 私有数据,通常用来保存驱动自己的数据
void *private_data;
// 关联到文件的目录入口( dentry )结构, 驱动一般只用到filp->f_dentry->d_inode存取inode结构
struct dentry *f_dentry;
};
概述:这个结构用来在内核中描述一个打开的文件,内核在open文件时会创建它,并且会将其在文件上操作的任何函数,直到最后的关闭,一定要所有打开该文件的程序都关闭该文件后才释放这个结构;filp通常表示这个结构的指针;
struct cdev {
struct kobject kobj; // 内嵌的kobject对象
struct module *owner; // 所属模块
struct file_operations *ops; // 文件操作结构体
struct list_head list;
dev_t dev; // 设备号
unsigned int count;
};
【字符设备创建】
[1] 【设备结构分配和初始化】
[2] 【文件操作接口】实现
[3] 【设备编号注册】
[4] 【字符设备添加】
[5] 【设备文件节点创建】
【字符设备销毁】
[1] 【设备文件节点销毁】
[2] 【字符设备删除】
[3] 【设备编号回收】
【设备编号获取】
[头文件]
#include <linux/types.h> dev_t定义
#include <linux/kdev_t.h> 操作宏定义
[宏]
/*
* @brief 获取主编号
* @param[in] dev 设备编号
* @return 主编号
*/
MAJOR(dev_t dev);
/*
* @brief 获取次编号
* @param[in] dev 设备编号
* @return 次编号
*/
MINOR(dev_t dev);
/*
* @brief 获取设备编号
* @param[in] major 主编号
* @param[in] minor 次编号
* @return 设备编号
*/
MKDEV(major, minor); 获取编号
[头文件]
#include<linux/fs.h>
[函数]
/*
* @brief 获取次编号
* @param[in] inode 设备文件节点
* @return 次编号
*/
static inline unsigned iminor(const struct inode *inode);
/*
* @brief 获取主编号
* @param[in] inode 设备文件节点
* @return 主编号
*/
static inline unsigned imajor(const struct inode *inode);
【设备编号注册/回收】
[头文件]
#include <linux/fs.h>
[函数]
/*
* @brief 分配设备编号
* @param[in] first 起始设备编号
* @param[in] count 分配编号数量
* @param[in] name 设备名称(在/proc/devices文件中可见)
* @return =0 分配成功
* <0 错误码
*/
int register_chrdev_region(dev_t first, unsigned int count, char *name);
/*
* @brief 动态分配设备编号
* @param[out] dev 设备编号
* @param[in] firstminor 分配第一个次编号
* @param[in] count 分配编号数量
* @param[in] name 设备名称(在/proc/devices文件中可见)
* @return =0 分配成功
* <0 错误码
*/
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
/*
* @brief 回收设备编号
* @param[in] first 第一个次编号
* @param[in] count 分配编号数量
*/
void unregister_chrdev_region(dev_t first, unsigned int count);
【设备结构构造】
[头文件]
#include <linux/cdev.h>
[函数]
/*
* @brief 动态分配struct cdev结构(描述一个字符设备)
* @return struct cdev结构指针
*/
struct cdev *cdev_alloc(void);
/*
* @brief 初始化struct cdev结构
* @param[in] cdev struct cdev结构已经定义存在
* @param[in] fops 驱动操作接口,为文件操作提供响应
* @notes 函数会自动做cdev->ops = fops;但是没有dev->cdev.owner = THIS_MODULE;
*/
void cdev_init(struct cdev *cdev, struct file_operations *fops);
【字符设备添加/删除】
[头文件]
#include <linux/cdev.h>
[函数]
/*
* @brief 增加一个字符设备到系统
* @param[in] p 描述字符设备的struct cdev结构
* @param[in] dev 关联到这个设备的第一个设备号
* @param[in] count 关联到这个设备的设备号个数
* @return 0 添加成功
* <0 错误码
* @notes 函数执行成功后,立即可以使用该字符设备,也就是说调用这个之前设备驱动已经完全就绪
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
[函数]
/*
* @brief 删除一个字符设备
* @param[in] cdev 描述字符设备的struct cdev结构
*/
void cdev_del(struct cdev *dev);
【文件操作接口】
[系统调用]
头文件:<linux/ioctl.h>
/*
* @brief 提供各种硬件控制能力
* @param[in] fd 文件描述符
* @param[in] cmd 控制命令
* @param[in | out] ... 不表示参数个数不定,只是不让编译器检查这个参数的类型
* @return 0 操作成功
* <0 错误码
*/
int ioctl(int fd, unsigned long cmd, ...);
[驱动接口]
/*
* @brief 提供各种硬件控制能力
* @param[in] inode 文件节点
* @param[in] filp 文件结构,用来在内核中表示一个打开的文件
* @param[in] cmd 控制命令,就是系统调用的cmd参数
* @param[arg] arg 命令参数,对应系统调用的...
* @return 返回给系统调用
* 0 操作成功
* <0 错误码
* 无效命令 -ENIVAL 或者 -ENOTTY
*/
int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
[cmd]
+--------------+---------+-----------+--------+
| direction | size | type | number |
+--------------+---------+-----------+--------+
|<-----2------>|<---14-->|<----8---->|<--8--->|
<type> 魔数,见Documentation/ioctl/ioctl-number.txt的安排
<number> 顺序号,本驱动里的第几条命令
<direction> 方向,_IOC_NONE,_IOC_WRITE和_IOC_READ
<size> 涉及用户数据的大小
【设备文件节点创建】
[头文件]
#include <inux/device.h>
[函数]
/*
* @brief 建立一个类结构
* @param[in] owner 模块所有者
* @param[in] name 类名
* @return 类结构
* IS_ERR(返回值) 判断是否是错误码
* PTR_ERR(返回值) 取得错误码
*/
struct class *class_create(struct module *owner, const char *name);
/*
* @brief 建立设备并且注册它到sysfs中
* @param[in] class 设备所要注册到的类
* @param[in] parent 父设备
* @param[in] devt 字符设备号
* @param[in] drvdata 驱动私有数据
* @param[in] fmt 格式化的设备名
* @return 返回设备结构
* IS_ERR(返回值) 判断是否是错误码
* PTR_ERR(返回值) 取得错误码
*/
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...);
【设备文件节点销毁】
[头文件]
#include <inux/device.h>
[函数]
/*
* @brief 设备销毁
* @param[in] class 设备所属类
* @param[in] devt 设备号
*/
void device_destroy(struct class * class, dev_t devt);
/*
* @brief 类销毁
* @param[in] class 要销毁的类
*/
void class_destroy (struct class * cls);
四、驱动基础知识图解
1. Linux系统结构图
2. Linux内核功能模块图
3. Linux系统内存结构图
4. Linux内核层次结构图
5. 驱动程序VS应用程序
6. 驱动作用原理
五、典型Makefile详解
#打印$(KERNELRELEASE)变量中的值
$(warning KERNELRELEASE=$(KERNELRELEASE))
#判断是否被内核顶层目录的Makefile所调用
ifeq ($(KERNELRELEASE),)
#KERNELDIR ?= /home/lht/kernel2.6/linux-2.6.14
#内核源代码位置(内核顶层目录下的Makefile位置)
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
#KERNELDIR ?= /home/linux/work/kernel/linux-2.6.35
PWD := $(shell pwd)
modules:
# 调用内核顶层目录下的Makefile,然后通知内核顶层目录下的Makefile调用当前路径
# 下的Makefile完成内核模块的编译
# -C 指定运行的Makefile所在的目录
# M= 指定要编译的内核模块所在的位置
# modules 告诉内核顶层目录的Makefile,当前编译的是内核模块
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
#清楚编译出来的所有文件
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions Module* modules*
#声明伪目标
.PHONY: modules modules_install clean
else
#内核模块Makefile内容
obj-m := hello.o
endif