userfaultfd(2)

الاسم
userfaultfd - إنشاء واصف ملف لمعالجة أخطاء الصفحات في مساحة المستخدم

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

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

int syscall(SYS_userfaultfd, int flags);

ملاحظة: لا توفر glibc غلافًا لـ userfaultfd()، مما يستلزم استخدام syscall(2).

الوصف
ينشئ userfaultfd() كائن userfaultfd جديد يمكن استخدامه لتفويض معالجة أخطاء الصفحات إلى تطبيق في مساحة المستخدم، ويعيد واصف ملف يشير إلى الكائن الجديد. يُضبط كائن userfaultfd الجديد باستخدام ioctl(2).

بمجرد ضبط كائن userfaultfd، يمكن للتطبيق استخدام read(2) لاستقبال إشعارات userfaultfd. قد تكون القراءات من userfaultfd حاجبة أو غير حاجبة، اعتمادًا على قيمة flags المستخدمة لإنشاء userfaultfd أو الاستدعاءات اللاحقة لـ fcntl(2).

قد تُجمع القيم التالية بعملية OR على مستوى البت في flags لتغيير سلوك userfaultfd():

O_CLOEXEC
    فعّل علم الإغلاق عند التنفيذ (close-on-exec) لواصف ملف userfaultfd الجديد. انظر وصف علم O_CLOEXEC في open(2).

O_NONBLOCK
    يفعّل التشغيل غير الحاجب لكائن userfaultfd. انظر وصف علم O_NONBLOCK في open(2).

UFFD_USER_MODE_ONLY
    هذا علم خاص بـ userfaultfd أُدخل في لينكس 5.11. عند ضبطه، سيكون كائن userfaultfd قادرًا فقط على معالجة أخطاء الصفحات الناشئة من مساحة المستخدم في المناطق المسجلة. عندما يُقدح خطأ ناشئ من النواة في النطاق المسجل باستخدام userfaultfd هذا، ستُرسل إشارة SIGBUS.

عند إغلاق آخر واصف ملف يشير إلى كائن userfaultfd، تُلغى تسجيلات جميع نطاقات الذاكرة التي سُجلت مع الكائن وتُفرغ الأحداث غير المقروءة.

يدعم Userfaultfd ثلاثة أوضاع للتسجيل:

UFFDIO_REGISTER_MODE_MISSING (منذ لينكس 4.10)
    عند التسجيل بالوضع UFFDIO_REGISTER_MODE_MISSING، ستتلقى مساحة المستخدم إشعار خطأ صفحة عند الوصول إلى صفحة مفقودة. سيُوقف الخيط المتعطل عن التنفيذ حتى يُحل خطأ الصفحة من مساحة المستخدم بواسطة ioctl إما UFFDIO_COPY أو UFFDIO_ZEROPAGE.

UFFDIO_REGISTER_MODE_MINOR (منذ لينكس 5.13)
    عند التسجيل بالوضع UFFDIO_REGISTER_MODE_MINOR، ستتلقى مساحة المستخدم إشعار خطأ صفحة عند حدوث خطأ صفحة ثانوي. أي عندما تكون صفحة السند في خبيئة الصفحات، ولكن مدخلات جدول الصفحات غير موجودة بعد. سيُوقف الخيط المتعطل عن التنفيذ حتى يُحل خطأ الصفحة من مساحة المستخدم بواسطة ioctl UFFDIO_CONTINUE.

UFFDIO_REGISTER_MODE_WP (منذ لينكس 5.7)
    عند التسجيل بالوضع UFFDIO_REGISTER_MODE_WP، ستتلقى مساحة المستخدم إشعار خطأ صفحة عند كتابة صفحة محمية ضد الكتابة. سيُوقف الخيط المتعطل عن التنفيذ حتى تُزيل مساحة المستخدم حماية الكتابة عن الصفحة باستخدام ioctl UFFDIO_WRITEPROTECT.

يمكن تمكين أوضاع متعددة في نفس الوقت لنفس نطاق الذاكرة.

منذ لينكس 4.14، يمكن لإشعار خطأ صفحة userfaultfd أن يضمّن بشكل انتقائي معلومات معرف الخيط المتعطل في الإشعار. يحتاج المرء إلى تمكين هذه الميزة صراحةً باستخدام بت الميزة UFFD_FEATURE_THREAD_ID عند تهيئة سياق userfaultfd. الإبلاغ عن معرف الخيط معطل مبدئيًا.

  الاستخدام
