一、scull的设计
“Simple Character Utility for Loading Localities,区域装载的简单字符工具”
scull是一个操作内存区域的字符设备驱动程序,这片内存区就相当于一个设备。
scull的优点在于它不和硬件相关,而只是操作从内核分配的一些内存。
除了展示内核和字符设备驱动程序
scull0~scull3:全局、持久。
全局是指如果设备被多次打开,则打开它的所有文件描述符可共享该设备所包含的数据。
持久是指如果设备关闭后再打开,则其中的数据不会丢失。
可以使用常用命令来访问和测试这个设备,如cp、cat以及shell的IO重定向。
scullpipe0~scullpipe3
这四个FIFO设备与管道类似,一个进程读取由另一个进程写入的数据。如果多个进程读取同一个设备,他们就会为数据发生竞争。scullpipe的内部实现将说明在不借助于中断的情况下如何实现阻塞式和非阻塞式读、写操作。
scullsingle
一次只允许一个进程使用该驱动程序
scullpriv
对每个虚拟控制台是私有的,这是因为每个控制台、终端上的进程将获取不同的内存区。
sculluid和scullwuid
可以被多次打开,但每次只能由一个用户打开,如果另一个用户锁定了该设备,sculluid将返回"Device Busy"的错误,而scullwuid则实现了阻塞式open。
这些scull设备的变种混淆了“机制”和“策略”,但这类处理是值得去了解的,因为某些真正的设备要类似的管理方式。
二、主设备号和次设备号
1、
对字符设备的访问是通过文件系统内的设备名称进行。
这些名称被称为特殊文件、设备文件、或简单称之为文件系统树节点,他们位于/dev目录。
字符设备驱动程序的设备文件可通过 ls-l命令输出的第一列中的"c"来识别。
块设备也会出现在/dev下但他们由字符"b"标识。
如果执行ls -l 命令,则可在设备文件项的最后修改日期前看到两个数,这个位置通常显示的是文件的长度。
而对于设备文件,这两个数就是相应设备的主设备号和次设备号。
主设备号标识设备对应的驱动程序如/dev/null和/dev/zero由驱动程序1管理。而虚拟控制台和串口终端由驱动程序4管理,vcsl和vcsal设备都由驱动程序7管理。
一般“一个主设备号对应一个驱动程序”
次设备号由内核使用,用于正确确定设备文件所指的设备。我们可以通过次设备号获得一个指向内核设备的直接指针,也可以将此设备号当作设备本地数组的索引。内核本身基本上不关心关于次设备号的任何其他信息。
2、设备编号的内部表达
在内核中,dev_t类型用来保存设备编号------包括主设备号和次设备号。
在linux2.6里dev_t是个32位的,前12位用来标识主设备号,其余20位标识次设备号。
要始终使用<linux/kdev_t.h>中定义的宏
比如要获得dev_t的主设备号或次设备号,应使用:
MAJOR(dev_t dev);
MINOR(dev_t dev);
相反,如果需要将主设备号和次设备号转换成dev_t类型,则用:
MKDEV(int major,int minor);
3、分配和释放设备编号
在创建一个字符设备之前我们首先要做的就是获得一个或多个设备编号。完成该工作的必要函数是
register_chrdev_region
该函数在
<linux/fs.h>声明
int register_chrdev_region(dev_t first, unsigned int count, char * name);
其中,first是要分配的设备编号范围的起始值。first的次设备号经常被置为0,但对该函数来讲并不是必需的。count是所请求的连续设备编号的个数。如果count非常大,则所请求的范围可能和下一个主设备号重叠。
name是和该编号范围关联的设备名称,它将出现在**/proc/devices和sysfs中**。
register_chrdev_region的返回值在分配成功时为0,在错误情况下将返回一个负的错误码,并且不能使用所请求的编号区域。
如果我们提前明确知道所需要的设备编号,则register_chrdev_region会工作的很好。但是,我们经常不知道设备将要使用哪些主设备号;因此,Linux内核开发社区一直在努力向设备编号的动态分配。在运行过程中使用下面的函数,内核将为我们恰当分配所需的主设备号:
int alloc_chrdev_region(dev_t *dev,unsigned int firstminor,unsigned int count, char * name)
在上面这个函数中,dev是仅用于输出的参数,在成功完成调用后将保存已分配范围的第一个编号。firstminor应该是要使用的被请求的第一个次设备号,它通常是0.,count和name参数与register_chrdev_region是一样的。
在不适用时释放这些设备编号:
void unregister_chrdev_region(dev_t first, unsigned int count);
通常,我们在模块的清除函数中调用
unregister_chrdev_region
函数。
3、动态分配主设备号
一部分主设备号已经静态分配给大部分常见设备,在内核源码树的Documentation/devices.txt文件中可以找到这些设备的清单。
可以简单选定一个尚未被使用的编号,或者通过动态方式分配主设备号。
动态分配缺点是:由于分配的主设备号不能保证始终一致,所以无法预先创建设备节点。一旦分配了设备号,就可以从/proc/devices中读取得到。
为了加载一个使用动态主设备号的设备驱动程序,对insmod的调用可替换为一个简单的脚本,该脚本在调用insmod之后读取/proc/devices以获得新分配的主设备号,然后创建对应的设备文件。
典型的/proc/devices文件:

