futex(2)

الاسم
futex - قفل سريع في مساحة المستخدم

المكتبة
مكتبة سي المعيارية (libc، -lc)

موجز
#include <linux/futex.h>  /* تعريف ثوابت FUTEX_* */
#include <sys/syscall.h>  /* تعريف ثوابت SYS_* */
#include <unistd.h>

long syscall(SYS_futex, uint32_t *uaddr, int op, ...);

الوصف
يوفر استدعاء النظام futex() وسيلة للانتظار حتى يتحقق شرط معين. يُستخدم عادةً كبنية حابسة في سياق مزامنة الذاكرة المشتركة. عند استخدام الـ futexes، تُنفذ أغلبية عمليات المزامنة في مساحة المستخدم. يوظف برنامج مساحة المستخدم استدعاء النظام futex() فقط عندما يُرجح أن البرنامج سيُحبس لفترة طويلة حتى يتحقق الشرط. يمكن استخدام عمليات futex() الأخرى لإيقاظ أي عمليات أو خيوط تنتظر شرطًا محددًا.

الـ futex هو قيمة بحجم 32 بتًا —يُشار إليها أدناه باسم كلمة futex— يُزود استدعاء النظام futex() بعنوانها. (يبلغ حجم الـ futexes حوالي 32 بتًا في جميع المنصات، بما في ذلك الأنظمة بمعمارية 64 بتًا). تُحكم جميع عمليات الـ futex بهذه القيمة. ولمشاركة futex بين العمليات، يُوضع في منطقة من الذاكرة المشتركة، تُنشأ باستخدام (على سبيل المثال) mmap(2) أو shmat(2). (وبذلك، قد تملك كلمة الـ futex عناوين افتراضية مختلفة في عمليات مختلفة، ولكن هذه العناوين تشير جميعها إلى الموقع نفسه في الذاكرة الفيزيائية). في البرامج متعددة الخيوط، يكفي وضع كلمة الـ futex في متغير عام مشترك بين جميع الخيوط.

عند تنفيذ عملية futex تطلب حبس خيط، ستحبسه النواة فقط إذا كانت كلمة futex تملك القيمة التي قدمها المستدعِي (كأحد معطيات استدعاء futex()) بصفتها القيمة المتوقعة لكلمة futex. عملية تحميل قيمة كلمة futex، ومقارنة تلك القيمة مع القيمة المتوقعة، والحبس الفعلي ستحدث بشكل ذري وستكون مرتبة كليًا بالنسبة للعمليات المتزامنة التي تجريها الخيوط الأخرى على كلمة futex نفسها. وبذلك، تُستخدم كلمة futex لربط المزامنة في مساحة المستخدم بتنفيذ الحبس من قبل النواة. وعلى غرار عملية المقارنة والتبديل (compare-and-exchange) الذرية التي قد تغير الذاكرة المشتركة، فإن الحبس عبر futex هو عملية مقارنة وحبس ذرية.

أحد استخدامات الـ futexes هو تنفيذ الأقفال. حالة القفل (أي: مستحوذ عليه أو غير مستحوذ عليه) يمكن تمثيلها كراية يُوصَل إليها ذريًا في الذاكرة المشتركة. في حالة عدم التنازع، يمكن للخيط الوصول إلى حالة القفل أو تعديلها باستخدام تعليمات ذرية، على سبيل المثال تغييره ذريًا من غير مستحوذ عليه إلى مستحوذ عليه باستخدام تعليمات مقارنة وتبديل ذرية. (تُنفذ مثل هذه التعليمات بالكامل في وضع المستخدم، ولا تحتفظ النواة بأي معلومات عن حالة القفل). ومن ناحية أخرى، قد يتعذر على خيط الاستحواذ على قفل لأنه مستحوذ عليه بالفعل من قبل خيط آخر. يمكنه حينئذٍ تمرير راية القفل ككلمة futex والقيمة التي تمثل حالة الاستحواذ كقيمة متوقعة لعملية انتظار futex(). ستقوم عملية futex() هذه بالحبس إذا وفقط إذا كان القفل لا يزال مستحوذًا عليه (أي أن القيمة في كلمة futex لا تزال تطابق "حالة الاستحواذ"). عند تحرير القفل، يجب على الخيط أولاً إعادة ضبط حالة القفل إلى غير مستحوذ عليه ثم تنفيذ عملية futex توقظ الخيوط المحبوسة على راية القفل المستخدمة ككلمة futex (يمكن تحسين ذلك لاحقًا لتجنب عمليات الإيقاظ غير الضرورية). انظر futex(7) لمزيد من التفاصيل حول كيفية استخدام الـ futexes.

