Linux内核学习(二)编写最简单的字符设备驱动

写在前面

之前做项目的时候,有前辈告诉自己,要去学一下Linux内核,对很多方面都有帮助,现在闲下来,来花时间学一下这一部分的知识点,也算是一个学习笔记
目前跟着B站UP主——简说linux 的教程《Linux内核开发100讲》学习,链接如下:
简说linux个人空间
本章参考学习的链接如下:
Makefile中($(KERNELRELEASE),)执行分析
《Linux内核设计与实现》
在学习的过程中,我也会对遇到的各种问题进行深一步学习, 从而总结知识点到博客当中,这就会出现内容可能会四处跳跃,但是这种跳跃符合我的学习过程。

整体环境

为了学习代码,我们需要一个一套Linux环境,因为为了方便自己记笔记和学习,没有用双系统,直接在windows10下面用VMware建了一个虚拟机进行试验。
开发环境:VMWare虚拟机 Ubuntu 18.04
Linux源码版本:linux4.9.229

学习笔记

设备

在Linux中,大部分设备驱动就是表示物理设备,只有一些设备驱动是虚拟的,仅提供访问内核功能。因此大部分设备驱动就是调用实际中的设备所提供的代码

模块

Linux是“单块内核”(monolithic)的操作系统,即整个系统内核都运行在一个单独的保护域中,但其内核时模块化组成在,在运行的过程中可以动态的向其中插入或从中删除代码。这些代码(包括相关的子例程、数据、函数入口和函数出口)被一并组合在一个单独的二进制镜像中(即后文中的Makefile文件中的obj-m := helloDev.o这一个操作也就是.ko文件),这个就叫做模块。

因此给Linux内核加入新的设备的过程大致如下

设备驱动代码
编译为模块
插入到内核当中

字符设备驱动的代码

首先,UP主给出了最简单的一个字符设备驱动的代码,具体代码如下,学习过程中的笔记就当写注释了

#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/sched.h>
#include <linux/slab.h>

#define BUFFER_MAX    (10)
#define OK            (0)
#define ERROR         (-1)

struct cdev *gDev;
struct file_operations *gFile;
dev_t  devNum;
unsigned int subDevNum = 1;
int reg_major  =  232;    
int reg_minor =   0;
char *buffer;
int flag = 0;
int hello_open(struct inode *p, struct file *f)
{
    printk(KERN_EMERG"hello_open\r\n");
    return 0;
}

ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l)
{
    printk(KERN_EMERG"hello_write\r\n");
    return 0;
}
ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l)
{
    printk(KERN_EMERG"hello_read\r\n");      
    return 0;
}
int hello_init(void)
{
    
    devNum = MKDEV(reg_major, reg_minor);
    //根据主次设备号生成一个devNum,具体实现是将主设备号作一个偏移,然后将其或上次设备号
    //主次设备号用来唯一标识一个设备,主:标识一类设备 次:该类设备下的不同设备
    if(OK == register_chrdev_region(devNum, subDevNum, "helloworld")){ //register_chrdev_region是将设备注册到内核里面
    //将设备号注册到内核里面后,该设备号其他人就无法再注册
        printk(KERN_EMERG"register_chrdev_region ok \n"); 
    }else {
    printk(KERN_EMERG"register_chrdev_region error n");
        return ERROR;
    }
    printk(KERN_EMERG" hello driver init \n");
    gDev = kzalloc(sizeof(struct cdev), GFP_KERNEL);
    gFile = kzalloc(sizeof(struct file_operations), GFP_KERNEL);
    //声明一个file_operation类型的变量gFile
    //struct file_operations结构体里面声明了许多对文件操作的函数
    gFile->open = hello_open;
    gFile->read = hello_read;
    gFile->write = hello_write;
    //对gFile中的三个操作进行指向,最终由应用层请求进行调用
    gFile->owner = THIS_MODULE;
    cdev_init(gDev, gFile);
    //建立了gDev和gFile之间的联系
    //注:在学习内核的过程中,有些函数特别复杂,我们可以不去细究。类似这样的,我们知道这个函数将两者建立了联系即可
    cdev_add(gDev, devNum, 3);
    //建立gDev和设备号之间的联系
  	//至此,建立了设备号与gDev与gFile的联系
    return 0;
}

void __exit hello_exit(void) //驱动卸载函数
{
    cdev_del(gDev); //与cdev_add操作对应
    unregister_chrdev_region(devNum, subDevNum);//与register_chrdev_region操作对应
    return;
}
module_init(hello_init); //声明驱动的入口函数是hello_init
module_exit(hello_exit); //声明驱动的出口函数是hello_exit
MODULE_LICENSE("GPL");	//声明了版权

学习笔记

  • 字符设备通常缩写为cdev,它是不可寻址的,仅提供数据的流式访问,就是一个个字符或者一个个字节。
  • hello_init()就是这一个模块的入口点,它通过module_init()注册到系统中,在内核装载到时候被调用。而module_init()实际上不是一个函数调用,而是一个宏调用。唯一的参数就是模块的初始化函数。模块的所有初始化函数必须满足int my_init(void)这样的格式
  • hello_exit()是模块的出口函数,由module_exit()注册到系统中,当模块从内存卸载的时候,便会调用此函数。即对这个模块的清理工作,其退出函数必须负荷void my_exit(void)格式
  • 由于init和exit函数通常不会被外部函数直接调用,因此我们不必导出该函数,因此这两个函数都可以标记为static类型

字符设备驱动的Makefile(构建模块)

