20200524のLinuxに関する記事は13件です。

中華レトロゲーム機を解析してみる


title: 中華レトロゲーム機を解析してみる
tags: MIPS Linux
author: yochy4671

slide: 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.bat
dd 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=4096

uBoot.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.bin
U-Boot 1.1.6 (Jul 26 2018 - 14:28:08)

1.1.6となると2006年頃のものだろうか,ずいぶん古いものを使っている.ま,ブートローダーなので古くても構わないか.

現行のリリースバージョンはv2019.07のようだが,jz4760向けの実装は見当たらずにjz4780の実装しかなさそうである.

RetroGame_v1.0_S_B.uBoot.bin
Board: 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.bin
Linux-2.6.31.3

2009年頃のものだろうか,これまた古いものを使っている.

現行の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 menuconfig

buildrootのコンフィグレーションは,以下のようにするよう記載されている.

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
$ make

makeの最中に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-gccsdl-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

したあとに,

Makefile
ifeq ($(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が使えるようになった,とある.えっと...PyMenuSimpleMenuのことかな.

あとでもう少し見ておこうかな.

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

中華レトロゲーム機を解析してみる

概要

一部の方面で話題になっている,中華レトロゲーム機を購入したので解析をしてみた.物忘れの激しいワタシ向けのメモのようなものである.

解析のゴールを何にするのかが悩ましいところであるが,中華レトロゲーム機向けのアプリケーションの開発環境の構築方法を示せればいいかなと思う.

そうすれば大勢の発想豊かなどなたかが,なにかいいものを作ってくれるのではないかと.

イメージファイル

入手

電源を入れると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.bat
dd 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=4096

uBoot.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.bin
U-Boot 1.1.6 (Jul 26 2018 - 14:28:08)

1.1.6となると2006年頃のものだろうか,ずいぶん古いものを使っている.ま,ブートローダーなので古くても構わないか.

現行のリリースバージョンはv2019.07のようだが,jz4760向けの実装は見当たらずにjz4780の実装しかなさそうである.

RetroGame_v1.0_S_B.uBoot.bin
Board: 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.bin
Linux-2.6.31.3

2009年頃のものだろうか,これまた古いものを使っている.

現行の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 menuconfig

buildrootのコンフィグレーションは,以下のようにするよう記載されている.

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
$ make

makeの最中に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-gccsdl-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

したあとに,

Makefile
ifeq ($(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が使えるようになった,とある.えっと...PyMenuSimpleMenuのことかな.

あとでもう少し見ておこうかな.

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Linux シグナルの基本と仕組み

この記事は、Linux シグナルの基本と仕組み (カーネル実装) について調査したことのまとめです。

シグナルは普段から利用しているものの仕組みについては理解してなかったので、カーネル勉強の題材として調べてみました。想像以上に複雑でボリュームがあったため、書き切れていない部分 (調査しきれなかった部分) もありますが、一通りの流れ (仕組み) は理解できたと思います。

なお、この記事は「■ 基本編 」と「■ カーネル編 (v5.5)」に分かれています。仕組みを理解するには基本も知る必要があると思い、このような構成となりました。

■ 基本編

はじめにシグナルの基本について、ざっと整理します。

なお、例で登場するコマンドや API (C 言語) の細かい使い方やエラー処理などは省きます。詳細は man や参考文献の情報等をご活用ください。

1. シグナルとは

プロセスやプロセスグループへ様々なイベントを通知するためにあるカーネルの機能です (ソフトウェア割り込み)。イベントの通知は様々な場所 (自分/他プロセス、カーネル) から行うことが可能で、次のようなことができます。

  • ハングしたプロセスにシグナルを送信して強制終了させる
  • シグナルを送信してプロセスの処理を一時停止・再開させる
  • ハードウェア例外 (0 除算、メモリアクセス違反など) 時にシグナルを送信してプロセスを終了させる
  • シグナルを送信する特殊なキー (Ctrl + C など) を入力しプロセスを終了させる
  • シグナル受信時にユーザ定義の関数 (シグナルハンドラ) を実行させる

なお、シグナルは次のような流れで処理されます。詳細は追々見ていきます。

all_signal-flow.jpg

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 ps

3. シグナル番号とシグナル名

シグナルは、用途に応じて番号と名前が割り振られています。

$ 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 は標準動作以外にはできません。

all_signal-sigaction.jpg

  • 無視

    シグナルを受信しても何もしません。

    (設定するには、後述の 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 のみです (次図参照)。

all_signal-kernel_handler.jpg

標準シグナルと対応する標準動作

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

シグナルアクションの変更例

SIGKILLSIGSTOP 以外のシグナルアクションを変更するには、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 1

sigaction (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

all_signal-sigblock.jpg

シグナルブロックの設定例

シグナルブロックを設定するには 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 など多くの構造体が関連しています。

大変混乱しやすいと思いますので、個人的に重要だと思う箇所に関しては、以下のように用途ごとにまとめました。

保留中シグナルを示すフラグ

シグナル生成によって、シグナルを受け取ったプロセスは、シグナル配送によって処理されるまでの間、シグナル保留状態 (シグナル配送待ち) となります。この状態は TIF_SIGPENDING というフラグで示され、スレッドフラグ (flags) に記録されます。

all_signal-thread_flag.jpg

シグナル保留用のキュー (特定プロセス用、スレッドグループ全体用)

受信したシグナルは、sigqueue 構造体によって管理され、関連する幾つかの情報 (※1) を持ちます。それらは保留用のキューに入れられます (sigpending の list につなげられます。その際、シグナルの送信宛に応じて 2 種類 (特定プロセス用、スレッドグループ全体用) の内のどちらかのキューが使用されます。

また、保留されたシグナルは signal に記録されます。この値は /proc/<PID>/status から確認できます (前述の「シグナルの状態確認」と後述の「おまけ」参照)。

  • 特定プロセス用

    all_signal-sig_pending.jpg

  • スレッドグループ全体用

    all_signal-sig_share_pending.jpg

(※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 から確認できます (前述の「シグナルの状態確認」 と後述の「おまけ」参照)。

all_signal-sig_block.jpg

シグナルアクション関連の情報

sigaction() などでシグナルアクションの設定を変更すると、シグナルハンドラディスクリプタ (sighand_struct) に幾つかの情報 (※) が格納されます。また、シグナルハンドラディスクリプタの action[x] はシグナル番号 (_NSIG) 個の配列になっているため (action[シグナル番号-1])、シグナル毎に設定されます。

all_signal-sig_handle.jpg

(※) 幾つかの情報とは、次の sigaction 構造体のメンバを指します。

メンバ 意味
sa_flags シグナルの使い方を示す SA_* フラグ7
sa_handler SIG_IGN、SIG_DFL、シグナルハンドラへのポインタなど、シグナルアクションの種類
sa_mask ハンドラ実行時にブロックする (受信を禁止する) シグナル

2. 実装

シグナルを送信してからシグナルアクションが処理される (kill -TERM 1234 を実行した際の処理) までの流れを見ていきます。

以降、「シグナル生成」と「シグナル配送」で分けてそれぞれ見ていきます (「...」 の部分は省略箇所です)

シグナル生成

この段階での主な仕事は、送信先プロセスのデータ構造に「シグナルを送信した (シグナル配送待ちがある)」ということを送信先プロセスのデータ構造に記録し、通知することです (標準動作といったシグナルアクションはシグナル配送で処理されます)。

以降で行うコードリーディングの要約として、シグナル生成における処理の全体像を図示します。なお、関数の呼び出しが多いため、コードリーディングでは、この中でも特に重要な関数 (コア関数) である __send_signal() から読み進めていきます。

all_signal-signal generate.jpg

__send_signal()

この関数は、送信されたシグナルを配送するかどうかを決め (prepare_signal())、シグナルを保留用キューに入れます (_sigqueue_alloc(), list_add_tail()) その際、保留シグナルの番号を記録します (sigaddset())。また、配送する場合は、プロセスディスクリプタのスレッドフラグに TIFSIGPENDING を立て、配送可能なプロセスを起床します (complete_signal())。

詳細は以降をご参照ください。

kernel/signal.c (L1065)

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;
1109

1105 行目の 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=0

complete_signal()

この関数は、送信されたシグナルを __send_signal() で配送すると決めた場合に呼び出されます。

そして、この関数でシグナル配送の準備ができているプロセスを探します。見つけたら該当プロセスのスレッドフラグに TIF_SIGPENDING を立て、起床します (通知します)。

詳細は以降をご参照ください。

kernel/signal.c (L984)

 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 配下) のコードを見ていきます。

詳細は以降をご参照ください。

all_signal-signal delivery.jpg

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     }
820

815 行目の 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()

この関数は、シグナル保留用キューからシグナルを取り出し、シグナルに応じて標準動作の実行を行います。

kernel/signal.c (L2521)

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, &current->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 = &current->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 を入れています。

fs/proc/array.c#L266 (L266)

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 をブロックするように設定しても実際は設定されない)。

kernel/signal.c (L3004)

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 }

■ 参考文献


  1. 詳細は man 7 signal をご参照ください。 

  2. 詳細は man 5 core をご参照ください。 

  3. 詳細は man 7 pipe ご参照ください。 

  4. 詳細は man 2 setrlimit ご参照ください。 

  5. 詳細は man 2 seccomp ご参照ください。 

  6. 詳細は man 1 bash をご参照ください。 

  7. 詳細は man 2 sigaction をご参照ください。 

  8. 詳細は man 2 signal をご参照ください。 

  9. シグナル無視の場合も解除はできますが、シグナル無視の状態で受信したシグナルは保留されないため、解除された後も処理されません。無視を解除した上で再度シグナルを受信する必要があります。 

  10. 詳細は man 2 sigprocmask をご参照ください。 

  11. カーネルバージョンは調査時点での最新である v5.5 を使用します。 

  12. man 2 execve カーネルソースなどを調べましたが、この原文以上の情報が見当たらず、意味がよく理解できなかったため、原文のまま記載します。 

  13. real_blocked は、rt_sigtimedwait(), rt_sigtimedwait_time32(), rt_sigtimedwait_time64() システムコール使用時に設定されるようです。 

  14. do_signal() でのシステムコール再実行は、handle_signal() を実行しない場合に通るパス (get_signal() が 偽 の時) なので、ここでのシステムコール再実行処理とは重複しません。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Linux シグナルの基本と仕組み (カーネル)

この記事は、Linux シグナルの基本と仕組み (カーネル) について調査したことのまとめです。

シグナルは普段から利用しているものの仕組みについては理解してなかったので、カーネル勉強の題材として調べてみました。想像以上に複雑でボリュームがあったため、書き切れていない部分 (調査しきれなかった部分) もありますが、一通りの流れ (仕組み) は理解できたと思います。

なお、この記事は「■ 基本編 」と「■ カーネル編 (v5.5)」に分かれています。仕組みを理解するには基本も知る必要があると思い、このような構成となっています。基本を理解されている方は読み飛ばしてください。

■ 基本編

はじめにシグナルの基本について、ざっと整理します。

なお、例で登場するコマンドや API (C 言語) の細かい使い方やエラー処理などは省きます。詳細は man や参考文献の情報等をご活用ください。

1. シグナルとは

プロセスやプロセスグループへ様々なイベントを通知するためにあるカーネルの機能です (ソフトウェア割り込み)。イベントの通知は様々な場所 (自分/他プロセス、カーネル) から行うことが可能で、次のようなことができます。

  • ハングしたプロセスにシグナルを送信して強制終了させる
  • シグナルを送信してプロセスの処理を一時停止・再開させる
  • ハードウェア例外 (0 除算、メモリアクセス違反など) 時にシグナルを送信してプロセスを終了させる
  • シグナルを送信する特殊なキー (Ctrl + C など) を入力しプロセスを終了させる
  • シグナル受信時にユーザ定義の関数 (シグナルハンドラ) を実行させる

なお、シグナルは次のような流れで処理されます。詳細は追々見ていきます。

all_signal-flow.jpg

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 ps

3. シグナル番号とシグナル名

シグナルは、用途に応じて番号と名前が割り振られています。

$ 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 は標準動作以外にはできません。

all_signal-sigaction.jpg

  • 無視

    シグナルを受信しても何もしません。

    (設定するには、後述の 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 のみです (次図参照)。

all_signal-kernel_handler.jpg

標準シグナルと対応する標準動作

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

シグナルアクションの変更例

SIGKILLSIGSTOP 以外のシグナルアクションを変更するには、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 1

sigaction (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

all_signal-sigblock.jpg

シグナルブロックの設定例

シグナルブロックを設定するには 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 など多くの構造体が関連しています。

大変混乱しやすいと思いますので、個人的に重要だと思う箇所に関しては、以下のように用途ごとにまとめました。

保留中シグナルを示すフラグ

シグナル生成によって、シグナルを受け取ったプロセスは、シグナル配送によって処理されるまでの間、シグナル保留状態 (シグナル配送待ち) となります。この状態は TIF_SIGPENDING というフラグで示され、スレッドフラグ (flags) に記録されます。

all_signal-thread_flag.jpg

シグナル保留用のキュー (特定プロセス用、スレッドグループ全体用)

受信したシグナルは、sigqueue 構造体によって管理され、関連する幾つかの情報 (※1) を持ちます。それらは保留用のキューに入れられます (sigpending の list につなげられます)。その際、シグナルの送信宛に応じて 2 種類 (特定プロセス用、スレッドグループ全体用) の内のどちらかのキューが使用されます。

また、保留されたシグナルは signal に記録されます。この値は /proc/<PID>/status から確認できます (前述の「シグナルの状態確認」と後述の「おまけ」参照)。

  • 特定プロセス用

    all_signal-sig_pending.jpg

  • スレッドグループ全体用

    all_signal-sig_share_pending.jpg

(※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 から確認できます (前述の「シグナルの状態確認」 と後述の「おまけ」参照)。

all_signal-sig_block.jpg

シグナルアクション関連の情報

sigaction() などでシグナルアクションの設定を変更すると、シグナルハンドラディスクリプタ (sighand_struct) に幾つかの情報 (※) が格納されます。また、シグナルハンドラディスクリプタの action[x] はシグナル番号 (_NSIG) 個の配列になっているため (action[シグナル番号-1])、シグナル毎に設定されます。

all_signal-sig_handle.jpg

(※) 幾つかの情報とは、次の sigaction 構造体のメンバを指します。

メンバ 意味
sa_flags シグナルの使い方を示す SA_* フラグ7
sa_handler SIG_IGN、SIG_DFL、シグナルハンドラへのポインタなど、シグナルアクションの種類
sa_mask ハンドラ実行時にブロックする (受信を禁止する) シグナル

2. 実装

シグナルを送信してからシグナルアクションが処理される (kill -TERM 1234 を実行した際の処理) までの流れを見ていきます。

以降、「シグナル生成」と「シグナル配送」で分けてそれぞれ見ていきます (「...」 の部分は省略箇所です)

シグナル生成

この段階での主な仕事は、送信先プロセスのデータ構造に「シグナルを送信した (シグナル配送待ちがある)」ということを送信先プロセスのデータ構造に記録し、通知することです (標準動作といったシグナルアクションはシグナル配送で処理されます)。

以降で行うコードリーディングの要約として、シグナル生成における処理の全体像を図示します。なお、関数の呼び出しが多いため、コードリーディングでは、この中でも特に重要な関数 (コア関数) である __send_signal() から読み進めていきます。

all_signal-signal generate.jpg

__send_signal()

この関数は、送信されたシグナルを配送するかどうかを決め (prepare_signal())、シグナルを保留用キューに入れます (_sigqueue_alloc(), list_add_tail()) その際、保留シグナルの番号を記録します (sigaddset())。また、配送する場合は、プロセスディスクリプタのスレッドフラグに TIFSIGPENDING を立て、配送可能なプロセスを起床します (complete_signal())。

詳細は以降をご参照ください。

kernel/signal.c (L1065)

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;
1109

1105 行目の 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=0

complete_signal()

この関数は、送信されたシグナルを __send_signal() で配送すると決めた場合に呼び出されます。

そして、この関数でシグナル配送の準備ができているプロセスを探します。見つけたら該当プロセスのスレッドフラグに TIF_SIGPENDING を立て、起床します (通知します)。

詳細は以降をご参照ください。

kernel/signal.c (L984)

 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 配下) のコードを見ていきます。

詳細は以降をご参照ください。

all_signal-signal delivery.jpg

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     }
820

815 行目の 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()

この関数は、シグナル保留用キューからシグナルを取り出し、シグナルに応じて標準動作の実行を行います。

kernel/signal.c (L2521)

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, &current->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->kaka のアドレスを入れます。これは 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 = &current->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 を入れています。

fs/proc/array.c#L266 (L266)

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 をブロックするように設定しても実際は設定されない)。

kernel/signal.c (L3004)

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 }

■ 参考文献


  1. 詳細は man 7 signal をご参照ください。 

  2. 詳細は man 5 core をご参照ください。 

  3. 詳細は man 7 pipe ご参照ください。 

  4. 詳細は man 2 setrlimit ご参照ください。 

  5. 詳細は man 2 seccomp ご参照ください。 

  6. 詳細は man 1 bash をご参照ください。 

  7. 詳細は man 2 sigaction をご参照ください。 

  8. 詳細は man 2 signal をご参照ください。 

  9. シグナル無視の場合も解除はできますが、シグナル無視の状態で受信したシグナルは保留されないため、解除された後も処理されません。無視を解除した上で再度シグナルを受信する必要があります。 

  10. 詳細は man 2 sigprocmask をご参照ください。 

  11. カーネルバージョンは調査時点での最新である v5.5 を使用します。 

  12. man 2 execve カーネルソースなどを調べましたが、この原文以上の情報が見当たらず、意味がよく理解できなかったため、原文のまま記載します。 

  13. real_blocked は、rt_sigtimedwait(), rt_sigtimedwait_time32(), rt_sigtimedwait_time64() システムコール使用時に設定されるようです。 

  14. do_signal() でのシステムコール再実行は、handle_signal() を実行しない場合に通るパス (get_signal() が 偽 の時) なので、ここでのシステムコール再実行処理とは重複しません。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Linux シグナルの仕組み (カーネル実装) を調べました

この記事は、Linux シグナルの仕組み (カーネル実装) について調査したことのまとめです。

シグナルは普段から利用しているものの仕組みについては理解してなかったので、カーネル勉強の題材として調べてみました。想像以上に複雑でボリュームがあったため、書き切れていない部分 (調査しきれなかった部分) もありますが、一通りの流れ (仕組み) は理解できたと思います。

なお、この記事は「基本編」と「カーネル編」に分かれています。仕組みを理解するには基本も知る必要があると思い、このような構成となりました。

基本編

はじめにシグナルの基本について、ざっと整理します。

なお、例で登場するコマンドや API (C 言語) の細かい使い方やエラー処理などは省きます。詳細は man や参考文献の情報等をご活用ください。

シグナルとは

プロセスやプロセスグループへ様々なイベントを通知するためにあるカーネルの機能です (ソフトウェア割り込み)。イベントの通知は様々な場所 (自分/他プロセス、カーネル) から行うことが可能で、次のようなことができます。

  • ハングしたプロセスにシグナルを送信して強制終了させる
  • シグナルを送信してプロセスの処理を一時停止・再開させる
  • ハードウェア例外 (0 除算、メモリアクセス違反など) 時にシグナルを送信してプロセスを終了させる
  • シグナルを送信する特殊なキー (Ctrl + C など) を入力しプロセスを終了させる
  • シグナル受信時にユーザ定義の関数 (シグナルハンドラ) を実行させる

なお、シグナルは次のような流れで処理されます。詳細は追々見ていきます。

all_signal-flow.jpg

シグナル利用例

イメージし易いように、ユーザ視点でのシグナル利用例を 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 は標準動作以外にはできません。

all_signal-sigaction.jpg

  • 無視

    シグナルを受信しても何もしません。

    (設定するには、後述の 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 のみです (次図参照)。

all_signal-kernel_handler.jpg

標準シグナルと対応する標準動作

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

シグナルアクションの変更例

SIGKILLSIGSTOP 以外のシグナルアクションを変更するには、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 1

sigaction (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

all_signal-sigblock.jpg

シグナルブロックの設定例

シグナルブロックを設定するには 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 など多くの構造体が関連しています。

大変混乱しやすいと思いますので、個人的に重要だと思う箇所に関しては、以下のように用途ごとにまとめました。

保留中シグナルを示すフラグ

シグナル生成によって、シグナルを受け取ったプロセスは、シグナル配送によって処理されるまでの間、シグナル保留状態 (シグナル配送待ち) となります。この状態は TIF_SIGPENDING というフラグで示され、スレッドフラグ (flags) に記録されます。

all_signal-thread_flag.jpg

シグナル保留用のキュー (特定プロセス用、スレッドグループ全体用)

受信したシグナルは、sigqueue 構造体によって管理され、関連する幾つかの情報 (※1) を持ちます。それらは保留用のキューに入れられます (sigpending の list につなげられます。その際、シグナルの送信宛に応じて 2 種類 (特定プロセス用、スレッドグループ全体用) の内のどちらかのキューが使用されます。

また、保留されたシグナルは signal に記録されます。この値は /proc/<PID>/status から確認できます (前述の「シグナルの状態確認」と後述の「おまけ」参照)。

  • 特定プロセス用

    all_signal-sig_pending.jpg

  • スレッドグループ全体用

    all_signal-sig_share_pending.jpg

(※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 から確認できます (前述の「シグナルの状態確認」 と後述の「おまけ」参照)。

all_signal-sig_block.jpg

シグナルアクション関連の情報

sigaction() などでシグナルアクションの設定を変更すると、シグナルハンドラディスクリプタ (sighand_struct) に幾つかの情報 (※) が格納されます。また、シグナルハンドラディスクリプタの action[x] はシグナル番号 (_NSIG) 個の配列になっているため (action[シグナル番号-1])、シグナル毎に設定されます。

all_signal-sig_handle.jpg

(※) 幾つかの情報とは、次の sigaction 構造体のメンバを指します。

メンバ 意味
sa_flags シグナルの使い方を示す SA_* フラグ7
sa_handler SIG_IGN、SIG_DFL、シグナルハンドラへのポインタなど、シグナルアクションの種類
sa_mask ハンドラ実行時にブロックする (受信を禁止する) シグナル

実装

シグナルを送信してからシグナルアクションが処理される (kill -TERM 1234 を実行した際の処理) までの流れを見ていきます。

以降、「シグナル生成」と「シグナル配送」で分けてそれぞれ見ていきます (「...」 の部分は省略箇所です)

シグナル生成

この段階での主な仕事は、送信先プロセスのデータ構造に「シグナルを送信した (シグナル配送待ちがある)」ということを送信先プロセスのデータ構造に記録し、通知することです (標準動作といったシグナルアクションはシグナル配送で処理されます)。

以降で行うコードリーディングの要約として、シグナル生成における処理の全体像を図示します。なお、関数の呼び出しが多いため、コードリーディングでは、この中でも特に重要な関数 (コア関数) である __send_signal() から読み進めていきます。

all_signal-signal generate.jpg

__send_signal()

この関数は、送信されたシグナルを配送するかどうかを決め (prepare_signal())、シグナルを保留用キューに入れます (_sigqueue_alloc(), list_add_tail()) その際、保留シグナルの番号を記録します (sigaddset())。また、配送する場合は、プロセスディスクリプタのスレッドフラグに TIFSIGPENDING を立て、配送可能なプロセスを起床します (complete_signal())。

詳細は以降をご参照ください。

kernel/signal.c (L1065)

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;
1109

1105 行目の 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=0

complete_signal()

この関数は、送信されたシグナルを __send_signal() で配送すると決めた場合に呼び出されます。

そして、この関数でシグナル配送の準備ができているプロセスを探します。見つけたら該当プロセスのスレッドフラグに TIF_SIGPENDING を立て、起床します (通知します)。

詳細は以降をご参照ください。

kernel/signal.c (L984)

 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 配下) のコードを見ていきます。

all_signal-signal delivery.jpg

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     }
820

815 行目の 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()

この関数は、シグナル保留用キューからシグナルを取り出し、シグナルに応じて標準動作の実行を行います。

kernel/signal.c (L2521)

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, &current->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 = &current->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 を入れています。

fs/proc/array.c#L266 (L266)

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 をブロックするように設定しても実際は設定されない)。

kernel/signal.c (L3004)

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 }

参考文献


  1. 詳細は man 7 signal をご参照ください。 

  2. 詳細は man 5 core をご参照ください。 

  3. 詳細は man 7 pipe ご参照ください。 

  4. 詳細は man 2 setrlimit ご参照ください。 

  5. 詳細は man 2 seccomp ご参照ください。 

  6. 詳細は man 1 bash をご参照ください。 

  7. 詳細は man 2 sigaction をご参照ください。 

  8. 詳細は man 2 signal をご参照ください。 

  9. シグナル無視の場合も解除はできますが、シグナル無視の状態で受信したシグナルは保留されないため、解除された後も処理されません。無視を解除した上で再度シグナルを受信する必要があります。 

  10. 詳細は man 2 sigprocmask をご参照ください。 

  11. カーネルバージョンは調査時点での最新である v5.5 を使用します。 

  12. man 2 execve カーネルソースなどを調べましたが、この原文以上の情報が見当たらず、意味がよく理解できなかったため、原文のまま記載します。 

  13. real_blocked は、rt_sigtimedwait(), rt_sigtimedwait_time32(), rt_sigtimedwait_time64() システムコール使用時に設定されるようです。 

  14. do_signal() でのシステムコール再実行は、handle_signal() を実行しない場合に通るパス (get_signal() が 偽 の時) なので、ここでのシステムコール再実行処理とは重複しません。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Linuxのインストールや設定でマイナートラブルの対応記録

VirtualBox上にLinuxをインストールしたときのマイナートラブルの対応記録

共通

  1. Guest OS上のHDDインストール時に起動時に使用したISOファイルを指定する必要があるが、Guest Additionsがまだインストールできない状況なので、ISOファイルをどこかに置いて、scpやwgetでGuest OS内にダウンロードする。
  2. 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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 clang

2. .emacs記述による構築

Emacsの設定には .emacs というファイルに色々と記述していく.
ちなみにほかの記事を調べると .emacs.elinit.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では intclass などにタイプが設定されており,それぞれに色を設定することでコードが見やすくなっている.
この状態で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)))

