Linux Simple Scull驱动

参考《Linux设备驱动程序第3版》

Scull字符型驱动程序的设备描述

字符设备驱动程序是分配一段内存,然后提供对这个设备的管理方式,我们通过它提供的操作来操纵字符驱动设备。本质上是一个我们分配的模拟内存来模拟驱动设备。

###设备编号
字符型驱动设备可以通过命令ls -l查看文件类型为c的设备。

  • major主设备编号标识设备对应的驱动程序
  • minor次设备号由内核使用,用于正确确定设备文件所指的设备。
  • 在内核中,dev_t(定义在<linux/types.h>中)用来保存设备编号,包括major和minor。
  • 在建立一个字符设备之前,驱动程序首先要获取一个或多个设备编号。函数为register_chrdev_region在<linux/fs.h中声明>。清除函数为unregister_chrdev_region。这种方式属于静态分配一个当前没有使用的设备号为主设备号。
  • 动态分配alloc_chrdev_region
  • 一旦分配了设备号,就可以从/proc/devices中得到。
  • 安装scull设备,使用内核命令insmod(/sbin/insmod)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    #!/bin/sh
    module="scull"
    device="scull"
    mode="664"

    #install scull,invoke insmod with all arguments we got
    #use a pathname, as insmod doesn't look in . by default
    /sbin/insmod ./$module.ko $* || exit 1

    #retrieve the major number
    major=$( awk "\$2==\"$module" {print \$1}" /proc/devices)
    #which is same as
    major=$( awk "\$2==\"scull\" {print \$1}" \proc\devices)

    #remove stale nodes
    rm -f /dev/${device}[0-3]

    echo ${device}0 ${major}0

    #make nodes,创建了四个设备
    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

    ln -sf ${device}0 /dev/${device}

    # grep -q表示quiet不显示任何结果,正则表达式匹配^表示匹配字
    符串起始位置
    # wheel组包含特殊权限,可以执行root权限但并不需要知道root管理员密码。
    if grep -q '^staff:' /etc/group; then
    group='staff'
    else
    group='wheel'
    fi

    chgrp $group /dev/${device}[0-3]
    chmod $mode /dev/${device}[0-3] #配置权限664
  • 卸载设备
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #!/bin/sh
    module="scull"
    device="scull"

    #remove module from the kernel
    /sbin/rmmod $module $* || exit 1

    #remove the device file
    rm -f /dev/${device} /dev/${device}[0-3]

安装好的dev
可以看出在/dev下,scull这个模块包含四个设备,保存在一个文件夹中,并且文件夹指向scull0设备。

  • 除了使用load和unload脚本之外,还可以编写一个init脚本,并保存在发行版使用的init脚本目录中(scull.init)。接收约定的参数(start、stop、restart)也可以完成scull_load和scull_unload双重任务。
  • 分配设备号的最佳方式。默认采用动态方式,同时保留加载或编译是指定的主设备编号。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    dev_t dev = 0;
    int result;
    if(scull_major){ //存在驱动程序
    dev = MKDEV(scull_major, scull_minor);
    result = register_chrdev_region(dev, scull_nr_devs, "scull"); //在dev设备上注册设备
    } else{
    result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull"); //分配major驱动程序编号
    scull_major = MAJOR(dev); //获取设备驱动
    }
    if(result < 0){
    printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
    return result;
    }

驱动程序需要的数据结构

大部分基本的驱动程序操作涉及到三个重要的内核数据结构,分别是file_operations、file和inode。

file_operations 文件操作

  • file_operations是建立驱动程序操作与设备编号之间的连接。该结构定义在<linux/fs.h>,其中包含一组函数指针。
  • 每个打开的文件(file)和一组函数关联。这些操作主要用来实现系统调用。我们可以认为文件是一个”对象“,而操作他的函数是”方法“。
  • 在查看file_operations方法清单时,我们会注意到许多参数包含__user字符串,表明指针是一个用户空间地址,不能被直接饮用。

基本的file_operations数据结构:

  • struct module *owner :指向”拥有“该结构的木偶快的指针。内核使用这个字段来避免在模块的操作正在被使用时卸载该模块。该成员都会被初始化为THIS_MODULE, which is defined in <linux/module.h>
  • loff_t (**llseek) (struct file *, loff_t, int); 该方法用来修改文件当前读写位置,并将新的位置(正的)作为返回值返回。loff_t是一个长偏移量。如果这个函数指针是NULL,那么对seek的调用将以某种不可预期的方式修改file结构中的位置计数器。
  • ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); 从设备中读取数据,该函数指针为NULL时,将导致read系统调用出错并返回-EINVAL(invalid argument)。函数返回成功读取的字节数。对应的异步读取函数为aio_read。
  • ssize_t (*write) (struct file *, const char __user *, size_t, loff_t); 向设备发送数据。对应的异步写入操作 (aio_write)
  • int (*open) (struct inode *, struct file *); 这是对设备文件执行的第一个操作,但并不是一定要声明,如果入口为NULL,设备的打开操作会永远成功,但系统不会通知驱动程序。
  • int (*lock) (struct file *, int, struct file_lock *); lock方法用于实现文件锁定,锁定是常规文件不可缺少的特性,但设备驱动程序基本不会实现这个方法。