إلى جانب وظائف الانتظار والإيقاظ الأساسية في futex، هناك عمليات futex إضافية تهدف إلى دعم حالات استخدام أكثر تعقيدًا.

لاحظ أنه لا يلزم تهيئة أو تدمير صريح لاستخدام الـ futexes؛ تحتفظ النواة بالـ futex (أي أثر التنفيذ الداخلي للنواة) فقط أثناء تنفيذ عمليات مثل FUTEX_WAIT(2const) على كلمة futex معينة.

  المعطيات
يشير المعطى uaddr إلى كلمة futex. في جميع المنصات، تكون الـ futexes أعدادًا صحيحة بحجم أربعة بايتات ويجب أن تكون محاذية على حدود أربعة بايتات. تُحدد العملية المراد تنفيذها على الـ futex في المعطى op.

  عمليات Futex
يتكون المعطى op من جزأين: أمر يحدد العملية المراد تنفيذها، مدمجًا عبر عملية OR بتية مع خيار واحد أو أكثر يعدل سلوك العملية. الخيارات التي قد تُضمن في op هي كما يلي:

FUTEX_PRIVATE_FLAG (منذ لينكس 2.6.22)
    يمكن استخدام بت الخيار هذا مع جميع عمليات futex. يخبر النواة أن الـ futex خاص بالعملية وليس مشتركًا مع عملية أخرى (أي يُستخدم للمزامنة فقط بين خيوط العملية نفسها). يسمح هذا للنواة بإجراء بعض تحسينات الأداء الإضافية.

    من باب التسهيل، يعرّف <linux/futex.h> مجموعة من الثوابت باللاحقة _PRIVATE التي تكافئ جميع العمليات المدرجة أدناه، ولكن مع دمج FUTEX_PRIVATE_FLAG في قيمة الثابت عبر عملية OR. وبذلك، توجد FUTEX_WAIT_PRIVATE وFUTEX_WAKE_PRIVATE، وهكذا.

FUTEX_CLOCK_REALTIME (منذ لينكس 2.6.28)
    يمكن استخدام بت الخيار هذا فقط مع عمليات FUTEX_WAIT_BITSET(2const)، وFUTEX_WAIT_REQUEUE_PI(2const)، و(منذ لينكس 4.5) FUTEX_WAIT(2const)، و(منذ لينكس 5.14) FUTEX_LOCK_PI2(2const).

    إذا ضُبط هذا الخيار، تقيس النواة مهلة الانتظار timeout مقابل ساعة الوقت الحقيقي CLOCK_REALTIME.

    إذا لم يُضبط هذا الخيار، تقيس النواة مهلة الانتظار timeout مقابل ساعة الوقت الرتيب CLOCK_MONOTONIC.

العملية المحددة في op هي واحدة مما يلي:

FUTEX_WAIT(2const)
FUTEX_WAKE(2const)
FUTEX_FD(2const)
FUTEX_REQUEUE(2const)
FUTEX_CMP_REQUEUE(2const)
FUTEX_WAKE_OP(2const)
FUTEX_WAIT_BITSET(2const)
FUTEX_WAKE_BITSET(2const)

  Futexes توريث الأولويات
يدعم لينكس futexes توريث الأولويات (PI) للتعامل مع مشكلات انعكاس الأولويات التي قد تواجه أقفال futex العادية. انعكاس الأولويات هو المشكلة التي تحدث عندما تُحبس مهمة عالية الأولوية بانتظار الاستحواذ على قفل تملكه مهمة منخفضة الأولوية، بينما تقوم مهام ذات أولوية متوسطة باستمرار بإزاحة المهمة منخفضة الأولوية من المعالج. ونتيجة لذلك، لا تحرز المهمة منخفضة الأولوية أي تقدم نحو تحرير القفل، وتظل المهمة عالية الأولوية محبوسة.

