【notes8】kbuild,内核模块化,设备树,休眠&唤醒,proc,调用堆栈,printk/动态打印,trace,性能工具,debugfs
文章目录
1.kbuild编译系统
kernel源码目录第一层有Kconfig(后厨原材料)和.config(点菜的清单)文件。menuconfig(菜单),Deconfig(预制菜单 熟客使用)。
obj-y += foo.o bar.o // 表示foo.c和bar.c文件被编译成foo.o和bar.o
// kernel/driver/spi/build-in.a静态库是该目录下所有目标文件(.o文件)打包而成
目标由多个源文件编译得到:追加的效果




malloc不是内核API而是libc函数,参考【notes7】。
1.1 Kconfig
参考【bmc1】内核添加驱动。
驱动模块使用的资源一摸一样(如gpio),只能加载一个,如下只能选一个,互斥:
depends on只有依赖条件成立,配置项才显示。
1.2 Makefile

条件编译:最常用的两种格式#ifdef和#ifndef 。#undef :取消已定义的标识符
如下book145.c和_public.c都有 #include"_public.h",会重复包含。
在_public.c中如下这样写,_public.h就不会被重复包含。
如下$前一个tab键不能8个空格。make默认是make all,如果将all这行book3删除,则make不会编译book3,可指定make book3,book3相当于标签。-欧2是让编译效率最高,一般正式发布用。gcc命令选项 :-c编译不链接。

如下是编译内核,第一行判断路径是否存在。


EXRA_CFLAGS用于额外编译选项:
2.内核模块化



2.1 传参
权限位S_IRUGO是用在sysfs文件系统里,static int…只是初始化给默认值。


2.2 符号导出
驱动程序编译生成的 ko 文件是相互独立的, 即模块之间变量或者函数在正常情况下无法进行互相访问。extern:声明来自另一个模块。

2.4 错误码


如下fail里将return ret。如上地址是64位内核空间最后一页,指针落到这段地址说明是错误的无效指针。
2.5 uevent

struct kobject *mykobject01;
struct kset *mykset;
struct kobj_type mytype;
static int mykobj_init(void)
{
int ret;
// 创建并添加一个kset
mykset = kset_create_and_add("mykset", NULL, NULL);
// 分配并初始化一个kobject
mykobject01 = kzalloc(sizeof(struct kobject), GFP_KERNEL);
mykobject01->kset = mykset;
// 初始化并添加kobject到kset
ret = kobject_init_and_add(mykobject01, &mytype, NULL, "%s", "mykobject01"); // mykobject01
// 触发一个uevent事件,表示kobject的属性发生了变化
ret = kobject_uevent(mykobject01, KOBJ_CHANGE);
return 0;
}
static void mykobj_exit(void)
{
kobject_put(mykobject01); // 释放kobject
kset_unregister(mykset);
}
module_init(mykobj_init);
module_exit(mykobj_exit);
MODULE_LICENSE("GPL");
// insmod 上面.ko
// ls /sys/mykset/mykobject01/ (里面是空的,没有任何属性文件)
// dmesg | tail -n 1 或 udevadm monitor --kernel
// kobject_uevent: KOBJ_CHANGE for mykobject01



如下device_create里调用kobject_uevent告诉用户态的二进制udev机制,说有一个设备节点的创建,udev帮对设备文件创建。


2.3 设备号
具有相同主设备号(哪一类)设备使用相同驱动程序,次设备号(哪一个)用来标识连接系统中相同的设备。l:符号链接,s:套接字,p:管道。

如下次设备号个数不是次设备号(hello_minor)。

块设备申请主设备号
hello_major为0是系统自动分配的主设备号即register_blkdev函数分配。

2.4 字符设备驱动
如下crw—中c就是char字符设备(以字节为单位输入输出,没有固定大小,也没有缓冲区,数据会立即传输: 串口、鼠标)。块设备以b开头(以块为单位输入输出,有缓冲区,数据下发以后会在缓冲区中缓存,可以被分区,还可以格式化它的文件系统【不能裸设备访问,坏块多,都会上文件系统】:硬盘、U盘)。网络设备ifconfig查看。

cdev_init是方法填充,cdev_add是属性(如主次设备号)的填充。


设备读写

