Interrupts -6- (IPI cross-call)

<kernel v5.4>

IPI (Inter Processor Interrupts)

인터럽트의 특별한 케이스로 SMP 시스템에서 하나의 cpu에서 다른 cpu로 발생시킨다.

시스템에서 처리하는 IPI 타입은 다음과 같다.

1) ARM64

  •  IPI_RESCHEDULE
    • 리스케줄링 IPI
  • IPI_CALL_FUNC
    • call function IPI
  • IPI_CPU_STOP
    • cpu stop IPI
  • IPI_CPU_CRASH_STOP
    • cpu crash stop IPI
  • IPI_TIMER
    • broadcast 타이머 호출 IPI
  • IPI_IRQ_WORK
    • IRQ work IPI
  • IPI_WAKEUP
    • 다른 cpu를 깨우는 IPI

 

2) ARM32

SGI0~7에 해당하는 IPI들은 다음과 같다. SGI8-15는 secure 펌웨어 의해 예약되어 있다.

 


IPI 발생

IPI 함수 초기 설정

각 인터럽트 컨트롤러 초기화 함수에서 IPI 발생시킬 때 사용할 함수를 지정한다.

  • gic v3 드라이버
    • gic_smp_init() 함수에서 gic_raise_softirq() 함수를 지정
  • rpi2 IC 드라이버
    • bcm2709_smp_init_cpus() 함수에서 bcm2835_send_doorbell() 함수를 지정

 

set_smp_cross_call() – ARM64

arch/arm64/kernel/smp.c

void __init set_smp_cross_call(void (*fn)(const struct cpumask *, unsigned int))
{
        __smp_cross_call = fn;
}

IPI 발생시킬 때 사용할 함수를 지정한다.

 

set_smp_cross_call() – ARM32

arch/arm/kernel/smp.c

/*
 * Provide a function to raise an IPI cross call on CPUs in callmap.
 */
void __init set_smp_cross_call(void (*fn)(const struct cpumask *, unsigned int))
{
        if (!__smp_cross_call)
                __smp_cross_call = fn;
}

IPI 발생시킬 때 사용할 함수를 지정한다.

 

IPI 발생 API

smp_cross_call()

arch/arm64/kernel/smp.c & arch/arm/kernel/smp.c

static void smp_cross_call(const struct cpumask *target, unsigned int ipinr)
{
        trace_ipi_raise(target, ipi_types[ipinr]);
        __smp_cross_call(target, ipinr);
}

전역 __smp_cross_call에 등록된 핸들러 함수를 호출하여 @ipinr에 해당하는 IPI를 비트 마스크로 표현된 @target cpu들에 발생시킨다.

 

IPI 발생 – GIC v3용

아래 함수 명에 사용된 softirq는 커널의 softirq subsystem을 뜻하는 것이 아니다. 외부 디바이스가 발생시키는 인터럽트가 아닌 cpu 내부에서 소프트웨어 명령으로 인해 발생되는 인터럽트라는 의미의 아키텍처 용어이다. ARM GIC에서는 이를 SGI(Software Generate Interrupt)로 표현한다.

 

다음 그림은 GIC v3 사용 시 최대 16개의 타겟 cpu들에 IPI(SGI)를 발생시키는 과정을 보여준다.

  • SGI는 0~15번까지 지정하여 인터럽트를 발생시킬 수 있다.

 

gic_raise_softirq()

drivers/irqchip/irq-gic-v3.c

static void gic_raise_softirq(const struct cpumask *mask, unsigned int irq)
{
        int cpu;

        if (WARN_ON(irq >= 16))
                return;

        /*
         * Ensure that stores to Normal memory are visible to the
         * other CPUs before issuing the IPI.
         */
        wmb();

        for_each_cpu(cpu, mask) {
                u64 cluster_id = MPIDR_TO_SGI_CLUSTER_ID(cpu_logical_map(cpu));
                u16 tlist;

                tlist = gic_compute_target_list(&cpu, mask, cluster_id);
                gic_send_sgi(cluster_id, tlist, irq);
        }

        /* Force the above writes to ICC_SGI1R_EL1 to be executed */
        isb();
}

요청 받은 cpu 비트 @mask에 대해  @irq 번호의 IPI를 발생시킨다.

  • 코드 라인 5~6에서 @irq 번호가 16 이상인 경우 처리를 중단하고 함수를 빠져나간다.
    • GIC의 경우 SGI#0 ~ SGI#15까지 발생 가능하다.
  • 코드 라인 12에서 IPI 발생 전에 다른 cpu들에서 저장한 노멀 메모리들의 저장을 확실하게 하는 메모리 베리어를 수행한다.
  • 코드 라인 14~20에서 cpu에 해당하는 mpidr 값에서 클러스터 id만 가져온다.

 

gic_compute_target_list()

drivers/irqchip/irq-gic-v3.c

static u16 gic_compute_target_list(int *base_cpu, const struct cpumask *mask,
                                   unsigned long cluster_id)
{
        int next_cpu, cpu = *base_cpu;
        unsigned long mpidr = cpu_logical_map(cpu);
        u16 tlist = 0;

        while (cpu < nr_cpu_ids) {
                tlist |= 1 << (mpidr & 0xf);

                next_cpu = cpumask_next(cpu, mask);
                if (next_cpu >= nr_cpu_ids)
                        goto out;
                cpu = next_cpu;

                mpidr = cpu_logical_map(cpu);

                if (cluster_id != MPIDR_TO_SGI_CLUSTER_ID(mpidr)) {
                        cpu--;
                        goto out;
                }
        }
out:
        *base_cpu = cpu;
        return tlist;
}

