GNU C semaphore sem_post 在 glibc v2.0 v2.1 之比較

前言

在翻 futex man document 的時候,不小心看到 Linux Futex的设计与实现 這篇文章。文章中有提到在執行 sem_post 的時候,雖然沒有與其他 thread 競爭,還是會用到 fuxtex system call。當然文章中有提到原因,不過看了原因,覺得這看起來很明顯的效能問題,應該會被提出來並改進吧?後來翻了一下目前的 source code ,看到這個是 GLIBC_2_0 的版本(那篇文章也很久了,所以能想見當時的版本應該蠻舊的),而現在 GLIBC_2_1 就有把這部分改進,因此就來簡單記錄一下差異在哪裡吧~

Version

glibc 2.29

GLIBC_2_0 version

 1int attribute_compat_text_section __old_sem_post (sem_t *sem)
 2{
 3  int *futex = (int *) sem;
 4  atomic_write_barrier ();
 5  (void) atomic_increment_val (futex);
 6  int err = lll_futex_wake(futex, 1, LLL_SHARED);
 7  if (__builtin_expect (err, 0) < 0)
 8    {
 9      __set_errno (-err);
10      return -1;
11    }
12  return 0;
13}

從 source code 可以看到,在將 sem value + 1 後,就直接使用 lll_futex_wake 呼叫 futex wake system call,其原因在文章內也有提到,是因為當 sem value 加 1 後,當時的機制並無法確認是否有其他 thread 正在 wait。

也因此,才會有原文提到的:

120533 futex(0xb7db1be8, FUTEX_WAIT, 20534, NULL <unfinished ...> // pthread_join
220534 futex(0x8049870, FUTEX_WAKE, 1)   = 0 // sem_post
320533 <... futex resumed> )             = 0

這樣的情形發生。

GLIBC_2_1 version

 1// 這邊假設為 64 bit processor 
 2int __new_sem_post (sem_t *sem)
 3{
 4  struct new_sem *isem = (struct new_sem *) sem;
 5  int private = isem->private;
 6  uint64_t d = atomic_load_relaxed (&isem->data);
 7  do
 8    {
 9      // SEM_VALUE_MASK = 32
10      // SEM_VALUE_MAX = 2147483647 (Maximum value the semaphore can have)
11      if ((d & SEM_VALUE_MASK) == SEM_VALUE_MAX)
12        {
13          __set_errno (EOVERFLOW);
14          return -1;
15        }
16    }
17  while (!atomic_compare_exchange_weak_release (&isem->data, &d, d + 1));
18  if ((d >> SEM_NWAITERS_SHIFT) > 0)
19    futex_wake (((unsigned int *) &isem->data) + SEM_VALUE_OFFSET, 1, private);
20  return 0;
21}

在新版可以看到,為了解決無法確認當前是否有其他 threads 正在 wait的問題, sem value 被分為兩個部分: nwaiters and value,然後使用 bit shift 來取出 nwaiters number ,只有當 nwaiters > 0 時,才會呼叫 futex_wake 了。

接著就使用原文中的實驗程式,來看一下新版的結果:

 1set_tid_address(0x7feb89cd39d0)         = 3771
 2set_robust_list(0x7feb89cd39e0, 24)     = 0
 3rt_sigaction(SIGRTMIN, {sa_handler=0x7feb898a0bd0, sa_mask=[], sa_flags=SA_RESTORER|SA_SIGINFO, sa_restorer=0x7feb898ac0e0}, NULL, 8) = 0
 4rt_sigaction(SIGRT_1, {sa_handler=0x7feb898a0c60, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART|SA_SIGINFO, sa_restorer=0x7feb898ac0e0}, NULL, 8) = 0
 5rt_sigprocmask(SIG_UNBLOCK, [RTMIN RT_1], NULL, 8) = 0
 6getrlimit(RLIMIT_STACK, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
 7mmap(NULL, 8392704, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7feb88cfb000
 8brk(NULL)                               = 0x5599ebd5e000
 9brk(0x5599ebd7f000)                     = 0x5599ebd7f000
10mprotect(0x7feb88cfb000, 4096, PROT_NONE) = 0
11clone(child_stack=0x7feb894faff0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7feb894fb9d0, tls=0x7feb894fb700, child_tidptr=0x7feb894fb9d0) = 3772
12futex(0x7feb894fb9d0, FUTEX_WAIT, 3772, NULLsem value = 0 // pthread_join
13) = 0
14exit_group(0)

可以清楚看到,除了 pthread_join 的 futex wait 之外,並沒有發現如同舊版的 futex wake 了。

結論

目前看到很多大神的 source code 分析。其實都有點年代了,建議還是要再翻一次當前版本的 source code ,才不會只學到舊觀念,而沒跟上新版本呀!

附錄

實驗 source code

 1#include <stdio.h>
 2#include <unistd.h>
 3#include <semaphore.h>
 4#include <pthread.h>
 5
 6sem_t sem_a;
 7void *task1();
 8
 9int main(void){
10  int ret=0;
11  pthread_t thrd1;
12  sem_init(&sem_a,0,1);
13  ret=pthread_create(&thrd1,NULL,task1,NULL); 
14  pthread_join(thrd1,NULL);
15}
16
17void *task1()
18{
19  int sval = 0;
20  sem_wait(&sem_a); 
21  sleep(1);
22  sem_getvalue(&sem_a,&sval);
23  printf("sem value = %d\n",sval);
24  
25  (&sem_a);
26}