root/arch/mips/mm/context.c
// SPDX-License-Identifier: GPL-2.0
#include <linux/atomic.h>
#include <linux/mmu_context.h>
#include <linux/percpu.h>
#include <linux/spinlock.h>

static DEFINE_RAW_SPINLOCK(cpu_mmid_lock);

static atomic64_t mmid_version;
static unsigned int num_mmids;
static unsigned long *mmid_map;

static DEFINE_PER_CPU(u64, reserved_mmids);
static cpumask_t tlb_flush_pending;

static bool asid_versions_eq(int cpu, u64 a, u64 b)
{
        return ((a ^ b) & asid_version_mask(cpu)) == 0;
}

void get_new_mmu_context(struct mm_struct *mm)
{
        unsigned int cpu;
        u64 asid;

        /*
         * This function is specific to ASIDs, and should not be called when
         * MMIDs are in use.
         */
        if (WARN_ON(IS_ENABLED(CONFIG_DEBUG_VM) && cpu_has_mmid))
                return;

        cpu = smp_processor_id();
        asid = asid_cache(cpu);

        if (!((asid += cpu_asid_inc()) & cpu_asid_mask(&cpu_data[cpu]))) {
                if (cpu_has_vtag_icache)
                        flush_icache_all();
                local_flush_tlb_all();  /* start new asid cycle */
        }

        set_cpu_context(cpu, mm, asid);
        asid_cache(cpu) = asid;
}
EXPORT_SYMBOL_GPL(get_new_mmu_context);

void check_mmu_context(struct mm_struct *mm)
{
        unsigned int cpu = smp_processor_id();

        /*
         * This function is specific to ASIDs, and should not be called when
         * MMIDs are in use.
         */
        if (WARN_ON(IS_ENABLED(CONFIG_DEBUG_VM) && cpu_has_mmid))
                return;

        /* Check if our ASID is of an older version and thus invalid */
        if (!asid_versions_eq(cpu, cpu_context(cpu, mm), asid_cache(cpu)))
                get_new_mmu_context(mm);
}
EXPORT_SYMBOL_GPL(check_mmu_context);

static void flush_context(void)
{
        u64 mmid;
        int cpu;

        /* Update the list of reserved MMIDs and the MMID bitmap */
        bitmap_zero(mmid_map, num_mmids);

        /* Reserve an MMID for kmap/wired entries */
        __set_bit(MMID_KERNEL_WIRED, mmid_map);

        for_each_possible_cpu(cpu) {
                mmid = xchg_relaxed(&cpu_data[cpu].asid_cache, 0);

                /*
                 * If this CPU has already been through a
                 * rollover, but hasn't run another task in
                 * the meantime, we must preserve its reserved
                 * MMID, as this is the only trace we have of
                 * the process it is still running.
                 */
                if (mmid == 0)
                        mmid = per_cpu(reserved_mmids, cpu);

                __set_bit(mmid & cpu_asid_mask(&cpu_data[cpu]), mmid_map);
                per_cpu(reserved_mmids, cpu) = mmid;
        }

        /*
         * Queue a TLB invalidation for each CPU to perform on next
         * context-switch
         */
        cpumask_setall(&tlb_flush_pending);
}

static bool check_update_reserved_mmid(u64 mmid, u64 newmmid)
{
        bool hit;
        int cpu;

        /*
         * Iterate over the set of reserved MMIDs looking for a match.
         * If we find one, then we can update our mm to use newmmid
         * (i.e. the same MMID in the current generation) but we can't
         * exit the loop early, since we need to ensure that all copies
         * of the old MMID are updated to reflect the mm. Failure to do
         * so could result in us missing the reserved MMID in a future
         * generation.
         */
        hit = false;
        for_each_possible_cpu(cpu) {
                if (per_cpu(reserved_mmids, cpu) == mmid) {
                        hit = true;
                        per_cpu(reserved_mmids, cpu) = newmmid;
                }
        }

        return hit;
}

static u64 get_new_mmid(struct mm_struct *mm)
{
        static u32 cur_idx = MMID_KERNEL_WIRED + 1;
        u64 mmid, version, mmid_mask;

        mmid = cpu_context(0, mm);
        version = atomic64_read(&mmid_version);
        mmid_mask = cpu_asid_mask(&boot_cpu_data);

        if (!asid_versions_eq(0, mmid, 0)) {
                u64 newmmid = version | (mmid & mmid_mask);

                /*
                 * If our current MMID was active during a rollover, we
                 * can continue to use it and this was just a false alarm.
                 */
                if (check_update_reserved_mmid(mmid, newmmid)) {
                        mmid = newmmid;
                        goto set_context;
                }

                /*
                 * We had a valid MMID in a previous life, so try to re-use
                 * it if possible.
                 */
                if (!__test_and_set_bit(mmid & mmid_mask, mmid_map)) {
                        mmid = newmmid;
                        goto set_context;
                }
        }

        /* Allocate a free MMID */
        mmid = find_next_zero_bit(mmid_map, num_mmids, cur_idx);
        if (mmid != num_mmids)
                goto reserve_mmid;

        /* We're out of MMIDs, so increment the global version */
        version = atomic64_add_return_relaxed(asid_first_version(0),
                                              &mmid_version);

        /* Note currently active MMIDs & mark TLBs as requiring flushes */
        flush_context();

        /* We have more MMIDs than CPUs, so this will always succeed */
        mmid = find_first_zero_bit(mmid_map, num_mmids);

reserve_mmid:
        __set_bit(mmid, mmid_map);
        cur_idx = mmid;
        mmid |= version;
set_context:
        set_cpu_context(0, mm, mmid);
        return mmid;
}

