تنفيذ Epoll ، الجزء 2

أثناء نشر ترجمة المقالة الأولى من سلسلة التنفيذ epoll، أجرينا دراسة استقصائية حول جدوى استمرار ترجمة الدورة. أيد أكثر من 90٪ من المشاركين في الاستطلاع ترجمة بقية المقالات. لذلك ، ننشر اليوم ترجمة للمادة الثانية من هذه الدورة.







دالة Ep_insert ()



الوظيفة ep_insert()هي واحدة من أهم الوظائف في التنفيذ epoll. يعد فهم كيفية عملها أمرًا في غاية الأهمية لفهم كيفية epollحصوله بالضبط على معلومات حول الأحداث الجديدة من الملفات التي يشاهدها. يمكن العثور على



الإعلان ep_insert()في السطر 1267 من الملف fs/eventpoll.c. لنلقِ نظرة على بعض مقتطفات التعليمات البرمجية لهذه الوظيفة:



user_watches = atomic_long_read(&ep->user->epoll_watches);
if (unlikely(user_watches >= max_user_watches))
  return -ENOSPC;


في مقتطف الشفرة هذا ، ep_insert()تتحقق الوظيفة أولاً لمعرفة ما إذا كان العدد الإجمالي للملفات التي يشاهدها المستخدم الحالي ليس أكبر من القيمة المحددة في /proc/sys/fs/epoll/max_user_watches. إذا user_watches >= max_user_watches، فإن الوظيفة تنتهي فورًا errnoبالمجموعة إلى ENOSPC.



ثم ep_insert()يخصص الذاكرة باستخدام آلية إدارة ذاكرة Linux kernel slab:



if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))
  return -ENOMEM;


إذا كانت الوظيفة قادرة على تخصيص ذاكرة كافية لها struct epitem، فسيتم تنفيذ عملية التهيئة التالية:



/*  ... */
INIT_LIST_HEAD(&epi->rdllink);
INIT_LIST_HEAD(&epi->fllink);
INIT_LIST_HEAD(&epi->pwqlist);
epi->ep = ep;
ep_set_ffd(&epi->ffd, tfile, fd);
epi->event = *event;
epi->nwait = 0;
epi->next = EP_UNACTIVE_PTR;


بعد ذلك ، ep_insert()سيحاول تسجيل رد الاتصال في واصف الملف. ولكن قبل أن نتحدث عن ذلك ، نحتاج إلى التعرف على بعض هياكل البيانات المهمة.



الإطار poll_tableكيان مهم يستخدمه تطبيق poll()VFS. (أفهم أن هذا قد يكون محيرًا ، لكن هنا أود أن أوضح أن الوظيفة التي poll()ذكرتها هنا هي تنفيذ لعملية ملف poll()، وليست استدعاء نظام poll()). تم الإعلان عنها في include/linux/poll.h:



typedef struct poll_table_struct {
  poll_queue_proc _qproc;
  unsigned long _key;
} poll_table;


poll_queue_procيمثل الكيان نوعًا من وظيفة رد الاتصال التي تبدو كالتالي:



typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);


عضو في _keyالجدول poll_tableليس ما يبدو عليه لأول مرة. وبالتحديد ، على الرغم من أن الاسم يوحي بـ "مفتاح" معين ، في _keyالواقع ، يتم تخزين أقنعة الأحداث التي تهمنا. في التنفيذ ، تم epoll _keyضبطه على ~0(مكمل لـ 0). هذا يعني أنه epollيسعى لتلقي معلومات حول الأحداث من أي نوع. هذا أمر منطقي ، حيث يمكن لتطبيقات مساحة المستخدم تغيير قناع الحدث في أي وقت باستخدام epoll_ctl()، وقبول جميع الأحداث من VFS ثم تصفيتها في التنفيذ epoll، مما يجعل الأمور أسهل.



من أجل تسهيل استعادة poll_queue_procالهيكل الأصلي epitem، فإنه epollيستخدم بنية بسيطة تسمىep_pqueueالذي يعمل كغلاف poll_tableمع مؤشر للبنية المقابلة epitem(ملف fs/eventpoll.c، السطر 243):



/* -,    */
struct ep_pqueue {
  poll_table pt;
  struct epitem *epi;
};


ثم يتم ep_insert()التهيئة struct ep_pqueue. التعليمة البرمجية التالية يكتب أولا إلى epiبنية عضوا في ep_pqueueمؤشر إلى بنية epitemالمقابلة لملف نحاول إضافة، ثم يكتب ep_ptable_queue_proc()إلى _qprocهيكل عضو ep_pqueueو _keyيكتب عليه ~0.



