您的位置:首页 > 运维架构 > Linux

Linux驱动开发1-内核入门之hello模块

2018-03-21 19:59 190 查看
一、Linux内核简介
    现在我们从一个比较高的高度来审视一下 GNU/Linux 操作系统的体系结构。如下图所示,最上面是用户(或应用程序)空间,这是用户应用程序执行的地方。用户空间之下是内核空间,Linux 内核正是位于这里。C基础库(如glibc、eglibc、uclibc等)也属于应用程序空间,它提供了连接内核的系统调用接口,还提供了在用户空间应用程序和内核之间进行转换的机制。这点非常重要,因为内核和用户空间的应用程序使用的是不同的保护地址空间。每个用户空间的进程都使用自己的虚拟地址空间,而内核则占用单独的地址空间。

    Linux 内核可以进一步划分成 3 层。最上面是系统调用接口(SCI,System Call Interface),它实现了一些基本的功能,例如 open()、read()、write()、close()等。系统调用接口之下是内核代码,可以更精确地定义为独立于体系结构的内核代码。这些代码是 Linux 所支持的所有处理器体系结构所通用的。在这些代码之下是依赖于体系结构的代码,构成了通常称为 BSP(Board Support Package)的部分。这些代码用作给定体系结构的处理器和特定于平台的代码。



    Linux 内核实现了很多重要的体系结构属性。在或高或低的层次上,内核被划分为多个子系统。Linux 也可以看作是一个整体,因为它会将所有这些基本服务都集成到内核中。这与微内核的体系结构不同,后者会提供一些基本的服务,例如通信、I/O、内存和进程管理,更具体的服务都是插入到微内核层中的。每种内核都有自己的优点,不过这里并不对此进行讨论。随着时间的流逝,Linux 内核在内存和 CPU 使用方面具有较高的效率,并且非常稳定。但是对于 Linux 来说,最为有趣的是在这种大小和复杂性的前提下,依然具有良好的可移植性。Linux 编译后可在大量处理器和具有不同体系结构约束和需求的平台上运行。一个例子是 Linux 可以在一个具有内存管理单元(MMU)的处理器上运行,也可以在那些不提供 MMU 的处理器上运行。Linux 内核的 uClinux 移植提供了对非MMU 的支持。



系统调用接口

    SCI 层提供了某些机制执行从用户空间到内核的函数调用。正如前面讨论的一样,这个接口依赖于体系结构,甚至在相同的处理器家族内也是如此。SCI 实际上是一个非常有用的函数调用多路复用和多路分解服务。在 ./linux/kernel 中您可以找到 SCI 的实现,并在 ./linux/arch 中找到依赖于体系结构的部分。

进程管理

    进程管理的重点是进程的执行。在内核中,这些进程称为线程,代表了单独的处理器虚拟化(线程代码、数据、堆栈和 CPU 寄存器)。在用户空间,通常使用进程 这个术语,不过 Linux 实现并没有区分这两个概念(进程和线程)。内核通过 SCI 提供了一个应用程序编程接口(API)来创建一个新进程(fork、exec 或 Portable Operating System Interface [POSIX] 函数),停止进程(kill、exit),并在它们之间进行通信和同步(signal 或者 POSIX 机制)。进程管理还包括处理活动进程之间共享 CPU 的需求。内核实现了一种新型的调度算法,不管有多少个线程在竞争 CPU,这种算法都可以在固定时间内进行操作。这种算法就称为 O(1) 调度程序,这个名字就表示它调度多个线程所使用的时间和调度一个线程所使用的时间是相同的。 O(1) 调度程序也可以支持多处理器(称为对称多处理器或 SMP)。您可以在 ./linux/kernel 中找到进程管理的源代码,在 ./linux/arch 中可以找到依赖于体系结构的源代码。