void check_switch_mmu_context(struct mm_struct *mm)
{
        unsigned int cpu = smp_processor_id();
        u64 ctx, old_active_mmid;
        unsigned long flags;

        if (!cpu_has_mmid) {
                check_mmu_context(mm);
                write_c0_entryhi(cpu_asid(cpu, mm));
                goto setup_pgd;
        }

        /*
         * MMID switch fast-path, to avoid acquiring cpu_mmid_lock when it's
         * unnecessary.
         *
         * The memory ordering here is subtle. If our active_mmids is non-zero
         * and the MMID matches the current version, then we update the CPU's
         * asid_cache with a relaxed cmpxchg. Racing with a concurrent rollover
         * means that either:
         *
         * - We get a zero back from the cmpxchg and end up waiting on
         *   cpu_mmid_lock in check_mmu_context(). Taking the lock synchronises
         *   with the rollover and so we are forced to see the updated
         *   generation.
         *
         * - We get a valid MMID back from the cmpxchg, which means the
         *   relaxed xchg in flush_context will treat us as reserved
         *   because atomic RmWs are totally ordered for a given location.
         */
        ctx = cpu_context(cpu, mm);
        old_active_mmid = READ_ONCE(cpu_data[cpu].asid_cache);
        if (!old_active_mmid ||
            !asid_versions_eq(cpu, ctx, atomic64_read(&mmid_version)) ||
            !cmpxchg_relaxed(&cpu_data[cpu].asid_cache, old_active_mmid, ctx)) {
                raw_spin_lock_irqsave(&cpu_mmid_lock, flags);

                ctx = cpu_context(cpu, mm);
                if (!asid_versions_eq(cpu, ctx, atomic64_read(&mmid_version)))
                        ctx = get_new_mmid(mm);

                WRITE_ONCE(cpu_data[cpu].asid_cache, ctx);
                raw_spin_unlock_irqrestore(&cpu_mmid_lock, flags);
        }

        /*
         * Invalidate the local TLB if needed. Note that we must only clear our
         * bit in tlb_flush_pending after this is complete, so that the
         * cpu_has_shared_ftlb_entries case below isn't misled.
         */
        if (cpumask_test_cpu(cpu, &tlb_flush_pending)) {
                if (cpu_has_vtag_icache)
                        flush_icache_all();
                local_flush_tlb_all();
                cpumask_clear_cpu(cpu, &tlb_flush_pending);
        }

        write_c0_memorymapid(ctx & cpu_asid_mask(&boot_cpu_data));

        /*
         * If this CPU shares FTLB entries with its siblings and one or more of
         * those siblings hasn't yet invalidated its TLB following a version
         * increase then we need to invalidate any TLB entries for our MMID
         * that we might otherwise pick up from a sibling.
         *
         * We ifdef on CONFIG_SMP because cpu_sibling_map isn't defined in
         * CONFIG_SMP=n kernels.
         */
#ifdef CONFIG_SMP
        if (cpu_has_shared_ftlb_entries &&
            cpumask_intersects(&tlb_flush_pending, &cpu_sibling_map[cpu])) {
                /* Ensure we operate on the new MMID */
                mtc0_tlbw_hazard();

                /*
                 * Invalidate all TLB entries associated with the new
                 * MMID, and wait for the invalidation to complete.
                 */
                ginvt_mmid();
                sync_ginv();
        }
#endif

setup_pgd:
        TLBMISS_HANDLER_SETUP_PGD(mm->pgd);
}
EXPORT_SYMBOL_GPL(check_switch_mmu_context);

static int mmid_init(void)
{
        if (!cpu_has_mmid)
                return 0;

        /*
         * Expect allocation after rollover to fail if we don't have at least
         * one more MMID than CPUs.
         */
        num_mmids = asid_first_version(0);
        WARN_ON(num_mmids <= num_possible_cpus());

        atomic64_set(&mmid_version, asid_first_version(0));
        mmid_map = bitmap_zalloc(num_mmids, GFP_KERNEL);
        if (!mmid_map)
                panic("Failed to allocate bitmap for %u MMIDs\n", num_mmids);

        /* Reserve an MMID for kmap/wired entries */
        __set_bit(MMID_KERNEL_WIRED, mmid_map);

        pr_info("MMID allocator initialised with %u entries\n", num_mmids);
        return 0;
}
early_initcall(mmid_init);