天天看点

Linux自己编写一个字符设备驱动的实例(+代码)字符设备驱动

字符设备驱动

Linux字符设备提供连续的数据流,应用程序可以顺序读取,通常不支持随机存取。相反,此类设备支持按字节/字符来读写设备。举例来说,键盘,串口,调制解调器都是典型的字符设备。

设备分类

linux系统将设备分为3类:字符设备、块设备、网络设备。

  • 字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。
  • 块设备:是指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。
  • 网络设备:网络设备比较特殊,不在是对文件进行操作,而是由专门的网络接口来实现。应用程序不能直接访问网络设备驱动程序。在/dev目录下也没有文件来表示网络设备。

每一个字符设备或块设备都在/dev目录下对应一个设备文件。linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备。

相关函数调用

struct cdev 描述字符设备的结构体

struct cdev {
	struct kobject kobj;//内嵌的内核对象.  
	struct module *owner;//该字符设备所在的内核模块(所有者)的对象指针,一般为THIS_MODULE主要用于模块计数  
	const struct file_operations *ops;//该结构描述了字符设备所能实现的操作集(打开、关闭、读/写、...),是极为关键的一个结构体
	struct list_head list;//用来将已经向内核注册的所有字符设备形成链表
	dev_t dev;//字符设备的设备号,由主设备号和次设备号构成(如果是一次申请多个设备号,此设备号为第一个)
	unsigned int count;//隶属于同一主设备号的次设备号的个数
};
           

cdev_alloc 动态申请(构造)cdev内存(设备对象)

struct cdev *cdev_alloc(void);  

struct cdev *cdev_alloc(void)
{
	struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
    //为设备申请内核内存,并对申请到的内存内容清零,GFP_KERNEL —— 正常分配内存。
	if (p) {
		INIT_LIST_HEAD(&p->list);//初始化链表
		kobject_init(&p->kobj, &ktype_cdev_dynamic);//初始化内核对象
	}
	return p;
}

           

成功的话返回值为cdev对象首地址

cdev_init 初始化cdev的成员,并建立cdev和file_operations之间关联起来

void cdev_init(struct cdev *p, const struct file_operations *p);
/* 参数:
    struct cdev *p - 被初始化的 cdev对象
    const struct file_operations *fops - 字符设备操作方法集 */
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
	memset(cdev, 0, sizeof *cdev);
    //将为cdev设置的内存空间初始化,全设为0.
	INIT_LIST_HEAD(&cdev->list);
	kobject_init(&cdev->kobj, &ktype_cdev_default);
	cdev->ops = fops;//建立cdev和file_operations之间的关系
}
           

cdev_add 注册cdev设备对象(添加到系统字符设备列表中)

int cdev_add(struct cdev *p, dev_t dev, unsigned count);
/* 参数:
    struct cdev *p - 被注册的cdev对象
    dev_t dev - 设备的第一个设备号
    unsigned - 这个设备连续的次设备号数量
   返回值:
    成功:0
    失败:负数(绝对值是错误码)*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
	p->dev = dev;
	p->count = count;//填充cdev结构体
	return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);//添加设备号到系统字符设备列表中
}

           

cdev_del 将cdev对象从系统中移除(注销 )

void cdev_del(struct cdev *p);
/*参数: 
    struct cdev *p - 要移除的cdev对象 */
void cdev_del(struct cdev *p)
{
	cdev_unmap(p->dev, p->count);
	kobject_put(&p->kobj);//减少内核对象的引用计数
}

static void cdev_unmap(dev_t dev, unsigned count)
{
	kobj_unmap(cdev_map, dev, count);//将设备号从系统字符设备列表中删除
}
           

cdev_put 释放cdev内存

void cdev_put(struct cdev *p);
/*参数:
    struct cdev *p - 要移除的cdev对象 */
void cdev_put(struct cdev *p)
{
	if (p) {
		struct module *owner = p->owner;
		kobject_put(&p->kobj);
		module_put(owner);//模块卸载
	}
}
           

设备号申请/释放

一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。linux内核中,设备号用dev_t来描述:

typedef u_long dev_t;  // 在32位机中是4个字节,高12位表示主设备号,低20位表示次设备号。 
           

实现dev_t的宏

