RT-Thread论坛
直播中

卞轮辉

9年用户 1170经验值
私信 关注
[问答]

ulog后端缓存清除写入文件时异常,导致(fd != RT_NULL) has assert failed at dfs_elm_lseek:605.怎么解决?

应该是同步输出时候两个线程竞争fd导致,新增输出锁
ulog后端输出清除缓存写入文件系统中如果出错又调用了ulog输出,重复进入写文件导致sem_sd1信号量获取两次却不释放,死等待sem_sd1信号量,导致ulog不能使用

  • /*
  • * Copyright (c) 2006-2023, RT-Thread Development Team
  • *
  • * SPDX-License-Identifier: Apache-2.0
  • *
  • * Change Logs:
  • * Date           Author        Notes
  • * 2011-07-25     weety     first version
  • */



  • /**
  • * flush all backends's log
  • */
  • void ulog_flush(void)
  • {
  •     rt_slist_t *node;
  •     ulog_backend_t backend;

  •     if (!ulog.init_ok)
  •         return;

  • #ifdef ULOG_USING_ASYNC_OUTPUT
  •     ulog_async_output();
  • #endif

  •     /* flush all backends */
  •     for (node = rt_slist_first(&ulog.backend_list); node; node = rt_slist_next(node))
  •     {
  •         backend = rt_slist_entry(node, struct ulog_backend, list);
  •         if (backend->flush)
  •         {
  •             output_lock();
  •             backend->flush(backend);
  •             output_unlock();
  •         }
  •     }
  • }

回帖(1)

红旧衫

2025-10-11 16:10:40

根据错误信息,问题出现在`dfs_elm_lseek`函数中的第605行,断言`(fd != RT_NULL)`失败。这意味着在尝试进行文件定位操作时,文件描述符`fd`为空指针。这通常是由于文件操作过程中文件描述符被意外关闭或未正确初始化造成的。

结合上下文描述,问题可能由两个线程同时操作同一个文件描述符引起(竞争条件)。此外,在写入文件出错时,又调用了ulog输出,导致重复进入写文件操作,进而造成信号量`sem_sd1`被获取两次而没有释放,最终导致死锁。

解决方案:
1. 确保文件操作(特别是写入和关闭)是线程安全的,通过互斥锁(mutex)保护对同一个文件描述符的访问。
2. 在写入文件出错时,避免在错误处理中再次调用ulog输出,以防止递归调用和信号量重复获取。
3. 检查文件描述符的状态,确保在操作之前它是有效的。

根据提供的代码片段,我们无法看到完整的实现,但可以针对描述的问题提出以下修改建议:

### 步骤1:添加互斥锁保护文件操作
在ulog后端输出到文件的部分,确保每个文件操作(打开、写入、关闭)都被同一个互斥锁保护,避免多个线程同时操作同一个文件描述符。

例如,定义一个静态的互斥锁:
```c
static rt_mutex_t file_ops_mutex = RT_NULL;
```
在初始化时创建这个互斥锁。

### 步骤2:修改文件写入函数
在写入文件之前获取互斥锁,写入完成后释放互斥锁。同时,在遇到错误时,不要调用ulog输出,而是直接处理错误(如关闭文件、设置错误标志等)。

### 步骤3:避免在错误处理中调用ulog
在文件操作出错时,不要使用ulog输出错误信息,因为ulog本身可能尝试写入同一个文件,导致递归和死锁。可以设置一个错误标志,或者使用其他方式记录错误(如串口输出)。

### 步骤4:修复信号量重复获取的问题
确保在每次获取信号量`sem_sd1`后,无论操作成功与否,都要在函数退出前释放信号量。避免在错误处理分支中忘记释放信号量。

### 示例代码片段修改
假设原代码中写文件操作如下(这里只是一个示例):
```c
static void ulog_file_backend_output(/* ... */)
{
    // ... 其他代码 ...

    /* 获取信号量 */
    rt_sem_take(&sem_sd1, RT_WAITING_FOREVER);

    if (write_file() < 0)
    {
        // 错误处理,这里如果调用ulog输出,可能会再次尝试获取信号量,导致死锁
        // 所以,不能在这里调用ulog_xxx函数
        // 可以设置错误标志或使用rt_kprintf(如果可用)输出错误信息
        rt_kprintf("ulog write file errorn");
        // 然后,注意在错误处理中也要释放信号量
        rt_sem_release(&sem_sd1);
        return;
    }

    // ... 其他操作 ...

    rt_sem_release(&sem_sd1);
}
```

但是,根据描述,错误处理中又调用了ulog,这会导致再次进入这个函数,从而再次获取信号量,而之前已经获取了一次,所以会等待。因此,我们需要确保在错误处理中不调用ulog。

### 步骤5:检查文件描述符的有效性
在每次使用文件描述符之前,检查其是否为`RT_NULL`,避免对空指针进行操作。

### 步骤6:重构代码
考虑重构代码,将文件操作封装成一个函数,并使用互斥锁保护整个文件操作过程,确保同一时间只有一个线程执行文件操作。

