Linux驱动的学习方法《一》--------驱动简介

1. Linux驱动学习方法

Linux内核中有上百个驱动,知识点多且杂,对于想学习驱动的同学来说,需要尽快掌握基础知识:如开发板的基本使用,硬件基础知识,开发环境的搭建,Linux常用工具,内核的编译以及烧写,Linux shell命令,C语言基础,Linux内核的简单裁减和配置,Linux系统编程等等

没有上面的基础知识,驱动的学习无疑是在建空中楼阁。

Linux操作系统相当于“一个球”,程序员要做的事情就是在这个球上添加驱动来实现具体的功能,不用去管这个球是从哪里开始旋转,转到什么地方了。更简单的理解就是,Linux只是一个工具,学会使用就可以了,就像学习汽车驾驶,没有教练会从发动机原理开始讲解,只会给你发一些指令如“方向盘右转一圈”“方向盘左转一圈”“拉手刹”“换挡”等。

当然学习Linux的最好的方法是阅读内核,但是在没有基础之前不要过多的去研究内核的东西。

在嵌入式Linux驱动工程师的工作中,移植驱动是必须掌握的技能,在学会了如何移植驱动,找到合适的工作之后,如果你对内核源码感兴趣,而且还有富余的时间,可以看一看内核中“精妙”的代码,不过这对于工作并没有太多直接的帮助,可以纯粹的作为一个兴趣爱好来研读。

2. Linux设备驱动的分类

Linux设备驱动程序在Linux的内核源代码中占有很大的比例,源代码的长度日益增加,主要是驱动程序代码的不断充实。在Linux内核的不断升级过程中,驱动程序的结构却是相对稳定的。

Linux系统的设备分为字符设备(char device),块设备(block device)和网络设备(network device)三种。字符设备是指存取时没有缓存的设备。块设备的读写都有缓存来支持,并且块设备必须能够随机存取(random access),字符设备则没有这个要求。典型的字符设备包括鼠标、键盘、串行口等。块设备主要包括硬盘软盘设备、CD-ROM等。一个文件系统要安装(mount)到操作系统的块设备上。

网络设备在Linux里做专门的处理。Linux的网络系统主要是基于BSD unix的socket机制。在系统和驱动程序之间定义有专门的数据结构(sk_buff)进行数据的传递。系统里支持对发送数据和接收数据的缓存,提供流量控制机制,提供对多协议的支持。

3.以模块的形式编译驱动

模块加载函数

模块的加载函数“module_init(function)”,返回整数型,如果执行成功,则返回0。否则返回错误信息。

有时候芯片供应商并不提供芯片驱动的源码,只是提供驱动module的ko文件,这个时候就需要调用request_module(module_name)来加载驱动。

在Linux中,标示为_ _init的函数都是初始化函数,这些函数占用的内存空间,在Linux启动或者模块加载初始化之后,是会被释放掉的。除了函数,数据也是可以被定义为“_ _数据”,这些数据在Linux启动或者或者模块加载初始化之后,也是会被释放掉。这一部分的知识,在后面会逐渐使用到。

模块卸载函数

模块卸载就是模块加载的“逆向过程”,比较容易理解。

模块的卸载函数“module_init(function)”不返回任何值。

一般说来,我们在卸载函数中要完成和加载函数中相反的功能,例如你调用了系统或者硬件的资源,那么你在卸载函数中,就需要释放掉。

模块编译的流程

Linux模块一般是使用脚本语言来编译,脚本语言的种类非常多,语法丰富,不过我们只需要学会使用即可,能够仿照着写就可以了,这并不影响我们开发。

如下图所示,模块的编译流程图。

通过上图可以看到编译中,当执行执行make命令之后,调用Makefile文件进行Linux模块的编译。

Linux模块的编译分为两个条线。

红色的线:进入Linux源码中,调用版本信息以及一些头文件等。

这一条线经过的是整个Linux的源码文件。

橙色的线:搜集完Linux源码树的信息之后,Makefile继续执行,调用编译.KO文件的源码文件,这里是mini_linux_module.c这个“iTop4412_Kernel_3.0”整个源码。

