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 mem
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
10 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 tee
Block devices:
1 ramdisk
259 blkext
7 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_class
s3g_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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
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-onlyu
if( 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 device
fd = s3gOpenMinor(0);
status = s3gCreateDdevice(fd, &create);
//保存fd和device
s3g->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的代码