字符设备驱动-3-GPIO驱动KEY示例

1 APP 读取按键方式#

  1. 查询方式
  2. 休眠-唤醒方式
  3. poll 方式
  4. 异步通知方式

第2、3、4种方法,都涉及中断服务程序。

1.1 查询方式#

image

APP 调用 open 时,导致驱动中对应的 open 函数被调用,在里面配置 GPIO 为输入引脚。 APP 调用 read 时,导致驱动中对应的 read 函数被调用,它读取寄存器,把引脚状态直接返回给 APP,APP需要反复read查询引脚电平状态。

很明显,查询方式用的非阻塞IO(O_NONBLOCK)。

1.2 休眠-唤醒方式#

image

  1. APP 调用 open 时,导致驱动中对应的 open 函数被调用,在里面配置GPIO 为输入引脚;并且注册 GPIO 的中断处理函数。
  2. APP 调用 read 时,导致驱动中对应的 read 函数被调用,如果有按键数据则直接返回给 APP;否则 APP 在内核态read函数中休眠。
  3. 当用户按下按键时, GPIO 中断被触发,导致驱动程序之前注册的中断服务程序被执行。它会记录按键数据,read函数被唤醒,执行驱动代码,把按键数据返回给APP(用户空间)。

1.3 poll 方式#

上面的休眠-唤醒方式有个缺点:如果一直没操作按键,那么 APP 就会永远休眠。
我们可以给 APP 定个闹钟,这就是 poll 方式。当超时后就直接返回不再休眠。
image

  1. APP 调用 open 时,导致驱动中对应的 open 函数被调用,在里面配置GPIO 为输入引脚;并且注册 GPIO 的中断处理函数。
  2. APP 调用 poll 或 select 函数,意图是“查询”是否有数据,这 2 个函数都可以指定一个超时时间,即在这段时间内没有数据的话就返回错误。这会导致驱动中对应的 drv_poll 函数被调用,如果有按键数据则直接返回给 APP;否则 APP 在内核态休眠一段时间。
  3. 当按下按键时, GPIO 中断被触发,导致驱动程序之前注册的中断服务程序被执行。它会记录按键数据,并唤醒休眠中的 APP。如果用户没按下按键,但是超时时间到了,内核也会唤醒 APP。

所以 APP 被唤醒有 2 种原因:用户操作了按键或者超时。被唤醒的 APP 在内核态继续运行,即继续执行驱动代码,把 “状态” 返回给 APP(用户空间)。APP 得到 poll/select 函数的返回结果后,如果确认是有数据的,则再调用 read 函数,这会导致驱动中的 read 函数被调用,这时驱动程序中含有数据,会直接返回数据。

1.4 异步通知方式#

image
异步通知的实现原理是:内核给 APP 发信号。信号有很多种,这里发的是SIGIO。
驱动程序中构造、注册一个 file_operations 结构体,里面提供有对应的open,read,fasync 函数。

  1. APP 调用 open 时,导致驱动中对应的 open 函数被调用,在里面配置GPIO 为输入引脚;并且注册 GPIO 的中断处理函数。
  2. APP 给信号 SIGIO 注册自己的处理函数: my_signal_fun
  3. APP 调用fcntl函数,把驱动程序的 flag 改为 FASYNC,这会导致驱动程序的fasync函数被调用,它只是简单记录进程 PID。
  4. 当用户按下按键时, GPIO 中断被触发,导致驱动程序之前注册的中断服务程序被执行。它会记录按键数据,然后给进程 PID 发送 SIGIO 信号。
  5. APP 收到信号后会被打断,先执行信号处理函数:在信号处理函数中可以去调用 read 函数读取按键值。
  6. 信号处理函数返回后, APP 会继续执行原先被打断的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void my_sig_func(int signo) {
printf("get a signal : %d\n", signo);
}
int main(int argc, char **argv) {
int i = 0;
signal(SIGIO, my_sig_func);
while (1) {
printf("Hello, world %d!\n", i++);
sleep(2);
}
return 0;
}

image-20240728184640793
image
发送一个SIGIO讯号给进程3581339 my_sig_func函数,可以看到打印”get a signal 29“,即为SIGIO信号。
image

1.5 总结4种读按键方式#

关于这几种方式的具体原理和示例后面会进行深度剖析:

[字符设备驱动-6.POLL底层驱动机制 - fuzidage - 博客园 (cnblogs.com)

字符设备驱动-7.异步通知 - fuzidage - 博客园 (cnblogs.com)

字符设备驱动-8.休眠唤醒机制 - fuzidage - 博客园 (cnblogs.com)

字符设备驱动-9.内核定时器 - fuzidage - 博客园 (cnblogs.com)

2 GPIO按键驱动#

2.1 按键驱动框架#

按键驱动程序最简单的方法:
image

我们的目的写出一个容易扩展到各种芯片、各种板子的按键驱动程序,所以驱动程序分为上下两层

  1. button_drv.c 分配/设置/注册 file_operations 结构体起承上启下的作用,向上提供 button_open,button_read 供 APP 调用。而这 2 个函数又会调用底层硬件提供的 p_button_opr 中的 init、 read 函数操作硬件。
  2. board_xxx.c 实现 p_button_opr结构体,这个结构体是我们自己抽象出来的,里面定义单板 xxx 的按键操作函数。这样的结构易于扩展,对于不同的单板,只需要替换board_xxx.c提供自己的 button_operations 结构体即可。
    image

2.1.1 button_operations 结构体#

button_drv.h
1
2
3
4
5
6
7
8
9
10
#ifndef _BUTTON_DRV_H
#define _BUTTON_DRV_H
struct button_operations {
int count;
void (*init) (int which);
int (*read) (int which);
};
void register_button_operations(struct button_operations *opr);
void unregister_button_operations(void);
#endif
board_xxx.c
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
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/sched.h>
#include <linux/slab.h>
#include <linux/fcntl.h>
#include <linux/fs.h>
#include <linux/signal.h>
#include <linux/mutex.h>
#include <linux/mm.h>
#include <linux/timer.h>
#include <linux/wait.h>
#include <linux/skbuff.h>
#include <linux/proc_fs.h>
#include <linux/poll.h>
#include <linux/capi.h>
#include <linux/kernelcapi.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/moduleparam.h>
#include "button_drv.h"
static void board_xxx_button_init_gpio (int which){
printk("%s %s %d, init gpio for button %d\n", __FILE__, __FUNCTION__, __LINE__, which);
}
static int board_xxx_button_read_gpio (int which){
printk("%s %s %d, read gpio for button %d\n", __FILE__, __FUNCTION__, __LINE__, which);
return 1;
}
static struct button_operations my_buttons_ops ={
.count = 2,
.init = board_xxx_button_init_gpio,
.read = board_xxx_button_read_gpio,
};
int board_xxx_button_init(void){
register_button_operations(&my_buttons_ops);
return 0;
}
void board_xxx_button_exit(void){
unregister_button_operations();
}
module_init(board_xxx_button_init);
module_exit(board_xxx_button_exit);
MODULE_LICENSE("GPL");
board_xxx.c里面实现了具体单板的button_operations,当insmod这个驱动时,调用 register_button_operations 函数,把my_buttons_ops这个结构体注册到上层驱动中,这里.init .read函数先不去实现。

2.1.2 file_operations 结构体#

button_drv.c
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
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/sched.h>
#include <linux/slab.h>
#include <linux/fcntl.h>
#include <linux/fs.h>
#include <linux/signal.h>
#include <linux/mutex.h>
#include <linux/mm.h>
#include <linux/timer.h>
#include <linux/wait.h>
#include <linux/skbuff.h>
#include <linux/proc_fs.h>
#include <linux/poll.h>
#include <linux/capi.h>
#include <linux/kernelcapi.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/moduleparam.h>
#include "button_drv.h"

static int major = 0;
static struct button_operations *p_button_opr;
static struct class *button_class;
static int button_open (struct inode *inode, struct file *file)
{
int minor = iminor(inode);
p_button_opr->init(minor);
return 0;
}
static ssize_t button_read (struct file *file, char __user *buf, size_t size, loff_t *off)
{
unsigned int minor = iminor(file_inode(file));
char level;
int err;
level = p_button_opr->read(minor);
err = copy_to_user(buf, &level, 1);
return 1;
}
static struct file_operations button_fops = {
.open = button_open,
.read = button_read,
};
void register_button_operations(struct button_operations *opr)
{
int i;
p_button_opr = opr;
for (i = 0; i < opr->count; i++)
{
device_create(button_class, NULL, MKDEV(major, i), NULL, "100ask_button%d", i);
}
}
void unregister_button_operations(void)
{
int i;
for (i = 0; i < p_button_opr->count; i++)
{
device_destroy(button_class, MKDEV(major, i));
}
}

EXPORT_SYMBOL(register_button_operations);
EXPORT_SYMBOL(unregister_button_operations);

int button_init(void)
{
major = register_chrdev(0, "100ask_button", &button_fops);
button_class = class_create(THIS_MODULE, "100ask_button");
if (IS_ERR(button_class))
return -1;
return 0;
}
void button_exit(void)
{
class_destroy(button_class);
unregister_chrdev(major, "100ask_button");
}
module_init(button_init);
module_exit(button_exit);
MODULE_LICENSE("GPL");
上层是 `button_drv.c`,按照字符设备驱动标准框架编写,`register_button_operations`实现了将底层具体的`button_operations`对象注册进来,调用`open/read`时便可操作具体的单板,`device_create`为单板的具体按键创建设备节点。注意`insmod`顺序要先安装`button_drv.ko`, 具体单板驱动要后安装,否则`register_button_operations`函数是未定义的。

2.2 具体单板按键驱动#

imx6ull单板为例,按键引脚为 GPIO5_IO01GPIO4_IO14,平时按键电平为高,按下按键后电平为低,如下图:
image

  1. 使能电源/时钟控制器
  2. 配置引脚模式成gpio
  3. 配置引脚方向为输入
  4. 读取电平
    image

2.2.1 CCM时钟使能#

image
image
设置 CCM_CCGR1[31:30] CCM_CCGR3[13:12]就可以使能 GPIO5GPIO4,设置为什么值呢?
image

1
2
3
4
00:该 GPIO 模块全程被关闭
01:该 GPIO 模块在 CPU run mode 情况下是使能的;在 WAIT 或 STOP 模式下,关闭
10:保留
11:该 GPIO 模块全程使能

那么将CCM_CCGR1[31:30]CCM_CCGR3[13:12]设置成0b11即可。

2.2.2 配成GPIO 模式#

  1. GPIO5_IO01 pinmux 成 GPIO:
    image
    1. GPIO4_IO14 pinmux 成 GPIO:
      image

2.2.3 GPIO配成输入#

GPIO4,GPIO5寄存器地址:
image
方向设置寄存器:(offset 04)
image

2.2.4 读取gpio电平#

注意输入模式下,gpio电平状态得从GPIOx_PSR得到(offset 08
image

button_board_imx6ull.c
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
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/io.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <asm/io.h>
#include "button_drv.h"
struct imx6ull_gpio {
volatile unsigned int dr;
volatile unsigned int gdir;
volatile unsigned int psr;
volatile unsigned int icr1;
volatile unsigned int icr2;
volatile unsigned int imr;
volatile unsigned int isr;
volatile unsigned int edge_sel;
};
static volatile unsigned int *CCM_CCGR3;
static volatile unsigned int *CCM_CCGR1;

/* set GPIO5_IO03 as GPIO */
static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1;
/* set GPIO4_IO14 as GPIO */
static volatile unsigned int *IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B;

static struct imx6ull_gpio *gpio4;
static struct imx6ull_gpio *gpio5;
static void board_imx6ull_button_init (int which) /* 初始化button, which-哪个button */
{
if (!CCM_CCGR1){
CCM_CCGR1 = ioremap(0x20C406C, 4);
CCM_CCGR3 = ioremap(0x20C4074, 4);
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1 = ioremap(0x229000C, 4);
IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B = ioremap(0x20E01B0, 4);
gpio4 = ioremap(0x020A8000, sizeof(struct imx6ull_gpio));
gpio5 = ioremap(0x20AC000, sizeof(struct imx6ull_gpio));
}
if (which == 0){
/* 1. enable GPIO5
* CG15, b[31:30] = 0b11
*/
*CCM_CCGR1 |= (3<<30);
/* 2. set GPIO5_IO01 as GPIO
* MUX_MODE, b[3:0] = 0b101
*/
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1 = 5;
/* 3. set GPIO5_IO01 as input
* GPIO5 GDIR, b[1] = 0b0
*/
gpio5->gdir &= ~(1<<1);
} else if(which == 1){
/* 1. enable GPIO4
* CG6, b[13:12] = 0b11
*/
*CCM_CCGR3 |= (3<<12);
/* 2. set GPIO4_IO14 as GPIO
* MUX_MODE, b[3:0] = 0b101
*/
*IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B = 5;
/* 3. set GPIO4_IO14 as input
* GPIO4 GDIR, b[14] = 0b0
*/
gpio4->gdir &= ~(1<<14);
}
}
static int board_imx6ull_button_read (int which) /* 读button, which-哪个 */
{
if (which == 0)
return (gpio5->psr & (1<<1)) ? 1 : 0;
else
return (gpio4->psr & (1<<14)) ? 1 : 0;
}
static struct button_operations my_buttons_ops = {
.count = 2,
.init = board_imx6ull_button_init,
.read = board_imx6ull_button_read,
};
int board_imx6ull_button_drv_init(void){
register_button_operations(&my_buttons_ops);
return 0;
}
void board_imx6ull_button_drv_exit(void){
unregister_button_operations();
}
module_init(board_imx6ull_button_drv_init);
module_exit(board_imx6ull_button_drv_exit);
MODULE_LICENSE("GPL");

具体单板驱动insmod会调用 register_button_operations把具体的my_buttons_ops注册进去。当用户open,就会进行board_imx6ull_button_init进行按键寄存器配置。当用户read的时候调用board_imx6ull_button_read读取按键值。

image

3 测试#

Makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH, 比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH, 比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
# 请参考各开发板的高级用户使用手册
KERN_DIR = /home/book/100ask_roc-rk3399-pc/linux-4.4
all:
make -C $(KERN_DIR) M=`pwd` modules
$(CROSS_COMPILE)gcc -o button_test button_test.c
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
rm -f ledtest
# 参考内核源码drivers/char/ipmi/Makefile
# 要想把a.c, b.c编译成ab.ko, 可以这样指定:
# ab-y := a.o b.o
# obj-m += ab.o
obj-m += button_drv.o
obj-m += board_xxx.o
测试代码
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
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
/*
* ./button_test /dev/100ask_button0
*/
int main(int argc, char **argv) {
int fd;
char val;
/* 1. 判断参数 */
if (argc != 2)
{
printf("Usage: %s <dev>\n", argv[0]);
return -1;
}
/* 2. 打开文件 */
fd = open(argv[1], O_RDWR);
if (fd == -1)
{
printf("can not open file %s\n", argv[1]);
return -1;
}
/* 3. 写文件 */
read(fd, &val, 1);
printf("get button : %d\n", val);
close(fd);
return 0;
}

image

字符设备驱动-2-总线模型和平台设备驱动

1 总线设备驱动模型#

img

设备定义资源,platform_device结构体

驱动定义platform_driver结构体,实现probe, file_operations

总线驱动模型优点:

驱动只是一套控制驱动框架,基本不用修改,和单板硬件相关的都在设备代码里面,硬件修改只需要修改设备资源相关的代码,不用关心具体的函数实现和寄存器控制。

1.1 总线/平台设备/平台驱动描述#

1.1.1 struct bus_type#

img

有一个很关键的函数,match函数。当设备与设备枚举过程中match函数会按照规则进行匹配,规则见1.3.1

1.1.2 struct platform_driver#

img

1.1.3 struct platform_device#

img

1.1.4 总线/设备/驱动三者关系#

  1. 系统启动后,会调用buses_init()函数创建/sys/bus文件目录。

  2. 接下来就是通过总线注册函数bus_register()进行总线注册,注册完成后,在/sys/bus目录下生成device文件夹和driver文件夹。

  3. 最后分别通过device_register()driver_register()函数注册对应的设备和驱动。
    image-20240727214421349

1.2 platform_device的注册过程#

image-20240727164939263

image-20240726004737362

  1. 系统初始化时,调用platform_add_devices函数,把所有放置在板级platform_device数组中的platform_device注册到系统中去。
  • 1.1 此函数循环调用platform_device_register函数,来注册每个platform_device

  • 1.2 而platform_device_register中会调用platform_device_add函数。

  1. platform_device全部注册到系统之后,便可以通过platform的操作接口,来获取platform_device中的resource资源。
  • 2.1 比如地址、中断号等,以进行request_mem_regionioremap(将resource分配的物理地址映射到kernel的虚拟空间来)和request_irq操作。

  • 2.2 platform的操作接口包括platform_get_irqplatform_get_irq_bynameplatform_get_resourceplatform_get_resource_byname等。这个后面设备树专题会专门介绍字符设备驱动-5.设备树函数 - fuzidage - 博客园 (cnblogs.com)

1.3 platform_driver的注册过程#

insmod设备驱动的时候会透过module_init调用, 过程如下:

1
2
3
4
5
6
7
8
9
10
platform_driver_register()
driver_register()
bus_add_driver//加入driver链表
driver_attach()
bus_for_each_dev()
__driver_attach()
driver_match_device//drvier匹配device成功调用driver的probe
platform_match
driver_probe_device//drvier匹配device成功调用driver的probe
drv->probe(dev);

1.3.1 device和driver的匹配过程#

image-20240727170404969

1.3.1.1 device和driver的匹配优先级顺序#

一个驱动是可以匹配多个设备的,平台总线中的驱动要具有三种匹配信息的能力,基于这种需求,platform_driver中使用不同的成员来进行相应的匹配。系统为platform总线定义了一个bus_type 的实例platform_bus_type, 会不断循环呼叫platform_match函数去遍历所有设备和驱动:

img

匹配优先级顺序实现位于drivers/base/platform.c的platform_match函数,下面按照优先级由高到底的匹配顺序介绍:

1.3.1.1.1 of_match_table#

of_match_table就是从dts中对应node的compatible属性去匹配设备和驱动。

compatible属性也叫做“兼容性”属性,是一个字符串列表, 格式如下所示:

1
“manufacturer,model”

manufacturrer表示厂商,model表示驱动名字,该属性用于将设备和驱动绑定起来。

platform_device.dev.of_node platform_driver.driver.of_match_table介绍:

由设备树节点转换得来的 platform_device 中,含有一个结构体:of_node。 它的类型如下:

img

platform_driver.driver.of_match_table 类型如下:

img

一般驱动程序都会有一个of_match_table匹配表,此of_match_table匹配表保存着一些compatible值,如果dts中的compatible属性值和of_match_table匹配表中的有一个值相等,那么就表示设备可以使用这个驱动。

如下图dts定义了一个mipi_rx: cif节点:

img

驱动程序中的定义如下:

img

img

那么这里驱动程序中的.of_match_table和dts能够匹配,那么就说明match成功,匹配成功后调用platform driverprobe函数。一般在驱动程序module int的时候,也就是insmod的时候,会用platform_driver_register来进行match过程。

1.3.1.1.2 ID table#

下面这个例子就是用一个驱动来匹配两个分别叫"demo0""demo1"的设备,注意,数组最后的{}是一定要的,这个是内核判断数组已经结束的标志。

1
2
3
4
5
6
static struct platform_device_id tbl[] = {
{"demo0"},
{"demo1"},
{},
};
MODULE_DEVICE_TABLE(platform, tbl);

image-20240727174003803

1.3.1.1.3 name#

假如前面两项匹配规则都不满足,那么最后可以是用name来匹配。例如:上面的mipi_rx: cif节点:

image-20240727174511089

转换后就对应一个platform_deviceplatform_device.name= “cif”,利用名字也能匹配上。

1.3.2 匹配成功后执行probe#

image-20240727171618120

drvier匹配device成功调用driver的probe函数。一般平台设备都不需要驱动代码去定义,而是直接放入设备树作为设备树节点,内核启动后遍历设备树节点,将其转换成platform_device

1.3.3 总结下platform_driver的注册执行过程#

image-20240727170620921

1.4 总结平台设备/平台驱动的注册过程#

img

1.6 平台设备/平台驱动相关API#

1.6.1 注册/反注册#

1
2
3
4
5
6
//\include\linux\platform_device.h
//
 platform_device_register/ platform_device_unregister
 platform_driver_register/ platform_driver_unregister

 platform_add_devices // 注册多个 device

image-20240727175522278

1.6.2 资源获取释放#

1.6.2.1 IO resource#

1
struct resource *platform_get_resource(struct platform_device *, unsigned int, unsigned int);

kernel\include\linux\ioport.h中有resource结构。用来描述hw设备的资源信息, include\linux\ioport.h:

img

flags一般有以下几种:比如中断资源, IO端口资源, IO内存资源, DMA资源

img

  • IORESOURCE_IO:表示IO资源,cpu需要用特殊指令才能访问或需要用特殊访问方式才能访问,不能直接用指针来寻址
  • IORESOURCE_MEM:表示IO内存,可以直接用指针引用来直接寻址操作

这里举个例子:

img

img

打印如下:那这里的pdev对应dts中的mipi_rx节点。platform_get_resource可以从dts node中找到io内存资源。

那这里循环获取4次,如下所示地址范围和上面的dts节点一致:

img

1.6.2.2 IRQ#

img

img

  • IORESOURCE_IRQ: 中断irq资源

img

中断触发类型:

1
2
3
4
5
//include\linux\platform_device.h
#define IRQ_TYPE_EDGE_RISING 1 //上升沿触发
#define IRQ_TYPE_EDGE_FALLING 2 //下降沿触发
#define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING) // 双边沿触发
#define IRQ_TYPE_LEVEL_HIGH 4 //电平触发-高电平

这里又要引入新概念GIC: 设备驱动-10.中断子系统-3.中断设备树表述与解析 - fuzidage - 博客园 (cnblogs.com)

设备驱动-10.中断子系统-5 armv7 GIC架构解析 - fuzidage - 博客园 (cnblogs.com)

Dts中描述到了GIC相关基础知识。(Generic Interrupt Controller)是ARM公司提供的一个通用的中断控制器。

GIC 3要素

    • 中断类型
    • 中断号
    • 中断触发方式这三个要素
  1. GIC的外设中断(除去SGI)类型有两类:
    • SPI,共享外设中断(由GIC内部的distributor来分发到相关CPU),中断号:32~1019
    • PPI,私有外设中断(指定CPU接收),中断号:16~31
  1. 外设中断号的分配规则如下:
    • 32~1019给SPI
    • 16~31给PPI
  1. 所有外设中断都支持四种触发方式:
    • 上升沿触发
    • 下降沿触发
    • 高电平触发
    • 低电平触发

所以DTS中接在GIC的device nodeinterrupts属性也是用这三个要素来描述一个具体的中断。

格式如:interrupts = <interruptType interruptNumber triggerType>

Interrrupt Types Interrrupt Number Trigger Type
0 = SPI, 1 = PPI 32… …1019 1 = low to high, 2 = high to low, 4 = high level, 8 = low level

sample code如下:

img

img

打印结果如下:

img

那么最后dts解析的结果为:

1
2
3
4
5
6
7
8
9
out_irq->np = interrupt-parent = gic node
out_irq->args[0] = GIC_SPI;
out_irq->args[1] = 硬件中断号 = 155
out_irq->args[2] = 中断触发类型 = IRQ_TYPE_LEVEL_HIGH

out_irq->np = interrupt-parent = gic node
out_irq->args[0] = GIC_SPI;
out_irq->args[1] = 硬件中断号 = 156
out_irq->args[2] = 中断触发类型 = IRQ_TYPE_LEVEL_HIGH

platform_get_irq返回一个虚拟中断号,这里对应的是27, 28.

devm_request_irq用来申请中断,分配isr中断处理函数。该函数可以在驱动卸载时不用主动调用free_irq显示释放中断请求。

可以看到两次call devm_request_irq却是用的同一个中断服务程序cif_isr,这也是允许的,我们看一下函数原型:

image-20240727182250194

devm_request_irq会建立中断号irq_num和中断服务程序isr的绑定,最后一个参数会传给中断服务程序isr.

中断服务程序isr能够根据中断号irq_num和传进的参数进行区分中断源。

1.6.2.3 GPIO#

of_get_named_gpio_flags获取dts中gpio 编号,并且会找到device_node,找到of_gpio_flags

gpio_request申请gpio

gpio_direction_output设置成outputset gpio val

1
2
3
4
5
6
int of_get_named_gpio_flags(struct device_node *np, const char *list_name,
int index, enum of_gpio_flags *flags);
int gpio_request(unsigned gpio, const char *label);
static inline int gpio_direction_output(unsigned gpio, int value);
//include\asm-generic\gpio.h
//include\linux\gpio.h

image-20240727183103383

举个例子:

img

1
2
3
#define GPIO_ACTIVE_HIGH 0
#define GPIO_ACTIVE_LOW 1
//include\linux\gpio\machine.h

img

img

img

img

这里的gpio 编号=411 = GPIO_D + offset = 404 + 7 =411(也就是dts中配置的portd 7),这里由于是of_gpio_flags OF_GPIO_ACTIVE_LOW =0x01,所以snsr_rst_pol = 1.

2 平台设备驱动示例#

1
2
3
4
5
6
7
8
#ifndef _LED_OPR_H
#define _LED_OPR_H
struct led_operations {
int (*init) (int which); /* 初始化LED, which-哪个LED */
int (*ctl) (int which, char status); /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
};
struct led_operations *get_board_led_opr(void);
#endif

①分配/设置/注册platform_device结构体 在里面定义所用资源,指定设备名字。-Board_A_led.c

②分配/设置/注册 platform_driver 结构体 在其中的 probe 函数里,分配/设置/注册 file_operations 结构体, 并从 platform_device 中确实所用硬件资源。 指定 platform_driver 的名字。 -Chip_demo_gpio.c

2.1 通用字符设备驱动框架#

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
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include "led_opr.h"
*/
static int major = 0;
static struct class *led_class;
struct led_operations *p_led_opr;

void led_class_create_device(int minor) {
device_create(led_class, NULL, MKDEV(major, minor), NULL, "100ask_led%d", minor); /* /dev/100ask_led0,1,... */
}
void led_class_destroy_device(int minor) {
device_destroy(led_class, MKDEV(major, minor));
}
void register_led_operations(struct led_operations *opr) {
p_led_opr = opr;
}
EXPORT_SYMBOL(led_class_create_device);
EXPORT_SYMBOL(led_class_destroy_device);
EXPORT_SYMBOL(register_led_operations);

static ssize_t led_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset) {
return 0;
}
/* write(fd, &val, 1); */
static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset){
int err;
char status;
struct inode *inode = file_inode(file);
int minor = iminor(inode);
err = copy_from_user(&status, buf, 1);
/* 根据次设备号和status控制LED */
p_led_opr->ctl(minor, status);
return 1;
}

