linux驱动开发学习一:创建一个字符设备
首先是内核初始化函数。代码如下。主要是三个步骤。1 生成设备号。 2 注册设备号。3 创建设备。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | #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插入到了链表中去
用段代码来验证下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | 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节点。同样的也可以用这种方式进行删除节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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的初始化。其中最重要的工作就是注册设备的操作函数。设备的注册函数实现如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | 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;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | 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; } |
至此设备的初始化,注册,插入功能都已全部完成,整个过程可以归纳如下:
1. 注册设备号, 通过调用 __register_chrdev_region() 来实现
2. 分配一个cdev, 通过调用 cdev_alloc() 来实现
3. 将cdev添加到驱动模型中, 这一步将设备号和驱动关联了起来. 通过调用 cdev_add() 来实现
4. 将第一步中创建的 struct char_device_struct 对象的 cdev 指向第二步中分配的cdev.由于register_chrdev()是老的接口,这一步在新的接口中并不需要。文件系统中对字符设备文件的访问 ,对于一个字符设备文件, 其inode->i_cdev 指向字符驱动对象cdev, 如果i_cdev为 NULL ,则说明该设备文件没有被打开.,由于多个设备可以共用同一个驱动程序。所以,通过字符设备的inode 中的i_devices 和 cdev中的list组成一个链表
首先,系统调用open打开一个字符设备的时候, 通过一系列调用,最终会执行到 chrdev_open。(最终是通过调用到def_chr_fops中的.open, 而def_chr_fops.open = chrdev_open.
int chrdev_open(struct inode * inode, struct file * filp)
chrdev_open()所做的事情可以概括如下:
1. 根据设备号(inode->i_rdev), 在字符设备驱动模型中查找对应的驱动程序, 这通过kobj_lookup() 来实现, kobj_lookup()会返回对应驱动程序cdev的kobject.
2. 设置inode->i_cdev , 指向找到的cdev.
3. 将inode添加到cdev->list的链表中.
4. 使用cdev的ops 设置file对象的f_op
5. 如果ops中定义了open方法,则调用该open方法
6. 返回.
执行完 chrdev_open()之后,file对象的f_op指向cdev的ops,因而之后对设备进行的read, write等操作,就会执行cdev的相应操作。
下面来试下功能。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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架
2017-11-05 Django之CSRF
2017-11-05 Django之模型的创建