2 पॉइंट द्वारा GN⁺ 3 시간 전 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • 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 path filename में से किसी एक के ज़रिए 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);
  • यह call executable template को दर्शाने वाला file descriptor लौटाता है
  • executable को file descriptor execfd या absolute path filename में से किसी एक से निर्दिष्ट करना होता है; दोनों को एक साथ इस्तेमाल नहीं किया जा सकता
  • kernel निर्दिष्ट फ़ाइल खोलता है और बाद में उसे तेज़ी से execute करने के लिए ज़रूरी कई जानकारियाँ cache करता है
  • हर execution में अलग arguments, environment, file descriptor changes और signal handling changes हो सकते हैं
  • execution की विस्तृत जानकारी spawn_template_spawn_args structure में रखी जाती है
    • argv program को दिए जाने वाले arguments की सूची की ओर इशारा करने वाला pointer है
    • envp program environment की ओर इशारा करने वाला pointer है
    • actions file descriptor और signal handling changes देने वाले spawn_template_action array का pointer है
    विज्ञापन
  • spawn_template_action में type, flags, fd, newfd, arg fields होती हैं
    • अगर child में file descriptor 4 बंद करना हो तो type को SPAWN_TEMPLATE_ACTION_CLOSE और fd को 4 पर सेट किया जाता है
    • दूसरे actions file descriptor duplication, file open, working directory change और signal handling change को support करते हैं
  • execution information भरने के बाद spawn_template_spawn() से नया process चलाया जाता है
    • signature: int spawn_template_spawn(int template_fd, struct spawn_template_spawn_args *args, int args_size);
  • इसका अंदरूनी व्यवहार सामान्य 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 टिप्पणियां

 
