说明

内容来源于韦东山视频:入口出口细讲,字符设备调用关系实验

安装相关的头文件

在写驱动程序的时候,需要用到很多内核的方法,这些方法在相关的内核源码中,因此需要安装相关的头文件,比如:register_chrdev,就是一个内核提供的方法

1
major = register_chrdev(0,"led_drv",&led_fops);

在ubuntu中安装本操作系统内核版本的相关的内核头文件,命令如下:

1
2
apt-cache search linux-headers-$(uname -r) //确认有没有
sudo apt-get install linux-headers-$(uname -r)//下载安装

安装成功后:

1
2
3
4
5
6
7
8
book@100ask:~$ sudo apt-get install linux-headers-$(uname -r)
[sudo] password for book:
Reading package lists... Done
Building dependency tree
Reading state information... Done
linux-headers-5.4.0-91-generic is already the newest version (5.4.0-91.102~18.04.1).
linux-headers-5.4.0-91-generic set to manually installed.
0 upgraded, 0 newly installed, 0 to remove and 246 not upgraded.

安装成功后在/ls/modules里面会多出了相应的文件夹,里面装了相关的文件

1
2
book@100ask:~$ ls /lib/modules
4.18.0-15-generic 5.4.0-90-generic 5.4.0-91-generic

比如5.4.0.91这个内核版本,里面有这些文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
book@100ask:/lib/modules/5.4.0-91-generic$ ls -lA
total 5804
lrwxrwxrwx 1 root root 39 Nov 11 08:48 build -> /usr/src/linux-headers-5.4.0-91-generic
drwxr-xr-x 2 root root 4096 Nov 11 08:48 initrd
drwxr-xr-x 17 root root 4096 Dec 11 21:53 kernel
-rw-r--r-- 1 root root 1400153 Dec 11 21:53 modules.alias
-rw-r--r-- 1 root root 1376983 Dec 11 21:53 modules.alias.bin
-rw-r--r-- 1 root root 8105 Nov 11 08:48 modules.builtin
-rw-r--r-- 1 root root 10257 Dec 11 21:53 modules.builtin.bin
-rw-r--r-- 1 root root 63546 Nov 11 08:48 modules.builtin.modinfo
-rw-r--r-- 1 root root 610120 Dec 11 21:53 modules.dep
-rw-r--r-- 1 root root 852703 Dec 11 21:53 modules.dep.bin
-rw-r--r-- 1 root root 330 Dec 11 21:53 modules.devname
-rw-r--r-- 1 root root 219972 Nov 11 08:48 modules.order
-rw-r--r-- 1 root root 947 Dec 11 21:53 modules.softdep
-rw-r--r-- 1 root root 614790 Dec 11 21:53 modules.symbols
-rw-r--r-- 1 root root 747885 Dec 11 21:53 modules.symbols.bin
drwxr-xr-x 3 root root 4096 Dec 11 21:53 vdso

