本章目标是编写一个完整的字符设备驱动程序——scull。即“simply character unility for loading localites,区域装载的简单字符设备”。scull是一个操作内存区域的字符设备驱动程序,这片内存就相当于一个设备。scull的优点在于不和硬件相关,而只是操作从内核中分配的一些内存。
scull的设计
scull0~scull3
这四个设备分别由一个全局且持久的内存区域组成。
“全局”:如果设备被多次打开,则打开它的所有文件描述符可共享该设备所包含的数据。
“持久”:如果设备关闭后再打开,则其中数据不会丢失。
scullpipe0~scullpipe3
这四个FIFO(先入先出)设备与管道类似。
如果多个进程读取同一个设备,它们就会为数据发生竞争。
scullsingle
scullpriv
sculluid
scullwuid
sculluid和scullwuid可以被多次打开,但每次只能由一个用户打开;如果另一个用户锁定了该设备,sculluid将返回“Device Busy”的错误,而scullwuid则实现阻塞式open。
主设备号和次设备号
对字符设备的访问通过文件系统内的设备名称进行。位于/dev下
ls -al
字符设备用“c“标识
块设备用”b“标识
主设备号表示设备对应的驱动程序;次设备号由内核使用,用于正确确定设备文件所指的设备。
现代Linux内核允许多个驱动程序共享主设备号,但大多还是“一个主设备号对应一个驱动程序”。
设备编号的内部表达
内核用dev_t类型(<linux/types.h>)来保存设备编号,dev_t是一个32位的数,12位表示主设备号,20为表示次设备号。
使用<linux/kdev_t.h>中定义的宏来转换格式。
转换 | 宏 |
---|---|
(dev_t)–>主设备号 | MAJOR(dev_t dev) |
(dev_t)–>次设备号 | MINOR(dev_t dev) |
主设备号、次设备号–>(dev_t) | MKDEV(int major,int minor) |
2.6内核之前只允许255个主设备号和255个次设备号。
分配和释放设备编号
#include<linux/fs.h>
操作 | 函数 | 返回值 |
---|---|---|
静态指定 | int register_chrdev_region(dev_t from, unsigned count, const char *name); | 0成功;负数失败 |
动态分配 | int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name); | 0成功;负数失败 |
释放 | void unregister_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name); |
参数说明:
/* fs/char_dev.c */
int register_chrdev_region(dev_t from, unsigned count, const char *name)
//from: 要分配的设备编号范围内的起始值
//count: 所请求连续设备编号的个数
//name: 和该编号范围关联的设备名称,将出现在/proc/devices和sysfs中
{
struct char_device_struct *cd;
dev_t to = from + count;
dev_t n, next;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
if (next > to)
next = to;
cd = __register_chrdev_region(MAJOR(n), MINOR(n),
next - n, name);
if (IS_ERR(cd))
goto fail;
}
return 0;
fail:
to = n;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
}
return PTR_ERR(cd);
}
/* fs/char_dev.c */
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
//dev: 输出的参数,在完成调用后将保存已分配范围的第一个编号
//baseminor: 被请求的第一个次设备号
//count: 被请求的次设备号数目
//name: 和该编号范围关联的设备名称,将出现在/proc/devices和sysfs中
{
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);
return 0;
}
/* fs/char_dev.c */
void unregister_chrdev_region(dev_t from, unsigned count)
//from: 要释放的设备编号范围内的起始值
//count: 要释放的设备的数目
{
dev_t to = from + count;
dev_t n, next;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
if (next > to)
next = to;
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
}
}
动态分配设备号
驱动程序应该始终使用alloc_chrdev_region而不是register_chrdev_region函数。
动态分配的缺点:由于分配的主设备号不能保证始终一致,所以无法预先创建设备节点。但是一旦分配了设备号,可以通过/proc/devices中读取得到。
动态设备号并不是随机生成的,如果不受其他(动态)模块影响,可以预期获得相同的动态主设备号。
分配之设备号的最佳方式是:默认采用动态分配,同时保留在加载甚至是编译时指定主设备号的余地。
以下是在scull.c中用来获取主设备号的代码:
if (scull_major) {
dev = MKDEV(scull_major, scull_minor);
result = register_chrdev_region(dev, scull_nr_devs, "scull");
} else {
result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs,
"scull");
scull_major = MAJOR(dev);
}
if (result < 0) {
printk(KERN_WARNING "scull: can't get ma