@cluster_id 들에 소속한 @mask에 포함된 cpu들에 대해 @base_cpu 부터 16비트의 비트마스크 값으로 반환한다.

  • 코드 라인 8~9에서 해당 cpu의 mpidr 값의 4비트가 cpu 번호이며 이 값을 반환하기 위해 사용할 tlist의 해당 cpu 비트를 설정한다.
  • 코드 라인 11~14에서 @mask에 설정된 다음 cpu를 알아온다.
  • 코드 라인 16~21에서 알아온 cpu의 클러스터 id가 @cluster_id와 다른 경우 cpu 번호를 그 전 성공한 번호로 감소시킨 후 함수를 빠져나가기 위해 out 레이블로 이동한다.
  • 코드 라인 23~25에서 인자 @base_cpu에 최종 타겟 cpu를 담고, 클러스터 @cluster_id에 포함되는 16비트 cpu 비트마스크를 반환한다.
    • 16비트 값을 반환하는 이유는 SGI가 최대 16개 cpu까지 지원하기 때문이다.

 

예) cluseter_id#0에 0~3번 cpu, cluster_id#1에 4~7번 cpu들이 배치되었을 때

  • base_cpu=2, mask=0x00ff, cluseter_id=0인 경우
    • 출력 인자 base_cpu=3이고, 반환 값=0x000c이다. (cpu2~3에 해당)

 

gic_send_sgi()

drivers/irqchip/irq-gic-v3.c

static void gic_send_sgi(u64 cluster_id, u16 tlist, unsigned int irq)
{
        u64 val;

        val = (MPIDR_TO_SGI_AFFINITY(cluster_id, 3)     |
               MPIDR_TO_SGI_AFFINITY(cluster_id, 2)     |
               irq << ICC_SGI1R_SGI_ID_SHIFT            |
               MPIDR_TO_SGI_AFFINITY(cluster_id, 1)     |
               MPIDR_TO_SGI_RS(cluster_id)              |
               tlist << ICC_SGI1R_TARGET_LIST_SHIFT);

        pr_devel("CPU%d: ICC_SGI1R_EL1 %llx\n", smp_processor_id(), val);
        gic_write_sgi1r(val);
}

@irq 번호에 해당하는 SGI(Software Generated Interrupt)를 @cluster_id의 @tlist cpu들에 발생시킨다.

  • ICC_SGI1R_EL1 레지스터에 @cluster_id로 aff3, aff2, aff1을 지정하고, 타겟 cpu 비트마스크인 @tlist를 대입한다.

 

IPI 발생 – rpi2 IC용

bcm2835_send_doorbell()

arch/arm/mach-bcm2709/bcm2709.c

static void bcm2835_send_doorbell(const struct cpumask *mask, unsigned int irq)
{
        int cpu;
        /*
         * Ensure that stores to Normal memory are visible to the
         * other CPUs before issuing the IPI.
         */
        dsb();

        /* Convert our logical CPU mask into a physical one. */
        for_each_cpu(cpu, mask)
        {
                /* submit softirq */
                writel(1<<irq, __io_address(ARM_LOCAL_MAILBOX0_SET0 + 0x10 * MPIDR_AFFINITY_LEVEL(cpu_logical_map(cpu), 0)));
        }
}

요청 받은 cpu들에 대해 IPI를 발생시킨다.

 


Call Function IPI

IPI(Inter Process Interrupts)를 수신 받아 처리하는 핸들러 함수는 handle_IPI()이다. 이 함수에서 사용하는 여러 IPI 항목 중 다음 2개의 항목은 다른 cpu의 요청에 의해 전달받은 call function data를 통해 이에 등록된 함수를 호출한다. 이러한 기능을 지원하기 위해 call_function_init() 함수를 통해 초기화한다.

  • IPI_CALL_FUNC
    • 요청한 함수들을 동작시킨다.
  • IPI_CALL_FUNC_SINGLE
    • 요청한 함수 하나를 동작시킨다.

 

Call Function 초기화

call_function_init()

kernel/smp.c

void __init call_function_init(void)
{
        int i;

        for_each_possible_cpu(i)
                init_llist_head(&per_cpu(call_single_queue, i));

        smpcfd_prepare_cpu(smp_processor_id());
}

call function에 사용되는 IPI 기능을 초기화한다.

  • 코드 라인 5~6에서 call function이 담기는 call_single_queue를 cpu 수 만큼초기화한다.
  • 코드 라인 8에서 현재 부트 cpu에 대한 call function data를 준비한다.

 

smpcfd_prepare_cpu()

kernel/smp.c

int smpcfd_prepare_cpu(unsigned int cpu)
{
        struct call_function_data *cfd = &per_cpu(cfd_data, cpu);

        if (!zalloc_cpumask_var_node(&cfd->cpumask, GFP_KERNEL,
                                     cpu_to_node(cpu)))
                return -ENOMEM;
        if (!zalloc_cpumask_var_node(&cfd->cpumask_ipi, GFP_KERNEL,
                                     cpu_to_node(cpu))) {
                free_cpumask_var(cfd->cpumask);
                return -ENOMEM;
        }
        cfd->csd = alloc_percpu(call_single_data_t);
        if (!cfd->csd) {
                free_cpumask_var(cfd->cpumask);
                free_cpumask_var(cfd->cpumask_ipi);
                return -ENOMEM;
        }

        return 0;
}

@cpu에 대한 call function data를 할당하고 초기화한다.

  • 코드 라인 5~7에서 cfd->cpumask를 할당한다.
  • 코드 라인 8~12에서 cfd->cpumask_ipi를 할당한다.
  • 코드 라인 13~18에서 per-cpu 멤버인 cfd->csd를 할당한다.
  • 코드 라인 20에서 성공 값 0을 반환한다.

 

function call IPI 발생 – many cpu

smp_call_function_many()

kernel/smp.c

