カテゴリー別アーカイブ: Linux

[Linux] kernel version string

Linux kernel の version 表記がどのようにして生成されているのかよく分からなかったので調べてみました。

kernel version の文字列

kernel version は major version, patchlevel, sublevel (開発バージョンはさらに extraversion) から成り、それぞれ linux/Makefile に記載されています。”make kernelversion” でそれらを表示します。

$ head -5 Makefile
VERSION = 4
PATCHLEVEL = 13
SUBLEVEL = 0
EXTRAVERSION = -rc7
NAME = Fearless Coyote
$ make kernelversion
4.13.0-rc7

kernel release version の文字列はどうやって決まる?

release version は kernel version に加えてビルド時のリポジトリ情報を追加したもので、 “make releaseversion” で表示します。config 情報を使うので、先に config を実行しておく必要があります。

$ make ARCH=arm64 defconfig
$ make ARCH=arm64 kernelrelease
4.13.0-rc7-01000-gXXXXXXX-dirty

これは linux/scripts/setlocalversion の実行結果で、config の内容とリポジトリの情報を反映しています。影響する config は CONFIG_LOCALVERSION_AUTO と CONFIG_LOCALVERSION です。

CONFIG_LOCALVERSION_AUTO によって大きく動作が異なるため、それぞれについて説明します。”make menuconfig” では以下で設定します。

"General setup   --->
  [*] Automatically append version information to the version string
CONFIG_LOCALVERSION_AUTO=y の場合

次の文字列を連結したものが localversion 文字列になります。

  • linux/localversion* のファイルがあればその文字列
  • CONFIG_LOCALVERSION で定義された文字列
  • 環境変数 LOCALVERSION で定義された文字列
  • リポジトリ情報
  • ローカル変更があった場合の “-dirty”

linux/localversion* ファイルは自分で作成したものです。ファイル名は linux/localversion-hoge でも何でもよいですが、ワイルドカードの評価順に文字列が続きます。

$ cat localversion
-localv1.0
$ cat localversion-foo
-FOO
$ cat localversion-bar
-BAR
$ cat localversion-hoge
-HOGE
$ make ARCH=arm64 kernelrelease
4.13.0-rc7-localv1.0-BAR-FOO-HOGE-01000-gXXXXXXX-dirty

CONFIG_LOCALVERSION は .config で定義される文字列です。”make menuconfig” では以下で設定します。

"General setup   --->
  () Local version - append to kernel release

環境変数 LOCALVERSION は make 実行時に定義されていたものが有効になります。

$ LOCALVERSION=-TMP make ARCH=arm64 kernelrelease
4.13.0-rc7-TMP-01000-gXXXXXXX-dirty

リポジトリ情報は、”git describe –dirty” の結果が適用されます。

annotated tag の付いた commit (通常 kernel version の更新ポイント) から commit の適用数(5桁)と、現在の commit ID が文字列となります。

“-01000-gXXXXXXX” の場合、kernel version から 1000 個の commit が適用され、現在 commit ID が XXXXXXX のリポジトリを使っていることになります。

ソースは変更されているがリポジトリに反映されていない場合、最後に “-dirty” が付きます。

これらを全部適用すると以下のようになります(長い…)。

$ grep LOCALVERSION .config
CONFIG_LOCALVERSION="-TEST"
CONFIG_LOCALVERSION_AUTO=y
$ cat localversion
-localv1.0
$ cat localversion-foo
-FOO
$ cat localversion-bar
-BAR
$ cat localversion-hoge
-HOGE
$ LOCALVERSION=-TMP make ARCH=arm64 kernelrelease
4.13.0-rc7-localv1.0-BAR-FOO-HOGE-TEST-TMP-01000-gXXXXXXX-dirty

linux/scripts/setlocalversion を都度適用する代わりに、予め linux/scripts/setlocalversion  の結果を保存した文字列を適用することもできます。
以下を実行すると linux/.scmversion に保存され、この文字列が常に適用されます。

$ ./script/setlocalversion --save-scmversion .
CONFIG_LOCALVERSION_AUTO is not set の場合

次の文字列を連結したものが localversion 文字列になります。上の3つは “CONFIG_LOCALVERSION=y” と同じです。

  • linux/localversion* のファイルがあればその文字列
  • CONFIG_LOCALVERSION で定義された文字列
  • 環境変数 LOCALVERSION で定義された文字列
  • (LOCALVERSION がない場合) リポジトリ更新を示す “+”

環境変数 LOCALVERSION が定義されていなければ、リポジトリが kernel version から更新されているかを示す “+” が付きます。

環境変数 LOCALVERSION が定義されていればその文字列だけが付きます。LOCALVERSION を空定義すると何も付かないことになります。

