You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 05:54:53