تنفيذ تيارات تعاونية بسيطة في ج

مرحبا هبر!



شكرًا لك على اهتمامك بمنشورنا المترجم السابق على REST . نعرض اليوم إلقاء نظرة على موضوع تصميم النظام من زاوية مختلفة قليلاً ونشر ترجمة لمقال بقلم ستيفن برينان ، أحد الشخصيات البارزة في نظام Linux ، والذي يتحدث عن تنفيذه الخاص لتعدد المهام في مساحة المستخدمين وكيف يمكن أن يكون مفيدًا.



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



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

برنامج C عادي (وليس نظام تشغيل).



استطرادا غنائيا عن setjmp و longjmp



سيعتمد المجدول بشكل كبير على الوظائف setjmp()و longjmp(). يبدون سحريين بعض الشيء ، لذا سأصف أولاً ما يفعلونه ، وبعد ذلك سأستغرق بعض الوقت وأخبرك كيف بالضبط. تتيح لك



الوظيفة setjmp()تسجيل معلومات حول مرحلة تنفيذ البرنامج ، بحيث يمكنك لاحقًا الانتقال مرة أخرى إلى هذه النقطة. يتم تمرير متغير نوع إليه jmp_buf، حيث سنخزن هذه المعلومات. في المرة الأولى التي تعود فيها الدالة ، setjmp()تُرجع القيمة 0.



لاحقًا ، يمكنك استخدام الوظيفة longjmp(jmp_buf, value)لاستئناف التنفيذ فورًا من النقطة التي تم استدعاؤها setjmp(). في برنامجك ، سيبدو هذا الموقف كما لو أنه setjmp()عاد مرة أخرى. سوف تعود الحجة هذه المرةvalueالتي مررت بها longjmp()- من الأنسب تمييز العائد الثاني عن الأول. إليك مثال لتوضيح هذه النقطة:



#include <stdio.h>
#include <setjmp.h>

jmp_buf saved_location;
int main(int argc, char **argv)
{
    if (setjmp(saved_location) == 0) {
        printf("We have successfully set up our jump buffer!\n");
    } else {
        printf("We jumped!\n");
        return 0;
    }

    printf("Getting ready to jump!\n");
    longjmp(saved_location, 1);
    printf("This will never execute...\n");
    return 0;
}


إذا قمنا بتجميع هذا البرنامج وتشغيله ، فسنحصل على المخرجات التالية:



We have successfully set up our jump buffer!
Getting ready to jump!
We jumped!


نجاح باهر! إنها مثل تعليمة goto ، لكن في هذه الحالة يمكن استخدامها للقفز خارج الدالة. كما أنها أكثر صعوبة في القراءة مما gotoهي عليه لأنها تبدو وكأنها مكالمة دالة عادية. إذا استخدامات الشفرة setjmp()و في وفرة longjmp()، فإنه سيكون من الصعب جدا لقراءته على أي شخص (بما فيهم أنت).



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



لن يقوم Setjmp و longjmp بحفظ المكدس الخاص بك

True والوظائف setjmp()وlongjmp()ليس الغرض منها دعم أي نوع من التنقل. تم تصميمها لحالة عملية محددة للغاية. تخيل أنك تقوم ببعض العمليات المعقدة ، مثل تقديم طلب HTTP. في هذه الحالة ، سيتم تضمين مجموعة معقدة من استدعاءات الوظائف ، وإذا فشل أي منها ، فسيتعين عليك إرجاع رمز خطأ خاص من كل منها. سيبدو هذا الرمز في القائمة التالية ، أينما اتصلت بالوظيفة (ربما عشرات المرات):



int rv = do_function_call();
if (rv != SUCCESS) {
    return rv;
}


المعنى setjmp()و longjmp()هذا ما setjmp()يساعد على حصة من مكان قبل الشروع في صعبة حقا هذه المهمة. بعد ذلك ، يمكنك مركزية جميع عمليات معالجة الأخطاء في مكان واحد:



int rv;
jmp_buf buf;
if ((rv = setjmp(buf)) != 0) {
    /*    */
    return;
}
do_complicated_task(buf, args...);


إذا فشلت أي وظيفة متضمنة do_complicated_task()، فإنها تحدث فقط longjmp(buf, error_code). هذا يعني أن كل وظيفة في التكوين do_complicated_task()يمكن أن تفترض أن أي استدعاء دالة ناجح ، مما يعني أنه لا يمكنك وضع هذا الرمز للتعامل مع الأخطاء في كل استدعاء للدالة (في الممارسة العملية ، هذا لا يتم أبدًا تقريبًا ، ولكن هذا موضوع لمقال منفصل) ...



