ch03.字符设备驱动程序

编写驱动程序的第一步就是定义驱动程序为用户程序提供的能力(机制)。接下来以scull(“Simple Character Utility for Loading Localities”区域装载的简单字符工具,scull是一个操作内存区域的字符设备驱动程序,这片内存区域就相当于一个设备)为例说明。

主设备号和次设备号

对字符设备的访问时通过文件系统内的设备名称进行的。这些名称被称为特殊文件、设备文件或者简单称之为文件系统树的节点。它们通常位于/dev目录,字符设备驱动程序的设备文件可通过ls -l命令输出的第一列中的“c”来识别。

ls -l执行后,可在设备文件项的最后修改日期前看到两个数(用逗号隔开),这两个数就是对应设备的主次设备号。通常而言,主设备号标识设备对应的驱动程序。linux内核允许多个驱动程序共享主设备号。此设备号由内核使用,用于正确确定设备文件所指的设备。

内核中,dev_t类型用来保存设备编号----包括主次设备号。dev_t是一个32位的数,其中的12位用来表示主设备号,其余20位用来表示此设备号。要获得dev_t的主设备号或此设备号,使用:

获取主设备号:MAJOR(dev_t  dev);

获取此设备号:MINOR(dev_t  dev); 

将主次设备号转换成dev_t类型:MKDEV(int major , int minor );

分配和释放设备编号

在建立字符设备之前,我们的驱动程序首先要做的就是获得一个或者多个设备编号。

int register_chrdev_region(dev_t first,unsigned int count,char *name);                               (静态分配设备号   <linux/fs.h>)

参数1:要分配的设备编号范围的起始值 (first的次设备号通常被设置为0)                参数2:所请求得连续设备号的个数                参数3:name是和该编号相关的设备名称(出现在/proc/devices和sysfs中)

返回值:成功   0

失败    负的错误码

如果提前知道所需要的设备编号,则register_chrdev_region会工作得很好。但是我们经常不知道设备将要使用哪些主设备号;因此,linux内核开发社区一直在努力转向设备编号的动态分配。

int alloc_chrdev_region(dev_t *dev ,unsigned int firstminor ,unsigned int count ,char *name);          (动态分配所需要的主设备号)

参数1:&dev           参数2:要分配的第一个次设备号,通常为0         参数3和4与register_chrdev_region函数一样。

在不使用时释放这些设备编号:

void unregister_chrdev_region(dev_t first ,unsigned int count);

通常在模块清除函数中使用unregister_chrdev_region函数。

在用户空间程序可访问上述设备编号之前,驱动程序需要将设备编号和内部函数链接起来,这些内部函数用来实现设备的操作。

重要的数据结构

设备号的注册仅仅是驱动程序代码必须完成的许多工作中的第一件事情。大部分基本的驱动程序操作涉及到三个重要的内核数据结构,分别是file_operations、file和inode。

文件操作file_operations结构体

file_operations结构就是用来建立驱动程序操作和设备编号的连接的。(<linux/fs.h>)其中包含了一组函数指针。习惯上,file_operations结构体或者指向这类结构体的指针称为fops,这个结构体中的每一个字段都必须指向驱动程序中实现特定操作的函数,对于不支持的操作,对应的字段可置为NULL值。

  struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); //__user表示指针是一个用户空间地址,因此不能
//被直接引用
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
int (*show_fdinfo)(struct seq_file *m, struct file *f);
};

struct file_operations Code

struct module *owner;

第一个file_operations字段并不是一个操作,相反,它是指向“拥有”该结构的模块的指针。内核使用这个字段以避免在模块的操作正在被使用时卸载该模块。几乎所有的情况下,该成员都会被初始化为THIS_MODULE。

ssize_t (*read)(struct file* ,char __user* ,size_t ,loff_t *);

用来从设备中读取数据。

ssize_t (*write)(struct file* ,const char __user* ,size_t ,loff_t *);

用来向设备发送数据。

unsigned int (*poll)(struct file*,struct poll_table_struct *);

