Linux设备驱动程序 三 字符设备驱动 笔记

第三章 字符驱动设备

本章会编写一个完整的字符设备,字符设备简单,易于理解,

名字是scull:Simple Caracter Utility for Loading Localities,区域装载的简单字符工具,

scull是一个操作内存区域的字符设备driver,这片内存区域就相当于一个设备。

它不和硬件相关,只是操作从内核分配的一些内存

1,scull的设计

第一步,定义driver为用户提供的能力,也就是机制

我们的设备是内存,他可以是顺序或者随机存取设备,可以是一个或者多个设备,

我们实现了若干设备抽象,每个都有自己的特点,

由模块实现的每种设备称为一种 类型 :

scull0 ~ scull3

这四个设备分别由一个全局且持久的内存区域组成,

全局:若device被多次打开,则打开它的所有fd可共享这个device的数据

持久:如果devicce关闭再打开,data不会丢。

可以用常用命令访问和测试这个device,如cp,cat和shell的I/O重定向等

scullpipe0 ~ scullpipe3

这4个FIFO先入先出 设备与管道类似。一个进程读取另一个进程写入的数据。

若多个进程读取同一个device,他们会为数据发送竞争。

scullpipe的内部实现说明在不借助中断的情况下,如何实现阻塞式和非阻塞式 的 读写操作。

虽然实际的driver使用 硬件的中断与他们的drivce保持同步,

但阻塞式和非阻塞式操作是重要的内容,有别于中断处理

scullsingle

scullpriv

sculliud

scullwiud

这些设备与scull0类似,但是何时允许open操作方面有些限制。

scullsingle一次只允许一个进程使用这个driver,

scullpriv对每个虚拟控制台(或X终端会话)是私有的,这是因为每个控制台/终端上的进程将获取不同的内存区。

sculluid和scullwuid可以被多次打开,但是每次只能由一个用户打开;

如果另一个用户锁定了device,sculluid返回Device Busy error,

而scullwiud实现了阻塞式open。

这些scull设备的变种混淆了机制和策略,但是值得了解,很多真正的设备需要类似的管理方式

本章讲scull0~scull3的内部结构,更复杂的在第六章将,scullpipe在 阻塞式I/O 讲,其他在在 设备文件的访问控制 讲。

2,主设备号和次设备号

对字符设备的访问是通过文件系统内的设备名称进行的。被称为特殊文件,设备文件,文件系统树的节点,

它们位于/dev/

ls -l时,字符设备 在第一列用 c 识别。也可以看到主次设备号,

主设备号代表device对应的driver。一般一个主设备号对应一个driver

次设备号由内核使用,用于正确确定设备文件所指的设备,

我们可以通过次设备号获得一个指向内核设备的直接指针,也可以将次设备号当做设备本地数组的索引。

3,设备编号的内部表达

dev_t(lnux/type.h)保存设备编号,包括主设备号和次设备号

dev_t是32bit,12bit代表主设备号,其余20bit代表次设备号,

获取方式:

MAJOR(dev_t dev);

MINOR(dev_t dev);

反向操作:

MKDEV(int major, int minor);

4,分配和释放设备编号

建立char deivce前, 先要获得一个或者多个设备编号。

通过register_chrdev_region,它在linux/fs.h 声明:

int register_chrdev_region(dev_t first, unsigned int count, char *name);

first是要分配的设备编号的范围的起始值,可以给0

count是请求的连续设备编号的个数

name是设备名称,出现在/proc/devices和sysfs中

和多数内核函数一样,成功return 0 ,

如果不知道设备要使用的主设备号,需要动态分配:

int alloc_chrdev_region(dev_t *dev, unsigned int firestminor, uint count, cahr *name);

dev用于输出,调用成功后保存分配的第一个编号。

firstminor可以给0,

释放设备编号:

void unregister_chrdev_region(dev_t first, unisnged int count);

一般在清除函数调用它

有了设备编号,AP可以访问它,driver需要将设备编号与内部函数连接起来,内部函数实现设备的操作。

5,动态分配主设备号

尽量用动态分配,分配了设备号,就可以通过/proc/.devices/和/sys/中读取得到

所以insmod可以替换为一个简单的脚本

脚本调用insmod后读取/proc/devices来获得新分配的主设备号,然后创建对应的设备文件。

可以利用awk从/proc/devices获取信息,在/dev/创建设备文件

