usb挂起与文件描述符

问题分析

当 USB 设备挂起时,已经打开的文件描述符通常不会自动关闭,这是因为:

  1. 文件描述符是内核资源:即使底层硬件不可用,文件描述符仍存在于进程中

  2. 引用计数机制:只要进程持有引用,描述符就不会自动释放

  3. USB 挂起 ≠ 设备移除:挂起是电源管理状态,设备仍在系统总线上

可能的原因

1. 描述符状态变化

// 典型的现象
int fd = open("/dev/ttyUSB0", O_RDWR | O_NOCTTY);
// 挂起 USB 设备
system("echo suspend > /sys/bus/usb/devices/X-X/power/level");
// fd 仍然有效,但 I/O 操作会失败或阻塞

2. 检查描述符状态的几种方法

# 查看文件描述符信息
ls -l /proc/<PID>/fd/
ls -l /proc/<PID>/fdinfo/

# 查看 USB 设备状态
lsusb -t
cat /sys/bus/usb/devices/X-X/power/runtime_status

3. 程序示例:检测挂起状态

c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/ioctl.h>
#include <linux/serial.h>

int check_port_status(int fd) {
    struct serial_struct serinfo;
    
    if (ioctl(fd, TIOCGSERIAL, &serinfo) < 0) {
        perror("TIOCGSERIAL failed");
        return -1;
    }
    
    // 检查端口状态
    printf("Port type: %d\n", serinfo.type);
    printf("Port flags: 0x%x\n", serinfo.flags);
    
    return 0;
}

int main() {
    int fd = open("/dev/ttyUSB0", O_RDWR | O_NOCTTY);
    if (fd < 0) {
        perror("open failed");
        return 1;
    }
    
    printf("Descriptor opened: fd=%d\n", fd);
    
    // 尝试读取(可能会阻塞或失败)
    char buf[10];
    int n = read(fd, buf, sizeof(buf));
    if (n < 0) {
        printf("Read failed: %s (errno=%d)\n", strerror(errno), errno);
    }
    
    close(fd);
    return 0;
}

解决方案

方案1:监控 USB 事件并重新初始化

c
// 使用 udev 监控 USB 事件
#include <libudev.h>
#include <poll.h>

void monitor_usb_events() {
    struct udev *udev = udev_new();
    struct udev_monitor *mon = udev_monitor_new_from_netlink(udev, "udev");
    udev_monitor_filter_add_match_subsystem_devtype(mon, "usb", NULL);
    udev_monitor_enable_receiving(mon);
    
    int fd = udev_monitor_get_fd(mon);
    struct pollfd pfd = {.fd = fd, .events = POLLIN};
    
    while (1) {
        poll(&pfd, 1, -1);
        if (pfd.revents & POLLIN) {
            struct udev_device *dev = udev_monitor_receive_device(mon);
            const char *action = udev_device_get_action(dev);
            const char *syspath = udev_device_get_syspath(dev);
            
            printf("USB event: %s on %s\n", action, syspath);
            
            if (strcmp(action, "suspend") == 0) {
                // 处理挂起事件
                handle_suspend_event();
            } else if (strcmp(action, "resume") == 0) {
                // 处理恢复事件
                handle_resume_event();
            }
            
            udev_device_unref(dev);
        }
    }
}

方案2:使用非阻塞模式和超时

c
// 设置非阻塞 I/O 和超时
int setup_serial_timeout(int fd) {
    // 设置非阻塞
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
    
    // 设置超时
    struct termios options;
    tcgetattr(fd, &options);
    options.c_cc[VMIN] = 0;      // 最小字符数
    options.c_cc[VTIME] = 10;    // 超时(1秒 = 10*0.1秒)
    tcsetattr(fd, TCSANOW, &options);
    
    return 0;
}

方案3:完整的重连机制

c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <sys/epoll.h>

#define MAX_EVENTS 10

typedef struct {
    int fd;
    char device[64];
    int connected;
} serial_port_t;

void reconnect_port(serial_port_t *port) {
    if (port->fd >= 0) {
        close(port->fd);
        port->fd = -1;
    }
    
    int retries = 5;
    while (retries-- > 0) {
        port->fd = open(port->device, O_RDWR | O_NOCTTY | O_NONBLOCK);
        if (port->fd >= 0) {
            printf("Reconnected to %s\n", port->device);
            port->connected = 1;
            
            // 重新配置串口参数
            struct termios options;
            tcgetattr(port->fd, &options);
            cfsetispeed(&options, B9600);
            cfsetospeed(&options, B9600);
            tcsetattr(port->fd, TCSANOW, &options);
            
            break;
        }
        sleep(1);
    }
    
    if (port->fd < 0) {
        fprintf(stderr, "Failed to reconnect to %s\n", port->device);
        port->connected = 0;
    }
}

int main() {
    serial_port_t port = {
        .fd = -1,
        .device = "/dev/ttyUSB0",
        .connected = 0
    };
    
    // 初始连接
    reconnect_port(&port);
    
    int epoll_fd = epoll_create1(0);
    struct epoll_event ev, events[MAX_EVENTS];
    
    ev.events = EPOLLIN | EPOLLERR | EPOLLHUP;
    ev.data.fd = port.fd;
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, port.fd, &ev);
    
    while (1) {
        int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, 1000);
        
        for (int i = 0; i < nfds; i++) {
            if (events[i].events & EPOLLHUP) {
                printf("Device disconnected or suspended\n");
                port.connected = 0;
                reconnect_port(&port);
            }
            
            if (events[i].events & EPOLLIN) {
                char buffer[256];
                ssize_t n = read(port.fd, buffer, sizeof(buffer));
                if (n > 0) {
                    // 处理数据
                } else if (n < 0 && errno != EAGAIN) {
                    // 错误处理
                    port.connected = 0;
                    reconnect_port(&port);
                }
            }
        }
    }
    
    return 0;
}

建议的调试步骤

  1. 确认挂起状态

    bash
    cat /sys/bus/usb/devices/<bus>-<port>/power/runtime_status
  2. 监控内核消息

    bash
    dmesg -w
    # 或
    journalctl -f -k
  3. 测试文件描述符

    bash
    # 检查文件描述符是否还有效
    ls -l /proc/self/fd/3
    stat /proc/self/fd/3
  4. 使用 strace 跟踪

    bash
    strace -e openat,close,read,write -p <PID>

最佳实践

  1. 总是检查 I/O 操作的返回值

  2. 实现超时和重试机制

  3. 使用非阻塞模式或 select/poll/epoll

  4. 监控 udev 事件来响应硬件状态变化

  5. 实现优雅的重连逻辑

文件描述符在 USB 挂起时不会自动消失是正常行为,你需要主动检测和处理挂起/恢复事件,并实现相应的错误恢复机制。

posted @ 2026-01-26 08:10  FBshark  阅读(23)  评论(0)    收藏  举报