Linux字符设备驱动从helloworld认识内核

7 篇文章 0 订阅
2 篇文章 0 订阅
本文讲解了Linux内核中的模块化结构,重点介绍了字符设备驱动的定义、操作流程,包括模块的加载、卸载、以及如何通过mknod创建设备节点。还提供了示例代码和Makefile编译指导,以及如何在用户态与内核态之间交互。
摘要由CSDN通过智能技术生成

字符设备是内核的接口的一种,即可以通过字符设备进行用户空间和内核空间的交互,这里侧重讲下字符设备,对模块知识及内核态用户态略微提及,后面会单独发相关的帖。

1、模块化

Linux 内核整体结构很庞大,包含了很多的组件,目前最多的也是最好的处理方式:将需要的功能编译成模块,在需要的时候动态地加载,包含进内核当中。这也就是所谓的模块化

常用操作

insmod 将模块添加进内核
rmmod 将模块从内核卸载
lsmod 查看已安装到内核的模块
modprobe 载入指定的个别模块,或是载入一组相依赖的 模块。 modprobe 会根据 depmod 所产生的依赖关系,决 定要载入哪些模块。若在载入 过程中发生错误,在 modprobe 会卸载整组的模块。依赖关系是通过读取 /lib/modules/2.6.xx/modules.dep 得到的。而该文件是通过 depmod 所建立。
modinfo 查看模块信息。使用方法:modinfo XXX.ko
tree -a 查看当前目录的整个树结构

注意:这些操作需要超级用户权限,即root权限。

2、字符驱动设备

定义
字符设备是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。

helloworld.c
废话不多说,show code:

#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 (32)
#define SUCCESS (0)
#define ERROR (-1)

struct cdev *gDev;//使用cdev结构体来描述字符设备
struct file_operations *gFile;//通过其成员file_operations来定义字符设备驱动提供给VFS的接口函数,如常见的open()、read()、write()等

dev_t devNum;//通过其成员dev_t来定义设备号(分为主、次设备号)以确定字符设备的唯一性

unsigned int subDevNum = 1;//请求的连续设备个数
int reg_major = 813;//主设备号
int reg_minor = 0;  //次设备号
char *buffer;
/*定义内核相关操作函数open()、read()、write()*/
int hello_open(struct inode *p, struct file *f)
{
	 printk(KERN_INFO "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_INFO "hello_write\r\n");
	  return 0;
}
ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l)
{
	 printk(KERN_INFO "hello_read\r\n");
	  return 0;
}

/*初始化函数*/
int hello_init(void)
{
	 devNum = MKDEV(reg_major, reg_minor);//通过主次设备号生成设备号
	 if(SUCCESS == register_chrdev_region(devNum, subDevNum, "helloworld")){
		   printk(KERN_INFO "register_chrdev_region ok \n");
	 }
	 else {
		 printk(KERN_INFO "register_chrdev_region error n");
		 return ERROR;
	 }
	 printk(KERN_INFO " hello driver init \n");
	 gDev = kzalloc(sizeof(struct cdev), GFP_KERNEL);
	 gFile = kzalloc(sizeof(struct file_operations), GFP_KERNEL);
   
	 gFile->open = hello_open;
	 gFile->read = hello_read;
	 gFile->write = hello_write;
	
	 gFile->owner = THIS_MODULE;
	  
	 cdev_init(gDev, gFile);
	 cdev_add(gDev, devNum, 1);
		
	 return 0;
}
void __exit hello_exit(void)
{
	 printk(KERN_INFO " hello driver exit \n");
	 
	 cdev_del(gDev);
	  
	 kfree(gFile);
	   
	 kfree(gDev);
	
	 unregister_chrdev_region(devNum, subDevNum);
		
	 return;
}
/*模块操作相关*/
module_init(hello_init); 
module_exit(hello_exit);
MODULE_LICENSE("GPL");

代码里已添加了简单注释说明,为了增加可读性,一些需要自行查阅相关书籍或搜索学习。(可直接复制使用)

Makefile
直接上代码:

ifneq ($(KERNELRELEASE),)
obj-m := helloworld.o
else
	PWD := $(shell pwd)
KDIR:= /lib/modules/3.10.0-1127.el7.x86_64/build
#KDIR := /lib/modules/`uname -r`/build
all:
	make -C $(KDIR) M=$(PWD)
clean:
	rm -rf *.o *.ko *.mod.c *.symvers *.c~ *~
endif

makefile我学过一些,和用户态的makefile有些差异,这里不再详细说明,只把需要注意的一些地方说一下:
1、开始宏KERNELRELEASE并未定义,进入else分支顺序执行到最后,并进入内核,由于内核里定义了宏KERNELRELEASE,makefile文件将执行第二次,将执行obj-m := helloDev.o,进行编译。
2、KDIR这里指向编译模块的架构,你可以通过命令:uname -r查看。

展示:
未编译:
在这里插入图片描述
编译:
在这里插入图片描述
将.c文件和makefile文件放在同一路径即可,编译后可以看见生成的,.o、.ko等文件,还有build-in.o(感兴趣可学习下),再输入命令:make clean。就可以执行rm -rf *.o *.ko *.mod.c *.symvers *.c~ *~了。

3、模块加载、卸载等

通过上述操作已经生成了我们需要的ko文件。
下面就是加载到内核了,前面说过这个操作需要超级管理员权限。
在这里插入图片描述
通过命令:
insmod helloworld.ko
即可插入到内核。

如何查看是否插入成功呢?
1、lsmod
在这里插入图片描述
2、dmesg
在这里插入图片描述
可以看到helloworld.c代码里的打印。
另外,可以用dmesg -c 先清除下。

4、字符设备映射

Linux下的设备都在/dev目录下
在这里插入图片描述
如何使用这些驱动呢?
需要mknod 命令建立一个目录项和一个特殊文件的对应索引节点(即字符设备文件)

使用说明:
mknod [options] name {bc} major minor
name:设备名称
{bc}:设备类型
p FIFO型
b 块文件
c 字符文件
major、minor主次设备号。
ps:由root 用户或系统组成员运行。

创建设备节点:
mknod /dev/hello c 813 0
在这里插入图片描述
可以看到创建的字符设备节点hello。
后面可以使用节点hello来测试了。

5、应用层程序测试

我们这里写一个用户态的程序测试下前面的内核代码(ko文件)。
内核态、用户态如何交互呢,这里不详细说明了,简单po张图来展示下。
字符设备、字符设备驱动与用户空间的关系
在这里插入图片描述
test.c

#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/select.h>

#define DATA_NUM 64

int main(int argc,char *argv[]){
	int fd,i;
	int r_len,w_len;
	fd_set fdset;
	char buf[DATA_NUM] = "hello world";
	memset(buf,0,sizeof(buf));
	fd = open("/dev/hello",O_RDWR);//通过open函数打开之前的字符设备节点
	printf("%d\r\n",fd);

	if(-1 == fd){
		perror("open file error\r\n");
		return -1;
	}
	else{
		printf("open successe\r\n");
	}

	w_len = write(fd,buf,DATA_NUM);//调用内核的write函数
	r_len = read(fd,buf,DATA_NUM);
	printf("%d %d \r\n",w_len,r_len);
	printf("%s\r\n",buf);

	return 0;
}

当然这里没有做太多打印,只是测试下,说明这样一个过程,后面有时间优化下代码,然后再展示说明。

参考

《Linux设备驱动程序》第三版
b站:技术简说
http://blog.sina.com.cn/s/blog_7da9e4aa0101lrp9.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值