Saya mencoba memeriksa apa yang sebenarnya terjadi pada pthreads saat salah satu dari mereka menjalankan vfork.
Spesifikasi mengatakan bahwa "utas kontrol" induk "ditangguhkan" hingga proses anak memanggil exec* atau _exit.
Seperti yang saya pahami, konsensusnya adalah bahwa itu berarti seluruh proses induk (yaitu:dengan semua pthreadnya) ditangguhkan.
Saya ingin mengonfirmasinya menggunakan eksperimen.
Sejauh ini saya melakukan beberapa percobaan, yang semuanya menunjukkan bahwa pthreads lain sedang berjalan. Karena saya tidak memiliki pengalaman linux, saya menduga bahwa interpretasi saya terhadap eksperimen ini salah, dan mempelajari interpretasi sebenarnya dari hasil ini dapat membantu menghindari kesalahpahaman lebih lanjut dalam hidup saya.
Jadi, inilah eksperimen yang saya lakukan:
Eksperimen I
#include<unistd.h>
#include<signal.h>
#include<errno.h>
#include<cstring>
#include<string>
#include<iostream>
using namespace std;
void * job(void *x){
int pid=vfork();
if(-1==pid){
cerr << "failed to fork: " << strerror(errno) << endl;
_exit(-3);
}
if(!pid){
cerr << "A" << endl;
cerr << "B" << endl;
if(-1 == execlp("/bin/ls","ls","repro.cpp",(char*)NULL)){
cerr << "failed to exec : " << strerror(errno) << endl;
_exit(-4);//serious problem, can not proceed
}
}
return NULL;
}
int main(){
signal(SIGPIPE,SIG_IGN);
signal(SIGCHLD,SIG_IGN);
const int thread_count = 4;
pthread_t thread[thread_count];
int err;
for(size_t i=0;i<thread_count;++i){
if((err = pthread_create(thread+i,NULL,job,NULL))){
cerr << "failed to create pthread: " << strerror(err) << endl;
return -7;
}
}
for(size_t i=0;i<thread_count;++i){
if((err = pthread_join(thread[i],NULL))){
cerr << "failed to join pthread: " << strerror(err) << endl;
return -17;
}
}
}
Ada 44 pthreads, yang semuanya melakukan vfork, dan exec pada anak.
Setiap proses anak, melakukan dua operasi keluaran antara vfork dan exec “A” dan “B”.
Teori menunjukkan bahwa outputnya harus membaca ABABABABABA…tanpa bersarang.
Namun outputnya berantakan total:misalnya:
AAAA
BB
B
B
Eksperimen II
Mencurigai bahwa menggunakan I/O lib setelah vfork bisa menjadi ide yang buruk, saya telah mengganti fungsi job() dengan yang berikut:
const int S = 10000000;
int t[S];
void * job(void *x){
int pid=vfork();
if(-1==pid){
cerr << "failed to fork: " << strerror(errno) << endl;
_exit(-3);
}
if(!pid){
for(int i=0;i<S;++i){
t[i]=i;
}
for(int i=0;i<S;++i){
t[i]-=i;
}
for(int i=0;i<S;++i){
if(t[i]){
cout << "INCONSISTENT STATE OF t[" << i << "] = " << t[i] << " DETECTED" << endl;
}
}
if(-1 == execlp("/bin/ls","ls","repro.cpp",(char*)NULL)){
cerr << "failed to execlp : " << strerror(errno) << endl;
_exit(-4);
}
}
return NULL;
}
Kali ini, saya melakukan dua loop sehingga yang kedua membatalkan hasil dari yang pertama, jadi di akhir tabel global t[]
harus kembali ke keadaan awal (yang menurut definisi adalah semua nol).
Jika memasuki proses anak membekukan pthreads lain yang membuat mereka tidak dapat memanggil vfork sampai anak saat ini menyelesaikan loop, maka array harus semua nol di akhirnya.
Dan saya mengkonfirmasi bahwa ketika saya menggunakan fork() alih-alih vfork() maka kode di atas tidak menghasilkan output apa pun.
Namun, ketika saya mengubah fork() menjadi vfork() saya mendapatkan banyak inkonsistensi dilaporkan ke stdout.
Eksperimen III
Satu eksperimen lagi dijelaskan di sini https://unix.stackexchange.com/a/163761/88901 – ini melibatkan pemanggilan sleep, tetapi sebenarnya hasilnya sama ketika saya menggantinya dengan for
lingkaran.
Jawaban yang Diterima:
Halaman manual Linux untuk vork
cukup spesifik:
vfork()
berbeda darifork(2)
dalam panggilan utas ditangguhkan sampai anak berakhir
Ini bukan keseluruhan proses, tapi memang utas pemanggilan . Perilaku ini tidak dijamin oleh POSIX atau standar lain, implementasi lain dapat melakukan hal yang berbeda (hingga dan termasuk hanya mengimplementasikan vfork
dengan garpu
biasa ).
(Rich Felker juga mencatat perilaku ini di vfork yang dianggap berbahaya.)
Menggunakan garpu
dalam program multi-utas cukup sulit untuk dipikirkan, memanggil vfork
setidaknya sama buruknya. Pengujian Anda penuh dengan perilaku yang tidak ditentukan, Anda bahkan tidak diizinkan untuk memanggil fungsi (apalagi melakukan I/O) di dalam vfork
'd anak, kecuali untuk exec
-ketik fungsi dan _exit
(bahkan tidak keluar
, dan kembali menyebabkan kekacauan).
Berikut adalah contoh yang diadaptasi dari Anda yang saya yakini hampir bebas dari perilaku tidak terdefinisi dengan asumsi kompiler/implementasi yang tidak menghasilkan panggilan fungsi untuk membaca dan menulis atom di int
s. (Satu-satunya masalah adalah menulis ke start
setelah vfork
– itu tidak diperbolehkan.) Penanganan kesalahan dihilangkan untuk mempersingkatnya.
#include<unistd.h>
#include<signal.h>
#include<errno.h>
#include<atomic>
#include<cstring>
#include<string>
#include<iostream>
std::atomic<int> start;
std::atomic<int> counter;
const int thread_count = 4;
void *vforker(void *){
std::cout << "vforker starting\n";
int pid=vfork();
if(pid == 0){
start = 1;
while (counter < (thread_count-1))
;
execlp("/bin/date","date",nullptr);
}
std::cout << "vforker done\n";
return nullptr;
}
void *job(void *){
while (start == 0)
;
counter++;
return NULL;
}
int main(){
signal(SIGPIPE,SIG_IGN);
signal(SIGCHLD,SIG_IGN);
pthread_t thread[thread_count];
counter = 0;
start = 0;
pthread_create(&(thread[0]), nullptr, vforker, nullptr);
for(int i=1;i<thread_count;++i)
pthread_create(&(thread[i]), nullptr, job, nullptr);
for(int i=0;i<thread_count;++i)
pthread_join(thread[i], nullptr);
}
Idenya adalah ini:utas normal menunggu (loop sibuk) untuk variabel global atom start
menjadi 1
sebelum menambah penghitung atom global. Utas yang melakukan vfork
set mulai
ke 1
di anak vfork, lalu menunggu (loop sibuk lagi) hingga utas lainnya menambah penghitung.
Jika utas lainnya ditangguhkan selama vfork
, tidak ada kemajuan yang dapat dibuat:utas yang ditangguhkan tidak akan pernah menambah counter
(mereka akan ditangguhkan sebelum start
disetel ke 1
), sehingga utas vforker akan terjebak dalam penantian sibuk yang tak terbatas.