poll方法是poll、epoll和select这三个系统调用的后端实现。这三个系统调用可用来查询某个或者多个文件描述符的读取或写入是否会被阻塞。poll方法应该返回一个位掩码,用来指出非阻塞的读取或写入是否可能,并且也会向内核提供将调用进程置于休眠状态直到i/o变为可能时的信息。如果驱动程序将poll方法定义为NULL,则设备会被认为既可读也可写,并且不会被阻塞。

long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

unlocked_ioctl提供了一种执行设备特定命令的方法。

int (*open)(struct inde*,struct file*);

尽管这始终是对设备文件执行的第一个操作,然而却并不要求驱动程序一定要声明一个相应的方法。如果这个入口为NULL,设备的打开操作永远成功,但系统不会通知驱动程序。

int (*release)(struct inode*,struct file*);

当file结构体被释放时,将调用这个操作。

 struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
.unlocked_ioctl = scull_unlocked_ioctl,
.open = scull_open,
.release = scull_release,
};

常用的file_opreations初始化 Code

file结构

设备驱动程序使用的第二个最重要的内核数据结构是struct file。file结构代表一个打开的文件(它并不仅仅限定于设备驱动程序,系统中每个打开的文件在内核空间都有一个对应的file结构)。它由内核在open时创建,并传递给在该文件上进行操作的所有函数,直到最后的close函数,内核会释放这个数据结构。内核源码中,指向struct file的指针通常被称为filep。

mode_t f_mode;

  文件模式

loff_t  f_pos;

当前的读/写位置。loff_t是一个64位的数(用gcc的术语就是long long)。如果驱动程序需要直到文件中的当前位置,可以读取这个值,但不要去修改它。read/write会使用它们接收到的最后那个指针参数来更新这一位置,而不是直接对filp->f_pos进行操作。

struct file_opreations *f_op;

与文件相关的操作。

void *private_data;

  私有数据。

inode结构

内核用inode结构在内部表示文件,因此它和file结构不同,后者表示打开的文件描述符。对单个文件,可能会有许多个表示打开的文件描述符的file结构,但他们都指向单个inode结构(包含了大量的文件信息)。

为了鼓励编写可移植性更强的代码,内核开发者增加了两个新的宏,可用来从一个inode中获得主设备号和次设备号:

unsigned int iminor(struct inode *inode);

unsigned int imajor(struct inode *inode);

字符设备的注册

内核内部使用struct cdev结构来表示字符设备。在内核调用设备操作之前,必须分配并注册一个或者多个上述结构(<linux/cdev.h>)。

void cdev_init(struct cdev *cdev,struct file_operations *fops);

初始化已分配到的结构。(struct cdev也有一个所有者字段,应该被设置为THIS_MODULE)

在cdev结构体设置好之后,最后的步骤是通过cdev_add告诉内核该结构的信息(注册):

int cdev_add(struct cdev *cdev,dev_t num,unsigned int count);

参数1:cdev结构     参数2:该设备对应的第一个设备编号      参数3:和该设备关联的设备编号的个数 (通常取1)

注:在驱动程序还没有完全准备好处理设备上的操作时(初始化),就不能调用cdev_add。

cdev_del(struct cdev * dev);

从系统中移除(注销)一个字符设备。

scull中的设备注册

scull内部通过struct scull_dev的结构体来表示每个设备。

cdev结构体镶嵌在该结构体中,现在,我们的注意力在cdev上,即内核和设备间的接口struct cdev。该结构必须如上所述的被初始化并添加到系统中。

open和release

open方法

int (*open)(struct inode *inode,struct file *filp);

其中的inode参数在其i_cdev字段中包含可我们所需要的信息,及我们先前设置的cdev结构。现在的问题是,我们通常不需要cdev结构本身,而是希望得到包含cdev结构的scull_dev结构。内核中实现了一种技巧,通过定义在<linux/kernel.h>头文件中的

container_of宏函数实现:

container_of(pointer,container_type,container_field);          ----已知结构体成员的指针反推结构体的指针

这个宏函数需要一个container_field字段的指针,该字段包含在container_type类型的结构中,然后返回包含该字段的结构指针。

 struct scull_dev *dev;            /*device  information*/

 dev = container_of(inode->i_cdev,struct scull_dev,cdev);
