Apakah utas diimplementasikan sebagai proses di Linux?

65

Saya akan membaca buku ini , Pemrograman Linux Lanjut oleh Mark Mitchell, Jeffrey Oldham, dan Alex Samuel. Ini dari tahun 2001, jadi agak tua. Tapi bagaimanapun saya merasa cukup baik.

Namun, saya sampai pada titik ketika ia menyimpang dari apa yang diproduksi Linux saya di output shell. Pada halaman 92 (116 dalam penampil), bab 4.5 Implementasi Utas GNU / Linux dimulai dengan paragraf yang berisi pernyataan ini:

Implementasi utas POSIX pada GNU / Linux berbeda dari implementasi utas pada banyak sistem mirip UNIX lainnya dalam cara yang penting: pada GNU / Linux, utas diimplementasikan sebagai proses.

Ini sepertinya titik kunci dan kemudian diilustrasikan dengan kode C. Output dalam buku ini adalah:

main thread pid is 14608
child thread pid is 14610

Dan di Ubuntu 16.04 saya adalah:

main thread pid is 3615
child thread pid is 3615

ps output mendukung ini.

Saya kira sesuatu pasti telah berubah antara tahun 2001 dan sekarang.

Sub bab berikutnya pada halaman berikutnya, 4.5.1 Penanganan Sinyal, dibangun berdasarkan pernyataan sebelumnya:

Perilaku interaksi antara sinyal dan utas bervariasi dari satu sistem mirip UNIX ke yang lain. Di GNU / Linux, perilaku ditentukan oleh fakta bahwa utas diimplementasikan sebagai proses.

Dan sepertinya ini akan menjadi lebih penting nanti di buku ini. Bisakah seseorang menjelaskan apa yang terjadi di sini?

Saya pernah melihat yang satu ini. Apakah kernel Linux benar-benar proses kernel? , tapi itu tidak banyak membantu. Saya bingung.

Ini adalah kode 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;
}
Tomasz
sumber
1
Saya tidak mengerti apa sumber kebingungan Anda. Thread diimplementasikan sebagai proses berbagi ruang alamat dengan orang tua mereka.
Johan Myréen
2
@ JohanMyréen Jadi mengapa urutan utas sama?
Tomasz
Ah, sekarang saya mengerti. Ya, sesuatu benar-benar telah berubah. Lihat jawaban @ ilkkachu.
Johan Myréen
5
Thread masih diimplementasikan sebagai proses - namun sekarang getpidmengembalikan apa yang disebut ID grup thread dan untuk mendapatkan ID unik untuk suatu proses yang perlu Anda gunakan gettid. Namun, selain kernel, kebanyakan orang dan alat akan menyebut grup utas proses, dan menyebut proses utas, untuk konsistensi dengan sistem lain.
user253751
Tidak juga. Sebuah proses memiliki memori dan berkas sendiri deskriptor, ia tidak pernah disebut thread, hal itu akan menjadi di konsisten dengan sistem lain.
reinierpost

Jawaban:

50

Saya pikir ini bagian dari clone(2)halaman manual dapat menjelaskan perbedaannya. PID:

CLONE_THREAD (sejak Linux 2.4.0-test8)
Jika CLONE_THREAD diatur, anak tersebut ditempatkan di grup utas yang sama dengan proses panggilan.
Grup utas adalah fitur yang ditambahkan di Linux 2.4 untuk mendukung gagasan utas POSIX dari serangkaian utas yang berbagi PID tunggal. Secara internal, PID bersama ini adalah apa yang disebut pengenal grup utas (TGID) untuk grup utas. Sejak Linux 2.4, panggilan untuk mendapatkan (2) mengembalikan TGID penelepon.

Frase "utas diimplementasikan sebagai proses" mengacu pada masalah utas yang memiliki PID terpisah sebelumnya. Pada dasarnya, Linux pada awalnya tidak memiliki utas dalam suatu proses, hanya proses terpisah (dengan PID terpisah) yang mungkin memiliki beberapa sumber daya bersama, seperti memori virtual atau deskriptor file. CLONE_THREADdan pemisahan ID proses (*) dan ID utas membuat perilaku Linux lebih mirip sistem lain dan lebih seperti persyaratan POSIX dalam pengertian ini. Meskipun secara teknis OS masih belum memiliki implementasi terpisah untuk utas dan proses.

Penanganan sinyal adalah area bermasalah lainnya dengan implementasi lama, hal ini dijelaskan secara lebih rinci dalam makalah @FooF yang merujuk pada jawaban mereka .

Seperti disebutkan dalam komentar, Linux 2.4 juga dirilis pada tahun 2001, tahun yang sama dengan buku, jadi tidak mengherankan berita tidak sampai ke cetakan itu.