static int led_drv_open (struct inode *node, struct file *file){
int minor = iminor(node);
/* 根据次设备号初始化LED */
p_led_opr->init(minor);

return 0;
}
static int led_drv_close (struct inode *node, struct file *file){
return 0;
}
static struct file_operations led_drv = {
.owner = THIS_MODULE,
.open = led_drv_open,
.read = led_drv_read,
.write = led_drv_write,
.release = led_drv_close,
};

static int __init led_init(void){
int err;
major = register_chrdev(0, "100ask_led", &led_drv);
led_class = class_create(THIS_MODULE, "100ask_led_class");
err = PTR_ERR(led_class);
if (IS_ERR(led_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "led");
return -1;
}
return 0;
}
static void __exit led_exit(void) {
class_destroy(led_class);
unregister_chrdev(major, "100ask_led");
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

这里只实现一个框架, 具体的opr操作函数需要具体单板去实现。先注册字符设备驱动,确定好class和主设备号。

因为暂时还不知道具体led驱动是啥,因此需要外部去注册具体的led驱动,交给platform_driver去建立。

暂时先不建立设备节点,设备节点交给platform_device去建立,因为暂时不知道设备的led资源信息。

EXPORT_SYMBOL导出led_class_create_deviceled_class_destroy_deviceregister_led_operations函数。

2.2 具体单板资源描述驱动(platform_device暂不使用dts)#

1
2
3
4
5
6
7
8
9
#ifndef _LED_RESOURCE_H
#define _LED_RESOURCE_H
/* GPIO3_0 */
/* bit[31:16] = group */
/* bit[15:0] = which pin */
#define GROUP(x) (x>>16)
#define PIN(x) (x&0xFFFF)
#define GROUP_PIN(g,p) ((g<<16) | (p))
#endif

Board_A_led.c这里实现了单板的资源定义,这里是gpio3_1,gpio5_8

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
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/platform_device.h>
#include "led_resource.h"

static void led_dev_release(struct device *dev)
{
}
static struct resource resources[] = {
{
.start = GROUP_PIN(3,1),
.flags = IORESOURCE_IRQ,
.name = "100ask_led_pin",
},
{
.start = GROUP_PIN(5,8),
.flags = IORESOURCE_IRQ,
.name = "100ask_led_pin",
},
};
static struct platform_device board_A_led_dev = {
.name = "100ask_led",
.num_resources = ARRAY_SIZE(resources),
.resource = resources,
.dev = {
.release = led_dev_release,
},
};

static int __init led_dev_init(void){
int err;
err = platform_device_register(&board_A_led_dev);
return 0;
}
static void __exit led_dev_exit(void){
platform_device_unregister(&board_A_led_dev);
}
module_init(led_dev_init);
module_exit(led_dev_exit);
MODULE_LICENSE("GPL"); 

注意:

如果platform_device中不提供 release 函数,如下图所示不提供红框部分的函数:

img

则在调用 platform_device_unregister 时会出现警告,如下图所示, 因此我们可以将release实现为空。

img

2.3 具体芯片驱动(platform_driver)#

1
2
3
4
5
6
7
#ifndef _LEDDRV_H
#define _LEDDRV_H
#include "led_opr.h"
void led_class_create_device(int minor);
void led_class_destroy_device(int minor);
void register_led_operations(struct led_operations *opr);
#endif /* _LEDDRV_H */

Chip_demo_gpio.c实现opr的gpio控制。

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
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/platform_device.h>
#include "led_opr.h"
#include "leddrv.h"
#include "led_resource.h"

static int g_ledpins[100];
static int g_ledcnt = 0;

static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */
{
printk("init gpio: group %d, pin %d\n", GROUP(g_ledpins[which]), PIN(g_ledpins[which]));
switch(GROUP(g_ledpins[which])){
case 0:{
printk("init pin of group 0 ...\n");
break;
}
case 1:{
printk("init pin of group 1 ...\n");
break;
}
case 2:{
printk("init pin of group 2 ...\n");
break;
}
case 3:{
printk("init pin of group 3 ...\n");
break;
}
}
return 0;
}
static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
{
printk("set led %s: group %d, pin %d\n", status ? "on" : "off", GROUP(g_ledpins[which]), PIN(g_ledpins[which]));
switch(GROUP(g_ledpins[which])){
case 0:{
printk("set pin of group 0 ...\n");
break;
}
case 1:{
printk("set pin of group 1 ...\n");
break;
}
case 2:{
printk("set pin of group 2 ...\n");
break;
}
case 3:{
printk("set pin of group 3 ...\n");
break;
}
}
return 0;
}
static struct led_operations board_demo_led_opr = {
.init = board_demo_led_init,
.ctl = board_demo_led_ctl,
};

static int chip_demo_gpio_probe(struct platform_device *pdev){
struct resource *res;
int i = 0;

while (1){
res = platform_get_resource(pdev, IORESOURCE_IRQ, i++);
if (!res)
break;

g_ledpins[g_ledcnt] = res->start;//获取gpio num
led_class_create_device(g_ledcnt);//利用EXPORT_SYMBOL导出的函数为每个led创建设备节点
g_ledcnt++;
}
return 0;

}
static int chip_demo_gpio_remove(struct platform_device *pdev) {
struct resource *res;
int i = 0;

while (1) {
res = platform_get_resource(pdev, IORESOURCE_IRQ, i);
if (!res)
break;
led_class_destroy_device(i);
i++;
g_ledcnt--;
}
return 0;
}
static struct platform_driver chip_demo_gpio_driver = {
.probe = chip_demo_gpio_probe,
.remove = chip_demo_gpio_remove,
.driver = {
.name = "100ask_led",
},
};

static int __init chip_demo_gpio_drv_init(void) {
int err;
err = platform_driver_register(&chip_demo_gpio_driver);
register_led_operations(&board_demo_led_opr);
return 0;
}
static void __exit lchip_demo_gpio_drv_exit(void){
platform_driver_unregister(&chip_demo_gpio_driver);
}
module_init(chip_demo_gpio_drv_init);
module_exit(lchip_demo_gpio_drv_exit);
MODULE_LICENSE("GPL");

platform_deviceplatform_driverinsmod(注册)后,总线设备驱动模型会进行match匹配,匹配成功调用probe函数,这里使用name进行匹配的。

  1. chip_demo_gpio_probe中, 获取单板定义的资源信息,依次创建设备节点。
  2. register_led_operations注册了具体chip的opr操作函数(寄存器操作不具体展开实现,opr暂定为空)。

当用户调用open/write时便可操作具体chip的led驱动方法。

2.4 测试#

2.4.1 Makefile#

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
# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH, 比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH, 比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
# 请参考各开发板的高级用户使用手册

KERN_DIR = /home/book/100ask_roc-rk3399-pc/linux-4.4

all:
make -C $(KERN_DIR) M=`pwd` modules
$(CROSS_COMPILE)gcc -o ledtest ledtest.c

clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
rm -f ledtest

# 参考内核源码drivers/char/ipmi/Makefile
# 要想把a.c, b.c编译成ab.ko, 可以这样指定:
# ab-y := a.o b.o
# obj-m += ab.o

obj-m += leddrv.o chip_demo_gpio.o board_A_led.o

编译出3个ko,依次insmod leddrv.ko chip_demo_gpio.ko board_A_led.ko

2.4.2 ledtest测试程序#

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
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
/*
* ./ledtest /dev/100ask_led0 on
* ./ledtest /dev/100ask_led0 off
*/
int main(int argc, char **argv) {
int fd;
char status;
if (argc != 3) {
printf("Usage: %s <dev> <on | off>\n", argv[0]);
return -1;
}
fd = open(argv[1], O_RDWR);
if (fd == -1){
printf("can not open file %s\n", argv[1]);
return -1;
}
if (0 == strcmp(argv[2], "on")){
status = 1;
write(fd, &status, 1);
}else{
status = 0;
write(fd, &status, 1);
}
close(fd);
return 0;
}

img

2.5 IS_ERR/ERR_PTR/PTR_ERR/NULL_PTR函数族#

include\linux\err.h:

image-20240727204217358

内核中的函数常常返回指针,如果出错,也希望能够通过返回的指针体现出来。那么有三种情况:合法指针,NULL指针和非法指针

1.合法指针:内核函数返回的指针一般是4K对齐,即 ptr & 0xfff == 0,也就是0x1000的倍数。其中Linux采用分页机制管理内存,而CPU访问的是线性地址需要通过页表转化成物理地址。所以内核就约定留出最后一页4k(0xfffffffffffff000 ~ 0xffffffffffffffff)用来记录内核空间的错误指针(32位的话就是(0xfffff000 ~ 0xffffffff)).

2.非法指针:一般内核函数地址不会落在(0xfffff000,0xffffffff)之间,而一般内核的出错代码也是一个小负数,在-4095到0之间,转变成unsigned long,正好在(0xfffff000,0xffffffff)之间。因此可以用 (unsigned long)ptr > (unsigned long)-1000L

-1000L正好是0xfffff000

3.linux内核中有一个宏MAX_ERRNO = 4095。errno见如下:

1
2
3
include/asm-generic/errno-base.h  //1-34
include\uapi\asm-generic\errno.h //35-133
include\linux\errno.h //512-530

2.5.1 IS_ERR#

作用:判断是否无效非法指针。

实现见上面图片,例如一个地址0xfffff,ffaa,那么代入后:很明显返回1,是一个错误非法指针。

1
0xfffffffaa > 0xfffff000

2.5.2 IS_ERR_OR_NULL#

和IS_ERR基本等同,会先提前判断一下是否空。

2.5.3 PTR_ERR#

作用:将非法指针转成错误码返回。

实现见上面图片. 将传入的void *类型指针强转为long类型,并返回

2.5.4 ERR_PTR#

将传入的long类型强转为void *类型指针,并返回

3 引入sysfs#

讲到总线设备驱动模型,那不能少了sysfs。sysfs是一种虚拟文件系统,旨在提供一种访问内核数据结构的方法,从而允许用户空间程序查看和控制系统的设备和资源。

例如设备驱动-16-Linux 内核LED子系统 - fuzidage - 博客园 (cnblogs.com) 操作led:

echo 1 > /sys/class/leds/red/brightness

又例如某个驱动修改设置module_param:

echo "8" >/sys/module/my_drv/parameters/lg_lv

image-20240727220331721

kobjectkset 是构成 /sys 目录下的目录节点和文件节点的核心,也是层次化组织总线、设备、驱动的核心数据结构,kobject、kset 数据结构都能表示一个目录或者文件节点。在这个目录下面的每一个子目录,其实都是相同类型的kobject集合。然后不同的kset组织成树状层次的结构,就构成了sysfs子系统。

image-20240727220824743

字符设备驱动-1-GPIO驱动LED示例

GPIO: General-purpose input/output,通用输入输出接口。下面以IMX6ULL芯片的GPIO寄存器来展开介绍。

1 GPIO 寄存器的 2 种操作方法#

  1. 直接读写:读出、修改对应位、写入。
    1
    2
    3
    4
    5
    6
    7
    8
    a) 要设置 bit n:
      val = data_reg;
      val = val | (1<<n);
      data_reg = val;
    b) 要清除 bit n:
      val = data_reg;
      val = val & ~(1<<n);
      data_reg = val;
  2. set-and-clear protocol:(芯片不一定支持)

  set_reg, clr_reg, data_reg 三个寄存器对应的是同一个物理寄存器:

  a) 要设置 bit n:set_reg = (1<<n);

  b) 要清除 bit n:clr_reg = (1<<n);

2 GPIO 寄存器配置流程#

2.1 CCM时钟设置#

CCM寄存器为GPIO 模块提供时钟:

img

以IMX6ULL 芯片为列,GPIOn 要用 CCM_CCGRx 寄存器中的 2 位来决定该组 GPIO 是否使能。将对应的clk gating enable

img

1
2
3
4
00:该 GPIO 模块全程被关闭
01:该 GPIO 模块在 CPU run mode 情况下是使能的;在 WAIT 或 STOP 模式下,关闭
10:保留
11:该 GPIO 模块全程使能

例如:用CCM_CCGR0[bit31:30]使能GPIO2 的时钟:

img

例如:用CCM_CCGR1[bit31:30]使能GPIO5 的时钟:

例如:用CCM_CCGR1[bit27:26]使能GPIO1 的时钟:

img

例如:用CCM_CCGR2[bit27:26]使能GPIO3的时钟:

img

例如:用CCM_CCGR3[bit13:12]使能GPIO4的时钟:

img

2.2 引脚模式电器属性设置#

img

MUX seting用来配置pin的模式,比如GPIO。Pad setting用来设置GPIO的电器属性,比如电平,上下拉情况。

对于某个/某组引脚,IOMUXC 中有 2 个寄存器用来设置它:

2.2.1 IOMUX功能#

 a) `IOMUXC_SW_MUX_CTL_PAD_ <PAD_NAME>`:`Mux pad xxx`,选择某个引脚的功能

 b) IOMUXC_SW_MUX_CTL_GRP_<GROUP_NAME>Mux grp xxx,选择某组引脚的功能

img

某个引脚,或是某组预设的引脚,都有 8 个可选的模式(alternate (ALT) MUX_MODE),设成ALT5表示选择GPIO。

img

2.2.2 电器属性功能#

a) IOMUXC_SW_PAD_CTL_PAD_<PAD_NAME>:pad pad xxx,设置某个引脚的电器属性

b) IOMUXC_SW_PAD_CTL_GRP_<GROUP_NAME>pad grp xxx,设 置某组引脚的电器属性

img

pad参数有很多不只是上下拉,还有很多属性如IO驱动能力。

img

2.2.2.1 GPIO驱动LED的4种方式#

img

① 使用引脚输出 3.3V 点亮 LED,输出 0V 熄灭 LED。

② 使用引脚拉低到 0V 点亮 LED,输出 3.3V 熄灭 LED。

③有的芯片为了省电等原因,其引脚驱动能力不足,这时可以使用三极管驱动。 使用引脚输出 1.2V 点亮 LED,输出 0V 熄灭 LED。

④使用引脚输出 0V 点亮 LED,输出 1.2V 熄灭 LED

2.2.3 GPIO方向#

当iomux成gpio模式后,就需要配置成gpio输出。

GPIOx_GDIR:设置引脚方向,每位对应一个引脚,1-output0-input.

img

确定每组gpio基地址如下:加4就对应方向寄存器。

img

2.2.4 GPIO值#

GPIOx_DR:(GPIOx的data register)。设置输出引脚的电平,每位对应一个引脚,1-高电平,0-低电平。

img

如果是配成了输入引脚,GPIOx_PSR:读取引脚的电平,每位对应一个引脚,1-高电平,0-低电平:

img

3 字符设备驱动程序框架#

img

img

字符驱动编写流程:

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
/*
1. 确定主设备号,也可以让内核动态分配.
2. 定义自己的 file_operations 结构体 实现对应的 drv_open/drv_read/drv_write 等函数
填入 file_operations 结构体,把 file_operations 结构体告诉内核。
3. register_chrdev/unregister_chrdev
4. 其他完善:提供设备信息,自动创建设备节点:class_create, device_create
5. 操作硬件:通过 ioremap 映射寄存器的物理地址得到虚拟地址,读写虚拟地址
6. 驱动怎么和 APP 传输数据:通过 copy_to_user、copy_from_user 等操作函数。
*/
if (newchrled.major) { /* 定义了设备号,静态分配 */
newchrled.devid = MKDEV(newchrled.major, 0);
register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
} else { /* 没有定义设备号,动态分配 */
alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME); /* 申请设备号 */
newchrled.major = MAJOR(newchrled.devid); /* 获取主设备号 */
newchrled.minor = MINOR(newchrled.devid); /* 获取次设备号 */
}
printk("newcheled major=%d,minor=%d\r\n",newchrled.major, newchrled.minor);

/* 2、初始化cdev */
newchrled.cdev.owner = THIS_MODULE;
cdev_init(&newchrled.cdev, &newchrled_fops);

/* 3、添加一个cdev */
cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);

/* 4、创建类 */
newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
if (IS_ERR(newchrled.class))
return PTR_ERR(newchrled.class);

/* 5、创建设备 */
newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME);
if (IS_ERR(newchrled.device))
return PTR_ERR(newchrled.device);

3.1 实现通用性驱动模板#

3.1.1 led_drv.c#

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
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include "led_opr.h"

/* 确定主设备号 */
static int major = 0;
static struct class *led_class;
struct led_operations *p_led_opr;

#define MIN(a, b) (a < b ? a : b)
/* 实现对应的open/read/write等函数,填入file_operations结构体 */
static ssize_t led_drv_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}

/* write(fd, &val, 1); */
static ssize_t led_drv_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
int err;
char status;
struct inode *inode = file_inode(file);
int minor = iminor(inode);

printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = copy_from_user(&status, buf, 1);
/* 根据次设备号和status控制LED */
p_led_opr->ctl(minor, status);
return 1;
}

static int led_drv_open(struct inode *node, struct file *file)
{
int minor = iminor(node);

printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
/* 根据次设备号初始化LED */
p_led_opr->init(minor);
return 0;
}

static int led_drv_close (struct inode *node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}

/* 定义自己的file_operations结构体 */
static struct file_operations led_drv = {
.owner = THIS_MODULE,
.open = led_drv_open,
.read = led_drv_read,
.write = led_drv_write,
.release = led_drv_close,
};

/* 把file_operations结构体告诉内核:注册驱动程序 */
/* 入口函数:安装驱动程序时,就会去调用这个入口函数 */
static int __init led_init(void)
{
int err;
int i;

printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
major = register_chrdev(0, "100ask_led", &led_drv);
led_class = class_create(THIS_MODULE, "100ask_led_class");
err = PTR_ERR(led_class);
if (IS_ERR(led_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "led");
return -1;
}
p_led_opr = get_board_led_opr();
/* creat device node, eg: /dev/100ask_led0,1,... */
for (i = 0; i < p_led_opr->num; i++)
device_create(led_class, NULL, MKDEV(major, i), NULL, "100ask_led%d", i);
return 0;
}
/* 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数 */
static void __exit led_exit(void)
{
int i;

printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
for (i = 0; i < p_led_opr->num; i++)
device_destroy(led_class, MKDEV(major, i)); /* /dev/100ask_led0,1,... */
class_destroy(led_class);
unregister_chrdev(major, "100ask_led");
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
  1. register_chrdev, 如果传入主设备号,则静态注册,传入0则动态注册返回主设备号。
  2. class_create创建类/sys/class/100ask_led_class
  3. get_board_led_opr获取具体单板的操作operation函数,后面具体单板实现。
  4. 获取到具体单板的led数量后,device_create为每一个led灯都建立设备节点。

再来看file_operations中的操作:

  1. led_drv_open根据次设备号,调用具体单板的init函数,比如gpio 引脚复用,电器属性设置等。
  2. led_drv_write就可以根据次设备号, 控制具体单板的led引脚,设置高低电平,从而控制亮灭。

3.2 具体单板led驱动#

3.2.1 led_opr.h#

1
2
3
4
5
6
7
8
9
#ifndef _LED_OPR_H
#define _LED_OPR_H
struct led_operations {
int num;
int (*init) (int which); /* 初始化LED, which-哪个LED */
int (*ctl) (int which, char status); /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
};
struct led_operations *get_board_led_opr(void);
#endif

定义一个led_operationsnum表示有几个led, init表示初始化led(drv_open的时候调用,配置pinmuxio mode, enable pin clk等)。

3.2.2 board_100ask_imx6ull-qemu.c分析#

现在有一块board_100ask_imx6ull-qemu板子有4个LED,占2组GPIO,分别是GPIO5_3GPIO1_3, GPIO1_5, GPIO1_6

img

3.2.2.1 CCM时钟配置#

寄存器配置参考2.1。使能时钟gpio5和gpio1的时钟,CCM_CCGR1[CG13]CCM_CCGR1[CG15]配置成0x11。

1
2
3
4
5
6
7
8
/* 1. enable GPIO1
* CG13, b[27:26] = 0b11 */

*CCM_CCGR1 |= (3<<26);

/* 1. enable GPIO5
* CG15, b[31:30] = 0b11 */
*CCM_CCGR1 |= (3<<30);

3.2.2.2 IOMUX成gpio#

iomux配置4个引脚复用成gpio功能。

3.2.2.2.1 gpio5_3 进行iomux#

基地址为0x2290014。用ioremap进行映射到虚拟地址,就可以直接操作寄存器地址了。但是一般建议用writel, writeb等函数族。配成5表示gpio模式。

img

1
2
3
4
5
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3=ioremap(0x2290014, 4);        
/* 2. set GPIO5_IO03 as GPIO
* MUX_MODE, b[3:0] = 0b101 */

*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = 5;
3.2.2.2.2 gpio1_3/gpio1_5/gpio1_6 进行iomux#

img

img

img

每次映射4个字节太繁琐,干脆对整个gpio的iomux地址进行映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct iomux {
volatile unsigned int unnames[23];
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO00; /* offset 0x5c*/
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO01;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO02;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO05;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO06;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO07;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO08;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO09;
};

iomux = ioremap(0x20e0000, sizeof(struct iomux));

这里偷懒用了一个技巧,unnames[23] 92(0x5c)字节,刚好IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO00地址就是0x20e0000+0x5c,就不用把所有寄存器都搬进来到struct iomux

同理IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03地址就是0x20e0000+0x68, 因此:

1
2
3
4
/* MUX_MODE, b[3:0] = 0b101 */
iomux->IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = 5;
iomux->IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO05 = 5;
iomux->IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO06 = 5;

3.2.2.3 gpio配成输出#

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct imx6ull_gpio {
volatile unsigned int dr;
volatile unsigned int gdir;
volatile unsigned int psr;
volatile unsigned int icr1;
volatile unsigned int icr2;
volatile unsigned int imr;
volatile unsigned int isr;
volatile unsigned int edge_sel;
};
/* GPIO1 GDIR, b[5] = 0b1*/
gpio1 = ioremap(0x209C000, sizeof(struct imx6ull_gpio));
gpio1->gdir |= (1<<3);
gpio1->gdir |= (1<<5);
gpio1->gdir |= (1<<6);

offset为0表示data register, offset为4表示方向寄存器。以gpio1_3/gpio1_5/gpio1_6举例,gdirbit_n置1就表示哪个gpio配成输出。

3.2.2.4 gpio值设置#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (which == 0) {
if (status) /* on : output 0 */
gpio5->dr &= ~(1<<3);
else /* on : output 1 */
gpio5->dr |= (1<<3);
} else if (which == 1) {
if (status) /* on : output 0 */
gpio1->dr &= ~(1<<3);
else /* on : output 1 */
gpio1->dr |= (1<<3);
} else if (which == 2) {
if (status) /* on : output 0 */
gpio1->dr &= ~(1<<5);
else /* on : output 1 */
gpio1->dr |= (1<<5);
} else if (which == 3) {
if (status) /* on : output 0 */
gpio1->dr &= ~(1<<6);
else /* on : output 1 */
gpio1->dr |= (1<<6);
}

同理dr就表示数据寄存器。一共4个led:

1
2
3
4
which等于0表示gpio5_3
which等于1示gpio1_3
which等于2示gpio1_5
which等于3示gpio1_6

3.2.2.5 board_100ask_imx6ull-qemu.c#

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
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/io.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include "led_opr.h"

struct iomux {
volatile unsigned int unnames[23];
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO00; /* offset 0x5c*/
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO01;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO02;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO05;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO06;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO07;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO08;
volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO09;
};
struct imx6ull_gpio {
volatile unsigned int dr;
volatile unsigned int gdir;
volatile unsigned int psr;
volatile unsigned int icr1;
volatile unsigned int icr2;
volatile unsigned int imr;
volatile unsigned int isr;
volatile unsigned int edge_sel;
};

/* enable GPIO1,GPIO5 */
static volatile unsigned int *CCM_CCGR1;

static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
static struct iomux *iomux;

static struct imx6ull_gpio *gpio1;
static struct imx6ull_gpio *gpio5;

static struct led_operations board_demo_led_opr = {
.num = 4,
.init = board_demo_led_init,
.ctl = board_demo_led_ctl,
};

static int board_demo_led_init(int which) {
if (!CCM_CCGR1) {
CCM_CCGR1 = ioremap(0x20C406C, 4);
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x2290014, 4);
iomux = ioremap(0x20e0000, sizeof(struct iomux));
gpio1 = ioremap(0x209C000, sizeof(struct imx6ull_gpio));
gpio5 = ioremap(0x20AC000, sizeof(struct imx6ull_gpio));
}

