Apa hal terdekat yang dimiliki Windows dengan fork ()?


124

Saya kira pertanyaan itu menjelaskan semuanya.

Saya ingin bercabang di Windows. Operasi apa yang paling mirip dan bagaimana cara menggunakannya.

Jawaban:


86

Cygwin memiliki garpu berfitur lengkap () di Windows. Jadi jika menggunakan Cygwin dapat diterima untuk Anda, maka masalahnya teratasi jika kinerja kasus tidak menjadi masalah.

Jika tidak, Anda dapat melihat bagaimana Cygwin mengimplementasikan fork (). Dari dokumen arsitektur Cygwin yang cukup tua :

5.6. Proses Pembuatan Panggilan garpu di Cygwin sangat menarik karena tidak dipetakan dengan baik di atas Win32 API. Ini membuatnya sangat sulit untuk diterapkan dengan benar. Saat ini, percabangan Cygwin adalah implementasi non-copy-on-write yang mirip dengan yang ada di versi awal UNIX.

Hal pertama yang terjadi ketika proses orang tua membagi proses anak adalah bahwa orang tua menginisialisasi spasi dalam tabel proses Cygwin untuk anak tersebut. Ini kemudian membuat proses anak ditangguhkan menggunakan panggilan Win32 CreateProcess. Selanjutnya, proses induk memanggil setjmp untuk menyimpan konteksnya sendiri dan menetapkan penunjuk ke ini di area memori bersama Cygwin (dibagi di antara semua tugas Cygwin). Ini kemudian mengisi bagian .data dan .bss anak dengan menyalin dari ruang alamatnya sendiri ke ruang alamat anak yang ditangguhkan. Setelah ruang alamat anak diinisialisasi, anak dijalankan sementara orang tua menunggu di mutex. Anak tersebut menemukan bahwa ia telah bercabang dan longjump menggunakan buffer lompat yang disimpan. Anak itu kemudian menyetel mutex yang ditunggunya oleh induknya dan memblokirnya di mutex lain. Ini adalah sinyal bagi induk untuk menyalin tumpukan dan heap-nya ke dalam anak, setelah itu melepaskan mutex yang ditunggu oleh anak dan kembali dari panggilan fork. Akhirnya, anak tersebut terbangun dari pemblokiran pada mutex terakhir, membuat ulang area yang dipetakan memori yang diteruskan kepadanya melalui area bersama, dan kembali dari fork itu sendiri.

Meskipun kami memiliki beberapa ide tentang bagaimana mempercepat implementasi fork kami dengan mengurangi jumlah pengalih konteks antara proses induk dan anak, fork hampir pasti selalu tidak efisien di bawah Win32. Untungnya, dalam banyak situasi, keluarga panggilan yang disediakan oleh Cygwin dapat diganti dengan pasangan fork / exec dengan hanya sedikit usaha. Panggilan ini dipetakan dengan rapi di atas Win32 API. Hasilnya, mereka jauh lebih efisien. Mengubah program driver kompiler untuk memanggil spawn dan bukan fork adalah perubahan yang sepele dan meningkatkan kecepatan kompilasi sebesar dua puluh hingga tiga puluh persen dalam pengujian kami.

Namun, spawn dan exec menghadirkan kesulitan mereka sendiri. Karena tidak ada cara untuk melakukan eksekusi sebenarnya di bawah Win32, Cygwin harus menciptakan ID Proses (PID) sendiri. Akibatnya, ketika suatu proses melakukan beberapa panggilan exec, akan ada beberapa PID Windows yang terkait dengan satu Cygwin PID. Dalam beberapa kasus, rintisan dari masing-masing proses Win32 ini mungkin tertinggal, menunggu proses Cygwin yang dijalankan mereka untuk keluar.

Kedengarannya banyak pekerjaan, bukan? Dan ya, itu slooooow.

EDIT: dokumen sudah usang, silakan lihat jawaban luar biasa ini untuk pembaruan


