Vengineerの戯言

人生は短いけど、長いです。人生を楽しみましょう!

Apple M1でLinuxがnativeに動くようになったのでソースコードを眺めてみた

@Vengineerの戯言 : Twitter
SystemVerilogの世界へようこそすべては、SystemC v0.9公開から始まった 

はじめに、

今日のブログは、今週、Apple M1 (Mac mini)で native な Linux (Ubuntu) が動くようになったという下記のサイトの情報からgithubに公開されたLinuxソースコードを追っていくという作業やっていきます。
www.omgubuntu.co.uk

Apple M1 で native な Linux (Ubuntu)は、どんな感じなのか?

このツイートによると、USB経由で full Ubuntu desktop で立ち上がり、USB dongle でネットワークが動きます。USB、I2C、DART (DARTってなんだろう)が動くようです。

github に公開されている linuxソースコードは、ここにあります。最初に commit されてのは、2021年1月20日っぽいです。
github.com

linuxソースコードを眺めてみて、変更している部分は、

  • CPU の enable_method
  • 割り込みコントローラ (AIC)

がメインです。Apple M1 の中を知るには、dts ファイル:apple-m1-j274.dtsを見ればいいです。

CPU の enable_method

Linux では、CPU の enable_method として、spin_table と psci があります。一般的には sleep をするしないによって分けていて、spin_table が sleep なし、psci が sleep ありです。Android用のKernelでは、sleep ありの psci が基本です。 spin_table を使っているのは、サーバーようです。PSCI に関しては、Slideshareにアップしていますので、興味がある方は見てください、

Apple M1 の場合は、sleep ありですが、psci をサポートしていないので独自の enable_method を 次のように”apple,startcpu" として定義しています。

        cpu@0 {
            device_type = "cpu";
            compatible = "apple,v1";
            reg = <0>;
            enable-method = "apple,startcpu";
        };


"apple,startcpu"は、arch/arm64/kernel/apple_cpustart.c で定義されています。
この中で boot の部分 (cpu_apple_start_boot) が定義されています。

cpu_apple_start_bootのこの部分が Apple M1独自のコードだと思います。この部分の後に、dsb(sy); sev(); で CPU を起こしています。

    writel(1 << cpu, info->pmgr_start);
    if(info->pmgr_start_size >= 12) {
        if(cpu < 4) {
            writel(1 << cpu, info->pmgr_start + 4);
            writel(0, info->pmgr_start + 8);
        } else {
            writel(0, info->pmgr_start + 4);
            writel(1 << (cpu - 4), info->pmgr_start + 8);
        }
    } else
        writel(1 << cpu, info->pmgr_start + 4);

    dsb(sy);
    sev();

ここで使われている info->pmgr_start は、どこに設定しているかというと、dts ファイル:apple-m1-j274.dtsapplestart の部分です。この reg の一部を info->pmgr_start に設定しています。

        applestart: applestart@23b754004 {
            compatible = "apple,startcpu";
            reg = <0x2 0x3b754004 0x0 0xc   0x2 0x10050000 0x0 0x8   0x2 0x10030fb0 0x0 0x4
                   0x2 0x3b754004 0x0 0xc   0x2 0x10150000 0x0 0x8   0x2 0x10130fb0 0x0 0x4
                   0x2 0x3b754004 0x0 0xc   0x2 0x10250000 0x0 0x8   0x2 0x10230fb0 0x0 0x4
                   0x2 0x3b754004 0x0 0xc   0x2 0x10350000 0x0 0x8   0x2 0x10330fb0 0x0 0x4
                   0x2 0x3b754004 0x0 0xc   0x2 0x11050000 0x0 0x8   0x2 0x11030fb0 0x0 0x4
                   0x2 0x3b754004 0x0 0xc   0x2 0x11150000 0x0 0x8   0x2 0x11130fb0 0x0 0x4
                   0x2 0x3b754004 0x0 0xc   0x2 0x11250000 0x0 0x8   0x2 0x11230fb0 0x0 0x4
                   0x2 0x3b754004 0x0 0xc   0x2 0x11350000 0x0 0x8   0x2 0x11330fb0 0x0 0x4>;
        };

info->pmgr_start の他に、info->cputrc_rvbar と info->dbg_unlock も apple_cpustart.c のここで、reg の値から設定しています。