if (which == 0) {
*CCM_CCGR1 |= (3<<30);
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = 5;
gpio5->gdir |= (1<<3);
} else if(which == 1) {
*CCM_CCGR1 |= (3<<26);
iomux->IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = 5;
gpio1->gdir |= (1<<3);
} else if(which == 2) {
*CCM_CCGR1 |= (3<<26);
iomux->IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO05 = 5;
gpio1->gdir |= (1<<5);
} else if(which == 3) {
*CCM_CCGR1 |= (3<<26);
iomux->IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO06 = 5;
gpio1->gdir |= (1<<6);
}
return 0;
}
static int board_demo_led_ctl(int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
{
if (which == 0) {
if (status)
gpio5->dr &= ~(1<<3);
else
gpio5->dr |= (1<<3);
} else if (which == 1) {
if (status)
gpio1->dr &= ~(1<<3);
else
gpio1->dr |= (1<<3);
} else if (which == 2) {
if (status)
gpio1->dr &= ~(1<<5);
else
gpio1->dr |= (1<<5);
} else if (which == 3) {
if (status)
gpio1->dr &= ~(1<<6);
else
gpio1->dr |= (1<<6);
}
return 0;
}

struct led_operations *get_board_led_opr(void) {
return &board_demo_led_opr;
}

open的时候调用get_board_led_opr得到具体单板的操作函数集。进一步调用board_demo_led_init初始化led。

write的时候调用具体单板的操作函数集,进一步调用board_demo_led_ctl操控led。

4 字符设备驱动基础概念#

4.1 EXPORT_SYMBOL#

EXPORT_SYMBOL:导出函数,让别的module也能使用。

image-20240725202519779

EXPORT_SYMBOL_GPL:

image-20240725211903017

4.2 MODULE_INFO#

MODULE_INFO(intree, "Y");的作用是将可加载内核模块标记为 in-tree

加载树外 LKM 会导致内核打印警告:这是从module.c中的检查引起的:

img

module: loading out-of-tree module taints kernel.

4.2 module_param#

module_param(name,type,perm);

功能:指定模块参数,用于在加载模块时或者模块加载以后传递参数给模块。

module_param_array( name, type, nump, perm);

可用sysfs进行查看修改:

img

讲到module_param,把其他的也一笔带入:

1
2
3
4
MODULE_DESCRIPTION("Freescale PM rpmsg driver");
MODULE_AUTHOR("Anson Huang <Anson.Huang@nxp.com>");
MODULE_LICENSE("GPL");
MODULE_VERSION("v2.0");

4.2.1 type#

type: 数据类型:

1
2
3
4
5
6
7
8
9
bool : 布尔型
inbool : 布尔反值
charp: 字符指针(相当于char *,不超过1024字节的字符串)
short: 短整型
ushort : 无符号短整型
int : 整型
uint : 无符号整型
long : 长整型
ulong: 无符号长整型

4.2.2 perm#

perm表示此参数在sysfs文件系统中所对应的文件节点的属性,其权限在include/linux/stat.h中有定义:

image-20240725205237500

1
2
3
4
5
6
7
8
9
10
11
#define S_IRUSR 00400 //文件所有者可读
#define S_IWUSR 00200 //文件所有者可写
#define S_IXUSR 00100 //文件所有者可执行

#define S_IRGRP 00040 //与文件所有者同组的用户可读
#define S_IWGRP 00020
#define S_IXGRP 00010

#define S_IROTH 00004 //与文件所有者不同组的用户可读
#define S_IWOTH 00002
#define S_IXOTH 00001

image-20240725205907528

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static char *alg = NULL;
static u32 type;
static u32 mask;
static int mode;

module_param(alg, charp, 0);
module_param(type, uint, 0);
module_param(mask, uint, 0);
module_param(mode, int, 0);

static int fish[10];
static int nr_fish;
module_param_array(fish, int, &nr_fish, 0664);
static char media[8];
module_param_string(media, media, sizeof(media), 0);

可以用sysfs设置fish数组,或者insmod时伴随设置。

4.3 设备节点#

cat /proc/devices

img

4.3.1 手动建立设备节点#

手动建立设备节点命令是mknod, 由于这里的字符设备都是用的misc杂项设备方式,因此主设备号都为10:

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
/mnt/Athena2_FPGA_SDK_Veriry/demo/workspace/ko # ls -l /dev/mmcblk0p1
brw-rw---- 1 root root 179, 1 Jan 1 00:05 /dev/mmcblk0p1
/mnt/Athena2_FPGA_SDK_Veriry/demo/workspace/ko # ls -l /dev/mmcblk0
brw-rw---- 1 root root 179, 0 Jan 1 00:05 /dev/mmcblk0

/dev # ls -l cvi-*
crw-rw---- 1 root root 10, 0 Jan 1 00:05 /dev/cvi-base
crw-rw---- 1 root root 10, 61 Jan 1 00:05 /dev/cvi-dwa
crw-rw---- 1 root root 10, 58 Jan 1 00:30 /dev/cvi-ldc
crw-rw---- 1 root root 10, 60 Jan 1 00:04 /dev/cvi-stitch
crw-rw---- 1 root root 10, 62 Jan 1 00:05 /dev/cvi-sys
crw-rw---- 1 root root 10, 59 Jan 1 00:04 /dev/cvi-vpss

mknod /dev/mmcblk0 b 179 0
mknod /dev/mmcblk0p1 b 179 1

mknod /dev/cvi-base c 10 0
mknod /dev/cvi-sys c 10 62
mknod /dev/cvi-dwa c 10 61
mknod /dev/cvi-ldc c 10 58
mknod /dev/cvi-stitch c 10 60
mknod /dev/cvi-vpss c 10 59
crw-rw---- 1 root root 10, 0 Jan 1 00:08 /dev/cvi-base
crw-rw---- 1 root root 10, 61 Jan 1 00:08 /dev/cvi-dwa
crw-rw---- 1 root root 10, 59 Jan 1 00:07 /dev/cvi-ldc
crw-rw---- 1 root root 10, 60 Jan 1 00:07 /dev/cvi-stitch
crw-rw---- 1 root root 10, 62 Jan 1 00:08 /dev/cvi-sys

mknod /dev/cvi-base c 10 0
mknod /dev/cvi-sys c 10 62
mknod /dev/cvi-dwa c 10 61
mknod /dev/cvi-ldc c 10 59
mknod /dev/cvi-stitch c 10 60

4.3.2 自动创建设备节点#

4.3.2.1 mdev机制#

udev是一个用户程序,在 Linux下通过 udev来实现设备文件的创建与删除, udev可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。比如使用modprobe命令成功加载驱动模块以后就自动在 /dev目录下创建对应的设备节点文件 ,使用rmmod命令卸载驱动模块以后就 删除掉 /dev目录下的设备节点文件。 使用 busybox构建根文件系统的时候, busybox会创建一个 udev的简化版本 mdev,所以在嵌入式 Linux中我们使用mdev来实现设备节点文件的自动创建与删除, Linux系统中的热插拔事件也由 mdev管理:

1
echo /sbin/mdev > /proc/sys/kernel/hotplug

4.4 设置文件私有数据#

一般open函数里面设置好私有数据以后,在 write、 read、 close等函数中直接读取 private_data即可得到设备结构体。

img

4.5 设备号#

include\linux\kdev_t.h

img

1
2
3
4
5
MINORBITS 表示次设备号位数,一共是 20 位;
MINORMASK 表示次设备号掩码;
MAJOR 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可
MINOR 用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可
MKDEV 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号

img

定义了major主设备就用静态注册,否则动态分配设备号注册字符设备。

4.5.1 静态分配和释放一个设备号#

1
2
3
#include <linux/fs.h>
register_chrdev_region()
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
#include <linux/module.h> 
#include <linux/cdev.h>
#include <linux/fs.h>

#define MY_MAJOR_NUM 202 //主设备号
static const struct file_operations my_dev_fops = {
.owner = THIS_MODULE,
.open = my_dev_open,
.release = my_dev_close,
.unlocked_ioctl = my_dev_ioctl,
};
static int __init hello_init(void){
int ret;
dev_t dev = MKDEV(MY_MAJOR_NUM, 0);

/* Allocate device numbers */
ret = register_chrdev_region(dev, 1, "my_char_device");
if (ret < 0){
pr_info("Unable to allocate mayor number %d\n", MY_MAJOR_NUM);
return ret;
}
/* Initialize the cdev structure and add it to the kernel space */
cdev_init(&my_dev, &my_dev_fops);
ret= cdev_add(&my_dev, dev, 1);
if (ret < 0){
unregister_chrdev_region(dev, 1);
pr_info("Unable to add cdev\n");
return ret;
}
return 0;
}
static void __exit hello_exit(void) {
cdev_del(&my_dev);
unregister_chrdev_region(MKDEV(MY_MAJOR_NUM, 0), 1);
}

4.5.2 动态分配和释放一个设备号#

1
2
3
#include <linux/fs.h>
alloc_chrdev_region()
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
45
static struct class*  helloClass;
static struct cdev my_dev;
dev_t dev;
static int __init hello_init(void) {
int ret;
dev_t dev_no;
int Major;
struct device* helloDevice;
ret = alloc_chrdev_region(&dev_no, 0, 1, DEVICE_NAME);
if (ret < 0){
pr_info("Unable to allocate Mayor number \n");
return ret;
}
Major = MAJOR(dev_no);
dev = MKDEV(Major,0);
cdev_init(&my_dev, &my_dev_fops);
ret = cdev_add(&my_dev, dev, 1);
if (ret < 0){
unregister_chrdev_region(dev, 1);
pr_info("Unable to add cdev\n");
return ret;
}
helloClass = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(helloClass)){
unregister_chrdev_region(dev, 1);
cdev_del(&my_dev);
pr_info("Failed to register device class\n");
return PTR_ERR(helloClass);
}
helloDevice = device_create(helloClass, NULL, dev, NULL, DEVICE_NAME);
if (IS_ERR(helloDevice)){
class_destroy(helloClass);
cdev_del(&my_dev);
unregister_chrdev_region(dev, 1);
pr_info("Failed to create the device\n");
return PTR_ERR(helloDevice);
}
return 0;
}
static void __exit hello_exit(void) {
device_destroy(helloClass, dev); /* remove the device */
class_destroy(helloClass); /* remove the device class */
cdev_del(&my_dev);
unregister_chrdev_region(dev, 1); /* unregister the device numbers */
}

4.6 添加设备和类#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct class *class; /* 类 */ 
struct device *device; /* 设备 */
dev_t devid; /* 设备号 */
static int __init led_init(void) {
class = class_create(THIS_MODULE, "xxx");
device = device_create(class, NULL, devid, NULL, "xxx");
return 0;
}
static void __exit led_exit(void) {
device_destroy(newchrled.class, newchrled.devid);
class_destroy(newchrled.class);
}
module_init(led_init);
module_exit(led_exit);

5 内核源码树添加一个字符设备驱动#

5.1 准备驱动源码#

这里以misc device为例, 进入drivers/misc目录,新建目录hello_drv。放入驱动源码MakefileKconfig

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
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>

static int major = 0;
static struct cdev hello_cdev;
static char kernel_buf[1024];
static struct class *hello_class;

static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset){
int err;
err = copy_to_user(buf, kernel_buf, min(1024, size));
return min(1024, size);
}
static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset){
int err;
err = copy_from_user(kernel_buf, buf, min(1024, size));
return min(1024, size);
}
static int hello_drv_open (struct inode *node, struct file *file){
return 0;
}
static int hello_drv_close (struct inode *node, struct file *file){
return 0;
}
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,
};

static int __init hello_init(void){
int err;
int rc;
dev_t devid;
#if 0
//major = register_chrdev(0, "hello", &hello_drv); /* /dev/hello */
#else
rc = alloc_chrdev_region(&devid, 0, 1, "hello");
major = MAJOR(devid);
cdev_init(&hello_cdev, &hello_drv);
cdev_add(&hello_cdev, devid, 1);
#endif
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;
}

static void __exit hello_exit(void){
device_destroy(hello_class, MKDEV(major, 0));
class_destroy(hello_class);
#if 0
//unregister_chrdev(major, "hello");
#else
cdev_del(&hello_cdev);
unregister_chrdev_region(MKDEV(major,0), 1);
#endif
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

5.2 MakeFile#

img

1
2
userprogs-always-y += hello_test
userccflags += -I usr/include

这里表示用userspace方式去编译应用程序,hello_test就是用户程序。

假如我们多个文件hello1.c hello2.c, 如何得到hello.ohello.ko呢?如下参考:

image-20240725223651990

5.3 Kconfig#

img

5.4 修改上一级Makefile和Kconfig#

img

img

hello_drv目录中的Kconfig也能被内核识别,输入make menuconfig,即可选择将其编译成内核模块还是直接编译进内核镜像,默认default n,也就是CONFIG_HELLO等于n, hello_drv目录是obj-n, 不编译;选择y则表示编译进内核镜像,选择m表示编译成内核模块。

编译成内核模块,则会在.config中产生CONFIG_HELLO=m的一项配置,编译产生hello.ko

img

编译成内核镜像,则会在.config中产生CONFIG_HELLO=y的一项配置,编译产生built-in.a,最终该 built-in.a会合入vmlinux。

Linux下sysfs-procfs-debugfs使用

1 Linux下sysfs-procfs-debugfs使用#

Linux内核空间与用户空间的交互如何能透过文件系统这层关系,把需要参数写入文件中呢?

当然有办法,linux内核提供了3种 “内存文件系统”,分别是sysfsdebugfsprocfs,驱动工程师可以通过任意的一种文件系统向用户空间传递信息。

1
2
3
Sysfs的挂载点为/sys
Debugfs的挂载点为/sys/kernel/debug
Procfs的挂载点为/proc

内存文件系统: 一种临时文件系统,一般会利用脚本挂载到rootfs,但是这些目录都是使用RAM空间,他们中的信息只存在于内存中,下电后即消失。他们的出现旨在提供一种与用户空间交互信息的方式。

脚本如下:

1
2
3
4
5
6
7
8
9
[root@xxx]/etc# cat fstab
# <file system> <mount pt> <type> <options> <dump> <pass>
/dev/root / ext2 rw,noauto 0 1
proc /proc proc defaults 0 0
devpts /dev/pts devpts defaults,gid=5,mode=620,ptmxmode=0666 0 0
tmpfs /dev/shm tmpfs mode=0777 0 0
tmpfs /tmp tmpfs mode=1777 0 0
sysfs /sys sysfs defaults 0 0
nodev /sys/kernel/debug debugfs defaults 0 0

输入mount查看挂载信息:可以看到有挂载procfs, sysfs,以及debugfs

1
2
3
4
5
6
7
8
9
[root@xxx]~# mount
/dev/root on / type squashfs (ro,relatime)
devtmpfs on /dev type devtmpfs (rw,relatime,size=1381884k,nr_inodes=345471,mode=755)
proc on /proc type proc (rw,relatime) #procfs
sysfs on /sys type sysfs (rw,relatime) #sysfs
nodev on /sys/kernel/debug type debugfs (rw,relatime) #debugfs
/dev/mmcblk0p6 on /mnt/cfg type ext4 (rw,sync,relatime)
/dev/mmcblk0p7 on /mnt/data type ext4 (rw,sync,relatime)
/dev/mmcblk0p7 on /var/log type ext4 (rw,sync,relatime)

1.1 sysfs#

设备驱动模型中诞生了sys这个新的虚拟文件系统。

1.1.1 sysfs举例#

sysfs在linux驱动开发过程使用非常常见,比如gpio子系统 led子系统 led子系统-hexo gpio子系统-hexo

1
2
3
4
5
6
echo 256 > /sys/class/gpio/export  #/sys/class/gpio会生成gpio256目录
echo 256 > /sys/class/gpio/unexport

echo 255 > /sys/class/leds/led1/brightness
cat /sys/class/leds/led1/brightness
cat /sys/class/leds/led1/max_brightness

这就是利用sysfs写入文件,这个文件是用户态和内核态共享的。方便驱动动态读取用户配置和对驱动的控制。

1.1.2 sysfs使用#

1
2
3
4
5
6
7
8
9
10
[root@xxx]/sys/module/soph_stitch/drivers# ls -l
total 0
lrwxrwxrwx 1 root root 0 Jan 1 10:00 platform:stitch -> ../../../bus/platform/drivers/stitch

[root@cvitek]/sys/module/soph_stitch/parameters# ls -l
#对应驱动模块的模块参数,比如module_param(clk_sys_freq, int, 0644);module_param(gStitchDumpReg, int, 0644);
-rw-r--r-- 1 root root 4096 Jan 1 10:01 clk_sys_freq
-rw-r--r-- 1 root root 4096 Jan 1 10:01 gStitchDumpDmaCfg
-rw-r--r-- 1 root root 4096 Jan 1 10:01 gStitchDumpReg
-rw-r--r-- 1 root root 4096 Jan 1 10:01 stitch_log_lv

/sys/module/xxx/parameters下定义了驱动xxx模块的模块参数。

1.1.2.0 syfs下的platform设备和驱动信息#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@cvitek]/sys/bus/platform/devices# ls -l
...
lrwxrwxrwx 1 root root 0 Jan 1 10:06 680b8000.stitch -> ../../../devices/platform/680b8000.stitch
lrwxrwxrwx 1 root root 0 Jan 1 10:06 680ba000.dpu -> ../../../devices/platform/680ba000.dpu
lrwxrwxrwx 1 root root 0 Jan 1 10:06 680be000.sys -> ../../../devices/platform/680be000.sys
lrwxrwxrwx 1 root root 0 Jan 1 10:06 68100000.cif -> ../../../devices/platform/68100000.cif
lrwxrwxrwx 1 root root 0 Jan 1 10:06 68100000.cif_v4l2 -> ../../../devices/platform/68100000.cif_v4l2
lrwxrwxrwx 1 root root 0 Jan 1 10:06 Fixed MDIO bus.0 -> ../../../devices/platform/Fixed MDIO bus.0
lrwxrwxrwx 1 root root 0 Jan 1 10:06 base -> ../../../devices/platform/base
...
[root@cvitek]/sys/bus/platform/drivers# ls -l
...
drwxr-xr-x 2 root root 0 Jan 1 10:15 cif
drwxr-xr-x 2 root root 0 Jan 1 10:00 stitch
...
# cd /sys/bus/platform/drivers/stitch进来瞅瞅
[root@cvitek]/sys/bus/platform/drivers/stitch# ls -l
total 0
lrwxrwxrwx 1 root root 0 Jan 1 10:00 680b8000.stitch -> ../../../../devices/platform/680b8000.stitch
lrwxrwxrwx 1 root root 0 Jan 1 10:00 module -> ../../../../module/soph_stitch
--w------- 1 root root 4096 Jan 1 10:00 uevent

可以看到只要用platform_driver_registerplatform_device_register注册的驱动和设备就会建立如上的sysfs关系链。

1.1.2.1 syfs下的misc设备信息#

1
2
3
4
5
6
7
8
9
[root@cvitek]/sys/class/misc# ls
misccvitekadc_0 soph-dpu soph-stitch watchdog
misccvitekadc_1 soph-ldc soph-sys
misccvitekdac_0 soph-mipi-rx soph-vpss
[root@cvitek]/sys/class/misc# ls -l soph-stitch
lrwxrwxrwx 1 root root 0 Jan 1 13:57 soph-stitch -> ../../devices/virtual/misc/soph-stitch

[root@cvitek]/sys/class/soph-vi# ls -l
lrwxrwxrwx 1 root root 0 Jan 1 08:02 soph-vi -> ../../devices/platform/68000000.vi/soph-vi/soph-vi

可以看到只要是misc设备注册的字符设备,都会在/sys/class/misc下。device_create函数内部会调用到device_add函数,会在/sys/device目录下生成相应的sys文件,同时会判断device结构中的devt变量是否可用,如果可用才会调用devtmpfs_create_node(dev);在/dev目录下生成对应的设备文件。所以说device_add是否会生成设备文件需要根据device结构体中是否传入了设备号来决定的。

1
device_create(dev->vi_class, dev->dev, dev->cdev_id, NULL, "%s", VI_DEV_NAME);

1.1.2.2 syfs API#

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
static inline int __must_check sysfs_create_file(struct kobject *kobj,
const struct attribute *attr)//生成sysfs属性文件,此接口用于生成单个属性文件

//在参数kobj目录下面创建一个属性集合,并且显示该集合的文件。
//attribute_group *grp 中描述的是一组属性类型
int __must_check sysfs_create_group(struct kobject *kobj,
const struct attribute_group *grp);

int __must_check sysfs_create_groups(struct kobject *kobj,
const struct attribute_group **groups);

//动态生成一个struct kobject数据结构,然后将其注册到sysfs文件系统
/*
name就是要创建的文件或者目录的名称,
parent指向父目录的kobject数据结构,若parent为NULL,说明父目录就是/sys目录,
比如:kobject_create_and_add()在/sys 目录下建立一个名为“kernel”的目录,
然后sysfs_create_group()函数在该目录下面创建一些属性集合
*/
struct kobject *kobject_create_and_add(const char *name, struct kobject*parent);

//会调用到sysfs_create_file函数来生成sysfs属性文件,此接口用于生成单个属性文件
int device_create_file ( struct device * dev, const struct device_attribute * attr);

//移除组属性
void sysfs_remove_group(struct kobject *kobj,
const struct attribute_group *grp);

//Y:\linux_5.10\include\linux\sysfs.h

1.1.2.3 给驱动模块添加sysfs参数举例#

  1. 使用DEVICE_ATTR声明一个sys节点, 这里是一个led_status节点,申明了led_status_showled_status_store函数。
1
2
3
4
5
6
7
/*
led_status:在sys接口中显示的节点名字
0600:表示操作这个led_status节点的权限
led_status_show:使用cat命令查看sys接口时调用的函数
led_status_store:使用echo命令往sys接口写入内容时调用的函数
*/
static DEVICE_ATTR(led_status, 0600, led_status_show, led_status_store);
  1. 完成sys节点的读写函数,执行 cat /sys/devices/platform/leds/led_status时会调用led_status_show,把buf内容显示出来。用echo命令往sys节点写入内容时调用led_status_storeled_status_show()函数和led_status_store()函数的作用分为打印led变量的值和修改led变量的值.
1
2
3
4
5
6
7
8
9
10
11
12
static unsigned int led = 0;

static ssize_t led_status_show(struct device *dev, struct device_attribute *attr, char *buf){
return sprintf(buf, "%s:%d.\n", "led", led);
}

static ssize_t led_status_store(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count){
//写入的内容会存放到buf中,这里将buf内容赋值给led变量
sscanf(buf, "%d", &led);
return count;
}
  1. 定义struct attributestruct attribute_group数组
1
2
3
4
5
6
7
8
9
10
11
12
13
static struct attribute *led_attributes[]={
/*上述使用了DEVICE_ATTR声明节点名字为led_status,
* 则struct attribute名字应为:
* dev_attr_ + (节点名) + .attr
* 所以名字为dev_attr_led_status.attr
*/
&dev_attr_led_status.attr,
NULL,
};

static const struct attribute_group led_attrs={
.attrs = led_attributes,//引用上述struct attribute数组
};
  1. 调用sysfs_create_group()注册sysfs接口, 完整驱动实例如下:
    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
    static unsigned int led = 0;

    static ssize_t led_status_show(struct device *dev, struct device_attribute *attr, char *buf){
    return sprintf(buf, "%s:%d.\n", "led", led);
    }

    static ssize_t led_status_store(struct device *dev, struct device_attribute *attr,
    const char *buf, size_t count){
    sscanf(buf, "%d", &led);
    return count;
    }

    static DEVICE_ATTR(led_status, 0600, led_status_show, led_status_store);
    static struct attribute *led_attributes[]={
    &dev_attr_led_status.attr,
    NULL,
    };
    static const struct attribute_group led_attrs={
    .attrs = led_attributes,
    };


    static int xx_led_probe(struct platform_device *pdev){
    sysfs_create_group(&pdev->dev.kobj, &led_attrs);
    return 0;
    }
    static int xx_led_remove(struct platform_device *pdev){
    sysfs_remove_group(&pdev->dev.kobj, &led_attrs);
    return 0;
    }
    static const struct of_device_id xx_led_of_match[] = {
    {.compatible = "xx,xx-led"},
    };
    static struct platform_driver xx_led_driver = {
    .probe = xx_led_probe,
    .remove = xx_led_remove,
    .driver = {
    .name = "xx-led",
    .owner = THIS_MODULE,
    .of_match_table = xx_led_of_match,
    },
    };
    static int __init xx_led_init(void){
    return platform_driver_register(&xx_led_driver);
    }
    static void __exit xx_led_exit(void){
    platform_driver_unregister(&xx_led_driver);
    }
    module_init(xx_led_init);
    module_exit(xx_led_exit);

1.1.3 DEVICE_ATTR宏#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define DEVICE_ATTR(_name, _mode, _show, _store) \
struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)
#define __ATTR(_name, _mode, _show, _store) { \
.attr = {.name = __stringify(_name), \
.mode = VERIFY_OCTAL_PERMISSIONS(_mode) }, \
.show = _show, \
.store = _store, \
}

struct device_attribute {
struct attribute attr;
ssize_t (*show)(struct device *dev, struct device_attribute *attr,
char *buf);
ssize_t (*store)(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count);
};

DEVICE_ATTR宏会定义一个struct device_attribute结构体实例dev_attr_##_name和初始化。一般用device_create_file生成sysfs属性文件。

注意:属性文件的权限mode不可以随便定义,是有限制的,mode不合理会报错:

1
error: negative width in bit-field anonymous

1.1.3.0 DEVICE_ATTR示例#

Linux下Framebuffer子系统(cnblogs.com-fuzidage) 字符设备驱动-Framebuffer子系统 | Hexo (fuzidage.github.io) 为例,打开linux_5.10/drivers/video/fbdev/core/fbsysfs.c:

image-20240912223158978

可以看到很多属性文件,都调用device_create_file建立了sysfs属性文件。

1.1.3.1 device_create_file#

image-20240912223254045

可以看到本质还是用sysfs_creat_file创建sysfs属性文件。

1.2 procfs#

procfs是用户获取进程的有用信息、系统的有用信息等。可以查看某个进程的相关信息,也可以查看系统的信息,比如/proc/meminfo 用来查看内存的管理信息,/proc/cpuinfo用来观察CPU的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@cvitek]~# ls -l /proc/
total 0
dr-xr-xr-x 8 root root 0 Jan 1 08:00 1
dr-xr-xr-x 8 root root 0 Jan 1 08:00 234
dr-xr-xr-x 8 root root 0 Jan 1 08:00 273
-r--r--r-- 1 root root 0 Jan 1 08:02 cmdline
-r--r--r-- 1 root root 0 Jan 1 08:02 cpuinfo
dr-xr-xr-x 3 root root 0 Jan 1 08:02 dynamic_debug
-r--r--r-- 1 root root 0 Jan 1 08:02 fb
-r--r--r-- 1 root root 0 Jan 1 08:02 filesystems
dr-xr-xr-x 8 root root 0 Jan 1 08:02 fs
-r--r--r-- 1 root root 0 Jan 1 08:02 interrupts
-r--r--r-- 1 root root 0 Jan 1 08:02 iomem
-r--r--r-- 1 root root 0 Jan 1 08:02 ioports
dr-xr-xr-x 92 root root 0 Jan 1 08:02 irq
-r--r--r-- 1 root root 0 Jan 1 08:02 meminfo
-r-------- 1 root root 0 Jan 1 08:02 pagetypeinfo
-r--r--r-- 1 root root 0 Jan 1 08:02 partitions
-r--r--r-- 1 root root 0 Jan 1 08:02 sched_debug
lrwxrwxrwx 1 root root 0 Jan 1 08:00 self -> 291
lrwxrwxrwx 1 root root 0 Jan 1 08:00 thread-self -> 291/task/291
-r-------- 1 root root 0 Jan 1 08:02 vmallocinfo
-r--r--r-- 1 root root 0 Jan 1 08:02 vmstat
-r--r--r-- 1 root root 0 Jan 1 08:02 zoneinfo