这一条线走的是mini_linux_module.c,虽然都是源码,但是此源码非彼源码。

Makefile文件通过执行上面两条线,通过搜集到信息,最终编译生成.KO模块

这里我们要学习和理解的重点就是,编译模块也必须用到内核的源码,因为这涉及到内核版本以及头文件。如果版本不对,那么模块有可能无法加载和运行;如果没有头文件,编译就无法通过。

脚本文件Makefile  

在单片机或者上位机编程的时候,都有集成开发工具。程序员按照开发工具的规则,将代码放入指定的位置,通常是一个main.c文件加上很多.c文件,代码写好了,开发工具中某个按钮一点,就给你自动编译成了二进制文件。

在Linux中,并没有这样的集成开发工具,这里还需要自己写编译文件Makefile。

编译文件一般是用脚本编写,脚本成千上万,脚本语言学也学不完,脚本的学习最好是用到哪里学到哪。

Makefile编译文件如下,下面的这个文件可以在Linux驱动视频目录“02_HelloDriverModule”下找到。

#!/bin/bash

#通知编译器我们要编译模块的哪些源码

#这里是编译itop4412_hello.c这个文件编译成中间文件mini_linux_module.o

obj-m += mini_linux_module.o

#源码目录变量,这里用户需要根据实际情况选择路径

#作者是将Linux的源码拷贝到目录/home/topeet/android4.0下并解压的

KDIR := /home/topeet/android4.0/iTop4412_Kernel_3.0

#当前目录变量

PWD ?= $(shell pwd)

#make命名默认寻找第一个目标

#make -C就是指调用执行的路径

#$(KDIR)Linux源码目录,作者这里指的是/home/topeet/android4.0/iTop4412_Kernel_3.0

#$(PWD)当前目录变量

#modules要执行的操作

all:

        make -C $(KDIR) M=$(PWD) modules

#make clean执行的操作是删除后缀为o的文件

clean:

        rm -rf *.o

如上图所示,就是编译mini_linux_module的脚本文件。下面详细的介绍每一句的含义。

#!/bin/bash

通知编译器这个脚本使用的是那个脚本语言

obj-m += mini_linux_module.o

这是一个标准用法,表示要将mini_linux_module.c文件编译成mini_linux_module.o文件,如果还需要编译其它的文件,则在后面添加即可。

KDIR := /home/topeet/android4.0/iTop4412_Kernel_3.0

这一行代码表示内核代码的目录,如果没有内核源码,那么模块的编译无法进行,因为会缺乏版本支持和头文件。KDIR是一个变量。

PWD ?= $(shell pwd)

这一句是提供一个变量,然后将当前目录的路径传给这个变量。pwd是一个命令,表示当前目录,PWD是一个变量。

all:

        make -C $(KDIR) M=$(PWD) modules

在使用执行脚本编译命令“make”的时候,它会默认来寻找这一句,make -C 表示调用执行的路径,也就是变量KDIR,变量KDIR中有内核源码目录的路径。

PWD表示当前目录。

modules表示将驱动编译成模块的形式,也是就是最终生成KO文件。

clean:

        rm -rf *.o

在重新修改了源码之后,可以执行“make clean”命令来清除一些无用的中间文件,这里选择的是清除后缀为“o”的文件。

#include <linux/init.h>
#include <linux/module.h>

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Hcamal");

int hello_init(void)
{
    printk(KERN_INFO "Hello World\n");
    return 0;
}

void hello_exit(void)
{
    printk(KERN_INFO "Goodbye World\n");
}

module_init(hello_init);
module_exit(hello_exit);

Linux下的驱动是使用C语言进行开发的,但是和我们平常写的C语言也有不同,因为我们平常写的C语言使用的是Libc库,但是驱动是跑在内核中的程序,内核中却不存在libc库,所以要使用内核中的库函数。

比如printk可以类比为libc中的printf,这是在内核中定义的一个输出函数,但是我觉得更像Python里面logger函数,因为printk的输出结果是打印在内核的日志中,可以使用dmesg命令进行查看

