読者です 読者をやめる 読者になる 読者になる

pthread_exit(3)を調べてみる

linux

(´-`).。oO(ふと気になったのでpthread_exit(3)を調べてみる。

まず、man 3 pthread_exitによる基本的な部分はこのような感じ。

  • pthread_exit()を呼び出したスレッドを終了する
  • cleanupハンドラでスレッド終了時に実行させたい処理を登録できる
  • スレッド終了時はatexit()で登録した関数は呼ばれない
  • プロセスの最後のスレッドが終了する時にexit(3)が呼ばれる
    • この時はatexit()で登録した関数が呼ばれる
  • mainスレッドがchildスレッドを残して終了する場合はexit(3)ではなくpthread_exit(3)を呼ぶ必要がある

その他に、proc(5)でもpthread_exit()について言及されていて、mainスレッドが終了していた場合は制限が出てくる。
例えば以下のファイルなどは参照できなくなる

  • /proc/[pid]/cwd
  • /proc/[pid]/exe

実際に試してみるためこんなプログラムを用意します。開くのは/proc/net/tcpで/proc/netは/proc/selfへのシンボリックリンクです。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

int do_open(char *name)
{
    FILE *fp;

    printf("open file %s\n", name);
    fp = fopen(name, "r");
    if (!fp)
        return -1;

    fclose(fp);
    return 0;
}

int open_by_pid(void)
{
    char buf[32] = { 0 };

    sprintf(buf, "/proc/%d/net/tcp", getpid());
    return do_open(buf);
}

int open_by_self(void)
{
    return do_open("/proc/net/tcp");
}

void *test(void *arg)
{
    printf("child thread's pid %d\n", getpid());
    sleep(1);

    if (open_by_self()) 
        printf("open_by_self failed\n");
    else
        printf("open_by_self success\n");

    if (open_by_pid()) 
        printf("open_by_pid failed\n");
    else
        printf("open_by_pid success\n");

    system("ls /proc");
    return NULL;
}

int main(int argc, char **argv)
{
    pthread_t th;

    printf("main thread's pid %d\n", getpid());
    pthread_create(&th, NULL, &test, NULL);
    pthread_exit(0);
}


上記コードをコンパイルして実行すると以下のようになり、/proc/net/tcp、/proc/[pid]/net/tcpともにファイル開くのに失敗します。/procには17775が見ているけどchildスレッドからアクセスできないんですね。

masami@saga:~$ ./a.out
main thread's pid 17775
child thread's pid 17775
open file /proc/net/tcp
open_by_self failed
open file /proc/17775/net/tcp
open_by_pid failed
1      1088   12105  133    14223  168    181  221  268  320  355  427  51   534  658  688  71   747   79   93         consoles     iomem          lockdep_stats  scsi           uptime
10     11     123    13344  14232  169    19   226  27   328  36   428  514  55   659  690  716  75    794  94         cpuinfo      ioports        locks          self           version
1040   1111   125    134    14257  17     2    228  28   329  37   43   52   56   66   691  718  76    8    97         crypto       irq            meminfo        slabinfo       vmallocinfo
1046   1119   126    135    143    170    20   23   29   33   38   45   525  57   661  693  719  763   80   9722       devices      kallsyms       misc           softirqs       vmstat
1054   1138   127    136    15     171    202  230  3    331  40   46   526  58   668  694  72   768   81   9821       diskstats    kcore          modules        stat           zoneinfo
1055   1160   128    13773  16     17129  209  234  30   335  41   461  528  60   669  696  723  77    83   acpi       dma          key-users      mounts         swaps
1057   11826  129    138    16019  17471  21   24   303  336  413  47   529  61   67   698  724  7768  838  asound     driver       kmsg           mtrr           sys
1061   119    12905  13930  163    177    210  25   304  34   414  48   53   62   674  7    729  778   84   buddyinfo  execdomains  kpagecount     net            sysrq-trigger
1062   11997  13     14     164    17775  211  26   307  348  416  489  530  621  676  70   73   78    85   bus        fb           kpageflags     pagetypeinfo   sysvipc
10642  12     130    14018  165    17777  213  260  31   35   42   490  531  622  68   701  730  781   9    cgroups    filesystems  latency_stats  partitions     timer_list
10643  12074  131    14019  166    18     219  264  318  351  420  5    532  63   684  703  733  787   90   cmdline    fs           loadavg        sched_debug    timer_stats
1083   121    132    14147  167    180    22   265  32   353  424  50   533  65   687  708  74   789   92   config.gz  interrupts   lockdep        schedstat      tty

次にこれをstraceで見てみようと思うんだけど、ファイルオープンとかの余計な処理は見る必要ないのでシンプル版で実行。

#include <unistd.h>
#include <pthread.h>
#include <unistd.h>

void *test(void *arg)
{
        sleep(1);
        return NULL;
}

int main(int argc, char **argv)
{
        pthread_t th;

        pthread_create(&th, NULL, &test, NULL);
        pthread_exit(0);
        return 0;
}

実行するとトレースは以下のように。

19988 execve("./a.out", ["./a.out"], [/* 53 vars */]) = 0
19988 brk(0)                            = 0x113a000
19988 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
19988 open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
19988 fstat(3, {st_mode=S_IFREG|0644, st_size=119644, ...}) = 0
19988 mmap(NULL, 119644, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f66c44aa000
19988 close(3)                          = 0
19988 open("/usr/lib/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
19988 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0000`\0\0\0\0\0\0"..., 832) = 832
19988 fstat(3, {st_mode=S_IFREG|0755, st_size=149301, ...}) = 0
19988 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f66c44a9000
19988 mmap(NULL, 2217104, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f66c408a000
19988 mprotect(0x7f66c40a2000, 2097152, PROT_NONE) = 0
19988 mmap(0x7f66c42a2000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x18000) = 0x7f66c42a2000
19988 mmap(0x7f66c42a4000, 13456, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f66c42a4000
19988 close(3)                          = 0
19988 open("/usr/lib/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
19988 read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\20\1\2\0\0\0\0\0"..., 832) = 832
19988 fstat(3, {st_mode=S_IFREG|0755, st_size=2047384, ...}) = 0
19988 mmap(NULL, 3858192, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f66c3cdc000
19988 mprotect(0x7f66c3e80000, 2097152, PROT_NONE) = 0
19988 mmap(0x7f66c4080000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1a4000) = 0x7f66c4080000
19988 mmap(0x7f66c4086000, 16144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f66c4086000
19988 close(3)                          = 0
19988 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f66c44a8000
19988 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f66c44a7000
19988 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f66c44a6000
19988 arch_prctl(ARCH_SET_FS, 0x7f66c44a7700) = 0
19988 mprotect(0x7f66c4080000, 16384, PROT_READ) = 0
19988 mprotect(0x7f66c42a2000, 4096, PROT_READ) = 0
19988 mprotect(0x7f66c44c8000, 4096, PROT_READ) = 0
19988 munmap(0x7f66c44aa000, 119644)    = 0
19988 set_tid_address(0x7f66c44a79d0)   = 19988
19988 set_robust_list(0x7f66c44a79e0, 24) = 0
19988 rt_sigaction(SIGRTMIN, {0x7f66c408fb10, [], SA_RESTORER|SA_SIGINFO, 0x7f66c40994b0}, NULL, 8) = 0
19988 rt_sigaction(SIGRT_1, {0x7f66c408fba0, [], SA_RESTORER|SA_RESTART|SA_SIGINFO, 0x7f66c40994b0}, NULL, 8) = 0
19988 rt_sigprocmask(SIG_UNBLOCK, [RTMIN RT_1], NULL, 8) = 0
19988 getrlimit(RLIMIT_STACK, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
19988 mmap(NULL, 8392704, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7f66c34db000
19988 brk(0)                            = 0x113a000
19988 brk(0x115b000)                    = 0x115b000
19988 mprotect(0x7f66c34db000, 4096, PROT_NONE) = 0
19988 clone(child_stack=0x7f66c3cdaff0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f66c3cdb9d0, tls=0x7f66c3cdb700, child_tidptr=0x7f66c3cdb9d0) = 19989

ここでclone()が呼ばれているのでpthread_create()の処理をしているはず。さらに見ていって、

19989 set_robust_list(0x7f66c3cdb9e0, 24 <unfinished ...>
19988 open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC <unfinished ...>
19989 <... set_robust_list resumed> )   = 0
19988 <... open resumed> )              = 3
19989 rt_sigprocmask(SIG_BLOCK, [CHLD],  <unfinished ...>
19988 fstat(3,  <unfinished ...>
19989 <... rt_sigprocmask resumed> [], 8) = 0
19988 <... fstat resumed> {st_mode=S_IFREG|0644, st_size=119644, ...}) = 0
19989 rt_sigaction(SIGCHLD, NULL,  <unfinished ...>
19988 mmap(NULL, 119644, PROT_READ, MAP_PRIVATE, 3, 0 <unfinished ...>
19989 <... rt_sigaction resumed> {SIG_DFL, [], 0}, 8) = 0
19988 <... mmap resumed> )              = 0x7f66c44aa000
19989 rt_sigprocmask(SIG_SETMASK, [],  <unfinished ...>
19988 close(3 <unfinished ...>
19989 <... rt_sigprocmask resumed> NULL, 8) = 0
19988 <... close resumed> )             = 0
19989 nanosleep({1, 0},  <unfinished ...>

ここがsleep(1)の処理。{1, 0}というのはstruct timespecのtv_secに1、tv_nsecに0で1秒のsleepを設定。

19988 open("/usr/lib/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = 3
19988 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\260*\0\0\0\0\0\0"..., 832) = 832
19988 fstat(3, {st_mode=S_IFREG|0644, st_size=90088, ...}) = 0
19988 mmap(NULL, 2185952, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f66c32c5000
19988 mprotect(0x7f66c32db000, 2093056, PROT_NONE) = 0
19988 mmap(0x7f66c34da000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x15000) = 0x7f66c34da000
19988 close(3)                          = 0
19988 munmap(0x7f66c44aa000, 119644)    = 0
19988 futex(0x7f66c34da850, FUTEX_WAKE_PRIVATE, 2147483647) = 0
19988 _exit(0)                          = ?

この_exit()はpthread_exit()かな。

19989 <... nanosleep resumed> 0x7f66c3cdad60) = 0

ここでchildスレッドがsleep(1)から復帰。

19989 exit_group(0)                     = ?
19989 +++ exited with 0 +++
19988 +++ exited with 0 +++

最後にexit()ではなくてexit_group()で終了。 なぜexit()ではなく、exit_group()なのか?というのはman exit_groupに答えが書いてあり「glibc 2.3 以降では、 exit(2) のラッパー関数が呼び出された際に、 このシステムコールが起動される。」ということらしい。

じゃあ、実際にglibcのコードでも見てみようということで、まずはpthread_exit()を。
ファイルはnptl/pthread_exit.c

void
__pthread_exit (value)
     void *value;
{
  THREAD_SETMEM (THREAD_SELF, result, value);

  __do_cancel ();
}

THREAD_SETMEM()はスレッドの戻り値をpthread_exitに渡された引数に設定するだけだと思うのでdo_cancel()を探す。 do_cancel()はnptl/pthreadP.hにあり、inline関数で書かれている

/* Called when a thread reacts on a cancellation request.  */
static inline void
__attribute ((noreturn, always_inline))
__do_cancel (void)
{
  struct pthread *self = THREAD_SELF;

  /* Make sure we get no more cancellations.  */
  THREAD_ATOMIC_BIT_SET (self, cancelhandling, EXITING_BIT);

  __pthread_unwind ((__pthread_unwind_buf_t *)
            THREAD_GETMEM (self, cleanup_jmp_buf));
}

__pthread_unwind()はnptl/unwind.cに存在。処理はHAVE_FORCED_UNWINDの定義によって変わっているので、とりあえずHAVE_FORCED_UNWINDが定義されていない場合を見てみよう。c++の例外処理に使ってそう。

void
__cleanup_fct_attribute __attribute ((noreturn))
__pthread_unwind (__pthread_unwind_buf_t *buf)
{
  struct pthread_unwind_buf *ibuf = (struct pthread_unwind_buf *) buf;
  struct pthread *self = THREAD_SELF;

#ifdef HAVE_FORCED_UNWIND
  /* This is not a catchable exception, so don't provide any details about
     the exception type.  We do need to initialize the field though.  */
  THREAD_SETMEM (self, exc.exception_class, 0);
  THREAD_SETMEM (self, exc.exception_cleanup, &unwind_cleanup);

  _Unwind_ForcedUnwind (&self->exc, unwind_stop, ibuf);
#else

ここはpthread_cleanup_push()で登録した関数を実行してるのかな。

  /* Handle the compatibility stuff first.  Execute all handlers
     registered with the old method.  We don't execute them in order,
     instead, they will run first.  */
  struct _pthread_cleanup_buffer *oldp = ibuf->priv.data.cleanup;
  struct _pthread_cleanup_buffer *curp = THREAD_GETMEM (self, cleanup);

  if (curp != oldp)
    {
      do
    {
      /* Pointer to the next element.  */
      struct _pthread_cleanup_buffer *nextp = curp->__prev;

      /* Call the handler.  */
      curp->__routine (curp->__arg);

      /* To the next.  */
      curp = nextp;
    }
      while (curp != oldp);

      /* Mark the current element as handled.  */
      THREAD_SETMEM (self, cleanup, curp);
    }

次にlongjmpしているけどこれはどこでsetjmpしているはず。

  /* We simply jump to the registered setjmp buffer.  */
  __libc_unwind_longjmp ((struct __jmp_buf_tag *) ibuf->cancel_jmp_buf, 1);
#endif
  /* NOTREACHED */

とりあえず先に進んでみると、ここは辿り着いたらダメなのでabort()で終了させる。

  /* We better do not get here.  */
  abort ();
}
hidden_def (__pthread_unwind)

先ほど飛ばしたsejmpしている箇所だけど、これはnptl/pthread_create.cのstart_thread()というところでセット。細かいところはpthread_create()の話になってくるのでこれ以上は調べませんが。

  /* This is where the try/finally block should be created.  For
     compilers without that support we do use setjmp.  */
  struct pthread_unwind_buf unwind_buf;

  /* No previous handlers.  */
  unwind_buf.priv.data.prev = NULL;
  unwind_buf.priv.data.cleanup = NULL;

  int not_first_call;
  not_first_call = setjmp ((struct __jmp_buf_tag *) unwind_buf.cancel_jmp_buf);

ここまでの流れをgdbで確認してみよう。まず_exit、abort、pthread_exitにbreakpointを張って、それから実行。

masami@saga:~$ gdb ./a.out
Reading symbols from ./a.out...done.
(gdb) b _exit
Function "_exit" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (_exit) pending.
(gdb) b abort
Function "abort" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 2 (abort) pending.
(gdb) b pthread_exit
Breakpoint 3 at 0x400550
(gdb) r
Starting program: /home/masami/a.out
warning: Could not load shared library symbols for linux-vdso.so.1.
Do you need "set solib-search-path" or "set sysroot"?
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
[New Thread 0x7ffff780f700 (LWP 31248)]

最初に止まるのはpthread_exit()ですね。

Breakpoint 3, 0x00007ffff7bc63d0 in pthread_exit () from /usr/lib/libpthread.so.0
(gdb) bt
#0  0x00007ffff7bc63d0 in pthread_exit () from /usr/lib/libpthread.so.0
#1  0x00000000004006b7 in main (argc=1, argv=0x7fffffffe688) at test.c:16

引き続き実行してみるとexit()に着て、ここでbacktraceを見るとこんな感じに。exit(3)が呼ばれてそこからexit()にたどり着く。

(gdb) c
Continuing.
[Thread 0x7ffff780f700 (LWP 31248) exited]

Breakpoint 1, 0x00007ffff78c8d80 in _exit () from /usr/lib/libc.so.6
(gdb) bt
#0  0x00007ffff78c8d80 in _exit () from /usr/lib/libc.so.6
#1  0x00007ffff784682f in __run_exit_handlers () from /usr/lib/libc.so.6
#2  0x00007ffff78468d5 in exit () from /usr/lib/libc.so.6
#3  0x00007ffff7830007 in __libc_start_main () from /usr/lib/libc.so.6
#4  0x0000000000400599 in _start ()
(gdb) c
Continuing.
[Inferior 1 (process 31244) exited normally]

これをglibcのコードで見るとstdlib/exit.cでexit()は下記のように書かれていて__run_exit_handlers()を呼ぶだけ。

void
exit (int status)
{
  __run_exit_handlers (status, &__exit_funcs, true);
}

_run_exit_handlers()も同じくstdlib/exit.cにある。略したところはatexit()の呼び出しとかを実行している部分。最後の最後でexit()を呼び出して終わり。

/* Call all functions registered with `atexit' and `on_exit',
   in the reverse of the order in which they were registered
   perform stdio cleanup, and terminate program execution with STATUS.  */
void
attribute_hidden
__run_exit_handlers (int status, struct exit_function_list **listp,
             bool run_list_atexit)
{
〜略〜
  _exit (status);
}

_exit()システムコールなので此処から先はカーネルになる。 ここまで読んでstraceで見たpthread_exit()が_exit()を呼ぶフローは分かったけど、/proc/selfにアクセスできなくなるという部分に関してはここまで全く出てきてないので(まあ、当然と言えば当然なんだけど)次はsys_exit()でも読もうかな。というか、そちらのほうが気になる。。

並行コンピューティング技法 ―実践マルチコア/マルチスレッドプログラミング

並行コンピューティング技法 ―実践マルチコア/マルチスレッドプログラミング