صُممت آلية userfaultfd للسماح لخيط في برنامج متعدد الخيوط بأداء ترحيل صفحات في مساحة المستخدم للخيوط الأخرى في العملية. عند حدوث خطأ صفحة لإحدى المناطق المسجلة لكائن userfaultfd، يُوضع الخيط المتعطل في حالة سكون ويُولد حدث يمكن قراءته عبر واصف ملف userfaultfd. يقرأ خيط معالجة الأخطاء الأحداث من واصف الملف هذا ويخدمها باستخدام العمليات الموصوفة في ioctl_userfaultfd(2). عند خدمة أحداث خطأ الصفحة، يمكن لخيط معالجة الأخطاء قدح إيقاظ للخيط النائم.

من الممكن أن تعمل الخيوط المتعطلة وخيوط معالجة الأخطاء في سياق عمليات مختلفة. في هذه الحالة، قد تنتمي هذه الخيوط إلى برامج مختلفة، ولن يتعاون البرنامج الذي ينفذ الخيوط المتعطلة بالضرورة مع البرنامج الذي يعالج أخطاء الصفحات. في هذا الوضع غير التعاوني، تحتاج العملية التي تراقب userfaultfd وتعالج أخطاء الصفحات إلى أن تكون على دراية بالتغييرات في تخطيط الذاكرة الافتراضية للعملية المتعطلة لتجنب فساد الذاكرة.

منذ لينكس 4.11، يمكن لـ userfaultfd أيضًا إخطار خيوط معالجة الأخطاء حول التغييرات في تخطيط الذاكرة الافتراضية للعملية المتعطلة. بالإضافة إلى ذلك، إذا استدعت العملية المتعطلة fork(2)، فقد تُكرر كائنات userfaultfd المرتبطة بالأصل في العملية الفرعية وسيُخطر مراقب userfaultfd (عبر UFFD_EVENT_FORK الموصوف أدناه) بشأن واصف الملف المرتبط بكائنات userfault المنشأة للعملية الفرعية، مما يسمح لمراقب userfaultfd بأداء ترحيل صفحات في مساحة المستخدم للعملية الفرعية. على عكس أخطاء الصفحات التي يجب أن تكون متزامنة وتتطلب إيقاظًا صريحًا أو ضمنيًا، تُسلم جميع الأحداث الأخرى بشكل غير متزامن وتستأنف العملية غير التعاونية التنفيذ بمجرد أن ينفذ مدير userfaultfd read(2). يجب على مدير userfaultfd مزامنة استدعاءات UFFDIO_COPY بعناية مع معالجة الأحداث.

النموذج غير المتزامن الحالي لتسليم الأحداث هو الأمثل لتطبيقات مدير userfaultfd غير التعاونية أحادية الخيط.

منذ لينكس 5.7، أصبح userfaultfd قادرًا على القيام بتتبع متزامن لاتساخ الصفحات باستخدام وضع التسجيل الجديد للحماية من الكتابة. يجب على المرء التحقق من بت الميزة UFFD_FEATURE_PAGEFAULT_FLAG_WP قبل استخدام هذه الميزة. على غرار وضع الفقدان الأصلي لـ userfaultfd، سيُولد وضع الحماية من الكتابة إشعار userfaultfd عند كتابة الصفحة المحمية. يحتاج المستخدم إلى حل خطأ الصفحة عن طريق إزالة الحماية عن الصفحة المتعطلة وركل الخيط المتعطل للمتابعة. لمزيد من المعلومات، يُرجى الرجوع إلى قسم "وضع الحماية من الكتابة في Userfaultfd".

  عملية Userfaultfd
بعد إنشاء كائن userfaultfd باستخدام userfaultfd()، يجب على التطبيق تمكينه باستخدام عملية UFFDIO_API ioctl(2). تسمح هذه العملية بمصافحة من خطوتين بين النواة ومساحة المستخدم لتحديد إصدار API والميزات التي تدعمها النواة، ثم تمكين الميزات التي تريدها مساحة المستخدم. يجب تنفيذ هذه العملية قبل أي من عمليات ioctl(2) الأخرى الموصوفة أدناه (وإلا ستفشل تلك العمليات مع الخطأ EINVAL).