ilkkachu
sumber
2
proses terpisah yang mungkin memiliki sumber daya bersama, seperti memori virtual atau deskriptor file. Itu masih banyak cara kerja thread Linux, dengan masalah yang Anda sebutkan telah dibersihkan. Saya akan mengatakan memanggil unit penjadwalan yang digunakan di kernel "utas" atau "proses" benar-benar tidak relevan. Fakta bahwa mereka mulai di Linux disebut hanya "proses" tidak berarti hanya itu yang ada sekarang.
Andrew Henle
@AndrewHenle, yeah, diedit sedikit. Saya harap itu menangkap pikiran Anda, meskipun saya tampaknya kesulitan dengan kata-kata. (maju dan edit bagian itu jika Anda mau.) Saya telah memahami bahwa beberapa OS lain yang mirip Unix memiliki pemisahan utas yang lebih berbeda dibandingkan proses, dengan Linux menjadi semacam pengecualian hanya dalam benar-benar memiliki satu jenis penyajian keduanya berfungsi. Tetapi saya tidak cukup tahu tentang sistem lain dan tidak memiliki sumber yang berguna, jadi sulit untuk mengatakan sesuatu yang konkret.
ilkkachu
@tomas Perhatikan bahwa jawaban ini menjelaskan cara kerja Linux sekarang. Seperti ilkkachu mengisyaratkan, itu bekerja secara berbeda ketika buku itu ditulis. Jawaban FooF menjelaskan bagaimana Linux bekerja pada saat itu.
Gilles 'SANGAT berhenti menjadi jahat'
38

Anda benar, memang "sesuatu harus berubah antara tahun 2001 dan sekarang". Buku yang Anda baca menggambarkan dunia menurut implementasi historis pertama utas POSIX di Linux, disebut LinuxThreads (lihat juga artikel Wikipedia untuk sebagian).

LinuxThreads memiliki beberapa masalah kompatibilitas dengan standar POSIX - misalnya utas tidak berbagi PID - dan beberapa masalah serius lainnya. Untuk memperbaiki kekurangan ini, implementasi lain yang disebut NPTL (Native POSIX Thread Library) dipelopori oleh Red Hat untuk menambahkan dukungan kernel dan ruang perpustakaan pengguna yang diperlukan untuk mencapai kepatuhan POSIX yang lebih baik (mengambil bagian yang baik dari proyek implementasi ulang lain yang bersaing oleh IBM bernama NGPT (" Next Generation Posixs Threads "), lihat artikel Wikipedia di NPTL ). Bendera tambahan yang ditambahkan ke clone(2)system call (terutama CLONE_THREADyang @ikkkachumenunjukkan jawabannya ) mungkin merupakan bagian paling jelas dari modifikasi kernel. Bagian ruang pengguna dari karya tersebut akhirnya dimasukkan ke dalam GNU C Library.

Masih saat ini beberapa SDK Linux tertanam menggunakan implementasi LinuxThreads lama karena mereka menggunakan versi jejak memori yang lebih kecil dari LibC yang disebut uClibc (juga disebut µClibc) , dan butuh waktu bertahun-tahun sebelum implementasi ruang pengguna NPTL dari GNU LibC porting dan diasumsikan sebagai implementasi threading POSIX default, seperti umumnya platform khusus ini tidak berusaha untuk mengikuti mode terbaru dengan kecepatan kilat. Ini dapat diamati dengan memperhatikan bahwa memang PID untuk utas yang berbeda pada platform tersebut juga berbeda dengan standar POSIX - seperti buku yang Anda baca menjelaskan. Sebenarnya begitu Anda meneleponpthread_create(), Anda tiba-tiba meningkatkan hitungan proses dari satu menjadi tiga karena proses tambahan diperlukan untuk menjaga kekacauan tetap sama.

Halaman manual Linux pthreads (7) memberikan gambaran yang komprehensif dan menarik dari perbedaan keduanya. Deskripsi lain yang mencerahkan, meskipun ketinggalan zaman, perbedaannya adalah makalah ini oleh Ulrich Depper dan Ingo Molnar tentang desain NPTL.

Saya sarankan Anda untuk tidak menganggap bagian buku itu terlalu serius. Saya malah merekomendasikan thread POSIX Pemrograman Butenhof dan halaman manual POSIX dan Linux tentang subjek ini. Banyak tutorial tentang masalah ini tidak akurat.

FooF
sumber
22

Utas (Userspace) tidak diimplementasikan sebagai proses seperti pada Linux, dalam hal bahwa mereka tidak memiliki ruang alamat pribadi mereka sendiri, mereka masih berbagi ruang alamat dari proses induk.