驱动代码只有一个入口点和一个出口点,把驱动加载到内核中,会执行module_init函数定义的函数,在上面代码中就是hello_init函数。当驱动从内核被卸载时,会调用module_exit函数定义的函数,在上面代码中就是hello_exit函数。

上面的代码就很清晰了,当加载驱动时,输出Hello World,当卸载驱动时,输出Goodbye World

PSS: printk输出的结果要加一个换行,要不然不会刷新缓冲区

编译驱动

驱动需要通过make命令进行编译,Makefile如下所示:

ifneq ($(KERNELRELEASE),)

    obj-m := hello.o

else

    KERN_DIR ?= /usr/src/linux-headers-$(shell uname -r)/
    PWD := $(shell pwd)

default:
    $(MAKE) -C $(KERN_DIR) M=$(PWD) modules

endif


clean:
    rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions

一般情况下,内核的源码都存在与/usr/src/linux-headers-$(shell uname -r)/目录下

比如:

$ uname -r
4.4.0-135-generic

/usr/src/linux-headers-4.4.0-135/  --> 该内核源码目录
/usr/src/linux-headers-4.4.0-135-generic/    --> 该内核编译好的源码目录

而我们需要的是编译好后的源码的目录,也就是/usr/src/linux-headers-4.4.0-135-generic/

驱动代码的头文件都需要从该目录下进行搜索

M=$(PWD)该参数表示,驱动编译的结果输出在当前目录下

最后通过命令obj-m := hello.o,表示把hello.o编译出hello.ko, 这个ko文件就是内核模块文件

加载驱动到内核

需要使用到的一些系统命令:

  • lsmod: 查看当前已经被加载的内核模块
  • insmod: 加载内核模块,需要root权限
  • rmmod: 移除模块

比如:

# insmod hello.ko        // 把hello.ko模块加载到内核中
# rmmod hello            // 把hello模块从内核中移除

知识点1 -- 驱动分类

驱动分为3类,字符设备、块设备和网口接口,上面代码举例的是字符设备,其他两种,之后再说。

如上图所示,brw-rw----权限栏,b开头的表示块设备(block),c开头的表示字符设备(char)

知识点2 -- 主次编号

主编号用来区分驱动,一般主编号相同的表示由同一个驱动程序控制。

一个驱动中能创建多个设备,用次编号来区分。

主编号和次编号一起,决定了一个驱动设备。

如上图所示,

brw-rw----  1 root disk      8,   0 Dec 17 13:02 sda
brw-rw----  1 root disk      8,   1 Dec 17 13:02 sda1

设备sdasda1的主编号为8,一个此编号为0一个此编号为1

知识点3 -- 驱动是如何提供API的

在我的概念中,驱动提供的接口是/dev/xxx,在Linux下Everything is File,所以对驱动设备的操作其实就是对文件的操作,所以一个驱动就是用来定义,打开/读/写/......一个/dev/xxx将会发生啥,驱动提供的API也就是一系列的文件操作。

有哪些文件操作?都被定义在内核<linux/fs.h>[5]头文件中,file_operations结构体

上面我举例的代码中:

struct file_operations scull_fops = {
    .owner =    THIS_MODULE,
    .llseek =   scull_llseek,
    .read =     scull_read,
    .write =    scull_write,
    .open =     scull_open,
    .release =  scull_release,
};

我声明了一个该结构体,并赋值,除了owner,其他成员的值都为函数指针

之后我在scull_setup_cdev函数中,使用cdev_add向每个驱动设备,注册该文件操作结构体

比如我对该驱动设备执行open操作,则会去执行scull_open函数,相当于hook了系统调用中的open函数

知识点4 -- 在/dev下生成相应的设备

对上面的代码进行编译,得到scull.ko,然后对其进行签名,最后使用insmod加载进内核中

查看是否成功加载:lsmod

虽然驱动已经加载成功了,但是并不会在/dev目录下创建设备文件,需要我们手动使用mknod进行设备链接

参考:

https://paper.seebug.org/779/

https://www.oschina.net/question/2371345_2186010

发布了26 篇原创文章 · 获赞 34 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/qq_36662437/article/details/98218434