LinuxLinux字符设备驱动程序字符设备驱动程序
Linux驱动程序的分类
字符设备驱动:用于驱动能够像字节流(文件)一样被访问
的设备。应用程序通常可以利用open、close、read、write
等系统调用访问字符设备驱动。
块设备驱动:块设备和字符设备只在系统内核内部的管理上
有所区别。应用程序对于字符设备的每一个I/O操作都会被
内核直接传递给对应的驱动程序;而应用程序对于块设备的
操作要经过虚拟文件系统(VFS)和缓冲区管理系统间接地
传递给驱动程序处理。
网络设备驱动:应用程序必须利用套接字(socket)接口访问
网络设备。
网络设备驱动程序
字符设备驱动程序基本结构
字符设备开发的基本步骤
确定主设备号和次设备号
实现字符驱动程序
实现file_operations结构体
构造字符设备结构体cdev
在模块加载函数中注册字符设备
在模块卸载函数中注销字符设备
创建设备文件节点
设备文件与设备号
为了体现“一切都是文件”的设计思想,linux将每个已安装
的设备都表示为一个设备文件。
设备文件通常位于/dev子目录。
对于字符设备,应用程序可以利用open、close、read、
write等系统调用访问其设备文件,这些I/O操作都被直接传递
给该设备文件所对应的设备。
每个设备文件中都存储了该设备的“主设备号”和“次设备
号”。
一般由同一个内核模块管理的多个设备占用同一个主设备号,
具体设备用次设备号标识。
用mknod filename c major minor命令创建设备文件
用rm filename命令删除设备文件。注意删除设备文件并不会
影响驱动模块。
应用程序如何访问设备
fd1 = open(“/dev/ttyS1”, O_RDWR); // 阻塞
fd2 = open(“/dev/ttyS1”, O_RDWR | O_NONBLOCK); // 非阻塞
int read(int fd, const void *buf, size_t length);
int write(int fd, const void *buf, size_t length);
int lseek(int fd, offset_t offset, int whence);
int ioctl( int fd, int cmd, void *arg);
int close(int fd);
设备号的内部表达
设备编号的内部表达
dev_t类型(32位):
用来保存设备编号(包括主设备号(12位)和次设备
号(20位))
从dev_t获得主设备号和次设备号:
MAJOR(dev_t);
MINOR(dev_t);
将主设备号和次设备号转换成dev_t类型:
MKDEV(int major,int minor);
主设备号与次设备号
分配主设备号
手工分配主设备号:找一个内核没有使用的主设备号来使用。
#include <linux/>
int register_chrdev_region( dev_t first, unsigned int count, char *name );
要分配的设备编
号范围的起始值,
次设备号经常为0
所请求的连续设
备编号的个数
和该编号范围关
联的设备名称
主设备号与次设备号
动态分配主设备号:
#include <linux/>
int alloc_chrdev_resion(dev_t *dev,unsigned int firstminor,
unsigned int count,char *name);
输出的设备号
要使用的被请求的
第一个次设备号
主设备号与次设备号
释放设备号
void unregister_chrdev_region(dev_t first, unsigned int count);
通常在模块的清
除函数中调用。
记录字符设备的结构体cdev
实现字符驱动程序
cdev 结构体
struct cdev
{
struct kobject kobj; /* 内嵌的kobject 对象
*/
struct module *owner; /*所属模块*/
struct file_operations *ops; /*文件操作结构体*/
struct list_head list;
dev_t dev; /*设备号*/
unsigned int count;
};
cdev的kobj、 list 、 count字段不用我们关系和维护(内核代
劳),我们只需将其ops字段指向为我们自己的file operations
结构。
对cdev结构体的操作
操作cdev的函数
void cdev_init( struct cdev *, struc t file_operations *);
struct cdev *cdev_alloc(void) ;
int cdev_add(st ruct cdev *, dev_t, unsigned) ;
void cdev_del(struct cdev *);
用于初始化cdev的成员,
并建立cdev和
file_operations之间的连接
分别向系统删除一个cdev
,完成字符设备的注销,通
常在模块的卸载函数中调用
分别向系统添加一个cdev
,完成字符设备的注册,通
常在模块加载函数中调用
函数用于动态申请一个cdev 内存
file_operations 结构体
file_operations 结构体
字符驱动和内核的接口:
在include/linux/定义
字符驱动只要实现一个file_operations结构体并注册到内核中,
内核就有了操作此设备的能力。
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
// ………………………
};
file_operations 结构体
file_operations的主要成员:
struct module *owner: 指向模块自身
open:打开设备
release:关闭设备
read:从设备上读数据
write:向设备上写数据
ioctl:I/O控制函数
llseek:定位读写指针
mmap:映射设备空间到进程的地址空间
file operations的初始化
struct file_operations my_fops = {
.owner = THIS_MODULE,
.llseek = my_llseek,
.read = my_read,
.write = my_write,
.ioctl = my_ioctl,
.open = my_open,
.release = my_release,
};
file结构体
file结构代表一个打开的文件,它由内核在应用程序
open时创建,并将该文件所对应的file operations记
录在file结构中。
在应用程序调用close函数,内核会释放该数据结构。
struct file {
struct file_operations *f_op;
unsigned int f_flags;
fmode_t f_mode;
loff_t f_pos;
struct dentry *f_dentry
void* private_data;
};
file结构体
file 结构体
file结构:
file_operations结构相关的一个结构体。
描述一个正在打开的设备文件。
成员:
loff_t f_pos:
当前读/写位置
unsigned int f_flags
标识文件打开时,是否可读或可写
O_RDONLY
O_NONBLOCK
O_SYNC
struct file_operations *f_op
文件相关的操作,指向所实现的struct file_operations
void *private_data:
私有数据指针。驱动程序可以将这个字段用于任何目的或者忽略这个字段。
inode结构体
内核用inode结构在内部表示文件,用于存储文件访问权限、
属主、组、大小、生产时间等VFS关心的信息。
其字段中我们只关心i_rdev(设备号),和i_cdev(和该文件所
对应的cdev结构)
我们在创建设备文件时,内核会自动创建一个对应的inode结
构体,并将其i_cdev字段指向对应的字符设备结构体cdev(事
先已经在内核中注册过)。
Inode与file的区别:file表示打开的文件描述符,多个file结构,
可以指向单个inode结构。
struct inode {
dev_t i_rdev;
struct cdev *i_cdev;
// ………………………
};
Inode结构体
Inode结构中的两个主要字段:
dev_t i_rdev;
对表示设备文件的inode结构,该字段包含了真正的设备编号。
struct cdev *i_cdev;
struct cdev是表示字符设备的内核的内部结构。
当inode指向一个字符设备文件时,该字段包含了指向struct cdev结构的指针
从一个inode中获得主设备号和次设备号:
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
字符设备驱动程序基本结构
注册设备 ,在模块或驱动初始化时调用
及之前
int register_chrdev(unsigned int major,
const char *name,
struct file_operations *fops)
如何操作字符设
备的接口
void cdev_init( struct cdev *, struc t file_operations *);
int cdev_add(st ruct cdev *, dev_t, unsigned) ;
字符设备驱动程序基本结构
注销设备:在模块卸载时调用
及之前
int unregister_chrdev(unsigned int major,
const char *name);
void cdev_del (struct cdev *);
字符设备驱动程序基本结构
/ /设备驱动模块加载函数
static int __init xxx_init(void)
{
...
cdev_init(&, &xxx_fops); / /初始化cdev
= THIS_MODULE;
/ /获取字符设备号
if (xxx_major)
{
register_chrdev_region(xxx_dev_no, 1, DEV_NAME);
}
else
{
alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME);
}
ret = cdev_add(&, xxx_dev_no, 1 ) ; / /注册设备
...
}
字符设备驱动程序基本结构
/*设备驱动模块卸载函数*/
static void __exit xxx_exit(void)
{
unregister_chrdev_region(xxx_dev_no, 1); / /释放占用的设备号
cdev_del(&); / /注销设备
...
}
字符设备驱动程序基本结构
打开
模块使用计数加1
识别次设备号
硬件操作:
检查设备相关错误(诸如设备未就绪或类似的硬件问题);
如果设备是首次打开,则对其初始化;
如果有中断操作,申请中断处理程序;
int open(struct inode *inode, struct file *filp) ;
字符设备驱动程序基本结构
关闭
模块使用计数减1
释放由open分配的,保存在filp>private_data里的所有内容。
硬件操作:
如果申请了中断,则释放中断处理程序。
在最后一次关闭操作时关闭设备。
int release(struct inode *inode, struct file *filp) ;
字符设备驱动程序基本结构
read/write
ssize_t read(struct file *filp, char __user *buff, size_t count, loff_t *offp);
ssize_t write(struct file *filp, const char __user *buff, size_t count, loff_t *offp);
指向用户空间的缓冲区,这个缓冲
区或者保存将写入的数据,或者是
一个存放新读入数据的空缓冲区。
用户在文件中存取
操作的位置
字符设备驱动程序基本结构
用户空间和内核空间之间的数据拷贝过程,
不能简单的用指针操作或者memcpy来进行数据拷贝
用户空间的数据是可以被换出的,会产生一个页面失效异常。
用户空间的地址无法在内核空间中使用。
用户空间和内核空间之间进行数据拷贝的函数:
如果要复制的内存是简单类型,如char、int、long 等,
put_user()和get_user()
unsigned long copy_from_user(void *to, const void __user *from,
unsigned long count) ;
unsigned long copy_to_user(void __user *to, const void *from,
unsigned long count );
字符设备驱动程序基本结构
读设备模板
ssize_t xxx_read(struct file *filp, char __user *buf,
size_t count ,loff_t*f_pos)
{
...
copy_to_user(buf, ..., ... );
...
}
写设备模板
ssize_t xxx_write(struct file *fil p, const char __user *buf ,
size_t count ,loff_t *f_pos)
{
...
copy_from_user(..., buf, ... );
...
}
字符设备驱动程序基本结构
ioctl函数
为设备驱动程序执行“命令”提供了一个特有的入口点
用来设置或者读取设备的属性信息。
int ioctl (struct inode *inode, struct file *filp,
unsigned int cmd, unsigned long arg);
事先定义的IO控
制命令 代码
arg为对应于cmd
命令的参数
字符设备驱动程序基本结构
cmd 参数的定义
不推荐用0x1,0x2,0x3之类的值
Linux对ioctl()的cmd参数有特殊的定义
构造命令编号的宏:
_IO(type,nr)用于构造无参数的命令编号;
_IOR(type,nr,datatype)用于构造从驱动程序中读取数据的命令编号;
_IOW(type,nr,datatype)用于写入数据的命令;
_IOWR(type,nr,datatype)用于双向传输。
type和number位字段通过参数传入,而size位字段通过对datatype参数取sizeof获得。
设备类型(type) 序列号(number) 方向(direction) 数据尺寸(size)
8bit 8bit 2bit 13/14bit
字符设备驱动程序基本结构
Ioctl函数模板
int xxx_ioctl( struct inode *inode, struct f ile *filp, unsigned int cmd,
unsigned long arg)
{
...
switch (cmd)
{
case XXX_CMD1:
...
break;
case XXX_CMD2:
...
break;
default: ///*不能支持的命令 */
return - ENOTTY;
}
return 0;
}
阶段总结
阶段总结
添加驱动程序到内核
配置内核
编译内核
添加驱动程序到内核中
添加驱动程序到内核
配置内核
配置命令包括:
make config
make menuconfig
make xconfig
make gconfig
可通过“上”、“下”、“左”、“右”键移动菜单,选择某项按“Y”,取消
选择按“N”,如果选择某项编译为模块按“M”,进入子菜单按“Enter”,返回
上一级菜单按 “Esc”
使用make config、make menuconfig等命令后,会生成一个.config配置文件(是
隐身文件,通过ls –a才能看到)
添加驱动程序到内核
编译内核
可用如下命令编译内核:
make ARC=arm CROSS_COMPILE=arm-linux- zImage
源代码根目录的Makefile中将ARCH和CROSS_COMPILE直接指定为arm和arm-
linux-,如:
这样就没有必要每次编译的时候都指定体系结构和交叉
编译器了,只须使用下面命令就可以了:
make zImage
ARCH ?= arm
CROSS_COMPILE ?= arm-linux-
添加驱动程序到内核
添加驱动程序到内核
Linux 内核的配置系统由以下3个部分组成。
Makefile:分布在Linux内核源代码中的Makefite
定义Linux内核的编译规则
配置文件(Kconfig):给用户提供配置选择的功能。
配置工具:
包括配置命令解释器(对配置脚本中使用的配置命令进行解释)
配置用户界面(提供字符界面和图形界面)。
这些配置工具都是使用脚本语言编写的,如Tcl/TK、Perl等。
在Linux内核中增加程序需要完成以下3项工作。
将编写的源代码复制到Linux内核源代码的相应目录。
在目录的Kconfig文件中增加新源代码对应项目的编译配置选项。
在目录的Makefile文件中增加对新源代码的编译条目。
添加驱动程序到内核
实例:在内核源代码drivers目录下为ARM体系结构新增test driver
test driver的树形目录:
步骤:
1、拷贝test到drivers路径下
2、为新增目录创建Kconfig和Makefile
3、修改新增目录父目录的Kconfig和
Makefile,以便新增的Kconfig和
Makefile能够被引用
4、在arch/arm/Kconfig里增加
source “drivers/test/Kconfig”
添加驱动程序到内核
步骤:
1、拷贝test到drivers路径下
cp –fr test linux_kernel_path/drivers
2、为新增目录创建Kconfig和Makefile
添加驱动程序到内核
修改新增目录的父目录的Kconfig和Makefile
在drivers/Kconig中加入:source "drivers/test/Kconfig“
在drivers/Makefile中加入:obj-$(CONFIG_TEST) += test/
在arch/arm/Kconfig里加入:source “drivers/test/Kconfig”
增加了Kconfig和Makefile文件之后的新的test树型目录如下所示:
用户空间调用设备驱动程序
创建设备节点
$mknod /dev/node_name c major minor
示例代码
int main(void)
{
int dev_fd;
char read_buf[10];
dev_fd = open("/dev/node_name",O_RDWR | O_NONBLOCK);
if ( dev_fd == -1 ) {
printf("Cann't open file /dev/ node_name \n");
exit(1);
}
read(dev_fd, read_buf, 5);
ioctl (dev_fd, XXX_IOCTL_CMD,0);
close(dev_fd);
return 0;
}