可以看到很多信息,我们敲的命令ps、top等很多shell命令正是从proc系统中读取信息,且更具可读性。又例如free命令就是解析/proc/meminifo。

1.2.1 procfs API#

procfs文件系统提供了一些常用的API,这些API函数定义在fs/proc/internal.h

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
struct proc_dir_entry *proc_mkdir(const char *name,
struct proc_dir_entry *parent);// 如果传入的名字是null, 那么就在/proc/下创建一个目录

//添加一个proc条目, linux5.10后file_operations换成了proc_ops
#if (LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 0))
struct proc_dir_entry *proc_create_data(const char *name, umode_t mode,
struct proc_dir_entry *parent,
const struct proc_ops *proc_ops, void *data);
#else
struct proc_dir_entry *proc_create_data(const char *name, umode_t mode,
struct proc_dir_entry *parent,
const struct file_operations *proc_fops,
void *data);
#endif
//也可以直接用这种添加条目,支持多级目录如/proc/aaa/bbb/ccc条目
struct proc_dir_entry *proc_create_data(const char *name, umode_t mode,
struct proc_dir_entry *parent,
const struct file_operations *proc_fops,
void *data);
//删除条目
void remove_proc_entry(const char *name, struct proc_dir_entry *parent);
//删除目录
void proc_remove(struct proc_dir_entry *de);

// procfs的实现见Y:\linux_5.10\fs\proc\generic.c
// 头文件见Y:\linux_5.10\include\linux\proc_fs.h

1.2.2 使用举例#

举个例子:misc杂项设备 misc杂项设备-Hexo子系统初始化时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static int __init misc_init(void) {
int err;
#ifdef CONFIG_PROC_FS
proc_create("misc", 0, NULL, &misc_proc_fops);
#endif
misc_class = class_create(THIS_MODULE, "misc");
err = PTR_ERR(misc_class);
if (IS_ERR(misc_class))
goto fail_remove;
err = -EIO;
if (register_chrdev(MISC_MAJOR,"misc",&misc_fops))
goto fail_printk;
misc_class->devnode = misc_devnode;
return 0;

fail_printk:
printk("unable to get major %d for misc devices\n", MISC_MAJOR);
class_destroy(misc_class);
fail_remove:
remove_proc_entry("misc", NULL);
return err;
}

就创建了/proc/misc条目和/sys/class/misc目录。/proc/misc条目统计了包含的misc杂项字符设备:

1
2
3
4
5
6
[root@xxxx]/proc# cat misc
48 soph-stitch
49 soph-dpu
50 soph-mipi-tx1
51 soph-mipi-tx0
52 soph-rgn