توريث الأولويات هو آلية للتعامل مع مشكلة انعكاس الأولويات. بموجب هذه الآلية، عندما تُحبس مهمة عالية الأولوية بسبب قفل تملكه مهمة منخفضة الأولوية، تُرفع أولوية المهمة منخفضة الأولوية مؤقتًا إلى مستوى أولوية المهمة عالية الأولوية، بحيث لا تزيحها أي مهام ذات مستوى متوسط، وبذلك يمكنها إحراز تقدم نحو تحرير القفل. ليكون توريث الأولويات فعالاً، يجب أن يكون متعديًا، مما يعني أنه إذا حُبست مهمة عالية الأولوية بسبب قفل تملكه مهمة منخفضة الأولوية والمحبوسة هي نفسها بسبب قفل تملكه مهمة أخرى متوسطة الأولوية (وهكذا، لسلاسل بأي طول)، فإن كلتا المهمتين (أو بشكل أعم، جميع المهام في سلسلة القفل) تُرفع أولوياتها لتصبح مماثلة للمهمة عالية الأولوية.

من منظور مساحة المستخدم، ما يجعل الـ futex مدركًا لتوريث الأولويات (PI-aware) هو اتفاق سياسة (موصوف أدناه) بين مساحة المستخدم والنواة حول قيمة كلمة futex، مقترنًا باستخدام عمليات PI-futex الموضحة أدناه. (على عكس عمليات futex الأخرى الموضحة أعلاه، صُممت عمليات PI-futex لتنفيذ آليات اتصال بين العمليات (IPC) محددة للغاية).

تختلف عمليات PI-futex الموضحة أدناه عن عمليات futex الأخرى في أنها تفرض سياسة على استخدام قيمة كلمة futex:

•   إذا لم يُستحوذ على القفل، يجب أن تكون قيمة كلمة futex هي 0.

•   إذا استُحوذ على القفل، يجب أن تكون قيمة كلمة futex هي معرف الخيط (TID؛ انظر gettid(2)) للخيط المالك.

•   إذا كان القفل مملوكًا وهناك خيوط تتنازع عليه، يجب ضبط بت FUTEX_WAITERS في قيمة كلمة futex؛ بعبارة أخرى، هذه القيمة هي:

        FUTEX_WAITERS | TID

    (لاحظ أنه من غير الصالح أن تكون كلمة PI futex بلا مالك مع ضبط FUTEX_WAITERS).

مع تطبيق هذه السياسة، يمكن لتطبيق مساحة المستخدم الاستحواذ على قفل غير مستحوذ عليه أو تحرير قفل باستخدام تعليمات ذرية تُنفذ في وضع المستخدم (على سبيل المثال، عملية مقارنة وتبديل مثل cmpxchg في معمارية x86). يتكون الاستحواذ على قفل ببساطة من استخدام المقارنة والتبديل لضبط قيمة كلمة futex ذريًا إلى معرف خيط (TID) المستدعي إذا كانت قيمتها السابقة 0. يتطلب تحرير القفل استخدام المقارنة والتبديل لضبط قيمة كلمة futex إلى 0 إذا كانت القيمة السابقة هي TID المتوقع.

إذا كان الـ futex مستحوذًا عليه بالفعل (أي يملك قيمة غير صفرية)، يجب على المنتظرين توظيف عمليات FUTEX_LOCK_PI(2const) أو FUTEX_LOCK_PI2(2const) للاستحواذ على القفل. إذا كانت هناك خيوط أخرى تنتظر القفل، يُضبط بت FUTEX_WAITERS في قيمة الـ futex؛ في هذه الحالة، يجب على مالك القفل توظيف عملية FUTEX_UNLOCK_PI(2const) لتحرير القفل.

في الحالات التي يُجبر فيها المستدعون على الدخول إلى النواة (أي يُطلب منهم إجراء استدعاء futex())، فإنهم يتعاملون مباشرة مع ما يسمى RT-mutex، وهي آلية إقفال في النواة تنفذ دلالات توريث الأولويات المطلوبة. بعد الاستحواذ على RT-mutex، تُحدث قيمة الـ futex وفقًا لذلك، قبل أن يعود الخيط المستدعي إلى مساحة المستخدم.

