ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

嵌入式Linux开发必修:高频命令与main函数传参实战

嵌入式Linux开发必修:高频命令与main函数传参实战 做了这么多年嵌入式 Linux 开发被问得最多的一个问题不是怎么写驱动也不是怎么调设备树而是终端里那堆命令到底哪些要背以及main 函数里的 argc、argv 到底是什么鬼。这两个问题看着基础实际上决定了你能不能顺利把程序跑在板子上、能不能在出问题时快速定位故障。嵌入式 Linux 项目里高频命令和主函数传参这两项技能就是每天的饭。这篇是嵌入式 Linux 实战系列的第二篇我把这两块结合自己在 i.MX、RK、全志等平台上的实际项目体验完整拆一遍。适合这么几类人看刚结束了单片机裸机开发、准备转向嵌入式 Linux 的软件工程师已经会写 Linux 程序但总觉得命令不熟、传参用得别扭的开发者还有准备嵌入式 Linux 面试、想快速把基本功补齐的朋友。文章不追求命令大全只讲真正高频的那一批以及传参从原理到实战的全过程。1. 命令和传参为什么值得专门写一篇1.1 没有命令的嵌入式开发寸步难行很多人从单片机跳过来时有个错觉以为嵌入式 Linux 开发就是写代码、烧固件和终端没什么关系。真上手板子就发现串口一接U-Boot 的命令行先把你拦住了内核启动起来shell 又把你拦住了。你想看驱动有没有加载成功随手就是dmesg | grep xxx你想确认网络通不通ping是逃不掉的你想把编译好的程序从 PC 弄到板子上scp、tftp、nfs总得用上。我实际带过的一个新人调一块网口插上网线后板子 ping 不通 PC他第一反应是改代码重新编译。实际上只需要ifconfig看一眼 IPping探测一下再用ethtool看链路状态五分钟就能判断问题在硬件还是软件。命令熟练度直接决定了排障速度。尤其嵌入式环境里很多设备没有图形界面、没有 SDK 调试器终端就是你唯一能看见系统内部状态的眼睛。1.2 传参是程序从死配置走向活配置的一步再说主函数传参。很多写了好几年 C 的工程师从来没用过argc和argv因为在单片机上写 C入口函数根本没有参数这回事程序的行为全在编译时定死。要改一个波特率、改一个 GPIO 编号就得改宏定义、重新编译、重新烧录。一旦上了 Linux这种思路会非常吃亏。同样是做一个串口测试工具写死参数的版本每次换波特率都得重新编译用main(int argc, char *argv[])接收./uart_test -d /dev/ttymxc0 -b 115200这样传进来的参数一份二进制就能适配不同的设备节点和波特率。这就是嵌入式 Linux 里的运行时配置思想程序不再是一块烧死的固件而是可以被脚本、被命令行、被服务管理器任意编排的工具。说它是嵌入式开发从裸机思维转向Linux 思维的分水岭一点都不夸张。2. 我每天都会用到的高频命令清单命令这种东西靠背列表是没用的按使用场景记最快。我按四个场景整理文件系统与设备节点、网络调试与文件传输、进程内存与内核日志、文本过滤与管道组合。2.1 文件系统与设备节点操作命令ls、cd、cp、mv、rm、mkdir这些基础命令就不啰嗦了重点说两个嵌入式场景里容易被忽略的ls -l和file。ls -l不只是看权限在 /dev 目录下你能看到设备节点的主次设备号比如ls -l /dev/ttymxc0会显示crw-rw---- 1 root root 207, 16 Jan 1 00:00 ttymxc0。c表示字符设备后面的207, 16是主次设备号。驱动开发时设备节点有没有创建、权限对不对全靠这行信息判断。file命令更值得多用。交叉编译环境下一个可执行文件到底是 ARM 架构还是 x86 架构肉眼看不出来file一看便知。比如输出ELF 32-bit LSB executable, ARM, EABI5就说明编译目标没错如果输出的是x86-64那你拷到板子上肯定会报Exec format error。2.2 网络调试与文件传输命令嵌入式调试绕不开网络。ifconfig或ip addr看板子的 IP这个每个人都会。关键在排查链路板子 ping 不通 PC先ifconfig确认板子 IP 和 PC 在同一网段再ping网关地址确认链路通不通然后在 PC 上抓包确认有没有收到 ARP 请求。一层层剥比瞎改代码强多了。文件传输是刚需。小型根文件系统里经常没装rsync最常用的就是scpscp ./app root192.168.1.100:/tmp/。也可以搭个 TFTP 服务器在 U-Boot 阶段加载内核不过日常调试中scp更直接。还有一类特殊传输是挂载 NFS后面单独讲它不只是传文件而是把 PC 上的目录直接变成板子的文件系统一部分改完代码重新编译板子上立刻看到新版本调试效率非常高。# 查看网络状态 ip addr show # 测试连通性加 -c 限制数量避免一直ping ping -c 4 192.168.1.100 # 从PC拷贝程序到板子 scp ./gpio_tool root192.168.1.100:/tmp/2.3 进程、内存与内核日志命令程序跑起来不代表跑得对。ps看进程是否存在top看 CPU 占用free看内存余量df -h看存储占用这几个是分析程序异常的基本工具。嵌入式板子上内存通常只有几十到几百 MBfree的输出里 available 大幅度下降多半就是程序内存泄漏或者调试模块没关。dmesg是我用得最多的命令之一。驱动加载失败、NFS 挂载失败、中断申请失败内核都会往dmesg里写日志。配合dmesg | grep -i error过滤问题往往一眼就能看到。cat /proc/interrupts可以看某个中断触发了多少次调试按键和 GPIO 中断时很管用cat /proc/cpuinfo看处理器型号确认内核跑在预期的 SoC 上。2.4 文本过滤与管道组合技巧单个命令能力有限组合起来才强大。最典型的是ps | grep app把人从大量进程输出里捞出来。tail -f /var/log/messages动态跟踪日志程序输出随时能看到。还有一段我很常用的cat /proc/meminfo | grep MemAvailable ifconfig eth0 | grep inet addr | awk {print $2} | cut -d: -f2管道和重定向不是花架子。程序不打印日志但你需要看输出时./app /tmp/log 21 一套组合下来后台运行、日志落盘全有了。嵌入式系统里没有 IDE 调试器文件重定向就是你的日志系统雏形。命令典型用途嵌入式场景备注dmesg查看内核日志驱动加载、挂载错误、U盘识别file查看文件类型确认交叉编译产物架构scp传输文件比U盘拷来拷去效率高mount -t nfs网络挂载改完源码直接同步到板子cat /proc/interrupts查看中断次数按键、GPIO中断调试3. 嵌入式环境里命令的几个反直觉差异3.1 BusyBox 精简命令别拿 PC 的习惯硬套嵌入式 Linux 的 rootfs 里绝大多数命令来自 BusyBox。BusyBox 是一个命令的精简集合把几十个常用命令打包成一个可执行文件函数上按够用标准裁剪。这就导致同一个命令在 PC 和板子上的选项差异非常大。比如 PC 上的find支持-printf自定义输出格式BusyBox 的find基本不支持PC 上的grep支持-P使用 Perl 正则BusyBox 的 grep 是用基础正则实现的top的参数和输出列也不一样BusyBox 默认输出精简很多。我的习惯是在板子上用某个命令的复杂选项之前先敲命令 --help看看当前版本支持什么。这不是不信任记忆是 BusyBox 版本差异真的能坑人。还有一点板子的/bin/sh通常不是 bash而是 ashBusyBox 里内置的 shell。bash 里常用的[[ ]]条件测试、${var,,}大小写转换、let算术命令在 ash 里可能都不支持。写板子上的启动脚本要默认自己用的是 POSIX 语法而不是把 PC 上的 bash 脚本直接拷贝过去。3.2 NFS 挂载与根文件系统调试的正确打开方式NFS 是嵌入式开发里不可绕过的工具。最常见的两种用法一是在 U-Boot 内核启动参数里直接指定root/dev/nfs nfsroot服务器IP:/路径,vers3让内核启动时通过网络把根文件系统挂载起来二是板子跑起来之后手动把 PC 上的目录挂到某个挂载点把编译好的可执行文件、动态库临时同步过去。手动挂载的典型命令mount -t nfs -o nolock,vers3,timeo50,retrans2 192.168.1.100:/home/nfs /mnt/nfs其中nolock是为了绕过老式的锁管理协议很多嵌入式环境不支持,加上它反而少很多麻烦vers3指定 NFS 版本老内核或老服务器默认可能用 NFSv4两边版本对不上就会挂载失败timeo和retrans是超时和重传参数网络环境差时适当调大能减少卡死现象。挂载失败一般就几类内核没开 NFS 客户端支持报wrong fs type, bad option服务器没导出目录或权限不对报Permission denied防火墙挡了端口报Connection refused。排查时先dmesg | tail看内核日志基本能给出方向。3.3 板子脚本的 shell 语法和权限陷阱嵌入式文件系统大多是只读的或者只有个别分区可写。往 /etc 下写配置、往 /usr/bin 下放程序重启后可能就没了。我一般把调试用的程序放到/tmp或挂载的 NFS 目录/tmp通常是 tmpfs重启清空但至少权限宽松。脚本权限也是常见坑。PC 上拷贝脚本到板子经常忘了chmod x执行时报Permission denied。U-Boot 阶段还有个隐藏坑很多脚本用\r\n换行拷到 Linux 里\r会被当成命令的一部分报错信息十分诡异。遇到这种情况用sed -i s/\r$// 脚本名处理一下就行。4. main 函数传参程序入口的第一道配置4.1 argc 和 argv 在系统层面是怎么来的先回到 C 语言本身。当我们写int main(int argc, char *argv[])时这两个参数不是编译器凭空造出来的而是 shell 配合内核给程序的。在终端输入./myapp -d /dev/ttymxc0 -b 115200shell 做的第一件事是把这行内容按空白字符拆成若干单词./myapp、-d、/dev/ttymxc0、-b、115200。然后调用execve系统调用把这些单词放进一个字符串数组里传给内核。C 程序启动后运行时环境crt0 等把数组整理好交给main。于是argc等于 5argv[0]是./myapp后面依次是各个参数。argv[argc]还有一个约定好的 NULL 结尾方便遍历。这个概念对嵌入式开发有什么用有用。你在 U-Boot 里设置bootargs里面有一长串参数内核启动后用cat /proc/cmdline能看到。这些参数最终也会被解析成一个个字符串传给内核各子系统的初始化函数。理解了argc/argv是怎么来的你就理解了 Linux 世界里命令行参数的通用玩法。#include stdio.h int main(int argc, char *argv[]) { printf(argc %d\n, argc); for (int i 0; i argc; i) { printf(argv[%d] %s\n, i, argv[i]); } return 0; }编译后在 PC 或板子上运行一次立刻能验证刚才说的内容。我建议每个入门嵌入式 Linux 的人都实际跑一遍这个小程序你会突然明白终端、shell、程序入口这三者之间的衔接关系。4.2 嵌入式场景里传参到底解决了什么实际问题传参最大的价值是让同一个二进制在不同场景下复用。举例产线上同一块板子不同型号需要测试不同 GPIO、不同串口波特率。如果没有传参每个型号就得维护一个固件版本有了传参测试工具做成一个工厂工人只需敲不同的命令行参数即可。另一个更常见的场景是程序启动时指定配置文件路径。很多服务程序支持-c /etc/app/config.ini源码里就把配置文件路径留给运行时决定。这样一套代码既能用在系统默认目录也能在调试时指向/tmp下的临时配置不用改代码。嵌入式 Linux 里还有一类和单片机的本质区别程序往往不是人手手动启动的而是开机脚本、systemd 服务、其他程序通过exec等方式拉起的。这些启动器都需要把参数拼接成命令行字符串所以参数接口设计得越规范后期被这些机制调度就越顺畅。4.3 参数解析的两种思路拿到argv之后怎么解析最简单的路子是顺序比较if (argc 1 strcmp(argv[1], -d) 0) { device argv[2]; }这种写法不是不行但一旦参数多起来、顺序不固定、有些参数可选代码就会变成一堆if-else嵌套还要自己处理参数缺失值不够的情况。我在项目中见到的土办法解析十有八九对用户不友好参数一多就把自己绕晕了。这也是为什么接下来要专门介绍getopt_long。5. 参数解析别用土办法getopt_long 从入门到避坑5.1 getopt_long 的标准用法与代码骨架Linux 下的 C 库提供了getopt和getopt_long两个函数专门用来解析命令行参数。getopt_long支持短选项-d和长选项--device这是 GNU 工具链里最常见的交互风格也符合大多数工程人员的预期。#include getopt.h static struct option long_options[] { {device, required_argument, 0, d}, {baud, required_argument, 0, b}, {help, no_argument, 0, h}, {0, 0, 0, 0} }; int main(int argc, char *argv[]) { const char *device NULL; int baud 0; int opt; while ((opt getopt_long(argc, argv, d:b:h, long_options, NULL)) ! -1) { switch (opt) { case d: device optarg; break; case b: baud atoi(optarg); break; case h: default: fprintf(stderr, 用法: %s -d 设备节点 -b 波特率\n, argv[0]); return -1; } } return 0; }这里optarg是全局变量指向当前选项后面的参数字符串。d:b:h里的冒号表示-d需要一个参数。循环直到getopt_long返回 -1说明所有选项都处理完了。剩余的普通参数比如文件名从optind开始取。5.2 数字参数转换的隐蔽坑atoi 还是 strtol参数从命令行进来全是字符串数字要转换。最省事的是atoi但它有三个致命问题不检查是否包含非法字符、不检查是否越界、出错时返回 0 且无法区分错误。举例用户敲了-b 115200abcatoi返回 115200程序照样用这个波特率去跑看起来没毛病但其实是非法输入被悄悄忽略了。再比如-b abcatoi返回 0波特率变成 0程序可能直接崩溃或挂死报错信息却没有指向参数问题。正确的做法是用strtol并检查结束指针char *endptr; long val strtol(optarg, endptr, 10); if (*endptr ! \0 || val 0) { fprintf(stderr, 无效参数: %s\n, optarg); return -1; } baud (int)val;strtol会把endptr停在第一个无法解析的字符处。如果*endptr不是\0说明后面还有垃圾内容直接报错。解析 GPIO 号、波特率、IP 端口这类关键参数时严格校验能避免很多线上事故。5.3 传参在启动脚本和 systemd 服务中的配合参数解析做完了还要注意调用方。板子开机时很多服务是通过/etc/init.d下的脚本启动的。脚本里拼接参数最常见的错误是变量没加引号BAUD115200 /usr/bin/myapp -d /dev/ttymxc0 -b $BAUD这里的$BAUD在引号外当 BAUD 为空时命令行会变成/usr/bin/myapp -d /dev/ttymxc0 -b-b缺少参数getopt_long直接返回 ?。正确写法是/usr/bin/myapp -d /dev/ttymxc0 -b $BAUDsystemd 服务单元里注意ExecStart不会再做 shell 分词参数按空格拆分每段引号不会被解析成 shell 语法。所以带空格的值要特别处理不过嵌入式场景下参数值一般不含空格问题不大。6. 实战项目一个支持命令行传参的 GPIO 调试工具6.1 工具需求与参数接口设计理论知识讲完落到一个完整的例子上。我做一块板子时经常要手动拉某个 GPIO 的电平来验证外设比如触发继电器、指示灯、读按键电平。如果每次都要echo 68 /sys/class/gpio/export再写 direction、再写 value太繁琐了。于是写一个gpio_tool通过命令行传参快速控制。接口设计如下./gpio_tool -g 68 -v 1 # 把gpio68设置为高电平 ./gpio_tool -g 68 -r # 读取gpio68当前电平 ./gpio_tool -g 68 -v 0 -e # 自动export后设置为低电平 ./gpio_tool -g 68 -u # 反导出gpio68-g指定 GPIO 编号-v指定输出电平-r表示读取模式-e自动导出-u反导出。所有参数都能灵活组合这正体现了传参的价值同一个工具不同参数完成不同任务。6.2 完整代码与关键函数拆解基于 sysfs 接口完整代码如下#include stdio.h #include stdlib.h #include string.h #include fcntl.h #include unistd.h #include getopt.h static void usage(const char *prog) { fprintf(stderr, 用法: %s -g gpio编号 [-v 0|1] [-r] [-e] [-u]\n, prog); } static int write_file(const char *path, const char *buf) { int fd open(path, O_WRONLY); if (fd 0) { perror(path); return -1; } if (write(fd, buf, strlen(buf)) 0) { perror(write); close(fd); return -1; } close(fd); return 0; } static int read_file(const char *path, char *buf, int size) { int fd open(path, O_RDONLY); if (fd 0) { perror(path); return -1; } int n read(fd, buf, size - 1); close(fd); if (n 0) { perror(read); return -1; } buf[n] \0; return n; } int main(int argc, char *argv[]) { int gpio -1; int value -1; int read_mode 0; int do_export 0; int do_unexport 0; int opt; while ((opt getopt_long(argc, argv, g:v:reu, NULL, NULL)) ! -1) { switch (opt) { case g: gpio atoi(optarg); break; case v: value atoi(optarg); break; case r: read_mode 1; break; case e: do_export 1; break; case u: do_unexport 1; break; default: usage(argv[0]); return -1; } } if (gpio 0) { usage(argv[0]); return -1; } char gpio_dir[64]; snprintf(gpio_dir, sizeof(gpio_dir), /sys/class/gpio/gpio%d, gpio); if (do_export) { char buf[16]; snprintf(buf, sizeof(buf), %d, gpio); if (write_file(/sys/class/gpio/export, buf) 0) return -1; usleep(100000); } if (read_mode) { char path[80]; char buf[16]; snprintf(path, sizeof(path), %s/value, gpio_dir); if (read_file(path, buf, sizeof(buf)) 0) return -1; printf(gpio%d %s, gpio, buf); } else { if (value 0 || value 1) { usage(argv[0]); return -1; } char path[80]; char buf[16]; snprintf(path, sizeof(path), %s/direction, gpio_dir); write_file(path, out); snprintf(path, sizeof(path), %s/value, gpio_dir); snprintf(buf, sizeof(buf), %d, value); if (write_file(path, buf) 0) return -1; printf(gpio%d - %d\n, gpio, value); } if (do_unexport) { char buf[16]; snprintf(buf, sizeof(buf), %d, gpio); write_file(/sys/class/gpio/unexport, buf); } return 0; }write_file和read_file是对 sysfs 虚拟文件的通用读写封装。usleep(100000)是给内核一点时间在 export 后生成 gpio68 目录不加的话紧接着操作可能报No such file or directory。-g解析同样可以用strtol加强校验这里为了代码清晰先用atoi你在实际项目中记得换成严格校验。6.3 交叉编译、拷贝到板子并逐项验证编译命令取决于你的交叉工具链比如 ARM 平台arm-linux-gnueabihf-gcc gpio_tool.c -o gpio_tool file gpio_tool scp gpio_tool root192.168.1.100:/tmp/在板子上验证/tmp/gpio_tool -g 68 -e -v 1 cat /sys/class/gpio/gpio68/value输出gpio68 - 1 1再试读取模式如果外设把引脚拉低输出就是 0如果引脚悬空或上拉可能是 1。这个工具体验了传参的完整链路命令行解析、选项参数转换、调用 sysfs 接口。更重要的是你以后换一块板子GPIO 编号不同直接在命令行改-g参数就行完全不用重新编译。7. 我实际踩过的三个坑完整排查链路与经验汇总7.1 开机脚本传参被吃掉引号与空变量现象板子上电后程序应该自动启动并接收脚本里的参数但实际跑起来后程序报参数错误。我在启动脚本里写的是RATE /usr/bin/myapp -d /dev/ttymxc0 -b $RATE排查链路先手动在终端执行脚本发现同样的报错。然后在脚本里加一行echo $RATE输出是空的再打印完整命令行echo /usr/bin/myapp -d /dev/ttymxc0 -b $RATE结果发现-b后面直接没了。原因就是$RATE为空时shell 把这个空参数折叠掉了-b后面没有参数getopt_long解析失败。修法很简单变量加引号并给默认值RATE${RATE:-115200} /usr/bin/myapp -d /dev/ttymxc0 -b $RATE这个坑提醒我脚本里给程序传参凡是变量必须加引号否则空变量会导致参数数量变化。不能假设变量一定非空。7.2 NFS v3 挂载失败从内核配置到服务器权限现象板子启动后手动挂载 NFS 目录报mount: wrong fs type, bad option, bad superblock。这个报错很有迷惑性好像挂载命令写错了实际往往不是命令本身的问题。排查链路先dmesg | tail内核日志明确提示 NFS 客户端不受支持。这时候回到内核配置检查CONFIG_NFS_FS、CONFIG_NFS_V3、CONFIG_ROOT_NFS是否开启。我遇到的板子因为是裁剪过的内核把 NFS 客户端裁掉了所以怎么敲 mount 都不行重新配置内核开启 NFS 相关选项后解决。还有一次报Permission denied检查服务端/etc/exports发现导出目录后面少了no_root_squash选项导致 NFS 客户端用 root 访问时被压缩成 nobody权限不足。改成/opt/rootfs *(rw,sync,no_root_squash,no_subtree_check)后正常。这类问题三件套要一一验证内核选项、服务器导出配置、挂载命令选项。7.3 BusyBox 的 ps/grep 组合失效命令选项的差异现象写了一个监控脚本在 PC 上运行正常拷到板子上执行后一直查不到目标进程即使进程明明在跑。排查链路单独运行ps发现 BusyBox 的ps默认只列出了当前终端相关的进程不是所有系统进程。再运行ps -ef才看到完整的进程列表和管理命令行。进一步发现ps -ef | grep myapp里 grep 也会匹配自己的进程得再加grep -v grep或者直接用pidof myapp。pidof myapp ps -ef | grep myapp | grep -v grep把脚本改成用pidof后问题消失。这个经历非常典型同样的命令BusyBox 实现和 GNU 实现差异巨大脚本里不能用 PC 上的经验直接套用。正确做法是写任何板子脚本前先用命令 --help确认当前支持的选项再动手。我个人在两块板子上都栽过这种跟头后来养成了一个习惯凡是往板子部署的脚本先在板子上用sh -n做语法检查再用sh -x跟踪执行把每条命令展开看一遍。这两步能避免 90% 的脚本问题。高频命令和主函数传参看起来是入门内容但项目里大量的交付事故、调试返工最后都追溯到这两个基本功上。先把地基打牢再往上堆根文件系统定制、设备树、驱动开发这些上层能力你会觉得顺畅很多。
返回列表