目录
随着自己工作的进行,接触到的技术栈也越来越多。给我一个很直观的感受就是,某一项技术/经验在刚开始接触的时候都记得很清楚。往往过了几个月都会忘记的差不多了,只有经常会用到的东西才有可能真正记下来。存在很多在特殊情况下有一点用处的技巧,用的不多的技巧可能一个星期就忘了。
想了很久想通过一些手段把这些事情记录下来。也尝试过在书上记笔记,这也只是一时的,书不在手边的时候那些笔记就和没记一样,不是很方便。
很多时候我们遇到了问题,一般情况下都是选择在搜索引擎检索相关内容,这样来的也更快一点,除非真的找不到才会去选择翻书。后来就想到了写博客,博客作为自己的一个笔记平台倒是挺合适的。随时可以查阅,不用随身携带。
同时由于写博客是对外的,既然是对外的就不能随便写,任何人都可以看到。经验对于我来说那就只是经验而已,公布出来说不一定我的一些经验可以帮助到其他的人。遇到和我相同问题时可以少走一些弯路。
既然决定了要写博客,那就只能认真去写。不管写的好不好,尽力就行。千里之行始于足下,一步一个脚印,慢慢来
,写的多了慢慢也会变好的。权当是记录自己的成长的一个过程,等到以后再往回看时,就会发现自己以前原来这么菜😂。
本系列博客所述资料均来自互联网资料
,并不是本人原创(只有博客是自己写的)。出于热心,本人将自己的所学笔记整理并推出相对应的使用教程,方面其他人学习。为国内的物联网事业发展尽自己的一份绵薄之力,没有为自己谋取私利的想法
。若出现侵权现象,请告知本人,本人会立即停止更新,并删除相应的文章和代码。
字符设备架构
字符设备的驱动架构,我们可将其分为四个部分,分别是用户层、VFS层、驱动层、物理层。
用户层
在Linux的设计理念中,一切皆是文件,所有的硬件设备操作到应用层都会被抽象成文件的操作。
字符设备也不例外,在Linux操作系统中,每个驱动程序在应用层的/dev目录下都会有一个设备文件和它对应。
shell
root@ubuntu:/home/peng/Desktop/driver/Learning/5_cdev# ll /dev/ttyS*
crw-rw---- 1 root dialout 4, 64 Jul 25 06:30 /dev/ttyUSB0
crw-rw---- 1 root dialout 4, 65 Jul 25 06:30 /dev/ttyUSB1
以ttyUSB设备举例,我们可以通过标准文件操作的的方式,也就是open、read、write等方式直接访问该设备。
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void main(void)
{
int fd;
fd = open("/dev/ttyUSB0",O_RDWR);
if(fd<0)
{
perror("open fail \n");
return;
}
printf("open ok \n ");
}
除文件接口之外,还可以通过shell的方式,也就是echo、cat等命令对其进行读写操作。这里不做演示。
VFS层
在Linux文件系统中,每个文件都用一个struct inode结构体来描述,这个结构体里面记录了这个文件的所有信息。这里操作的是字符设备驱动,需要注意的信息有设备号、设备类型、设备的结构体。
c
struct inode {
// 记录设备的类型
umode_t i_mode;
xxxxxxxxxxxxx
// 记录文件所对应的设备号
dev_t i_rdev;
xxxxxxxxxxxxx
union {
xxxxxxxxxxxxx
// 记录描述字符设备的结构体
struct cdev *i_cdev;
};
};
当用户层打开设备文件时,可以根据设备文件对应的struct inode结构体描述的信息,知道接下来要操作的设备类型(字符设备还是块设备)。
在Linux操作系统中,每个驱动程序都要分配一个主设备号。在设备号注册章节中提到了怎么注册设备到/proc/devices
中,其中就可以通过自动或者手动的方式去申请设备号。
根据struct inode结构体里面记录的设备号,就可以找到对应的驱动程序。
前面的操作主要是在打开文件时能找到驱动设备,在文件被打开后。VFS层会给应用层返回一个文件描述符(fd)。
这个fd 是和struct file 结构体对应的。上层的应用程序就可以通过fd 来找到strut file ,然后在由struct file 找到操作字符设备的函数接口(file_operations )了。struct file是在打开文件找到驱动设备后,才被赋值的,驱动层部分会讲到。
c
struct file {
xxxxxxxxxxxxx
//记录字符设备的操作函数
const struct file_operations *f_op;
xxxxxxxxxxxxx
//文件指针偏移值
loff_t f_pos;
xxxxxxxxxxxxx
};
驱动层
以字符设备为例,在Linux操作系统中每个字符设备都对应一个char_device_struct 结构体,该结构体和主设备号相关联。该结构体中struct cdev成员描述了字符设备所有的信息。
c
/*fs/char_dev.c*/
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];
/* fs.h */
#define CHRDEV_MAJOR_HASH_SIZE 255
struct cdev 结构体所记录的就是设备驱动相关的数据。其中file_operations就是用户层中用户调用接口的具体实现。其他的数据设备号(完整的设备号),以及次设备的个数了解即可。
c
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops; //接口函数集合
struct list_head list;//内核链表
dev_t dev;//设备号
unsigned int count;//次设备号个数
};
找到struct cdev 结构体后,Linux内核就会将struct cdev 结构体所在的内存空间首地记录在struct inode 结构体的i_cdev 成员中。将struct cdev 结构体的中记录的函数操作接口file_operations 地址记录在struct file 结构体的f_op成员中。
物理层
skip
驱动实现
模块挂载
在注册设备号之后(register_chrdev_region )。我们需要将devno 保存到cdev 结构体中(cdev_add )。在保存之前还需要对cdev 结构体进行初始化(cdev_init)。
c
static dev_t devno;
static struct cdev cdev;
static int hello_init(void)
{
int result;
int error;
printk("hello_init \n");
devno = MKDEV(237,0);
result = register_chrdev_region(devno, 1, "hello");
if(result<0)
{
printk("register_chrdev_region fail \n");
return result;
}
cdev_init(&cdev,&hello_ops);
error = cdev_add(&cdev,devno,1);
if(error < 0)
{
printk("cdev_add fail \n");
unregister_chrdev_region(devno,1);
return error;
}
return 0;
}
module_init(hello_init);
驱动操作函数
cdev 初始化时,传入的就是设备文件的操作函数。这里就简单实现了一个open 和close函数。
在file_operations 结构体的成员中,release 函数指针对应的就是文件的close函数。
当用户层app调用该设备文件的open 函数时,经过内核处理,理应调用到hello_open 函数往dmesg中打印"hello_open()\n"
。同理,app中调用到close 函数时,内核驱动中会调用到hello_release 函数往dmesg中打印"hello_release()\n"
c
static int hello_open(struct inode *inode, struct file *filep)
{
printk("hello_open()\n");
return 0;
}
int hello_release (struct inode *inode, struct file *file)
{
printk("hello_release()\n");
return 0;
}
static struct file_operations hello_ops =
{
.open = hello_open,
.release = hello_release,
};
模块注销
模块注销时,需要按照挂载的相反顺序去执行。先删除cdev ,后对chrdev进行解注册。
c
static void hello_exit(void)
{
printk("hello_exit \n");
cdev_del(&cdev);
unregister_chrdev_region(devno,1);
return;
}
module_exit(hello_exit);
代码验证
文中编写的驱动模块,由于没有配套的自动关联程序,并不会将设备号自动关联到/dev/目录中的具体设备中。
我们只需要手动运行下面的命令,即可创建设备驱动文件绑定到主设备号为237,从设备号为0的字符设备中(命令中c代表字符设备)。
shell
root@ubuntu:# mknod /dev/hello_test0 c 237 0
在用户层编写app(test.c ),仅调用open函数打开我们注册的**/dev/hello_test0**文件即可。其他的函数暂时未实现,直接使用则会报错。
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void main(void)
{
int fd;
fd = open("/dev/hello_test0",O_RDWR);
if(fd<0)
{
perror("open fail \n");
return;
}
printf("open ok \n");
close(fd);
printf("close ok \n");
}
调用gcc编译app,从app的日志中可以看到打开设备文件是正确的。查看dmesg中驱动的日志,也能看到驱动中的hello_open函数被正常调用。
shell
root@ubuntu:/# gcc ./test.c -o test
root@ubuntu:# ./test
open ok
close ok
root@ubuntu:# dmesg
[ 5964.438242] hello_open()
[ 5964.438331] hello_release()
注意:若前面没有执行mknod 函数去创建设备文件。或者mknod 时填入的主设备号,次设备号错误。那么这里的open函数调用也是会报错的。
shell
root@ubuntu:# rm -rf /dev/hello
root@ubuntu:# ./test
open fail
: No such file or directory
root@ubuntu:# mknod /dev/hello c 238 0
root@ubuntu:# ./test
open fail
: No such device or address
那么本篇博客就到此结束了,这里只是记录了一些我个人的学习笔记,其中存在大量我自己的理解。文中所述不一定是完全正确的,可能有的地方我自己也理解错了。如果有些错的地方,欢迎大家批评指正。如有问题直接在对应的博客评论区指出即可,不需要私聊我。我们交流的内容留下来也有助于其他人查看,说不一定也有其他人遇到了同样的问题呢😂。