الفكرة الأساسية هنا هي longjmp()أنه يسمح لك فقط بالقفز من الوظائف المتداخلة بعمق. لا يمكنك القفز إلى تلك الوظيفة المتداخلة بعمق التي قفزت منها سابقًا. هذا ما يبدو عليه المكدس عند القفز من الوظيفة. علامة النجمة (*) تعني مؤشر المكدس الذي يتم تخزينه عنده setjmp().



  | Stack before longjmp    | Stack after longjmp
      +-------------------------+----------------------------
stack | main()              (*) | main()
grows | do_http_request()       |
down  | send_a_header()         |
 |    | write_bytes()           |
 v    | write()  - fails!       |


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



      | Stack at setjmp() | Stack later      | Stack after longjmp()
      +-------------------+------------------+----------------------
stack | main()            | main()           | main()
grows | do_task_one()     | do_task_two()    | do_stack_two()
down  | subtask()         | subtask()        | subtask()
 |    | foo()             |                  | ???
 v    | bar()         (*) |              (*) | ???               (*)


سيشير مؤشر المكدس المحفوظ setjmp()إلى إطار مكدس لم يعد موجودًا وربما تم استبداله ببيانات أخرى في وقت ما. إذا حاولنا longjmp()العودة إلى الوظيفة التي عدنا منها بمساعدة ، فستبدأ أشياء غريبة جدًا ، مما قد يؤدي إلى انهيار البرنامج بأكمله.



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



لنكتب جدولة API



هذا الاستطراد طويل بعض الشيء ، لكنه مسلح بما تعلمناه ، يمكننا الآن تنفيذ تدفقات مساحة المستخدم. بادئ ذي بدء ، ألاحظ أنه من المفيد جدًا تصميم واجهة برمجة التطبيقات بنفسك لتهيئة سلاسل الرسائل وإنشائها وبدءها. إذا قمنا بذلك مسبقًا ، فسيكون لدينا فهم أفضل بكثير لما نحاول بنائه بالضبط!



void scheduler_init(void);
void scheduler_create_task(void (*func)(void*), void *arg);
void scheduler_run(void);


سيتم استخدام هذه الوظائف لتهيئة المجدول ، وإضافة المهام ، ثم بدء المهام أخيرًا في المجدول. بمجرد إطلاقه ، scheduler_run()سيتم تشغيله حتى تكتمل جميع المهام. ستحتوي المهام قيد التشغيل حاليًا على واجهات برمجة التطبيقات التالية:



void scheduler_exit_current_task(void);
void scheduler_relinquish(void);


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



من أجل مثال ملموس ، ضع في اعتبارك حالة استخدام افتراضية لواجهة برمجة التطبيقات الخاصة بنا ، والتي سنختبر بها المجدول:



#include <stdlib.h>
#include <stdio.h>

#include "scheduler.h"

struct tester_args {
    char *name;
    int iters;
};

void tester(void *arg)
{
    int i;
    struct tester_args *ta = (struct tester_args *)arg;
    for (i = 0; i < ta->iters; i++) {
        printf("task %s: %d\n", ta->name, i);
        scheduler_relinquish();
    }
    free(ta);
}

void create_test_task(char *name, int iters)
{
    struct tester_args *ta = malloc(sizeof(*ta));
    ta->name = name;
    ta->iters = iters;
    scheduler_create_task(tester, ta);
}

int main(int argc, char **argv)
{
    scheduler_init();
    create_test_task("first", 5);
    create_test_task("second", 2);
    scheduler_run();
    printf("Finished running all tasks!\n");
    return EXIT_SUCCESS;
}


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



task first: 0
task second: 0
task first: 1
task second: 1
task first: 2
task first: 3
task first: 4
Finished running all tasks!


دعونا ننفذ API المجدول



لتنفيذ واجهة برمجة التطبيقات ، نحتاج إلى نوع من التمثيل الداخلي للمهمة. لذا ، دعنا نبدأ العمل ؛ لنجمع الحقول التي نحتاجها:



struct task {
    enum {
        ST_CREATED,
        ST_RUNNING,
        ST_WAITING,
    } status;

    int id;

    jmp_buf buf;

    void (*func)(void*);
    void *arg;

    struct sc_list_head task_list;