内存管理

    内核所管理的另外一个重要资源是内存。为了提高效率,如果由硬件管理虚拟内存,内存是按照所谓的内存页 方式进行管理的(对于大部分体系结构来说都是 4KB)。Linux 包括了管理可用内存的方式,以及物理和虚拟映射所使用的硬件机制。不过内存管理要管理的可不止 4KB 缓冲区。Linux 提供了对 4KB 缓冲区的抽象,例如 slab 分配器。这种内存管理模式使用 4KB 缓冲区为基数,然后从中分配结构,并跟踪内存页使用情况,比如哪些内存页是满的,哪些页面没有完全使用,哪些页面为空。这样就允许该模式根据系统需要来动态调整内存使用。为了支持多个用户使用内存,有时会出现可用内存被消耗光的情况。由于这个原因,页面可以移出内存并放入磁盘中。这个过程称为交换,因为页面会被从内存交换到硬盘上。Linux系统中,被用于交换的分区叫swap分区,在windows系统下叫做虚拟内存。内存管理的源代码可以在 ./linux/mm 中找到。

文件系统

    虚拟文件系统(VFS)是 Linux 内核中非常有用的一个方面,因为它为文件系统提供了一个通用的接口抽象。VFS 在 SCI 和内核所支持的文件系统之间提供了一个交换层。在 VFS 上面,是对诸如open、close、read 和 write 之类的函数的一个通用 API 抽象。在 VFS 下面是文件系统抽象,它定义了上层函数的实现方式。

网络管理

    网络堆栈在设计上遵循模拟协议本身的分层体系结构。回想一下,Internet Protocol (IP) 是传输协议(通常称为传输控制协议或 TCP)下面的核心网络层协议。TCP 上面是 socket 层,它是通过 SCI 进行调用的。socket 层是网络子系统的标准 API,它为各种网络协议提供了一个用户接口。从原始帧访问到 IP 协议数据单元(PDU),再到 TCP 和 User Datagram Protocol (UDP),socket 层提供了一种标准化的方法来管理连接,并在各个终点之间移动数据。内核中网络源代码可以在 ./linux/net中找到。

设备管理(驱动程序)

    Linux 内核中有大量代码都在设备驱动程序中,它们能够运转特定的硬件设备。Linux 源码树提供了一个驱动程序子目录,这个目录又进一步划分为各种支持设备,例如 Bluetooth、I2C、serial 等。设备驱动程序的代码可以在 ./linux/drivers 中找到。

二、Linux内核编
4000


    Linux是"单块内核"的操作系统,这是说整个系统内核都运行于一个单独的保护域中,但是linux内核是模块化组成的,它允许内核在运行时动态地向其中插入或从中删除代码。这些代码(包括相关的子线程、数据、函数入口和函数出口)被一并组合在一个单独的二进制镜像中,即所谓的可装载内核模块中,或简称为模块。支持模块的好处是基本内核镜像尽可能的小,因为可选的功能和驱动程序可以利用模块形式再提供。模块允许我们方便地删除和重新载入内核代码,也方便了调试工作。而且当热插拔新设备时,可通过命令载入新的驱动程序。

    设备驱动程序就像一个个的“黑盒子”,使某个特定硬件响应一个定义良好的内部编程接口,这些操作完全隐藏了设备的工作细节。用户的操作通过一组标准化的调用执行,而这些调用独立于特定的驱动程序。将这些调用映射到作用于实际硬件的设备特有操作上,则是设备驱动程序的任务。 Linux内核设计的原则是: 只提供机制(需要提供什么功能),不实现策略(如何使用这些功能)!

内核编程与用户空间编程在许多方面不同:

    (1)内核编程与传统应用程序编程方式很大不同的是并发问题. 大部分应用程序, 多线程的应用程序是一个明显的例外, 典型地是顺序运行的, 从头至尾, 不必要担心其他事情会发生而改变它们的环境. 内核代码没有运行在这样的简单世界中, 即便最简单的内核模块必须在这样的概念下编写, 很多事情可能马上发生.
    (2)Linux内核运行起来之后会创建init进程开始启动相应的应用程序,而这些应用程序都是在应用程序空间运行,此外C基础库(如glibc等)也是在应用程序空间的。Linux内核和应用程序空间是隔离开来的,这也就意味着我们并不能在编写内核驱动模块时调用glibc提供的一些函数,如printf()、malloc()等。这时我们需要调用Linux内核里定义的相应函数,如printk()、kmalloc()等;
    (3)应用程序存在于虚拟内存中, 有一个非常大的堆栈区和堆栈, 而内核相反, 他只有一个非常小的堆栈; 它可能小到一个, 4096 字节的页. 你的函数必须与整个内核空间调用链共享这个堆栈. 因此, 如果你需要存储一个较大的结构时, 你应当在调用时间内动态分配。
    (4)每个运行的进程有自己独立的4GB虚拟地址空间,这样我们在编写应用程序时,如果指针出错只会导致本进程退出而不会影响其他进程;而如果在写Linux内核模块时出现非法地址访问则会导致整个系统死掉;所以编写内核代码时需要异常小心;
    (5)常常, 当你查看内核 API 时, 你会遇到以双下划线(__)开始的函数名. 这样标志的函数名通常是一个低层的接口组件, 应当小心使用. 本质上讲, 双下划线告诉程序员:" 如果你调用这个函数, 确信你知道你在做什么."

    (6)内核代码不能做浮点算术. 使能浮点将要求内核在每次进出内核空间的时候保存和恢复浮点处理器的状态 -- 至少, 在某些体系上. 在这种情况下, 内核代码真的没有必要包含浮点, 额外的负担不值得.