130|console:/ # cat /proc/devices
Character devices:1 mem4 ttyS5 /dev/tty5 /dev/console5 /dev/ptmx10 misc
....
180 usb
188 ttyUSB
189 usb_device
216 rfcomm
227 s3gcard
244 roccat
245 hidraw
246 rtk_btusb
247 zxtz
248 bsg
249 watchdog
250 ptp
251 pps
252 media
253 rtc
254 teeBlock devices:1 ramdisk
259 blkext7 loop
....
134 sd
135 sd
179 mmc
254 device-mapper

脚本:

device="scull"/sbin/insmod ./$module.ko $* || exit 1#删除原有节点
rm -f /dev/${device}[0-3] major=$(awk "\$2= =\"$module\" {print \$1} /proc/devices)mknod /dev/${device}0 c $major 0
mknod /dev/${device}1 c $major 1
mknod /dev/${device}2 c $major 2
mknod /dev/${device}3 c $major 3

。。。。

6,一些重要的数据结构

三个重要的内核数据结构:

file_oerations,file,inode

在编写真正的drvier前,要对他们有个基本的认识

7,file_operations结构体

上面我们保留了设备编号,但是没有与driver建立连接。

file_operations就是用来建立连接的,定义在linux/fd.h,包含了一组函数指针。

每个打开的文件(内部用一个file表示)和一组函数关联(通过包含指向file_operations的f_op字段)

这些操作主要用来实现系统调用如open,read等,

可以认为文件是一个对象,操作它的函数是方法,用oop的术语:对象声明的动作将作用于其本身。

这是linux内核看到的面向对象编程的第一个例子,后面更多。

按照惯例,file_operations结构或者指向这类结构的指针称为fops,

这个结构中的每个字段必须指向驱动程序中实现特定操作的函数,不支持就置null

通读file_operations方法的清单时,会看到许多参数包含__user字符传,它表明指针是一个用户空间的地址,不能被直接引用。

struct module *owner

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

poll方法是poll,epoll,select三个系统调用的后端实现,他们可以用来查询某个或者多个fd上读取或写入是否会被阻塞。

poll返回一个位掩码,指出非阻塞的读取或者写入是否可能,并且也会向内核提供将调用进程置于休眠状态直到I/O变为可能时的信息。

如果poll置null,device被认为可读可写不会阻塞,、

int (*mmap)(struct file *, struct vm_area_struct *);

mmap用于请求将设备内存映射到进程地址空间,

scull device driver程序实现的只是最重要的设备方法,file_operations结构init为:

struct file_operations scull_fops = {.owner = THIS_MODULE,....read = scull_read,.ioctl = scull_ioctl,.open = scull_open,.realse = scull_release,
}

这个声明采用标准c的标记化结构初始化语法,

这种语法值得采用,因为它让driver在结构的定义发生变化时更有可移植性,使代码更紧凑和易读。

标记化的init方法允许对结构成员重新排列,

某些场合,把频繁 被访问的成员放在相同的硬件缓存行,可大大提高性能

8,file结构体

struct file是driver的第二个重要的数据结构, 定义在linux/fs.h

file结构与UMD的FILE没有任何关联。FILE是C库定义的,不会出现在内核的代码。

file结构代表一个打开的文件,不仅driver,每个打开的文件在KMD都对应一个file结构,

内核在open时创建它,并传递给在这个文件上进行操作的所有函数,

内核源码中,指向struct file的指针叫filp(文件指针),

struct file的重要成员:

mode_t f_mode,文件模式

struct file_operations *f_op; //与文件相关的操作

内核在open时赋值这个指针,以后需要处理这些操作时读这个指针,

filp->f_op的值不会为方便引用而保存起来,。。。。

这里提到了,这种替换文件操作的能力在OOP里叫 方法重载

void *private_data;

private_data是跨系统调用时保存状态信息的非常有用的资源,

记得要在内核销毁file前在release方法里释放它的内存

9,inode结构体

内核用inode结构体在内部表示文件,它与file不同,file是打开的文件描述符。

有用字段:

dev_t i_rdev;

包含了真正的设备编号

struct cdev *i_cdev;

cdev表示字符设备的内核的内部结构。当inode指向一个字符设备文件时,

这个字段包含了指向struct cdev的指针

10,字符设备的注册

内核内部用struct cdev表示字符设备,

linux/cdev.h

//分配和init cdev有两种方法,
//如果要在运行时获取一个独立的cdev结构:
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;
//可以把cdev结构嵌入自己的设备特定结构中,scull就是这样做的,
//这时,必须用下面的代码去init
void cdev_init(struct cdev *cdev, struct file_operations *fops);//告诉内核改结构的信息
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);//Scull的设备注册
//scull通过struct scull_dev的结构表示每一个设备,结构定义如下:
struct scull_dev {struct scull_qset *data; //指向第一个量子集的指针,int quantum;  //当前量子的大小int qset; //当前数组的大小unisgned long size; //数据总量unsigned int access_key; struct semaphore sem; //互斥信号量struct cdev cdev;  //字符设备结构
}//现在我们集中注意力在cdev,即内核和设备之间的接口struct cdev.
//struct cdev必须如上述被init并添加到系统,
static void scull_setup_cdev(struct scull_dev *dev, int index)
{int err, devno = MKDEV(scull_major, scull_minor+index);cdev_init(&dev->cdev, &scull_fops);dev->cdev.owner = THIS_MODULKE;dev->cdev.ops = &scull_fops;err = cdev_add(&dev->cdev, devno, 1);if(err)printk(KERN_NOTICE"Error %d\n",err);
}
因为cdev被嵌入了struct scull_dev中,所以必须调用cdev_init执行该结构的初始化,

