linux驱动开发学习一:创建一个字符设备
首先是内核初始化函数。代码如下。主要是三个步骤。1 生成设备号。 2 注册设备号。3 创建设备。
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h> #define GLOBALMEM_SIZE 0X1000
#define MEM_CLEAR 0X1
#define GLOBALMEM_MAJOR 230 static int globalmem_major= GLOBALMEM_MAJOR;
module_param(globalmem_major,int,S_IRUGO); struct globalmem_dev{
struct cdev cdev;
unsigned char mem[GLOBALMEM_SIZE];
}; static int __init globalmem_init(void)
{
int ret;
dev_t devno=MKDEV(globalmem_major,0); (1)
if(globalmem_major)
ret=register_chrdev_region(devno,1,"globalmem_tmp"); (2)
else{
ret=alloc_chrdev_region(&devno,0,1,"globalmem_tmp");
globalmem_major=MAJOR(devno);
}
if(ret < 0)
return ret;
globalmem_devp=kzalloc(sizeof(struct globalmem_dev),GFP_KERNEL);
if(!globalmem_devp){
ret=-EFAULT;
goto fail_malloc;
} globalmem_setup_dev(globalmem_dev,0); (3)
return 0;
fail_malloc:
unregister_chrdev_region(devno,1);
return ret;
}
(1) 生成设备号
我们要注册一个设备,首先要生成这个设备的设备号。这里先分配一块大小为4KB的内存空间。同时将该值赋值给globalmem_major用于生成设备号
Linux的设备管理是和文件系统紧密结合的,各种设备都以文件的形式存放在/dev目录下,称为设备文件。应用程序可以打开、关闭和读写这些设备文件,完成对设备的操作,就像操作普通的数据文件一样。为了管理这些设备,系统为设备编了号,每个设备号又分为主设备号和次设备号。主设备号用来区分不同种类的设备,而次设备号用来区分同一类型的多个设备
如下在dev下的设备,中,都是以b开头的。证明都是block设备。然后主设备号都是7,0,1,10都是次设备号
nb-test:/dev$ ls -al
brw-rw---- 1 root disk 7, 0 10月 24 16:36 loop0
brw-rw---- 1 root disk 7, 1 10月 24 16:36 loop1
brw-rw---- 1 root disk 7, 10 10月 24 16:36 loop10
和设备号相关的代码如下,
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
设备号是个32bit,高12bit是主设备号,低20bit是次设备号。MAJOR宏将设备号向右移动20位得到主设备号,MINOR将设备号的高12位清0。MKDEV将主设备号ma左移20位,然后与次设备号mi相与得到设备号。
(2) 注册设备号
设备号生成,接下来的任务就是将设备号注册到系统中去。由于我们是创建有一个字符型的设备,因此调用函数register_chrdev_region。
函数的原型:int register_chrdev_region(dev_t from, unsigned count, const char *name)
from是设备号,count是设备个数,name是设备名。实际上在里面调用的是
__register_chrdev_region 函数。这里面主要步骤包含几个
>1 申请一个设备结构体内存
cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
>2在chrdevs中找到cd的插入位置,在chrdevs中是以升序排列的。
for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
if ((*cp)->major > major ||
((*cp)->major == major &&
(((*cp)->baseminor >= baseminor) ||
((*cp)->baseminor + (*cp)->minorct > baseminor))))
break;
chrdevs是一个结构体指针数组,里面存储的的都是每个结构体的指针。这里为什么要用到结构体指针数组,下面会介绍
static struct char_device_struct {
struct char_device_struct *next;
unsigned int major;
unsigned int baseminor;
int minorct;
char name[64];
struct cdev *cdev; /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
>3 找到位置后,将cd插入到cp中去。这一段插入充分利用了指针的性质,在对于一个单链表的插入来说非常的巧妙。
cd->next = *cp;
*cp = cd;
cd和cp的类型申明如下。
struct char_device_struct *cd, **cp;
cd是char_device_struct的指针。cp是char_device_struct 指针的指针。在前面寻找插入位置的时候。循环控制方式如下,也就是说cp指向的是上一个节点的next指针的地址。
for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
cd->next=*cp这个好理解,就是将cd的下一个节点指向*cp。那么*cp=cd相对比较抽象,这个的意思将cp地址存储的内容修改为cd。而cp地址指向的是上一个节点的next指针地址,将整个*cp赋值为cd,也就是将上一个节点的next指针地址所存储的值变为cd。这样就实现了将cd插入到了链表中去
用段代码来验证下:
struct linklist
{
int num;
struct linklist *next;
};
int main(int argc, char **argv)
{
int i;
struct linklist head;
struct linklist_tmp *s;
head.num = 0;
head.next = NULL;
struct linklist *tmp = NULL;
struct linklist **ttmp = NULL; len = sizeof(a)/sizeof(int);
for (i = 1; i < 6; i += 2)
{
tmp = (struct linklist *)malloc(sizeof(struct linklist));
tmp->num = i;
tmp->next = head.next;
head.next = tmp;
}
ttmp = &(head.next);
while (*ttmp)
{
printf("%d, %016x, %016x, %016x\n", (*ttmp)->num, ttmp, *ttmp, (*ttmp)->next);
ttmp = &((*ttmp)->next);
} printf("============================\n");
struct linklist addnode = { .num = 2,.next = NULL };
ttmp = &(head.next);
while (*ttmp)
{
if ((*ttmp)->num < addnode.num)
{
break;
}
ttmp = &((*ttmp)->next);
}
addnode.next = *ttmp;
*ttmp = &addnode;
ttmp = &(head.next);
while (*ttmp)
{
printf("%d, %016x, %016x, %016x,%016x\n", (*ttmp)->num, ttmp, *ttmp, (*ttmp)->next,&((*ttmp)->next));
ttmp = &((*ttmp)->next);
} return 0;
}
执行结果如下:
可以看到节点值为2 指针的指针就是以前节点值为1的地址。而节点值为1 指针的指针则被挪到了另外一个位置。
用下面这个图来表示更直观,*cp = cd; 也就意味着地址为1d7696c存储的值变为0b3fab4,而地址0b3fab4存储的节点就是插入的节点2。而0b3fab4指向节点1的地址也就是1d76930。而1d76930的地址则变为另外一个。
通过这种二级指针的方式实现了单链表的插入。这种方法避免了传统的删除或插入链表节点需要记录链表prev节点。同样的也可以用这种方式进行删除节点
void remove_if(node ** head, remove_fn rm)
{
for (node** curr = head; *curr; )
{
node * entry = *curr;
if (rm(entry))
{
*curr = entry->next;
free(entry);
}
else
curr = &entry->next;
}
}
(3) Cdev的初始化和添加。
>1 首先是cdev的初始化。其中最重要的工作就是注册设备的操作函数。设备的注册函数实现如下。
static int globalmem_open(struct inode *inode,struct file *filp)
{
filp->private_data=globalmem_devp;
return 0;
} static int globalmem_release(struct inode *inode,struct file *filp)
{
return 0;
} static long globalmem_ioctl(struct file *filp,unsigned int cmd,unsigned long arg)
{
struct globalmem_dev *dev=filp->private_data;
switch(cmd)
{
case MEM_CLEAR:
memset(dev->mem,0,GLOBALMEM_SIZE);
printk(KERN_INFO "globalmem is set to zero\n");
default:
return -EINVAL;
}
return 0;
} static ssize_t globalmem_read(struct file *filp,char __user *buf,size_t size,loff_t *ppos)
{
unsigned long p=*ppos;
unsigned int count=size;
int ret=0;
struct globalmem_dev *dev=filp->private_data;
if(p > GLOBALMEM_SIZE)
return 0;
if(count > GLOBALMEM_SIZE-p)
count=GLOBALMEM_SIZE-p;
if(copy_to_user(buf,dev->mem+p,count)){
ret=-EFAULT;
}
else{
*ppos+=count;
ret=count;
}
printk(KERN_INFO “read %u bytes(s) from %lu\n”,count,p);
return ret;
} static ssize_t globalmem_write(struct file *filp,const char __user *buf,size_t size, loff_t *ppos)
{
unsigned long p=*ppos;
unsigned int count=size;
int ret=0;
struct globalmem_dev *dev=filp->private_data;
if(p > GLOBALMEM_SIZE)
return 0;
if(count > GLOBALMEM_SIZE-p)
count=GLOBALMEM_SIZE-p;
if(copy_from_user(dev->mem+p,buf,count))
ret=-EFAULT;
else{
*ppos+=count;
ret=count;
printk(KERN_INFO "written %u bytes(s) from %lu\n",count,p);
}
return ret;
} static loff_t globalmem_llseek(struct file *filp,loff_t offset,int orig)
{
loff_t ret=0;
switch(orig){
case 0:
if (offset <0)
ret=-EFAULT;
break;
if ((unsigned int)offset > GLOBALMEM_SIZE){
ret=-EFAULT;
break;
}
filp->f_pos=(unsigned int)offset;
ret=filp->f_pos;
break;
case 1:
if((filp->f_pos+offset) > GLOBALMEM_SIZE){
ret=-EFAULT;
break;
}
if((filp->f_pos+offset) < 0){
ret=-EFAULT;
break;
}
filp->f_pos+=offset;
ret=filp->f_pos;
break;
}
return ret;
}
globalmem_fops就是操作的函数指针结构体。
static const struct file_operations globalmem_fops={
.owner=THIS_MODULE,
.llseek=globalmem_llseek,
.read=globalmem_read,
.write=globalmem_write,
.unlocked_ioctl=globalmem_ioctl,
.open=globalmem_open,
.release=globalmem_release,
};
cdev_init的工作就是将这些操作函数赋给cdev->ops
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
这里还有一个kobject_init函数,是用来初始化kobj对象的。这个下面介绍
>2 添加cdev设备。这里首先介绍kobj_map结构体
struct kobj_map {
struct probe {
struct probe *next; 链表结构
dev_t dev; 设备号
unsigned long range; 设备号的范围
struct module *owner;
kobj_probe_t *get;
int (*lock)(dev_t, void *);
void *data; 指向struct cdev对象
} *probes[255];
struct mutex *lock;
};
结构体中有一个互斥锁lock,一个probes[255]数组,数组元素为struct probe的指针。
根据下面的函数作用来看,kobj_map结构体是用来管理设备号及其对应的设备的。
kobj_map函数就是将指定的设备号加入到该数组,kobj_lookup则查找该结构体,然后返回对应设备号的kobject对象,利用利用该kobject对象,我们可以得到包含它的对象如cdev。struct probe结构体中的get函数指针就是用来获得kobject对象的
因此cdev_add其实就是想kobj中添加设备的过程,具体实现是用kobj_map函数。
其中cdev_map是定义在char_dev.c中的一个静态变量。
static struct kobj_map *cdev_map;
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
p->dev = dev;
p->count = count;
return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}
Kobj_map的代码如下
int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,
struct module *module, kobj_probe_t *probe,
int (*lock)(dev_t, void *), void *data)
{
unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;
unsigned index = MAJOR(dev);
unsigned i;
struct probe *p; if (n > 255)
n = 255; p = kmalloc(sizeof(struct probe) * n, GFP_KERNEL); if (p == NULL)
return -ENOMEM; for (i = 0; i < n; i++, p++) {
p->owner = module;
p->get = probe;
p->lock = lock;
p->dev = dev;
p->range = range;
p->data = data;
}
mutex_lock(domain->lock);
for (i = 0, p -= n; i < n; i++, p++, index++) {
struct probe **s = &domain->probes[index % 255];
while (*s && (*s)->range < range)
s = &(*s)->next;
p->next = *s;
*s = p;
}
mutex_unlock(domain->lock);
return 0;
}
至此设备的初始化,注册,插入功能都已全部完成,下面来试下功能。Makefile文件如下
#Makefile文件注意:假如前面的.c文件起名为first.c,那么这里的Makefile文件中的.o文
#件就要起名为first.o 只有root用户才能加载和卸载模块
obj-m:=global_test.o #产生global_test模块的目标文件
#目标文件 文件 要与模块名字相同
CURRENT_PATH:=$(shell pwd) #模块所在的当前路径
LINUX_KERNEL:=$(shell uname -r) #linux内核代码的当前版本
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
CONFIG_MODULE_SIG=n
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean #清理模块
插入模块:sudo insmod global_test.ko。 此时在/proc/devices下能看到多出了主设备号为230的globalmem_tmp字符设备驱动
接下来创建节点,执行命令sudo mknod -m 766 /dev/globalmem_tmp c 230 0。 显示创建成功
cat /dev/globalmem_tmp 读取设备数据。可以看到能正常的读出数据
test:~/linux_prj/globalman$ cat /dev/globalmem_tmp
hello world
linux驱动开发学习一:创建一个字符设备的更多相关文章
- Linux驱动开发学习的一些必要步骤
1. 学会写简单的makefile 2. 编一应用程序,可以用makefile跑起来 3. 学会写驱动的makefile 4. 写一简单char驱动,makefile编译通过,可以insmod, ...
- Linux驱动实践:你知道【字符设备驱动程序】的两种写法吗?
作 者:道哥,10+年嵌入式开发老兵,专注于:C/C++.嵌入式.Linux. 关注下方公众号,回复[书籍],获取 Linux.嵌入式领域经典书籍:回复[PDF],获取所有原创文章( PDF 格式). ...
- Linux驱动开发学习笔记(1):LINUX驱动版本的hello world
1.关于目录 /lib/modules/2.6.9-42.ELsmp/build/ 这个是内核源码所在的目录 一般使用这样的命令进入这个目录:cd /lib/modules/$(una ...
- 在dev目录创建一个字符设备驱动的流程
1.struct file_operations 字符设备文件接口 1: static int mpu_open(struct inode *inode, struct file *file) 2: ...
- linux驱动开发学习二:创建一个阻塞型的字符设备
在Linux 驱动程序中,可以使用等待队列来实现阻塞进程的唤醒.等待队列的头部定义如下,是一个双向列表. struct list_head { struct list_head *next, *pre ...
- (57)Linux驱动开发之三Linux字符设备驱动
1.一般情况下,对每一种设备驱动都会定义一个软件模块,这个工程模块包含.h和.c文件,前者定义该设备驱动的数据结构并声明外部函数,后者进行设备驱动的具体实现. 2.典型的无操作系统下的逻辑开发程序是: ...
- Linux内核(17) - 高效学习Linux驱动开发
这本<Linux内核修炼之道>已经开卖(网上的链接为: 卓越.当当.china-pub ),虽然是严肃文学,但为了保证流畅性,大部分文字我还都是斟词灼句,反复的念几遍才写上去的,尽量考虑到 ...
- Hasen的linux设备驱动开发学习之旅--时钟
/** * Author:hasen * 參考 :<linux设备驱动开发具体解释> * 简单介绍:android小菜鸟的linux * 设备驱动开发学习之旅 * 主题:时钟 * Date ...
- Linux驱动开发2——字符设备驱动
1.申请设备号 #include <linux/fs.h> int register_chrdev_region(dev_t first, unsigned int count, char ...
随机推荐
- python 实现 AES ECB模式加解密
AES ECB模式加解密使用cryptopp完成AES的ECB模式进行加解密. AES加密数据块分组长度必须为128比特,密钥长度可以是128比特.192比特.256比特中的任意一个.(8比特 == ...
- 错误 Unable to connect to a repository at URL 'svn://ip地址' 和 No repository found in 'svn://ip地址'
SVN服务器是CentOS6.10 使用TortoiseSVN客户端检出时遇到如下图所示的错误: 是因为没有指定SVN仓库的路径 在SVN服务器执行命令:svnserve -d -r /SVN版本库的 ...
- 33、安装MySQL
一.Windows安装MySQL 1.下载 打开网址,页面如下,确认好要下载的操作系统,点击Download. 可以不用登陆或者注册,直接点击No thanks,just start my downl ...
- Ruby break, next, redo, retry
# -*- coding: UTF-8 -*- # E3.10-5.rb 演示break, next, redo, retry puts "演示break" c='a' for i ...
- 证明StringBuffer线程安全,StringBuilder线程不安全
证明StringBuffer线程安全,StringBuilder线程不安全证明StringBuffer线程安全StringBuilder线程不安全测试思想测试代码结果源码分析测试思想分别用1000个线 ...
- redux沉思录
要素:store.reducer.dispatch/subscribe connect:将业务逻辑剥离到容器类,数据的双向绑定: 数据.操作.UI分离.命令封装 核心思想:对共享状态的维护: 核心代码 ...
- 2019 Nowcoder Multi-University Training Contest 1 H-XOR
由于每个元素贡献是线性的,那么等价于求每个元素出现在多少个异或和为$0$的子集内.因为是任意元素可以去异或,那么自然想到线性基.先对整个集合A求一遍线性基,设为$R$,假设$R$中元素个数为$r$,那 ...
- OpenCV 学习笔记(10)HSV颜色空间及颜色空间转换(RGB-HSV)
1.1 颜色空间介绍 RGB 颜色空间是大家最熟悉的颜色空间,即三基色空间,任何一种颜色都可以由该三种 颜色混合而成.然而一般对颜色空间的图像进行有效处理都是在 HSV 空间进行的,HSV(色 调 H ...
- Vuejs组件基础
一.概念 ①组件就是对局部视图的封装,组件思想就是把一个很大的复杂的 Web 页面视图给拆分成一块一块的组件视图,然后利用某种特定的方式把它们组织到一起完成完整的 Web 应用构建. ②目前主流的前端 ...
- 第03组 Alpha冲刺
队名:不等式方程组 组长博客 作业博客 团队项目进度 组员一:张逸杰(组长) 过去两天完成的任务: 文字/口头描述: 制定了初步的项目计划,并开始学习一些推荐.搜索类算法 GitHub签入纪录: 暂无 ...