色設定は完全に好みです.
コピペで使用できますが,色設定に関しては上述したようにコマンドでの設定をお勧めするので,
消してから使用してください. (一応コメントアウトはしてあります)

自分の中でのベース環境なので,ここからもっと拡張してもいいですし,これだけでもいいです.
あとは個人個人好きなように環境を作っていってください.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 probes

caseDirにpostProcessingというディレクトリが生成され,中に時系列データのファイルが出力されます.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Linux基礎7 -bashの設定-

はじめに

今回はbashの様々な設定方法を紹介していきます。
WindowsやMacOSを使用する際に自分の好みに合わせて配色や配置を変更するように、Linuxでも同様にユーザ環境の設定が可能です。それだけでなくLinuxではカーネル動作などのシステム基幹部分までも設定を変更できるようになっています。Linuxの設定変更の全てを紹介することはできませんので、今回は一番身近である、bashのカスタマイズに絞ってお話しします。

おまけ
タイプミスがきっかけでこの強調方法を知りました…見やすいので使っていこうと思います。

エイリアス

エイリアスとは、既存のコマンドに別名をつけて実行できるようにする機能のことです。

エイリアスの設定

例えばls -Fコマンドはファイルの種別を表示するコマンドで、大変頻繁に利用すると思います。
毎回ls -Fとコマンドを入力するのは面倒なので、ls -Flsという名前をつけてしまえば、lsと打つだけで ls -Fが実行されファイルの種別を表示してくれます。

