rtos 環境でのスレッドセーフについて

65
RTOS 環境での スレッドセーフについて nagayah http://nagayah.blog46.fc2.com/

Upload: nagayah

Post on 31-May-2015

1.951 views

Category:

Documents


8 download

DESCRIPTION

Rtos 環境でのスレッドセーフについて。μITRON などの環境を想定しています。

TRANSCRIPT

Page 1: Rtos 環境でのスレッドセーフについて

RTOS 環境での スレッドセーフについて

nagayah

http://nagayah.blog46.fc2.com/

Page 2: Rtos 環境でのスレッドセーフについて

目次 マルチタスクについて

マルチタスクOSって?

マルチタスクOS特有の注意点

スレッドセーフとは?

リエントラントについて

スタックと関数コール

bss/dataセクション

関数の再入って?

非リエントラント関数の動き

リエントラント関数の動き

リエントラントの要件

排他制御について

クリティカルセクション

クリティカルセクションの保護の仕方

アトミックアクセスについて少しだけ

まとめ

Page 3: Rtos 環境でのスレッドセーフについて

目次

マルチタスクについて

マルチタスクOSって?

マルチタスクOS特有の注意点

スレッドセーフとは?

リエントラントについて

排他制御について

まとめ

Page 4: Rtos 環境でのスレッドセーフについて

マルチタスクOSって?

1つの CPU で複数のタスクを平行して実行

時分割で実行するタスクを切り替える

トータルで見れば並行して実行している

擬似的なマルチ CPU 環境

擬似CPU 1 タスクA

擬似CPU 2

擬似CPU 3

タスクB

タスクC

本当のCPU

Page 5: Rtos 環境でのスレッドセーフについて

マルチタスクOSの注意点

1つの資源を複数のタスクが使うときに問題

あるタスクが資源にアクセス中に

他のタスクがアクセスする場合

アクセスが競合して処理が破綻!

共有資源

競合発生!!

Page 6: Rtos 環境でのスレッドセーフについて

競合を回避するにはどうするの?

競合が起きないようにする手法があります

これをスレッドセーフ化といいます

スレッド≒タスク

マルチタスク環境では必須

スレッドセーフ化すればOK!

共有資源

Page 7: Rtos 環境でのスレッドセーフについて

スレッドセーフ化するための方法

一般的にこんな方法があります

リエントラント

排他制御

アトミックアクセス

これらについて、順次説明していきます

Page 8: Rtos 環境でのスレッドセーフについて

一旦、ここまでをまとめます

マルチタスクOS

1CPU での、擬似的なマルチコア環境

複数タスクから共有資源を使う場合の問題

競合の可能性がある

スレッドセーフ化で解決可能

リエントラント

排他制御

アトミックアクセス

Page 9: Rtos 環境でのスレッドセーフについて

目次

マルチタスクについて

リエントラントについて

スタックと関数コール

bss/dataセクション

関数の再入って?

非リエントラント関数の動き

リエントラント関数の動き

リエントラントの要件

排他制御について

まとめ

Page 10: Rtos 環境でのスレッドセーフについて

リエントラント|Reentrant

「再入可能な関数」という意味

Re:再び entrant:入れる

マルチタスクOS 環境では

普通に起こる現象

再入を前提にしておく必要あり

void f(void)

{

/*

* 何かしらの処理

*/

return;

}

Page 11: Rtos 環境でのスレッドセーフについて

void f(void)

{

/*

* 何かしらの処理

*/

return;

}

関数の再入って?

関数の途中で実行を中断し、

他のタスクが同じ関数をコールすること

タスクAがf()をコール

タスクA

Page 12: Rtos 環境でのスレッドセーフについて

void f(void)

{

/*

* 何かしらの処理

*/

return;

}

関数の再入って?

関数の途中で実行を中断し、

他のタスクが同じ関数をコールすること

タスクAがf()をコール

タスクAはf()の途中まで実行

タスクA

Page 13: Rtos 環境でのスレッドセーフについて

void f(void)

{

/*

* 何かしらの処理

*/

return;

}

関数の再入って?

関数の途中で実行を中断し、

他のタスクが同じ関数をコールすること

タスクAがf()をコール

