Linux内核分析(五)----字符设备驱动实现

简介: 原文:Linux内核分析(五)----字符设备驱动实现Linux内核分析(五) 昨天我们对linux内核的子系统进行简单的认识,今天我们正式进入驱动的开发,我们今后的学习为了避免大家没有硬件的缺陷,我们都会以虚拟的设备为例进行学习,所以大家不必害怕没有硬件的问题。
原文: Linux内核分析(五)----字符设备驱动实现

Linux内核分析(五)

昨天我们对linux内核的子系统进行简单的认识,今天我们正式进入驱动的开发,我们今后的学习为了避免大家没有硬件的缺陷,我们都会以虚拟的设备为例进行学习,所以大家不必害怕没有硬件的问题。

今天我们会分析到以下内容:

1.      字符设备驱动基础

2.      简单字符设备驱动实现

3.      驱动测试

 

l  字符设备基础

1.       字符设备描述结构

linux2.6内核中,使用cdev结构体描述一个字符设备,其定义如下:

 

1 struct cdev {
2     struct kobject kobj;/*基于kobject*/
3     struct module *owner; /*所属模块*/
4     const struct file_operations *ops; /*设备文件操作函数集*/
5     struct list_head list; 
6     dev_t dev; /*设备号*/
7     unsigned int count; /*该种类型设备数目*/
8 };

上面结构中需要我们进行初始化的有opsdev,下面我们会对这两个成员进行分析。

注:kobject结构是驱动中很重要的一个结构,由于其复杂性,我们现在不进行介绍,后面会详细介绍。

2.       设备号

1.        何为设备号:cdev结构体中dev成员定义了设备号,而dev_t则为U32类型的也就是32位,其中12位为主设备号,20位为次设备号。我们执行ls –l /dev/可看到下图,其中左边红框为主设备号,右边为次设备号

2.        何为主设备号:用来对应该设备为何种类型设备。(比如串口我们用一个数字识别,而串口有好几个)

3.        何为次设备号:用来对应同一类型设备下的具体设备。(用次设备号来具体区分是哪个串口)

4.        设备号相关操作:

1.        通过主设备号和次设备号获取devdev = MKDEV(主,次);

2.        通过dev获取主设备号: = MAJOR(dev);

3.        通过dev获取次设备号:dev = MINOR(dev);

5.        设备号分配:设备号的分配有两种方式,一种是静态的,另一种是动态的,下面一一分析

1.        静态分配:也就是程序员自己指定设备号,通过register_chrdev_region();函数向内核申请,可能会导致和内核已有的冲突,从而失败。

2.        动态分配:通过 alloc_chrdev_region(); 函数向内核申请设备号。

3.        释放设备号:通过 unregister_chrdev_region(); 释放申请到的设备号。

3.       file_operations操作函数集

file_operations结构体中的成员函数在我们驱动开发过程中极为重要,其中的内容相当庞大,下面我们看看其定义:

 

 1 struct file_operations {
 2     struct module *owner;/*拥有该结构的模块的指针,一般为THIS_MODULES*/
 3     loff_t (*llseek) (struct file *, loff_t, int); /*用来修改当前文件的读写指针*/
 4     ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);/*从设备读取数据*/
 5     ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);/*向设备发送数据*/
 6     ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); /*初始化一个异步的读取操作*/
 7     ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); /*初始化一个异步的写入操作*/
 8     int (*readdir) (struct file *, void *, filldir_t); /*只用于读取目录,对于设备文件该字段为NULL*/
 9     unsigned int (*poll) (struct file *, struct poll_table_struct *);/*轮询函数,判断目前是否可以进行非阻塞的读取或写入*/