エイリアス(alias)の設定
$ alias <名前>='コマンド'

$ alias ls='ls -F' #lsコマンドの設定例

エイリアス設定後のlsの実行例
スクリーンショット 2020-05-24 10.36.02.png
エイリアスを設定したことで、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

lsにエイリアスが実行されていた場合の実行結果
スクリーンショット 2020-05-24 10.51.04.png

一度設定したエイリアスを削除するにはunaliasコマンドを使用します。

エイリアスの削除
$ unalias ls

unalias実行後にエイリアスが削除された様子
スクリーンショット 2020-05-24 10.53.32.png

エイリアスを一時的に無効にする

エイリアスを設定したものの、一時的に何もオプションをつけずにコマンドを実行したいタイミングがあります。
その場合はいくつかの方法がありますので、紹介します。

一時的にエイリアスを無効にする
$ /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のオプションをオンにした例

なおshoptsetでは指定できるオプション名が異なります。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には、コマンドを実行する際にシェルがコマンドを探すディレクトリを指定します。
この変数にはシェルがコマンドを探すディレクトリを「:」区切りで連結した文字列が設定されています。
スクリーンショット 2020-05-24 11.40.19.png
PATHには通常、ディストリビューションごとに適切な値が設定されていますが、自分で作成した独自のディレクトリや個別でインストールしたアプリケーションをコマンド検索パスに含めたい場合は、PATH変数の値を変更することで、コマンドをフルパスで実行する必要がなくなります

