LoginSignup
0
1

日本一詳しいSystem V Semaphores

Last updated at Posted at 2023-12-28

概要

プロセス間の同期をpthreadmutexを使った方法から,System Vのセマフォに切り替える必要があったため,調べたり動作検証したことをまとめておく.以前,C++とpthreadを使ってRead/Writeパターンを書いたことがあった.これと同じようなことをSystem Vのセマフォーで行う,という内容になっている.

プロセス間の同期

プロセス間で共有リソースを扱う際,書き込みと読み込みが同時に起こると不整合を起こしてしまう.ここでの不整合については詳しく述べないが,複数プロセスの書き書き問題,読み書き問題を解決することが目的となる.このような目的を達成する仕組みとして,言語(とかライブラリ)が用意しているロック・アンロックを使ったり,pthreadmutexを使ったり,今回紹介するセマフォを使ったりする.なお,mutexとセマフォの違いが曖昧だったが,ざっくりいうとセマフォはmutexでできることを内包する.よって,mutexを使ってできることは,セマフォを使ってできる,ということになる.

主要API

セマフォを使う上で重要なのが以下3つのAPI

セマフォの生成
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
セマフォの制御操作(e.g., 値の読み書き,セマフォの削除)
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
セマフォの操作(e.g., ロック/アンロック)
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);

ここで,semctlsemopは両方ともセマフォの操作ができるので紛らわしいが,後述するように,"ロックを取れなかったらスリープ",のような操作はsemopでしかできず,逆に"スリープしているプロセスを起こす",みたいな操作は両方でできる.このことを理解するには,セマフォの詳細な理解が必要なので,ここでは割愛するが,他のロック機構(e.g., pthread_mutex_lock)のように,ロックやアンロックを行うAPIが用意されているわけではなく,セマフォの値を操作することでロックやアンロックを行うことが他の排他制御のAPIと大きく異なる(これが正しく理解できないとわけがわからなくなる).

semget

semgetは,セマフォーを生成するAPIである.セマフォーは集合として生成され,生成時に個数を指定する.

int semid = semget(key, n, 0666|IPC_CREATE)

例えば上記の呼び出しでは,n個のセマフォーの集合を生成しており,その識別子としてsemidが返される.そして,各セマフォはインデックスによって識別され,1つのセマフォは4つの値によって構成される.以下,イメージ図
Artboard 1-100.jpg
各データ(変数)の意味は以下の通り

変数名 意味
semval セマフォーの値
sempid 最終操作のプロセス ID
semnct semval が現行値より大きくなるのを待っているプロセスの数
semznct semval がゼロになるのを待っているプロセスの数

この中で重要なのがsemvalで,semctlsemop両方から操作できる.それ以外の値は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は,複数のセマフォーに対してアトミックに一気に操作することもできる.これによって,複数のプロセスの協調動作みたいなものも実現できる(実際にはそれほど複雑なことはやってないので,具体的な説明はできません)

[再掲]セマフォの操作(e.g., ロック/アンロック
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);

ここで,第二引数のsembufは,

sem.h
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つに分類される.ここがとても混乱する(実際には,フラグ指定によって意味が振る舞いが変わってくるが,わかりやすさのためにフラグは指定されないものとする)

  1. sem_op > 0: 現在のsemvalにsem_opの値を加算する(代入ではなく加算).実行したプロセスはそのまま実行を続ける.
  2. sem_op == 0: セマフォーのsemvalが0であれば,そのまま実行を続ける.この場合,semvalの値は変更されない(当たり前だが).semvalが0以外であれば,プロセスはスリープし.semzcntをインクリメントした後,semvalが0になるのを待つ(他のプロセスが当該セマフォーのsemvalを0にした時に起きる)
  3. 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を実行しようとするので,スリープする.これはすなわちロックの獲得,ということになる.
Artboard 2-100.jpg

P1がロックを獲得後クリティカルセクションを実行し,t2でsem_op=1として処理を実行する.sem_op=1>0なので,この処理は必ず実行できる(1.のケース).その結果,semval=1となり,現行値である0よりも大きくなるのでP2が起こされる.その後,P2はsem_op=-1が実行できるので,ロックを獲得し,何らかの処理を継続する.
Artboard 2_1-100.jpg

以上のように,semvalの値とsem_opの値を利用することで,自由にロック・アンロックをコントロールすることが可能になる.