Namun, utas ini diterapkan untuk menggunakan sistem akuntansi proses kernel, jadi dialokasikan ID Utas sendiri (TID), tetapi diberi PID dan 'ID grup utas' (TGID) yang sama dengan proses induk - ini berbeda dengan garpu, tempat TGID dan PID baru dibuat, dan TID sama dengan PID.

Jadi tampaknya kernel baru-baru ini memiliki TID terpisah yang dapat ditanyakan, apakah ini yang berbeda untuk utas, cuplikan kode yang sesuai untuk menunjukkan ini di masing-masing utama () thread_function () di atas adalah:

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

Jadi seluruh kode dengan ini adalah:

#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;                                                                                                                                                 
} 

Memberikan contoh output dari:

main TID is 17963
main thread pid is 17963
thread TID is 17964
child thread pid is 17963
einonm
sumber
3
@tomas einonm benar. Mengabaikan apa yang dikatakan buku itu, itu sangat membingungkan. Tidak tahu ide apa yang ingin disampaikan oleh pengarangnya, tetapi ia gagal total. Jadi, di Linux Anda memiliki utas Kernel dan utas ruang Pengguna. Utas kernel pada dasarnya adalah proses tanpa ruang pengguna sama sekali. Utas ruang pengguna adalah utas POSIX normal. Ruang pengguna memproses deskriptor file berbagi, dapat berbagi segmen kode, tetapi tinggal di Ruang Alamat Virtual yang benar-benar terpisah. Utas ruang pengguna dalam segmen kode proses berbagi, memori statis dan tumpukan (memori dinamis), tetapi memiliki set register dan tumpukan prosesor yang terpisah.
Boris Burkov
8

Pada dasarnya, informasi dalam buku Anda secara historis akurat, karena sejarah implementasi utas yang sangat buruk di Linux. Jawaban saya atas pertanyaan terkait pada SO juga berfungsi sebagai jawaban untuk pertanyaan Anda:

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

Kerancuan ini semua berasal dari fakta bahwa para pengembang kernel awalnya memegang pandangan yang tidak rasional dan salah bahwa utas dapat diimplementasikan hampir seluruhnya di ruang pengguna menggunakan proses kernel sebagai primitif, selama kernel menawarkan cara untuk membuat mereka berbagi memori dan deskriptor file . Ini mengarah pada implementasi LinuxThreads yang sangat buruk dari thread POSIX, yang agak keliru karena tidak memberikan apa pun yang menyerupai semantik thread POSIX. Akhirnya LinuxThreads diganti (oleh NPTL), tetapi banyak terminologi yang membingungkan dan kesalahpahaman masih ada.

Hal pertama dan paling penting untuk disadari adalah bahwa "PID" memiliki arti yang berbeda dalam ruang kernel dan ruang pengguna. Apa yang disebut PID oleh kernel sebenarnya adalah id thread tingkat kernel (sering disebut TIDs), jangan dikelirukan dengan pthread_tyang merupakan pengidentifikasi terpisah. Setiap utas pada sistem, apakah dalam proses yang sama atau berbeda, memiliki TID unik (atau "PID" dalam terminologi kernel).

Apa yang dianggap sebagai PID dalam arti POSIX "proses", di sisi lain, disebut "ID grup utas" atau "TGID" di kernel. Setiap proses terdiri dari satu atau lebih utas (proses kernel) masing-masing dengan TID (kernel PID) masing-masing, tetapi semuanya berbagi TGID yang sama, yang sama dengan TID (kernel PID) dari utas awal tempat mainmenjalankan.

Ketika topmenunjukkan Anda utas, itu menunjukkan TUT (kernel PID), bukan PID (TGID kernel), dan inilah mengapa setiap utas memiliki yang terpisah.

Dengan munculnya NPTL, sebagian besar panggilan sistem yang mengambil argumen PID atau bertindak pada proses pemanggilan diubah untuk memperlakukan PID sebagai TGID dan bertindak pada keseluruhan "grup utas" (proses POSIX).

R ..
sumber
8

Secara internal, tidak ada yang namanya proses atau utas di kernel linux. Proses dan utas sebagian besar adalah konsep userland, kernel itu sendiri hanya melihat "tugas", yang merupakan objek terjadwal yang mungkin tidak berbagi, sebagian, atau semua sumber dayanya dengan tugas lain. Utas adalah tugas yang telah dikonfigurasikan untuk membagikan sebagian besar sumber dayanya (ruang alamat, mmaps, pipa, penangan file terbuka, soket, dll.) Dengan tugas induk, dan proses adalah tugas yang telah dikonfigurasikan untuk berbagi sumber daya minimal dengan tugas induk .