Linuxでは個別のコマンドを配置するディレクトリとして~/binというディレクトリがよく使用されます。ここにパスを設定する場合には次のように設定します。

個別パスの追加
 $ PATH="$PATH:~/bin"

LANG ロケール

ロケール(Locale)は言語や国、地域などを特定するための識別子です。表示する言語や日付の書式などは、このロケールを基準にして切り替えられます。
現在使用しているロケールはLANGというシェル変数で指定されています。

ロケールの設定
$ echo $LANG        #現在設定されているロケールの表示
$ LANG=en_US.UTF-8  #ロケールの設定

使用できるロケールはlocale -aコマンドで表示できますので適切な設定を探してみてください。
スクリーンショット 2020-05-24 11.49.20.png

その他のシェル変数

ここまで紹介した意外にも特別な意味を持つシェル変数はあります。ここではよく使われるシェル変数を紹介します。

シェル変数名 内容
HISTFILE コマンドラインに履歴を保存するファイル名。デフォルト値は~/.bash_history
HISTFILESIZE 履歴ファイルに保存するコマンドライン履歴の最大行数
HISTSIZE コマンドライン履歴を保持する最大行数
HOME ホームディレクトリ
SHELL ログインシェルのパス
PWD カレントディレクトリ

bashのシェル変数については、bashマニュアルの「Shell Variable」を参照しください。