11,早期的方法

int register_chrdev(unsigned int major, const char *name, struct file_opertions *fops);

它会为给定的主设备号注册0~255次设备号,并为每个设备建立一个对应的默认cdev结构。

name是driver的名称,出现在/proc/devices中,

s3g的init方法:

new_kernel/linux/s3.c
static int __init s3g_init(void)
{//#define S3G_DEV_NAME "s3gcard"  @s3g_ioctl.h//#define S3G_PROC_NAME "driver/s3g"ret = register_chrdev(S3G_MAJOR, S3G_DEV_NAME, &s3g_fops);//struct class *s3g_classs3g_class =  class_create(THIS_MODULE, S3G_DEV_NAME);//初始化s3g_card[0]全局变量,通过s3g_card_init(s3g, NULL)//里面也会创建proc节点ret = s3g_register_driver();
}//linux/s3g_drvier.c
int s3g_card_init(s3g_card_t *s3g, void *pdev)
{proc_create_data(S3G_PROC_NAME, 0, NULL, &s3g_proc_fops, s3g);
}static const struct file)operations s3g_proc_fops =
{.owner = THIS_MODULE,.read = s3g_proc_read,.write = s3g_proc_write,
};

12,open和release

看看file_operations的的字段如何使用,

open需要完成的工作:

1,检查设备特定错误,如硬件没有就绪

2,若device首次打开,init它

3,若有必要,更新f_op指针

4,分配并填写置于flip->private_data的数据结构

s3.c有一个file_operations,linux_fb为什么没有?是因为使用了linux内核的接口吗?

s3.h include了所有需要的内核的头文件

//new_kernel/linux/s3.h
#ifdef __KERNEL__
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/time.h>
#include <linux/interrupt.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/shmem_fs.h>
#include <linux/writeback.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/file.h>
#include <linux/leds.h>
#include <linux/platform_device.h>
#include <linux/console.h>
#include <linux/version.h>
#include <linux/sched.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <linux/mm.h>
#include <linux/pagemap.h>
#include <linux/vmalloc.h>
#include <linux/poll.h>
#include <linux/slab.h>
#include <linux/platform_device.h>
#include <linux/anon_inodes.h>
#include <linux/list.h>
#include <linux/input.h>
#include <linux/rwsem.h>
#include <linux/seq_file.h>
#include <linux/debugfs.h>
#include <linux/gpio.h>
#include <linux/regulator/consumer.h>

open的原型:

int (*open)(struct inode *inode, struct file *flip)

inode在i_cdev字段包含了我们需要的信息,就是我们之前设置的cdev结构,/

然而我们通常不需要cdev,而是需要包含cdev的scull_dev结构,

C语言的一些技巧可以完成这类转换,但不该滥用,因为难以理解,幸好内核黑客已经实现了这个技巧,

//linux/kernel.h

contaner_of(pointer, container_type, contaner_field);

它需要一个contaner_field字段的指针,它包含在container_type类型的结构中,

返回包含该字段的结构指针,

在scull_open中,这个宏用来找到适当的设备结构,

struct scull_dev *dev;

dev = container_of(inode->i_cdev, struct scull_dev, cdev);

参数:cdev的指针,包含cdev字段的结构体,cdev字段。

找到scull_dev结构后,保存到file结构的private_data字段,方便以后对该指针的访问。

略微简化的scull_open代码:

int scull_open(struct inode *inode, struct file *filp)
{struct scull_dev *dev;dev = container_of(inode->i_cdev, struct scull_dev, cdev);filp->private_data = dev;//trim to 0.....if open was write-onlyuif( flip->f_flags == O_WRONLY) {scull_trim(dev);}return 0; //success
}