11
Ini adalah jawaban yang bagus jika Anda ingin menulis aplikasi Cygwin di windows. Tetapi secara umum itu bukan hal terbaik untuk dilakukan. Pada dasarnya, proses * nix dan Windows serta model utas sangat berbeda. CreateProcess () dan CreateThread () adalah API yang umumnya setara
Foredecker

2
Pengembang harus mengingat bahwa ini adalah mekanisme yang tidak didukung, dan IIRC memang cenderung rusak setiap kali beberapa proses lain pada sistem menggunakan injeksi kode.
Harry Johnston

1
Tautan penerapan yang berbeda tidak lagi valid.
PythonNut

Diedit untuk meninggalkan tautan jawaban lain saja
Laurynas Biveinis

@Foredecker, Sebenarnya Anda tidak boleh melakukannya meskipun Anda mencoba menulis "aplikasi cygwin". Ia mencoba meniru Unix forknamun menyelesaikan ini dengan solusi yang bocor dan Anda harus siap menghadapi situasi yang tidak terduga.
Pacerier

66

Saya tentu tidak tahu detailnya karena saya belum pernah melakukannya, tetapi NT API asli memiliki kemampuan untuk melakukan fork suatu proses (subsistem POSIX pada Windows membutuhkan kemampuan ini - Saya tidak yakin apakah subsistem POSIX bahkan didukung lagi).

Pencarian untuk ZwCreateProcess () akan memberi Anda beberapa detail lebih lanjut - misalnya sedikit informasi dari Maxim Shatskih ini :

Parameter terpenting di sini adalah SectionHandle. Jika parameter ini NULL, kernel akan memotong proses saat ini. Jika tidak, parameter ini harus menjadi pegangan dari objek bagian SEC_IMAGE yang dibuat pada file EXE sebelum memanggil ZwCreateProcess ().

Meskipun perhatikan bahwa Corinna Vinschen menunjukkan bahwa Cygwin ditemukan menggunakan ZwCreateProcess () masih tidak dapat diandalkan :

Iker Arizmendi menulis:

> Because the Cygwin project relied solely on Win32 APIs its fork
> implementation is non-COW and inefficient in those cases where a fork
> is not followed by exec.  It's also rather complex. See here (section
> 5.6) for details:
>  
> http://www.redhat.com/support/wpapers/cygnus/cygnus_cygwin/architecture.html

Dokumen ini agak tua, 10 tahun atau lebih. Meskipun kami masih menggunakan panggilan Win32 untuk meniru fork, metode ini telah berubah secara signifikan. Terutama, kami tidak lagi membuat proses anak dalam status ditangguhkan, kecuali jika struktur data tertentu memerlukan penanganan khusus di induknya sebelum disalin ke turunan. Dalam rilis 1.5.25 saat ini, satu-satunya kasus untuk anak yang ditangguhkan adalah soket terbuka di induknya. Rilis 1.7.0 mendatang tidak akan ditangguhkan sama sekali.

Salah satu alasan untuk tidak menggunakan ZwCreateProcess adalah hingga rilis 1.5.25 kami masih mendukung pengguna Windows 9x. Namun, dua upaya untuk menggunakan ZwCreateProcess pada sistem berbasis NT gagal karena satu dan lain alasan.

Akan sangat menyenangkan jika hal ini akan lebih baik atau sama sekali didokumentasikan, terutama beberapa struktur data dan bagaimana menghubungkan proses ke subsistem. Meskipun fork bukanlah konsep Win32, saya tidak melihat bahwa akan menjadi hal yang buruk untuk membuat fork lebih mudah diimplementasikan.


Ini jawaban yang salah. CreateProcess () dan CreateThread () adalah padanan umum.
Foredecker

2
Interix tersedia di Windows Vista Enterprise / Ultimate sebagai "Subsistem untuk Aplikasi UNIX": en.wikipedia.org/wiki/Interix
bk1e

15
@Foredecker - ini mungkin jawaban yang salah, tetapi CreateProcess () / CreateThread () mungkin juga salah. Itu tergantung pada apakah seseorang mencari 'cara Win32 untuk melakukan sesuatu' atau 'sedekat mungkin dengan semantik fork ()'. CreateProcess () berperilaku sangat berbeda dari fork (), yang merupakan alasan cygwin perlu melakukan banyak pekerjaan untuk mendukungnya.
Michael Burr