主要是里面的build文件夹,里面是很多头文件夹:

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
book@100ask:/lib/modules/5.4.0-91-generic/build$ ls -lA
total 2092
drwxr-xr-x 3 root root 4096 Dec 11 21:53 arch
lrwxrwxrwx 1 root root 39 Nov 11 08:48 block -> ../linux-hwe-5.4-headers-5.4.0-91/block
lrwxrwxrwx 1 root root 39 Nov 11 08:48 certs -> ../linux-hwe-5.4-headers-5.4.0-91/certs
-rw-r--r-- 1 root root 237642 Nov 11 08:48 .config
-rw-r--r-- 1 root root 237801 Nov 11 08:48 .config.old
lrwxrwxrwx 1 root root 40 Nov 11 08:48 crypto -> ../linux-hwe-5.4-headers-5.4.0-91/crypto
lrwxrwxrwx 1 root root 47 Nov 11 08:48 Documentation -> ../linux-hwe-5.4-headers-5.4.0-91/Documentation
lrwxrwxrwx 1 root root 41 Nov 11 08:48 drivers -> ../linux-hwe-5.4-headers-5.4.0-91/drivers
lrwxrwxrwx 1 root root 36 Nov 11 08:48 fs -> ../linux-hwe-5.4-headers-5.4.0-91/fs
-rw-r--r-- 1 root root 39 Nov 11 08:48 .gitignore
drwxr-xr-x 4 root root 4096 Dec 11 21:53 include
lrwxrwxrwx 1 root root 38 Nov 11 08:48 init -> ../linux-hwe-5.4-headers-5.4.0-91/init
lrwxrwxrwx 1 root root 37 Nov 11 08:48 ipc -> ../linux-hwe-5.4-headers-5.4.0-91/ipc
lrwxrwxrwx 1 root root 40 Nov 11 08:48 Kbuild -> ../linux-hwe-5.4-headers-5.4.0-91/Kbuild
lrwxrwxrwx 1 root root 41 Nov 11 08:48 Kconfig -> ../linux-hwe-5.4-headers-5.4.0-91/Kconfig
drwxr-xr-x 2 root root 4096 Dec 11 21:53 kernel
lrwxrwxrwx 1 root root 37 Nov 11 08:48 lib -> ../linux-hwe-5.4-headers-5.4.0-91/lib
lrwxrwxrwx 1 root root 42 Nov 11 08:48 Makefile -> ../linux-hwe-5.4-headers-5.4.0-91/Makefile
-rw-r--r-- 1 root root 1194 Nov 11 08:48 .missing-syscalls.d
lrwxrwxrwx 1 root root 36 Nov 11 08:48 mm -> ../linux-hwe-5.4-headers-5.4.0-91/mm
-rw-r--r-- 1 root root 1619694 Nov 11 08:48 Module.symvers
lrwxrwxrwx 1 root root 37 Nov 11 08:48 net -> ../linux-hwe-5.4-headers-5.4.0-91/net
lrwxrwxrwx 1 root root 41 Nov 11 08:48 samples -> ../linux-hwe-5.4-headers-5.4.0-91/samples
drwxr-xr-x 8 root root 12288 Dec 11 21:53 scripts
lrwxrwxrwx 1 root root 42 Nov 11 08:48 security -> ../linux-hwe-5.4-headers-5.4.0-91/security
lrwxrwxrwx 1 root root 39 Nov 11 08:48 sound -> ../linux-hwe-5.4-headers-5.4.0-91/sound
drwxr-xr-x 3 root root 4096 Dec 11 21:53 tools
lrwxrwxrwx 1 root root 40 Nov 11 08:48 ubuntu -> ../linux-hwe-5.4-headers-5.4.0-91/ubuntu
lrwxrwxrwx 1 root root 37 Nov 11 08:48 usr -> ../linux-hwe-5.4-headers-5.4.0-91/usr
lrwxrwxrwx 1 root root 38 Nov 11 08:48 virt -> ../linux-hwe-5.4-headers-5.4.0-91/virt

写一个空的字符设备驱动

只需要一个c文件,一个makefile文件,就可以编译出来ko文件(驱动的生成结果)了,c文件的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
#include <linux/module.h>
int hello_init(void)
{
return 0;
}
void hello_exit(void)
{

}
module_init(hello_init);//给init_module起的别名,也可以直接将入口参数改成init_module,而不使用这个宏
module_exit(hello_exit);//给cleanup_module起的别名,也可以直接将出口参数改成cleanup_module,而不使用这个宏
MODULE_LICENSE("GPL");//必须使用gpl协议

Makefile文件的内容如下:

1
2
3
4
5
6
7
8
KVERSION = $(shell uname -r)
KERN_DIR = /lib/modules/$(KVERSION)/build
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf moduls.order
obj-m += hello_drv.o

需要在ubunt中编译,在编译的时候吗,需要设置好相关的环境变量,在terminal中执行:

1
2
export CROSS_COMPILE=
export ARCH=x86_64

然后就可以make了:

1
2
3
4
5
6
7
8
9
book@100ask:~/mydir/hello_drv1$ make
make -C /lib/modules/5.4.0-91-generic/build M=`pwd` modules
make[1]: Entering directory '/usr/src/linux-headers-5.4.0-91-generic'
CC [M] /home/book/mydir/hello_drv1/hello_drv.o
Building modules, stage 2.
MODPOST 1 modules
CC [M] /home/book/mydir/hello_drv1/hello_drv.mod.o
LD [M] /home/book/mydir/hello_drv1/hello_drv.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.4.0-91-generic'

得到了如下的文件:

1
2
3
book@100ask:~/mydir/hello_drv1$ ls
hello_drv.c hello_drv.ko hello_drv.mod hello_drv.mod.c hello_drv.mod.o hello_drv.o Makefile modules.order Module.symvers

