web-dev-qa-db-ja.com

スレッドはLinuxのプロセスとして実装されていますか?

私は この本 、Mark Mitchell、Jeffrey Oldham、およびAlex SamuelによるAdvanced Linux Programmingを進めています。 2001年のものなので、少し古いです。しかし、とにかくそれはかなり良いと思います。

ただし、Linuxがシェル出力で生成するものとは異なる点に達しました。 92ページ(ビューアの116)で、章4.5 GNU/Linuxスレッドの実装は、このステートメントを含む段落で始まります。

GNU/LinuxでのPOSIXスレッドの実装は、他の多くのUNIXライクなシステムでのスレッド実装とは重要な点で異なります。GNU/ Linuxでは、スレッドはプロセスとして実装されます。

これは重要なポイントのように思われ、後でCコードで示します。本の出力は次のとおりです。

main thread pid is 14608
child thread pid is 14610

そして私のUbuntu 16.04では:

main thread pid is 3615
child thread pid is 3615

ps出力はこれをサポートします。

2001年から今までに何かが変わったに違いない。

次のページの次のサブチャプター、4.5.1シグナル処理は、前のステートメントに基づいています。

シグナルとスレッド間の相互作用の動作は、UNIXに似たシステムごとに異なります。 GNU/Linuxでは、スレッドがプロセスとして実装されるという事実によって、動作が決まります。

そして、これは本の後半でさらに重要になるでしょう。誰かがここで何が起こっているのか説明できますか?

私はこれを見ました Linuxカーネルスレッドは本当にカーネルプロセスですか? ですが、あまり役に立ちません。よくわかりません。

これはCコードです:

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

void* thread_function (void* arg)
{
    fprintf (stderr, "child thread pid is %d\n", (int) getpid ());
    /* Spin forever. */
    while (1);
    return NULL;
}

int main ()
{
    pthread_t thread;
    fprintf (stderr, "main thread pid is %d\n", (int) getpid ());
    pthread_create (&thread, NULL, &thread_function, NULL);
    /* Spin forever. */
    while (1);
    return 0;
}
66
user147505

clone(2) manページ のこの部分は、違いを明らかにするかもしれません。 PID:

CLONE_THREAD(Linux 2.4.0-test8以降)
CLONE_THREADが設定されている場合、子は呼び出しプロセスと同じスレッドグループに配置されます。
スレッドグループは、Linux 2.4で追加された機能で、単一のPIDを共有する一連のスレッドのPOSIXスレッド概念をサポートします。内部的には、この共有PIDは、スレッドグループのいわゆるスレッドグループ識別子(TGID)です。 Linux 2.4以降、getpid(2)を呼び出すと、呼び出し元のTGIDが返されます。

「スレッドはプロセスとして実装されます」というフレーズは、過去に個別のPIDがあったスレッドの問題を指します。基本的に、Linuxは元々、プロセス内にスレッドを持たず、仮想メモリやファイル記述子などの共有リソースを持つ可能性のある個別のプロセス(個別のPIDを持つ)だけでした。 CLONE_THREADおよびプロセスIDの分離(*) この意味で、スレッドIDにより、Linuxの動作は他のシステムのようになり、POSIX要件のようになります。技術的にはOSにはまだスレッドとプロセスの個別の実装がありません。

シグナル処理は、古い実装のもう1つの問題のある領域でした。これについては、 paper で詳しく説明しています。@ FooFは 回答で を参照しています。

コメントで述べたように、Linux 2.4も2001年にこの本と同じ年にリリースされたので、ニュースがその印刷物に届かなかったのは当然のことです。

51
ilkkachu

あなたは正しい、確かに「何かが2001年から今までに変わったに違いない」。あなたが読んでいる本は LinuxThreads と呼ばれるLinuxでのPOSIXスレッドの最初の歴史的な実装に従って世界を説明しています(一部については Wikipedia の記事も参照してください)。

