Ubuntu下读取故障外接SSD时read系统调用无限阻塞的问题排查
我在Ubuntu系统上,通过ICY BOX SATA转USB连接器连接故障的Apacer Panther外接SSD,尝试读取数据并创建镜像用于数据恢复。运行下方的最小可复现代码时,read系统调用会在某个地址处挂起。这个地址在连续运行时基本稳定,但不同日期可能会变化。当块大小设为1时,read会卡在某扇区的第一个字节。
此时程序完全冻结,没有信号能中断read:按下Ctrl+C只会在终端打印“^C”,但无法终止程序,且SIGALRM的信号处理函数从未被调用。关闭终端后在新终端重新运行程序,read会在第一次迭代就挂起,完全无法读取。只有断开并重新连接SSD后,才能再次读取磁盘;但如果在read阻塞时断开驱动器,程序会恢复运行。
如果修改程序以标准输入(stdin)作为文件描述符运行,SIGINT和SIGALRM都能正常中断read。
问题
a) 根据read的man手册,它应该能被信号中断,为什么会出现无限阻塞的情况?
b) 有什么方法可以解决这个问题?
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/select.h> #include <unistd.h> #include <errno.h> #include <signal.h> void sig_handler(int signum){ printf("Alarm handler\n"); } int main(int argc, char *argv[]) { // Register ALARM signal handler to prevent read() from blocking indefinitely struct sigaction alarm_int_handler = {.sa_handler=sig_handler}; sigaction(SIGALRM, &alarm_int_handler, 0); char* disk_name = "/dev/sdb"; const int block_size = 512; int offset = 0; char block[block_size]; // Open disk to read as binary file via file descriptor int fd = open(disk_name, O_RDONLY | O_NONBLOCK); if (fd == -1){ perror(disk_name); exit(0); } int i; int position = offset; for (i=0; i<100000; i++){ // Reset alarm to 1 sec (to interrupt blocked read) alarm(1); // Seek to current position int seek_pos = lseek(fd, position, SEEK_SET); if (seek_pos == -1){ perror("Seek"); } printf("Reading... "); fflush(stdout); int len = read(fd, block, block_size); printf("Read %d chars at %d\n", len, position); if (len == -1){ if (errno != EINTR){ perror("Read"); } else { printf("Read aborted due to interrupt\n"); // TODO: handle it } } position += len; } close(fd); printf("Position %d (%d)\n", position, i * block_size); printf("Done\n"); return 0; }
. . . Reading... Read 1 chars at 29642749 Reading... Read 1 chars at 29642750 Reading... Read 1 chars at 29642751 Reading...
针对问题a)
这种无限阻塞不是read本身的问题,而是底层存储栈的硬件/驱动行为导致的:
- 当故障SSD遇到坏扇区时,ICY BOX的SATA转USB桥接器可能会进入无限重试循环,试图从故障区域读取数据。此时内核块设备驱动会被这个循环卡住,进入不可中断睡眠状态(
TASK_UNINTERRUPTIBLE)——这种状态下,驱动完全忽略任何信号,因为内核为了避免数据不一致,会强制让硬件相关的阻塞操作处于不可中断上下文。 - 用stdin测试时能正常中断,是因为stdin的驱动不会进入这种不可中断阻塞状态,而块设备在处理硬件故障时,部分驱动逻辑会优先保证数据完整性,牺牲可中断性。
针对问题b)
可以通过以下几种方法解决:
1. 禁用设备的硬件错误重试
使用hdparm关闭SSD的硬件级读取重试,让驱动在遇到坏扇区时快速返回错误,而非无限阻塞:
hdparm -r0 /dev/sdb
2. 用pread替代lseek+read
pread能原子性完成定位和读取操作,避免lseek与read之间的竞态,同时在部分驱动场景下更易触发中断处理。修改代码中的读取逻辑:
// 替换原有的lseek和read步骤 printf("Reading... "); fflush(stdout); int len = pread(fd, block, block_size, position); printf("Read %d chars at %d\n", len, position);
3. 非阻塞IO配合poll实现超时控制
既然已经用O_NONBLOCK打开设备,可结合poll监听可读事件并设置超时,完全不依赖信号:
struct pollfd pfd = {.fd = fd, .events = POLLIN}; // 超时1秒 int poll_ret = poll(&pfd, 1, 1000); if (poll_ret == 0) { printf("Read timeout at %d\n", position); // 跳过当前块,继续后续读取 position += block_size; continue; } else if (poll_ret > 0 && (pfd.revents & POLLIN)) { int len = read(fd, block, block_size); // 原有的读取结果处理逻辑 }
4. 使用专业数据恢复工具
专业工具如ddrescue内置了超时处理、坏扇区跳过、智能重试等机制,比自行编写代码更可靠:
ddrescue /dev/sdb sdb_image.img sdb_logfile.log
它会记录已读取区域,跳过无法读取的坏块,后续还能针对坏块区域进行针对性重试。
内容的提问来源于stack exchange,提问作者Alex P