mknod /dev/${device}0 c $major 0
mknod /dev/${device}1 c $major 1
mknod /dev/${device}2 c $major 2
#给定适当的组属性及许可,并修改属组
#并非所有的发行版都具有staff组,有些有wheel组
group="staff"
grep -q '^staff:' /etc/group || group="wheel"
chgrp $group /dev/${device}[0-3]
chmod $mode /dev/${device}[0-3]
。这些操作主要用来实现系统调用,命名为open\read等等。我们可以认为文件是一个“对象”,而操作它的函数是“方法”。如果采用面向对象编程的术语来表达就是,对象声明的动作将作用于其本身。
file_operations结构这类的指针称为fops。这个结构中的每一个字段都必须指向驱动程序中实现特定操作的函数,对不支持的操作对应字段可以设置为NULL。
2、file
这里的file和用户空间的FILE是不同的,FILE在C库中定义不会出现在内核代码中。file代表一个打开的文件。它由内核 open时候创建,并传递给在该文件上进行操作的所有函数,直到最后的close函数。内存会释放这个结构。
在内核源码中,指向struct file的指针通常被称为file或filp
3、inode结构
四、字符设备的注册
1、
struct cdev 表示字符设备
在<linux/cdev.h>
struct cdev *my_dev = cdev_alloc();
my_cdev->ops = &my_fops;
用这个函数来初始化已分配到的结构:
void cdev_init(struct cdev*cdev , struct file_operations * fops);
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
void cdev_del(struct cdev * dev);
2、Scull中的设备注册
在scull内部,它通过struct scull_dev的结构来表示每个设备,该结构定义如下:
struct scull_dev{
struct scull_qset *data; /*指向第一个量子集的指针*/
int quantum; /*当前量子的大小*/
int qset /*当前数组的大小*/
unsigned long size; /*保存在其中的数据总量*/
unsigned int access_key; /*由sculluid和scullpriv使用*/
struct semaphore sem; /*互斥信号量*/
struct cdev cdev; /*字符设备结构*/
};
我们会在遇到该结构字段时候讨论他们,现在我们聚焦于cdev上,即内核和设备直接的接口struct cdev。该结构必须如上所述地被初始化并添加到系统中,scull中完成这一工作的代码如下:
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_MODULE;
dev->cdev.ops = &scull_fops;
err = cdev_add(&dev->cdev,devno,1);
/*Fail gracefully if need be*/
if(err)
printk(KERN_NOTICE "Error %d adding scull%d",err,index);
}
因为cdev结构被嵌入到struct scull_dev中,因此必须调用cdev_init来执行该结构的初始化。
五、open和release
1、open
int scull_open(struct inode *inode,struct file * filp)
{
struct scull_dev *dev; /*device infomation*/
dev = container_of(inode->i_cdev,struct scull_dev,cdev);
filp->private_data = dev; /*for other methods*/
/*now trim to 0 the length of the device if open was write-only*/
if((filp->f_flags&O_ACCMODE)==O_WRONLY)
{
scull_trim(dev); /*ignore errors*/
}
return 0;
}
2、release方法
device_close

{
return 0;
}
六、scull的内存使用
scull驱动程序引入了Linux内核中用于内存管理的两个核心函数,定义在<linux/slab.h>中
void *kmalloc(size_t size, int flags);
void kfree(void *ptr);
对kmalloc的调用将试图分配size个字节大小的内存;其返回值指向该内存的指针,分配失败时返回NULL。flags参数用来描述内存的分配方法。
对分配大的内存区域来说,kmalloc并不是最有效的方法。
在scull中每个设备都是一个指针链表,每个指针都指向一个scull_qset结构。

{
struct scull_qset *next , *dptr;
int qset = dev->qset; /*"dev"非空*/
int i;
for(dptr = dev->data;dptr;dptr = next)
{
/*所有链表项*/
if(dptr->data)
{
for(i=0;i<qset;i++)
kfree(dptr->data[i]);
kfree(dptr->data);
dptr->data = NULL;
}
next = dptr->next;
kfree(dptr);
}
dev->size = 0;
dev->quantum = scull_quantum;
dev->qset = scull_qset;
dev->data = NULL;
return 0;
}
小技巧:
输入cat /proc/devices可以查看已经被使用的设备号