/**
 * smp_call_function_many(): Run a function on a set of other CPUs.
 * @mask: The set of cpus to run on (only runs on online subset).
 * @func: The function to run. This must be fast and non-blocking.
 * @info: An arbitrary pointer to pass to the function.
 * @wait: If true, wait (atomically) until function has completed
 *        on other CPUs.
 *
 * If @wait is true, then returns once @func has returned.
 *
 * You must not call this function with disabled interrupts or from a
 * hardware interrupt handler or from a bottom half handler. Preemption
 * must be disabled when calling this function.
 */
void smp_call_function_many(const struct cpumask *mask,
                            smp_call_func_t func, void *info, bool wait)
{
        struct call_function_data *cfd;
        int cpu, next_cpu, this_cpu = smp_processor_id();

        /*
         * Can deadlock when called with interrupts disabled.
         * We allow cpu's that are not yet online though, as no one else can
         * send smp call function interrupt to this cpu and as such deadlocks
         * can't happen.
         */
        WARN_ON_ONCE(cpu_online(this_cpu) && irqs_disabled()
                     && !oops_in_progress && !early_boot_irqs_disabled);

        /*
         * When @wait we can deadlock when we interrupt between llist_add() and
         * arch_send_call_function_ipi*(); when !@wait we can deadlock due to
         * csd_lock() on because the interrupt context uses the same csd
         * storage.
         */
        WARN_ON_ONCE(!in_task());
        /* Try to fastpath.  So, what's a CPU they want? Ignoring this one. */
        cpu = cpumask_first_and(mask, cpu_online_mask);
        if (cpu == this_cpu)
                cpu = cpumask_next_and(cpu, mask, cpu_online_mask);

        /* No online cpus?  We're done. */
        if (cpu >= nr_cpu_ids)
                return;

        /* Do we have another CPU which isn't us? */
        next_cpu = cpumask_next_and(cpu, mask, cpu_online_mask);
        if (next_cpu == this_cpu)
                next_cpu = cpumask_next_and(next_cpu, mask, cpu_online_mask);

        /* Fastpath: do that cpu by itself. */
        if (next_cpu >= nr_cpu_ids) {
                smp_call_function_single(cpu, func, info, wait);
                return;
        }

        cfd = this_cpu_ptr(&cfd_data);

        cpumask_and(cfd->cpumask, mask, cpu_online_mask);
        cpumask_clear_cpu(this_cpu, cfd->cpumask);

        /* Some callers race with other cpus changing the passed mask */
        if (unlikely(!cpumask_weight(cfd->cpumask)))
                return;

        cpumask_clear(cfd->cpumask_ipi);
        for_each_cpu(cpu, cfd->cpumask) {
                call_single_data_t *csd = per_cpu_ptr(cfd->csd, cpu);

                csd_lock(csd);
                if (wait)
                        csd->flags |= CSD_FLAG_SYNCHRONOUS;
                csd->func = func;
                csd->info = info;
                if (llist_add(&csd->llist, &per_cpu(call_single_queue, cpu)))
                        __cpumask_set_cpu(cpu, cfd->cpumask_ipi);
        }

        /* Send a message to all CPUs in the map */
        arch_send_call_function_ipi_mask(cfd->cpumask_ipi);

        if (wait) {
                for_each_cpu(cpu, cfd->cpumask) {
                        call_single_data_t *csd;

                        csd = per_cpu_ptr(cfd->csd, cpu);
                        csd_lock_wait(csd);
                }
        }
}
EXPORT_SYMBOL(smp_call_function_many);

현재 cpu를 제외한 @mask cpu들에서 @func 함수가 실행되게 한다. @wait 인수를 설정하는 경우 각 cpu에서 함수의 실행이 완료될 때 까지 기다린다.

fastpath: 실행해야 할 타겟 cpu가 1개만 있는 경우

  • 코드 라인 24~30에서 전송할 대상 cpu들 @mask & online cpu들 중 첫 번째 cpu를 알아온다. 단 현재 cpu는 제외한다.
  • 코드 라인 33~35에서 다음 전송할 두 번째 cpu를 알아온다. 단 현재 cpu는 제외한다.
  • 코드 라인 38~41에서 전송 대상이 하나의 cpu인 경우 해당 cpu로 function call IPI를 발생시킨다.

slowpath: 실행해야 할 타겟 cpu가 2개 이상인 경우

  • 코드 라인 43~50에서 cfd_data라는 이름의 전역 per-cpu 객체의 cpumask에 현재 cpu를 제외한 타겟 cpu를 비트마스크 형태로 대입한다. 보낼 타겟 cpu가 하나도 없는 경우 함수를 빠져나간다.
  • 코드 라인 52~63에서 타겟 cpu들을 순회하며 per-cpu 타입인 call_single_data_t 타입 csd에 전송할 정보들을 저장한다.
    • csd_lock() 함수에서는 csd->flags에 lock 비트가 설정되어 있는 동안 sleep하고 설정되어 있지 않으면 lock 비트를 설정한다.
    • 현재 cpu의 call_single_queue의 선두에 추가한다.
  • 코드 라인 66에서 타겟 cpu들에 대해 function call IPI를 발생시킨다.
  • 코드 라인 68~75에서 인자 @wait이 지정된 경우 다른 cpu에서 func이 모두 실행되어 complete될 때까지 기다린다.

 

cfd_data

kernel/smp.c

static DEFINE_PER_CPU_ALIGNED(struct call_function_data, cfd_data);

 

다음 그림은 로컬 cpu를 제외한 타겟 cpu들에 대해 fastpath와 slowpath로 나뉘어 처리되는 과정을 보여준다.

smp_call_function_many-1b

 

smp_call_function_single()

kernel/smp.c

/*
 * smp_call_function_single - Run a function on a specific CPU
 * @func: The function to run. This must be fast and non-blocking.
 * @info: An arbitrary pointer to pass to the function.
 * @wait: If true, wait until function has completed on other CPUs.
 *
 * Returns 0 on success, else a negative status code.
 */