    void *stack_bottom;
    void *stack_top;
    int stack_size;
};


دعونا نناقش كل من هذه الحقول على حدة. يجب أن تكون جميع المهام التي تم إنشاؤها في حالة "تم الإنشاء" قبل التنفيذ. عندما تبدأ المهمة في التنفيذ ، فإنها تنتقل إلى حالة "التشغيل" ، وإذا كانت المهمة بحاجة إلى انتظار عملية غير متزامنة ، فيمكن وضعها في حالة "الانتظار". الحقل idهو ببساطة معرف فريد لمهمة. bufيحتوي هذا على معلومات حول الوقت الذي سيتم فيه longjmp()استئناف المهمة. يتم تمرير الحقول funcو argإلى scheduler_create_task()وهي مطلوبة لبدء المهمة. الحقل task_listمطلوب لتنفيذ قائمة مرتبطة بشكل مزدوج بجميع المهام. الحقول stack_bottom، stack_topو stack_sizeينتمون جميعا إلى كومة منفصلة مخصصة تحديدا لهذه المهمة. الجزء السفلي هو العنوان الذي تم إرجاعه بواسطةmalloc()لكن "top" هو مؤشر إلى عنوان أعلى المنطقة المحددة في الذاكرة مباشرةً. نظرًا لأن مكدس x86 ينمو إلى أسفل ، فأنت بحاجة إلى ضبط مؤشر المكدس على قيمة stack_top، وليس stack_bottom.



في مثل هذه الظروف ، يمكنك تنفيذ الوظيفة scheduler_create_task():



void scheduler_create_task(void (*func)(void *), void *arg)
{
    static int id = 1;
    struct task *task = malloc(sizeof(*task));
    task->status = ST_CREATED;
    task->func = func;
    task->arg = arg;
    task->id = id++;
    task->stack_size = 16 * 1024;
    task->stack_bottom = malloc(task->stack_size);
    task->stack_top = task->stack_bottom + task->stack_size;
    sc_list_insert_end(&priv.task_list, &task->task_list);
}


من خلال الاستخدام static int، نضمن أنه في كل مرة يتم فيها استدعاء الوظيفة ، يتم زيادة حقل المعرف ، ويوجد رقم جديد. يجب أن يكون كل شيء آخر واضحًا بدون تفسير ، باستثناء الوظيفة sc_list_insert_end()التي تضيف ببساطة struct taskإلى القائمة العالمية. يتم تخزين القائمة العمومية داخل بنية ثانية تحتوي على كافة البيانات الخاصة للمجدول. يوجد أدناه الهيكل نفسه ، بالإضافة إلى وظيفة التهيئة الخاصة به:



struct scheduler_private {
    jmp_buf buf;
    struct task *current;
    struct sc_list_head task_list;
} priv;

void scheduler_init(void)
{
    priv.current = NULL;
    sc_list_init(&priv.task_list);
}


يُستخدم الحقل task_listللإشارة إلى قائمة المهام (ليس من المستغرب). currentيخزن الحقل المهمة التي يتم تنفيذها حاليًا (أو في nullحالة عدم وجود مثل هذه المهام في الوقت الحالي). الأهم من ذلك ، bufسيتم استخدام الحقل للانتقال إلى الكود scheduler_run():



enum {
    INIT=0,
    SCHEDULE,
    EXIT_TASK,
};

void scheduler_run(void)
{
    /*     ! */
    switch (setjmp(priv.buf)) {
    case EXIT_TASK:
        scheduler_free_current_task();
    case INIT:
    case SCHEDULE:
        schedule();
        /*       ,    */
        return;
    default:
        fprintf(stderr, "Uh oh, scheduler error\n");
        return;
    }
}


بمجرد استدعاء الوظيفة scheduler_run()، نقوم بتعيين المخزن المؤقت setjmp()حتى نتمكن دائمًا من العودة إلى هذه الوظيفة. في المرة الأولى ، يتم إرجاع 0 (INIT) ، ونتصل على الفور schedule(). بعد ذلك ، يمكننا تمرير ثوابت SCHEDULE أو EXIT_TASK longjmp()، والتي ستثير سلوكيات مختلفة. في الوقت الحالي ، دعنا نتجاهل حالة EXIT_TASK وننتقل مباشرةً إلى التنفيذ schedule():