من المهم ملاحظة أن النواة ستحدث قيمة كلمة futex قبل العودة إلى مساحة المستخدم. (يمنع هذا احتمال انتهاء قيمة كلمة futex في حالة غير صالحة، مثل وجود مالك بينما القيمة هي 0، أو وجود منتظرين دون ضبط بت FUTEX_WAITERS).

إذا كان لـ futex ما يقابله من RT-mutex في النواة (أي يوجد منتظرون محبوسون) ومات مالك الـ futex/RT-mutex بشكل غير متوقع، تقوم النواة بتنظيف الـ RT-mutex وتسليمه للمنتظر التالي. وهذا بدوره يتطلب تحديث قيمة مساحة المستخدم وفقًا لذلك. وللإشارة إلى أن هذا مطلوب، تضبط النواة بت FUTEX_OWNER_DIED في كلمة futex مع معرف الخيط للمالك الجديد. يمكن لمساحة المستخدم اكتشاف هذا الموقف عبر وجود بت FUTEX_OWNER_DIED وهي المسؤولة حينئذٍ عن تنظيف الحالة القديمة التي تركها المالك الميت.

تُجرى العمليات على PI futexes عبر تحديد إحدى القيم المدرجة أدناه في op. لاحظ أنه يجب استخدام عمليات PI futex كعمليات مزدوجة وتخضع لبعض المتطلبات الإضافية:

•   تزدوج FUTEX_LOCK_PI(2const) وFUTEX_LOCK_PI2(2const) و FUTEX_TRYLOCK_PI(2const) مع FUTEX_UNLOCK_PI(2const). يجب استدعاء FUTEX_UNLOCK_PI(2const) فقط على futex يملكه الخيط المستدعي، كما هو محدد في سياسة القيمة، وإلا سينتج الخطأ EPERM.

•   تزدوج FUTEX_WAIT_REQUEUE_PI(2const) مع FUTEX_CMP_REQUEUE_PI(2const). يجب أن يتم ذلك من futex ليس PI إلى PI futex مختلف (وإلا سينتج الخطأ EINVAL). بالإضافة إلى ذلك، يجب أن يكون عدد المنتظرين المراد إيقاظهم 1 (وإلا سينتج الخطأ EINVAL).

عمليات PI futex هي كما يلي:

FUTEX_LOCK_PI(2const)
FUTEX_LOCK_PI2(2const)
FUTEX_TRYLOCK_PI(2const)
FUTEX_UNLOCK_PI(2const)
FUTEX_CMP_REQUEUE_PI(2const)
FUTEX_WAIT_REQUEUE_PI(2const)

أُضيفت FUTEX_WAIT_REQUEUE_PI(2const) وFUTEX_CMP_REQUEUE_PI(2const) لدعم حالة استخدام محددة تمامًا: دعم متغيرات حالة خيوط POSIX المدركة لتوريث الأولويات. الفكرة هي أن هذه العمليات يجب أن تزدوج دائمًا لضمان بقاء مساحة المستخدم والنواة متزامنتين. وبذلك، في عملية FUTEX_WAIT_REQUEUE_PI(2const)، يحدد تطبيق مساحة المستخدم مسبقًا هدف إعادة الصف (requeue) التي تحدث في عملية FUTEX_CMP_REQUEUE_PI(2const).

قيمة الإرجاع
عند الخطأ، تُعاد القيمة -1، ويُضبط errno للإشارة إلى الخطأ.

تعتمد قيمة الإرجاع عند النجاح على العملية.

الأخطاء

EACCES
    لا يوجد وصول قراءة إلى ذاكرة كلمة futex.

EFAULT
    لا يشير uaddr إلى عنوان صالح في مساحة المستخدم.

EINVAL
    لا يشير uaddr إلى كائن صالح —أي أن العنوان ليس محاذيًا لأربعة بايتات.

EINVAL
    معطى غير صالح.

ENOSYS
    عملية غير صالحة محددة في op.

ENOSYS
    حُدد الخيار FUTEX_CLOCK_REALTIME في op، ولكن العملية المرافقة لم تكن FUTEX_WAIT_BITSET(2const)، ولا FUTEX_WAIT_REQUEUE_PI(2const)، ولا FUTEX_LOCK_PI2(2const).

