驱动框架

为了更加直观,首先直接将字符设备的驱动代码给出,然后再慢慢解释。

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

#define MINORCOUNT 10

//我们驱动内部自定义的结构体
struct globalmem_device
{
struct cdev cdev;
char mem[4096];
}device_list;

static int device_major = 0;

//open方法的实现
int mydriver_open(struct inode *inode, struct file *filp)
{
struct globalmem_device *dev = NULL;

dev = container_of(inode->i_cdev, struct globalmem_device, cdev);
filp->private_data = dev; /* for other methods */

try_module_get(THIS_MODULE);

/** **/
return 0;
}

//file_operations结构体,最终的open,read系统调用即调用该结构体中的相关成员
struct file_operations dev_fops = {
.owner = THIS_MODULE,
.open = mydriver_open,
.release = ...,
};


static int globalmem_init(void)
{
dev_t devno = 0;
int retval = 0;

//申请资源
alloc_chrdev_region(&devno, 0, MINORCOUNT, “globalmem”);
device_major = MAJOR(devno);

devno = MKDEV(device_major, 0);

//初始化cdev结构体
cdev_init(&device_list->cdev, &dev_fops);

//注册cdev结构体到内核中
cdev_add (&device_list->cdev, devno, 1)


printk("globalmem_init sucess\n");
return 0;
}

static void globalmem_remove(void)
{
dev_t devno = 0;

printk("globalmem_remove\n");

cdev_del(&(device_list->cdev));

devno = MKDEV(device_major, 0);
unregister_chrdev_region(devno, MINORCOUNT);
}


static int __init my_init_module(void)
{
printk("----------init-----------\n");
globalmem_init();

return 0;
}

static void __exit my_exit_module(void)
{
printk("-----------exit----------\n");
globalmem_remove();

}

module_init(my_init_module);
module_exit(my_exit_module);

MODULE_LICENSE("Dual BSD/GPL");
MODULE_DESCRIPTION("this is test driver");

流程介绍

资源申请/注销

名称 说明
alloc_chrdev_region 申请资源,自动分配可用的主设备号
unregister_chrdev_region 释放资源
chrdevs 该数组记录了当前系统中所有的字符设备资源

chrdevs数组

下面先给出这个chrdevs数组的定义

1
2
3
4
5
6
7
8
9
10
11
//fs/char_dev.c
static struct char_device_struct {
    struct char_device_struct *next;
    unsigned int major;
    unsigned int baseminor;
    int minorct;
    const char *name;
    struct file_operations *fops;
    struct cdev *cdev;      /* will die */
} *chrdevs[MAX_PROBE_HASH];

该数组是在内核源码中的全局变量,记录了系统中所有的字符设备的资源,根据目前的学习程度了解到,该数组仅仅记录了哪些设备号已申请/未申请,并没有在驱动工作时起到真正的作用。

这里指的驱动工作时代表由用户层的open等系统调用,经历层层函数最终执行到驱动中的实现的open方法的过程。

alloc_chrdev_region

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* alloc_chrdev_region() - register a range of char device numbers
* @dev: output parameter for first assigned number
* @baseminor: first of the requested range of minor numbers
* @count: the number of minor numbers required
* @name: the name of the associated device or driver
*
* Allocates a range of char device numbers. The major number will be
* chosen dynamically, and returned (along with the first minor number)
* in @dev. Returns zero or a negative error code.
*/

//fs/char_dev.c

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
            const char *name)
{
    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;
}



static struct char_device_struct *
__register_chrdev_region(unsigned int majorunsigned int baseminor,
               int minorctconst char *name)
{
    struct char_device_struct *cd, **cp;
    int ret = 0;
    int i;
    cd = kmalloc(sizeof(struct char_device_struct), GFP_KERNEL);
    if (cd == NULL)
        return ERR_PTR(-ENOMEM);
    memset(cd, 0sizeof(struct char_device_struct));
    down(&chrdevs_lock);
    /* temporary */
//major 为0时代表自动分配主设备号,遍历chardevs数组,若为NULL,则代表该主设备号还没有分配
    if (major == 0) {
        for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
            if (chrdevs[i] == NULL)
                break;
        }
        if (i == 0) {
            ret = -EBUSY;
            goto out;
        }
        major = i;
        ret = major;
    }

