باختصار ، فإن استدعاءات timeBeginPeriod من عملية واحدة تؤثر الآن على العمليات الأخرى أقل من ذي قبل ، على الرغم من أن التأثير لا يزال موجودًا.
أعتقد أن السلوك الجديد هو في الأساس تحسن ، لكنه غريب ويستحق التوثيق. بصراحة ، أحذرك - ليس لدي سوى نتائج تجاربي الخاصة ، لذلك لا يمكنني إلا أن أخمن الأهداف وبعض الآثار الجانبية لهذا التغيير. إذا كانت أي من نتائجي غير صحيحة ، فيرجى إبلاغي بذلك.
المقاطعات الموقت وسبب وجودها
أولاً ، القليل من السياق حول تصميم نظام التشغيل. من المستحسن أن يكون البرنامج قادرًا على النوم والاستيقاظ لاحقًا. في الواقع ، لا ينبغي القيام بذلك كثيرًا - فعادة ما تنتظر الخيوط الأحداث ، وليس المؤقتات - ولكن في بعض الأحيان يكون ذلك ضروريًا. لذلك ، يحتوي Windows على وظيفة Sleep - قم بتمرير مدة السكون المرغوبة بالمللي ثانية وسوف تستيقظ العملية:
Sleep (1) ؛
يجدر النظر في كيفية تنفيذ ذلك. من الناحية المثالية ، عندما يتم استدعاء Sleep (1) ، سينام المعالج. ولكن كيف يستيقظ نظام التشغيل الخيط إذا كان المعالج في وضع السكون؟ الجواب هو مقاطعات الأجهزة. يقوم نظام التشغيل ببرمجة شريحة - جهاز مؤقت للأجهزة ، والذي يقوم بعد ذلك بتشغيل مقاطعة تؤدي إلى تنبيه المعالج ، ثم يبدأ نظام التشغيل في تشغيل مؤشر الترابط.
تحتوي الدالتان WaitForSingleObject و WaitForMultipleObjects أيضًا على قيم المهلة ، ويتم تنفيذ هذه المهلات باستخدام نفس الآلية.
إذا كان هناك العديد من مؤشرات الترابط في انتظار المؤقتات ، فيمكن لنظام التشغيل برمجة جهاز مؤقت للأجهزة لفترة فردية لكل مؤشر ترابط ، ولكن هذا يؤدي عادةً إلى حقيقة أن الخيوط تستيقظ في وقت عشوائي ، ولا ينام المعالج بشكل طبيعي. تعتمد كفاءة طاقة وحدة المعالجة المركزية بشكل كبير على وقت سكونها ( الوقت العادي هو 8 مللي ثانية ) ، ولا تساهم عمليات الاستيقاظ العشوائية في ذلك. إذا تزامن عدة مؤشرات ترابط أو انتظر جهاز ضبط الوقت ، يصبح النظام أكثر كفاءة في استخدام الطاقة.
هناك العديد من الطرق للجمع بين التنبيهات ، ولكن الآلية الرئيسية في Windows هي مقاطعة مؤقت يعمل بشكل عام بمعدل ثابت. عندما يستدعي مؤشر ترابط Sleep (n) ، سيقوم نظام التشغيل بجدولة مؤشر الترابط ليبدأ فورًا بعد مقاطعة المؤقت الأول. هذا يعني أن الخيط قد ينتهي به الأمر إلى الاستيقاظ بعد قليل ، لكن Windows ليس نظام تشغيل في الوقت الفعلي ، ولا يضمن وقت تنبيه محدد على الإطلاق (في هذا الوقت قد تكون نوى المعالج مشغولة) ، لذلك من الطبيعي أن تستيقظ بعد ذلك بقليل.
يعتمد الفاصل الزمني بين مقاطعات المؤقت على إصدار Windows والأجهزة ، ولكن على جميع أجهزتي ، تم ضبطه افتراضيًا على 15.625 مللي ثانية (1000 مللي ثانية / 64). هذا يعني أنه إذا اتصلت بـ Sleep (1)في وقت عشوائي ، سيتم إيقاظ العملية في مكان ما بين 1.0 مللي ثانية و 16.625 مللي ثانية في المستقبل عند تشغيل مقاطعة المؤقت العام التالي (أو بعد ذلك مرة واحدة إذا كان الوقت مبكرًا جدًا).
باختصار ، طبيعة تأخيرات المؤقت هي أنه (ما لم يتم استخدام الانتظار النشط للمعالج ، ويرجى عدم استخدامه ) ، يمكن لنظام التشغيل تنبيه سلاسل الرسائل فقط في أوقات معينة باستخدام مقاطعات المؤقت ، ويستخدم Windows المقاطعات العادية.
بعض البرامج لا تستوعب مثل هذا النطاق الواسع من زمن الوصول (WPF ، و SQL Server ، و Quartz ، و PowerDirector ، و Chrome ، و Go Runtime ، والعديد من الألعاب ، وما إلى ذلك). لحسن الحظ ، يمكنهم حل المشكلة باستخدام وظيفة timeBeginPeriodمما يسمح للبرنامج بطلب فاصل زمني أصغر. هناك أيضًا وظيفة NtSetTimerResolution التي تسمح بضبط الفاصل الزمني على أقل من ميلي ثانية ، ولكن نادرًا ما يتم استخدامها ولن تحتاجها أبدًا ، لذلك لن أذكرها مرة أخرى.
عقود من الجنون
هذا شيء مجنون: يمكن استدعاء timeBeginPeriod من قبل أي برنامج وهو يغير الفاصل الزمني لمقاطعة المؤقت ، ومقاطعة المؤقت هي مورد عالمي.
لنتخيل أن العملية A في حلقة مع استدعاء للنوم (1) . هذا خطأ ، لكنه كذلك ، ويستيقظ افتراضيًا كل 15.625 مللي ثانية ، أو 64 مرة في الثانية. ثم تدخل العملية B وتستدعي timeBeginPeriod (2) . يؤدي هذا إلى إطلاق المؤقت في كثير من الأحيان ، وفجأة تستيقظ العملية A 500 مرة في الثانية بدلاً من 64 مرة في الثانية. هذا جنون! ولكن هذه هي الطريقة التي يعمل بها Windows دائمًا.
في هذه المرحلة ، إذا ظهرت العملية C وتسمى timeBeginPeriod (4)، لن يغير ذلك أي شيء - ستستمر العملية "أ" في الاستيقاظ 500 مرة في الثانية. في مثل هذه الحالة ، ليست المكالمة الأخيرة هي التي تحدد القواعد ، ولكن المكالمة مع الحد الأدنى من الفاصل الزمني.
وبالتالي ، يمكن أن يؤدي استدعاء timeBeginPeriod من أي برنامج قيد التشغيل إلى تعيين الفاصل الزمني لمقاطعة المؤقت العام. إذا تم إنهاء هذا البرنامج أو استدعاء timeEndPeriod ، فسيتم تفعيل الحد الأدنى الجديد. إذا كان أحد البرامج يستدعي timeBeginPeriod (1) ، فهذا هو الآن الفاصل الزمني للمقاطعة على مستوى النظام. إذا استدعى أحد البرامج timeBeginPeriod (1) واستدعى برنامج آخر timeBeginPeriod (4) ، فإن الفاصل الزمني لمقاطعة جهاز ضبط الوقت الذي يبلغ مللي ثانية يصبح قانونًا عالميًا.
هذا مهم لأن ارتفاع معدل مقاطعة المؤقت - ومعدل جدولة مؤشر الترابط المرتفع المصاحب - يمكن أن يهدر طاقة كبيرة لوحدة المعالجة المركزية ، كما تمت مناقشته هنا .
أحد التطبيقات التي تحتاج إلى جدولة تعتمد على المؤقت هو متصفح الويب. يحتوي معيار JavaScript على وظيفة setTimeout التي تطلب من المتصفح استدعاء وظيفة JavaScript بعد بضع أجزاء من الثانية. يستخدم Chromium أجهزة ضبط الوقت لتنفيذ هذه الميزة وغيرها من الميزات (بشكل أساسي WaitForSingleObject مع المهلات ، وليس Sleep). يتطلب هذا غالبًا زيادة معدل مقاطعة المؤقت. للحفاظ على عمر البطارية منخفضًا ، تمت إعادة تصميم Chromium مؤخرًا للحفاظ على معدل مقاطعة المؤقت أقل من 125 هرتز (فاصل 8 مللي ثانية) على طاقة البطارية .
الوقت
ترجع وظيفة timeGetTime (التي يجب عدم الخلط بينها وبين GetTickCount) الوقت الحالي ، المحدّث بواسطة مقاطعة جهاز ضبط الوقت. تاريخياً ، لم تكن المعالجات جيدة جدًا في الحفاظ على الوقت الدقيق (تتأرجح ساعاتها عمدًا لتجنب العمل كأجهزة إرسال FM ، ولأسباب أخرى) ، لذلك تعتمد وحدات المعالجة المركزية غالبًا على مولدات ساعة منفصلة للحفاظ على وقت دقيق. القراءة من هذه الرقائق باهظة الثمن ، وهذا هو السبب في أن Windows يحافظ على تحديث عداد ميلي ثانية 64 بت بمقاطعة المؤقت. يتم تخزين هذا المؤقت في الذاكرة المشتركة ، لذلك يمكن لأي عملية قراءة الوقت الحالي بثمن بخس دون الحاجة إلى الذهاب إلى الساعة. timeGetTime يدعو ReadInterruptTick ، التي تنص أساسا هذا فقط 64 بت العداد. بكل بساطة!
نظرًا لأنه يتم تحديث العداد بواسطة مقاطعة المؤقت ، يمكننا تعقبه وإيجاد تردد مقاطعة المؤقت.
واقع جديد غير موثق
مع إصدار Windows 10 2004 (أبريل 2020) ، تغيرت بعض هذه الآليات بشكل طفيف ، ولكن بطريقة مربكة للغاية. أولاً ، كانت هناك رسائل تفيد بأن timeBeginPeriod لم يعد يعمل . في الواقع ، تبين أن كل شيء أكثر تعقيدًا.
أعطت التجارب الأولى نتائج مختلطة. عندما قمت بتشغيل البرنامج باستدعاء timeBeginPeriod (2) ، أظهرت clockres فاصلًا مؤقتًا يبلغ 2.0 مللي ثانية ، لكن برنامج اختبار منفصل مع حلقة Sleep (1) استيقظ حوالي 64 مرة في الثانية بدلاً من 500 مرة كما في الإصدارات السابقة من Windows.
تجربة علمية
ثم كتبت برنامجين لدراسة سلوك النظام. برنامج واحد ( change_interval.cpp ) يجلس في حلقة ، يستدعي timeBeginPeriod بفواصل زمنية من 1 إلى 15 مللي ثانية . تمسك كل فاصل لمدة أربع ثوان ، ثم تنتقل إلى التي تليها ، وهكذا في دائرة. خمسة عشر سطرا من التعليمات البرمجية. سهل.
يقوم برنامج آخر ( Meas_interval.cpp ) بإجراء عدة اختبارات للتحقق من كيفية تغير سلوكه عند تغيير change_interval.cpp. يراقب البرنامج ثلاثة معايير.
- تسأل نظام التشغيل عن الدقة الحالية للمؤقت العام باستخدام NtQueryTimerResolution .
- timeGetTime, , — , .
- Sleep(1), . .
أجرتFelixPetriconi اختبارات لي على Windows 10 1909 وأجريت اختبارات على Windows 10 2004. فيما يلي النتائج الخالية من الاهتزازات :
وهذا يعني أن timeBeginPeriod لا يزال يحدد الفاصل الزمني العام على جميع إصدارات Windows. من نتائج timeGetTime () ، يمكننا القول أن المقاطعة يتم تشغيلها بهذا المعدل على مركز معالج واحد على الأقل ، ويتم تحديث الوقت. لاحظ أيضًا أن الإصدار 2.0 في السطر الأول لعام 1909 كان أيضًا 2.0 في Windows XP ، ثم 1.0 في Windows 7/8 ، ومن ثم يبدو أنه عاد إلى الإصدار 2.0 مرة أخرى؟
ومع ذلك ، يتغير سلوك الجدولة بشكل كبير في نظام التشغيل Windows 10 2004. سابقًا ، كان تأخير وضع السكون (1)في أي عملية تساوي فقط الفاصل الزمني لمقاطعة المؤقت ، باستثناء timeBeginPeriod (1) ، مما يعطي رسمًا بيانيًا مثل هذا:
في Windows 10 2004 ، العلاقة بين timeBeginPeriod ووقت استجابة السكون في عملية أخرى (التي لم تستدعي timeBeginPeriod ) تبدو غريبة:
الشكل الدقيق للجانب الأيسر من الرسم البياني غير واضح ، ولكن إنه يسير بالتأكيد في الاتجاه المعاكس للاتجاه السابق!
لماذا ا؟
تأثيرات
كما هو موضح في مناقشة reddit و hacker-news ، من المحتمل أن يكون النصف الأيسر من الرسم البياني هو محاولة لتقليد زمن الانتقال "الطبيعي" بأكبر قدر ممكن ، نظرًا للدقة المتاحة لمقاطعة المؤقت العالمي. أي ، مع فاصل مقاطعة يبلغ 6 مللي ثانية ، يحدث التأخير بحوالي 12 مللي ثانية (دورتان) ، ومع فاصل مقاطعة يبلغ 7 مللي ثانية ، يحدث التأخير بحوالي 14 مللي ثانية (دورتان). ومع ذلك ، فإن قياس التأخيرات الفعلية يظهر أن الواقع أكثر إرباكًا. مع ضبط مقاطعة المؤقت على 7 مللي ثانية ، فإن وقت استجابة السكون (1) الذي يبلغ 14 مللي ثانية ليس هو النتيجة الأكثر شيوعًا:
قد يلقي بعض القراء باللوم على الضوضاء العشوائية في النظام ، ولكن عندما يكون معدل مقاطعة المؤقت 9 مللي ثانية أو أعلى ، فإن الضوضاء تكون صفرية ، لذلك هذا ليس كذلك يمكن أن يكون تفسيرا.حاول تشغيل الكود المحدث بنفسك . يبدو أن فترات المقاطعة من 4 مللي ثانية إلى 8 مللي ثانية مثيرة للجدل بشكل خاص. ربما يجب إجراء قياسات الفاصل الزمني باستخدام QueryPerformanceCounter لأن الكود الحالي يتأثر بشكل عشوائي بالتغييرات في قواعد الجدولة والتغييرات في دقة جهاز ضبط الوقت.
كل هذا غريب جدًا ولا أفهم المنطق أو التنفيذ. قد يكون هذا خطأ ، لكنني أشك في ذلك. أعتقد أن هناك منطق توافق مع الإصدارات السابقة وراء ذلك. لكن الطريقة الأكثر فاعلية لتجنب مشاكل التوافق هي توثيق التغييرات ، ويفضل أن يكون ذلك مقدمًا ، وهنا يتم إجراء التعديلات دون أي إشعار.
لن يؤثر هذا على معظم البرامج. إذا كانت العملية تريد مقاطعة عداد الوقت بشكل أسرع ، فيجب أن تستدعي timeBeginPeriod نفسه.... ومع ذلك ، قد تحدث المشكلات التالية:
- قد يفترض البرنامج عن طريق الخطأ أن Sleep (1) و timeGetTime لهما نفس الدقة ، وهو ما لم يعد كذلك. على الرغم من أن مثل هذا الافتراض يبدو غير مرجح.
- , . — Windows System Timer Tool TimerResolution 1.2. «» , . , . , , .
- , , . , . . , , timeBeginPeriod , , .
لا يعمل برنامج الاختبار change_interval.cpp إلا إذا لم يطلب أحد معدل مقاطعة أعلى للمؤقت. نظرًا لأن كلاً من Chrome و Visual Studio لديهما عادة القيام بذلك ، فقد اضطررت إلى القيام بمعظم تجاربي دون الوصول إلى الإنترنت والتشفير في المفكرة . اقترح شخص ما Emacs ، لكن الانخراط في هذا النقاش يفوق سلطتي