$ make ARCH=arm64 kernelrelease
4.13.0-rc7+
$ LOCALVERSION=-TMP make ARCH=arm64 kernelrelease
4.13.0-rc7-TMP
$ LOCALVERSION= make ARCH=arm64 kernelrelease
4.13.0-rc7

release version と kernel module

kernel module は install directory に release version が付く(/lib/modules/<release-version>) ため、気づかないうちに version 文字列が変わっててトラブルの原因になることがあります。そのため、module 開発者はどの文字列が kernel の何の設定で付与されているのかを正しく理解しておく必要があります。

[RPi][Linux] boot upstream Linux from U-boot on Raspberry Pi 3

U-boot を Raspberry Pi 3 に導入して、ビルドした mainline Linux kernel を起動してみました。Raspberry Pi は 既存の bootloader が提供されているので、SDカードの FATパーティションに kernel image を置けば起動するのですが、ビルドした kernel を直接投入するには network 経由で起動したくなるものです。

clone & build

U-boot を clone してビルドします。

$ git clone git://git.denx.de/u-boot.git
$ cd u-boot
$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- CONFIG_EFI=y rpi_3_defconfig
$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-
$ cd -

Linux kernel を clone してビルドします。

$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/qemu.org/torvalds/linux.git
$ cd linux
$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig
$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- Image dtbs
$ cd -

tftp サーバを用意します。手順は省略しますが、ここではサーバディレクトリを /tftpboot とします。kernel image と devicetree を tftp サーバへコピーします。

$ cp linux/arch/arm64/boot/Image /tftpboot/
$ cp linux/arch/arm64/boot/dts/broadcom/bcm2837-rpi-3-b.dtb /tftpboot/

ここでは Linaro が提供する rootfs image を取得して tftp サーバへコピーします。取得する image は initramfs の uboot で読める形式で、armv8 のものを選択します。

$ wget http://releases.linaro.org/openembedded/images/minimal-initramfs-armv8/latest/linaro-image-minimal-initramfs-genericarmv8-20170127-888.rootfs.cpio.gz.u-boot
$ cp linaro-image-minimal-initramfs-genericarmv8-20170127-888.rootfs.cpio.gz.u-boot /tftpboot/rootfs.cpio.gz.u-boot

SD カードを host PC に接続し、パーティションを作成して FAT でフォーマットしマウントします。フォーマットまでは Windows で用意してもよいです。ここでは SD カードは /dev/sdX で認識されたものとしますが、dmesg などで確認して間違えないように注意してください。

※ パーティション分割やフォーマットについての詳細は他の解説にお任せします。情報は Web で探すと出てくると思います…。

$ sudo fdisk /dev/sdX
...
(パーティションを作成)
...
$ sudo mkfs.vfat -v -c -F 32 /dev/sdX1
$ sudo mount -t vfat /dev/sdX1 /mnt

github で提供されている firmware を clone して SD カードにコピーします。

