سؤال

في أنظمة UNIX التي نعرفها malloc() هي دالة غير مرجعية (استدعاء النظام). لماذا هذا؟

بصورة مماثلة، printf() ويقال أيضا أنه غير المريء. لماذا ا؟

أعرف تعريف إعادة الدخول ، لكنني أردت أن أعرف لماذا ينطبق على هذه الوظائف. ما الذي يمنعهم من إعادة إدخاله؟

هل كانت مفيدة؟

المحلول

malloc و printf عادةً ما تستخدم الهياكل العالمية ، وتوظيف التزامن القائم على القفل داخليًا. لهذا السبب لم يعودوا.

ال malloc يمكن أن تكون الوظيفة إما آمنة لخيط أو مؤشر ترابط غير آمن. كلاهما لا يعود إلى:

  1. يعمل Malloc على كومة عالمية ، ومن الممكن أن اثنين من الدعوات المختلفة malloc يحدث هذا في نفس الوقت ، وإرجاع كتلة الذاكرة نفسها. (يجب أن تحدث مكالمة Malloc الثانية قبل جلب عنوان القطعة ، لكن الجزء غير متاح على أنه غير متوفر). هذا ينتهك ما بعد الشرط malloc, ، لذلك لن يتم إعادة إدخال هذا التنفيذ.

  2. لمنع هذا التأثير ، تنفيذ آمن مؤشر ترابط malloc سوف تستخدم التزامن القائم على القفل. ومع ذلك ، إذا تم استدعاء Malloc من معالج الإشارة ، فقد يحدث الموقف التالي:

    malloc();            //initial call
      lock(memory_lock); //acquire lock inside malloc implementation
    signal_handler();    //interrupt and process signal
    malloc();            //call malloc() inside signal handler
      lock(memory_lock); //try to acquire lock in malloc implementation
      // DEADLOCK!  We wait for release of memory_lock, but 
      // it won't be released because the original malloc call is interrupted
    

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

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

نصائح أخرى

دعونا نفهم ما نعنيه إعادة إدخال. يمكن استدعاء وظيفة إعادة إدخال قبل الانتهاء من الاحتجاج السابق. قد يحدث هذا إذا

  • يتم استدعاء وظيفة في معالج الإشارات (أو بشكل عام من معالج Unix بعض معالج المقاطعة) لإشارة تم رفعها أثناء تنفيذ الوظيفة
  • تسمى الوظيفة بشكل متكرر

لم يتم إعادة إدخال Malloc لأنها تدير العديد من هياكل البيانات العالمية التي تتبع كتل الذاكرة المجانية.

PRINTF ليس إعادة إدخاله لأنه يعدل متغيرًا عالميًا ، أي محتوى الملف* Stout.

هناك ثلاثة مفاهيم على الأقل هنا ، وكلها مختلطة في اللغة العامية ، وهذا قد يكون السبب في الخلط بينك.

  • آمن الخيط
  • جزء حرج
  • إعادة إدخال

لأخذ أسهل واحد أولا: كلاهما malloc و printf نكون آمن الخيط. لقد تم ضمان أن يكونوا آمنين لخيط الخيط في المعيار C منذ عام 2011 ، وفي POSIX منذ عام 2001 ، وفي الممارسة العملية منذ فترة طويلة قبل ذلك. ما يعنيه هذا هو أن البرنامج التالي مضمون لعدم تعطل السلوك السيئ أو إظهاره:

#include <pthread.h>
#include <stdio.h>

void *printme(void *msg) {
  while (1)
    printf("%s\r", (char*)msg);
}

int main() {
  pthread_t thr;
  pthread_create(&thr, NULL, printme, "hello");        
  pthread_create(&thr, NULL, printme, "goodbye");        
  pthread_join(thr, NULL);
}

مثال على وظيفة ليس آمن الخيط هو strtok. إذا اتصلت strtok من خيطين مختلفين في وقت واحد ، والنتيجة هي سلوك غير محدد - لأن strtok يستخدم داخليًا مخزنًا مؤقتًا ثابتًا لتتبع حالته. يضيف GLIBC strtok_r لإصلاح هذه المشكلة ، وأضاف C11 نفس الشيء (ولكن اختياريًا وتحت اسم مختلف ، لأنه لم يتم اختراعه هنا) strtok_s.

حسنًا ، لكن لا printf استخدم الموارد العالمية لبناء إنتاجها أيضًا؟ في الواقع ، ماذا سيفعل ذلك يعني للطباعة إلى stdout من خيطين الوقت ذاته؟ هذا يقودنا إلى الموضوع التالي. بوضوح printf سيكون جزء حرج في أي برنامج يستخدمه. يُسمح بتأسيس واحد فقط من التنفيذ داخل القسم الحرج في وقت واحد.