三、编写与测试内核hello模块

    学习内核编程的最简单的方式也许就是写个内核模块:一段可以动态加载进内核的代码。模块所能做的事是有限的——例如,他们不能在类似进程描述符这样的公共数据结构中增减字段(可能会破坏整个内核及系统的功能)。但是,在其它方面,他们是成熟的内核级的代码,可以在需要时随时编译进内核(这样就可以摒弃所有的限制了)。完全可以在Linux源代码树以外来开发并编译一个模块(这并不奇怪,它称为树外开发),如果你只是想稍微玩玩,而并不想提交修改以包含到主线内核中去,这样的方式是很方便的。

    下面我们以hello模块为例讲解Linux内核模块的编写、编译以及使用的过程。

3.1 创建项目驱动程序工作路径

    在FL2440项目下创建驱动模块存放路径,今后编写的驱动全部放在该路径下:

[zusi@centos6_master ~]$ cd gitee/fl2440/
[zusi@centos6_master fl2440]$ ls
3rdparty     bootloader     crosstool     LICENSE     linux     README.md     x86_tools
[zusi@centos6_master fl2440]$ mkdir driver

[zusi@centos6_master fl2440]$ cd driver
3.2 编写hello模块C文件

[zusi@centos6_master driver]$ vim kernel_hello.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
static __init int hello_init(void)
{
    printk(KERN_ALERT "Hello, LingYun IoT Studio!\n");
    return 0;
}
static __exit void hello_exit(void)
{
    printk(KERN_ALERT "Goodbye, I have found a good job!\n");
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_AUTHOR("zusi<1446105267@qq.com>");
MODULE_DESCRIPTION("Linux Kernel hello module (C) LingYun IoT Studio");

MODULE_LICENSE("Dual BSD/GPL");
    这个模块定义了两个函数, 一个在模块加载到内核时被调用( hello_init )以及一个在模块被去除时被调用( hello_exit ). moudle_init 和 module_exit 这几行使用了特别的内核宏来指出这两个函数的角色. 另一个特别的宏 (MODULE_LICENSE) 是用来告知内核, 该模块带有一个自由的许可证; 没有这样的说明, 在模块加载时内核会抱怨。
3.3. 在X86主机上测试内核模块

    创建x86文件夹,在这里面编译并测试hello内核模块。

[zusi@centos6_master driver]$ mkdir x86
[zusi@centos6_master driver]$ cd x86
[zusi@centos6_master x86]$ ln -s ../kernel_hello.c

[zusi@centos6_master x86]$ vim Makefile
KERNAL_DIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
obj-m := kernel_hello.o

modules:
    $(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules
    @make clear

clear:
    @rm -f *.o *.cmd *.mod.c
    @rm -rf *~ core .depend .tmp_versions Module.symvers modules.order -f
    @rm -f .*ko.cmd .*.o.cmd .*.o.d
    @rm -f *.unsigned

clean:
    @rm -f hello.ko
[zusi@centos6_master x86]$ ls
kernel_hello.c     Makefile

[zusi@centos6_master x86]$ make
[zusi@centos6_master x86]$ ls
kernel_hello.c     kernel_hello.ko     Makefile
dmesg查看Linux内核的打印信息,dmesg -c将会清除之前Linux内核的打印信息
[zusi@centos6_master x86]$ sudo dmesg
[zusi@centos6_master x86]$ sudo dmesg -c
安装Linux内核模块,并查看内核的打印信息:
[zusi@centos6_master x86]$ sudo insmod kernel_hello.ko
[zusi@centos6_master x86]$ dmesg
Hello, LingYun IoT Studio!
lsmod命令查看当前linux内核安装了的内核模块
[zusi@centos6_master x86]$ sudo lsmod | grep kernel_hello
kernel_hello     891     0
rmmod将会删除linux内核安装了的内核模块
[zusi@centos6_master x86]$ sudo rmmod kernel_hello
[zusi@centos6_master x86]$ dmesg

Hello, LingYun IoT Studio!
Goodbye, I have found a good job!
3.4 在ARM板上测试内核模块

[zusi@centos6_master x86]$ cd .. 
[zusi@centos6_master driver]$ vim Makefile

LINUX_SRC = ${shell pwd}/../linux/linux-3.0/
CROSS_COMPILE=/opt/xtools/arm920t/bin/arm-linuxINST_PATH=/tftp
PWD := $(shell pwd)
EXTRA_CFLAGS+=-DMODULE
obj-m += kernel_hello.o

modules:
    @make -C $(LINUX_SRC) M=$(PWD) modules
    @make clear

uninstall:
    rm -f ${INST_PATH}/*.ko

install: uninstall
    cp -af *.ko ${INST_PATH}
<
b4bc
span style="font-family:'KaiTi_GB2312';">

clear:
    @rm -f *.o *.cmd *.mod.c
    @rm -rf *~ core .depend .tmp_versions Module.symvers modules.order -f
    @rm -f .*ko.cmd .*.o.cmd .*.o.d

clean: clear
    @rm -f *.ko
关于Makefile文件的说明
1,LINUX_SRC 应该指定开发板所运行的Linux内核源码的路径,并且这个linux内核源码必须makemenuconfig并且make过的,因为Linux内核的一个模块可能依赖另外一个模块,如果另外一个没有编译则会出现问题。所以Linux内核必须编译过,这样才能确认这种依赖关系;
2, 交叉编译器必须使用 CROSS_COMPILE 变量指定;
3, 如果编译Linux内核需要其它的一些编译选项,那可以使用 EXTRA_CFLAGS 参数来指定;
4,  obj-m += kernel_hello.o 该行告诉Makefile要将 kernel_hello.c 源码编译生成内核模块文件kernel_hello.ko;
5, @make -C $(LINUX_SRC) M=$(PWD) modules @ make是不打印这个命令本身 -C:把工作目录切换到-C后面指定的参数目录,M是Linux内核源码Makefile里面的一个变量,作用是回到当前目录继续读取Makefile。当使用make命令编译内核驱动模块时,将会进入到 KERNEL_SRC 指定的Linux内核源码中去编译,并在当前目录下生成很多临时文件以及驱动模块文件kernel_hello.ko;

6, clear 目标将编译linux内核过着产生的一些临时文件全部删掉;
[zusi@centos6_master driver]$ make

开发板上测试:

使用之前的老内核测试刚编译的驱动模块:
~ >: tftp -gr kernel_hello.ko 192.168.0.5

kernel_hello.ko 100% |*******************************| 23746 0:00:00 ETA 
~ >: ls
apps     info     mnt     tmp
bin     init     proc     usr
data     kernel_hello.ko     root     var
dev     lib     sbin
etc     linuxrc     sys 
~ >: lsmod 
~ >: insmod kernel_hello.ko
kernel_hello: Unknown symbol __aeabi_unwind_cpp_pr0 (err 0)
insmod: can't insert 'kernel_hello.ko': unknown symbol in module or invalid parameter
这个错误是因为驱动模块与开发板现在正在运行的内核不一致,重新编译升级最新的Linux内核之后重新测试OK:

~ >: insmod kernel_hello.ko
Hello, LingYun IoT Studio! 
~ >: lsmod
kernel_hello 541 0 - Live 0xbf000000 
~ >: rmmod kernel_hello
Goodbye, I have found a good job!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Linux 嵌入式