LinuxThreadsには、POSIX標準との互換性の問題がいくつかありました(たとえば、PIDを共有しないスレッドなど)、およびその他のいくつかの深刻な問題。これらの欠陥を修正するために、NPTL(ネイティブPOSIXスレッドライブラリ)と呼ばれる別の実装がRed Hatによって先頭に立ち、必要なカーネルとユーザースペースライブラリのサポートを追加して、POSIXコンプライアンスを改善しました(NGPT( "次世代Posixスレッド ")、 NPTLに関するWikipediaの記事 を参照)。 clone(2) システムコールに追加された追加フラグ(特にCLONE_THREAD それ @ikkkachuで指摘 彼の回答 )は、おそらくカーネル変更の最も明白な部分です。作業のユーザースペース部分は、最終的にGNU Cライブラリに組み込まれました。

それでも今日、一部の組み込みLinux SDKは古いLinuxThreads実装を使用しています。これは Clibc(µClibcとも呼ばれる) と呼ばれるより小さなメモリフットプリントバージョンのLibCを使用しているため、NPTLユーザースペースの実装にかなりの年月がかかりました。 GNU LibCが移植されたがデフォルトのPOSIXスレッド実装として想定されている電光石火の速さを備えた最新のファッションです。これは、これらのプラットフォームの異なるスレッドのPIDが、POSIX標準で指定されているものとは異なり、実際に読んでいる本と同じであることに気づくことで確認できます。実際にpthread_create()を呼び出すと、 、混乱を維持するために追加のプロセスが必要になったため、突然プロセス数が1から3に増えました。

Linux pthreads(7) マニュアルページは、2つの違いの包括的で興味深い概要を提供します。もう古くはありますが、違いの説明は、NPTLの設計に関するUlrich DepperとIngo Molnarによる paper です。

本のその部分をあまり真剣に受け止めないことをお勧めします。代わりに、ButenhofのプログラミングPOSIXスレッドと、この件に関するPOSIXおよびLinuxのマニュアルページをお勧めします。このテーマに関する多くのチュートリアルは不正確です。

38
FooF

(ユーザースペース)スレッドは、Linuxのようにプロセスとして実装されていません。独自のプライベートアドレス空間がないため、親プロセスのアドレス空間を共有しています。

ただし、これらのスレッドはカーネルプロセスアカウンティングシステムを使用するように実装されているため、独自のスレッドID(TID)が割り当てられますが、親プロセスと同じPIDおよび「スレッドグループID」(TGID)が与えられます。これは、新しいTGIDとPIDが作成されるフォーク。TIDはPIDと同じです。

したがって、最近のカーネルには照会可能な個別のTIDがあったようです。これはスレッドによって異なります。上記のmain()thread_function()のそれぞれでこれを示すのに適したコードスニペットは次のとおりです。

    long tid = syscall(SYS_gettid);
    printf("%ld\n", tid);

したがって、これを含むコード全体は次のようになります。

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

void* thread_function (void* arg)                                                                                                                             
{                                                                                                                                                             
    long tid = syscall(SYS_gettid);                                                                                                                           
    printf("child thread TID is %ld\n", tid);                                                                                                                 
    fprintf (stderr, "child thread pid is %d\n", (int) getpid ());                                                                                            
    /* Spin forever. */                                                                                                                                       
    while (1);                                                                                                                                                
    return NULL;                                                                                                                                              
}                                                                                                                                                             

int main ()                                                                                                                                                   
{                                                                                                                                               
    pthread_t thread;                                                                               
    long tid = syscall(SYS_gettid);     
    printf("main TID is %ld\n", tid);                                                                                             
    fprintf (stderr, "main thread pid is %d\n", (int) getpid ());                                                    
    pthread_create (&thread, NULL, &thread_function, NULL);                                           
    /* Spin forever. */                                                                                                                                       
    while (1);                                                                                                                                                
    return 0;                                                                                                                                                 
} 

次の出力例を示します。

main TID is 17963
main thread pid is 17963
thread TID is 17964
child thread pid is 17963
21
einonm