1
@jon: Saya telah mencoba memperbaiki tautan dan menyalin teks yang relevan ke dalam jawaban (jadi tautan rusak di masa mendatang bukanlah masalah). Namun, jawaban ini sudah cukup lama sehingga saya tidak 100% yakin kutipan yang saya temukan hari ini adalah apa yang saya maksudkan pada tahun 2009.
Michael Burr

4
Jika orang ingin " forkdengan segera exec", maka mungkin CreateProcess adalah kandidatnya. Tetapi forktanpa execsering diinginkan dan ini adalah apa yang mendorong orang untuk meminta secara nyata fork.
Aaron McDaid

37

Nah, jendela sebenarnya tidak memiliki sesuatu yang seperti itu. Terutama karena garpu dapat digunakan secara konseptual untuk membuat utas atau proses di * nix.

Jadi, saya harus mengatakan:

CreateProcess()/CreateProcessEx()

dan

CreateThread()(Saya pernah mendengar bahwa untuk aplikasi C, _beginthreadex()lebih baik).


17

Orang-orang telah mencoba menerapkan garpu di Windows. Ini adalah hal terdekat yang dapat saya temukan:

Diambil dari: http://doxygen.scilab.org/5.3/d0/d8f/forkWindows_8c_source.html#l00216

static BOOL haveLoadedFunctionsForFork(void);