再举一个例子:透过procfs进行cif驱动的状态显示到用户,以及用户配置参数,动态调用cif驱动的流程控制。

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
127
128
129
130
131
132
133
134
135
#define CIF_PROC_NAME "v4l2/mipi-rx"
static struct proc_dir_entry *cif_proc_entry;
//根据txt_buff做一些驱动流程控制,复位处理,时钟配置等等
int dbg_hdler(struct cvi_cif_dev *dev, char const *input){
struct cvi_link *link = &dev->link[0];
struct cif_ctx *ctx;
//int reset;
u32 num;
u8 str[80] = {0};
u8 t = 0;
u32 a, v, v2;
u8 i, n;
u8 *p;

num = sscanf(input, "%s %d %d %d", str, &a, &v, &v2);
if (num > 4) {
dbg_print_usage(link->dev);
return -EINVAL;
}

dev_info(link->dev, "input = %s %d\n", str, num);
/* convert to lower case for following type compare */
p = str;
for (; *p; ++p)
*p = tolower(*p);
n = ARRAY_SIZE(dbg_type);
for (i = 0; i < n; i++) {
if (!strcmp(str, dbg_type[i])) {
t = i;
break;
}
}
if (i == n) {
dev_info(link->dev, "unknown type(%s)!\n", str);
dbg_print_usage(link->dev);
return -EINVAL;
}
switch (t) {
case 0:
/* reset */
if (a > MAX_LINK_NUM)
return -EINVAL;

link = &dev->link[a];
ctx = &link->cif_ctx;

if (link->is_on) {
link->sts_csi.errcnt_ecc = 0;
link->sts_csi.errcnt_crc = 0;
link->sts_csi.errcnt_wc = 0;
link->sts_csi.errcnt_hdr = 0;
link->sts_csi.fifo_full = 0;
cif_clear_csi_int_sts(ctx);
cif_unmask_csi_int_sts(ctx, 0x0F);
}
break;
case 1:
/* hs-settle */
if (a > MAX_LINK_NUM)
return -EINVAL;

link = &dev->link[a];
ctx = &link->cif_ctx;
cif_set_hs_settle(ctx, v);
break;
}
//cat /proc/v4l2/mipi-rx会调用进行proc输出cif驱动状态信息
int proc_cif_show(struct seq_file *m, void *v)
{
struct cvi_cif_dev *dev = (struct cvi_cif_dev *)m->private;
int i;

seq_printf(m, "\nModule: [MIPI_RX], Build Time[%s]\n",
UTS_VERSION);
seq_puts(m, "\n------------Combo DEV ATTR--------------\n");
for (i = 0; i < MAX_LINK_NUM; i++)
if (dev->link[i].is_on)
cif_show_dev_attr(m, &dev->link[i].attr);

seq_puts(m, "\n------------MIPI info-------------------\n");
for (i = 0; i < MAX_LINK_NUM; i++)
if (dev->link[i].is_on
&& (dev->link[i].attr.input_mode == INPUT_MODE_MIPI)) {
cif_show_mipi_sts(m, &dev->link[i]);
cif_show_phy_sts(m, &dev->link[i]);
}
return 0;
}
static ssize_t cif_proc_write(struct file *file, const char __user *user_buf,
size_t count, loff_t *ppos)
{
struct cvi_cif_dev *dev = PDE_DATA(file_inode(file));
#if (KERNEL_VERSION(5, 10, 0) <= LINUX_VERSION_CODE)
char txt_buff[MAX_CIF_PROC_BUF];

count = simple_write_to_buffer(txt_buff, MAX_CIF_PROC_BUF, ppos,
user_buf, count);

dbg_hdler(dev, txt_buff);
#else
dbg_hdler(dev, user_buf);//根据txt_buff做一些驱动流程控制,复位处理,时钟配置等等
#endif
return count;
}

static int proc_cif_open(struct inode *inode, struct file *file)
{
struct cvi_cif_dev *dev = PDE_DATA(inode);
return single_open(file, proc_cif_show, dev);//proc_cif_show 输出cif驱动状态信息
}

#if (KERNEL_VERSION(5, 10, 0) <= LINUX_VERSION_CODE)
static const struct proc_ops cif_proc_fops = {
.proc_open = proc_cif_open,
.proc_read = seq_read,
.proc_write = cif_proc_write,
.proc_lseek = seq_lseek,
.proc_release = single_release,
};
#else
static const struct file_operations cif_proc_fops = {
.owner = THIS_MODULE,
.open = proc_cif_open,
.read = seq_read,
.write = cif_proc_write,
.llseek = seq_lseek,
.release = single_release,
};
#endif
#ifdef CONFIG_PROC_FS
cif_proc_entry = proc_create_data(CIF_PROC_NAME, 0, NULL,
&cif_proc_fops, dev);
if (!cif_proc_entry)
dev_err(&pdev->dev, "cif: can't init procfs.\n");
#endif

1.先创建proc条目:

image-20240910223622694

2.实现proc_ops中的成员函数,主要是.proc_open.proc_write。注意这里当我们Linux内核版本超过5.10,叫做proc_ops, 否则还是叫做file_operations.

image-20240910224117513

procfs通常会和seq_file接口一起使用。seq_file是一个序列文件接口,当我们创建的proc数据内容由一系列数据顺序组合而成或者是比较大的proc文件系统时,都建议使用seq_file接口,例如cat /proc/meminfo就会显示很多内容。

seq_file接口主要就是解决proc接口编程存在的问题,推荐在proc接口编程时使用seq_file接口,另外.read、.llseek、.release成员函数也可以直接用seq_read、seq_lseek和seq_release

1.2.2.1 seq机制#

当用户顺序读取proc接口时, 比如cat /proc/v4l2/mipi-rx, proc_cif_open被调用,然后调用seq_read。和应用程序一样,操作一个文件就是open, read, close操作…

1.2.2.1.0 sigle_open#

proc_cif_open调用sigle_opensigle_open直接调用了seq_open(file, op),该函数会创建个seq_file实例,并添加到file->private_data

image-20240910233011578

还会创建一个seq_operations *op, 可以看到驱动设置的show函数cif_proc_show被给到op的成员函数show,其余几个成员函数赋值为:

1
2
3
4
op->start = single_start;
op->next = single_next;
op->stop = single_stop;
op->show = show;

继续看seq_open,把前面的seq_operations *op实例给到seq_file *p->op

1.2.2.1.1 seq_file基础#

image-20240910232328785

open完后,好接着调用seq_read。seq机制实现见:

Y:\linux_5.10\fs\seq_file.c

Y:\linux_5.10\include\linux\seq_file.h

1.2.2.1.2 seq_read#

image-20240910234459044

可以看到不就是调用sigle_open时注册的single_start, single_next函数嘛,包括驱动自己注册的show函数,我这里是cif_proc_show.

网上找了一份图总结了seq机制:

img

1.3 debugfs#

debugfs也是一种用来调试内核的内存文件系统,内核开发者可以通过debugfs和用户空间交换数据,有点类似于前文提到的procfs和sysfs。

procfs是为了反映系统以及进程的状态信息sysfs用于Linux设备驱动模型:

把私有的调试信息加入这两个虚拟文件系统不太合适,因此内核多添加了一个虚拟文件系统,也就是debugfs。

最常见的就是linux内核的dynamic debug dynamic_debug-hexo 可以在程序运行后动态开关模块的打印,甚至是具体某个文件,某个函数的打印。

1.3.1 debugfs API#

debufs文件系统中有不少API函数可以使用,它们定义在include/linux/debugfs.h头文件中。

1
2
3
4
5
6
struct dentry *debugfs_create_dir(const char *name,struct dentry *parent)
void debugfs_remove(struct dentry *dentry)
struct dentry *debugfs_create_blob(const char *name, umode_t mode,struct dentry *parent,
struct debugfs_blob_wrapper *blob)
struct dentry *debugfs_create_file(const char *name, umode_t mode,struct dentry *parent,
void *data,const struct file_operations *fops)

1.3.2 debugfs 示例#

image-20240911203637060

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
#ifdef CONFIG_DEBUG_FS
static ssize_t vpu_debug_read(struct file *file, char __user *user_buf,
size_t count, loff_t *ppos)
{
char buf[256];
unsigned int len;
unsigned int running, pc, vpu_to_host, host_to_vpu, wdt;
int ret;
struct device *dev = file->private_data;
struct mtk_vpu *vpu = dev_get_drvdata(dev);

ret = vpu_clock_enable(vpu);
if (ret) {
dev_err(vpu->dev, "[VPU] enable clock failed %d\n", ret);
return 0;
}

/* vpu register status */
running = vpu_running(vpu);
pc = vpu_cfg_readl(vpu, VPU_PC_REG);
wdt = vpu_cfg_readl(vpu, VPU_WDT_REG);
host_to_vpu = vpu_cfg_readl(vpu, HOST_TO_VPU);
vpu_to_host = vpu_cfg_readl(vpu, VPU_TO_HOST);
vpu_clock_disable(vpu);

if (running) {
len = snprintf(buf, sizeof(buf), "VPU is running\n\n"
"FW Version: %s\n"
"PC: 0x%x\n"
"WDT: 0x%x\n"
"Host to VPU: 0x%x\n"
"VPU to Host: 0x%x\n",
vpu->run.fw_ver, pc, wdt,
host_to_vpu, vpu_to_host);
} else {
len = snprintf(buf, sizeof(buf), "VPU not running\n");
}
return simple_read_from_buffer(user_buf, count, ppos, buf, len);
}

static struct dentry *vpu_debugfs;
static const struct file_operations vpu_debug_fops = {
.open = simple_open,
.read = vpu_debug_read,
};
vpu_debugfs = debugfs_create_file("mtk_vpu", S_IRUGO, NULL, (void *)dev,
&vpu_debug_fops);
#endif

很简单,就是调用debugfs_create_file后会在debugfs挂载目录下也就是/sys/kernel/debug/建立一个mtk_vpu文件。

cat /sys/kernel/debug/mtk_vpu会打开该文件,也就是调用simple_open,然后调用vpu_debug_read,进一步调用simple_read_from_buffer

image-20240911204546367

你可以规定/sys/kernel/debug/mtk_vpu文件的格式和信息,这里它是dump了一些寄存器信息:

1
2
3
4
VPU_PC_REG
VPU_WDT_REG
HOST_TO_VPU
VPU_TO_HOST

当然你也可以不用simple_open这一套机制,那就要你自己去实现vpu_debug_fops中的成员函数,自己去调用copy_to_user。这里是利用simple_open, simple_read机制帮忙简化了,不需要去调用copy_to_user

simple_open,simple_read机制见:Y:linux_5.10\fs\libfs.c

buildroot教程

1 引入buildroot#

Buildroot是Linux平台上一个开源的嵌入式Linux系统自动构建框架。用来制作根文件系统,我们还要自己去移植一些第三方软件和库,比如 alsaiperfmplayer 等等。

那么有没有一种傻瓜式的方法或软件,它不仅包含了 busybox 的功能,而且里面还集成了各种软件,需要什么软件就选择什么软件,不需要我们去移植。答案肯定是有的,buildroot 就是这样一种工具。

1.1 下载buildroot#

Buildroot版本每2个月,2月,5月,8月和11月发布一次。版本号的格式为YYYY.MM,例如2013.02、2014.08。

可以从http://buildroot.org/downloads/获得发行包。

也可通过github仓库获取最新版本:

1
git clone git://git.busybox.net/buildroot

buildroot ubootLinux kernel 一样也支持图形化配置:
make menuconfig
image

1.2 buildroot目录结构#

1.2.0 buildroot源目录#

  • arch: CPU架构相关的配置脚本

  • board: 在构建系统时,board默认的boot和Linux kernel配置文件,以及一些板级相关脚本

  • boot: uboot配置脚本目录

  • configs: 板级配置文件,该目录下的配置文件记录着该机器平台或者方案使用的工具链,boot, kernel,各种应用软件包的配置

  • dl: download的简写,下载一些开源包。第一次下载后,下次就不会再去从官网下载了,而是从dl/目录下拿开源包,以节约时间

  • docs:

  • fs: 各种文件系统的自动构建脚本

  • linux: 存放Linux kernel的自动构建脚本

  • package: 第三方开源包的自动编译构建脚本,用来配置编译dl目录下载的开源包

  • support:

  • system: 存放文件系统目录的和设备节点的模板,这些模板会被拷贝到output/目录下,用于制作根文件系统rootfs

  • toolchain/ 目录中存放着各种制作工具链的脚本

1.2.1 编译出的output输出目录介绍#

  • images/存储所有映像(内核映像,引导加载程序和根文件系统映像)的位置。这些是您需要放在目标系统上的文件。

  • build/构建所有组件的位置(包括主机上Buildroot所需的工具和针对目标编译的软件包)。该目录为每个组件包含一个子目录。

  • host/包含为主机构建的工具和目标工具链。

  • staging/是到内部目标工具链host/的符号链接

  • target/它几乎包含了目标的完整根文件系统。除了设备文件/dev/(Buildroot无法创建它们,因为Buildroot不能以root身份运行并且不想以root身份运行)之外,所需的一切都存在。

1.3 配置 Target options#

1
2
3
4
5
6
7
Target options
-> Target Architecture = ARM (little endian)
-> Target Binary Format = ELF
-> Target Architecture Variant = cortex-A7
-> Target ABI = EABIhf
-> Floating point strategy = NEON/VFPv4
-> ARM instruction set = ARM

配置输出目标选项,架构,格式,浮点策略,指令集啊。配置好后如下:
image

1.4 配置工具链#

Buildroot为交叉编译工具链提供了两种解决方案:

1.4.1 内部工具链#

  • 内部工具链,称为Buildroot toolchainbuildroot 其实是可以自动下载交叉编译器的,但是都是从国外服务器下载的, 鉴于国内的网络环境,推荐大家设置成自己所使用的交叉编译器(也就是外部工具链)。
    image

1.4.2 外部工具链#

  • 外部工具链External toolchain
1
2
3
4
5
6
7
8
9
10
11
12
13
Toolchain
-> Toolchain type = External toolchain
-> Toolchain = Custom toolchain //选择用户自己的交叉编译器
-> Toolchain origin = Pre-installed toolchain //选择预装的编译器,否则Toolchain to be downloaded and installed
-> Toolchain path =/usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf
-> Toolchain prefix = $(ARCH)-linux-gnueabihf //前缀
-> External toolchain gcc version = 4.9.x
-> External toolchain kernel headers series = 4.1.x
-> External toolchain C library = glibc/eglibc
-> [*] Toolchain has SSP support? (NEW) //选中
-> [*] Toolchain has RPC support? (NEW) //选中
-> [*] Toolchain has C++ support? //选中
-> [*] Enable MMU support (NEW) //选中

Toolchain:设置为 Custom toolchain,表示使用用户自己的交叉编译器。
Toolchain origin:设置为 Pre-installed toolchain,表示使用预装的交叉编译器。
Toolchain path:设置自己安装的交叉编译器绝对路径!buildroot 要用到。
Toolchain prefix:设置交叉编译器前缀,要根据自己实际所使用的交叉编译器来设置,比如我们使用的是 arm-linux-gnueabihf-gcc,因此前缀就是$(ARCH)-linux-gnueabihf,其中 ARCH 我们前面已经设置为了 arm。
image

1.5 配置 build options#

编译选项,编译第三方插件使用静态还是动态链接等。

1.6 配置 System configuration#

系统配置,比如开发板名字、欢迎语、用户名、密码等。

1
2
3
4
5
6
7
System configuration
-> System hostname = alpha_imx6ull //平台名字,自行设置
-> System banner = Welcome to alpha i.mx6ull //欢迎语
-> Init system = BusyBox //使用 busybox
-> /dev management = Dynamic using devtmpfs + mdev //使用 mdev
-> [*] Enable root login with password (NEW) //使能登录密码
-> Root password = 123456 //登录密码为 123456

1.7 配置 Filesystem images#

根文件系统格式。

1
2
3
4
-> Filesystem images
-> [*] ext2/3/4 root filesystem //如果是 EMMC 或 SD 卡的话就用 ext3/ext4
-> ext2/3/4 variant = ext4 //选择 ext4 格式
-> [*] ubi image containing an ubifs root filesystem //如果使用 NAND 的话就用 ubifs

image

1.8 禁止编译 Linux 内核和 uboot#

一版不建议uboot和kernel也用buildroot。buildroot 不仅仅能构建根文件系统,也可以编译 linux 内核和 uboot。

buildroot如果开启了uboot和Linux内核的编译,会自动下载最新的 linux 内核和 uboot,那么最新的内核和uboot会对编译器版本号有要求,可能导致编译失败。

1
2
-> Kernel
-> [ ] Linux Kernel //不要选择编译 Linux Kernel 选项!

image

image

1.9 配置 Target packages#

配置要选择的第三方库或软件、比如 alsa-utils、ffmpeg、iperf等工具。
image

2 编译buildroot#

2.1 make help#

可以看到buildroot下make的使用细节,包括对package、uclibc、busybox、linux以及文档生成等配置。

2.2 make print-version#

打印buildroot版本号

2.3 make menuconfig#

或者(make linux-menuconfig…):进行图形化配置

2.4 make xxxx_defconfig#

1
2
3
4
5
6
7
8
9
10
Buildroot_2020.02.x/configs$ ls
100ask nexbox_a95x_defconfig
100ask_imx6ull_mini_ddr512m_systemV_core_defconfig nitrogen6sx_defconfig
100ask_imx6ull_mini_ddr512m_systemV_qt5_defconfig nitrogen6x_defconfig
100ask_imx6ull_pro_ddr512m_systemV_core_defconfig nitrogen7_defconfig
100ask_imx6ull_pro_ddr512m_systemV_qt5_defconfig nitrogen8m_defconfig
100ask_stm32mp157_pro_ddr512m_busybox_core_defconfig odroidxu4_defconfig
100ask_stm32mp157_pro_ddr512m_systemD_core_defconfig olimex_a10_olinuxino_lime_defconfig
100ask_stm32mp157_pro_ddr512m_systemD_qt5_defconfig olimex_a13_olinuxino_defconfig
100ask_stm32mp157_pro_ddr512m_systemV_core_defconfig olimex_a20_olinuxino_lime2_defconfig

make 100ask_imx6ull_pro_ddr512m_systemV_core_defconfig即可产生.configoutput目录
image

2.5 make#

sudo make //注意不能-jxxx,来指定多核编译

make命令通常将执行以下步骤:

  1. 下载源文件(根据需要);
  2. 配置、构建和安装交叉编译工具链,或仅导入外部工具链;
  3. 配置、构建和安装选定的目标软件包;
  4. 构建内核映像(如果选择);
  5. 构建引导加载程序映像(如果选择);
  6. 以选定的格式创建一个根文件系统
  • make clean:delete all build products (including build directories, host, staging and target trees, the images and the toolchain)
  • make distclean: 等于make clean+删除配置
  • make show-targets:显示出本次配置所要编译所有的目标
  • make pkg-target:单独编译某个pkg模块
  • make pkg-rebuild:重新编译pkg
  • make pkg-extract:只下载解压pkg,不编译,pkg解压后放在 output/build/对应的pkg-dir目录下
  • make pkg-source:只下载某pkg,然后不做任何事情
  • make list-defconfigs:例举所有可用的defconfigs。
  • make xxx_menuconfig:比如make linux-menuconfig

image

rootfs.tar就是编译出的根文件系统,解压缩后就能使用。

2.5.1 nfs 挂载根文件系统#

1
2
3
setenv bootargs 'console=tty1 console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.1.253:
/home/zuozhongkai/linux/nfs/buildrootfs rw ip=192.168.1.251:192.168.1.253:192.168.1.1:255.255.
255.0::eth0:off'

image

image

可以看到能进入rootfs,但是驱动ko和第三方软件和库没有。

2.6 make show-targets#

make show-targets显示出本次配置所要编译所有的目标。
image

3 buildroot框架原理#

Buildroot提供了函数框架和变量命令框架,采用它的框架编写的app_pkg.mk这种Makefile格式的自动构建脚本,将被package/pkg-generic.mk 这个核心脚本展开填充到buildroot主目录下的Makefile中去。

最后make all执行Buildroot主目录下的Makefile,生成你想要的image。 package/pkg-generic.mk中通过调用同目录下的pkg-download.mkpkg-utils.mk文件,已经帮你自动实现了下载、解压、依赖包下载编译等一系列机械化的流程。

你只要需要按照格式写app_pkg.mk,填充下载地址,链接依赖库的名字等一些特有的构建细节即可。 总而言之,Buildroot本身提供构建流程的框架,开发者按照格式写脚本,提供必要的构建细节,配置整个系统,最后自动构建出你的系统。

3.1 添加自己的软件包#

3.1.1 package/Config.in总入口添加菜单#

添加如下语句:

1
2
3
menu "myown(fuzidage) package"
source "package/helloworld/Config.in"
endmenu

为自己的软件包添加入口,这样在make menuconfig的时候就可以找到自己的软件包的Config.in,如果在make menuconfig的时候选中helloworld,那么"BR2_PACKAGE_HELLOWORLD=y"也会同步到.config中去。

3.1.2 配置APP对应的Config.in和mk文件#

package中新增目录helloworld,并在里面添加Config.inhelloworld.mk

3.1.2.1 Config.in#

1
2
3
4
config BR2_PACKAGE_HELLOWORLD
bool "helloworld"
help
This is a demo to add myown(fuzidage) package.

helloworld/Config.in文件,可以通过make menuconfig可以对helloworld进行选择。只有在BR2_PACKAGE_HELLOWORLD=y条件下,才会调用helloworld.mk进行编译

3.1.2.2 helloworld.mk#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
################################################################################
#
# helloworld
#
################################################################################
HELLOWORLD_VERSION:= 1.0.0
HELLOWORLD_SITE:= $(CURDIR)/work/helloworld
HELLOWORLD_SITE_METHOD:=local
HELLOWORLD_INSTALL_TARGET:=YES

define HELLOWORLD_BUILD_CMDS
$(MAKE) CC="$(TARGET_CC)" LD="$(TARGET_LD)" -C $(@D) all
endef

define HELLOWORLD_INSTALL_TARGET_CMDS
$(INSTALL) -D -m 0755 $(@D)/helloworld $(TARGET_DIR)/bin
endef

define HELLOWORLD_PERMISSIONS
/bin/helloworld f 4755 0 0 - - - - -
endef
$(eval $(generic-package))

helloworld.mk包括源码位置、安装目录、权限设置等。

3.1.3 编写APP源码和Makefile#

创建一个work/helloworld目录,建立hello_world.cmakefile

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main(){
printf("hello world\n");
return 0;
}
all: helloworld
helloworld: helloworld.o
$(CC) -o helloworld helloworld.o
clean:
rm -rf *.o
rm -rf helloworld
install:
$(INSTALL) -D -m 0755 helloworld $(TARGET_DIR)/bin

3.1.4 通过make menuconfig选中APP#

通过上面对package/Config.in入口的配置, 我们可以通过make menuconfig,进入Target packages可以看见多了一个"myown(fuzidage) package"入口,选中,保存配置到.config
image

然后make savedefconfig,对helloworld的配置就会保存到对应的xxx_defconfig中。
image

3.1.5 编译使用APP#

可以和整个平台一起编译APP;或者make helloworld单独编译。
image

编译过程中,会被拷贝到output/build/helloworld-1.0.0文件夹中。然后生成的bin文件拷贝到output/target/bin/helloworld,这个文件会打包到文件系统中。
image

如果需要清空相应的源文件,通过make helloworld-dirclean

image

3.2 如何重新编译软件包#

经过第一次完整编译后,如果我们需要对源码包重新配置,我们不能直接在buildroot上的根目录下直接make,buildroot是不知道你已经对源码进行重新配置,它只会将第一次编译出来的文件,再次打包成根文件系统镜像文件。

那么可以通过以下2种方式重新编译:

  1. 直接删除源码包,然后make all

    1
    2
    例如我们要重新编译helloworld,那么可以直接删除output/build/helloworld目录,
    那么当你make的时候,就会自动从dl文件夹下,解压缩源码包,并重新安装。这种效率偏低
  2. 进行xxx-rebuild,然后make all

    1
    2
    也是以helloworld为例子,我们直接输入make helloworld-rebuild,
    即可对build/helloworld/目录进行重新编译,然后还要进行make all(或者make world 或者 make target-post-image)
  3. 如果要重新配置编译安装:

    1
    make <package>-reconfigure; make all

3.3 使能第三方软件和库#

前面 1.9配置Targetpackages 有引入介绍。

3.3.1 使能音频的ALSA库套件#

image

3.3.2 使能busybox套件#

image

使能后,buildroot 会自动下载 busybox 压缩包,buildroot 下载的源码压缩包都存 放在/dl 目录下,在 dl 目录下就有一个叫做“busybox”的文件夹,此目录下保存着 busybox 压 缩包:
image

make all编译完后, buildroot 将所有解压缩后的软件保存在/output/build 软件中,我们可以找到/output/build/busybox-1.29.3 这个文件夹,此文件夹就是解压后的 busybox 源码:
image
image

3.3.2.1 修改配置busybox套件#

修改busybox源码就直接在/output/build/busybox-1.29.3修改。

make busybox-menuconfig可以配置busybox套件选择哪些功能:
image

3.3.2.2 rebuild busybox套件#

make busybox或者make busybox-rebuild即可重新编译。

编译完后还要make或者make target-post-image对其进行打包进根文件系统。

3.3.3 PS1环境变量#

我们构建的根文件系统启动以后会发现, 输入命令的时候命令行前面一直都是“#”,如果我们进入到某个目录的话前面并不会显示当前目录路径:
image

PS1 用于设置命令提示符格式,格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PS1 = ‘命令列表’
命令列表中可选的参数如下:
\! 显示该命令的历史记录编号。
\# 显示当前命令的命令编号。
\$ 显示$符作为提示符,如果用户是 root 的话,则显示#号。
\\ 显示反斜杠。
\d 显示当前日期。
\h 显示主机名。
\n 打印新行。
\nnn 显示 nnn 的八进制值。
\s 显示当前运行的 shell 的名字。
\t 显示当前时间。
\u 显示当前用户的用户名。
\W 显示当前工作目录的名字。
\w 显示当前工作目录的路径

我们打开/etc/profie,修改成如下:
image

1
2
PS1='[\u@\h]:\w$:'
export PS1

3.4 单独生成目标(build out of tree)#

make O=/home/XXX/output

4 buildroot官方教程链接#

buildroot官方训练教程

buildroot中文手册

image-20240814004339288

可以下载正点原子翻译的中文版buildroot手册。

5 附录#

5.1 buildroot编译log#

1
2
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v7_defconfig V=1 > log.1 2>&1
make V=1 > buildroot.build.log 2>&1

见附件:https://files.cnblogs.com/files/fuzidage/buildroot.build.rar?t=1724326727&download=true

Linux内核-异常输出函数调用栈calltrace分析

1 dump_stack函数#

打印内核调用堆栈。举个例子:

我们定义四个函数aaabbbcccddd,然后bbb中调用aaaccc中调用bbbddd函数谁都不调用。在入口函数中,我们调用cccddd函数,看看堆栈打印效果如何:

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
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/delay.h>
void aaa(void) {
printk(KERN_EMERG "aaa\n");
dump_stack();
msleep(100);
}
void bbb(void) {
printk(KERN_EMERG "bbb\n");
aaa();
msleep(100);
}
void ccc(void) {
printk(KERN_EMERG "ccc\n");
bbb();
msleep(100);
}

void ddd(void) {
printk(KERN_EMERG "ddd\n");
msleep(100);
}
static int __init chrdevTest_init(void) {
printk(KERN_EMERG "INIT func\r\n");
ccc();
ddd();

return 0;
}
static void __exit chrdevTest_exit(void) {
printk(KERN_EMERG "EXIT func\r\n");
}
module_init(chrdevTest_init);
module_exit(chrdevTest_exit);
MODULE_LICENSE("GPL");

可以看到当打印完aaa后开始dump_stack, 打印出函数调用栈。

image-20240727224310304

2 内核态异常call trace等级#

内核态call trace 有三种出错情况,分别是bug, oopspanic

1、 bug- bug只是提示警告
BUG: sleeping function called from invalid context at …, 比如在原子上下文中休眠,总断服务函数休眠,spin_lock中进行might_sleep等。

我在某个设备驱动的中断处理函数 XXX_ISR() 里加了 msleep(10) 之后:

image-20240727230940026

可以看到跑出了BUG打印,为什么是BUG: scheduling while atomic呢?而不是BUG: sleeping function called from invalid context at …

那是因为在原子上文中发生了调度,我们调用might_sleep是会时间片到了啊,让出CPU自然就进行了schedule。

BUG: spinlock bad magic on CPU错误表示自旋锁使用时没有初始化。

2、 Oops- oops会终止进程,但是不会系统崩溃
程序在内核态进入一种异常情况,比如引用非法指针导致的数据异常,数组越界导致的取指异常,此时异常处理机制能够捕获此异常,并将系统关键信息打印到串口上,正常情况下Oops消息会被记录到系统日志中去。

3、 Panic -panic系统崩溃
当Oops发生在中断上下文中或者在进程0和1中,系统将彻底挂起,因为中断服务程序异常后,将无法恢复,这种情况即称为内核panic。

2.1 WARN_ON函数#

我们把上面的实验aaa函数中dump_stack改成WARN_ON(1)函数。可以看到WARN_ON(1)就是调用了dump_stack,多了绿色打印部分而已:

image-20240727232452233

image-20240727232843642

注意只有当condition=1时才会真正调用__warn:

:

2.2 BUG_ON函数#

BUG_ON这句,一旦执行就会抛出oops,导致栈的回溯和错误信息的打印,大部分体系结构把BUG()BUG_ON()定义成某种非法操作,这样自然会产生需要的oops。类似一种断言,让进程终止。我们把上面的实验aaa函数中dump_stack改成BUG_ON(1)函数:

image-20240727233540771

2.3 panic函数#

当Oops发生在中断上下文中或者在进程0和1中,系统将彻底挂起,因为中断服务程序异常后,将无法恢复,这种情况即称为内核panic

3 Ftrace工具集#

FtraceFunction Trace的简写。它是一个内核函数追踪工具,旨在帮助内核设计和开发人员去追踪系统内部的函数调用流程。

还可以用来调试和分析系统的延迟和性能问题,并发展成为一个追踪类调试工具的框架:

image-20240728145336807

可以看到还包括了用户态的ltraceftrace

3.1 Ftrace是如何记录信息的#

Ftrace采用了静态插桩和动态插桩两种方式来实现。

3.1.1 静态插桩#

Kernel中打开了CONFIG_FUNCTION_TRACER功能后,会增加一个-pg的一个编译选项,这样每个函数入口处,都会插入bl mcount跳转指令,使得每个函数运行时都会进入mcount函数。

既然每个函数都静态插桩,这带来的性能开销是惊人的,有可能导致人们弃用Ftrace功能。为了解决这个问题,开发者推出了Dynamic ftrace,以此来优化整体的性能。

3.1.2 动态插桩#

既然静态插桩记录这些可追踪的函数,为了减少性能消耗,将跳转函数替换为nop指令,动态将被调试函数的nop指令,替换为跳转指令,以实现追踪。

3.2 使能Ftrace#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CONFIG_FTRACE=y                             # 启用了 Ftrace
CONFIG_FUNCTION_TRACER=y # 启用函数级别的追踪器
CONFIG_HAVE_FUNCTION_GRAPH_TRACER=y # 表示内核支持图形显示
CONFIG_FUNCTION_GRAPH_TRACER=y # 以图形的方式显示函数追踪过程
CONFIG_STACK_TRACER=y # 启用堆栈追踪器,用于跟踪内核函数调用的堆栈信息。
CONFIG_DYNAMIC_FTRACE=y # 启用动态 Ftrace,允许在运行时启用和禁用 Ftrace 功能。
CONFIG_HAVE_FTRACE_NMI_ENTER=y # 表示内核支持非屏蔽中断(NMI)时进入 Ftrace 的功能
CONFIG_HAVE_FTRACE_MCOUNT_RECORD=y # 表示内核支持通过 mcount 记录函数调用关系。
CONFIG_FTRACE_NMI_ENTER=y # 表示内核支持通过 mcount 记录函数调用关系。
CONFIG_FTRACE_SYSCALLS=y # 系统调用的追踪
CONFIG_FTRACE_MCOUNT_RECORD=y # 启用 mcount 记录函数调用关系。
CONFIG_SCHED_TRACER=y # 支持调度追踪
CONFIG_CONTEXT_SWITCH_TRACER #使能上下文切换追踪功能,可以用来跟踪进程之间的切换。
CONFIG_NOP_TRACER #使能空操作追踪功能,可以用来在不需要追踪的情况下占位。
CONFIG_FUNCTION_PROFILER=y # 启用函数分析器,主要用于记录函数的执行时间和调用次数
CONFIG_DEBUG_FS=y # 启用 Debug 文件系统支持

上述配置不一定全部打开,勾选自己需要的即可,通常我们选择CONFIG_FUNCTION_TRACERCONFIG_HAVE_FUNCTION_GRAPH_TRACER即可,然后编译烧录到开发板。

通过make menuconfig的方式写入:

1
2
3
4
5
Kernel hacking  --->
Tracers ─>
[*] Kernel Function Tracer
[*] Kernel Function Graph Tracer (NEW)
// (下面还有几个追踪器的选项,可以根据自己的需要选择)

Ftrace 通过 debugfs 向用户态提供了访问接口,所以还需要将 debugfs 编译进内核:

1
2
Kernel hacking  --->
-*- Debug Filesystem

3.2.1 挂载debugfs#

1
2
3
#用户态需要挂载debugfs,or通过配置修改etc/fstab文件
mount -t debugfs none /sys/kernel/debug
或者 mount -t tracefs nodev /sys/kernel/tracing

img

我们能够在/sys/kernel/debug下看到内核支持的所有的调试信息:

1
2
3
4
5
6
7
8
9
10
11
# cd /sys/kernel/debug/
# ls
asoc gpio regmap
bdi ieee80211 sched_debug
block memblock sched_features
clk mmc0 sleep_time
device_component mmc1 suspend_stats
devices_deferred mtd tracing
dma_buf opp ubi
extfrag pinctrl ubifs
fault_around_bytes pm_qos wakeup_sources

3.3 Ftrace 使用#

3.3.1 /sys/kernel/tracing介绍#

image-20240728163630564

3.3.2 trace和trace_pipe使用#

cat trace_pipe是堵塞读取,有数据就读,没数据就等待。

打开关闭追踪:

1
2
3
echo 1 > tracing_on             // 打开跟踪
do_someting
echo 0 > tracing_on // 关闭跟踪
1
2
cat trace  > /tmp/log //一次性导出log
cat trace_pipe > /tmp/log &//后台导出log
1
2
3
4
5
6
7
8
9
10
11
12
cat current_tracer                    // 查看当前追踪器
cat available_tracers // 查看当前内核中可用跟踪器
cat available_events // 查看当前内核中可用事件
cat available_filter_functions
// 查看当前内核中可用函数,可以被追踪的函数列表,
// 即可以写到 set_ftrace_filter,set_ftrace_notrace,set_graph_function,
// set_graph_notrace 文件的函数列表

echo function > current_tracer // 选用 function 追踪器,
echo function_graph > current_tracer // 选用 function_graph 追踪器,
echo [func] > set_ftrace_filter // 选择追踪指定 [func] 函数的调用栈
echo [pid] > set_ftrace_pid // 选择追踪指定 [pid] 进程的调用栈

3.3.2.1 选用函数追踪#

image-20240728153553129

3.3.2.2 选用图像化函数追踪#

image-20240728153651223

3.3.2.3 选用动态过滤追踪#

image-20240728155333300

3.3.2.4 追踪特定进程#

1
2
3
4
 echo 0 > tracing_on                                 # 关闭追踪器
echo function > current_tracer # 设置当前追踪类别

echo > trace; echo $$ > set_ftrace_pid; echo 1 > tracing_on; your_command; echo 0 > tracing_on

$$表示当前bash的pid,这样可以追踪任意命令。

如果我们要抓执行a.out的trace信息,那么先要获取到a.out程序的pid。

为什么要写成一条语句?

因为ftrace当打开时,在没有过滤的情况下,瞬间会抓取到内核所有的函数调用,为了更准确的抓取我们执行的命令,所以需要打开trace,执行完命令后,马上关闭。

3.3.2.5 追踪特定函数#

1
echo 1 > options/func_stack_trace
1
2
3
4
5
6
7
echo 0 > tracing_on									# 关闭追踪器
cat available_filter_functions | grep "xxxxxx" # 搜索函数是否存在
echo xxxxxx > set_ftrace_filter # 设定追踪的函数
echo function > current_tracer # 设置当前追踪类别
echo 1 > options/func_stack_trace # 记录堆栈信息
echo > trace # 清空缓存
echo 1 > tracing_on

查看结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# cat trace
# tracer: function
#
# entries-in-buffer/entries-written: 2/2 #P:3
#
# _-----=> irqs-off
# / _----=> need-resched
# | / _---=> hardirq/softirq
# || / _--=> preempt-depth
# ||| / delay
# TASK-PID CPU# |||| TIMESTAMP FUNCTION
# | | | |||| | |
kworker/1:1-59 [001] .... 168.954199: mmc_rescan <-process_one_work
kworker/1:1-59 [001] .... 168.954248: <stack trace>
=> mmc_rescan
=> process_one_work
=> worker_thread
=> kthread
=> ret_from_fork
=> 0

3.3.2.6 追踪特定ko模块#

编译ko需要加上编译参数-pg。否则你在available_filter_functions列表中,查找不到你想要的函数。

1
2
3
# 示例
Format: :mod:<module-name>
example: echo :mod:ext3 > set_ftrace_filter

追踪ext3模块内的所有函数。

3.3.2.7 重置追踪#

1
2
echo 0 > tracing_on         # 关闭trace
echo > trace # 清空当前trace记录

3.3.2.8 事件追踪#

查看事件:

1
2
3
4
5
6
7
8
9
root@100ask:/sys/kernel/debug/tracing/events# ls
alarmtimer exceptions i2c migrate power signal
block ext4 initcall mmc printk skb
enable hyperv mdio percpu

root@100ask:/sys/kernel/debug/tracing/events/sched# ls
enable sched_move_numa sched_process_free sched_stat_runtime sched_switch
filter sched_pi_setprio sched_process_hang sched_stat_sleep
sched_kthread_stop sched_process_exec sched_process_wait sched_stat_wait

追踪一个/若干事件:

1
2
3
4
5
6
7
8
9
10

# echo 1 > events/sched/sched_wakeup/enable
...(省略追踪过程)

# cat trace | head -10
# tracer: nop
#TASK-PID CPU# TIMESTAMP FUNCTION
# || | |
bash-2613 [001] 425.078164: sched_wakeup: task bash:2613 [120] success=0 [001]
bash-2613 [001] 425.078184: sched_wakeup: task bash:2613 [120] success=0 [001]

追踪所有事件:

1
2
3
4
5
6
7
8
9
10
# echo 1 > events/enable
...

# cat trace | head -10
# tracer: nop
#TASK-PID CPU# TIMESTAMP FUNCTION
# | | | |
cpid-1470 [001] 794.947181: kfree: call_site=ffffffff810c996d ptr=(null)
acpid-1470 [001] 794.947182: sys_read -> 0x1

3.3.2.n trace_printk函数使用#

内核头文件 include/linux/kernel.h 中描述了 ftrace 提供的工具函数的原型,这些函数包括 trace_printktracing_on/tracing_off 等。

3.4 引入用户态ltrace和strace#

3.4.1 ltrace#

跟踪进程调用C库函数的情况。

常用的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
-a : 对齐具体某个列的返回值。
-c : 计算时间和调用,并在程序退出时打印摘要。
-d : 打印调试信息。
-h : 打印帮助信息。
-i : 打印指令指针,当库调用时。
-l : 只打印某个库中的调用。
-o, --output=file : 把输出定向到文件。
-p : PID 附着在值为PID的进程号上进行ltrace。
-r : 打印相对时间戳。
-S : 显示系统调用。
-t, -tt, -ttt : 打印绝对时间戳。
-T : 输出每个调用过程的时间开销。
-V, --version : 打印版本信息,然后退出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(void) {
int n = 10;
int *arr = (int *)malloc(n * sizeof(int));
if (arr == NULL) {
printf("failed!\r\n");
return 1;
}
memset(arr, 2, n*sizeof(int));
for (int i = 0; i < n; i++) {
printf("%d\t", arr[i]);
}
return 0;
}

1.查看c库调用:

image-20240728165618548

2.查看c库调用次数:

image-20240728165742636

3.查看c库执行时间:

image-20240728165842926

4.查看系统调用情况:

image-20240728170027545

3.4.2 strace#

跟踪进程系统调用System Call使用情况。

常用的参数如下:

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
-c 统计每一系统调用的所执行的时间,次数和出错的次数等.
-d 输出strace关于标准错误的调试信息.
-f 跟踪由fork调用所产生的子进程.
-ff 如果提供-o filename,则所有进程的跟踪结果输出到相应的filename.pid中,pid是各进程的进程号.
-F 尝试跟踪vfork调用.在-f时,vfork不被跟踪.
-h 输出简要的帮助信息.
-i 输出系统调用的入口指针.
-q 禁止输出关于脱离的消息.
-r 打印出相对时间关于,,每一个系统调用.
-t 在输出中的每一行前加上时间信息.
-tt 在输出中的每一行前加上时间信息,微秒级.
-ttt 微秒级输出,以秒了表示时间.
-T 显示每一调用所耗的时间.
-v 输出所有的系统调用.一些调用关于环境变量,状态,输入输出等调用由于使用频繁,默认不输出.
-V 输出strace的版本信息.
-x 以十六进制形式输出非标准字符串
-xx 所有字符串以十六进制形式输出.
-a column 设置返回值的输出位置.默认 为40.
-e expr 指定一个表达式,用来控制如何跟踪.格式:[qualifier=][!]value1[,value2]...
qualifier只能是 trace,abbrev,verbose,raw,signal,read,write其中之一.value是用来限定的符号或数字.默认的 qualifier是 trace.感叹号是否定符号.例如:-eopen等价于 -e trace=open,表示只跟踪open调用.而-etrace!=open 表示跟踪除了open以外的其他调用.有两个特殊的符号 all 和 none. 注意有些shell使用!来执行历史记录里的命令,所以要使用\\.
-e trace=set 只跟踪指定的系统 调用.例如:-e trace=open,close,rean,write表示只跟踪这四个系统调用.默认的为set=all.
-e signal=set 指定跟踪的系统信号.默认为all.如 signal=!SIGIO(或者signal=!io),表示不跟踪SIGIO信号.
-e read=set 输出从指定文件中读出 的数据.例如: -e read=3,5
-e write=set 输出写入到指定文件中的数据.
-o filename 将strace的输出写入文件filename
-p pid 跟踪指定的进程pid.

查看系统调用的时间:

image-20240728170644866

ToolChain工具链命令介绍

1 ToolChain官方下载地址#

下载地址: https://releases.linaro.org/components/toolchain/binaries/4.9-2017.01/arm-linux-gnueabihf/

2 readelf#

2.1 elf格式#

elf是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储格文件的文件格式。是UNIX系统实验室(USL)作为应用程序二进制接口(Application Binary Interface,ABI)而开发和发布的,也是Linux的主要可执行文件格式。
image

2.1.1 readelf命令#

readelf是一个读取elf格式的命令,比如下图是一个vmlinux原始elf文件,ARM 32bit LSB格式,使用静态库。使用readelf -h可以查看elf header信息。无论elf是什么格式,elf header只是描述头部信息,因此用什么工具链的命令都一样。
image
image
用hexdump看头部果然位7f 45 4c 46
image

再看Entry point address: 0x8000,8000这个表示加载入口点。

2.2 elf文件类型#

elf 文件通常有三种类型: ①可重定位目标文件 ②可执行文件 ③动态库

2.2.1 可重定位目标文件#

实际上就是在编译过程中生成的 .o 文件,或者是静态库文件。
image
elf头部:整个文件的描述+目录
sections:也就是我们经常提到的代码段、数据段所存放的位置。
section headers table:对文件中所有的段进行描述,段的起始地址,大小等信息
符号表以及字符串表: 注意这里的字符串表并不是应用程序中定义的字符串内容,而是编译时的一些符号字符串,比如 printf、main、.test、.bss 等

2.2.1.1 读头部#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
root@hd:~# readelf -h foo.o

ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: ARM
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 264 (bytes into file)
Flags: 0x5000000, Version5 EABI
Size of this header: 52 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 40 (bytes)
Number of section headers: 10
Section header string table index: 7

头部对应的C语言中结构体,这个结构体可以在 readelf 的源代码中找到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct {
unsigned char e_ident[16]; /* ELF "magic number" */
unsigned char e_type[2]; /* Identifies object file type */
unsigned char e_machine[2]; /* Specifies required architecture */
unsigned char e_version[4]; /* Identifies object file version */
unsigned char e_entry[4]; /* Entry point virtual address */
unsigned char e_phoff[4]; /* Program header table file offset */
unsigned char e_shoff[4]; /* Section header table file offset */
unsigned char e_flags[4]; /* Processor-specific flags */
unsigned char e_ehsize[2]; /* ELF header size in bytes */
unsigned char e_phentsize[2]; /* Program header table entry size */
unsigned char e_phnum[2]; /* Program header table entry count */
unsigned char e_shentsize[2]; /* Section header table entry size */
unsigned char e_shnum[2]; /* Section header table entry count */
unsigned char e_shstrndx[2]; /* Section header string table index */
} Elf32_External_Ehdr;

**e_ident(magic部分)**:
e_ident 是一个包含 16 字节的数组成员,对应 readelf -h 给出的 magic 部分,关于这一部分就需要参考 readelf 源码来进行分析了,分析结果如下:

1
2
3
4
5
6
7
前四个字节:7f 45 4c 46,识别码, 0x450x4c0x46 三个字节的 ascii 码对应 ELF 字母,通过这四个字节就可以判断文件是不是 elf 文件.
第五个字节:其中 01 表示 32 位 elf 文件,02 表示 64 位.
第六个字节:其中 01 表示 小端模式,02 表示 大端模式.
第七个字节:表示 EI_version,1 表示 EV_CURRENT,只有 1 才是合理的(代码中是 EI_versoin,但是博主没有进一步具体研究).
第八个字节: 00 表示 OS_ABI
第九个字节: 00 表示 ABI version
其它字段,源码中没有找到对应的解析,暂定为reserver.

e_type(elf类型)

  • 可重定位的目标文件
  • 可执行文件
  • 动态链接文件
  • coredump 文件,这是系统生成的调试文件.

这四种类型的文件各有各的特点,比如可重定位的目标文件针对的是链接器.

而可执行文件针对加载器,需要被静态加载到内存中执行,而动态链接文件则是运行过程中的加载.

e_machine(机器架构)

标识指定的机器,比如 40 代表 ARM.

e_entry(程序入口虚拟地址)

程序的入口虚拟地址,对于可重定位的目标文件默认是0,而对于可执行文件而言是真实的程序入口.

程序入口是被加载器使用的,在程序加载过程中会读取该程序入口,作为应用程序的开始执行地址,在实际的加载过程中,内核加载完当前 elf 可执行文件之后其实并不是跳到该入口地址,而是先执行动态链接器代码,在动态链接完成之后才会跳到该入口地址。

e_phoff

四个字节的 program headers 的起始偏移地址

e_shoff

四个字节的 section headers 的起始偏移地址

e_ehsize

指示 elf header 的 size,对于 arm 而言,52 或者 64.

2.2.1.2 读section#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
readelf -S foo.o

There are 10 section headers, starting at offset 0x108:

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 000010 00 AX 0 0 4
[ 2] .data PROGBITS 00000000 000044 000004 00 WA 0 0 4
[ 3] .bss NOBITS 00000000 000048 000004 00 WA 0 0 4
[ 4] .comment PROGBITS 00000000 000048 000033 01 MS 0 0 1
[ 5] .note.GNU-stack PROGBITS 00000000 00007b 000000 00 0 0 1
[ 6] .ARM.attributes ARM_ATTRIBUTES 00000000 00007b 000035 00 0 0 1
[ 7] .shstrtab STRTAB 00000000 0000b0 000055 00 0 0 1
[ 8] .symtab SYMTAB 00000000 000298 0000f0 10 9 11 4
[ 9] .strtab STRTAB 00000000 000388 000027 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)

一共10个段。

2.2.2 可执行文件#

可执行文件是给加载器使用,而可重定位目标文件是给链接器使用。

2.2.2.1 读头部#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
readelf -h foo

ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: ARM
Version: 0x1
Entry point address: 0x82e1
Start of program headers: 52 (bytes into file)
Start of section headers: 4508 (bytes into file)
Flags: 0x5000402, has entry point, Version5 EABI, hard-float ABI
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 9
Size of section headers: 40 (bytes)
Number of section headers: 30
Section header string table index: 27

2.2.1 可重定位目标文件对比,有以下几点不同:

  • Type 由 REL (Relocatable file) 变成了 EXEC (Executable file),表示这是一个可执行文件.
  • Entry point address,即程序入口为 0x82e1 而不再是 0,表示程序在加载时需要将入口代码放到该地址执行.
  • 多了一个 Program header,起始偏移地址为 52,紧随着 elf header.
  • section 的数量增加到了 30 个,这是因为程序在链接过程中不仅仅包含用户编写的源代码,还会链接 glibc 库,增加的那些 section 从 glibc 而来,同时增加了 9 个 Program header,表示该程序有 9 个 segment.