$ git clone git://github.com/raspberrypi/firmware
$ cp -a firmware/boot/* /mnt/

u-boot image をSDカードにコピーし、boot 設定ファイルを作成します。

  • arm_control=0x200 を指定すると arm64 モードで起動するようになります。
  • enable_uart=1 で GPIO 端子上の Serial を有効にします。Linux では ttyS0 として見えます。
  • kernel=u-boot.bin は起動する image のファイル名を指定します。default は(arm64モードの場合) kernel8.img です。

詳しくは RPiconfig にいろいろ解説があります。

$ cp u-boot.bin /mnt/
$ cat > config.txt <<EOF
arm_control=0x200
enable_uart=1
kernel=u-boot.bin
EOF

マウントを解除して SDカードを PC から外し、Raspberry Pi 3 本体に挿入します。

$ sync
$ sudo umount /mnt

console は Serial を使うため、GPIO 端子から 接続する必要があります。今回は手持ちの FTDI chip 搭載の TTL-232r-3v3 を使用しました。Raspberry Pi 初代でも使っていた USB-Serial 変換ケーブルです。直接接続できないので、GPIO 端子を 40pin リボンケーブル(余っていた IDE ケーブル!) を介しています。

※ Raspberry Pi 3 の接続端子の詳細は他の解説にお任せします。情報は Web で探すと出てくると思います…。

また、Ethernet ケーブルで host PC との接続も行っておきます。当たり前ですが、ストレートケーブルは直結できないのでローカル LAN などに接続することになりますので、ローカル LAN の IP アドレス情報は知っておく必要があります。

電源を入れると serial に U-boot のログが出ます。一旦キーを押して止めます。

U-Boot 2017.07-00001-g13e3ef2 (Aug 13 2017 - 23:46:40 +0900)

DRAM:  944 MiB
RPI 3 Model B (0xa32082)
MMC:   sdhci@7e300000: 0
reading uboot.env
In:    serial
Out:   vidconsole
Err:   vidconsole
Net:   No ethernet found.
starting USB...
USB0:   Core Release: 2.80a
scanning bus 0 for devices... 3 USB Device(s) found
     scanning usb for storage devices... 0 Storage Device(s) found
Hit any key to stop autoboot:  0
U-Boot> 

ネットワークの設定を行います。ipaddr に target(Raspberry Pi 3) の IP アドレス、serverip に tftp サーバを立ち上げた host PC の IP アドレスを指定します。以下は手元で行った例ですので、自分の環境に合わせて設定します。

U-boot> setenv ipaddr 192.168.1.200            # target(RPi3) の IP アドレス
U-boot> setenv serverip 192.168.1.135          # tftp サーバの IP アドレス
U-boot> setenv netmask 255.255.255.0
U-boot> setenv bootargs root=/dev/ram0 rw console=ttyS0,115200

tftp で kernel image、devicetree、rootfs image をメモリへ転送し、kernel を起動します。

U-Boot> tftp 0x80000 Image
U-Boot> tftp 0x08000000 bcm2837-rpi-3-b.dtb
U-Boot> tftp 0x08008000 rootfs.cpio.gz.u-boot
U-Boot> booti 0x80000 0x08008000 0x08000000

以下のように起動が確認できました。

Starting kernel ...

[    0.000000] Booting Linux on physical CPU 0x0
[    0.000000] Linux version 4.13.0-rc4 (kunih@ubuntu) (gcc version 6.3.1 20170404 (Linaro GCC 6.3-2017.05)) #1 SMP PREEMPT Fri Aug 11 11:06:59 JST 2017
[    0.000000] Boot CPU: AArch64 Processor [410fd034]
[    0.000000] Machine model: Raspberry Pi 3 Model B
...
[    2.229403] Freeing unused kernel memory: 1088K
linaro-test [rc=0]# 

Raspberry Pi の Ethernet Host は USB で接続されているようですが、現時点での kernel では USB(dwc2) の DMA に問題がありエラーで認識しませんでした。contribute が少ないためか device の mainline への対応はまだまだのようです。

参考

[QEMU][Linux] boot upstream Linux from U-boot on QEMU

U-boot on arm QEMU にて QEMU で U-boot を立ち上げて 外から tftp でメモリに転送しました。次にビルドした Linux kernel を転送して起動してみます。

Linux mainline の repository から clone してビルドします。

$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/qemu.org/torvalds/linux.git
$ cd linux
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- multi_v7_defconfig
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage dtbs

ビルドでできたイメージファイルを tftp サーバのディレクトリ (/tftpboot) にコピーします。

$ cp arch/arm/boot/zImage /tftpboot/
$ cp arch/arm/boot/vexpress-v2p-ca9.dtb /tftpboot/

root ファイルシステムを用意します。busybox で作るのが楽ですが、ここでは Linaro が提供している最小構成イメージを使うことにします。サイトから U-boot で読める形式のものをダウンロードします(時期によりファイル名が変更されるので先に web で確認してください)。

$ wget http://releases.linaro.org/openembedded/images/minimal-initramfs-armv7a/latest/linaro-image-minimal-initramfs-genericarmv7a-20170127-460.rootfs.cpio.gz.u-boot
$ cp linaro-image-minimal-initramfs-genericarmv7a-20170127-460.rootfs.cpio.gz.u-boot /tftpboot/rootfs.cpio.gz.u-boot

U-boot on arm QEMU  の手順で U-boot を起動します。起動したら、ネットワークの設定を行います。

=> setenv ipaddr 192.168.1.200     # target(QEMU) の IP アドレス
=> setenv serverip 192.168.1.1     # tftp サーバの IP アドレス
=> setenv netmask 255.255.255.0

bootargs に Linux の command line を設定します。

=> setenv bootargs root=/dev/ram0 rw console=ttyAMA0

tftp で先ほどサーバに置いたファイルの転送を行います。vexpress-a9 は 0x60000000 から SDRAM が実装されています。しかし先頭は kernel の実際の配置先に使用されるため、少し離して配置します。devicetree と rootfs も衝突しない場所に配置します。

=> tftp 0x62000000 zImage
=> tftp 0x63000000 vexpress-v2p-ca9.dtb
=> tftp 0x63008000 rootfs.cpio.gz.u-boot

転送した場所を指定して kernel を起動します。今回は圧縮イメージを使用しているので bootz を使用します。引数は「zImage の配置先」、「rootfs.cpio.gz.u-boot の配置先」、「vexpress-v2p-ca9.dtb の配置先」の順で指定します。

=> bootz 0x62000000 0x63008000 0x63000000
Kernel image @ 0x62000000 [ 0x000000 - 0x774fd8 ]
## Loading init Ramdisk from Legacy Image at 63008000 ...
Image Name:   linaro-image-minimal-initramfs-g
Image Type:   ARM Linux RAMDisk Image (gzip compressed)
Data Size:    8170953 Bytes = 7.8 MiB
Load Address: 00000000
Entry Point:  00000000
Verifying Checksum ... OK
## Flattened Device Tree blob at 63000000
Booting using the fdt blob at 0x63000000
Loading Ramdisk to 67710000, end 67edadc9 ... OK
Loading Device Tree to 67709000, end 6770f963 ... OK

Starting kernel ...

[    0.000000] Booting Linux on physical CPU 0x0
[    0.000000] Linux version 4.13.0-rc4 (kunih@ubuntu) (gcc version 6.3.1 20170404 (Linaro GCC 6.3-2017.05)) #1 SMP Fri Aug 11 10:47:38 JST 2017
[    0.000000] CPU: ARMv7 Processor [410fc090] revision 0 (ARMv7), cr=10c5387d
...
[    3.598945] Freeing unused kernel memory: 2048K
linaro-test [rc=0]#

無事 shell まで起動できました。

[RPi] Raspberry Pi 3

以前 Raspberri Pi を15か月も待って購入したのですが、なかなかいじる時間がなく、他にも放置してるガジェットがあるので Raspberri Pi 3はいいかなと思ってたのですが、手元に最新環境が扱えるプラットフォームがないのもいまいちなので、遅ればせながら共立電子で購入しました。AC アダプタは定格以上のものを用意しないと不安定になるとのことで、お店推奨の 5V/4A の変換コネクタつきを購入しています。

内蔵のeMMC に書き込みが必要なボードが多い中、micro SDカードで直接立ち上がるのはいろいろ便利です。

続きを読む

[Linux] User I/O

今更ながら User I/O (UIO) を使ってみました.UIO は 2.6.23-rc1 で kernel に merge された機能で,ドライバモジュールを書くことなくハードウェア資源を userland からアクセスできる手段を提供します.

I/O にアクセスするだけなら /dev/mem を mmap すれば実現できますが,アドレスが制限されないため安全性が担保できません.UIO では予め使用する領域を定義するとともに,割込み応答もできるようになります.

UIO を利用可能にするには,下記の1行目の CONFIG を有効にします.さらに 2行目の CONFIG を有効にすることで,platform driver として扱うことができ,device tree で I/O 領域や割込みが定義できるようになります(もちろん CONFIG_OF=y が必要).CONFIG_UIO_PDRV_GENIRQ を ”m” にしているのは,ユーザが I/O を使い始めるタイミングを userland から制御できるようにするためです.

CONFIG_UIO=y
CONFIG_UIO_PDRV_GENIRQ=m

device tree には次のようなノードを追加します.reg には使用したい I/O アドレスを page 単位で指定します.これは page 単位(default は 4KB)である必要があります.この領域は例では2つ定義していますが,ノードごとに 5 つまで指定できます.interrupts はノードに対して割込み番号を 1つだけ指定できます.
※ interrupts の数値の意味はアーキテクチャにより異なります.ARM GIC では 0 は SPI, 100 は物理割込み番号の32を引いた値,4はレベル割込みを表します.

/{
        uio {
                compatible = "generic-uio", "myuio";
                reg = <0x40000000 0x1000>,<0x41000000 0x1000>;
                interrupts = <0 100 4>;
        };
};

UIO のノードを1つ作成すると,対応するデバイスファイル(/dev/uioX)が1つできます.マイナー番号は昇順に振られるため,複数のノードを作成するとuio0から順にデバイスファイルが対応付けられます.それぞれのデバイスファイルを制御することでノードごとに定義した割込みを同時に扱うことができます.

ターゲット上では modprobe (insmod) でドライバを有効にしますが,その際に引数に devicetree で定義した compatible 文字列を指定します.これで定義した I/O 領域や割込みが取り込まれ,デバイスファイルが作成されます.

# modprobe uio_pdrv_genirq.ko of_id=myuio

ユーザプログラムでは通常のドライバと同様にデバイスファイルを open した後,mmap すれば device tree で定義した I/O 領域へのアクセスが可能になります.mmap で指定する offset は page 単位で連続した相対アドレスです.例では,上記 0x40000000 が base1,0x41000000 が base2 に にマップされます.

void main()
{
        int fd;
        void __iomem *base1, *base2;

        fd = open("/dev/uio0", O_RDWR);
        base1 = mmap(NULL, PAGE_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
        base2 = mmap(NULL, PAGE_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 1 * PAGE_SIZE);
        ...
        munmap(base2);
        munmap(base1);
        close(fd);
}

割込みを待つ場合は,まず割込みを許可するためにデバイスファイルに 1 を書き込みます.その後,read() を呼ぶと割込みが入るまでブロックします.再度割込みを待つ場合は再びデバイスファイルに 1 を書いて割込みを許可します.タイムアウトを追加したい場合は,ppoll() で待って read() で読み出す処理を行うことで実現できます.

void main()
{
        int fd, irqsw, count;
        fd = open("/dev/uio0", O_RDWR);

        irqsw = 1;
        write(fd, &irqsw, sizeof(irqsw)); /* enable interrupt */
        ...
        read(fd, &count, sizeof(count)); /* wait for interrupt */
        ...
        irqsw = 0;
        write(fd, &irqsw, sizeof(irqsw));  /* disable interrupt */
        close(fd);
}