タスクAはf()の途中まで実行

タスクAが実行を中断、

タスクBがf()をコール

タスクA タスクB

Page 14: Rtos 環境でのスレッドセーフについて

void f(void)

{

/*

* 何かしらの処理

*/

return;

}

関数の再入って?

関数の途中で実行を中断し、 他のタスクが同じ関数をコールすること

タスクAがf()をコール

タスクAはf()の途中まで実行

タスクAが実行を中断、 タスクBがf()をコール

再入が起きても破綻しない リエントラント

タスクA タスクB

Page 15: Rtos 環境でのスレッドセーフについて

ちょっとC言語の変数の基本をおさらい

自動変数

ローカル変数とも呼ばれる

スタック上に配置される

静的変数

グローバル変数と static 変数のこと

初期値があれば data セクションに配置

初期値がなければ bss セクションに配置

bss は 0 クリアされます

int g = 1;

void f(void)

{

int a;

static b;

}

Page 16: Rtos 環境でのスレッドセーフについて

スタックと自動変数についておさらい

関数をコールするたびに確保

引数や自動変数をそこに置く

アドレスの若い方へ伸張

関数を抜けるたびに解放

void funcB(int arg) {

int ret = arg;

return ret;

}

void funcA(void) {

int dat = 3;

funcB(dat);

}

自動変数 dat

0番地

Page 17: Rtos 環境でのスレッドセーフについて

スタックと自動変数についておさらい

関数をコールするたびに確保

引数や自動変数をそこに置く

アドレスの若い方へ伸張

関数を抜けるたびに解放

void funcB(int arg) {

int ret = arg;

return ret;

} 自動変数 dat

引数 arg

自動変数 ret

void funcA(void) {

int dat = 3;

funcB(dat);

}

0番地

Page 18: Rtos 環境でのスレッドセーフについて

スタックと自動変数についておさらい

関数をコールするたびに確保

引数や自動変数をそこに置く

アドレスの若い方へ伸張

関数を抜けるたびに解放

void funcB(int arg) {

int ret = arg;

return ret;

}

void funcA(void) {

int dat = 3;

funcB(dat);

}

0番地

自動変数 dat

Page 19: Rtos 環境でのスレッドセーフについて

複数タスクからコールされたとき

複数タスクからコールされれば

スタックも複数個確保される

自動変数 dat

引数 arg

自動変数 ret

0番地

TaskA用のスタック領域

void funcB(int arg) {

int ret = arg;

return ret;

}

void funcA(void) {

int dat = 3;

funcB(dat);

}

TaskA

Page 20: Rtos 環境でのスレッドセーフについて

複数タスクからコールされたとき

複数タスクからコールされれば

スタックも複数個確保される

自動変数 dat

引数 arg

自動変数 ret

0番地

自動変数 dat

引数 arg

自動変数 ret

TaskA用のスタック領域

TaskB用のスタック領域

void funcB(int arg) {

int ret = arg;

return ret;

}

void funcA(void) {

int dat = 3;

funcB(dat);

}

TaskA TaskB

Page 21: Rtos 環境でのスレッドセーフについて

静的変数 [bss/data セクション]

静的変数の領域

初期値ある ⇒ dataセクション

初期値なし ⇒ bss セクション

スタックと違って1つしかない!

複数タスクからアクセスされても

新たに確保されたりはしない!

静的変数はタスク間の共有資源!

0番地

BSS

DATA

TaskA スタック領域

TaskB スタック領域

Page 22: Rtos 環境でのスレッドセーフについて

非リエントラント関数の例

非リエントラント関数で再入が

起きるとどうなるか見ていきます

静的変数を使用する下記の関数を

TaskA、TaskB からコールしてみます。

0番地

TaskA スタック領域

BSS セクション

data==0

TaskB スタック領域

int data;

void f(int arg)

{

data = arg;

printf(“data is %d¥n”, data)

}

Page 23: Rtos 環境でのスレッドセーフについて

非リエントラント関数の例

TaskA が f(10); をコール

引数がスタックへ積まれる

int data;

void f(int arg)

{

data = arg;

printf(“data is %d¥n”, data)

}

0番地

TaskA スタック領域

BSS セクション

data:0

arg:10

