大家好,我是杂烩君。
上次分享了一波实用代码片段嵌入式 Linux 必知:几个超实用代码小片段,不少朋友留言说想看更多,那今天就再来一波。
这次的内容涉及结构体内存布局、文件读写封装、终端进度条、core dump 调试这几块,都是平时项目里高频用到的东西。废话不多说,直接上菜。
本文代码均基于 Linux + GCC 环境验证。
终端进度条
先来个有意思的——终端进度条。
做 OTA 升级、固件烧写、批量文件拷贝的时候,光看日志刷屏心里没底,加个进度条一目了然。效果先看:
是不是还挺像回事?实现起来其实也不复杂,核心就是 r 回车符不换行,配合 fflush 刷新输出缓冲区来实现同行刷新。
代码:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
typedefstruct _progress
{
int cur_size;
int sum_size;
}progress_t;
void progress_bar(progress_t *progress_data)
{
int percentage = 0;
int cnt = 0;
char proc[102]; // 100个字符位 + 最后一个'#' + ''
memset(proc, '', sizeof(proc));
percentage = (int)((longlong)progress_data->cur_size * 100 / progress_data->sum_size);
printf("percentage = %d %%n", percentage);
if (percentage <= 100)
{
while (cnt <= percentage)
{
printf("[%-100s] [%d%%]r", proc, cnt);
fflush(stdout);
proc[cnt] = '#';
usleep(100000);
cnt++;
}
}
printf("n");
}
int main(int arc, char *argv[])
{
progress_t progress_test = {0};
progress_test.cur_size = 65;
progress_test.sum_size = 100;
progress_bar(&progress_test);
return0;
}
运行结果:
快速获取结构体成员大小及偏移量
搞嵌入式的应该都知道,结构体的内存布局、对齐方式这些细节经常要关注——通信协议解析、共享内存操作的时候一不注意就踩坑。
获取结构体成员偏移量,标准库 <stddef.h> 里有个 offsetof 宏可以用,不过我们也可以自己实现一版,顺便搞清楚它背后的原理。
思路很简单:把 0 地址强转成结构体指针,再去取成员的地址,这个地址的值就是偏移量——因为结构体基地址是 0 嘛。获取成员大小同理,对这个"虚拟成员"做 sizeof 就行。
代码:
#include <stdio.h>
#define GET_MEMBER_SIZE(type, member) sizeof(((type*)0)->member)
#define GET_MEMBER_OFFSET(type, member) ((size_t)(&(((type*)0)->member)))
typedefstruct _test_struct0
{
char x;
char y;
char z;
}test_struct0;
typedefstruct _test_struct1
{
char a;
char c;
short b;
int d;
test_struct0 e;
}test_struct1;
int main(int arc, char *argv[])
{
printf("GET_MEMBER_SIZE(test_struct1, a) = %zun", GET_MEMBER_SIZE(test_struct1, a));
printf("GET_MEMBER_SIZE(test_struct1, c) = %zun", GET_MEMBER_SIZE(test_struct1, c));
printf("GET_MEMBER_SIZE(test_struct1, b) = %zun", GET_MEMBER_SIZE(test_struct1, b));
printf("GET_MEMBER_SIZE(test_struct1, d) = %zun", GET_MEMBER_SIZE(test_struct1, d));
printf("GET_MEMBER_SIZE(test_struct1, e) = %zun", GET_MEMBER_SIZE(test_struct1, e));
printf("test_struct1 size = %zun", sizeof(test_struct1));
printf("GET_MEMBER_OFFSET(a): %zun", GET_MEMBER_OFFSET(test_struct1, a));
printf("GET_MEMBER_OFFSET(c): %zun", GET_MEMBER_OFFSET(test_struct1, c));
printf("GET_MEMBER_OFFSET(b): %zun", GET_MEMBER_OFFSET(test_struct1, b));
printf("GET_MEMBER_OFFSET(d): %zun", GET_MEMBER_OFFSET(test_struct1, d));
printf("GET_MEMBER_OFFSET(e): %zun", GET_MEMBER_OFFSET(test_struct1, e));
return0;
}
运行结果:
跑出来的结果可以留意一下偏移量——b 的偏移是 2 而不是紧挨着 c 后面的 1+1=2(呃,这里恰好对上了),但 d 的偏移是 4,这就是内存对齐在起作用。编译器为了让 int 对齐到 4 字节边界,会在 b 后面自动填充。搞通信协议的时候不注意这个,对端解析大概率就乱套了。
文件操作封装
文件读写的代码我们几乎每个项目都要写,什么配置参数存储啊、日志落盘啊、固件数据读写啊,太常见了。与其每次都重新写一遍 fopen/fwrite/fclose 那套,不如封装两个通用函数,拿来直接用:
代码:
#include <stdio.h>
static int file_opt_write(const char *filename, void *ptr, int size)
{
FILE *fp;
size_t num;
fp = fopen(filename, "wb");
if (NULL == fp)
{
printf("open %s file error!n", filename);
return-1;
}
num = fwrite(ptr, 1, size, fp);
if (num != size)
{
fclose(fp);
printf("write %s file error!n", filename);
return-1;
}
fclose(fp);
return (int)num;
}
static int file_opt_read(const char *filename, void *ptr, int size)
{
FILE *fp;
size_t num;
fp = fopen(filename, "rb");
if (NULL == fp)
{
printf("open %s file error!n", filename);
return-1;
}
num = fread(ptr, 1, size, fp);
if (num != size)
{
fclose(fp);
printf("read %s file error!n", filename);
return-1;
}
fclose(fp);
return (int)num;
}
typedefstruct _test_struct
{
char a;
char c;
short b;
int d;
}test_struct;
#define FILE_NAME "./test_file"
int main(int arc, char *argv[])
{
test_struct write_data = {0};
write_data.a = 1;
write_data.b = 2;
write_data.c = 3;
write_data.d = 4;
printf("write_data.a = %dn", write_data.a);
printf("write_data.b = %dn", write_data.b);
printf("write_data.c = %dn", write_data.c);
printf("write_data.d = %dn", write_data.d);
file_opt_write(FILE_NAME, (test_struct*)&write_data, sizeof(test_struct));
test_struct read_data = {0};
file_opt_read(FILE_NAME, (test_struct*)&read_data, sizeof(test_struct));
printf("read_data.a = %dn", read_data.a);
printf("read_data.b = %dn", read_data.b);
printf("read_data.c = %dn", read_data.c);
printf("read_data.d = %dn", read_data.d);
return0;
}
这里用 "wb" / "rb" 模式打开文件,就是二进制读写,跟 "w" / "r" 文本模式的区别在于不会对换行符做转换。写结构体数据用二进制模式才是对的,不然在 Windows 上可能会多出 r 来。
运行结果:
后台运行生成 core 文件
程序跑着跑着突然挂了,段错误、非法访问这类问题最头疼——出了现场就没了。这时候如果程序在崩溃时能自动吐出 core 文件,事后再用 gdb 加载分析,就能精准定位到哪行代码出的问题。
代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/resource.h>
#define SHELL_CMD_CONF_CORE_FILE "echo /var/core-%e-%p-%t > /proc/sys/kernel/core_pattern"
#define SHELL_CMD_DEL_CORE_FILE "rm -f /var/core*"
static int enable_core_dump(void)
{
int resource = RLIMIT_CORE;
struct rlimit rlim;
rlim.rlim_cur = RLIM_INFINITY;
rlim.rlim_max = RLIM_INFINITY;
system(SHELL_CMD_DEL_CORE_FILE);
if (0 != setrlimit(resource, &rlim))
{
printf("setrlimit error!n");
return-1;
}
system(SHELL_CMD_CONF_CORE_FILE);
printf("core dump enabled, pattern: /var/core-%%e-%%p-%%tn");
return0;
}
int main(int argc, char **argv)
{
enable_core_dump();
printf("==================segmentation fault test==================n");
// 下面故意触发段错误,仅为演示 core dump 功能
int *p = NULL;
*p = 1234;
return0;
}
core 文件生成后,用 gdb ./your_program /var/core-xxx 加载,bt 命令看调用栈,基本就能锁定崩溃位置了。
好了,以上就是本次分享的几个代码片段,简单汇总一下:
| 代码片段 | 适用场景 |
|---|---|
| 终端进度条 | OTA 升级、固件烧写、批量操作 |
| 结构体成员大小/偏移 | 协议解析、内存布局分析 |
| 文件读写封装 | 配置存储、日志落盘、数据持久化 |
| core dump 使能 | 崩溃后定位、事后调试 |
这些代码片段我自己是一直在用的,建议收藏起来丢到自己的工具库里,用到的时候直接拿。
你平时还有哪些压箱底的代码片段?欢迎评论区聊聊,说不定下一期就整理进来了。
期待你的三连支持!
194