ドライバを作る前に I/O を直接操作して動作を確かめたい場合や,ドライバを作るまでもないHardware制御を行いたいときに,UIO を使うと手軽に実現できます.また,諸事情により公開が難しい Proprietary Driver を作成せざるを得ない場合でも,UIO を使うことで  userland 上に実現することが可能です.

Open Source Summit Japan 2017

1年ぶりの更新になります.
先日有明のコンファレンスセンターにて開催された Open Source Summit Japan に参加してきました.このイベントは初参加で Community への貢献もまだまだの中で,全セッション英語とあってまずは雰囲気を知る機会で2日間参加しました.

最近は LinuxCon と ContainerCon と CloudOpen の統合,Automotive Linux Summit との共催で講演が多岐に渡って行われる反面,深い技術トピックが少なかったのが技術者・開発者としては寂しいところです.組込み Linux の開発関連は Automotive 側に話題が多く,OSSJ は Cloud や Server,Service の話題が多かったように思います.

KeyNote を一通り聞いた後,以下のセッション(とこの他いくつか)を聴講しました.

  • Building Debian-Based Products: Experiences in Collaboration
    • Debian の OE レシピと Image 作成のシステムを統合した話
  • Understanding SCHED_DEADLINE
    • EDF アルゴリズム(最もdeadlineが近いタスクに割り当てる)に基づくスケジューリングの話
  • Taking ARM Servers Mainstream: Designing, Building, and Deploying in the Real World
    • ARMv8 サーバにおける RHEL のソフトウェア・ハードウェア要件の話
  • Building, Deploying and Testing an Industrial Linux Platform
    • 工業向け Linux の長期間継続的なメンテナンス・テストの話
  • Adding new CPU architecture to QEMU
    • QEMU に新アーキを追加する際の translation の仕組みの話
  • Bare Metal Container
    • 遠隔サーバ上の OS 非依存のコンテナを扱うシステムの話