#define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))
    #define MINORBITS	20
    //dev右移20位得到主设备号,即为高12位

	#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))
	#define MINORMASK	((1U << MINORBITS) - 1)
	//MINOR宏将dev_t的高12位清零,得到次设备号。
	
           

设备号申请的方法

静态:

int register_chrdev_region(dev_t from, unsigned count, const char *name);
/*功能:申请使用从from开始的count 个设备号(主设备号不变,次设备号增加)*/
           

静态申请相对较简单,但是一旦驱动被广泛使用,这个随机选定的主设备号可能会导致设备号冲突,而使驱动程序无法注册。

动态:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
/*功能:请求内核动态分配count个设备号,且次设备号从baseminor开始。*/
           

动态申请简单,易于驱动推广,但是无法在安装驱动前创建设备文件(因为安装前还没有分配到主设备号)。

释放设备号

编写简单的字符设备驱动

device_drive.c

# include <linux/module.h>
# include <linux/fs.h>
# include <linux/uaccess.h>
# include <linux/init.h>
# include <linux/cdev.h>
# define DEMO_NAME "my_demo_dev"

static dev_t dev;
static struct cdev *demo_cdev;
static signed count = 1;
//打开操作
static int demodrv_open(struct inode *inode, struct file *file)
{
	int major = MAJOR(inode->i_rdev);
   
	int minor = MINOR(inode->i_rdev);

printk("%s: major=%d, minor=%d\n",__func__,major,minor);
return 0;
}

//读操作
static ssize_t demodrv_read(struct file *file, char __user *buf,size_t lbuf,loff_t *ppos)
{
	printk("%s enter\n",__func__);
	

return 0;

}

//写操作
static ssize_t demodrv_write(struct file *file, const char __user *buf,size_t count,loff_t *f_pos)
{
	printk("%s enter\n",__func__);
	

return 0;

}

//实现设备操作 
static const struct file_operations demodrv_fops = {
	.owner = THIS_MODULE,
	.open = demodrv_open,
	.read = demodrv_read,
	.write = demodrv_write
};


static int __init simple_char_init(void)
{
	int ret;

ret = alloc_chrdev_region(&dev,0,count,DEMO_NAME);
    /*
    int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
    功能:请求内核动态分配count个设备号,且次设备号从baseminor开始。
    */
if(ret)
{
	printk("failed to allocate char device region\n");
	return ret;
}
demo_cdev = cdev_alloc();
if(!demo_cdev) 
{
	printk("cdev_alloc failed\n");
	goto unregister_chrdev;
}

cdev_init(demo_cdev,&demodrv_fops);

ret = cdev_add(demo_cdev,dev,count);
if(ret)
{
	printk("cdev_add failed\n");
	goto cdev_fail;
}

printk("successed register char device: %s\n",DEMO_NAME);
printk("Major number = %d,minor number = %d\n",MAJOR(dev),MINOR(dev));

return 0;

cdev_fail:
	cdev_del(demo_cdev);

unregister_chrdev:
	unregister_chrdev_region(dev,count);

return ret;

} 


static void __exit simple_char_exit(void)
{
	printk("removing device\n");

if(demo_cdev)
	cdev_del(demo_cdev);

unregister_chrdev_region(dev,count);

}

module_init(simple_char_init);
module_exit(simple_char_exit);

MODULE_LICENSE("GPL");
           

Makefile

#Makefile文件注意:假如前面的.c文件起名为first.c,那么这里的Makefile文件中的.o文
#件就要起名为first.o    只有root用户才能加载和卸载模块
obj-m:=device_drive.o                          #产生device_drive模块的目标文件
#目标文件  文件  要与模块名字相同
CURRENT_PATH:=$(shell pwd)             #模块所在的当前路径
LINUX_KERNEL:=$(shell uname -r)        #linux内核代码的当前版本
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)

all:
	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules    #编译模块
#[Tab]              内核的路径       当前目录编译完放哪  表明编译的是内核模块

clean:
	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean      #清理模块
           

test.c

# include <stdio.h>

# include <fcntl.h>

# include <unistd.h>

# define DEMO_DEV_NAME "/dev/demo_drv"

int main() 
{
	char buffer[64];
	int fd;

fd = open(DEMO_DEV_NAME,O_RDONLY);
if(fd<0) 
{
	printf("open device %s failed\n",DEMO_DEV_NAME);
	return -1;
}

read(fd,buffer,64);
close(fd);

return 0;

}