Table of Contents generated with DocToc

说明

该文章是liode学习笔记,根据https://www.bilibili.com/video/BV1Yb4y1t7Uj?p=4&spm_id_from=pageDriver来做的总结

应用程序如何使用驱动

应用程序只能通过open,read,write等标准库函数来访问到驱动程序,不能直接访问到硬件,只有驱动程序能访问到硬件,因此,驱动程序要提供对应的drv_open,drv_read,drv_write等库函数对应的底层的驱动
函数,来操作硬件。

下面这些是应用程序可以做的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 2. 打开文件 */
fd = open("/dev/hello", O_RDWR);
if (fd == -1)
{
printf("can not open file /dev/hello\n");
return -1;
}

/* 3. 写文件或读文件 */
if ((0 == strcmp(argv[1], "-w")) && (argc == 3))
{
len = strlen(argv[2]) + 1;
len = len < 1024 ? len : 1024;
write(fd, argv[2], len);
}
else
{
len = read(fd, buf, 1024);
buf[1023] = '\0';
printf("APP read : %s\n", buf);
}
close(fd);

为什么open某一个文件,就能找到驱动

linux通过文件的属性来找到的驱动,比如:

image

这个tty4是c字符设备,主设备号是4,次设备号是4;linux根据这三个来找,主要是前两个。

大概的流程是这样的,这是个c设备,所以驱动在chrdevs[]里面,索引就是主设备号,这样就找到了相应的驱动。

在fs/char_dev.c中定义了这个容器,里面装了很多字符驱动

1
2
3
4
5
6
7
8
static struct char_device_struct {
struct char_device_struct *next;
unsigned int major;
unsigned int baseminor;
int minorct;
char name[64];
struct cdev *cdev; /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

怎么写驱动

1. 确定主设备号(数组的索引),0的话为系统自动分配

使用下面的命令可以查看有哪些驱动,哪些主设备号被占用了:

1
2
3
4
5
6
book@100ask:/dev$ cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS

比如我现在就像被分配,那我就选一个0

2. 构造结构体 file_operations

用到什么构造什么,常用的read,write之类的,不需要全部构造,这是个很大的结构
在fs.h中:

1
2
3
4
5
6
7
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 *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
...

使用的时候像下面这样,左边是成员名,右边是我们的函数名。

1
2
3
4
5
6
7
8
/* 2. 定义自己的file_operations结构体                                              */
static struct file_operations hello_drv = {
.owner = THIS_MODULE,
.open = hello_drv_open,
.read = hello_drv_read,
.write = hello_drv_write,
.release = hello_drv_close,
};

这个.是什么意思呢?这是c99引入的语法,如果没有这个东西,就必须像下面这样初始化结构体了

1
2
3
4
5
6
7
8
9
/* 2. 定义自己的file_operations结构体                                              */
static struct file_operations hello_drv = {
THIS_MODULE,
Null,
hello_drv_read,
hello_drv_write,
NULL,
...
};

没有的项也必须写出来,而且必须要按顺序赋值。非常坑。

3. 注册驱动

注册也就是说,要把某个结构体,放到某个容器中去。

1
major = register_chrdev(0, "hello", &hello_drv);  /* /dev/hello */

第一个是主设备号,0是说系统返回一个可以用的。只要驱动文件的主设备号等于这个值,就可以
访问到驱动程序了。
那么谁来调用这个驱动程序呢?

4. 描述入口函数

就像应用程序有main入口一样,驱动也有入口函数,我们写的驱动程序,需要用module_init(**)
来描述它。当安装驱动的时候,就会使用到。
比如下面这样,这个hello_init函数经过module_init之后,就会被识别为入口函数了,而上面的注册,
就在这个函数里面。

1
module_init(hello_init);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int __init hello_init(void)
{
int err;

printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
major = register_chrdev(0, "hello", &hello_drv); /* /dev/hello */


hello_class = class_create(THIS_MODULE, "hello_class");
err = PTR_ERR(hello_class);
if (IS_ERR(hello_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "hello");
return -1;
}

device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */

return 0;
}

其他

module_init是如何实现的

module_init是将一个普通函数,注册为驱动入口函数的功能。那么它是怎么实现这个功能的呢?

首先,这是一个宏,在module.h中被定义。

如果定义了module,它就是下面的宏,而我们的驱动都是一个module的形式

1
2
3
4
#define module_init(initfn)					\
static inline initcall_t __inittest(void) \
{ return initfn; } \
int init_module(void) __attribute__((alias(#initfn)));

比如我们的hello_init是一个普通的函数,经过宏展开变成了什么呢?

1
2
3
static inline initcall_t __inittest(void)		\
{ return hello_init; } \
int init_module(void) __attribute__((alias(#hello_init)));

变成了两个函数,一个是测试函数,并不重要,下面的很重要

1
int init_module(void) __attribute__((alias(#hello_init)));

相当于定义了一个init_module函数,同时它的别名是hello_init

也就是说,一个驱动程序,他的入口函数永远都是init_module,只不过它的别名不一样。

换句话说,如果你不想多一行代码:

1
module_init(hello_init);

那么就不要定义为hello_init,而需要定义为init_module

次设备号有什么作用

根据主设备号就已经确定了唯一的驱动,那么次设备号又有什么作用呢?

次设备的作用由驱动程序自己决定,完全可以不用,也可以使用。这个号码,可以作为参数被驱动程序
都进去,进而可以发挥作用。

比如:有两个led,这两个led的驱动都是一样的,但是他们的地址不同,那么我们就可以公用一个驱动程序,比如他们的驱动主设备号都是60,但是次设备号一个是1,一个是2,进去驱动之后,驱动根据1,2来
操作不同的地址来点灯。

open的时候文件名是怎么传递给内核的

寄存器

编译进内核重名吗?

每一个驱动程序最终的名字都是init_module,会重名吗?

不会,编译成ko文件与编译进内核不同,编译进内核的话,注册的宏定义会发生变化,builtin宏如下

第一层

1
#define module_init(x)	__initcall(x);

第二层

1
#define __initcall(fn) device_initcall(fn)

第三层

1
#define device_initcall(fn)		__define_initcall(fn, 6)

第四层

1
2
3
#define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn;

当hello_init为驱动函数名的手,最终得到

1
2
static initcall_t __initcall_hello_init6 __used \
__attribute__((__section__(".initcall6.init"))) = hello_init;

也就是说,当驱动程序编译进内核的时候,使用module_init(hello_init),最终定义了一个叫__initcall_hello_init6的结构体,它有一个属性:段属性(这些驱动程序的结构体都放在一起)
而结构体的值等于hello_init这个函数指针,而这个结构体的名字是不会重名的。