/*      */
epq.epi = epi;
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);


ep_insert()سوف يتم الاتصال بعد ذلك ep_item_poll(epi, &epq.pt);، مما سينتج عنه استدعاء للتنفيذ poll()المرتبط بالملف.



دعنا نلقي نظرة على مثال يستخدم تطبيق poll()مكدس Linux TCP ونكتشف بالضبط ما يفعله هذا التنفيذ poll_table.



الوظيفة tcp_poll()هي تنفيذ poll()لمآخذ توصيل TCP. يمكن العثور على الكود الخاص به في الملف net/ipv4/tcp.c، في السطر 436. هنا مقتطف من هذا الرمز:



unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait)
{
  unsigned int mask;
  struct sock *sk = sock->sk;
  const struct tcp_sock *tp = tcp_sk(sk);

  sock_rps_record_flow(sk);

  sock_poll_wait(file, sk_sleep(sk), wait);

  //  
}


tcp_poll()تستدعي الوظيفة sock_poll_wait()، تمرير ، كوسيطة ثانية ، وكالوسيطة sk_sleep(sk)الثالثة - wait(هذا هو tcp_poll()الجدول الذي تم تمريره مسبقًا إلى الوظيفة poll_table).



ما هذا sk_sleep()؟ كما اتضح ، هذا مجرد برنامج للوصول إلى قائمة انتظار الحدث لهيكل معين sock(ملف include/net/sock.h، السطر 1685):



static inline wait_queue_head_t *sk_sleep(struct sock *sk)
{
  BUILD_BUG_ON(offsetof(struct socket_wq, wait) != 0);
  return &rcu_dereference_raw(sk->sk_wq)->wait;
}


ماذا sock_poll_wait()ستفعل بقائمة انتظار الحدث؟ اتضح أن هذه الوظيفة ستقوم ببعض الفحص البسيط ثم استدعاء poll_wait()نفس المعلمات. poll_wait()ستقوم الوظيفة بعد ذلك باستدعاء رد الاتصال الذي حددناه وتمريره إلى قائمة انتظار الحدث (ملف include/linux/poll.h، السطر 42):



static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
  if (p && p->_qproc && wait_address)
    p->_qproc(filp, wait_address, p);
}


في حالة epollالكيان ، _qprocستكون دالة ep_ptable_queue_proc()معرّفة في الملف fs/eventpoll.cفي السطر 1091.



/*
*  - ,       
*     ,    .
*/
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
       poll_table *pt)
{
  struct epitem *epi = ep_item_from_epqueue(pt);
  struct eppoll_entry *pwq;

  if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {
    init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
    pwq->whead = whead;
    pwq->base = epi;
    add_wait_queue(whead, &pwq->wait);
    list_add_tail(&pwq->llink, &epi->pwqlist);
    epi->nwait++;
  } else {
    /*       */
    epi->nwait = -1;
  }
}


أولاً ، ep_ptable_queue_proc()يحاول استعادة البنية epitemالتي تتوافق مع الملف من قائمة انتظار الانتظار التي نعمل معها. نظرًا لأنه epollيستخدم بنية مجمعة ep_pqueue، فإن الاستعادة epitemمن المؤشر poll_tableهي عملية مؤشر بسيطة.



بعد ذلك ، تقوم ep_ptable_queue_proc()فقط بتخصيص أكبر قدر من الذاكرة حسب الحاجة struct eppoll_entry. تعمل هذه البنية بمثابة "رابط" بين قائمة الانتظار للملف الذي تتم مشاهدته والهيكل المقابل epitemلهذا الملف. ومن epollالمهم للغاية أن نعرف أين رئيس الانتظار قائمة انتظار لملف تحت المراقبة. وإلا epollفلن يتمكن من إلغاء تسجيل قائمة الانتظار لاحقًا. بناءeppoll_entryيتضمن أيضًا قائمة انتظار ( pwq->wait) مع توفير وظيفة استئناف العملية ep_poll_callback(). ربما pwq->waitيكون هذا هو الجزء الأهم في التنفيذ بأكمله epoll، حيث يتم استخدام هذا الكيان لحل المهام التالية:



  1. مراقبة الأحداث التي تحدث مع ملف معين يتم مراقبته.
  2. استئناف عمل العمليات الأخرى في حالة نشوء مثل هذه الحاجة.


ثم يتم ep_ptable_queue_proc()إرفاقه pwq->waitبقائمة انتظار الملف الهدف ( whead). ستضيف الوظيفة أيضًا struct eppoll_entryإلى القائمة المرتبطة من struct epitem( epi->pwqlist) وتزيد القيمة التي epi->nwaitتمثل طول القائمة epi->pwqlist.