المعايير
لينكس.

التاريخ
لينكس 2.6.0.

دُمج دعم futex الأولي في لينكس 2.5.7 ولكن بدلالات مختلفة عما وُصف أعلاه. قُدم استدعاء نظام بأربعة معطيات مع الدلالات الموضحة في هذه الصفحة في لينكس 2.5.40. أُضيف معطى خامس في لينكس 2.5.70، وأُضيف معطى سادس في لينكس 2.6.7.

أمثلة
يوضح البرنامج أدناه استخدام الـ futexes في برنامج حيث تستخدم فيه عملية أب وعملية ابن زوجًا من الـ futexes الموجودة داخل تخطيط مجهول مشترك لمزامنة الوصول إلى مورد مشترك: الطرفية. تكتب كل واحدة من العمليتين nloops رسالة (معطى في سطر الأوامر قيمته المبدئية 5 إذا حُذف) إلى الطرفية وتوظف بروتوكول مزامنة يضمن تبادلهما في كتابة الرسائل. عند تشغيل هذا البرنامج، نرى مخرجات مثل ما يلي:

    $ ./futex_demo;
    Parent (18534) 0
    Child  (18535) 0
    Parent (18534) 1
    Child  (18535) 1
    Parent (18534) 2
    Child  (18535) 2
    Parent (18534) 3
    Child  (18535) 3
    Parent (18534) 4
    Child  (18535) 4

  مصدر البرنامج
    /* futex_demo.c

       الاستخدام: futex_demo [nloops]
                        (المبدئي: 5)

       توضيح استخدام الـ futexes في برنامج حيث يستخدم الأب والابن
       زوجًا من الـ futexes الموجودة داخل تخطيط مجهول مشترك لـ
       مزامنة الوصول إلى مورد مشترك: الطرفية. تكتب كل واحدة من
       العمليتين رسائل بـ 'num-loops' إلى الطرفية وتوظف
       بروتوكول مزامنة يضمن تبادلهما في
       كتابة الرسائل.
    */
    #define _GNU_SOURCE
    #include <err.h>
    #include <errno.h>
    #include <linux/futex.h>
    #include <stdatomic.h>
    #include <stdint.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/mman.h>
    #include <sys/syscall.h>
    #include <sys/time.h>
    #include <sys/wait.h>
    #include <unistd.h>

    static uint32_t *futex1, *futex2, *iaddr;

    static int
    futex(uint32_t *uaddr, int op, uint32_t val,
          const struct timespec *timeout, uint32_t *uaddr2, uint32_t val3)
    {
        return syscall(SYS_futex, uaddr, op, val,
                       timeout, uaddr2, val3);
    }

    /* الاستحواذ على الـ futex الذي يشير إليه 'futexp': الانتظار حتى تصبح قيمته
       1، ثم ضبط القيمة إلى 0.  */

    static void
    fwait(uint32_t *futexp)
    {
        long            s;
        const uint32_t  one = 1;

        /* atomic_compare_exchange_strong(ptr, oldval, newval)
           ينفذ ذريًا ما يعادل:

               if (*ptr == *oldval)
                   *ptr = newval;

           يعيد true إذا كان الاختبار صحيحًا وحُدث *ptr.  */

        for (;;) {

            /* هل الـ futex متاح؟ */
            if (atomic_compare_exchange_strong(futexp, &one, 0))
                break;      /* نعم */

            /* الـ futex غير متاح؛ انتظر.  */

            s = futex(futexp, FUTEX_WAIT, 0, NULL, NULL, 0);
            if (s == -1 && errno != EAGAIN)
                err(EXIT_FAILURE, "futex-FUTEX_WAIT");
        }
    }

    /* تحرير الـ futex الذي يشير إليه 'futexp': إذا كانت قيمة الـ futex حاليًا
       0، فاضبط قيمته إلى 1 ثم أيقظ أي منتظرين لـ futex،
       بحيث إذا كان النظير محبوسًا في fwait()، يمكنه المتابعة.  */

    static void
    fpost(uint32_t *futexp)
    {
        long            s;
        const uint32_t  zero = 0;

        /* atomic_compare_exchange_strong() شُرحت
           في التعليقات أعلاه.  */

        if (atomic_compare_exchange_strong(futexp, &zero, 1)) {
            s = futex(futexp, FUTEX_WAKE, 1, NULL, NULL, 0);
            if (s  == -1)
                err(EXIT_FAILURE, "futex-FUTEX_WAKE");
        }
    }

    int
    main(int argc, char *argv[])
    {
        pid_t         childPid;
        unsigned int  nloops;

        setbuf(stdout, NULL);

        nloops = (argc > 1) ? atoi(argv[1]) : 5;

        /* إنشاء تخطيط مجهول مشترك سيحتوي على الـ futexes.
           بما أن الـ futexes ستُشارك بين العمليات، فسنستخدم
           لاحقًا عمليات الـ futex "المشتركة" (أي ليس
           التي تنتهي بـ "_PRIVATE").  */

        iaddr = mmap(NULL, sizeof(*iaddr) * 2, PROT_READ | PROT_WRITE,
                     MAP_ANONYMOUS | MAP_SHARED, -1, 0);
        if (iaddr == MAP_FAILED)
            err(EXIT_FAILURE, "mmap");

        futex1 = &iaddr[0];
        futex2 = &iaddr[1];

        *futex1 = 0;        /* الحالة: غير متاح */
        *futex2 = 1;        /* الحالة: متاح */

        /* إنشاء عملية ابن ترث التخطيط المجهول المشترك.
           */

        childPid = fork();
        if (childPid == -1)
            err(EXIT_FAILURE, "fork");

        if (childPid == 0) {        /* الابن */
            for (unsigned int j = 0; j < nloops; j++) {
                fwait(futex1);
                printf("Child  (%jd) %u\n", (intmax_t) getpid(), j);
                fpost(futex2);
            }

            exit(EXIT_SUCCESS);
        }

        /* الأب يصل إلى هنا.  */

        for (unsigned int j = 0; j < nloops; j++) {
            fwait(futex2);
            printf("Parent (%jd) %u\n", (intmax_t) getpid(), j);
            fpost(futex1);
        }

        wait(NULL);

        exit(EXIT_SUCCESS);
    }