例如:
```c
static void file_write_with_mutex(const char *buf, size_t size)
{
    rt_mutex_take(file_ops_mutex, RT_WAITING_FOREVER);
    // 检查fd是否有效
    if (fd == RT_NULL)
    {
        // 处理无效fd,比如尝试重新打开文件?
        // 注意,重新打开文件操作也要在互斥锁保护下
    }
    // 写入操作
    ssize_t ret = write(fd, buf, size);
    if (ret < 0)
    {
        // 错误处理,不要调用ulog
        // 可以记录错误标志,或者尝试关闭并重新打开文件
        // 如果文件描述符无效,则需要关闭(如果之前是打开的)并重新打开
        close(fd);
        fd = RT_NULL;
        // 设置需要重新打开文件的标志
    }
    rt_mutex_release(file_ops_mutex);
}
```

### 关于信号量`sem_sd1`
根据描述,信号量`sem_sd1`是用于同步输出的。但是,如果错误处理中再次调用ulog,会导致重复获取这个信号量(同一个线程递归获取两次),因为第一次获取后还没有释放就再次进入获取。所以,需要避免在持有信号量的情况下再次调用ulog。

### 总结修改方案
1. 使用互斥锁保护文件操作,确保线程安全。
2. 在错误处理中避免使用ulog输出,改用其他方式(如rt_kprintf)。
3. 确保每次获取信号量后,在函数所有退出路径上都有释放信号量。
4. 在文件操作前检查文件描述符的有效性,并在无效时进行适当的恢复(如重新打开文件)。

由于没有完整的代码,以上只能提供大致的解决方向。具体实现需要根据实际代码调整。

根据问题描述,异常发生在文件系统操作中,主要原因是多线程竞争文件描述符(fd)和信号量管理不当,导致死锁。以下是针对性的解决方案:


问题分析



  1. 断言失败 fd != RT_NULL:文件操作时文件描述符失效(可能被意外关闭)。

  2. 死锁问题:写入失败时调用日志函数,导致递归获取信号量 sem_sd1

  3. 多线程竞争:多个线程同时操作同一个文件描述符。


解决方案


1. 修复文件描述符竞争(关键)


在文件操作函数中增加互斥锁,确保同一时间只有一个线程访问文件描述符。修改日志后端的输出函数:


// 定义全局文件操作锁
static rt_mutex_t file_ops_mutex = RT_NULL;

// 初始化锁(在模块初始化时调用)
int ulog_file_backend_init(void) {
    file_ops_mutex = rt_mutex_create("ulog_file", RT_IPC_FLAG_FIFO);
    if (!file_ops_mutex) return -RT_ERROR;
    // ... 其他初始化
}

// 修改输出函数
static void ulog_file_backend_output(...) {
    rt_mutex_take(file_ops_mutex, RT_WAITING_FOREVER); // 加锁

    if (fd != RT_NULL) {
        // 原有写文件操作
        ssize_t ret = write(fd, log_buf, len);
        if (ret < 0) {
            // 错误处理(禁止调用任何ulog函数!)
            rt_kprintf("!! Write failed: %dn", ret); // 改用rt_kprintf
            _close_file_on_error(fd); // 关闭文件并置空fd
            fd = RT_NULL;
        }
    }

    rt_mutex_release(file_ops_mutex); // 解锁
}

2. 处理递归死锁


在错误处理中禁止调用 ulog 函数,改用安全输出(如 rt_kprintf):


// 自定义安全关闭函数(避免递归)
static void _close_file_on_error(int fd) {
    if (fd != RT_NULL) {
        close(fd); // 关闭文件
        fd = RT_NULL;
    }
}

3. 修复信号量重复获取


确保信号量释放逻辑覆盖所有分支:


// 原有信号量操作位置(示例)
void log_function(...) {
    rt_sem_take(&sem_sd1, RT_WAITING_FOREVER); // 获取信号量

    if (do_operation() != SUCCESS) {
        rt_sem_release(&sem_sd1); // 错误分支释放信号量!
        return;
    }

    rt_sem_release(&sem_sd1); // 正常释放
}

4. 增强文件描述符健壮性


在操作文件前检查 fd 有效性:


if (fd == RT_NULL) {
    rt_kprintf("!! Invalid file descriptorn");
    return;
}

额外建议



  1. 错误隔离:文件操作失败后,禁用文件后端日志,避免持续失败。

  2. 重试机制:可添加文件重连逻辑(需谨慎设计避免频繁重试)。

  3. 日志队列:使用异步日志队列(生产者-消费者模型)解耦日志产生和写入操作。


关键原则



  1. 禁止在文件操作错误中调用 ulog

    错误处理只允许使用 rt_kprintf 等非依赖文件系统的输出。

  2. 锁覆盖范围:文件操作锁需覆盖所有 fd 操作(读/写/定位/关闭)。

  3. 信号量释放:确保每个 take() 都有对应的 release(),包括错误分支。



注意:修改后需彻底测试多线程场景下的文件操作稳定性和异常恢复能力。


举报

更多回帖

发帖
×
20
完善资料,
赚取积分