2.2.3 动态库文件#

2.2.3.1 位置无关码#

当静态链接时,链接器为每条指令和数据分配独立的地址空间,当指令要访问数据时,访问的是数据的绝对地址。因此每个进程使用的库独立。

当动态链接时,多个进程共享一块内存的.text, 即共享库。因此使用相对地址,共享库相对于某个进程A,进程B,进程C有一个偏移量。

https://fuzidage.github.io/2024/04/15/s3c2440%E8%A3%B8%E6%9C%BA%E7%BC%96%E7%A8%8B-%E4%BB%A3%E7%A0%81%E9%87%8D%E5%AE%9A%E4%BD%8D%E5%92%8C%E6%B8%85bss/

2.2.3.2 读头部#

使用 readelf -h 命令查看动态库的 elf 头,差异如下:

  • 文件类型不一样,动态库的类型为 DNY(Shared object file)。
  • 从动态库中段的组成来看,它和可执行文件几乎是差不多的,同时包含了 .init,.fini,.init_array,.fini_array,.text 等段内容,同时也有 segment table,这两者最大的差别在于:动态库的重定位过程放到加载时完成,因此其每个段,每个 segment 对应的虚拟地址都是不确定的,逻辑上从 0 开始。
  • 动态库中的代码都是位置无关代码,这是由动态库共享的特性决定的
  • 静态链接中,符号表、重定位表、字符串表这些都是作为链接阶段的辅助,所以在加载过程中属于无用的信息,但是动态库中这些段需要辅助动态库进行运行时的符号解析以及重定位,在加载时同样会被加载到内存中。

3 objcopy#

objcopy刚好和readelf相反,readelf是提取头部,而objdump是裁去掉头部,只剩二进制文件中的代码段,数据段,bss等。

1
2
arm-linux-gnueabihf-objcopy -O binary -S -g led.elf led.bin
arm-linux-gnueabihf-objcopy -O binary -S ledc.elf $@

“binary”表示以二进制格式输出,选项“-S”表示不要复制源文件中的重定位信息和符号信息,“-g”表示不复制源文件中的调试信息

4 objdump#

反汇编.
image
image

1
2
3
arm-linux-gnueabihf-objdump -D led.elf > led.dis
arm-linux-gnueabihf-objdump -D -m arm ledc.elf > ledc.dis
aarch64-linux-gnu-objdump -S -d -l soph_ldc.ko >oops.asm

5 addr2line#

当程序出现crash掉后,会打印出异常信息地址,addr2line可以通过地址来用来对可执行程序找出源代码的文件函数行号具体位置。前提条件是gcc编译用-g选项。
image

6 size#

size命令用于查看elf文件的进程地址空间各段的大小.
image

7 nm#

nm命令用来查看可执行程序的符号表。
image
第1列表示符号地址,第2列表示符号类型,T表示全局的函数,D表示全局变量,d表示静态变量, t表示静态函数。如上图main函数就是T类型。第3列表示符号。

8 strip#

strip命令用来剔除符号表。可以看到对于可执行文件,一般都是包含符号表的,那么为了节省可执行程序空间,用strip可以剔除符号表。
image
剔除符号表后不影响可执行程序的功能。
下图可以看到strip后可执行文件变小了,用file命令查看状态变成了stripped, 用nm命令可以看到符号表已经消失。
image

9 strings#

查看可执行程序中的用双引号表示的字符串,比如printf("hello world\n"); 这里hello world就属于strings。
image

12 gcc#

源代码编译

1
2
3
arm-linux-gnueabihf-gcc -g -c led.s -o led.o
%.o:%.s
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<

12.1 常用选项#

参数 含义
-o 指定文件输出路径
-E 对源文件进行预处理操作,输出.i文件
-S 对源文件进行预处理、编译操作,输出.s文件
-c 对源文件进行预处理、编译、汇编操作,输出.o文件
-I 包含头文件路径 例:gcc -I src/include/
-L 添加链接库路径 例: gcc -L src/lib/
-l 链接库文件 例: gcc -llibA.so
-fPIC 生成位置无关代码(position-independent code)
-Wall 对代码所有可能有问题的地方发出警告
-g 在目标文件中嵌入调试信息,便于gdb调试
-v 打印出gcc编译一个文件的时候所有的步骤
-D 使用编译时的宏 例子:gcc -Wall -DMY_MACRO main.c -o main
-std 指定支持的c/c++标准 例:gcc -std=c++11 main.cpp
-static /-shared 静态编译文件(把动态库的函数和其它依赖都编译进最终文件) or 动态编译文件
-O(n) 优化等级

12.2 编译过程#

分4个阶段:预处理、编译、汇编、链接

12.2.1 预处理#

预编译阶段主要处理源文件中的以“#”开始的预编译指令。比如“#include”、“#define”。预处理会对头文件递归展开,宏定义进行替换。

gcc -E main.c -o main.i

image

12.2.2 编译#

生成相应的汇编代码.

1
gcc -S main.c -o main.s

12.2.2.1 强弱符号和强弱引用#

  1. 强符号屏蔽弱符号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void test_func(void);//test.h


#include <stdio.h>
void __attribute__((weak)) weak_func(void) {
printf("defualt weak func is running!\n");
}
void test_func(void) {
weak_func();
}// test.c申明一个弱符号weak_func



#include <stdio.h>
#include "test.h"
void weak_func(void) {
printf("custom strong func override!\n");
}

int main() {
test_func();
return 0;
}//main.c
1
2
3
4
gcc -c test.c
ar -rsc libtest.a test.o #将test.c编译成静态库

gcc main.c test.h -L. -ltest -o test

输出结果:

1
custom strong func override!

比如我们调用其他人的库,但是不得不自己去实现库里面的函数xxx, 那么就可以在对应头文件申明xxx是一个弱函数。

  1. 强弱引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void test_func(void);//test.h


include <stdio.h>
static __attribute__((weakref("test"))) void weak_ref(void);//申明weak_ref是test的一个弱引用。
void test_func(void)
if(weak_ref){
weak_ref();
} else{
printf("weak ref function is null\n");
}
}//test.c


#include <stdio.h>
#include "test.h"
void test(void)
printf("running custom weak ref function!\n");
}

int main()
test_func();
return 0;
}//main.c

输出结果:

1
running custom weak ref function!

当把弱引用声明去掉,输出结果:

1
running custom weak ref function!

12.2.3 汇编#

将汇编代码转换成二进制机器代码,也就是目标文件。汇编使用as指令完成的。

1
2
gcc -c main.s -o main.o
或者as main.s -o main.o

12.2.4 链接#

汇编将代码编译成了二进制文件,但还需要和系统其他组件(比如标准库、动态链接库等)结合起来才能正常运行,比如调用print函数打印。链接就是打包各种目标文件。

1
gcc hello.o sub.o display.o -o hello

12.2.4.1 链接器#

链接器,-Ttext表示链接地址,也就是运行地址。一般会用lds链接脚本来进行。

1
2
arm-linux-gnueabihf-ld -Ttext 0X87800000 led.o -o led.elf
arm-linux-gnueabihf-ld -Timx6ul.lds -o ledc.elf $^

12.2.4.2 动态静态库#

1
2
gcc -fpic -shared -o libhello.so hello.o
ar -rsc liba.a test1.o test2.o test3.o #静态库的打包

12.2.4.3 链接选项#

1
2
3
4
5
6
7
8
-L 指定链接库的目录
-l 指定需要链接的库名称
-T:-T 参数表示指定链接脚本,用户可以通过 ld -T file 来指定使用自己的链接脚本
-EB、-EL:指定大小端,这会覆盖掉系统默认的大小端设置
-s、--strip-all:丢弃可执行文件中的符号,以减小尺寸
-static:不使用动态库,静态地链接
-nostdlib:默认情况下链接标准库,该参数显示地指明不链接标准库。
-shared:创建一个动态库

12.2.4.n 链接脚本lds#

1 SECTIONS{
2 	. = 0X87800000;
3 	.text :
4 	{
5 		start.o 
6 		main.o 
7		 *(.text)
8 	}
9 	.rodata ALIGN(4) : {*(.rodata*)} 
10 	.data ALIGN(4) : { *(.data) } 
11	 __bss_start = .; 
12 	.bss ALIGN(4) : { *(.bss) *(COMMON) } 
13 	__bss_end = .;
14 }

第 2 行设置定位计数器为0X87800000,因为我们的链接地址就是0X87800000。
第5行设置链接到开始位置的文件为start.o,因为 start.o 里面包含着第一个要执行的指令,所以一定要链接到最开始的地方。
第 6 行是 main.o这个文件,其实可以不用写出来,因为 main.o 的位置就无所谓了,可以由编译器自行决定链接位置。
第9行第10行定义了只读数据段和数据段。ALIGN(4)表示地址按照4对齐。
在第 11、13 行有“__bss_start”和“__bss_end”这两个东西?这个是什么呢?“__bss_start”和“__bss_end”是符号,第 11、13 这两行其实就是对这两个符号进行赋值,其值为定位符“.”,这两个符号用来保存.bss 段的起始地址和结束地址。前面说了.bss 段是定义了但是没有被初始化的变量,我们需要手动对.bss 段的变量清的,因此我们需要知道.bss 段的起始和结束地址,这样我们直接对这段内存赋 0 即可完成清零。通过第 11、13 行代码,.bss 段的起始地址和结束地址就保存在了“__bss_start”和“__bss_end”中,我们就可以直接在汇编或者 C 文件里面使用这两个符号。

代码重定位和清bss详细原理介绍可以参考我之前的介绍:
https://fuzidage.github.io/2024/04/15/s3c2440%E8%A3%B8%E6%9C%BA%E7%BC%96%E7%A8%8B-%E4%BB%A3%E7%A0%81%E9%87%8D%E5%AE%9A%E4%BD%8D%E5%92%8C%E6%B8%85bss/

12.2.4.n.1 链接脚本语法#
12.2.4.n.1.1 MEMORY/SECTIONS#
1
2
3
4
5
MEMORY
{
name [(attr)] : ORIGIN = origin, LENGTH = len
...
}

属性 attr 部分是可选的,它主要有以下几个选项:

‘R’:只读段
‘W’:读写段
‘X’:可执行段
‘A’:需要分配内存的段
‘I’,’L’:初始化段
‘!’:和上述的属性合并使用,表示反转给出的属性

1
2
3
4
5
6
7
8
9
10
11
12
MEMORY
{
rom (rx) : ORIGIN = 0, LENGTH = 256K
ram (w) : org = 0x40000000, l = 4M
}
SECTIONS
{
. = 0x80000000;
.text : { *(.text) } > rom
.data : { *(.data) } > ram
.bss : { *(.bss) }
}

上述示例中,定义了两个内存区域,rom 区域从 0 开始,占据 256K 字节,而 ram 区域从 0x40000000 开始,占用 4M 空间.
而输出段 .text 指定放在 rom 中, .data 放在 ram 区域, .bss 没有指定,但是由于 .bss 段的属性是 ‘w’ 类型的,所以匹配的区域是 ram,被放在 .data 随后的地址处.
而地址定位符的赋值语句 ". = 0x80000000;" 会被忽略,编译完成之后可以通过 readelf -S 命令查看输出文件,可以看到各个段对应的虚拟地址.

在 linux 系统中,通常不会使用这种指定内存区域的定义方式,而是使用 SECTIONS 中的地址定位符,因为 linux 中对于系统内存的规定是比较严格的,通常不支持自定义的内存区域,而在裸机或者实时操作系统中这种方式使用得比较多.

Linux内核-kmalloc与vmalloc及CMA内存

1 kmalloc/vmalloc区别#

函数 位置 特性 大小限制
kmalloc 物理内存映射区域 物理地址虚拟地址均连续 不能超过128K
kzalloc 物理内存映射区域 物理地址虚拟地址均连续 不能超过128K
vmalloc 虚拟内存映射区域 虚拟地址连续,物理地址不一定连续 无限制
vzalloc 虚拟内存映射区域 虚拟地址连续,物理地址不一定连续 无限制

kzalloc只是相当于附加了 __GFP_ZERO 标志。所以它除了申请内核内存外,还会对申请到的内存内容清零。
同理,vzalloc也是一样,会对申请内存内容清零。
image

1.1 kmalloc函数原型:#

image

1
static __always_inline void *kmalloc(size_t size, gfp_t flags)

1.1.1 gpf flags含义#

1
2
3
4
5
6
7
|– 在进程上下文,可以睡眠     GFP_KERNEL
|– 在进程上下文,不可以睡眠,如: GFP_ATOMIC
|  |– 中断处理程序       GFP_ATOMIC
|  |– 软中断          GFP_ATOMIC
|  |– Tasklet         GFP_ATOMIC
|– 用于DMA的内存,可以睡眠   GFP_DMA | GFP_KERNEL
|– 用于DMA的内存,不可以睡眠  GFP_DMA |GFP_ATOMIC

如果进程上下文允许睡眠情况下尽量用GFP_KERNEL, 如果进程上下文禁止休眠的话(如中断,taskletd等)必须用GFP_ATOMIC

1.2 vmalloc函数原型:#

image

1
extern void *vmalloc(unsigned long size);

注意:vmalloc和vfree可以睡眠,因此中断上下文禁止使用。

1.3 内存释放#

1
2
void kfree(const void *);
extern void vfree(const void *addr);

2 kmalloc/vmalloc内存分配原理#

slab机制,等后面学习完后介绍。

3 CMA介绍#

3.0 引入Linux内核Buddy系统#

Linux伙伴系统(Buddy)使用 Page 粒度来管理内存,每个页面大小为4K。伙伴系统按照空闲内存块的长度,把内存挂载到不同长度的 free_list链表中。free_list 的单位是以 (2^order个Page) 来递增的,即 1 page、2 page、… 2^n,通常情况下最大 order 为10 对应的空闲内存大小为 4M bytes。我们使用伙伴系统来申请连续的物理页面最大的页面最大小4M bytes。

图片

当系统内存碎片化严重的时候,也很难分配到高order的页面,这时就引入了CMA概念,接着往下看。

3.1 CMA概述#

连续内存分配器(Contiguous Memory Allocator),简称CMA。在系统长时间运行后,内存可能碎片化,很难找到连续的物理页,CMA很好的避免了这个问题。
举个例子:
手机上1300万像素的摄像头,一个像素占用3字节,拍摄一张照片需要大约37MB内存。在系统长时间运行后,内存可能碎片化,很难找到连续的物理页,页分配器(kmalloc)和块分配器(vmalloc)很可能无法分配这么大的连续内存块。

方案1:
最开始的一种解决方案是为设备保留一块大的内存区域,比如为摄像头驱动预留一块大内存,通过ioremap来映射后作为私有内存使用,缺点是:当设备驱动不使用的时候(大多数时间手机摄像头是空闲的),内核的其他模块不能使用这块内存。
方案2:
连续内存分配器CMA很好的解决了这个问题,保留一块大的内存区域,当设备驱动不使用的时候,内核的其他模块可以使用。一般我们把这块区域定义为reserved-memory
image

3.2 CMA内核使能#

编译内核时需要开启以下配置宏:
(1)配置宏CONFIG_CMA,启用连续内存分配器。
(2)配置宏CONFIG_CMA_AREAS,指定CMA区域的最大数量,默认值是7。
(3)配置宏CONFIG_DMA_CMA,启用允许设备驱动分配内存的连续内存分配器

3.3 CMA的定义#

CMA每个区域实际上就是一个reserved memory。CMA分两种:

  1. 通用的CMA区域,该区域是给整个系统分配使用的;如下面的"linux,cma"
  2. 专用的CMA区域,这种是专门为单个模块定义的。如下面的"ion”

dts中CMA属性:

1
2
3
4
5
1. reusable:表示当前的内存区域除了被dma使用之外,还可以被内存管理(buddy)子系统reuse。
2. no-map:表示是否需要创建页表映射,对于通用的内存,必须要创建映射才可以使用,共享CMA是可以作为通用内存进行分配使用的,因此必须要创建页表映射。
3. 对于共享的CMA区域,需要配置上linux,cma-default属性,标志着它是共享的CMA。
4. alignment:对齐参数,保留内存的起始地址需要向该参数对齐
5. alloc-ranges:指定可以用来申请动态保留内存的区间

下面定义了3段区域CMA:
1.全局CMA区域,节点名称是“linux,cma”,大小是2GB,8K对齐。配置上linux,cma-default属性,reusable属性。
2.私有CMA区域,节点名字“de_mem0” “de_mem1”,128M给GPU 2D engine使用,私有无需建立页表映射。
3.私有CMA区域,节点名字“ion”,给video pipeline使用,私有无需建立页表映射。
​ 2de模块中定义memory-region属性,并且把对应dts定义的cma节点de_reserved0,de_reserved1传递给该模块。

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
reserved-memory {
#address-cells = <0x2>;
#size-cells = <0x2>;
ranges;
cma_reserved: linux,cma {
compatible = "shared-dma-pool";
reusable;//表示 cma 内存可被 buddy 系统使用
size = <0x0 0x80000000>; // 2GB
alignment = <0x0 0x2000>; // 8KB
linux,cma-default;
};
de_reserved0: de_mem0 {
reg = <0x1 0x10000000 0x0 0x8000000>; // 128M, for 2de
no-map;
};
de_reserved1: de_mem1 {
reg = <0x1 0x18000000 0x0 0x8000000>; // 128M, for 2de
no-map;
};
ion_reserved: ion {
compatible = "ion-region";
size = <0x0 0x04000000>; // 64MB
};
vo_2de0 {
compatible = "sophgo,vg-lite0";
memory-region = <&de_reserved0>;
interrupt-parent = <&gic>;
interrupts = <GIC_SPI 27 IRQ_TYPE_LEVEL_HIGH>;
interrupt-names = "vo_2de0";
};
vo_2de1 {
compatible = "sophgo,vg-lite1";
memory-region = <&de_reserved1>;
interrupt-parent = <&gic>;
interrupts = <GIC_SPI 28 IRQ_TYPE_LEVEL_HIGH>;
interrupt-names = "vo_2de1";
};

3.5 CMA内存原理和流程#

设备驱动程序不能直接使用连续内存分配器,而是调用DMA映射框架来使用连续内存分配器CMA。

3.5.1 CMA调用层次框架#

image

  1. 最底层为页分配器(以后分析)
  2. cma_alloc用来从CMA区域分配页,cma_release用来释放从CMA区域分配的页。
  3. 第3层为DMA映射框架专用的连续内存分配器,简称DMA专用连续内存分配器,提供的接口dma_alloc_from_contiguous用来从CMA区域分配页,接口dma_release_from_contiguous用来释放从CMA区域分配的页。
  4. 第4层就是DMA通用映射框架,供驱动程序调用dma_alloc_coherentdma_alloc_noncoherent用来分配内存,接口dma_free_coherentdma_free_noncoherent用来释放内存。

3.5.2 CMA结构体#

1
2
3
4
5
6
7
8
9
10
11
12
13
mm/cma.h
struct cma {
unsigned long base_pfn; //该CMA区域的起始页帧号
unsigned long count; //该cma区域的页数
unsigned long *bitmap; //位图,每个位描述对应的页的分配状态,0表示空闲,1表示已分配
unsigned int order_per_bit;//位图中的每个位描述的物理页的阶数,目前取值为0,表示每个位描述一页
struct mutex lock;
const char *name;
};

mm/cma.c
struct cma cma_areas[MAX_CMA_AREAS];//定义多个CMA区域。
unsigned cma_area_count;//表示实际使用的cma区域数量

cma模块使用bitmap来管理其内存的分配,0表示free,1表示已经分配。

重点解释order_per_bit:如果order_per_bit等于0,表示按照一个一个page来分配和释放,如果order_per_bit等于1,表示按照2个page组成的block来分配和释放,以此类推。

image-20240721215512066

上图cma_area[0]的.order_per_bit = 1,对应2个page,起始页帧号为0x2000, 0x400个页数,对应size为0x400 *2* 2K = 4M

刚好对应4M。

3.5.3 CMA区域初始化#

3.5.3.0 整个memory初始化#

1
2
3
4
5
6
7
8
start_kernel
------>setup_arch
------>setup_machine_fdt
------>early_init_dt_scan_nodes
------>of_scan_flat_dt
------>early_init_dt_scan_memory
------>early_init_dt_add_memory_arch
------>memblock_add

image-20240721223712191

3.5.3.1 dts描述中cma内存的初始化#

linux内核首先需要解析dtb中节点“memory”,把内存块添加到memblock的memory类型,memory类型保存内存块的物理地址范围,reserved类型保存保留内存块的物理地址范围,CMA区域就属于保留内存块。
image
创建CMA区域的执行流程如下所示:
image

1
2
3
4
5
6
7
8
start_kernel
------>setup_arch
------>arm_memblock_init
------>early_init_fdt_scan_reserved_mem
------>of_scan_flat_dt
------> __fdt_scan_reserved_mem
------> fdt_init_reserved_mem
------> memblock_add

linux内核启动时,当调用到__reserved_mem_init_node时会调用所有使用RESERVEDMEM_OF_DECLARE声明的CMA区域。

__reserved_mem_init_node会遍历__reservedmem_of_table section中的内容,检查到dts中有compatible匹配(CMA这里为“shared-dma-pool”)就进一步执行对应的initfn。通过RESERVEDMEM_OF_DECLARE定义的都会被链接到__reservedmem_of_table这个section段中,最终会调到使用RESERVEDMEM_OF_DECLARE定义的函数:

image-20240721190202046

其中全局CMA区域的初始化函数是rmem_cma_setup

3.5.3.1.1 全局cma内存初始化rmem_cma_setup#

rmem_cma_setup的作用就是将reserved-memory添加到cma子系统:

image

1
2
3
4
rmem_cma_setup
|------>cma_init_reserved_mem // 将reserved-memory 添加到cma_areas数组中
|------>dma_contiguous_early_fixup// dma remap
|------>dma_contiguous_set_default// set_default cma area
3.5.3.1.1.1 cma_init_reserved_mem#

来看调用的cma_init_reserved_mem:
image
从数组cma_areas分配一个数组项,保存CMA区域的起始页帧号和页数。dts指定了属性“linux,cma-default”,那么这个CMA区域是默认的CMA区域,最后设置全局变量dma_contiguous_default_area指向这个CMA区域(默认全局CMA区域)
红色圈出了该cma区域的dts描述和dts是不是完全吻合。

3.5.3.2 dts没有描述cma内存的初始化#

如果内核参数或配置宏配置全局CMA区域,cma初始化则流程如下所示:
image
image
image

3.5.4 CMA 区域内存映射#

CMA 区域创建初始化完后还不能直接使用,需要单独进行页表映射。前面:

linux内核-3.Linux 内核启动流程 - fuzidage - 博客园 (cnblogs.com) 2.1.1.1.1 start_kernel

Linux内核启动流程 | Hexo (fuzidage.github.io)

小结有介绍start_kernel启动流程。

1
2
3
4
start_kernel
------>setup_arch
------>paging_init//建立页表映射,包括非保留内存和保留内存。
------>dma_contiguous_remap

3.5.4.1 dma_contiguous_remap-建立cma area的页表映射#

image-20240721230004292

prepare_page_table负责普通内存的页表映射。dma_contiguous_remap建立cma area的页表映射:

image-20240721231037582

3.5.5 cma_init_reserved_areas-激活cma area内存#

cma_activate_area函数用于将 CMA 区域内的预留页全部释放添加到 Buddy 管理器内,然后激活 CMA 区域供系统使用。

image-20240721174230275

3.5.5.1 cma_activate_area#

image-20240721232741149

3.5.5.1.1 init_cma_reserved_pageblock#

cma默认是从reserved memory中分配的,通常情况这块内存是直接分配并预留不做任何使用,无形之中造成了浪费。所以在不用的时候放入伙伴系统,作为普通内存使用。

4 CMA内存使用#

1
2
struct page   *page = NULL;
page = cma_alloc(dev_get_cma_area(dev),mem_size, 0, GFP_KERNEL);

dev_get_cma_area可以获取对应的cma handler,如果获取不到,比如对应模块中并未定义memory-region,那么就会返回共享的cma handler,还记的上面的 linux,cma-default属性吗,共享cma区域会被作为缺省cma来使用。

4.1 dma_alloc_from_contiguous#

4.2 dma_release_from_contiguous#

不过一般内核模块要使用CMA内存时,使用的接口依然是dma的接口:

1
2
extern void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag);
extern void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t dma_handle);

最终也会进入dma_alloc_from_contiguous调用cma_alloc分配内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct page *dma_alloc_from_contiguous(struct device *dev, size_t count,
unsigned int align, bool no_warn)
{
if (align > CONFIG_CMA_ALIGNMENT)
align = CONFIG_CMA_ALIGNMENT;
return cma_alloc(dev_get_cma_area(dev), count, align, no_warn);
}

bool dma_release_from_contiguous(struct device *dev, struct page *pages,
int count)
{
return cma_release(dev_get_cma_area(dev), pages, count);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* cma_alloc() - allocate pages from contiguous area
* @cma: Contiguous memory region for which the allocation is performed.
* @count: Requested number of pages.
* @align: Requested alignment of pages (in PAGE_SIZE order).
* @no_warn: Avoid printing message about failed allocation
*
* This function allocates part of contiguous memory on specific
* contiguous memory area.
*/
struct page *cma_alloc(struct cma *cma, size_t count, unsigned int align,
bool no_warn);
extern void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag);
extern void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t dma_handle);
struct page *dma_alloc_from_contiguous(struct device *dev, size_t count,
unsigned int align, bool no_warn);
bool dma_release_from_contiguous(struct device *dev, struct page *pages,
int count);

4.3 cma_alloc#

图片

从指定的CMA 区域上分配count个连续的页面,按照align对齐。

4.4 cma_release#

图片

释放已经分配count个连续的页面。

1
2
3
4
5
6
7
8
9
10
11
/**
* cma_release() - release allocated pages
* @cma: Contiguous memory region for which the allocation is performed.
* @pages: Allocated pages.
* @count: Number of allocated pages.
*
* This function releases memory allocated by cma_alloc().
* It returns false when provided pages do not belong to contiguous area and
* true otherwise.
*/
bool cma_release(struct cma *cma, const struct page *pages, unsigned int count)