int smp_call_function_single(int cpu, smp_call_func_t func, void *info,
                             int wait)
{
        int this_cpu;
        int err;

        /*
         * prevent preemption and reschedule on another processor,
         * as well as CPU removal
         */
        this_cpu = get_cpu();

        /*
         * Can deadlock when called with interrupts disabled.
         * We allow cpu's that are not yet online though, as no one else can
         * send smp call function interrupt to this cpu and as such deadlocks
         * can't happen.
         */
        WARN_ON_ONCE(cpu_online(this_cpu) && irqs_disabled()
                     && !oops_in_progress);

        /*
         * When @wait we can deadlock when we interrupt between llist_add() and
         * arch_send_call_function_ipi*(); when !@wait we can deadlock due to
         * csd_lock() on because the interrupt context uses the same csd
         * storage.
         */
        WARN_ON_ONCE(!in_task());

        csd = &csd_stack;
        if (!wait) {
                csd = this_cpu_ptr(&csd_data);
                csd_lock(csd);
        }
        err = generic_exec_single(cpu, NULL, func, info, wait);

        if (wait)
                csd_lock_wait(csd);
        put_cpu();

        return err;
}
EXPORT_SYMBOL(smp_call_function_single);

지정한 @cpu에서 @func 함수를 동작시키도록 function call IPI를 발생시킨다. 인자 @wait이 설정된 경우 실행이 완료될 때까지 기다린다.

 

generic_exec_single()

kernel/smp.c

/*
 * Insert a previously allocated call_single_data element
 * for execution on the given CPU. data must already have
 * ->func, ->info, and ->flags set.
 */
static int generic_exec_single(int cpu, struct call_single_data *csd,
                               smp_call_func_t func, void *info, int wait)
{
        if (cpu == smp_processor_id()) {
                unsigned long flags;

                /*
                 * We can unlock early even for the synchronous on-stack case,
                 * since we're doing this from the same CPU..
                 */
                csd_unlock(csd);
                local_irq_save(flags);
                func(info);
                local_irq_restore(flags);
                return 0;
        }

        if ((unsigned)cpu >= nr_cpu_ids || !cpu_online(cpu)) {
                csd_unlock(csd);
                return -ENXIO;
        }

        if (!csd) {
                csd = &csd_stack;
                if (!wait)
                        csd = this_cpu_ptr(&csd_data);
        }

        csd->func = func;
        csd->info = info;

        /*
         * The list addition should be visible before sending the IPI
         * handler locks the list to pull the entry off it because of
         * normal cache coherency rules implied by spinlocks.
         *
         * If IPIs can go out of order to the cache coherency protocol
         * in an architecture, sufficient synchronisation should be added
         * to arch code to make it appear to obey cache coherency WRT
         * locking and barrier primitives. Generic code isn't really
         * equipped to do the right thing...
         */
        if (llist_add(&csd->llist, &per_cpu(call_single_queue, cpu)))
                arch_send_call_function_single_ipi(cpu);

        return 0;
}

지정한 @cpu에서 @func 함수를 동작시키도록 function call IPI를 발생시킨다.

  • 코드 라인 4~16에서 요청 받은 cpu가 현재 cpu인 경우 IPI 발생 없이 그냥 함수를 호출하여 수행한다.
  • 코드 라인 18~21에서 허용 online cpu가 아니면 -ENXIO 에러를 반환한다.
  • 코드 라인 23~27에서 csd가 지정되지 않은 경우 전역 &csd_data를 사용한다.
  • 코드 라인 29~30에서 csd에 호출할 함수와 인수 정보를 대입한다.
  • 코드 라인 43~44에서 csd를 call_single_queue에 추가한 후 IPI를 발생시킨다.
  • 코드 라인 46에서 성공 값 0을 반환한다.

 

csd_data

kernel/smp.c

static DEFINE_PER_CPU_SHARED_ALIGNED(call_single_data_t, csd_data);

 

arch_send_call_function_single_ipi()

arch/arm64/kernel/smp.c & arch/arm/kernel/smp.c

void arch_send_call_function_single_ipi(int cpu)
{
        smp_cross_call(cpumask_of(cpu), IPI_CALL_FUNC);
}

지정된 @cpu에 대해 call function IPI를 발생시킨다.

  • 호출된 cpu에서 call_single_queue에 추가된 함수를 수행한다.

 


IPI 핸들러

아키텍처 및 사용하는 인터럽트 컨트롤러마다 IPI 핸들러를 호출하는 곳이 다르다.

  • arm
    • do_IPI() -> handle_IPI()
  • arm GIC v3
    • gic_handle_irq() -> handle_IPI()

 

handle_IPI() – ARM64

arch/arm64/kernel/smp.c

/*
 * Main handler for inter-processor interrupts
 */
void handle_IPI(int ipinr, struct pt_regs *regs)
{
        unsigned int cpu = smp_processor_id();
        struct pt_regs *old_regs = set_irq_regs(regs);

        if ((unsigned)ipinr < NR_IPI) {
                trace_ipi_entry_rcuidle(ipi_types[ipinr]);
                __inc_irq_stat(cpu, ipi_irqs[ipinr]);
        }

        switch (ipinr) {
        case IPI_RESCHEDULE:
                scheduler_ipi();
                break;

        case IPI_CALL_FUNC:
                irq_enter();
                generic_smp_call_function_interrupt();
                irq_exit();
                break;

        case IPI_CPU_STOP:
                irq_enter();
                local_cpu_stop();
                irq_exit();
                break;

        case IPI_CPU_CRASH_STOP:
                if (IS_ENABLED(CONFIG_KEXEC_CORE)) {
                        irq_enter();
                        ipi_cpu_crash_stop(cpu, regs);

                        unreachable();
                }
                break;

#ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST
        case IPI_TIMER:
                irq_enter();
                tick_receive_broadcast();
                irq_exit();
                break;
#endif

#ifdef CONFIG_IRQ_WORK
        case IPI_IRQ_WORK:
                irq_enter();
                irq_work_run();
                irq_exit();
                break;
#endif

#ifdef CONFIG_ARM64_ACPI_PARKING_PROTOCOL
        case IPI_WAKEUP:
                WARN_ONCE(!acpi_parking_protocol_valid(cpu),
                          "CPU%u: Wake-up IPI outside the ACPI parking protocol\n",
                          cpu);
                break;
#endif

        default:
                pr_crit("CPU%u: Unknown IPI message 0x%x\n", cpu, ipinr);
                break;
        }

        if ((unsigned)ipinr < NR_IPI)
                trace_ipi_exit_rcuidle(ipi_types[ipinr]);
        set_irq_regs(old_regs);
}