وهنا لدي سؤال واحد. لماذا epollاستخدام قائمة مرتبطة لتخزين بنية eppoll_entryداخل بنية epitemملف واحد؟ أليست هناك حاجة epitemلعنصر واحد فقط eppoll_entry؟



ومع ذلك ، لا يمكنني الإجابة على هذا السؤال بالضبط. بقدر ما أستطيع أن أقول ، ما لم يكن شخص ما سيستخدم حالات epollفي بعض الحلقات المجنونة ، فستحتوي القائمة epi->pwqlistعلى عنصر واحد فقط struct eppoll_entry، وepi->nwaitبالنسبة لمعظم الملفات على الأرجح 1.



الشيء الجيد هو أن الغموض المحيط epi->pwqlistلا يؤثر بأي شكل من الأشكال على ما سأتحدث عنه أدناه. وبالتحديد ، سنتحدث عن كيفية قيام Linux بإعلام حالات epollالأحداث التي تحدث للملفات التي تتم مراقبتها.



تذكر ما تحدثنا عنه في القسم السابق؟ كان حول ما epollيلحق wait_queue_tبقائمة الانتظار للملف (الملفات) الهدف wait_queue_head_t. على الرغم من wait_queue_tاستخدامها بشكل شائع كآلية لاستئناف العمليات ، إلا أنها في الأساس مجرد بنية تخزن مؤشرًا لوظيفة سيتم استدعاؤها عندما يقرر Linux استئناف العمليات من قائمة الانتظار wait_queue_tالمرفقة بها wait_queue_head_t. في هذه الوظيفةepollيمكن أن تقرر ما يجب فعله بإشارة الاستئناف ، ولكن epollليست هناك حاجة لاستئناف أي عملية! كما سترى لاحقًا ، عادةً ep_poll_callback()لا يحدث شيء عند استدعاء استئناف.



أفترض أنه من الجدير بالذكر أيضًا أن آلية استئناف العملية المستخدمة poll()تعتمد بالكامل على التنفيذ. في حالة ملفات مقبس TCP ، يكون رأس قائمة الانتظار عضوًا sk_wqمخزّنًا في الهيكل sock. يوضح هذا أيضًا الحاجة إلى استخدام رد اتصال ep_ptable_queue_proc()للعمل مع قائمة الانتظار. نظرًا لأنه في تطبيقات قائمة الانتظار لملفات مختلفة ، يمكن أن يظهر رأس قائمة الانتظار في أماكن مختلفة تمامًا ، فلا توجد لدينا طريقة للعثور على القيمة التي نحتاجهاwait_queue_head_tدون استخدام رد الاتصال.



متى بالضبط يتم استئناف العمل sk_wqفي الهيكل sock؟ كما اتضح ، يتبع نظام Linux socket نفس مبادئ التصميم "OO" مثل VFS. sockيعلن الهيكل عن الخطافات التالية في السطر 2312 من الملف net/core/sock.c:



void sock_init_data(struct socket *sock, struct sock *sk)
{
  //  ...
  sk->sk_data_ready  =   sock_def_readable;
  sk->sk_write_space =  sock_def_write_space;
  //  ...
}


B sock_def_readable()و sock_def_write_space()المكالمة wake_up_interruptible_sync_poll()ل (struct sock)->sk_wqغرض وظيفة الاستدعاء، عمل عملية قابلة للتجديد.



متى sk->sk_data_ready()و سوف يطلق sk->sk_write_space()؟ ذلك يعتمد على التنفيذ. لنأخذ مآخذ TCP كمثال. sk->sk_data_ready()سيتم استدعاء الوظيفة في النصف الثاني من معالج المقاطعة عندما يكمل اتصال TCP إجراء تبادل الإشارات ثلاثي الاتجاهات ، أو عند استلام مخزن مؤقت لمقبس TCP معين. sk->sk_write_space()سيتم استدعاء الوظيفة عندما تتغير حالة المخزن المؤقت من fullإلى available. إذا كنت تضع ذلك في الاعتبار عند تحليل الموضوعات التالية ، لا سيما تلك المتعلقة بالتحفيز الأمامي ، فستبدو هذه الموضوعات أكثر إثارة للاهتمام.



النتيجة



بهذا تنتهي المقالة الثانية في سلسلة من المقالات حول التنفيذ epoll. في المرة القادمة ، epollدعنا نتحدث عما يفعله بالضبط في رد الاتصال المسجل في قائمة انتظار استئناف عملية المقبس.



هل استخدمت epoll؟










All Articles