بعد نجاح عملية UFFDIO_API، يقوم التطبيق بتسجيل نطاقات عناوين الذاكرة باستخدام عملية UFFDIO_REGISTER ioctl(2). بعد الإكمال الناجح لعملية UFFDIO_REGISTER، سيقوم النواة بإعادة توجيه أي خطأ صفحة يحدث في نطاق الذاكرة المطلوب، ويحقق الوضع المحدد وقت التسجيل، إلى تطبيق مساحة المستخدم. يمكن للتطبيق بعد ذلك استخدام عمليات ioctl(2) متنوعة (مثل UFFDIO_COPY، أو UFFDIO_ZEROPAGE، أو UFFDIO_CONTINUE) لحل خطأ الصفحة.

منذ لينكس 4.14، إذا ضبط التطبيق بت الميزة UFFD_FEATURE_SIGBUS باستخدام UFFDIO_API ioctl(2)، فلن يُعاد توجيه أي إشعار بخطأ صفحة إلى مساحة المستخدم. بدلاً من ذلك، تُسلم إشارة SIGBUS إلى العملية المتعطلة. باستخدام هذه الميزة، يمكن استخدام userfaultfd لأغراض المتانة للقبض ببساطة على أي وصول لمناطق ضمن نطاق العناوين المسجل لا تحتوي على صفحات مخصصة، دون الحاجة للاستماع لأحداث userfaultfd. لن يكون هناك حاجة لمراقب userfaultfd للتعامل مع مثل هذه الوصولات للذاكرة. على سبيل المثال، يمكن أن تكون هذه الميزة مفيدة للتطبيقات التي تريد منع النواة من تخصيص الصفحات آليًا وملء الفجوات في الملفات المتفرقة عند الوصول إلى الفجوة عبر تعيين ذاكرة.

تُورث ميزة UFFD_FEATURE_SIGBUS ضمنيًا من خلال fork(2) إذا استُخدمت مع UFFD_FEATURE_FORK.

يمكن العثور على تفاصيل عمليات ioctl(2) المختلفة في ioctl_userfaultfd(2).

منذ لينكس 4.11، يمكن تمكين أحداث بخلاف أخطاء الصفحات أثناء عملية UFFDIO_API.

حتى لينكس 4.11، كان يمكن استخدام userfaultfd فقط مع تعيينات الذاكرة الخاصة المجهولة. منذ لينكس 4.11، يمكن أيضًا استخدام userfaultfd مع hugetlbfs وتعيينات الذاكرة المشتركة.

  وضع الحماية من الكتابة في Userfaultfd (منذ لينكس 5.7)
منذ لينكس 5.7، يدعم userfaultfd وضع الحماية من الكتابة للذاكرة المجهولة. يحتاج المستخدم أولاً للتحقق من توفر هذه الميزة باستخدام ioctl UFFDIO_API مقابل بت الميزة UFFD_FEATURE_PAGEFAULT_FLAG_WP قبل استخدام هذه الميزة.

منذ لينكس 5.19، دُعم وضع الحماية من الكتابة أيضًا على أنواع الذاكرة shmem و hugetlbfs. يمكن اكتشافه ببت الميزة UFFD_FEATURE_WP_HUGETLBFS_SHMEM.

للتسجيل مع وضع الحماية من الكتابة لـ userfaultfd، يحتاج المستخدم إلى بدء ioctl UFFDIO_REGISTER مع تعيين الوضع UFFDIO_REGISTER_MODE_WP. لاحظ أنه من القانوني مراقبة نفس نطاق الذاكرة بأوضاع متعددة. على سبيل المثال، يمكن للمستخدم القيام بـ UFFDIO_REGISTER مع تعيين الوضع إلى UFFDIO_REGISTER_MODE_MISSING | UFFDIO_REGISTER_MODE_WP. عندما يتم تسجيل UFFDIO_REGISTER_MODE_WP فقط، لن تتلقى مساحة المستخدم أي إشعار عند كتابة صفحة مفقودة. بدلاً من ذلك، ستتلقى مساحة المستخدم إشعار خطأ صفحة للحماية من الكتابة فقط عند كتابة صفحة موجودة ولكنها محمية من الكتابة.