TaskA TaskB スタック領域

Page 24: Rtos 環境でのスレッドセーフについて

非リエントラント関数の例

TaskA が f(10); をコール

引数がスタックへ積まれる

dataセクション のdata が更新される

int data;

void f(int arg)

{

data = arg;

printf(“data is %d¥n”, data)

}

0番地

TaskA スタック領域

BSS セクション

data:10

arg:10

TaskA TaskB スタック領域

Page 25: Rtos 環境でのスレッドセーフについて

非リエントラント関数の例

TaskB が再入、 f(20); をコール

引数がスタックへ積まれる

int data;

void f(int arg)

{

data = arg;

printf(“data is %d¥n”, data)

}

0番地

TaskA スタック領域

BSS セクション

data:10

arg:10

arg:20

TaskA TaskB TaskB スタック領域

Page 26: Rtos 環境でのスレッドセーフについて

非リエントラント関数の例

TaskB が再入、 f(20); をコール

引数がスタックへ積まれる

dataセクション のdata が更新される

競合!TaskAの値を上書き!

int data;

void f(int arg)

{

data = arg;

printf(“data is %d¥n”, data)

}

0番地

TaskA スタック領域

BSS セクション

data:20

arg:10

arg:20

TaskB TaskB スタック領域 TaskA

Page 27: Rtos 環境でのスレッドセーフについて

非リエントラント関数の例

TaskB が再入、 f(20); をコール

引数がスタックへ積まれる

dataセクション のdata が更新される

競合!TaskAの値を上書き!

期待通り “data is 20.” と出力される

int data;

void f(int arg)

{

data = arg;

printf(“data is %d¥n”, data)

}

0番地

TaskA スタック領域

BSS セクション

data:20

arg:10

arg:20

TaskB スタック領域 TaskA TaskB

Page 28: Rtos 環境でのスレッドセーフについて

非リエントラント関数の例

TaskA が実行再開

引数がスタックへ積まれる

dataセクション のdata が更新される

期待を裏切り “data is 20.” と出力される

引数は10だったのに!

int data;

void f(int arg)

{

data = arg;

printf(“data is %d¥n”, data)

}

0番地

TaskA スタック領域

BSS セクション

data:20

arg:10

arg:20

TaskB スタック領域 TaskB TaskA

Page 29: Rtos 環境でのスレッドセーフについて

非リエントラント関数の例

再入が発生すると処理が破綻した

これは非リエントラントな関数

int data;

void f(int arg)

{

data = arg;

printf(“data is %d¥n”, data)

}

0番地

TaskA スタック領域

BSS セクション

data:20

arg:10

arg:20

TaskB スタック領域 TaskB TaskA

Page 30: Rtos 環境でのスレッドセーフについて

リエントラントな関数に修正

静的変数を使わないように修正

変数をBSSセクションでなく

スタックに積むように変更しただけ

void f(int arg)

{

int data;

data = arg;

printf(“data is %d¥n”, data)

}

0番地

TaskA スタック領域

BSS セクション

TaskB スタック領域

Page 31: Rtos 環境でのスレッドセーフについて

リエントラントな関数に修正

TaskA が f(10); をコール

引数がスタックへ積まれる

void f(int arg)

{

int data;

data = arg;

printf(“data is %d¥n”, data)

}

0番地

TaskA スタック領域

BSS セクション

arg:10

TaskB スタック領域 TaskA

Page 32: Rtos 環境でのスレッドセーフについて

void f(int arg)

{

int data;

data = arg;

printf(“data is %d¥n”, data)

}

リエントラントな関数に修正

TaskA が f(10); をコール

引数がスタックへ積まれる

自動変数 data が更新される

0番地

TaskA スタック領域

BSS セクション

arg:10

data:10

TaskB スタック領域 TaskA

Page 33: Rtos 環境でのスレッドセーフについて

void f(int arg)

{

int data;

data = arg;

printf(“data is %d¥n”, data)

}

リエントラントな関数に修正

TaskB が再入、 f(20); をコール

引数がスタックへ積まれる

0番地

TaskA スタック領域

BSS セクション

arg:20

arg:10

data:10

TaskB スタック領域 TaskA TaskB