على الأقل في الأنظمة المتوافقة مع POSIX ، يتم تحقيق ذلك من خلال وجود printf ابدأ مع مكالمة إلى flockfile(stdout) وينتهي مع دعوة إلى funlockfile(stdout), ، وهو ما يشبه في الأساس أخذ طفرات عالمية مرتبطة بـ stdout.

ومع ذلك ، كل متميزة FILE في البرنامج مسموح له أن يكون لها mutex. هذا يعني أن موضوع واحد يمكنه الاتصال fprintf(f1,...) في نفس الوقت الذي يكون فيه الخيط الثاني في منتصف المكالمة fprintf(f2,...). لا يوجد حالة سباق هنا. (ما إذا كانت LIBC تدير فعليًا هاتين المكالمتين بالتوازي مع ذلك Qoi القضية. أنا لا أعرف في الواقع ماذا يفعل Glibc.)

بصورة مماثلة، malloc من غير المرجح أن يكون قسمًا مهمًا في أي نظام حديث ، لأن الأنظمة الحديثة موجودة ذكي بما يكفي للحفاظ على مجموعة واحدة من الذاكرة لكل موضوع في النظام, ، بدلاً من أن تقاتل جميع المواضيع n على تجمع واحد. (ال sbrk من المحتمل أن تكون استدعاء النظام قسمًا مهمًا ، ولكن malloc يقضي القليل جدا من وقته في sbrk. أو mmap, ، أو أيا كان الأطفال الرائعين يستخدمون هذه الأيام.)

حسنًا ، لذا ماذا فعلت إعادة الدخول في الواقع يعني؟ في الأساس ، هذا يعني أنه يمكن تسمية الوظيفة بأمان بشكل متكرر - يتم الاحتجاج الحالي "معلق" أثناء تشغيل الاحتجاج الثاني ، ثم لا يزال الاحتجاج الأول قادرًا على "التقاط المكان الذي توقفت فيه". (من الناحية الفنية هذا قد لا تكون بسبب مكالمة متكررة: قد يكون الاحتجاج الأول في الموضوع A ، والذي يتم مقاطعة في الوسط بواسطة الخيط B ، مما يجعل الاحتجاج الثاني. لكن هذا السيناريو هو مجرد حالة خاصة سلامة الموضوع, ، لذلك يمكننا أن ننسى ذلك في هذه الفقرة.)

لا هذا ولا ذاك printf ولا malloc يمكن أن يكون يطلق عليها بشكل متكرر مؤشر ترابط واحد ، لأنها وظائف أوراق (لا يطلقون على أنفسهم ولا يتصلون بأي رمز يسيطر عليه المستخدم يمكنه إجراء مكالمة متكررة). وكما رأينا أعلاه ، فقد كانوا آمنين لمكالمات إعادة إدخال الخيوط *متعددة الخيوط منذ عام 2001 (باستخدام الأقفال).

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


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

#include <stdio.h>
#include <printf.h>  // glibc extension

int widget(FILE *fp, const struct printf_info *info, const void *const *args) {
  static int count = 5;
  int w = *((const int *) args[0]);
  printf("boo!");  // direct recursive call
  return fprintf(fp, --count ? "<%W>" : "<%d>", w);  // indirect recursive call
}
int widget_arginfo(const struct printf_info *info, size_t n, int *argtypes) {
  argtypes[0] = PA_INT;
  return 1;
}
int main() {
  register_printf_function('W', widget, widget_arginfo);
  printf("|%W|\n", 42);
}

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

ذلك لأن كلاهما يعملان مع موارد عالمية: هياكل ذاكرة الكومة ووحدة التحكم.

تحرير: الكومة ليست سوى هيكل القائمة المرتبطة الرقيقة. كل malloc أو free يعدلها ، لذلك فإن وجود العديد من المواضيع في نفس الوقت مع كتابة الوصول إليها سيضر به تناسقه.

EDIT2: تفاصيل أخرى: يمكن جعلها تعود إلى الافتراضي باستخدام Mutexes. لكن هذا النهج مكلف ، وليس هناك ربح بأنه سيتم استخدامه دائمًا في بيئة MT.

لذلك هناك حلان: لجعل وظيفتين للمكتبة ، واحد إعادة إدخال واحد أو واحد لا أو يترك جزء mutex للمستخدم. لقد اختاروا الثانية.

أيضًا ، يمكن أن يكون ذلك لأن الإصدارات الأصلية من هذه الوظائف كانت غير مربية ، لذلك تم الإعلان عنها من أجل التوافق.

إذا حاولت الاتصال بـ Malloc من موضوعين منفصلين (إلا إذا كان لديك إصدار آمن لخيط مؤشرات الترابط ، غير مضمون بواسطة C Standard) ، تحدث أشياء سيئة ، لأن هناك كومة واحدة فقط لخيوطين. نفس الشيء بالنسبة للطباعة- السلوك غير محدد. هذا ما يجعلهم في الواقع غير رياضي.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top