بعد اكتمال ioctl UFFDIO_REGISTER مع تعيين وضع UFFDIO_REGISTER_MODE_WP، يمكن للمستخدم حماية أي ذاكرة موجودة ضمن النطاق من الكتابة باستخدام ioctl UFFDIO_WRITEPROTECT حيث يجب تعيين uffdio_writeprotect.mode إلى UFFDIO_WRITEPROTECT_MODE_WP.

عند حدوث حدث حماية من الكتابة، ستتلقى مساحة المستخدم إشعار خطأ صفحة حيث سيكون uffd_msg.pagefault.flags مع تعيين علم UFFD_PAGEFAULT_FLAG_WP. ملاحظة: نظرًا لأن الكتابات فقط يمكنها تشغيل هذا النوع من الأخطاء، فإن إشعارات الحماية من الكتابة ستحتوي دائمًا على بت UFFD_PAGEFAULT_FLAG_WRITE مضبوطًا مع بت UFFD_PAGEFAULT_FLAG_WP.

لحل خطأ صفحة الحماية من الكتابة، يجب على المستخدم بدء ioctl UFFDIO_WRITEPROTECT آخر، حيث يجب أن يكون uffd_msg.pagefault.flags مع مسح العلم UFFDIO_WRITEPROTECT_MODE_WP على الصفحة أو النطاق المتسبب في الخطأ.

  وضع الخطأ الثانوي لـ Userfaultfd (منذ لينكس 5.13)
منذ لينكس 5.13، يدعم userfaultfd وضع الخطأ الثانوي. في هذا الوضع، يتم إنتاج رسائل الخطأ ليس للأخطاء الرئيسية (حيث كانت الصفحة مفقودة)، بل للأخطاء الثانوية، حيث توجد صفحة في خبيئة الصفحة، ولكن إدخالات جدول الصفحة غير موجودة بعد. يحتاج المستخدم أولاً إلى التحقق من توفر هذه الميزة باستخدام ioctl UFFDIO_API مع تعيين بتات الميزة المناسبة قبل استخدام هذه الميزة: UFFD_FEATURE_MINOR_HUGETLBFS منذ لينكس 5.13، أو UFFD_FEATURE_MINOR_SHMEM منذ لينكس 5.14.

للتسجيل مع وضع الخطأ الثانوي لـ userfaultfd، يحتاج المستخدم إلى بدء ioctl UFFDIO_REGISTER مع تعيين الوضع UFFD_REGISTER_MODE_MINOR.

عند حدوث خطأ ثانوي، ستتلقى مساحة المستخدم إشعار خطأ صفحة حيث سيكون uffd_msg.pagefault.flags مع تعيين علم UFFD_PAGEFAULT_FLAG_MINOR.

لحل خطأ صفحة ثانوي، يجب على المعالج تحديد ما إذا كانت محتويات الصفحة الحالية تحتاج إلى تعديل أولاً أم لا. إذا كان الأمر كذلك، فيجب القيام بذلك في المكان من خلال تعيين ثانٍ غير مسجل بـ userfaultfd لنفس الصفحة الداعمة (على سبيل المثال، عن طريق تعيين ملف shmem أو hugetlbfs مرتين). بمجرد اعتبار الصفحة "محدثة"، يمكن حل الخطأ عن طريق بدء ioctl UFFDIO_CONTINUE، الذي يقوم بتثبيت إدخالات جدول الصفحة و(بشكل مبدئي) إيقاظ الخيوط المتسببة في الخطأ.

يدعم وضع الخطأ الثانوي فقط الذاكرة المدعومة بـ hugetlbfs (منذ لينكس 5.13) والمدعومة بـ shmem (منذ لينكس 5.14).

  القراءة من بنية userfaultfd
كل read(2) من واصف ملف userfaultfd يُرجع بنية uffd_msg واحدة أو أكثر، كل منها يصف حدث خطأ صفحة أو حدثًا مطلوبًا لاستخدام userfaultfd غير التعاوني:

    struct uffd_msg {
        __u8  event;            /* Type of event */
        ...
        union {
            struct {
                __u64 flags;    /* Flags describing fault */
                __u64 address;  /* Faulting address */
                union {
                    __u32 ptid; /* Thread ID of the fault */
                } feat;
            } pagefault;

            struct {            /* Since Linux 4.11 */
                __u32 ufd;      /* Userfault file descriptor
                                   of the child process */
            } fork;

            struct {            /* Since Linux 4.11 */
                __u64 from;     /* Old address of remapped area */
                __u64 to;       /* New address of remapped area */
                __u64 len;      /* Original mapping size */
            } remap;

            struct {            /* Since Linux 4.11 */
                __u64 start;    /* Start address of removed area */
                __u64 end;      /* End address of removed area */
            } remove;
            ...
        } arg;

        /* Padding fields omitted */
    } __packed;