//填充内容
    cd->major = major;
    cd->baseminor = baseminor;
    cd->minorct = minorct;
    cd->name = name;
    i = major_to_index(major);

//下面两个与int alloc_chrdev_region无关,因为此时我们找到的chardevs数组的成员为NULL
    for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
        if ((*cp)->major > major ||
            ((*cp)->major == major && (*cp)->baseminor >= baseminor))
            break;
    if (*cp && (*cp)->major == major &&
        (*cp)->baseminor < baseminor + minorct) {
        ret = -EBUSY;
        goto out;
    }
//将申请的资源赋值到chardevs数组上
    cd->next = *cp;
    *cp = cd;
    up(&chrdevs_lock);
    return cd;
out:
    up(&chrdevs_lock);
    kfree(cd);
    return ERR_PTR(ret);
}

可以看到申请资源的流程:
alloc_chrdev_region
=>__register_chrdev_region
=> 遍历chardevs找到成员为NULL的,代表该主设备号未分配
=>将我们申请的资源信息保存到该成员上
=>函数返回申请到的设备号

unregister_chrdev_region

注销的流程就不赘述了,下面给出源码

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

/**
* unregister_chrdev_region() - return a range of device numbers
* @from: the first in the range of numbers to unregister
* @count: the number of device numbers to unregister
*
* This function will unregister a range of @count device numbers,
* starting with @from. The caller should normally be the one who
* allocated those numbers in the first place...
*/

//fs/char_dev.c
void unregister_chrdev_region(dev_t from, unsigned count)
{
    dev_t to = from + count;
    dev_t n, next;
    for (n = from; n < to; n = next) {
        next = MKDEV(MAJOR(n)+10);//注意这里的next的计算,一般这个for循环都是执行一次
        if (next > to)
            next = to;
        kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
    }
}


static struct char_device_struct *
__unregister_chrdev_region(unsigned majorunsigned baseminorint minorct)
{
    struct char_device_struct *cd = NULL, **cp;
    int i = major_to_index(major);
    down(&chrdevs_lock);
    for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
        if ((*cp)->major == major &&
            (*cp)->baseminor == baseminor &&
            (*cp)->minorct == minorct)
            break;
    if (*cp) {
        cd = *cp;
        *cp = cd->next;
    }
    up(&chrdevs_lock);
    return cd;
}


设备注册/注销

名称 说明
cdev_init 初始化设备结构体
cdev_add 注册设备到内核中
cdev_del 注销设备
struct cdev 设备对象结构体
struct file_operations 各种回调函数的结构体
struct kobj_map 保存了系统中所有注册了的设备信息的一个map

file_operations/cdev

Linux一切皆文件,那么作为一个设备文件,它的操作方法接口封装在struct file_operations,当我们写一个驱动的时候,一定要实现相应的接口,这样才能使这个驱动可用。

struct cdev代表一个设备对象,保存了该字符设备的信息。

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
//inlcude/linux/fs.h
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
/*.......*/
};


//inlcude/linux/cdev.h

//设备对象结构体
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;//回调函数
struct list_head list;
dev_t dev;
unsigned int count;
};

cdev_init

该函数初始化cdev结构体,由源码可以发现主要就是将fops结构体赋值到cdev结构体中,并且初始化kobject对象,这个我们暂时不去深入了解他

1
2
3
4
5
6
7
8
//fs/char_dev.c
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
    memset(cdev, 0sizeof *cdev);
    INIT_LIST_HEAD(&cdev->list);
    kobject_init(&cdev->kobj, &ktype_cdev_default);
    cdev->ops = fops;
}

