adbd挂了怎么办?嵌入式Linux本地检测与自动重启
2025-03-31 22:37:41
本地与 adbd 掰手腕:如何检测嵌入式设备上的 adbd 是否正常工作
搞嵌入式 Linux 开发的时候,adbd
是个挺方便的工具,能让我们通过 adb shell
轻松连上设备。但有时候,它也会闹点小脾气。比如,你可能会遇到明明设备跑得好好的,adb shell
却突然告诉你 error: no devices/emulators found
,非得跑到串口控制台手动重启 adbd
才行。这可太折腾人了!
咋办呢?要是能在设备上自己检查 adbd
是不是挂了,然后自动重启它,不就省事多了?这篇博客,咱们就来聊聊怎么在设备本地跟 adbd
"交流",判断它的状态。
先看看现场情况:在一个跑着 kernel-5.10.188 和 busybox 的嵌入式 Linux 系统里,adbd
确实在运行。用 lsof
和 netstat
瞅瞅:
# lsof | grep adbd
201 /usr/bin/adbd ... (省略部分输出) ...
201 /usr/bin/adbd 12 socket:[3943] # Unix domain socket?
201 /usr/bin/adbd 13 socket:[3944] # Unix domain socket?
... (省略部分输出) ...
# netstat -antp | grep 5037 # 过滤 TCP 监听端口
tcp 0 0 127.0.0.1:5037 0.0.0.0:* LISTEN -
# netstat -anp # 看下完整的,包含 unix domain sockets
Active Internet connections (servers and established)
...
tcp 0 0 localhost:5037 0.0.0.0:* LISTEN
...
Active UNIX domain sockets (servers and established)
Proto RefCnt Flags Type State I-Node Path
...
unix 2 [ ACC ] STREAM LISTENING 3344 @jdwp-control # 这个看起来和 adbd 有关
...
unix 3 [ ] STREAM CONNECTED 3944 # adbd 的 lsof 输出里有
unix 3 [ ] STREAM CONNECTED 3346 # adbd 的 lsof 输出里有
unix 3 [ ] STREAM CONNECTED 3943 # adbd 的 lsof 输出里有
...
从 netstat
输出看,adbd
(或者某个相关进程) 确实在 127.0.0.1
的 5037
端口上监听 TCP 连接。
直觉上,既然它在监听 5037 端口,那我们直接用代码连上去,发个命令试试水不就行了?于是,有人尝试了下面的 C 代码,想通过 127.0.0.1:5037
发送 host:version
和 host:devices
命令:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define ADB_HOST "127.0.0.1"
#define ADB_PORT 5037
void send_adb_command(const char *command) {
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket creation failed");
return;
}
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(ADB_PORT),
.sin_addr.s_addr = inet_addr(ADB_HOST)
};
// 设置一个短暂的连接和接收超时,避免卡死
struct timeval timeout = {2, 0}; // 2 秒超时
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout));
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (const char*)&timeout, sizeof(timeout));
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("connect failed");
close(sock);
return;
}
// ADB 协议:先发4字节的命令长度(十六进制字符串),再发命令本身
char header[5];
snprintf(header, sizeof(header), "%04X", (unsigned int)strlen(command));
if (send(sock, header, 4, 0) < 0) {
perror("send header failed");
close(sock);
return;
}
if (send(sock, command, strlen(command), 0) < 0) {
perror("send command failed");
close(sock);
return;
}
char response[1024] = {0}; // 初始化,避免打印垃圾数据
ssize_t bytes_received = recv(sock, response, sizeof(response)-1, 0);
if (bytes_received < 0) {
perror("recv failed");
} else if (bytes_received == 0) {
printf("Connection closed by peer.\n");
} else {
// response[bytes_received] = '\0'; // 确保字符串结尾
printf("Raw Response (length %zd): %s\n", bytes_received, response);
// 这里可以加更复杂的解析,检查是否符合预期
}
close(sock);
}
int main() {
printf("Attempting command: host:version\n");
send_adb_command("host:version");
printf("\nAttempting command: host:devices\n");
send_adb_command("host:devices");
return 0;
}
运行这段代码,结果可能并不如意。往往是连接成功,但收不到预期的响应,甚至直接报错。
为啥 host:
命令不好使?
问题的关键在于 adb
的架构和 adbd
的角色。
通常我们用 adb
的时候,是这样的:
adb
客户端 (PC 上) :你敲adb shell
的那个程序。adb
服务器 (PC 上) :一个后台进程,通常叫adb server
。它负责管理所有连接的设备,监听 TCP 端口 5037 (默认在本机127.0.0.1
)。客户端发出的命令,比如adb devices
,实际上是发给这个服务器的。adbd
守护进程 (设备上) :跑在你的嵌入式设备或手机上的后台进程。它负责执行具体的操作,比如启动一个 shell。adbd
会通过 USB 或者 TCP/IP 跟 PC 上的adb server
连接。
现在我们分析一下 host:
前缀的命令,比如 host:version
、host:devices
、host:transport:serial-number
。这些命令其实是 adb
客户端发给 PC 上的 adb server
的,用来查询服务器信息或请求连接到特定设备。
而设备上的 adbd
呢?它的主要任务是 响应来自 adb server
的针对 本设备 的请求 ,比如执行 shell
命令、传输文件 (push
/pull
) 等。它通常 不 处理 host:
这种管理性质的命令。
用户提到 adbd
是用 ADB_HOST = 0
编译的。这通常意味着 adbd
在编译时被配置为 不 扮演 "host" (即 adb server
) 的角色。当你在设备本地尝试连接 127.0.0.1:5037
并发送 host:
命令时,adbd
很可能直接拒绝或者不理解这种命令,因为它觉得自己只是一个 "device daemon",不是 "host server"。即使它监听了 5037 端口(通常是为了支持 TCP/IP 模式的 adb 连接),它的内部逻辑也决定了它只处理设备相关的指令流。
所以,想用 host:version
这类命令来探测本地 adbd
的状态,路子可能就走歪了。
那该咋探测本地 adbd
?
既然 host:
命令不行,我们需要找到 adbd
能听懂 的命令。adbd
真正关心的,是那些没有 host:
前缀的、具体到设备操作的命令。那么,哪些方法能用来判断本地 adbd
还活着并且能响应呢?
方法一:检查进程是否存在
最简单粗暴的办法:看看 adbd
进程还在不在。
# 使用 pgrep (如果 busybox 支持)
pgrep adbd
# 或者使用 ps 配合 grep
ps w | grep -v grep | grep adbd
- 原理: 直接检查操作系统进程列表。
- 优点: 非常简单、快速。
- 缺点: 只能确认进程存在,不能保证它没卡死或者功能正常。有时候进程活着,但已经无法响应连接了。
方法二:检查端口是否在监听
既然我们看到 adbd
在 127.0.0.1:5037
监听,那就检查这个端口是不是真的还在监听状态。
# 使用 netstat (比较通用)
netstat -ltnp | grep ':5037' | grep 'LISTEN'
# 或者使用 ss (更新一些的系统可能更推荐)
ss -ltnp | grep ':5037' | grep 'LISTEN'
- 原理: 查看系统的网络连接状态表,确认是否有进程在指定地址和端口上等待连接。
- 优点: 能确认网络服务的基础设施还在。
- 缺点: 同方法一,监听不代表服务一定健康。可能
adbd
监听着端口,但内部处理逻辑已经卡住了。
方法三:模拟客户端,发送有效服务请求
这是最靠谱的方法,因为它直接模拟了正常客户端(比如 adb shell
命令背后发生的事)与 adbd
的交互过程。我们需要连接到 adbd
监听的端口(这里是 127.0.0.1:5037
),并发送一个 adbd
能理解的、代表具体设备操作的命令。
啥命令能用呢?
adbd
支持多种服务 (service)。比如,启动一个 shell 会用到 shell:
服务。我们可以尝试发送一个非常简单的 shell 命令,比如 echo
一个特定字符串。
一个完整的 ADB 交互流程(简化版)是:
- 客户端连接到
adbd
。 - 客户端发送 "transport" 请求(如果需要指定设备,但我们是本地直连,或许可以省略,或者用一个通用方式指定本地)。更正: 当直接连接
adbd
(特别是 TCP 模式),通常是先建立连接,然后直接发送服务请求。 - 客户端发送服务请求,格式为
NNNN<service-string>
。NNNN
是服务请求字符串长度的4位十六进制表示。例如,请求执行echo test
,服务字符串可能是shell:echo test
。 adbd
响应OKAY
或FAIL
,后面跟上数据(比如 shell 的输出)或错误信息。
如何实施?
方案 A: 使用修改后的 C 代码
我们可以修改之前的 C 代码,把 host:version
换成一个有效的服务请求,比如 shell:echo adb_ping
。
这个命令字符串 "shell:echo adb_ping"
的长度是 19。转换成 4 位十六进制是 0013
。 (注意: 实际实现中,最好动态计算长度)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h> // 引入 errno
#define ADB_HOST "127.0.0.1"
#define ADB_PORT 5037
#define TIMEOUT_SECONDS 2
// 返回值:0 表示成功(收到 OKAY),-1 表示失败
int check_adbd_responsive() {
int sock = -1;
char response_prefix[5] = {0}; // 用于接收 OKAY 或 FAIL
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket creation failed");
return -1;
}
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(ADB_PORT),
.sin_addr.s_addr = inet_addr(ADB_HOST)
};
struct timeval timeout = {TIMEOUT_SECONDS, 0};
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout));
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (const char*)&timeout, sizeof(timeout));
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
// 这里要区分是连接被拒绝(端口没监听)还是超时
// 如果 connect 返回 ECONNREFUSED,说明端口没监听或adbd挂了
// 如果返回 ETIMEDOUT,可能是网络问题或 adbd 卡死无响应
perror("connect failed");
close(sock);
return -1;
}
const char *command = "shell:echo adb_ping"; // 一个简单的 shell 命令
int cmd_len = strlen(command);
char header[5];
snprintf(header, sizeof(header), "%04X", cmd_len);
printf("Sending header: %s\n", header);
if (send(sock, header, 4, 0) != 4) {
perror("send header failed");
close(sock);
return -1;
}
printf("Sending command: %s\n", command);
if (send(sock, command, cmd_len, 0) != cmd_len) {
perror("send command failed");
close(sock);
return -1;
}
// adbd 应该先回复一个 4 字节的 "OKAY" 或 "FAIL"
ssize_t bytes_received = recv(sock, response_prefix, 4, 0);
if (bytes_received < 0) {
perror("recv response status failed");
close(sock);
return -1;
} else if (bytes_received == 0) {
printf("Connection closed by peer unexpectedly after sending command.\n");
close(sock);
return -1;
} else if (bytes_received == 4) {
response_prefix[4] = '\0'; // 确保 null 结尾
printf("Received status: %s\n", response_prefix);
if (strcmp(response_prefix, "OKAY") == 0) {
printf("adbd responded OKAY. Seems healthy.\n");
// 这里可以继续接收后续数据(echo 的输出),或者直接认为 OKAY 就行
// 为了简单起见,我们收到 OKAY 就认为成功
close(sock);
return 0; // 成功
} else {
printf("adbd responded with non-OKAY status: %s\n", response_prefix);
// 收到 FAIL 或其他东西,也算 adbd 有反应,但可能服务不可用?
// 根据需求,这里可以返回 0 (有反应) 或 -1 (非预期反应)
// 我们严格点,非 OKAY 算失败
close(sock);
return -1; // 失败
}
} else {
printf("Received unexpected number of bytes for status: %zd\n", bytes_received);
close(sock);
return -1; // 失败
}
// 代码实际上到不了这里
close(sock);
return -1;
}
int main() {
printf("Checking local adbd on %s:%d...\n", ADB_HOST, ADB_PORT);
if (check_adbd_responsive() == 0) {
printf("Result: adbd seems responsive.\n");
} else {
printf("Result: adbd might be down or unresponsive.\n");
}
return 0;
}
- 原理: 遵循 ADB 协议,连接到
adbd
并发送一个shell:
服务请求。adbd
如果正常工作,应该会响应一个OKAY
。 - 优点: 直接测试了
adbd
的核心服务处理能力,比只检查进程或端口更准确。 - 缺点: 需要编写和编译代码,稍微复杂点。需要处理好网络编程的各种细节(超时、错误处理)。
- 安全建议: 确保 C 代码健壮,正确处理各种错误情况和边界条件,避免自身成为问题源头。
方案 B: 使用 netcat
(nc) 工具
如果你的 busybox 包含了 nc
(netcat) 工具,或者你可以安装一个,那事情就简单多了,可以用脚本来模拟这个过程。
# 命令: shell:echo adb_ping (长度 19 => 0x13)
COMMAND="shell:echo adb_ping"
CMD_LEN=$(printf "%04X" $(echo -n "$COMMAND" | wc -c))
REQUEST="${CMD_LEN}${COMMAND}"
# 发送请求并等待响应(设置超时 -w 2 秒)
# 注意:不同版本的 nc 参数可能略有不同, -w 或 -W
RESPONSE=$(echo -n "$REQUEST" | nc -w 2 127.0.0.1 5037)
# 检查响应是否以 "OKAY" 开头
# adbd 对 shell:echo 会先回 OKAY,然后马上跟一个长度头+数据
# 所以简单的响应可能看起来像 "OKAY000aadb_ping\n" (长度是adb_ping\n = 9+1 = 10 => 0x000a)
# 我们只关心开头的 OKAY
if [[ "$RESPONSE" == OKAY* ]]; then
echo "adbd seems responsive (via nc)."
exit 0
else
echo "adbd did not respond OKAY (via nc). Response: [$RESPONSE]"
# 这里可能需要更仔细地分析 $RESPONSE
# 比如完全没响应,或者响应了 FAIL 等
exit 1
fi
- 原理: 和 C 代码类似,但用现成的命令行工具发送数据和接收响应。
- 优点: 无需编译,方便集成到 shell 脚本中。
- 缺点: 依赖
nc
工具。对响应的解析可能不如 C 代码那么灵活。nc
的超时行为和错误处理可能需要适配具体版本。 - 安全建议: 无特殊安全建议,但脚本本身的健壮性(如错误检查)仍然重要。
方法四:尝试使用本地 adb
客户端连接本地 adbd
(如果可行)
有时候,设备上可能也包含了 adb
客户端工具。可以试试强制它连接本地 127.0.0.1:5037
。
# 尝试通过指定 Host(-H) 连接本地 adbd
adb -H 127.0.0.1 shell echo adb_ping
- 原理: 让设备上的
adb
客户端直接与同一设备上的adbd
通信(如果adbd
允许这种连接并且监听了 TCP 端口)。 - 优点: 使用标准工具,简单直接。
- 缺点:
- 不一定所有嵌入式系统都自带
adb
客户端。 - 如前所述,
adbd
(特别是ADB_HOST=0
编译的) 可能不接受来自本地 TCP 端口的连接,或者即使连接上了也不响应被它识别为 "host" 的某些命令流。需要实际测试验证这种方法在你的特定系统上是否有效。 它很可能会失败。
- 不一定所有嵌入式系统都自带
方法五:检查 adbd
相关日志
如果 adbd
出了问题,它可能会在系统日志里留下线索。
# 检查 dmesg 内核日志
dmesg | grep -i adb
# 检查系统日志文件 (具体路径取决于你的系统配置, 可能是 /var/log/messages 等)
# logread # 如果使用 openwrt 或类似系统
cat /var/log/messages | grep -i adb # 示例路径
# 如果是 Android 系统 (虽然这里是通用 Linux, 但提一下)
logcat -d | grep -i adb
- 原理: 通过分析日志判断
adbd
是否崩溃、报错或有异常行为。 - 优点: 能提供更详细的故障信息。
- 缺点: 需要知道日志在哪里,
adbd
不一定总会留下清晰的日志,且日志检查是被动的。
整合:写一个简单的监控脚本
结合上面几种方法,我们可以写一个 shell 脚本来监控 adbd
状态,并在发现问题时尝试重启它。优先使用最可靠的方法(比如方法三)。
#!/bin/sh
ADB_HOST="127.0.0.1"
ADB_PORT="5037"
ADBD_COMMAND="/usr/bin/adbd" # adbd 实际路径
RESTART_DELAY=5 # 重启后的等待时间 (秒)
MAX_RETRY=3 # 最多尝试重启次数
RETRY_COUNT=0
check_adbd() {
# 优先使用 netcat 方式检查响应性
COMMAND="shell:echo adb_ping_$(date +%s)" # 加时间戳避免缓存?(可能不需要)
CMD_LEN=$(printf "%04X" $(echo -n "$COMMAND" | wc -c))
REQUEST="${CMD_LEN}${COMMAND}"
# 使用 timeout 命令控制 nc 的整体运行时间,更保险
RESPONSE=$(echo -n "$REQUEST" | timeout 3 nc -w 2 $ADB_HOST $ADB_PORT)
NC_EXIT_CODE=$?
if [ $NC_EXIT_CODE -eq 0 ] && [[ "$RESPONSE" == OKAY* ]]; then
echo "$(date): OK - adbd responded OKAY via nc."
return 0 # 健康
elif [ $NC_EXIT_CODE -eq 124 ]; then # timeout 命令超时
echo "$(date): FAIL - nc command timed out."
return 1 # 不健康
else
echo "$(date): FAIL - nc exited with code $NC_EXIT_CODE or response was not OKAY. Response: [$RESPONSE]"
# 进一步检查进程是否存在,区分是完全没跑还是卡死了
if ! pgrep adbd > /dev/null; then
echo "$(date): FAIL - adbd process not found."
else
echo "$(date): FAIL - adbd process exists but unresponsive/failed check."
fi
return 1 # 不健康
fi
}
restart_adbd() {
echo "$(date): Attempting to restart adbd..."
# 先尝试杀掉旧进程,避免多个实例
killall adbd > /dev/null 2>&1
sleep 1
# 如果 killall 不行,可以尝试 pkill 或 kill $(pgrep adbd)
pkill -9 adbd > /dev/null 2>&1 # 强制杀死
sleep 1
# 启动 adbd (根据你的系统启动方式修改)
echo "$(date): Starting adbd process: $ADBD_COMMAND"
$ADBD_COMMAND & # 在后台启动
sleep $RESTART_DELAY # 等待 adbd 初始化
}
# 主循环
while true; do
if ! check_adbd; then
RETRY_COUNT=$((RETRY_COUNT + 1))
if [ $RETRY_COUNT -le $MAX_RETRY ]; then
echo "$(date): adbd health check failed ($RETRY_COUNT/$MAX_RETRY). Restarting..."
restart_adbd
else
echo "$(date): adbd failed health check after $MAX_RETRY retries. Giving up for a while."
# 这里可以加入更复杂的逻辑,比如发送报警、等待更长时间再试等
RETRY_COUNT=0 # 重置计数器,过段时间再重试
sleep 300 # 等待 5 分钟
fi
else
# 如果健康,重置重试计数器
RETRY_COUNT=0
fi
# 每隔一段时间检查一次,例如 60 秒
sleep 60
done
- 说明:
- 这个脚本会定期(默认 60 秒)调用
check_adbd
函数。 check_adbd
使用nc
方法发送shell:echo
命令来探测。增加了timeout
命令防止nc
卡死。- 如果检查失败,它会调用
restart_adbd
来杀掉旧的adbd
进程并重新启动。 - 加入了简单的重试逻辑,避免因为短暂的网络波动或其他瞬时问题导致频繁重启。
- 你需要根据自己系统上
adbd
的实际路径和启动方式修改脚本。 - 需要确保
nc
和timeout
(通常在coreutils
包里) 可用。
- 这个脚本会定期(默认 60 秒)调用
- 进阶使用技巧:
- 可以将日志输出到文件而不是控制台。
- 集成到系统的服务管理框架(如 systemd, sysvinit)中,使其更稳定可靠。
- 根据
nc
返回的具体错误码和响应内容做更细致的判断。 - 如果 TCP 端口监听在
0.0.0.0:5037
而非127.0.0.1:5037
,务必考虑安全影响,可能的话配置防火墙规则限制访问。如果只在本地使用,监听127.0.0.1
更安全。
总结一下
要从设备本地检测 adbd
是否正常工作,直接照搬 PC 上 adb
客户端发给 adb server
的 host:
命令通常行不通。更靠谱的方法是模拟一个合法的客户端请求,比如通过 socket 连接到 adbd
监听的端口(可能是 TCP 5037 或某个 Unix domain socket),发送一个 adbd
能理解的服务命令(如 shell:echo test
),并检查是否收到预期的 OKAY
响应。你可以用 C 代码实现精确控制,或者利用 nc
等工具快速集成到监控脚本里。别忘了结合进程检查和端口监听检查,多管齐下更放心。有了这样的本地监控和自动恢复机制,下次再遇到 adb shell
连接不上的问题,说不定它自己就悄悄解决了。