static void schedule(void)
{
    struct task *next = scheduler_choose_task();

    if (!next) {
        return;
    }

    priv.current = next;
    if (next->status == ST_CREATED) {
        /*
         *     .   
         * ,        .
         */
        register void *top = next->stack_top;
        asm volatile(
            "mov %[rs], %%rsp \n"
            : [ rs ] "+r" (top) ::
        );

        /*
         *   
         */
        next->status = ST_RUNNING;
        next->func(next->arg);

        /*
         *     ,    .   – ,   
         *   
         */
        scheduler_exit_current_task();
    } else {
        longjmp(next->buf, 1);
    }
    /*   */
}


أولاً ، نقوم باستدعاء الوظيفة الداخلية لتحديد المهمة التالية للتشغيل. سيعمل هذا المجدول مثل منصة عرض دائرية عادية ، لذلك سيختار ببساطة مهمة جديدة من القائمة. إذا كانت هذه الدالة ترجع NULL ، فلن يكون لدينا المزيد من المهام لأداءها ، ونعود. خلاف ذلك ، يجب علينا إما بدء تنفيذ المهمة (إذا كانت في حالة ST_CREATED) ، أو استئناف تنفيذها.



لتشغيل المهمة التي تم إنشاؤها ، نستخدم تعليمات التجميع لـ x86_64 لتعيين الحقل إلى stack_topسجل rsp(مؤشر المكدس). ثم نغير حالة المهمة ونشغل الوظيفة ونخرج. ملاحظة: setjmp()كلاً من longjmp()تخزين مؤشرات المكدس وإعادة ترتيبها ، لذلك علينا فقط استخدام المُجمِّع لتغيير مؤشر المكدس.



إذا كانت المهمة قد بدأت بالفعل ، فيجب bufأن يحتوي الحقل على السياق الذي نحتاجه longjmp()لاستئناف المهمة ، وهذا ما نفعله.

بعد ذلك ، دعنا نلقي نظرة على وظيفة المساعدة التي تحدد المهمة التالية للتشغيل. هذا هو قلب برنامج الجدولة ، وكما قلت سابقًا ، يعمل هذا المجدول مثل دائري:



static struct task *scheduler_choose_task(void)
{
    struct task *task;

    sc_list_for_each_entry(task, &priv.task_list, task_list, struct task)
    {
        if (task->status == ST_RUNNING || task->status == ST_CREATED) {
            sc_list_remove(&task->task_list);
            sc_list_insert_end(&priv.task_list, &task->task_list);
            return task;
        }
    }

    return NULL;
}


إذا لم تكن معتادًا على تطبيق القائمة المرتبطة (مأخوذ من Linux kernel) ، فلا مشكلة كبيرة. الوظيفة sc_list_for_each_entry()هي ماكرو يسمح لك بالتكرار على جميع المهام في قائمة المهام. تتم إزالة المهمة الأولى القابلة للتحديد (ليست في حالة معلقة) التي وجدناها من موقعها الحالي ونقلها إلى نهاية قائمة المهام. هذا يضمن أنه في المرة القادمة التي نقوم فيها بتشغيل المجدول ، سنحصل على مهمة مختلفة (إذا كانت هناك مهمة). نعيد المهمة الأولى المتاحة للاختيار ، أو NULL إذا لم تكن هناك مهام على الإطلاق.



أخيرًا ، دعنا ننتقل إلى التنفيذ scheduler_relinquish()لنرى كيف يمكن للمهمة القضاء على نفسها:



void scheduler_relinquish(void)
{
    if (setjmp(priv.current->buf)) {
        return;
    } else {
        longjmp(priv.buf, SCHEDULE);
    }
}


هذه حالة استخدام أخرى للوظيفة setjmp()في برنامج الجدولة لدينا. في الأساس ، قد يبدو هذا الخيار مربكًا بعض الشيء. عندما تستدعي المهمة هذه الوظيفة ، نستخدمها setjmp()لحفظ السياق الحالي (بما في ذلك مؤشر المكدس الفعلي). ثم نستخدمه longjmp()لإدخال المجدول (مرة أخرى في scheduler_run()) واجتياز وظيفة SCHEDULE ؛ وبالتالي نطلب منك تعيين مهمة جديدة.



عند استئناف المهمة ، setjmp()تعود الدالة بقيمة غير صفرية ، ونخرج من أي مهمة ربما كنا نقوم بها من قبل!

أخيرًا ، إليك ما يحدث عند إنهاء مهمة (يتم ذلك إما صراحةً ، عن طريق استدعاء وظيفة الخروج ، أو بالعودة من وظيفة المهمة المقابلة):