内部的には、Linuxカーネルにはプロセスやスレッドなどはありません。プロセスとスレッドは主にユーザーランドの概念であり、カーネル自体は「タスク」のみを認識します。「タスク」は、他のタスクとリソースをまったく共有しないか、一部またはすべて共有できるスケジュール可能なオブジェクトです。スレッドは、そのリソースのほとんど(アドレススペース、mmaps、パイプ、オープンファイルハンドラー、ソケットなど)を親タスクと共有するように構成されたタスクであり、プロセスは、最小限のリソースを親タスクと共有するように構成されたタスクです。

Linux APIを直接使用する場合( clone() の代わりに fork() および pthread_create() )を使用すると、はるかに柔軟になります。共有するまたは共有しないリソースの量を定義する際に、完全にプロセスでもスレッドでもないタスクを作成できます。これらの低レベルの呼び出しを直接使用する場合は、新しいTGIDを使用してタスクを作成し(したがって、ほとんどのユーザーランドツールによってプロセスとして扱われます)、実際にすべてのリソースを親タスクと共有するか、またはその逆で、親タスクとリソースを共有しない共有TGID(したがって、ほとんどのユーザーランドツールによってスレッドとして扱われる)を持つタスク。

Linux 2.4はTGIDを実装していますが、これはほとんどリソースアカウンティングの利益のためだけです。多くのユーザーとユーザースペースツールでは、関連するタスクをグループ化し、リソースの使用状況を一緒に報告できると便利です。

Linuxでのタスクの実装は、ユーザー空間ツールによって提示されるプロセスとスレッドの世界観よりもはるかに流動的です。

9
Lie Ryan

Linuxでのスレッドの実装履歴は非常に悪いため、基本的に、本の情報は歴史的に正確です。 SOに関する関連質問への私によるこの回答は、あなたの質問への回答としても役立ちます:

https://stackoverflow.com/questions/9154671/distinction-between-processes-and-threads-in-linux/9154725#9154725

これらの混乱はすべて、カーネルがメモリとファイル記述子を共有する方法を提供する限り、カーネル開発者が最初にカーネルプロセスをプリミティブとして使用してスレッドをほぼ完全にユーザー空間に実装できるという不合理で間違った見方をしていたという事実から生じます。これにより、悪名高い悪いPOSIXスレッドのLinuxThreads実装がもたらされました。これは、リモートでPOSIXスレッドのセマンティクスに似たものを提供しなかったため、かなり誤ったものでした。最終的にLinuxThreadsは(NPTLによって)置き換えられましたが、混乱を招く用語と誤解の多くが残っています。

最初に理解するべき最も重要なことは、「PID」はカーネル空間とユーザー空間で異なることを意味するということです。カーネルがPIDと呼ぶものは、実際にはカーネルレベルのスレッドID(TIDと呼ばれることが多い)であり、個別の識別子であるpthread_tと混同しないでください。システム上の各スレッドは、同じプロセスか別のプロセスかに関係なく、一意のTID(またはカーネルの用語では「PID」)を持っています。

一方、POSIXの「プロセス」の意味でPIDと見なされるものは、カーネルでは「スレッドグループID」または「TGID」と呼ばれます。各プロセスは、それぞれが独自のTID(カーネルPID)を持つ1つ以上のスレッド(カーネルプロセス)で構成されますが、すべてが同じTGIDを共有します。これは、mainが実行される初期スレッドのTID(カーネルPID)と同じです。

topがスレッドを表示する場合、PID(カーネルTGID)ではなくTID(カーネルPID)を表示しているため、各スレッドに個別のスレッドがあります。

NPTLの登場により、PID引数を取るか、呼び出しprocessに作用するほとんどのシステムコールは、PIDをTGIDとして扱い、作用するように変更されました「スレッドグループ」全体(POSIXプロセス)。

Linus Torvaldsは、1996年のカーネルメーリングリストの投稿で、「スレッドとプロセスの両方が「実行のコンテキスト」として扱われる」と述べました。これは、「CoEのすべての状態の集まりにすぎません。状態、MMU状態、権限、およびさまざまな通信状態(開いているファイル、シグナルハンドラーなど) "。

// simple program to create threads that simply sleep
// compile in debian jessie with apt-get install build-essential
// and then g++ -O4 -Wall -std=c++0x -pthread threads2.cpp -o threads2
#include <string>
#include <iostream>
#include <thread>
#include <chrono>