image-20240721213654864

5 通过procfs查看cma area#

5.1 获得ram地址范围#

第一个比较重要的是获得系统物理内存的范围:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ cat /proc/iomem 
10000000-17ffffff : System RAM
10008000-107fffff : Kernel code
10900000-10960677 : Kernel data
40002000-4000201f : serial
4000a000-4000a01f : codec@4000a000
40010000-40011fff : i2c0@40010000
40014000-4001401f : serial
40016000-4001601f : serial
40020000-40021fff : i2c2@40020000
4003a000-4003a01f : wdt0@4003a000
40056000-40056fff : video0@40056000
4005a000-4005bfff : dma0@4005a000
40064000-40064fff : video1@40064000
40068000-40069fff : dma1@40068000
4006a000-4006a1ff : lsacc2d@4006a000
40080000-40081fff : clock@40080000
40082000-40082027 : msgunit@40082000
400a0000-400a00ff : mmc0@400a0000
400aa000-400aa1ff : gauss@400aa000

“System RAM”, 其代表系统物理内存的起始物理地址和终止物理地址。

5.2 获得reserved-memory范围#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ cat /sys/kernel/debug/memblock/reserved 
0: 0x10004000..0x10007fff
1: 0x10100000..0x10960677
2: 0x12000000..0x127fffff
3: 0x1579b000..0x157a1fff
4: 0x17ea1cc0..0x17eb9fc3
5: 0x17eba000..0x17ee0fff
6: 0x17ee3180..0x17ee347f
7: 0x17ee34b0..0x17ffefff
8: 0x17fff100..0x17fff177
9: 0x17fff180..0x17fff1c4
10: 0x17fff200..0x17fff23b
11: 0x17fff240..0x17fff3c3
12: 0x17fff400..0x17fff5c4
13: 0x17fff600..0x17fff677
14: 0x17fff680..0x17fff68b
15: 0x17fff6c0..0x17fff6cb
......

通过这个命令可以知道系统已预留的内存信息,这些已预留的内存信息不可使用。

6 dts的reserved-memory内容解析#

通常使用memory-region将设备和reserved memory 关联起,cvifb 通过 memory-region 关联到 fb_reserved 这块 reserved memory 上面.
image
image
通过cvifb节点的memory-region属性找到reserved-memoryof_reserved_mem_lookup根据关联的reserved-momory节点找到预留内存地址.
image

Linux内核-并发与同步

1 并发场景#

Linux 系统并发产生的原因很复杂,总结一下有下面几个主要原 因:

  1. 多线程并发访问,Linux 是多任务(线程)的系统,所以多线程访问是最基本的原因。
  2. 抢占式并发访问,从 2.6 版本内核开始,Linux 内核支持抢占,也就是说调度程序可以 在任意时刻抢占正在运行的线程,从而运行其他的线程。
  3. 中断程序并发访问,这个无需多说,学过 STM32 的同学应该知道,硬件中断的权利可 是很大的。
  4. SMP(多核)核间并发访问,现在 ARM 架构的多核 SOC 很常见,多核 CPU 存在核间并 发访问。

2 并发解决方案#

2.1 内存屏障#

2.1.1 编译器指令重排(Compiler Instruction Reordering)#

1
2
3
4
5
6
int a, b;

void foo(void){
a = b + 1;
b = 0;
}
1
2
3
4
5
6
7
8
<foo>:
...
ldr w0, [x0] //load b to w0
add w1, w0, #0x1
...
str w1, [x0] //a = b + 1
...
str wzr, [x0] //b = 0

我们把编译优化打开到-O2,再次反汇编:

1
2
3
4
5
6
<foo>:
...
ldr w2, [x0] //load b to w2
str wzr, [x0] //b = 0
add w0, w2, #0x1
str w0, [x1] //a = b + 1

可以看到编译器 ”自作聪明“, b被提前赋值了。因此要使用内存屏障。

2.1.2 内存屏障API#

1
2
3
barrier();
cpu_relax();
READ_ONCE(val);

2.1.2.1 barrier#

前面用-O2选项顺序会被错误的排列。加入barrier();//插入内存屏障,再次反汇编可以看到OK符合我们的逻辑了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int a, b;

void foo(void){
a = b + 1;
barrier();//插入内存屏障
b = 0;
}

//反汇编如下:
<foo>:
...
ldr w2, [x0] //load b to w2
add w2, w2, #0x1
str w2, [x1] //a = a + 1
str wzr, [x0] //b = 0
...

2.1.2.2 cpu_relax#

1
2
3
4
5
int run = 1;
void foo(void) {
while (run)
;
}

run 是个全局变量,foo() 在一个进程中执行,一直循环。我们期望的结果是 foo() 一直等到其他进程修改 run 的值为 0 才退出循环。反汇编看看:

1
2
3
4
5
6
7
0000000000000748 <foo>:
748: 90000080 adrp x0, 10000
74c: f947e800 ldr x0, [x0, #4048]
750: b9400000 ldr w0, [x0] //load run to w0
754: d503201f nop
758: 35000000 cbnz w0, 758 <foo+0x10> //if (w0) while (1);
75c: d65f03c0 ret

但实际上编译器帮我们优化成了等效下面的样子:这样永远也不会退出。

1
2
3
4
5
6
7
8
int run = 1;
void foo(void) {
int reg = run;

if (reg)
while (1)
;
}

因此加上内存屏障如下:

1
2
3
4
5
int run = 1;
void foo(void) {
while (run)
cpu_relax();
}

2.1.2.3 READ_ONCE()#

也可用这种方式作为内存屏障。

1
2
3
4
5
int run = 1;
void foo(void){
while (READ_ONCE(run)) /* similar to while (*(volatile int *)&run) */
;
}

2.2 原子操作#

2.2.0 临界区的原子操作引入#

所谓的临界区就是共享数据段,如全局变量,对于临界区必须保证一次只有一个线程访问,也就是要保证临 界区是原子访问的。我们都知道,原子是化学反应不可再分的基本微粒,这里的原子访问就表示这一个访问是一个步骤,不能再进行拆分。

示例1:

a=3;

假设变量 a 的地址为 0X3000000,“a=3”这一行 C 语言可能会被编译为如下所示的汇编代码:

1
2
3
ldr r0, =0X30000000 /* 变量 a 地址 */
2 ldr r1, = 3 /* 要写入的值 */
3 str r1, [r0] /* 将 3 写入到 a 变量中 */

如果多线程同时执行这条语句。我们期望的执行顺序:

image

实际执行顺序可能是:

image

线程 A 最终将变量 a 设置为了 20,而并不是要求的 10线程 B 没有问题,是期望的a设置成了20,这就是并发竞态。我们希望三条汇编指令一次性执行完,不被打断和拆解,这就是原子操作。

#

2.2.1 整数原子操作API#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct {
int counter;
} atomic_t;
atomic_t b = ATOMIC_INIT(0); //定义原子变量 b 并赋初值为 0
atomic_set(&b, 10); /* 设置 b=10 */
atomic_read(&b); /* 读取 b 的值,肯定是 10 */
atomic_inc(&b); /* b 的值加 1,v=11 */

typedef struct {
long long counter;
} atomic64_t;
atomic64_t c = ATOMIC64_INIT(0); //定义64位系统原子变量 c 并赋初值为 0
//注意:如果使用的是64 位的 SOC,那么建议使用 64 位的原子操作函数。Cortex-A7 (armv7)是 32 位的架构,Cortex-A53(armv8)64位架构。

函数 描述
ATOMIC_INIT(int i) 定义原子变量的时候对其初始化。
int atomic_read(atomic_t *v) 读取 v 的值,并且返回。
void atomic_set(atomic_t *v, int i) 向 v 写入 i 值。
void atomic_add(int i, atomic_t *v) 给 v 加上 i 值。
void atomic_sub(int i, atomic_t *v) 从 v 减去 i 值。
void atomic_inc(atomic_t *v) 给 v 加 1,也就是自增。
void atomic_dec(atomic_t *v) 从 v 减 1,也就是自减
int atomic_dec_return(atomic_t *v) 从 v 减 1,并且返回 v 的值。
int atomic_inc_return(atomic_t *v) 给 v 加 1,并且返回 v 的值。
atomic_add_return(int i, atomic_t *v) 给 v 加 i,并且返回 v 的值。
atomic_sub_return(int i, atomic_t *v) 给 v 减 i,并且返回 v 的值。
int atomic_sub_and_test(int i, atomic_t *v) 从 v 减 i,如果结果为 0 就返回真,否则返回假
int atomic_dec_and_test(atomic_t *v) 从 v 减 1,如果结果为 0 就返回真,否则返回假
int atomic_inc_and_test(atomic_t *v) 给 v 加 1,如果结果为 0 就返回真,否则返回假
int atomic_add_negative(int i, atomic_t *v) 给 v 加 i,如果结果为负就返回真,否则返回假
atomic_cmpxchg(atomic_t *ptr, int old, int new) 比较old和原子变量ptr中的值,如果相等,那么就把new值赋给原子变量。返回旧的原子变量ptr中的值

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static int led_open(struct inode *inode, struct file *filp)
{
if (!atomic_dec_and_test(&gpioled.lock)) {
atomic_inc(&gpioled.lock);/* 小于 0 的话就加 1,使其原子变量等于 0 */
return -EBUSY; /* LED 被使用,返回忙 */
}
filp->private_data = &gpioled;
return 0;
}

static int led_release(struct inode *inode, struct file *filp)
{
struct gpioled_dev *dev = filp->private_data;

/* 关闭驱动文件的时候释放原子变量 */
atomic_inc(&dev->lock);
return 0;
}

static int __init led_init(void)
{
/* 初始化原子变量 */
atomic_set(&gpioled.lock, 1); /* 原子变量初始值为 1 */
}

该例子用来实现一次只能允许一个应用访问 LED 灯,不能多个进程同时操作LED。第一个用户程序进行open,成功此时原子值counter=0,第二个用户程序进行open就会fail, 直到第一个用户程序close, counter会进行加一,此时第二个程序才可以open成功。

2.2.2 位原子操作API#

原 子位操作不像原子整形变量那样有个 atomic_t 的数据结构,原子位操作是直接对内存进行操作:

函数 描述
void set_bit(int nr, void *p) 将 p 地址的第 nr 位置 1。
void clear_bit(int nr,void *p) 将 p 地址的第 nr 位清零。
void change_bit(int nr, void *p) 将 p 地址的第 nr 位进行翻转。
int test_bit(int nr, void *p) 获取 p 地址的第 nr 位的值。
int test_and_set_bit(int nr, void *p) 将 p 地址的第 nr 位置 1,并且返回 nr 位原来的值。
int test_and_clear_bit(int nr, void *p) 将 p 地址的第 nr 位清零,并且返回 nr 位原来的值。
int test_and_change_bit(int nr, void *p) 将 p 地址的第 nr 位翻转,并且返回 nr 位原来的值。

2.3 自旋锁#

原子操作只能对整形变量或者位进行保护,但是,在实际的使用环境中怎么可能只有整形 变量或位这么简单的临界区。

自旋锁的定义:

对于自旋锁而言,如果自旋锁 正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线 程 B 不会进入休眠状态或者说去做其他的处理,而是会一直傻傻的在那里“转圈圈”的等待锁 可用。比如现在有个公用电话亭,一次肯定只能进去一个人打电话,现在电话亭里面有人正在 打电话,相当于获得了自旋锁。此时你到了电话亭门口,因为里面有人,所以你不能进去打电 话,相当于没有获取自旋锁,这个时候你肯定是站在原地等待,你可能因为无聊的等待而转圈 圈消遣时光,反正就是哪里也不能去,要一直等到里面的人打完电话出来。终于,里面的人打 完电话出来了,相当于释放了自旋锁,这个时候你就可以使用电话亭打电话了,相当于获取到 了自旋锁。

自旋锁的“自旋”也就是“原地打转”的意思,“原地打转”的目的是为了等待自旋锁可以 用,可以访问共享资源。把自旋锁比作一个变量 a,变量 a=1 的时候表示共享资源可用,当 a=0 的时候表示共享资源不可用。现在线程 A 要访问共享资源,发现 a=0(自旋锁被其他线程持有), 那么线程 A 就会不断的查询 a 的值,直到 a=1。可

缺点:获取自旋锁会原地等待,会浪费处理器时间,降低系统性能,所以自旋锁 的持有时间不能太长。如果临界区比较大,运行时间比较长的话要选择信号量和互斥体。

适用范围:适用于短时期的轻量级加锁。

image

2.3.1 自旋锁API#

函数 描述
DEFINE_SPINLOCK(spinlock_t lock) 定义并初始化一个自选变量。
int spin_lock_init(spinlock_t *lock) 初始化自旋锁。
void spin_lock(spinlock_t *lock) 获取指定的自旋锁,也叫做加锁。
void spin_unlock(spinlock_t *lock) 释放指定的自旋锁。
int spin_trylock(spinlock_t *lock) 尝试获取指定的自旋锁,如果没有获取到就返回 0
int spin_is_locked(spinlock_t *lock) 检查指定的自旋锁是否被获取,如果没有被获取就返回非 0,否则返回 0。

被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的 API 函数,否则的话会可能会导致死锁现象(不能带锁休眠)。因为自旋锁会禁止抢占,也就说当线程 A 得到锁以后会暂时禁止内核抢占,那既然禁止内核抢占自己又休眠了,粗俗的形容就是“占着茅坑不拉屎”,自己休眠了又没有释放锁就导致死锁。

2.3.1.1 自旋锁和中断相关#

中断里面访问临界资源,也是可以使用自旋锁的,但是在获取锁之前一定要先禁止本地中断。

函数 描述
void spin_lock_irq(spinlock_t *lock) 禁止本地中断,并获取自旋锁。
void spin_unlock_irq(spinlock_t *lock) 激活本地中断,并释放自旋锁。
void spin_lock_irqsave(spinlock_t *lock,unsigned long flags) 保存中断状态,禁止本地中断,并获取自旋锁。
void spin_unlock_irqrestore(spinlock_t*lock, unsigned long flags) 将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁

如下图就是没有禁止本地中断导致死锁的例子:

image

线程 A 先运行,并且获取到了 lock 这个锁,当线程 A 运行 functionA 函 数的时候中断发生了,中断抢走了 CPU 使用权。右边的中断服务函数也要获取 lock 这个锁, 但是这个锁被线程 A 占有着,中断就会一直自旋,等待锁有效。但是在中断服务函数执行完之前,线程 A 是不可能执行的,线程 A 说“你先放手”,中断说“你先放手”,场面就这么僵持着, 死锁发生!

使用 spin_lock_irq/spin_unlock_irq 的时候需要用户能够确定加锁之前的中断状态,但实际上我们是很难确定某个时刻的中断状态,因此不推荐使用 spin_lock_irq/spin_unlock_irq。建议使用 spin_lock_irqsave/spin_unlock_irqrestore,因为这一组函数会保存中断状态,在释放锁的时候会恢复中断状态。一般在线程中使用 spin_lock_irqsave/ spin_unlock_irqrestore,在中断中使用 spin_lock/spin_unlock,例如下面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DEFINE_SPINLOCK(lock);
/* 线程 A */
void functionA (){
unsigned long flags; /* 中断状态 */
spin_lock_irqsave(&lock, flags); /* 获取锁 */
/* 临界区 */
spin_unlock_irqrestore(&lock, flags); /* 释放锁 */
}

/* 中断服务函数 */
void irq() {
spin_lock(&lock); /* 获取锁 */
/* 临界区 */
spin_unlock(&lock); /* 释放锁 */
}

有人说为什么中断服务程序不去使用spin_lock_irqsave呢? 难道不需要去禁止中断吗?

因为GIC中断总入口已经帮我们做了禁止中断。调用了local_irq_disable(),详见设备驱动-10.中断子系统-1异常中断引入 - fuzidage - 博客园 (cnblogs.com)

image

如果下半部(BH)也会竞争共享资源,要在下半部里面使用自旋锁:

函数 描述
void spin_lock_bh(spinlock_t *lock) 关闭下半部,并获取自旋锁
void spin_unlock_bh(spinlock_t *lock) 打开下半部,并释放自旋锁

2.3.2 读写自旋锁#

读写自旋锁为读和写操作提供了不同的锁,一次只能允许一个写操作,也就是只能一个线程持有写锁,而且不能进行读操作。但是当没有写操作的时候允许一个或多个线程持有读锁, 可以进行并发的读操作。Linux 内核使用 rwlock_t 结构体表示读写锁,结构体定义如下(删除了 条件编译):

1
2
3
typedef struct {
arch_rwlock_t raw_lock;
} rwlock_t;

使用场景:

现在有个学生信息表,此表存放着学生的年龄、家庭住址、班级等信息,此表可以随时被 修改和读取,那么必须要对其进行保护,如果我们现在使用自旋锁对其进行 保护。每次只能一个读操作或者写操作,但是,实际上此表是可以并发读取的。只需要保证在 修改此表的时候没人读取,或者在其他人读取此表的时候没有人修改此表就行了。也就是此表 的读和写不能同时进行,但是可以多人并发的读取此表。像这样,当某个数据结构符合读/写或 生产者/消费者模型的时候就可以使用读写自旋锁。

函数 描述
DEFINE_RWLOCK(rwlock_t lock) 定义并初始化读写锁
void rwlock_init(rwlock_t *lock) 初始化读写锁。
读操作
void read_lock(rwlock_t *lock) 获取读锁。
void read_unlock(rwlock_t *lock) 释放读锁。
void read_lock_irq(rwlock_t *lock) 禁止本地中断,并且获取读锁。
void read_unlock_irq(rwlock_t *lock) 打开本地中断,并且释放读锁。
void read_lock_irqsave(rwlock_t *lock, unsigned long flags) 保存中断状态,禁止本地中断,并获取读锁。
void read_unlock_irqrestore(rwlock_t *lock,unsigned long flags) 将中断状态恢复到以前的状态,并且激活本地中断,释放读锁。
void read_lock_bh(rwlock_t *lock) 关闭下半部,并获取读锁。
void read_unlock_bh(rwlock_t *lock) 打开下半部,并释放读锁。
写操作
void write_lock(rwlock_t *lock) 获取写锁。
void write_unlock(rwlock_t *lock) 释放写锁。
void write_lock_irq(rwlock_t *lock) 禁止本地中断,并且获取写锁。
void write_unlock_irq(rwlock_t *lock) 打开本地中断,并且释放写锁。
void write_lock_irqsave(rwlock_t *lock,unsigned long flags) 保存中断状态,禁止本地中断,并获取写锁
void write_unlock_irqrestore(rwlock_t *lock,unsigned long flags) 将中断状态恢复到以前的状态,并且激活本地中断,释放读锁。
void write_lock_bh(rwlock_t *lock) 关闭下半部,并获取读锁。
void write_unlock_bh(rwlock_t *lock) 打开下半部,并释放读锁。

2.3.3 顺序锁#

顺序锁在读写锁的基础上衍生而来的,使用读写锁的时候读操作和写操作不能同时进行。使用顺序锁的话可以允许在写的时候进行读操作,也就是实现同时读写,但是不允许同时进行并发的写操作。

1
2
3
4
typedef struct {
struct seqcount seqcount;
spinlock_t lock;
} seqlock_t;
函数 描述
DEFINE_SEQLOCK(seqlock_t sl) 定义并初始化顺序锁
void seqlock_ini seqlock_t *sl) 初始化顺序锁。
void write_seqlock(seqlock_t *sl) 获取写顺序锁。
void write_sequnlock(seqlock_t *sl) 释放写顺序锁。
void write_seqlock_irq(seqlock_t *sl) 禁止本地中断,并且获取写顺序锁
void write_sequnlock_irq(seqlock_t *sl) 打开本地中断,并且释放写顺序锁。
void write_seqlock_irqsave(seqlock_t *sl,unsigned long flags) 保存中断状态,禁止本地中断,并获取写顺序锁。
void write_sequnlock_irqrestore(seqlock_t *sl,unsigned long flags) 将中断状态恢复到以前的状态,并且激活本地中断,释放写顺序锁。
void write_seqlock_bh(seqlock_t *sl) 关闭下半部,并获取写读锁。
void write_sequnlock_bh(seqlock_t *sl) 打开下半部,并释放写读锁。
unsigned read_seqbegin(const seqlock_t *sl) 读单元访问共享资源的时候调用此函数,此函数会返回顺序锁的顺序号。
unsigned read_seqretry(const seqlock_t *sl,unsigned start) 读结束以后调用此函数检查在读的过程中有没有对资源进行写操作,如果有的话就要重读

2.4 信号量#

相比于自旋锁,信号量可以使线程进入休眠状态,比如 A 与 B、C 合租了一套房子,这个 房子只有一个厕所,一次只能一个人使用。某一天早上 A 去上厕所了,过了一会 B 也想用厕 所,因为 A 在厕所里面,所以 B 只能等到 A 用来了才能进去。B 要么就一直在厕所门口等着, 等 A 出来,这个时候就相当于自旋锁。B 也可以告诉 A,让 A 出来以后通知他一下,然后 B 继 续回房间睡觉,这个时候相当于信号量。可以看出,使用信号量会提高处理器的使用效率,毕 竟不用一直傻乎乎的在那里“自旋”等待。但是,信号量的开销要比自旋锁大,因为信号量使 线程进入休眠状态以后会切换线程,切换线程就会有开销。

信号量的特点:

适用于那些占用资源比较久的场合,如线程同步。

因此信号量等待不能用于中断中,因为中断不能休眠。

2.4.1 信号量 API#

1
2
3
4
5
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
函数 描述
DEFINE_SEAMPHORE(name) 定义一个信号量,并且设置信号量的值为 1。
void sema_init(struct semaphore *sem, int val) 初始化信号量 sem,设置信号量值为 val。
void down(struct semaphore *sem) 获取信号量,因为会导致休眠,因此不能在中断中使用。
int down_trylock(struct semaphore *sem); 尝试获取信号量,如果能获取到信号量就获取,并且返回 0。如果不能就返回非 0,并且不会进入休眠。
int down_interruptible(struct semaphore *sem) 获取信号量,和 down 类似,只是使用 down 进入休眠状态的线程不能被信号打断。而使用此函数进入休眠以后是可以被信号打断的。
void up(struct semaphore *sem) 释放信号量
1
2
3
4
5
6
7
8
struct semaphore sem; /* 定义信号量 */
sema_init(&sem, 1); /* 初始化信号量 */
threadA(){
down(&sem); /* 申请信号量 */
}
theadB(){
up(&sem); /* 释放信号量 */
}

2.5 互斥锁#

将信号量的值设置为 1 就可以使用信号量进行互斥访问了,虽然可以通过信号量实现互斥,但是 Linux 提供了一个比信号量更专业的机制来进行互斥,它就是互斥体—mutex。互斥访问表示一次只有一个线程可以访问共享资源,不能递归申请互斥锁。

1
2
3
4
5
struct mutex {
/* 1: unlocked, 0: locked, negative: locked, possible waiters */
atomic_t count;
spinlock_t wait_lock;
};

使用 mutex 的时候要注意如下几点:

1. mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。

2. 和信号量一样,mutex 保护的临界区可以调用引起阻塞的 API 函数。

3. 因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并且 mutex 不能递归上锁和解锁。

2.5.1 互斥锁API#

函数 描述
DEFINE_MUTEX(name) 定义并初始化一个 mutex 变量。
void mutex_init(mutex *lock) 初始化 mutex。
void mutex_lock(struct mutex *lock) 获取 mutex,也就是给 mutex 上锁。如果获取不到就进休眠。
void mutex_unlock(struct mutex *lock) 释放 mutex,也就给 mutex 解锁。
int mutex_trylock(struct mutex *lock) 尝试获取 mutex,如果成功就返回 1,如果失败就返回 0。
int mutex_is_locked(struct mutex *lock) 判断 mutex 是否被获取,如果是的话就返回1,否则返回 0。
int mutex_lock_interruptible(struct mutex *lock) 使用此函数获取信号量失败进入休眠以后可以被信号打断
1
2
3
4
5
struct mutex lock; /* 定义一个互斥体 */
mutex_init(&lock); /* 初始化互斥体 */
mutex_lock(&lock); /* 上锁 */
/* 临界区 */
mutex_unlock(&lock); /* 解锁 */

2.6 SMP 架构下percpu变量机制#

随着 SMP(对称多处理器架构) 的发展,程序确实是在并发执行,也为数据同步带来了更大的挑战。

在 SMP 架构中,每个 CPU 都拥有自己的高速缓存,通常,L1 cache 是 CPU 独占的,每个 CPU 都有一份,它的速度自然是最快的,而 L2 cache 通常是所有 CPU 共享的高速缓存,当 CPU 载入一个全局数据时,会逐级地查看高速缓存,如果没有在缓存中命中,就从内存中载入,并加入到各级 cache 中,当下次需要读取这个值时,直接读取 cache 。

假如进程在 CPU0 上操作一个共享变量,在某个时刻进程被调度到 CPU1 上执行时,CPU0 和 CPU1 上的 共享变量值就不同。

percpu机制:为了避免多个 CPU 对全局数据的竞争而导致的性能损失,percpu 直接为每个 CPU 生成一份独有的数据备份,每个数据备份占用独立的内存,CPU 不应该修改不属于自己的这部分数据,这样就避免了多 CPU 对全局数据的竞争问题。

2.6.0 percpu 变量的存储格式#

对于普通的变量而言,变量的加载地址就是程序中使用的该变量的地址,可以使用取址符获取变量地址。

percpu 变量:percpu 变量的加载地址是不允许访问的,取而代之的是对于 n 核的 SMP 架构系统,内核将会为每一个 CPU 另行开辟一片内存,将该 percpu 变量复制 n 份分别放在每个 CPU 独有的内存区中。

image

也就是说,为 percpu 分配内存的时候,原始的变量 var 与 percpu 变量内存偏移值 offset 被保存了下来,每个 CPU 对应的 percpu 变量地址为 (&var + offset),当然真实情况要比这个复杂,将在后文中讲解。

2.6.1 percpu变量使用场景#

  1. 计数器和统计信息:如果你有计数器或者统计信息需要在每个CPU上独立维护,那么percpu变量将会非常有用。
  2. 异步任务处理:通过percpu变量来维护异步任务的上下文信息。

