fork() + exec() से आगे
(lwn.net)- spawn templates उन applications के लिए Linux kernel में process creation का एक प्रस्ताव है जो एक ही executable को बार-बार चलाते हैं, ताकि kernel executable की जानकारी cache करके बाद के process start को तेज़ कर सके
- fork() में child process के लिए memory सहित पूरा process state कॉपी करना पड़ता है, और उसके तुरंत बाद आने वाला exec() अक्सर उस memory को फेंक देता है, जिससे पारंपरिक pattern में inefficiency पैदा होती है
- spawn_template_create()
execfdया absolute pathfilenameमें से किसी एक के ज़रिए executable को निर्दिष्ट करके template file descriptor लौटाता है, और kernel उस फ़ाइल को खोलकर तेज़ execution के लिए ज़रूरी जानकारी cache करता है - spawn_template_spawn() सामान्य fork()/exec() path के क़रीब तरीके से काम करता है, नए file execution पर लागू checks को बनाए रखता है, और cover letter के benchmark में लगभग 2% सुधार दर्ज किया गया {p:2}
- pidfd-आधारित empty process creation और
pidfd_config()configuration को बेहतर approach माना जा रहा है, और लक्ष्य user space मेंposix_spawn()implementation को support करना है
Unix process creation model की सीमाएँ
- Unix की शुरुआत से fork() parent की copy के रूप में child process बनाता है, और exec() मौजूदा process की जगह नया program चलाने वाला मुख्य process-oriented system call रहा है
- Linux kernel में यही मूल functionality clone() और execve() के नाम से अधिक जानी जाती है
- इस process creation model में elegance भी है और कमियाँ भी; Li Chen का spawn templates proposal अपने मौजूदा रूप में Linux kernel में स्वीकार नहीं किया जाएगा, लेकिन यह भविष्य में नए process-creation primitive की दिशा खोल सकता है
- fork() child process बनाने के लिए memory सहित पूरा process state कॉपी करने वाला अपेक्षाकृत महंगा system call है
- वर्षों में कई optimizations हुए हैं, लेकिन fork() मूल रूप से अब भी costly operation है
- बहुत बार fork() call के तुरंत बाद exec() आता है, और exec() child के लिए कॉपी की गई सारी memory को छोड़ देता है
- vfork() जैसे optimization प्रयास हुए हैं, लेकिन fork() के बाद exec() वाला pattern अब भी जितना सस्ता हो सकता है उससे ज़्यादा महंगा है
Spawn templates
- Li Chen का patch set fork() और exec() pattern को optimize करने के लिए उन applications पर केंद्रित है जो एक ही executable को बार-बार चलाते हैं
- उदाहरण के तौर पर repository content information पाने के लिए Git को बार-बार चलाने वाला program ऐसी स्थिति में आता है
- ऐसे मामलों में program कई executions में setup cost को बाँटने के लिए template बनाता है, और उसी template से invocation को तेज़ करता है
- template creation के लिए
spawn_template_create()system call का उपयोग होता है- signature:
int spawn_template_create(struct spawn_template_create_args *args, size_t args_size);
- signature:
- यह call executable template को दर्शाने वाला file descriptor लौटाता है
- executable को file descriptor
execfdया absolute pathfilenameमें से किसी एक से निर्दिष्ट करना होता है; दोनों को एक साथ इस्तेमाल नहीं किया जा सकता - kernel निर्दिष्ट फ़ाइल खोलता है और बाद में उसे तेज़ी से execute करने के लिए ज़रूरी कई जानकारियाँ cache करता है
- हर execution में अलग arguments, environment, file descriptor changes और signal handling changes हो सकते हैं
- execution की विस्तृत जानकारी
spawn_template_spawn_argsstructure में रखी जाती हैargvprogram को दिए जाने वाले arguments की सूची की ओर इशारा करने वाला pointer हैenvpprogram environment की ओर इशारा करने वाला pointer हैactionsfile descriptor और signal handling changes देने वालेspawn_template_actionarray का pointer है
spawn_template_actionमेंtype,flags,fd,newfd,argfields होती हैं- अगर child में file descriptor 4 बंद करना हो तो
typeकोSPAWN_TEMPLATE_ACTION_CLOSEऔरfdको 4 पर सेट किया जाता है - दूसरे actions file descriptor duplication, file open, working directory change और signal handling change को support करते हैं
- अगर child में file descriptor 4 बंद करना हो तो
- execution information भरने के बाद
spawn_template_spawn()से नया process चलाया जाता है- signature:
int spawn_template_spawn(int template_fd, struct spawn_template_spawn_args *args, int args_size);
- signature:
- इसका अंदरूनी व्यवहार सामान्य fork()/exec() path के क़रीब है
- नए file execution पर लागू होने वाले सामान्य checks सभी वैसे ही बने रहते हैं
- template में cache की गई जानकारी से पूरे creation flow की गति बढ़ती है
- cover letter के benchmark नतीजे लगभग 2% improvement दिखाते हैं, और अपेक्षित pattern वाले applications में यह फर्क मायने रख सकता है {p:2}
posix_spawn() की ओर
- Mateusz Guzik का आकलन है कि “पूरा fork + exec idiom भयानक है और इसे हट जाना चाहिए”
- patch set का अजीब पहलू यह है कि यह fork() वाले हिस्से को जस का तस छोड़ देता है, जबकि लागत का बड़ा हिस्सा वहीं माना जाता है
- optimization का लक्ष्य मौजूदा process की copy हटाकर “pristine process” बनाना होना चाहिए
- Christian Brauner का कहना है कि exec के लिए builder API का विचार “इतना अजीब नहीं” है
- लेकिन नई API को मौजूदा pidfd abstraction के ऊपर बनाना बेहतर approach माना जा रहा है
- ठोस details नहीं दी गईं, लेकिन pidfd_open() में empty process बनाने का option जोड़ना सही दिशा माना गया
- इसके बाद नए
pidfd_config()system call को कई बार बुलाकर environment, execute की जाने वाली image आदि जैसी मनचाही settings नए process पर लागू की जा सकती हैं pidfd_config()की भूमिका fsconfig() जैसी होगी- नए interface का एक महत्वपूर्ण लक्ष्य user space में posix_spawn() implementation को support करना है
- posix_spawn() fork()/exec() pattern के विकल्प के रूप में उपयुक्त है
- मौजूदा implementation अंदरूनी तौर पर fork() और exec() को छिपाती है, जबकि native implementation की संरचना अलग होगी
- Li Chen ने भी माना कि Brauner द्वारा व्यापक रूप से रेखांकित API बेहतर लगती है, और आगे का काम उसी दिशा में ले जाने की योजना है
- Linux kernel में spawn templates नहीं आएँगे, लेकिन अगर आगे का काम सफल रहा तो Linux के पास एक सही posix_spawn() implementation हो सकता है
1 टिप्पणियां
Hacker News टिप्पणियाँ
संबंधित चर्चा के रूप में A fork() in the road नाम का पेपर है: https://www.microsoft.com/en-us/research/wp-content/uploads/...
इसके abstract में कहा गया है कि Unix का
fork()+exec()संयोजन प्रेरणादायक डिज़ाइन होने की आम धारणा के विपरीत, 1970 के दशक की मशीनों और प्रोग्रामों के लिए एक चतुर hack था, लेकिन अब आधुनिक programmers के लिए एक खराब abstraction है और operating system implementation को भी सीमित करता हैइसका मत है कि इसे operating system की first-class primitive सुविधा के रूप में बनाए रखने के बजाय एक ऐतिहासिक अवशेष की तरह पढ़ाया जाना चाहिए, और यह वह तरीका नहीं होना चाहिए जिससे छात्र सबसे पहले process creation सीखें
fork()+exec()ऐसा इसलिए बना क्योंकि इसका उद्देश्य इतने बड़े प्रोग्राम चलाना था कि वे parent program के साथ memory में एक साथ समा नहीं सकते थेमूल implementation में
fork()कॉल होने पर fork करने वाले program को disk पर swap out कर दिया जाता था, फिर control लौटने से पहले process table entry को duplicate और adjust किया जाता था ताकि memory में मौजूद process और swap out हुआ process दोनों बन जाएँ, और memory में मौजूद वाला control पाकरexec()कॉल कर सकेइस तरीके की वजह से छोटे PDP-11 मशीनों पर भी बड़े program चलाए जा सके, और यह उस दौर में ज़रूरी था जब memory बहुत महँगी थी
दिलचस्प बात यह है कि QNX में program loading operating system के अंदर नहीं बल्कि library में है। यह executable header पढ़ती है, memory allocate करती है, program को load करके execution के लिए तैयार करती है, फिर शुरू करने वाली
.soसे link करती है, और program loader बिना विशेषाधिकार वाले user space में चलता है। शायद यही तरीका सही के अधिक करीब हैfork()का उपयोग नहीं करने वाला सबसे व्यापक रूप से इस्तेमाल किया जाने वाला “बड़ा” operating system है, उसमें process creation बहुत धीमा हैमैं इस बात से सहमत हूँ कि
fork()के अलावा भी कोई primitive होनी चाहिए, लेकिन यह पक्का नहीं कि performance इसका सबसे मजबूत तर्क हैfork()सहित scalable interfaces के सूक्ष्म पहलुओं को कवर करता है: The Scalable Commutativity Rule: Designing Scalable Software for Multicore Processors https://people.csail.mit.edu/nickolai/papers/clements-sc.pdffork()zygote pattern के लिए बेहतरीन हैउतना ही efficient और elegant optimization सोच पाना मुश्किल है
हाल में मुझे एक अस्पष्ट bug मिला जो fork किए गए process में और ज़्यादा file descriptors बंद करने की ज़रूरत के कारण हुआ
मेरे अनुभव में “मुझे current process की एक copy चाहिए” की तुलना में “मुझे एक बिल्कुल नया process चाहिए” कहीं ज़्यादा आम है, लेकिन अजीब बात यह है कि बाद वाली बात को सीधे व्यक्त करने का तरीका नहीं है; केवल पहले clone करो और बाद में post-facto सुधार करके उसका लगभग रूप बना लो
O_CLOEXECसे हल नहीं हो जाता?posix_spawnका काम नहीं है?यह कहना कि “
fork()अपेक्षाकृत महँगा system call है, और child process के लिए memory सहित पूरे process state को copy करना पड़ता है। वर्षों में बहुत optimization हुए हैं, लेकिन मूल रूप से यह एक महँगा काम है। इससे भी बुरी बात यह है किfork()कॉल के तुरंत बाद अक्सरexec()आ जाता है, जिससे child के लिए मेहनत से copy की गई सारी memory फेंक दी जाती है” और फिर copy-on-write का ज़िक्र न करना अजीब हैयह वही optimization है जो पूरी memory को वास्तव में copy होने से बचाती है, लेकिन यहाँ उसका उल्लेख नहीं है
भले ही actual pages जिस memory की ओर इशारा करते हैं वह shared रहे, इन structures की copies रखने के लिए नई pages allocate करनी पड़ती हैं। और उन structures को पूरा traverse करके copy करना अपने आप में अब भी महँगा है
fork()memory को खुद copy नहीं करता, लेकिन page tables फिर भी copy करनी पड़ती हैंअगर process के पास कई दर्जन GB RAM हो, तो
fork()में लंबा समय लग सकता है, और Redis जब भी.rdbfile dump करता है या binary log यानी AOF को दोबारा लिखता है, यह एक बार होता है2012 में भी एक पोस्ट थी जिसमें इस काम की ऊँची लागत दिखाई गई थी: https://redis.io/blog/testing-fork-time-on-awsxen-infrastruc...
लगभग 25GB RAM इस्तेमाल करने वाले
m2.xlargeपरfork()में 5.67 सेकंड लगे थे। यह देखते हुए कि Redis clients आम तौर पर ज़्यादातर कामों में single-digit millisecond latency देखते हैं, यह बहुत लंबा pause है। और यह सिर्फ page table copy समय हैयह हैरान करने वाला है कि huge page का ज़िक्र नहीं है, क्योंकि यहाँ वह एक मुख्य विचार जैसा लगता है। 14 साल बाद hardware तेज़ हुआ होगा, लेकिन Redis instances भी संभवतः ज़्यादा RAM इस्तेमाल करते होंगे, इसलिए इस benchmark को फिर से चलाना दिलचस्प होगा
fork()को उसका setup cost चुकाना पड़ता है। अगर parent process में बहुत busy threads हों, तो उदाहरण के लिए Java मेंexec()चलने से पहले काफ़ी अनावश्यक copy-on-write हो सकता हैबड़े virtual memory size वाले program को fork करना धीमा होता है, यह एक अच्छी तरह से जाना-पहचाना मुद्दा है
fork()+exec()मॉडल की खूबसूरती इस बात में है किfork()के बाद सामान्य API को वैसे ही इस्तेमाल करके हर तरह की configuration की जा सकती हैअब तक देखे गए combined call वाले विकल्प बुनियादी तौर पर कमजोर लगे, क्योंकि हर configuration option को call parameters में जोड़ना पड़ता है, और बाद में extend भी करना होता है बिना इसे अव्यवस्थित बनाए
fork()/exec()कुछ मामलों में उपयोगी हो सकता है, लेकिन अगर APIspidfdargument लें तो यह काफी अच्छा लग सकता है. 0 का मतलब current process रखा जा सकता हैसमस्या
setuid/setgidbinaries जैसी चीज़ों में होगी, लेकिन इस मामले मेंexecमें special handling करना बेहतर हो सकता हैउदाहरण के लिए
pidfd_t ps = spawn();से एक stopped process बनाया जाए, फिरsetuid(ps, 33);,capset(ps, ...);,socket(ps, ...);,mmap(ps, ...);,process_vm_writev(ps, ...);,exec(ps, ...);,signal(ps, SIGCONT);की तरह इसे configure किया जा सकता हैयह इस बात की आलोचना भी है कि आम system call APIs आम तौर पर इस संभावना पर पर्याप्त विचार नहीं करते कि “अगर मैं यह काम किसी दूसरे process पर करना चाहूँ, जिस पर मेरी access permission हो?”
इस तरह
fork()में thread safety भी कुछ हद तक संभव हो सकती हैहालांकि मैं इस बात से सहमत हूँ कि
CreateProcessजैसा ढेर सारे parameters लेने वाला तरीका user-space API के रूप में बहुत अच्छा नहीं हैउदाहरण के लिए ऐसे APIs हैं जो किसी object को file descriptor number 4 बना देती हैं, और फिर program चलाकर उससे descriptor 4 पर वही object ढूँढने की उम्मीद की जाती है. यह अजीब है
Windows में बहुत सारी कमियाँ होने के बावजूद
fork()+exec()नहीं है, बल्कि यह मुख्य रूप से process creation के तरीकों के लिए options देता है. यह खूबसूरत नहीं था, लेकिन दिशा सही थीfork()+exec()के इतिहास की path dependence हैअगर किसी दूसरी दुनिया में
fork()+exec()कभी होता ही नहीं, तो उन “general APIs” में से कई के पास किसी दूसरे process की settings बदलने के लिए explicitpidargument होता. Fuchsia मोटे तौर पर ऐसा ही करता हैइस दुनिया के कई फायदे हैं. सबसे साफ़ फायदा यह है कि configuration errors report करने के लिए किसी अलग IPC mechanism को जादुई तरीके से गढ़ने की ज़रूरत नहीं पड़ती, और child की properties adjust करने वाला manager process रखना भी काफी उपयोगी हो सकता है. debugger को यह खास तौर पर पसंद आएगा
fork()को हटाने का सही तरीका यह है कि process state बदलने वाले general APIs explicit process handle लेंतब उसी API से empty process को configure किया जा सकता है, और IPC या debugging जैसे दूसरे तरीकों के साथ भी इसे जोड़ा जा सकता है
अगर process
ptrace-attached state में और बिना threads के शुरू हो, तो configuration चरण में उससे system calls ज़बरदस्ती कराए जा सकते हैं. Linux में तो “threadless process” का concept भी नहीं है, इसलिए शायद एक dummy thread चाहिए होगायह ग़लतफ़हमी हैरान करने वाली हद तक आम है कि
fork()सस्ता है, जबकि process size के हिसाब से यह O(N) है, और हमेशा से रहा हैसही है, यह copy-on-write है. लेकिन process size और उसे represent करने के लिए चाहिए page table entries की संख्या के बीच linear relation होता है
Chen का patch reject होना हैरान करने वाला नहीं है. यह बहुत ही खास use case है, इसलिए इसे support करने की value कम है
shell developer के नज़रिए से मैं इस निष्कर्ष से सहमत हूँ कि “developers संभवतः ऐसी native implementation का स्वागत करेंगे जो मौजूदा implementation की तरह अंदर
fork()औरexec()को छिपाती नहीं है”fork()पहली बार सीखने से ही conceptually भयानक लगा. अगर आप एक ही काम करना चाहते हैं, यानी process start करना, तो उसके लिए एक अलग और असंबंधित काम, यानी current process को fork करने वाला पहेली जैसा मंत्र क्यों करना पड़े?लेख के उदाहरण की तरह, जहाँ एक process बहुत सारे
gitchild processes चलाता है, उसे सबसे अच्छे तरीके से कैसे handle किया जाए, यह जानने की जिज्ञासा है. कोई लंबे समय तक चलने वाला parent task बीच-बीच में बार-बारgitको शुरू से launch करे, यह समझदारी नहीं लगती; तो वही नतीजा देने वाला low-cost abstraction क्या होगा?fork()conceptually सरल है. अगर आप दूसरी layers को न घसीटें, तो process शुरू करने के लिए एकमात्र चीज़ जिससे आप निश्चित रूप से परिचित हैं, वह आप स्वयं हैंवरना process बनाना, उसे चलाने के लिए किसी चीज़ से भरना, और फिर उसे execute होने के लिए schedule करना जैसे कई चरण चाहिए. या फिर Win32 की तरह file system, object loader, linker जैसी दूसरी layers को स्थायी रूप से गूँथकर मिला देना होगा
fork()+exec()मॉडल बिल्कुल समझ में नहीं आया था. अब समझता हूँ कि यह बस एक historical oddity है, लेकिन आज भी कुछ लोग ऐसे हैं जो दिखाते हैं मानोfork()+exec()सचमुच अच्छा होlibgit2है. pipes या sockets के ज़रिए किसीgitdसे बात करने का तरीका कल्पना किया जा सकता है, लेकिन यह क्यों अच्छा विचार होगा, समझ नहीं आता. नहीं तो process launch करना पड़ेगाexec/forkको replace करना मुश्किल होने की वजह यह है कि नए process को आम तौर पर configure करना पड़ता है. उदाहरण के लिए signal handler settings, file descriptors को बंद या खोलना, namespace switch करना,seccompसेट करना, privileges adjust करना ज़रूरी हो सकता हैलेकिन इसके लिए जो system calls हैं वे अभी सिर्फ current process पर लागू होते हैं, इसलिए किसी विकल्प की ज़रूरत पड़ती है. लेख का प्रस्ताव इसके लिए नया API बनाना था
मेरे हिसाब से
spawnजैसा नया system call एक empty process बना सकता है, उसमें एक हल्का loader डाल सकता है, और उसे मनमाना configuration data दे सकता है. फिर loader process को configure करे और main program कोexec()करेइससे memory को fork किए बिना मौजूदा API बरकरार रखी जा सकती है, लेकिन file descriptors और दूसरी चीज़ें फिर भी duplicate करनी होंगी
अगर यह मज़ाक नहीं था तो माफ़ कीजिए, लेकिन
posix_spawn()पहले से मौजूद है और glibc मेंforkबसclone()का alias हैभले ही यह मूल प्रस्ताव से बिल्कुल एक जैसा न हो,
fork()/exec()सच में legacy के काफ़ी करीब हैंअगर
forkऔरexeccopy-on-write प्रकृति से आगे बढ़कर लगातार और algebraic behavior दिखा सकें, तो वे न सिर्फ ज़्यादा उपयोगी होंगे बल्कि इस्तेमाल करने में और दिलचस्प भी. उदाहरण के लिए इन्हें lazy evaluation में इस्तेमाल किया जा सकता हैइस पुराने API पर Hacker News में काफी चर्चा हुई है, उदाहरण के लिए https://news.ycombinator.com/item?id=31739794