ifneq ($(KERNELRELEASE),) 
obj-m := helloDev.o #给内核的编译系统识别,内核系统会将所有obj-m的二进制文件变为驱动文件
#此处执行就是将helloDev.c生成为helloDev.o文件,再将其编译为驱动文件即.ko文件
else
PWD := $(shell pwd) #得到当前目录
#KDIR:= /lib/modules/4.4.0-31-generic/build #自己编译的一个内核下的驱动,插入编译的这个内核的驱动中
KDIR := /lib/modules/`uname -r`/build #当前运行的Ubuntu的系统所在的地方,插入到自己系统的驱动中
all:
	make -C $(KDIR) M=$(PWD)
	#make -C $(KDIR)表示进入内核目录,并执行其中的Makefile文件
	#M=$(PWD) 表示执行完了之后返回到当前的目录之下继续读入、执行当前的Makefile文件
clean:	
	rm -rf *.o *.ko *.mod.c *.symvers *.c~ *~
endif

笔记

  1. 首先是最简单ifneq的使用:ifneq($(变量名), 变量值)ifneq是为了比较两个参数是否不相同,不相等为true,相等为false
    第二个变量是NULL即为空。即如果变量为空,则为false,进入else语句

补充一点 Makefile笔记
ifeq 判断参数是否不相等,相等为 true,不相等为 false。
ifneq 判断参数是否不相等,不相等为 true,相等为 false。
ifdef 判断是否有值,有值为 true,没有值为 false。
ifndef 判断是否有值,没有值为 true,有值为 false。

  1. 在执行这个Makefile文件的时候,如果使用的make指令,那么Makefile文件就会执行all:后面的语句,即上方的make -C $(KDIR) M=$(PWD),如果使用make clean指令,则会执行clean:后面的语句
  2. 而这个文件最关键的是会执行两次该Makefile文件,原因如下:
    当第一次执行ifneq ($(KERNELRELEASE),) 时,此时的KERNELRELEASE变量还没有被定义,因此此时判断为fasle进入else后面的执行语句,然后进入all:后面的语句,all:后面的语句执行过程如下
    make -C $(KDIR)表示进入内核目录,并执行其中的Makefile文件,此时,KERNELRELEASE变量已被定义,不再为空
    M=$(PWD) 表示执行完了之后返回到当前的目录之下继续读入、执行当前的Makefile文件
    再次执行当前Makefile文件之后,ifneq判断为true,执行obj-m := helloDev.o

管理模块的位置

在前面的这些知识里面我们可以知道,.ko文件就是一个文件镜像,Linux内核系统就是把这样的一个镜像文件插入到运行的内核当中。而这里面有一个重要的点是,我们在哪里管理我们的模块,也就是把.ko文件放在哪?

  1. 放到内核源代码树种
    在前面Linux内核学习(一)里面我们知道,在Linux内核代码里面设备驱动程序有一个专门的目录即/drivers,在内部有许多的子目录,而我们也可以将我们自己编写的设备驱动模块放在其内部。这里面有drivers/char(存放字符设备),/drivers/usb(存放USB设备)

    注:这个规矩并不是墨守成规的,许多usb设备也属于字符设备,但这样的目录规则有助于我们理解各个设备的关系

    但是我们如果将我们的文件放到这里面的目录之下,该目录下会同时存在大量的C源代码文件和其他文件目录,不便于自己编写。
    因此,我们也可以自己在/drivers/char下创建一个自己的一个目录,例如/drivers/char/helloDev,如果是这样的话,我们就需要在drivers/char/Makefile里面加入一行obj-m += helloDev/,意思是编译模块的时候,要进入helloDev目录。然后我们需要在drivers/char/helloDev里面加入一个Makefile文件,并包含obj-m += helloDev.o(即将helloDev.c编译为helloDev.ko文件)

  2. 放在内核代码之外
    我们也可以将其放在一个自己的文件夹里面,来进行维护,那么就只需要在你当前的目录之下建立一个Makefile文件,里面包含
    obj-m :=helloDev.o,这样就把文件编译为.ko文件了,当然,如果你有多个元件文件需要编译到helloDev.o里面,我们就需要加入helloDev-objs := helloDev.o goodbye.o,这样helloDev.c和goodbye.c就被编译到helloDev.ko里面了
    当然我们也要让内核知道我们要编译的模块在哪
    可以选择在Makefile里面加入make -C $(KDIR) M=$(PWD)例如前面的文件种
    或者使用make指令的时候使用make -C /kernel/source/location SUBDIRS=$PWD modules

驱动的插入和卸载

有了上述的准备工作之后,我们就可以将我们的helloDev.ko驱动加入到我们的Linux进程当中啦
首先,我们先把我们的内核日志中的所有内容清理一下,便于我们查看后续插入我们自己的驱动而输出的信息

sudo dmesg -C #需要在root权限下清理日志
dmesg # 查看日志内容 

dmesg没有输出内容之后,代表已经清理完了
输入sudo insmod helloDev.ko将我们的设备驱动加入到Linux内核当中
然后我们输入lsmod查看我们是否加入成功
在这里插入图片描述
发现已经存在了,代表已经插入成功
这个时候,我们再使用dmesg 查看我们插入驱动的输出信息,发现确实已经输出了
在这里插入图片描述
当我们想要把这个设备卸载的时候,我们使用sudo rmmod helloDev即可以把驱动卸载,卸载之后,再用lsmod就看不到啦

但这两个指令并不只能,先进工具modprobe更智能,它还会自动加载任何安装的模块及任何它所依赖的模块
插入模块指令modprobe module [ module parameters] 卸载模块指令modprobe -r modules,都需要在root权限下运行

  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

暮尘依旧

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值