reg は、3個のレジスタ x 8組になっていて、3個のレジスタの内、最初の値が info->pmgr_start、2番目の値が info->cputrc_rvbar、最後の値が info->dbg_unlock の値になります。
info->pmgr_start の設定値は、すべて同じで 0x2 0x3b7540000 0x0 0xc になっています。最初の2個が64ビットのアドレスで 0x2_3b754_0000 になり、残りの2個がサイズになり、0xcなので12バイトです。info->pmgr_start に 0x2_3b74_0000 が、info->pmgr_start_size に 0xc が設定されることになります。of_get_address(node,cpu*3, &info->pmgr_start_size, NULL)の戻り値が0の時は、info->pmgr_start_size に 8 を再設定してます。

    if(!info->pmgr_start) {
        info->pmgr_start = of_iomap(node, cpu * 3);
        if(!info->pmgr_start) {
            pr_err("%s: failed to map start register for CPU %d.\n", __func__, cpu);
            return -EINVAL;
        }
        if(!of_get_address(node, cpu * 3, &info->pmgr_start_size, NULL))
            info->pmgr_start_size = 8;
    }

    if(!info->cputrc_rvbar) {
        info->cputrc_rvbar = of_iomap(node, cpu * 3 + 1);
        if(!info->cputrc_rvbar) {
            pr_err("%s: failed to map reset address register for CPU %d.\n", __func__, cpu);
            return -EINVAL;
        }
    }

    if(!info->dbg_unlock) {
        info->dbg_unlock = of_iomap(node, cpu * 3 + 2);
        if(!info->dbg_unlock) {
            pr_err("%s: failed to map unlock register for CPU %d.\n", __func__, cpu);
            return -EINVAL;
        }
    }

再度、cpu_apple_start_boot に戻ってみます。info->pmgr_start_size が 12(0xc)以上の時は、cpu の値によって、info->pmgr_start +4 と +8 の値に 0 or 1 を書いています。
info->pmgr_start_size が 12未満の時は、info->pmgr_start + 4 に 1を書いています。

    writel(1 << cpu, info->pmgr_start);
    if(info->pmgr_start_size >= 12) {
        if(cpu < 4) {
            writel(1 << cpu, info->pmgr_start + 4);
            writel(0, info->pmgr_start + 8);
        } else {
            writel(0, info->pmgr_start + 4);
            writel(1 << (cpu - 4), info->pmgr_start + 8);
        }
    } else
        writel(1 << cpu, info->pmgr_start + 4);

    dsb(sy);
    sev();

上記の最後で、sev() にて、イベントを発生しています。では、このイベントを待っているところはどこでしょうか?

linux-m1/process.c at master · corellium/linux-m1 · GitHub の _cpu_do_idleです。ops->cpu_wfi が設定されている場合は、ops->cpu_wfi() が呼び出されています。ops->cpu_wfi が設定されていない場合は、wfi() で待っています。

static void noinstr __cpu_do_idle(void)
{
	const struct cpu_operations *ops = get_cpu_ops(task_cpu(current));

	if (ops->cpu_wfi) {
		ops->cpu_wfi();
	} else {
		dsb(sy);
		wfi();
	}
}

ops->cpu_wfi には、linux-m1/apple_cpustart.c at master · corellium/linux-m1 · GitHubにある cpu_apple_wfi が設定されています。cpu_apple_wfi では、wfi() ではなく、wfe() になっています。

static void cpu_apple_wfi(void)
{
    /* can't do a proper WFI, because the CPU tends to lose state; will need
       a proper wrapper sequence */
    dsb(sy);
    wfe();
}

割り込みコントローラ(AIC)

Linux の arm64での割り込みコントローラはARM社のGICを使っていますが、Apple M1では独自の割り込みコントローラ(AIC)を使っているようです。
割り込みコントローラ(AIC)は、デバイスドライバ(irqchip)のディレクトリの irq-apple-aic.c になります。
この中の apple_aic_handle_irq が割り込み処理を行っています。割り込みの種類は、REG_IRQ_ACK_TYPE_NONE, REG_IRQ_ACK_TYPE_IRQ, REG_IRQ_ACK_TYPE_IPIです。REG_IRQ_ACK_TYPE_NONEは特に何もやっていません。REG_IRQ_ACK_TYPE_IRQは普通の割り込み処理として handle_domain_irq にて処理しています。最後の REG_IRQ_ACK_TYPE_IPI が Apple M1 特有の処理になっています。IPIは、Inter-process Interrupt の略だと思います。この割り込みの処理では、割り込みを発生元を if(ack == REG_IRQ_ACK_IPI_SELF) で判断し、1 のときは aic.base + REG_IPI_CLEARレジスタへの書く値(REG_IPI_FLAG_SELF or REG_IPI_FLAGE_OTHER)を変えています。

