- 投稿日:2020-05-24T22:02:22+09:00
中華レトロゲーム機を解析してみる
title: 中華レトロゲーム機を解析してみる
tags: MIPS Linux
author: yochy4671slide: false
概要
一部の方面で話題になっている,中華レトロゲーム機を購入したので解析をしてみた.物忘れの激しいワタシ向けのメモのようなものである.
解析のゴールを何にするのかが悩ましいところであるが,中華レトロゲーム機向けのアプリケーションの開発環境の構築方法を示せればいいかなと思う.
そうすれば大勢の発想豊かなどなたかが,なにかいいものを作ってくれるのではないかと.
イメージファイル
入手
電源を入れるとRetroFWの画面が表示された.これをキーワードにググるとRetroFWがすぐに見つかる.
リリースファイルのRetroFW_v1.2.zipをダウンロードし,中身を見てみる.
配布物の中身
zipファイルを展開すると,RetroFW.imgとバッチファイルとdd.exe,サブフォルダに各種uBoot.binとuImage.binが現れる.
バッチファイルから見えてくること
バッチファイルの中を見ると,dd.exeを使ってRetroFW.imgの中の特定の位置にuBoot.binとuImage.binを書き込む処理が書かれていることがわかる.例えばRetroGame_v1.0_S_B.batは以下のようになっている.
RetroGame_v1.0_S_B.batdd if=kernel/RetroGame_v1.0_S_B.uBoot.bin of=RetroFW.img conv=notrunc bs=512 seek=1 && dd if=kernel/RetroGame_v1.0_S_B.uImage.bin of=RetroFW.img conv=notrunc bs=1024 seek=4096uBoot.binはファイルの先頭から512byteオフセットした位置から,uImage.binはファイルの先頭から4Mbyteオフセットした位置から書かれることがわかる.
U-Bootは電源が入ってすぐの初期の段階で動作を開始し,ハードウエアの初期化を行った後にOSの起動を行う,いわゆるブートローダーである.
これがファイルの先頭から512byteオフセットした位置に書かれるのだから,この中華レトロゲーム機で採用しているチップはmicroSDの先頭から512byteオフセットした位置(仮にLBA 1としておく)からをRAMに読み出してジャンプする仕組みを備えていることが想像できる.
更にこのU-BootはmicroSDの先頭から4Mbyteオフセットした位置に置かれているuImage.binをRAMに読み出してジャンプするように書かれていると想像できる.
イメージファイルから見えてくること
RetroFW.imgを見ると,最初の512byteがMBRであるように見える.
パーティションテーブルのわかりやすい部分だけを抜き出して,表にすると以下のようになる.
パーティションNo. 先頭セクタ(LBA) セクタ数 パーティション種類 #1 0x0000_4000 0x0004_0000 Linux #2 0x0004_4000 0x0008_0000 Linux swap #3 0x000c_4000 0x0013_c000 FAT32 #4 - - 空き 上記に従いRetroFW.imgをバイナリエディタで見てみると,確かに第1パーティション(オフセット0x0080_0000から)がrootfs,第2パーティション(オフセット0x0800_0000から)がswapに見える.
しかし第3パーティション(オフセット0x1800_0000から)は,しばらくの間0で埋められている.こうなっていると,恐らくLinuxからマウントしようとすると失敗するはずで,失敗した場合の動作があらかじめ仕込まれていると考えられる.
具体的には,FAT32でマウントを失敗したらmkfsする,という動作である.もう少し気の利いた設計がしてあるなら,FAT32領域をmicroSDの末尾まで拡張してからmkfsしてくれる事も考えられる(raspbianなんかはそうだから).これは実際に動きを確認すればよいだろう.
MBRのオフセット0からのローダー部分には実行コードが配置されることになっており実際に何かかかれているが,このチップでもそうなのかは判断しづらい.
ひとまず先頭の4byte,
FA B8 00 10
を逆アセンブルしてみると...00000000 1000b8fa b loc_fffee000とのことで,相対ジャンプ命令でしかも飛び先はマイナス方向のようである.これは実行されないような気がしてならない.これ以上突っ込んで調べるのはやめて,ランダム値を書き込んでみてどうなるか,実際に動きを確認すればよいだろう.
Linuxを使ってもっと調べる
ここまでバイナリエディタで調べたことであるが,Linux機を使えばもっと簡単に調べられる事に気づいた.
$ fdisk -l ./RetroFW.img Disk ./RetroFW.img: 1 GiB, 1073741824 bytes, 2097152 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: dos Disk identifier: 0xbb005712 Device Boot Start End Sectors Size Id Type ./RetroFW.img1 16384 278527 262144 128M 83 Linux ./RetroFW.img2 278528 802815 524288 256M 82 Linux swap / Solaris ./RetroFW.img3 802816 2097151 1294336 632M c W95 FAT32 (LBA)調べたとおりである.せっかくなのでもう少し踏み込む.
$ sudo mount -t ext4 -o ro,loop,offset=8388608 RetroFW.img /mnt $ file /mnt/bin/busybox /mnt/bin/busybox: setuid ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped $ readelf -a /mnt/bin/busybox ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: MIPS R3000 Version: 0x1 Entry point address: 0x402fb0 Start of program headers: 52 (bytes into file) Start of section headers: 800132 (bytes into file) Flags: 0x50001007, noreorder, pic, cpic, o32, mips32 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 9 Size of section headers: 40 (bytes) Number of section headers: 26 Section header string table index: 25 : Attribute Section: gnu File Attributes Tag_GNU_MIPS_ABI_FP: Hard float (double precision)MIPS32のバイナリで,FPU搭載のチップを使っているようである.
実際に購入した中華レトロゲーム機の製品名でググってみるとJZ4760というチップを使っている,という情報がありIngenic Semiconductor - Wikipediaを見るとMIPS32 rev1でFPUありの情報が得られる.
またバイナリはuClibcを使うようビルドされていることがわかる.
U-Bootのバージョン
バイナリエディタでU-Bootのバイナリを見てみると,採用しているバージョンが以下であることがわかる.
RetroGame_v1.0_S_B.uBoot.binU-Boot 1.1.6 (Jul 26 2018 - 14:28:08)1.1.6となると2006年頃のものだろうか,ずいぶん古いものを使っている.ま,ブートローダーなので古くても構わないか.
現行のリリースバージョンはv2019.07のようだが,jz4760向けの実装は見当たらずにjz4780の実装しかなさそうである.
RetroGame_v1.0_S_B.uBoot.binBoard: Ingenic LEPUS (CPU Speed %d MHz)という文字列も見つけた.LEPUSというのはリファレンス・デザインボードの名称のようであるため,これをキーワードに探せばjz4760向けの実装も見つかるのではないかと思われる.
ひとまずu-boot-1.1.6-jz-20120904-r1819.patch.gzなんてものを見つけた.これにはがっつりjz4760向けの実装が含まれている.RAMが使えるようにする部分の実装は,ボードの定数由来のパラメータが含まれることがある.リファレンス・デザインにしたがったボード設計になっているかがキモになるハズ.
Linuxのバージョン
同様にバイナリエディタでuImageのバイナリを見てみると,採用しているバージョンが以下であることがわかる.
RetroGame_v1.0_S_B.uImage.binLinux-2.6.31.32009年頃のものだろうか,これまた古いものを使っている.
現行のstableバージョンは5.2.14のようだが,これまたjz4760向けの実装は見当たらずにjz4740, jz4770, jz4780の実装があるようだ.jz4740との違いはクロック周波数とFPUの有無,GPUの有無くらいと書かれていたので,jz4740向けのビルドで動いてしまうのかもしれない(ホントか?).
こちらもGPLだから,探せばjz4760向けの実装も見つかるのではないかと思われる.
ひとまずlinux-2.6.31.3-jz-20120904-r1448.patch.gzなんてものを見つけた.これにはがっつりjz4760向けの実装が含まれている.
標準的な開発環境
RetroFWの標準的な開発環境を探してみたところ,さっくりと見つかった.
Topic: RetroFW - Developer Support Threadに書かれてあるチュートリアルどおりに構築すれば,サンプルのIPKを作るところまであっさりできた.
開発環境構築の要点
チュートリアルに書かれてある要点は以下の通りである.
- Ubuntuが動作する環境を用意する
- BuildRootをビルドするためのパッケージを導入する
- BuildRootをビルドする
- ビルドしたツールチェインにパスを通す
チュートリアルにはVirtualBoxを使ってUbuntuの動作環境を用意していたが,試しにWindows Subsystem for LinuxのUbuntuを使ってみたところ,一部を除き問題なく構築できた.なおWSLの導入については,説明を割愛する.
次にWSLのUbuntuに,BuildRootをビルドするためのパッケージを導入する.以下の通り.
$ sudo apt install build-essential libncurses5 libncurses5-dev git python unzip bc次にBuildRootを取得し,ビルドする.
BuildRootのバージョンは2018.02.9を使うよう指定されており,新しいバージョンがリリースされていることに気づいても使わないように記載されている(ツールチェインのバージョンに敏感だから,とある).
$ wget https://buildroot.org/downloads/buildroot-2018.02.9.tar.gz $ tar -xzvf buildroot-2018.02.9.tar.gz $ cd buildroot-2018.02.9 $ make menuconfigbuildrootのコンフィグレーションは,以下のようにするよう記載されている.
Target Options Target Architecture - MIPS (little endian) Disable soft-float Toolchain C library (uClibc-ng) Enable WCHAR support Enable C++ support Target Packages Graphic libraries and applications (graphic/text) SDL SDL_gfx SDL_image SDL_mixer SDL_net SDL_sound SDL_TTFあとはビルドするだけ.ツールチェイン(クロスコンパイラ)もまるごとビルドするため,結構時間がかかる.
$ export FORCE_UNSAFE_CONFIGURE=1 $ makemakeの最中にrootfsを作るのだが,WSLではこれがエラーになるようだ(fakerootがなんたら,fakedがなんたら,と出る).今回は開発環境が構築できればよく,rootfsは使わないため無視してしまう.同様の手順をVirtualBox + Ubuntu 18.04.3 LTSで試したところ,エラーが出ることなくビルドが終了した.
このあとは好みに依存するが,ツールチェインを指定のディレクトリにコピーしてPATHを通すよう書かれている.
$ mkdir /opt/rs97tools $ cp -R output/host/* /opt/rs97tools/ $ export PATH=/opt/rs97tools/mipsel-buildroot-linux-uclibc/sysroot/usr/bin:$PATH $ export PATH=/opt/rs97tools/bin:$PATHこれで
mipsel-linux-gcc
やsdl-config --libs
が実行できれば良い,とある.
お好みでサンプルプロジェクトのビルドを試してみるのも良い.$ mkdir /opt/rs97apps $ cd /opt/rs97apps $ git clone https://github.com/jbanes/rs97-commander $ cd rs97-commander $ makeまとめ
購入した中華レトロゲーム機ではOSとしてLinuxが動作しており,エミュレータがLinuxアプリケーションとして動作していることがわかった.
U-BootやLinuxは結構古いものを使っているが,チュートリアルが用意されているため,開発環境構築は容易である.
またSDL(1.2)に準拠したアプリケーションを書くことで,中華レトロゲーム機で動作させられそうなことがわかった.
SDL2をサポートしたRetroFW v2.0が開発中とのことで,リリースを心待ちにしている.アプリケーションの開発環境を改善すべく,longtermなLinuxカーネルをポーティングしてみるとか試してみてたけど中断してしまった.
おまけ
ビルドを試す
せっかくツールチェインが用意できたので,U-Bootのコンパイルを試みる.
U-Boot
$ wget ftp://ftp.denx.de/pub/u-boot/u-boot-1.1.6.tar.bz2 $ tar jxvf u-boot-1.1.6.tar.bz2 $ cd u-boot-1.1.6 $ gzip -cd ../u-boot-1.1.6-jz-20120904-r1819.patch.gz | patch -p1この例を参考に,
include/asm-mips
以下にあるヘッダファイルの内容を変更する.大雑把に言うとinline
や__inline__
が付加されているにも関わらずextern
されているのをstatic
に修正する.$ make lepus_msc_config $ vi Makefile (SUBDIRS から examples を削除) $ makeこれで
u-boot-msc.bin
ができる.動くかは試してない.BuildRootで作られるツールチェインは,U-Bootをビルドするのにバージョンがマッチしないようだということがわかった.これは,ヘッダファイルの修正の参考にしたペイジの記載によると,ツールチェインが新しい場合にこのエラーになることが書かれているためである.
実はWSLのUbuntuでインストールできるgcc-mipsel-linux-gnu(gcc-7.4.0)でも,ビルドが通る.
$ apt install gcc-mipsel-linux-gnuしたあとに,
Makefileifeq ($(ARCH),mips) CROSS_COMPILE = mipsel-linux-gnu- endifと変更してmakeすれば良い.
.configを見たくて聞いてみたら,ソースコードを持っていないと言われた.
しかもスクラッチから作った方がいい,とか言うし.いやぁ,こっちはコンパイル,リンクまでてきているので確認して動けばそれでいいと思っている.動かなかったときのデバッグ方法を調べたうえで,仕込んでから確認しないとハマってしまうけど(LEDデバッグかなぁ).
Linux
$ wget https://mirrors.edge.kernel.org/pub/linux/kernel/v2.6/linux-2.6.31.3.tar.gz $ tar zxvf linux-2.6.31.3.tar.gz $ cd linux-2.6.31.3 $ gzip -cd ../linux-2.6.31.3-jz-20120904-r1448.patch.gz | patch -p1 $ make lepus_defconfig $ make CHK include/linux/version.h CHK include/linux/utsrelease.h SYMLINK include/asm -> include/asm-mips HOSTCC scripts/basic/fixdep HOSTCC scripts/basic/docproc HOSTCC scripts/basic/hash CC kernel/bounds.s In file included from include/linux/compiler.h:40:0, from include/linux/stddef.h:4, from include/linux/posix_types.h:4, from include/linux/types.h:14, from include/linux/page-flags.h:8, from kernel/bounds.c:9: include/linux/compiler-gcc.h:86:30: fatal error: linux/compiler-gcc6.h: No such file or directory #include gcc_header(__GNUC__) ^ compilation terminated. /home/yochy/linux-2.6.31.3/./Kbuild:35: recipe for target 'kernel/bounds.s' failed make[1]: *** [kernel/bounds.s] Error 1 Makefile:977: recipe for target 'prepare0' failed make: *** [prepare0] Error 2というわけで,このままではコンパイルが通らない.使用しているコンパイラがgcc-6.4.0なのに対して,Linuxカーネルソースはgcc6を知らない,と言っているため.Ingenicが出している文書(Ingenic Linux Development Guide)を見ると,使用しているgccは4.1.2である.
WSLで作業を続けるには,これの64bitバイナリを作る必要がある.VirtualBoxのUbuntuなら32bitラインタイムライブラリと,ビルド済みのgcc-4.1.2を入れるだけでよいだろう.
一旦作業をVIrtualBox上のUbuntuに変え,以下のようにすると...
$ cd /opt $ sudo tar jxvf ~/mipseltools-gcc412-glibc261.tar.bz2 $ export PATH=/opt/mipseltools-gcc412-glibc261/bin:$PATH $ sudo dpkg --add-architecture i386 $ sudo apt install libc6:i386これで使えるようになる.
$ make lepus_defconfig $ make : TIMEC kernel/timeconst.h Can't use 'defined(@array)' (Maybe you should just omit the defined()?) at kernel/timeconst.pl line 373.こんどはperlで文句を言われる.ここを参考に
kernel/timeconst.pl
を書き換えれば良い.次はこんな感じ.
$ make : drivers/video/jz4760_lcd.c:141: error: 'LCD_CTRL_BST_64' undeclared here (not in a function)これは
.config
で定義しているLCDの宣言に関係する..config# CONFIG_JZ4760_LCD_TOPPOLY_TD025THEA7_RGB_DELTA is not set CONFIG_JZ4760_LCD_TOPPOLY_TD043MGEB1=y # CONFIG_JZ4760_LCD_TRULY_TFTG320240DTSW_18BIT is not setどれをyにすればよいのかわからないので,ひとまずここまで.
LCD_CTRL_BST_64
自体はarch/mips/include/asm/mach-jz4760b/jz4760blcdc.h
にあるので,これを#include
すれば通るようになるだろう..configを見たくて聞いてみたら,カーネルソースを公開してもらえた.ここまでくれば,今どきのカーネルバージョンにポーティングも不可能ではないが,アプリケーションがちゃんと動くかわからないので保留かな.
v1.2.1とv1.2を比較する
RetroFW v1.2.1が出ているのに気づいたので,解析時に用いていたv1.2との差をナナメな方向から調べてみた.
結論を言ってしまうと,中身は同じものである.リリースのコメントを見ても,commit messageを見ても,中身を変えたとは書かれていない.
RetroFW.binの中身を各ゲーム機向けに書き換えるバッチファイルの名前が親切になったくらいか.
正直に言うと,RetroFW.binの0x0000_0000から0x087f_ffff(Linux領域の終端)までを切り取ってmicroSDに書き込めばFAT32領域が初期化されることなくそのままでアップデートできるからソフト入れ直す必要ないですよ,と書きたかった.残念.
U-Boot/Linux kernel
U-BootとLinux kernelは同一であった(sha256の値が同一).これ目当てでアップデートする意味はない.
RetroFW.img
RegroFW.imgの中身は異なっていた(sha256の値が異なる).
U-BootとLinux kernelが同一であることはわかっているため,MBRから求めたLinux領域に差があれば,なにかしら恩恵を受ける可能性がある.
そんなわけでRetroFW.imgのオフセット0x80_0000(LBAで0x4000セクタ)以降を比較したのだが,残念なことに内容は同一であった.
v2.0とv1.2を比較する
RetroFW v2.0が出ているのに気づいたので,解析時に用いていたv1.2との差を調べてみた.
結論から言うと,BuildRootの中身が変わっているので試す価値はあると思われる.詳しくはCHANGELOG.mdを参照されたい.
なおU-BootやLinuxカーネルは(恐らく)同一である.
パーティション構成
MBRを見ると,パーティション構成(サイズ)に若干差があったので示しておく.
パーティションNo. 先頭セクタ(LBA) セクタ数 パーティション種類 #1 0x0000_4000 0x0004_f800 Linux #2 0x0005_3800 0x0007_0800 Linux swap #3 0x000c_4000 0x0013_c000 FAT32 #4 - - 空き Linux領域が若干拡張され,その分だけswap領域が減らされている.FAT32領域の先頭セクタは変わっていない.
従ってMBRとLinux領域のみを差し替えることで,FAT32領域を初期化せずに更新可能だと思われる.
BuildRootのバージョン
一緒にリリースされているBuildRootのバージョンを確認すると,2018.02.11を使っているようである(CHANGESより).先に示したチュートリアルには2018.02.9を使うように書かれているが,バージョンを更新したようである.
v2.1とv2.0を比較する
RetroFW v2.1が出ていた.
v2.0以降は更新情報が親切に書かれているので,リンク先のリリースノートを読んで必要性の可否を判断すればいいだろう.
ちなみにFAT32領域以前を差し替えて更新しようとしたら,再初期化が走った.転送済みのあれやこれが消えてしまうので,バックアップをお忘れなく.
V2.2とv2.1を比較する
RetroFW v2.2が出ていた.
なぜか今回は更新情報がちゃんと書かれていない.Readme.mdにそれっぽいものが書かれているが,システムリカバリのQ.O.L.変更って何のことだろう(この場合のQ.O.L.ってQuarity of Lifeでいいんだろうか).
今回のイメージファイルはちょっと変わっていて,パーティションが1つしか含まれていない.
パーティションNo. 先頭セクタ(LBA) セクタ数 パーティション種類 #1 0x0000_4000 0x0004_f800 Linux #2 - - 空き #3 - - 空き #4 - - 空き microSDに書き込んで電源を入れてみたところ,当然のように初期化処理が走った.これまでの処理に比べて早く終了したように感じる.初期化処理を済ませたあとのパーティション構成は以下のようになっていた.
パーティションNo. 先頭セクタ(LBA) セクタ数 パーティション種類 #1 0x0000_4000 0x0004_f800 Linux #2 0x0005_3800 0x0007_0800 Linux swap #3 0x000c_4000 0x01c6_a000 FAT32 #4 - - 空き これまでと比較してFAT32領域が広くなった.
ところでイメージファイルのサイズが175,112,704 byteなのだけど,これが342,017(0x53801) sectorで,ぎりぎりswapファイルのパーティションに食い込んでいる.1セクタ分小さくしなかったのは理由があるんだろうか.
さてもう少しReadme.mdを読むと,libopkやopkrunが取り込まれPyMenuやSimpleMenuが使えるようになった,とある.えっと...PyMenu,SimpleMenuのことかな.
あとでもう少し見ておこうかな.
参考文献
- 投稿日:2020-05-24T22:01:25+09:00
中華レトロゲーム機を解析してみる
概要
一部の方面で話題になっている,中華レトロゲーム機を購入したので解析をしてみた.物忘れの激しいワタシ向けのメモのようなものである.
解析のゴールを何にするのかが悩ましいところであるが,中華レトロゲーム機向けのアプリケーションの開発環境の構築方法を示せればいいかなと思う.
そうすれば大勢の発想豊かなどなたかが,なにかいいものを作ってくれるのではないかと.
イメージファイル
入手
電源を入れるとRetroFWの画面が表示された.これをキーワードにググるとRetroFWがすぐに見つかる.
リリースファイルのRetroFW_v1.2.zipをダウンロードし,中身を見てみる.
配布物の中身
zipファイルを展開すると,RetroFW.imgとバッチファイルとdd.exe,サブフォルダに各種uBoot.binとuImage.binが現れる.
バッチファイルから見えてくること
バッチファイルの中を見ると,dd.exeを使ってRetroFW.imgの中の特定の位置にuBoot.binとuImage.binを書き込む処理が書かれていることがわかる.例えばRetroGame_v1.0_S_B.batは以下のようになっている.
RetroGame_v1.0_S_B.batdd if=kernel/RetroGame_v1.0_S_B.uBoot.bin of=RetroFW.img conv=notrunc bs=512 seek=1 && dd if=kernel/RetroGame_v1.0_S_B.uImage.bin of=RetroFW.img conv=notrunc bs=1024 seek=4096uBoot.binはファイルの先頭から512byteオフセットした位置から,uImage.binはファイルの先頭から4Mbyteオフセットした位置から書かれることがわかる.
U-Bootは電源が入ってすぐの初期の段階で動作を開始し,ハードウエアの初期化を行った後にOSの起動を行う,いわゆるブートローダーである.
これがファイルの先頭から512byteオフセットした位置に書かれるのだから,この中華レトロゲーム機で採用しているチップはmicroSDの先頭から512byteオフセットした位置(仮にLBA 1としておく)からをRAMに読み出してジャンプする仕組みを備えていることが想像できる.
更にこのU-BootはmicroSDの先頭から4Mbyteオフセットした位置に置かれているuImage.binをRAMに読み出してジャンプするように書かれていると想像できる.
イメージファイルから見えてくること
RetroFW.imgを見ると,最初の512byteがMBRであるように見える.
パーティションテーブルのわかりやすい部分だけを抜き出して,表にすると以下のようになる.
パーティションNo. 先頭セクタ(LBA) セクタ数 パーティション種類 #1 0x0000_4000 0x0004_0000 Linux #2 0x0004_4000 0x0008_0000 Linux swap #3 0x000c_4000 0x0013_c000 FAT32 #4 - - 空き 上記に従いRetroFW.imgをバイナリエディタで見てみると,確かに第1パーティション(オフセット0x0080_0000から)がrootfs,第2パーティション(オフセット0x0800_0000から)がswapに見える.
しかし第3パーティション(オフセット0x1800_0000から)は,しばらくの間0で埋められている.こうなっていると,恐らくLinuxからマウントしようとすると失敗するはずで,失敗した場合の動作があらかじめ仕込まれていると考えられる.
具体的には,FAT32でマウントを失敗したらmkfsする,という動作である.もう少し気の利いた設計がしてあるなら,FAT32領域をmicroSDの末尾まで拡張してからmkfsしてくれる事も考えられる(raspbianなんかはそうだから).これは実際に動きを確認すればよいだろう.
MBRのオフセット0からのローダー部分には実行コードが配置されることになっており実際に何かかかれているが,このチップでもそうなのかは判断しづらい.
ひとまず先頭の4byte,
FA B8 00 10
を逆アセンブルしてみると...00000000 1000b8fa b loc_fffee000とのことで,相対ジャンプ命令でしかも飛び先はマイナス方向のようである.これは実行されないような気がしてならない.これ以上突っ込んで調べるのはやめて,ランダム値を書き込んでみてどうなるか,実際に動きを確認すればよいだろう.
Linuxを使ってもっと調べる
ここまでバイナリエディタで調べたことであるが,Linux機を使えばもっと簡単に調べられる事に気づいた.
$ fdisk -l ./RetroFW.img Disk ./RetroFW.img: 1 GiB, 1073741824 bytes, 2097152 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: dos Disk identifier: 0xbb005712 Device Boot Start End Sectors Size Id Type ./RetroFW.img1 16384 278527 262144 128M 83 Linux ./RetroFW.img2 278528 802815 524288 256M 82 Linux swap / Solaris ./RetroFW.img3 802816 2097151 1294336 632M c W95 FAT32 (LBA)調べたとおりである.せっかくなのでもう少し踏み込む.
$ sudo mount -t ext4 -o ro,loop,offset=8388608 RetroFW.img /mnt $ file /mnt/bin/busybox /mnt/bin/busybox: setuid ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped $ readelf -a /mnt/bin/busybox ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: MIPS R3000 Version: 0x1 Entry point address: 0x402fb0 Start of program headers: 52 (bytes into file) Start of section headers: 800132 (bytes into file) Flags: 0x50001007, noreorder, pic, cpic, o32, mips32 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 9 Size of section headers: 40 (bytes) Number of section headers: 26 Section header string table index: 25 : Attribute Section: gnu File Attributes Tag_GNU_MIPS_ABI_FP: Hard float (double precision)MIPS32のバイナリで,FPU搭載のチップを使っているようである.
実際に購入した中華レトロゲーム機の製品名でググってみるとJZ4760というチップを使っている,という情報がありIngenic Semiconductor - Wikipediaを見るとMIPS32 rev1でFPUありの情報が得られる.
またバイナリはuClibcを使うようビルドされていることがわかる.
U-Bootのバージョン
バイナリエディタでU-Bootのバイナリを見てみると,採用しているバージョンが以下であることがわかる.
RetroGame_v1.0_S_B.uBoot.binU-Boot 1.1.6 (Jul 26 2018 - 14:28:08)1.1.6となると2006年頃のものだろうか,ずいぶん古いものを使っている.ま,ブートローダーなので古くても構わないか.
現行のリリースバージョンはv2019.07のようだが,jz4760向けの実装は見当たらずにjz4780の実装しかなさそうである.
RetroGame_v1.0_S_B.uBoot.binBoard: Ingenic LEPUS (CPU Speed %d MHz)という文字列も見つけた.LEPUSというのはリファレンス・デザインボードの名称のようであるため,これをキーワードに探せばjz4760向けの実装も見つかるのではないかと思われる.
ひとまずu-boot-1.1.6-jz-20120904-r1819.patch.gzなんてものを見つけた.これにはがっつりjz4760向けの実装が含まれている.RAMが使えるようにする部分の実装は,ボードの定数由来のパラメータが含まれることがある.リファレンス・デザインにしたがったボード設計になっているかがキモになるハズ.
Linuxのバージョン
同様にバイナリエディタでuImageのバイナリを見てみると,採用しているバージョンが以下であることがわかる.
RetroGame_v1.0_S_B.uImage.binLinux-2.6.31.32009年頃のものだろうか,これまた古いものを使っている.
現行のstableバージョンは5.2.14のようだが,これまたjz4760向けの実装は見当たらずにjz4740, jz4770, jz4780の実装があるようだ.jz4740との違いはクロック周波数とFPUの有無,GPUの有無くらいと書かれていたので,jz4740向けのビルドで動いてしまうのかもしれない(ホントか?).
こちらもGPLだから,探せばjz4760向けの実装も見つかるのではないかと思われる.
ひとまずlinux-2.6.31.3-jz-20120904-r1448.patch.gzなんてものを見つけた.これにはがっつりjz4760向けの実装が含まれている.
標準的な開発環境
RetroFWの標準的な開発環境を探してみたところ,さっくりと見つかった.
Topic: RetroFW - Developer Support Threadに書かれてあるチュートリアルどおりに構築すれば,サンプルのIPKを作るところまであっさりできた.
開発環境構築の要点
チュートリアルに書かれてある要点は以下の通りである.
- Ubuntuが動作する環境を用意する
- BuildRootをビルドするためのパッケージを導入する
- BuildRootをビルドする
- ビルドしたツールチェインにパスを通す
チュートリアルにはVirtualBoxを使ってUbuntuの動作環境を用意していたが,試しにWindows Subsystem for LinuxのUbuntuを使ってみたところ,一部を除き問題なく構築できた.なおWSLの導入については,説明を割愛する.
次にWSLのUbuntuに,BuildRootをビルドするためのパッケージを導入する.以下の通り.
$ sudo apt install build-essential libncurses5 libncurses5-dev git python unzip bc次にBuildRootを取得し,ビルドする.
BuildRootのバージョンは2018.02.9を使うよう指定されており,新しいバージョンがリリースされていることに気づいても使わないように記載されている(ツールチェインのバージョンに敏感だから,とある).
$ wget https://buildroot.org/downloads/buildroot-2018.02.9.tar.gz $ tar -xzvf buildroot-2018.02.9.tar.gz $ cd buildroot-2018.02.9 $ make menuconfigbuildrootのコンフィグレーションは,以下のようにするよう記載されている.
Target Options Target Architecture - MIPS (little endian) Disable soft-float Toolchain C library (uClibc-ng) Enable WCHAR support Enable C++ support Target Packages Graphic libraries and applications (graphic/text) SDL SDL_gfx SDL_image SDL_mixer SDL_net SDL_sound SDL_TTFあとはビルドするだけ.ツールチェイン(クロスコンパイラ)もまるごとビルドするため,結構時間がかかる.
$ export FORCE_UNSAFE_CONFIGURE=1 $ makemakeの最中にrootfsを作るのだが,WSLではこれがエラーになるようだ(fakerootがなんたら,fakedがなんたら,と出る).今回は開発環境が構築できればよく,rootfsは使わないため無視してしまう.同様の手順をVirtualBox + Ubuntu 18.04.3 LTSで試したところ,エラーが出ることなくビルドが終了した.
このあとは好みに依存するが,ツールチェインを指定のディレクトリにコピーしてPATHを通すよう書かれている.
$ mkdir /opt/rs97tools $ cp -R output/host/* /opt/rs97tools/ $ export PATH=/opt/rs97tools/mipsel-buildroot-linux-uclibc/sysroot/usr/bin:$PATH $ export PATH=/opt/rs97tools/bin:$PATHこれで
mipsel-linux-gcc
やsdl-config --libs
が実行できれば良い,とある.
お好みでサンプルプロジェクトのビルドを試してみるのも良い.$ mkdir /opt/rs97apps $ cd /opt/rs97apps $ git clone https://github.com/jbanes/rs97-commander $ cd rs97-commander $ makeまとめ
購入した中華レトロゲーム機ではOSとしてLinuxが動作しており,エミュレータがLinuxアプリケーションとして動作していることがわかった.
U-BootやLinuxは結構古いものを使っているが,チュートリアルが用意されているため,開発環境構築は容易である.
またSDL(1.2)に準拠したアプリケーションを書くことで,中華レトロゲーム機で動作させられそうなことがわかった.
SDL2をサポートしたRetroFW v2.0が開発中とのことで,リリースを心待ちにしている.アプリケーションの開発環境を改善すべく,longtermなLinuxカーネルをポーティングしてみるとか試してみてたけど中断してしまった.
おまけ
ビルドを試す
せっかくツールチェインが用意できたので,U-Bootのコンパイルを試みる.
U-Boot
$ wget ftp://ftp.denx.de/pub/u-boot/u-boot-1.1.6.tar.bz2 $ tar jxvf u-boot-1.1.6.tar.bz2 $ cd u-boot-1.1.6 $ gzip -cd ../u-boot-1.1.6-jz-20120904-r1819.patch.gz | patch -p1この例を参考に,
include/asm-mips
以下にあるヘッダファイルの内容を変更する.大雑把に言うとinline
や__inline__
が付加されているにも関わらずextern
されているのをstatic
に修正する.$ make lepus_msc_config $ vi Makefile (SUBDIRS から examples を削除) $ makeこれで
u-boot-msc.bin
ができる.動くかは試してない.BuildRootで作られるツールチェインは,U-Bootをビルドするのにバージョンがマッチしないようだということがわかった.これは,ヘッダファイルの修正の参考にしたペイジの記載によると,ツールチェインが新しい場合にこのエラーになることが書かれているためである.
実はWSLのUbuntuでインストールできるgcc-mipsel-linux-gnu(gcc-7.4.0)でも,ビルドが通る.
$ apt install gcc-mipsel-linux-gnuしたあとに,
Makefileifeq ($(ARCH),mips) CROSS_COMPILE = mipsel-linux-gnu- endifと変更してmakeすれば良い.
.configを見たくて聞いてみたら,ソースコードを持っていないと言われた.
しかもスクラッチから作った方がいい,とか言うし.いやぁ,こっちはコンパイル,リンクまでてきているので確認して動けばそれでいいと思っている.動かなかったときのデバッグ方法を調べたうえで,仕込んでから確認しないとハマってしまうけど(LEDデバッグかなぁ).
Linux
$ wget https://mirrors.edge.kernel.org/pub/linux/kernel/v2.6/linux-2.6.31.3.tar.gz $ tar zxvf linux-2.6.31.3.tar.gz $ cd linux-2.6.31.3 $ gzip -cd ../linux-2.6.31.3-jz-20120904-r1448.patch.gz | patch -p1 $ make lepus_defconfig $ make CHK include/linux/version.h CHK include/linux/utsrelease.h SYMLINK include/asm -> include/asm-mips HOSTCC scripts/basic/fixdep HOSTCC scripts/basic/docproc HOSTCC scripts/basic/hash CC kernel/bounds.s In file included from include/linux/compiler.h:40:0, from include/linux/stddef.h:4, from include/linux/posix_types.h:4, from include/linux/types.h:14, from include/linux/page-flags.h:8, from kernel/bounds.c:9: include/linux/compiler-gcc.h:86:30: fatal error: linux/compiler-gcc6.h: No such file or directory #include gcc_header(__GNUC__) ^ compilation terminated. /home/yochy/linux-2.6.31.3/./Kbuild:35: recipe for target 'kernel/bounds.s' failed make[1]: *** [kernel/bounds.s] Error 1 Makefile:977: recipe for target 'prepare0' failed make: *** [prepare0] Error 2というわけで,このままではコンパイルが通らない.使用しているコンパイラがgcc-6.4.0なのに対して,Linuxカーネルソースはgcc6を知らない,と言っているため.Ingenicが出している文書(Ingenic Linux Development Guide)を見ると,使用しているgccは4.1.2である.
WSLで作業を続けるには,これの64bitバイナリを作る必要がある.VirtualBoxのUbuntuなら32bitラインタイムライブラリと,ビルド済みのgcc-4.1.2を入れるだけでよいだろう.
一旦作業をVIrtualBox上のUbuntuに変え,以下のようにすると...
$ cd /opt $ sudo tar jxvf ~/mipseltools-gcc412-glibc261.tar.bz2 $ export PATH=/opt/mipseltools-gcc412-glibc261/bin:$PATH $ sudo dpkg --add-architecture i386 $ sudo apt install libc6:i386これで使えるようになる.
$ make lepus_defconfig $ make : TIMEC kernel/timeconst.h Can't use 'defined(@array)' (Maybe you should just omit the defined()?) at kernel/timeconst.pl line 373.こんどはperlで文句を言われる.ここを参考に
kernel/timeconst.pl
を書き換えれば良い.次はこんな感じ.
$ make : drivers/video/jz4760_lcd.c:141: error: 'LCD_CTRL_BST_64' undeclared here (not in a function)これは
.config
で定義しているLCDの宣言に関係する..config# CONFIG_JZ4760_LCD_TOPPOLY_TD025THEA7_RGB_DELTA is not set CONFIG_JZ4760_LCD_TOPPOLY_TD043MGEB1=y # CONFIG_JZ4760_LCD_TRULY_TFTG320240DTSW_18BIT is not setどれをyにすればよいのかわからないので,ひとまずここまで.
LCD_CTRL_BST_64
自体はarch/mips/include/asm/mach-jz4760b/jz4760blcdc.h
にあるので,これを#include
すれば通るようになるだろう..configを見たくて聞いてみたら,カーネルソースを公開してもらえた.ここまでくれば,今どきのカーネルバージョンにポーティングも不可能ではないが,アプリケーションがちゃんと動くかわからないので保留かな.
v1.2.1とv1.2を比較する
RetroFW v1.2.1が出ているのに気づいたので,解析時に用いていたv1.2との差をナナメな方向から調べてみた.
結論を言ってしまうと,中身は同じものである.リリースのコメントを見ても,commit messageを見ても,中身を変えたとは書かれていない.
RetroFW.binの中身を各ゲーム機向けに書き換えるバッチファイルの名前が親切になったくらいか.
正直に言うと,RetroFW.binの0x0000_0000から0x087f_ffff(Linux領域の終端)までを切り取ってmicroSDに書き込めばFAT32領域が初期化されることなくそのままでアップデートできるからソフト入れ直す必要ないですよ,と書きたかった.残念.
U-Boot/Linux kernel
U-BootとLinux kernelは同一であった(sha256の値が同一).これ目当てでアップデートする意味はない.
RetroFW.img
RegroFW.imgの中身は異なっていた(sha256の値が異なる).
U-BootとLinux kernelが同一であることはわかっているため,MBRから求めたLinux領域に差があれば,なにかしら恩恵を受ける可能性がある.
そんなわけでRetroFW.imgのオフセット0x80_0000(LBAで0x4000セクタ)以降を比較したのだが,残念なことに内容は同一であった.
v2.0とv1.2を比較する
RetroFW v2.0が出ているのに気づいたので,解析時に用いていたv1.2との差を調べてみた.
結論から言うと,BuildRootの中身が変わっているので試す価値はあると思われる.詳しくはCHANGELOG.mdを参照されたい.
なおU-BootやLinuxカーネルは(恐らく)同一である.
パーティション構成
MBRを見ると,パーティション構成(サイズ)に若干差があったので示しておく.
パーティションNo. 先頭セクタ(LBA) セクタ数 パーティション種類 #1 0x0000_4000 0x0004_f800 Linux #2 0x0005_3800 0x0007_0800 Linux swap #3 0x000c_4000 0x0013_c000 FAT32 #4 - - 空き Linux領域が若干拡張され,その分だけswap領域が減らされている.FAT32領域の先頭セクタは変わっていない.
従ってMBRとLinux領域のみを差し替えることで,FAT32領域を初期化せずに更新可能だと思われる.
BuildRootのバージョン
一緒にリリースされているBuildRootのバージョンを確認すると,2018.02.11を使っているようである(CHANGESより).先に示したチュートリアルには2018.02.9を使うように書かれているが,バージョンを更新したようである.
v2.1とv2.0を比較する
RetroFW v2.1が出ていた.
v2.0以降は更新情報が親切に書かれているので,リンク先のリリースノートを読んで必要性の可否を判断すればいいだろう.
ちなみにFAT32領域以前を差し替えて更新しようとしたら,再初期化が走った.転送済みのあれやこれが消えてしまうので,バックアップをお忘れなく.
V2.2とv2.1を比較する
RetroFW v2.2が出ていた.
なぜか今回は更新情報がちゃんと書かれていない.Readme.mdにそれっぽいものが書かれているが,システムリカバリのQ.O.L.変更って何のことだろう(この場合のQ.O.L.ってQuarity of Lifeでいいんだろうか).
今回のイメージファイルはちょっと変わっていて,パーティションが1つしか含まれていない.
パーティションNo. 先頭セクタ(LBA) セクタ数 パーティション種類 #1 0x0000_4000 0x0004_f800 Linux #2 - - 空き #3 - - 空き #4 - - 空き microSDに書き込んで電源を入れてみたところ,当然のように初期化処理が走った.これまでの処理に比べて早く終了したように感じる.初期化処理を済ませたあとのパーティション構成は以下のようになっていた.
パーティションNo. 先頭セクタ(LBA) セクタ数 パーティション種類 #1 0x0000_4000 0x0004_f800 Linux #2 0x0005_3800 0x0007_0800 Linux swap #3 0x000c_4000 0x01c6_a000 FAT32 #4 - - 空き これまでと比較してFAT32領域が広くなった.
ところでイメージファイルのサイズが175,112,704 byteなのだけど,これが342,017(0x53801) sectorで,ぎりぎりswapファイルのパーティションに食い込んでいる.1セクタ分小さくしなかったのは理由があるんだろうか.
さてもう少しReadme.mdを読むと,libopkやopkrunが取り込まれPyMenuやSimpleMenuが使えるようになった,とある.えっと...PyMenu,SimpleMenuのことかな.
あとでもう少し見ておこうかな.
参考文献
- 投稿日:2020-05-24T21:05:30+09:00
Linux シグナルの基本と仕組み
この記事は、Linux シグナルの基本と仕組み (カーネル実装) について調査したことのまとめです。
シグナルは普段から利用しているものの仕組みについては理解してなかったので、カーネル勉強の題材として調べてみました。想像以上に複雑でボリュームがあったため、書き切れていない部分 (調査しきれなかった部分) もありますが、一通りの流れ (仕組み) は理解できたと思います。
なお、この記事は「■ 基本編 」と「■ カーネル編 (v5.5)」に分かれています。仕組みを理解するには基本も知る必要があると思い、このような構成となりました。
■ 基本編
はじめにシグナルの基本について、ざっと整理します。
なお、例で登場するコマンドや API (C 言語) の細かい使い方やエラー処理などは省きます。詳細は man や参考文献の情報等をご活用ください。
1. シグナルとは
プロセスやプロセスグループへ様々なイベントを通知するためにあるカーネルの機能です (ソフトウェア割り込み)。イベントの通知は様々な場所 (自分/他プロセス、カーネル) から行うことが可能で、次のようなことができます。
- ハングしたプロセスにシグナルを送信して強制終了させる
- シグナルを送信してプロセスの処理を一時停止・再開させる
- ハードウェア例外 (0 除算、メモリアクセス違反など) 時にシグナルを送信してプロセスを終了させる
- シグナルを送信する特殊なキー (Ctrl + C など) を入力しプロセスを終了させる
- シグナル受信時にユーザ定義の関数 (シグナルハンドラ) を実行させる
なお、シグナルは次のような流れで処理されます。詳細は追々見ていきます。
2. シグナル利用例
イメージし易いように、ユーザ視点でのシグナル利用例を 3 つ記載します。
kill コマンド
kill コマンドでシグナル (SIGTERM) を送り sleep プロセスを終了させます。
$ ps PID TTY TIME CMD 2662 pts/0 00:00:00 bash 2679 pts/0 00:00:00 sleep 2685 pts/0 00:00:00 ps $ kill 2679 [1]+ Terminated sleep 1000 $ ps PID TTY TIME CMD 2662 pts/0 00:00:00 bash 2696 pts/0 00:00:00 ps割り込みキー
割り込みキー (Ctrl + C) でシグナル (SIGINT) を送り、無限ループするコマンド (プロセスグループ) を終了させます。
$ while :; do sleep 1; echo "Stop me !"; done Stop me ! Stop me ! Stop me ! ^Cプログラム (C 言語 API)
以下のように自前のプログラム (send_sig.c) でシグナル (SIGTERM) を送り sleep プロセスを終了させることもできます。
#include <signal.h> #include <stdlib.h> int main(int argc, char *argv[]) { pid_t pid = atoi(argv[1]); kill(pid, SIGTERM); return 0; }コンパイル & 実行例
$ ps PID TTY TIME CMD 30285 pts/0 00:00:00 bash 30597 pts/0 00:00:00 sleep 30631 pts/0 00:00:00 ps $ gcc -o send_sig send_sig.c $ ./send_sig 30597 [1]+ Terminated sleep 1000 $ ps PID TTY TIME CMD 30285 pts/0 00:00:00 bash 30663 pts/0 00:00:00 ps3. シグナル番号とシグナル名
シグナルは、用途に応じて番号と名前が割り振られています。
$ kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAXただし、シグナル番号の一部はアーキテクチャに依存するため、上述の実行例 (x86_64) とは番号が異なる場合があります 1。
4. 標準シグナル、リアルタイムシグナル
シグナルは、標準シグナルとリアルタイムシグナルの 2 種類に大別できます。
ざっくりとした違いは、
- 標準シグナル ... SIGKILL など伝統的に使用しているシグナル (1 〜 31 番)
- リアルタイムシグナル ... 標準シグナルの拡張版 (32 〜 64 番)
です。もう少し細かい違いは、次のとおりです。
標準シグナル リアルタイムシグナル 新/旧 旧 新 シグナル名 シグナル毎に様々 SIGRTMIN(+n), SIGRTMAX(-n) シグナル番号 1 〜 31 32 〜 64 標準動作 (詳細は後述) シグナル毎に様々 プロセス終了 (全シグナル) 用途 シグナル毎に様々 ユーザが定義する (全シグナル) 複数の同じシグナル
受信時の挙動1 つのみ受信 全て受信 複数の同じシグナル
送信時の順序規定なし 送信された順に到着 複数の異なるシグナル
送信時の順序規定なし シグナル番号の小さい順に到着 なお、本稿ではこれ以上深くリアルタイムシグナルについては言及しません 1。
5. シグナルアクション
シグナル受信時の動作 (シグナルアクション) には、次の 3 種類があります。この動作は後述の sigaction() などから変更できます。ただし、SIGKILL と SIGSTOP は標準動作以外にはできません。
無視
シグナルを受信しても何もしません。
(設定するには、後述の sigaction() などで sa_handler に SIG_IGN を指定する)
標準動作
シグナル受信すると、シグナル毎に定義された標準動作 (後述の Term, Ign, Core, Stop, Cont) を実行します。
(設定するには、後述の sigaction() などで sa_handler に SIG_DFL を指定 (デフォルト) する)
シグナルハンドラ
シグナル受信すると、ユーザが定義した動作を実行します。
(設定するには、後述の sigaction() などで sa_handler へユーザ定義の関数を指定する)
標準動作
標準動作は、シグナル毎に定義されており、次の 5 種類があります。
動作名 意味 Term プロセス終了 Ign 何もしない (sigaction() で設定できる SIG_IGN と同じ) Core プロセス終了とコアダンプ生成 2 Stop プロセス一時停止 (TASK_STOPPED 状態に遷移させる) Cont 一時停止したプロセスの再開 (TASK_STOPPED 状態からの復帰) ただし、これは標準シグナルに限ります。リアルタイムシグナルの場合は Term のみです (次図参照)。
標準シグナルと対応する標準動作
1 から 31 番までの標準シグナルは、各々に紐づく標準動作を持ちます。
シグナル名 シグナル番号 (x86_64) 標準動作 意味 SIGHUP 1 Term 制御端末のハングアップ検出 or 制御プロセスの死 SIGINT 2 Term キーボードからの割り込み (Ctrl + C) SIGQUIT 3 Core キーボードからの終了 (Ctrl + Q) SIGILL 4 Core 不正な命令 SIGTRAP 5 Core デバッグ用のトレース/ブレークポイントシグナル SIGABRT 6 Core abort(3) からの中断シグナル SIGBUS 7 Core バスエラー (不正なメモリアクセス) SIGFPE 8 Core 浮動小数点例外 SIGKILL 9 Term Kill シグナル SIGUSR1 10 Term ユーザ定義のシグナル (その1) SIGSEGV 11 Core 不正なメモリ参照 SIGUSR2 12 Term ユーザ定義のシグナル (その2) SIGPIPE 13 Term パイプ破壊 (読み手の無いパイプへの書き出し) 3。 SIGALRM 14 Term alarm(2) からのタイマーシグナル SIGTERM 15 Term 終了シグナル SIGSTKFLT 16 Term 数値演算プロセッサ (コプロセッサ)におけるスタックフォルト (未使用) SIGCHLD 17 Ign 子プロセスの一時停止 (再開) または終了 SIGCONT 18 Cont 一時停止プロセスの再開 SIGSTOP 19 Stop プロセスの一時停止 SIGTSTP 20 Stop 制御端末からの停止 (Ctrl + Z) SIGTTIN 21 Stop バッググラウンドプロセスが制御端末を読み取った SIGTTOU 22 Stop バッググラウンドプロセスが制御端末へ書き込んだ SIGURG 23 Ign ソケットに帯域外データ、緊急データが存在する SIGXCPU 24 Core CPU 時間の上限 (RLIMIT_CPU) を超えた 4。 SIGXFSZ 25 Core ファイルサイズの上限 (RLIMIT_FSIZE) を超えた 4。 SIGVTALRM 26 Term 仮想タイマ (プロセスのユーザモードでの CPU 時間) がタイムアウトした SIGPROF 27 Term プロファイリングタイマがタイムアウトした SIGWINCH 28 Ign 制御端末のウィンドウサイズが変更された SIGIO 29 Term 非同期 I/O イベント SIGPWR 30 Term 電源の異常 SIGSYS 31 Core 不正なシステムコールを実行した 5。 シグナルアクションの変更例
SIGKILL と SIGSTOP 以外のシグナルアクションを変更するには、trap コマンド や sigaction() などの API (C 言語) を使用します。
trap コマンド
trap は Bash に組み込まれたビルトインコマンドの一種で、シグナルアクションを設定できます 6。
ここでは、SIGINT 受信時に何もしない (シグナル無視) ように設定 (第一引数 '') してみます。
シグナルを無視に設定した例$ trap '' SIGINTこの状態では、SIGINT を受信しても反応しないため、次の実行例のように割り込みキー (Ctrl + C) が効きません。
$ while true; do sleep 1; echo "Stop me !"; done Stop me ! Stop me ! ^CStop me ! <--- Ctrl + C 押下しても効かず、コマンド継続 Stop me ! Stop me ! ^Z <--- Ctrl + Z 押下で停止 (SIGTSTP 送信) [1]+ Stopped sleep 1sigaction (C 言語 API)
sigaction() は、シグナルの動作を設定できる API (C 言語) です 7。
次のサンプルプログラム (set_sigh.c) では、SIGINT 受信時に handler 関数を実行するように設定してみます。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <unistd.h> static void handler(int signo) { /* * 本来ハンドラ内では、非同期シグナルセーフな関数を使用するべきですが、 * ここでは、そうでない printf()、exit() などの関数を使用しています。 * 非同期シグナルセーフについては $ man 7 signal をご参照ください。 */ printf(" ... Caught the SIGINT (%d)\n", signo); exit(EXIT_SUCCESS); } int main(void) { unsigned int sec = 1; struct sigaction act; // SIGINT 受信時に handler() を実行するように設定。 memset(&act, 0, sizeof act); act.sa_handler = handler; sigaction(SIGINT, &act, NULL); // Ctrl + C などで終了されるまで、1 秒ごとにメッセージを出力する。 for (;;) { sleep(sec); printf("Stop me !\n"); } return 0; }次の実行例では、SIGINT 受信後 handler() によってメッセージ (... Caught the SIGINT (2)) を出力しプログラムを終了しています。
$ gcc -o set_sigh set_sigh.c $ ./set_sigh Stop me ! Stop me ! Stop me ! ^C ... Caught the SIGINT (2) <--- Ctrl + C 押下で handler 関数が実行されるなお、その他の API として signal() がありますが、こちらは移植性の観点から非推奨となっています (古い API)。特別な理由が無い限りは使用を避けた方が良いと思います 8。
6. シグナルブロック
SIGKILL と SIGSTOP 以外のシグナルは、プロセス毎にブロックすることができます。
たとえば、あるプロセスが SIGINT を受信した場合、通常は標準動作によりプロセスが終了させられますが、SIGINT をブロックしていた場合は、SIGINT を受信しても、シグナル無視のように無反応となります。
シグナル無視との違いは、受信したシグナルを保留するか否かです。シグナル無視の場合はシグナルを保留しませんが、シグナルブロックの場合は保留できます。そのため、ブロックが解除されると、再度シグナルを受信する必要なく処理できます 9。
シグナルブロックの設定例
シグナルブロックを設定するには sigprocmask() などの API (C 言語) を使用します 10。
次のサンプルプログラム (block_sig.c) では、SIGINT をブロックしてから 5 秒後に SIGINT のブロックを解除してみます。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> int main(void) { unsigned int sec = 5; sigset_t set; // シグナル集合を空にし、SIGINT 追加 sigemptyset(&set); sigaddset(&set, SIGINT); // SIGINT をブロック (保留) する sigprocmask(SIG_BLOCK, &set, NULL); printf("Block the SIGINT for %d sec\n", sec); sleep(sec); // SIGINT のブロックを解除する printf("\nPassed %d sec, unblock the SIGINT\n", sec); sigprocmask(SIG_UNBLOCK, &set, NULL); printf("Done !\n"); return 0; }実行例では SIGINT ブロック後に SIGINT を送信していますが、SIGNINT 受信時の動作が実行されるのはブロック解除後となっているのが分かります。
$ gcc -o block_sig block_sig.c $ ./block_sig Block the SIGINT for 5 sec ^C^C^C^C^C^C^C^C^C^C <--- Ctrl + C 押下しても反応しない Passed 5 sec, unblock the SIGINT <--- SIGINT により終了するため、その後の "Done !" が無出力7. シグナルの状態確認
シグナルブロック (保留)、無視といった状態は
/proc/<PID>/status
から確認できます。$ cat /proc/29498/status Name: bash --- snip --- SigQ: 0/29305 // このプロセスの実 UID 宛にキューイングされたシグナル / キューイングされたシグナルの上限値 SigPnd: 0000000000000000 // 特定プロセス (スレッド) 宛の処理待ちシグナルの数 ShdPnd: 0000000000000000 // スレッドグループ全体宛の処理待ちシグナルの数 SigBlk: 0000000000010000 // ブロックされるシグナル (ビットマスク値) SigIgn: 0000000000380004 // 無視されるシグナル (ビットマスク値) SigCgt: 000000004b817efb // 捕捉待ちのシグナル (ビットマスク値) --- snip ---この中で、SigBlk、SigIgn、SigCgt は複数のシグナルをまとめて表現するために シグナルセット というマスク値で管理されているため、読み方が少しややこしいです。たとえば、SigIgn (0000000000380004) の場合は次のような意味になります (16進数を2進数に変換、1 の位置から対応するシグナルを判断)。つまり、PID 29498 の SigIgn (無視されるシグナル) は 3, 20, 21, 22 に対応する 4 つのシグナルという意味になります。
0000000000380004 (hex) -> 1110000000000000000100 (bit) シグナル名 番号 ||| *---------> SIGQUIT (03) ||*--------------------------> SIGTSTP (20) |*---------------------------> SIGTTIN (21) *----------------------------> SIGTTOU (22)なお、
ps s
でも同じような情報は確認できます。$ ps s UID PID PENDING BLOCKED IGNORED CAUGHT STAT TTY TIME COMMAND 1000 29498 0000000000000000 0000000000010000 0000000000380004 000000004b817efb Ss pts/0 0:00 /bin/bash 1000 29517 0000000000000000 0000000000010000 0000000000380004 000000004b817efb Ss pts/1 0:00 /bin/bash 1000 29572 0000000000000000 0000000000000000 0000000000003000 00000001ef804eff S+ pts/0 0:00 vim kernel/signal.c 1000 29581 0000000000000000 0000000000000000 0000000000000000 0000000000000000 S pts/1 0:00 sleep 100 1000 29588 0000000000000000 0000000000000000 0000000000000000 00000001f3d1fef9 R+ pts/1 0:00 ps s■ カーネル編 (v5.5)
ここからは、カーネル観点での話です。シグナルを送信してからシグナルアクションが処理されるまでの流れを見ていきます。
まずデータ構造を見ていきます。データ構造とは、カーネル実装で登場する構造体のことを指します。シグナルでは多くの構造体が関連しているため、先に把握しておくことで、コードが理解し易くなると思います。
次にカーネル実装 11 を見ていきます。シグナルは次のようにシグナル生成 (1 段階目)、シグナル配送 (2 段階目) という順に段階的な処理構成となっているため、それぞれ見ていきます。
- シグナル生成 (1 段階目) ... シグナルを送信したことを送信先のプロセスのデータ構造に記録する (シグナルを保留する)
- シグナル配送 (2 段階目) ... シグナルに応じて、シグナルアクションを実行する
1. データ構造
シグナルは、次のとおり、 task_struct、signal_struct、sighand_struct など多くの構造体が関連しています。
- task_struct (プロセスディスクリプタ)
- signal_struct (シグナルディスクリプタ)
- sighand_struct (シグナルハンドラディスクリプタ)
- sigpending
- k_sigaction
- sigaction
- thread_info
- siginfo
- kernel_siginfo
大変混乱しやすいと思いますので、個人的に重要だと思う箇所に関しては、以下のように用途ごとにまとめました。
保留中シグナルを示すフラグ
シグナル生成によって、シグナルを受け取ったプロセスは、シグナル配送によって処理されるまでの間、シグナル保留状態 (シグナル配送待ち) となります。この状態は TIF_SIGPENDING というフラグで示され、スレッドフラグ (flags) に記録されます。
シグナル保留用のキュー (特定プロセス用、スレッドグループ全体用)
受信したシグナルは、sigqueue 構造体によって管理され、関連する幾つかの情報 (※1) を持ちます。それらは保留用のキューに入れられます (sigpending の list につなげられます。その際、シグナルの送信宛に応じて 2 種類 (特定プロセス用、スレッドグループ全体用) の内のどちらかのキューが使用されます。
また、保留されたシグナルは signal に記録されます。この値は
/proc/<PID>/status
から確認できます (前述の「シグナルの状態確認」と後述の「おまけ」参照)。(※1) 幾つかの情報とは、次の kernel_siginfo 構造体のメンバを指します。
メンバ 意味 si_signo シグナル番号 si_code シグナルの発生元を示すコード (※2) si_errno シグナル発生を発生させることになった命令のエラーコード __sifilelds union のため条件に応じて si_pid (送信先 pid)、si_uid (送信先 uid) などに変化する (※2) シグナルの発生元を示すコードには次のような値が入ります。
include/uapi/asm-generic/siginfo.h より抜粋
定義名 値 意味 SI_USER 0 kill()、sigsend()、raise() によるシグナル送信 SI_KERNEL 0x80 カーネルによるシグナル送信 SI_QUEUE -1 sigqueue() によるシグナル送信 SI_TIMER -2 POSIX タイマの時間経過によるシグナル送信 SI_MESGQ -3 POSIX メッセージキューの状態変化によるシグナル送信 SI_ASYNCIO -4 非同期 I/O (AIO) 完了によるシグナル送信 SI_SIGIO -5 SIGIO のキューイングによるシグナル送信 SI_TKILL -6 tkill()、tgkill() によるシグナル送信 SI_DETHREAD -7 sent by execve() killing subsidiary threads 12 SI_ASYNCNL -60 getaddrinfo_a() での名前検索完了によるシグナル送信 なお、上記はシグナル共通の値です。特定のシグナルによっては、次のように別の値が入ることもあります 7。
シグナル名 si_code に入る値 SIGBUG BUS_* SIGCHLD CLD_* SIGFPE FPE_* SIGILL ILL_* SIGPOLL/SIGIO POLL_* SIGSEGV SEGV_* SIGTRAP TRAP_* SIGSYS SYS_SECCOMP シグナルブロック情報
sigprocmask() などによってブロックされたシグナル番号は、ブロック情報 (blocked, readl_blocked13) に記録されます。この値は
/proc/<PID>/status
から確認できます (前述の「シグナルの状態確認」 と後述の「おまけ」参照)。シグナルアクション関連の情報
sigaction() などでシグナルアクションの設定を変更すると、シグナルハンドラディスクリプタ (sighand_struct) に幾つかの情報 (※) が格納されます。また、シグナルハンドラディスクリプタの action[x] はシグナル番号 (_NSIG) 個の配列になっているため (action[シグナル番号-1])、シグナル毎に設定されます。
(※) 幾つかの情報とは、次の sigaction 構造体のメンバを指します。
メンバ 意味 sa_flags シグナルの使い方を示す SA_* フラグ7 sa_handler SIG_IGN、SIG_DFL、シグナルハンドラへのポインタなど、シグナルアクションの種類 sa_mask ハンドラ実行時にブロックする (受信を禁止する) シグナル 2. 実装
シグナルを送信してからシグナルアクションが処理される (
kill -TERM 1234
を実行した際の処理) までの流れを見ていきます。以降、「シグナル生成」と「シグナル配送」で分けてそれぞれ見ていきます (「...」 の部分は省略箇所です)
シグナル生成
この段階での主な仕事は、送信先プロセスのデータ構造に「シグナルを送信した (シグナル配送待ちがある)」ということを送信先プロセスのデータ構造に記録し、通知することです (標準動作といったシグナルアクションはシグナル配送で処理されます)。
以降で行うコードリーディングの要約として、シグナル生成における処理の全体像を図示します。なお、関数の呼び出しが多いため、コードリーディングでは、この中でも特に重要な関数 (コア関数) である __send_signal() から読み進めていきます。
__send_signal()
この関数は、送信されたシグナルを配送するかどうかを決め (prepare_signal())、シグナルを保留用キューに入れます (_sigqueue_alloc(), list_add_tail()) その際、保留シグナルの番号を記録します (sigaddset())。また、配送する場合は、プロセスディスクリプタのスレッドフラグに TIFSIGPENDING を立て、配送可能なプロセスを起床します (complete_signal())。
詳細は以降をご参照ください。
1065 static int __send_signal(int sig, struct kernel_siginfo *info, struct task_struct *t, 1066 enum pid_type type, bool force) 1067 { 1068 struct sigpending *pending; 1069 struct sigqueue *q; 1070 int override_rlimit; 1071 int ret = 0, result; .... 1076 if (!prepare_signal(sig, t, force)) 1077 goto ret;1076 行目の prepare_signal() では、シグナルを配送する必要があるかどうかををチェックします。たとえば、シグナルハンドラ (t->sighand->action[sig - 1].sa.sa_handler) に SIG_IGN が設定されている場合や、標準動作が無視 (Ign) のシグナルの場合は、配送する必要がないので、ret ラベルまで飛びます。シグナルがブロックされていた場合は、シグナルを保留する必要があるため、以降の処理を行います。
また、標準動作が停止 (Stop) のシグナルと SIGCONT に対しては以下の処理も行います。
標準動作が停止 (Stop) のシグナル
シグナル保留用キュー (※) から SIGCONT を取り除きます。
SIGCONT
シグナル保留用キュー (※) から全ての停止系シグナルを取り除き、wake_up_state() で __TASK_STOPPED 状態のスレッドを起床します (プロセスを再開します)。
またプロセス (スレッドグループ) が終了処理中 (SIGNAL_STOP_STOPPED) の場合は、処理が完了したことにして、プロセスディスクリプタのフラグ (flags) に SIGNAL_CLD_CONTINUED と SIGNAL_STOP_CONTINUED を立てます。終了処理中でなくとも group_stop_count がカウントされている場合は、既に終了処理が終わったということなので (おそらく)、プロロセスディスクリプタのフラグ (flags) に SIGNAL_CLD_STOPPED と SIGNAL_STOP_CONTINUED を立てます。
(※) 「データ構造」でも説明しましたが、シグナル保留用キューには、「特定プロセス用 (t->pending)」と「スレッドグループ全体用 (t->signal->shared_pending)」の 2 種類があります。ここでは、その両方のキューから該当するシグナルを取り除きます。
1078 1079 pending = (type != PIDTYPE_PID) ? &t->signal->shared_pending : &t->pending;1079 行目では、pending に「特定プロセス用」「スレッドグループ全体用」のどちらのシグナル保留用キューを使用するかを決めています。今回は kill コマンド実行により、type に PIDTYPE_TGID が渡されるため、pending には、スレッドグループ全体用の方が使われます。
1080 /* 1081 * Short-circuit ignored signals and support queuing 1082 * exactly one non-rt signal, so that we can get more 1083 * detailed information about the cause of the signal. 1084 */ 1085 result = TRACE_SIGNAL_ALREADY_PENDING; 1086 if (legacy_queue(pending, sig)) 1087 goto ret;1086 行目の legacy_queue() では、sig が「標準シグナル」かつ「既にシグナル保留用キューに存在するなら」 ret ラベルまで飛びます (標準シグナルは複数の同一のシグナルを受信しても 1 つしか受信できない)。
1088 1089 result = TRACE_SIGNAL_DELIVERED; 1090 /* 1091 * Skip useless siginfo allocation for SIGKILL and kernel threads. 1092 */ 1093 if ((sig == SIGKILL) || (t->flags & PF_KTHREAD)) 1094 goto out_set;コメントのとおりですが、シグナルが SIGKILL OR 送信先がカーネルスレッド (PF_KTHREAD フラグ) の場合は、out_set ラベルまで飛び、以降のシグナル保留キュー登録処理などを行いません。
1096 /* 1097 * Real-time signals must be queued if sent by sigqueue, or 1098 * some other real-time mechanism. It is implementation 1099 * defined whether kill() does so. We attempt to do so, on 1100 * the principle of least surprise, but since kill is not 1101 * allowed to fail with EAGAIN when low on memory we just 1102 * make sure at least one signal gets delivered and don't 1103 * pass on the info struct. 1104 */ 1105 if (sig < SIGRTMIN) 1106 override_rlimit = (is_si_special(info) || info->si_code >= 0); 1107 else 1108 override_rlimit = 0; 11091105 行目の SIGRTMIN は 32 なので、sig が標準シグナルがどうかで処理が分岐します。
真 のルート (1106 行目) の is_si_special()では、info が SEND_SIG_NOINFO (ユーザモードのプロセスからシグナルが送信された) か、SEND_SIGPRIV (カーネルからシグナルが送信された) の場合は 真 を返します。右の info->si_code が 0 以上 (SI_USER か SI_KERNEL) の場合も 真 を返します。
なお、今回は kill コマンド実行により info->si_code に SI_USER が設定されるため、override_rlimit には 真 が入ります。
1110 q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit); 1111 if (q) { 1112 list_add_tail(&q->list, &pending->list); 1113 switch ((unsigned long) info) { 1114 case (unsigned long) SEND_SIG_NOINFO: 1115 clear_siginfo(&q->info); 1116 q->info.si_signo = sig; 1117 q->info.si_errno = 0; 1118 q->info.si_code = SI_USER; 1119 q->info.si_pid = task_tgid_nr_ns(current, 1120 task_active_pid_ns(t)); 1121 rcu_read_lock(); 1122 q->info.si_uid = 1123 from_kuid_munged(task_cred_xxx(t, user_ns), 1124 current_uid()); 1125 rcu_read_unlock(); 1126 break; 1127 case (unsigned long) SEND_SIG_PRIV: 1128 clear_siginfo(&q->info); 1129 q->info.si_signo = sig; 1130 q->info.si_errno = 0; 1131 q->info.si_code = SI_KERNEL; 1132 q->info.si_pid = 0; 1133 q->info.si_uid = 0; 1134 break; 1135 default: 1136 copy_siginfo(&q->info, info); 1137 break; 1138 } ....1110 行目では sigqueue_alloc() にて、シグナル管理用の sigqueue 型 q を新しく確保 (アロケーション) するように試みます。この関数の内部処理では、送信先プロセスの所有者 (ユーザ) が保留するシグナルの数が上限を超えないかどうかをチェックし、超えない場合にアロケーションを試みます。ただし、overriderlimit が 真 なら上限チェックをせずにアロケーションを試みます。
1112 行目から 1138 行目はアロケーション成功時の処理です。
1112 行目の list_add_tail() では、シグナル保留用キューにアロケーションした q を登録し (リストへつなぎ)、1113 行目以降の処理では、引数 info に応じて q へシグナルの情報を格納していきます。
1157 out_set: .... 1159 sigaddset(&pending->signal, sig); ....シグナル保留用キューのシグナルセットに現在のシグナル番号を追加します (前述の「シグナルの状態確認」OR 「データ構造」参照)。
1175 complete_signal(sig, t, type); 1176 ret: 1177 trace_signal_generate(sig, info, t, type != PIDTYPE_PID, result); 1178 return ret; 1179 }1175 行目の complete_signal() については後述します。
1177 行目の trace_signal_generate() は TRACE_EVENT マクロで signal_generate という名前のトレースポイントを作成しているようです。これは、次のようなカーネルトレース情報の出力に利用 (レポート) されています。
kill-5371 [003] 1058202.036613: signal_generate: sig=15 errno=0 code=0 comm=sleep pid 5359 grp=1 res=0complete_signal()
この関数は、送信されたシグナルを __send_signal() で配送すると決めた場合に呼び出されます。
そして、この関数でシグナル配送の準備ができているプロセスを探します。見つけたら該当プロセスのスレッドフラグに TIF_SIGPENDING を立て、起床します (通知します)。
詳細は以降をご参照ください。
984 static void complete_signal(int sig, struct task_struct *p, enum pid_type type) 985 { 986 struct signal_struct *signal = p->signal; 987 struct task_struct *t; 988 989 /* 990 * Now find a thread we can wake up to take the signal off the queue. 991 * 992 * If the main thread wants the signal, it gets first crack. 993 * Probably the least surprising to the average bear. 994 */ 995 if (wants_signal(sig, p)) 996 t = p; ...995 行目の wants_signal() では、次のパターンでシグナル配送可能 (準備ができている) なプロセスを探します。
シグナルがブロックされてない (SIGKILL, SIGSTOP はブロック不可) AND
- プロセスが終了処理中でない (p->flags に PF_ EXITING が無い) AND
- シグナル (sig) が SIGKILL (プロセスの状態やフラグ有無は見ないで強制する)
シグナルがブロックされてない (SIGKILL, SIGSTOP はブロック不可) AND
- プロセスが終了処理中でない (p->flags に PF_ EXITING が無い) AND
- プロセスの状態 (p->state) が一時停止中 (TASK_STOPPED) か、デバッガなどにより停止中 (TASK_TRACED) でない AND
- プロセスが CPU 上で実行中 (カレントプロセス) OR プロセスのスレッドフラグ (p->thread_info->flags) に TIF_SIGPENDING が無い
1021 /* 1022 * Found a killable thread. If the signal will be fatal, 1023 * then start taking the whole group down immediately. 1024 */ 1025 if (sig_fatal(p, sig) && 1026 !(signal->flags & SIGNAL_GROUP_EXIT) && 1027 !sigismember(&t->real_blocked, sig) && 1028 (sig == SIGKILL || !p->ptrace)) {コメントのとおり、Kill 可能なスレッドを探します。次の条件を全て満たす場合、1039 行目から 1042 行目の処理を行います。
- sig_fatal() が 真 (※) AND
- スレッドグループが終了中でない (SIGNAL_GROUP_EXIT は「スレッドグループが終了中」というフラグ) AND
- シグナルがブロックされていない (sigismember() は、t->real_blocked (ブロック情報) に sig が含まれていれば 真) AND
- シグナルが SIGKILL OR ptrace 関連フラグ (p->ptrace) がない時
(※) sig_fatal() は次の 2 パターンで 真 を返します。
シグナルが標準シグナル AND
- 標準動作が無視 (Ign) / 停止 (Stop) のシグナルでない AND
- シグナルアクションが SIG_DFL
シグナルがリアルタイムシグナル AND
- シグナルアクションが SIG_DFL
1029 /* 1030 * This signal will be fatal to the whole group. 1031 */ 1032 if (!sig_kernel_coredump(sig)) {sig_kernel_coredump() は、標準動作がコアダンプ (Core) のシグナルでない時 真 を返します。
1033 /* 1034 * Start a group exit and wake everybody up. 1035 * This way we don't have other threads 1036 * running and doing things after a slower 1037 * thread has the fatal signal pending. 1038 */ 1039 signal->flags = SIGNAL_GROUP_EXIT; 1040 signal->group_exit_code = sig; 1041 signal->group_stop_count = 0; 1042 t = p; 1043 do { 1044 task_clear_jobctl_pending(t, JOBCTL_PENDING_MASK); 1045 sigaddset(&t->pending.signal, SIGKILL); 1046 signal_wake_up(t, 1); 1047 } while_each_thread(p, t); 1048 return; 1049 } 1050 }1039 行目から 1042 行目までの処理を行った後は、do-while でスレッドグループ内を走査し終わるまで、次に示す処理をループします。
- 1044 行目の task_clear_jobctl_pending() で、保留中のジョブコントロールフラグ (t->jobctl) をクリア
- 1045 行目の sigaddset() で、シグナル保留用キューのシグナルセットに SIGKILL (番号) を追加
- 1046 行目の signal_wake_up() で、スレッドフラグ (t->flags) に TIF_SIGPENDING を立て、TASK_WAKEKILL (SIGKILL などの重要なシグナルを受信可能) か TASK_INTERRUPTIBLE (何らかのイベント待ち) のプロセスを起床 (通知)
ループを抜けると、この関数での処理を終了します。
1052 /* 1053 * The signal is already in the shared-pending queue. 1054 * Tell the chosen thread to wake up and dequeue it. 1055 */ 1056 signal_wake_up(t, sig == SIGKILL); 1057 return; 1058 }ここまで到達できた場合、signal_wake_up() で、スレッドフラグ (t->flags) に TIF_SIGPENDING を立て、TASK_INTERRUPTIBLE 状態のプロセスを起床します (保留シグナルを通知)。また、シグナルが SIGKILL の場合は TASK_WAKEKILL 状態のプロセスも起床します。
以上で、「シグナル生成」の処理は終わりです。続いて、「シグナル配送」を見ていきます。
シグナル配送
この段階での主な仕事は、シグナルハンドラや標準動作などのシグナルアクションを実行することです。ただし、この処理はプロセスが保留シグナルを持つ (前述の「シグナル生成」で説明した TIF_SIGPENDING フラグを持つ) 場合に限ります。
以降で行うコードリーディングの要約として、シグナル配送における処理の全体像を図示します。なお、シグナル配送は一部アーキテクチャに依存するコードがありますが、ここでは x86 (arch/x86 配下) のコードを見ていきます。
詳細は以降をご参照ください。
exit_to_usermode_loop()
この関数は、カーネルモードからユーザモードへ戻る度 (割り込み/例外ハンドラ、システコール処理後など) に呼び出され、スレッドフラグをチェックします。その際、TIF_SIGPENDING があると、do_signal() を呼びだします (160 行目)。
arch/x86/entry/common.c (L136)
136 static void exit_to_usermode_loop(struct pt_regs *regs, u32 cached_flags) 137 { 138 /* 139 * In order to return to user mode, we need to have IRQs off with 140 * none of EXIT_TO_USERMODE_LOOP_FLAGS set. Several of these flags 141 * can be set at any time on preemptible kernels if we have IRQs on, 142 * so we need to loop. Disabling preemption wouldn't help: doing the 143 * work to clear some of the flags can sleep. 144 */ 145 while (true) { 146 /* We have work to do. */ 147 local_irq_enable(); ... 158 /* deal with pending signal delivery */ 159 if (cached_flags & _TIF_SIGPENDING) 160 do_signal(regs); ... 171 /* Disable IRQs and retry */ 172 local_irq_disable(); 173 174 cached_flags = READ_ONCE(current_thread_info()->flags); 175 176 if (!(cached_flags & EXIT_TO_USERMODE_LOOP_FLAGS)) 177 break; 178 } 179 }do_signal()
この関数は、標準動作の実行 (get_signal()) やシグナルハンドラの実行 (handle_signal()) を行います。また、必要に応じてシステコールの再実行や特定のシステムコールによって一時的に書き換えられたシグナルブロック情報の復元を行います。
arch/x86/kernel/signal.c (L806)
806 /* 807 * Note that 'init' is a special process: it doesn't get signals it doesn't 808 * want to handle. Thus you cannot kill init even with a SIGKILL even by 809 * mistake. 810 */ 811 void do_signal(struct pt_regs *regs) 812 { 813 struct ksignal ksig; 814 815 if (get_signal(&ksig)) { 816 /* Whee! Actually deliver the signal. */ 817 handle_signal(&ksig, regs); 818 return; 819 } 820815 行目の get_signal() では標準動作の実行と保留用キューからのシグナル取り出しを行います。その結果が 真 (取り出せたら) なら handle_signal() を実行しシグナルハンドラの実行を行います。偽 (取り出せなかった) なら以降の処理を行います (get_signal()、handle_signal() は重要な関数なので詳細は後述します)。
821 /* Did we come from a system call? */ 822 if (syscall_get_nr(current, regs) >= 0) { 823 /* Restart the system call - no handlers present */ 824 switch (syscall_get_error(current, regs)) { 825 case -ERESTARTNOHAND: 826 case -ERESTARTSYS: 827 case -ERESTARTNOINTR: 828 regs->ax = regs->orig_ax; 829 regs->ip -= 2; 830 break; 831 832 case -ERESTART_RESTARTBLOCK: 833 regs->ax = get_nr_restart_syscall(regs); 834 regs->ip -= 2; 835 break; 836 } 837 }ここはシステムコールの再実行に関する処理です (システムコールの処理中にシグナルを受信した場合、エラーを吐いて処理を中断することがあるため、このような処理があります)。
822 行目の syscall_get_nr() は、ユーザモードのレジスタ (regs) からシステムコール番号を取得します。それが 0 以上だったら、824 行目の syscall_get_erro() でエラー番号を取得し、そのエラー番号に応じてカーネルがシステムコールを再実行します。
なお、システムコールの再実行については「Linuxのシステムコール再実行について」という記事に詳しく書かれていました。regs->ip -= 2 の意味など詳細はこちらをご参照ください。
838 839 /* 840 * If there's no signal to deliver, we just put the saved sigmask 841 * back. 842 */ 843 restore_saved_sigmask(); 844 }restore_saved_sigmask() では、ppoll、pselect、epoll、sigsuspend などのシステムコールによって一時的に書き換えられたシグナルブロック情報 (task_struct->blocked) を、あらかじめ退避させていたシグナルブロック情報 (task_struct->saved_sigmask) から復元します。
get_signal()
この関数は、シグナル保留用キューからシグナルを取り出し、シグナルに応じて標準動作の実行を行います。
2521 bool get_signal(struct ksignal *ksig) 2522 { 2523 struct sighand_struct *sighand = current->sighand; 2524 struct signal_struct *signal = current->signal; 2525 int signr; .... 2540 relock: .... 2542 /* 2543 * Every stopped thread goes here after wakeup. Check to see if 2544 * we should notify the parent, prepare_signal(SIGCONT) encodes 2545 * the CLD_ si_code into SIGNAL_CLD_MASK bits. 2546 */ 2547 if (unlikely(signal->flags & SIGNAL_CLD_MASK)) { 2548 int why; 2549 2550 if (signal->flags & SIGNAL_CLD_CONTINUED) 2551 why = CLD_CONTINUED; 2552 else 2553 why = CLD_STOPPED; 2554 2555 signal->flags &= ~SIGNAL_CLD_MASK; 2556 2557 spin_unlock_irq(&sighand->siglock); 2558 2559 /* 2560 * Notify the parent that we're continuing. This event is 2561 * always per-process and doesn't make whole lot of sense 2562 * for ptracers, who shouldn't consume the state via 2563 * wait(2) either, but, for backward compatibility, notify 2564 * the ptracer of the group leader too unless it's gonna be 2565 * a duplicate. 2566 */ 2567 read_lock(&tasklist_lock); 2568 do_notify_parent_cldstop(current, false, why); 2569 2570 if (ptrace_reparented(current->group_leader)) 2571 do_notify_parent_cldstop(current->group_leader, 2572 true, why); 2573 read_unlock(&tasklist_lock); 2574 2575 goto relock; 2576 } ....2543 行目のコメントにも少し書かれていますが、2547 行目のフラグ (SIGNAL_CLD_MASK == SIGNAL_CLD_STOPPED or SIGNAL_CLD_CONTINUED )は、シグナル生成 (_send_signal()) で呼び出される prepare_signal() で SIGCONT の処理をするときに設定されるます。このフラグが設定されている場合は、donotify_parent_cldstop() で親にシグナル (SIGCHLD) を送信します。
2588 for (;;) { 2589 struct k_sigaction *ka; .... 2616 /* 2617 * Signals generated by the execution of an instruction 2618 * need to be delivered before any other pending signals 2619 * so that the instruction pointer in the signal stack 2620 * frame points to the faulting instruction. 2621 */ 2622 signr = dequeue_synchronous_signal(&ksig->info); 2623 if (!signr) 2624 signr = dequeue_signal(current, ¤t->blocked, &ksig->info); 2625 2626 if (!signr) 2627 break; /* will return 0 */ ....2622 行目の dequeue_synchronous_signal() で同期シグナル (SIG[SEGV|BUS|ILL|TRAP|FPE|SYS]) をキューから取り出します。なければ、2624 行目の dequeue_signal() で非同期シグナルをキューから取り出します。それでもなければ、break でループを抜けこの関数での処理を終了します。
2635 ka = &sighand->action[signr-1]; 2636 2637 /* Trace actually delivered signals. */ 2638 trace_signal_deliver(signr, &ksig->info, ka);2638 行目の trace_signal_deliver() は TRACE_EVENT マクロで signal_deliver という名前のトレースポイントを作成するようです。これは、次のようなカーネルトレース情報の出力に利用 (レポート) されています。定義元のコメントを見るに、ここに到達できた時点でシグナルが配送されたことになるようです。
trace-cmd-17636 [001] 607498.455844: signal_deliver: sig=2 errno=0 code=128 sa_handler=5567b90d1540 sa_flags=1400000以降は、シグナル無視、シグナルハンドラ、標準動作などのシグナルアクションに関する処理です。ka (2635 行目) の内容により分岐します。
2639 2640 if (ka->sa.sa_handler == SIG_IGN) /* Do nothing. */ 2641 continue;C 言語 API のsigaction() などでシグナルアクションに無視 (SIG_IGN) を設定していた場合は、何もしません。
2642 if (ka->sa.sa_handler != SIG_DFL) { 2643 /* Run the handler. */ 2644 ksig->ka = *ka; 2645 2646 if (ka->sa.sa_flags & SA_ONESHOT) 2647 ka->sa.sa_handler = SIG_DFL; 2648 2649 break; /* will return non-zero "signr" value */ 2650 }2642 行目は、SIG_DFL でない場合 (つまり、シグナルハンドラを設定していた場合) 、2644 行目で ksig->ka に ka のアドレスを入れます。これは get_signal() を抜けたあとの handle_signal() でユーザプロス用のレジスタに設定する時に使用します。
2646 行目の SA_ONESHOT (sigaction() で設定可能なフラグ) はシグナルハンドラ実行後にシグナルアクションをデフォルト (SIG_DFL) に戻すという意味です。つまり、シグナルハンドラの実行するのは、初回のシグナル受信時だけです。
最後に break でループを抜け、この関数での処理を終えます。この場合 signr には取り出したシグナル番号が入っているはずなので、後述の handle_signal() が実行されます。
2651 2652 /* 2653 * Now we are doing the default action for this signal. 2654 */ 2655 if (sig_kernel_ignore(signr)) /* Default is nothing. */ 2656 continue; ....ここからは、標準動作の実行に関する処理です。
2655 行目の sig_kernel_ignore() は signr の標準動作が無視 (Ign) なら 真 を返します。その場合、何もしません。
2671 2672 if (sig_kernel_stop(signr)) { 2673 /* 2674 * The default action is to stop all threads in 2675 * the thread group. The job control signals 2676 * do nothing in an orphaned pgrp, but SIGSTOP 2677 * always works. Note that siglock needs to be 2678 * dropped during the call to is_orphaned_pgrp() 2679 * because of lock ordering with tasklist_lock. 2680 * This allows an intervening SIGCONT to be posted. 2681 * We need to check for that and bail out if necessary. 2682 */ 2683 if (signr != SIGSTOP) { 2684 spin_unlock_irq(&sighand->siglock); 2685 2686 /* signals can be posted during this window */ 2687 2688 if (is_current_pgrp_orphaned()) 2689 goto relock; 2690 2691 spin_lock_irq(&sighand->siglock); 2692 } 2693 2694 if (likely(do_signal_stop(ksig->info.si_signo))) { 2695 /* It released the siglock. */ 2696 goto relock; 2697 } 2698 2699 /* 2700 * We didn't actually stop, due to a race 2701 * with SIGCONT or something like that. 2702 */ 2703 continue; 2704 }2672 行目の sig_kernel_stop() は signr の標準動作がプロセス一時停止 (Stop) のシグナルなら 真 を返します。真 なら、2694 行目の do_signal_stop() でプロセスディスクリプタのフラグ (flags) に SIGNAL_STOP_STOPPED を立て、プロセスの一時停止 (TASK_STOPPED) を行います。
ただし、シグナルが SIGSTOP 以外かつ、プロセスグループが孤児 (親なし) の 場合は do_signal_stop() を実行しません (一時停止しません)。
2705 2706 fatal: 2707 spin_unlock_irq(&sighand->siglock); .... 2711 /* 2712 * Anything else is fatal, maybe with a core dump. 2713 */ 2714 current->flags |= PF_SIGNALED; 2715 2716 if (sig_kernel_coredump(signr)) { 2717 if (print_fatal_signals) 2718 print_fatal_signal(ksig->info.si_signo); 2719 proc_coredump_connector(current); 2720 /* 2721 * If it was able to dump core, this kills all 2722 * other threads in the group and synchronizes with 2723 * their demise. If we lost the race with another 2724 * thread getting here, it set group_exit_code 2725 * first and our do_group_exit call below will use 2726 * that value and ignore the one we pass it. 2727 */ 2728 do_coredump(&ksig->info); 2729 } ....2716 行目の sig_kernel_coredump() は signr の標準動作がコアダンプ (Core) なら 真 を返します。真 なら 2728 行目の do_coredump() でシグナルディスクリプタのフラグ (flags) に SIGNAL_GROUP_COREDUMP を立て、コアダンプ生成処理を行います。
2731 /* 2732 * Death signals, no core dump. 2733 */ 2734 do_group_exit(ksig->info.si_signo); 2735 /* NOTREACHED */ 2736 }signr の標準動作がプロセス終了 (Term) やコアダンプ (Core) の場合、2724 行目の do_group_exit() でシグナルディスクリプタのフラグ (flags) に SIGNAL_GROUP_EXIT を立て、プロセス (スレッドグループ) の終了を行います。
なお、2736 行目の
}
は 2588 行目のfor (;;) {
の対になる部分です。このループは、2627 行目の break か 2649 行目の break で抜けることができます。前者はシグナル保留用キューから取り出すものが無くなった場合で、後者はシグナルアクション (sa_handler) にシグナルハンドラが設定されていた場合です。2737 spin_unlock_irq(&sighand->siglock); 2738 2739 ksig->sig = signr; 2740 return ksig->sig > 0; 2741 }最後にキューから取り出したシグナル番号が 0 より大きいかどうかを判定します。大きい場合はシグナルを取り出せたということで 真 を返し、次の handle_signal() へ進みます。
handle_signal()
この関数は、システムコールの再実行 (必要なら) と、シグナルハンドラ実行用にユーザモード時のスタックフレームを設定します。
arch/x86/kernel/signal.c (L71)
710 static void 711 handle_signal(struct ksignal *ksig, struct pt_regs *regs) 712 { 713 bool stepping, failed; 714 struct fpu *fpu = ¤t->thread.fpu; 715 ... 719 /* Are we from a system call? */ 720 if (syscall_get_nr(current, regs) >= 0) { 721 /* If so, check system call restarting.. */ 722 switch (syscall_get_error(current, regs)) { 723 case -ERESTART_RESTARTBLOCK: 724 case -ERESTARTNOHAND: 725 regs->ax = -EINTR; 726 break; 727 728 case -ERESTARTSYS: 729 if (!(ksig->ka.sa.sa_flags & SA_RESTART)) { 730 regs->ax = -EINTR; 731 break; 732 } 733 /* fallthrough */ 734 case -ERESTARTNOINTR: 735 regs->ax = regs->orig_ax; 736 regs->ip -= 2; 737 break; 738 } 739 } 740 ...do_signal() でも出ましたが14、ここにもシステムコールの再実行に関する処理があります。ただし、カーネルがシステムコールを再実行するケースは次の 2 パターンのみです。
- エラーが -ERESTARTSYS (728 行目) かつ sa_flags に SA_RESTART (sigaction() で設定可能なフラグ) が立っている場合
- エラーが 734 行目の -ERESTARTNOINTR (734 行目) だった場合
それ以外の場合、カーネルはシステムコールを再実行せず regs->ax に -EINTR (関数呼び出しが割り込まれたというエラー) を設定します。システムコールを再実行するかどうかは、このエラーからユーザ (ユーザプログラムの条件) が判断します。
750 failed = (setup_rt_frame(ksig, regs) < 0); ... 751 if (!failed) { 752 /* 753 * Clear the direction flag as per the ABI for function entry. 754 * 755 * Clear RF when entering the signal handler, because 756 * it might disable possible debug exception from the 757 * signal handler. 758 * 759 * Clear TF for the case when it wasn't set by debugger to 760 * avoid the recursive send_sigtrap() in SIGTRAP handler. 761 */ 762 regs->flags &= ~(X86_EFLAGS_DF|X86_EFLAGS_RF|X86_EFLAGS_TF); 763 /* 764 * Ensure the signal handler starts with the new fpu state. 765 */ 766 fpu__clear(fpu); 767 } 768 signal_setup_done(failed, ksig, stepping); 769 }750 行目の setup_rt_frame() では、シグナルハンドラ実行用にユーザモード時のスタックフレームを設定します。スタックフレームには、シグナルハンドラ実行に必要な情報 (シグナル番号、siginfo 構造体 の情報や rt_sigreturn() システムコールをユーザモードプロセスから呼び出すための情報が格納されています。rt_sigreturn() はシグナルハンドラ実行後にカーネルモードへ復帰するのに必要です。
また、setup_rt_frame() での設定が成功した場合は、762、766 行目でフラグ (regs->flags) や FPU state (浮動小数点数レジスタ) をクリアします (理由は不明)。
そして、最後 (768 行目) の signal_setup_done() では setup_rt_frame() での設定成否で処理が分岐します。
成功した場合:
sigorsets() でカレントプロセスのブロック情報 (current->blocked) と sigaction() でユーザが設定したブロック情報 (sigaction->sa_mask) の論理和 (OR) を取り、それをカレントプロセスのブロック情報に設定します。また、スレッドフラグに TIF_SINGLESTEP (デバッガで使用) が設定されていた場合は、ptrace_notify(SIGTRAP) を実行します。失敗した場合:
force_sigsegv() でカレントプロセスに SIGSEGV を送信します。以上で、「シグナル配送」の処理は終わりです。
3. おまけ
/proc/<PID>/status
にあるシグナル関連のコード
/proc/<PID>/status
にある SigPnd、ShdPnd、などの値はどこに由来するのかコード上から調べました。task_sig() に答えがありました。たとえば、SigBlk は 283 行目で p->blocked を入れています。
266 static inline void task_sig(struct seq_file *m, struct task_struct *p) 267 { 268 unsigned long flags; 269 sigset_t pending, shpending, blocked, ignored, caught; 270 int num_threads = 0; 271 unsigned int qsize = 0; 272 unsigned long qlim = 0; 273 274 sigemptyset(&pending); 275 sigemptyset(&shpending); 276 sigemptyset(&blocked); 277 sigemptyset(&ignored); 278 sigemptyset(&caught); 279 280 if (lock_task_sighand(p, &flags)) { 281 pending = p->pending.signal; 282 shpending = p->signal->shared_pending.signal; 283 blocked = p->blocked; 284 collect_sigign_sigcatch(p, &ignored, &caught); 285 num_threads = get_nr_threads(p); 286 rcu_read_lock(); /* FIXME: is this correct? */ 287 qsize = atomic_read(&__task_cred(p)->user->sigpending); 288 rcu_read_unlock(); 289 qlim = task_rlimit(p, RLIMIT_SIGPENDING); 290 unlock_task_sighand(p, &flags); 291 } 292 293 seq_put_decimal_ull(m, "Threads:\t", num_threads); 294 seq_put_decimal_ull(m, "\nSigQ:\t", qsize); 295 seq_put_decimal_ull(m, "/", qlim); 296 297 /* render them all */ 298 render_sigset_t(m, "\nSigPnd:\t", &pending); 299 render_sigset_t(m, "ShdPnd:\t", &shpending); 300 render_sigset_t(m, "SigBlk:\t", &blocked); 301 render_sigset_t(m, "SigIgn:\t", &ignored); 302 render_sigset_t(m, "SigCgt:\t", &caught); 303 }SIGKILL、SIGSTOP がブロックできない理由
SIGKILL と SIGSTOP がブロックできない理由をコード上から探してみました。
シグナルブロックに使う sigprocmask() 実行時に呼び出される sys_rt_sigprocmask() に答えがありました。3025 行目の sigdelsetmask() で新しく設定したブロック情報 (new_set) から SIGKILL と SIGSTOP を削除しています (つまり、SIGKILL と SIGSTOP をブロックするように設定しても実際は設定されない)。
3003 /** 3004 * sys_rt_sigprocmask - change the list of currently blocked signals 3005 * @how: whether to add, remove, or set signals 3006 * @nset: stores pending signals 3007 * @oset: previous value of signal mask if non-null 3008 * @sigsetsize: size of sigset_t type 3009 */ 3010 SYSCALL_DEFINE4(rt_sigprocmask, int, how, sigset_t __user *, nset, 3011 sigset_t __user *, oset, size_t, sigsetsize) 3012 { 3013 sigset_t old_set, new_set; 3014 int error; .... 3022 if (nset) { 3023 if (copy_from_user(&new_set, nset, sizeof(sigset_t))) 3024 return -EFAULT; 3025 sigdelsetmask(&new_set, sigmask(SIGKILL)|sigmask(SIGSTOP)); 3026 3027 error = sigprocmask(how, &new_set, NULL); 3028 if (error) 3029 return error; 3030 } .... 3037 return 0; 3038 }なお、シグナルブロックは sigaction() の sa_mask でも設定できますので、こちらも見てみます。
sigaction() の場合は sys_rt_sigaction() -> do_sigaction() の 3967、3968 行目で同様の処理を行っていました。
3949 int do_sigaction(int sig, struct k_sigaction *act, struct k_sigaction *oact) 3950 { 3951 struct task_struct *p = current, *t; 3952 struct k_sigaction *k; 3953 sigset_t mask; .... 3966 if (act) { 3967 sigdelsetmask(&act->sa.sa_mask, 3968 sigmask(SIGKILL) | sigmask(SIGSTOP)); .... 3988 } .... 3991 return 0; 3992 }SIGKILL、SIGSTOP を無視 or シグナルハンドラを設定できない理由
SIGKILL、SIGSTOP が無視 or 動作変更できない理由もコード上から調べてみました。
sigaction() 実行時に呼び出される sys_rt_sigaction() -> do_sigaction() の 3955 行目に答えがありました。一番右の条件で、act が有効値かつ sig_kernel_only() が 真 (シグナルが SIGKILL か SIGSTOP の場合) の時に -EINVAL でエラー (引数が無効) で失敗します。 sigaction() に SIGKILL および SIGSTOP を渡すと失敗するので、当然ですが、無視 (SIG_IGN) もシグナルハンドラ (ユーザ定義の関数) も設定できないということになります。
3949 int do_sigaction(int sig, struct k_sigaction *act, struct k_sigaction *oact) 3950 { 3951 struct task_struct *p = current, *t; 3952 struct k_sigaction *k; 3953 sigset_t mask; 3954 3955 if (!valid_signal(sig) || sig < 1 || (act && sig_kernel_only(sig))) 3956 return -EINVAL; .... 3966 if (act) { .... 3969 *k = *act; 3970 /* 3971 * POSIX 3.3.1.3: 3972 * "Setting a signal action to SIG_IGN for a signal that is 3973 * pending shall cause the pending signal to be discarded, 3974 * whether or not it is blocked." 3975 * 3976 * "Setting a signal action to SIG_DFL for a signal that is 3977 * pending and whose default action is to ignore the signal 3978 * (for example, SIGCHLD), shall cause the pending signal to 3979 * be discarded, whether or not it is blocked" 3980 */ 3981 if (sig_handler_ignored(sig_handler(p, sig), sig)) { 3982 sigemptyset(&mask); 3983 sigaddset(&mask, sig); 3984 flush_sigqueue_mask(&mask, &p->signal->shared_pending); 3985 for_each_thread(p, t) 3986 flush_sigqueue_mask(&mask, &t->pending); 3987 } 3988 } .... 3991 return 0; 3992 }■ 参考文献
- Linux システムプログラミング (9 章シグナル)
- Linux プログラミングインタフェース (20 章シグナル: 基礎、21 章シグナル: シグナルハンドラ, 22 章シグナル: 応用)
- 詳解 Linux カーネル 第3版 (11 章シグナル)
- Linux カーネル解読室 (8 章 シグナル処理)
- Mastering Linux Kernel Development (Chapter 3: Signal Management)
- kernel source code (v5.5)
- Using the TRACE_EVENT() macro (Part 1)
- TASK_KILLABLE: Linux での新しいプロセスの状態
man 1 bash
man 2 setrlimit
man 2 seccomp
man 2 signal
man 2 sigaction
man 3 getaddrinfo_a
man 5 proc
man 7 signal
man 7 pipe
詳細は
man 7 signal
をご参照ください。 ↩詳細は
man 5 core
をご参照ください。 ↩詳細は
man 7 pipe
ご参照ください。 ↩詳細は
man 2 setrlimit
ご参照ください。 ↩詳細は
man 2 seccomp
ご参照ください。 ↩詳細は
man 1 bash
をご参照ください。 ↩詳細は
man 2 sigaction
をご参照ください。 ↩詳細は
man 2 signal
をご参照ください。 ↩シグナル無視の場合も解除はできますが、シグナル無視の状態で受信したシグナルは保留されないため、解除された後も処理されません。無視を解除した上で再度シグナルを受信する必要があります。 ↩
詳細は
man 2 sigprocmask
をご参照ください。 ↩カーネルバージョンは調査時点での最新である v5.5 を使用します。 ↩
man 2 execve
カーネルソースなどを調べましたが、この原文以上の情報が見当たらず、意味がよく理解できなかったため、原文のまま記載します。 ↩real_blocked は、rt_sigtimedwait(), rt_sigtimedwait_time32(), rt_sigtimedwait_time64() システムコール使用時に設定されるようです。 ↩
do_signal() でのシステムコール再実行は、handle_signal() を実行しない場合に通るパス (get_signal() が 偽 の時) なので、ここでのシステムコール再実行処理とは重複しません。 ↩
- 投稿日:2020-05-24T21:05:30+09:00
Linux シグナルの基本と仕組み (カーネル)
この記事は、Linux シグナルの基本と仕組み (カーネル) について調査したことのまとめです。
シグナルは普段から利用しているものの仕組みについては理解してなかったので、カーネル勉強の題材として調べてみました。想像以上に複雑でボリュームがあったため、書き切れていない部分 (調査しきれなかった部分) もありますが、一通りの流れ (仕組み) は理解できたと思います。
なお、この記事は「■ 基本編 」と「■ カーネル編 (v5.5)」に分かれています。仕組みを理解するには基本も知る必要があると思い、このような構成となっています。基本を理解されている方は読み飛ばしてください。
■ 基本編
はじめにシグナルの基本について、ざっと整理します。
なお、例で登場するコマンドや API (C 言語) の細かい使い方やエラー処理などは省きます。詳細は man や参考文献の情報等をご活用ください。
1. シグナルとは
プロセスやプロセスグループへ様々なイベントを通知するためにあるカーネルの機能です (ソフトウェア割り込み)。イベントの通知は様々な場所 (自分/他プロセス、カーネル) から行うことが可能で、次のようなことができます。
- ハングしたプロセスにシグナルを送信して強制終了させる
- シグナルを送信してプロセスの処理を一時停止・再開させる
- ハードウェア例外 (0 除算、メモリアクセス違反など) 時にシグナルを送信してプロセスを終了させる
- シグナルを送信する特殊なキー (Ctrl + C など) を入力しプロセスを終了させる
- シグナル受信時にユーザ定義の関数 (シグナルハンドラ) を実行させる
なお、シグナルは次のような流れで処理されます。詳細は追々見ていきます。
2. シグナル利用例
イメージし易いように、ユーザ視点でのシグナル利用例を 3 つ記載します。
kill コマンド
kill コマンドでシグナル (SIGTERM) を送り sleep プロセスを終了させます。
$ ps PID TTY TIME CMD 2662 pts/0 00:00:00 bash 2679 pts/0 00:00:00 sleep 2685 pts/0 00:00:00 ps $ kill 2679 [1]+ Terminated sleep 1000 $ ps PID TTY TIME CMD 2662 pts/0 00:00:00 bash 2696 pts/0 00:00:00 ps割り込みキー
割り込みキー (Ctrl + C) でシグナル (SIGINT) を送り、無限ループするコマンド (プロセスグループ) を終了させます。
$ while :; do sleep 1; echo "Stop me !"; done Stop me ! Stop me ! Stop me ! ^Cプログラム (C 言語 API)
以下のように自前のプログラム (send_sig.c) でシグナル (SIGTERM) を送り sleep プロセスを終了させることもできます。
#include <signal.h> #include <stdlib.h> int main(int argc, char *argv[]) { pid_t pid = atoi(argv[1]); kill(pid, SIGTERM); return 0; }コンパイル & 実行例
$ ps PID TTY TIME CMD 30285 pts/0 00:00:00 bash 30597 pts/0 00:00:00 sleep 30631 pts/0 00:00:00 ps $ gcc -o send_sig send_sig.c $ ./send_sig 30597 [1]+ Terminated sleep 1000 $ ps PID TTY TIME CMD 30285 pts/0 00:00:00 bash 30663 pts/0 00:00:00 ps3. シグナル番号とシグナル名
シグナルは、用途に応じて番号と名前が割り振られています。
$ kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAXただし、シグナル番号の一部はアーキテクチャに依存するため、上述の実行例 (x86_64) とは番号が異なる場合があります 1。
4. 標準シグナル、リアルタイムシグナル
シグナルは、標準シグナルとリアルタイムシグナルの 2 種類に大別できます。
ざっくりとした違いは、
- 標準シグナル ... SIGKILL など伝統的に使用しているシグナル (1 〜 31 番)
- リアルタイムシグナル ... 標準シグナルの拡張版 (32 〜 64 番)
です。もう少し細かい違いは、次のとおりです。
標準シグナル リアルタイムシグナル 新/旧 旧 新 シグナル名 シグナル毎に様々 SIGRTMIN(+n), SIGRTMAX(-n) シグナル番号 1 〜 31 32 〜 64 標準動作 (詳細は後述) シグナル毎に様々 プロセス終了 (全シグナル) 用途 シグナル毎に様々 ユーザが定義する (全シグナル) 複数の同じシグナル
受信時の挙動1 つのみ受信 全て受信 複数の同じシグナル
送信時の順序規定なし 送信された順に到着 複数の異なるシグナル
送信時の順序規定なし シグナル番号の小さい順に到着 なお、本稿ではこれ以上深くリアルタイムシグナルについては言及しません 1。
5. シグナルアクション
シグナル受信時の動作 (シグナルアクション) には、次の 3 種類があります。この動作は後述の sigaction() などから変更できます。ただし、SIGKILL と SIGSTOP は標準動作以外にはできません。
無視
シグナルを受信しても何もしません。
(設定するには、後述の sigaction() などで sa_handler に SIG_IGN を指定する)
標準動作
シグナル受信すると、シグナル毎に定義された標準動作 (後述の Term, Ign, Core, Stop, Cont) を実行します。
(設定するには、後述の sigaction() などで sa_handler に SIG_DFL を指定 (デフォルト) する)
シグナルハンドラ
シグナル受信すると、ユーザが定義した動作を実行します。
(設定するには、後述の sigaction() などで sa_handler へユーザ定義の関数を指定する)
標準動作
標準動作は、シグナル毎に定義されており、次の 5 種類があります。
動作名 意味 Term プロセス終了 Ign 何もしない (sigaction() で設定できる SIG_IGN と同じ) Core プロセス終了とコアダンプ生成 2 Stop プロセス一時停止 (TASK_STOPPED 状態に遷移させる) Cont 一時停止したプロセスの再開 (TASK_STOPPED 状態からの復帰) ただし、これは標準シグナルに限ります。リアルタイムシグナルの場合は Term のみです (次図参照)。
標準シグナルと対応する標準動作
1 から 31 番までの標準シグナルは、各々に紐づく標準動作を持ちます。
シグナル名 シグナル番号 (x86_64) 標準動作 意味 SIGHUP 1 Term 制御端末のハングアップ検出 or 制御プロセスの死 SIGINT 2 Term キーボードからの割り込み (Ctrl + C) SIGQUIT 3 Core キーボードからの終了 (Ctrl + Q) SIGILL 4 Core 不正な命令 SIGTRAP 5 Core デバッグ用のトレース/ブレークポイントシグナル SIGABRT 6 Core abort(3) からの中断シグナル SIGBUS 7 Core バスエラー (不正なメモリアクセス) SIGFPE 8 Core 浮動小数点例外 SIGKILL 9 Term Kill シグナル SIGUSR1 10 Term ユーザ定義のシグナル (その1) SIGSEGV 11 Core 不正なメモリ参照 SIGUSR2 12 Term ユーザ定義のシグナル (その2) SIGPIPE 13 Term パイプ破壊 (読み手の無いパイプへの書き出し) 3。 SIGALRM 14 Term alarm(2) からのタイマーシグナル SIGTERM 15 Term 終了シグナル SIGSTKFLT 16 Term 数値演算プロセッサ (コプロセッサ)におけるスタックフォルト (未使用) SIGCHLD 17 Ign 子プロセスの一時停止 (再開) または終了 SIGCONT 18 Cont 一時停止プロセスの再開 SIGSTOP 19 Stop プロセスの一時停止 SIGTSTP 20 Stop 制御端末からの停止 (Ctrl + Z) SIGTTIN 21 Stop バッググラウンドプロセスが制御端末を読み取った SIGTTOU 22 Stop バッググラウンドプロセスが制御端末へ書き込んだ SIGURG 23 Ign ソケットに帯域外データ、緊急データが存在する SIGXCPU 24 Core CPU 時間の上限 (RLIMIT_CPU) を超えた 4。 SIGXFSZ 25 Core ファイルサイズの上限 (RLIMIT_FSIZE) を超えた 4。 SIGVTALRM 26 Term 仮想タイマ (プロセスのユーザモードでの CPU 時間) がタイムアウトした SIGPROF 27 Term プロファイリングタイマがタイムアウトした SIGWINCH 28 Ign 制御端末のウィンドウサイズが変更された SIGIO 29 Term 非同期 I/O イベント SIGPWR 30 Term 電源の異常 SIGSYS 31 Core 不正なシステムコールを実行した 5。 シグナルアクションの変更例
SIGKILL と SIGSTOP 以外のシグナルアクションを変更するには、trap コマンド や sigaction() などの API (C 言語) を使用します。
trap コマンド
trap は Bash に組み込まれたビルトインコマンドの一種で、シグナルアクションを設定できます 6。
ここでは、SIGINT 受信時に何もしない (シグナル無視) ように設定 (第一引数 '') してみます。
シグナルを無視に設定した例$ trap '' SIGINTこの状態では、SIGINT を受信しても反応しないため、次の実行例のように割り込みキー (Ctrl + C) が効きません。
$ while true; do sleep 1; echo "Stop me !"; done Stop me ! Stop me ! ^CStop me ! <--- Ctrl + C 押下しても効かず、コマンド継続 Stop me ! Stop me ! ^Z <--- Ctrl + Z 押下で停止 (SIGTSTP 送信) [1]+ Stopped sleep 1sigaction (C 言語 API)
sigaction() は、シグナルの動作を設定できる API (C 言語) です 7。
次のサンプルプログラム (set_sigh.c) では、SIGINT 受信時に handler 関数を実行するように設定してみます。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <unistd.h> static void handler(int signo) { /* * 本来ハンドラ内では、非同期シグナルセーフな関数を使用するべきですが、 * ここでは、そうでない printf()、exit() などの関数を使用しています。 * 非同期シグナルセーフについては $ man 7 signal をご参照ください。 */ printf(" ... Caught the SIGINT (%d)\n", signo); exit(EXIT_SUCCESS); } int main(void) { unsigned int sec = 1; struct sigaction act; // SIGINT 受信時に handler() を実行するように設定。 memset(&act, 0, sizeof act); act.sa_handler = handler; sigaction(SIGINT, &act, NULL); // Ctrl + C などで終了されるまで、1 秒ごとにメッセージを出力する。 for (;;) { sleep(sec); printf("Stop me !\n"); } return 0; }次の実行例では、SIGINT 受信後 handler() によってメッセージ (... Caught the SIGINT (2)) を出力しプログラムを終了しています。
$ gcc -o set_sigh set_sigh.c $ ./set_sigh Stop me ! Stop me ! Stop me ! ^C ... Caught the SIGINT (2) <--- Ctrl + C 押下で handler 関数が実行されるなお、その他の API として signal() がありますが、こちらは移植性の観点から非推奨となっています (古い API)。特別な理由が無い限りは使用を避けた方が良いと思います 8。
6. シグナルブロック
SIGKILL と SIGSTOP 以外のシグナルは、プロセス毎にブロックすることができます。
たとえば、あるプロセスが SIGINT を受信した場合、通常は標準動作によりプロセスが終了させられますが、SIGINT をブロックしていた場合は、SIGINT を受信しても、シグナル無視のように無反応となります。
シグナル無視との違いは、受信したシグナルを保留するか否かです。シグナル無視の場合はシグナルを保留しませんが、シグナルブロックの場合は保留できます。そのため、ブロックが解除されると、再度シグナルを受信する必要なく処理できます 9。
シグナルブロックの設定例
シグナルブロックを設定するには sigprocmask() などの API (C 言語) を使用します 10。
次のサンプルプログラム (block_sig.c) では、SIGINT をブロックしてから 5 秒後に SIGINT のブロックを解除してみます。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> int main(void) { unsigned int sec = 5; sigset_t set; // シグナル集合を空にし、SIGINT 追加 sigemptyset(&set); sigaddset(&set, SIGINT); // SIGINT をブロック (保留) する sigprocmask(SIG_BLOCK, &set, NULL); printf("Block the SIGINT for %d sec\n", sec); sleep(sec); // SIGINT のブロックを解除する printf("\nPassed %d sec, unblock the SIGINT\n", sec); sigprocmask(SIG_UNBLOCK, &set, NULL); printf("Done !\n"); return 0; }実行例では SIGINT ブロック後に SIGINT を送信していますが、SIGNINT 受信時の動作が実行されるのはブロック解除後となっているのが分かります。
$ gcc -o block_sig block_sig.c $ ./block_sig Block the SIGINT for 5 sec ^C^C^C^C^C^C^C^C^C^C <--- Ctrl + C 押下しても反応しない Passed 5 sec, unblock the SIGINT <--- SIGINT により終了するため、その後の "Done !" が無出力7. シグナルの状態確認
シグナルブロック (保留)、無視といった状態は
/proc/<PID>/status
から確認できます。$ cat /proc/29498/status Name: bash --- snip --- SigQ: 0/29305 // このプロセスの実 UID 宛にキューイングされたシグナル / キューイングされたシグナルの上限値 SigPnd: 0000000000000000 // 特定プロセス (スレッド) 宛の処理待ちシグナルの数 ShdPnd: 0000000000000000 // スレッドグループ全体宛の処理待ちシグナルの数 SigBlk: 0000000000010000 // ブロックされるシグナル (ビットマスク値) SigIgn: 0000000000380004 // 無視されるシグナル (ビットマスク値) SigCgt: 000000004b817efb // 捕捉待ちのシグナル (ビットマスク値) --- snip ---この中で、SigBlk、SigIgn、SigCgt は複数のシグナルをまとめて表現するために シグナルセット というマスク値で管理されているため、読み方が少しややこしいです。たとえば、SigIgn (0000000000380004) の場合は次のような意味になります (16進数を2進数に変換、1 の位置から対応するシグナルを判断)。つまり、PID 29498 の SigIgn (無視されるシグナル) は 3, 20, 21, 22 に対応する 4 つのシグナルという意味になります。
0000000000380004 (hex) -> 1110000000000000000100 (bit) シグナル名 番号 ||| *---------> SIGQUIT (03) ||*--------------------------> SIGTSTP (20) |*---------------------------> SIGTTIN (21) *----------------------------> SIGTTOU (22)なお、
ps s
でも同じような情報は確認できます。$ ps s UID PID PENDING BLOCKED IGNORED CAUGHT STAT TTY TIME COMMAND 1000 29498 0000000000000000 0000000000010000 0000000000380004 000000004b817efb Ss pts/0 0:00 /bin/bash 1000 29517 0000000000000000 0000000000010000 0000000000380004 000000004b817efb Ss pts/1 0:00 /bin/bash 1000 29572 0000000000000000 0000000000000000 0000000000003000 00000001ef804eff S+ pts/0 0:00 vim kernel/signal.c 1000 29581 0000000000000000 0000000000000000 0000000000000000 0000000000000000 S pts/1 0:00 sleep 100 1000 29588 0000000000000000 0000000000000000 0000000000000000 00000001f3d1fef9 R+ pts/1 0:00 ps s■ カーネル編 (v5.5)
ここからは、カーネル観点での話です。シグナルを送信してからシグナルアクションが処理されるまでの流れを見ていきます。
まずデータ構造を見ていきます。データ構造とは、カーネル実装で登場する構造体のことを指します。シグナルでは多くの構造体が関連しているため、先に把握しておくことで、コードが理解し易くなると思います。
次にカーネル実装 11 を見ていきます。シグナルは次のようにシグナル生成 (1 段階目)、シグナル配送 (2 段階目) という順に段階的な処理構成となっているため、それぞれ見ていきます。
- シグナル生成 (1 段階目) ... シグナルを送信したことを送信先のプロセスのデータ構造に記録する (シグナルを保留する)
- シグナル配送 (2 段階目) ... シグナルに応じて、シグナルアクションを実行する
1. データ構造
シグナルは、次のとおり、 task_struct、signal_struct、sighand_struct など多くの構造体が関連しています。
- task_struct (プロセスディスクリプタ)
- signal_struct (シグナルディスクリプタ)
- sighand_struct (シグナルハンドラディスクリプタ)
- sigpending
- k_sigaction
- sigaction
- thread_info
- siginfo
- kernel_siginfo
大変混乱しやすいと思いますので、個人的に重要だと思う箇所に関しては、以下のように用途ごとにまとめました。
保留中シグナルを示すフラグ
シグナル生成によって、シグナルを受け取ったプロセスは、シグナル配送によって処理されるまでの間、シグナル保留状態 (シグナル配送待ち) となります。この状態は TIF_SIGPENDING というフラグで示され、スレッドフラグ (flags) に記録されます。
シグナル保留用のキュー (特定プロセス用、スレッドグループ全体用)
受信したシグナルは、sigqueue 構造体によって管理され、関連する幾つかの情報 (※1) を持ちます。それらは保留用のキューに入れられます (sigpending の list につなげられます)。その際、シグナルの送信宛に応じて 2 種類 (特定プロセス用、スレッドグループ全体用) の内のどちらかのキューが使用されます。
また、保留されたシグナルは signal に記録されます。この値は
/proc/<PID>/status
から確認できます (前述の「シグナルの状態確認」と後述の「おまけ」参照)。(※1) 幾つかの情報とは、次の kernel_siginfo 構造体のメンバを指します。
メンバ 意味 si_signo シグナル番号 si_code シグナルの発生元を示すコード (※2) si_errno シグナル発生を発生させることになった命令のエラーコード __sifilelds union のため条件に応じて si_pid (送信先 pid)、si_uid (送信先 uid) などに変化する (※2) シグナルの発生元を示すコードには次のような値が入ります。
include/uapi/asm-generic/siginfo.h より抜粋
定義名 値 意味 SI_USER 0 kill()、sigsend()、raise() によるシグナル送信 SI_KERNEL 0x80 カーネルによるシグナル送信 SI_QUEUE -1 sigqueue() によるシグナル送信 SI_TIMER -2 POSIX タイマの時間経過によるシグナル送信 SI_MESGQ -3 POSIX メッセージキューの状態変化によるシグナル送信 SI_ASYNCIO -4 非同期 I/O (AIO) 完了によるシグナル送信 SI_SIGIO -5 SIGIO のキューイングによるシグナル送信 SI_TKILL -6 tkill()、tgkill() によるシグナル送信 SI_DETHREAD -7 sent by execve() killing subsidiary threads 12 SI_ASYNCNL -60 getaddrinfo_a() での名前検索完了によるシグナル送信 なお、上記はシグナル共通の値です。特定のシグナルによっては、次のように別の値が入ることもあります 7。
シグナル名 si_code に入る値 SIGBUG BUS_* SIGCHLD CLD_* SIGFPE FPE_* SIGILL ILL_* SIGPOLL/SIGIO POLL_* SIGSEGV SEGV_* SIGTRAP TRAP_* SIGSYS SYS_SECCOMP シグナルブロック情報
sigprocmask() などによってブロックされたシグナル番号は、ブロック情報 (blocked, readl_blocked13) に記録されます。この値は
/proc/<PID>/status
から確認できます (前述の「シグナルの状態確認」 と後述の「おまけ」参照)。シグナルアクション関連の情報
sigaction() などでシグナルアクションの設定を変更すると、シグナルハンドラディスクリプタ (sighand_struct) に幾つかの情報 (※) が格納されます。また、シグナルハンドラディスクリプタの action[x] はシグナル番号 (_NSIG) 個の配列になっているため (action[シグナル番号-1])、シグナル毎に設定されます。
(※) 幾つかの情報とは、次の sigaction 構造体のメンバを指します。
メンバ 意味 sa_flags シグナルの使い方を示す SA_* フラグ7 sa_handler SIG_IGN、SIG_DFL、シグナルハンドラへのポインタなど、シグナルアクションの種類 sa_mask ハンドラ実行時にブロックする (受信を禁止する) シグナル 2. 実装
シグナルを送信してからシグナルアクションが処理される (
kill -TERM 1234
を実行した際の処理) までの流れを見ていきます。以降、「シグナル生成」と「シグナル配送」で分けてそれぞれ見ていきます (「...」 の部分は省略箇所です)
シグナル生成
この段階での主な仕事は、送信先プロセスのデータ構造に「シグナルを送信した (シグナル配送待ちがある)」ということを送信先プロセスのデータ構造に記録し、通知することです (標準動作といったシグナルアクションはシグナル配送で処理されます)。
以降で行うコードリーディングの要約として、シグナル生成における処理の全体像を図示します。なお、関数の呼び出しが多いため、コードリーディングでは、この中でも特に重要な関数 (コア関数) である __send_signal() から読み進めていきます。
__send_signal()
この関数は、送信されたシグナルを配送するかどうかを決め (prepare_signal())、シグナルを保留用キューに入れます (_sigqueue_alloc(), list_add_tail()) その際、保留シグナルの番号を記録します (sigaddset())。また、配送する場合は、プロセスディスクリプタのスレッドフラグに TIFSIGPENDING を立て、配送可能なプロセスを起床します (complete_signal())。
詳細は以降をご参照ください。
1065 static int __send_signal(int sig, struct kernel_siginfo *info, struct task_struct *t, 1066 enum pid_type type, bool force) 1067 { 1068 struct sigpending *pending; 1069 struct sigqueue *q; 1070 int override_rlimit; 1071 int ret = 0, result; .... 1076 if (!prepare_signal(sig, t, force)) 1077 goto ret;1076 行目の prepare_signal() では、シグナルを配送する必要があるかどうかををチェックします。たとえば、シグナルハンドラ (t->sighand->action[sig - 1].sa.sa_handler) に SIG_IGN が設定されている場合や、標準動作が無視 (Ign) のシグナルの場合は、配送する必要がないので、
ret
ラベルまで飛びます。シグナルがブロックされていた場合は、シグナルを保留する必要があるため、以降の処理を行います。また、標準動作が停止 (Stop) のシグナルと SIGCONT に対しては以下の処理も行います。
標準動作が停止 (Stop) のシグナル
シグナル保留用キュー (※) から SIGCONT を取り除きます。
SIGCONT
シグナル保留用キュー (※) から全ての停止系シグナルを取り除き、wake_up_state() で __TASK_STOPPED 状態のスレッドを起床します (プロセスを再開します)。
またプロセス (スレッドグループ) が終了処理中 (SIGNAL_STOP_STOPPED) の場合は、処理が完了したことにして、プロセスディスクリプタのフラグ (flags) に SIGNAL_CLD_CONTINUED と SIGNAL_STOP_CONTINUED を立てます。終了処理中でなくとも group_stop_count がカウントされている場合は、既に終了処理が終わったということなので (おそらく)、プロロセスディスクリプタのフラグ (flags) に SIGNAL_CLD_STOPPED と SIGNAL_STOP_CONTINUED を立てます。
(※) 「データ構造」でも説明しましたが、シグナル保留用キューには、「特定プロセス用 (t->pending)」と「スレッドグループ全体用 (t->signal->shared_pending)」の 2 種類があります。ここでは、その両方のキューから該当するシグナルを取り除きます。
1078 1079 pending = (type != PIDTYPE_PID) ? &t->signal->shared_pending : &t->pending;1079 行目では、pending に「特定プロセス用」「スレッドグループ全体用」のどちらのシグナル保留用キューを使用するかを決めています。今回は kill コマンド実行により、type に PIDTYPE_TGID が渡されるため、pending には、スレッドグループ全体用の方が使われます。
1080 /* 1081 * Short-circuit ignored signals and support queuing 1082 * exactly one non-rt signal, so that we can get more 1083 * detailed information about the cause of the signal. 1084 */ 1085 result = TRACE_SIGNAL_ALREADY_PENDING; 1086 if (legacy_queue(pending, sig)) 1087 goto ret;1086 行目の legacy_queue() では、
sig
が「標準シグナル」かつ「既にシグナル保留用キューに存在するなら」ret
ラベルまで飛びます (標準シグナルは複数の同一のシグナルを受信しても 1 つしか受信できない)。1088 1089 result = TRACE_SIGNAL_DELIVERED; 1090 /* 1091 * Skip useless siginfo allocation for SIGKILL and kernel threads. 1092 */ 1093 if ((sig == SIGKILL) || (t->flags & PF_KTHREAD)) 1094 goto out_set;コメントのとおりですが、シグナルが SIGKILL OR 送信先がカーネルスレッド (PF_KTHREAD フラグ) の場合は、
out_set
ラベルまで飛び、以降のシグナル保留キュー登録処理などを行いません。1096 /* 1097 * Real-time signals must be queued if sent by sigqueue, or 1098 * some other real-time mechanism. It is implementation 1099 * defined whether kill() does so. We attempt to do so, on 1100 * the principle of least surprise, but since kill is not 1101 * allowed to fail with EAGAIN when low on memory we just 1102 * make sure at least one signal gets delivered and don't 1103 * pass on the info struct. 1104 */ 1105 if (sig < SIGRTMIN) 1106 override_rlimit = (is_si_special(info) || info->si_code >= 0); 1107 else 1108 override_rlimit = 0; 11091105 行目の SIGRTMIN は 32 なので、
sig
が標準シグナルがどうかで処理が分岐します。真 のルート (1106 行目) の is_si_special() では、
info
が SEND_SIG_NOINFO (ユーザモードのプロセスからシグナルが送信された) か、SEND_SIGPRIV (カーネルからシグナルが送信された) の場合は 真 を返します。右のinfo->si_code >=0
(SI_USER か SI_KERNEL) の場合も 真 を返します。なお、今回は kill コマンド実行により
info->si_code
に SI_USER が設定されるため、override_rlimit
には 真 が入ります。1110 q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit); 1111 if (q) { 1112 list_add_tail(&q->list, &pending->list); 1113 switch ((unsigned long) info) { 1114 case (unsigned long) SEND_SIG_NOINFO: 1115 clear_siginfo(&q->info); 1116 q->info.si_signo = sig; 1117 q->info.si_errno = 0; 1118 q->info.si_code = SI_USER; 1119 q->info.si_pid = task_tgid_nr_ns(current, 1120 task_active_pid_ns(t)); 1121 rcu_read_lock(); 1122 q->info.si_uid = 1123 from_kuid_munged(task_cred_xxx(t, user_ns), 1124 current_uid()); 1125 rcu_read_unlock(); 1126 break; 1127 case (unsigned long) SEND_SIG_PRIV: 1128 clear_siginfo(&q->info); 1129 q->info.si_signo = sig; 1130 q->info.si_errno = 0; 1131 q->info.si_code = SI_KERNEL; 1132 q->info.si_pid = 0; 1133 q->info.si_uid = 0; 1134 break; 1135 default: 1136 copy_siginfo(&q->info, info); 1137 break; 1138 } ....1110 行目では _sigqueuealloc() にて、シグナル管理用の sigqueue 型
q
を新しく確保 (アロケーション) するように試みます。この関数の内部処理では、送信先プロセスの所有者 (ユーザ) が保留するシグナルの数が上限を超えないかどうかをチェックし、超えない場合にアロケーションを試みます。ただし、override_rlimit
が 真 なら上限チェックをせずにアロケーションを試みます。1112 行目から 1138 行目はアロケーション成功時の処理です。
1112 行目の list_add_tail() では、シグナル保留用キューにアロケーションした
q
を登録し (リストへつなぎ)、1113 行目以降の処理では、引数info
に応じてq
へシグナルの情報を格納していきます。1157 out_set: .... 1159 sigaddset(&pending->signal, sig); ....シグナル保留用キューのシグナルセットに現在のシグナル番号を追加します (前述の「シグナルの状態確認」と「データ構造」参照)。
1175 complete_signal(sig, t, type); 1176 ret: 1177 trace_signal_generate(sig, info, t, type != PIDTYPE_PID, result); 1178 return ret; 1179 }1175 行目の complete_signal() については後述します。
1177 行目の trace_signal_generate() は TRACE_EVENT マクロで signal_generate という名前のトレースポイントを作成しているようです。これは、次のようなカーネルトレース情報の出力に利用 (レポート) されています。
kill-5371 [003] 1058202.036613: signal_generate: sig=15 errno=0 code=0 comm=sleep pid 5359 grp=1 res=0complete_signal()
この関数は、送信されたシグナルを __send_signal() で配送すると決めた場合に呼び出されます。
そして、この関数でシグナル配送の準備ができているプロセスを探します。見つけたら該当プロセスのスレッドフラグに TIF_SIGPENDING を立て、起床します (通知します)。
詳細は以降をご参照ください。
984 static void complete_signal(int sig, struct task_struct *p, enum pid_type type) 985 { 986 struct signal_struct *signal = p->signal; 987 struct task_struct *t; 988 989 /* 990 * Now find a thread we can wake up to take the signal off the queue. 991 * 992 * If the main thread wants the signal, it gets first crack. 993 * Probably the least surprising to the average bear. 994 */ 995 if (wants_signal(sig, p)) 996 t = p; ...995 行目の wants_signal() では、次のパターンでシグナル配送可能 (準備ができている) なプロセスを探します。
シグナルがブロックされてない (SIGKILL, SIGSTOP はブロック不可) AND
- プロセスが終了処理中でない (p->flags に PF_ EXITING が無い) AND
- シグナル (
sig
) が SIGKILL (プロセスの状態やフラグ有無は見ないで強制する)シグナルがブロックされてない (SIGKILL, SIGSTOP はブロック不可) AND
- プロセスが終了処理中でない (p->flags に PF_ EXITING が無い) AND
- プロセスの状態 (p->state) が一時停止中 (TASK_STOPPED) か、デバッガなどにより停止中 (TASK_TRACED) でない AND
- プロセスが CPU 上で実行中 (カレントプロセス) OR プロセスのスレッドフラグ (p->thread_info->flags) に TIF_SIGPENDING が無い
1021 /* 1022 * Found a killable thread. If the signal will be fatal, 1023 * then start taking the whole group down immediately. 1024 */ 1025 if (sig_fatal(p, sig) && 1026 !(signal->flags & SIGNAL_GROUP_EXIT) && 1027 !sigismember(&t->real_blocked, sig) && 1028 (sig == SIGKILL || !p->ptrace)) {コメントのとおり、Kill 可能なスレッドを探します。次の条件を全て満たす場合、1039 行目から 1042 行目の処理を行います。
- sig_fatal() が 真 (※) AND
- スレッドグループが終了中でない (SIGNAL_GROUP_EXIT は「スレッドグループが終了中」というフラグ) AND
- シグナルがブロックされていない (sigismember() は、t->real_blocked (ブロック情報) に sig が含まれていれば 真) AND
- シグナルが SIGKILL OR ptrace 関連フラグ (p->ptrace) がない時
(※) sig_fatal() は次の 2 パターンで 真 を返します。
シグナルが標準シグナル AND
- 標準動作が無視 (Ign) / 停止 (Stop) のシグナルでない AND
- シグナルアクションが SIG_DFL
シグナルがリアルタイムシグナル AND
- シグナルアクションが SIG_DFL
1029 /* 1030 * This signal will be fatal to the whole group. 1031 */ 1032 if (!sig_kernel_coredump(sig)) {sig_kernel_coredump() は、標準動作がコアダンプ (Core) のシグナルでない時 真 を返します。
1033 /* 1034 * Start a group exit and wake everybody up. 1035 * This way we don't have other threads 1036 * running and doing things after a slower 1037 * thread has the fatal signal pending. 1038 */ 1039 signal->flags = SIGNAL_GROUP_EXIT; 1040 signal->group_exit_code = sig; 1041 signal->group_stop_count = 0; 1042 t = p; 1043 do { 1044 task_clear_jobctl_pending(t, JOBCTL_PENDING_MASK); 1045 sigaddset(&t->pending.signal, SIGKILL); 1046 signal_wake_up(t, 1); 1047 } while_each_thread(p, t); 1048 return; 1049 } 1050 }1039 行目から 1042 行目までの処理を行った後は、do-while でスレッドグループ内を走査し終わるまで、次に示す処理をループします。
- 1044 行目の task_clear_jobctl_pending() で、保留中のジョブコントロールフラグ (t->jobctl) をクリア
- 1045 行目の sigaddset() で、シグナル保留用キューのシグナルセットに SIGKILL (番号) を追加
- 1046 行目の signal_wake_up() で、スレッドフラグ (t->flags) に TIF_SIGPENDING を立て、TASK_WAKEKILL (SIGKILL などの重要なシグナルを受信可能) か TASK_INTERRUPTIBLE (何らかのイベント待ち) のプロセスを起床 (通知)
ループを抜けると、この関数での処理を終了します。
1052 /* 1053 * The signal is already in the shared-pending queue. 1054 * Tell the chosen thread to wake up and dequeue it. 1055 */ 1056 signal_wake_up(t, sig == SIGKILL); 1057 return; 1058 }ここまで到達できた場合、signal_wake_up() で、スレッドフラグ (t->flags) に TIF_SIGPENDING を立て、TASK_INTERRUPTIBLE 状態のプロセスを起床します (保留シグナルを通知)。また、シグナルが SIGKILL の場合は TASK_WAKEKILL 状態のプロセスも起床します。
以上で、「シグナル生成」の処理は終わりです。続いて、「シグナル配送」を見ていきます。
シグナル配送
この段階での主な仕事は、シグナルハンドラや標準動作などのシグナルアクションを実行することです。ただし、この処理はプロセスが保留シグナルを持つ (前述の「シグナル生成」で説明した TIF_SIGPENDING フラグを持つ) 場合に限ります。
以降で行うコードリーディングの要約として、シグナル配送における処理の全体像を図示します。なお、シグナル配送は一部アーキテクチャに依存するコードがありますが、ここでは x86 (arch/x86 配下) のコードを見ていきます。
詳細は以降をご参照ください。
exit_to_usermode_loop()
この関数は、カーネルモードからユーザモードへ戻る度 (割り込み/例外ハンドラ、システコール処理後など) に呼び出され、スレッドフラグをチェックします。その際、TIF_SIGPENDING があると、do_signal() を呼びだします (160 行目)。
arch/x86/entry/common.c (L136)
136 static void exit_to_usermode_loop(struct pt_regs *regs, u32 cached_flags) 137 { 138 /* 139 * In order to return to user mode, we need to have IRQs off with 140 * none of EXIT_TO_USERMODE_LOOP_FLAGS set. Several of these flags 141 * can be set at any time on preemptible kernels if we have IRQs on, 142 * so we need to loop. Disabling preemption wouldn't help: doing the 143 * work to clear some of the flags can sleep. 144 */ 145 while (true) { 146 /* We have work to do. */ 147 local_irq_enable(); ... 158 /* deal with pending signal delivery */ 159 if (cached_flags & _TIF_SIGPENDING) 160 do_signal(regs); ... 171 /* Disable IRQs and retry */ 172 local_irq_disable(); 173 174 cached_flags = READ_ONCE(current_thread_info()->flags); 175 176 if (!(cached_flags & EXIT_TO_USERMODE_LOOP_FLAGS)) 177 break; 178 } 179 }do_signal()
この関数は、標準動作の実行 (get_signal()) やシグナルハンドラの実行 (handle_signal()) を行います。また、必要に応じてシステコールの再実行や特定のシステムコールによって一時的に書き換えられたシグナルブロック情報の復元を行います。
arch/x86/kernel/signal.c (L806)
806 /* 807 * Note that 'init' is a special process: it doesn't get signals it doesn't 808 * want to handle. Thus you cannot kill init even with a SIGKILL even by 809 * mistake. 810 */ 811 void do_signal(struct pt_regs *regs) 812 { 813 struct ksignal ksig; 814 815 if (get_signal(&ksig)) { 816 /* Whee! Actually deliver the signal. */ 817 handle_signal(&ksig, regs); 818 return; 819 } 820815 行目の get_signal() では標準動作の実行と保留用キューからのシグナル取り出しを行います。その結果が 真 (取り出せたら) なら handle_signal() を実行しシグナルハンドラの実行を行います。偽 (取り出せなかった) なら以降の処理を行います (get_signal()、handle_signal() は重要な関数なので詳細は後述します)。
821 /* Did we come from a system call? */ 822 if (syscall_get_nr(current, regs) >= 0) { 823 /* Restart the system call - no handlers present */ 824 switch (syscall_get_error(current, regs)) { 825 case -ERESTARTNOHAND: 826 case -ERESTARTSYS: 827 case -ERESTARTNOINTR: 828 regs->ax = regs->orig_ax; 829 regs->ip -= 2; 830 break; 831 832 case -ERESTART_RESTARTBLOCK: 833 regs->ax = get_nr_restart_syscall(regs); 834 regs->ip -= 2; 835 break; 836 } 837 }ここはシステムコールの再実行に関する処理です (システムコールの処理中にシグナルを受信した場合、エラーを吐いて処理を中断することがあるため、このような処理があります)。
822 行目の syscall_get_nr() は、ユーザモードのレジスタ (
regs
) からシステムコール番号を取得します。それが 0 以上だったら、824 行目の syscall_get_erro() でエラー番号を取得し、そのエラー番号に応じてカーネルがシステムコールを再実行します。なお、システムコールの再実行については「Linuxのシステムコール再実行について」という記事に詳しく書かれていました。
regs->ip -= 2
の意味など詳細はこちらをご参照ください。838 839 /* 840 * If there's no signal to deliver, we just put the saved sigmask 841 * back. 842 */ 843 restore_saved_sigmask(); 844 }restore_saved_sigmask() では、ppoll、pselect、epoll、sigsuspend などのシステムコールによって一時的に書き換えられたシグナルブロック情報 (task_struct->blocked) を、あらかじめ退避させていたシグナルブロック情報 (task_struct->saved_sigmask) から復元します。
get_signal()
この関数は、シグナル保留用キューからシグナルを取り出し、シグナルに応じて標準動作の実行を行います。
2521 bool get_signal(struct ksignal *ksig) 2522 { 2523 struct sighand_struct *sighand = current->sighand; 2524 struct signal_struct *signal = current->signal; 2525 int signr; .... 2540 relock: .... 2542 /* 2543 * Every stopped thread goes here after wakeup. Check to see if 2544 * we should notify the parent, prepare_signal(SIGCONT) encodes 2545 * the CLD_ si_code into SIGNAL_CLD_MASK bits. 2546 */ 2547 if (unlikely(signal->flags & SIGNAL_CLD_MASK)) { 2548 int why; 2549 2550 if (signal->flags & SIGNAL_CLD_CONTINUED) 2551 why = CLD_CONTINUED; 2552 else 2553 why = CLD_STOPPED; 2554 2555 signal->flags &= ~SIGNAL_CLD_MASK; 2556 2557 spin_unlock_irq(&sighand->siglock); 2558 2559 /* 2560 * Notify the parent that we're continuing. This event is 2561 * always per-process and doesn't make whole lot of sense 2562 * for ptracers, who shouldn't consume the state via 2563 * wait(2) either, but, for backward compatibility, notify 2564 * the ptracer of the group leader too unless it's gonna be 2565 * a duplicate. 2566 */ 2567 read_lock(&tasklist_lock); 2568 do_notify_parent_cldstop(current, false, why); 2569 2570 if (ptrace_reparented(current->group_leader)) 2571 do_notify_parent_cldstop(current->group_leader, 2572 true, why); 2573 read_unlock(&tasklist_lock); 2574 2575 goto relock; 2576 } ....2543 行目のコメントにも少し書かれていますが、2547 行目のフラグ (SIGNAL_CLD_MASK == SIGNAL_CLD_STOPPED or SIGNAL_CLD_CONTINUED )は、シグナル生成 (_send_signal()) で呼び出される prepare_signal() で SIGCONT の処理をするときに設定されるます。このフラグが設定されている場合は、donotify_parent_cldstop() で親にシグナル (SIGCHLD) を送信します。
2588 for (;;) { 2589 struct k_sigaction *ka; .... 2616 /* 2617 * Signals generated by the execution of an instruction 2618 * need to be delivered before any other pending signals 2619 * so that the instruction pointer in the signal stack 2620 * frame points to the faulting instruction. 2621 */ 2622 signr = dequeue_synchronous_signal(&ksig->info); 2623 if (!signr) 2624 signr = dequeue_signal(current, ¤t->blocked, &ksig->info); 2625 2626 if (!signr) 2627 break; /* will return 0 */ ....2622 行目の dequeue_synchronous_signal() で同期シグナル (SIG[SEGV|BUS|ILL|TRAP|FPE|SYS]) をキューから取り出します。なければ、2624 行目の dequeue_signal() で非同期シグナルをキューから取り出します。それでもなければ、break でループを抜けこの関数での処理を終了します。
2635 ka = &sighand->action[signr-1]; 2636 2637 /* Trace actually delivered signals. */ 2638 trace_signal_deliver(signr, &ksig->info, ka);2638 行目の trace_signal_deliver() は TRACE_EVENT マクロで signal_deliver という名前のトレースポイントを作成するようです。これは、次のようなカーネルトレース情報の出力に利用 (レポート) されています。定義元のコメントを見るに、ここに到達できた時点でシグナルが配送されたことになるようです。
trace-cmd-17636 [001] 607498.455844: signal_deliver: sig=2 errno=0 code=128 sa_handler=5567b90d1540 sa_flags=1400000以降は、シグナル無視、シグナルハンドラ、標準動作などのシグナルアクションに関する処理です。
ka
(2635 行目) の内容により分岐します。2639 2640 if (ka->sa.sa_handler == SIG_IGN) /* Do nothing. */ 2641 continue;C 言語 API のsigaction() などでシグナルアクションに無視 (SIG_IGN) を設定していた場合は、何もしません。
2642 if (ka->sa.sa_handler != SIG_DFL) { 2643 /* Run the handler. */ 2644 ksig->ka = *ka; 2645 2646 if (ka->sa.sa_flags & SA_ONESHOT) 2647 ka->sa.sa_handler = SIG_DFL; 2648 2649 break; /* will return non-zero "signr" value */ 2650 }2642 行目は、SIG_DFL でない場合 (つまり、シグナルハンドラを設定していた場合) 、2644 行目で
ksig->ka
にka
のアドレスを入れます。これは get_signal() を抜けたあとの handle_signal() でユーザプロス用のレジスタに設定する時に使用します。2646 行目の SA_ONESHOT (sigaction() で設定可能なフラグ) はシグナルハンドラ実行後にシグナルアクションをデフォルト (SIG_DFL) に戻すという意味です。つまり、シグナルハンドラの実行するのは、初回のシグナル受信時だけです。
最後に break でループを抜け、この関数での処理を終えます。この場合
signr
には取り出したシグナル番号が入っているはずなので、後述の handle_signal() が実行されます。2651 2652 /* 2653 * Now we are doing the default action for this signal. 2654 */ 2655 if (sig_kernel_ignore(signr)) /* Default is nothing. */ 2656 continue; ....ここからは、標準動作の実行に関する処理です。
2655 行目の sig_kernel_ignore() は
signr
の標準動作が無視 (Ign) なら 真 を返します。その場合、何もしません。2671 2672 if (sig_kernel_stop(signr)) { 2673 /* 2674 * The default action is to stop all threads in 2675 * the thread group. The job control signals 2676 * do nothing in an orphaned pgrp, but SIGSTOP 2677 * always works. Note that siglock needs to be 2678 * dropped during the call to is_orphaned_pgrp() 2679 * because of lock ordering with tasklist_lock. 2680 * This allows an intervening SIGCONT to be posted. 2681 * We need to check for that and bail out if necessary. 2682 */ 2683 if (signr != SIGSTOP) { 2684 spin_unlock_irq(&sighand->siglock); 2685 2686 /* signals can be posted during this window */ 2687 2688 if (is_current_pgrp_orphaned()) 2689 goto relock; 2690 2691 spin_lock_irq(&sighand->siglock); 2692 } 2693 2694 if (likely(do_signal_stop(ksig->info.si_signo))) { 2695 /* It released the siglock. */ 2696 goto relock; 2697 } 2698 2699 /* 2700 * We didn't actually stop, due to a race 2701 * with SIGCONT or something like that. 2702 */ 2703 continue; 2704 }2672 行目の sig_kernel_stop() は
signr
の標準動作がプロセス一時停止 (Stop) のシグナルなら 真 を返します。真 なら、2694 行目の do_signal_stop() でプロセスディスクリプタのフラグ (flags) に SIGNAL_STOP_STOPPED を立て、プロセスの一時停止 (TASK_STOPPED) を行います。ただし、シグナルが SIGSTOP 以外かつ、プロセスグループが孤児 (親なし) の 場合は do_signal_stop() を実行しません (一時停止しません)。
2705 2706 fatal: 2707 spin_unlock_irq(&sighand->siglock); .... 2711 /* 2712 * Anything else is fatal, maybe with a core dump. 2713 */ 2714 current->flags |= PF_SIGNALED; 2715 2716 if (sig_kernel_coredump(signr)) { 2717 if (print_fatal_signals) 2718 print_fatal_signal(ksig->info.si_signo); 2719 proc_coredump_connector(current); 2720 /* 2721 * If it was able to dump core, this kills all 2722 * other threads in the group and synchronizes with 2723 * their demise. If we lost the race with another 2724 * thread getting here, it set group_exit_code 2725 * first and our do_group_exit call below will use 2726 * that value and ignore the one we pass it. 2727 */ 2728 do_coredump(&ksig->info); 2729 } ....2716 行目の sig_kernel_coredump() は
signr
の標準動作がコアダンプ (Core) なら 真 を返します。真 なら 2728 行目の do_coredump() でシグナルディスクリプタのフラグ (flags) に SIGNAL_GROUP_COREDUMP を立て、コアダンプ生成処理を行います。2731 /* 2732 * Death signals, no core dump. 2733 */ 2734 do_group_exit(ksig->info.si_signo); 2735 /* NOTREACHED */ 2736 }
signr
の標準動作がプロセス終了 (Term) やコアダンプ (Core) の場合、2724 行目の do_group_exit() でシグナルディスクリプタのフラグ (flags) に SIGNAL_GROUP_EXIT を立て、プロセス (スレッドグループ) の終了を行います。なお、2736 行目の
}
は 2588 行目のfor (;;) {
の対になる部分です。このループは、2627 行目の break か 2649 行目の break で抜けることができます。前者はシグナル保留用キューから取り出すものが無くなった場合で、後者はシグナルアクション (sa_handler) にシグナルハンドラが設定されていた場合です。2737 spin_unlock_irq(&sighand->siglock); 2738 2739 ksig->sig = signr; 2740 return ksig->sig > 0; 2741 }最後にキューから取り出したシグナル番号が 0 より大きいかどうかを判定します。大きい場合はシグナルを取り出せたということで 真 を返し、次の handle_signal() へ進みます。
handle_signal()
この関数は、システムコールの再実行 (必要なら) と、シグナルハンドラ実行用にユーザモード時のスタックフレームを設定します。
arch/x86/kernel/signal.c (L71)
710 static void 711 handle_signal(struct ksignal *ksig, struct pt_regs *regs) 712 { 713 bool stepping, failed; 714 struct fpu *fpu = ¤t->thread.fpu; 715 ... 719 /* Are we from a system call? */ 720 if (syscall_get_nr(current, regs) >= 0) { 721 /* If so, check system call restarting.. */ 722 switch (syscall_get_error(current, regs)) { 723 case -ERESTART_RESTARTBLOCK: 724 case -ERESTARTNOHAND: 725 regs->ax = -EINTR; 726 break; 727 728 case -ERESTARTSYS: 729 if (!(ksig->ka.sa.sa_flags & SA_RESTART)) { 730 regs->ax = -EINTR; 731 break; 732 } 733 /* fallthrough */ 734 case -ERESTARTNOINTR: 735 regs->ax = regs->orig_ax; 736 regs->ip -= 2; 737 break; 738 } 739 } 740 ...do_signal() でも出ましたが14、ここにもシステムコールの再実行に関する処理があります。ただし、カーネルがシステムコールを再実行するケースは次の 2 パターンのみです。
- エラーが -ERESTARTSYS (728 行目) かつ sa_flags に SA_RESTART (sigaction() で設定可能なフラグ) が立っている場合
- エラーが 734 行目の -ERESTARTNOINTR (734 行目) だった場合
それ以外の場合、カーネルはシステムコールを再実行せず
regs->ax
に -EINTR (関数呼び出しが割り込まれたというエラー) を設定します。システムコールを再実行するかどうかは、このエラーからユーザ (ユーザプログラムの条件) が判断します。750 failed = (setup_rt_frame(ksig, regs) < 0); ... 751 if (!failed) { 752 /* 753 * Clear the direction flag as per the ABI for function entry. 754 * 755 * Clear RF when entering the signal handler, because 756 * it might disable possible debug exception from the 757 * signal handler. 758 * 759 * Clear TF for the case when it wasn't set by debugger to 760 * avoid the recursive send_sigtrap() in SIGTRAP handler. 761 */ 762 regs->flags &= ~(X86_EFLAGS_DF|X86_EFLAGS_RF|X86_EFLAGS_TF); 763 /* 764 * Ensure the signal handler starts with the new fpu state. 765 */ 766 fpu__clear(fpu); 767 } 768 signal_setup_done(failed, ksig, stepping); 769 }750 行目の setup_rt_frame() では、シグナルハンドラ実行用にユーザモード時のスタックフレームを設定します。スタックフレームには、シグナルハンドラ実行に必要な情報 (シグナル番号、siginfo 構造体 の情報や rt_sigreturn() システムコールをユーザモードプロセスから呼び出すための情報が格納されています。rt_sigreturn() はシグナルハンドラ実行後にカーネルモードへ復帰するのに必要です。
また、setup_rt_frame() での設定が成功した場合は、762、766 行目でフラグ (
regs->flags
) や FPU state (浮動小数点数レジスタ) をクリアします (理由は不明)。そして、最後 (768 行目) の signal_setup_done() では setup_rt_frame() での設定成否で処理が分岐します。
成功した場合:
sigorsets() でカレントプロセスのブロック情報 (current->blocked) と sigaction() でユーザが設定したブロック情報 (sigaction->sa_mask) の論理和 (OR) を取り、それをカレントプロセスのブロック情報に設定します。また、スレッドフラグに TIF_SINGLESTEP (デバッガで使用) が設定されていた場合は、ptrace_notify(SIGTRAP) を実行します。失敗した場合:
force_sigsegv() でカレントプロセスに SIGSEGV を送信します。以上で、「シグナル配送」の処理は終わりです。
3. おまけ
/proc/<PID>/status
にあるシグナル関連のコード
/proc/<PID>/status
にある SigPnd、ShdPnd、などの値はどこに由来するのかコード上から調べました。task_sig() に答えがありました。たとえば、SigBlk (
blocked
) には 283 行目でp->blocked
を入れています。266 static inline void task_sig(struct seq_file *m, struct task_struct *p) 267 { 268 unsigned long flags; 269 sigset_t pending, shpending, blocked, ignored, caught; 270 int num_threads = 0; 271 unsigned int qsize = 0; 272 unsigned long qlim = 0; 273 274 sigemptyset(&pending); 275 sigemptyset(&shpending); 276 sigemptyset(&blocked); 277 sigemptyset(&ignored); 278 sigemptyset(&caught); 279 280 if (lock_task_sighand(p, &flags)) { 281 pending = p->pending.signal; 282 shpending = p->signal->shared_pending.signal; 283 blocked = p->blocked; 284 collect_sigign_sigcatch(p, &ignored, &caught); 285 num_threads = get_nr_threads(p); 286 rcu_read_lock(); /* FIXME: is this correct? */ 287 qsize = atomic_read(&__task_cred(p)->user->sigpending); 288 rcu_read_unlock(); 289 qlim = task_rlimit(p, RLIMIT_SIGPENDING); 290 unlock_task_sighand(p, &flags); 291 } 292 293 seq_put_decimal_ull(m, "Threads:\t", num_threads); 294 seq_put_decimal_ull(m, "\nSigQ:\t", qsize); 295 seq_put_decimal_ull(m, "/", qlim); 296 297 /* render them all */ 298 render_sigset_t(m, "\nSigPnd:\t", &pending); 299 render_sigset_t(m, "ShdPnd:\t", &shpending); 300 render_sigset_t(m, "SigBlk:\t", &blocked); 301 render_sigset_t(m, "SigIgn:\t", &ignored); 302 render_sigset_t(m, "SigCgt:\t", &caught); 303 }SIGKILL、SIGSTOP がブロックできない理由
SIGKILL と SIGSTOP がブロックできない理由をコード上から探してみました。
シグナルブロックに使う sigprocmask() 実行時に呼び出される sys_rt_sigprocmask() に答えがありました。3025 行目の sigdelsetmask() で新しく設定したブロック情報 (new_set) から SIGKILL と SIGSTOP を削除しています (つまり、SIGKILL と SIGSTOP をブロックするように設定しても実際は設定されない)。
3003 /** 3004 * sys_rt_sigprocmask - change the list of currently blocked signals 3005 * @how: whether to add, remove, or set signals 3006 * @nset: stores pending signals 3007 * @oset: previous value of signal mask if non-null 3008 * @sigsetsize: size of sigset_t type 3009 */ 3010 SYSCALL_DEFINE4(rt_sigprocmask, int, how, sigset_t __user *, nset, 3011 sigset_t __user *, oset, size_t, sigsetsize) 3012 { 3013 sigset_t old_set, new_set; 3014 int error; .... 3022 if (nset) { 3023 if (copy_from_user(&new_set, nset, sizeof(sigset_t))) 3024 return -EFAULT; 3025 sigdelsetmask(&new_set, sigmask(SIGKILL)|sigmask(SIGSTOP)); 3026 3027 error = sigprocmask(how, &new_set, NULL); 3028 if (error) 3029 return error; 3030 } .... 3037 return 0; 3038 }なお、シグナルブロックは sigaction() の sa_mask でも設定できますので、こちらも見てみます。
sigaction() の場合は sys_rt_sigaction() -> do_sigaction() の 3967、3968 行目で同様の処理を行っていました。
3949 int do_sigaction(int sig, struct k_sigaction *act, struct k_sigaction *oact) 3950 { 3951 struct task_struct *p = current, *t; 3952 struct k_sigaction *k; 3953 sigset_t mask; .... 3966 if (act) { 3967 sigdelsetmask(&act->sa.sa_mask, 3968 sigmask(SIGKILL) | sigmask(SIGSTOP)); .... 3988 } .... 3991 return 0; 3992 }SIGKILL、SIGSTOP を無視 or シグナルハンドラを設定できない理由
SIGKILL、SIGSTOP が無視 or 動作変更できない理由もコード上から調べてみました。
sigaction() 実行時に呼び出される sys_rt_sigaction() -> do_sigaction() の 3955 行目に答えがありました。一番右の条件で、
act
が有効値かつ sig_kernel_only() が 真 (シグナルが SIGKILL か SIGSTOP の場合) の時に -EINVAL でエラー (引数が無効) で失敗します。 sigaction() に SIGKILL および SIGSTOP を渡すと失敗するので、当然ですが、無視 (SIG_IGN) もシグナルハンドラ (ユーザ定義の関数) も設定できないということになります。3949 int do_sigaction(int sig, struct k_sigaction *act, struct k_sigaction *oact) 3950 { 3951 struct task_struct *p = current, *t; 3952 struct k_sigaction *k; 3953 sigset_t mask; 3954 3955 if (!valid_signal(sig) || sig < 1 || (act && sig_kernel_only(sig))) 3956 return -EINVAL; .... 3966 if (act) { .... 3969 *k = *act; 3970 /* 3971 * POSIX 3.3.1.3: 3972 * "Setting a signal action to SIG_IGN for a signal that is 3973 * pending shall cause the pending signal to be discarded, 3974 * whether or not it is blocked." 3975 * 3976 * "Setting a signal action to SIG_DFL for a signal that is 3977 * pending and whose default action is to ignore the signal 3978 * (for example, SIGCHLD), shall cause the pending signal to 3979 * be discarded, whether or not it is blocked" 3980 */ 3981 if (sig_handler_ignored(sig_handler(p, sig), sig)) { 3982 sigemptyset(&mask); 3983 sigaddset(&mask, sig); 3984 flush_sigqueue_mask(&mask, &p->signal->shared_pending); 3985 for_each_thread(p, t) 3986 flush_sigqueue_mask(&mask, &t->pending); 3987 } 3988 } .... 3991 return 0; 3992 }■ 参考文献
- Linux システムプログラミング (9 章シグナル)
- Linux プログラミングインタフェース (20 章シグナル: 基礎、21 章シグナル: シグナルハンドラ, 22 章シグナル: 応用)
- 詳解 Linux カーネル 第3版 (11 章シグナル)
- Linux カーネル解読室 (8 章 シグナル処理)
- Mastering Linux Kernel Development (Chapter 3: Signal Management)
- kernel source code (v5.5)
- Using the TRACE_EVENT() macro (Part 1)
- TASK_KILLABLE: Linux での新しいプロセスの状態
man 1 bash
man 2 setrlimit
man 2 seccomp
man 2 signal
man 2 sigaction
man 3 getaddrinfo_a
man 5 proc
man 7 signal
man 7 pipe
詳細は
man 7 signal
をご参照ください。 ↩詳細は
man 5 core
をご参照ください。 ↩詳細は
man 7 pipe
ご参照ください。 ↩詳細は
man 2 setrlimit
ご参照ください。 ↩詳細は
man 2 seccomp
ご参照ください。 ↩詳細は
man 1 bash
をご参照ください。 ↩詳細は
man 2 sigaction
をご参照ください。 ↩詳細は
man 2 signal
をご参照ください。 ↩シグナル無視の場合も解除はできますが、シグナル無視の状態で受信したシグナルは保留されないため、解除された後も処理されません。無視を解除した上で再度シグナルを受信する必要があります。 ↩
詳細は
man 2 sigprocmask
をご参照ください。 ↩カーネルバージョンは調査時点での最新である v5.5 を使用します。 ↩
man 2 execve
カーネルソースなどを調べましたが、この原文以上の情報が見当たらず、意味がよく理解できなかったため、原文のまま記載します。 ↩real_blocked は、rt_sigtimedwait(), rt_sigtimedwait_time32(), rt_sigtimedwait_time64() システムコール使用時に設定されるようです。 ↩
do_signal() でのシステムコール再実行は、handle_signal() を実行しない場合に通るパス (get_signal() が 偽 の時) なので、ここでのシステムコール再実行処理とは重複しません。 ↩
- 投稿日:2020-05-24T21:05:30+09:00
Linux シグナルの仕組み (カーネル実装) を調べました
この記事は、Linux シグナルの仕組み (カーネル実装) について調査したことのまとめです。
シグナルは普段から利用しているものの仕組みについては理解してなかったので、カーネル勉強の題材として調べてみました。想像以上に複雑でボリュームがあったため、書き切れていない部分 (調査しきれなかった部分) もありますが、一通りの流れ (仕組み) は理解できたと思います。
なお、この記事は「基本編」と「カーネル編」に分かれています。仕組みを理解するには基本も知る必要があると思い、このような構成となりました。
基本編
はじめにシグナルの基本について、ざっと整理します。
なお、例で登場するコマンドや API (C 言語) の細かい使い方やエラー処理などは省きます。詳細は man や参考文献の情報等をご活用ください。
シグナルとは
プロセスやプロセスグループへ様々なイベントを通知するためにあるカーネルの機能です (ソフトウェア割り込み)。イベントの通知は様々な場所 (自分/他プロセス、カーネル) から行うことが可能で、次のようなことができます。
- ハングしたプロセスにシグナルを送信して強制終了させる
- シグナルを送信してプロセスの処理を一時停止・再開させる
- ハードウェア例外 (0 除算、メモリアクセス違反など) 時にシグナルを送信してプロセスを終了させる
- シグナルを送信する特殊なキー (Ctrl + C など) を入力しプロセスを終了させる
- シグナル受信時にユーザ定義の関数 (シグナルハンドラ) を実行させる
なお、シグナルは次のような流れで処理されます。詳細は追々見ていきます。
シグナル利用例
イメージし易いように、ユーザ視点でのシグナル利用例を 3 つ記載します。
kill コマンド
kill コマンドでシグナル (SIGTERM) を送り sleep プロセスを終了させます。
$ ps PID TTY TIME CMD 2662 pts/0 00:00:00 bash 2679 pts/0 00:00:00 sleep 2685 pts/0 00:00:00 ps $ kill 2679 [1]+ Terminated sleep 1000 $ ps PID TTY TIME CMD 2662 pts/0 00:00:00 bash 2696 pts/0 00:00:00 ps割り込みキー
割り込みキー (Ctrl + C) でシグナル (SIGINT) を送り、無限ループするコマンド (プロセスグループ) を終了させます。
$ while :; do sleep 1; echo "Stop me !"; done Stop me ! Stop me ! Stop me ! ^Cプログラム (C 言語 API)
以下のように自前のプログラム (send_sig.c) でシグナル (SIGTERM) を送り sleep プロセスを終了させることもできます。
#include <signal.h> #include <stdlib.h> int main(int argc, char *argv[]) { pid_t pid = atoi(argv[1]); kill(pid, SIGTERM); return 0; }コンパイル & 実行例
$ ps PID TTY TIME CMD 30285 pts/0 00:00:00 bash 30597 pts/0 00:00:00 sleep 30631 pts/0 00:00:00 ps $ gcc -o send_sig send_sig.c $ ./send_sig 30597 [1]+ Terminated sleep 1000 $ ps PID TTY TIME CMD 30285 pts/0 00:00:00 bash 30663 pts/0 00:00:00 psシグナル番号とシグナル名
シグナルは、用途に応じて番号と名前が割り振られています。
$ kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAXただし、シグナル番号の一部はアーキテクチャに依存するため、上述の実行例 (x86_64) とは番号が異なる場合があります 1。
標準シグナル、リアルタイムシグナル
シグナルは、標準シグナルとリアルタイムシグナルの 2 種類に大別できます。
ざっくりとした違いは、
- 標準シグナル ... SIGKILL など伝統的に使用しているシグナル (1 〜 31 番)
- リアルタイムシグナル ... 標準シグナルの拡張版 (32 〜 64 番)
です。もう少し細かい違いは、次のとおりです。
標準シグナル リアルタイムシグナル 新/旧 旧 新 シグナル名 シグナル毎に様々 SIGRTMIN(+n), SIGRTMAX(-n) シグナル番号 1 〜 31 32 〜 64 標準動作 (詳細は後述) シグナル毎に様々 プロセス終了 (全シグナル) 用途 シグナル毎に様々 ユーザが定義する (全シグナル) 複数の同じシグナル
受信時の挙動1 つのみ受信 全て受信 複数の同じシグナル
送信時の順序規定なし 送信された順に到着 複数の異なるシグナル
送信時の順序規定なし シグナル番号の小さい順に到着 なお、本稿ではこれ以上深くリアルタイムシグナルについては言及しません 1。
シグナルアクション
シグナル受信時の動作 (シグナルアクション) には、次の 3 種類があります。この動作は後述の sigaction() などから変更できます。ただし、SIGKILL と SIGSTOP は標準動作以外にはできません。
無視
シグナルを受信しても何もしません。
(設定するには、後述の sigaction() などで sa_handler に SIG_IGN を指定する)
標準動作
シグナル受信すると、シグナル毎に定義された標準動作 (後述の Term, Ign, Core, Stop, Cont) を実行します。
(設定するには、後述の sigaction() などで sa_handler に SIG_DFL を指定 (デフォルト) する)
シグナルハンドラ
シグナル受信すると、ユーザが定義した動作を実行します。
(設定するには、後述の sigaction() などで sa_handler へユーザ定義の関数を指定する)
標準動作
標準動作は、シグナル毎に定義されており、次の 5 種類があります。
動作名 意味 Term プロセス終了 Ign 何もしない (sigaction() で設定できる SIG_IGN と同じ) Core プロセス終了とコアダンプ生成 2 Stop プロセス一時停止 (TASK_STOPPED 状態に遷移させる) Cont 一時停止したプロセスの再開 (TASK_STOPPED 状態からの復帰) ただし、これは標準シグナルに限ります。リアルタイムシグナルの場合は Term のみです (次図参照)。
標準シグナルと対応する標準動作
1 から 31 番までの標準シグナルは、各々に紐づく標準動作を持ちます。
シグナル名 シグナル番号 (x86_64) 標準動作 意味 SIGHUP 1 Term 制御端末のハングアップ検出 or 制御プロセスの死 SIGINT 2 Term キーボードからの割り込み (Ctrl + C) SIGQUIT 3 Core キーボードからの終了 (Ctrl + Q) SIGILL 4 Core 不正な命令 SIGTRAP 5 Core デバッグ用のトレース/ブレークポイントシグナル SIGABRT 6 Core abort(3) からの中断シグナル SIGBUS 7 Core バスエラー (不正なメモリアクセス) SIGFPE 8 Core 浮動小数点例外 SIGKILL 9 Term Kill シグナル SIGUSR1 10 Term ユーザ定義のシグナル (その1) SIGSEGV 11 Core 不正なメモリ参照 SIGUSR2 12 Term ユーザ定義のシグナル (その2) SIGPIPE 13 Term パイプ破壊 (読み手の無いパイプへの書き出し) 3。 SIGALRM 14 Term alarm(2) からのタイマーシグナル SIGTERM 15 Term 終了シグナル SIGSTKFLT 16 Term 数値演算プロセッサ (コプロセッサ)におけるスタックフォルト (未使用) SIGCHLD 17 Ign 子プロセスの一時停止 (再開) または終了 SIGCONT 18 Cont 一時停止プロセスの再開 SIGSTOP 19 Stop プロセスの一時停止 SIGTSTP 20 Stop 制御端末からの停止 (Ctrl + Z) SIGTTIN 21 Stop バッググラウンドプロセスが制御端末を読み取った SIGTTOU 22 Stop バッググラウンドプロセスが制御端末へ書き込んだ SIGURG 23 Ign ソケットに帯域外データ、緊急データが存在する SIGXCPU 24 Core CPU 時間の上限 (RLIMIT_CPU) を超えた 4。 SIGXFSZ 25 Core ファイルサイズの上限 (RLIMIT_FSIZE) を超えた 4。 SIGVTALRM 26 Term 仮想タイマ (プロセスのユーザモードでの CPU 時間) がタイムアウトした SIGPROF 27 Term プロファイリングタイマがタイムアウトした SIGWINCH 28 Ign 制御端末のウィンドウサイズが変更された SIGIO 29 Term 非同期 I/O イベント SIGPWR 30 Term 電源の異常 SIGSYS 31 Core 不正なシステムコールを実行した 5。 シグナルアクションの変更例
SIGKILL と SIGSTOP 以外のシグナルアクションを変更するには、trap コマンド や sigaction() などの API (C 言語) を使用します。
trap コマンド
trap は Bash に組み込まれたビルトインコマンドの一種で、シグナルアクションを設定できます 6。
ここでは、SIGINT 受信時に何もしない (シグナル無視) ように設定 (第一引数 '') してみます。
シグナルを無視に設定した例$ trap '' SIGINTこの状態では、SIGINT を受信しても反応しないため、次の実行例のように割り込みキー (Ctrl + C) が効きません。
$ while true; do sleep 1; echo "Stop me !"; done Stop me ! Stop me ! ^CStop me ! <--- Ctrl + C 押下しても効かず、コマンド継続 Stop me ! Stop me ! ^Z <--- Ctrl + Z 押下で停止 (SIGTSTP 送信) [1]+ Stopped sleep 1sigaction (C 言語 API)
sigaction() は、シグナルの動作を設定できる API (C 言語) です 7。
次のサンプルプログラム (set_sigh.c) では、SIGINT 受信時に handler 関数を実行するように設定してみます。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <unistd.h> static void handler(int signo) { /* * 本来ハンドラ内では、非同期シグナルセーフな関数を使用するべきですが、 * ここでは、そうでない printf()、exit() などの関数を使用しています。 * 非同期シグナルセーフについては $ man 7 signal をご参照ください。 */ printf(" ... Caught the SIGINT (%d)\n", signo); exit(EXIT_SUCCESS); } int main(void) { unsigned int sec = 1; struct sigaction act; // SIGINT 受信時に handler() を実行するように設定。 memset(&act, 0, sizeof act); act.sa_handler = handler; sigaction(SIGINT, &act, NULL); // Ctrl + C などで終了されるまで、1 秒ごとにメッセージを出力する。 for (;;) { sleep(sec); printf("Stop me !\n"); } return 0; }次の実行例では、SIGINT 受信後 handler() によってメッセージ (... Caught the SIGINT (2)) を出力しプログラムを終了しています。
$ gcc -o set_sigh set_sigh.c $ ./set_sigh Stop me ! Stop me ! Stop me ! ^C ... Caught the SIGINT (2) <--- Ctrl + C 押下で handler 関数が実行されるなお、その他の API として signal() がありますが、こちらは移植性の観点から非推奨となっています (古い API)。特別な理由が無い限りは使用を避けた方が良いと思います 8。
シグナルブロック
SIGKILL と SIGSTOP 以外のシグナルは、プロセス毎にブロックすることができます。
たとえば、あるプロセスが SIGINT を受信した場合、通常は標準動作によりプロセスが終了させられますが、SIGINT をブロックしていた場合は、SIGINT を受信しても、シグナル無視のように無反応となります。
シグナル無視との違いは、受信したシグナルを保留するか否かです。シグナル無視の場合はシグナルを保留しませんが、シグナルブロックの場合は保留できます。そのため、ブロックが解除されると、再度シグナルを受信する必要なく処理できます 9。
シグナルブロックの設定例
シグナルブロックを設定するには sigprocmask() などの API (C 言語) を使用します 10。
次のサンプルプログラム (block_sig.c) では、SIGINT をブロックしてから 5 秒後に SIGINT のブロックを解除してみます。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> int main(void) { unsigned int sec = 5; sigset_t set; // シグナル集合を空にし、SIGINT 追加 sigemptyset(&set); sigaddset(&set, SIGINT); // SIGINT をブロック (保留) する sigprocmask(SIG_BLOCK, &set, NULL); printf("Block the SIGINT for %d sec\n", sec); sleep(sec); // SIGINT のブロックを解除する printf("\nPassed %d sec, unblock the SIGINT\n", sec); sigprocmask(SIG_UNBLOCK, &set, NULL); printf("Done !\n"); return 0; }実行例では SIGINT ブロック後に SIGINT を送信していますが、SIGNINT 受信時の動作が実行されるのはブロック解除後となっているのが分かります。
$ gcc -o block_sig block_sig.c $ ./block_sig Block the SIGINT for 5 sec ^C^C^C^C^C^C^C^C^C^C <--- Ctrl + C 押下しても反応しない Passed 5 sec, unblock the SIGINT <--- SIGINT により終了するため、その後の "Done !" が無出力シグナルの状態確認
シグナルブロック (保留)、無視といった状態は
/proc/<PID>/status
から確認できます。$ cat /proc/29498/status Name: bash --- snip --- SigQ: 0/29305 // このプロセスの実 UID 宛にキューイングされたシグナル / キューイングされたシグナルの上限値 SigPnd: 0000000000000000 // 特定プロセス (スレッド) 宛の処理待ちシグナルの数 ShdPnd: 0000000000000000 // スレッドグループ全体宛の処理待ちシグナルの数 SigBlk: 0000000000010000 // ブロックされるシグナル (ビットマスク値) SigIgn: 0000000000380004 // 無視されるシグナル (ビットマスク値) SigCgt: 000000004b817efb // 捕捉待ちのシグナル (ビットマスク値) --- snip ---この中で、SigBlk、SigIgn、SigCgt は複数のシグナルをまとめて表現するために シグナルセット というマスク値で管理されているため、読み方が少しややこしいです。たとえば、SigIgn (0000000000380004) の場合は次のような意味になります (16進数を2進数に変換、1 の位置から対応するシグナルを判断)。つまり、PID 29498 の SigIgn (無視されるシグナル) は 3, 20, 21, 22 に対応する 4 つのシグナルという意味になります。
0000000000380004 (hex) -> 1110000000000000000100 (bit) シグナル名 番号 ||| *---------> SIGQUIT (03) ||*--------------------------> SIGTSTP (20) |*---------------------------> SIGTTIN (21) *----------------------------> SIGTTOU (22)なお、
ps s
でも同じような情報は確認できます。$ ps s UID PID PENDING BLOCKED IGNORED CAUGHT STAT TTY TIME COMMAND 1000 29498 0000000000000000 0000000000010000 0000000000380004 000000004b817efb Ss pts/0 0:00 /bin/bash 1000 29517 0000000000000000 0000000000010000 0000000000380004 000000004b817efb Ss pts/1 0:00 /bin/bash 1000 29572 0000000000000000 0000000000000000 0000000000003000 00000001ef804eff S+ pts/0 0:00 vim kernel/signal.c 1000 29581 0000000000000000 0000000000000000 0000000000000000 0000000000000000 S pts/1 0:00 sleep 100 1000 29588 0000000000000000 0000000000000000 0000000000000000 00000001f3d1fef9 R+ pts/1 0:00 ps sカーネル編 (v5.5)
ここからは、カーネル観点での話です。シグナルを送信してからシグナルアクションが処理されるまでの流れを見ていきます。
まずデータ構造を見ていきます。データ構造とは、カーネル実装で登場する構造体のことを指します。シグナルでは多くの構造体が関連しているため、先に把握しておくことで、コードが理解し易くなると思います。
次にカーネル実装 11 を見ていきます。シグナルは次のようにシグナル生成 (1 段階目)、シグナル配送 (2 段階目) という順に段階的な処理構成となっているため、それぞれ見ていきます。
- シグナル生成 (1 段階目) ... シグナルを送信したことを送信先のプロセスのデータ構造に記録する (シグナルを保留する)
- シグナル配送 (2 段階目) ... シグナルに応じて、シグナルアクションを実行する
データ構造
シグナルは、次のとおり、 task_struct、signal_struct、sighand_struct など多くの構造体が関連しています。
- task_struct (プロセスディスクリプタ)
- signal_struct (シグナルディスクリプタ)
- sighand_struct (シグナルハンドラディスクリプタ)
- sigpending
- k_sigaction
- sigaction
- thread_info
- siginfo
- kernel_siginfo
大変混乱しやすいと思いますので、個人的に重要だと思う箇所に関しては、以下のように用途ごとにまとめました。
保留中シグナルを示すフラグ
シグナル生成によって、シグナルを受け取ったプロセスは、シグナル配送によって処理されるまでの間、シグナル保留状態 (シグナル配送待ち) となります。この状態は TIF_SIGPENDING というフラグで示され、スレッドフラグ (flags) に記録されます。
シグナル保留用のキュー (特定プロセス用、スレッドグループ全体用)
受信したシグナルは、sigqueue 構造体によって管理され、関連する幾つかの情報 (※1) を持ちます。それらは保留用のキューに入れられます (sigpending の list につなげられます。その際、シグナルの送信宛に応じて 2 種類 (特定プロセス用、スレッドグループ全体用) の内のどちらかのキューが使用されます。
また、保留されたシグナルは signal に記録されます。この値は
/proc/<PID>/status
から確認できます (前述の「シグナルの状態確認」と後述の「おまけ」参照)。(※1) 幾つかの情報とは、次の kernel_siginfo 構造体のメンバを指します。
メンバ 意味 si_signo シグナル番号 si_code シグナルの発生元を示すコード (※2) si_errno シグナル発生を発生させることになった命令のエラーコード __sifilelds union のため条件に応じて si_pid (送信先 pid)、si_uid (送信先 uid) などに変化する (※2) シグナルの発生元を示すコードには次のような値が入ります。
include/uapi/asm-generic/siginfo.h より抜粋
定義名 値 意味 SI_USER 0 kill()、sigsend()、raise() によるシグナル送信 SI_KERNEL 0x80 カーネルによるシグナル送信 SI_QUEUE -1 sigqueue() によるシグナル送信 SI_TIMER -2 POSIX タイマの時間経過によるシグナル送信 SI_MESGQ -3 POSIX メッセージキューの状態変化によるシグナル送信 SI_ASYNCIO -4 非同期 I/O (AIO) 完了によるシグナル送信 SI_SIGIO -5 SIGIO のキューイングによるシグナル送信 SI_TKILL -6 tkill()、tgkill() によるシグナル送信 SI_DETHREAD -7 sent by execve() killing subsidiary threads 12 SI_ASYNCNL -60 getaddrinfo_a() での名前検索完了によるシグナル送信 なお、上記はシグナル共通の値です。特定のシグナルによっては、次のように別の値が入ることもあります 7。
シグナル名 si_code に入る値 SIGBUG BUS_* SIGCHLD CLD_* SIGFPE FPE_* SIGILL ILL_* SIGPOLL/SIGIO POLL_* SIGSEGV SEGV_* SIGTRAP TRAP_* SIGSYS SYS_SECCOMP シグナルブロック情報
sigprocmask() などによってブロックされたシグナル番号は、ブロック情報 (blocked, readl_blocked13) に記録されます。この値は
/proc/<PID>/status
から確認できます (前述の「シグナルの状態確認」 と後述の「おまけ」参照)。シグナルアクション関連の情報
sigaction() などでシグナルアクションの設定を変更すると、シグナルハンドラディスクリプタ (sighand_struct) に幾つかの情報 (※) が格納されます。また、シグナルハンドラディスクリプタの action[x] はシグナル番号 (_NSIG) 個の配列になっているため (action[シグナル番号-1])、シグナル毎に設定されます。
(※) 幾つかの情報とは、次の sigaction 構造体のメンバを指します。
メンバ 意味 sa_flags シグナルの使い方を示す SA_* フラグ7 sa_handler SIG_IGN、SIG_DFL、シグナルハンドラへのポインタなど、シグナルアクションの種類 sa_mask ハンドラ実行時にブロックする (受信を禁止する) シグナル 実装
シグナルを送信してからシグナルアクションが処理される (
kill -TERM 1234
を実行した際の処理) までの流れを見ていきます。以降、「シグナル生成」と「シグナル配送」で分けてそれぞれ見ていきます (「...」 の部分は省略箇所です)
シグナル生成
この段階での主な仕事は、送信先プロセスのデータ構造に「シグナルを送信した (シグナル配送待ちがある)」ということを送信先プロセスのデータ構造に記録し、通知することです (標準動作といったシグナルアクションはシグナル配送で処理されます)。
以降で行うコードリーディングの要約として、シグナル生成における処理の全体像を図示します。なお、関数の呼び出しが多いため、コードリーディングでは、この中でも特に重要な関数 (コア関数) である __send_signal() から読み進めていきます。
__send_signal()
この関数は、送信されたシグナルを配送するかどうかを決め (prepare_signal())、シグナルを保留用キューに入れます (_sigqueue_alloc(), list_add_tail()) その際、保留シグナルの番号を記録します (sigaddset())。また、配送する場合は、プロセスディスクリプタのスレッドフラグに TIFSIGPENDING を立て、配送可能なプロセスを起床します (complete_signal())。
詳細は以降をご参照ください。
1065 static int __send_signal(int sig, struct kernel_siginfo *info, struct task_struct *t, 1066 enum pid_type type, bool force) 1067 { 1068 struct sigpending *pending; 1069 struct sigqueue *q; 1070 int override_rlimit; 1071 int ret = 0, result; .... 1076 if (!prepare_signal(sig, t, force)) 1077 goto ret;1076 行目の prepare_signal() では、シグナルを配送する必要があるかどうかををチェックします。たとえば、シグナルハンドラ (t->sighand->action[sig - 1].sa.sa_handler) に SIG_IGN が設定されている場合や、標準動作が無視 (Ign) のシグナルの場合は、配送する必要がないので、ret ラベルまで飛びます。シグナルがブロックされていた場合は、シグナルを保留する必要があるため、以降の処理を行います。
また、標準動作が停止 (Stop) のシグナルと SIGCONT に対しては以下の処理も行います。
標準動作が停止 (Stop) のシグナル
シグナル保留用キュー (※) から SIGCONT を取り除きます。
SIGCONT
シグナル保留用キュー (※) から全ての停止系シグナルを取り除き、wake_up_state() で __TASK_STOPPED 状態のスレッドを起床します (プロセスを再開します)。
またプロセス (スレッドグループ) が終了処理中 (SIGNAL_STOP_STOPPED) の場合は、処理が完了したことにして、プロセスディスクリプタのフラグ (flags) に SIGNAL_CLD_CONTINUED と SIGNAL_STOP_CONTINUED を立てます。終了処理中でなくとも group_stop_count がカウントされている場合は、既に終了処理が終わったということなので (おそらく)、プロロセスディスクリプタのフラグ (flags) に SIGNAL_CLD_STOPPED と SIGNAL_STOP_CONTINUED を立てます。
(※) 「データ構造」でも説明しましたが、シグナル保留用キューには、「特定プロセス用 (t->pending)」と「スレッドグループ全体用 (t->signal->shared_pending)」の 2 種類があります。ここでは、その両方のキューから該当するシグナルを取り除きます。
1078 1079 pending = (type != PIDTYPE_PID) ? &t->signal->shared_pending : &t->pending;1079 行目では、pending に「特定プロセス用」「スレッドグループ全体用」のどちらのシグナル保留用キューを使用するかを決めています。今回は kill コマンド実行により、type に PIDTYPE_TGID が渡されるため、pending には、スレッドグループ全体用の方が使われます。
1080 /* 1081 * Short-circuit ignored signals and support queuing 1082 * exactly one non-rt signal, so that we can get more 1083 * detailed information about the cause of the signal. 1084 */ 1085 result = TRACE_SIGNAL_ALREADY_PENDING; 1086 if (legacy_queue(pending, sig)) 1087 goto ret;1086 行目の legacy_queue() では、sig が「標準シグナル」かつ「既にシグナル保留用キューに存在するなら」 ret ラベルまで飛びます (標準シグナルは複数の同一のシグナルを受信しても 1 つしか受信できない)。
1088 1089 result = TRACE_SIGNAL_DELIVERED; 1090 /* 1091 * Skip useless siginfo allocation for SIGKILL and kernel threads. 1092 */ 1093 if ((sig == SIGKILL) || (t->flags & PF_KTHREAD)) 1094 goto out_set;コメントのとおりですが、シグナルが SIGKILL OR 送信先がカーネルスレッド (PF_KTHREAD フラグ) の場合は、out_set ラベルまで飛び、以降のシグナル保留キュー登録処理などを行いません。
1096 /* 1097 * Real-time signals must be queued if sent by sigqueue, or 1098 * some other real-time mechanism. It is implementation 1099 * defined whether kill() does so. We attempt to do so, on 1100 * the principle of least surprise, but since kill is not 1101 * allowed to fail with EAGAIN when low on memory we just 1102 * make sure at least one signal gets delivered and don't 1103 * pass on the info struct. 1104 */ 1105 if (sig < SIGRTMIN) 1106 override_rlimit = (is_si_special(info) || info->si_code >= 0); 1107 else 1108 override_rlimit = 0; 11091105 行目の SIGRTMIN は 32 なので、sig が標準シグナルがどうかで処理が分岐します。
真 のルート (1106 行目) の is_si_special()では、info が SEND_SIG_NOINFO (ユーザモードのプロセスからシグナルが送信された) か、SEND_SIGPRIV (カーネルからシグナルが送信された) の場合は 真 を返します。右の info->si_code が 0 以上 (SI_USER か SI_KERNEL) の場合も 真 を返します。
なお、今回は kill コマンド実行により info->si_code に SI_USER が設定されるため、override_rlimit には 真 が入ります。
1110 q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit); 1111 if (q) { 1112 list_add_tail(&q->list, &pending->list); 1113 switch ((unsigned long) info) { 1114 case (unsigned long) SEND_SIG_NOINFO: 1115 clear_siginfo(&q->info); 1116 q->info.si_signo = sig; 1117 q->info.si_errno = 0; 1118 q->info.si_code = SI_USER; 1119 q->info.si_pid = task_tgid_nr_ns(current, 1120 task_active_pid_ns(t)); 1121 rcu_read_lock(); 1122 q->info.si_uid = 1123 from_kuid_munged(task_cred_xxx(t, user_ns), 1124 current_uid()); 1125 rcu_read_unlock(); 1126 break; 1127 case (unsigned long) SEND_SIG_PRIV: 1128 clear_siginfo(&q->info); 1129 q->info.si_signo = sig; 1130 q->info.si_errno = 0; 1131 q->info.si_code = SI_KERNEL; 1132 q->info.si_pid = 0; 1133 q->info.si_uid = 0; 1134 break; 1135 default: 1136 copy_siginfo(&q->info, info); 1137 break; 1138 } ....1110 行目では sigqueue_alloc() にて、シグナル管理用の sigqueue 型 q を新しく確保 (アロケーション) するように試みます。この関数の内部処理では、送信先プロセスの所有者 (ユーザ) が保留するシグナルの数が上限を超えないかどうかをチェックし、超えない場合にアロケーションを試みます。ただし、overriderlimit が 真 なら上限チェックをせずにアロケーションを試みます。
1112 行目から 1138 行目はアロケーション成功時の処理です。
1112 行目の list_add_tail() では、シグナル保留用キューにアロケーションした q を登録し (リストへつなぎ)、1113 行目以降の処理では、引数 info に応じて q へシグナルの情報を格納していきます。
1157 out_set: .... 1159 sigaddset(&pending->signal, sig); ....シグナル保留用キューのシグナルセットに現在のシグナル番号を追加します (前述の「シグナルの状態確認」OR 「データ構造」参照)。
1175 complete_signal(sig, t, type); 1176 ret: 1177 trace_signal_generate(sig, info, t, type != PIDTYPE_PID, result); 1178 return ret; 1179 }1175 行目の complete_signal() については後述します。
1177 行目の trace_signal_generate() は TRACE_EVENT マクロで signal_generate という名前のトレースポイントを作成しているようです。これは、次のようなカーネルトレース情報の出力に利用 (レポート) されています。
kill-5371 [003] 1058202.036613: signal_generate: sig=15 errno=0 code=0 comm=sleep pid 5359 grp=1 res=0complete_signal()
この関数は、送信されたシグナルを __send_signal() で配送すると決めた場合に呼び出されます。
そして、この関数でシグナル配送の準備ができているプロセスを探します。見つけたら該当プロセスのスレッドフラグに TIF_SIGPENDING を立て、起床します (通知します)。
詳細は以降をご参照ください。
984 static void complete_signal(int sig, struct task_struct *p, enum pid_type type) 985 { 986 struct signal_struct *signal = p->signal; 987 struct task_struct *t; 988 989 /* 990 * Now find a thread we can wake up to take the signal off the queue. 991 * 992 * If the main thread wants the signal, it gets first crack. 993 * Probably the least surprising to the average bear. 994 */ 995 if (wants_signal(sig, p)) 996 t = p; ...995 行目の wants_signal() では、次のパターンでシグナル配送可能 (準備ができている) なプロセスを探します。
シグナルがブロックされてない (SIGKILL, SIGSTOP はブロック不可) AND
- プロセスが終了処理中でない (p->flags に PF_ EXITING が無い) AND
- シグナル (sig) が SIGKILL (プロセスの状態やフラグ有無は見ないで強制する)
シグナルがブロックされてない (SIGKILL, SIGSTOP はブロック不可) AND
- プロセスが終了処理中でない (p->flags に PF_ EXITING が無い) AND
- プロセスの状態 (p->state) が一時停止中 (TASK_STOPPED) か、デバッガなどにより停止中 (TASK_TRACED) でない AND
- プロセスが CPU 上で実行中 (カレントプロセス) OR プロセスのスレッドフラグ (p->thread_info->flags) に TIF_SIGPENDING が無い
1021 /* 1022 * Found a killable thread. If the signal will be fatal, 1023 * then start taking the whole group down immediately. 1024 */ 1025 if (sig_fatal(p, sig) && 1026 !(signal->flags & SIGNAL_GROUP_EXIT) && 1027 !sigismember(&t->real_blocked, sig) && 1028 (sig == SIGKILL || !p->ptrace)) {コメントのとおり、Kill 可能なスレッドを探します。次の条件を全て満たす場合、1039 行目から 1042 行目の処理を行います。
- sig_fatal() が 真 (※) AND
- スレッドグループが終了中でない (SIGNAL_GROUP_EXIT は「スレッドグループが終了中」というフラグ) AND
- シグナルがブロックされていない (sigismember() は、t->real_blocked (ブロック情報) に sig が含まれていれば 真) AND
- シグナルが SIGKILL OR ptrace 関連フラグ (p->ptrace) がない時
(※) sig_fatal() は次の 2 パターンで 真 を返します。
- シグナルが標準シグナル AND
- 標準動作が無視 (Ign) / 停止 (Stop) のシグナルでない AND
- シグナルアクションが SIG_DFL
- シグナルがリアルタイムシグナル AND
- シグナルアクションが SIG_DFL
1029 /* 1030 * This signal will be fatal to the whole group. 1031 */ 1032 if (!sig_kernel_coredump(sig)) {sig_kernel_coredump() は、標準動作がコアダンプ (Core) のシグナルでない時 真 を返します。
1033 /* 1034 * Start a group exit and wake everybody up. 1035 * This way we don't have other threads 1036 * running and doing things after a slower 1037 * thread has the fatal signal pending. 1038 */ 1039 signal->flags = SIGNAL_GROUP_EXIT; 1040 signal->group_exit_code = sig; 1041 signal->group_stop_count = 0; 1042 t = p; 1043 do { 1044 task_clear_jobctl_pending(t, JOBCTL_PENDING_MASK); 1045 sigaddset(&t->pending.signal, SIGKILL); 1046 signal_wake_up(t, 1); 1047 } while_each_thread(p, t); 1048 return; 1049 } 1050 }1039 行目から 1042 行目までの処理を行った後は、do-while でスレッドグループ内を走査し終わるまで、次に示す処理をループします。
- 1044 行目の task_clear_jobctl_pending() で、保留中のジョブコントロールフラグ (t->jobctl) をクリア
- 1045 行目の sigaddset() で、シグナル保留用キューのシグナルセットに SIGKILL (番号) を追加
- 1046 行目の signal_wake_up() で、スレッドフラグ (t->flags) に TIF_SIGPENDING を立て、TASK_WAKEKILL (SIGKILL などの重要なシグナルを受信可能) か TASK_INTERRUPTIBLE (何らかのイベント待ち) のプロセスを起床 (通知)
ループを抜けると、この関数での処理を終了します。
1052 /* 1053 * The signal is already in the shared-pending queue. 1054 * Tell the chosen thread to wake up and dequeue it. 1055 */ 1056 signal_wake_up(t, sig == SIGKILL); 1057 return; 1058 }ここまで到達できた場合、signal_wake_up() で、スレッドフラグ (t->flags) に TIF_SIGPENDING を立て、TASK_INTERRUPTIBLE 状態のプロセスを起床します (保留シグナルを通知)。また、シグナルが SIGKILL の場合は TASK_WAKEKILL 状態のプロセスも起床します。
以上で、「シグナル生成」の処理は終わりです。続いて、「シグナル配送」を見ていきます。
シグナル配送
この段階での主な仕事は、シグナルハンドラや標準動作などのシグナルアクションを実行することです。ただし、この処理はプロセスが保留シグナルを持つ (前述の「シグナル生成」で説明した TIF_SIGPENDING フラグを持つ) 場合に限ります。
以降で行うコードリーディングの要約として、シグナル配送における処理の全体像を図示します。なお、シグナル配送は一部アーキテクチャに依存するコードがありますが、ここでは x86 (arch/x86 配下) のコードを見ていきます。
exit_to_usermode_loop()
この関数は、カーネルモードからユーザモードへ戻る度 (割り込み/例外ハンドラ、システコール処理後など) に呼び出され、スレッドフラグをチェックします。その際、TIF_SIGPENDING があると、do_signal() を呼びだします (160 行目)。
arch/x86/entry/common.c (L136)
136 static void exit_to_usermode_loop(struct pt_regs *regs, u32 cached_flags) 137 { 138 /* 139 * In order to return to user mode, we need to have IRQs off with 140 * none of EXIT_TO_USERMODE_LOOP_FLAGS set. Several of these flags 141 * can be set at any time on preemptible kernels if we have IRQs on, 142 * so we need to loop. Disabling preemption wouldn't help: doing the 143 * work to clear some of the flags can sleep. 144 */ 145 while (true) { 146 /* We have work to do. */ 147 local_irq_enable(); ... 158 /* deal with pending signal delivery */ 159 if (cached_flags & _TIF_SIGPENDING) 160 do_signal(regs); ... 171 /* Disable IRQs and retry */ 172 local_irq_disable(); 173 174 cached_flags = READ_ONCE(current_thread_info()->flags); 175 176 if (!(cached_flags & EXIT_TO_USERMODE_LOOP_FLAGS)) 177 break; 178 } 179 }do_signal()
この関数は、標準動作の実行 (get_signal()) やシグナルハンドラの実行 (handle_signal()) を行います。また、必要に応じてシステコールの再実行や特定のシステムコールによって一時的に書き換えられたシグナルブロック情報の復元を行います。
arch/x86/kernel/signal.c (L806)
806 /* 807 * Note that 'init' is a special process: it doesn't get signals it doesn't 808 * want to handle. Thus you cannot kill init even with a SIGKILL even by 809 * mistake. 810 */ 811 void do_signal(struct pt_regs *regs) 812 { 813 struct ksignal ksig; 814 815 if (get_signal(&ksig)) { 816 /* Whee! Actually deliver the signal. */ 817 handle_signal(&ksig, regs); 818 return; 819 } 820815 行目の get_signal() では標準動作の実行と保留用キューからのシグナル取り出しを行います。その結果が 真 (取り出せたら) なら handle_signal() を実行しシグナルハンドラの実行を行います。偽 (取り出せなかった) なら以降の処理を行います (get_signal()、handle_signal() は重要な関数なので詳細は後述します)。
821 /* Did we come from a system call? */ 822 if (syscall_get_nr(current, regs) >= 0) { 823 /* Restart the system call - no handlers present */ 824 switch (syscall_get_error(current, regs)) { 825 case -ERESTARTNOHAND: 826 case -ERESTARTSYS: 827 case -ERESTARTNOINTR: 828 regs->ax = regs->orig_ax; 829 regs->ip -= 2; 830 break; 831 832 case -ERESTART_RESTARTBLOCK: 833 regs->ax = get_nr_restart_syscall(regs); 834 regs->ip -= 2; 835 break; 836 } 837 }ここはシステムコールの再実行に関する処理です (システムコールの処理中にシグナルを受信した場合、エラーを吐いて処理を中断することがあるため、このような処理があります)。
822 行目の syscall_get_nr() は、ユーザモードのレジスタ (regs) からシステムコール番号を取得します。それが 0 以上だったら、824 行目の syscall_get_erro() でエラー番号を取得し、そのエラー番号に応じてカーネルがシステムコールを再実行します。
なお、システムコールの再実行については「Linuxのシステムコール再実行について」という記事に詳しく書かれていました。regs->ip -= 2 の意味など詳細はこちらをご参照ください。
838 839 /* 840 * If there's no signal to deliver, we just put the saved sigmask 841 * back. 842 */ 843 restore_saved_sigmask(); 844 }restore_saved_sigmask() では、ppoll、pselect、epoll、sigsuspend などのシステムコールによって一時的に書き換えられたシグナルブロック情報 (task_struct->blocked) を、あらかじめ退避させていたシグナルブロック情報 (task_struct->saved_sigmask) から復元します。
get_signal()
この関数は、シグナル保留用キューからシグナルを取り出し、シグナルに応じて標準動作の実行を行います。
2521 bool get_signal(struct ksignal *ksig) 2522 { 2523 struct sighand_struct *sighand = current->sighand; 2524 struct signal_struct *signal = current->signal; 2525 int signr; .... 2540 relock: .... 2542 /* 2543 * Every stopped thread goes here after wakeup. Check to see if 2544 * we should notify the parent, prepare_signal(SIGCONT) encodes 2545 * the CLD_ si_code into SIGNAL_CLD_MASK bits. 2546 */ 2547 if (unlikely(signal->flags & SIGNAL_CLD_MASK)) { 2548 int why; 2549 2550 if (signal->flags & SIGNAL_CLD_CONTINUED) 2551 why = CLD_CONTINUED; 2552 else 2553 why = CLD_STOPPED; 2554 2555 signal->flags &= ~SIGNAL_CLD_MASK; 2556 2557 spin_unlock_irq(&sighand->siglock); 2558 2559 /* 2560 * Notify the parent that we're continuing. This event is 2561 * always per-process and doesn't make whole lot of sense 2562 * for ptracers, who shouldn't consume the state via 2563 * wait(2) either, but, for backward compatibility, notify 2564 * the ptracer of the group leader too unless it's gonna be 2565 * a duplicate. 2566 */ 2567 read_lock(&tasklist_lock); 2568 do_notify_parent_cldstop(current, false, why); 2569 2570 if (ptrace_reparented(current->group_leader)) 2571 do_notify_parent_cldstop(current->group_leader, 2572 true, why); 2573 read_unlock(&tasklist_lock); 2574 2575 goto relock; 2576 } ....2543 行目のコメントにも少し書かれていますが、2547 行目のフラグ (SIGNAL_CLD_MASK == SIGNAL_CLD_STOPPED or SIGNAL_CLD_CONTINUED )は、シグナル生成 (_send_signal()) で呼び出される prepare_signal() で SIGCONT の処理をするときに設定されるます。このフラグが設定されている場合は、donotify_parent_cldstop() で親にシグナル (SIGCHLD) を送信します。
2588 for (;;) { 2589 struct k_sigaction *ka; .... 2616 /* 2617 * Signals generated by the execution of an instruction 2618 * need to be delivered before any other pending signals 2619 * so that the instruction pointer in the signal stack 2620 * frame points to the faulting instruction. 2621 */ 2622 signr = dequeue_synchronous_signal(&ksig->info); 2623 if (!signr) 2624 signr = dequeue_signal(current, ¤t->blocked, &ksig->info); 2625 2626 if (!signr) 2627 break; /* will return 0 */ ....2622 行目の dequeue_synchronous_signal() で同期シグナル (SIG[SEGV|BUS|ILL|TRAP|FPE|SYS]) をキューから取り出します。なければ、2624 行目の dequeue_signal() で非同期シグナルをキューから取り出します。それでもなければ、break でループを抜けこの関数での処理を終了します。
2635 ka = &sighand->action[signr-1]; 2636 2637 /* Trace actually delivered signals. */ 2638 trace_signal_deliver(signr, &ksig->info, ka);2638 行目の trace_signal_deliver() は TRACE_EVENT マクロで signal_deliver という名前のトレースポイントを作成するようです。これは、次のようなカーネルトレース情報の出力に利用 (レポート) されています。定義元のコメントを見るに、ここに到達できた時点でシグナルが配送されたことになるようです。
trace-cmd-17636 [001] 607498.455844: signal_deliver: sig=2 errno=0 code=128 sa_handler=5567b90d1540 sa_flags=1400000以降は、シグナル無視、シグナルハンドラ、標準動作などのシグナルアクションに関する処理です。ka (2635 行目) の内容により分岐します。
2639 2640 if (ka->sa.sa_handler == SIG_IGN) /* Do nothing. */ 2641 continue;C 言語 API のsigaction() などでシグナルアクションに無視 (SIG_IGN) を設定していた場合は、何もしません。
2642 if (ka->sa.sa_handler != SIG_DFL) { 2643 /* Run the handler. */ 2644 ksig->ka = *ka; 2645 2646 if (ka->sa.sa_flags & SA_ONESHOT) 2647 ka->sa.sa_handler = SIG_DFL; 2648 2649 break; /* will return non-zero "signr" value */ 2650 }2642 行目は、SIG_DFL でない場合 (つまり、シグナルハンドラを設定していた場合) 、2644 行目で ksig->ka に ka のアドレスを入れます。これは get_signal() を抜けたあとの handle_signal() でユーザプロス用のレジスタに設定する時に使用します。
2646 行目の SA_ONESHOT (sigaction() で設定可能なフラグ) はシグナルハンドラ実行後にシグナルアクションをデフォルト (SIG_DFL) に戻すという意味です。つまり、シグナルハンドラの実行するのは、初回のシグナル受信時だけです。
最後に break でループを抜け、この関数での処理を終えます。この場合 signr には取り出したシグナル番号が入っているはずなので、後述の handle_signal() が実行されます。
2651 2652 /* 2653 * Now we are doing the default action for this signal. 2654 */ 2655 if (sig_kernel_ignore(signr)) /* Default is nothing. */ 2656 continue; ....ここからは、標準動作の実行に関する処理です。
2655 行目の sig_kernel_ignore() は signr の標準動作が無視 (Ign) なら 真 を返します。その場合、何もしません。
2671 2672 if (sig_kernel_stop(signr)) { 2673 /* 2674 * The default action is to stop all threads in 2675 * the thread group. The job control signals 2676 * do nothing in an orphaned pgrp, but SIGSTOP 2677 * always works. Note that siglock needs to be 2678 * dropped during the call to is_orphaned_pgrp() 2679 * because of lock ordering with tasklist_lock. 2680 * This allows an intervening SIGCONT to be posted. 2681 * We need to check for that and bail out if necessary. 2682 */ 2683 if (signr != SIGSTOP) { 2684 spin_unlock_irq(&sighand->siglock); 2685 2686 /* signals can be posted during this window */ 2687 2688 if (is_current_pgrp_orphaned()) 2689 goto relock; 2690 2691 spin_lock_irq(&sighand->siglock); 2692 } 2693 2694 if (likely(do_signal_stop(ksig->info.si_signo))) { 2695 /* It released the siglock. */ 2696 goto relock; 2697 } 2698 2699 /* 2700 * We didn't actually stop, due to a race 2701 * with SIGCONT or something like that. 2702 */ 2703 continue; 2704 }2672 行目の sig_kernel_stop() は signr の標準動作がプロセス一時停止 (Stop) のシグナルなら 真 を返します。真 なら、2694 行目の do_signal_stop() でプロセスディスクリプタのフラグ (flags) に SIGNAL_STOP_STOPPED を立て、プロセスの一時停止 (TASK_STOPPED) を行います。
ただし、シグナルが SIGSTOP 以外かつ、プロセスグループが孤児 (親なし) の 場合は do_signal_stop() を実行しません (一時停止しません)。
2705 2706 fatal: 2707 spin_unlock_irq(&sighand->siglock); .... 2711 /* 2712 * Anything else is fatal, maybe with a core dump. 2713 */ 2714 current->flags |= PF_SIGNALED; 2715 2716 if (sig_kernel_coredump(signr)) { 2717 if (print_fatal_signals) 2718 print_fatal_signal(ksig->info.si_signo); 2719 proc_coredump_connector(current); 2720 /* 2721 * If it was able to dump core, this kills all 2722 * other threads in the group and synchronizes with 2723 * their demise. If we lost the race with another 2724 * thread getting here, it set group_exit_code 2725 * first and our do_group_exit call below will use 2726 * that value and ignore the one we pass it. 2727 */ 2728 do_coredump(&ksig->info); 2729 } ....2716 行目の sig_kernel_coredump() は signr の標準動作がコアダンプ (Core) なら 真 を返します。真 なら 2728 行目の do_coredump() でシグナルディスクリプタのフラグ (flags) に SIGNAL_GROUP_COREDUMP を立て、コアダンプ生成処理を行います。
2731 /* 2732 * Death signals, no core dump. 2733 */ 2734 do_group_exit(ksig->info.si_signo); 2735 /* NOTREACHED */ 2736 }signr の標準動作がプロセス終了 (Term) やコアダンプ (Core) の場合、2724 行目の do_group_exit() でシグナルディスクリプタのフラグ (flags) に SIGNAL_GROUP_EXIT を立て、プロセス (スレッドグループ) の終了を行います。
なお、2736 行目の
}
は 2588 行目のfor (;;) {
の対になる部分です。このループは、2627 行目の break か 2649 行目の break で抜けることができます。前者はシグナル保留用キューから取り出すものが無くなった場合で、後者はシグナルアクション (sa_handler) にシグナルハンドラが設定されていた場合です。2737 spin_unlock_irq(&sighand->siglock); 2738 2739 ksig->sig = signr; 2740 return ksig->sig > 0; 2741 }最後にキューから取り出したシグナル番号が 0 より大きいかどうかを判定します。大きい場合はシグナルを取り出せたということで 真 を返し、次の handle_signal() へ進みます。
handle_signal()
この関数は、システムコールの再実行 (必要なら) と、シグナルハンドラ実行用にユーザモード時のスタックフレームを設定します。
arch/x86/kernel/signal.c (L71)
710 static void 711 handle_signal(struct ksignal *ksig, struct pt_regs *regs) 712 { 713 bool stepping, failed; 714 struct fpu *fpu = ¤t->thread.fpu; 715 ... 719 /* Are we from a system call? */ 720 if (syscall_get_nr(current, regs) >= 0) { 721 /* If so, check system call restarting.. */ 722 switch (syscall_get_error(current, regs)) { 723 case -ERESTART_RESTARTBLOCK: 724 case -ERESTARTNOHAND: 725 regs->ax = -EINTR; 726 break; 727 728 case -ERESTARTSYS: 729 if (!(ksig->ka.sa.sa_flags & SA_RESTART)) { 730 regs->ax = -EINTR; 731 break; 732 } 733 /* fallthrough */ 734 case -ERESTARTNOINTR: 735 regs->ax = regs->orig_ax; 736 regs->ip -= 2; 737 break; 738 } 739 } 740 ...do_signal() でも出ましたが14、ここにもシステムコールの再実行に関する処理があります。ただし、カーネルがシステムコールを再実行するケースは次の 2 パターンのみです。
- エラーが -ERESTARTSYS (728 行目) かつ sa_flags に SA_RESTART (sigaction() で設定可能なフラグ) が立っている場合
- エラーが 734 行目の -ERESTARTNOINTR (734 行目) だった場合
それ以外の場合、カーネルはシステムコールを再実行せず regs->ax に -EINTR (関数呼び出しが割り込まれたというエラー) を設定します。システムコールを再実行するかどうかは、このエラーからユーザ (ユーザプログラムの条件) が判断します。
750 failed = (setup_rt_frame(ksig, regs) < 0); ... 751 if (!failed) { 752 /* 753 * Clear the direction flag as per the ABI for function entry. 754 * 755 * Clear RF when entering the signal handler, because 756 * it might disable possible debug exception from the 757 * signal handler. 758 * 759 * Clear TF for the case when it wasn't set by debugger to 760 * avoid the recursive send_sigtrap() in SIGTRAP handler. 761 */ 762 regs->flags &= ~(X86_EFLAGS_DF|X86_EFLAGS_RF|X86_EFLAGS_TF); 763 /* 764 * Ensure the signal handler starts with the new fpu state. 765 */ 766 fpu__clear(fpu); 767 } 768 signal_setup_done(failed, ksig, stepping); 769 }750 行目の setup_rt_frame() では、シグナルハンドラ実行用にユーザモード時のスタックフレームを設定します。スタックフレームには、シグナルハンドラ実行に必要な情報 (シグナル番号、siginfo 構造体 の情報や rt_sigreturn() システムコールをユーザモードプロセスから呼び出すための情報が格納されています。rt_sigreturn() はシグナルハンドラ実行後にカーネルモードへ復帰するのに必要です。
また、setup_rt_frame() での設定が成功した場合は、762、766 行目でフラグ (regs->flags) や FPU state (浮動小数点数レジスタ) をクリアします (理由は不明)。
そして、最後 (768 行目) の signal_setup_done() では setup_rt_frame() での設定成否で処理が分岐します。
成功した場合:
sigorsets() でカレントプロセスのブロック情報 (current->blocked) と sigaction() でユーザが設定したブロック情報 (sigaction->sa_mask) の論理和 (OR) を取り、それをカレントプロセスのブロック情報に設定します。また、スレッドフラグに TIF_SINGLESTEP (デバッガで使用) が設定されていた場合は、ptrace_notify(SIGTRAP) を実行します。失敗した場合:
force_sigsegv() でカレントプロセスに SIGSEGV を送信します。以上で、「シグナル配送」の処理は終わりです。
おまけ
/proc/<PID>/status
にあるシグナル関連のコード
/proc/<PID>/status
にある SigPnd、ShdPnd、などの値はどこに由来するのかコード上から調べました。task_sig() に答えがありました。たとえば、SigBlk は 283 行目で p->blocked を入れています。
266 static inline void task_sig(struct seq_file *m, struct task_struct *p) 267 { 268 unsigned long flags; 269 sigset_t pending, shpending, blocked, ignored, caught; 270 int num_threads = 0; 271 unsigned int qsize = 0; 272 unsigned long qlim = 0; 273 274 sigemptyset(&pending); 275 sigemptyset(&shpending); 276 sigemptyset(&blocked); 277 sigemptyset(&ignored); 278 sigemptyset(&caught); 279 280 if (lock_task_sighand(p, &flags)) { 281 pending = p->pending.signal; 282 shpending = p->signal->shared_pending.signal; 283 blocked = p->blocked; 284 collect_sigign_sigcatch(p, &ignored, &caught); 285 num_threads = get_nr_threads(p); 286 rcu_read_lock(); /* FIXME: is this correct? */ 287 qsize = atomic_read(&__task_cred(p)->user->sigpending); 288 rcu_read_unlock(); 289 qlim = task_rlimit(p, RLIMIT_SIGPENDING); 290 unlock_task_sighand(p, &flags); 291 } 292 293 seq_put_decimal_ull(m, "Threads:\t", num_threads); 294 seq_put_decimal_ull(m, "\nSigQ:\t", qsize); 295 seq_put_decimal_ull(m, "/", qlim); 296 297 /* render them all */ 298 render_sigset_t(m, "\nSigPnd:\t", &pending); 299 render_sigset_t(m, "ShdPnd:\t", &shpending); 300 render_sigset_t(m, "SigBlk:\t", &blocked); 301 render_sigset_t(m, "SigIgn:\t", &ignored); 302 render_sigset_t(m, "SigCgt:\t", &caught); 303 }SIGKILL、SIGSTOP がブロックできない理由
SIGKILL と SIGSTOP がブロックできない理由をコード上から探してみました。
シグナルブロックに使う sigprocmask() 実行時に呼び出される sys_rt_sigprocmask() に答えがありました。3025 行目の sigdelsetmask() で新しく設定したブロック情報 (new_set) から SIGKILL と SIGSTOP を削除しています (つまり、SIGKILL と SIGSTOP をブロックするように設定しても実際は設定されない)。
3003 /** 3004 * sys_rt_sigprocmask - change the list of currently blocked signals 3005 * @how: whether to add, remove, or set signals 3006 * @nset: stores pending signals 3007 * @oset: previous value of signal mask if non-null 3008 * @sigsetsize: size of sigset_t type 3009 */ 3010 SYSCALL_DEFINE4(rt_sigprocmask, int, how, sigset_t __user *, nset, 3011 sigset_t __user *, oset, size_t, sigsetsize) 3012 { 3013 sigset_t old_set, new_set; 3014 int error; .... 3022 if (nset) { 3023 if (copy_from_user(&new_set, nset, sizeof(sigset_t))) 3024 return -EFAULT; 3025 sigdelsetmask(&new_set, sigmask(SIGKILL)|sigmask(SIGSTOP)); 3026 3027 error = sigprocmask(how, &new_set, NULL); 3028 if (error) 3029 return error; 3030 } .... 3037 return 0; 3038 }なお、シグナルブロックは sigaction() の sa_mask でも設定できますので、こちらも見てみます。
sigaction() の場合は sys_rt_sigaction() -> do_sigaction() の 3967、3968 行目で同様の処理を行っていました。
3949 int do_sigaction(int sig, struct k_sigaction *act, struct k_sigaction *oact) 3950 { 3951 struct task_struct *p = current, *t; 3952 struct k_sigaction *k; 3953 sigset_t mask; .... 3966 if (act) { 3967 sigdelsetmask(&act->sa.sa_mask, 3968 sigmask(SIGKILL) | sigmask(SIGSTOP)); .... 3988 } .... 3991 return 0; 3992 }SIGKILL、SIGSTOP を無視 or シグナルハンドラを設定できない理由
SIGKILL、SIGSTOP が無視 or 動作変更できない理由もコード上から調べてみました。
sigaction() 実行時に呼び出される sys_rt_sigaction() -> do_sigaction() の 3955 行目に答えがありました。一番右の条件で、act が有効値かつ sig_kernel_only() が 真 (シグナルが SIGKILL か SIGSTOP の場合) の時に -EINVAL でエラー (引数が無効) で失敗します。 sigaction() に SIGKILL および SIGSTOP を渡すと失敗するので、当然ですが、無視 (SIG_IGN) もシグナルハンドラ (ユーザ定義の関数) も設定できないということになります。
3949 int do_sigaction(int sig, struct k_sigaction *act, struct k_sigaction *oact) 3950 { 3951 struct task_struct *p = current, *t; 3952 struct k_sigaction *k; 3953 sigset_t mask; 3954 3955 if (!valid_signal(sig) || sig < 1 || (act && sig_kernel_only(sig))) 3956 return -EINVAL; .... 3966 if (act) { .... 3969 *k = *act; 3970 /* 3971 * POSIX 3.3.1.3: 3972 * "Setting a signal action to SIG_IGN for a signal that is 3973 * pending shall cause the pending signal to be discarded, 3974 * whether or not it is blocked." 3975 * 3976 * "Setting a signal action to SIG_DFL for a signal that is 3977 * pending and whose default action is to ignore the signal 3978 * (for example, SIGCHLD), shall cause the pending signal to 3979 * be discarded, whether or not it is blocked" 3980 */ 3981 if (sig_handler_ignored(sig_handler(p, sig), sig)) { 3982 sigemptyset(&mask); 3983 sigaddset(&mask, sig); 3984 flush_sigqueue_mask(&mask, &p->signal->shared_pending); 3985 for_each_thread(p, t) 3986 flush_sigqueue_mask(&mask, &t->pending); 3987 } 3988 } .... 3991 return 0; 3992 }参考文献
- Linux システムプログラミング (9 章シグナル)
- Linux プログラミングインタフェース (20 章シグナル: 基礎、21 章シグナル: シグナルハンドラ, 22 章シグナル: 応用)
- 詳解 Linux カーネル 第3版 (11 章シグナル)
- Linux カーネル解読室 (8 章 シグナル処理)
- Mastering Linux Kernel Development (Chapter 3: Signal Management)
- kernel source code (v5.5)
- Using the TRACE_EVENT() macro (Part 1)
- TASK_KILLABLE: Linux での新しいプロセスの状態
man 1 bash
man 2 setrlimit
man 2 seccomp
man 2 signal
man 2 sigaction
man 3 getaddrinfo_a
man 5 proc
man 7 signal
man 7 pipe
詳細は
man 7 signal
をご参照ください。 ↩詳細は
man 5 core
をご参照ください。 ↩詳細は
man 7 pipe
ご参照ください。 ↩詳細は
man 2 setrlimit
ご参照ください。 ↩詳細は
man 2 seccomp
ご参照ください。 ↩詳細は
man 1 bash
をご参照ください。 ↩詳細は
man 2 sigaction
をご参照ください。 ↩詳細は
man 2 signal
をご参照ください。 ↩シグナル無視の場合も解除はできますが、シグナル無視の状態で受信したシグナルは保留されないため、解除された後も処理されません。無視を解除した上で再度シグナルを受信する必要があります。 ↩
詳細は
man 2 sigprocmask
をご参照ください。 ↩カーネルバージョンは調査時点での最新である v5.5 を使用します。 ↩
man 2 execve
カーネルソースなどを調べましたが、この原文以上の情報が見当たらず、意味がよく理解できなかったため、原文のまま記載します。 ↩real_blocked は、rt_sigtimedwait(), rt_sigtimedwait_time32(), rt_sigtimedwait_time64() システムコール使用時に設定されるようです。 ↩
do_signal() でのシステムコール再実行は、handle_signal() を実行しない場合に通るパス (get_signal() が 偽 の時) なので、ここでのシステムコール再実行処理とは重複しません。 ↩
- 投稿日:2020-05-24T20:49:40+09:00
Linuxのインストールや設定でマイナートラブルの対応記録
VirtualBox上にLinuxをインストールしたときのマイナートラブルの対応記録
共通
- Guest OS上のHDDインストール時に起動時に使用したISOファイルを指定する必要があるが、Guest Additionsがまだインストールできない状況なので、ISOファイルをどこかに置いて、scpやwgetでGuest OS内にダウンロードする。
- Guest OS上でモニタを認識できないときは、ディスプレイ設定で”VMSVGA”以外を試す。
Guest Additionsインストール時の"Kernel headers not found for target kernel"エラー
apt install gcc make perl linux-headers-
`uname -r
`
を実行する。参考。TinyCore
ibus-mozcインストール後にキーボードが英語になってしまう
この方法で日本語キーボードに戻す。
終わりに
先人のみなさま、ありがとうございます。
(このページに追記していくかも。)
v1.0 2020May24
- 投稿日:2020-05-24T17:50:52+09:00
Emacs ベース環境構築
普段Emacsをよく使っており,仮想環境で新たにLinux環境を構築した場合もまずEmacsを入れている筆者です.
Vagrantには毎回emacsインストールコマンドを書いてます.Emacsが使えるだけで満足するならよかったんですが,自分好みにカスタマイズして普段使っているせいで,初期状態Emacsでは何かと不便になってしまった.
そこで普段Emacsを使う際の ベース環境 を構築する手順を書いておきます.
内容
1. 事前準備
2. .emacs記述による構築
2.1 表示関連設定
2.2 色設定
2.3 パッケージ関連設定
2.4 コード補完関連設定3. M-xコマンドによる構築
3.1 色設定
3.2 コード補完関連導入4. .emacs記述全体
1. 事前準備
コード補完関連の構築に必要なものをまずインストール.
sudo apt-get install -y cmake clang2. .emacs記述による構築
Emacsの設定には
.emacs
というファイルに色々と記述していく.
ちなみにほかの記事を調べると.emacs.el
やinit.el
に記述などあるが,
今まで.emacsで全て設定できたので,.emacs
を前提に書いていきます.
ちなみにファイルの設置場所は$HOME/.emacsです.
2.1 表示関連設定
(global-linum-mode t) (menu-bar-mode -1) (tool-bar-mode -1) (setq inhibit-startup-message t) (setq make-backup-files nil)1行目は行番号を表示するための記述. 必須です.
以降は各種バー類を非表示にするものと,スタートアップ時のメッセージを非表示にする記述.
正直普段-nw
オプションで起動するため,あまり効果がない部分. 適宜設定.2.2 色設定
この項目も.emacsに記述されるが,手動での入力は非推奨.
設定が反映されない可能性が高いので,後ほどの 2. M-xコマンドによる構築 で説明する.
ちなみにコメントアウトで以下のような警告文が表示される.;; custom-set-faces was added by Custom. ;; If you edit it by hand, you could mess it up, so be careful. ;; Your init file should contain only one such instance. ;; If there is more than one, they won't work right.簡単に言うと,手動で変更するときはうまく機能しないので注意してくださいという感じです.
2.3 パッケージ関連設定
(require 'package) (add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/") t) (package-initialize)後述する機能をインストールする際に必要になる設定.
Melpaというパッケージを使えるようにしておきましょう.
また(package-initialize)
は,各種パッケージ設定を書いた後に実行する必要があるため,
Melpa以外にもパッケージを追加したい場合は必ずこの設定の前に書いてください.2.4 コード補完関連設定
company(when (locate-library "company") (global-company-mode 1) (global-set-key (kbd "C-M-i") 'company-complete) (define-key company-active-map (kbd "C-s") 'company-select-next) (define-key company-active-map (kbd "C-w") 'company-select-previous) (define-key company-search-map (kbd "C-s") 'company-select-next) (define-key company-search-map (kbd "C-w") 'company-select-previous) (define-key company-active-map (kbd "<tab>") 'company-complete-selection))irony(eval-after-load "irony" '(progn (add-to-list 'company-backends 'company-irony) (add-hook 'irony-mode-hook 'irony-cdb-autosetup-compile-options) (add-hook 'c-mode-common-hook 'irony-mode)))yasnippet(eval-after-load "yasnippet" '(progn (define-key yas-keymap (kbd "<tab>") nil) (yas-global-mode 1)))
company
+irony
+yasnippet
を用いてコード補完機能を構築.
companyの各種操作方法は好みで変えてもらって大丈夫です. 自分は左手でCtrl押しながら選択できるようにw,sでバインドしています.
なおこの記述だけでは勿論実行されないので,後ほど 2. M-xコマンドによる構築 で
インストール手順を説明します.
companyとirony, yasnippetに関しては他記事の方が詳しく説明されているので,各自調べてみてください.
ちなみに先にパッケージをインストールしていないとemacs起動時にエラーが表示されると思います.
なのでインストール後にこの記述を書くのをお勧めします.3. M-xコマンドによる構築
M-xコマンドを用いた各種設定を説明していきます.
ちなみにM-xはAlt + x
です. MはMetaキーに由来してるとか.3.1 色設定
先ほど説明した通り,手書き非推奨なのでコマンドで設定していきます.
設定したい部分にカーソルをもっていって(例えば変数の色を設定したい場合はintなどにカーソルを)M-x describe-face RETと入力する.
そうすると今回の例だとDescribe face (default ‘font-lock-type-face’):
と表示される.
emacsではint
やclass
などにタイプが設定されており,それぞれに色を設定することでコードが見やすくなっている.
この状態でENTERキーを押すと,画面が分割されてこのフォントタイプの情報画面に移行できる.
ちなみにNo Windowモードで起動している場合はマウスでの操作切り替えができないので,Ctrl-x + o
で操作切り替えができる.次にこの設定画面内の1行目,
customize this face
状にカーソルをもっていってENTERキーを押す.
下の方に行くと[X]Foreground: color-xxx [choose] (sample)
という欄があるので,
chooseを押して色一覧が表示される. この中から各自好きな色を設定してください.ちなみに環境やバージョンによってはForegroundではなく
[X]Inherit
になっている場合がある.
その時はすぐ下にあるShow All Attributes
をおして, Foregroundを選択してください.
設定終了後は上の方にある[Apply and save]
または通常の保存同様Ctrl-x + Ctrl-s
で保存.3.2 コード補完関連導入
M-x package-install RET irony RET M-x package-install RET company-irony RET M-x package-install RET yasnippet RET M-x package-install RET flycheck RET.emacs上でのパッケージ記述が間違っていなければ,問題なくインストールできると思う.
なおironyだけはインストール後にM-x irony-install-server RETを実行してください. このコマンドを実行することでirony関連の設定をしてくれる.
4. .emacs記述全体
最後に今まで設定してきたものを含めた.emacs全体を載せておきます.
.emacs(global-linum-mode t) (menu-bar-mode -1) (tool-bar-mode -1) (setq inhibit-startup-message t) (setq make-backup-files nil) ;; faces color ;;(custom-set-faces ;; custom-set-faces was added by Custom. ;; If you edit it by hand, you could mess it up, so be careful. ;; Your init file should contain only one such instance. ;; If there is more than one, they won't work right. ;; '(font-lock-comment-face ((t (:foreground "color-71")))) ;; '(font-lock-constant-face ((t (:foreground "color-105")))) ;; '(font-lock-function-name-face ((t (:foreground "color-43")))) ;; '(font-lock-keyword-face ((t (:foreground "color-33")))) ;; '(font-lock-preprocessor-face ((t (:foreground "color-141")))) ;; '(font-lock-string-face ((t (:foreground "color-172")))) ;; '(font-lock-type-face ((t (:foreground "color-45")))) ;; '(font-lock-variable-name-face ((t (:foreground "color-179"))))) ;; package (require 'package) (add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/") t) (package-initialize) ;; yasnippet (eval-after-load "yasnippet" '(progn (define-key yas-keymap (kbd "<tab>") nil) (yas-global-mode 1))) ;; company (when (locate-library "company") (global-company-mode 1) (global-set-key (kbd "C-M-i") 'company-complete) (define-key company-active-map (kbd "C-s") 'company-select-next) (define-key company-active-map (kbd "C-w") 'company-select-previous) (define-key company-search-map (kbd "C-s") 'company-select-next) (define-key company-search-map (kbd "C-w") 'company-select-previous) (define-key company-active-map (kbd "<tab>") 'company-complete-selection)) ;; irony (eval-after-load "irony" '(progn (add-to-list 'company-backends 'company-irony) (add-hook 'irony-mode-hook 'irony-cdb-autosetup-compile-options) (add-hook 'c-mode-common-hook 'irony-mode)))色設定は完全に好みです.
コピペで使用できますが,色設定に関しては上述したようにコマンドでの設定をお勧めするので,
消してから使用してください. (一応コメントアウトはしてあります)自分の中でのベース環境なので,ここからもっと拡張してもいいですし,これだけでもいいです.
あとは個人個人好きなように環境を作っていってください.
- 投稿日:2020-05-24T16:18:31+09:00
OpenFOAMのポスト処理チートシート(随時更新)
はじめに
この記事ではOpenFOAMのポスト処理ユーティリティpostProcessの使い方をまとめています.メッシュや物理量について,様々な情報を出力することができるユーティリティです.シミュレーション中も後も使用可能ですが,この記事ではシミュレーション後についてまとめます.
OpenFOAMの作業ディレクトリをcaseDirとします.
各時間の物理量の場を取得する方法
Q(速度勾配テンソルの第2不変量)
実行には速度の計算結果が必要です.
caseDir$ postProcess -func Q時間ディレクトリ内にQという名前でファイル出力されます.
y+
wallパッチ上のy+分布を出力します.postProcessユーティリティをオプションにする形で実行します.pimpleFoamで計算したケースなら
caseDir$ pimpleFoam -postProcess -func yPlus時間ディレクトリ内にyPlusという名前でファイル出力されます.
特定の点の時系列データを取得する方法
probe
実行には取得したい物理量の計算結果と,systems内にprobesというディクショナリファイルを用意する必要があります.中身は
probes/*--------------------------------*- C++ -*----------------------------------*\ ========= | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox \\ / O peration | Website: https://openfoam.org \\ / A nd | Version: 7 \\/ M anipulation | ------------------------------------------------------------------------------- Description Writes out values of fields from cells nearest to specified locations. \*---------------------------------------------------------------------------*/ #includeEtc "caseDicts/postProcessing/probes/probes.cfg" fields (U); //括弧内に取得したい物理量を入力.複数の場合は半角スペースで区切る (U p ...) probeLocations ( (0.12 0.01 0.05) // 取得したい点の座標(x, y, z) (0.2 0 -0.5) ); // ************************************************************************* //実行
caseDir$ postProcess -func probescaseDirにpostProcessingというディレクトリが生成され,中に時系列データのファイルが出力されます.
- 投稿日:2020-05-24T15:52:03+09:00
Seccon beginners ctf 2020 write up (mask)
今年も懲りずに挑戦しました
今年もSeccon Beginners ctf 2020に挑戦しました。まあ、だいたい解けなかったわけですが、1問だけ自力で解けたのでWriteUPを書きます。
mask
難易度は"beginner"。問題のzipファイルを解答すると、maskというファイルが出てくる。これを解析することになる。
まず、これは何?ということで、
file
コマンドを使って調べてみると$ file mask mask: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=49166a467aee16fbfe167daf372d3263837b4887, for GNU/Linux 3.2.0, not strippedということで、Linuxの実行ファイルであることがわかる。
では、とりあえず実行してみると
$ ./mask Usage: ./mask [FLAG]となっていて、いきなりFLAGを入れれば良いようだ。
では、ということで、昨年まとめておいたコマンドを使って、調査してみた。
まずは、strings
を使用してみる。(結果的には、今回これだけでなんとかなった)$ strings mask /lib64/ld-linux-x86-64.so.2 7-2c libc.so.6 strcpy puts __stack_chk_fail strlen __cxa_finalize ーー略ーーこんな感じででてくる。この中にctf4g{hogehoge}っていう文字列があればよかったのだが、流石にそこまで簡単ではない。
少し考えてみた
FLAGはctf4g{hogehote}である。なので、適当に入れてみよう。
$ ./mask ctf4b{hogehoge} Putting on masks... atd4`q`eee`eeeu c`b bkhkcahkcai Wrong FLAG. Try again.こんな感じで間違っていると怒られる。これを何度実行しても、同じ文言が出てくる。
そこで、$ ./mask abcdefghijklmnopqrstuvwxyz Putting on masks... a`adede`a`adedepqpqtutupqp abc`abchijkhijk`abc`abchij Wrong FLAG. Try again.とやってみると、"c","t","f"の位置に、上下で”ac”,"t`","db"となっていることがわかる。
同様に、英語大文字と"{}"を入力してみる。$ ./mask ABCDEFGHIJKLMNOPQRSTUVWXYZ Putting on masks... A@ADEDE@A@ADEDEPQPQTUTUPQP ABC@ABCHIJKHIJK@ABC@ABCHIJ Wrong FLAG. Try again. $ ./mask {} Putting on masks... qu ki Wrong FLAG. Try again.となることがわかった。
ここで、もう一度
strings
の出力の中に、"ctf4b{"がエンコードされた文字列が無いか調べてみた。$ strings ./mask | grep atd atd4`qdedtUpetepqeUdaaeUeaqauあるじゃん。
実はここまではすぐ見つけたのだが、次に気がつくまでに時間がかかった。
迷い込んだのは、2行あるうちの上の行しか考慮しなかったことによる。例えばエンコードされた文字が"a"の場合、もとの文字は"a","c","i","k"の4文字ある。
なので、これらの組み合わせを考えるのだろうと思い、クロスワードパズルで単語が出てくるサイトなどを調べていたのだが、ことごとく外れた。そこで、もう一度考え直して、下の段にも意味があるのであろうということで、"ctf4b"に対応する下の段もあるのだろうと思って、"c"の文字が入っているものを出力してみた。
$ strings ./mask | grep c 7-2c libc.so.6 strcpy __stack_chk_fail __cxa_finalize strcmp __libc_start_main c`b bk`kj`KbababcaKbacaKiacki Correct! Submit your FLAG. GCC: (Arch Linux 9.3.0-1) 9.3.0 init.c crtstuff.c deregister_tm_clones completed.7393 mask.c __libc_csu_fini strcpy@@GLIBC_2.2.5 __stack_chk_fail@@GLIBC_2.4 __libc_start_main@@GLIBC_2.2.5 strcmp@@GLIBC_2.2.5 __libc_csu_init __cxa_finalize@@GLIBC_2.2.5 .dynamic .commentこの中に、
c`b bk`kj`KbababcaKbacaKiacki
という文字列がある。
ctf4b
をmaskに与えたときに出てくる、c`b b
が入っている。
文字数も、上の段と一緒なので、これがエンコードされた下の段であることがわかる。atd4`qdedtUpetepqeUdaaeUeaqau c`b bk`kj`KbababcaKbacaKiackiここまでくれば、あとは、上の段と下の段の組み合わせの文字が何なのかをアルファベットから見つければよい。
例えば、小文字のアルファベットだけ並べてみると、
abcdefghijklmnopqrstuvwxyz a`adede`a`adedepqpqtutupqp abc`abchijkhijk`abc`abchijまた、上の段と下の段が"ui"なら、"}"である。
1文字ずつ調べていくと、atd4`qdedtUpetepqeUdaaeUeaqau c`b bk`kj`KbababcaKbacaKiacki ctf?b{dont?reverse?face?mask}となる。("?"はまだ見つかっていない文字)
途中、"UK"という組み合わせが出てくるが、この前後で単語がきちんと意味をなしていることから、
これは何かの記号だろうと思ったので、とりあえず"_"をmaskでエンコードしてみたら、$ ./mask _ Putting on masks... U K Wrong FLAG. Try again.勘が冴えていた。
また、数字もmaskでエンコードしてあげると、$ ./mask 1234567890 Putting on masks... 1014545010 !"# !"#() Wrong FLAG. Try again.全部の組み合わせがみつかった。
flagは
ctf4b{dont_reverse_face_mask}
$ ./mask ctf4b{dont_reverse_face_mask} Putting on masks... atd4`qdedtUpetepqeUdaaeUeaqau c`b bk`kj`KbababcaKbacaKiacki Correct! Submit your FLAG.以上
- 投稿日:2020-05-24T14:52:40+09:00
Linux基礎7 -bashの設定-
はじめに
今回はbashの様々な設定方法を紹介していきます。
WindowsやMacOSを使用する際に自分の好みに合わせて配色や配置を変更するように、Linuxでも同様にユーザ環境の設定が可能です。それだけでなくLinuxではカーネル動作などのシステム基幹部分までも設定を変更できるようになっています。Linuxの設定変更の全てを紹介することはできませんので、今回は一番身近である、bashのカスタマイズに絞ってお話しします。おまけ
タイプミスがきっかけでこの強調方法
を知りました…見やすいので使っていこうと思います。エイリアス
エイリアスとは、
既存のコマンドに別名をつけて実行できるようにする機能
のことです。エイリアスの設定
例えば
ls -F
コマンドはファイルの種別を表示するコマンドで、大変頻繁に利用すると思います。
毎回ls -F
とコマンドを入力するのは面倒なので、ls -F
にls
という名前をつけてしまえば、ls
と打つだけでls -F
が実行されファイルの種別を表示してくれます。エイリアス(alias)の設定$ alias <名前>='コマンド' $ alias ls='ls -F' #lsコマンドの設定例エイリアス設定後の
ls
の実行例
エイリアスを設定したことで、ls
と入力するとls -F
が実行されました。ここではよく使われるエイリアス設定を紹介しておきます。
よく使われるエイリアス設定$ alias la='ls -a' #全てのファイルを表示する $ alias ll='ls -l' #ファイルの詳細情報を表示する $ alias rm='rm -i' #削除前に確認する $ alias cp='cp -i' #上書き前に確認する $ alias mv='mv -i' #上書き前に確認するエイリアスの確認と削除
あるコマンドが本当にコマンドなのかエイリアスなのかを確認するためには
typeコマンド
を使用します。エイリアスかコマンドかの確認$ type ls一度設定したエイリアスを削除するには
unaliasコマンド
を使用します。エイリアスの削除$ unalias lsエイリアスを一時的に無効にする
エイリアスを設定したものの、一時的に何もオプションをつけずにコマンドを実行したいタイミングがあります。
その場合はいくつかの方法がありますので、紹介します。一時的にエイリアスを無効にする$ /bin/ls #フルパスで指定する $ command ls #commandを使う $ \ls #\を先頭につける\を使用する方法が、タイプ数が少なくて済むため、よく利用されています。
bashのオプション
bashではシェル自身が持つ機能を利用するためにオプションを指定することができます。オプションは書く機能ごとにオン・オフいずれかの値をとり、これによりbashの各機能の有効・無効を設定できます。
オプションの設定
オプションを設定するには、
setコマンド
かshoptコマンド
を使用します。
なおbashのオプションには、シェルスクリプトという方法でシェルを利用するときにしか意味を持たないものもあります。これについては後の記事で触れます。setコマンド
setコマンド
では-o/+o
でオプションのオン/オフを切り替えます。+oがオフ
なので注意しましょう。setコマンド$ set -o/+o <オプション名> $ set -o ignoreeof #ignoreeofのオプションをオンにした例
setコマンド
で設定できるオプションはたくさんありますので、man bash
で読んでみましょう。
ここでは、よく使用されるオプションをいくつか紹介します。
オプション 内容 ignoreeof Ctrl+Dを押してもシェルを終了しない noclobber 既に存在するファイルをリダイレクトで上書きしない noglob パス名展開を無効にする。はシェルに解釈されず。そのままとなる shoptコマンド
shoptコマンド
はsetコマンド
と同様に、オプションをオン・オフするコマンドです。
-s
がオン、-u
がオフに設定します。shoptコマンド$ shopt -s/-u <オプション名> $ shopt -s cdspell #cdspellのオプションをオンにした例なお
shopt
とset
では指定できるオプション名が異なります。set
と合わせてman bash
で確認してみてください。ここではよく利用されている
shoptコマンド
をいくつか紹介します。
オプション名 内容 autocd ディレクトリ名のコマンドを実行すると、それがcdコマンドの引数に指定されたものとして実行される dotglob *や?を使ったパス名展開の結果に、「.」で始まるファイルも含める cdspell cdコマンドを実行時、ディレクトリのちょっとしたタイプミスが自動修正される globstar パス名展開で**というパターンを使用すると、サブディレクトリまで含めた全てのファイルにマッチする histappend bashを終了するとき、履歴ファイルにコマンド履歴を追記し、上書きしない シェル変数
シェル変数とは、bash内部で使用される変数のことです。これは数値や文字列を保存して、様々な値を設定することができます。
bashには特別な意味をもつシェル変数がたくさん用意されていて、これを設定することで、シェルの機能のカスタマイズを行うことができます。変数の設定
変数の設定$ <変数名>=<値> $ var1='test variable' #var1に値を設定 $ echo $var1 #設定した変数名は$<変数名>で参照 test variableこのとき、値にスペースを含む場合は、
'または"
で囲む必要があります。
設定した変数名は$<変数名>
で参照する必要があることに注意してください。PS1 プロンプト設定
ここではシェルのプロンプトを設定してみましょう。
プロンプトの設定$ PS1='bash> ' bash>このようにPS1変数は設定した文字列がそのままプロンプトとして表示されます。
またこの際特別な意味を持つ記号があり、それらを利用するといろいろな情報をプロンプトに含めることができます。
記号 内容 \d 「曜日 月 日」という形式の日付 \h ホスト名のうち、最初の.までの部分 \H ホスト名 \n 改行 \t HH:MM:SS形式の現在時刻 \u ユーザ名 \w カレントディレクトリ \W カレントディレクトリの末尾のディレクトリ名 \$ rootユーザの場合は#、それ以外のユーザの場合は$ \ \そのもの PATH コマンド検索パス
シェル変数PATHには、コマンドを実行する際にシェルがコマンドを探すディレクトリを指定します。
この変数にはシェルがコマンドを探すディレクトリを「:」区切りで連結した文字列
が設定されています。
PATHには通常、ディストリビューションごとに適切な値が設定されていますが、自分で作成した独自のディレクトリや個別でインストールしたアプリケーションをコマンド検索パスに含めたい場合は、PATH変数の値を変更することで、コマンドをフルパスで実行する必要がなくなります
。Linuxでは個別のコマンドを配置するディレクトリとして
~/bin
というディレクトリがよく使用されます。ここにパスを設定する場合には次のように設定します。個別パスの追加$ PATH="$PATH:~/bin"LANG ロケール
ロケール(Locale)は言語や国、地域などを特定するための識別子です。表示する言語や日付の書式などは、このロケールを基準にして切り替えられます。
現在使用しているロケールはLANG
というシェル変数で指定されています。ロケールの設定$ echo $LANG #現在設定されているロケールの表示 $ LANG=en_US.UTF-8 #ロケールの設定使用できるロケールは
locale -a
コマンドで表示できますので適切な設定を探してみてください。
その他のシェル変数
ここまで紹介した意外にも特別な意味を持つシェル変数はあります。ここではよく使われるシェル変数を紹介します。
シェル変数名 内容 HISTFILE コマンドラインに履歴を保存するファイル名。デフォルト値は~/.bash_history HISTFILESIZE 履歴ファイルに保存するコマンドライン履歴の最大行数 HISTSIZE コマンドライン履歴を保持する最大行数 HOME ホームディレクトリ SHELL ログインシェルのパス PWD カレントディレクトリ bashのシェル変数については、bashマニュアルの「Shell Variable」を参照しください。
環境変数
コマンドには、実行ファイルとしてファイルシステム上に存在するコマンドを
外部コマンド
、一方、シェル自体に内蔵しているコマンドを組み込みコマンド
と呼びます。コマンドが
外部コマンド
か組み込みコマンド
かを判断するにはtypeコマンド
で確認できます。
ここで注意点は、
現在設定されているシェル変数の値を外部コマンドは参照することができない
ということです。しかし、LANGによるロケールの設定のように、シェル上だけでなく外部コマンドでも常に設定を反映しておきたい変数も多数存在します。このような用途のために
外部コマンドからも値を参照できるようにした変数を環境変数
と言います。変数LANGは、特に指定しなくても自動的に環境変数として設定されているため、catのような外部コマンドもLANGの指定によって出力メッセージが変化します。catコマンドをはじめとして、多くのコマンドは、
環境変数の値を確認してその設定に従って動作します
。printenvコマンド 環境変数の表示
現在シェルに設定されている環境変数を確認するには、
printenvコマンド
を実行します。
下記のようにLANGなどの他にも多くのシェル変数が環境変数として設定されていることがわかります。
exportコマンド 環境変数の設定
環境変数を自分で設定するには、
exportコマンド
を使用します。指定したシェル変数を環境変数に設定$ export <シェルの変数名> $ LESS='--no-init' #lessコマンドの環境変数LESSにlessコマンド終了時に画面を消去しない--no-initオプションを指定 $ export LESS #指定したシェル変数を環境変数に設定 $ export LESS='--no-init' #上記と同じ内容bashの設定ファイル
ここまで、シェルの設定を行ってきましたが、これはbashを終了するとすべて消えてしまいます。
次回以降ログインしたときにも同じ設定をするには、bashの設定ファイルを記述する必要があります
。/etc/profile, ~/.bash_profile, ~/.bashrc
bashの起動時に読み込まれるファイルは数多くあります。ここではbashの設定ファイルについて簡単にお話しします。
bashをログインシェルとして起動したときは、ます/etc/profile
が読み込まれて、続いて~/.bash_profile
が読み込まれます。bashはこれらのファイルに書かれた内容を実行します。
/etc/profile
はシステム全体で使用する設定ファイルです。全てのユーザに共通の設定を行いたい場合はこのファイルに記述しますが、通常はディストリビューションごとに適切な設定が予め書かれているので、今回の範囲では修正する必要はありません。
~/.bash_profile
は各ユーザ個別の設定ファイルです。多くのディストリビューションでは、~/.bash_profile
から~/.bashrc
を読み込むために以下のような内容が記述されていると思います。これは~/.bashrc
ファイルが存在すれば、その内容を現在のシェルに反映するという設定です。詳しくは後の記事で紹介します。if [ -f ~/.bashrc ]; then source ~/.bashrc fi設定はどのファイルに書くべきか
~/.bash_profile
はログインしたときだけ読み込まれて、~/.bashrc
はbashは起動するたびに読み込まれるという違いがあります。
ログイン時に一度だけ設定すればいい項目は~/.bash_profile
に書き、それ以外のbashを起動する度に毎回設定する必要がある項目は~/.bashrc
に記述します。基本的には~/.bashrc
に書いておけば問題ありません。設定ファイル変更時の注意
~/.bashrc
ファイルはバックアップを取っておきましょう。誤った記述設定によって簡単に復旧できないような状態になってしまう可能性もあります。
対応としては、設定を行うシェルとは別にもう一つシェルを起動しておくことをお勧めします。参考書籍
- 投稿日:2020-05-24T13:54:42+09:00
ubuntu コードネーム
ubuntu version
ubuntuのリリースは半年に一回のペースで行われている。
このうち、4回のリリースのうちの1回は、長期サポート版として、リリースされている。
これが「LTS」である。
ubuntu コードネーム
ubuntuのversionには、それぞれ通称がある。
ubuntu 16.04 LTS (Xenial Xerus)
Xenial Xerus (ズィーニィア ジリス)
おもてなしの、友好的な
アラゲジリス (Xerus rutilus)
ジリスは、リスの仲間
ubutnu 18.04 LTS (Bionic Beaver)
Bionic Beaver (バイオニック ビーバー)
生物工学の、サイボーグの
ビーバー
ubuntu 20.04 LTS (Focal Fossa)
Focal Fossa (フォーカル フォッサ)
焦点の
フォッサ (マダガスカルに住んでいる)
os-release
/etc/os-release
でubuntuに関する情報が得られる。
$ cat /etc/os-release NAME="Ubuntu" VERSION="20.04 LTS (Focal Fossa)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 20.04 LTS" VERSION_ID="20.04" HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" VERSION_CODENAME=focal UBUNTU_CODENAME=focal
- 投稿日:2020-05-24T11:41:53+09:00
Jetson nanoをセットアップする
Jetsonをセットアップする
サークルでJetsonを買ったのでセットアップしていきますー!
この記事はOSが起動するまでを書いていきます
必要なもの
電源(5v2A以上のUSB充電器か、5.5mmのDC電源)
SDカード
Wifiドングル
マウス、キーボード
ディスプレイ開封
内容物はJetson本体、説明書、紙の台だけ
この微妙に余った段ボールのスペースには何も入ってません(筆者が振って確かめました)
この余分なもの削ればいいのに…電源について
最低でも5v2A以上の電源を買ってください
一応5v2AでのUSB給電での動作は確認しました
ただし、これはjetsonの性能をセーブした状態での動作になるため5v4Aぐらいの電源の方がいいですDCジャック電源を利用する時
DCジャック電源を利用する時はジャンパピンをショートさせます
画像の場所にジャンパピンがついてると思うので隣のピンとショートさせます
なんか昔はジャンパピンが付属してなくて大変だったそうです追記
JetsonNanoにはUSBデバイスモードというのがあり、USBでSSHできるらしのですが、それを利用するときはDCジャックからの給電が必須となります。OS
Jetson nanoのOSは下記からダウンロードできます
https://developer.nvidia.com/embedded/downloads
このOSはUbuntu18.04ベースです
ファイルは6GBぐらいで解凍するとimgファイルが入っていると思うので、それをSDカードに書き込んでください
ここら辺はJetsonを買おうと思っている方にとっては経験あると思うので割愛しますwifiにつて
Jetson nanoにはwifiモジュールはついてないので自分で買う必要があります
選択肢は2つあって
・ jetson推奨のWifiモジュールを買う(ノートのWifiモジュールみたいなやつ)
・ USBドングルのWi-Fiモジュールを買う前者につては少々お高いです、なので今回はUSBWi-Fiモジュールを使いました。
https://www.amazon.co.jp/gp/product/B008IFXQFU/ref=ppx_yo_dt_b_asin_image_o01_s00?ie=UTF8&psc=1TL-WN725Nという格安モジュールです(安さは正義!)
調べてみればわかるのですが、USBWi-Fiモジュールは相性問題があり、運が悪いとドライバのインストールとかめんどくさいことになるので注意
起動
問題がなければ以下の画面が表示されます
されなかった人は....ファイト!
この記事では紹介しませんがJetcardというパッケージをインストールするとsampleでGPGPUできます
楽しーーー!(変人の自覚はある)
最後に
Jetson nanoは1万でCUDAとLinuxが遊べるようになるのでおススメ
時代はGPUよ!
CUDAで上げろ、自分の市場価値!
- 投稿日:2020-05-24T09:27:01+09:00
Real Time Clock (RTC) Drivers for Linux
https://www.kernel.org/doc/html/latest/admin-guide/rtc.html
Real Time Clock (RTC) Drivers for Linux
When Linux developers talk about a “Real Time Clock”, they usually mean something that tracks wall clock time and is battery backed so that it works even with system power off. Such clocks will normally not track the local time zone or daylight savings time – unless they dual boot with MS-Windows – but will instead be set to Coordinated Universal Time (UTC, formerly “Greenwich Mean Time”).
Linux 開発者が"Real Time Clock"と言った場合、通常それはwall clock timeを追従し、system power offであっても動き続けるようにバッテリーでバックアップされているものを示します。このようなclockは通常、ローカルタイムゾーンや夏時間を追従しません(MS-Windowsとのdual bootをしていなければ)。その代わりに、協定世界時(UTC, 従来の「グリニッジ標準時)に設定されます。
The newest non-PC hardware tends to just count seconds, like the time(2) system call reports, but RTCs also very commonly represent time using the Gregorian calendar and 24 hour time, as reported by gmtime(3).
最新のnon-PCハードウェアでは、time(2) system callでの報告のように、秒単位でカウントする傾向があります。ただし、RTCはまた、gmtime(3)での報告のように、グレゴリオ暦と24時間の時間を使用して時間を表現することも非常に一般的です。
Linux has two largely-compatible userspace RTC API families you may need to know about:
Linuxには、知っておくべき、2つの大きな互換性のあるユーザー空間RTC API familiyがあります。
・/dev/rtc … is the RTC provided by PC compatible systems, so it’s not very portable to non-x86 systems.
・/dev/rtc0, /dev/rtc1 … are part of a framework that’s supported by a wide variety of RTC chips on all systems.・/dev/rtc … PC互換システムで提供されるRTC。非-x86 systemでは移植性がない。
・/dev/rtc0, /dev/rtc1 … 全てのシステムにおいて幅広いRTC chipでサポートされているフレームワークの一部。Programmers need to understand that the PC/AT functionality is not always available, and some systems can do much more. That is, the RTCs use the same API to make requests in both RTC frameworks (using different filenames of course), but the hardware may not offer the same functionality. For example, not every RTC is hooked up to an IRQ, so they can’t all issue alarms; and where standard PC RTCs can only issue an alarm up to 24 hours in the future, other hardware may be able to schedule one any time in the upcoming century.
プログラマは、PC/AT機能性が常に有効ではないことを理解しなければなりません。そして、システムによってはそれ以上のことができることもあります。これはつまり、RTCは2つのRTC frameworkからの要求に対して同じAPIを使います。しかし、ハードウェアは同じ機能性を提供しません。例えば、全てのRTCはIRQでフックできるわけではなく、alarm問題を取り扱えません。また、標準的なPCのRTCは将来の24時間に対するalarmをセットできますが、他のハードウェアでは更に将来にわたってのスケジュールを組むこともできます。
Old PC/AT-Compatible driver: /dev/rtc
All PCs (even Alpha machines) have a Real Time Clock built into them. Usually they are built into the chipset of the computer, but some may actually have a Motorola MC146818 (or clone) on the board. This is the clock that keeps the date and time while your computer is turned off.
全てのPC(Alpha machineも含む)には、Real Time Clockが組み込まれています。通常これらは、コンピュータのチップセットに組み込まれているか、ボード上にMotorola MC146818(あるいはそのクローン)を有しているでしょう。これは、コンピュータの電源が切れている間の、日付と時刻を保持するclockです。
ACPI has standardized that MC146818 functionality, and extended it in a few ways (enabling longer alarm periods, and wake-from-hibernate). That functionality is NOT exposed in the old driver.
ACPIは、MC146818の機能性の標準化し、いくつかの拡張もしました(alarm periodの延長や、wake-from-hibernate)。この機能性は、過去のドライバで提供されていないものです。
However it can also be used to generate signals from a slow 2Hz to a relatively fast 8192Hz, in increments of powers of two. These signals are reported by interrupt number 8. (Oh! So that is what IRQ 8 is for…) It can also function as a 24hr alarm, raising IRQ 8 when the alarm goes off. The alarm can also be programmed to only check any subset of the three programmable values, meaning that it could be set to ring on the 30th second of the 30th minute of every hour, for example. The clock can also be set to generate an interrupt upon every clock update, thus generating a 1Hz signal.
ただし、これはsignalの生成に使うことができます、最も遅いケースでは2Hz、比較的早い8192Hzまで、2の階乗の増分で。このシグナルは、割り込み番号8で通知されます(Oh! それはIRQ 8の役割です)。これは、alarmが到達するとIRQ 8信号が下がることで、24時間alarmとして機能をさせることができます。Alarmはまた、3つの変更可能な変数のサブセットだけをチェックすることができます、すなわち、例えば、毎時の30分の30秒に鳴るようにセットすることもできます。clockはまた、クロックが更新されるたびに割り込みを生成することもでき、この場合1Hzの信号が生成されます。
The interrupts are reported via /dev/rtc (major 10, minor 135, read only character device) in the form of an unsigned long. The low byte contains the type of interrupt (update-done, alarm-rang, or periodic) that was raised, and the remaining bytes contain the number of interrupts since the last read. Status information is reported through the pseudo-file /proc/driver/rtc if the /proc filesystem was enabled. The driver has built in locking so that only one process is allowed to have the /dev/rtc interface open at a time.
割り込みは、/dev/rtc(major 10, minor 135, read only character device)を介して、unsigned longの形で通知されます。low byteには、割り込みの種別(update-done, alarm-rang, or periodic)が発生したことが含まれ、残りのバイトには最後に読み込んでからの割り込み回数が含まれます。/proc filesystemが有効な場合、Status informationが、疑似ファイル /proc/driver/rtcを介して通知されます。ドライバーにはロックが組み込まれているため、一度の1つのプロセスだけが/dev/rtcを開くことが出います。
A user process can monitor these interrupts by doing a read(2) or a select(2) on /dev/rtc – either will block/stop the user process until the next interrupt is received. This is useful for things like reasonably high frequency data acquisition where one doesn’t want to burn up 100% CPU by polling gettimeofday etc. etc.
ユーザープロセスは、これらの割り込みを、/dev/rtcに対するread(2)やselect(2)で監視することができます。これらは次の割り込みがくるまでuser processはblock/stopするでしょう。これは、gettimeofdayなど等をポーリングしてCPU使用率100%で回り続けたくない場合に、高い頻度でデータ取得するのに有益です。
At high frequencies, or under high loads, the user process should check the number of interrupts received since the last read to determine if there has been any interrupt “pileup” so to speak. Just for reference, a typical 486-33 running a tight read loop on /dev/rtc will start to suffer occasional interrupt pileup (i.e. > 1 IRQ event since last read) for frequencies above 1024Hz. So you really should check the high bytes of the value you read, especially at frequencies above that of the normal timer interrupt, which is 100Hz.
高頻度、あるいは高い付加において、ユーザープロセスは、最後の読み取り以後に受信した割り込みの数を確認し、いわゆる割り込みの「山」があったかどうかを判断しなければなりません。参考までに、/dev/rtcを厳しく読んでいる、一般的な486-33(訳注:i486 の 33MHz と思われる)では、1024Hzを超える周波数で、割り込みのpileupが時々発生しています(つまり、 1 IRQ イベントが最後の読み込みよりも大きい)。したがって、特に通常のタイマー割り込みが100Hzを越える周波数であれば、読み取った値の上位バイトをチェックする必要があります。
Programming and/or enabling interrupt frequencies greater than 64Hz is only allowed by root. This is perhaps a bit conservative, but we don’t want an evil user generating lots of IRQs on a slow 386sx-16, where it might have a negative impact on performance. This 64Hz limit can be changed by writing a different value to /proc/sys/dev/rtc/max-user-freq. Note that the interrupt handler is only a few lines of code to minimize any possibility of this effect.
64Hzを越える、プログラミング and/or 割り込みの有効化は、rootだけが許可されます。これは、少し保守的かもしれません。しかし、遅い386sx-16(訳注:i386sx-16MHz)で、悪意のあるユーザーが大量のIRQを発生させることは、パフォーマンスに悪影響があります。この64Hzの制限は、/proc/sys/dev/rtc/max-user-freqを書き換える事で変更できます。割り込みハンドラは、この影響を最小に抑えるための数行のコードでしかないことを注意してください。
Also, if the kernel time is synchronized with an external source, the kernel will write the time back to the CMOS clock every 11 minutes. In the process of doing this, the kernel briefly turns off RTC periodic interrupts, so be aware of this if you are doing serious work. If you don’t synchronize the kernel time with an external source (via ntp or whatever) then the kernel will keep its hands off the RTC, allowing you exclusive access to the device for your applications.
更に、kernel timeが外部ソースによって同期を取られる場合、kernelはCOMS clockを毎11分毎に書き戻します。この処理の過程で、カーネルはRTCのperiodic interruptsを無効化します。そのため、seriousな作業をしている場合は気を付けてください。もし、外部ソースでのkernel timeの補正を必要としない場合(例えば、ntpやその他)には、kernelは、RTCから手を離し、アプリケーションへの排他的アクセスを許可します。
The alarm and/or interrupt frequency are programmed into the RTC via various ioctl(2) calls as listed in ./include/linux/rtc.h . Rather than write 50 pages describing the ioctl() and so on, it is perhaps more useful to include a small test program that demonstrates how to use them, and demonstrates the features of the driver. This is probably a lot more useful to people interested in writing applications that will be using this driver. See the code at the end of this document.
RTCに対するalarmや割り込み頻度は、include/linux/rtc.hにリスト化された様々なioctl(2)呼び込みによってプログラムできます。50ページにわたってioctl()の説明を記述するよりも、おそらく、デモを通して使い方を説明する小さいテストプログラムと、ドライバの機能をデモンストレーションするほうが有益でしょう。これはおそらく、このドライバーを利用するアプリケーションを書く多くの人々にとって有益です。このドキュメントの最後にあるコードを参照ください。
(The original /dev/rtc driver was written by Paul Gortmaker.)
New portable “RTC Class” drivers: /dev/rtcN
Because Linux supports many non-ACPI and non-PC platforms, some of which have more than one RTC style clock, it needed a more portable solution than expecting a single battery-backed MC146818 clone on every system. Accordingly, a new “RTC Class” framework has been defined. It offers three different userspace interfaces:
Linuxが多くのnon-ACPI で、non-PC platformをサポートしており、そしてそれらは1つ以上のRTC style clockを有したため、全てのシステムで、1つのバッテリーバックアップするMC146818 cloneを期待するよりも、移植性のある解決策が必要となりました。そのため、新しい "RTC Class" frameworkが定義されました。これは、3つの異なるユーザーインタフェイスを提供します。
・/dev/rtcN … much the same as the older /dev/rtc interface
・/sys/class/rtc/rtcN … sysfs attributes support readonly access to some RTC attributes.
・/proc/driver/rtc … the system clock RTC may expose itself using a procfs interface. If there is no RTC for the system clock, rtc0 is used by default. More information is (currently) shown here than through sysfs.・/dev/rtcN … /dev/rtc interfaceと類似しているもの
・/sys/class/rtc/rtcN … RTC attributeへのreadonly accessを許可する、sysfs attributes
・/proc/driver/rtc … system clock RTCは、自身をprocfs interfaceを介して、公開する必要がある場合があります。もし、system clockのRTCがない場合には、rtc0がデフォルトで使われます。(現在の)sysfsを介した情報よりも多くの情報がここに提示されます。The RTC Class framework supports a wide variety of RTCs, ranging from those integrated into embeddable system-on-chip (SOC) processors to discrete chips using I2C, SPI, or some other bus to communicate with the host CPU. There’s even support for PC-style RTCs … including the features exposed on newer PCs through ACPI.
RTC Class frameworkは、非常に幅広いRTCをサポートしています。組込み用のsystem-on-chip(SOC) processorに統合されたものから、I2CやSPIあるいはほかのホストCPUと通信可能なバスに接続されたものまで。ACPIを介して新しいPCで公開されている機能を含めた、PC-style RTCのサポートさえあります。
The new framework also removes the “one RTC per system” restriction. For example, maybe the low-power battery-backed RTC is a discrete I2C chip, but a high functionality RTC is integrated into the SOC. That system might read the system clock from the discrete RTC, but use the integrated one for all other tasks, because of its greater functionality.
新しいframeworkではまた、"one RTC per system"の制約を無くします。例えば、省電力バックアップバッテリーのRTCは、分離されたI2C chipであり、高機能なRTCはSOCにはに統合されています。このシステムでは、system clockは分離されたRTCから読み出しますが、高度な機能性から、そのほかの処理については、統合されたRTCを使うでしょう。
Check out tools/testing/selftests/rtc/rtctest.c for an example usage of the ioctl interface.
ioctl interfaceの使い方の例については、tools/testing/selftests/rtc/rtctest.cを確認してください。
もともと、Linux Kernelのソースコードの一部なので、GPLv2扱いになる(はずの認識)。
https://www.kernel.org/doc/html/latest/index.html
Licensing documentation
The following describes the license of the Linux kernel source code (GPLv2), how to properly mark the license of individual files in the source tree, as well as links to the full license text.
https://www.kernel.org/doc/html/latest/process/license-rules.html#kernel-licensing