哪些是驱动:
驱动就是对底层硬件设备的操作进行封装,并向下层提供函数插口。
设备分类:
linux系统将设备分为3类:字符设备、块设备、网络设备。
字符设备:指只能一个字节一个字节读写的设备,不能随机读取设备显存中的某一数据,读取数据须要根据先后次序。字符设备是面向流的设备linux 驱动 开发,常见的字符设备有滑鼠、键盘、串口、控制台和LED设备等。块设备:指可以从设备的任意位置读取一定厚度数据的设备。块设备包括硬碟、磁盘、U盘和SD卡等。网路设备:网路设备可以是一个硬件设备,如网卡;但也可以是一个纯粹的软件设备,例如回环插口(lo).一个网路插口负责发送和接收数据报文。驱动认知:
先看一张图,图中描述了流程,有助了解驱动。
用户态:
内核态:
驱动数组:管理所有设备的驱动,添加或查找,添加是发生在我们编撰完驱动程序,加载到内核。查找是在调用驱动程序,由应用层用户空间去查找使用open函数。驱动插入数组的次序由设备号检索,就是说主设备号和次设备号不仅能分辨不同种类的设备和不同类型的设备,能够起到将驱动程序加载到数组的某个位置,在下边介绍的驱动代码的开发无非就是添加驱动(添加设备号、设备名和设备驱动函数)和调用驱动。
补充:
每位系统调用都对应一个系统调用号,而系统调用号就对应内核中的相应处理函数。所有系统调用都是通过中断0x80来触发的。使用系统调用时,通过eax寄存器将系统调用号传递到内核,系统调用的入参通过ebx、ecx……依次传递到内核和函数一样,系统调用的返回值保存在eax中,所有要从eax中取出字符设备驱动工作原理
字符设备驱动工作原理在linux的世界里一切皆文件,所有的硬件设备操作到应用层就会被具象成文件的操作。我们晓得倘若应用层要访问硬件设备,它必将要调用到硬件对应的驱动程序。Linux内核有这么多驱动程序,应用如何能够精确的调用到底层的驱动程序呢?
补充:
(1)当open函数打开设备文件时,可以依据设备文件对应的structinode结构体描述的信息,可以晓得接出来要操作的设备类型(字符设备还是块设备),都会分配一个structfile结构体。
(2)依据structinode结构体上面记录的设备号,可以找到对应的驱动程序。这儿以字符设备为例。在Linux操作系统中每位字符设备都有一个structcdev结构体。此结构体描述了字符设备所有信息,其中最重要的一项就是字符设备的操作函数插口。
(3)找到structcdev结构体后,linux内核都会将structcdev结构体所在的显存空间首地址记录在structinode结构体i_cdev成员中,将structcdev结构体中的记录的函数操作插口地址记录在structfile结构体的f_ops成员中。
(4)任务完成,VFS层会给应用返回一个文件描述符(fd)。这个fd是和structfile结构体对应的。接出来下层应用程序就可以通过fd找到structfile,之后在structfile找到操作字符设备的函数插口file_operation了。
其中,cdev_init和cdev_add在驱动程序的入口函数中就早已被调用,分别完成字符设备与file_operation函数操作插口的绑定,和将字符驱动注册到内核的工作。
相关视频推荐
免费学习地址:LinuxC/C 开发(前端/音视频/游戏/嵌入式/高性能网路/储存/基础构架/安全)
须要C/C Linux服务器构架师学习资料加qun579733396获取(资料包括C/C ,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDNlinux 输入法,P2P,K8S,Docker,TCP/IP,解释器,DPDK,ffmpeg等),免费分享
字符设备、字符设备驱动与用户空间访问该设备的程序两者之间的关系
如图,在Linux内核中使用cdev结构体来描述字符设备,通过其成员dev_t来定义设备号(分为主、次设备号)以确定字符设备的惟一性。通过其成员file_operations来定义字符设备驱动提供给VFS的插口函数,如常见的open()、read()、write()等。
在Linux字符设备驱动中,模块加载函数通过register_chrdev_region()或alloc_chrdev_region()来静态或则动态获取设备号,通过cdev_init()构建cdev与file_operations之间的联接,通过cdev_add()向系统添加一个cdev以完成注册。模块卸载函数通过cdev_del()来注销cdev,通过unregister_chrdev_region()来释放设备号。
用户空间访问该设备的程序通过Linux系统调用,如open()、read()、write(),来“调用”file_operations来定义字符设备驱动提供给VFS的插口函数。
驱动程序开发步骤
Linux内核就是由各类驱动组成的,内核源码中有大概85%是各类驱动程序的代码。内核中驱动程序种类齐全,可以在同类驱动的基础上进行更改以符合具体单板。
编撰驱动程序的难点并不是硬件的具体操作,而是弄清楚现有驱动程序的框架,在这个框架中加入这个硬件。
通常来说,编撰一个linux设备驱动程序的大致流程如下:
下边就以一个简单的字符设备驱动框架代码来进行驱动程序的开发、编译等。
基于驱动框架的代码开发
下层调用代码
#include #include #include #include void main() { int fd,data; fd = open("/dev/pin4",O_RDWR); if(fd<0){ printf("open failn"); perror("reson:"); } else{ printf("open successn"); } fd=write(fd,'1',1); }
驱动框架代码
#include //file_operations声明 #include //module_initmodule_exit声明 #include //__init__exit 宏定义声明 #include //classdevise声明 #include//copy_from_user 的头文件 #include//设备号dev_t 类型声明 #include //ioremap iounmap的头文件 static struct class *pin4_class; static struct device *pin4_class_dev; static dev_t devno;//设备号 static int major = 231; //主设备号 static int minor = 0; //次设备号 static char *module_name = "pin4"; //模块名 //led_open函数 static int pin4_open(struct inode *inode,struct file *file) { printk("pin4_openn");//内核的打印函数和printf类似 return 0; } //led_write函数 static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos) { printk("pin4_writen"); return 0; } static struct file_operations pin4_fops = { .owner = THIS_MODULE, .open= pin4_open, .write = pin4_write, }; int __init pin4_drv_init(void) //真实驱动入口 { int ret; devno = MKDEV(major, minor);//创建设备号 ret = register_chrdev(major, module_name, &pin4_fops);//注册驱动告诉内核,把这个驱动加入到内核驱动的链表中 pin4_class=class_create(THIS_MODULE, "myfirstdemo"); //用代码在dev自动生成设备 pin4_class_dev =device_create(pin4_class, NULL, devno, NULL, module_name);//创建设备文件 return 0; } void __exit pin4_drv_exit(void) { device_destroy(pin4_class, devno); class_destroy(pin4_class); unregister_chrdev(major, module_name);//卸载驱动 } module_init(pin4_drv_init);//入口,内核加载该驱动(insmod)的时候,这个宏被使用 module_exit(pin4_drv_exit); MODULE_LICENSE("GPL v2");
里面说过驱动开发的重点难点在于看懂框架代码,在上面进行设备的添加和更改,下边就来了解一下这个框架逻辑。
驱动框架设计流程
1.确定主设备号
2.定义结构体类型file_operations
3.实现对应的drv_open/drv_read/drv_write等函数,填入file_operations结构体
4.实现驱动入口:安装驱动程序时,才会去调用这个入口函数,执行工作:
①把file_operations结构体告诉内核:注册驱动程序register_chrdev.
②创建类class_create.
③创建设备device_create.
5.实现出口:卸载驱动程序时,才会去调用这个出口函数,执行工作:
①把file_operations结构体从内核注销:unregister_chrdev.
②销毁类class_create.
③销毁设备结点device_destroy.
6.其他建立:GPL合同、入口加载
1、确定主设备、变量定义
static struct class *pin4_class; static struct device *pin4_class_dev; static dev_t devno;//设备号 static int major = 231; //主设备号 static int minor = 0; //次设备号 static char *module_name = "pin4"; //模块名
2、定义file_operations结构体,加载到内核驱动数组中
这是Linux内核中的file_operations结构体
按照下层调用函数定义结构体成员
static struct file_operations pin4_fops = { .owner = THIS_MODULE, .open= pin4_open, .write = pin4_write, .read= pin4_read, };
3、实现结构体成员pin4_read等函数
static int pin4_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { printk("pin4_readn"); return 0; } //led_open函数 static int pin4_open(struct inode *inode,struct file *file) { printk("pin4_openn");//内核的打印函数和printf类似 return 0; } //led_write函数 static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos) { printk("pin4_writen"); return 0; }
4、驱动入口
int __init pin4_drv_init(void) //真实驱动入口 { int ret; devno = MKDEV(major, minor);//创建设备号 ret = register_chrdev(major, module_name, &pin4_fops);//注册驱动告诉内核,把这个驱动加入到内核驱动的链表中 pin4_class=class_create(THIS_MODULE, "myfirstdemo");//由代码在dev自动生成设备 pin4_class_dev =device_create(pin4_class, NULL, devno, NULL, module_name);//创建设备文件 return 0; }
其中pin4_class=class_create(THIS_MODULE,"myfirstdemo");//由代码在dev手动生成设备,除此之外还可以自动生成设备,在dev目录下sudomknod+设备名子+设备类型(c表示字符设备驱动)+主设备号+次设备号。
5、出口
void __exit pin4_drv_exit(void) { device_destroy(pin4_class, devno); class_destroy(pin4_class); unregister_chrdev(major, module_name);//卸载驱动 }
6、GPI合同,入口加载,出口加载
module_init(pin4_drv_init);//入口,内核加载该驱动(insmod)的时候,这个宏被使用 module_exit(pin4_drv_exit); MODULE_LICENSE("GPL v2");
驱动模块代码编译和测试
编译阶段
驱动模块代码编译(模块的编译须要配置过的内核源码,编译、连接后生成的内核模块后缀为.ko,编译过程首先会到内核源码目录下,读取顶楼的Makefile文件,之后再返回模块源码所在目录。)
#include //file_operations声明 #include //module_initmodule_exit声明 #include //__init__exit 宏定义声明 #include //classdevise声明 #include//copy_from_user 的头文件 #include//设备号dev_t 类型声明 #include //ioremap iounmap的头文件 static struct class *pin4_class; static struct device *pin4_class_dev; static dev_t devno;//设备号 static int major = 231;//主设备号 static int minor = 0;//次设备号 static char *module_name = "pin4"; //模块名 static int pin4_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { printk("pin4_readn"); return 0; } //led_open函数 static int pin4_open(struct inode *inode,struct file *file) { printk("pin4_openn");//内核的打印函数和printf类似 return 0; } //led_write函数 static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos) { printk("pin4_writen"); return 0; } static struct file_operations pin4_fops = { .owner = THIS_MODULE, .open= pin4_open, .write = pin4_write, .read= pin4_read, }; int __init pin4_drv_init(void) //真实驱动入口 { int ret; devno = MKDEV(major, minor);//创建设备号 ret = register_chrdev(major, module_name, &pin4_fops);//注册驱动告诉内 核,把这个驱动加入到内核驱动的链表中 pin4_class=class_create(THIS_MODULE, "myfirstdemo");//用代码 在dev自动生成设备 return 0; } void __exit pin4_drv_exit(void) { device_destroy(pin4_class, devno); class_destroy(pin4_class); unregister_chrdev(major, module_name);//卸载驱动 } module_init(pin4_drv_init);//入口,内核加载该驱动(insmod)的时候,这个宏被使>用 module_exit(pin4_drv_exit); MODULE_LICENSE("GPL v2");
将该驱动代码拷贝到linux-rpi-4.14.y/drivers/char目录下文件中(也可选择设备目录下其它文件)
更改该文件夹下Makefile(驱动代码放在那个目录,就更改该目录下的Makefile),将前面的代码编译生成模块,文件内容如右图所示:(-y表示编译进内核,-m表示生成驱动模块,CONFIG_表示是按照config生成的),所以只须要将obj-m+=pin4drive.o添加到Makefile中即可。
回到linux-rpi-4.14.y/编译驱动文件
使用指令:ARCH=armCROSS_COMPILE=arm-linux-gnueabihf-KERNEL=kernel7makemodules进行编译生成驱动模块。
编译生成驱动模块会生成以下几个文件:
.o的文件是object文件,.ko是kernelobject,与.o的区别在于其多了一些sections,例如.modinfo。.modinfosection是由kernelsource里的modpost工具生成的,包括MODULE_AUTHOR,MODULE_DESCRIPTION,MODULE_LICENSE,deviceIDtable以及模块依赖关系等等。depmod工具按照.modinfosection生成modules.dep,modules.*map等文件,便于modprobe更便捷的加载模块。
编译过程中,经历了这样的步骤:先步入Linux内核所在的目录,并编译出pin4drive.o文件,运行MODPOST会生成临时的pin4drive.mod.c文件linux 驱动 开发,而后依据此文件编译出pin4drive.mod.o,然后联接pin4drive.o和pin4drive.mod.o文件得到模块目标文件pin4drive.ko,最后离开Linux内核所在的目录。
将生成的.ko文件发送给猕猴桃派:[email protected]:/home/pi
将pin4test.c(下层调用代码)进行交叉编译后发送给猕猴桃派,就可以看见pi目录下存在发送过来的.ko文件和pin4test这两个文件,
加载内核驱动
sudo insmod pin4drive.ko
加载内核驱动(相当于通过insmod调用了module_init这个宏,之后将整个结构体加载到驱动数组中)加载完成后就可以在dev下边见到名子为pin4的设备驱动(这个和驱动代码上面staticchar*module_name="pin4";//模块名这行代码有关),设备号也和代码上面相关。
lsmod查看系统的驱动模块,执行下层代码,赋于权限
查看内核复印的信息,
dmesg |grep pin4
如右图所示:表示驱动调用成功
在装完驱动后可以使用指令:sudormmod+驱动名(不须要写ko)将驱动卸载。
调用流程:
我们下层空间的open去查找dev下的驱动(文件名),文件名背后包含了驱动的主设备号和次设备号,此时用户open触发一个系统调用linux是什么系统,系统调用经过vfs(虚拟文件系统),vfs按照文件名背后的设备号去调用sys_open去判定,找到内核中驱动数组的驱动位置,再去调用驱动上面自己的dev_open函数
为何生成驱动模块须要在虚拟机上生成?猕猴桃派不行吗?
生成驱动模块须要编译环境(linux源码而且编译,须要下载和系统版本相同的Linux内核源代码),也可以在猕猴桃派里面编译,但在猕猴桃派里编译,效率会很低,要特别久。
以上是底层硬件设备驱动插入链表的顺序(驱动)的详细内容。更多信息请关注PHP中文网其他相关文章!