ここに出てくる REG_IPI_FLAG_SELF と REG_IPI_FLAG_OTHER というのがどうやらポイントだと思います。

static void __exception_irq_entry apple_aic_handle_irq(struct pt_regs *regs)
{
    atomic_t *maskptr;
    uint32_t ack;
    unsigned done = 0, irqnr;
    unsigned long mask;

    while(1) {
        ack = readl(aic.base + REG_IRQ_ACK);
        switch(ack & REG_IRQ_ACK_TYPE_MASK) {
        case REG_IRQ_ACK_TYPE_NONE:
            done = 1;
            break;
        case REG_IRQ_ACK_TYPE_IRQ:
            handle_domain_irq(aic.domain, ack & REG_IRQ_ACK_NUM_MASK, regs);
            break;
        case REG_IRQ_ACK_TYPE_IPI:
#ifdef CONFIG_SMP
            if(ack == REG_IRQ_ACK_IPI_SELF)
                writel(REG_IPI_FLAG_SELF, aic.base + REG_IPI_CLEAR);
            else
                writel(REG_IPI_FLAG_OTHER, aic.base + REG_IPI_CLEAR);
            maskptr = get_cpu_ptr(&aic_ipi_mask);
            smp_mb__before_atomic();
            mask = atomic_xchg(maskptr, 0);
            smp_mb__after_atomic();
            put_cpu_ptr(&aic_ipi_mask);
            for_each_set_bit(irqnr, &mask, NUM_IPI) {
                handle_domain_irq(aic.domain, aic.num_irqs + 2 + irqnr, regs);
            }
            if(ack == REG_IRQ_ACK_IPI_SELF)
                writel(REG_IPI_FLAG_SELF, aic.base + REG_PERCPU(REG_IPI_ENABLE, __smp_processor_id()));
            else
                writel(REG_IPI_FLAG_OTHER, aic.base + REG_PERCPU(REG_IPI_ENABLE, __smp_processor_id()));
#endif
            break;
        }
        if(done)
            break;
    }
}

irq-apple-aic.c の最初の方では、apple_aic_ipi_send_mask では、aic.fast_ipi が設定されていないと、apple_aic_handle_irqと同じように aic.base + REG_IPI_SET に REG_IPI_FLAG_SELF or REG_IPI_FLAG_OTHER を書き込んでいます。

aic.fast_ipi が設定されている場合は、(lcpu >> 2) と (cpu >> 2) の値が同じ時は SR_APPLE_IPI_LOCAL に、違う時は SR_APPLE_IPI_REMOTE に設定しています。
(lcpu >> 2) および (cpu >> 2) で、各値を2ビット、右にシフトしています。これは cpuの番号が 0-3 と 4-7 のものを比較していて、cpuがbigコアのクラスタに属するCPUコア、LITTLEコアのクラスタに属するCPUコアのどちらに属しているのかを調べているんだと思います。同じクラスタに属しているときはSR_APPLE_IPI_LOCALに、違うクラスタに属しているSR_APPLE_IPI_REMOTEに設定しています。

#ifdef CONFIG_SMP
static void apple_aic_ipi_send_mask(struct irq_data *d, const struct cpumask *mask)
{
    int cpu, lcpu;
    int irqnr = d->hwirq - (aic.num_irqs + 2);

    if (WARN_ON(irqnr < 0  || irqnr >= NUM_IPI))
        return;

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

    for_each_cpu(cpu, mask) {
        smp_mb__before_atomic();
        atomic_or(1u << irqnr, per_cpu_ptr(&aic_ipi_mask, cpu));
        smp_mb__after_atomic();
        lcpu = get_cpu();
        if(aic.fast_ipi) {
            if((lcpu >> 2) == (cpu >> 2))
                write_sysreg(cpu & 3, SR_APPLE_IPI_LOCAL);
            else
                write_sysreg((cpu & 3) | ((cpu >> 2) << 16), SR_APPLE_IPI_REMOTE);
        } else
            writel(lcpu == cpu ? REG_IPI_FLAG_SELF : (REG_IPI_FLAG_OTHER << cpu), aic.base + REG_IPI_SET);
        put_cpu();
    }

    /* Force the above writes to be executed */
    if(aic.fast_ipi)
        isb();
}
#else
#define apple_aic_ipi_send_mask NULL
#endi