انظر أيضًا
get_robust_list(2), restart_syscall(2), pthread_mutexattr_getprotocol(3), futex(7), sched(7)

ملفات مصدر النواة التالية:

•   Documentation/locking/pi-futex.rst

•   Documentation/locking/futex-requeue-pi.rst

•   Documentation/locking/rt-mutex.rst

•   Documentation/locking/rt-mutex-design.rst

•   Documentation/robust-futex-ABI.rst

Franke, H., Russell, R., and Kirwood, M., 2002.
Fuss, Futexes and Furwocks: Fast Userlevel Locking in Linux (http://kernel.org/doc/ols/2002/ols2002-pages-479-495.pdf) (من وقائع ندوة أوتاوا للينكس 2002).

Hart, D., 2009. A futex overview and update (http://lwn.net/Articles/360699/).

Hart, D. و Guniguntala, D., 2009. Requeue-PI: جعل glibc Condvars مدركة لـ PI (http//lwn.net/images/conf/rtlws11/papers/proc/p10.pdf) (من وقائع ورشة عمل لينكس للوقت الحقيقي 2009).

Drepper, U., 2011. الفوتكسات خادعة (http//www.akkadia.org/drepper/futex.pdf).

مكتبة أمثلة فوتكس، futex-*.tar.bz2 (https//mirrors.kernel.org/pub/linux/kernel/people/rusty/).

ترجمة
تُرجمت هذه الصفحة من الدليل بواسطة زايد السعيدي <zayed.alsaidi@gmail.com>

هذه الترجمة هي وثيقة مجانية؛ راجع رخصة جنو العامة الإصدار 3 (https://www.gnu.org/licenses/gpl-3.0.html) أو ما بعده للاطلاع على شروط حقوق النشر. لا توجد أي ضمانات.

إذا وجدت أي أخطاء في ترجمة صفحة الدليل هذه، يرجى إرسال بريد إلكتروني إلى قائمة بريد المترجمين: kde-l10n-ar@kde.org.