10     long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); /* 不用BLK的文件系统,将使用此函数代替ioctl*/
11     long (*compat_ioctl) (struct file *, unsigned int, unsigned long); /* 代替ioctl*/
12     int (*mmap) (struct file *, struct vm_area_struct *);/*用于请求将设备内存映射到进程地址空间*/
13     int (*open) (struct inode *, struct file *);/*打开*/
14     int (*flush) (struct file *, fl_owner_t id); /*在进程关闭它的设备文件描述符的拷贝时调用; 它应当执行(并且等待)设备的任何未完成的操作. */
15     int (*release) (struct inode *, struct file *);/*关闭*/
16     int (*fsync) (struct file *, int datasync); /*刷新待处理数据*/
17     int (*aio_fsync) (struct kiocb *, int datasync); /*异步fsync*/
18     int (*fasync) (int, struct file *, int); /*通知设备FASYNC标志发生变化*/
19     int (*lock) (struct file *, int, struct file_lock *);/* 实现文件加锁*/
20     ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); /*通常为NULL*/
21     unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); /*在当前的进程地址空间找的一个未映射的内存段*/
22     int (*check_flags)(int); /*法允许模块检查传递给 fnctl(F_SETFL...) 调用的标志*/
23     int (*flock) (struct file *, int, struct file_lock *);/**/
24     ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); /*由VFS调用,将管道数据粘贴到文件*/
25     ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); /*由VFS调用,将文件数据粘贴到管道*/
26     int (*setlease)(struct file *, long, struct file_lock **);/**/
27     long (*fallocate)(struct file *file, int mode, loff_t offset,
28               loff_t len); /**/
29 }; 
View Code

上面结构体中的函数指针所指向的函数,在我们在进行openwriteread等系统调用的时候最终会被调用到,所以我们的驱动中想为应用层实现那种调用就要在此实现。

4.       字符设备驱动初始化

我们通过上面的分析对设备号和操作函数集有了一定的了解下面我们来看字符设备驱动初始化,其主要步骤如下。

1.        分配cdev结构:有静态(直接定义)动态(cdev_alloc();)两种方式

2.        初始化cdev结构:使用 cdev_init(struct cdev *cdev, const struct file_operations *fops) 初始化

3.        驱动注册:使用 int cdev_add(struct cdev *p, dev_t dev, unsigned count)//count为该种类型的设备个数注册 

4.        硬件初始化:阅读芯片手册进行硬件设备的初始化

5.        完成操作函数集:实现要用的操作(设备方法)

6.        驱动注销:使用 void cdev_del(struct cdev *p) 注销

5.       字符设备驱动模型及调用关系

下面我通过一张图将字符设备的驱动结构、以及字符设备驱动与用户空间的调用关系进行展示:

6.       遗漏知识

我们内核空间和用户空间的数据交互要用到下面两个函数:

1 copy_from_user();//从用户空间读
2 copy_to_user();//写入用户空间 

l  简单字符设备驱动实现

经过上面的分析我们对字符设备有一定了解,下面我们来完成一个最简单的字符设备驱动。我只展示最主要的代码,整个项目工程https://github.com/wrjvszq/myblongs.git欢迎大家关注。

1.       字符设备驱动编写

