一.Linux设备分类
字符设备: 以字节为单位读写的设备。
块设备 : 以块为单位(效率最高)读写的设备。
网络设备 : 用于网络通讯设备。
字符设备:
字符(char)设备是个能够像字节流(类似文件)一样被访问的设备,由字符设备驱动程序
来实现这种特性。字符设备驱动程序通常至少要实现open、close、read和write的系统调
用。字符终端(/dev/console)和串口(/dev/ttyS0以及类似设备)就是两个字符设备,
它们能很好的说明“流”这种抽象概念。字符设备可以通过FS节点来访问,比如/dev/tty1
和/dev/lp0等。这些设备文件和普通文件之间的唯一差别在于对普通文件的访问可以前后
移动访问位置,而大多数字符设备是一个只能顺序访问的数据通道。然而,也存在具有数据
区特性的字符设备,访问它们时可前后移动访问位置。例如framebuffer就是这样的一个设
备,app可以用mmap或lseek访问抓取的整个图像。
块设备:
和字符设备类似,块设备也是通过/dev目录下的文件系统节点来访问。块设备(例如磁盘)
上能够容纳filesystem。在大多数的Unix系统中,进行I/O操作时块设备每次只能传输一个
或多个完整的块,而每块包含512字节(或2的更高次幂字节的数据)。Linux可以让app像
字符设备一样地读写块设备,允许一次传递任意多字节的数据。因此,块设备和字符设备的
区别仅仅在于内核内部管理数据的方式,也就是内核及驱动程序之间的软件接口,而这些不
同对用户来讲是透明的。在内核中,和字符驱动程序相比,块驱动程序具有完全不同的接口。
网络设备:
任何网络事物都需要经过一个网络接口形成,网络接口是一个能够和其他主机交换数据的设
备。接口通常是一个硬件设备,但也可能是个纯软件设备,比如回环(loopback)接口。
网络接口由内核中的网络子系统驱动,负责发送和接收数据包。许多网络连接(尤其是使用
TCP协议的连接)是面向流的,但网络设备却围绕数据包的传送和接收而设计。网络驱动程
序不需要知道各个连接的相关信息,它只要处理数据包即可。
由于不是面向流的设备,因此将网络接口映射到filesystem中的节点(比如/dev/tty1)比
较困难。Unix访问网络接口的方法仍然是给它们分配一个唯一的名字(比如eth0),但这个
名字在filesystem中不存在对应的节点。内核和网络设备驱动程序间的通信,完全不同于内
核和字符以及块驱动程序之间的通信,内核调用一套和数据包相关的函数而不是read、
write等。
二. 上层应用程序是如何访问到底层驱动程序的呢?
在linux的世界里一切皆文件,所有的硬件设备操作到应用层都会被抽象成文件的操作。我们知道如果应用层要访问硬件设备,它必定要调用到硬件对应的驱动程序。Linux内核有那么多驱动程序,应用怎么才能精确的调用到底层的驱动程序呢?
在这里我们拿字符设备为例,来看一下应用程序如何和底层驱动程序关联起来。
必须知道的知识:
(1) 在Linux文件系统中,每个文件都用一个struct inode结构体来描述,这个结构体记录了这个文件的所有信息,例如文件类型,访问权限等。
(2) 在linux操作系统中,每个驱动程序在应用层的/dev目录或者其他如/sys目录下都会有一个文件与之对应。
(3) 在linux操作系统中, 每个驱动程序都有一个设备号。
(4) 在linux操作系统中,每打开一次文件,Linux操作系统会在VFS层分配一个struct file结构体来描述打开的文件。
注意:常常我们认为,struct inode描述的是文件的静态信息,即这些信息很少会改变,而struct file描述的是动态信息,即对文件的操作的时候,struct file里面的信息经常会发生变化。典型的是struct file结构体里面的f_ops(记录当前文件的位移量),每次读写一个普通文件时f_ops的值都会发生改变。
通过上图我们可以知道,如果想访问底层设备,就必须打开对应的设备文件。也就是在这个打开的过程中,Linux内核将应用层和对应的驱动程序关联起来。
(1) 当open函数打开设备文件时,可以根据设备文件对应的struct inode结构体描述的信息,可以知道接下来要操作的设备类型(字符设备还是块设备),还会分配一个struct file结构体。
(2) 根据struct inode结构体里面记录的设备号,可以找到对应的驱动程序。这里以字符设备为例。在Linux操作系统中每个字符设备都有一个struct cdev结构体。此结构体描述了字符设备所有信息,其中最重要的一项就是字符设备的操作函数接口。
(3) 找到struct cdev结构体后,linux内核就会将struct cdev结构体所在的内存空间首地址记录在struct inode结构体i_cdev成员中,将struct cdev结构体中的记录的函数操作接口地址记录在struct file结构体的f_ops成员中。
(4) 任务完成,VFS层会给应用返回一个文件描述符(fd)。这个fd是和struct file结构体对应的。接下来上层应用程序就可以通过fd找到struct file,然后在有struct file找到操作字符设备的函数接口了。
三 . 如何编写字符设备驱动。
四. 字符驱动相关函数分析。
五. 开始写字符设备驱动程序
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#define MAJOR_NUM 168
struct mycdev
{
unsigned char buffer[50];
struct cdev cdev;
}dev_fifo;
MODULE_LICENSE("GPL");
static int dev_fifo_open(struct inode *inode,struct file *file){
printk("dev_fifo_open success!")
return 0;
}
static ssize_t dev_fifo_read(struct file *file,char __user *buf,size_t size,loff_t *ppos)
{
printk("dev_fifo_read success");
return 0;
}
static ssize_t dev_fifo_write(struct file *file,const char __user *buf,size_t size,loff_t *ppos)
{
printk("dev_fifo_write success");
return 0;
}
static const struct file_operations fifo_operations = {
.owner = THIS_MODULE,
.open = dev_fifo_open,
.read = dev_fifo_read,
.write = dev_fifo_write,
};
int __init dev_fifo_init(void)
{
int ret;
dev_t dev_num;
//初始化字符设备
cdev_init(&dev_fifo.cdev,&fifo_operations);
//设备号:主设备号(12Bit)|次设备号(20BIT)
dev_num = MKDEV(MAJOR_NUM,0);
//注册设备号
ret = register_chrdev_region(dev_num,1,"dev_fifo");
if(ret < 0)
{
printk("Fail to register_chrdev_region");
return -EIO;
}
//添加设备到操作系统。
ret = cdev_add(&dev_fifo.cdev,dev_num,1);
if(ret < 0)
{
printk("fail to cdev_add");
goto unregister_chrdev;
}
printk("Register dev_fifo to system.ok!\n");
return 0;
unregister_chrdev:
unregister_chrdev_region(MKDEV(MAJOR_NUM,0),1);
return -1;
}
void __exit dev_fifo_exit(void)
{
//从系统中删除添加的字符设备
cdev_del(&dev_fifo.cdev);
//释放申请的设备号
unregister_chrdev_region(MKDEV(MAJOR_NUM,0),1);
printk("Exit dev_fifo ok!");
return;
}
module_init(dev_fifo_init);
module_exit(dev_fifo_exit);
内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料
学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈
原文作者:极致Linux内核
原文地址:linux内核设备驱动框架 - 知乎(版权归原文作者所有,侵权留言联系删除)