这段代码很小,因为没有针对某个特定device的处理,

scull设备是全局且持久的,所以不用特别处理。

而且不维护scull的打开计数,值维护模块的使用计数,也就没有“首次打开init device”的动作,

对device唯一的操作是,如果以写方式打开,长度就截断为0,。

因为当用更短的文件覆盖一个scull设备时,设备数据区应该缩小,

就像写文件打开时,长度截断为0,

如何打开s3gcard driver并使用它?

打开用open,其余操作全在ioctl

//libkeinterface/s3g_keinterface.c
int s3gOpenMinor(int minor)
{char path[64];sprintf(path, "%s%s%d", S3G_DIR_NAME, S3G_DEV_NAME, minor);if((fd = open(path, O_RDWR, 0)) >= 0){return fd;}return -errno;
}//gralloc/gralloc_s3g.c
__attribute_((constructor)) void gralloc_s3g_init()
{gralloc_s3g_t *s3g = &HAL_MODULE_INFO_SYM;//all gralloc device share one s3g devicefd = s3gOpenMinor(0);status = s3gCreateDdevice(fd, &create);//保存fd和devices3g->fd = fd;s3g->device = create.device;
}//libkeinterface/s3g_keinterface.c
int s3gCreateDevice(int fd, s3g_create_device_t* create_device)
{if(ioctl(fd, S3G_IOCTL_CREATE_DEVICE, create_device)){return -errno;}return 0;
}

13,release方法

任务:

1,释放由open分配的,保存在flip->private_data的所有内容,

2,在最后一次操作时关闭设备

如果关闭device的次数比打开的多怎么办?

因为dup和fork系统调用都是不调用open,就创建以打开文件的副本。但是每个副本在程序终止时都要关闭。

如,多数程序从来不打开他们的stdin设备,但是都在终止时关闭,

driver如何知道何时真正关闭?

回答:

不是每个close系统调用都引起release的调用,真正释放设备数据结构的close才做。

内核对每个file结构维护其被使用次数的计数器。

fork和dup,都不会创建新的数据结构,只有open才会创建新的数据结构,fork和dup只是增加已有结构的计数。

只有file结构的计数归0时,close系统调用才会执行release方法,

这样就保证了一次open对应一次release

在进程退出时,内核在内部用close系统调用自动关闭所有相关文件,

14,scull的内存使用,

引入内核 内存管理的核心函数,在linux/slab.h

void *kmalloc(size_t size, int flags); void kree(void *ptr);

kmalloc分配size字节的内存,返回其指针,flags一般是GFP_KERNEL,

对于分配大内存区,kmalloc不是最好的方法,第八章讲,

分配整个页面更有效

为测试内存短缺,可以让scull吃光内存,

cp /dev/zero /dev/scull0,可以用光RAM

也可以用dd工具选择复制多少数据到scull设备

scull中,每个设备都是一个指针链表,每个指针指向一个scull_qset结构,

用来一个有1000个指针的数组,每个指针指向一个4000字节的区域。

。。。。介绍了scull_qset数据结构相关

14,read和write

他们的任务是copy data到UMD,反过来就是从UMD copy data。所以原型相似:

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

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

filp是文件指针,count是请求传输的数据长度,buffer是UMD的缓冲区,

offp是指向long offset type对象的指针,它指明用户在文件中做存取操作的位置。

buff是UMD的指针,内核不能直接引用其中的内容,原因:

1,UMD的指针在KMD可能是无效的,该地址可能根本无法被映射到KMD,

2,即使指针在KMD代表相同的东西,但UMD的内存是分页的,而在系统调用被调用时,涉及的内存可能根本不在RAM中,

对UMD内存的直接饮用会导致页错误,oops会导致调用这个系统调用的进程死亡。

3,不安全,如果driver盲目引用UMD指针,UMD就可以随意访问和覆盖系统内存

这种访问由UMD专用函数完成,在asm/uaccess.h定义,六章ioctl介绍。

对于UMD和KMD的数据拷贝,使用:

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

它的行为像memcpy,但是要小心,

被寻址的UMD的page可能不在内存,这是虚拟内存子系统会让进程进入休眠状态,直到page在期望的位置,

如page必须从swap空间取回时。

这样的结果是,访问UMD的函数必须是可重入的,必须能和其他driver函数并发执行,第五章讨论

他们得作用不限于copy data,还检查了UMD的指针是否有效,

无效UMD point在第六章讨论。

下面介绍了read和write的代码