セッションの間はドリンクやクッキー,フルーツなどが自由に楽しめました.日本開催ということでお約束の抹茶ケーキやおかきセットなどもありました.ランチは自力解決とはいえ,有明という立地上選択肢が少なく参加者は大変だったかもしれません.

参加期間中,Community の方々に交じってランチや夕食に 同席させていただいたのですが,せっかくの機会なのに英語力の無さでいろいろ申し訳ない応答になったのが無念でした…

[Linux] arm と arm64

64bit ARMプロセッサ

携帯機器のほぼ標準となっている ARM プロセッサは、32-bit の v7 アーキテクチャから 64-bit の v8 アーキテクチャに移行しつつあります。とはいえまだソフトウェアは v7 アーキテクチャ向けに書かれたものばかりですので、いきなり移行することはできません。そのため、32-bit と 64-bit を保護レベルの切り替え時にモード切替することができるようになっていて、互換性を保つように設計されています。x86 プロセッサも全く新しい IA-64 アーキテクチャを発表したものの、後で AMD が amd64 アーキテクチャで 32-bit と互換性を保つ技術を導入し、Intel が同調した経緯があります。

boot 時は 64-bit(AArch64) で起動した場合、EL1(OSレベル)やEL0(userレベル) に切り替える際に 32-bit(AArch32) モードを選択することができます。

Linux の互換性

Linux では ARMv7 までのアーキテクチャを「arm」、ARMv8 アーキテクチャを「arm64」と別々のアーキテクチャで扱っています。一方 gcc ではARMの呼び名に合わせてそれぞれ「AArch32」「AArch64」と区別しているので注意が必要です。

