
驱动工程师的工作是连接应用工程师和硬件工程师: 当应用调用你的接口但功能未实现时:
检查驱动日志,添加调试信息(如 dev_dbg),然后重新复现问题查看日志
使用 strace 命令跟踪系统调用日志进行排查
一、前言
1、面向应用开发者
当我们遇到应用性能问题,但系统指标都正常时,该怎么办? 关键是要突破应用和内核的边界,看看内核到底在做什么。以下是具体方法:
1. 先搞懂应用在系统调用里干了啥
应用和内核的分界线就是系统调用(比如 read()、write() 这些函数)。你可以:
记录调用细节:用工具(比如
strace)跟踪应用调用了哪些系统调用,每个调用用了多长时间。看内核在做什么:比如应用调用
read()读文件时,内核可能在等待磁盘响应、处理缓存,或者在排队等待其他任务。
2. 区分“应用自己在忙”和“内核在卡顿”
应用自己耗时:比如应用在计算、循环,这时候问题在代码逻辑里。
内核在耗时:比如系统调用用了很长时间(比如
sleep()突然卡住),说明内核可能在忙其他事(比如处理大量进程、磁盘 IO)。
3. 看内核在忙什么“额外任务”
即使指标正常,内核可能在偷偷做其他事:
优先级问题:比如内核在忙低优先级任务(比如定时任务、后台服务),导致你的应用被“挤占”CPU。
资源争夺:比如多个进程争抢同一资源(内存、磁盘),导致你的应用被阻塞。
隐藏的开销:比如频繁的上下文切换、锁竞争,这些指标可能不会直接显示,但会拖慢应用。
4. 直接观察内核的“黑盒行为”
即使指标正常,也可以通过以下方式“看穿”内核:
抓内核执行轨迹:用工具(比如
perf)记录内核在系统调用中具体做了什么(比如调用了哪些函数)。找时间消耗点:比如应用调用
malloc()时,内核可能在频繁分配内存,导致卡顿。对比正常和异常场景:在问题发生时和正常时,对比内核的执行路径差异,找到异常点。
总结一句话:
应用和内核的分界线是系统调用,分析问题时要“穿透”系统调用,看内核在背后到底在做什么“额外动作”。即使指标正常,也可能存在内核资源竞争、隐藏开销等问题,需要直接追踪内核行为才能定位。
2、内核开发者
作为应用开发者,即使你对程序的业务逻辑不熟悉,也可以通过一个叫 strace 的工具来分析问题。这个工具就像一个"程序行为记录仪",能帮你了解程序在运行时和操作系统之间的互动。
为什么这个工具有用?
它能记录程序每次请求操作系统执行任务(系统调用)的详细过程
可以看到每个操作具体用了多长时间
能同时跟踪多线程程序的所有操作
基本使用方法示例:
strace -T -tt -ff -p 进程号 -o 记录文件(这个命令会跟踪指定进程的所有线程,记录每个系统调用的执行时间和具体操作)
关键知识点:
工作原理:strace 通过拦截程序和操作系统之间的通信来记录数据。理解这一点很重要,因为:
如果问题出在程序内部逻辑(比如算法错误),strace 可能帮不上忙
如果问题涉及操作系统交互(比如文件读写、网络请求),strace 能直接定位问题位置
典型应用场景:
当程序响应变慢时,通过
-T参数能快速找到耗时的操作步骤当程序卡住时,通过记录可以定位到具体卡在哪个系统操作上
分析多线程程序时,
-ff参数会自动生成分文件记录,让调试更清晰
记住:工具本身只是手段,理解它的运作方式才能知道什么时候用、怎么用。就像修车时,知道听诊器只能检测发动机异响,却检测不了轮胎磨损一样。
二、strace 工具的原理
当我们想用 strace 监控某个进程时,这个工具会通过 ptrace 系统调用"附着"到目标进程上。具体来说:
工作原理:
strace会用PTRACE_SYSCALL模式追踪进程。每当进程执行系统调用时,就会触发一个SIGTRAP信号强制暂停进程。这时strace会接管,记录下系统调用的详细信息(比如名称、参数、耗时等),处理完后再让进程继续运行。这个过程就像每次系统调用都被按了暂停键,处理完才能继续。性能问题: 每次系统调用都被中断并等待
strace处理,会让目标进程执行变慢。比如原本 1 秒完成的任务,可能变成多次暂停+继续,总耗时明显增加。因此在生产环境(比如线上服务器)使用时要格外小心,最好提前做好回滚或监控预案。问题定位下一步: 如果发现某个系统调用特别耗时(比如
read或write用了几十毫秒),接下来需要:
确认具体场景:检查该调用的参数(如文件路径、网络地址)和上下文(如调用前后的代码逻辑)。
深入内核层分析:查看系统日志(
dmesg)、磁盘 IO 状态(iostat)、网络延迟(tcpdump)等,确定是硬件瓶颈(如磁盘慢)、资源竞争(如锁等待),还是内核处理逻辑的问题。对比测试:在相同条件下单独测试该系统调用,确认是否是环境问题还是代码问题。
简单来说:strace 像一个"暂停-记录-继续"的监控器,但频繁暂停会影响性能。若发现某个系统调用异常,需要结合具体参数、内核状态和环境数据进一步排查根本原因。
三、strace 工具的使用
1、相关参数
-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.2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