إذا توفرت أحداث متعددة وكان الوسيط (buffer) الموفر كبيراً بما يكفي، يعيد الاستدعاء read(2) أكبر عدد ممكن من الأحداث التي يمكن احتواؤها في الوسيط الموفر. أما إذا كان الوسيط الموفر للاستدعاء read(2) أصغر من حجم بنية uffd_msg، فيخفق الاستدعاء read(2) مع الخطأ EINVAL.

الحقول المضبوطة في بنية uffd_msg هي كما يلي:

event
    نوع الحدث. وبناءً على نوع الحدث، تمثل الحقول المختلفة لاتحاد arg (union) التفاصيل المطلوبة لمعالجة الحدث. لا تُولّد الأحداث التي ليست من نوع خطأ-الصفحة (page-fault) إلا عند تفعيل الميزة المناسبة أثناء مصافحة واجهة برمجة التطبيقات عبر UFFDIO_API ioctl(2).

    يمكن أن تظهر القيم التالية في حقل event:

    UFFD_EVENT_PAGEFAULT (منذ لينكس 4.3)
        حدث خطأ-صفحة. تفاصيل خطأ-الصفحة متاحة في الحقل pagefault.

    UFFD_EVENT_FORK (منذ لينكس 4.11)
        يُتولد عندما تستدعي العملية المتسببة بالخطأ fork(2) (أو clone(2) بدون علمة CLONE_VM). تفاصيل الحدث متوفرة في حقل fork.

    UFFD_EVENT_REMAP (منذ لينكس 4.11)
        يُتولد عندما تستدعي العملية المتسببة بالخطأ mremap(2). تفاصيل الحدث متوفرة في حقل remap.

    UFFD_EVENT_REMOVE (منذ لينكس 4.11)
        يُتولد عندما تستدعي العملية المتسببة بالخطأ madvise(2) مع نصيحة MADV_DONTNEED أو MADV_REMOVE. تفاصيل الحدث متوفرة في حقل remove.

    UFFD_EVENT_UNMAP (منذ لينكس 4.11)
        يُتولد عندما تلغي العملية المتسببة بالخطأ تعيين نطاق ذاكرة، إما صراحة باستخدام munmap(2) أو ضمنًا أثناء mmap(2) أو mremap(2). تفاصيل الحدث متوفرة في حقل remove.

pagefault.address
    العنوان الذي أثار خطأ الصفحة.

pagefault.flags
    قناع بتات للأعلام التي تصف الحدث. بالنسبة لـ UFFD_EVENT_PAGEFAULT، قد يظهر العلم التالي:

    UFFD_PAGEFAULT_FLAG_WP
        إذا كان هذا العلم مضبوطًا، فإن الخطأ كان خطأ حماية من الكتابة.

    UFFD_PAGEFAULT_FLAG_MINOR
        إذا كان هذا العلم مضبوطًا، فإن الخطأ كان خطأ بسيطًا.

    UFFD_PAGEFAULT_FLAG_WRITE
        إذا كان هذا العلم مضبوطًا، فإن الخطأ كان خطأ كتابة.

    إذا لم يكن أي من UFFD_PAGEFAULT_FLAG_WP أو UFFD_PAGEFAULT_FLAG_MINOR مضبوطًا، فإن الخطأ كان خطأ فقدان.

pagefault.feat.pid
    معرف الخيط الذي أثار خطأ الصفحة.

fork.ufd
    واصف الملف المرتبط بكائن userfault الذي أُنشئ للطفل الذي أنشأته fork(2).

remap.from
    العنوان الأصلي لنطاق الذاكرة الذي أُعيد تعيينه باستخدام mremap(2).

remap.to
    العنوان الجديد لنطاق الذاكرة الذي أُعيد تعيينه باستخدام mremap(2).

remap.len
    الحجم الأصلي لنطاق الذاكرة الذي أُعيد تعيينه باستخدام mremap(2).

