皆様こんにちは。18のhiraです。
前回ではRISC-Vマルチコアのために必要なハードを書きました。今回はマルチコア上で動くLinuxの作製方法についてです。
(とはいえ動けばいいやのニワカ調べですが…)
Linuxの作製
作製したマルチコア上ではLinux kernelを自分でビルドしました。これには以下のようなツールが必要です。
- Linux kernel: Linux本体のソースコード
- Busybox: Linux上で動くlsやmvなどのコマンドやツールのセット
- riscv-pk: OSとハードの橋渡しをする低レイヤのプログラム
- riscv-gnu-toolchain: RISC-Vコンパイラ
これらを組み合わせて使用しますが、基本的には32bit RISC-V Linuxを作りQEMUで実行するで書いたような手順を行います。
Busyboxとinitramfsのビルド
主な手順は32bit RISC-V Linuxを作りQEMUで実行するの「4. BusyBoxのビルド」と同じです。これを「Build static binary」設定の次のビルドまで行います。
今回はinitramfsの作製に少し変更を加えました。ハードの構成によっては元の記事のままの作製方法で動く可能性もありますし、また別の設定を加える必要があります。
現在のディレクトリは「busybox」ディレクトリにいて、その直下に実行ファイルの「busybox」があるとします。まずは以下のコマンドでinitramfsディレクトリを作成し、ビルドしたbusyboxのツール(_installディレクトリ下)をinitramfsにコピーします。
busybox$ mkdir -p initramfs/{bin,sbin,dev,etc,home,mnt,proc,sys,usr,tmp}
busybox$ cp -a _install/* initramfs
続いてmknodコマンドでスペシャルファイルを作製します。このファイルを経由してUSB等の外部デバイスとの読み書きが行われます。以下のコマンドで2つのファイルを作製します。
busybox$ cd initramfs/dev
dev$ sudo mknod -m 666 null c 1 3
dev$ sudo mknod -m 666 console c 5 1
上のconsoleの方の場合、1. 「c: charecter 」1バイトごとに読み書きする。2. 「Major number: 5」LinuxドライバのTTYドライバ番号。3. 「Minor number 1」TTYドライバの1番目のデバイス。であることを指定しています。
続いてinitスクリプトを作製します。initramfsディレクトリに戻り「init」ファイルを作製します。
dev$ cd ../
initramfs$ vim init
initファイルの中に以下の4行を記述してセーブします。
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
/bin/sh
ブートの最終段階でinitスクリプトが実行されるとprocとsysfsがファイルシステムとしてマウントされます。procは proc/[pid]/status といった構造でpid番のタスクの稼働状態や親プロセス情報を保持します。sysfsはデバイスとのやりとりに使われ、/sys/block ならSSDやCD-ROMといったブロックデバイスの情報を保持します。
最後に、initファイルに権限を与え、initramfs.cpio.gzに固めます。
initramfs$ chmod +x init
initramfs$ find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz
これによりinitramfsディレクトリの隣にinitramfs.cpio.gzが作製されます。次に作成するLinuxカーネルにこのファイルを渡すことで、カーネルのブート字にここで定義したファイル構造が展開されます。
Linuxのビルド
続いてマルチコアRISC-Vシステム上で動作するLinuxの作製をします。Linuxカーネルソースコードは公式のgithubやアーカイブページ下の各バージョンのtar.gzか手に入ります。コードの規模が大きいため、アーカイブから特定のバージョンだけを選んで最小限のサイズで取得すると楽だと思います。
僕の環境ではv5.10.99を使いました。
基本は32bit RISC-V Linuxを作りQEMUで実行するで紹介した通り、コンフィグを指定してriscv-gnu-toolchainでビルドします。今回はデフォルトのコンフィグである「defconfig」に代わり、最小構成である「tinyconfig」を使いました。menuconfigを開いて以下の設定を追加しました。Eが有効でNが無効を意味します。またinitramfs.cpio.gzのディレクトリの登録はファイルの置かれているディレクトリに合わせて変更する必要があります。
– General setup
- Initial RAM filesystem and RAM disk (initramfs/initrd) support: E
- Initramfs source file(s):
/your_dir_to/initramfs/initramfs.cpio.gz
- Compiler optimization level: Optimize for performance
- Configure standard kernel features (expert users)
- Enable support for printk: E
- Configure standard kernel features (expert users) —>
- Enable support for printk: E
- Enable futex support: E
– Platform type
- Base ISA: RV32I
- Emit compressed instructions when building Linux (*先にBoot optionsのUEFI runtime supportをOFFにすると切り替え可能)
-FPU support
- Symmetric Multi-Processing: E
– Boot options —>
- UEFI runtime support: N
– Executable file formats —>
- Kernel support for ELF binaries: E
- Kernel support for scripts starting with #!: E
- Kernel support for MISC binaries: E
– File systems —>
- Pseudo filesystems —>
- /proc file system support: E
- sysfs file system support: E
– Device Drivers
- Character devices —>
- Enable TTY: Y
- RISC-V SBI console support : Y
ここで重要となるのはPlatform typeの「SMP」ですSMPはSymmetric Multi-Processingの略で、これが「マルチコアを使用する」ことを意味します。これをONにすると、コードがマルチコア用でビルドされます。実際に使用するコア数は次に作成するデバイスツリーソースで指定します。マルチコア用でも1コア上で正常に動作しますが、冗長な動作が多くなります。
ビルドが成功すると「vmlinux」というファイルが作製されます。これがマルチコア上で動くLinuxの本体です。
デバイスツリーソース(DTS)の作製
DTSはプロセッサやメモリ、UART、CLINTやその他各種デバイスの情報を管理するスクリプトです。ここに各デバイスの情報を記述することで、Linuxがそれらのデバイスを適切に使うことができます。Linuxのコンフィグにはデバイスドライバ設定が多くありますが、それとも関連しています。
「riscv_multi.dts」のようなdtsファイルを作成し、以下の内容を記述します。
(動きはしましたが、32bitで動かすならcellsを32bit用の1とかに変更し方が良いかもしれません)
/dts-v1/;
/ {
#address-cells = <2>;
#size-cells = <2>;
compatible = "riscv";
chosen {
bootargs = "console=hvc0 root=/dev/hda rw 8250_core.nr_uarts=1 pty.legacy_count=8";
};
cpus {
#address-cells = <1>;
#size-cells = <0>;
timebase-frequency = <20000000>;
cpu@0 {
device_type = "cpu";
reg = <0>;
status = "okay";
compatible = "riscv";
riscv,isa = "rv32imasu";
mmu-type = "riscv,sv32";
clock-frequency = <20000000>;
L10: interrupt-controller {
#interrupt-cells = <1>;
compatible = "riscv,cpu-intc";
interrupt-controller;
};
};
cpu@1 {
device_type = "cpu";
reg = <1>;
status = "okay";
compatible = "riscv";
riscv,isa = "rv32imasu";
mmu-type = "riscv,sv32";
clock-frequency = <20000000>;
L11: interrupt-controller {
#interrupt-cells = <1>;
compatible = "riscv,cpu-intc";
interrupt-controller;
};
};
};
clint@10010000 {
compatible = "riscv,clint0";
interrupts-extended = <&L10 3 &L10 7 &L11 3 &L11 7>;
reg = <0x00000000 0x10010000 0x00000000 0x10000>;
reg-names = "control";
};
memory@80000000 {
device_type = "memory";
reg = <0x00000000 0x80000000 0x00000000 0x40000000>;
};
uart@10000000 {
compatible = "c2rtl,uart";
reg = <0x0 0x10000000 0x0 0x00000020>;
};
interrupt-controller@10020000 {
#interrupt-cells = <1>;
compatible = "lic0";
interrupt-controller;
interrupts-extended = <&L10 11 &L10 9 &L11 11 &L11 9>;
reg = <0x0 0x10020000 0x0 0x00001000>;
riscv,ndev = <32>;
};
};
このdtsファイルでcpuのISAの指定や割り込みコントローラとの接続を定義しています。uartやメモリのアドレス空間はregで指定します。例えばプログラムがprintfとかでUARTにアクセスする際は、このファイルを見て0x10000000のアドレスにストアやロードを行います。これらのアドレスはハードウェアで定義されているものです。
interrupts-extendedはまだよく分かっていないのですが、CLINT関連のものです。3,7,9,11という数字はCSR:mcauseのマシン・ソフトウェア割り込み、マシン・タイマ割り込み、スーパバイザ外部割り込み、マシン外部割り込みの割り込みコードと一致します。コレ関連かもしれません。ちなみに次に使うriscv-pkのコード(machine/fdt.c)のplic_prop()でinterrupts-extendedを取得しています。
riscv-pkによるブートローダ(BBL)の作製
ビルドしたLinuxだけでは動作しません。Linuxという抽象的な存在と、下位レイヤの個々のハードの具体的動作をつなぐためのプログラムが必要です。これを提供するのがriscv-pkで、riscv-pkを用いてLinuxのブートを行うブートローダを作製します。
riscv-pkは公式のgithubから入手できます。riscv-pkを用いて作製できるのがBerkeley Boot Loaderとブートローダです。ただブートローダにはいくつか選択肢があり、近年より多く使われているのはOpen-SBIというもののようです。実際QEMUでRISC-V用Linuxを動かそうとすると、QEMUに内在のOpen-SBIが走ります。
自分の実装ではBBLを使ったため、ここではBBLの作り方を記載します。
まずはBBL作製用のディレクトリ(ここではbbl)を作成し、そこに移動します。その後、以下のコマンドのように作成したvmlinuxとdtsのディレクトリを指定してBBL作製の設定を行います。
bbl$ /your_dir_to/riscv-pk/configure \
--enable-logo --host=riscv32-unknown-linux-gnu \
--with-payload=/your_dir_to/vmlinux \
--with-dts=/you_dir_to/riscv_multi.dts
その後、makeコマンドを実行します。
bbl$ make
makeが成功すれば、「bbl」というファイルが作製されます。このbblをマルチコアプロセッサに読み込ませることでLinuxのブートが開始されます。
ブートに成功すると以下のような表示が出ます。「smp: Bringing up secondary CPUs …」や「smp: Brought up 1 node, 2 CPUs」のような2コア目の起動が見られます。
見にくいですが、CPU0(ブートの主となるCPU)とCPU1(後で起こされるCPU)のブートフローを示したのが次の図になります。主に縦に3つに割られていますが、真ん中が共通して実行されるプロセス、左がCPU0のみのプロセス、右がCPU1のみのプロセスとなっています。各四角が実行される関数名を表し、関数名の下のカッコは左側がそれを呼び出している親となる関数、右側がその関数が記述されているファイル名です。
真ん中のrest_init()から左側に伸びているdo_idle()ですが、おそらくどちらのCPUも最終的にここに到着します。Linuxではschedule()という関数が定期的に走っていて、やるべきタスクがあれば内部のスケジューリング規則(どのプロセッサが暇そうか等)に基づき、各プロセッサにタスクを割り当てます。
実行するタスクがないプロセッサはidleプロセスという待ちぼうけプロセスに入ります。
アプリの実行 (クイックソートアプリ)
ブートだけではなんなので、ブート後にアプリを走らせてみました。アプリは事前にCコードをriscv-gnu-toolchainでRISC-V向けにビルドし、initramfs内のbinディレクトリに生成された実行ファイルを老いておきます。これによりブートプロセスのinitramfs読み込み時にアプリも読み込まれます。
例えば、1000個のランダムな整数をクイックソートするプログラムは以下のようになります。クイックソートは配列の未ソート部分から要素(pivot)を1つ取り出し、「pivotより小さい小配列」と「pivotより大きい小配列」に分けます。この2つの少配列をまたソートするという作業を繰り返します。
これはpthreadを使用してNUM_THREADSでdefineされている個数のスレッドで分担してソートします。Linuxは各プロセッサの使用状況に応じて、配列の未ソート部分を割り当てます。
#define _GNU_SOURCE #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <semaphore.h> #include <unistd.h> #define N 1000 int a[N]; #define NUM_THREADS 2 #include <pthread.h> #include <stdio.h> #include <sched.h> pthread_mutex_t mtx_q; sem_t sem_jobs; pthread_t sort_thread[NUM_THREADS]; int sort_finishd_cnt = 0; int sort_finish = 0; struct SortJob { int l; int r; }; #define JobStackSize 100 struct SortJob job_stack[JobStackSize]; int num_item_job_stack = 0; int read_cpu_id() { int cpu_id = sched_getcpu(); return cpu_id; } void push_job_stack(struct SortJob job) { if (num_item_job_stack < JobStackSize) { job_stack[num_item_job_stack] = job; num_item_job_stack++; } else { printf("job_stack overflow!\n\r"); } } struct SortJob pop_job_stack() { if (num_item_job_stack > 0) { num_item_job_stack--; return job_stack[num_item_job_stack]; } else { printf("job_stack underflow!\n\r"); struct SortJob fault_job = {0, 0}; return fault_job; } } int partition(int* a, int l, int r) { int p = l; r++; if (l < r) { while (l < r) { do { l++; } while ((a[l] <= a[p]) && (l < r)); do { r--; } while (a[r] > a[p]); if (l < r) { int t = a[l]; a[l] = a[r]; a[r] = t; } } } int t = a[r]; a[r] = a[p]; a[p] = t; return r; } void* quick_sort() { printf("quick_sort() tid: %lu\n\r", (unsigned long)pthread_self()); while (1) { sem_wait(&sem_jobs); pthread_mutex_lock(&mtx_q); if (sort_finish > 0) { printf("notified finish tid: %lu, cpu_id: %d\n\r", (unsigned long)pthread_self(), read_cpu_id()); return NULL; } struct SortJob job = pop_job_stack(); pthread_mutex_unlock(&mtx_q); if (job.l < job.r) { int p = partition(a, job.l, job.r); pthread_mutex_lock(&mtx_q); struct SortJob new_job_l = {job.l, p - 1}; push_job_stack(new_job_l); sem_post(&sem_jobs); struct SortJob new_job_r = {p + 1, job.r}; push_job_stack(new_job_r); sem_post(&sem_jobs); pthread_mutex_unlock(&mtx_q); sort_finishd_cnt++; } else if (job.l == job.r) { sort_finishd_cnt++; } pthread_mutex_lock(&mtx_q); pthread_t thread_id = pthread_self(); printf("job_stack %d, sort_finishd_cnt %d, tid: %lu, cpu_id: %d\n\r", num_item_job_stack, sort_finishd_cnt, (unsigned long)thread_id, read_cpu_id()); pthread_mutex_unlock(&mtx_q); if (sort_finishd_cnt == N) { int finish = 0; pthread_mutex_lock(&mtx_q); sort_finish++; finish = sort_finish; for (int i = 0; i < NUM_THREADS - 1; i++) { struct SortJob job_finish = {0, 0}; push_job_stack(job_finish); sem_post(&sem_jobs); } pthread_mutex_unlock(&mtx_q); if (finish == 1) { int error_idx = 0; for (int i = 1; i < N; i++) { if (a[i] < a[i - 1]) { error_idx = i; printf("quick sort Error: a[%d] = %d, a[%d] = %d\n\r", i - 1, a[i - 1], i, a[i]); } } if (error_idx == 0) { printf("quick sort OK\n\r"); for (int i = 0; i < N; i++) { printf(" %2d", a[i]); } printf("\n\r"); } } printf("sort finish tid: %lu\n\r", (unsigned long)pthread_self()); return NULL; } } } int main() { printf("quick sort\n\r"); for (int i = 0; i < N; i++) { a[i] = rand() % N; printf(" %d", a[i]); } printf("\n\r"); pthread_mutex_init(&mtx_q, NULL); sem_init(&sem_jobs, 0, 1); // セマフォ初期値1 struct SortJob init_job = {0, N - 1}; push_job_stack(init_job); for (int thread = 0; thread < NUM_THREADS; thread++) { if (pthread_create(&sort_thread[thread], NULL, quick_sort, NULL) != 0) { perror("pthread_create failed"); return 1; } else { printf("created thread %d\n\r", thread); } } for (int thread = 0; thread < NUM_THREADS; thread++) { if (pthread_join(sort_thread[thread], NULL)) { printf("Failed to join thread\n\r"); } else { printf("joined thread %d\n\r", thread); } } return 0; }
プログラムのビルドには、「-static -lpthread」オプションを付けました。おそらくこれが無いとビルド失敗したり、ライブラリが見つかりませんといったエラーが出てしまいます。
ブート終了時に自動で実行するには、以下のようにinitramfs作成時のinitファイルの最後に実行ファイルを指定すれば処理が始まります。ソートが終了したら通常のコンソール(/bin/sh)が始まります。
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
/bin/quick_sort.out
/bin/sh