因为驱动本身就是一个内核模块,下面的字符设备驱动只实现了部分方法,在后面的博客中我们会基于此驱动慢慢修改,希望大家掌握。

 

  1 #include<linux/module.h>
  2 #include<linux/init.h>
  3 #include<linux/cdev.h>
  4 #include<linux/fs.h>
  5 #include<asm/uaccess.h>
  6 
  7 #define MEM_SIZE 1024
  8 
  9 MODULE_LICENSE("GPL");
 10 
 11 struct mem_dev{
 12     struct cdev cdev;
 13     int mem[MEM_SIZE];//全局内存4k
 14     dev_t devno;
 15 };
 16 
 17 struct mem_dev my_dev;
 18 
 19 /*打开设备*/
 20 int mem_open(struct inode *inode, struct file *filp){
 21     int num = MINOR(inode->i_rdev);/*获取次设备号*/
 22 
 23     if(num == 0){/*判断为那个设备*/
 24         filp -> private_data = my_dev.mem;/*将设备结构体指针复制给文件私有数据指针*/
 25     }
 26     return 0;
 27 }
 28 /*文件关闭函数*/
 29 int mem_release(struct inode *inode, struct file *filp){
 30       return 0;
 31 }
 32 
 33 static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos){
 34     int * pbase = filp -> private_data;/*获取数据地址*/
 35     unsigned long p = *ppos;/*读的偏移*/
 36     unsigned int count = size;/*读数据的大小*/
 37     int ret = 0;
 38 
 39     if(p >= MEM_SIZE)/*合法性判断*/
 40         return 0;
 41     if(count > MEM_SIZE - p)/*读取大小修正*/
 42         count = MEM_SIZE - p;
 43 
 44     if(copy_to_user(buf,pbase + p,size)){
 45        ret = - EFAULT;
 46     }else{
 47         *ppos += count;
 48         ret = count;
 49     }
 50 
 51     return ret;
 52 }
 53 
 54 static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos){
 55     unsigned long p = *ppos;
 56     unsigned int count = size;
 57     int ret = 0;
 58     int *pbase = filp -> private_data;
 59 
 60     if(p >= MEM_SIZE)
 61         return 0;
 62     if(count > MEM_SIZE - p)
 63         count = MEM_SIZE - p;
 64 
 65     if(copy_from_user(pbase + p,buf,count)){
 66        ret = - EFAULT;
 67     }else{
 68         *ppos += count;
 69         ret = count;
 70     }
 71     return ret;
 72 }
 73 
 74 /*seek文件定位函数*/
 75 static loff_t mem_llseek(struct file *filp, loff_t offset, int whence){ 
 76 
 77     loff_t newpos;
 78 
 79     switch(whence) {
 80         case SEEK_SET:/*从文件头开始定位*/
 81             newpos = offset;
 82             break;
 83         case SEEK_CUR:/*从当前位置开始定位*/ 
 84             newpos = filp->f_pos + offset;
 85             break;
 86         case SEEK_END: 
 87             newpos = MEM_SIZE * sizeof(int)-1 + offset;/*从文件尾开始定位*/
 88             break;
 89         default:
 90             return -EINVAL;
 91     }
 92 
 93      if ((newpos<0) || (newpos>MEM_SIZE * sizeof(int)))/*检查文件指针移动后位置是否正确*/
 94          return -EINVAL;
 95      
 96      filp->f_pos = newpos;
 97      return newpos;
 98 
 99 }
100 
101 const struct file_operations mem_ops = {
102     .llseek = mem_llseek,
103     .open = mem_open,
104     .read = mem_read,
105     .write = mem_write,
106     .release = mem_release,
107 };
108 
109 static int memdev_init(void){
110     int ret = -1;
111 
112     /*动态分配设备号*/
113     ret = alloc_chrdev_region(&my_dev.devno,0,1,"memdev");
114     if (ret >= 0){
115         cdev_init(&my_dev.cdev,&mem_ops);/*初始化字符设备*/
116         cdev_add(&my_dev.cdev,my_dev.devno,1);/*添加字符设备*/
117     }
118 
119     return ret;   
120 }
121 
122 static void memdev_exit(void){
123     cdev_del(&my_dev.cdev);
124     unregister_chrdev_region(my_dev.devno,1);
125 
126 }
127 
128 module_init(memdev_init);
129 module_exit(memdev_exit); 
View Code

l  驱动测试