GN⁺ 3 시간 전
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 में चलता है। शायद यही तरीका सही के अधिक करीब है
    • यह दिलचस्प है कि Windows, जो fork() का उपयोग नहीं करने वाला सबसे व्यापक रूप से इस्तेमाल किया जाने वाला “बड़ा” operating system है, उसमें process creation बहुत धीमा है
      मैं इस बात से सहमत हूँ कि fork() के अलावा भी कोई primitive होनी चाहिए, लेकिन यह पक्का नहीं कि performance इसका सबसे मजबूत तर्क है
    • यह paper भी अच्छा है, और संदर्भ [29] भी खास तौर पर अच्छा लगा क्योंकि वह fork() सहित scalable interfaces के सूक्ष्म पहलुओं को कवर करता है: The Scalable Commutativity Rule: Designing Scalable Software for Multicore Processors https://people.csail.mit.edu/nickolai/papers/clements-sc.pdf
    • उस समय की चर्चा यहाँ है: https://news.ycombinator.com/item?id=19621799 - A fork() in the road (2019-04-10, 178 comments)
    • fork() zygote pattern के लिए बेहतरीन है
      उतना ही efficient और elegant optimization सोच पाना मुश्किल है
  • हाल में मुझे एक अस्पष्ट bug मिला जो fork किए गए process में और ज़्यादा file descriptors बंद करने की ज़रूरत के कारण हुआ
    मेरे अनुभव में “मुझे current process की एक copy चाहिए” की तुलना में “मुझे एक बिल्कुल नया process चाहिए” कहीं ज़्यादा आम है, लेकिन अजीब बात यह है कि बाद वाली बात को सीधे व्यक्त करने का तरीका नहीं है; केवल पहले clone करो और बाद में post-facto सुधार करके उसका लगभग रूप बना लो

    • आम तौर पर आप उस process से communicate करना चाहते हैं, इसलिए उदाहरण के लिए file descriptors जैसी चीज़ें सेट करनी पड़ती हैं और parent process की जानकारी पास करनी होती है
    • क्या यह O_CLOEXEC से हल नहीं हो जाता?
    • अगर “बाद वाली बात को सीधे व्यक्त करने का तरीका” चाहिए, तो क्या वही posix_spawn का काम नहीं है?
    • “बिल्कुल नया process” से ठीक-ठीक क्या मतलब है?
  • यह कहना कि “fork() अपेक्षाकृत महँगा system call है, और child process के लिए memory सहित पूरे process state को copy करना पड़ता है। वर्षों में बहुत optimization हुए हैं, लेकिन मूल रूप से यह एक महँगा काम है। इससे भी बुरी बात यह है कि fork() कॉल के तुरंत बाद अक्सर exec() आ जाता है, जिससे child के लिए मेहनत से copy की गई सारी memory फेंक दी जाती है” और फिर copy-on-write का ज़िक्र न करना अजीब है
    यह वही optimization है जो पूरी memory को वास्तव में copy होने से बचाती है, लेकिन यहाँ उसका उल्लेख नहीं है

    • लेख में इसे implicit रूप से शामिल किया गया है, लेकिन यहाँ process state की copy से मतलब memory management structures है। मुख्य रूप से page tables और VMA
      भले ही actual pages जिस memory की ओर इशारा करते हैं वह shared रहे, इन structures की copies रखने के लिए नई pages allocate करनी पड़ती हैं। और उन structures को पूरा traverse करके copy करना अपने आप में अब भी महँगा है
    • Redis ऐसा process type है जहाँ यह लागत बहुत मायने रखती है। fork() memory को खुद copy नहीं करता, लेकिन page tables फिर भी copy करनी पड़ती हैं
      अगर process के पास कई दर्जन GB RAM हो, तो fork() में लंबा समय लग सकता है, और Redis जब भी .rdb file 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 को फिर से चलाना दिलचस्प होगा
    • इस तरह के paper के लक्षित पाठकों के लिए copy-on-write शायद बुनियादी ज्ञान है, इसलिए इसे छोड़ दिया गया होगा
    • copy-on-write होने पर भी fork() को उसका setup cost चुकाना पड़ता है। अगर parent process में बहुत busy threads हों, तो उदाहरण के लिए Java में exec() चलने से पहले काफ़ी अनावश्यक copy-on-write हो सकता है
    • मूल लेख में “state” कहा गया था। copy-on-write होने पर भी केवल contents copy नहीं होते; page table entries की संख्या के अनुपात में लागत बनी रहती है
      बड़े virtual memory size वाले program को fork करना धीमा होता है, यह एक अच्छी तरह से जाना-पहचाना मुद्दा है
  • fork()+exec() मॉडल की खूबसूरती इस बात में है कि fork() के बाद सामान्य API को वैसे ही इस्तेमाल करके हर तरह की configuration की जा सकती है
    अब तक देखे गए combined call वाले विकल्प बुनियादी तौर पर कमजोर लगे, क्योंकि हर configuration option को call parameters में जोड़ना पड़ता है, और बाद में extend भी करना होता है बिना इसे अव्यवस्थित बनाए

    • मैं थोड़ा असहमत हूँ, लेकिन इसकी उपयोगिता दिखती है. fork()/exec() कुछ मामलों में उपयोगी हो सकता है, लेकिन अगर APIs pidfd argument लें तो यह काफी अच्छा लग सकता है. 0 का मतलब current process रखा जा सकता है
      समस्या setuid/setgid binaries जैसी चीज़ों में होगी, लेकिन इस मामले में 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 के रूप में बहुत अच्छा नहीं है
    • मैं पूरी तरह उल्टा सोचता हूँ. UNIX-style model की बड़ी गलती यह है कि process creation के समय बहुत ज़्यादा state preserve हो जाती है
      उदाहरण के लिए ऐसे APIs हैं जो किसी object को file descriptor number 4 बना देती हैं, और फिर program चलाकर उससे descriptor 4 पर वही object ढूँढने की उम्मीद की जाती है. यह अजीब है
      Windows में बहुत सारी कमियाँ होने के बावजूद fork()+exec() नहीं है, बल्कि यह मुख्य रूप से process creation के तरीकों के लिए options देता है. यह खूबसूरत नहीं था, लेकिन दिशा सही थी
    • उसे elegant कहना fork()+exec() के इतिहास की path dependence है
      अगर किसी दूसरी दुनिया में fork()+exec() कभी होता ही नहीं, तो उन “general APIs” में से कई के पास किसी दूसरे process की settings बदलने के लिए explicit pid argument होता. 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 जैसे दूसरे तरीकों के साथ भी इसे जोड़ा जा सकता है
    • क्रम spawn, configure, exec होना चाहिए
      अगर 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() को छिपाती नहीं है”

    • किसी खास implementation की नहीं, बल्कि उस concept की खुद में दिलचस्पी दिखती है
  • fork() पहली बार सीखने से ही conceptually भयानक लगा. अगर आप एक ही काम करना चाहते हैं, यानी process start करना, तो उसके लिए एक अलग और असंबंधित काम, यानी current process को fork करने वाला पहेली जैसा मंत्र क्यों करना पड़े?
    लेख के उदाहरण की तरह, जहाँ एक process बहुत सारे git child processes चलाता है, उसे सबसे अच्छे तरीके से कैसे handle किया जाए, यह जानने की जिज्ञासा है. कोई लंबे समय तक चलने वाला parent task बीच-बीच में बार-बार git को शुरू से launch करे, यह समझदारी नहीं लगती; तो वही नतीजा देने वाला low-cost abstraction क्या होगा?

    • fork() conceptually सरल है. अगर आप दूसरी layers को न घसीटें, तो process शुरू करने के लिए एकमात्र चीज़ जिससे आप निश्चित रूप से परिचित हैं, वह आप स्वयं हैं
      वरना process बनाना, उसे चलाने के लिए किसी चीज़ से भरना, और फिर उसे execute होने के लिए schedule करना जैसे कई चरण चाहिए. या फिर Win32 की तरह file system, object loader, linker जैसी दूसरी layers को स्थायी रूप से गूँथकर मिला देना होगा
    • Windows से आने वाले व्यक्ति के रूप में 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.1-2001 में इसे पहले ही जोड़ आया :)
      अगर यह मज़ाक नहीं था तो माफ़ कीजिए, लेकिन posix_spawn() पहले से मौजूद है और glibc में fork बस clone() का alias है
      भले ही यह मूल प्रस्ताव से बिल्कुल एक जैसा न हो, fork()/exec() सच में legacy के काफ़ी करीब हैं
  • अगर fork और exec copy-on-write प्रकृति से आगे बढ़कर लगातार और algebraic behavior दिखा सकें, तो वे न सिर्फ ज़्यादा उपयोगी होंगे बल्कि इस्तेमाल करने में और दिलचस्प भी. उदाहरण के लिए इन्हें lazy evaluation में इस्तेमाल किया जा सकता है

  • इस पुराने API पर Hacker News में काफी चर्चा हुई है, उदाहरण के लिए https://news.ycombinator.com/item?id=31739794