int fork(void) 
{
    HANDLE hProcess = 0, hThread = 0;
    OBJECT_ATTRIBUTES oa = { sizeof(oa) };
    MEMORY_BASIC_INFORMATION mbi;
    CLIENT_ID cid;
    USER_STACK stack;
    PNT_TIB tib;
    THREAD_BASIC_INFORMATION tbi;

    CONTEXT context = {
        CONTEXT_FULL | 
        CONTEXT_DEBUG_REGISTERS | 
        CONTEXT_FLOATING_POINT
    };

    if (setjmp(jenv) != 0) return 0; /* return as a child */

    /* check whether the entry points are 
       initilized and get them if necessary */
    if (!ZwCreateProcess && !haveLoadedFunctionsForFork()) return -1;

    /* create forked process */
    ZwCreateProcess(&hProcess, PROCESS_ALL_ACCESS, &oa,
        NtCurrentProcess(), TRUE, 0, 0, 0);

    /* set the Eip for the child process to our child function */
    ZwGetContextThread(NtCurrentThread(), &context);

    /* In x64 the Eip and Esp are not present, 
       their x64 counterparts are Rip and Rsp respectively. */
#if _WIN64
    context.Rip = (ULONG)child_entry;
#else
    context.Eip = (ULONG)child_entry;
#endif

#if _WIN64
    ZwQueryVirtualMemory(NtCurrentProcess(), (PVOID)context.Rsp,
        MemoryBasicInformation, &mbi, sizeof mbi, 0);
#else
    ZwQueryVirtualMemory(NtCurrentProcess(), (PVOID)context.Esp,
        MemoryBasicInformation, &mbi, sizeof mbi, 0);
#endif

    stack.FixedStackBase = 0;
    stack.FixedStackLimit = 0;
    stack.ExpandableStackBase = (PCHAR)mbi.BaseAddress + mbi.RegionSize;
    stack.ExpandableStackLimit = mbi.BaseAddress;
    stack.ExpandableStackBottom = mbi.AllocationBase;

    /* create thread using the modified context and stack */
    ZwCreateThread(&hThread, THREAD_ALL_ACCESS, &oa, hProcess,
        &cid, &context, &stack, TRUE);

    /* copy exception table */
    ZwQueryInformationThread(NtCurrentThread(), ThreadBasicInformation,
        &tbi, sizeof tbi, 0);
    tib = (PNT_TIB)tbi.TebBaseAddress;
    ZwQueryInformationThread(hThread, ThreadBasicInformation,
        &tbi, sizeof tbi, 0);
    ZwWriteVirtualMemory(hProcess, tbi.TebBaseAddress, 
        &tib->ExceptionList, sizeof tib->ExceptionList, 0);

    /* start (resume really) the child */
    ZwResumeThread(hThread, 0);

    /* clean up */
    ZwClose(hThread);
    ZwClose(hProcess);

    /* exit with child's pid */
    return (int)cid.UniqueProcess;
}
static BOOL haveLoadedFunctionsForFork(void)
{
    HANDLE ntdll = GetModuleHandle("ntdll");
    if (ntdll == NULL) return FALSE;

    if (ZwCreateProcess && ZwQuerySystemInformation && ZwQueryVirtualMemory &&
        ZwCreateThread && ZwGetContextThread && ZwResumeThread &&
        ZwQueryInformationThread && ZwWriteVirtualMemory && ZwClose)
    {
        return TRUE;
    }

    ZwCreateProcess = (ZwCreateProcess_t) GetProcAddress(ntdll,
        "ZwCreateProcess");
    ZwQuerySystemInformation = (ZwQuerySystemInformation_t)
        GetProcAddress(ntdll, "ZwQuerySystemInformation");
    ZwQueryVirtualMemory = (ZwQueryVirtualMemory_t)
        GetProcAddress(ntdll, "ZwQueryVirtualMemory");
    ZwCreateThread = (ZwCreateThread_t)
        GetProcAddress(ntdll, "ZwCreateThread");
    ZwGetContextThread = (ZwGetContextThread_t)
        GetProcAddress(ntdll, "ZwGetContextThread");
    ZwResumeThread = (ZwResumeThread_t)
        GetProcAddress(ntdll, "ZwResumeThread");
    ZwQueryInformationThread = (ZwQueryInformationThread_t)
        GetProcAddress(ntdll, "ZwQueryInformationThread");
    ZwWriteVirtualMemory = (ZwWriteVirtualMemory_t)
        GetProcAddress(ntdll, "ZwWriteVirtualMemory");
    ZwClose = (ZwClose_t) GetProcAddress(ntdll, "ZwClose");

    if (ZwCreateProcess && ZwQuerySystemInformation && ZwQueryVirtualMemory &&
        ZwCreateThread && ZwGetContextThread && ZwResumeThread &&
        ZwQueryInformationThread && ZwWriteVirtualMemory && ZwClose)
    {
        return TRUE;
    }
    else
    {
        ZwCreateProcess = NULL;
        ZwQuerySystemInformation = NULL;
        ZwQueryVirtualMemory = NULL;
        ZwCreateThread = NULL;
        ZwGetContextThread = NULL;
        ZwResumeThread = NULL;
        ZwQueryInformationThread = NULL;
        ZwWriteVirtualMemory = NULL;
        ZwClose = NULL;
    }
    return FALSE;
}

4
Perhatikan bahwa sebagian besar pemeriksaan kesalahan hilang - misalnya ZwCreateThread mengembalikan nilai NTSTATUS yang dapat diperiksa menggunakan makro SUCCEEDED dan FAILED.
BCran

1
Apa yang terjadi jika forkmacet, apakah program macet, atau utas macet begitu saja? Jika program membuat crash, maka ini sebenarnya bukan forking. Hanya penasaran, karena saya sedang mencari solusi nyata, dan berharap ini bisa menjadi alternatif yang layak.
leetNightshade

1
Saya ingin mencatat bahwa ada bug dalam kode yang disediakan. hasLoadedFunctionsForFork adalah fungsi global di header, tetapi fungsi statis di file c. Keduanya harus mendunia. Dan saat ini garpu macet, menambahkan pemeriksaan kesalahan sekarang.
leetNightshade

Situs sudah mati dan saya tidak tahu bagaimana saya dapat mengumpulkan contoh di sistem saya sendiri. Saya berasumsi saya kehilangan beberapa header atau termasuk yang salah, bukan? (contoh tidak menunjukkannya.)
Paul Stelian

6

Sebelum Microsoft memperkenalkan opsi "Linux subsystem for Windows" mereka yang baru, CreateProcess()adalah hal yang paling dekat dengan Windows fork(), tetapi Windows mengharuskan Anda menentukan executable untuk dijalankan dalam proses itu.