C言語で動作確認

動作原理がわかったところで,実際に振る舞いを確かめてみる.ただ,プロセスのロックやアンロックは確認が難しいので,こちらを参考に,getcharを使ってあえて入力待ちをつくり,目視で確認しやすくした.

動作確認用プログラムのまとめ

ファイル名 機能
seminit セマフォを作成する.今回は3つ作る
semrm セマフォを削除する
semview 引数で指定したセマフォのsemvalを表示する
semproc ロックを取って入力待ちし,入力を受け取ったらロックを開放する
seminit.c
#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;
}
semrm.c
#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;
}
semproc.c
#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を実行
Screenshot 2023-12-27 at 13.59.43.png

画面左でセマフォを初期化し,画面中央ではロックが取得できて,ユーザからの入力待ちであることがわかる.この状態で,画面右でさらにsemprocを実行する
Screenshot 2023-12-27 at 14.04.11.png
画面右ではロックを取得しようとするが,取得できずにスリープしている.また,val=0と表示されている通り,セマフォの値(semval)が0に対し,sem_op=-1を試みたためスリープしていることもわかる.

この状態で,画面中央でリターンキーを押す.

Screenshot 2023-12-27 at 14.07.01.png
すると,画面中央は動作を再開し,ロックを開放してプログラムを終了する.一方画面右は,画面中央でロックを開放したためロックを取得でき,入力待ちになっていることがわかる.
この状態で,画面左でsemview 0を実行し,1番目のセマフォーの値を確認してみる

Screenshot 2023-12-27 at 14.09.25.png

すると,"0"となっており,画面中央がロックを開放するとそれが画面右のプロセスに通知され,即座にロックを獲得した結果,セマフォーの値は0になっていることがわかる.
最後に,画面右でリターンキーを押し,その後画面左でセマフォーの値を確認してみる
Screenshot 2023-12-27 at 14.10.58.png

画面右でロックが開放され,セマフォーの値は初期値の1に戻っている.こうすることで,ロック・アンロックを実現する.なお,semprocの実行を増やしても,1度にロックを取れるプロセスは1つだけであることに変わりはない.

引き続きテスト
続いて,ここにあるような,スレッドを使った条件待ちスリープと通知(ただし今回はpthread_cond_wait/pthread_cond_broadcast)もやってみる.ここでのポイントは

  • プロセス(P1)はロックを獲得する
  • P1は,ある条件が成立するまでスリープするが,スリープの直前でロックを開放する
  • P1はスリープする
  • 別のプロセス(P2)が条件の成立を確認し,ロックを獲得する
  • P2はある条件を待っているプロセスを全員を起こす.
  • P2はロックを開放する
  • 条件を待っているプロセスはP2によって全員起こされる
  • 起こされたプロセス(P1)はロックを獲得する
  • 何らかの処理を実行する
  • P1はロックを開放する
    以下,P1以外にP2によって起こされたプロセスがいる場合は,順番にロックを取得して処理する

pthreadを使って書くとこんな感じ

wait/broadcast

#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)することで順番に処理を再開する.
こんな感じ

Screenshot 2023-12-28 at 12.11.48.png

このプログラムでは,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 ロックを取ったあと,条件を待つプロセスを全員起こしてアンロックする
condlock.c
// 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);   
}
condrelease.c
// 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.ccond_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)");       
    }
}

この関数では,

  1. sem1のアンロック
  2. sem2を使ったスリープ
  3. sem1を使ったロック

を実行している.前提として,sem1のロックを取った後に実行されることを想定している.ここで,sem2を使ったスリープはsem_op=0を使って実現している.sem2の初期値は1なので,sem_op=0とすることで,sem2の値が0になるまで待つ.なお,ここでsemopを3回に分けて呼び出しているのは,これを1回の呼び出しで実行してしまうと,2でスリープするので1のアンロックも実行されないためである(実行して確認済み).

次に大事なのは,condrelease.ccond_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.crelease_lockで行っている.
なお,pthread_cond_singlal(条件を待っているプロセスのうち,任意の1つを起こす)はどうやるのか...1つだけ選択して起こすことは出来なさそうなので,全員起こしたあとで再びsem2を1にして,残りのプロセスを再び条件付きでスリープさせる,みたいなことをエミュレーションするしかないような..