cdev_add

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
//driver/base/map.c
struct kobj_map {
struct probe {
struct probe *next;
dev_t dev;
unsigned long range;
struct module *owner;
kobj_probe_t *get;
int (*lock)(dev_t, void *);
void *data;//一般用这个保存cdev设备对象结构体
} *probes[255];
struct mutex *lock;
};
//可以发现在kobj_map中probe数组元素也是对应了每个主设备号


//fs/char_dev.c

static struct kobj_map *cdev_map;//全局变量cdev_map,保存所有注册的设备信息

static struct kobject *exact_match(dev_t dev, int *part, void *data)
{
struct cdev *p = data;
return &p->kobj;
}

static int exact_lock(dev_t dev, void *data)
{
struct cdev *p = data;
return cdev_get(p) ? 0 : -1;
}


int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
    int error;
    p->dev = dev;
    p->count = count;
    error = kobj_map(cdev_map, dev, count, NULL,
             exact_match, exact_lock, p);
    if (error)
        return error;
    kobject_get(p->kobj.parent);
    return 0;
}

由源码可知,cdev_add这个函数主要调用了kobj_map,且把全局变量cdev_map当作参数传入,所以猜想肯定是在map中找到可用的地方,然后把cdev设备对象保存起来把,接着往下看

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

//driver/base/map.c
int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,
struct module *module, kobj_probe_t *probe,
int (*lock)(dev_t, void *), void *data)
{
unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;//注意n的计算,一般来说都是1
unsigned index = MAJOR(dev);
unsigned i;
struct probe *p;

if (n > 255)
n = 255;

p = kmalloc(sizeof(struct probe) * n, GFP_KERNEL);//为本次注册的设备申请内存

if (p == NULL)
return -ENOMEM;

//填充内容
for (i = 0; i < n; i++, p++) {
p->owner = module;
p->get = probe;
p->lock = lock;
p->dev = dev;
p->range = range;
p->data = data;//这个data参数传入的就是cdev结构体,此时将其保存在data中
}
mutex_lock(domain->lock);

//使用主设备号直接查看这个probe数组
for (i = 0, p -= n; i < n; i++, p++, index++) {
struct probe **s = &domain->probes[index % 255];
while (*s && (*s)->range < range)//可以发现这个链表是根据range有小到大建立的
s = &(*s)->next;
p->next = *s;//将
*s = p;
}
mutex_unlock(domain->lock);
return 0;
}

总结cdev_add的调用流程即为:

cdev_add
=>kobj_map,将cdev_map和cdev设备对象结构体作为参数传入
=>使用主设备号直接查看cdev_map中的probe数组
=>将设备资源保存到cdev_map

cdev_del

同样删除就不再赘述了,源码如下

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
//fs/char_dev.c

void cdev_del(struct cdev *p)
{
    cdev_unmap(p->dev, p->count);
    kobject_put(&p->kobj);
}

static void cdev_unmap(dev_t dev, unsigned count)
{
    kobj_unmap(cdev_map, dev, count);
}


//driver/base/map.c
void kobj_unmap(struct kobj_map *domain, dev_t dev, unsigned long range)
{
unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;
unsigned index = MAJOR(dev);
unsigned i;
struct probe *found = NULL;

if (n > 255)
n = 255;

mutex_lock(domain->lock);
for (i = 0; i < n; i++, index++) {
struct probe **s;
for (s = &domain->probes[index % 255]; *s; s = &(*s)->next) {
struct probe *p = *s;
if (p->dev == dev && p->range == range) {
*s = p->next;
if (!found)
found = p;
break;
}
}
}
mutex_unlock(domain->lock);
kfree(found);
}

如何关联且工作?

名称 说明
init_special_inode 初始化inode节点的默认操作
chrdev_open 文件默认的open方法
kobj_lookup 查找cdev_map
def_chr_fops 新创建的文件的默认fops
struct inode inode节点
struct inode inode节点