Page 34: Rtos 環境でのスレッドセーフについて

void f(int arg)

{

int data;

data = arg;

printf(“data is %d¥n”, data)

}

リエントラントな関数に修正

TaskB が再入、 f(20); をコール

引数がスタックへ積まれる

自動変数 dataが更新される

TaskA の変数 data には影響しない

0番地

TaskA スタック領域

BSS セクション

arg:10

arg:20

data:10

data:20

TaskB スタック領域 TaskA TaskB

Page 35: Rtos 環境でのスレッドセーフについて

void f(int arg)

{

int data;

data = arg;

printf(“data is %d¥n”, data)

}

リエントラントな関数に修正

TaskB が再入、 f(20); をコール

引数がスタックへ積まれる

自動変数 dataが更新される

TaskA の変数 data には影響しない

期待通り “data is 20.” と出力される

0番地

TaskA スタック領域

BSS セクション

arg:10

arg:20

data:10

data:20

TaskB スタック領域 TaskA TaskB

Page 36: Rtos 環境でのスレッドセーフについて

void f(int arg)

{

int data;

data = arg;

printf(“data is %d¥n”, data)

}

リエントラントな関数に修正

TaskA が実行再開

引数がスタックへ積まれる

自動変数 data が更新される

期待通り “data is 10.” と出力される

0番地

TaskA スタック領域

BSS セクション

arg:10

arg:20

data:10

data:20

TaskB スタック領域 TaskA TaskB

Page 37: Rtos 環境でのスレッドセーフについて

リエントラントな関数に修正

再入しても破綻しなかった!

これはリエントラントな関数

void f(int arg)

{

int data;

data = arg;

printf(“data is %d¥n”, data)

}

0番地

TaskA スタック領域

BSS セクション

arg:10

arg:20

data:10

data:20

TaskB スタック領域 TaskA TaskB

Page 38: Rtos 環境でのスレッドセーフについて

リエントラントの条件

「1つしかない」ものにアクセスしない

使ってはダメなものの例

静的変数(static、グローバル変数)

ASIC レジスタ

volatile 変数

非リエントラント関数

引数とローカル変数のみを使えばOKです!

ローカル変数=スタックに詰まれる変数

Page 39: Rtos 環境でのスレッドセーフについて

リエントラントはスレッドセーフ?

静的変数のような、タスク間の

共有資源となりうるものを一切使っていない

なのでそもそもタスク間の競合が発生しない

リエントラントなら必ずスレッドセーフ

共有資源

競合が発生しえない

Page 40: Rtos 環境でのスレッドセーフについて

リエントラントのまとめ

リエントラントにすればスレッドセーフになる

リエントラントにするには、

引数と自動変数だけを使用する

スタックは関数コールごとに確保される

同じ関数でも複数コールされれれば

複数別々のスタックを確保する

静的変数はbss/dataセクションに置かれる

bss/dataセクションは1つしかない

Page 41: Rtos 環境でのスレッドセーフについて

目次

マルチタスクについて

リエントラントについて

排他制御について

クリティカルセクション

クリティカルセクションの保護の仕方

アトミックなアクセスについて少しだけ

まとめ

Page 42: Rtos 環境でのスレッドセーフについて

リエントラントはわかったけど・・

どうしてもタスク共有資源が必要なときはどうする?

共有資源にアクセスしている間は

他のタスクがアクセスできないように

排他制御すればよい

排他制御が必要な区間

クリティカルセクション

Page 43: Rtos 環境でのスレッドセーフについて

クリティカルセクションの例

再入がおきると破綻する区間はどこ?

共有資源にアクセスしているところは?

int sum(int arg)

{

static int sumv;

int tmp;

tmp = sumv;

tmp += arg;

sumv = tmp;

return sum;

}

Page 44: Rtos 環境でのスレッドセーフについて

クリティカルセクションの例

再入がおきると破綻する区間はどこ?

共有資源にアクセスしているところは?

ココのどこかで再入が

起きると破綻する!

つまりココが

クリティカルセクション!

int sum(int arg)

{

static int sumv;

int tmp;

tmp = sumv;

tmp += arg;

sumv = tmp;

return sumv;

}

Page 45: Rtos 環境でのスレッドセーフについて