@ipinr 번호에 해당하는 IPI를 발생시킨다.

  • 코드 라인 4에서 per-cpu 전역 변수인 __irq_regs에 @regs를 기록하고, 그 전 값은  이 루틴이 끝날때까지 old_regs 임시 변수에 저장한다.
  • 코드 라인 6~9에서 @ipinr가 NR_IPI 범위내에 있는 경우 IPI 시작에 대한 trace 출력을 하고 해당 @ipinr에 해당하는 ipi 카운터를 증가시킨다.
  • 코드 라인 11~14에서 IPI_RESCHEDULE 요청을 받은 경우 현재 프로세서에 대해 리스케쥴한다.
  • 코드 라인 16~20에서 IPI_CALL_FUNC 요청을 받은 경우 미리 등록된 call function 함수들을 호출한다.
  • 코드 라인 22~26에서 IPI_CPU_STOP 요청을 받은 경우 현재  cpu가 부팅 중 또는 동작 중인 경우 “CPU%u: stopping” 메시지 출력 및 스택 덤프를 하고 해당 cpu의 irq, fiq를 모두 정지 시키고 offline 상태로 바꾼 후 정지(spin)한다.
  • 코드 라인 28~35에서 IPI_CPU_CRASH_STOP 요청을 받은 경우 CONFIG_KEXEC_CORE 커널 옵션이 있는 경우에 한해 crash 처리를 한 후 cpu를 offline 상태로 바꾸고 정지(spin)시킨다.
  • 코드 라인 38~42에서 IPI_TIMER 요청을 받은 경우 tick 디바이스에 등록된 브로드 캐스트 이벤트 디바이스의 핸들러 함수를 호출한다.
  • 코드 라인 46~50에서 IPI_IRQ_WORK 요청을 받은 경우 현재의 모든 irq 작업들을 즉시 수행하게 한다.
  • 코드 라인 54~58에서 IPI_WAKEUP 요청을 받은 경우 아무것도 처리하지 않는다.
    • 이미 깨어나서 돌고 있고 아울러 트래킹을 위해 이미 해당 통계 카운터도 증가시켰다.
    • 호출 방법 1: arch_send_wakeup_ipi_mask(cpumask) 함수를 사용하여 cpumask에 해당하는 각 cpu들에 대해 WFI에 의해 잠들어 있는 경우 프로세서를 깨울 수 있다.
    • 호출 방법 2: 빅/리틀 hot plug cpu 시스템에서 gic_raise_softirq() 함수를 사용하여 WFI에 의해 대기하고 있는 cpu들을 깨운다. 이 때 인수로 1대신 0을 사용하여 호출하면 불필요한 printk 메시지(“CPU%u: Unknown IPI message 0x00000001”)를 출력하지 않고 트래킹을 위해 해당 카운터 수도 추적할 수 있다.
      • PM으로 전력 기능까지 콘트롤하는 GIC(Generic Interrupt Controller) #0을 사용하는 방법으로 지정한 cpu를 wakeup 시킨다.
    • 참고: ARM: 7536/1: smp: Formalize an IPI for wakeup
  • 코드 라인 61~64 정의 되지 않은 @ipinr 번호의 IPI를 요청했을 때 경고 메시지를 출력한다.
  • 코드 라인 66~67에서 @ ipinr이 범위내에 있는 경우 IPI 종료에 대한 trace 출력을 한다.
  • 코드 라인 68에서 백업해 두었던 old_regs를 per-cpu 전역 변수인 __irq_regs에 복원한다.

 

handle_IPI() – ARM32

arch/arm/kernel/smp.c

void handle_IPI(int ipinr, struct pt_regs *regs)
{
        unsigned int cpu = smp_processor_id();
        struct pt_regs *old_regs = set_irq_regs(regs);

        if ((unsigned)ipinr < NR_IPI) {
                trace_ipi_entry(ipi_types[ipinr]);
                __inc_irq_stat(cpu, ipi_irqs[ipinr]);
        }

        switch (ipinr) {
        case IPI_WAKEUP:
                break;

#ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST
        case IPI_TIMER:
                irq_enter();
                tick_receive_broadcast();
                irq_exit();
                break;
#endif

        case IPI_RESCHEDULE:
                scheduler_ipi();
                break;

        case IPI_CALL_FUNC:
                irq_enter();
                generic_smp_call_function_interrupt();
                irq_exit();
                break;

        case IPI_CPU_STOP:
                irq_enter();
                ipi_cpu_stop(cpu);
                irq_exit();
                break;

#ifdef CONFIG_IRQ_WORK
        case IPI_IRQ_WORK:
                irq_enter();
                irq_work_run();
                irq_exit();
                break;
#endif

        case IPI_COMPLETION:
                irq_enter();
                ipi_complete(cpu);
                irq_exit();
                break;

        case IPI_CPU_BACKTRACE:
                printk_nmi_enter();
                irq_enter();
                nmi_cpu_backtrace(regs);
                irq_exit();
                printk_nmi_exit();
                break;
        default:
                pr_crit("CPU%u: Unknown IPI message 0x%x\n",
                        cpu, ipinr);
                break;
        }

        if ((unsigned)ipinr < NR_IPI)
                trace_ipi_exit(ipi_types[ipinr]);
        set_irq_regs(old_regs);
}

