メモ
Linuxのプロセススケジューラではプロセスが実際にCPUを割り当てられる際に負荷の低いプロセスに割り当てられる仕組みがある。
簡単なperlプログラムを使って実験。
第一引数に並列数、第二引数に1以上の数値を与えるとシステムコールを発行、0の場合は単純にループしてプロセッサを使い続ける仕様
#!/bin/perl my $nprocs = $ARGV[0] || 1; for( my $i=0; $i<$nprocs; $i++ ){ my $pid = fork; die $! if( $pid < 0 ); if( $pid == 0 ){ while(1){ if( $ARGV[1] ){ open(IN, ">/dev/null"); close(IN); } } } } wait;
基本状態のdstat(プログラむ実行なし)
[root@imp02 ~]# dstat -a -C 0,1 -------cpu0-usage--------------cpu1-usage------ -dsk/total- -net/total- ---paging-- ---system-- usr sys idl wai hiq siq:usr sys idl wai hiq siq| read writ| recv send| in out | int csw 0 0 100 0 0 0: 0 0 100 0 0 0| 348B 7363B| 0 0 | 0 0 | 35 52 0 0 100 0 0 0: 0 0 100 0 0 0| 0 0 | 66B 1070B| 0 0 | 52 71 0 0 100 0 0 0: 0 0 100 0 0 0| 0 0 | 66B 422B| 0 0 | 42 63
このプログラムをまずは第一引数1、第二引数0で実行してみる。その結果をdstatでコアごとにcpu使用率を確認してみる。
[root@imp02 ~]# dstat -a -C 0,1,total -------cpu0-usage--------------cpu1-usage-----------total-cpu-usage---- -dsk/total- -net/total- ---paging-- ---system-- usr sys idl wai hiq siq:usr sys idl wai hiq siq:usr sys idl wai hiq siq| read writ| recv send| in out | int csw 0 0 100 0 0 0: 0 0 100 0 0 0: 0 0 100 0 0 0| 348B 7361B| 0 0 | 0 0 | 35 52 100 0 0 0 0 0: 0 1 99 0 0 0: 50 1 50 0 0 0| 0 0 | 66B 1318B| 0 0 |1058 79 100 0 0 0 0 0: 0 0 100 0 0 0: 50 0 50 0 0 0| 0 0 | 66B 494B| 0 0 |1043 67 100 0 0 0 0 0: 0 0 100 0 0 0: 50 0 50 0 0 0| 0 0 | 66B 494B| 0 0 |1050 75 100 0 0 0 0 0: 0 0 100 0 0 0: 51 0 50 0 0 0| 0 0 | 66B 494B| 0 0 |1041 63 100 0 0 0 0 0: 0 0 100 0 0 0: 50 0 50 0 0 0| 0 0 | 66B 494B| 0 0 |1035 58
cpu0のusr使用率が0%になっていることがわかった。
この状態では単純にユーザ空間でループしてるだけ。
今度はシステムコールを呼び出してみる。 システムコールを呼び出すことで今度はカーネル空間での処理が必要になるためcpu使用率はusrとsysで半分半分になる。
[root@imp02 ~]# dstat -a -C 0,1,total -------cpu0-usage--------------cpu1-usage-----------total-cpu-usage---- -dsk/total- -net/total- ---paging-- ---system-- usr sys idl wai hiq siq:usr sys idl wai hiq siq:usr sys idl wai hiq siq| read writ| recv send| in out | int csw 14 17 63 0 0 6: 17 26 54 0 0 3: 16 22 58 0 0 4| 0 0 | 402B 698B| 0 0 |1636 944 20 26 47 0 0 7: 22 27 47 0 0 4: 21 26 47 0 0 5| 0 0 | 66B 494B| 0 0 |2054 1172 21 27 46 0 0 6: 21 26 48 0 0 6: 21 27 47 0 0 5| 0 0 | 66B 494B| 0 0 |2093 1204 18 22 53 0 0 7: 23 32 42 0 0 3: 20 27 47 0 0 5| 0 0 | 66B 494B| 0 0 |2085 1205 21 27 47 0 0 5: 21 26 47 0 0 6: 21 26 47 0 0 5| 0 0 | 66B 494B| 0 0 |2054 1155 27 30 39 0 0 4: 18 21 54 0 0 7: 22 26 47 0 0 5| 0 0 | 66B 494B| 0 0 |2100 1205 18 23 52 0 0 7: 24 31 41 0 0 4: 20 27 47 0 0 5| 0 0 | 140B 548B| 0 0 |2038 1170 17 25 52 0 0 5: 22 30 43 0 0 5: 20 28 47 0 0 5| 0 0 | 66B 494B| 0 0 |2104 1219 24 28 44 0 0 5: 21 23 50 0 0 7: 22 25 47 0 0 5| 0 0 | 66B 494B| 0 0 |2052 1194 15 16 64 0 0 4: 15 20 62 0 0 3: 15 18 63 0 0 4| 0 0 | 300B 738B| 0 0 |1504 874
こんな感じ。
今度はカーネルでの処理が発生するためにcpu使用率がcpu0とcpu1で処理されていることがわかった。
つまり
プログラム実行
↓
cpu0でループ開始
↓
システムコール発行
↓
プロセススケジューラによってcpu1の空きが多いことを確認、cpu1を使ってカーネルでの処理
↓
ユーザ空間へ(プロセススケジューラでcpu0とcpu1の空き状況を確認して割り当て)
って感じ。
プログラム上はシングルプロセスしか使ってないように見えても実はlinux側でいい感じにマルチコアを割り当ててくれています。
ただしこんな頻度でユーザーモードとカーネルモード間のコンテキストスイッチを発生させるプログラムがあるかと言われるとないかなって思いますが。。w
(dstatをみてもなんというコンテキストの数がとんでもないことがわかりますねーw)
ちなみにコンテキストスイッチの時に発生するレジスタ退避はカーネルマクロのインラインアセンブリで記述されている。
#define switch_to(prev,next,last) do { \ unsigned long esi,edi; \ asm volatile("pushfl\n\t" \ ――<3> "pushl %%ebp\n\t" \ ――<4> "movl %%esp,%0\n\t" \ ――<5> "movl %5,%%esp\n\t" \ ――<6> "movl $1f,%1\n\t" \ ――<7> "pushl %6\n\t" \ ――<8> "jmp __switch_to\n" \ ――<9> "1:\t" \ ――<10> "popl %%ebp\n\t" \ ――<11> "popfl" \ ――<12> :"=m" (prev->thread.esp),"=m" (prev->thread.eip), \ "=a" (last),"=S" (esi),"=D" (edi) \ :"m" (next->thread.esp),"m" (next->thread.eip), \ "2" (prev), "d" (next)); \ } while (0)
マルチプロセスのスケジューリング
ユーザ空間で動作する高負荷プロセスが他コアのアイドリングしてるコアに移らなかった理由として考えられるのはCPUがコアごとに持ってるキャッシュへのヒット率だと考えられる。
CPU1コアごとにL1,L2と共有のL3キャッシュが存在する。
引用:http://www.pasonisan.com/customnavi/z1012_cpu/05baseht.html
仮に特定のプロセスがCPUを跨いで実行されるとこのL1、L2のキャッシュが利用されなくて新たに生成するコストが必要となる。
スケジューラではこのような現象を防ぐために1プロセスはできるだけ同一のCPUで実行されるような仕組みとなっている(実装は読んでません><)