经过上面的代码我们已经实现了一个简单的字符设备驱动,我们下面进行测试。(应用程序在https://github.com/wrjvszq/myblongs.git 上)

1.       加载内核模块

我们使用 insmod memdev.ko 命令加载内核模块

2.       获取设备号

我们的设备号是动态申请到的,所以我们要通过下面的命令查看设备号

 cat /proc/devices 

找到我们的设备memdev的设备号

3.       建立设备文件

使用如下命令建立设备文件

 mknod /dev/文件名 c 主设备号次设备号 

上面命令中文件名为我们在应用程序中打开的文件名

c代表字符设备

主设备号为上一步找到的,我的位249

次设备号非负即可,但不能超过自己所创建的设备数。

比如我的就是 mknod /dev/memdev0 c 249 0 

4.       编译应用程序并测试

使用gcc对应用程序进行编译,然后先使用write对设备进行写入,在使用read对设备读取,完成测试。

 

目录
打赏
0
0
0
0
217
分享
相关文章
理解Linux操作系统内核中物理设备驱动(phy driver)的功能。
综合来看,物理设备驱动在Linux系统中的作用是至关重要的,它通过与硬件设备的紧密配合,为上层应用提供稳定可靠的通信基础设施。开发一款优秀的物理设备驱动需要开发者具备深厚的硬件知识、熟练的编程技能以及对Linux内核架构的深入理解,以确保驱动程序能在不同的硬件平台和网络条件下都能提供最优的性能。
40 0
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
195 67
Linux2.6内核进程调度队列
本篇文章是Linux进程系列中的最后一篇文章,本来是想放在上一篇文章的结尾的,但是想了想还是单独写一篇文章吧,虽然说这部分内容是比较难的,所有一般来说是简单的提及带过的,但是为了让大家对进程有更深的理解与认识,还是看了一些别人的文章,然后学习了学习,然后对此做了总结,尽可能详细的介绍明白。最后推荐一篇文章Linux的进程优先级 NI 和 PR - 简书。
43 0
|
3月前
|
Linux内核中的current机制解析
总的来说,current机制是Linux内核中进程管理的基础,它通过获取当前进程的task_struct结构的地址,可以方便地获取和修改进程的信息。这个机制在内核中的使用非常广泛,对于理解Linux内核的工作原理有着重要的意义。
150 11
linux命令—tree
tree是一款强大的Linux命令行工具,用于以树状结构递归展示目录和文件,直观呈现层级关系。支持多种功能,如过滤、排序、权限显示及格式化输出等。安装方法因系统而异常用场景包括:基础用法(显示当前或指定目录结构)、核心参数应用(如层级控制-L、隐藏文件显示-a、完整路径输出-f)以及进阶操作(如磁盘空间分析--du、结合grep过滤内容、生成JSON格式列表-J等)。此外,还可生成网站目录结构图并导出为HTML文件。注意事项:使用Tab键补全路径避免错误;超大目录建议限制遍历层数;脚本中推荐禁用统计信息以优化性能。更多详情可查阅手册mantree。
linux命令—tree
linux命令—cd
`cd` 命令是 Linux/Unix 系统中用于切换工作目录的基础命令。支持相对路径与绝对路径,常用选项如 `-L` 和 `-P` 分别处理符号链接的逻辑与物理路径。实际操作中,可通过 `cd ..` 返回上级目录、`cd ~` 回到家目录,或利用 `cd -` 在最近两个目录间快速切换。结合 Tab 补全和 `pwd` 查看当前路径,能显著提升效率。此外,需注意特殊字符路径的正确引用及脚本中绝对路径的优先使用。
|
28天前
|
Linux命令拓展:为cp和mv添加进度显示
好了,就这样,让你的Linux复制体验充满乐趣吧!记住,每一个冷冰冰的命令背后,都有方法让它变得热情起来。
99 8
Linux环境下必备的基础命令概览
以上就是Linux系统中的基本命令和工具,掌握它们就能帮你在Linux世界里游刃有余。这其实就像是学习驾驭一辆新车,熟悉了仪表盘,调整好了座椅,之后的旅程就只需要享受风驰电掣的乐趣了。
48 4
详解Ubuntu的strings与grep命令:Linux开发的实用工具。
这就是Ubuntu中的strings和grep命令,透明且强大。我希望你喜欢这个神奇的世界,并能在你的Linux开发旅程上,通过它们找到你的方向。记住,你的电脑是你的舞台,在上面你可以做任何你想做的事,只要你敢于尝试。
165 32
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问