@ipinr 번호에 해당하는 IPI를 발생시킨다.

  • 코드 라인 4에서 per-cpu 전역 변수인 __irq_regs에 @regs를 기록하고, 그 전 값은  이 루틴이 끝날때까지 old_regs 임시 변수에 저장한다.
  • 코드 라인 6~9에서 @ipinr가 NR_IPI 범위내에 있는 경우 IPI 시작에 대한 trace 출력을 하고 해당 @ipinr에 해당하는 ipi 카운터를 증가시킨다.
  • 코드 라인 11~13에서 IPI_WAKEUP 요청을 받은 경우 아무것도 처리하지 않는다.
    • 이미 깨어나서 돌고 있고 아울러 트래킹을 위해 이미 해당 통계 카운터도 증가시켰다.
    • 호출 방법 1: arch_send_wakeup_ipi_mask(cpumask) 함수를 사용하여 cpumask에 해당하는 각 cpu들에 대해 WFI에 의해 잠들어 있는 경우 프로세서를 깨울 수 있다.
    • 호출 방법 2: 빅/리틀 hot plug cpu 시스템에서 gic_raise_softirq() 함수를 사용하여 WFI에 의해 대기하고 있는 cpu들을 깨운다. 이 때 인수로 1대신 0을 사용하여 호출하면 불필요한 printk 메시지(“CPU%u: Unknown IPI message 0x00000001”)를 출력하지 않고 트래킹을 위해 해당 카운터 수도 추적할 수 있다.
      • PM으로 전력 기능까지 콘트롤하는 GIC(Generic Interrupt Controller) #0을 사용하는 방법으로 지정한 cpu를 wakeup 시킨다.
    • 참고: ARM: 7536/1: smp: Formalize an IPI for wakeup
  • 코드 라인 16~20에서 IPI_TIMER 요청을 받은 경우 tick 디바이스에 등록된 브로드 캐스트 이벤트 디바이스의 핸들러 함수를 호출한다.
  • 코드 라인 23~25에서 IPI_RESCHEDULE 요청을 받은 경우 현재 프로세서에 대해 리스케쥴한다.
  • 코드 라인 27~31에서 IPI_CALL_FUNC 요청을 받은 경우 미리 등록된 call function 함수들을 호출한다.
  • 코드 라인 33~37에서 IPI_CPU_STOP 요청을 받은 경우 현재  cpu가 부팅 중 또는 동작 중인 경우 “CPU%u: stopping” 메시지 출력 및 스택 덤프를 하고 해당 cpu의 irq, fiq를 모두 정지 시키고 offline 상태로 바꾼 후 정지(spin)한다.
  • 코드 라인 40~44에서 IPI_IRQ_WORK 요청을 받은 경우 현재의 모든 irq 작업들을 즉시 수행하게 한다.
  • 코드 라인 47~51에서 IPI_COMPLETION 요청을 받은 경우 register_ipi_completion() 함수로 등록해 놓은 per-cpu cpu_completion 에서 wait_for_completion()등으로 대기하고 있는 스레드를 깨운다.
  • 코드 라인 53~59에서 IPI_CPU_BACKTRACE 요청을 받은 경우 nmi backtrace를 지원하는 cpu들의 backtrace를 출력 한다.
  • 코드 라인 60~64 정의 되지 않은 @ipinr 번호의 IPI를 요청했을 때 경고 메시지를 출력한다.
  • 코드 라인 66~67에서 @ ipinr이 범위내에 있는 경우 IPI 종료에 대한 trace 출력을 한다.
  • 코드 라인 68에서 백업해 두었던 old_regs를 per-cpu 전역 변수인 __irq_regs에 복원한다.

 


IPI_TIMER 처리

tick_receive_broadcast()

kernel/time/tick-broadcast.c

#ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST
int tick_receive_broadcast(void)
{
        struct tick_device *td = this_cpu_ptr(&tick_cpu_device);
        struct clock_event_device *evt = td->evtdev;

        if (!evt)
                return -ENODEV;

        if (!evt->event_handler)
                return -EINVAL;

        evt->event_handler(evt);
        return 0;
}
#endif

tick 디바이스에 등록된 이벤트 디바이스의 핸들러 함수를 호출한다.

  • CONFIG_GENERIC_CLOCKEVENTS_BROADCAST 커널 옵션을 사용하는 경우에만 동작한다.
  • 호출 방법: tick_do_broadcast() 함수를 사용하여 해당 cpu들에 tick을 전달한다.
  • 참고: clockevents: Add generic timer broadcast receiver

 

  • 코드 라인 4~8에서 tick 디바이스에 등록된 clock 이벤트 디바이스를 알아온다. 없으면 -ENODEV 에러를 반환한다.
  • 코드 라인 10~11에서 clock 이벤트 디바이스에 이벤트 핸들러 함수가 등록되어 있지 않으면 -EINVAL 에러를 반환한다.
  • 코드 라인 13에서 등록된 이벤트 핸들러 함수를 호출한다.
  • 코드 라인 14에서 성공 값 0을 반환한다.

 


IPI_RESCHEDULE 처리

scheduler_ipi()

kernel/sched/core.c

void scheduler_ipi(void)
{
        /*            
         * Fold TIF_NEED_RESCHED into the preempt_count; anybody setting
         * TIF_NEED_RESCHED remotely (for the first time) will also send
         * this IPI.
         */
        preempt_fold_need_resched();

        if (llist_empty(&this_rq()->wake_list) && !got_nohz_idle_kick())
                return;

        /*
         * Not all reschedule IPI handlers call irq_enter/irq_exit, since
         * traditionally all their work was done from the interrupt return
         * path. Now that we actually do some work, we need to make sure
         * we do call them.
         *
         * Some archs already do call them, luckily irq_enter/exit nest
         * properly.
         *
         * Arguably we should visit all archs and update all handlers,
         * however a fair share of IPIs are still resched only so this would
         * somewhat pessimize the simple resched case.
         */
        irq_enter();
        sched_ttwu_pending();

        /*
         * Check if someone kicked us for doing the nohz idle load balance.
         */
        if (unlikely(got_nohz_idle_kick())) {
                this_rq()->idle_balance = 1;
                raise_softirq_irqoff(SCHED_SOFTIRQ);
        }
        irq_exit();
}