write函数中一个进程写没问题,两进程写:第一个进程运行到kzalloc时,第二个进程也执行了kzalloc,只第二个进程地址保存在c中,第一个进程分配内存空间地址丢失造成内存泄漏。第一个进程运行到kzalloc时第二个进程调用了kfree,这时第一个进程执行到copy_from_user出现问题。

如下cdev_map是整个绿色:
杂项设备

struct file_operations misc_fops = { //文件操作集
.owner = THIS_MODULE ////将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模
.read =
.write =
};
struct miscdevice misc_dev = { //杂项设备结构体
.minor = MISC_DYNAMIC_MINOR, //动态申请的次设备号
.name = "test", //杂项设备名字是hello_misc
.fops = &misc_fops, //文件操作集
};
static int __init misc_init(void)
{
int ret;
ret = misc_register(&misc_dev); //在初始化函数中注册杂项设备
if (ret < 0)
{
printk("misc registe is error \n"); //打印注册杂项设备失败
}
return 0;
}
static void __exit misc_exit(void)
{
misc_deregister(&misc_dev); //在卸载函数中注销杂项设备
printk(" misc goodbye! \n");
}
module_init(misc_init);
module_exit(misc_exit);
MODULE_LICENSE("GPL");
2.5 platform总线



案例:

// paltform_device.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#define GPIO0_BASE (0xfdd60000)
#define GPIO0_DR (GPIO0_BASE + 0x0004)
#define GPIO0_DDR (GPIO0_BASE + 0x000C)
static struct resource led_resource[] = {
[0] = DEFINE_RES_MEM(GPIO0_DR, 4), //数据 寄存器
[1] = DEFINE_RES_MEM(GPIO0_DDR, 4), //方向 寄存器
};
static void led_release(struct device *dev)
{}
unsigned int led_hwinfo[1] = { 7 }; // 片选偏移 ,led - GPIIO0_C7引脚
static struct platform_device led_pdev = { // 没有设备树之前 //////////////////////////重点
.name = "led_pdev", //匹配下面paltform_driver.c
.id = 0,
.num_resources = ARRAY_SIZE(led_resource),
.resource = led_resource,
.dev = {
.release = led_release,
.platform_data = led_hwinfo,
},
};
///////////////////////////////////////////////////////////////////////////////////////
static __init int led_pdev_init(void)
{
printk("pdev init\n");
platform_device_register(&led_pdev); // platform总线添加一设备
return 0;
}
module_init(led_pdev_init);
static __exit void led_pdev_exit(void)
{
printk("pdev exit\n");
platform_device_unregister(&led_pdev);
}
module_exit(led_pdev_exit);
MODULE_DESCRIPTION("Platform LED device");
MODULE_AUTHOR("embedfire");
MODULE_LICENSE("GPL");
// paltform_driver.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#define DEV_MAJOR 243
#define DEV_NAME "led"
static struct class *led_test_class;
//结构体led_data来管理我们LED灯的硬件信息
struct led_data {
unsigned int led_pin;
unsigned int __iomem *va_DDR;
unsigned int __iomem *va_DR;
struct cdev led_cdev;
};
static int led_cdev_open(struct inode *inode, struct file *filp)
{
unsigned int val = 0;
struct led_data *cur_led = container_of(inode->i_cdev, struct led_data, led_cdev);
printk("led_cdev_open() \n");
// 设置引脚输出
val = readl(cur_led->va_DDR); // 方向
val |= ((unsigned int)0X1 << (cur_led->led_pin+16));
val |= ((unsigned int)0X1 << (cur_led->led_pin));
writel(val,cur_led->va_DDR);
//设置默认输出高电平
val = readl(cur_led->va_DR); // 数据
val |= ((unsigned int)0X1 << (cur_led->led_pin+16));
val |= ((unsigned int)0x1 << (cur_led->led_pin));
writel(val, cur_led->va_DR);
filp->private_data = cur_led;
return 0;
}
static int led_cdev_release(struct inode *inode, struct file *filp)
{
return 0;
}
static ssize_t led_cdev_write(struct file *filp, const char __user * buf,
size_t count, loff_t * ppos)
{
unsigned long val = 0;
unsigned long ret = 0;
int tmp = count;
struct led_data *cur_led = (struct led_data *)filp->private_data;
val = kstrtoul_from_user(buf, tmp, 10, &ret);
val = readl(cur_led->va_DR); // 方向
if (ret == 0)
{
val |= ((unsigned int)0x1 << ((cur_led->led_pin)+16));
val &= ~((unsigned int)0X1 << (cur_led->led_pin));
}
else
{
val |= ((unsigned int)0x1 << (cur_led->led_pin+16));
val |= ((unsigned int)0X1 << (cur_led->led_pin));
}
writel(val, cur_led->va_DR);
*ppos += tmp; // 更新文件位置指针
return tmp; // 返回写入的字节数
}
static struct file_operations led_cdev_fops = {
.open = led_cdev_open,
.release = led_cdev_release,
.write = led_cdev_write,
};
/////////////////////////////////////////////////////////////////////////////////////////
static int led_pdrv_probe(struct platform_device *pdev) // 传入device,驱动需要去提取设备的资源,完成字符设备的注册等工作
{
struct led_data *cur_led;
unsigned int *led_hwinfo;
struct resource *mem_DR;
struct resource *mem_DDR;
dev_t cur_dev;
int ret = 0;
printk("led platform driver probe\n");
//第一步:提取平台设备提供的资源
//devm_kzalloc函数申请cur_led和led_hwinfo结构体内存大小
cur_led = devm_kzalloc(&pdev->dev, sizeof(struct led_data), GFP_KERNEL);
if(!cur_led)
return -ENOMEM;
led_hwinfo = devm_kzalloc(&pdev->dev, sizeof(unsigned int)*2, GFP_KERNEL);
if(!led_hwinfo)
return -ENOMEM;
led_hwinfo = dev_get_platdata(&pdev->dev); // LED灯的寄存器偏移量 即 7
cur_led->led_pin = led_hwinfo[0];
mem_DR = platform_get_resource(pdev, IORESOURCE_MEM, 0); // 數據寄存器
mem_DDR = platform_get_resource(pdev, IORESOURCE_MEM, 1); //方向寄存器
cur_led->va_DR = devm_ioremap(&pdev->dev, mem_DR->start, resource_size(mem_DR)); // 寄存器地址转化为虚拟地址
cur_led->va_DDR = devm_ioremap(&pdev->dev, mem_DDR->start, resource_size(mem_DDR));
//第二步:注册字符设备(才有fops)
cur_dev = MKDEV(DEV_MAJOR, pdev->id);
register_chrdev_region(cur_dev, 1, "led_cdev");
cdev_init(&cur_led->led_cdev, &led_cdev_fops);
ret = cdev_add(&cur_led->led_cdev, cur_dev, 1);
if(ret < 0)
{
printk("fail to add cdev\n");
goto add_err;
}
device_create(led_test_class, NULL, cur_dev, NULL, DEV_NAME "%d", pdev->id); // /dev/led0
//platform_set_drvdata函数,将LED数据信息存入在平台驱动结构体中pdev->dev->driver_data中
platform_set_drvdata(pdev, cur_led);
return 0;
add_err:
unregister_chrdev_region(cur_dev, 1);
return ret;
}
static int led_pdrv_remove(struct platform_device *pdev)
{
dev_t cur_dev;
//platform_get_drvdata,获取当前LED灯对应的结构体
struct led_data *cur_data = platform_get_drvdata(pdev);
printk("led platform driver remove\n");
cur_dev = MKDEV(DEV_MAJOR, pdev->id);
//cdev_del删除对应的字符设备
cdev_del(&cur_data->led_cdev);
//删除/dev目录下的设备
device_destroy(led_test_class, cur_dev);
//unregister_chrdev_region, 注销掉当前的字符设备编号
unregister_chrdev_region(cur_dev, 1);
return 0;
}
static struct platform_device_id led_pdev_ids[] = {
{.name = "led_pdev"}, //匹配
{}
};
MODULE_DEVICE_TABLE(platform, led_pdev_ids);
//平台总线匹配过程中 ,只会根据id_table中的name值进行匹配,若和平台设备的name值相等,则表示匹配成功; 反之,则匹配不成功,表明当前内核没有该驱动能够支持的设备。
static struct platform_driver led_pdrv = { //////////////////////////重点
.probe = led_pdrv_probe,
.remove = led_pdrv_remove,
.driver.name = "led_pdev",
.id_table = led_pdev_ids,
};
static __init int led_pdrv_init(void)
{
printk("led platform driver init\n");
//class_create,来创建一个led类
led_test_class = class_create(THIS_MODULE, "test_leds");
//调用函数platform_driver_register,注册我们的平台驱动结构体,这样当加载该内核模块时, 就会有新的平台驱动加入到内核中。 第20-27行,注销
platform_driver_register(&led_pdrv);
return 0;
}
module_init(led_pdrv_init);
static __exit void led_pdrv_exit(void)
{
printk("led platform driver exit\n");
platform_driver_unregister(&led_pdrv);
class_destroy(led_test_class);
}
module_exit(led_pdrv_exit);
MODULE_AUTHOR("Embedfire");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("the example for platform driver");
2.6 设备驱动模型

