IPCセマフォ(1)
IPCセマフォはユーザプロセス間で同期制御する時に使われるもので、その実装はカーネルのそれと非常に似ています。ただカーネルはアクセス制御カウンタとしてセマフォ値を1つしか有していませんが、IPCセマフォを複数値を有することができ、しかもセマフォそのものを複数で使用することも可能です。また、バグの無いことを前提とするカーネルと違って、バグが潜在的に有するユーザプロセスが使用するということで、セマフォの取り消しの機能も実装されています。
下図はIPCセマフォのデータ管理のイメージです。
semop()システムコールはtimeoutをNULLLとして、semtimedop()を呼ぶことでその処理を行っています。まずユーザプロセスから渡されて引数のチェックを行った後、ユーザプロセスからのsembufを、カーネル空間のsopsへ複写します。この時パフォーマンスを考慮して、セマフォ組がSEMOPM_FAST(64)を超える場合、kmalloc()でメモリを確保した物をそのバッファとしています。そうでないならカーネルスタックをバッファとしています。
if (timeout)はsemtimedop()の処理で、semdop()ではスキップです。次のforループでは処理するセマフォ(複数)での、sem[]インデックスの最大値、取り消し処理の必要の有無、及びセマフォカウンタ値を更新するかどうかのチェックを行います。もし、取り消し処理が必要なら、対応するstruct sem_undo *unを、sem_lock_check()でセマフォ識別子に対するstruct sem_array *smaを取得します。そしてtry_atomic_semop()をコールしています。この関数がセマフォを取得できるかどうか、取得可能なら掛かる取り消し処理を行うことになります。取得できたら0を、そうでないなら1を返します。マイナスはエラーです。取得できたら、ないしエラーならgoto out_unlock_freeで、ユーザプロセスに復帰することになります。
if (alter && error == 0)の時 update_queue()をコールしています。 update_queue()はsem_pendingでウエイトしているプロセスについて、再度セマフォが取得可能かどうか調べ、可能ならそのプロセスを起床させます。セマフォが取得でき、aleteが0出ないことはカレントプロセスにより、IPCセマフォ値が更新されたということです。従って他のウエイトしているプロセスが起床できるかもしれないと言う事です。
取得できなけえれば、struct sem_queue queueに必要な情報を設定したのち、sma->sem_pendingにリストした後、 current->state = TASK_INTERRUPTIBLEとしてschedule()で実行権を譲渡しています。
なお、sma->sem_pendingにリストするさい、alterが0でない時リストの末尾に、0の時リストの先頭に行っています。update_queue()ではsma->sem_pendingの先頭からチェックしていきます。従ってalterが0(セマフォ値を更新しない。)を先にチェックすることは理に適っているわけです。(先にセマフォ値を更新するプロセスを起床させれば、更新しないプロセスが起床できなくなる可能性があり、先に更新しないプロセス起床させることで、更新するプロセスも起床できる。)
スカラ型スーパコンピュータ京のように、たぶんマルチスレッドでお化けみたいな感じのアプリだと、たぶん我々凡人の域を超えたセマフォの使い方になると思いますし。
従ってalterを0/1とするのでなく、すべてセマフォの取得(セマフォ値の減算)時を考慮して、その時はalterを2とするなりして、update_queue()をコールしない処理とした方がいいのでは思うのですか?
下図はIPCセマフォのデータ管理のイメージです。
semop()システムコールはtimeoutをNULLLとして、semtimedop()を呼ぶことでその処理を行っています。まずユーザプロセスから渡されて引数のチェックを行った後、ユーザプロセスからのsembufを、カーネル空間のsopsへ複写します。この時パフォーマンスを考慮して、セマフォ組がSEMOPM_FAST(64)を超える場合、kmalloc()でメモリを確保した物をそのバッファとしています。そうでないならカーネルスタックをバッファとしています。
if (timeout)はsemtimedop()の処理で、semdop()ではスキップです。次のforループでは処理するセマフォ(複数)での、sem[]インデックスの最大値、取り消し処理の必要の有無、及びセマフォカウンタ値を更新するかどうかのチェックを行います。もし、取り消し処理が必要なら、対応するstruct sem_undo *unを、sem_lock_check()でセマフォ識別子に対するstruct sem_array *smaを取得します。そしてtry_atomic_semop()をコールしています。この関数がセマフォを取得できるかどうか、取得可能なら掛かる取り消し処理を行うことになります。取得できたら0を、そうでないなら1を返します。マイナスはエラーです。取得できたら、ないしエラーならgoto out_unlock_freeで、ユーザプロセスに復帰することになります。
if (alter && error == 0)の時 update_queue()をコールしています。 update_queue()はsem_pendingでウエイトしているプロセスについて、再度セマフォが取得可能かどうか調べ、可能ならそのプロセスを起床させます。セマフォが取得でき、aleteが0出ないことはカレントプロセスにより、IPCセマフォ値が更新されたということです。従って他のウエイトしているプロセスが起床できるかもしれないと言う事です。
取得できなけえれば、struct sem_queue queueに必要な情報を設定したのち、sma->sem_pendingにリストした後、 current->state = TASK_INTERRUPTIBLEとしてschedule()で実行権を譲渡しています。
なお、sma->sem_pendingにリストするさい、alterが0でない時リストの末尾に、0の時リストの先頭に行っています。update_queue()ではsma->sem_pendingの先頭からチェックしていきます。従ってalterが0(セマフォ値を更新しない。)を先にチェックすることは理に適っているわけです。(先にセマフォ値を更新するプロセスを起床させれば、更新しないプロセスが起床できなくなる可能性があり、先に更新しないプロセス起床させることで、更新するプロセスも起床できる。)
SYSCALL_DEFINE3(semop, int, semid, struct sembuf __user *, tsops, unsigned, nsops) { return sys_semtimedop(semid, tsops, nsops, NULL); } SYSCALL_DEFINE4(semtimedop, int, semid, struct sembuf __user *, tsops, unsigned, nsops, const struct timespec __user *, timeout) { int error = -EINVAL; struct sem_array *sma; struct sembuf fast_sops[SEMOPM_FAST]; struct sembuf* sops = fast_sops, *sop; struct sem_undo *un; int undos = 0, alter = 0, max; struct sem_queue queue; unsigned long jiffies_left = 0; struct ipc_namespace *ns; ns = current->nsproxy->ipc_ns; if (nsops < 1 || semid < 0) return -EINVAL; if (nsops > ns->sc_semopm) return -E2BIG; if(nsops > SEMOPM_FAST) { sops = kmalloc(sizeof(*sops)*nsops,GFP_KERNEL); if(sops==NULL) return -ENOMEM; } if (copy_from_user (sops, tsops, nsops * sizeof(*tsops))) { error=-EFAULT; goto out_free; } if (timeout) { struct timespec _timeout; if (copy_from_user(&_timeout, timeout, sizeof(*timeout))) { error = -EFAULT; goto out_free; } if (_timeout.tv_sec < 0 || _timeout.tv_nsec < 0 || _timeout.tv_nsec >= 1000000000L) { error = -EINVAL; goto out_free; } jiffies_left = timespec_to_jiffies(&_timeout); } max = 0; for (sop = sops; sop < sops + nsops; sop++) { if (sop->sem_num >= max) max = sop->sem_num; if (sop->sem_flg & SEM_UNDO) undos = 1; if (sop->sem_op != 0) alter = 1; } if (undos) { un = find_alloc_undo(ns, semid); if (IS_ERR(un)) { error = PTR_ERR(un); goto out_free; } } else un = NULL; sma = sem_lock_check(ns, semid); if (IS_ERR(sma)) { if (un) rcu_read_unlock(); error = PTR_ERR(sma); goto out_free; } error = -EIDRM; if (un) { if (un->semid == -1) { rcu_read_unlock(); goto out_unlock_free; } else { rcu_read_unlock(); } } error = -EFBIG; if (max >= sma->sem_nsems) goto out_unlock_free; error = -EACCES; if (ipcperms(&sma->sem_perm, alter ? S_IWUGO : S_IRUGO)) goto out_unlock_free; error = security_sem_semop(sma, sops, nsops, alter); if (error) goto out_unlock_free; error = try_atomic_semop (sma, sops, nsops, un, task_tgid_vnr(current)); if (error <= 0) { if (alter && error == 0) update_queue (sma); goto out_unlock_free; } queue.sops = sops; queue.nsops = nsops; queue.undo = un; queue.pid = task_tgid_vnr(current); queue.alter = alter; if (alter) list_add_tail(&queue.list, &sma->sem_pending); else list_add(&queue.list, &sma->sem_pending); queue.status = -EINTR; queue.sleeper = current; current->state = TASK_INTERRUPTIBLE; sem_unlock(sma); if (timeout) jiffies_left = schedule_timeout(jiffies_left); else schedule(); error = queue.status; while(unlikely(error == IN_WAKEUP)) { cpu_relax(); error = queue.status; } if (error != -EINTR) { goto out_free; } sma = sem_lock(ns, semid); if (IS_ERR(sma)) { error = -EIDRM; goto out_free; } error = queue.status; if (error != -EINTR) { goto out_unlock_free; } if (timeout && jiffies_left == 0) error = -EAGAIN; list_del(&queue.list); goto out_unlock_free; out_unlock_free: sem_unlock(sma); out_free: if(sops != fast_sops) kfree(sops); return error; }
備考
セマフォ値更新後の再チェックの処理としてupdate_queue()をコールする条件は、if (alter && error == 0)となっています。alterは0か1かで、セマフォ値が更新される場合1でした。しかしセマフォ更新の処理がすべて取得の場合、update_queue()をコールするのは意味が無いように思われます。スカラ型スーパコンピュータ京のように、たぶんマルチスレッドでお化けみたいな感じのアプリだと、たぶん我々凡人の域を超えたセマフォの使い方になると思いますし。
従ってalterを0/1とするのでなく、すべてセマフォの取得(セマフォ値の減算)時を考慮して、その時はalterを2とするなりして、update_queue()をコールしない処理とした方がいいのでは思うのですか?