현재 프로세서에 대해 리스케쥴한다.

  • 호출 방법: smp_send_reschedule() 명령을 사용하여 해당 cpu에서 리스케쥴링하도록 요청한다.

 

  • 코드 라인 8에서 현재 스레드에 리스케쥴 요청이 있는 경우 preempt count에 있는 리스쥴 요청중 비트를 제거한다.
  • 코드 라인 10~11애서 런큐의 wake_list가 비어 있으면서 현재 cpu의 런큐에 NOHZ_BALANCE_KICK 요청이 없거나, 현재 cpu가 idle 상태가 아니거나 리스케쥴 요청이 있는 경우 리스케쥴할 태스크가 없어서 함수를 빠져나간다.
  • 코드 라인 26에서 hard irq preecmption 카운터를 증가시켜 preemption을 막고 irq 소요 시간 및 latency를 측정할 수 있도록 한다.
  • 코드 라인 27에서 런큐의 wake_list에서 모두 꺼내서 다시 enqueue 하여 리스케쥴 한다.
  • 코드 라인 32~35에서 낮은 확률로 현재 cpu의 런큐에 NOHZ_BALANCE_KICK 요청이 있고 현재 cpu가 idle 상태이면서 리스케쥴 요청이 없는 경우 런큐의 idle_balance 를 1로 설정하고 SCHED_SOFTIRQ 를 정지시킨다.
  • 코드 라인 36에서  hard irq preecmption 카운터를 감소시켜 preemption을 다시 열어주고  irq 소요 시간 및 latency를 측정할 수 있도록 후처리 작업을 수행한다.

 


IPI_CALL_FUNC 처리

generic_smp_call_function_interrupt()

include/linux/smp.h

#define generic_smp_call_function_interrupt \
        generic_smp_call_function_single_interrupt

IPI에 의해 인터럽트 된 후 미리 등록된 함수를 호출한다.

  • 호출 방법: arch_send_call_function_ipi_mask() 명령을 사용하여 해당 cpu들에서 미리 등록된 함수들을 호출한다.

 

generic_smp_call_function_single_interrupt()

kernel/smp.c

/**
 * generic_smp_call_function_single_interrupt - Execute SMP IPI callbacks
 *
 * Invoked by arch to handle an IPI for call function single.
 * Must be called with interrupts disabled.
 */
void generic_smp_call_function_single_interrupt(void)
{
        flush_smp_call_function_queue(true);
}

IPI에 의해 인터럽트 된 후 미리 등록된 함수를 호출한다.

  • 호출 방법: arch_send_call_function_single_ipi() 명령을 사용하여 요청 cpu에서 미리 등록된 함수를 호출한다.

 

flush_smp_call_function_queue()

kernel/smp.c

/**
 * flush_smp_call_function_queue - Flush pending smp-call-function callbacks
 *
 * @warn_cpu_offline: If set to 'true', warn if callbacks were queued on an
 *                    offline CPU. Skip this check if set to 'false'.
 *
 * Flush any pending smp-call-function callbacks queued on this CPU. This is
 * invoked by the generic IPI handler, as well as by a CPU about to go offline,
 * to ensure that all pending IPI callbacks are run before it goes completely
 * offline.
 *
 * Loop through the call_single_queue and run all the queued callbacks.
 * Must be called with interrupts disabled.
 */
static void flush_smp_call_function_queue(bool warn_cpu_offline)
{
        struct llist_head *head;
        struct llist_node *entry;
        call_single_data_t *csd, *csd_next;
        static bool warned;

        lockdep_assert_irqs_disabled();

        head = this_cpu_ptr(&call_single_queue);
        entry = llist_del_all(head);
        entry = llist_reverse_order(entry);

        /* There shouldn't be any pending callbacks on an offline CPU. */
        if (unlikely(warn_cpu_offline && !cpu_online(smp_processor_id()) &&
                     !warned && !llist_empty(head))) {
                warned = true; 
                WARN(1, "IPI on offline CPU %d\n", smp_processor_id());

                /*
                 * We don't have to use the _safe() variant here
                 * because we are not invoking the IPI handlers yet.
                 */
                llist_for_each_entry(csd, entry, llist)
                        pr_warn("IPI callback %pS sent to offline CPU\n",
                                csd->func);
        }

        llist_for_each_entry_safe(csd, csd_next, entry, llist) {
                csd->func(csd->info);
                csd_unlock(csd);
        }

        /*
         * Handle irq works queued remotely by irq_work_queue_on().
         * Smp functions above are typically synchronous so they
         * better run first since some other CPUs may be busy waiting
         * for them.
         */
        irq_work_run();
}

call_single_queue에 있는 모든 call function들을 한꺼번에 처리하고 비운다. 또한 남은 irq work도 모두 처리하여 비운다.

  • 코드 라인 10~12에서 per-cpu call_single_queue에 등록된 엔트리들을 모두 제거하고 entry로 가져오는데 가장 처음에 추가한 call_single_data 엔트리가 앞으로 가도록 순서를 거꾸로 바꾼다.
  • 코드 라인 15~27에서 인수 warn_cpu_offline가 설정된 경우 현재 cpu가 offline된 cpu인 경우 한 번만 “IPI on offline CPU %d” 및 “IPI callback %pS sent to offline CPU”라는 경고 메시지를 출력하게 한다.
  • 코드 라인 29~32에서 등록되어 있는 함수들을 모두 호출하여 수행한다.
  • 코드 라인 40에서 현재의 모든 irq 작업들을 즉시 수행하게 한다.

 