arm64 kernel  では、CONFIG_COMPAT を有効にすることで  userland で AArch32 ELF バイナリを起動したときに AArch32 モードに切り替えて実行できるようになっています。arch/arm64/Kconfig には以下のように説明書きがあり、モードの切り替えだけでなく syscall や VFP サポートなどの基本的な機能もアーキテクチャの差異をを超えてサポートされています。

config COMPAT
	bool "Kernel support for 32-bit EL0"
	depends on ARM64_4K_PAGES || EXPERT
	select COMPAT_BINFMT_ELF
	select HAVE_UID16
	select OLD_SIGSUSPEND3
	select COMPAT_OLD_SIGACTION
	help
	  This option enables support for a 32-bit EL0 running under a 64-bit
	  kernel at EL1. AArch32-specific components such as system calls,
	  the user helper functions, VFP support and the ptrace interface are
	  handled appropriately by the kernel.

	  If you use a page size other than 4KB (i.e, 16KB or 64KB), please be aware
	  that you will only be able to execute AArch32 binaries that were compiled
	  with page size aligned segments.

	  If you want to execute 32-bit userspace applications, say Y.

AArch32 ELF バイナリを arm64 kernel で動かすには、次に示すようにいろいろ準備や注意が必要になってきます。

shared library の対応

単純に AArch32 ELF だけの root filesystem であれば何もする必要はないのですが、AArch64 ELF と共存する環境を作る場合は両方の shared library を用意する必要があります。debian などの distro であれば設定で解決できますが、ここでは構成を知るためにとりあえず手動で作る場合を考えてみます。

まず AArch64 ELF の sysroot (sysroot64/) を基準に sysroot をコピーし、AArch32 ELF の sysroot (sysroot32/) からライブラリをコピーします。

$ cp -pr sysroot64         sysroot
$ cp -pr sysroot32/lib     sysroot/lib32
$ cp -pr sysroot32/usr/lib sysroot/usr/lib32

kernel からプロセスとして ELF バイナリを起動する際に、依存関係のあるライブラリは共有ライブラリをリンクします。
その際に動的リンカ(ld.so)が呼び出されますが、このリンカを ELF のアーキテクチャごとに用意しておくことで、マルチアーキテクチャに対応できます。上記の例の場合、AArch32 ELF のローダを /lib に置きます。/lib にはもちろん AArch64 ELF ローダもあります。

$ cp sysroot32/lib/ld-linux-armhf.so.3 sysroot/lib
$ ls -1 sysroot/lib/ld*
ld-2.23.so
ld-linux-armhf.so.3
ld-linux-aarch64.so.1

ターゲット環境ではライブラリの検索パスを指定します。AArch64 ELF と AArch32 ELF の両方のパスを指定しておけば、実行時に自動的に判別して検索します。

# export LD_LIBRARY_PATH=/lib:/usr/lib:/lib32:/usr/lib32

これで arm64 kernel で AArch32 ELF バイナリを起動することができます。

 

device driver の対応

ioctl() は通常 struct file_operations の .unlocked_ioctl メンバに登録した関数を呼び出しますが、異なるアーキテクチャの場合は .compat_ioctl に登録した関数が呼ばれます。この時、引数が 32bit 定義でやってくるので .compat_ioctl 登録関数にて引数や戻り値を変換する必要があります。struct file_operations は linux/fs.h に定義されています。

struct file_operations {
        :
        long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
        long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
        :
};

また、ARM Linux が ILP32 (int/long/pointerが32bit) である一方、arm64 Linux は LP64 (long/pointerが64bit、intが32bit) を採用しています。そのため、sizeof で得られる型のサイズがアーキテクチャによって変化します。これを引数に使うと意味が変わってきます。
特に 第二引数の cmd は マクロ _IOC() を使って定義しますが、定義内に sizeof() を使うためそのままでは 値が変わってしまうことになります。

#define _IOC(dir,type,nr,size) \
        (((dir)  << _IOC_DIRSHIFT) | \
         ((type) << _IOC_TYPESHIFT) | \
         ((nr)   << _IOC_NRSHIFT) | \
         ((size) << _IOC_SIZESHIFT))

#define _IOC_TYPECHECK(t) (sizeof(t))

例えば以下の定義を kernel/user で共有した場合、

#define HOGE_IOC_TEST    _IOC(_IOC_WRITE, 'H', 0x00, long) 

アプリ側からみた HOGE_IOC_TEST は 0x40044800 で ドライバ側から見た HOGE_IOC_TEST は 0x40084800 になり、cmd を正しく判別できなくなるため、型サイズが変わる引数を避けるか .compat_ioctl で cmd をマスクして判別する必要があります。