2、跟踪已经在运行的进程
通过使用 -p 选项能用在运行的进程上。 ./strace -p 800

3、通过它启动要跟踪的进程
./strace ./example_uart 115200

四、收个尾🎉
strace 工具是应用和内核的边界,如果你是一名应用开发者,并且想去拓展分析问题的边界,那你就需要去了解 strace 的原理,还需要了解如何去分析 strace 发现的问题;
五、案例

源码:
应用
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#define CMD_TEST0 _IOW('L',0,int)
struct args{//定义要传递的结构体
int a;
int b;
int c;
};
int main(int argc,char *argv[]){
int fd;//定义int类型文件描述符
struct args test;//定义args类型的结构体变量test
test.a = 1;
test.b = 2;
test.c = 3;
fd = open("/dev/test",O_RDWR,0777);//打开/dev/test设备
if(fd < 0){
printf("file open error \n");
}
ioctl(fd,CMD_TEST0,&test);//使用ioctl函数传递结构体变量test地址
close(fd);
}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
驱动
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/uaccess.h>
#define CMD_TEST0 _IOW('L',0,int)
struct args{
int a;
int b;
int c;
};
struct device_test{
dev_t dev_num; //设备号
int major ; //主设备号
int minor ; //次设备号
struct cdev cdev_test; // cdev
struct class *class; //类
struct device *device; //设备
char kbuf[32];
};
static struct device_test dev1;
static long cdev_test_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct args test;
switch(cmd){
case CMD_TEST0:
if(copy_from_user(&test,(int *)arg,sizeof(test)) != 0){
printk("copy_from_user error\n");
}
printk("a = %d\n",test.a);
printk("b = %d\n",test.b);
printk("c = %d\n",test.c);
break;
default:
break;
}
return 0;
}
/*设备操作函数*/
struct file_operations cdev_test_fops = {
.owner = THIS_MODULE, //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
.unlocked_ioctl = cdev_test_ioctl,
};
static int __init timer_dev_init(void) //驱动入口函数
{
/*注册字符设备驱动*/
int ret;
/*1 创建设备号*/
ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name"); //动态分配设备号
if (ret < 0)
{
goto err_chrdev;
}
printk("alloc_chrdev_region is ok\n");
dev1.major = MAJOR(dev1.dev_num); //获取主设备号
dev1.minor = MINOR(dev1.dev_num); //获取次设备号
printk("major is %d \r\n", dev1.major); //打印主设备号
printk("minor is %d \r\n", dev1.minor); //打印次设备号
/*2 初始化cdev*/
dev1.cdev_test.owner = THIS_MODULE;
cdev_init(&dev1.cdev_test, &cdev_test_fops);
/*3 添加一个cdev,完成字符设备注册到内核*/
ret = cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
if(ret<0)
{
goto err_chr_add;
}
/*4 创建类*/
dev1. class = class_create(THIS_MODULE, "test");
if(IS_ERR(dev1.class))
{
ret=PTR_ERR(dev1.class);
goto err_class_create;
}
/*5 创建设备*/
dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");
if(IS_ERR(dev1.device))
{
ret=PTR_ERR(dev1.device);
goto err_device_create;
}
return 0;
err_device_create:
class_destroy(dev1.class); //删除类
err_class_create:
cdev_del(&dev1.cdev_test); //删除cdev
err_chr_add:
unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
err_chrdev:
return ret;
}
static void __exit timer_dev_exit(void) //驱动出口函数
{
/*注销字符设备*/
unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
cdev_del(&dev1.cdev_test); //删除cdev
device_destroy(dev1.class, dev1.dev_num); //删除设备
class_destroy(dev1.class); //删除类
}
module_init(timer_dev_init);
module_exit(timer_dev_exit);
MODULE_LICENSE("GPL v2");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