Linux设备驱动程序 三 字符设备驱动相关推荐

  1. 一起分析Linux系统设计思想——05字符设备驱动框架剖析(四)

    在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习.我的使命就是过滤掉大量的垃圾信息,将知识体系化,以短平快的方式直 ...

  2. Linux内核学习-字符设备驱动学习(二)

    在Linux内核学习-字符设备驱动学习(一)中编写字符设备驱动的一种方法,但是需要手动创建设备节点. 有没有能够自动的创建设备节点的呢? 有!使用class_create()和device_creat ...

  3. 设备树下字符设备驱动

    设备树下字符设备驱动 一.在设备树里添加自己的节点 二.驱动代码 三.makefile 四.应用层代码 运行测试 总结 一.在设备树里添加自己的节点 alphaled { 2 #address-cel ...

  4. 十四、linux 静态/动态申请字符设备号

    一.静态申请 • 所在目录:include/linux/fs.h – int register_chrdev_region(dev_t from, unsigned count, const char ...

  5. linux 读写设备文件,linux-中块设备文件及字符设备文件的本质区别

    原标题:linux-中块设备文件及字符设备文件的本质区别 在LINUX系统文件类型分类的文章中我们提到了 块设备 和 字符设备文件,那么什么是块设备 字符设备文件呢?他们之间有什么本质上的区别呢? 设 ...

  6. 你知道什么是 块设备 和 字符设备 吗?以及如何查看 块设备文件 和 字符设备文件 ?

    关于块设备 和 字符设备 介绍: 系统中能够随机(不需要按顺序)访问固定大小数据片(chunks)的设备被称作块设备,这些数据片就称作块.                 最常见的块设备是硬盘,除此以 ...

  7. Linux学习笔记(三)-设备管理与驱动

    设备管理与驱动 一. 设备分类:字符设备.块设备.特殊设备 1. 字符设备 - 是指每次与系统传输1个字符的设备 - 字符设备通常为传真.虚拟终端和串口调制解调器.键盘之类设备提供流通信服务 - 字符 ...

  8. Unix/Linux操作系统分析实验四 设备驱动: Linux系统下的字符设备驱动程序编程

    Unix/Linux操作系统分析实验一 进程控制与进程互斥 Unix/Linux操作系统分析实验二 内存分配与回收:Linux系统下利用链表实现动态内存分配 Unix/Linux操作系统分析实验三 文 ...

  9. Linux字符设备驱动程序开发(4)-LED驱动程序设计

    这里编写一个LED内核驱动代码.流程大概如下: 1.实现一个内核模块. 2.添加字符设备驱动框架. 3.在字符设备驱动中实现open和ioctl函数. 4.编写应用程序. led.c #include ...

最新文章

  1. 哪些人适合参加软件测试培训?
  2. 中文Visual Studio 2005 Express Beta2不能安装在英文Longhorn Beta1上吗?
  3. mysql commit慢_mysql autocommit问题导致的gtid同步变慢
  4. python【数据结构与算法】动态规划详解从背包到最长公共子序列(看不懂你来打我)
  5. 计算各种图形的周长(接口与多态)_JAVA
  6. 2020年联通软件研究院校招笔试第一题
  7. 计算机网络 | 网络层 :IP协议详解
  8. Q4_一个事物领导另一个
  9. 【caffe】ubuntu配置python接口----pycaffe
  10. mac pro制作iso系统光盘
  11. DiskGenius 强行拆分黑苹果HFS硬盘分区以给Windows扩容
  12. 使用阿里云OSS对象存储搭建个人图床
  13. 日常学习之:使用均值来填补缺失值的条件
  14. 不要再问芝士和奶酪有什么区别了!一次解释清楚
  15. ns3--TapBridge, TapNetDevice,FdNetDevice等
  16. 解决浏览器驱动和浏览器版本不匹配的报错:This version of ChromeDriver only supports Chrome version 97
  17. 【必须知道的职场情商训练7法】
  18. Advanced Installer修改默认安装路径判断盘符是否存在
  19. 关注而非监视孩子的成长--Leo育儿经(1)
  20. 增量模型(Incremental Model)

热门文章

  1. 【XSY2032】简单粗暴的题目 组合数
  2. H5实战(一):照片墙效果
  3. angularjs之循环遍历
  4. 灵遁者原创诗歌作品:石头会说话
  5. 视频教程-SharePoint Online 建站实战教程(上)-Office/WPS
  6. 开始学习 Microsoft SharePoint: Building Office 2003 Solutions
  7. vue.js实现图片、视频文件压缩后再上传
  8. 新手站长必须要主意的误区防范
  9. 腾讯天气前后端交互案例
  10. 703. 数独检查 - AcWing题库