1
2
3
4
5
6
7
8
9
struct file_operations scull_fops={
.owener = THIS_MODULE,
.llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
.ioctl = scull_ioctl,
.open = scull_open,
.release = scull_release,
};

file文件结构

  • file结构与用户空间程序中的FILE没有任何联系,(FILE是在C库中定义的,所以不会出现在内核中)struct file是一个内核结构,不会出现在用户结构中。
  • struct file表示一个打开的文件(不仅仅限定于设备驱动程序,系统中每个打开的文件在内核空间中都有一个对应的file结构)。由内核在open操作时创建,并传递给该文件上进行操作的所有函数,直到最后close函数。
  • mode_t f_mode: 文件模式,通过FMODE_READ和FMODE_WRITE来标识文件是否可读或可写。内核在调用驱动程序的read和write之前已经检查了访问权限,没有访问权限的情况下内核将拒绝对该文件进行读写操作。
  • loff_t f_pos: 当前read/write位置 (long offsite)。如果驱动程序需要知道文件中的当前位置,可以读取这个值。
  • unsigned int f_flags: 文件标志,检查用户请求的是否是非阻塞式操作。
  • struct file_operations *f_op 文件相关操作。
  • void *pricate_data: open调用前将这个指针置为NULL。驱动可以将这个字段用于任何目的,是跨系统调用时保存状态信息非常有用的资源。
  • struct dentry *f_dentry: 文件对应的目录项结构。

inode结构

  • 内核用inode结构在内部表示文件,和file结构不同,file表示打开的文件描述符。对单个文件,可能会有许多个表示打开的file结构,但他们都指向单个inode结构。
  • inode结构中包含了大量的有关文件信息。常规只有以下两个字段对驱动有用:
  • dev_t i_rdev 表示设备文件的inode结构,包含了真正的设备编号
  • struct cdev *i_cdev 表示私服设备的内核的内部结构。

Scull设备的注册

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

分配和初始化cdev的方法:

  • 在运行时获取一个独立的cdev结构:
    1
    2
    struct cdev *my_cdev = cdev_alloc();
    my_cdev->ops = *my_fops;
  • 也可以初始化的时候进行分配
    1
    void cdev_init(struct cdev *cdev, struct file_operations *fops);

定义Scull设备, 并初始化Scull设备

1
2
3
4
5
6
7
8
9
struct scull_dev{
struct scull_qset *data;
int quantum;
int qset;
unsigned long size;
unsigned int access_key;
struct semaphore sem;
struct cdev cdev; //定义这是一个字符设备结构
};

Scull设备初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
int scull_major =   SCULL_MAJOR; //defined in scull.h
int scull_minor = 0;
int scull_nr_devs = SCULL_NR_DEVS; /* number of bare scull devices */
int scull_quantum = SCULL_QUANTUM;
int scull_qset = SCULL_QSET;

struct scull_dev *scull_devices;

int scull_init_module(void){ //内核函数,无参数的标准形式都要加上void

int result, i;
dev_t dev;

//1. 定义驱动程序major
if(scull_major){
dev = MKDEV(scull_major, scull_minor); //创建dev
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 major %d\n", scull_major);
return result;
}

//2. 建立并分配设备文件(动态分配)
scull_devices = kmalloc(scull_nr_devs * sizeof(struct scull_dev), GFP_KERNEL);
if(!scull_devices){
result = -ENOMEM; //内存不足错误
goto fail;
}
memset(scull_devices, 0, scull_nr_devs * sizeof(struct scull_dev));

//3. 初始化每个设备
for(i = 0; i<scull_nr_devs; i++){
scull_device[i].quantum = scull_quantum;
scull_device[i].qset = scull_qset;
//linux 4.0内核更新, 初始化互斥量
sema_init(&scull_device[i], 1);
scull_setup_cdev(&scull_device[i], i);
}


dev = MKDEV(scull_major, scull_minor+scull_nr_devs);
dev += scull_p_init(dev);
dev += scull_access_init(dev);

#ifdef SCULL_DEBUG
scull_create_proc();
#endif

return 0;

fail:
scull_cleanup_module();
return result;

}

定义scull函数接口

1
2
3
4
5
6
7
8
9
struct file_operations scull_fops = { 
.owner = THIS_MODULE,
.llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
// .ioctl = scull_ioctl, 最新内核删掉了这个接口
.open = scull_open,
.release= scull_release,
};

Scull设备操作接口

open和release

open方法

open方法应该完成:

  • 检查设备特定的错误(如设备未就绪或类似的硬件问题)
  • 如果设备首次打开,则初始化
  • 如果有必要,更新f_op指针
  • 分配并填写置于filp->private_data里的数据结构

int (*open) (struct inode *inode, struct file *filp);
经过简化的scull_open代码,没有进行设备初始化工作。

1
2
3
4
5
6
7
8
9
10
11
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 the length of the device if open was write-only
if((filp->f_flags & O_ACCMODE) == O_WRONLY){
scull_trim(dev);
}
return 0;
}