aic.fast_ipi は、linux-m1/apple-m1-j274.dts at master · corellium/linux-m1 · GitHubで次のようにせってされています。
dtsファイル内の aic に、fast-ipi があるときに 設定されます。

        aic: interrupt-controller@23b100000 {
            compatible = "apple,aic";
            #interrupt-cells = <3>;
            interrupt-controller;
            reg = <0x2 0x3b100000 0x0 0x8000>;
            fast-ipi;
        };

apple_aic_handle_irqの割り込み要因として、FIQがありませんでしたが、linux-m1/irq-apple-aic.c at master · corellium/linux-m1 · GitHub にて、apple_aic_handle_fiq を設定しています。FIQに関しては、下記のようにapple_aic_handle_fiq にて処理されています。

static void __exception_irq_entry apple_aic_handle_fiq(struct pt_regs *regs)
{
#ifdef CONFIG_SMP
    atomic_t *maskptr;
    unsigned long mask;
    unsigned irqnr;

    if(aic.fast_ipi) {
        if(read_sysreg(SR_APPLE_IPI_STAT)) {
            write_sysreg(1, SR_APPLE_IPI_STAT);

            maskptr = get_cpu_ptr(&aic_ipi_mask);
            smp_mb__before_atomic();
            mask = atomic_xchg(maskptr, 0);
            smp_mb__after_atomic();
            put_cpu_ptr(&aic_ipi_mask);
            for_each_set_bit(irqnr, &mask, NUM_IPI)
                handle_domain_irq(aic.domain, aic.num_irqs + 2 + irqnr, regs);
        }
    }
#endif
    handle_domain_irq(aic.domain, aic.num_irqs, regs);
}

おわりに、

Apple M1で native な Linux が動くようになったということで、github に公開された Linux コードの中からポイントとなる次の2点について見てみました。

  • CPU の enable_method
  • 割り込みコントローラ (AIC)

Linux の Page size は通常4KBです。一方、iOS/iPadOS の Page size は16KBです。Apple M1 の Linux の Page size はどうなっているかは、まだ未調査です。

P.S
Page Size、16KBになっているのを確認できました。
https://github.com/corellium/linux-m1/blob/master/drivers/iommu/apple-dart-iommu.c (DART IOMMU on Apple SoCs)のところにありました。
IOMMUの page_bit を ここ で設定してます。

USB、I2C、DART (DARTってなんだろう)が動くようです。<= DART って、このことのようですね。

	if(of_property_read_u32(pdev->dev.of_node, "page-bits", &im->page_bits) < 0)
		im->page_bits = 12;

dts ファイルの"apple,dart-m1"のところ、以下の2か所の page-bits に <14> が設定されています。

        usb_dart0: usb_dart0@382f00000 {
            compatible = "apple,dart-m1";
            clocks = <&atc0_usb>;
            reg = <0x3 0x82f00000 0x0 0x80000   0x3 0x82f80000 0x0 0x4000>;
            interrupts = <0 781 4>;
            page-bits = <14>;
            sid-mask = <11>;
            sid-remap = <0 1>;
            #iommu-cells = <1>;
        };
        usb_dart1: usb_dart1@502f00000 {
            compatible = "apple,dart-m1";
            clocks = <&atc1_usb>;
            reg = <0x5 0x02f00000 0x0 0x80000   0x5 0x02f80000 0x0 0x4000>;
            interrupts = <0 861 4>;
            page-bits = <14>;
            sid-mask = <11>;
            sid-remap = <0 1>;
            #iommu-cells = <1>;
        };

14ビットということは、16KB です。IOMMU の Page size が 16KB ということは、当然、CPU側も 16KB になると思います。
kernel config の時に、ARM64_16K_PAGES を設定することで、Page size を 16KB にすることができると思います。
 

config ARM64_16K_PAGES
	bool "16KB"
	help
	  The system will use 16KB pages support. AArch32 emulation
	  requires applications compiled with 16K (or a multiple of 16K)
	  aligned segments.

System memory と IOMMU の接続は、Intel (左) と Apple Silicon (Apple M1、右)のように違うようです。
f:id:Vengineer:20210124130453p:plain

Apple M1 の Linux の dts ファイルから次のようになっていることが分かりました。

f:id:Vengineer:20210124130351p:plain
IOMMUについて