راهنمای توسعهی WaterWall
این راهنما برای توسعهدهندهها و coding agentهایی است که روی تونلهای موجود در
tunnels/ کار میکنند؛ از پیادهسازی و رفع
باگ گرفته تا review کد. راهنما هفت بخش دربارهٔ runtime و یک بخش ویژهٔ API دارد
و توضیحاتش بر اساس کد پروژه است.
در بخش ۱ با ساختار کلی آشنا میشوید و در بخشهای بعد قراردادهایی را میخوانید
که هنگام تغییر کد باید رعایت کنید.
WaterWall یک runtime مبتنی بر chain برای اجرای تونلهاست؛ بنابراین پیادهسازی درست یک تونل به parser یا encoder آن محدود نمیشود. باید جهت callbackها، طول عمر line، state هر line، padding buffer و رفتار packet line را رعایت کنید. تونل همچنین باید با هر node قبل و بعد از خود در config کاربر درست کار کند.
هنگام اصلاح یک تونل، بدون درخواست صریح مدل چرخهی عمر را تغییر ندهید.
از قراردادهای این راهنما و پیادهسازی فعلی
ww/net/ آغاز کنید؛ سپس
با تونلهای جاافتادهای مقایسه کنید که مسئلهای مشابه را حل کردهاند. پیادهسازی
مرجع بهتنهایی درستی یک الگو را ثابت نمیکند: آن را با قرارداد، callerها و همسایههایش تطبیق
دهید، زیرا ممکن است یک الگوی تکرارشده، خطایی تکرارشده هم داشته باشد. بازطراحی
صریحاً درخواستشدهی چرخهی عمر ممنوع نیست، اما کاری فراگیر است و باید همهی
پیادهسازیها، تستها و اسناد قراردادی متأثر را پوشش دهد.
ساختار این راهنما
| بخش | موضوع | چه زمانی به آن نیاز دارید؟ |
|---|---|---|
| بخش ۱ (همین صفحه) | نمای کلی، اشیای اصلی، جهتها و چرخهی عمر | همیشه ابتدا این بخش را بخوانید. |
| بخش ۲: lineها، callbackها و ایمنی طول عمر | Init/Est/Payload/Pause/Resume/Finish، re-entrancy، referenceهای line و بستن صحیح | هر تغییری که جریان اتصال را دستکاری میکند. |
| بخش ۳: bufferها، padding و shift bufferها | sbuf_t، buffer poolها، required_padding_left و prependکردن header | هر تغییری که payload را frame، prepend یا بازنویسی میکند. |
| بخش ۴: packet lineها و تونلهای packet | معنای packet line، تونلهای packet خالص و پلهای packet/stream | هر کار مربوط به layer 3 یا packet. |
| بخش ۵: ساختار یک تونل و روند کار | چیدمان دایرکتوری، metadata در node.c، فایل create.c، line state و قواعد HTTP | هنگام افزودن یا بازسازی ساختار یک تونل. |
| بخش ۶: build، تست و review | presetهای CMake، ctest، اعتبارسنجی، چکلیست review و خروجی عامل | پیش از آنکه تغییری را «تمامشده» بدانید. |
| بخش ۷: shutdown برنامه | shutdown controller، quiescence در workerها، مرحلههای چرخهی عمر، سیگنالها و ترتیب نابودی | هر تغییر در راهاندازی، shutdown، کنترل worker یا مالکیت async. |
راهنمای API و مثالهای اجرایی
برای اتصال برنامهٔ بیرونی، پروتکل API را بخوانید؛ برای افزودن operation عمومی به یک node، از پیادهسازی operationهای node شروع کنید. تنظیمات و مجوزهای gateway در مرجع APIServer هستند. راهاندازی کلاینت نمونه، مدیریت حسابها و ترکیب transportها در مثالهای API آمدهاند.
ابتدا این فایلها را بخوانید
پیش از تغییر رفتار تونل، کد منبعی را بخوانید که قرارداد را پیادهسازی میکند. این فایلها کوتاه و فشردهاند و سازوکار فعلی را تعریف میکنند، اما بینقصبودن پیادهسازی را ثابت نمیکنند. اگر کد منبع، تستها و این راهنما با هم ناسازگارند، بهجای آنکه بیسروصدا متن را با کد موجود یکسان کنید، اختلاف را بررسی و گزارش کنید.
| حوزه | فایلها |
|---|---|
| callbackهای تونل و ساخت chain | ww/net/tunnel.h، ww/net/tunnel.c |
| طول عمر line و state مختص هر line | ww/net/line.h، ww/net/line.c |
| نهاییسازی chain و packet lineها | ww/net/chain.h، ww/net/chain.c |
| رفتار پیشفرض تونل packet | ww/net/packet_tunnel.h، ww/net/packet_tunnel.c |
| shift bufferها و padding | ww/bufio/shiftbuffer.h، ww/bufio/shiftbuffer.c |
| buffer poolها | ww/bufio/buffer_pool.h، ww/bufio/buffer_pool.c |
| metadata مربوط به node | ww/objects/node.h |
تونلهای مرجع مناسب
نزدیکترین الگوهای موجود را برای مقایسه به کار ببرید، نه بهعنوان قالبی که چشمبسته کپی شود. تونلهای زیر شکلهای مفیدی از قرارداد را نشان میدهند، اما رفتار آنها نیز باید با نقش دقیق مورد تغییر سنجیده شود:
| مرجع | برای چه چیزی بخوانید؟ |
|---|---|
TcpListener، UdpListener | adapterهای ابتدای chain که مالک یک منبع سیستمعاملاند و lineهای عادی خود را میسازند و نابود میکنند. |
TcpConnector، UdpConnector | adapterهای انتهای chain که مالک یک منبع سیستمعاملاند، اما line ساختهشده در upstream را قرض میگیرند. |
TlsClient، EncryptionClient | بستهبندی stateful پروتکل، state مختص line و Finish صحیح همراه با byteهای پایانی. |
MuxClient | مالکیت داخلی line، lineهای parent/child و ایمنی در برابر re-entrancy. |
PacketsToStream، StreamToPackets، PacketsToConnection | مرزهای packet-to-stream و پلهایی که به packet line متکیاند. |
PingClient، PingServer | transformهای جفت و مستقیم ICMP Echo، دادهی decodeشده در همان جهت، و پاسخ دقیق در جهت مخالف. |
template | اسکلت حداقلی که نقطهی شروع همهی تونلهاست. |
مدل runtime
WaterWall instanceهای تونل را در یک chain مرتب کنار هم قرار میدهد. یک chain معمول stream چنین است:
TcpListener -> ObfuscatorClient -> TlsClient -> TcpConnector
node اول و آخر معمولاً adapter هستند. آنها مالک یک منبع سیستمعامل، مانند socket نوع TCP یا UDP، دستگاه TUN، raw socket و موارد مشابهاند. فقط همین nodeها از دنیای بیرون داده میخوانند یا در آن مینویسند.
هر چیزی میان adapterها یک تونل میانی است. تونلهای میانی باید قابل ترکیب بمانند: callbackها و payloadها را بدون هیچ فرضی دربارهی adapter دو طرف تغییر دهند. تونل میانیای که فقط در کنار یک adapter خاص کار میکند معیوب است، حتی اگر تستهای خودش موفق باشند.
-------------- chain --------------------------------------------------
------------ ------------ ------------
| | -- up -> | | -- up -> | |
| Tunnel 1 | | Tunnel 2 | | Tunnel 3 |
| | <- down- | | <- down- | |
------------ ------------ ------------
(adapter) (middle) (adapter)
-----------------------------------------------------------------------
در شکل، chain مجموعهی تونلهای بههممتصل را نشان میدهد. up مسیر upstream و
down مسیر downstream است؛ adapter مرز ارتباط با بیرون و middle تونل میانی است.
هر اتصال یک line_t است. line دو انتها دارد که در ww/net/line.h با
Down-end <----> Up-end توصیف شدهاند. chain head، یعنی نخستین adapter، رو به
down-end قرار میگیرد و chain tail، یعنی آخرین adapter، رو به Up-end. سازوکار
backpressure متقارن است: اگر نوشتن در down-end متوقف شود، Up-end در حالت Pause
قرار میگیرد و برعکس.
چهار شیء اصلی
| شیء | تعریف |
|---|---|
node_t | config parseشده بههمراه metadata شامل type، next، flags، layer_group، required_padding_left و createHandle. برای هر node در config JSON یک نمونه وجود دارد. تعریف در ww/objects/node.h. |
tunnel_t | instance اجرایی یک node. دوازده function pointer مربوط به callbackها، لینکهای next/prev، tunnel state، اندازهی line state و offsetهای chain را نگه میدارد. تعریف در ww/net/tunnel.h. |
line_t | یک اتصال: line عادی اتصال، line منطقی یا packet line مربوط به worker. شامل routing context، نشانههای user/auth، شناسهی worker مالک، reference count، flag مربوط به alive و state مختص line برای همهی تونلها است. تعریف در ww/net/line.h. |
tunnel_chain_t | مجموعهی مرتب تونلها. هنگام نهاییسازی، اندازهی کل line state، مجموع padding سمت چپ و packet lineهای هر worker را محاسبه میکند. تعریف در ww/net/chain.h. |
تفاوت tunnel state و line state
هر تونل میتواند مالک دو نوع state باشد:
- tunnel state یا
tstate— یک block بهازای هر instance تونل که میان همهی اتصالهای عبوری مشترک است. اندازهی آن هنگامtunnelCreate()ثابت میشود. - line state یا
lstate— یک slot بهازای هرline_tکه برای همان اتصال و همین تونل خصوصی است.
برای دسترسی به آنها از helperهای موجود استفاده کنید، نه دسترسی مستقیم به structها:
my_tstate_t *ts = tunnelGetState(t); // State سراسری این Tunnel
my_lstate_t *ls = lineGetState(line, t); // State همین Tunnel برای این Line
lineGetState() چگونه slot درست را پیدا میکند؟ هنگام indexشدن chain، runtime
به هر تونل یک lstate_offset میدهد و بهاندازهی lstate_size در هر line فضا
رزرو میکند؛ به tunnelDefaultOnIndex در ww/net/tunnel.c نگاه کنید.
lineGetState() پس از آن فقط برابر است با
line->tunnels_line_state + t->lstate_offset. به همین دلیل line state باید در
Init مقداردهی اولیه شود و پس از نابودشدن دیگر معتبر دانسته نشود: این حافظه
بخشی مشترک از فضای داخلی line است و اندازهی آن فقط یک بار برای کل chain تعیین
میشود.
اندازهی stateها با cache line همتراز است. هنگام zeroکردن line state، دقیقاً مانند تونلهای موجود، تمام ناحیهی همترازشده را پاک کنید:
memoryZeroAligned32(ls, tunnelGetCorrectAlignedLineStateSize(sizeof(my_lstate_t)));
جهت callbackها
مالکیت جهت اولین نکتهای است که باید درست فهمیده شود و رایجترین منشأ خطاست.
| جریان | مفهوم | helper مربوط به forward |
|---|---|---|
| upstream | مسیر request، خروجی یا رو به جلو؛ بهسمت Up-end و next | tunnelNextUpStream* |
| downstream | مسیر response، ورودی یا رو به عقب؛ بهسمت down-end و prev | tunnelPrevDownStream* |
هر تونل دوازده callback مربوط به جریان دارد؛ برای هر ترکیب {event} x {direction}
یک callback:
fnInitU fnEstU fnPayloadU fnPauseU fnResumeU fnFinU // Handlerهای upstream
fnInitD fnEstD fnPayloadD fnPauseD fnResumeD fnFinD // Handlerهای downstream
تقریباً هیچگاه این pointerها را مستقیماً صدا نمیزنید. از helperهای forwarding استفاده میکنید که handler متناظر را روی همسایه فراخوانی میکنند:
// «Next در upstream»: تابع self->next->fnPayloadU(self->next, line, buf) را صدا میزند
tunnelNextUpStreamPayload(t, line, buf);
// «Prev در downstream»: تابع self->prev->fnPayloadD(self->prev, line, buf) را صدا میزند
tunnelPrevDownStreamPayload(t, line, buf);
forwardکردن رویدادهای چرخهی عمر نیز از همین نامگذاری پیروی میکند:
tunnelNextUpStreamInit(t, line); // ادامهٔ زنجیره را در جهت جلو آغاز میکند
tunnelNextUpStreamFinish(t, line); // جهت روبهجلو را میبندد
tunnelPrevDownStreamEst(t, line); // برقراری اتصال را به سمت عقب اعلام میکند
tunnelPrevDownStreamFinish(t, line); // جهت روبهعقب را میبندد
اگر تونل callback مشخصی را override نکند، رفتار پیشفرض framework را میگیرد که
صرفاً رویداد را در همان جهت به همسایه pass-through میکند؛ به
tunnelDefaultUpStreamPayload و tunnelDefaultDownStreamPayload در
ww/net/tunnel.c نگاه کنید. بنابراین تونلی که فقط در upstream عمل obfuscation را
انجام میدهد، میتواند handler مربوط به payload در downstream را روی مقدار پیشفرض
بگذارد تا byteها بدون تغییر عبور کنند.
صرفاً چون تونل سمت server «برعکس به نظر میرسد»، helper مربوط به forwardکردن
buffer دریافتی را عوض نکنید. chain واقعی را رسم کنید، Up-end و down-end را
علامت بزنید و هر buffer خروجی را جداگانه دستهبندی کنید. در Ping v2، دادهی اصلی
پس از decode در همان جهت callback درخواست دریافتی ادامه مییابد، اما یک Echo Reply
دقیق که جداگانه clone شده، بهعنوان تأیید در جهت مخالف حرکت میکند. بنابراین
PingServer در handler درخواست upstream خود به downstream پاسخ میدهد و
PingClient در handler درخواست downstream خود به upstream پاسخ میدهد. دادهی
برنامه در جهت معکوس، یک Echo request جداگانه را آغاز میکند. برای جزئیات
بخش ۴ را ببینید.
کدام تابع forwarding را صدا بزنم؟
| در حال handleکردن... | و قصد فرستادن رویداد به... | تابع مناسب |
|---|---|---|
| هر چیزی | بخش بالاتر chain، بهسمت next | tunnelNextUpStream{Init,Payload,Est,Pause,Resume,Finish} |
| هر چیزی | بخش پایینتر chain، بهسمت prev | tunnelPrevDownStream{Init,Payload,Est,Pause,Resume,Finish} |
helperهای قرینهی tunnelUpStream* و tunnelDownStream* که Next/Prev ندارند،
handler را روی تونلی فراخوانی میکنند که pointer مستقیم آن را در اختیار دارید.
این حالت زمانی به کار میرود که تونل شاخهای را هدایت میکند که در پایین خود bind
کرده است؛ مانند route target، fallback یا شاخهی کمکی. ورودی واقعی چنین شاخهای
را با tunnelGetBranchEntry() پیدا کنید و فرض نکنید node خامی که دریافت کردهاید
همان head قابلفراخوانی است.
چرخهی عمر تونل
instance تونل از مجموعهای ثابت از lifecycle hookها عبور میکند. این hookها با callbackهای جریانِ مختص اتصال که در بالا دیدیم متفاوتاند: در زمان config، راهاندازی و خاموششدن و بهازای هر تونل یا worker یک بار اجرا میشوند، نه برای هر packet.
runtime تقریباً با ترتیب زیر آنها را اجرا میکند؛ توضیح ابتدای ww/net/tunnel.h
و پیادهسازیهای پیشفرض در ww/net/tunnel.c را ببینید:
createHandle(node) // node.c -> create.c: تخصیص، تعیین Callbackها و Parse تنظیمات
|
onChain // اتصال به next/prev و درج در زنجیره (پیشفرض node->next را میپیماید)
|
solve + onSolvedTopology // بررسی Snapshot حلشده؛ پس از تغییر توپولوژی تکرار میشود
|
onIndex // تعیین chain_index و lstate_offset و رزرو بایتهای Line State
|
onPrepare // آمادهسازی پیش از Start (نام پیشین: onChainingComplete)
|
onStart // زنجیره کامل است؛ آغاز کار واقعی / راهاندازی Packet Lineها
|
... // زمان اجرا: Callbackهای جریان اینجا اجرا میشوند
|
onQuiesceRequest // Thread اصلی: بستن پذیرش؛ درخواست توقف Producer؛ بدون انتظار
|
onWorkerQuiesce // هر Worker مالک: جداسازی Timer/Watcher/Handleهای وابسته به Worker
|
onQuiesceWait // Thread اصلی: Join/Detach ریشههای Callback خارجیِ از قبل پذیرفتهشده
|
onWorkerStop // هر Worker مالک: Drainکردن Lineهای عادی و منابع تحت مالکیت
|
onStop // Thread اصلی: آزادسازی منابع مشترک باقیماندهٔ مؤلفه
|
onDestroy // Thread اصلی: بازپسگیری Instance پس از پایان همهٔ Referenceها
کاربرد هر hook:
| hook | رفتار پیشفرض | مورد معمول override |
|---|---|---|
createHandle | الزامی است | ساخت تونل با tunnelCreate(node, sizeof(tstate), sizeof(lstate))، تنظیم دوازده callback، parse و اعتبارسنجی تنظیمات JSON و تخصیص منابع هر worker. |
onChain | tunnelDefaultOnChain با دنبالکردن node->next chain را میسازد | بهندرت override میشود. adapterها نیز مقدار پیشفرض را حفظ میکنند. |
onSolvedTopology | NULL (اختیاری) | snapshot فعلی توپولوژی حلشده را مصرف میکند و در صورت نیاز، پیش از indexing توپولوژی خصوصی وابسته به layer را میسازد. snapshot لزوماً نهایی نیست و hook ممکن است چند بار اجرا شود، پس باید تکرار را تحمل کند. فقط و فقط اگر توپولوژی تغییر کرد true برگردانید؛ در این حالت snapshot فوراً نامعتبر میشود و NodeManager پیش از اجرای hook بعدی دوباره مسئله را حل میکند. |
onIndex | tunnelDefaultOnIndex، offset مربوط به line state را تعیین میکند | پس از آنکه گسترش توپولوژی و حل layerها به نقطهی ثابت رسید، یک بار اجرا میشود. میتوان domainهای نهایی edgeهای حلشده را اینجا مصرف کرد، اما توپولوژی chain نباید تغییر کند. بهندرت override میشود. |
onPrepare | بدون عملیات | آمادهسازی پیش از start که به وجود chain نیاز دارد. |
onStart | بدون عملیات | آغاز listen/connect و Bootstrapکردن packet lineها؛ بخش ۴ را ببینید. |
onQuiesceRequest | بدون عملیات | روی thread اصلی، در زمانی محدود و بدون مسدودکردن، پذیرش را میبندد و توقف producer را درخواست میکند. اینجا join نکنید و منابع لازم برای drainکردن lineها را آزاد نکنید. |
onWorkerQuiesce | بدون عملیات | روی هر worker مالک، پیش از تأیید quiescence، timerها، watcherها و handleهای وابسته به همان WID را جدا میکند. |
onQuiesceWait | بدون عملیات | پس از quiesceشدن workerها، روی thread اصلی ریشههای callback خارجیِ از قبل پذیرفتهشده را join یا detach میکند؛ منابع لازم برای drain lineها باید همچنان زنده بمانند. |
onWorkerStop | بدون عملیات | هنگام drain روی هر worker مالک، فقط inventoryهای مرجع source و lineهای عادیِ تحت مالکیت داخلی را میبندد. ترتیب ذخیرهسازی chain تضمینی برای ترتیب همسایهها نیست؛ registryهای line قرضی را فقط در onDestroy نهایی و پس از drain همهی مالکان اعتبارسنجی کنید. |
onStop | بدون عملیات | پس از drainشدن همهی workerها، منابع مشترک باقیمانده را روی thread اصلی آزاد میکند؛ نباید به timer یا watcher وابسته به worker دست بزند. |
onDestroy | tunnelDestroy، instance را آزاد میکند | آزادکردن تنظیمات، SSL contextها و poolها و سپس فراخوانی tunnelDestroy. |
بیشتر تونلها فقط مرحلههای چرخهی عمر مربوط به منابعی را override میکنند که واقعاً مالکشان هستند؛ بهعلاوهی callbackهای جریان موردنیاز. رفتار پیشفرض chaining و indexing را نگه دارید، مگر آنکه قرارداد توپولوژی تونل چیز دیگری بخواهد. هر انحراف را با قواعد توپولوژی حلشده بسنجید، نه با کپی مکانیکی یک مرجع.
نتایج layer در دو نقطهی جداگانه مصرف میشوند. فقط هنگامی از onSolvedTopology
استفاده کنید که تونل باید پیش از indexing عملی انجام دهد؛ برای نمونه، توپولوژی
خصوصیای درج کند که محل آن به domainهای حلشدهی مجاور وابسته است. callbackای که
توپولوژی را تغییر میدهد باید استفاده از snapshot را متوقف کند و true برگرداند.
توپولوژی تنها وقتی نهایی است که یک دور کامل اجرای callbackها هیچ تغییری گزارش
نکند. اگر تونل فقط باید راهحل نهایی را بخواند، از onIndex استفاده کنید؛ این
راهحل برای onIndex و همهی callbackهای بعدی چرخهی عمر ثابت میماند. توپولوژی
داخلی ثابتی که وجود و محل آن پیش از حل معلوم است، همچنان باید در onChain ساخته
شود.
اسکلت حداقلی تونل
همهی تونلها از یک ساختار یکسان آغاز میشوند. نمونهی زیر نسخهی کمی خلاصهشدهی
create.c در تونل template است:
tunnel_t *templateTunnelCreate(node_t *node)
{
tunnel_t *t = tunnelCreate(node, sizeof(template_tstate_t), sizeof(template_lstate_t));
if (! t)
{
return NULL;
}
t->fnInitU = &templateTunnelUpStreamInit;
t->fnEstU = &templateTunnelUpStreamEst;
t->fnFinU = &templateTunnelUpStreamFinish;
t->fnPayloadU = &templateTunnelUpStreamPayload;
t->fnPauseU = &templateTunnelUpStreamPause;
t->fnResumeU = &templateTunnelUpStreamResume;
t->fnInitD = &templateTunnelDownStreamInit;
t->fnEstD = &templateTunnelDownStreamEst;
t->fnFinD = &templateTunnelDownStreamFinish;
t->fnPayloadD = &templateTunnelDownStreamPayload;
t->fnPauseD = &templateTunnelDownStreamPause;
t->fnResumeD = &templateTunnelDownStreamResume;
t->onPrepare = &templateTunnelOnPrepair;
t->onStart = &templateTunnelOnStart;
t->onStop = &templateTunnelOnStop;
t->onDestroy = &templateTunnelDestroy;
return t;
}
بخش ۵ ساختار کامل دایرکتوری پشت این اسکلت، از جمله metadata در node.c، فایل
line_state.c و محل هر callback را بررسی میکند.
«درستبودن» در اینجا یعنی چه؟
یک تغییر درست در تونل باید همزمان همهی موارد قابلاعمال زیر را حفظ کند:
- قابلیت ترکیب تونل، مستقل از همسایههای آن
- قواعد چرخهی عمر line: مقداردهی در
Init، یک بار destroyشدن و فقط بهدست مالک - مالکیت جهت: هرگز
Next*وPrev*را وارونه نکنید - فرضهای مربوط به padding buffer: رعایت
required_padding_left - استفادهی صحیح از shift buffer: فراخوانی
sbufShiftLeftفقط با ظرفیت کافی در چپ - ایمنی reference: جلوگیری از use-after-free برای
line_tو line state - معنای packet line: محلی برای worker و بدون destroy در زمان اجرا
- رفتار موجود chain: نبود regression در چیدمانهای دیگر
اگر یک تغییر پیشنهادی نتواند توضیح دهد که تکتک این موارد را چگونه حفظ میکند، هنوز آماده نیست. ادامهی این راهنما در واقع شرح مفصل همین فهرست است.
هویت thread مربوط به worker و WID
هویت thread مربوط به worker در WaterWall صریح است و بهطور پیشفرض نامعتبر در نظر گرفته میشود:
- WID یعنی index یک slot ثبتشدهی worker، نه شناسهی thread در سیستمعامل یا
برنامه مانند
getTID(). - همهی workerهای ثبتشده هنگام اجرای عادی event loop دارند و نقش ثبتشدهی
جداگانهای بدون event loop وجود ندارد.
getWorkersCount()تعداد همهی slotهای ثبتشده را برمیگرداند. - threadهای ثبتنشده، از جمله readerهای TUN در Darwin/BSD، readerهای capture،
callbackهای completion بومی و threadهای کمکی و تست، مقدار
getWID() == kInvalidWIDیعنیUINT8_MAXرا میبینند. workerWIDIsRegistered(wid)معتبر بودن slot را بررسی میکند وcurrentThreadIsWorker()هویت ثبتشدهی thread فراخواننده را. هیچکدام مالکیت state یک worker دیگر را ثابت نمیکند؛ برای بررسی مالک دقیق ازcurrentThreadOwnsWorker(wid)استفاده کنید.- threadهای کمکی باید کارِ دارای مالکیت را از طریق صف پیام worker ارسال کنند و هرگز نباید مستقیماً یک pool محلی worker را قرض بگیرند. custom event خام loop فقط برای عملیات بدون مالکیت و متعلق به خود loop رزرو شده است.
- lwIP روی workerهای عادی اجرا میشود و thread یا slot جداگانهای ندارد؛ سقف تعداد workerها همچنان ۲۵۴ است.
- threadهای native/library برای log و تشخیص خطا از
getTID()استفاده میکنند و تا زمانی که صریحاً باworkerBindCurrentThread()متصل نشوند WID نامعتبر دارند.
هویت worker پس از detachشدن loop و صف پیام هنگام teardown تا زمان unbindشدن thread
معتبر میماند. این هویت وجود loop یا pool، بازبودن admission یا فعالبودن callback
عادی را تضمین نمیکند؛ مرز resource و lifecycle لازم را جداگانه بررسی کنید.
nullبودن loop هرگز نشاندهندهی نقش دیگری برای worker نیست.
snapshotهای منابع پردازه نیز از همین قاعدهی مالکیت پیروی میکنند. providerهای host و cgroup فقط هنگام مقداردهی همگام global state و در sampler پسزمینهی worker 0 اجرا میشوند. در Linux، sampler بیشترین درصد مصرف و کمترین ظرفیت باقیمانده را در سلسلهمراتب محدود و قابلمشاهدهی cgroup منتشر میکند؛ محدودیتهای مبهم یا پنهان برای fallback محافظهکارانهی مصرفکننده همچنان ناموجود محسوب میشوند. مسیرهای داغ worker، tuple اتمیک محدود و یکپارچهی منتشرشده را مصرف میکنند و هرگز فایل سیستمی باز نمیکنند، state قدیمی host را تازه نمیکنند، تخصیص یا انتظار ندارند و mutex مربوط به sampler را نمیگیرند.
انتخاب helper مناسب برای context مربوط به worker
هرگز getWID() را مستقیماً در بررسی affinity، index یک array یا lookup منبع
بهکار نبرید. محدودترین helper سازگار با context موجود را انتخاب کنید. helper
دامنهی معتبر را در قرارداد خود کد میکند و اجازه نمیدهد یک thread ثبتنشده
صرفاً بهدلیل برابری اتفاقی از بررسی عبور کند.
| context | استفاده |
|---|---|
callback یک line_t * دارد | lineIsOnCurrentWorker(line)، lineGetWID(line)، lineGetBufferPool(line)، lineReuseBuffer(line, buf) |
callback پیام worker یک worker_t *worker دارد | worker->wid، worker->buffer_pool، worker->loop؛ TLS را دوباره نخوانید. |
| callback مربوط به event/timer/I/O یک loop یا event دارد | یک بار getLoopWorkerWID(weventGetLoop(ev)) را بگیرید و مقدار محلی را دوباره استفاده کنید. |
| callback واقعی event loop بدون line یا worker | getCurrentWorker()، getCurrentWorkerWID()، getCurrentWorkerBufferPool()، getCurrentWorkerLoop() |
callback چرخهی عمر که wid صریح دریافت کرده است | currentThreadOwnsWorker(wid) |
| کدی که بنا بر طراحی به worker 0 نیاز دارد | WID صریح 0 همراه currentThreadOwnsWorker(0)؛ هرگز آن را از TLS پیشفرض استنباط نکنید. |
| API قابلدسترسی از بیرون یا در غیر این صورت fallible | tryGetCurrentWorker() یا currentThreadIsWorker()؛ سپس error یا NULL برگردانید یا کار را queue کنید. |
| callback خام lwIP | مالک engine انتخابشده؛ پیش از ورود به stack، مالکیت engine و WID صریح را بررسی کنید. |
| thread کمکی device | فقط pool/channel متعلق به device؛ تحویل به یک event worker صریح. |
| فقط logging | استفادهی خام از getWID() مجاز است، به شرط عبور از workerWIDForLog() و چاپ با %d در کنار getTID()؛ مقدار -1 یعنی ثبتنشده. |
accessorهای بررسیشدهی getCurrentWorker*() فقط برای callbackهای
داخلیاند که قراردادشان از قبل حضور روی worker ثبتشده را تضمین میکند. این توابع
بهجای fallback به worker 0، abort میکنند. بنابراین هر چیزی که از thread دستگاه،
callback یک library یا API عمومی قابلدسترسی است باید از شکل predicate استفاده کند
و در صورت نبود context معتبر بهطور تمیز fail شود.
کار وابسته به line یک مرز cross-thread اختصاصی دارد. چهار تابع
lineSchedule*Task*() از هر thread قابل فراخوانیاند، اما فقط وقتی فراخواننده یک
live handle همگامشده دارد: reference همراه gate یا عملیات registry که تا گرفتن
reference داخلی scheduler مانع lineDestroy() شود. pointer خام یا referenceای که
فقط allocation را نگه میدارد برای race با مرگ منطقی کافی نیست و submission نیز
دسترسی به state یا pool worker مقصد نمیدهد.
callback همیشه روی event worker مالک line اجرا میشود. کار فوری و delay صفر queue
میشوند؛ delay مثبت از thread دیگر ابتدا setup timer را روی owner queue میکند.
پس AcceptedAsync فقط انتقال مالکیت به مسیر async را ثابت میکند، نه armشدن timer
یا اجرای task. قرارداد کامل نتیجه و لغو در بخش ۲ آمده است.
// Callback مربوط به Line: هم Affinity و هم مالکیت Pool از خود Line میآیند.
if (! lineIsOnCurrentWorker(l))
{
sendWorkerMessageForceQueueWithCleanup(lineGetWID(l), doWork, cleanup, t, l, buf);
return;
}
lineReuseBuffer(l, buf);
// Callback پیام Worker: پیام دقیقاً به یک Worker تحویل شده است.
static void onWorkerMessage(worker_t *worker, void *arg1, void *arg2, void *arg3)
{
line_t *packet_line = tunnelchainGetWorkerPacketLine(tunnelGetChain(t), worker->wid);
...
}
// Callback مربوط به Timer: مالک را از Loop بگیرید، یک بار بررسی و دوباره استفاده کنید.
const wid_t wid = getLoopWorkerWID(weventGetLoop(timer));
ts->worker_timers[wid] = NULL;
// خروجی lwIP: همسایههای دلخواه را حتی روی همان worker در صف بگذارید.
// PTC فقط میتواند sink مستقیم و تأییدشدهای را فراخوانی کند که صرفاً enqueue میکند.
queueToPacketWorker();
// Thread دستگاه: فقط منابع متعلق به دستگاه؛ تحویل با WID صریح.
sbuf_t *buf = bufferpoolGetLargeBuffer(dev->reader_buffer_pool);
sendWorkerMessageForceQueueWithCleanup(target_wid, deliver, cleanup, dev, buf, NULL);
پاکسازی buffer میان threadها با recycleکردن یکسان نیست. reuseBuffer(buf) یعنی
«روی event worker فعلی دوباره استفاده کن» و call siteای را که pool محلی worker
ندارد رد میکند. وقتی روی worker مالک buffer نیستید، آزادسازی درست
sbufDestroy(buf) است؛ این مسیرها را به reuseBuffer() تبدیل نکنید.
APIهای معمول تونل، یعنی <node>TunnelApi()، روی event worker اجرا میشوند و
مالکیت پیام خود را میگیرند. پیام را با tunnelapiRecycleMessage() یا
tunnelapiUnsupportedMessage() در ww/objects/node.h مصرف کنید. هر دو تابع وقتی
caller روی event worker نیست buffer را نابود میکنند و kApiResultError
برمیگردانند، نه اینکه pool مربوط به worker 0 را قرض بگیرند.
tests/worker_identity_source_policy_test.py این قواعد را اعمال میکند:
تودرتوکردن تازهی getWorkerXxx(getWID())، تصمیم مالکیت با getWID() == 0 خام،
مقایسهی خام affinity با WID فعلی و کد تکراری مربوط به recycle پیام tunnel API رد
میشوند.
انتخاب idle table مناسب
وقتی ساخت، جستوجو، refresh، حذف، callback انقضا، drain و نابودی item همگی متعلق
به یک event worker هستند، local_idle_table_t را انتخاب کنید. این table هیچ
همگامسازی یا تحویل میان workerها ندارد؛ هرگز خود table یا یکی از handleهای قرضی
itemهای آن را به worker دیگری ندهید.
فقط وقتی idle_table_t را انتخاب کنید که یک table باید itemهای متعلق به چند
event worker را index کند و انقضا باید به مالک ثبتشدهی هر item ارسال شود.
indexهای آن همگامسازی شدهاند، اما handle بازگشتی همچنان referenceای قرضی و
متعلق به worker است؛ table مشترک یک container عمومی و thread-safe برای handleها
نیست. صرفاً بهدلیل نام یا mutex آن تصور نکنید امنتر است. refresh مشترک عمداً
بدون lock انجام میشود و مرتبسازی دوبارهی heap را به پیمایش timer میسپارد.
هر دو table یک قاعدهی طول عمر منطقی دارند: حذف موفق، قابلیت lookup را پاک و همهی handleهای خام را پیش از بازگشت نامعتبر میکند. مصرفکنندهای که handle را ذخیره میکند باید آن slot مرجع را پیش از حذف پاک کند و پس از آن هرگز item را نخواند. بازپسگیری فیزیکی متفاوت است: حذف محلی معمولاً فوراً حافظه را آزاد میکند، اما یک item مشترک ممکن است تا تسویهی ایمن در مالکیت heap یا message باقی بماند.
واژهنامه
| اصطلاح | مفهوم |
|---|---|
| adapter | تونلی در ابتدا یا انتهای chain که مالک یک منبع سیستمعامل، مانند socket، TUN یا raw socket است. مالکیت منبع بهمعنای مالکیت line_t نیست: sourceای مانند TcpListener، line عادی را میسازد، اما tailای مانند TcpConnector آن را قرض میگیرد. |
| تونل میانی | تونلی غیر از adapter که callbackها یا payloadها را تغییر میدهد و باید قابل ترکیب بماند. |
| line | یک line_t؛ یک اتصال یا یک packet line مربوط به worker که state مختص line همهی تونلها را نگه میدارد. |
| upstream | جهت رو به جلو یا خروجی، بهسمت next و Up-end. |
| downstream | جهت رو به عقب یا ورودی، بهسمت prev و down-end. |
| tstate | tunnel state؛ مختص instance و مشترک میان همهی lineها. |
| lstate | line state؛ مختص line و خصوصی برای یک تونل. |
| packet line | یک line_t پایدار بهازای هر worker در chainهای layer 3؛ line اتصال نیست. |
| WID | index یک slot ثبتشدهی worker در بازهی 0 .. WORKERS_COUNT - 1؛ روی thread ثبتنشده kInvalidWID است. |
| kInvalidWID | مقدار sentinel برابر UINT8_MAX برای thread ثبتنشده یا کمکی. |
| re-entrancy | حالتی که callback میانتونلی پیش از بازگشت کنترل، بهصورت synchronous دوباره وارد تونل شما میشود و ممکن است line را ببندد. |
| reflection | forwardکردن اشتباه callback بهسمتی که قبلاً Finish شده است؛ یکی از علتهای رایج crash. |