remove.start
    عنوان البداية لنطاق الذاكرة الذي حُرر باستخدام madvise(2) أو أُلغي تعيينه

remove.end
    عنوان نهاية نطاق الذاكرة الذي تم تحريره باستخدام madvise(2) أو إلغاء تعيينه

قد تفشل عملية read(2) على واصف ملف userfaultfd مع الأخطاء التالية:

EINVAL
    لم يتم تمكين كائن userfaultfd بعد باستخدام عملية UFFDIO_API ioctl(2)

إذا تم تمكين علامة O_NONBLOCK في وصف الملف المفتوح المرتبط، يمكن مراقبة واصف ملف userfaultfd باستخدام poll(2) وselect(2) وepoll(7). عندما تتوفر الأحداث، يشير واصف الملف إلى أنه قابل للقراءة. إذا لم يتم تمكين علامة O_NONBLOCK، فإن poll(2) (دائمًا) يشير إلى أن الملف لديه حالة POLLERR، ويشير select(2) إلى أن واصف الملف قابل للقراءة والكتابة معًا.

قيمة الإرجاع
عند النجاح، تُرجع userfaultfd() واصف ملف جديد يشير إلى كائن userfaultfd. عند الخطأ، تُرجع -1، ويتم تعيين errno للإشارة إلى الخطأ.

الأخطاء

EINVAL
    عُيّنت قيمة غير مدعومة في flags.

EMFILE
    تم الوصول إلى الحد الأقصى لكل عملية لعدد واصفات الملفات المفتوحة

ENFILE
    وُصل إلى الحد الأقصى لإجمالي عدد الملفات المفتوحة على مستوى النظام.

ENOMEM
    ذاكرة النواة المتوفرة غير كافية.

EPERM (منذ Linux 5.2)
    المتصل ليس لديه صلاحية (ليس لديه قدرة CAP_SYS_PTRACE في مساحة اسم المستخدم الأولية)، وقيمة /proc/sys/vm/unprivileged_userfaultfd هي 0.

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

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

تمت إضافة دعم hugetlbfs ومناطق الذاكرة المشتركة وأحداث غير خطأ الصفحة في Linux 4.11

ملاحظات
يمكن استخدام آلية userfaultfd كبديل لتقنيات الترحيل التقليدية في مساحة المستخدم القائمة على استخدام إشارة SIGSEGV وmmap(2). يمكن استخدامها أيضًا لتنفيذ الاستعادة البطيئة لآليات نقطة التفتيش/الاستعادة، بالإضافة إلى الترحيل بعد النسخ للسماح بالتنفيذ (شبه) المتواصل عند نقل الأجهزة الافتراضية وحاويات Linux من مضيف إلى آخر.

العلل
إذا تم تمكين UFFD_FEATURE_EVENT_FORK وتم مقاطعة استدعاء نظام من عائلة fork(2) بواسطة إشارة أو فشل، فقد يتم إنشاء واصف userfaultfd قديم. في هذه الحالة، سيتم تسليم UFFD_EVENT_FORK زائف إلى مراقب userfaultfd.

أمثلة
يوضح البرنامج أدناه استخدام آلية userfaultfd. يقوم البرنامج بإنشاء خيطين، أحدهما يعمل كمعالج خطأ الصفحة للعملية، للصفحات في منطقة طلب صفحة صفرية تم إنشاؤها باستخدام mmap(2).

يأخذ البرنامج وسيطة سطر أوامر واحدة، وهي عدد الصفحات التي سيتم إنشاؤها في تعيين سيتم التعامل مع أخطاء صفحته عبر userfaultfd. بعد إنشاء كائن userfaultfd، يقوم البرنامج بعد ذلك بإنشاء تعيين خاص مجهول بالحجم المحدد ويسجل نطاق العنوان لذلك التعيين باستخدام عملية UFFDIO_REGISTER ioctl(2). ثم يقوم البرنامج بإنشاء خيط ثانٍ سيقوم بمهمة معالجة أخطاء الصفحة.

ثم يتجول الخيط الرئيسي عبر صفحات التعيين لجلب البايتات من الصفحات المتتالية. نظرًا لعدم الوصول إلى الصفحات بعد، فإن أول وصول لبايت في كل صفحة سيؤدي إلى تشغيل حدث خطأ صفحة على واصف ملف userfaultfd.