また、第三引数を構造体へのポインタとする場合にも注意が必要になります。構造体のメンバのメモリ位置やサイズもアプリとドライバで変わってしまう上、packed struct 属性を付けたりすると変換が複雑になってしまうため、避ける必要があります。

[Linux] device tree について

2.6.32 から 4.4 へ

Linux driver の参考書は kernel 2.6.32 辺りの知識で書かれていて,組込みの世界ではあまり新しい kernel を積極的に取り込まないこともあって,私がいる部門でも長い間内部で保持されてきた kernel を使って driver を書いていたのですが,独自の分岐で進化した kernel を maintainance する人材もいなくなり,昨年からいろいろ一新する上で kernel も vanilla kernel に追従することになりました.

昨今では自社向けソースをすべて内部で抱え続けるのも難しく,外に公開していける部分は OpenSource に貢献して協力関係を築いていくのも至極当然の流れでしょう.何故今までそうして来なかったのか不思議なくらいですが…という前置きはおいといて,そんな 2.6 時代の知識から突然最新 LTS の v4.4 の開発手法や kernel API に刷新する必要が出てきたわけです.

device tree とは

最もインパクトがあったのは device tree の導入でした.eLinuxの解説に記載がありますが,OpenFirmware の ePAPR 仕様で規定されているものを元にしたデバイス固有情報を記述するフォーマットです.Linux 以外にも FreeBSD や U-boot でも採用されていて,標準化の動きがあります.
(OpenFirmware とか PAPR と聞くと Mac 互換機の CHRP や BeOS を思い出してしまうのですが…)

実際の記述方法は devicetree.org の文法解説や Linux source の Documentation/devicetree/bindings/ に記載されていますが,実際の arch/arm/boot/dts/ 以下の各 SoC 向けソースが参考になります.

device tree の driver への取り込み方

device tree を導入すると,driver 側の記述の仕方も変わってきます.元々 module_init() や module_exit() で最初と最後を書くだけだったのが,device tree から情報を取り込む操作が必要になるわけですが,その操作は platform driver の framework がベースになります.

例えば,以下のように device tree に foo driver の記述を行うとします.識別文字列の他に,使いたいレジスタアドレスとサイズが書かれています.

/ {
    foo {
        compatible = "hoge,foo";
        reg = <0x12340000 0x200>;
    };
}; 

platform driver は,起動関数・終了関数・識別テーブルを定義します.

static struct platform_driver foo_driver = { 
    .probe  = foo_probe,
    .remove = foo_remove,
    .driver = { 
        .name = "foodriver",
        .of_match_table = foo_of_match,
    },
};
module_platform_driver(foo_driver);

識別テーブルの方は compatible で device tree に適合する識別文字列を定義します.

static const struct of_device_id foo_of_match[] = { 
    { .compatible = "hoge,foo",},
    {},
};
MODULE_DEVICE_TABLE(of, foo_of_match);

このように定義しておいて,起動関数 foo_probe() では device tree の情報を取り込んで,実際の動作に反映させていきます.

static void __init foo_probe(struct platform_device *pdev)
{
    sturce resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    void __iomem *vptr = devm_ioremap_resource(res);
    :
}

この他にも device tree 情報を取り込む platform 関数や of 関数はたくさんあり,方法もさまざまです.
一つサンプルを作って自分なりに確立しておくのが良いかと思います.

[Linux] process と CPU割り当て

CPU migration

Linux では SMP 構成をとる場合,できるだけ CPU を公平に使うようにスケジューリングされます.そのため,各プロセスの CPU 使用率を元に再スケジューリングのタイミングでプロセスの CPU migration (CPU 間の移動)が発生します.

migration で CPU 間をプロセスが移動するためには,一度動作を止めてコンテキスト(レジスタや状態情報など)を移動させて再開,という処理が行われますが,キャッシュ共有する「CPUコア間」に比べて共有しない「CPUクラスタ間」は移動にかかる時間が掛かるため,移動に重みづけしてスケジューラが次の CPU を判断します.

migration 動作を見る

ではプロセッサの migration でどうやって動いてるんだろう,と*手っ取り早く*調べたいと思ってたところ,
busybox の ps では CPU 割り付けが扱われないため,procps パッケージの ps を使うと確認できました.

$ ps -eo pid,psr,comm

ということで,下記のような shell script で回しておけば,出力ログを加工してグラフ化することで遷移の可視化もできます(かなり古典的手法ですが).

#!/bin/sh
(
    while :; do
        ps -eo pid,psr,comm | grep $PROG;
    done
) | awk '{print $1,",",$2}'

