Linuxシステムコール、POSIX スレッドの生成

UNIX / Linux システムコール・プログラミング

POSIXスレッドの生成

スレッドとは?

スレッドは、実行の単位ですがプロセスとは異なります。プロセスは、プロセスごとに独立した論理アドレス空間をもち、異なるプロセス間ではメモリの共有をしていません。

プロセスの生成に関しては下記を参照。
 → Linuxシステムコール、プロセスの生成

これに対してスレッドは、1つのプロセスのメモリ空間を共有する複数の実行単位となります。簡単に言ってしまえば、ある関数fx()を呼び出すときにスレッドとして呼び出すことでfx()を複数、同時並列的に実行できるようになります。
同様のことは fork()で別プロセスとして分岐し、fx()を呼ぶことで実現できますがfork()はプロセスのメモリイメージのコピー(実際にはコピーオンライトで異なる部分だけのコピーでその他は共有されます)が発生し、プロセス動作するためのスタック領域などが別個に確保されるためのコストが発生します。
スレッドは、このようなコストを必要としないという特徴があります。

また、スレッドはマルチコアのCPUの場合に特定のコアにスレッドを結びつけることも可能という特徴もあります。

また、通常c言語は、main() から始まりますが、この大本をメインスレッドとも表現します。


スレッドを動かしてみよう!

それでは、実際にLinux上でスレッドを使ったコードについて解説していきたいと思います。

サンプルコードは下記のような処理を行います。

1.3つの新しいスレッドを、pthread_create() で生成します。
 このとき、メインスレッドから起動時の引数として文字列を渡します。

2.それぞれのスレッドが同時並行で処理を行います。
 このコードでは下記の2つの処理を実装しています。
  スレッド関数1. 引数の文字列の長さ分タイマーウエイトを行う
  スレッド関数2. 引数の文字列を大文字・小文字変換して返す

3.メインスレッドは、各スレッドの完了を待ち合わせます。
 起動されたスレッドは終了時に、pthread_exit() を使って終了コードを通知します。
 メインスレッドは、pthread_join() を使って各スレッドの終了状態を受け取ります。


pthread.c

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <ctype.h>

void* thread1(void* arg); // Thread Entry function 1
void* thread2(void* arg); // Thread Entry function 2


//
// Main thread.
//
int main(int argc, char **argv) {

    int   ret;

    const char *arg1 = "Tokyo";
    const char *arg2 = "Supercalifragilisticexpialidocious";
    const char *arg3 = "Hokkaido";

    pthread_t thrd1, thrd2, thrd3;
    void* th_ret;

    // Make three threads
    ret = pthread_create(&thrd1, NULL, thread1, (void*)arg1);
    if (ret) {
        perror("pthread_create[Thread 1]");
        exit(1);
    }

    ret = pthread_create(&thrd2, NULL, thread2, (void*)arg2);
    if (ret) {
        perror("pthread_create[Thread 2]");
        exit(1);
    }

    ret = pthread_create(&thrd3, NULL, thread1, (void*)arg3);
    if (ret) {
        perror("pthread_create[Thread 3]");
        exit(1);
    }


    // Waiting threads quit.
    pthread_join(thrd3, &th_ret);
    printf("Thread 3 [%d]\n", *(int*)th_ret);
    free(th_ret);                          // Free return value memory.

    pthread_join(thrd2, &th_ret);
    printf("Thread 2 [%d]\n", *(int*)th_ret);
    free(th_ret);                          // Free return value memory.

    pthread_join(thrd1, &th_ret);
    printf("Thread 1 [%d]\n", *(int*)th_ret);
    free(th_ret);                          // Free return value memory.


    return 0;
}

// Thread entry function 1
//   The function waits from a giving string size.
void* thread1(void* arg) {

    int   t = 0;
    int*  ret = malloc(sizeof(int));       // Allocate a return value area.

    t = strlen((char*)arg);
    sleep( t );
    printf("[%s] %ds\n", (char*)arg, t);

    *ret = t;
    pthread_exit(ret);                     // Return value for main thread.
}

// Thread entry function 2
//   The function converts characters from a giving argument string.
void* thread2(void* arg) {

    int   i;
    char* c = (char*)arg;
    int*  ret = malloc(sizeof(int));       // Allocate a return value area.

    printf("%s --> ", c);
    for(i=0; i<strlen(c); i++) {
        if (isupper(*(c+i))) {
            printf("%c", (char)tolower(*(c+i)));
        }
        else {
            printf("%c", (char)toupper(*(c+i)));
        }
    }
    printf("\n");

    *ret = i;
    pthread_exit(ret);                     // Return value for main thread.
}


このコードでは、スレッドの戻り情報はそれぞれmalloc()で確保してメインスレッドに渡し、メインスレッド側で削除しています。

また、このサンプルはリンク時に、“libpthread”を必要とします。このため、コンパイル時に“-lpthread”を指定してください。
$ gcc pthread.c -o pthread -lpthread

このリンクしている libpthread が pthread_create() などのAPIを持っています。
実は、pthread_create() などはシステムコールではありません。

straceコマンドを導入することで、実際にどのようなシステムコールが呼ばれているかを確認することができます。

# strace ./pthread
   中略

clone(child_stack=0xb6e32f98, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0xb6e334c8, tls=0xb6e33920, child_tidptr=0xb6e334c8) = 466 mmap2(NULL, 8392704, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0xb5e32000 mprotect(0xb5e33000, 8388608, PROT_READ|PROT_WRITE) = 0 clone(child_stack=0xb6631f98, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0xb66324c8, tls=0xb6632920, child_tidptr=0xb66324c8) = 467 mmap2(NULL, 8392704, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0xb5631000 mprotect(0xb5632000, 8388608, PROT_READ|PROT_WRITE) = 0 clone(child_stack=0xb5e30f98, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0xb5e314c8, tls=0xb5e31920, child_tidptr=0xb5e314c8) = 468

clone()というシステムコールが呼ばれています。このclone()がスレッド生成の実態です。

もう一つ、タネを明かすと前回記事「Linuxシステムコール、プロセスの生成」で、プロセスの生成はfork()/exec()で行いますと、説明しましたが、今のLinuxバージョンではfork()システムコールではなく、このclone()が呼ばれるようになっています。
先程と同様に、straceコマンドを使ってシステムコールの追跡を行うと、clone()が呼び出されていることがわかります、

clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0xb6f48968) = 478

実はfork()ではなくclone()を呼び出すためにglibcのラッパーであるfork()を呼び出すと実際にはclone()が実行されるようになっています。
clone()を呼び出すときの引数で、実行するプログラムの実態を指定してこれまで同様のfork()/exec()による動作を可能としたり、スレッドとしての振る舞いを可能としています。


実際の設計においては、処理速度だけではなく、セキュリティや堅牢性などを考慮してプロセス実装とするか、スレッド実装とするかなどを決める必要があります。
ここあたりは、設計者としての腕の見せ所の一つですね。





Have a Happy Hucking!!

Lightning Brains

コメント

このブログの人気の投稿

Linuxシステムコール、共有メモリの使い方

Linuxシステムコール、メッセージキューの使い方

Linuxシステムコール、セマフォの使い方