filp->private_data = dev; /*for other methods*/

container_of Code

一旦代码找到scull_dev结构之后,scull将一个指针保存到了file结构的private_data字段中,这样可以方便今后对该指针的访问。

==》经过些微简化的scull_open代码:

 int scull_open(struct inode *inode,struct file *filp)
{
struct scull_dev *dev; /*device information*/
dev = container_of(inode->i_cdev,struct scull_dev,cdev);
filp->private_data = dev; /*for other methods*/
}

scull_open Code

release方法

release方法的作用正好和open相反。完成下面的任务:1.释放由open分配的、保存在filp->private_data中的所有内容。2.在最后一次关闭操作时关闭设备。

read和write

read:从内核拷贝数据到应用程序空间         对应调用copy_to_user

write:从应用程序空间拷贝数据到内核         对应调用copy_from_user

ssize_t read(struct file *filp,char __user *buff,size_t count,loff_t *offp);

ssize_t write(struct file *filp,const char __user *buff,size_t count,loff_t *offp);

参数1:filp文件流指针    参数2:指向用户空间的缓冲区        参数3:请求传输的数据长度        参数4:指针,指明用户在文件中进行存取操作的位置。

注:read和write方法的buff参数是用户空间的指针,因此,内核代码不能直接引用其中的内容。

原因:1.在内核模式中运行时,用户空间的指针可能是无效的,该地址可能根本无法被映射到内核空间,或者可能指向某些随机数据。2.用户空间的内存是分页的,对用户空间内存的直接引用将导致页错误,结果可能是‘oops’。3.如果我们的驱动程序盲目引用用户提供的指针,讲导致系统出现打开的后门,从而允许用户空间程序随意访问或覆盖系统中的内存。危及用户系统安全。

然而,驱动程序必须访问用户空间的缓冲区以便完成自己的工作。为了确保安全,这种访问应该始终通过内核提供的专用函数完成。

unsigned long copy_to_user(void __user *to,const void *from,unsigned long count);

unsigned long copy_from_user(void *to,const void __user *from,unsigned long count);

这两个函数的作用并不限于在内核空间和用户空间之间拷贝数据,它们还检查用户空间的指针是否有效。如果指针无效,就不会进行拷贝;如果在拷贝过程中遇到无效地址,则仅仅会复制部分数据。

返回值的问题:read和write方法在出错时都返回一个负值,大于等于0的返回值告诉调用程序成功传输了多少字节。如果在正确传输部分数据之后发生了错误,则返回值必须是成功传输的字节数,但这个错误只能在下一次函数调用时才会得到报告。

linux device drivers ch03的更多相关文章

  1. 《Linux Device Drivers》第十四章 Linux 设备型号

    基本介绍 2.6内核设备模型来提供的抽象叙述性描述的一般系统的结构,为了支持各种不同的任务 电源管理和系统关机 用户空间与通信 热插拔设备 设备类型 kobject.kset和子系统 kobject是 ...

  2. 《Linux Device Drivers》第十二章 PCI司机——note

    一个简短的引论 它给这一章总线架构的高级概述 集中访问讨论Peripheral Component Interconnect(PCI,外围组件互连)外设内核函数 PCI公交车是最好的支持的内核总线 本 ...

  3. 《Linux Device Drivers》 第十七章 网络驱动程序——note

    基本介绍 第三类是标准的网络接口Linux设备,本章介绍的内核,其余的交互网络接口描述 网络接口,必须使用特定的内核数据结构本身注册,与外部分组交换数据线打电话时准备 经常使用的文件上的网络接口操作是 ...

  4. 《Linux Device Drivers》第十五章 内存映射和DMA——note

    简单介绍 很多类型的驱动程序编程都须要了解一些虚拟内存子系统怎样工作的知识 当遇到更为复杂.性能要求更为苛刻的子系统时,本章所讨论的内容迟早都要用到 本章的内容分成三个部分 讲述mmap系统调用的实现 ...

  5. 《Linux Device Drivers》第十六章 块设备驱动程序——note

    基本介绍 块设备驱动程序通过主传动固定大小数据的随机访问设备 Linux核心Visual块设备作为基本设备和不同的字符设备类型 Linux块设备驱动程序接口,使块设备最大限度地发挥其效用.一个问题 一 ...

  6. 《Linux Device Drivers》第十八章 TTY驱动程序——note

    简单介绍 tty设备的名称是从过去的电传打字机缩写而来,最初是指连接到Unix系统上的物理或虚拟终端 Linux tty驱动程序的核心紧挨在标准字符设备驱动层之下,并提供了一系列的功能,作为接口被终端 ...

  7. 《Linux Device Drivers》第十章 中断处理——note

    概述:系统要及时的感知硬件的状态,通常有两种方式:一种是轮询.一种是通过响应硬件中断.前者会浪费处理器的时间,而后者不会. 准备并口 在没有节设定产生中断之前,并口是不会产生中断的 并口的标准规定设置 ...

  8. linux device drivers ch02

    ch02.构造和运行模块 模块的构造: #include <linux/init.h> #include <linux/module.h> MODULE_LICENSE(&qu ...

  9. linux device drivers ch01

    ch01. 设备驱动程序简介 设备驱动程序的作用在于提供机制(需要提供什么功能),而不是提供策略(如何使用这些功能). 内核功能划分: 进程管理:进程创建.销毁.进程间通信.共享cpu调度器. 内存管 ...