release方法

与open方法相反。release完成以下任务:

  • 释放由open分配的,保存在filp->private_data中的所有内容
  • 在最后一个关闭操作时关闭设备。
1
2
3
int scull_release(struct inode *inode, struct file *filp){
return 0;
}

并不是每个close系统调用都会引起release方法的调用。只有那些真正释放设备数据结构的close调用才会调用这个方法。内核对每个file结构维护其被使用多少次的计数器。无论是fork还是dup,都不会创建新的数据结构(数据结构只能由open创建),它只是增加已有结构的记数。只有在file结构的计数归0的时候,close系统调用才会执行release方法。

Scull内存使用

scull驱动程序引入了内存管理的两个核心函数kmalloc和kfree,定义在<linux/slab.h>。

void *kmalloc(size_t size, int flags);

  • 试图分配 size个自己的大小,返回值为内存指针。flags为描述内存的分配方法。目前始终使用GFP_KERNEL。
    void kfree(void *ptr);

在scull中,每个设备都使用一个指针链表,每个指针都只想一个scull_qset结构。如下图:
我们把每一个内存区成为一个量子(4000字节),指针数组成为量子集(1000个地址)。

scull内存分配

这样scull写入一个字节就会消耗8000或者12000个字节的内存。(每个量子占4000字节,一个量子集占1000*4或1000*8个字节)那么为量子和量子集选择合适的数值就可以配置如何使用该设备。

  • 在编译时可以修改scull.h中的宏SCULL_QUANTUM和SCULL_QSET。
  • 在加载模块时,可以设置scull_quantum和scull_qset的整数值。
  • 或者在运行时,使用ioctl修改当前值或默认值(可能不使用与linux 4.0)

read和write

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);

count是请求传输的数据长度,buff是指向用户空间的缓冲间,最后offp是一个指向长偏移量对象的指针,指明用户在文件中进行存取操作的位置。

显然,驱动程序必须访问用户空间的缓冲区以便完成自己的工作。这个过程由内核专用函数提供 <linux/uaccess.h> (在linux4.0版)

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);

这两个函数很像memcpy,但内核访问用户空间时,被寻址的用户空间页面可能并不在内存中,这时候该驱动进程就会转为睡眠状态直到页面加载进内存。带来的结果是访问用户空间的任何函数都必须是可重入的,并且能和其他驱动程序函数并发执行,而且必须处于能够合法休眠的状态。

read代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
ssize_t scull_read(struct file *filp, char __user *buf, size_t count, loff_t *f_ops){
struct scull_dev *dev = filp->private_data;
struct scull_qset *dptr;
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset;
int item, s_pos, q_pos, rest;
ssize_t retval = 0;

if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
if(*f_pos >= dev->size)
goto out;
if(*f_pos + count > dev->size)
count = dev->size - *f_pos;
//在量子集中寻找链表项,qset索引以及偏移量
item = (long) *f_pos / itemsize; //在哪一个qset
rest = (long) *f_pos % itemsize; //在当前qset的多少个字节偏移处
s_pos = rest / quantum; q_pos = rest%quantum; //s_pos找到是哪一个quantum,q_pos找到是当前quantum的哪一个偏移

//沿该链表前行,直到正确的位置
dptr = scull_follow(dev, item);

if(dptr == NULL || !dptr->data || !dptr->data[s_pos])
goto out;

if(count > quantum - q_pos)
count = quantum - q_pos;
if(copy_to_user(buf, dptr->data[s_pos], count)){ //拷贝成功返回0
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;

return retval;

out:
up(&dev->sem);
return retval;
}

write代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
ssize_t scull_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_ops){
struct scull_dev *dev = filp->private_data;
struct scull_qset *dptr;
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset;
int item, s_pos, q_pos, rest;
ssize_t retval = -ENOMEM;

if(down_interruptible(&dev->sem)) //就是获得信号量
return -ERESTARTSYS; //表示信号函数处理完毕后重新执行信号函数前的某个系统调用, linux上层系统受到这个返回值之后会重新执行这个信号函数的系统调用

item = *f_ops / itemsize;
rest = *f_ops % itemsize;
s_pos = rest / quantum;
q_pos = rest % quantum;

dptr = scull_follow(dev, item); //找到qset
if(dptr == NULL) goto out;
if(!dptr->data){ //找到的qset为空
dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL); //分配qset的数组地址,注意这个data是保存指针的
if(!dptr->data) goto out; //分配失败
memset(dptr->data, 0, qset * sizeof(char *));
}
if(!dptr->data[s_pos]){ //qset当前不是空, quantum是一个新的
dptr->data[s_pos] = kmalloc(quntum, GFP_KERNEL);
if(!dptr->data[s_pos]) goto out;
}

if(count > quantum - q_pos) count = quantum - q_pos;
if(copy_from_user(dptr->data[s_pos]+q_pos, buf, count)){
retval = -EFAULT;
goto out;
}

*f_pos += count;
retval = count;
//更新文件大小
if(dev->size < *f_pos) dev->size = *f_pos;

out:
up(&dev->sem);
return retval;
}