يتم التعامل مع كل حدث من أحداث خطأ الصفحة بواسطة الخيط الثاني، الذي يجلس في حلقة لمعالجة الإدخال من واصف ملف userfaultfd. في كل تكرار للحلقة، يستدعي الخيط الثاني أولاً poll(2) للتحقق من حالة واصف الملف، ثم يقرأ حدثًا من واصف الملف. يجب أن تكون كل هذه الأحداث أحداث UFFD_EVENT_PAGEFAULT، والتي يعالجها الخيط عن طريق نسخ صفحة من البيانات إلى المنطقة المتسببة في الخطأ باستخدام عملية UFFDIO_COPY ioctl(2).

فيما يلي مثال لما نراه عند تشغيل البرنامج:

    $ ./userfaultfd_demo 3;
    Address returned by mmap() = 0x7fd30106c000

    fault_handler_thread():
        poll() returns: nready = 1; POLLIN = 1; POLLERR = 0
        UFFD_EVENT_PAGEFAULT event: flags = 0; address = 7fd30106c00f
            (uffdio_copy.copy returned 4096)
    Read address 0x7fd30106c00f in main(): A
    Read address 0x7fd30106c40f in main(): A
    Read address 0x7fd30106c80f in main(): A
    Read address 0x7fd30106cc0f in main(): A

    fault_handler_thread():
        poll() returns: nready = 1; POLLIN = 1; POLLERR = 0
        UFFD_EVENT_PAGEFAULT event: flags = 0; address = 7fd30106d00f
            (uffdio_copy.copy returned 4096)
    Read address 0x7fd30106d00f in main(): B
    Read address 0x7fd30106d40f in main(): B
    Read address 0x7fd30106d80f in main(): B
    Read address 0x7fd30106dc0f in main(): B

    fault_handler_thread():
        poll() returns: nready = 1; POLLIN = 1; POLLERR = 0
        UFFD_EVENT_PAGEFAULT event: flags = 0; address = 7fd30106e00f
            (uffdio_copy.copy returned 4096)
    Read address 0x7fd30106e00f in main(): C
    Read address 0x7fd30106e40f in main(): C
    Read address 0x7fd30106e80f in main(): C
    Read address 0x7fd30106ec0f in main(): C

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

       Licensed under the GNU General Public License version 2 or later.
    */
    #define _GNU_SOURCE
    #include <err.h>
    #include <errno.h>
    #include <fcntl.h>
    #include <inttypes.h>
    #include <linux/userfaultfd.h>
    #include <poll.h>
    #include <pthread.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/ioctl.h>
    #include <sys/mman.h>
    #include <sys/syscall.h>
    #include <unistd.h>

    static int page_size;

    static void *
    fault_handler_thread(void *arg)
    {
        long  uffd;   /* userfaultfd file descriptor */

        static int      fault_cnt = 0; /* Number of faults so far handled */
        static char     *page = NULL;
        static struct uffd_msg  msg;  /* Data read from userfaultfd */

        uffd = (long) arg;

        /* Create a page that will be copied into the faulting region.  */

        if (page == NULL) {
            page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
                        MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
            if (page == MAP_FAILED)
                err(EXIT_FAILURE, "mmap");
        }

        /* Loop, handling incoming events on the userfaultfd
           file descriptor.  */

        for (;;) {
            int                 nready;
            ssize_t             nread;
            struct pollfd       pollfd;
            struct uffdio_copy  uffdio_copy;

            /* See what poll() tells us about the userfaultfd.  */

            pollfd.fd = uffd;
            pollfd.events = POLLIN;
            nready = poll(&pollfd, 1, -1);
            if (nready == -1)
                err(EXIT_FAILURE, "poll");

            printf("\nfault_handler_thread():\n");
            printf("    poll() returns: nready = %d; "
                   "POLLIN = %d; POLLERR = %d\n", nready,
                   (pollfd.revents & POLLIN) != 0,
                   (pollfd.revents & POLLERR) != 0);

            /* Read an event from the userfaultfd.  */

            nread = read(uffd, &msg, sizeof(msg));
            if (nread == 0) {
                printf("EOF on userfaultfd!\n");
                exit(EXIT_FAILURE);
            }

            if (nread == -1)
                err(EXIT_FAILURE, "read");

            /* We expect only one kind of event; verify that assumption.  */

            if (msg.event != UFFD_EVENT_PAGEFAULT) {
                fprintf(stderr, "Unexpected event on userfaultfd\n");
                exit(EXIT_FAILURE);
            }

            /* Display info about the page-fault event.  */

            printf("    UFFD_EVENT_PAGEFAULT event: ");
            printf("flags = %w64x; ", msg.arg.pagefault.flags);
            printf("address = %w64x\n", msg.arg.pagefault.address);

            /* Copy the page pointed to by 'page' into the faulting
               region.  Vary the contents that are copied in, so that it
               is more obvious that each fault is handled separately.  */

            memset(page, 'A' + fault_cnt % 20, page_size);
            fault_cnt++;

            uffdio_copy.src = (unsigned long) page;

            /* We need to handle page faults in units of pages(!).
               So, round faulting address down to page boundary.  */

            uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
                                               ~(page_size - 1);
            uffdio_copy.len = page_size;
            uffdio_copy.mode = 0;
            uffdio_copy.copy = 0;
            if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
                err(EXIT_FAILURE, "ioctl-UFFDIO_COPY");

            printf("        (uffdio_copy.copy returned %w64d)\n",
                   uffdio_copy.copy);
        }
    }

    int
    main(int argc, char *argv[])
    {
        int        s;
        char       *addr;   /* Start of region handled by userfaultfd */
        long       uffd;    /* userfaultfd file descriptor */
        size_t     size, i;  /* Size of region handled by userfaultfd */
        pthread_t  thr;     /* ID of thread that handles page faults */
        struct uffdio_api       uffdio_api;
        struct uffdio_register  uffdio_register;

        if (argc != 2) {
            fprintf(stderr, "Usage: %s num-pages\n", argv[0]);
            exit(EXIT_FAILURE);
        }

        page_size = sysconf(_SC_PAGE_SIZE);
        size = strtoull(argv[1], NULL, 0) * page_size;

        /* Create and enable userfaultfd object.  */

        uffd = syscall(SYS_userfaultfd, O_CLOEXEC | O_NONBLOCK);
        if (uffd == -1)
            err(EXIT_FAILURE, "userfaultfd");

        /* NOTE: Two-step feature handshake is not needed here, since this
           example doesn't require any specific features.

           Programs that *do* should call UFFDIO_API twice: once with
           `features = 0` to detect features supported by this kernel, and
           again with the subset of features the program actually wants to
           enable.  */
        uffdio_api.api = UFFD_API;
        uffdio_api.features = 0;
        if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
            err(EXIT_FAILURE, "ioctl-UFFDIO_API");

        /* Create a private anonymous mapping.  The memory will be
           demand-zero paged--that is, not yet allocated.  When we
           actually touch the memory, it will be allocated via
           the userfaultfd.  */

        addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
                    MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        if (addr == MAP_FAILED)
            err(EXIT_FAILURE, "mmap");

        printf("Address returned by mmap() = %p\n", addr);

        /* Register the memory range of the mapping we just created for
           handling by the userfaultfd object.  In mode, we request to track
           missing pages (i.e., pages that have not yet been faulted in).  */

        uffdio_register.range.start = (unsigned long) addr;
        uffdio_register.range.len = size;
        uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
        if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
            err(EXIT_FAILURE, "ioctl-UFFDIO_REGISTER");

        /* Create a thread that will process the userfaultfd events.  */

        s = pthread_create(&thr, NULL, fault_handler_thread, (void *) uffd);
        if (s != 0) {
            errc(EXIT_FAILURE, s, "pthread_create");
        }

        /* Main thread now touches memory in the mapping, touching
           locations 1024 bytes apart.  This will trigger userfaultfd
           events for all pages in the region.  */

        i = 0xf;    /* Ensure that faulting address is not on a page
                       boundary, in order to test that we correctly
                       handle that case in fault_handling_thread().  */
        while (i < size) {
            char  c;

            c = addr[i];
            printf("Read address %p in %s(): ", addr + i, __func__);
            printf("%c\n", c);
            i += 1024;
            usleep(100000);         /* Slow things down a little */
        }

        exit(EXIT_SUCCESS);
    }

انظر أيضًا
fcntl(2), ioctl(2), ioctl_userfaultfd(2), madvise(2), mmap(2)

الملف Documentation/admin-guide/mm/userfaultfd.rst في شجرة مصدر نواة لينكس

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

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

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