実行結果
今回は,画面を4分割し,左から

  1. 各セマフォーの値を表示
  2. condlockを実行
  3. condlockを実行
  4. condreleaseを実行

とする.
Screenshot 2023-12-28 at 13.57.44.png
まず,すべてのセマフォの値が1であることがわかる.
ターミナルで,それぞれのプログラムを実行する.

Screenshot 2023-12-28 at 13.59.10.png
画面左から2番目はsem1のロックを取れて入力まちで,それ以外はsem1のロックが取れずスリープしている.続いて,画面左から2番目でリターンを押す.

Screenshot 2023-12-28 at 14.06.36.png

画面左から2番めは,sem1のロックを開放し,sem2の条件でスリープする.その結果画面3でsem1のロックが取れて入力待ちになる.

続いて画面左から3番目でリターンを押すと,ロックを開放してsem2の条件でスリープする.その結果,画面右はsem1のロックが取れて,sem2アンロックのための入力待ちとなる
Screenshot 2023-12-28 at 14.07.42.png

画面右でリターンを押すと,sem2の条件を待っているプロセスを起こす.その結果,画面左から2番目と3番目のプロセスが起き,どちらかが早くsem1のロックを取得する.そして,早く取ったほうがある処理(例えばsleep(1)とか)を実行し,sem1のロックを開放した結果,もう一方のプロセスがロックを取得し...のように動いている.
Screenshot 2023-12-28 at 14.11.30.png

すべての処理が終わったあと,セマフォの値を確認してみると(画面左),初期状態に戻っていることが確認できる.

ひとまずこの辺で...

続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 ロックを取ったあと,条件を待つプロセスを全員起こしてアンロックする

まずはソースコード全体を載せます.

seminit.c
#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にしておく

semrm.c
#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;
}
semview.c
#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));
}
condlock2.c
// 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);
}
condrelease2.c
#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すればよいので,以下のコードで実現できる

getlock
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"と出力されて関数が返り,取れなければ(*)の部分でプロセスはスリープする.
一方,ロックの解放は以下のコードで実現している

release_lock()
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の呼び出しでプロセスがスリープすることはない.

条件付きロックは以下のコードで実現している

cond_lock
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)で再びスリープする.

この関数は以下のように使うことを想定している

main
 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でスリープしたプロセスは,以下のように起している

release_cond(condrelease2.c)
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の呼び出しはスリープせずに返ってくる.その結果,スリープしているプロセスが全部起こされる.なぜ起こされるのか,詳しくは後述.

そして,この関数は以下のように使う

condrelease2.c
    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の中でスリープしてしまう.

実行結果

画面左から4分割し,画面1~4とする
Screenshot 2024-04-24 at 10.30.04.png

まず,seminitを実行し,semviewで各種値を確認する.画面1をみると,各初期値がわかる.続いて,画面2と3でcondlock2を実行し,最後に画面4でcondrelease2を実行していく

Screenshot 2024-04-24 at 10.38.31.png
画面2でcondlockを実行すると,key1のロックが獲得できる.その時のkey1のsemvalの値を見ると,0であることがわかる.この状態で画面3でcondlock2を実行する

Screenshot 2024-04-24 at 10.41.11.png
画面3をみると分かる通り,ロックが獲得できない.そして,画面1のkey1のsemncntが1になっている.この状態で画面2でリターンキーを押す

Screenshot 2024-04-24 at 10.43.02.png
すると,画面2ではロックを開放し,cond_lockを実行した結果,スリープする.一方,画面3ではロックが開放されたので,ロックを獲得できている.画面1を見るとkey2のsemncntが1になり,key1のsemncntは0に戻っている.
この状態で画面3でリターンキーを押す

Screenshot 2024-04-24 at 10.45.56.png
すると,画面3も画面2と同様にkey1のロックを開放し,cond_lockを実行した結果スリープする.そして,画面1をみると,key1のsemvalが1に戻り,ロックが獲得できる状態になっていることがわかる.そして,key2のsemncntが2になり,2つのプロセスがスリープ状態になっていることがわかる.
この状態で画面4でcondrelease2を実行する

Screenshot 2024-04-24 at 10.59.12.png
画面4ではkey1のロックを獲得でき,画面1をみるとkey1のsemvalが0になっている.この状態で画面4でリターンキーを押す
Screenshot 2024-04-24 at 11.00.49.png
すると,スリープしていたプロセスが起こされ(画面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になったものと思われる.

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1