クリティカルセクションの保護|セマフォ

バイナリセマフォ

1 : 他タスクがクリティカル

セクション実行中

0:だれもクリティカル

セクション実行してない

先にクリティカルセクションに

入ったタスクが抜けるまで、

他のタスクは待たされる

int sum(int arg)

{

static int sumv;

int tmp;

wai_sem(id);

tmp = sumv;

tmp += arg;

sumv = tmp;

sig_sem(id);

return sumv;

}

Page 46: Rtos 環境でのスレッドセーフについて

バイナリセマフォでの排他例

TaskA が先に

クリティカルセクション突入

int sum(int arg)

{

static int sumv;

int tmp;

wai_sem(id);

tmp = sumv;

tmp += arg;

sumv = tmp;

sig_sem(id);

return sumv;

}

TaskA

Page 47: Rtos 環境でのスレッドセーフについて

int sum(int arg)

{

static int sumv;

int tmp;

wai_sem(id);

tmp = sumv;

tmp += arg;

sumv = tmp;

sig_sem(id);

return sumv;

}

バイナリセマフォでの排他例

TaskA が先に

クリティカルセクション突入

TaskB が再入。

セマフォ取得で待たされる。

TaskB TaskA

Page 48: Rtos 環境でのスレッドセーフについて

int sum(int arg)

{

static int sumv;

int tmp;

wai_sem(id);

tmp = sumv;

tmp += arg;

sumv = tmp;

sig_sem(id);

return sumv;

}

バイナリセマフォでの排他例

TaskA が先に

クリティカルセクション突入

TaskB が再入。

セマフォ取得で待たされる。

TaskA が再開。

クリティカルセクション終了。

TaskB TaskA

Page 49: Rtos 環境でのスレッドセーフについて

int sum(int arg)

{

static int sumv;

int tmp;

wai_sem(id);

tmp = sumv;

tmp += arg;

sumv = tmp;

sig_sem(id);

return sumv;

}

バイナリセマフォでの排他例

TaskA が先に

クリティカルセクション突入

TaskB が再入。

セマフォ取得で待たされる。

TaskA が再開。

クリティカルセクション終了。

TaskB が再開。

無事セマフォを取得し

クリティカルセクション完了

TaskB

Page 50: Rtos 環境でのスレッドセーフについて

割込みからもコールされる場合は?

割込みハンドラ内ではセマフォは使えない

割込み禁止にするしかない

他タスクへディスパッチする こともありえません

割込みハンドラすらありえません

何人たりとも邪魔はできません

最強の方法です

他への迷惑度も大きい 諸刃の剣。乱用はやめましょう

int sum(int arg)

{

static int sumv;

int tmp;

begin_intr_dis();

tmp = sumv;

tmp += arg;

sumv = tmp;

end_intr_dis();

return sumv;

}

Page 51: Rtos 環境でのスレッドセーフについて

いったん、排他制御についてまとめ

共有資源にアクセスしている箇所は

排他制御が必要となる

クリティカルセクション

クリティカルセクションは

バイナリセマフォで保護できる

割込みハンドラからもアクセスする場合は

セマフォではダメ。割込み禁止が必要。

でも割込み禁止は悪影響大。

使うのはほんとに最後の手段。

Page 52: Rtos 環境でのスレッドセーフについて

クリティカルセクション 応用編

静的変数に、もし1行でアクセスしてたら、

再入することはなくない・・・?

アクセス中に中断できる場所がなさそう

TaskA が tmp にアクセス中に

TaskB は再入できるのか?

もし再入できなければ、

これはスレッドセーフ!

void and_tmp (void)

{

static tmp;

tmp &= 0xfe;

}

TaskA TaskB

Page 53: Rtos 環境でのスレッドセーフについて

クリティカルセクション 応用編

C言語の行数と実際のCPU命令数は

一致しない

普通、値の変更には3命令以上必要

(CPUにもよるんですけど)

今の値をリードして、

それをモディファイして、

その結果をライトする。

void and_tmp (void)

{

static tmp;

tmp &= 0xfe;

}

ldr r0 [tmp] // リード and r0 r0 #0xfe // モディファイ str r0 [tmp] // ライト