... __list_add(struct list_head *new, //node3
struct list_head *prev, //head
struct list_head *next) //node2
{
next->prev = new; //node2->prev 等于 node3
new->next = next; //node3->next等于 node2
new->prev = prev; //node3->prev 等于 head
}
如下是类似管道,先入先出,node123从左往右按顺序排列。list_add函数调用如下栈,先入后出,如上图。
// kobject是最基础的结构体,/sys/device目录是一个kobject,里面还有很多kobject
// drivers/base/core.c:
static struct kobject * class_kobj = NULL;
static struct kobject * devices_kobj = NULL;
/* 创建/sys/class */
class_kobj = kobject_create_and_add("class", NULL);
/* 创建 /sys/devices */
devices_kobj = kobject_create_and_add("devices", NULL);

如上注册完bus后生成如下目录。
如下创建链接就行,没必要重复。

如下比device注册做的事情少很多。
创建链接指向module,表示该驱动是由哪个内核模块提供能力,同样module也反向指向驱动,表示它提供什么样的驱动能力。
driver去扫匹配成功后不退出,因为挂在这总线上设备不一定只有一个,比如usb上挂鼠标,挂键盘。device只扫一个driver就停止。bus_for_each_drv扫的是klist_driver。如下粉色框是driver_register封装。
3.设备树
dts -》 dtb - 》device_node - 》platform_device。
DTC(Device Tree Compiler,源代码和相关工具放在scripts/dtc目录,CONFIG_DTC,也可反编译)编译器将dts/dtsi文件编译成二进制dtb文件。
reg=<0x100 0x4> : 地址从0x100开始的4字节寄存器区域
spi和i2c设备属于非内存映射设备,它们的地址对cpu不可访问,而父设备的驱动(即总线控制器驱动)代表cpu访问,spi reg代表spi chip select 0 , i2c reg代表设备地址