到这里我们字符设备驱动注册已经讲完了,接下来我们讲解一下系统如何调用到我们驱动中的各种方法的(open,read等)

首先,在一个字符设备文件被创建的时候,内核会构造相应的inode,作为一种特殊文件,其inode初始化的时候,就会做一些准备工作

设备节点刚创建时,其实和真正的设备没有关系,除了主次设备号。第一次open时才去建立关联。

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
61
62
63
64
65
66
67
68
/*
* Keep mostly read-only and often accessed (especially for
* the RCU path lookup and 'stat' data) fields at the beginning
* of the 'struct inode'
*/
//include/linux/fs.h
//inode节点,此处只列出了我们关心的一些成员
struct inode {
umode_t i_mode;
unsigned short i_opflags;
kuid_t i_uid;
kgid_t i_gid;
unsigned int i_flags;

dev_t i_rdev;//设备主次设备号

union {
struct hlist_head i_dentry;
struct rcu_head i_rcu;
};

const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
struct file_lock *i_flock;
struct address_space i_data;

struct list_head i_devices;
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
};
/* .... */

};


//fs/char_dev.c
//inode节点默认的open方法
const struct file_operations def_chr_fops = {
.open = chrdev_open,
.llseek = noop_llseek,
};



//fs/inode.c
//inode节点创建时,需要初始化默认的open方法
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
inode->i_mode = mode;
if (S_ISCHR(mode)) {
inode->i_fop = &def_chr_fops;
inode->i_rdev = rdev;
} else if (S_ISBLK(mode)) {
inode->i_fop = &def_blk_fops;
inode->i_rdev = rdev;
} else if (S_ISFIFO(mode))
inode->i_fop = &pipefifo_fops;
else if (S_ISSOCK(mode))
inode->i_fop = &bad_sock_fops;
else
printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for"
" inode %s:%lu\n", mode, inode->i_sb->s_id,
inode->i_ino);
}



可以发现经过上面的一些操作后,将inode->i_fop成员填充,所以第一次open设备节点时,会调用chrdev_open函数,我们继续:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126

//include/linux/fs.h
//file结构体,open系统调用最先创建的结构体就是file
struct file {

struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
void *private_data;
/* .... */
};


/*
* Called every time a character special file is opened
*/

//fs/char_dev.c
static int chrdev_open(struct inode *inode, struct file *filp)
{
struct cdev *p;
struct cdev *new = NULL;
int ret = 0;

spin_lock(&cdev_lock);
p = inode->i_cdev;//p即为设备对象
if (!p) { //第一次打开,还没有建立关联
struct kobject *kobj;
int idx;
spin_unlock(&cdev_lock);
kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);//在cdev_map中查找对应的设备
if (!kobj)
return -ENXIO;
//此时已经找到了,new即为cdev对象
new = container_of(kobj, struct cdev, kobj);
spin_lock(&cdev_lock);
/* Check i_cdev again in case somebody beat us to it while
we dropped the lock. */
p = inode->i_cdev;
if (!p) {
//将找到的cdev设备对象保存在inode中,下次就无需再查找了
inode->i_cdev = p = new;
list_add(&inode->i_devices, &p->list);
new = NULL;
} else if (!cdev_get(p))
ret = -ENXIO;
} else if (!cdev_get(p))
ret = -ENXIO;
spin_unlock(&cdev_lock);
cdev_put(new);
if (ret)
return ret;

ret = -ENXIO;
//将cdev设备对象中保存的fops结构体(这个就是我们驱动中实现的各种方法了)保存到file结构体中
filp->f_op = fops_get(p->ops);
if (!filp->f_op)
goto out_cdev_put;

if (filp->f_op->open) {
//调用我们驱动中的open方法
ret = filp->f_op->open(inode, filp);
if (ret)
goto out_cdev_put;
}

return 0;

out_cdev_put:
cdev_put(p);
return ret;
}