在驱动程序中增加打印信息

在驱动中增加打印信息,==c文件如下==

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <linux/module.h>
int hello_init(void)
{
printk("hello_init\r\n");
return 0;
}
void hello_exit(void)
{
printk("hello_exit\r\n");
}
module_init(hello_init);//给init_module起的别名,也可以直接将入口参数改成init_module,而不使用这个宏
module_exit(hello_exit);//给cleanup_module起的别名,也可以直接将出口参数改成cleanup_module,而不使用这个宏
MODULE_LICENSE("GPL");//必须使用gpl协

使用到了printk 方法,但是没有增加新的头文件,最终也编译成功,可见printk在module.h中已经被声明了,module.h中已经包含了相关的头文件,因此没有报错

==装载驱动==

装载驱动的时候,入口函数(init_module)被调用,任何一个驱动,他的入口函数都是,只不过具体来说会有别名

1
2
sudo insmod hello_drv.ko//这是在安装ko文件,会调用入口函数
dmesg//查看信息

相关小知识:
printk来打印信息,这些信息会先存入一个内核的buffer里面,然后再通过串口,屏幕等显示出来,这些信息一定会放进去buffer里面,这些信息,可以通过dmesg命令来查看,具体是通过串口还是屏幕之类的显示,都不一定会真正的使用到,都是可以配置的。但是一定会放到buffer里面。

可以看到,使用了dmesg查看buffer,最后是安装驱动的时候打印的字符串

1
2
3
[ 1849.152855] hello_drv: loading out-of-tree module taints kernel.
[ 1849.152880] hello_drv: module verification failed: signature and/or required key missing - tainting kernel
[ 1849.153622] hello_init

==卸载驱动==

卸载驱动的时候,出口函数(cleanup_module)被调用,任何一个驱动,他的出口函数都是init_module和cleanup_module,只不过具体来说会有不同的别名

1
2
sudo rmmod hello_drv
dmesg//查看信息

卸载驱动方法会被调用,打印信息也可以通过dmesg查看,如下所示:

1
2
3
4
[ 1849.152855] hello_drv: loading out-of-tree module taints kernel.
[ 1849.152880] hello_drv: module verification failed: signature and/or required key missing - tainting kernel
[ 1849.153622] hello_init
[ 2152.015126] hello_exit

查看内核源码自带的驱动

驱动都在这个文件夹里面(大概)

1
linux-5.9\drivers\

这里随便找一个看看是怎么写的,比如下面这个

1
linux-5.9\drivers\atm\atmtcp.c

文件的最下面是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
static __init int atmtcp_init(void)
{
register_atm_ioctl(&atmtcp_ioctl_ops);
return 0;
}
static void __exit atmtcp_exit(void)
{
deregister_atm_ioctl(&atmtcp_ioctl_ops);
}

MODULE_LICENSE("GPL");
module_init(atmtcp_init);
module_exit(atmtcp_exit);

可以看到格式都差不多,就是一个入口函数,一个出口函数,还有GPL声明,那么,==为什么要加上__init 和 __exit 宏呢?==

__init的宏定义如下

1
2
3
4
5
6
7
/* These are for everybody (although not all archs will actually
discard it in modules) */
#define __init __section(.init.text) __cold __latent_entropy __noinitretpoline
#define __initdata __section(.init.data)
#define __initconst __section(.init.rodata)
#define __exitdata __section(.exit.data)
#define __exit_call __used __section(.exitcall.exit)

可以看到,这个宏,有一个段属性,说明它放在这一个段里:.init.text,开始的时候用完之后可以把这一部分释放掉。因为入口函数只会用一次,这样使用__init来标记它的段属性,可以节省空间。

==为什么出口函数也要加上呢?==

出口函数是在整个驱动程序生存的最后才会被调用,那用了__exit也没意义啊,毕竟不能像入口函数一样,能够节省空间。确实是这样的,对于不是编进内核的情况确实是没用。但是,对于编进内核的驱动,永远都不会调用到出口函数,因此这段代码可以释放掉,这时候,标记了__exit的函数所在的段,根本不会被链接进内核,这样就节省了空间。

问题

  1. 安装头文件的时候,是只有头文件吗,还是所有的内核代码都有,还是其他情况呢?