環境変数

コマンドには、実行ファイルとしてファイルシステム上に存在するコマンドを外部コマンド、一方、シェル自体に内蔵しているコマンドを組み込みコマンドと呼びます。

コマンドが外部コマンド組み込みコマンドかを判断するにはtypeコマンドで確認できます。
スクリーンショット 2020-05-24 12.07.31.png

ここで注意点は、現在設定されているシェル変数の値を外部コマンドは参照することができないということです。

しかし、LANGによるロケールの設定のように、シェル上だけでなく外部コマンドでも常に設定を反映しておきたい変数も多数存在します。このような用途のために外部コマンドからも値を参照できるようにした変数を環境変数と言います。

変数LANGは、特に指定しなくても自動的に環境変数として設定されているため、catのような外部コマンドもLANGの指定によって出力メッセージが変化します。catコマンドをはじめとして、多くのコマンドは、環境変数の値を確認してその設定に従って動作します

printenvコマンド 環境変数の表示

現在シェルに設定されている環境変数を確認するには、printenvコマンドを実行します。
下記のようにLANGなどの他にも多くのシェル変数が環境変数として設定されていることがわかります。
スクリーンショット 2020-05-24 12.20.45.png

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はこれらのファイルに書かれた内容を実行します。

スクリーンショット 2020-05-24 13.33.03.png
/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-24 14.48.18.png