Proses pembuatan UNIX sangat berbeda dengan Windows. Its fork()panggilan pada dasarnya duplikat proses saat ini hampir secara total, masing-masing dalam ruang alamat mereka sendiri, dan terus berjalan secara terpisah. Meskipun prosesnya sendiri berbeda, mereka masih menjalankan program yang sama . Lihat di sini untuk gambaran umum yang baik tentang fork/execmodel tersebut.

Kembali ke arah lain, padanan dari Windows CreateProcess()adalah fork()/exec() pasangan fungsi di UNIX.

Jika Anda mem-porting perangkat lunak ke Windows dan Anda tidak keberatan dengan lapisan terjemahan, Cygwin menyediakan kemampuan yang Anda inginkan tetapi itu agak kludgey.

Tentu saja, dengan yang baru subsistem Linux , hal yang paling dekat Windows memiliki untuk fork()ini benar-benar fork() :-)


2
Jadi, dengan adanya WSL, dapatkah saya menggunakan forkrata-rata aplikasi non-WSL?
Caesar


4

"segera setelah Anda ingin mengakses file atau printf maka io ditolak"

  • Anda tidak dapat memiliki kue dan memakannya juga ... di msvcrt.dll, printf () didasarkan pada API Konsol, yang dengan sendirinya menggunakan lpc untuk berkomunikasi dengan subsistem konsol (csrss.exe). Koneksi dengan csrss dimulai saat proses start-up, yang berarti bahwa setiap proses yang memulai eksekusinya "di tengah" akan melewatkan langkah tersebut. Kecuali Anda memiliki akses ke kode sumber sistem operasi, tidak ada gunanya mencoba menyambung ke csrs secara manual. Sebagai gantinya, Anda harus membuat subsistem Anda sendiri, dan karenanya menghindari fungsi konsol dalam aplikasi yang menggunakan fork ().

  • setelah Anda mengimplementasikan subsistem Anda sendiri, jangan lupa untuk juga menduplikasi semua pegangan induk untuk proses anak ;-)

"Selain itu, Anda mungkin sebaiknya tidak menggunakan fungsi Zw * kecuali Anda dalam mode kernel, Anda mungkin harus menggunakan fungsi Nt * sebagai gantinya."

  • Ini salah Saat diakses dalam mode pengguna, sama sekali tidak ada perbedaan antara Zw *** Nt ***; ini hanyalah dua nama yang diekspor (ntdll.dll) berbeda yang merujuk ke alamat virtual (relatif) yang sama.

ZwGetContextThread (NtCurrentThread (), & konteks);

  • mendapatkan konteks utas saat ini (berjalan) dengan memanggil ZwGetContextThread salah, kemungkinan macet, dan (karena panggilan sistem tambahan) juga bukan cara tercepat untuk menyelesaikan tugas.

2
Tampaknya ini tidak menjawab pertanyaan utama tetapi menjawab beberapa jawaban lain yang berbeda, dan mungkin akan lebih baik menjawab secara langsung masing-masing untuk kejelasan dan untuk mempermudah mengikuti apa yang sedang terjadi.
Leigh

Anda tampaknya berasumsi bahwa printf selalu menulis ke konsol.
Jasen

3

semantik fork () diperlukan jika anak memerlukan akses ke status memori aktual dari induk saat fork () instan dipanggil. Saya memiliki perangkat lunak yang bergantung pada mutex implisit dari penyalinan memori saat garpu instan () dipanggil, yang membuat utas tidak mungkin digunakan. (Ini diemulasikan pada platform * nix modern melalui semantik copy-on-write / update-memory-table.)

Yang terdekat yang ada di Windows sebagai syscall adalah CreateProcess. Hal terbaik yang dapat dilakukan adalah agar induknya membekukan semua utas lainnya selama ia menyalin memori ke ruang memori proses baru, lalu mencairkannya. Baik kelas Cygwin frok [sic] maupun kode Scilab yang diposting Eric des Courtis tidak melakukan pembekuan thread, yang bisa saya lihat.

