Linux 驱动程序开发
设备驱动概述
设备驱动在操作系统中的位置
- 设备驱动程序是 内核 代码的一部分 , 驱动程序的地址空间是内核的地址空间
- 驱动程序的代码直接对设备硬件 直接对设备硬件(实际是设 实际是设备的各种寄存器 备的各种寄存器) 进行控制( 读写操作)
- 应用程序通过操作系统的系统调用执行相应的驱动程序函数 ,应 中断则直接执行相应 的中断程序代码
- 设备驱动程序的file_operations结构体的 结构体的地址被注册到内核中的设备链表中
设备驱动的主要功能
- 对设备进行初始化 , 启动或停止设备的运行 。
- 把数据从内核传送到硬件和从硬件读取数据 。
- 读取应用程序传送给设备文件的数据和回送应用程序请求的数据 。
- 检测和处理设备出现的错误等
驱动程序与应用程序的区别
- 驱动程序没有main函数 ,通过使用宏 module_init (初始化函数名), 将初始化函数加入内核全局初始化函数列表中
- 驱动程序中有一个宏moudule_exit(退出处 处理函数名)注册退出处理函数 注册退出处理函数 。它在驱动退出时被调用 。
- 驱动程序没有内存保护机制
- 应用程序可以和GLIBC 库连接 库连接 , 因此可以包含标准的头文件,如:<stdio.h> <stdlib.h>。
- 在驱动程序中是不能使用标准C 库的 库的 ,因 因 此不能调用所有的 此不能调用所有的C 库函数 库函数,如:输出打印函数只能使用内核的printk函数;包含的头文件只能是内核的头文件,如:<linux/module.h >。
设备分类
- Linux有三种基本设备: 字符设备 、 块设备、 、 网络设备 。
- 字符设备:字符( char ) 设备是一种按字节来访问的设备 , 可以当作一个字节流来存取 。文本控制台( /dev/console )和串口( /dev/ttyS0 ) 是字符设备的例子
- 块设备 :块设备也通过位于 /dev 目录的文件系统结点来存取。Linux允许块设备传送任意数目的字节
- 网络设备:任何网络事务都通过一个接口来进行 , 一个能够与其他主机交换数据的设备。一个网络接口负责发送和接收数据报文,在内核网络子系统的驱动下,不必知道单个事务是如何映射到实际的被发送的报文的。
Linux设备号
-
设备号是一个数字 , 是设备的标志 , 由主设备号和次设备号组成
- 主设备号表明某一类设备,主设备号相同的设备使用相同的驱动程序
- 次设备号用来标识具体设备的实例。
- 系统中块设备IDE硬盘的主设备号是 3,多个 IDE硬盘及其各个分区的次设备号1、2、……
-
从dev_t ( unsigned int 32位整数)中分解主次设备号
- 主设备号 MAJOR(dev_t dev),高12位
- 次设备号 MINOR(dev_t dev),低20位
-
Linux 系统下有关主设备号的分配原则,可参看documentation/device.txt
-
Linux的设备驱动程序通常在 “/dev”下面存在一个对应的逻辑设备节点
创建设备文件
- 可使用mknod手工创建 手工创建
mknod filename type major minnor
- filename:设备文件名
- type:设备文件类型
- major:主设备号
- minor:次设备号
- 例 mknod s3c241sel c 231 0
IO控制方式
- 驱动程序的IO控制方式有:查询方式 、 中断方式 、 直接访问内存(DMA) 方式 、 通道方式
- 查询方式占用CPU,中断处理存在延迟
- 利用DMA进行数据传输的同时, 处理器仍然可以继续执行指令
- 追求高效的大中型机器采用具有独立处理器的通道来实现IO传输控制。
设备驱动程序接口
Linux设备驱动的加载方式
- 在LINUX下加载驱动程序可以采用动态 和 静态 两种方式 。
- 静态加载 就是把驱动程序直接编译到内核里 ,在执行make menuconfig 命令时选择
- 驱动编译进内核后,系统启动后可以直接调用
- Linux最基础的内核模块,如CPU、PCI总线、 TCP/IP协议、VFS等则直接编译在内核文件中
- 动态加载利用了 利用了LINUX的模块化特性,可在系统启动后用 insmod 命令把驱动程序(.o 文件)添加上去,在不需要的时候用rmmod命令来卸载
- 要把一般文件(设备文件通常在/dev目录)和内核模块链接在一起需要两个数据:主设备号和从设备号(两者都在模块定义时声明)。
- 主设备号用于内核把文件和它的驱动链接在一起。从设备号用于设备内部使用。
相关命令
- lsmod命令:查看当前加载到内核中的所有驱动模块 同时提供其它一些信息 , 比如其它模块是否在使用另一个模块
- modinfo命令:用来查看模块信息 ,如modinfo nfs
- rmmod命令:删除模块
- insmod命令:是插入模块的命令 , 但是它不会自动解决依存关系 , 所以一般加载内核模块时使用的命令为modprobe
- modprobe命令:智能插入模块 , 它可以根据模块间依存关系 , 以及/etc/modules.conf 文件中的内容智能插入模块
设备驱动程序接口
-
系统调用对设备的操作过程 ,比如open ,read ,write ,ioctl、 release 等操作
-
设备驱动程序所提供的这组入口点 , 分别是 file_operations 数据结构、 inode 数据结构和 file 数据结构 。
-
通常所说的设备驱动程序接口是指结构 file_operations{} ,它定义在 它定义在 include/linux/fs.h 中
常用操作
- *lseek, 移动文件指针的位置,只能用于可以随机存取的设备。
- *read, 进行读操作
- *write, 进行写操作,与read类似
- *readdir, 取得下一个目录入口点,只有与文件系统相关的设备程序才使用。
- *ioctl, 进行读、写以外的其他操作,参数cmd 为自定义的命令 。
- *mmap, 用于把设备的内容映射到地址空间,一般只有块设备驱动程序使用。
- *open, 打开设备准备进行I/O操作。返回0表示打开成功,返回负数表示失败。如果驱动程序 没有提供open入口,则只要/dev/driver文件存在就认为打开成功。
- *release, 即close操作。
设备驱动开发流程
- 1、编写驱动模块
- 2、编写Makefile文件
- 3、编译驱动模块
- 4、加载驱动模块
- 5、在 /dev 创建设备文件(如果是驱动模块)
1、编写驱动模块
-
应用程序一般有一个 main 函数,从头到尾执行 一个任务;驱动程序却不同,它没有 main函数
-
通 过 使 用 宏 ( module_init(初始化函数名) ),将初始化函数加入内核全局初始化函数列表中
-
驱 动 程 序 中 有 一 个 宏 ( moudule_exit(退出处理函数名) ) 。它在驱动退出时被调用。
驱动程序框架
-
头文件:大多的 Linux驱动程序需要包含下面三个头文件:
- #include <linux/init.h>:定义了驱动的初始化和退出相关的函数
- #include <linux/module.h>:定义了经常用到的函数原型及宏定义
- #include <linux/kernel.h>:定义了内核模块相关的函数、变量及宏
-
初始化
-
驱动程序是通过 module_init宏来声明初始化函数的:
-
-
卸载:如果驱动程序编译成模块(动态加 载)模式,那么它需要一个清理函数。当移除一个内核模块时这个函数被调用执行清理工作。
- 驱动程序是通过 module_exit宏来声明清理函数的:
- 驱动程序是通过 module_exit宏来声明清理函数的:
2、编写驱动模块的Makefile文件
-
针对以上源码写一个 Makefile文件用来编译它, Makefile和 hello. c文件保存在同一个目 录下。
-
Makefile文件的内容可以简单编写如下内容:
3、编译驱动模块
-
先进入 linux内核所在的目录,并编译出 hello. o 文件
-
然后创建模块,运行 MODPOST生成临时的 hello. mod. c文件,而后根据此文件编译出 hello. mod. o
-
连接 hello. o和 hello. mod. o文件得到模块目标文件 hello. ko
-
编译后可能结果
4、加载驱动模块
-
使用 insmod来加载该驱动模块,并且使用相关命令来验证或查看该驱动模块运行的信息
5、由于上面示例的模块创建不是驱动模块,只是创建了一个模块,所以即使在 /dev 创建了设备文件也无法和模块绑定。
-
设备模块声明示例(假设有一个内存驱动)
/* Necessary includes for device drivers */ #include <linux/init.h> #include <linux/config.h> #include <linux/module.h> #include <linux/kernel.h> /* printk() */ #include <linux/slab.h> /* kmalloc() */ #include <linux/fs.h> /* everything... */ #include <linux/errno.h> /* error codes */ #include <linux/types.h> /* size_t */ #include <linux/proc_fs.h> #include <linux/fcntl.h> /* O_ACCMODE */ #include <asm/system.h> /* cli(), *_flags */ #include <asm/uaccess.h> /* copy_from/to_user */ MODULE_LICENSE("Dual BSD/GPL"); /* Declaration of memory.c functions */ int memory_open(struct inode *inode, struct file *filp); int memory_release(struct inode *inode, struct file *filp); ssize_t memory_read(struct file *filp, char *buf, size_t count, loff_t *f_pos); ssize_t memory_write(struct file *filp, char *buf,size_t count, loff_t *f_pos); void memory_exit(void); int memory_init(void); /* Structure that declares the usual file */ /* access functions */ /* * 给 file_operations 定义的操作赋值(函数指针) * 即操作对应设备文件时会执行的操作 */ struct file_operations memory_fops = { read: memory_read, write: memory_write, open: memory_open, release: memory_release }; /* Declaration of the init and exit functions */ /** * 模块初始化操作 */ module_init(memory_init); module_exit(memory_exit); /* Global variables of the driver */ /* Major number */ /** * 指定对应的设备号 */ int memory_major = 60; /* Buffer to store data */ /** * 将用于存储该驱动的数据 */ char *memory_buffer;
-
“memory”驱动:绑定设备文件
- 在安装模块时,调用 register_chrdev 函数用于 在内核空间,把驱动和/dev下设备文件链接在一起
- 它由三个参数:主设备号,模块、名称和一个 file_operations结构的指针。
int memory_init(void) { int result; /* Registering device */ /** * 注册驱动 */ result = register_chrdev(memory_major, "memory",&memory_fops); if (result < 0) { printk("<1>memory: cannot obtain major number %d\n", memory_major); return result; } /* Allocating memory for the buffer */ /** * 为该驱动程序的缓冲区分配内存。 */ memory_buffer = kmalloc(1, GFP_KERNEL); if (!memory_buffer) { result = -ENOMEM; goto fail; } memset(memory_buffer, 0, 1); printk("<1>Inserting memory module\n"); return 0; /** * 如果注册主设备号或者分配内存失败,模块将退出。 */ fail: memory_exit(); return result; }
-
“memory”驱动:卸载驱动
- 在通过 memory_exit函数卸载模块时,需要调用 unregsiter_chrdev函数,其将释放驱动之前向内核申请的主设备号。
void memory_exit(void) { /* Freeing the major number */ unregister_chrdev(memory_major, "memory"); /* Freeing buffer memory */ if (memory_buffer) { // 为了完全的卸载该驱动,缓冲区也需要通过该函数进行释放。 kfree(memory_buffer); } printk("<1>Removing memory module\n"); }
-
“memory”驱动:创建设备文件
- 需要创建一个文件(该设备文件用于和设备驱动操作),
mknod /dev/memory c 60 0
- 其中,c说明创建的是字符设备,60是主设备号,0是从设备号。
- 需要创建一个文件(该设备文件用于和设备驱动操作),