3.1 时钟
时钟(类似心跳)生产者(板卡厂商,定义):1.clock-cells:=0表示一个时钟,=1表示多个时钟。
2.clock-frequency:频率,单位HZ
3.assigned-clocks:
4.assigned-clocks-rates:
时钟消费者(驱动,引用):
1.clocks:类似assigned-clocks
2.clock-names:
mac1: ftgmac@14060000 {
compatible = “aspeed,ast2700-mac”, “faraday,ftgmac100”;
reg = <0x0 0x14060000 0x0 0x200>;
interrupts-extended = <&intc1_4 1>;
clocks = <&syscon1 SCU1_CLK_GATE_MAC1CLK>;
syscon1: syscon@14c02000 {
compatible = “aspeed,ast2700-scu1”, “syscon”, “simple-mfd”;
aspeed,ast2700-scu.h , clk-ast2700.c
3.2 中断属性
1.interrupts:下图有3位(父亲是gic控制器),interrupt-cells给引用的子节点用的,不是当前节点。2.interrupt-controller:当前节点是一个中断控制器。
ft5x06:ft5x06@38{ //触摸屏有事件 → 产生中断信号 → 通过RK_PB5引脚 → GPIO控制器接收 → GPIO控制器向GIC报告中断号33 → CPU处理中断
interrupts=<RK_PB5 IRQ_TYPE_LEVEL_LOW>;
interrupt-parent = <&gpio0>
};
RK_PB5是Rockchip(瑞芯微)平台的GPIO引脚命名:
RK:Rockchip缩写
P:Port(端口)
B5:B组的第5个引脚
RK_PB5 通常对应具体的物理引脚号,比如GPIO0_B5
// 获取中断号
int irq = gpio_to_irq(RK_PB5); // 将GPIO引脚转换为Linux内核中断号
// 注册中断处理函数
request_irq(irq, touch_handler, IRQF_TRIGGER_LOW, "ft5x06", ...);
3.interrupt-cells:约束children的interrupt属性有几位组成,为了解析方便。4.interrupt-parent:指向谁提供中断即中断控制器。
irq_of_parse_and_map函数 :解析设备节点的"interrupts"属性, 并将对应的中断号映射到系统的中断号,拿到irq,为了后续request_irq。
irq_get_trigger_type 函数:获取interrupts属性的最后一位即触发方式。
3.3 pinctrl & gpio
pinctrl指定pin复用功能(gpio,串口,i2c…),

/proc/device-tree/…

如下gpio0:32到64。每个gpio控制器里有4组,每组8个。

4.休眠&唤醒
休眠状态指的是一种系统低功耗运行状态。在此状态下,各种支持休眠模式的外围设备也都全部进入休眠模式,CPU 挂起,所有用户态应用程序和内核态进程全部被冻结,内存处于自刷新模式。系统处于休眠状态会屏蔽所有除唤醒之外的所有命令,直到系统被某种原因唤醒才会解除此种状态。
// cat /sys/power/state【freeze: 可快速恢复,mem: 长时间 省电】 : 支持哪些挂起模式,不是当前状态
// echo freeze/mem > /sys/power/state : 触发休眠,命令敲完后,串口卡住没反应了 (在/sys/power/autosleep【CONFIG_PM_AUTOSLEEP:自动休眠唤醒】是off状态才行)
// kernel/power/suspend.c:
int pm_suspend(suspend_state_t state) // autosleep也是调用这行函数
{
error = enter_state(state); // 进入suspend状态, 具体的状态根据state值而定, 如果执行成功的话, 一直到系统退出suspend的状态后此函数才退出
}
// 如果外设没有实现dev_pm_ops,那么外设对于休眠唤醒没有任何动作
struct dev_pm_ops {
int (*suspend)(struct device *dev);//休眠,下电,与下面成对出现
int (*resume)(struct device *dev);//唤醒,上电
...
int (*runtime_suspend)(struct device *dev); // 单设备:并不是要求设备一定要进入低功耗状态, 而是要求设备在suspend后,不再处理数据,不再和CPUs、RAM进行任何的交互,直到设备的.runtime_resume被调用
int (*runtime_resume)(struct device *dev);
};
如整机休眠时调用下面外设的.suspend。

如下查看哪个驱动模块当前有活跃锁,导致整机休眠不下去。自动休眠唤醒suspend控制器发现如果/sys/kernel/debug/wakeup_sources中active_since一列【如下就是调用wake_lock函数,也可echo设置/sys/power/wake_lock】都为0即没有持锁【即没有唤醒】就休眠。
如下__pm_relax用来释放先前通过__pm_stay_awake增加的唤醒请求即active_since一列。

如按触屏和播放视频都不能休眠。

/*
有时候进程在读设备时,发现设备数据还没准备好,没办法正常读取设备。或在写设备时,发现设备缓冲区满,没办法正常写设备。进程在操作设备时,如果条件不满足,就让它进入休眠等待,直到条件满足,就可唤醒进程进行后面操作。
初始化:
DECLARE_WAIT_QUEUE_HEAD() //初始化等待队列头,宏的静态方式
或
wait_queue_head_t wq; //动态方式
init_waitqueue_head(&wq);
休眠:
wait_event() //不可被打断,死等,直到满足条件为止
wait_event_interruptible() //可使用信号打断它,常用
唤醒:
wake_up() //对应wait_event()
wake_up_interruptible() //对应上面可中断方式休眠的进程wait_event_interruptible()
*/
# include <linux/wait.h>
DECLARE_WAIT_QUEUE_HEAD(wq);
{ // hc_read函数中
printk(KERN_INFO "read hc_dev %p\n",hc_dev);
// wait_event(wq,hc_dev->c!=NULL); //第一个参数:等待队列头。第二个参数:等待条件: 如果设备字符串区(即echo值进设备文件里)为空就进入等待(唤醒方法在write函数中)。
wait_event_interruptible(wq,hc_dev->c!=NULL);
}
{ // hc_write函数中
printk(KERN_INFO"%s write done",current->comm);
// wake_up(&wq); // 当写入字符设备成功后调用wakeup函数唤醒等待队列上的进程
wake_up_interruptible(&wq);
return count;
}

如下等待条件满足,如上卡住的即休眠的进程会正常退出(两个进程cat卡住休眠,执行如下一行,两个进程都不会卡住)。
5.proc文件系统

/*
新调试方法:利用proc文件系统在pro文件夹下创建接口,读写这个接口就可实现对内核的调试
struct proc_ops //pro文件夹下创建接口第一种方式
proc_create()
struct seq_operations //第二种方式
proc_create_seq()
remove_proc_entry //移除接口
*/
#include<linux/module.h>
#include<linux/uaccess.h>
#include<linux/string.h>
#define PROC_DEBUG
#ifdef PROC_DEBUG
#include<linux/proc_fs.h> //传统第一种方式
#include<linux/seq_file.h> //seq第二种方式
#endif
char * str = "hello proc\n";
#ifdef PROC_DEBUG
int hp_open(struct inode * inode, struct file * filp)
{
printk(KERN_INFO"open %ld\n",strlen(str));
return 0;
}
ssize_t hp_read(struct file * filp, char __user * buff, size_t count, loff_t * f_pos)
{
ssize_t retval=0;
int n = strlen(str); // 内容总长度(11字节,含\n)
if(*f_pos >= n) // 已经读完
goto out;
if(*f_pos + count > n) // 防止越界
count = n - *f_pos; // 调整读取长度
if(copy_to_user(buff,str,count)) //将字符串str("hello proc\n")赋值到buff用户空间
{
retval = -EFAULT;
goto out;
}
*f_pos += count; // 更新偏移量
return count;
out:
return retval;
}
struct proc_ops hp_ops = { //下面 __init函数中proc_create调用hp_ops创建接口文件
.proc_open = hp_open,
.proc_read = hp_read,
};
//1111111111111111111111111111111111111111111111111111111111111111111111111111111111111
void * hp_seq_start (struct seq_file *m, loff_t *pos) //pos表示当前读到哪个位置或写到哪个位置了
{
printk(KERN_INFO"seq start\n");
if(*pos >= strlen(str)) //pos指当前读到或写到哪个位置,索引
return NULL;
return &str[*pos]; //拿出字符串中字符,将地址返回,这返回值将来作为下面其他函数的v传入
}
void hp_seq_stop(struct seq_file *m, void *v)
{
printk(KERN_INFO"seq stop\n"); //清除start函数一些工作,start里开辟一些空间或申请一些锁,这里清除
}
void * hp_seq_next (struct seq_file *m, void *v, loff_t *pos) //改变索引值
{
printk(KERN_INFO"seq next\n");
(*pos)++;
if(*pos >= strlen(str))
return NULL;
return &str[*pos];
}
int hp_seq_show (struct seq_file *m, void *v)
{
printk(KERN_INFO"seq show\n");
seq_putc(m,*(char*)v); //将获得到的字符一个一个打印出
return 0;
}
const struct seq_operations seq_ops={ //构建这结构体
.start = hp_seq_start,
.stop = hp_seq_stop,
.next = hp_seq_next,
.show = hp_seq_show,
};
#endif
//11111111111111111111111111111111111111111111111111111111111111111111111111111111
static int __init hello_init(void)
{
printk(KERN_INFO "HELLO LINUX MODULE\n");
#ifdef PROC_DEBUG
proc_create("hello_proc",0,NULL,&hp_ops); //第一个参数即显示在pro目录下文件名称,
//第二个参数默认0只读权限。第三个参数父节点,null默认pro目录。最后一个参数是操作的结构体地址
proc_create_seq("hello_seq_proc",0,NULL,&seq_ops); //就可在pro目录下创建对应节点
#endif
return 0;
}
static void __exit hello_exit(void)
{
#ifdef PROC_DEBUG
remove_proc_entry("hello_proc",NULL); //第二个参数是父节点
remove_proc_entry("hello_seq_proc",NULL);
#endif
printk(KERN_INFO "GOODBYE LINUX\n");
}
module_init(hello_init);
module_exit(hello_exit); ...
show next调用了11次,最后一次返回null会调用stop。cat又会再调用一次,调start后直接返回null到stop。
6.调用堆栈内核函数

WARN_ON(condition)函数实际也是调用dump_stack,如WARN_ON(1)条件判断成功,类似BUGON(1)。
7.printk
cat /proc/cmdline查看console=ttyS0,如下图左边分支是远程登录,缺点是log_buf是环形buff(会循环覆盖,日志不全)。左右分支都有dmesg,左分支dmesg不受下面x控制,所以dmesg是最全的。

// Linux 内核为printk定义了8个打印等级,KERN_EMERG等级最高,KERN_DEBUG等级最低。在内核配置时,有一个宏来设定系统默认的打印等级 CONFIG_MESSAGE_LOGLEVEL_DEFAULT,通常该值设置为4,那么只有打印等级高于4时才会打印到终端或者串口。
// kern_levels.h
#define KERN_EMERG KERN_SOH "0" /* system is unusable 紧急事件,一般是系统崩溃之前的提示消息 */
#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately 必须立即采取行动 */
#define KERN_CRIT KERN_SOH "2" /* critical conditions 临界状态,通常涉及严重的硬件或者软件操作失败 */
#define KERN_ERR KERN_SOH "3" /* error conditions 报告错误状态,经常用来报告硬件错误 */
#define KERN_WARNING KERN_SOH "4" /* warning conditions 对可能出现问题的情况进行警告,通常不会对系统造成严重问题 */
#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition 有必要的提示,通常用于安全相关的状况汇报 */
#define KERN_INFO KERN_SOH "6" /* informational 提示信息,驱动程序常用来打印硬件信息 */
#define KERN_DEBUG KERN_SOH "7" /* debug-level messages 用于调试信息 */
// printk("[me]%s[%d].\n",__func__,__LINE__);
// printk调用printk_safe_enter_irqsave --> local_irq_save【把当前的中断状态(开或关)保存到flags中,然后禁用当前处理器上的中断】,所以:板卡 --> 没有办法跟外界通信(I2C、UART、SPI都用到中断)

8.动态打印
printk是全局的且只能设打印等级,动态打印可控制选择模块的打印,在内核配置打开CONFIG_DYNAMIC_DEBUG:
printk会关中断影响性能,如果在usb的read/write里printk,那么usb就没法直接用了。所以用动态打印,调试时才打开,control节点默认不输出,如下操作才输出,+p是将动态打印语句【pr_debug/dev_dbg()】转为printk语句(相当于下面的define dev_dbg …)。
9.trace
ltrace,strace参考【notes7】epoll,这俩底层实现是ptrace(如下)。
9.1 tracefs

10.性能工具





10.1 磁盘io


10.2 内核热点函数

10.3 网络
ifconfig和ip分别属于net-tools和iproute2:
上面是统计信息,下面是网络吞吐:
10.4 找到cpu瓶颈


10.5 valgrind内存泄漏排查


10.6 主频设置

可echo上面支持的策略 > scaling_governor。
10.7 驱动稳定性
access_ok:判断arg地址的数据长度是不是和len一样大:
likely一般用在if判断里,cpu会把当前指令后面指令预取出来,等到执行时就去执行,效率提高,但是也要判断后面那条指令大概率执不执行,执行的话取出来,不执行则跳过。
10.8 平均负载
性能变慢uptime查看负载情况:平均负载=平均活跃进程数(这进程不仅仅包含正在使用cpu的进程,还包含等待cpu和等待io的进程)。
当平均负载高于cpu数量(参考【notes7】并发或top查看几个cpu核)70%时,就应该排查负载高的问题,一旦负载高,会导致进程响应慢,进而影响服务正常功能。
cpu密集型进程: # stress --cpu 2 --timeout 600 # 持续600秒后自动停止,top/mpstat -P ALL 5 【监控所有cpu,每5秒输出一组数据】查看有2个cpu核100%跑满了,共6个核占满了2个,平均负载30%,uptime中load average第一个即1分钟的平均负载慢慢=2(因为2个进程在跑)
io密集型进程: # stress --i 1 --timeout 600 # 启动1个IO压力线程,不停的刷sync,脏数据刷到磁盘中,没有指定cpu,sync即io操作(均衡负载是把一些sync操作平均分给cpu)会在每个cpu上挂一些系统调用,uptime中load average第一个即1分钟的平均负载慢慢=1
大量进程超过cpu核,等待cpu释放资源,希望cpu调度自己即等待态: # stress -c 8 --timeout 600,uptime中load average第一个即1分钟的平均负载慢慢=8,pidstat查看是不是出现了等待io情况(wait一列等待CPU的时间)
11.debugfs
procfs反应进程状态信息,sysfs用于linux设备模型,debugfs(打开CONFIG_DEBUG_FS, mount -t debugfs none /sys/kernel/debug)如上面第5章节,cat /proc/filesystems查看全部文件系统(nodev表示基于内存的文件系统,不需要物理设备,如像ext4文件系统必须基于块设备)。
cat /etc/fstab是自动挂载的配置文件:/dev/mmcblk0p2 /boot … (/boot里有dtb,image,logo.bmp等),查看flash或emmc能不能正常挂载,能正常挂载,上电Bootloader读取 /boot。
/sys/kernel/debug/gpio:有哪些gpio被注册使用。/sys/kernel/debug/pinctrl:驱动能力、上下拉(pinconf.c / cat pinconf-pins),复用功能(pinmux.c / cat pinmux-pins)。/sys/kernel/debug/wakeup_sources:见第一章。/sys/kernel/debug/sleep_time:系统睡眠时间,没有休眠过就没有。
11.1 源码分析
kernel/fs/debugfs:file.c / inode.c / internal.h / Makefile
static struct file_system_type debug_fs_type = {
.owner = THIS_MODULE,
.name = "debugfs",
.mount = debug_mount,
.kill_sb = kill_litter_super, // 卸载,对应上行
};
MODULE_ALIAS_FS("debugfs");
static int __init debugfs_init(void)
{
int retval;
//在 sysfs 中创建挂载点。
retval = sysfs_create_mount_point(kernel_kobj, "debug"); // 这里"debug"改了,对应 /etc/fstab自动挂载也需要改
if (retval)
return retval;
//注册 debugfs 文件系统类型(遍历file_system_type,添加nodev加入链表),cat /proc/filesystems。
retval = register_filesystem(&debug_fs_type);
//根据初始化结果清理或标记状态。
if (retval)
sysfs_remove_mount_point(kernel_kobj, "debug");
else
debugfs_registered = true;
return retval;
}
core_initcall(debugfs_init);
11.2 api
// 建立一个内核模块debugfs_hello,并在该内核模块中调用debugfs的API创建下面3个值:
// /sys/kernel/debug/debugfs_hello/hello: 可读写的整数值,即用户可用cat和echo命令来查看和修改该
// /sys/kernel/debug/debugfs_hello/add: 可写的,当用户调用echo命令向其写入一个值,该值会被内核模块加到sum这个值中
// /sys/kernel/debug/debugfs_hello/sum:只读的,用于计算所有输入给add的值的总和,用户可用cat命令查看该值
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/debugfs.h> ///////
static struct dentry *dir = NULL;
static unsigned int debugfs_hello;
static u32 sum = 0;
static int add_write(void *data, u64 value)
{
sum += value;
return 0;
}
DEFINE_SIMPLE_ATTRIBUTE(add_ops, NULL, add_write, "%llu\n"); // 填充add_ops
static __init int hello_init(void)
{
struct dentry *tmp_dir = NULL;
/* create /sys/kernel/debug/debugfs_hello/ directory */
dir = debugfs_create_dir("debugfs_hello", NULL);
if (!dir) {
printk(KERN_ALERT "debugfs_create_dir failed\n");
return -1;
}
/* create /sys/kernel/debug/debugfs_hello/hello value, mode: rw*/
tmp_dir = debugfs_create_u32("hello", 00666, dir, &debugfs_hello);
if (!tmp_dir) {
printk(KERN_ALERT "debugfs_create_u32 failed\n");
return -1;
}
/* create /sys/kernel/debug/debugfs_hello/add value, mode: w*/
tmp_dir = debugfs_create_file("add", 0222, dir, NULL, &add_ops);
if (!tmp_dir) {
printk(KERN_ALERT "debugfs_create_file failed\n");
return -1;
}
/* create /sys/kernel/debug/debugfs_hello/sum value, mode: r*/
tmp_dir = debugfs_create_u32("sum", 0444, dir, &sum);
if (!tmp_dir) {
printk(KERN_ALERT "debugfs_create_u32 failed\n");
return -1;
}
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Exit debugfs_hello module\n");
debugfs_remove_recursive(dir);
dir = NULL;
}
module_init(hello_init);
module_exit(hello_exit); ...
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)