随机推荐

  1. Web前端-Ajax基础技术(上)

    Web前端-Ajax基础技术(上) ajax是浏览器提供一套的api,用于向服务器发出请求,接受服务端返回的响应,通过javascript调用,实现通过代码控制请求与响应,实现网络编程. ajax发送 ...

  2. arcgis api for js入门开发系列二十打印地图的那些事

    前面我写过关于利用arcgis api for js打印地图的,但是打印地图服务都是基于arcgis server发布的,arcgis api加载在线地图,比如天地图.百度地图.高德地图等,底图都是打 ...

  3. CMMI 2.0术语变化

    过程域 vs. 实践域 “过程域”(Process Areas,PAs)在CMMI 2.0中变成了“实践域(Practice Areas,PAs)”.这样的改变,强调了CMMI 2.0是最佳实践的集合 ...

  4. SQLServer之删除用户定义的数据库角色

    删除用户定义的数据库角色注意事项 无法从数据库删除拥有安全对象的角色. 若要删除拥有安全对象的数据库角色,必须首先转移这些安全对象的所有权,或从数据库删除它们. 无法从数据库删除拥有成员的角色. 若要 ...

  5. DVWA-XSS学习笔记

    DVWA-XSS XSS概念:由于web应用程序对用户的输入过滤不严,通过html注入篡改网页,插入恶意脚本,从而在用户浏览网页时,控制用户浏览器的一种攻击. XSS类型: 反射型XSS:只是简单地把 ...

  6. vue使用npm run build命令打包

    vue使用npm run build命令打包项目   当我们使用vue-cli脚手架完成一个项目的时候,下一步肯定会想要怎么把这个项目放到互联网上或者本地直接打开呢,我们在本地调试的时候只要命令行执行 ...

  7. FineUIMvc的分部视图连接器 --VS-- FineUIPro的用户控件连接器

    [FineUIPro]用户控件 在FineUIPro中,我们可以将公用的部分定义为一个用户控件,然后添加到页面中. 一个简单的示例:http://pro.fineui.com/#/usercontro ...

  8. 随心测试_职场面试_001<SX的面试观点>

    快速理解_求职面试:必不可少的嘴 +  双向沟通交流 = 人与人之间的心理游戏 ps:以下为_面试题回答套路_案例,仅供参考,不挖坑 常见的面试题: 你是如何看待软件测试这个行业的? 说说你对软件测试 ...

  9. 类SimpleDateFormat

    概述 java.text.DateFormat 是日期/时间格式化子类的抽象类,不能直接使用.我们通过这个类的子类可以帮我们完成日期和文本之间的转换,也就是可以在Date对象与String对象之间进行 ...

  10. Centos7安装配置Nginx

    Nginx 安装 系统平台:CentOS 7.4 64位. 一,安装编译工具及文件 yum -y install make zlib zlib-devel gcc-c++ libtool openss ...