参考書籍

新しいLinuxの教科書

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Jetson nanoをセットアップする

Jetsonをセットアップする

サークルでJetsonを買ったのでセットアップしていきますー!
この記事はOSが起動するまでを書いていきます
0488D1C9-19BB-465D-899A-9A00CFA4CE9C.jpeg

必要なもの

電源(5v2A以上のUSB充電器か、5.5mmのDC電源)
SDカード
Wifiドングル
マウス、キーボード
ディスプレイ

開封

F82F0582-2B1D-4FBF-A6F1-64A9AFD61D8F.jpeg
案外シンプルかも

内容物はJetson本体、説明書、紙の台だけ
この微妙に余った段ボールのスペースには何も入ってません(筆者が振って確かめました)
この余分なもの削ればいいのに…

電源について

最低でも5v2A以上の電源を買ってください
一応5v2AでのUSB給電での動作は確認しました
ただし、これはjetsonの性能をセーブした状態での動作になるため5v4Aぐらいの電源の方がいいです

DCジャック電源を利用する時

DCジャック電源を利用する時はジャンパピンをショートさせます3353DDCE-A246-4C99-84CA-68D02F92471D.jpeg
画像の場所にジャンパピンがついてると思うので隣のピンとショートさせます
なんか昔はジャンパピンが付属してなくて大変だったそうです

追記
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=1

TL-WN725Nという格安モジュールです(安さは正義!)

調べてみればわかるのですが、USBWi-Fiモジュールは相性問題があり、運が悪いとドライバのインストールとかめんどくさいことになるので注意

起動

問題がなければ以下の画面が表示されます
されなかった人は....ファイト!
E89912A1-96C8-47A3-84EE-9DC7998D389D.jpeg

この記事では紹介しませんがJetcardというパッケージをインストールするとsampleでGPGPUできます

6EE40278-689F-4F6C-B9DB-7C15D91139D0.jpeg

楽しーーー!(変人の自覚はある)

最後に

Jetson nanoは1万でCUDAとLinuxが遊べるようになるのでおススメ
時代はGPUよ!
CUDAで上げろ、自分の市場価値!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む