// how many seconds will the threads sleep for?
#define SLEEPTIME 100
// how many threads should I start?
#define NUM_THREADS 25

using namespace std;

// The function we want to execute on the new thread.
void threadSleeper(int threadid){
    // output what number thread we've created
    cout << "task: " << threadid << "\n";
    // take a nap and sleep for a while
    std::this_thread::sleep_for(std::chrono::seconds(SLEEPTIME));
}

void main(){
    // create an array of thread handles
    thread threadArr[NUM_THREADS];
    for(int i=0;i<NUM_THREADS;i++){
        // spawn the threads
        threadArr[i]=thread(threadSleeper, i);
    }
    for(int i=0;i<NUM_THREADS;i++){
        // wait for the threads to finish
        threadArr[i].join();
    }
    // program done
    cout << "Done\n";
    return;
}

ご覧のように、このプログラムは一度に25のスレッドを生成し、それぞれが100秒間スリープしてから、メインプログラムに再び参加します。 25個のスレッドすべてがプログラムに再び参加すると、プログラムが終了し、終了します。

topを使用すると、「threads2」プログラムの25のインスタンスを表示できます。しかし、退屈な誘惑。 ps auwxの出力はさらに面白くありません...しかしps -eLfはちょっと面白くなります。

UID        PID  PPID   LWP  C NLWP STIME TTY          TIME CMD
debian     689   687   689  0    1 14:52 ?        00:00:00 sshd: debian@pts/0  
debian     690   689   690  0    1 14:52 pts/0    00:00:00 -bash
debian    6217   690  6217  0    1 15:04 pts/0    00:00:00 screen
debian    6218  6217  6218  0    1 15:04 ?        00:00:00 SCREEN
debian    6219  6218  6219  0    1 15:04 pts/1    00:00:00 /bin/bash
debian    6226  6218  6226  0    1 15:04 pts/2    00:00:00 /bin/bash
debian    6232  6219  6232  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6233  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6234  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6235  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6236  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6237  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6238  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6239  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6240  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6241  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6242  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6243  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6244  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6245  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6246  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6247  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6248  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6249  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6250  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6251  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6252  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6253  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6254  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6255  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6256  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6257  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6260  6226  6260  0    1 15:04 pts/2    00:00:00 ps -eLf

thread2プログラムが作成した26のCoEをすべて見ることができます。それらはすべて同じプロセスID(PID)と親プロセスID(PPID)を共有しますが、それぞれに異なるLWP ID(軽量プロセス)があり、LWP(NLWP)の数は26のCoE(メインプログラムとそれによって生成された25のスレッド。

5
ivanivan

Linuxプロセスとスレッドに関しては、種類と同じです。つまり、同じシステムコールcloneを使用して作成されているということです。

考えてみれば、スレッドとプロセスの違いは、カーネルオブジェクトが子と親によって共有されることです。プロセスについては、それほど多くありません。開いているファイル記述子、まだ書き込まれていないメモリセグメント、おそらく頭上で考えることができない他のいくつか。スレッドの場合、共有されるオブジェクトは多くありませんが、すべてではありません。

Linuxでスレッドとオブジェクトを近づけているのは、unshareシステムコールです。共有として開始されたカーネルオブジェクトは、スレッドの作成後に共有解除できます。したがって、たとえば、同じプロセスの2つのスレッドに異なるファイル記述子スペースを持たせることができます(スレッドの作成後にファイル記述子の共有を取り消すことにより)。スレッドを作成し、両方のスレッドでunshareを呼び出して、すべてのファイルを閉じ、両方のスレッドで新しいファイル、パイプ、またはオブジェクトを開くことにより、自分でテストできます。次に、/proc/your_proc_fd/task/*/fdと、各task(スレッドとして作成したもの)のfdが異なることがわかります。

実際、新しいスレッドと新しいプロセスの作成はどちらもライブラリルーチンであり、その下でcloneを呼び出し、新しく作成されたプロセススレッド-thingamajig(つまり、task)がどのカーネルオブジェクトを使用するかを指定します。呼び出しプロセス/スレッドと共有します。

3