CPUやスケジューリングポリシーを指定

プログラムで直接 sched_setaffinity() や sched_setscheduler() を呼び出せばいいのですが,常にプログラムを書き換えられるわけではないので,できれば shell で指定できる方が都合がいいです.util-linux パッケージを使うと可能になります.

taskset で起動プロセスの CPU を固定することができます.例えば CPU1 でプロセスを起動したい場合は
以下のように指定します.

$ taskset -c 1 PROG

chrt で起動プロセスのポリシーを指定することができます.例えば SCHED_FIFO の priority=50 で
起動したいなら次のようにします.

$ chrt -f 50 PROG

また,kernel の起動オプションで ‘isolcpus’ を付けてCPUを指定すると,そのCPUはプロセスのCPU割り付け対象から外されます.全ての CPU を指定すると全て CPU0 でプロセスを起動するようになります.SMP 構成ながら Asymmetric に CPU を使うこともできそうです.

isolcpus=0-3

余談ですが…

SMP 構成の組込みシステムの場合,応答速度や処理量で破綻する可能性があるのであまり CPU を移動するのはうれしくないですが,特定の CPU に処理が集中するのも不利になるため適度に分散させたいため,スケジュールポリシーの選択は重要になってきます.

SCHED_FIFO,RR や CPU 割り付けを固定で行うこれまでのリアルタイム設計は複雑なシステムになるほど不都合になるため,SCHED_DEADLINE (EDF) や新たに提案されている Energy-Aware などの heuristic スケジューリングにも期待したいところです.

VMware Player 環境の再構築

システムの置き換え

これまで HDD 2台でそれぞれ Windows, Ubuntu を置いて、Windows から VMware で Ubuntu を起動していました。システムドライブを SSD に置き換えるにあたり、Windows も Ubuntu も SSD に置くことにしたのですが、ここでハマってしまったので記録しておきます。

Windows7 のインストールの際に SSD(250GB) を NTFS と ext4 に分けることを想定してパーティションを設定し、Windows7 のインストール、Windows10への upgrade、VMware Player のインストールまでは特にハマるところはなく進みました。

仮想環境の構築に失敗

VMware Player で仮想マシンを作成し、以前と同じように設定した後、HDD は IDE 物理ディスクの「PhysicalDrive0 のパーティション3(ext4想定の空き)」を指定して Ubuntu Desktop のインストールを開始。

後半で Grub のインストール先を指定しますが、上記インストールパーティションを指定すると失敗します。Grub は MBR にインストールするものですが、VMware は物理ディスクの場合もブート時に MBR を見に行くためそのままでは失敗します。

Grub は基本パーティションの Linux FileSystem にある /boot/grub/ を見に行くため、最初に Linux をインストールしてから Windows をインストールすれば解決しそうですが、Grub から Windows を起動すると、VMware Player の Grub からホスト OS のWindows が選択できてしまうためこれは惨事になるのが予想できます。

もう一つのシステムディスク登場

結局、SSD の MBR に Grub をインストールせずに VMware Player から論理パーティション上の Ubuntu が起動するには、もう一つディスクを用意して MBR を Grub にすればよいわけですが、仮想環境なので仮想ディスクを使えばよいわけです。

VMware Player で「仮想マシンの設定の編集」→「(ハードウェアタブ)追加」→「ハード ディスク」で通常のディスクを作成し、Grub が起動できればいいので “とりあえず” Ubuntu Server をインストールしました。Grub はこの仮想ディスクである /dev/sdb の MBR にインストールします。

物理パーティションOSの起動

この時点で VMware Player から Grub 経由で Ubuntu Server が起動しますし、物理パーティションの Ubuntu Desktop も認識しているので、メニューから選択すれば起動できます。しかしこちらがメインなので timeout してすぐに起動するように設定します。Grub の設定を行う Ubuntu Server 側で以下の設定値を変更します。

$ sudo vi /etc/default/grub
-GRUB_DEFAULT=0
+GRUB_DEFAULT=4
-GRUB_TIMEOUT=10
+GRUB_TIMEOUT=3
$ sudo update-grub

GRUB_DEFAULT はメニューのデフォルトの選択値です。私の環境では4つ目に Ubuntu Desktop があったので “4” を指定しています。また、GRUB_TIMEOUT は自動選択までの待ち時間で 3秒に設定し直しています。最後に update-grub で /boot/grub/grub.cfg を更新します。

これでめでたく SSD の Ubuntu Desktop が起動できました。これまで使っていた HDD も「IDE 物理ディスク」として設定してマウントすることでそのまま使うことができます。