//driver/base/map.c
struct kobject *kobj_lookup(struct kobj_map *domain, dev_t dev, int *index)
{
struct kobject *kobj;
struct probe *p;
unsigned long best = ~0UL;

retry:
mutex_lock(domain->lock);
//根据主设备号定位probe数组,然后遍历链表
for (p = domain->probes[MAJOR(dev) % 255]; p; p = p->next) {
struct kobject *(*probe)(dev_t, int *, void *);
struct module *owner;
void *data;

//判断你要寻找的设备号和已经保存的设备的关系
if (p->dev > dev || p->dev + p->range - 1 < dev)
continue;
if (p->range - 1 >= best)
break;
if (!try_module_get(p->owner))
continue;

//此时代表已经找到对应的设备了
owner = p->owner;
data = p->data;//这个就是cdev设备对象
probe = p->get;
best = p->range - 1;
*index = dev - p->dev;//dev_t设备号
if (p->lock && p->lock(dev, data) < 0) {
module_put(owner);
continue;
}
mutex_unlock(domain->lock);
kobj = probe(dev, index, data);//这个函数本质就是返回cdev结构体中的kobj成员,下面给出了
/* Currently ->owner protects _only_ ->probe() itself. */
module_put(owner);
if (kobj)
return kobj;//返回cdev设备对象的kobj成员
goto retry;
}
mutex_unlock(domain->lock);
return NULL;
}

//fs/char_dev.c
//这个函数就是cdev_add传入的参数
static struct kobject *exact_match(dev_t dev, int *part, void *data)
{
struct cdev *p = data;
return &p->kobj;
}

在open一个设备文件时,最先创建的就是struct file这个结构体,在这个结构体中就存放了你上层调用open时的信息(比如打开方式、阻塞方式等),其中inode成员保存了设备信息

总结调用流程如下:

打开文件
=>创建fd,创建struct file结构体
=>之后这个file结构体就通过设备文件的名字找到和设备文件绑定的inode结构体
=>调用inode中指定的默认open方法chrdev_open
=>若inode->i_cdev为空,即第一次打开时inode还未和设备对象cdev建立关联,调用kobj_lookup函数
=>根据主次设备号查找cdev_map
=>找到后将cdev保存到inode中,将cdev->fops保存到file中
=>调用我们驱动中的open方法
=>下次在有系统调用时,直接通过file中的fops即可直接调用

container_of/private_data

在驱动中实现的各种open,read等方法中,其中利用到container_of宏,例如我们一开始的例子中

1
2
3
4
5
6
7
8
9
10
11
12
int mydriver_open(struct inode *inode, struct file *filp)
{
struct globalmem_device *dev = NULL;

dev = container_of(inode->i_cdev, struct globalmem_device, cdev);
filp->private_data = dev; /* for other methods */

try_module_get(THIS_MODULE);

/** **/
return 0;
}

在linux内核中大量使用了面向对象的设计思想,具体的实现方法就是“嵌入结构体”,例如在我们这里inode->i_cdev保存了我们注册的cdev设备对象,而我们的自定义的结构体为:

1
2
3
4
5
struct globalmem_device
{
struct cdev cdev;
char mem[4096];
}device_list;

可以看到里面有个成员是cdev,就可以将cdev理解为父类,globalmem_device为派生类,当你获得device_list->cdev时,就可以使用container_of宏来直接获得device_list的地址,这样就可以直接访问我们自定义的结构体了,有没有很神奇

这个宏的实现如下,说到底就是利用的C语言指针操作地址的特性,感兴趣可以搜索一下

1
2
3
#define container_of(ptr, type, member) ({            \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})

这种open方法中使用container_of获取自定义结构体的方式是经常使用的,这样完全避免的全局变量的使用,更加安全和便捷

那么还有一个方式即为filp->private_data = dev; /* for other methods */,就是把刚刚用container_of获取的我们的结构体保存在file结构体中,那么之后的方法(write,read,llsek等)都能够通过filp->private_data获取到我们的结构体