概要
プロセス間の同期をpthread
とmutex
を使った方法から,System Vのセマフォに切り替える必要があったため,調べたり動作検証したことをまとめておく.以前,C++とpthread
を使ってRead/Writeパターンを書いたことがあった.これと同じようなことをSystem Vのセマフォーで行う,という内容になっている.
プロセス間の同期
プロセス間で共有リソースを扱う際,書き込みと読み込みが同時に起こると不整合を起こしてしまう.ここでの不整合については詳しく述べないが,複数プロセスの書き書き問題,読み書き問題を解決することが目的となる.このような目的を達成する仕組みとして,言語(とかライブラリ)が用意しているロック・アンロックを使ったり,pthread
のmutex
を使ったり,今回紹介するセマフォを使ったりする.なお,mutex
とセマフォの違いが曖昧だったが,ざっくりいうとセマフォはmutex
でできることを内包する.よって,mutex
を使ってできることは,セマフォを使ってできる,ということになる.
主要API
セマフォを使う上で重要なのが以下3つのAPI
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
ここで,semctl
とsemop
は両方ともセマフォの操作ができるので紛らわしいが,後述するように,"ロックを取れなかったらスリープ",のような操作はsemop
でしかできず,逆に"スリープしているプロセスを起こす",みたいな操作は両方でできる.このことを理解するには,セマフォの詳細な理解が必要なので,ここでは割愛するが,他のロック機構(e.g., pthread_mutex_lock
)のように,ロックやアンロックを行うAPIが用意されているわけではなく,セマフォの値を操作することでロックやアンロックを行うことが他の排他制御のAPIと大きく異なる(これが正しく理解できないとわけがわからなくなる).
semget
semgetは,セマフォーを生成するAPIである.セマフォーは集合として生成され,生成時に個数を指定する.
int semid = semget(key, n, 0666|IPC_CREATE)
例えば上記の呼び出しでは,n個のセマフォーの集合を生成しており,その識別子としてsemid
が返される.そして,各セマフォはインデックスによって識別され,1つのセマフォは4つの値によって構成される.以下,イメージ図
各データ(変数)の意味は以下の通り
変数名 | 意味 |
---|---|
semval | セマフォーの値 |
sempid | 最終操作のプロセス ID |
semnct | semval が現行値より大きくなるのを待っているプロセスの数 |
semznct | semval がゼロになるのを待っているプロセスの数 |
この中で重要なのがsemval
で,semctl
,semop
両方から操作できる.それ以外の値はsemctl
を通じて読み書きできる.
semctl
semctlはセマフォーの各値(semval, sempidなど)を読み書きなどする時に利用する.semctlは可変引数で,第4引数は実行したい処理に依存して使用する.第4引数に指定するのは以下の共用体
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
} arg;
また,実行結果の返り値も,どんな処理をするかによって引数で返されたり,共用体のメンバーの変更によって行う.詳しくはこちらを参照.例えば,最も使用するであろうsemval
の読み書きは次のように行う
int current_val = semctl(semid, 0, GETVAL);
このコードは,semid
が指すセマフォ集合のなかで,0番目のセマフォのsemval
を取得し,current_val
に代入する.semctl
の第二引数で,セマフォ集合の何番目のセマフォなのかを指定する.
また,semval
への書き込みは
union semun arg;
arg.val=1;
semctl(semid, 0, SETVAL, arg)
このコードは,semid
が指すセマフォ集合のなかで,0番目のセマフォのsemval
に1(arg.val=1)を設定する.
semop
semopは,セマフォーのsemvalを操作し(正確には,しようとし),その結果によって現在のプロセスを
- そのまま実行を続ける(場合によってはスリープしているプロセスを起こす)
- スリープする
という機能を提供する.セマフォーではこのsemopを使ってロック機構を実装する.
また,semopは,複数のセマフォーに対してアトミックに一気に操作することもできる.これによって,複数のプロセスの協調動作みたいなものも実現できる(実際にはそれほど複雑なことはやってないので,具体的な説明はできません)
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
ここで,第二引数のsembufは,
struct sembuf {
unsigned short sem_num; //操作するセマフォのインデックス
short sem_op; //操作
short sem_flg; //フラグ
};
sem_numは操作するセマフォーの番号,sem_opは指定したセマフォーのsemvalに対する操作(後述).sem_flgはエラー時の処理などを指定するためのフラグ(本稿では割愛),である.semopの詳細はこちらへ.
semopの定義を見ると,第二引数がポインタになっていることがわかるが,複数の操作をアトミックに行いたければ,struct sembufを配列とし,その先頭ポインタを第二引数に,実行する命令数を第3引数にしていすれば,指定した命令数をアトミックに実行してくれる.
sem_opによる操作
sem_opでの操作は,基本的にsem_opに指定した値と,セマフォーとの値の比較によって3つに分類される.ここがとても混乱する(実際には,フラグ指定によって意味が振る舞いが変わってくるが,わかりやすさのためにフラグは指定されないものとする)
- sem_op > 0: 現在のsemvalにsem_opの値を加算する(代入ではなく加算).実行したプロセスはそのまま実行を続ける.
- sem_op == 0: セマフォーのsemvalが0であれば,そのまま実行を続ける.この場合,semvalの値は変更されない(当たり前だが).semvalが0以外であれば,プロセスはスリープし.semzcntをインクリメントした後,semvalが0になるのを待つ(他のプロセスが当該セマフォーのsemvalを0にした時に起きる)
- sem_op < 0: semval >= |sem_op|ならば,即座にsemvalからsem_opを減算し,実行したプロセスはそのまま実行を続ける.semval < |sem_op|ならば,semncntをインクリメントし,実行プロセスはスリープする
わかりやすく,1.は必ず処理が実行され,プロセスもスリープしない.ということは1のような操作は他のプロセスを起こすために使う.
次に,2.は,semvalが0以外ならばプロセスをスリープし,semvalは変化させない.ということは,何かのイベントを待つために使える?
最後に,3.はロックをとるために使える.sem_op<0なので,semval>=|sem_op|ということは.要するに計算結果(semval+sem_op)>=0ということになる.この場合,プロセスは計算を継続できるのでスリープしない.一方,計算結果(semval+sem_op)<0となる場合,プロセスはスリープする.これを利用し,以下の図のように例えばsemval=1としておき,2つのプロセスP1, P2がsem_op=-1としてt1で処理を実行したとする.semopはアトミックに実行されるので,仮にP1のsemop命令が先に実行されると,P1はそのまま処理を継続できるが,P2はsemval=0になったあとにsem_op==-1を実行しようとするので,スリープする.これはすなわちロックの獲得,ということになる.
P1がロックを獲得後クリティカルセクションを実行し,t2でsem_op=1として処理を実行する.sem_op=1>0なので,この処理は必ず実行できる(1.のケース).その結果,semval=1となり,現行値である0よりも大きくなるのでP2が起こされる.その後,P2はsem_op=-1が実行できるので,ロックを獲得し,何らかの処理を継続する.
以上のように,semvalの値とsem_opの値を利用することで,自由にロック・アンロックをコントロールすることが可能になる.
C言語で動作確認
動作原理がわかったところで,実際に振る舞いを確かめてみる.ただ,プロセスのロックやアンロックは確認が難しいので,こちらを参考に,getchar
を使ってあえて入力待ちをつくり,目視で確認しやすくした.
動作確認用プログラムのまとめ
ファイル名 | 機能 |
---|---|
seminit | セマフォを作成する.今回は3つ作る |
semrm | セマフォを削除する |
semview | 引数で指定したセマフォのsemvalを表示する |
semproc | ロックを取って入力待ちし,入力を受け取ったらロックを開放する |
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
key_t key;
int semid;
union semun arg;
/* ファイル名からユニークなキーを作る(ftok) */
if ((key = ftok("sem.dat", 'S')) == -1) {
perror("ftok");
exit(1);
}
printf("key = 0x%x\n", key);
/* セマフォを3つ作る */
if ((semid = semget(key, 3, 0666 | IPC_CREAT)) == -1) {
perror("semget");
exit(1);
}
/* 3つのセマフォのsemvalを1で初期化 */
arg.val = 1;
if (semctl(semid, 0, SETVAL, arg) == -1) {
perror("semctl");
exit(1);
}
if (semctl(semid, 1, SETVAL, arg) == -1) {
perror("semctl");
exit(1);
}
if (semctl(semid, 2, SETVAL, arg) == -1) {
perror("semctl");
exit(1);
}
return 0;
}
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main()
{
key_t key;
int semid;
union semun arg;
if ((key = ftok("sem.dat", 'S')) == -1) {
perror("ftok");
exit(1);
}
/* grab the semaphore set created by seminit */
if ((semid = semget(key, 1, 0)) == -1) {
perror("semget");
exit(1);
}
/* remove */
arg.val = 1;
if (semctl(semid, 0, IPC_RMID) == -1) {
perror("semctl");
exit(1);
}
return 0;
}
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
int main(int argc, char** argv) {
key_t key;
int semID;
struct sembuf sop;
union semun arg;
sop.sem_num = 0; // Semaphore number
sop.sem_op = -1; // Semaphore operation is Lock
sop.sem_flg = 0; // Operation flag
// Create key
if ((key = ftok("sem.dat", 'S')) == -1) {
perror("ftok");
exit(-1);
}
// Get created semaphore
if ((semID = semget(key, 1, 0)) == -1) {
perror("semget");
exit(-2);
}
printf("Be going to lock--- -\n");
// 各種値の取得と表示
int zero_count = semctl(semID, 0, GETZCNT);
printf("zero count = %d\n", zero_count);
int val = semctl(semID, 0, GETVAL);
printf("val = %d\n", val);
int semncnt = semctl(semID, 0, GETNCNT);
printf("semncnt = %d\n", semncnt);
// Try to lock semaphore
if (semop(semID, &sop, 1) == -1) {
perror("semop()");
if(errno == EIDRM) {
printf("errno=EIDRM, ERRNO=%d\n", errno);
printf(" I'm going to CANCEL this procedure.\n");
return(1);
}
else {
exit(-3);
}
}
printf("--Locked!\n\n");
// You are proccesing code.
printf("Press return --> Unclock, and quit self\n");
getchar();
// Try to release semaphore
sop.sem_op = 1;
if (semop(semID, &sop, 1) == -1) {
perror("semop");
exit(1);
}
printf("--Unlocked\n");
return 0;
}
実行してみる
コンソールを3つ用意し,seminit
を実行後,別のコンソールでsemproc
を実行
画面左でセマフォを初期化し,画面中央ではロックが取得できて,ユーザからの入力待ちであることがわかる.この状態で,画面右でさらにsemproc
を実行する
画面右ではロックを取得しようとするが,取得できずにスリープしている.また,val=0
と表示されている通り,セマフォの値(semval)が0に対し,sem_op=-1を試みたためスリープしていることもわかる.
この状態で,画面中央でリターンキーを押す.
すると,画面中央は動作を再開し,ロックを開放してプログラムを終了する.一方画面右は,画面中央でロックを開放したためロックを取得でき,入力待ちになっていることがわかる.
この状態で,画面左でsemview 0
を実行し,1番目のセマフォーの値を確認してみる
すると,"0"となっており,画面中央がロックを開放するとそれが画面右のプロセスに通知され,即座にロックを獲得した結果,セマフォーの値は0になっていることがわかる.
最後に,画面右でリターンキーを押し,その後画面左でセマフォーの値を確認してみる
画面右でロックが開放され,セマフォーの値は初期値の1に戻っている.こうすることで,ロック・アンロックを実現する.なお,semproc
の実行を増やしても,1度にロックを取れるプロセスは1つだけであることに変わりはない.
引き続きテスト
続いて,ここにあるような,スレッドを使った条件待ちスリープと通知(ただし今回はpthread_cond_wait/pthread_cond_broadcast)もやってみる.ここでのポイントは
- プロセス(P1)はロックを獲得する
- P1は,ある条件が成立するまでスリープするが,スリープの直前でロックを開放する
- P1はスリープする
- 別のプロセス(P2)が条件の成立を確認し,ロックを獲得する
- P2はある条件を待っているプロセスを全員を起こす.
- P2はロックを開放する
- 条件を待っているプロセスはP2によって全員起こされる
- 起こされたプロセス(P1)はロックを獲得する
- 何らかの処理を実行する
- P1はロックを開放する
以下,P1以外にP2によって起こされたプロセスがいる場合は,順番にロックを取得して処理する
pthreadを使って書くとこんな感じ
#include <stdio.h>
#include <stdlib.h>
#include <err.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex;
pthread_cond_t cond;
void f1() {
printf("start f1\n");
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
for (int i = 0; i < 3; ++i) {
printf("doing f1...\n");
}
pthread_mutex_unlock(&mutex); // コメントアウトするとロックを開放せずf2が実行を再開できないかも
printf("end f1\n");
}
void f2() {
printf("start f2\n");
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
for (int i = 0; i < 3; ++i) {
printf("doing f2...\n");
}
pthread_mutex_unlock(&mutex); // コメントアウトするとロックを開放せずf1が実行を再開できないかも
printf("end f2\n");
}
int main(void) {
pthread_t threads[2];
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
int ret1 = pthread_create(&threads[0],NULL,(void *)f1,NULL);
int ret2 = pthread_create(&threads[1],NULL,(void *)f2,NULL);
for (int i = 0; i < 5; ++i) {
printf("sleep...\n");
sleep(1);
}
pthread_mutex_lock(&mutex);
pthread_cond_broadcast(&cond); // これを実行しなかったらf1, f2ともに起きない.反対に,実行すれば必ず全員起きるので,f1, f2も(いずれ)起こされて処理が再開される
//pthread_cond_signal(&cond); // こちらを実行すると,f1かf2どちらかしか起きない.両者を実行するためには,f1, f2の中でさらにpthread_cond_signalを実行すべき
pthread_mutex_unlock(&mutex);
for (int i = 0; i < 2; ++i) {
printf("join %d\n", i);
pthread_join(threads[i], NULL);
}
printf("end of main\n");
}
この例では,f1とf2を別々のスレッドが実行し,両者条件付きロックでスリープさせてあと,mainでロックを解除(pthread_cond_broadcast)することで順番に処理を再開する.
こんな感じ
このプログラムでは,pthreadでf1とf2を並列実行しているが,各スレッドの中でpthread_cond_waitによって待ち状態なり,mutexのロックを開放してsleep状態になる.そのため,実行結果で"start f1" あるいは"start f2"と表示されたあと,スレッドの中で表示する"doing ..."はすぐには表示されない.一方,mainは5回"sleep..."を表示したあと,pthread_cond_broadcast
を呼び出すことによって,cond
で待ち状態になっていたすべてのスレッド(f1とf2)を起こす.その結果,f1とf2が起こされ,f1あるいはf2いずれかがロックを獲得し処理を実行するため,"doing f1/f2..."が表示される.そして,処理が終わると'mutex'を開放するので,f1あるいはf2のうち,先ほどロックが取れなかった方(この実行ログの場合にはf2)がロックを取り,処理を再開する,となっている.なお,コメントにあるように,pthread_cond_broadcast
ではなく,pthread_cond_signal
を使うと,待ち状態のスレッドのいずれか1つしか起こさないので,いつまで経ってもpthread_cond_signal
によって起こされたスレッド以外は起きない.よって,各スレッドの中で明示的にpthread_cond_xxx
を実行してあげないといけない(が,だったらbroadcastで事足りると思うのだが...)
これと同等のことをセマフォーを使って書いてみる
動作確認プログラムのまとめ
ファイル名 | 機能 |
---|---|
seminit | セマフォを作成する.今回は3つ作る |
semrm | セマフォを削除する |
semview | 引数で指定したセマフォのsemvalを表示する |
condlock | ロックを取ったあと,アンロックしつつスリープし,条件を待つ |
condrelease | ロックを取ったあと,条件を待つプロセスを全員起こしてアンロックする |
// condition lock test
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
void get_lock(int semID) {
struct sembuf sop;
sop.sem_num = 0; // Semaphore number
sop.sem_op = -1; // Semaphore operation is Lock
sop.sem_flg = 0;
if (semop(semID, &sop, 1) == -1) {
perror("semop()");
if(errno == EIDRM) {
printf("errno=EIDRM, ERRNO=%d\n", errno);
printf(" I'm going to CANCEL this procedure.\n");
return;
}
else {
exit(-3);
}
}
printf("--Locked!\n\n");
}
void cond_lock(int semID) {
struct sembuf sop[3];
sop[0].sem_num = 0; // Release Lock
sop[0].sem_op = 1; // Semaphore operation is Lock
sop[0].sem_flg = 0;
sop[1].sem_num = 1; //
sop[1].sem_op = 0; // condition Lock
sop[1].sem_flg = 0;
sop[2].sem_num = 0; // Release Lock
sop[2].sem_op = -1; // Semaphore operation is Lock
sop[2].sem_flg = 0;
if (semop(semID, &sop[0], 1) == -1) { // release lock
perror("semop(1)");
}
printf("--Release Lock and cond Lock!\n\n"); // condition sleep
if (semop(semID, &sop[1], 1) == -1) {
perror("semop(2)");
}
printf("--Try Lock \n\n"); // condition sleep
if (semop(semID, &sop[2], 1) == -1) {
perror("semop(3)");
}
}
void release_lock(int semID) {
struct sembuf sop;
sop.sem_num = 0; // Semaphore number
sop.sem_op = 1; // Semaphore operation is Lock
sop.sem_flg = 0;
if (semop(semID, &sop, 1) == -1) {
perror("semop");
exit(1);
}
union semun arg;
arg.val = 1;
if (semctl(semID, 1, SETVAL, arg) == -1) { // set condition to 1
perror("semctl");
exit(1);
}
printf("--Unlocked\n");
}
int main(int argc, char** argv) {
key_t key;
int semID;
struct sembuf sop;
// Create key
if ((key = ftok("sem.dat", 'S')) == -1) {
perror("ftok");
exit(-1);
}
// Get created semaphore
if ((semID = semget(key, 2, 0)) == -1) {
perror("semget");
exit(-2);
}
get_lock(semID);
printf("Press return --> Unclock, and quit self\n");
getchar();
cond_lock(semID);
printf("cond lock released\n");
release_lock(semID);
}
// condition lock test
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
void get_lock(int semID) {
struct sembuf sop;
sop.sem_num = 0; // Semaphore number
sop.sem_op = -1; // Semaphore operation is Lock
sop.sem_flg = 0;
//sop[1].sem_num = 1;
//sop[1].sem_op = 0;
//sop[1].sem_flg = 0;
if (semop(semID, &sop, 1) == -1) {
perror("semop()");
if(errno == EIDRM) {
printf("errno=EIDRM, ERRNO=%d\n", errno);
printf(" I'm going to CANCEL this procedure.\n");
return;
}
else {
exit(-3);
}
}
printf("--Locked!\n\n");
}
void cond_release(int semID) {
union semun arg;
arg.val = 0;
if (semctl(semID, 1, SETVAL, arg) == -1) { // notify
perror("semctl");
exit(1);
}
printf("notify\n");
}
void release_lock(int semID) {
struct sembuf sop;
sop.sem_num = 0; // Semaphore number
sop.sem_op = 1; // Semaphore operation is Lock
sop.sem_flg = 0;
if (semop(semID, &sop, 1) == -1) {
perror("semop");
exit(1);
}
printf("--Unlocked\n");
}
int main(int argc, char** argv) {
key_t key;
int semID;
struct sembuf sop;
// Create key
if ((key = ftok("sem.dat", 'S')) == -1) {
perror("ftok");
exit(-1);
}
// Get created semaphore
if ((semID = semget(key, 2, 0)) == -1) {
perror("semget");
exit(-2);
}
get_lock(semID);
printf("Press return --> Unclock, and quit self\n");
getchar();
cond_release(semID);
release_lock(semID);
}
この実装では,2つのセマフォを使っている(仮にsem1, sem2とする).sem1はロック・アンロックを取るためのセマフォ,sem2は条件付きロックを表すセマフォ.この2つのセマフォは初期状態でセマフォーの値を1とする(semval=1).
まず,大事なのはcondlock.c
のcond_lock
関数
void cond_lock(int semID) {
struct sembuf sop[3];
sop[0].sem_num = 0; // Release Lock
sop[0].sem_op = 1; // Semaphore operation is Lock
sop[0].sem_flg = 0;
sop[1].sem_num = 1; //
sop[1].sem_op = 0; // condition Lock
sop[1].sem_flg = 0;
sop[2].sem_num = 0; // Release Lock
sop[2].sem_op = -1; // Semaphore operation is Lock
sop[2].sem_flg = 0;
if (semop(semID, &sop[0], 1) == -1) { // release lock
perror("semop(1)");
}
printf("--Release Lock and cond Lock!\n\n"); // condition sleep
if (semop(semID, &sop[1], 1) == -1) {
perror("semop(2)");
}
printf("--Try Lock \n\n"); // condition sleep
if (semop(semID, &sop[2], 1) == -1) {
perror("semop(3)");
}
}
この関数では,
- sem1のアンロック
- sem2を使ったスリープ
- sem1を使ったロック
を実行している.前提として,sem1のロックを取った後に実行されることを想定している.ここで,sem2を使ったスリープはsem_op=0を使って実現している.sem2の初期値は1なので,sem_op=0とすることで,sem2の値が0になるまで待つ.なお,ここでsemopを3回に分けて呼び出しているのは,これを1回の呼び出しで実行してしまうと,2でスリープするので1のアンロックも実行されないためである(実行して確認済み).
次に大事なのは,condrelease.c
のcond_release
関数
void cond_release(int semID) {
union semun arg;
arg.val = 0;
if (semctl(semID, 1, SETVAL, arg) == -1) { // notify
perror("semctl");
exit(1);
}
printf("notify\n");
}
前提として,この関数もsem1のロックを取った状態で呼び出される.そして,sem2の値を0にすることでpthread_cond_broadcast
の振る舞いを実現している.なお,sem2を0にするためにsemopではなくsemctlを使っているため,自身がスリープすることはなく必ず実行される.この結果,sem2でスリープしていたプロセスが起き,最初にsem1のロックを取ったプロセスが処理を継続でき,それ以外は,今度はsem1のロックを取るためにスリープすることになる.この振る舞いはまさしくpthread_cond_broadcast
であり,いつか必ずsem2の条件を待っていたプロセスは実行される.最後に,sem2の値を1に戻しておく必要があるが(そうじゃないと次の条件付きロックでスリープしなくなる),これはcond_lock.c
のrelease_lock
で行っている.
なお,pthread_cond_singlal
(条件を待っているプロセスのうち,任意の1つを起こす)はどうやるのか...1つだけ選択して起こすことは出来なさそうなので,全員起こしたあとで再びsem2を1にして,残りのプロセスを再び条件付きでスリープさせる,みたいなことをエミュレーションするしかないような..
実行結果
今回は,画面を4分割し,左から
- 各セマフォーの値を表示
- condlockを実行
- condlockを実行
- condreleaseを実行
とする.
まず,すべてのセマフォの値が1であることがわかる.
ターミナルで,それぞれのプログラムを実行する.
画面左から2番目はsem1のロックを取れて入力まちで,それ以外はsem1のロックが取れずスリープしている.続いて,画面左から2番目でリターンを押す.
画面左から2番めは,sem1のロックを開放し,sem2の条件でスリープする.その結果画面3でsem1のロックが取れて入力待ちになる.
続いて画面左から3番目でリターンを押すと,ロックを開放してsem2の条件でスリープする.その結果,画面右はsem1のロックが取れて,sem2アンロックのための入力待ちとなる
画面右でリターンを押すと,sem2の条件を待っているプロセスを起こす.その結果,画面左から2番目と3番目のプロセスが起き,どちらかが早くsem1のロックを取得する.そして,早く取ったほうがある処理(例えばsleep(1)とか)を実行し,sem1のロックを開放した結果,もう一方のプロセスがロックを取得し...のように動いている.
すべての処理が終わったあと,セマフォの値を確認してみると(画面左),初期状態に戻っていることが確認できる.
ひとまずこの辺で...
続CondLock(追記)
スレッドを使った条件付き待ちの例で,もっと良いやり方があるので追記します.
この前の実装の問題点
- 他言語のsemaphoreライブラリで機能しない
- condlock.cにおいて,
release_lock
の最後にセマフォ1のsemvalを初期値に戻すような処理をしている(何となくダサい)
特に,他言語ライブラリ(具体的にはpython)では,1つのkeyに対してセマフォーを1つしか持つことができない.また,sem_op=0
とするようなAPIが提供されていない(致命的).
そこで,2つのセマフォーを扱うためにkeyを2つ使い,さらにsemvalを減算するやり方で同様の振る舞いをするように変更しました.
動作確認プログラムのまとめ
ファイル名 | 機能 |
---|---|
seminit | 2つのキーでセマフォを2つ作成する. |
semrm | セマフォを削除する |
semview | セマフォの各種値を表示する |
condlock2 | ロックを取ったあと,アンロックしつつスリープし,条件を待つ |
condrelease2 | ロックを取ったあと,条件を待つプロセスを全員起こしてアンロックする |
まずはソースコード全体を載せます.
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
key_t key, key2;
int semid, semid2;
union semun arg;
if ((key = ftok("sem.dat", 'S')) == -1) {
perror("ftok");
exit(1);
}
if ((key2 = ftok("sem2.dat", 'S')) == -1) {
perror("ftok");
exit(1);
}
printf("key = 0x%x\n", key);
printf("key2 = 0x%x\n", key2);
/* create a semaphore set with 1 semaphore: */
if ((semid = semget(key, 3, 0666 | IPC_CREAT)) == -1) {
perror("semget");
exit(1);
}
if ((semid2 = semget(key2, 1, 0666 | IPC_CREAT)) == -1) {
perror("semget");
exit(1);
}
/* initialize semaphore #0 to 1: */
arg.val = 1;
if (semctl(semid, 0, SETVAL, arg) == -1) {
perror("semctl");
exit(1);
}
arg.val = 0;
if (semctl(semid2, 0, SETVAL, arg) == -1) {
perror("semctl");
exit(1);
}
return 0;
}
key1で作るセマフォの初期値は1, key2で作るセマフォの初期値は0にしておく
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
/*
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
*/
int main()
{
key_t key, key2;
int semid, semid2;
union semun arg;
if ((key = ftok("sem.dat", 'S')) == -1) {
perror("ftok");
exit(1);
}
if ((key2 = ftok("sem2.dat", 'S')) == -1) {
perror("ftok");
exit(1);
}
/* grab the semaphore set created by seminit */
if ((semid = semget(key, 1, 0)) == -1) {
perror("semget");
exit(1);
}
if ((semid2 = semget(key2, 1, 0)) == -1) {
perror("semget");
exit(1);
}
/* remove */
if (semctl(semid, 0, IPC_RMID) == -1) {
perror("semctl");
exit(1);
}
if (semctl(semid2, 0, IPC_RMID) == -1) {
perror("semctl");
exit(1);
}
return 0;
}
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
int main(int argc, char** argv) {
key_t key, key2;
int semID, semID2;
struct sembuf sop;
union semun arg;
int snum = 0;
if (argc > 1) {
snum = argv[1][0] - '0';
}
// Create key
if ((key = ftok("sem.dat", 'S')) == -1) {
perror("ftok");
exit(-1);
}
if ((key2 = ftok("sem2.dat", 'S')) == -1) {
perror("ftok");
exit(-1);
}
// Get created semaphore 1
if ((semID = semget(key, 1, 0)) == -1) {
perror("semget");
exit(-2);
}
// Get created semaphore 2
if ((semID2 = semget(key2, 1, 0)) == -1) {
perror("semget");
exit(-2);
}
printf("--- For Sem1 ---\n");
printf("semnum = %d\n", snum);
printf("semval = %d\n", semctl(semID, snum, GETVAL));
printf("sempid = %d\n", semctl(semID, snum, GETPID));
printf("semncnt = %d\n", semctl(semID, snum, GETNCNT));
printf("semznct = %d\n", semctl(semID, snum, GETZCNT));
printf("--- For Sem2 ---\n");
printf("semval = %d\n", semctl(semID2, snum, GETVAL));
printf("sempid = %d\n", semctl(semID2, snum, GETPID));
printf("semncnt = %d\n", semctl(semID2, snum, GETNCNT));
printf("semznct = %d\n", semctl(semID2, snum, GETZCNT));
}
// condition lock test
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#define LOCK -1
#define UNLOCK 1
void get_lock(int semID) {
struct sembuf sop;
sop.sem_num = 0; // Semaphore number
sop.sem_op = LOCK; // Semaphore operation is Lock
sop.sem_flg = 0;
if (semop(semID, &sop, 1) == -1) {
perror("semop()");
if(errno == EIDRM) {
printf("errno=EIDRM, ERRNO=%d\n", errno);
printf(" I'm going to CANCEL this procedure.\n");
return;
}
else {
exit(-3);
}
}
printf("--Locked!\n\n");
}
void cond_lock(int lock_id, int cond_id) {
printf("cond lock invoked\n");
struct sembuf sop[3];
// unlock lock semaphore
sop[0].sem_num = 0;
sop[0].sem_op = UNLOCK;
sop[0].sem_flg = 0;
sop[1].sem_num = 0;
sop[1].sem_op = LOCK;
sop[1].sem_flg = 0;
sop[2].sem_num = 0;
sop[2].sem_op = LOCK;
sop[2].sem_flg = 0;
if (semop(lock_id, &sop[0], 1) == -1) { // release lock semaphore
perror("semop unlock");
}
printf("--Release Lock and cond Lock!\n\n"); // condition sleep
if (semop(cond_id, &sop[1], 1) == -1) {
perror("semop cond lock");
}
printf("--Try Lock \n\n"); // condition sleep
if (semop(lock_id, &sop[2], 1) == -1) {
perror("semop lock");
}
}
void release_lock(int semID) {
struct sembuf sop;
sop.sem_num = 0; // Semaphore number
sop.sem_op = 1; // Semaphore operation is Release
sop.sem_flg = 0;
if (semop(semID, &sop, 1) == -1) {
perror("semop");
exit(1);
}
printf("--Unlocked\n");
}
void init_semval(int semID) {
union semun arg;
arg.val = 0;
if (semctl(semID, 0, SETVAL, arg) == -1) {
perror("init error");
exit(1);
}
printf("init semval\n");
}
int main(int argc, char** argv) {
printf("condlock2\n");
key_t key1, key2;
int semId1, semId2;
struct sembuf sop;
// Create key
if ((key1 = ftok("sem.dat", 'S')) == -1) {
perror("ftok");
exit(-1);
}
if ((key2 = ftok("sem2.dat", 'S')) == -1) {
perror("ftok");
exit(-1);
}
// Get created semaphore
if ((semId1 = semget(key1, 1, 0)) == -1) {
perror("semget");
exit(-2);
}
if ((semId2 = semget(key2, 1, 0)) == -1) {
perror("semget");
return (-2);
}
get_lock(semId1);
printf("Press return --> Unclock, and quit self\n");
getchar();
cond_lock(semId1, semId2);
printf("do something!\n");
release_lock(semId1);
}
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#define LOCK -1
#define UNLOCK 1
void get_lock(int semID) {
struct sembuf sop;
sop.sem_num = 0; // Semaphore number
sop.sem_op = LOCK; // Semaphore operation is Lock
sop.sem_flg = 0;
if (semop(semID, &sop, 1) == -1) {
perror("semop()");
if(errno == EIDRM) {
printf("errno=EIDRM, ERRNO=%d\n", errno);
printf(" I'm going to CANCEL this procedure.\n");
return;
}
else {
exit(-3);
}
}
printf("--Locked!\n\n");
}
void release_lock(int semID) {
struct sembuf sop;
sop.sem_num = 0; // Semaphore number
sop.sem_op = 1; // Semaphore operation is UNLOCK
sop.sem_flg = 0;
if (semop(semID, &sop, 1) == -1) {
perror("semop");
exit(1);
}
printf("--Unlocked\n");
}
void release_cond(int semID) {
int semncnt = semctl(semID, 0, GETNCNT);
printf("semncnt = %d\n", semncnt);
struct sembuf sop;
sop.sem_num = 0;
sop.sem_op = semncnt;
sop.sem_flg = 0;
if (semop(semID, &sop, 1) == -1) {
perror("semop");
}
}
int main(int argc, char** argv) {
printf("cond release\n");
key_t key1, key2;
int semId1, semId2;
// Create key
if ((key1 = ftok("sem.dat", 'S')) == -1) {
perror("ftok");
exit(-1);
}
if ((key2 = ftok("sem2.dat", 'S')) == -1) {
perror("ftok");
exit(-1);
}
// Get created semaphore
if ((semId1 = semget(key1, 1, 0)) == -1) {
perror("semget");
exit(-2);
}
if ((semId2 = semget(key2, 1, 0)) == -1) {
perror("semget");
return (-2);
}
get_lock(semId1);
printf("Press return -> notify all threads\n");
getchar();
release_cond(semId2);
release_lock(semId1);
return 0;
}
まず,条件付きロックを行うためには2つのセマフォーが必要で,このプログラムではkey1, key2でそれぞれセマフォーを作成し,key1のセマフォーはロックを取るため,key2のセマフォーは条件付き待ち合わせのために利用する.今回は(そして多くの場合)ロックをとれるプロセスは1つなので,key1のsemvalは1で初期化,一方,条件の成立まで休止するためにkey2のsemvalは0で初期化する(seminit.c
参照).
ロックを取るには,key1のsemvalを-1すればよいので,以下のコードで実現できる
void get_lock(int semID) {
struct sembuf sop;
sop.sem_num = 0; // Semaphore number
sop.sem_op = LOCK; // Semaphore operation is Lock
sop.sem_flg = 0;
if (semop(semID, &sop, 1) == -1) { //---(*)
perror("semop()");
if(errno == EIDRM) {
printf("errno=EIDRM, ERRNO=%d\n", errno);
printf(" I'm going to CANCEL this procedure.\n");
return;
}
else {
exit(-3);
}
}
printf("--Locked!\n\n");
}
ロックが取れれば"--Locked"と出力されて関数が返り,取れなければ(*)の部分でプロセスはスリープする.
一方,ロックの解放は以下のコードで実現している
void release_lock(int semID) {
struct sembuf sop;
sop.sem_num = 0; // Semaphore number
sop.sem_op = 1; // Semaphore operation is Lock
sop.sem_flg = 0;
if (semop(semID, &sop, 1) == -1) {
perror("semop");
exit(1);
}
printf("--Unlocked\n");
}
sem_op = 1
> 0
なので,semopの呼び出しでプロセスがスリープすることはない.
条件付きロックは以下のコードで実現している
void cond_lock(int lock_id, int cond_id) {
printf("cond lock invoked\n");
struct sembuf sop[3];
// unlock lock semaphore
sop[0].sem_num = 0;
sop[0].sem_op = UNLOCK;
sop[0].sem_flg = 0;
sop[1].sem_num = 0;
sop[1].sem_op = LOCK;
sop[1].sem_flg = 0;
sop[2].sem_num = 0;
sop[2].sem_op = LOCK;
sop[2].sem_flg = 0;
if (semop(lock_id, &sop[0], 1) == -1) { // release lock semaphore (1)
perror("semop unlock");
}
printf("--Release Lock and cond Lock!\n\n"); // condition sleep (2)
if (semop(cond_id, &sop[1], 1) == -1) {
perror("semop cond lock");
}
printf("--Try Lock \n\n");
if (semop(lock_id, &sop[2], 1) == -1) { // lock (3)
perror("semop lock");
}
}
ここで,lock_id
はkey1のセマフォーを利用するためのsemid, cond_id
はkey2のセマフォーを利用するためのsemidである.前提として,この関数はkey1のロックを取った状態で呼び出される.そして,(1)でkey1のロックを開放し,(2)でkey2のロックを取ろうとする.しかし,key2のsemvalの初期値は0なのでロックは取れず,必ずスリープする.そして,何らかの方法でプロセスが起こされると(後述),(3)で再びkey1のロックを取ろうとする.このときロックが取れればこの関数は返り,取れなければ(3)で再びスリープする.
この関数は以下のように使うことを想定している
get_lock(semId1); ------------>(1)
printf("Press return --> Unclock, and quit self\n");
getchar();
cond_lock(semId1, semId2); --->(2)
printf("do something!\n");
release_lock(semId1);--------->(3)
(1)でロックを獲得し,(2)でcond_lockを呼び出してスリープさせる.実際にはif
など使って,条件によってスリープさせるか否か処理を分岐する.スリープした場合,何らかのイベントで起きるので,起きた後ロックが獲得できていれば何らかの継続処理(printfのような)を実行し,最後に(3)でロックを開放する.
次に,cond_lockでスリープしたプロセスは,以下のように起している
void release_cond(int semID) {
int semncnt = semctl(semID, 0, GETNCNT); --->(1)
printf("semncnt = %d\n", semncnt);
struct sembuf sop;
sop.sem_num = 0;
sop.sem_op = semncnt;
sop.sem_flg = 0;
if (semop(semID, &sop, 1) == -1) { --->(2)
perror("semop");
}
}
この関数では,(1)でセマフォーのsemncntを取得してる.この値は,"semval が現行値より大きくなるのを待っているプロセスの数"という定義だが,ここの例では先のcond_lock
を呼び出してスリープしているプロセスの数,である.そして,その数は必ず>0なので,(2)のsemop
の呼び出しはスリープせずに返ってくる.その結果,スリープしているプロセスが全部起こされる.なぜ起こされるのか,詳しくは後述.
そして,この関数は以下のように使う
get_lock(semId1); --->(1)
printf("Press return -> notify all threads\n");
getchar();
release_cond(semId2); -->(2)
release_lock(semId1); -->(3)
(1)でkey1のセマフォーのロックを獲得する.ロックを獲得した状態で(2)でkey2のセマフォーでrelease_condを呼び出す.最後に(3)でkey1のセマフォーのロックを開放する.
先のcond_lock
の最後でkey1のロックを獲得しに行っているが,この(3)でロックを開放しないと起こされたプロセスはcond_lock
の中でスリープしてしまう.
実行結果
まず,seminitを実行し,semviewで各種値を確認する.画面1をみると,各初期値がわかる.続いて,画面2と3でcondlock2を実行し,最後に画面4でcondrelease2を実行していく
画面2でcondlockを実行すると,key1のロックが獲得できる.その時のkey1のsemvalの値を見ると,0であることがわかる.この状態で画面3でcondlock2を実行する
画面3をみると分かる通り,ロックが獲得できない.そして,画面1のkey1のsemncntが1になっている.この状態で画面2でリターンキーを押す
すると,画面2ではロックを開放し,cond_lockを実行した結果,スリープする.一方,画面3ではロックが開放されたので,ロックを獲得できている.画面1を見るとkey2のsemncntが1になり,key1のsemncntは0に戻っている.
この状態で画面3でリターンキーを押す
すると,画面3も画面2と同様にkey1のロックを開放し,cond_lockを実行した結果スリープする.そして,画面1をみると,key1のsemvalが1に戻り,ロックが獲得できる状態になっていることがわかる.そして,key2のsemncntが2になり,2つのプロセスがスリープ状態になっていることがわかる.
この状態で画面4でcondrelease2を実行する
画面4ではkey1のロックを獲得でき,画面1をみるとkey1のsemvalが0になっている.この状態で画面4でリターンキーを押す
すると,スリープしていたプロセスが起こされ(画面2と3),すべてのプロセスが処理を終了する.また,画面1を見ると,各値が初期状態に戻っていることが確認できる.
なぜ起こされるのか?
こちらの解説によると,semvalがsem_opの絶対値より小さく...以下のいずれかの条件が生じるまでプロセスの実行が中断される,とある.今回の例では,待ち合わせのために使うkey2のsemvalは0なので,0以外どんな値を設定しようとしてもプロセスは中断される.そして,いずれかの条件,というものの中に,
「semval の値は、sem_op の絶対値 より大きいか等しくなります。 この場合は、指定セマフォーと関連した semncnt の値は減少させられ 、sem_op の絶対値は、semval から減算 されます。」とある.
key2のsemvalの値は減算が行われる前にプロセスはスリープするので,0から変わらず,その代わりkey2のsemncntが増加していく.そして,condrelease2では,semncntをkey2のsem_opに設定するので(この実行例では2),semval:sem_op=0:2であり,semval<sem_opなので,「semval の値は、sem_op の絶対値 より大きいか等しくなります」の条件に合致するため,起きる.そして,おきたプロセスの分だけkey2のsemncntが減少するため,最終的に0となって初期状態に戻るのだと思われる.また,semval=2を設定しているが,最終的に0になるので,-2+2=0になったものと思われる.