دریافت کدهای کرنل لینوکس
از آنجا که در این آزمایش نیاز است تا در کدهای کرنل لینوکس به گشت و گذار بپردازیم، پس از نصب git روی سیستم عامل خود، در یک فولدر دلخواه، فایل های حاوی کدهای لینوکسی را از ریپازیتوری آن بر روی Github، با استفاده از دستور زیر clone می نماییم:
git clone https://github.com/torvalds/linux.git ↲
پس از دانلود کامل فایل ها از روی ریپازیتوری، باید وارد فولدر linux شده و با استفاده از دستور زیر به ورژن درخواستی در تمرین سوئچ کنیم:
git checkout v5.10 ↲
در ادامه خواهیم دید که توسط VSCode در کدهای کرنل کنکاش خواهیم کرد.
ایجاد Workload (یک ارتباط TCP)
برای این آزمایش لازم است که یک Workload بر روی سیستم راه اندازی کنیم؛ این بارِ کاری، ایجاد یک کانکشنِ TCP خواهد بود. برای این منظور با کدنویسی توسط پایتون در دو فایل مجزا (server.py و client.py) و اجرای فایل ها یک ارتباط ساده ی TCP را ایجاد می نماییم:
فایل client.py
فایل server.py
اکنون ابتدا فایل سرور و سپس فایل کلاینت در دو تب مجزا در ترمینال را اجرا می کنیم؛ به محض اجرای فایل کلاینت، یک پیام ساده تحت ارتباط TCP بین سرور و کلاینت ما رد و بدل می شود:
ردیابی Stack
پیش از آنکه ارتباط TCP خود را به روشی که در قسمت قبل توضیح داده شده است شروع کنیم، نیاز است تا با استفاده از دستور زیر، یک ردیابی از پشته را برای خود ثبت و ذخیره سازیم:
sudo perf record -ae 'sock:*, net:*, block:*' --call-graph dwarf ↲
با اجرای دستور بالا، رخدادهای sock و net برای بررسی آتی پشته در نظر گرفته خواهند شد.
به محض اجرای دستور بالا، ارتباط کلاینت – سروی خود را در دو تب جداگانه دیگر ایجاد کرده و سپس در تبی که دستور بالا را وارد کرده ایم برای توقف ردیابی پشته، از کلیدهای ترکیبی Ctrl+c استفاده می کنیم:
همانطور که در تصویر بالا مشاهده می شود، حجم فایل ردیابی پشته 534.876 مگابایت می باشد. برای جلوگیری از ایجاد یک فایل ردیابی با حجم بالا ضروری است که تعداد رویدادهای مورد نظر را محدود در نظر گرفته و زمان ردیابی پشته را نیز بیش از اندازه به انجام نرسانیم.
اکنون وقت دیدن نتیجه ی ردیابی پشته با استفاده از دستور زیر است:
sudo perf script ↲
با اجرای این دستور، انبوهی از اطلاعات مربوط به ردیابی پشته بر روی صفحه ی نمایش ظاهر می گردد که می توانید در پایین صفحه با تایپ کاراکتر / یک الگوی دلخواه برای جستجو در متن نمایان شده را تعیین کنید:
به عنوان نمونه از طریق sock/ تمام رویدادهایی که دارای کلمه ی sock می باشند هایلایت خواهند شد. در تصویر بالا مشخص است که مطابق با کدهای پایتونی که در آن از پکیج socket برای ایجاد ارتباط TCP استفاده کرده بودیم، رویدادهای مشتمل بر sock در جاهایی که با python3 آغاز می شود، نمودار گشته اند. در نمایی بزرگتر از تصویر زیر:
گشت و گذار در کدهای کرنل لینوکس
اکنون فولدر حاوی کدهای کرنل لینوکس را در VSCode باز کرده و release_sock__ را جستجو می کنیم:
فایل sock.c توجه برانگیز می باشد از این رو آن را باز می کنیم؛ تابع آن مشخص است:
این تابع در فایل های sock.h و tcp.c فراخوانی می شود که به بنابر توضیحات موجود در فایل tcp.c هدف از فراخوانی آن مورد زیر است:
/* remove backlog if any, without releasing ownership. */
مدیریت درخواست SYN توسط TCP
به نظر می رسد که مدیریت بسته ی SYN در لینوکس واقعا پیچیده است. در این قمست کمی به این موضوع خواهیم پرداخت.
حکایت دو صف (Queue):
ابتدا باید دانیم که هر سوکت باند شده (Bound Socket) در حالت “TCP LISTENING” دو صف مجزا دارد:
• صف SYN
• صف پذیرش (The Accept Queue)
به این صفها اغلب نامهای دیگری مانند "reqsk_queue" ،"ACK backlog" ،"listen backlog" یا حتی "TCP backlog" نیز داده میشود.
SYN Queue:
SYN Queue بسته های SYN ورودی را ذخیره می کند (به طور خاص: struct inet_request_sock). این تابع مسئول ارسال بسته های SYN+ACK و امتحان مجدد آنها در مهلت زمانی است.
پس از ارسال SYN+ACK، صف SYN منتظر یک بسته ی ACK از کلاینت - آخرین بسته در hree-way-handshake است. تمام بسته های ACK دریافت شده باید ابتدا کاملا با جدول اتصال وضع شده و سپس با داده های موجود در صف SYN مربوطه مطابقت داده شود. در انطباق SYN Queue، کرنل آیتم را از صف SYN حذف کرده و یک اتصال کامل ایجاد می نماید (به طور خاص: struct inet_sock)، و آن را به صف پذیرش اضافه می کند.
در فایل ردیابی استک ما نیز این مرحله قابل مشاهده است:
Accept Queue:
صف پذیرش شامل اتصالات کاملاً ایجاد شده (Fully Established Connection) است: آماده دریافت توسط برنامه. هنگامی که یک فرآیند، accept() را فراخوانی میکند، سوکتها از صف خارج و به برنامه ارسال میشوند.
این یک نمای نسبتا ساده از مدیریت بسته SYN در لینوکس است:
API های بین لایه ها (L2/IP، IP/Transport، Transport/Application):
مدیریت IP Datagramها:
تابع زیر یک دیتاگرام skbuff را دریافت می کند:
struct sk_buff *__skb_try_recv_datagram(struct sock *sk, struct sk_buff_head *queue, unsigned int flags, int *off, int *err, struct sk_buff **last)
و این تابع یک دیتاگرام skbuff را به اجبار آزاد می کند:
int skb_kill_datagram(struct sock *sk, struct sk_buff *skb, unsigned int flags)
تابع زیر نیز دیتاگرام را در یک تکرار کننده ی iovec کپی و یک هش را به روز می نماید:
int skb_copy_and_hash_datagram_iter(const struct sk_buff *skb, int offset, struct iov_iter *to, int len, struct ahash_request *hash)
و یا تابع دیگری که فقط یک دیتاگرام را در تکرار کننده ی iovec کپی می کند:
int skb_copy_datagram_iter(const struct sk_buff *skb, int offset, struct iov_iter *to, int len)
مدیریت Frameها:
در struct sk_buff، مقدار wifi_acked مشخص می کند که آیا فریم روی وای فای اک شده است یا خیر.
تایع زیر مقدار allmulti count برای یک دستگاه را به روزرسانی کرده و دریافت همه ی فریم های همه پخشی (Multicast Frames) را به یک دستگاه اضافه یا حذف می کند:
int dev_set_allmulti(struct net_device *dev, int inc)
و تابع زیر طول هدر یک فریم اترنت را تعیین می نماید:
u32 eth_get_headlen(const struct net_device *dev, const void *data, u32 len)
const struct net_device *dev یک اشاره گر به دستگاه شبکه، const void *data یک اشاره گر برای فریم آغازین و u32 len معرف طول کل فریم می باشد.
یک فریم اترنت باید حداقل اندازه ی 60 بایت داشته باشد. این تابع فریم های کوتاه را می گیرد و آنها را تا سقف 60 بایت صفر می کند:
int eth_skb_pad(struct sk_buff *skb)
در struct net_device، مقدار tx_queue_len حداکثر تعداد فریم های مجاز در صف را مشخص می نماید و burst_cnt کنترل می کند که یک گره چند فریم اضافی را در فرصت ارسال واحد (Single Transmit Opportunity-TO) ارسال می کند. مقدار پیش فرض 0 به این معنی است که گره دقیقاً یک فریم در هر TO مجاز است. مقدار 1 اجازه دو فریم در هر TO را می دهد و…
مدیریت Segmentها:
انجام بخش بندی (Segmentation) پروتکل در skb را تابع زیر بر عهده دارد:
struct sk_buff *skb_segment(struct sk_buff *head_skb, netdev_features_t features)
تابع بالا، تقسیم بندی را در skb داده شده انجام می دهد و نشانگر را به اولین نشانگر در لیست skbs جدید برای سگمنت ها برمی گرداند. در صورت بروز خطا، ERR_PTR(err) را برمی گردد.
در تایع دیگری می توان محدودیت تعداد سگمنت های TCP را که دستگاه می تواند از یک سوپر فریم TSO تولید کند، تنظیم نمود. اگر به طور صریح تنظیم نشود، پشته مقدار GSO_MAX_SEGS را در نظر می گیرد:
void netif_inherit_tso_max(struct net_device *to, const struct net_device *from)
مدیریت packetها:
در struct sk_buff، مولفه هایی وجود دارد که مستقیما به پکت ها مربوط می شوند:
peeked: این پکت قبلاً دیده شده است، بنابراین در آمار آورده شده و دیگر در نظر گرفته نشود.
pp_recycle: پکت را برای بازیافت به جای آزاد کردن علامت گذاری می شود (به معنی پشتیبانی page_pool در درایور است).
pkt_type: کلاس پکت
nf_trace: پرچم ردیابی بسته ی netfilter
csum_level: تعداد چکسام متوالی یافت شده در بسته منهای یک را نشان میدهد که بهعنوان CHECKSUM_UNNECESSARY تأیید شدهاند (حداکثر 3).
redirected: پکت توسط طبقه بندی کننده ی پکت (Packet Classifier) هدایت شد.
from_ingress: پکت از مسیر ورودی هدایت شد.
Priority: اولویت صف پکت
و همچنین توابع چندی بر روی پکت ها کار انجام می دهند؛ مثلا تابع زیر پکت بعدی در صف را پس از skb بر می گرداند. فقط در صورتی معتبر است که skb_queue_is_last() را false ارزیابی کند:
struct sk_buff *skb_queue_next(const struct sk_buff_head *list, const struct sk_buff *skb)
و یا تابع زیر پکت قبلی ا در لیست را قبل از skb برمی گرداند. فقط زمانی معتبر است که skb_queue_is_first() را به اشتباه ارزیابی کند:
struct sk_buff *skb_queue_prev(const struct sk_buff_head *list, const struct sk_buff *skb)
دریافت Ping:
برای بررسی نحوه ی دریافت Ping قسمتی از فایل ردیابی پشته را بررسی می کنیم:
برای این منظور، برخی از آیتم های اطلاعات ردیابی پشته ی نشان داده شده در تصویر بالا را از پایین به بالا پیمایش خواهیم کرد:
cp_v4_connect را در VSCode جستجو می کنیم؛ تابع مربوطه در فایل tcp_ipv4.c پیدا می شود:
این تابع بسیار بلند بالاست؛ همانطور که از توضیحات سبزرنگ بالای تابع بر می آید، شروع کننده ی یک ارتباط خارجی خواهد بود.
با نگاه بعدی به پشته می بینیم که tcp_set_state ظاهر شده است؛ درهمان فایل tcp_ipv4.c و همان تابع tcp_v4_connect می توان دید که تابع tcp_set_state ظاهر شده است:
نوشته های سبز رنگ بالای تابع را ترجمه می کنیم:
هویت سوکت هنوز ناشناخته است (sport ممکن است صفر باشد). با این حال، وضعیت را روی SYN-SENT قرار می دهیم و پورت منبع انتخابی قفل سوکت را آزاد نمی کنیم، خود را وارد جداول هش و پس از آن مقداردهی اولیه را کامل می کنیم.مشاهده می شود که پس از فراخوانی تابع sk_set_txhash و عدم برقراری یک شرط، یک توضیح سبزرنگ دیگر آمده است:
خوب، اکنون مقصد را به سوکت اختصاص دهید.
اکنون در VSCode، عبارت tcp_set_state را جتستجو کرده تا تابع مربوطه را بیابیم. تابع tcp_set_state در فایل tcp.c تعریف شده است:
این تابع تشخیص می دهد که آیا یک مقدار حالت داخلی با مقدار BPF متفاوت است یا خیر. اگر چنین اتفاقی بیفتد، قبل از فراخوانی tcp_call_bpf_2arg، باید مقدار داخلی را به مقدار BPF دوباره ترسیم کنیم. برای حالتهای TCP که در BPF صادر میشوند، یک enum جدید تعریف شده است تا حالتهای TCP داخلی مجبور به فریز شدن نباشند.
در همین فایل و تابع، تابع دیگری با نام inet_sk_state_store فراخوانی شده است که در ردیابی پشته نیز به وضوح دیده می شود.
دریافت پکت ها از کارت شبکه
تفاوت اصلی بین ارسال و دریافت این است که هسته نمی تواند پیش بینی کند که یک پکت چه زمانی به دستگاه کارت شبکه می رسد. بنابراین، کد شبکه ای که از دریافت پکت ها مراقبت می کند، در کنترل کننده های وقفه و توابع قابل تعویق (Deferrable Functions) اجرا می شود.
بیایید یک زنجیره معمول از رویدادها را ترسیم کنیم که وقتی پکتیی حاوی آدرس سخت افزاری مناسب (شناسه ی کارت) به دستگاه شبکه می رسد چه رخ می دهد:
۱- دستگاه شبکه پکت را در یک بافر در حافظه ی دستگاه ذخیره می کند (کارت معمولاً چندین پکت را همزمان در یک بافر دایره ای نگه می دارد).
۲- دستگاه شبکه یک وقفه ایجاد می کند.
۳- کنترل کننده وقفه یک بافر سوکت جدید را برای پکت اختصاص داده و مقداردهی اولیه می کند.
۴- کنترل کننده وقفه پکت را از حافظه دستگاه در بافر سوکت کپی می کند.
۵-کنترل کننده ی وقفه یک تابع (مانند تابع ()eth_type_trans برای اترنت و IEEE 802.3) را فراخوانی می کند تا پروتکل پکت محصور شده در فریم Data Link را تعیین کند.
۶- کنترل کننده وقفه تابع ()netif_rx را فراخوانی می کند تا به کد شبکه ی لینوکس اطلاع دهد که یک بسته جدید وارد شده و باید پردازش شود.
نقش وقفه ها در دریافت بسته
وقفه یک سیگنال سخت افزاری از دستگاه به CPU است که به CPU می گوید که دستگاه نیاز به توجه دارد و CPU باید کاری که در حال انجام آن می باشد را متوقف کند و به دستگاه پاسخ دهد.
در بخشی از اطلاعات ردیابی شده ی پشته که مربوط به python3 می باشد عبارت block:block_bio_queue وجود دارد:
block:block_bio_queue تابعی است که وظیفه ی قرار دادن عملیات جدید بلوک IO در صف را بر عهده دارد:
void trace_block_bio_queue(struct bio *bio)
تابع irq_exit_rcu را در VSCode پیدا و بررسی می کنیم؛ این تابع در فایل softirq.c قرار دارد:
همانطور که از توضیحات سبزرنگ بالای تابع بر می آید، ()irq_exit_rcu بدون بهروزرسانی RCU از یک کانتکس وقفه خارج شده و همچنین در صورت نیاز و امکان، softirq ها را پردازش می کند.
تخصیص حافظه به یک پکت از راه رسیده
PACKET_MMAP یک بافر دایره ای قابل تنظیم در فضای کاربر را فراهم می کند که می تواند برای ارسال یا دریافت پکت ها استفاده شود. به این ترتیب برای خواندن بسته ها فقط باید منتظر آنها بود، در بیشتر مواقع نیازی به صدور یک سیستم کال نیست. در مورد انتقال، می توان چندین پکت را از طریق یک سیستم کال ارسال کرد تا بیشترین پهنای باند را به دست آورد. استفاده از یک بافر مشترک بین هسته و کاربر نیز مزیت به حداقل رساندن کپی برداری از پکت ها را به همراه خواهد داشت.
از نقطه نظر فراخوانی سیستم، استفاده از PACKET_MMAP شامل فرآیند زیر است:
• [setup]
()socket -------> ایجاد سوکت گرفتن
()setsockopt ---> تخصیص بافر دایره ای (حلقه)
()mmap ---------> نگاشت بافر اختصاص داده شده به فرآیند کاربر
• [capture]
()poll ---------> انتظار برای بسته های ورودی
• [shutdown]
()close --------> تخریب سوکت کپچر و توزیع همه ی منابع مرتبط
توسط پوریا بازیار
زیر نظر جناب دکتر سید وحید ازهری