IPI_CPU_STOP 처리

local_cpu_stop() – ARM64

arch/arm64/kernel/smp.c

static void local_cpu_stop(void)
{
        set_cpu_online(smp_processor_id(), false);

        local_daif_mask();
        sdei_mask_local_cpu();
        cpu_park_loop();
}

로컬 cpu를 파킹(stop)한다.

  • 코드 라인 3에서 로컬 cpu를 offline 상태로 변경한다.
  • 코드 라인 5에서 로컬 cpu에 Exception들이 진입하지 않도록 모두 마스크한다.
  • 코드 라인 6에서 SDEI(Software Delegated Exception Interface)를 통해 로컬 cpu로의 인터럽트를 mask한다.
  • 코드 라인 7에서 cpu를 파킹한다.

 

ipi_cpu_stop() – ARM32

arch/arm/kernel/smp.c

/*
 * ipi_cpu_stop - handle IPI from smp_send_stop()
 */
static void ipi_cpu_stop(unsigned int cpu)
{
        if (system_state == SYSTEM_BOOTING ||
            system_state == SYSTEM_RUNNING) {
                raw_spin_lock(&stop_lock);
                pr_crit("CPU%u: stopping\n", cpu);
                dump_stack();
                raw_spin_unlock(&stop_lock);
        }

        set_cpu_online(cpu, false);

        local_fiq_disable();
        local_irq_disable();

        while (1)
                cpu_relax();
}

현재  cpu가 부팅 중 또는 동작 중인 경우 “CPU%u: stopping” 메시지 출력 및 스택 덤프를 하고 해당 cpu의 irq, fiq를 모두 정지 시키고 offline 상태로 바꾼 후 정지(spin)한다.

  • 호출 방법: smp_send_stop() 함수를 사용하여 현재 cpu를 제외한 online cpu를 stop 시킨다.

 


IPI_IRQ_WORKIRQ 처리

irq_work_run()

kernel/irq_work.c

/*
 * hotplug calls this through:
 *  hotplug_cfd() -> flush_smp_call_function_queue()
 */
void irq_work_run(void)
{
        irq_work_run_list(this_cpu_ptr(&raised_list));
        irq_work_run_list(this_cpu_ptr(&lazy_list));
}       
EXPORT_SYMBOL_GPL(irq_work_run);

현재의 모든 irq 작업들을 즉시 수행하게 한다.

  • CONFIG_IRQ_WORK 커널 옵션을 사용하는 경우에만 동작한다.
  • 호출 방법: arch_irq_work_raise() 요청을 받은 경우 현재 자신의 cpu에서 모든 irq 작업들을 즉시 수행하게 한다.
  • 참고: ARM: 7872/1: Support arch_irq_work_raise() via self IPIs

 

  • 코드 라인 3에서 &raised_list에 있는 모든 irq 작업들을 수행하게 한다.
  • 코드 라인 4에서 &lazy_list에 있는 모든 irq 작업들을 수행하게 한다.

 


IPI_COMPLETION 처리- ARM32

ipi_complete() – ARM32

arch/arm/kernel/smp.c

static void ipi_complete(unsigned int cpu)
{
        complete(per_cpu(cpu_completion, cpu));
}

register_ipi_completion() 함수로 등록해 놓은 per-cpu cpu_completion 에서 wait_for_completion()등으로 대기하고 있는 스레드를 깨운다.

 


IPI_CPU_BACKTRACE 처리 – ARM32

nmi_cpu_backtrace()

lib/nmi_backtrace.c

bool nmi_cpu_backtrace(struct pt_regs *regs)
{
        int cpu = smp_processor_id();

        if (cpumask_test_cpu(cpu, to_cpumask(backtrace_mask))) {
                if (regs && cpu_in_idle(instruction_pointer(regs))) {
                        pr_warn("NMI backtrace for cpu %d skipped: idling at %pS\n",
                                cpu, (void *)instruction_pointer(regs));
                } else {
                        pr_warn("NMI backtrace for cpu %d\n", cpu);
                        if (regs)
                                show_regs(regs);
                        else
                                dump_stack();
                }
                cpumask_clear_cpu(cpu, to_cpumask(backtrace_mask));
                return true;
        }

        return false;
}
NOKPROBE_SYMBOL(nmi_cpu_backtrace);

현재 cpu에 대한 레지스터 및 backtrace 로그를 출력한다.

 


구조체

call_function_data 구조체

kernel/smp.c

struct call_function_data {
        call_single_data_t      __percpu *csd;
        cpumask_var_t           cpumask;
        cpumask_var_t           cpumask_ipi;
};

call function IPI 호출 시 사용할 cfd로 function이 포함된 csd와 타겟 cpu들을 지정한 cpumask들이 담겨있다.

 

call_single_data_t 타입

include/linux/smp.h

/* Use __aligned() to avoid to use 2 cache lines for 1 csd */
typedef struct __call_single_data call_single_data_t
        __aligned(sizeof(struct __call_single_data));

 

include/linux/smp.h

struct __call_single_data {
        struct llist_node llist;
        smp_call_func_t func;
        void *info;
        unsigned int flags;
};

call function IPI 호출 시 사용할 csd로 function 정보가 담겨있다.

  •  llist
    • csd 리스트
  • func
    • 수행할 함수
  • *info
    • 함수에 인수로 전달할 정보
  • flags
    • 아래 플래그 비트를 저장한다.

 

kernel/smp.c

enum {
        CSD_FLAG_LOCK           = 0x01,
        CSD_FLAG_WAIT           = 0x02,
};
  • SMP 시스템에서 call single data를 위한 cpu의 처리 상태

 

참고

 

 

댓글 남기기