Page 54: Rtos 環境でのスレッドセーフについて

ldr r0 [tmp] // リード and r0 r0 #0xfe // モディファイ str r0 [tmp] // ライト

クリティカルセクション 応用編

結局、CPU 命令の間で再入が発生

このクリティカルセクションを排他しない限り

スレッドセーフではない・・

C言語のコードに惑わされてはダメ!

void and_tmp (void)

{

static tmp;

tmp &= 0xfe;

}

TaskA TaskB

Page 55: Rtos 環境でのスレッドセーフについて

でも、ということは、逆にいうと?

CPU 1命令でアクセスすれば再入できない

つまり、スレッドセーフ!

実行中に分割できないアクセス

アトミックアクセス

CPU によって色々違うのですが、

一般的に通用しそうことをまとめてみます

一応、 ARM のような RISC CPU を想定

Page 56: Rtos 環境でのスレッドセーフについて

アトミックアクセスの簡単なまとめ

アトミックアクセスならスレッドセーフ でもかなり難易度が高い

アトミックアクセスになる場合 CPU が用意しているアトミック専用命令を使用

ARM なら swap, ldrex, strex など

int 型へのライトアクセス (絶対というわけではない)

リード・モディファイ・ライト操作はダメ

char/short などはマスクやシフトが必要だからダメ

アトミックかどうかの判断 専用命令を使う以外の場合は、実際にコンパイラが 生成した命令を確認する必要があります

CPU やコンパイラによって結果は変わる

Page 57: Rtos 環境でのスレッドセーフについて

目次

マルチタスクについて

リエントラントについて

排他制御について

まとめ

Page 58: Rtos 環境でのスレッドセーフについて

今日のまとめ

マルチタスク OS ではスレッドセーフが必要

スレッドセーフにする方法

リエントラントにする

クリティカルセクションを排他する

セマフォ|割込み禁止

もしくはアトミックアクセスにする

これは高難易度

Page 59: Rtos 環境でのスレッドセーフについて

スレッドセーフ化までの道のり (例)

複数タスクからコールされる?

共有資源使う?

割込みハンドラからもコールされる?

セマフォを使ってクリティカルセクション保護

[NO] スレッドセーフにする必要なし

リエントラントにしてスレッドセーフ化

アトミックアクセスにしてスレッドセーフ化

割り禁を使ってクリティカルセクション保護

リード・モディファイ・ライトする?

[Yes]

[NO]

[Yes]

[NO]

[Yes]

[Yes]

[NO]

Page 60: Rtos 環境でのスレッドセーフについて

今回出てきたキーワード

google 先生に聞けばたぶん教えてくれます

マルチタスクOS

スレッドセーフ

自動変数

bssセクション/dataセクション

スタック

リエントラント

排他制御

バイナリセマフォ

割込み禁止

アトミックアクセス

リード・モディファイ・ライト

Page 61: Rtos 環境でのスレッドセーフについて

参考文献 組み込みソフトウェアの設計&検証

藤倉 俊幸 [CQ出版]

RTOS でのスレッドセーフやアトミック操作についての説明があります

C言語ポインタ完全制覇 前橋 和弥 [技術評論社]

スタックについて分かりやすい説明があります

エキスパートCプログラミング Peter van der Linden [アスキー]

bss, data セクションについて分かりやすい説明があります

C言語 デバッグ完全解説 坂井 弘亮 [技術評論社]

スレッドセーフ関係のデバッグについて記載が在ります

ARM組み込みソフトウェア入門 Andrew N. Sloss [CQ出版]

ARM アセンブラ関係ではたぶん一番分かりやすい

Page 62: Rtos 環境でのスレッドセーフについて

以上です

Page 63: Rtos 環境でのスレッドセーフについて
Page 64: Rtos 環境でのスレッドセーフについて

以下、メモ

Page 65: Rtos 環境でのスレッドセーフについて

リエントラントの例 (再帰呼び出し版)

再帰関数でも再入は発生します

たとえば下記のような感じ

なので再帰関数もリエントラントである必要があります

void f(void)

{

if(!終了条件)

f();

return;

}

void f(void)

{

if(!終了条件)

f();

return;

}

void f(void)

{

if(!終了条件)

f();

return;

}