「前言」文章内容大致是关于 malloc 的线程安全与可重入问题。

malloc 的线程安全与可重入

线程安全就是多个线程并发执行相同代码,程序的结果依然是正确的。

可重入函数是指能被线程并发调用,且调用过程不会出现错误的函数。关键在于,当函数在执行过程中被中断,转而执行其他代码(比如被另一个线程调用),之后再返回继续执行时,它的执行结果和连续执行的结果相同,不会因为中断而出现数据混乱等错误。

可重入则要保证:

  • 不使用全局变量或静态变量。
  • 不使用用malloc或者new开辟出的空间。
  • 不调用不可重入函数。
  • 不返回静态或全局数据,所有数据都有函数的调用者提供。
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。

结论:malloc 函数是线程安全但是不可重入的。

因为malloc函数在用户空间要自己管理各进程共享的内存链表,由于有共享资源访问,本身会造成线程不安全。为了做到线程安全,需要加锁进行保护。(锁保证了线程安全,可重入必定是线程安全的,但线程安全不一定可重入的)

  1. 如果 malloc 使用普通锁进行加锁:直接就会发生死锁(信号处理函数中断了原程序的执行,在信号处理函数里再调用 malloc 函数,普通的锁就会造成死锁)。
  2. 如果 malloc 使用递归锁进行加锁:在信号处理程序中可以执行 malloc,但是可能会破坏共享的内存链表等信息。

虽然使用递归锁能够保证 malloc 函数的线程安全性,但是不能保证它的可重入性。按上面的场景,程序调用 malloc 函数时收到信号,在信号处理函数里再调用 malloc 函数就可能破坏共享的内存链表等资源,因而是不可重入的。

进行信号处理的时机:从内核态切换回用户态的时候,进行处理。

  • malloc 函数内部维护着共享的内存链表,用于管理内存的分配和回收。这个链表记录了哪些内存块是空闲的,哪些是已经分配的等信息。
  • 假设在正常的程序执行流程中,malloc函数正在对内存链表进行操作,比如正在调整链表节点来分配一块新的内存。此时如果收到一个信号,程序的执行流程会中断并跳转到信号处理程序。
  • 如果信号处理程序也调用了 malloc,由于 malloc 内部的共享内存链表可能处于一种不稳定的中间状态(因为之前的malloc操作还没完成),新的 malloc 调用就可能会错误地修改这个链表。例如,可能会把正在分配过程中的内存块误认为是空闲的,或者打乱了链表的顺序,从而破坏了共享的内存链表等信息。
  • 这是因为信号处理程序和正常的程序执行流程是异步的,信号处理程序无法知道主程序中 malloc 的执行状态,并且它们共享了 malloc 函数内部的关键数据结构(内存链表),没有合适的同步机制来处理这种情况,所以即使 malloc 使用了递归锁来处理多线程问题,在信号处理程序中调用 malloc 仍然可能会破坏共享的内存链表。

new 底层也是调用 malloc,同理。

————— END —————

「 作者 」 枫叶先生
「 更新 」 2024.10.28
「 声明 」 余之才疏学浅,故所撰文疏漏难免,
          或有谬误或不准确之处,敬请读者批评指正。