void scheduler_exit_current_task(void)
{
    struct task *task = priv.current;
    sc_list_remove(&task->task_list);
    longjmp(priv.buf, EXIT_TASK);
    /*   */
}

static void scheduler_free_current_task(void)
{
    struct task *task = priv.current;
    priv.current = NULL;
    free(task->stack_bottom);
    free(task);
}


هذه عملية من جزئين. يتم إرجاع الوظيفة الأولى مباشرة بواسطة المهمة نفسها. نحن نحذف الإدخال المقابل لهذا الإدخال من قائمة المهام ، لأنه لن يتم تعيينه بعد الآن. ثم ، باستخدام ، longjmp()نعود إلى الوظيفة scheduler_run(). هذه المرة نستخدم EXIT_TASK. هذه هي الطريقة التي نخبر بها المجدول بما يجب عليه الاتصال به قبل تعيين مهمة جديدة scheduler_free_current_task(). إذا عدت إلى الوصف scheduler_run()، فسترى أن هذا هو بالضبط ما يفعله scheduler_run().



لقد فعلنا ذلك على خطوتين ، منذ متىscheduler_exit_current_task()، فإنه يستخدم بنشاط المكدس الموجود في هيكل المهام. إذا قمت بتحرير المكدس واستمرت في استخدامه ، فهناك احتمال أن تظل الوظيفة قادرة على الوصول إلى نفس ذاكرة المكدس التي حررناها للتو! لضمان عدم حدوث ذلك ، سيتعين علينا longjmp()الرجوع إلى المجدول باستخدام مكدس منفصل مع المساعدة . ثم يمكننا إصدار البيانات المتعلقة بالمهمة بأمان.



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



ما هو استخدام النهج الموصوف؟



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



ومع ذلك ، أرى هذا كنقطة انطلاق لسلسلة كاملة من التطبيقات المثيرة للميزات المفيدة. يمكننا التحدث عن مهام الإدخال / الإخراج الثقيلة ، حول تنفيذ تطبيق غير متزامن أحادي السلسلة ، بنفس طريقة الأدوات المساعدة غير المتزامنة الجديدة في عمل Python. يمكن أيضًا تنفيذ المولدات والكروتينات باستخدام مثل هذا النظام. أخيرًا ، مع بعض العمل الشاق ، يمكن أيضًا مصادقة هذا النظام مع مؤشرات ترابط نظام التشغيل "الحقيقي" لتوفير توافق إضافي عند الحاجة. مشروع مثير للاهتمام مخفي وراء كل فكرة من هذه الأفكار ، وأنا أنصحك بمحاولة إكمال أحدها بنفسك ، عزيزي القارئ.



هل هي آمنة؟



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



يمكن بناء تنفيذ أكثر أمانًا لمثل هذا النظام باستخدام واجهة برمجة تطبيقات "غير مرتبطة بالسياق" (انظر man getcontext) ، والتي تسمح بالتبديل بين هذه الأنواع من "تدفقات" مساحة المستخدم دون تضمين رمز التجميع. لسوء الحظ ، لا تغطي المعايير واجهة برمجة التطبيقات هذه (تمت إزالتها من مواصفات POSIX). ولكن لا يزال من الممكن استخدامه كجزء من glibc.



كيف يمكن تحويل مثل هذه الآلية؟



يعمل هذا المجدول ، كما هو معروض هنا ، فقط إذا نقلت الخيوط التحكم بشكل صريح إلى المجدول. هذا ليس جيدًا لبرنامج عام ، على سبيل المثال ، لنظام تشغيل ، حيث أن الخيط الرديء يمكن أن يمنع تنفيذ جميع البرامج الأخرى (على الرغم من أن هذا لم يمنع استخدام تعدد المهام التعاوني في MS-DOS!) لا أعتقد أن هذا يجعل تعدد المهام التعاوني سيئًا بشكل واضح ؛ كل هذا يتوقف على التطبيق.



عند استخدام واجهة برمجة تطبيقات غير قياسية "خارج السياق" ، ستحتفظ إشارات POSIX بسياق الكود الذي تم تنفيذه مسبقًا. من خلال ضبط المؤقت على نغمات تنبيه دورية ، يمكن لمجدول مساحة المستخدم بالفعل توفير نسخة عمل من المهام المتعددة الوقائية! هذا مشروع رائع آخر يستحق مقالة منفصلة.



All Articles