Selain itu, Anda mungkin sebaiknya tidak menggunakan fungsi Zw * kecuali Anda dalam mode kernel, Anda mungkin harus menggunakan fungsi Nt * sebagai gantinya. Ada cabang tambahan yang memeriksa apakah Anda dalam mode kernel dan, jika tidak, melakukan semua pemeriksaan batas dan verifikasi parameter yang selalu dilakukan Nt *. Jadi, sangat kurang efisien untuk memanggilnya dari mode pengguna.


Informasi yang sangat menarik mengenai simbol yang diekspor Zw *, terima kasih.
Andon M. Coleman

Perhatikan bahwa fungsi Zw * dari ruang pengguna masih dipetakan ke fungsi Nt * di ruang kernel, demi keamanan. Atau setidaknya mereka harus melakukannya.
Paul Stelian


2

Tidak ada cara mudah untuk meniru fork () di Windows.

Saya menyarankan Anda untuk menggunakan utas sebagai gantinya.


Nah, dalam keadilan, pelaksana forkadalah persis apa yang Cygwin lakukan. Tapi, jika Anda pernah membaca tentang bagaimana mereka melakukannya, "tidak mudah" adalah kesalahpahaman yang besar :-)
paxdiablo


2

Seperti jawaban lain yang telah disebutkan, NT (kernel yang mendasari versi modern Windows) memiliki persamaan dengan Unix fork (). Bukan itu masalahnya.

Masalahnya adalah bahwa mengkloning seluruh status proses secara umum bukanlah hal yang waras untuk dilakukan. Ini sama benarnya di dunia Unix seperti di Windows, tetapi di dunia Unix, fork () digunakan sepanjang waktu, dan perpustakaan dirancang untuk menghadapinya. Perpustakaan Windows tidak.

Misalnya, sistem DLL kernel32.dll dan user32.dll mempertahankan sambungan pribadi ke proses server Win32 csrss.exe. Setelah bercabang, ada dua proses di ujung klien dari koneksi tersebut, yang akan menyebabkan masalah. Proses anak harus memberi tahu csrss.exe keberadaannya dan membuat koneksi baru - tetapi tidak ada antarmuka untuk melakukannya, karena pustaka ini tidak dirancang dengan fork ().

Jadi Anda punya dua pilihan. Salah satunya adalah melarang penggunaan kernel32 dan user32 dan pustaka lain yang tidak dirancang untuk bercabang - termasuk pustaka apa pun yang menautkan secara langsung atau tidak langsung ke kernel32 atau user32, yang hampir semuanya. Ini berarti Anda tidak dapat berinteraksi dengan desktop Windows sama sekali, dan terjebak di dunia Unixy Anda yang terpisah. Ini adalah pendekatan yang diambil oleh berbagai subsistem Unix untuk NT.

Opsi lainnya adalah menggunakan semacam peretasan yang mengerikan untuk mencoba membuat perpustakaan yang tidak sadar bekerja dengan fork (). Itulah yang dilakukan Cygwin. Ini menciptakan proses baru, memungkinkannya menginisialisasi (termasuk mendaftarkan dirinya dengan csrss.exe), lalu menyalin sebagian besar status dinamis dari proses lama dan berharap yang terbaik. Sungguh mengherankan saya bahwa ini pernah berhasil. Itu pasti tidak bekerja dengan andal - bahkan jika tidak gagal secara acak karena konflik ruang alamat, perpustakaan apa pun yang Anda gunakan mungkin diam-diam dibiarkan dalam keadaan rusak. Klaim jawaban yang diterima saat ini bahwa Cygwin memiliki "garpu berfitur lengkap ()" adalah ... meragukan.

Ringkasan: Dalam lingkungan seperti Interix, Anda bisa bercabang dengan memanggil fork (). Jika tidak, cobalah untuk melepaskan diri Anda dari keinginan untuk melakukannya. Bahkan jika Anda menargetkan Cygwin, jangan gunakan fork () kecuali Anda benar-benar harus melakukannya.


Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.