mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-09-18 23:09:29 +02:00
Bluetooth: btintel_pcie: split coredump worker into per-trigger works
btintel_pcie_coredump_worker() handled three unrelated jobs in one
work item: collect a DRAM trace coredump, read the hardware exception
event, and read the firmware-trigger event. The worker walked three
flag bits at runtime and each interrupt path mutated multiple bits
to communicate which sub-jobs the worker should run, which made the
ownership rules for those bits hard to reason about and entangled
the trigger reason with the in-progress accounting.
Replace the single combined worker with three single-purpose ones,
each owning exactly one flag:
coredump_work -> btintel_pcie_dump_traces()
guarded by COREDUMP_INPROGRESS
hwexp_work -> btintel_pcie_read_hwexp()
guarded by CORE_HALTED (already permanent until
re-probe; HWEXP_INPROGRESS is now redundant
and removed)
fwtrigger_work -> btintel_pcie_dump_fwtrigger_event()
guarded by FWTRIGGER_DUMP_INPROGRESS
All three workers are queued on a shared ordered workqueue (renamed
coredump_workqueue -> dump_workqueue) so a companion event reader
(hwexp/fwtrigger) and the coredump always run FIFO. Companion work
is queued before coredump_work so dmp_hdr.event_type/event_id are
populated by the time dump_traces() consumes them, preserving the
original ordering.
Introduce btintel_pcie_queue_coredump() to centralize the coredump
trigger contract: it is the single writer of COREDUMP_INPROGRESS and
of dmp_hdr.trigger_reason, sets both atomically against concurrent
triggers, and rolls back the bit if the workqueue is disabled
(reset/remove in progress) so a later trigger after re-probe can
succeed. All four trigger sites (HWEXP IRQ, FW-trigger IRQ,
devcoredump user trigger, resume() D0 error path) go through the
helper.
Per-work guard bits are now cleared at the tail of each worker
rather than in the middle of the combined worker, which closes a
subtle race where a duplicate IRQ could observe a cleared bit and
requeue while the previous pass was still finalizing
dev_coredumpv().
reset_work() and remove() now disable_work_sync() all three workers
and, on the FLR-failure path, enable_work() all three to keep their
disable counters balanced. The PLDR/FLR-success contract (re-probe
re-INIT_WORKs everything with counter 0) is preserved.
No functional change to the dump payloads; this is a pure
restructuring of the worker dispatch and its synchronization.
Signed-off-by: Kiran K <kiran.k@intel.com>
Assisted-by: GitHub-Copilot:claude-4.7-opus
Signed-off-by: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
This commit is contained in:
committed by
Luiz Augusto von Dentz
parent
a2b3b4f004
commit
86f8661893
@@ -1446,72 +1446,134 @@ exit_on_error:
|
||||
return err;
|
||||
}
|
||||
|
||||
/* Queue a coredump dump_traces() pass.
|
||||
*
|
||||
* Returns true if a new coredump was queued, false if one was already
|
||||
* in-flight (the BTINTEL_PCIE_COREDUMP_INPROGRESS bit serves as the
|
||||
* single-writer guard for the @coredump_work item) or the workqueue is
|
||||
* disabled (reset / remove in progress).
|
||||
*
|
||||
* Always queue this AFTER any companion event-reader work (hwexp /
|
||||
* fwtrigger) so that, on the ordered @dump_workqueue, the event reader
|
||||
* runs first and populates dmp_hdr.event_type / event_id before
|
||||
* dump_traces consumes them.
|
||||
*/
|
||||
static bool btintel_pcie_queue_coredump(struct btintel_pcie_data *data,
|
||||
u16 trigger_reason)
|
||||
{
|
||||
if (test_and_set_bit(BTINTEL_PCIE_COREDUMP_INPROGRESS, &data->flags))
|
||||
return false;
|
||||
|
||||
data->dmp_hdr.trigger_reason = trigger_reason;
|
||||
|
||||
if (queue_work(data->dump_workqueue, &data->coredump_work))
|
||||
return true;
|
||||
|
||||
/* Workqueue is disabled (reset/remove drained it). Release the
|
||||
* guard so a later trigger, after re-probe, can succeed.
|
||||
*/
|
||||
clear_bit(BTINTEL_PCIE_COREDUMP_INPROGRESS, &data->flags);
|
||||
return false;
|
||||
}
|
||||
|
||||
static void btintel_pcie_msix_fw_trigger_handler(struct btintel_pcie_data *data)
|
||||
{
|
||||
bt_dev_dbg(data->hdev, "Received firmware smart trigger cause");
|
||||
|
||||
if (test_and_set_bit(BTINTEL_PCIE_FWTRIGGER_DUMP_INPROGRESS, &data->flags))
|
||||
/* Per-work guard: deduplicate concurrent FW-trigger interrupts.
|
||||
* Cleared at the tail of btintel_pcie_fwtrigger_worker().
|
||||
*/
|
||||
if (test_and_set_bit(BTINTEL_PCIE_FWTRIGGER_DUMP_INPROGRESS,
|
||||
&data->flags))
|
||||
return;
|
||||
|
||||
/* Trigger device core dump when there is FW assert */
|
||||
if (!test_and_set_bit(BTINTEL_PCIE_COREDUMP_INPROGRESS, &data->flags))
|
||||
data->dmp_hdr.trigger_reason = BTINTEL_PCIE_TRIGGER_REASON_FW_ASSERT;
|
||||
if (!queue_work(data->dump_workqueue, &data->fwtrigger_work)) {
|
||||
clear_bit(BTINTEL_PCIE_FWTRIGGER_DUMP_INPROGRESS, &data->flags);
|
||||
return;
|
||||
}
|
||||
|
||||
queue_work(data->coredump_workqueue, &data->coredump_work);
|
||||
/* Queue coredump after the fwtrigger event reader so dmp_hdr.event_*
|
||||
* is populated before dump_traces consumes it.
|
||||
*/
|
||||
btintel_pcie_queue_coredump(data, BTINTEL_PCIE_TRIGGER_REASON_FW_ASSERT);
|
||||
}
|
||||
|
||||
static void btintel_pcie_msix_hw_exp_handler(struct btintel_pcie_data *data)
|
||||
{
|
||||
bt_dev_err(data->hdev, "Received hw exception interrupt");
|
||||
|
||||
/* CORE_HALTED is the single-writer guard for this handler. It is
|
||||
* set once on first HW exception and cleared only by re-probe
|
||||
* (data is reallocated), so it also serializes hwexp_work
|
||||
* scheduling without needing a separate bit.
|
||||
*/
|
||||
if (test_and_set_bit(BTINTEL_PCIE_CORE_HALTED, &data->flags))
|
||||
return;
|
||||
|
||||
if (test_and_set_bit(BTINTEL_PCIE_HWEXP_INPROGRESS, &data->flags))
|
||||
return;
|
||||
/* Queue companion coredump first so it is appended after hwexp_work
|
||||
* on the ordered @dump_workqueue (preserves the original
|
||||
* coredump-then-hwexp ordering).
|
||||
*/
|
||||
btintel_pcie_queue_coredump(data, BTINTEL_PCIE_TRIGGER_REASON_FW_ASSERT);
|
||||
|
||||
/* Trigger device core dump when there is HW exception */
|
||||
if (!test_and_set_bit(BTINTEL_PCIE_COREDUMP_INPROGRESS, &data->flags))
|
||||
data->dmp_hdr.trigger_reason = BTINTEL_PCIE_TRIGGER_REASON_FW_ASSERT;
|
||||
|
||||
queue_work(data->coredump_workqueue, &data->coredump_work);
|
||||
queue_work(data->dump_workqueue, &data->hwexp_work);
|
||||
}
|
||||
|
||||
static void btintel_pcie_coredump_worker(struct work_struct *work)
|
||||
{
|
||||
struct btintel_pcie_data *data = container_of(work,
|
||||
struct btintel_pcie_data, coredump_work);
|
||||
int err;
|
||||
|
||||
/* hdev is NULL until setup_hdev() succeeds, and is cleared on
|
||||
* teardown after disable_work_sync() drains us; bail in that case.
|
||||
*/
|
||||
if (!data->hdev)
|
||||
goto out;
|
||||
|
||||
btintel_pcie_dump_traces(data->hdev);
|
||||
out:
|
||||
/* Release guard last so a new trigger can run only after this
|
||||
* pass has fully completed (including dev_coredumpv()).
|
||||
*/
|
||||
clear_bit(BTINTEL_PCIE_COREDUMP_INPROGRESS, &data->flags);
|
||||
}
|
||||
|
||||
static void btintel_pcie_hwexp_worker(struct work_struct *work)
|
||||
{
|
||||
struct btintel_pcie_data *data = container_of(work,
|
||||
struct btintel_pcie_data, hwexp_work);
|
||||
|
||||
if (!data->hdev)
|
||||
return;
|
||||
|
||||
if (test_bit(BTINTEL_PCIE_FWTRIGGER_DUMP_INPROGRESS, &data->flags)) {
|
||||
err = btintel_pcie_dump_fwtrigger_event(data);
|
||||
if (err)
|
||||
bt_dev_warn(data->hdev, "failed to log fwtrigger event");
|
||||
clear_bit(BTINTEL_PCIE_FWTRIGGER_DUMP_INPROGRESS, &data->flags);
|
||||
}
|
||||
/* Unlike usb products, controller will not send hardware exception
|
||||
* event on exception. Instead controller writes the hardware event
|
||||
* to device memory along with optional debug events, raises MSIX
|
||||
* and halts. Driver shall read the exception event from device
|
||||
* memory and passes it to the stack for further processing.
|
||||
*
|
||||
* Re-entry is gated by BTINTEL_PCIE_CORE_HALTED in the IRQ
|
||||
* handler, which is only cleared by re-probe; no per-work bit
|
||||
* is needed here.
|
||||
*/
|
||||
btintel_pcie_read_hwexp(data);
|
||||
}
|
||||
|
||||
if (test_bit(BTINTEL_PCIE_COREDUMP_INPROGRESS, &data->flags)) {
|
||||
btintel_pcie_dump_traces(data->hdev);
|
||||
clear_bit(BTINTEL_PCIE_COREDUMP_INPROGRESS, &data->flags);
|
||||
}
|
||||
static void btintel_pcie_fwtrigger_worker(struct work_struct *work)
|
||||
{
|
||||
struct btintel_pcie_data *data = container_of(work,
|
||||
struct btintel_pcie_data, fwtrigger_work);
|
||||
int err;
|
||||
|
||||
if (test_bit(BTINTEL_PCIE_HWEXP_INPROGRESS, &data->flags)) {
|
||||
/* Unlike usb products, controller will not send hardware
|
||||
* exception event on exception. Instead controller writes the
|
||||
* hardware event to device memory along with optional debug
|
||||
* events, raises MSIX and halts. Driver shall read the
|
||||
* exception event from device memory and passes it stack for
|
||||
* further processing.
|
||||
*/
|
||||
btintel_pcie_read_hwexp(data);
|
||||
clear_bit(BTINTEL_PCIE_HWEXP_INPROGRESS, &data->flags);
|
||||
}
|
||||
if (!data->hdev)
|
||||
goto out;
|
||||
|
||||
err = btintel_pcie_dump_fwtrigger_event(data);
|
||||
if (err)
|
||||
bt_dev_warn(data->hdev, "failed to log fwtrigger event");
|
||||
out:
|
||||
/* Release guard last; matches set in fw_trigger handler. */
|
||||
clear_bit(BTINTEL_PCIE_FWTRIGGER_DUMP_INPROGRESS, &data->flags);
|
||||
}
|
||||
|
||||
static void btintel_pcie_rx_work(struct work_struct *work)
|
||||
@@ -2650,20 +2712,22 @@ static void btintel_pcie_reset_work(struct work_struct *wk)
|
||||
btintel_pcie_synchronize_irqs(data);
|
||||
|
||||
flush_work(&data->rx_work);
|
||||
/* Drain any in-flight coredump and block new ones across reset.
|
||||
* Safe from self-deadlock: coredump_work runs on a separate wq.
|
||||
/* Drain any in-flight dump workers and block new ones across reset.
|
||||
* Safe from self-deadlock: they all run on a separate wq.
|
||||
*/
|
||||
disable_work_sync(&data->coredump_work);
|
||||
disable_work_sync(&data->hwexp_work);
|
||||
disable_work_sync(&data->fwtrigger_work);
|
||||
|
||||
bt_dev_dbg(data->hdev, "Release bluetooth interface");
|
||||
|
||||
/* Both reset paths follow the same contract: on success they
|
||||
* destroy 'data' via device_reprobe() (a fresh probe re-INIT_WORKs
|
||||
* the coredump_work with disable count 0), so enable_work() must
|
||||
* the dump workers with disable count 0), so enable_work() must
|
||||
* NOT be called on the success path. Only the FLR path can fail
|
||||
* with 'data' still alive, in which case we balance the
|
||||
* disable_work_sync() above so a later successful reset is not
|
||||
* permanently blocked.
|
||||
* disable_work_sync() calls above so a later successful reset is
|
||||
* not permanently blocked.
|
||||
*
|
||||
* pci_lock_rescan_remove() (held above) serializes against PCI
|
||||
* device addition/removal (hotplug), so no device can be added to
|
||||
@@ -2674,8 +2738,11 @@ static void btintel_pcie_reset_work(struct work_struct *wk)
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (btintel_pcie_perform_flr(data))
|
||||
if (btintel_pcie_perform_flr(data)) {
|
||||
enable_work(&data->coredump_work);
|
||||
enable_work(&data->hwexp_work);
|
||||
enable_work(&data->fwtrigger_work);
|
||||
}
|
||||
|
||||
out:
|
||||
pci_dev_put(pdev);
|
||||
@@ -2869,8 +2936,8 @@ static int btintel_pcie_probe(struct pci_dev *pdev,
|
||||
if (!data->workqueue)
|
||||
return -ENOMEM;
|
||||
|
||||
data->coredump_workqueue = alloc_ordered_workqueue(KBUILD_MODNAME "_cd", 0);
|
||||
if (!data->coredump_workqueue) {
|
||||
data->dump_workqueue = alloc_ordered_workqueue(KBUILD_MODNAME "_cd", 0);
|
||||
if (!data->dump_workqueue) {
|
||||
destroy_workqueue(data->workqueue);
|
||||
return -ENOMEM;
|
||||
}
|
||||
@@ -2879,6 +2946,8 @@ static int btintel_pcie_probe(struct pci_dev *pdev,
|
||||
INIT_WORK(&data->rx_work, btintel_pcie_rx_work);
|
||||
INIT_WORK(&data->reset_work, btintel_pcie_reset_work);
|
||||
INIT_WORK(&data->coredump_work, btintel_pcie_coredump_worker);
|
||||
INIT_WORK(&data->hwexp_work, btintel_pcie_hwexp_worker);
|
||||
INIT_WORK(&data->fwtrigger_work, btintel_pcie_fwtrigger_worker);
|
||||
|
||||
data->boot_stage_cache = 0x00;
|
||||
data->img_resp_cache = 0x00;
|
||||
@@ -2921,7 +2990,7 @@ exit_error:
|
||||
/* reset device before exit */
|
||||
btintel_pcie_reset_bt(data);
|
||||
|
||||
destroy_workqueue(data->coredump_workqueue);
|
||||
destroy_workqueue(data->dump_workqueue);
|
||||
|
||||
pci_clear_master(pdev);
|
||||
|
||||
@@ -2940,12 +3009,14 @@ static void btintel_pcie_remove(struct pci_dev *pdev)
|
||||
return;
|
||||
}
|
||||
|
||||
/* Permanently block coredump triggers and drain the worker before
|
||||
* tearing down. Must run before cancel_work_sync(&reset_work) so
|
||||
* the disable counter stays >= 1 even after reset_work()'s
|
||||
/* Permanently block all dump triggers and drain the workers before
|
||||
* tearing down. Must run before disable_work_sync(&reset_work) so
|
||||
* the disable counters stay >= 1 even after reset_work()'s
|
||||
* balanced enable_work() (counter 2 -> 1, never reaching 0).
|
||||
*/
|
||||
disable_work_sync(&data->coredump_work);
|
||||
disable_work_sync(&data->hwexp_work);
|
||||
disable_work_sync(&data->fwtrigger_work);
|
||||
|
||||
/* Cancel pending reset work. Skip only when remove() is called from
|
||||
* within the reset work itself (PLDR device_reprobe path) to avoid
|
||||
@@ -2973,7 +3044,7 @@ static void btintel_pcie_remove(struct pci_dev *pdev)
|
||||
|
||||
btintel_pcie_release_hdev(data);
|
||||
|
||||
destroy_workqueue(data->coredump_workqueue);
|
||||
destroy_workqueue(data->dump_workqueue);
|
||||
destroy_workqueue(data->workqueue);
|
||||
|
||||
btintel_pcie_free(data);
|
||||
@@ -2992,16 +3063,8 @@ static void btintel_pcie_coredump(struct device *dev)
|
||||
if (!data)
|
||||
return;
|
||||
|
||||
if (test_and_set_bit(BTINTEL_PCIE_COREDUMP_INPROGRESS, &data->flags))
|
||||
return;
|
||||
|
||||
data->dmp_hdr.trigger_reason = BTINTEL_PCIE_TRIGGER_REASON_USER_TRIGGER;
|
||||
/* queue_work() returns false if the work is disabled (reset or
|
||||
* remove in progress); clear the in-progress bit so a later
|
||||
* trigger can succeed once the work is re-enabled.
|
||||
*/
|
||||
if (!queue_work(data->coredump_workqueue, &data->coredump_work))
|
||||
clear_bit(BTINTEL_PCIE_COREDUMP_INPROGRESS, &data->flags);
|
||||
btintel_pcie_queue_coredump(data,
|
||||
BTINTEL_PCIE_TRIGGER_REASON_USER_TRIGGER);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -3138,12 +3201,8 @@ static int btintel_pcie_resume(struct device *dev)
|
||||
if (btintel_pcie_in_error(data) ||
|
||||
btintel_pcie_in_device_halt(data)) {
|
||||
bt_dev_err(data->hdev, "Controller in error state for D0 entry");
|
||||
if (!test_and_set_bit(BTINTEL_PCIE_COREDUMP_INPROGRESS,
|
||||
&data->flags)) {
|
||||
data->dmp_hdr.trigger_reason =
|
||||
BTINTEL_PCIE_TRIGGER_REASON_FW_ASSERT;
|
||||
queue_work(data->coredump_workqueue, &data->coredump_work);
|
||||
}
|
||||
btintel_pcie_queue_coredump(data,
|
||||
BTINTEL_PCIE_TRIGGER_REASON_FW_ASSERT);
|
||||
set_bit(BTINTEL_PCIE_CORE_HALTED, &data->flags);
|
||||
btintel_pcie_reset(data->hdev);
|
||||
}
|
||||
|
||||
@@ -118,7 +118,6 @@ enum {
|
||||
|
||||
enum {
|
||||
BTINTEL_PCIE_CORE_HALTED,
|
||||
BTINTEL_PCIE_HWEXP_INPROGRESS,
|
||||
BTINTEL_PCIE_COREDUMP_INPROGRESS,
|
||||
BTINTEL_PCIE_FWTRIGGER_DUMP_INPROGRESS,
|
||||
BTINTEL_PCIE_RECOVERY_IN_PROGRESS,
|
||||
@@ -466,8 +465,11 @@ struct btintel_pcie_dump_header {
|
||||
* @workqueue: workqueue for RX work
|
||||
* @rx_skb_q: SKB queue for RX packet
|
||||
* @rx_work: RX work struct to process the RX packet in @rx_skb_q
|
||||
* @coredump_workqueue: dedicated workqueue for coredump collection
|
||||
* @coredump_work: work struct for coredump trace collection
|
||||
* @dump_workqueue: dedicated ordered workqueue serializing the coredump,
|
||||
* hardware exception, and firmware-trigger dump workers
|
||||
* @coredump_work: work struct for DRAM trace coredump collection
|
||||
* @hwexp_work: work struct for hardware exception event read
|
||||
* @fwtrigger_work: work struct for firmware-triggered diagnostic event read
|
||||
* @dma_pool: DMA pool for descriptors, index array and ci
|
||||
* @dma_p_addr: DMA address for pool
|
||||
* @dma_v_addr: address of pool
|
||||
@@ -516,8 +518,10 @@ struct btintel_pcie_data {
|
||||
struct work_struct rx_work;
|
||||
struct work_struct reset_work;
|
||||
|
||||
struct workqueue_struct *coredump_workqueue;
|
||||
struct workqueue_struct *dump_workqueue;
|
||||
struct work_struct coredump_work;
|
||||
struct work_struct hwexp_work;
|
||||
struct work_struct fwtrigger_work;
|
||||
|
||||
struct dma_pool *dma_pool;
|
||||
dma_addr_t dma_p_addr;
|
||||
|
||||
Reference in New Issue
Block a user