Saat Anda menggunakan Linux API secara langsung ( clone () , alih-alih fork () dan pthread_create () ), maka Anda memiliki lebih banyak fleksibilitas dalam menentukan berapa banyak sumber daya untuk dibagikan atau tidak dibagikan, dan Anda dapat membuat tugas yang tidak sepenuhnya memproses atau sepenuhnya thread. Jika Anda menggunakan panggilan tingkat rendah ini secara langsung, dimungkinkan juga untuk membuat tugas dengan TGID baru (dengan demikian diperlakukan sebagai proses oleh sebagian besar alat pengguna lahan) yang benar-benar membagikan semua sumber dayanya dengan tugas induk, atau sebaliknya, untuk membuat tugas dengan TGID yang dibagikan (dengan demikian diperlakukan sebagai utas oleh sebagian besar alat pengguna tanah) yang tidak berbagi sumber daya dengan tugas induknya.

Sementara Linux 2.4 mengimplementasikan TGID, ini sebagian besar hanya untuk kepentingan akuntansi sumber daya. Banyak pengguna dan alat userspace merasa berguna untuk dapat mengelompokkan tugas-tugas terkait bersama dan melaporkan penggunaan sumber daya mereka bersama-sama.

Implementasi tugas di Linux jauh lebih lancar daripada proses dan benang pandangan dunia yang disajikan oleh alat userspace.

Lie Ryan
sumber
The kertas @FooF terkait dengan menggambarkan sejumlah titik di mana kernel harus mempertimbangkan proses dan benang sebagai entitas yang terpisah (penanganan misalnya sinyal dan exec ()), jadi setelah membacanya, aku akan benar-benar mengatakan bahwa "tidak ada seperti hal seperti proses atau utas di kernel linux. "
ilkkachu
5

Linus Torvalds menyatakan dalam sebuah mailing list kernel pada tahun 1996 bahwa "baik utas maupun proses diperlakukan sebagai 'konteks eksekusi'", yang merupakan "hanya konglomerat dari semua keadaan CoE itu .... termasuk hal-hal seperti CPU status, status MMU, izin, dan berbagai status komunikasi (file terbuka, penangan sinyal, dll) ".

// 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;
}

Seperti yang Anda lihat program ini akan menelurkan 25 utas sekaligus, masing-masing akan tidur selama 100 detik dan kemudian bergabung dengan program utama lagi. Setelah semua 25 utas bergabung kembali dengan program, program selesai dan akan keluar.

Menggunakan topAnda akan dapat melihat 25 contoh program "threads2". Tapi kidna membosankan. Output dari ps auwxbahkan kurang menarik ... TAPI ps -eLfmendapat agak menarik.

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

Anda dapat melihat di sini ke-26 CoEr bahwa thread2program telah dibuat. Mereka semua berbagi ID proses (PID) yang sama dan ID proses induk (PPID) tetapi masing-masing memiliki ID LWP yang berbeda (proses ringan), dan jumlah LWP (NLWP) menunjukkan ada 26 CoE - program utama dan 25 utas melahirkannya.

ivanivan
sumber
Benar, utas hanyalah proses yang ringan (LWP)
fpmurphy
2

Ketika datang ke proses Linux dan utas adalah jenis hal yang sama. Yang mengatakan mereka diciptakan dengan system call yang sama: clone.

Jika Anda memikirkannya, perbedaan antara utas dan proses adalah di mana objek kernel akan dibagikan oleh anak dan orang tua. Untuk proses, itu tidak banyak: deskriptor file terbuka, segmen memori yang belum ditulis, mungkin beberapa lainnya yang saya tidak bisa memikirkan dari atas kepala saya. Untuk utas, lebih banyak objek dibagikan, tetapi tidak semua.

Apa yang membuat utas dan objek lebih dekat di Linux adalah unsharepanggilan sistem. Objek kernel yang mulai dibagikan dapat dibagikan setelah pembuatan utas. Jadi, Anda dapat, misalnya, memiliki dua utas dari proses yang sama yang memiliki ruang deskriptor file yang berbeda (dengan mencabut pembagian deskriptor file setelah utas dibuat). Anda dapat mengujinya sendiri dengan membuat utas, memanggil unsharekedua utas dan kemudian menutup semua file dan membuka file baru, pipa atau objek di kedua utas tersebut. Kemudian lihat ke dalam /proc/your_proc_fd/task/*/fddan Anda akan melihat bahwa masing-masing task(yang Anda buat sebagai utas) akan memiliki fd yang berbeda.

Faktanya, baik pembuatan utas baru maupun proses baru adalah rutinitas pustaka yang memanggil di clonebawahnya dan menentukan objek kernel mana yang taskakan dibagikan oleh proses-thread-thingamajig (yaitu, ) dengan proses pemanggilan / utas.

Dmitry Rubanovich
sumber