2.6.2 percpu 变量的定义#

1
2
3
DEFINE_PER_CPU(type, name);//静态定义一个 percpu变量,type 是变量类型,name 是变量名

type __percpu *ptr alloc_percpu(type);//动态分配一个percpu变量ptr,这只是一个原始数据,真正被使用的数据被 copy 成 n(n=CPU数量) 份分别保存在每个 CPU 独占的地址空间中,在访问 percpu 变量时就是对每个副本进行访问。

2.6.3 percpu 变量的读写#

静态定义的读写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DEFINE_PER_CPU(int, val)=0;

// 获取当前CPU的percpu变量的值
int value = per_cpu(val, smp_processor_id());
// 遍历所有CPU,并打印percpu变量的值
for_each_possible_cpu(cpu) {
value = per_cpu(val, cpu);
printk(KERN_INFO "my_percpu_var on CPU%d is %d\n", cpu, value);
}



/*put_cpu_var 和 get_cpu_var 是成对出现的,因为这段期间内静止内核抢占,
*它们之间的代码不宜执行太长时间。
*/
int *pint = &get_cpu_var(val);//获取当前 CPU 的 percpu 变量的地址进行操作
*pint++;
put_cpu_var(val);

为什么在调用 get_cpu_var 时,第一步是禁止内核抢占呢?

想想这样一个场景,进程 A 在 CPU0 上执行,读取了 percpu 变量到寄存器中,这时候进程被高优先级进程抢占,继续执行的时候可能被转移到 CPU1 上执行,这时候在 CPU1 执行的代码操作的仍旧是 CPU0 上的 percpu 变量,这显然是错误的。

动态定义的读写

1
2
3
4
int *pint = alloc_percpu(int);
...
int *p = per_cpu_ptr(pint,raw_smp_processor_id());//与静态变量的操作接口不一样,这个接口允许指定 CPU ,不再是只能获取当前 CPU 的值
(*p)++;

raw_smp_processor_id() 函数返回当前 CPU num,这个示例也就是操作当前 CPU 的 percpu 变量,这个接口并不需要禁止内核抢占,因为不管进程被切换到哪个 CPU 上执行,它所操作的都是第二个参数提供的 CPU。

2.6.4 percpu 变量实现原理#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define DEFINE_PER_CPU(type, name)                  \
DEFINE_PER_CPU_SECTION(type, name, "")

#define DEFINE_PER_CPU_SECTION(type, name, sec) \
__PCPU_ATTRS(sec) PER_CPU_DEF_ATTRIBUTES \
__typeof__(type) name

#define __PCPU_ATTRS(sec) \
__percpu __attribute__((section(PER_CPU_BASE_SECTION sec))) \
PER_CPU_ATTRIBUTES

#ifdef CONFIG_SMP
#define PER_CPU_BASE_SECTION ".data..percpu"
#else
#define PER_CPU_BASE_SECTION ".data"
#endif

展开:

1
2
3
4
5
6
#ifdef CONFIG_SMP
#define DEFINE_PER_CPU(type, name) \
__percpu __attribute__((section(".data..percpu"))) type name; \
#else \
__percpu __attribute__((section(".data"))) type name; \
#endif
  1. 在 SMP 架构下,被定义的 percpu 变量在编译后放在 .data..percpu 这个 section 中;
  2. 在单核系统中, percpu 变量被放在.data也就是数据段中;

2.6.4.1 get_cpu_var实现#

1
2
3
4
5
6
7
8
9
10
11
12
13
#define get_cpu_var(var)                    \
(*({ \
preempt_disable(); \
this_cpu_ptr(&var); \
}))

#define this_cpu_ptr(ptr) raw_cpu_ptr(ptr)

#define raw_cpu_ptr(ptr) \
({ \
__verify_pcpu_ptr(ptr); \
arch_raw_cpu_ptr(ptr); \
})

首先,preempt_disable 禁用内核抢占,然后使用 this_cpu_ptr 接口获取当前 cpu 上对应的 var 变量地址。

get_cpu_var展开:可以看到就能准确获取当前cpu的val地址。

1
2
3
4
5
#define get_cpu_var(var)                    \
(*({ \
preempt_disable(); \
&var + __per_cpu_offset[raw_smp_processor_id()] \
}))

使用完变量之后记得调用 put_cpu_var 以使能内核抢占功能,恢复系统状态。

3 linux内核下不同同步机制的适用场景#

  1. 原子操作:主要用于进行原子性的读写操作,适用于计数器等场景。
  2. 自旋锁:用于短时间内锁定互斥资源,适用于锁持有时间短的场景。
  3. 读写锁:用于提供读模式和写模式下的锁操作,适用于读多写少的场景。
  4. MUTEX:类似自旋锁,但是可以导致调用线程睡眠,适用于锁持有时间较长的场景。(允许休眠)
  5. 信号量:用于实现互斥和同步,适用于保护临界区和控制访问频率。但是可以导致调用线程睡眠,适用于锁持有时间较长的场景。(允许休眠)

Linux内核-rootfs构建移植

1 根文件系统的引入#

我们知道文件系统类型有 FATFS、FAT、EXT4、YAFFS 和 NTFS,squashfs等。文件系统可以让我们利用文件IO的形式对文件目录进行访问,而不用去访问flash存储地址,在使用上更为方便轻松。
根文件系统rootfs, 首先是内核启动时所 mount(挂载)的第一个文件系统,系统引导启动程序会在根文件系统挂载之后从中把一些基本的初始化脚本和服务等加载到内存中去运行。
百度百科上说内核代码镜像文件保存在根文件系统中(对电脑端的Ubuntu来说确实是,放在/boot/vmlinuz-5.4.0-152-generic)。但是我们嵌入式 Linux 并没有将内核代码镜像保存在根文件系统中,而是保存到了其他地方,比如 NAND Flash 的指定存储地址、EMMC 专用分区中。
嵌入式根文件系统和 Linux 内核是分开的,单独的 Linux 内核是没法正常工作的,必须要搭配根文件系统。如果不提供根文件系统,Linux 内核在启动的时候就会提示内核崩溃(Kernel panic):
image

2 根文件系统的组成#

image

2.1 /bin 目录#

存放着系统需要的可执行文件,一般都是一些命令,比如ls、cp、mv等命令。
image

2.2 /dev 目录#

此目录下的文件都是设备节点文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ls -l /dev/
crw-rw---- 1 root root 89, 9 Jan 1 08:00 /dev/i2c-9
crw-rw---- 1 root root 10, 62 Jan 1 08:00 /dev/ion
crw-rw---- 1 root root 1, 11 Jan 1 08:00 /dev/kmsg
srw-rw-rw- 1 root root 0 Jan 1 08:00 /dev/log
crw-rw---- 1 root root 10, 237 Jan 1 08:00 /dev/loop-control
brw-rw---- 1 root root 7, 0 Jan 1 08:00 /dev/loop0
brw-rw---- 1 root root 7, 1 Jan 1 08:00 /dev/loop1
brw-rw---- 1 root root 179, 0 Jan 1 08:00 /dev/mmcblk0
brw-rw---- 1 root root 179, 8 Jan 1 08:00 /dev/mmcblk0boot0
brw-rw---- 1 root root 179, 16 Jan 1 08:00 /dev/mmcblk0boot1
brw-rw---- 1 root root 179, 1 Jan 1 08:00 /dev/mmcblk0p1
brw-rw---- 1 root root 179, 2 Jan 1 08:00 /dev/mmcblk0p2
brw-rw---- 1 root root 179, 3 Jan 1 08:00 /dev/mmcblk0p3
brw-rw---- 1 root root 179, 4 Jan 1 08:00 /dev/mmcblk0p4

2.3 /etc 目录#

此目录下存放着各种配置文件。

2.4 /lib 目录#

存放着 Linux 所必须的库文件。这些库文件是共享库,命令和用户编写的应用程序要使用这些库文件。

2.5 /mnt 目录#

临时挂载目录,一般是空目录,可以在此目录下创建空的子目录,比如/mnt/sd、/mnt/usb,这样就可以将 SD 卡或者 U 盘挂载到/mnt/sd 或者/mnt/usb 目录中。

2.6 /proc 目录#

proc虚拟文件系统,输出硬件驱动模块的相关信息,各种输出打印。(后续专门出一篇博文介绍)
image

2.7 /usr 目录#

这里的usr 不是 user 的缩写,而是 Unix Software Resource 的缩写,也就是 Unix 操作系统软件资源目录。
image

2.8 /var 目录#

存放一些可以改变的数据。如/var/log。

[root@robin]/var/log# ls
messages    messages.0  messages.1  messages.2  messages.3

2.9 /sys 目录#

此目录作为 sysfs 文件系统的挂载点,sysfs 是一个类似于 proc 文件系统的特殊文件系统,sysfs 也是基于 ram 的文件系统,也就是说它也没有实际的存储设备。此目录是系统设备管理的重要目录,此目录通过一定的组织结构向用户提供详细的内核数据结构信息。

查看硬件模块IP的base addr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@robin]/sys/devices# cd platform/
[root@robin]/sys/devices/platform# ls
1040000.mon 29230000.spi3
1900000.rtos_cmdqu 29240000.can
203c0000.bmtpu 29300000.cv-emmc
20400000.pcie 29310000.cv-sd
20bc0000.sata 29320000.cv-sd
20bc0000.sata:sata-port@0 29330000.wifi-sd
20bc0000.sata:sata-port@1 29340000.dma
21020000.vc_drv 29350000.dma
27000000.cv-wd 39000100.usb
27010000.gpio 39000200.usb
27011000.gpio 41d0c00.pdm
27012000.gpio 5021000.gpio

查看debug相关:

1
2
3
4
5
6
7
8
9
[root@robin]/sys/kernel/debug# ls
27000000.cv-wd dw_spi2 mtd
asoc dw_spi3 pinctrl
bdi dynamic_debug pwm
block extfrag regmap
bluetooth fault_around_bytes sched_debug
clear_warn_once gpio sched_features
clk hid sleep_time
debug_enabled ieee80211 split_huge_pages

重要的有:

1
2
3
/sys/kernel/debug/debug_enabled
/sys/kernel/debug/dynamic_debug/control
/sys/kernel/debug/clk/clk_summary

查看和设置驱动模块module param变量:

1
2
3
4
5
6
cd /sys/module/soph_stitch/parameters
[root@cvitek]/sys/module/soph_stitch/parameters# cat stitch_log_lv
2
[root@cvitek]/sys/module/soph_stitch/parameters# echo 2 > stitch_log_lv
[root@cvitek]/sys/module/soph_stitch/parameters# cat stitch_log_lv
2

3 构建根文件系统#

3.1 busybox#

BusyBox 是一个集成了大量的 Linux 命令和工具的软件,像ls、mv、ifconfig等命令 BusyBox 都会提供。
一般下载 BusyBox 的源码,然后配置 BusyBox,选择自己想要的功能,最后编译即可。
BusyBox 可以在其官网下载到,官网地址为:
https://busybox.net/downloads/

3.1.1 配置编译 BusyBox#

这里我下载的busybox-1.29.0.tar.bz2, 将其进行解压:

1
mkdir rootfs;cd rootfs; tar xfj busybox-1.29.0.tar.bz2

image
修改Makefile添加toolchain:

1
2
3
164 CROSS_COMPILE ?= /usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-
......
190 ARCH ?= arm

3.1.1.1 busybox支持中文识别#

我们知道中文的编码如果采用utf8,那么ascii值肯定是大于0x7f的。因此需要修改busybox源码,让其能识别超过ascii的编码范围0x7f

utf8 中文编码
UTF-8是一种广泛使用的Unicode编码方式,它可以用来表示各种语言和符号。对于中文字来说,UTF-8采用了一种特殊的编码方式,即中文字符主要使用三个字节(21比特)来表示。具体来说,当一个汉字的Unicode码位于0x80到0x7FF之间的时,UTF-8会用三个字节来编码这个汉字。例如,汉字“爸”在UTF-8中的编码过程是这样的:

首先,将汉字“爸”Unicode(0x5BAD)转换为二进制数(0111 0010 0011 1000)
然后,按照UTF-8的三字节模式进行编码,即每个字节的最高位都设置为1,剩下的7位用于表示该字节所对应的Unicode值。在这个例子中,第一个字节的高位为1,表示这是一个16位的字节;接下来的三位为1011,对应于Unicode码中的第11位,即十进制的5;最后一位为0,代表这是一个字节的最低位。
最后,将这三个字节的二进制数转换为十六进制数(0xE788B8),这就是汉字“爸”在UTF-8中的编码形式。

1
vi libbb/unicode.c

打开函数unicode_conv_to_printable2,修改如下:
image

1
vi libbb/printable_string.c

image

3.1.1.2 busybox配置#

编译busybox前要先对 busybox 进行默认的配置,有以下几种配置选项:

1
2
3
4
5
6
①、defconfig,缺省配置,也就是默认配置选项。
②、allyesconfig,全选配置,也就是选中 busybox 的所有功能
③、allnoconfig,最小配置。

make defconfig
make menuconfig

image

1
2
-> Settings 
-> Build static binary (no shared libs)

这项请不要勾选,静态编译 busybox会DNS解析有问题。

1
2
-> Settings
-> vi-style line editing commands

建议勾选这项。

1
2
-> Linux Module Utilities
-> Simplified modutils

默认会选中“Simplified modutils”,这里我们要取消勾选。

1
2
-> Linux System Utilities
-> mdev (16 kb) //确保下面的全部选中,默认都是选中的

image

1
2
3
-> Settings
-> Support Unicode //选中
-> Check $LC_ALL, $LC_CTYPE and $LANG environment variables //选中

image

3.1.1.3 编译#

1
2
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
make install CONFIG_PREFIX=/media/cvitek/robin.lee/rootfs ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-

image
image

busybox 的所有工具和文件就会被安装到 rootfs 目录中,rootfs 目录内容如下:
image
命令和工具都再对应/bin /usr/bin对应的目录:
image

3.linux内核启动流程

Linux内核启动流程 | Hexo (fuzidage.github.io)

说过 Linux 内核 init 进程最后会查找用户空间的 init 程序,找到以后就会运行这个用户空间的 init 程序,从而切换到用户态。如果 bootargs 设置 init=/linuxrc,那么 linuxrc 就是可以作为用户空间的 init 程序,所以用户态空间的 init 程序是 busybox 来生成的。

我们的根文件系统此时就制作好了,但此时还不能使用,还需要一些其他的文件,我们继续来完善 rootfs。

3.1.2 根文件系统添加 lib 库#

3.1.2.1 /lib库添加#

Linux 中的应用程序一般都是需要动态库的,当然你也可以编译成静态的,但是静态的可执行文件会很大。如果编译为动态的话就需要动态库,所以我们需要向根文件系统中添加动态库。lib 库文件从交叉编译器中获取:

1
2
mkdir lib
cd gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/lib

libc库如下:将其拷贝进rootfs。
image

1
cp *so* *.a /media/cvitek/robin.lee/rootfs/lib -d #-d表示将软链接也一同拷贝

进入工具链的lib库:

1
2
cd gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/lib
cp *so* *.a /media/cvitek/robin.lee/rootfs/lib -d #-d表示将软链接也一同拷贝

最终根文件系统的/lib内容如下:
image

3.1.2.2 /usr/lib库添加#

1
2
mkdir /usr/lib
cd gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/usr/lib

libc/usr/lib库如下:将其拷贝进rootfs。
image

1
cp *so* *.a /media/cvitek/robin.lee/rootfs/usr/lib -d #-d表示将软链接也一同拷贝

至此,根文件系统的库文件就全部添加好了。来看下库占用存储空间多少:

1
2
3
robin.lee@WORKSTATION5:/media/cvitek/robin.lee/rootfs$ du ./lib ./usr/lib/ -sh
57M ./lib
67M ./usr/lib/

3.1.3 根文件系统添加其他目录#

dev、proc、mnt、sys、tmp 和 root这些目录需要在rootfs进行创建。
image

3.1.4 根文件系统测试#

初步测试我们的rootfs功能,我们最好是利用nfs挂载根文件系统,方便修改。之前bootargs都是设置:

1
setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw'

从emmc分区2启动,分区2存放了根文件系统。现在bootargs改成nfs挂载rootfs要如何设置呢?
文档为 Documentation/filesystems/nfs/nfsroot.txt,格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
root=/dev/nfs nfsroot=[<server-ip>:]<root-dir>[,<nfs-options>] ip=<client-ip>:<server-ip>:<gw-ip>:<netmask>:<hostname>:<device>:<autoconf>:<dns0-ip>:<dns1-ip>

<server-ip>:服务器 IP 地址,比如我的 Ubuntu 主机 IP 地址为 192.168.1.250。
<root-dir>:根文件系统的存放路径,要保证放在nfs共享目录下。比如我的就是/media/cvitek/robin.lee/rootfs
<nfs-options>:NFS 的其他可选选项,一般不设置。
<client-ip>:客户端 IP 地址,也就是我们开发板的 IP 地址
<server-ip>:服务器 IP 地址,前面已经说了。
<gw-ip>:网关地址,我的就是 192.168.1.1。
<netmask>:子网掩码,我的就是 255.255.255.0。
<hostname>:客户机的名字,一般不设置,此值可以空着。
<device>:设备名,也就是网卡名,一般是 eth0,eth1….,正点原子的 I.MX6U-ALPHA 开发板的 ENET2 为 eth0,ENET1 为 eth1。
<autoconf>:自动配置,一般不使用,所以设置为 off。
<dns0-ip>:DNS0 服务器 IP 地址,不使用。
<dns1-ip>:DNS1 服务器 IP 地址,不使用。

最终我们设置 bootargs 环境变量的 root 值如下:
root=/dev/nfs nfsroot=192.168.1.250:/media/cvitek/robin.lee/rootfs,proto=tcp rw ip=192.168.1.251:192.168.1.250:192.168.1.1:255.255.255.0::eth0:off

“proto=tcp”表示使用 TCP 协议,“rw”表示 nfs 挂载的根文件系统为可读可写。
修改保存环境变量:

1
2
setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.1.250:/media/cvitek/robin.lee/rootfs,proto=tcp rw ip=192.168.1.251:192.168.1.250:192.168.1.1:255.255.255.0::eth0:off' #设置 bootargs
saveenv #保存环境变量

设置好以后使用“boot”命令启动 Linux 内核,输出打印如下:
image
可以看到成功挂载rootfs,并且进入Linux控制台。

3.1.5 根文件系统添加 etc配置脚本#

3.1.5.1 创建/etc/init.d/rcS#

前面打印有一行报错,进入根文件系统的时候会有下面这一行错误提示:

1
can't run '/etc/init.d/rcS': No such file or directory

rcS 是个 shell 脚本,Linux 内核启动以后需要启动一些服务,而 rcS 就是规定启动哪些文件的脚本文件。在 rootfs 中创建/etc/init.d/rcS 文件,然后在 rcS 中输入如下所示内容:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/sh

PATH=/sbin:/bin:/usr/sbin:/usr/bin:$PATH
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/lib:/usr/lib
export PATH LD_LIBRARY_PATH

mount -a
mkdir /dev/pts
mount -t devpts devpts /dev/pts

echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s

PATH 环境变量保存着可执行文件可能存在的目录;
LD_LIBRARY_PATH 环境变量保存着库文件所在的目录;
mount 命令来挂载所有的文件系统,这些文件系统由文件/etc/fstab 来指定;
创建目录/dev/pts,然后将 devpts 挂载到/dev/pts 目录中。
使用 mdev 来管理热插拔设备,通过这两行,Linux 内核就可以在/dev 目录下自动创建设备节点。

我们是简单构造了一个最简单的rcS, 大家如果去看 Ubuntu 或者其他大型 Linux操作系统中的 rcS 文件,就会发现其非常复杂。而且这么复杂的 rcS 文件也是借助其他工具创建的,比如 buildroot 等。

1
chmod 777 rcS

重新启动 Linux 内核,启动以后如下图:
image

3.1.5.2 创建/etc/fstab#

fstab 在 Linux 开机以后自动配置哪些需要自动挂载的分区,格式如下:

1
2
3
4
5
6
7
8
<file system> <mount point> <type> <options> <dump> <pass>

<file system>:要挂载的特殊的设备,也可以是块设备,比如/dev/sda 等等
<mount point>:挂载点。
<type>:文件系统类型,比如 ext2、ext3、proc、romfs、tmpfs 等等。
<options>:挂载选项,一般使用 defaults,defaults 包含了 rw、suid、 dev、 exec、 auto、 nouser 和 async。
<dump>:为 1 的话表示允许备份,为 0 不备份,一般不备份,因此设置为 0。
<pass>:磁盘检查设置,为 0 表示不检查。根目录‘/’设置为 1,其他的都不能设置为 1,因此这里一般设置为 0

fstab内容设置如下:

1
2
3
4
#<file system>   <mount point>     <type>     <options>   <dump>    <pass>
proc /proc proc defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
sysfs /sys sysfs defaults 0 0

fstab 文件创建完成以后重新启动 Linux打印如下:
image

3.1.5.3 创建/etc/inittab文件#

inittab 的详细内容可以参考 busybox 下的文件 examples/inittab。init 程序会读取/etc/inittab这个文件,inittab 由若干条指令组成。每条指令的结构都是一样的,由以“:”分隔的 4 个段组成,格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<id>:<runlevels>:<action>:<process>
<id>:每个指令的标识符,不能重复。但是对于 busybox 的 init 来说,<id>有着特殊意义。对于 busybox 而言<id>用来指定启动进程的控制 tty,一般我们将串口或者 LCD 屏幕设置为控制 tty
<runlevels>:对 busybox 来说此项完全没用,所以空着。
<action>:动作,用于指定<process>可能用到的动作:
busybox 支持的动作如下:
sysinit: 在系统初始化的时候 process 才会执行一次。
respawn: 当 process 终止以后马上启动一个新的。
askfirst:和 respawn 类似,在运行 process 之前在控制台上显示“Please press Enter to activate this console.”。只要用户按下“Enter”键以后才会执行 process。
wait: 告诉 init,要等待相应的进程执行完以后才能继续执行。
once: 仅执行一次,而且不会等待 process 执行完成。
restart: 当 init 重启的时候才会执行 procee。
ctrlaltdel: 当按下 ctrl+alt+del 组合键才会执行 process。
shutdown: 关机的时候执行 process。
<process>:具体的动作,比如程序、脚本或命令等

我们的/etc/inittab内容设置如下:

1
2
3
4
5
6
7
#etc/inittab
::sysinit:/etc/init.d/rcS
console::askfirst:-/bin/sh
::restart:/sbin/init
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r
::shutdown:/sbin/swapoff -a

第 2 行,系统启动以后运行/etc/init.d/rcS 这个脚本文件。
第 3 行,将 console 作为控制台终端,也就是 ttymxc0。
第 4 行,重启的话运行/sbin/init。
第 5 行,按下 ctrl+alt+del 组合键的话就运行/sbin/reboot,看来 ctrl+alt+del 组合键用于重启系统。
第 6 行,关机的时候执行/bin/umount,也就是卸载各个文件系统。
第 7 行,关机的时候执行/sbin/swapoff,也就是关闭交换分区。

image

3.1.5.3 创建/etc/resolv.conf#

/etc/resolv.conf是DNS客户机配置文件,用于设置DNS服务器的IP地址及DNS域名,还包含了主机的域名搜索顺序。该文件是由域名解析器(resolver,一个根据主机名解析IP地址的库)使用的配置文件。

1
2
3
nameserver: 配置DNS服务器地址(顺序来查询,且只有当第一个nameserver没有反应时才查询下面的nameserver)
domain: 声明主机的域名,当查询不完全的域名时主机名将被使用(相当于search的默认值)
search: 它的多个参数指明域名查询顺序。当查询不完全的域名时会使用到(domain和search不能共存)

默认/etc/resolv.conf配置:

1
2
# Generated by NetworkManager
nameserver 192.168.248.2

一般将nameserver设置成网关地址,再加入8.8.8.8114.114.114.114

ping blog.csdn.com

1
2
3
4
[root@node2 ~]# ping blog.csdn.com
PING blog.csdn.com.com (45.11.57.36) 56(84) bytes of data.
64 bytes from comcomproxy1.com.com (45.11.57.36): icmp_seq=1 ttl=44 time=291 ms
64 bytes from comcomproxy1.com.com (45.11.57.36): icmp_seq=2 ttl=44 time=270 ms

ping blog,发现不通,需要设置domain域

1
2
[root@node2 ~]# ping blog
ping: blog: 未知的名称或服务

调整/etc/resolv.conf配置文件,添加domain

1
2
3
4
[root@node2 ~]# vi /etc/resolv.conf
# Generated by NetworkManager
nameserver 192.168.248.2
domain csdn.net

再次ping blog, ping不完整域名会自动补全

1
2
3
[root@node2 ~]# ping blog
PING blog.csdn.net (182.92.187.217) 56(84) bytes of data.
64 bytes from 182.92.187.217 (182.92.187.217): icmp_seq=1 ttl=89 time=20.4 ms

调整/etc/resolv.conf配置文件,添加search

1
2
3
4
[root@node2 ~]# vi /etc/resolv.conf
# Generated by NetworkManager
nameserver 192.168.248.2
search abc.com csdn.net

3.1.5.4 /proc/cmdline#

cat /proc/cmdline 可以看到内核启动时U-boot传入参数:
image

4 mkfs工具制作ext4#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
robin.lee@WORKSTATION5:/media/cvitek/robin.lee/zip/a2_dev/install/soc_cv186ah_wevb_emmc$ dd if=/dev/zero of=rootfs.ext4 bs=1M count=1024
1024+0 records in
1024+0 records out
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 0.391472 s, 2.7 GB/s
robin.lee@WORKSTATION5:/media/cvitek/robin.lee/zip/a2_dev/install/soc_cv186ah_wevb_emmc$ mkfs.ext4 -L rootfs rootfs.ext4
mke2fs 1.45.5 (07-Jan-2020)
Discarding device blocks: done
Creating filesystem with 262144 4k blocks and 65536 inodes
Filesystem UUID: a14867d2-d5db-4595-aa4b-fff7ef483bdd
Superblock backups stored on blocks:
32768, 98304, 163840, 229376

Allocating group tables: done
Writing inode tables: done
Creating journal (8192 blocks): done
Writing superblocks and filesystem accounting information: done