تبادل البيانات بين العمليات باستخدام مناطق الذاكرة المشتركة. تبادل البيانات بين التطبيقات باستخدام آلية UNIX System V IPC

16.04.2019

اتصال interprocess ( الاتصال بين العمليات (IPC)) عبارة عن مجموعة من الطرق لتبادل البيانات بين سلاسل العمليات. يمكن إطلاق العمليات على نفس الكمبيوتر وعلى أجهزة مختلفة متصلة عبر الشبكة. تأتي IPCs في عدة أنواع: "الإشارة"، "المقبس"، "الإشارة"، "الملف"، "الرسالة" ...

في هذه المقالة أريد أن ألقي نظرة على 3 أنواع فقط من IPC:

الاستطراد: هذه المقالة تعليمية ومخصصة للأشخاص الذين بدأوا للتو في طريق برمجة النظام. هدفها الرئيسي هو التعرف على الطرق المختلفة للتفاعل بين العمليات على نظام تشغيل متوافق مع POSIX.

الأنابيب المسماة

لنقل الرسائل، يمكنك استخدام المقابس والقنوات وD-bus وغيرها من التقنيات. يمكنك أن تقرأ عن المقابس الموجودة في كل زاوية، وتكتب مقالًا منفصلاً عن D-bus. لذلك، قررت التطرق إلى التقنيات غير المعروفة التي تلبي معايير POSIX وتقديم أمثلة عملية.

دعونا نلقي نظرة على تمرير الرسائل عبر الأنابيب المسماة. يبدو النقل بشكل تخطيطي كما يلي:

لإنشاء أنابيب مسماة سنستخدم الدالة، مكفيفو ():
#يشمل int mkfifo(const char *pathname, mode_t mode);
تقوم الوظيفة بإنشاء ملف FIFO خاص باسم اسم المسار، والمعلمة وضعيحدد أذونات الملف.

ملحوظة: وضعتستخدم بالاشتراك مع القيمة الحالية umaskبالطريقة الآتية: (وضع& ~ أوماسك). ستكون نتيجة هذه العملية قيمة جديدة umaskللملف الذي نقوم بإنشائه. لهذا السبب نستخدم 0777 ( S_IRWXO | S_IRWXG | S_IRWXU) حتى لا تتم الكتابة فوق أي جزء من القناع الحالي.
بمجرد إنشاء ملف، يمكن لأي عملية فتح الملف للقراءة أو الكتابة تمامًا كما تفتح ملفًا عاديًا. ومع ذلك، من أجل الاستخدام الصحيح للملف، من الضروري فتحه في وقت واحد من خلال عمليتين/خيطين، أحدهما لتلقي البيانات (قراءة ملف)، والآخر للإرسال (الكتابة إلى ملف).

إذا تم إنشاء ملف FIFO بنجاح، مكفيفو ()ترجع 0 (صفر). في حالة وجود أي أخطاء، تقوم الدالة بإرجاع -1 وتعيين رمز الخطأ إلى متغير خطأ.

الأخطاء النموذجية التي قد تحدث أثناء إنشاء القناة:

  • EACCES- لا توجد حقوق للتشغيل (التنفيذ) في أحد الأدلة الموجودة في المسار اسم المسار
  • موجود- ملف اسم المسارموجود بالفعل، حتى لو كان الملف عبارة عن رابط رمزي
  • لا شيء- لا يوجد دليل مذكور فيه اسم المسار، أو أن الرابط معطل
  • اينوسبك- لا توجد مساحة لإنشاء ملف جديد
  • إينوتدير- أحد الأدلة المذكورة في اسم المسار، في الواقع، فهو ليس كذلك
  • ايروفس- محاولة إنشاء ملف FIFO على نظام ملفات للقراءة فقط
تتم القراءة والكتابة في الملف الذي تم إنشاؤه باستخدام الوظائف يقرأ()و يكتب().

مثال

mkfifo.c
#يشمل #يشمل #يشمل #يشمل #define NAMEDPIPE_NAME "/tmp/my_named_pipe" #define BUFSIZE 50 int main (int argc, char ** argv) ( int fd, len; char buf; if (mkfifo(NAMEDPIPE_NAME, 0777)) ( perror("mkfifo"); return 1 ) printf("تم إنشاء %s\n"، NAMEDPIPE_NAME if ((fd = open(NAMEDPIPE_NAME, O_RDONLY)));<= 0) { perror("open"); return 1; } printf("%s is opened\n", NAMEDPIPE_NAME); do { memset(buf, "\0", BUFSIZE); if ((len = read(fd, buf, BUFSIZE-1)) <= 0) { perror("read"); close(fd); remove(NAMEDPIPE_NAME); return 0; } printf("Incomming message (%d): %s\n", len, buf); } while (1); } [скачать ]

نفتح الملف للقراءة فقط ( O_RDONLY). ويمكن استخدامها O_NONBLOCKمعدّل مصمم خصيصًا لملفات FIFO، حتى لا تنتظر فتح الملف للكتابة على الجانب الآخر. ولكن في الكود المعطى هذه الطريقة غير ملائمة.

نقوم بتجميع البرنامج ثم تشغيله:
$ gcc -o mkfifo mkfifo.c $ ./mkfifo
في النافذة الطرفية التالية نقوم بتنفيذ:
$ echo "مرحبًا، أنبوبي المُسمى!" > /tmp/my_named_pipe
ونتيجة لذلك سنرى المخرجات التالية من البرنامج:
$ ./mkfifo /tmp/my_named_pipe تم إنشاؤه /tmp/my_named_pipe مفتوح الرسالة الواردة (22): مرحبًا، أنبوبي المسمى! إقرأ : النجاح

ذكريات مشتركه

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


لتخصيص الذاكرة المشتركة سوف نستخدم وظيفة POSIX shm_open():
#يشمل int shm_open(const char *name, int oflag, mode_t mode);
تقوم الدالة بإرجاع واصف الملف المرتبط بكائن الذاكرة. يمكن استخدام هذا الواصف لاحقًا بواسطة وظائف أخرى (على سبيل المثال، مماب ()أو مبروتيك()).

يتم الحفاظ على سلامة كائن الذاكرة، بما في ذلك جميع البيانات المرتبطة به، حتى يتم فصل/حذف الكائن ( shm_unlink()). هذا يعني أن أي عملية يمكنها الوصول إلى كائن الذاكرة الخاص بنا (إذا كانت تعرف اسمه) حتى نستدعيه بشكل صريح shm_unlink().

عامل oflagهو bitwise OR من العلامات التالية:

  • O_RDONLY- مفتوح فقط مع أذونات القراءة
  • O_RDWR- فتح مع أذونات القراءة والكتابة
  • O_CREAT- إذا كان الكائن موجودًا بالفعل، فلن يكون للعلم أي تأثير. وبخلاف ذلك، يتم إنشاء الكائن وتعيين حقوق الوصول إليه وفقًا للوضع.
  • O_EXCL- سيؤدي تعيين هذه العلامة مع O_CREATE إلى قيام shm_open بإرجاع خطأ إذا كان مقطع الذاكرة المشتركة موجودًا بالفعل.
كيف يتم تعيين قيمة المعلمة وضعتم وصفه بالتفصيل في الفقرة السابقة "تمرير الرسالة".

بعد إنشاء كائن ذاكرة مشتركة، نقوم بتعيين حجم الذاكرة المشتركة عن طريق الاتصال فترونكات (). إدخال الوظيفة هو واصف الملف الخاص بالكائن والحجم الذي نحتاجه.

مثال

يوضح التعليمة البرمجية التالية إنشاء الذاكرة المشتركة وتعديلها وحذفها. ويوضح أيضًا كيف أنه بعد إنشاء الذاكرة المشتركة، يخرج البرنامج، ولكن في المرة التالية التي نبدأه فيها يمكننا الوصول إليه حتى يتم تنفيذه shm_unlink().
shm_open.c
#يشمل #يشمل #يشمل #يشمل #يشمل #يشمل #define SHARED_MEMORY_OBJECT_NAME "my_shared_memory" #define SHARED_MEMORY_OBJECT_SIZE 50 #define SHM_CREATE 1 #define SHM_PRINT 3 #define SHM_CLOSE 4 void use(const char * s) ( printf("Usage: %s" ["text"]\n"، s); ) int main (int argc, char ** argv) ( int shm, len, cmd, mode = 0; char *addr; if (argc< 2) { usage(argv); return 1; } if ((!strcmp(argv, "create") || !strcmp(argv, "write")) && (argc == 3)) { len = strlen(argv); len = (len<=SHARED_MEMORY_OBJECT_SIZE)?len:SHARED_MEMORY_OBJECT_SIZE; mode = O_CREAT; cmd = SHM_CREATE; } else if (! strcmp(argv, "print")) { cmd = SHM_PRINT; } else if (! strcmp(argv, "unlink")) { cmd = SHM_CLOSE; } else { usage(argv); return 1; } if ((shm = shm_open(SHARED_MEMORY_OBJECT_NAME, mode|O_RDWR, 0777)) == -1) { perror("shm_open"); return 1; } if (cmd == SHM_CREATE) { if (ftruncate(shm, SHARED_MEMORY_OBJECT_SIZE+1) == -1) { perror("ftruncate"); return 1; } } addr = mmap(0, SHARED_MEMORY_OBJECT_SIZE+1, PROT_WRITE|PROT_READ, MAP_SHARED, shm, 0); if (addr == (char*)-1) { perror("mmap"); return 1; } switch (cmd) { case SHM_CREATE: memcpy(addr, argv, len); addr = "\0"; printf("Shared memory filled in. You may run "%s print" to see value.\n", argv); break; case SHM_PRINT: printf("Got from shared memory: %s\n", addr); break; } munmap(addr, SHARED_MEMORY_OBJECT_SIZE); close(shm); if (cmd == SHM_CLOSE) { shm_unlink(SHARED_MEMORY_OBJECT_NAME); } return 0; } [скачать ]

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

هذه المرة تحتاج إلى تجميع التعليمات البرمجية مع الخيار -lrt:
$ دول مجلس التعاون الخليجي -o shm_open -lrt shm_open.c
دعونا نرى ما حدث:
$ ./shm_open قم بإنشاء "مرحبًا، ذاكرتي المشتركة!" الذاكرة المشتركة مملوءة. يمكنك تشغيل "./shm_open print" لمعرفة القيمة. $ ./shm_open print حصلت عليها من الذاكرة المشتركة: مرحبًا، ذاكرتي المشتركة! $ ./shm_open قم بإنشاء "مرحبًا!" الذاكرة المشتركة مملوءة. يمكنك تشغيل "./shm_open print" لمعرفة القيمة. $ ./shm_open print تم الحصول عليها من الذاكرة المشتركة: مرحبًا! $ ./shm_open أغلق $ ./shm_open طباعة shm_open: لا يوجد مثل هذا الملف أو الدليل
في برنامجنا، نستخدم الوسيطة "إنشاء" لإنشاء ذاكرة مشتركة ولتغيير محتوياتها.

بمعرفة اسم كائن الذاكرة، يمكننا تغيير محتويات الذاكرة المشتركة. ولكن يجب أن ندعو shm_unlink()كيف تتوقف الذاكرة عن الوصول إلينا و shm_open()بدون معلمة O_CREATEإرجاع الخطأ "لا يوجد مثل هذا الملف أو الدليل".

إشارة

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

هناك نوعان من الإشارات:

  1. إشارة مع عداد (يحسب الإشارة)، والذي يحدد حد الموارد للعمليات التي تصل إليها
  2. الإشارة الثنائية، التي تحتوي على حالتين "0" أو "1" (في أغلب الأحيان: "مشغول" أو "غير مشغول")
دعونا نلقي نظرة على كلا النوعين من الإشارات.

إشارة مع العداد

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

لذلك، لتنفيذ الإشارات سوف نستخدم وظيفة POSIX sem_open():
#يشمل sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
إلى وظيفة إنشاء الإشارة، نقوم بتمرير اسم الإشارة، التي تم إنشاؤها وفقًا لقواعد معينة، وأعلام التحكم. بهذه الطريقة نحصل على إشارة مسماة.
يتم إنشاء اسم الإشارة على النحو التالي: في البداية يوجد حرف "/" (شرطة مائلة)، متبوعًا بأحرف لاتينية. لا ينبغي استخدام رمز الشرطة المائلة بعد الآن. يمكن أن يصل طول اسم الإشارة إلى 251 حرفًا.

إذا كنا بحاجة إلى إنشاء إشارة، فسيتم تمرير إشارة التحكم O_CREATE. للبدء في استخدام إشارة موجودة بالفعل، إذن oflagيساوي الصفر. إذا جنبا إلى جنب مع العلم O_CREATEتمرير العلم O_EXCL، ثم الدالة sem_open()سيُرجع خطأ إذا كانت الإشارة بالاسم المحدد موجودة بالفعل.

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

لفتح إشارة موجودة بسرعة، نستخدم البناء التالي:
#يشمل sem_t *sem_open(const char *name, int oflag); ، حيث يتم تحديد اسم الإشارة وعلامة التحكم فقط.

مثال على إشارة مع عداد

دعونا نلقي نظرة على مثال لاستخدام الإشارة لمزامنة العمليات. في مثالنا، تعمل إحدى العمليات على زيادة قيمة الإشارة المرورية وتنتظر العملية الثانية لإعادة ضبطها لمواصلة التنفيذ.
sem_open.c
#يشمل #يشمل #يشمل #يشمل #define SEMAPHORE_NAME "/my_named_semaphore" int main(int argc, char ** argv) ( sem_t *sem; if (argc == 2) ( printf("Dropping semaphore...\n"); if ((sem = sem_open (SEMAPHORE_NAME, 0)) == SEM_FAILED) ( perror("sem_open"); return 1; ) sem_post(sem); printf("تم إسقاط الإشارة.\n" ) if ((sem = sem_open(SEMAPHORE_NAME, O_CREAT, 0777, 0)) == SEM_FAILED) ( perror("sem_open"); return 1; ) printf("تم أخذ الإشارة.\nفي انتظار إسقاطها.\n") ;< 0) perror("sem_wait"); if (sem_close(sem) < 0) perror("sem_close"); return 0; } [скачать ]

في وحدة تحكم واحدة نقوم بتشغيل:
$ ./sem_open تم أخذ الإشارة. في انتظار أن يتم إسقاطها.<-- здесь процесс в ожидании другого процесса sem_wait: Success sem_close: Success
في وحدة التحكم التالية نقوم بتشغيل:
$ ./sem_open 1 إسقاط الإشارة... sem_post: إسقاط إشارة النجاح.

إشارة ثنائية

بدلاً من الإشارة الثنائية، التي تُستخدم فيها الدالة sem_open أيضًا، سأفكر في إشارة أكثر استخدامًا تسمى كائن المزامنة (mutex).

كائن المزامنة (mutex) هو في الأساس نفس الشيء مثل الإشارة الثنائية (أي إشارة ذات حالتين: "مشغول" و"غير مشغول"). لكن المصطلح "mutex" يستخدم غالبًا لوصف مخطط يمنع عمليتين من مشاركة البيانات/المتغيرات في نفس الوقت. بينما يُستخدم مصطلح "الإشارة الثنائية" في أغلب الأحيان لوصف البنية التي تقيد الوصول إلى مورد واحد. وهذا يعني أنه يتم استخدام إشارة ثنائية حيث "تحتل" عملية واحدة إشارة، ثم "تحررها" عملية أخرى. بينما يتم تحرير كائن المزامنة (mutex) بنفس العملية/الخيط الذي يشغله.

لا يمكنك الاستغناء عن كائن المزامنة (mutex) عند كتابة قاعدة بيانات، على سبيل المثال، يمكن للعديد من العملاء الوصول إليها.

لاستخدام كائن المزامنة (mutex)، تحتاج إلى استدعاء الدالة pthread_mutex_init():
#يشمل Int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
تقوم الوظيفة بتهيئة كائن المزامنة (variable كائن المزامنة) يصف mutexattr. لو mutexattrيساوي باطل، ثم تتم تهيئة كائن المزامنة (mutex) إلى القيمة الافتراضية. إذا تم تنفيذ الوظيفة بنجاح (رمز الإرجاع 0)، فسيتم اعتبار كائن المزامنة (mutex) مهيأ و"مجانيًا".

الأخطاء النموذجية التي قد تحدث:

  • مرة أخرى- لا توجد موارد ضرورية كافية (باستثناء الذاكرة) لتهيئة كائن المزامنة (mutex).
  • ENOMEM- الذاكرة غير كافية
  • إيبيرم- لا يوجد حقوق لإجراء العملية
  • إيبوسي- محاولة تهيئة كائن المزامنة (mutex) الذي تمت تهيئته بالفعل ولكن لم يتم تدميره
  • إينفال- معنى mutexattrغير صالح
لاحتلال كائن المزامنة أو تحريره، نستخدم الوظائف:
int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex);
وظيفة pthread_mutex_lock()، لو كائن المزامنةلم يحتلها بعد، يحتلها، ويصبح مالكها ويغادر على الفور. إذا كان كائن المزامنة (mutex) مشغولاً، فإنه يمنع تنفيذ العملية وينتظر حتى يتم تحرير كائن المزامنة (mutex).
وظيفة pthread_mutex_trylock()متطابقة في السلوك مع الوظيفة pthread_mutex_lock()مع استثناء واحد - لا يمنع العملية إذا كائن المزامنةمشغول ولكن يعود إيبوسيشفرة.
وظيفة pthread_mutex_unlock()يطلق كائن المزامنة المزدحم.

رموز العودة ل pthread_mutex_lock():

  • EINVAL - لم تتم تهيئة كائن المزامنة (mutex) بشكل صحيح
  • EDEADLK - كائن المزامنة مشغول بالفعل بالعملية الحالية
رموز العودة ل pthread_mutex_trylock():
  • EBUSY - كائن المزامنة (mutex) مشغول بالفعل
رموز العودة ل pthread_mutex_unlock():
  • EINVAL - لم تتم تهيئة كائن المزامنة بشكل صحيح
  • EPERM - لا تمتلك عملية الاستدعاء كائن المزامنة (mutex).

مثال كائن المزامنة

mutex.c
#يشمل #يشمل #يشمل #يشمل عداد كثافة العمليات ثابت؛ // المورد المشترك static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; void incr_counter(void *p) ( do ( usleep(10); // دعنا نحصل على شريحة زمنية بين أقفال كائن المزامنة pthread_mutex_lock(&mutex); counter++; printf("%d\n", counter); Sleep(1) ; اضغط على "Enter" لتهيئة العداد بقيمة جديدة في أي وقت.\n"); Sleep(3); pthread_mutex_unlock(&mutex); // قم بإلغاء حظر كائن المزامنة المحظور حتى يعمل مؤشر ترابط آخر ( if (gets(buf) != buf) return // لا توجد حماية من الخداع! خطر التجاوز! num = atoi(buf); قفل كائن المزامنة باستخدام pthread_mutex_lock().\n"); pthread_mutex_lock(&mutex); else if (rc == 0) ( printf("واو! أنت في الوقت المحدد! تهانينا!\n"); ) else ( printf ("خطأ" : %d\n"، rc)؛ return; counter = num; printf("القيمة الجديدة للعداد هي %d\n"، counter); pthread_mutex_unlock(&mutex); ) بينما (1)؛ ) int main(int argc, char ** argv) ( pthread_t thread_1; pthread_t thread_2; counter = 0; pthread_create(&thread_1, NULL, (void *)&incr_counter, NULL); pthread_create(&thread_2, NULL, (void *)&reset_counter, NULL);

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

إذا لم نستخدم تقنية كائن المزامنة (mutex)، فلن نعرف القيمة التي ستكون في المتغير العام مع الوصول المتزامن بواسطة خيطين. أيضا أثناء بدء التشغيل الفرق بين pthread_mutex_lock()و pthread_mutex_trylock().

تحتاج إلى ترجمة التعليمات البرمجية باستخدام معلمة إضافية -lpthread:
$ دول مجلس التعاون الخليجي -o mutex -lpthread mutex.c
نبدأ ونغير قيمة المتغير ببساطة عن طريق إدخال قيمة جديدة في النافذة الطرفية:
$ ./mutex أدخل الرقم واضغط على "Enter" لتهيئة العداد بقيمة جديدة في أي وقت. 1 2 3 30 <--- новое значение переменной Mutex is already locked by another process. Let"s lock mutex using pthread_mutex_lock(). New value for counter is 30 31 32 33 1 <--- новое значение переменной Mutex is already locked by another process. Let"s lock mutex using pthread_mutex_lock(). New value for counter is 1 2 3

بدلا من الاستنتاج

في المقالات التالية أريد أن ألقي نظرة على تقنيات d-bus وRPC. اذا كنت مهتما اعلمني بذالك.
شكرًا لك.

محدث:تم تحديث الفصل الثالث حول الإشارات. تمت إضافة فصل فرعي حول كائن المزامنة (mutex).

العلامات: إضافة العلامات

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

يمكن إجراء النقل بعدة طرق:

- ذكريات مشتركه؛

- القناة (الأنبوب، الناقل) - ملف زائف تكتب فيه عملية واحدة وتقرأه أخرى؛

- المقابس هي آلية تدعمها النواة وتخفي ميزات البيئة وتسمح للعمليات بالتفاعل بشكل موحد، سواء على نفس الكمبيوتر أو على الشبكة؛

— صناديق البريد (Windows فقط)، أحادية الاتجاه، وإمكانية البث؛

- استدعاء الإجراء عن بعد، عملية أيمكن استدعاء إجراء قيد التقدم في، واسترجاع البيانات.

الناقلون (القنوات)

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

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

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

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

أرز. 2.22 مخطط تنفيذ القناة

قوائم انتظار الرسائل

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

ذكريات مشتركه

الذاكرة المشتركة هي جزء من الذاكرة الفعلية المعينة في مساحة العنوان الافتراضية لعمليتين أو أكثر.

أرز. 2.23 أ) ملف معين للذاكرة؛ ب) الذاكرة المشتركة

إحدى مزايا الملفات المعينة للذاكرة هي سهولة مشاركتها. تتيح تسمية كائن تعيين الملف إمكانية مشاركة الملف بين عمليات متعددة. في هذه الحالة، يتم تعيين محتوياته إلى الذاكرة الفعلية المشتركة.

صناديق البريد

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

كيفية استخدام قوائم انتظار الرسائل والإشارات والذاكرة المشتركة لتوصيل التطبيقات

تعمل العمليات التي تديرها نواة UNIX بشكل مستقل، مما يؤدي إلى نظام أكثر استقرارًا. ومع ذلك، يجد كل مطور نفسه في النهاية في موقف حيث يجب على مجموعة من العمليات التواصل مع مجموعة أخرى، على سبيل المثال، لتبادل البيانات أو تمرير الأوامر. تسمى طريقة المراسلة هذه اتصال interprocess (IPC). تحدد مواصفات نظام UNIX System V (SysV) ثلاث آليات لـ IPC، يشار إليها عادةً باسم SysV IPC:

  • قوائم انتظار الرسائل
  • الإشارات
  • ذكريات مشتركه

بالإضافة إلى ذلك، يمكن للعمليات أن تتفاعل بطرق أخرى، مثل:

  • القراءة والكتابة ومنع الوصول إلى الملفات
  • إشارات
  • مآخذ
  • القنوات
  • FIFO (يدخل أولاً، يخرج أولاً)

غالبًا ما يشار إلى المجموعة الأخيرة باسم IPC. تركز هذه المقالة على أساليب SysV IPC نظرًا لبساطتها وكفاءتها.

فهم نموذج SysV

أساليب SysV IPC الثلاثة لها بناء جملة مماثل، على الرغم من اختلاف أغراضها. عادة يجب عليك القيام بما يلي:

  1. تحديد مفتاح IPC مناسب للاستخدام مع ftok(3) .
  2. قم بتوفير معرف خاص بـ IPC مرتبط بمفتاح IPC باستخدام msgget(2) أو semget(2) أو shmget(2) لقوائم انتظار الرسائل أو الإشارات أو الذاكرة المشتركة على التوالي.
  3. قم بتغيير خصائص مثيل IPC باستخدام msgctl(2) أو semctl(2) أو shmctl(2) .
  4. استخدم مثيل IPC محدد.
  5. أخيرًا، قم بإنهاء مثيل IPC باستخدام msgctl(2) أو semctl(2) أو shmctl(2) وعلامة IPC_RMID.

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

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

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

هناك طريقة أسهل لإنشاء معرف مثيل IPC لتلك التطبيقات التي يمكنها القيام بذلك بنفسها. إذا كنت تستخدم مفتاح IPC_PRIVATE عند الاتصال بـ get لإنشاء IPC، فسيكون معرف المثيل فريدًا. لا تحتاج العمليات الأخرى التي تحتاج إلى الاتصال بـ IPC إلى الاتصال بها لأنها تحتوي بالفعل على معرف.

التطبيقات مجانية لاستخدام مثيلات IPC طالما أنها تحتوي على المعرف. تختلف كل طريقة IPC عن الأخرى ولا يمكن الوصول إليها إلا في القسم الخاص بها.

تمرير الرسائل من خلال قوائم الانتظار

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

يجب أن تتكون الرسائل من عدد صحيح طويل متبوعًا بمحتوى الرسالة. يُظهر بنية مشابهة في لغة C باستخدام رسالة بحجم 100 بايت.

القائمة 1. تحديد عينة الرسالة
struct mq_message (نوع طويل؛ /* النوع أو الوجهة */ char text; /* البيانات */ );

يستخدم مستلم الرسالة نوع الرسالة. عندما تتلقى رسالة من قائمة الانتظار، يمكنك تحديد أول رسالة متاحة أو يمكنك البحث عن نوع رسالة محدد. أنواع الرسائل التي سيتم استخدامها خاصة بالتطبيق لأنها تنشئ قوائم انتظار، والتي تختلف عن الأشكال الأخرى من IPC حيث تتعرف النواة على عمليات نقل بيانات التطبيق من خلال قراءة حقل النوع.

يعرض مثالاً لوضع رسالة في قائمة الانتظار.

القائمة 2. البرنامج الذي يرتب الرسائل
#يشمل #يشمل #يشمل #يشمل #يشمل int main (void) ( key_t ipckey; int mq_id; struct ( نوع طويل; نص char; ) mymsg; /* إنشاء مفتاح ipc */ ipckey = ftok("/tmp/foo", 42); printf("مفتاحي" هو %d\n"، ipckey)؛ /* إعداد قائمة انتظار الرسائل */ mq_id = msgget(ipckey, IPC_CREAT | 0666); printf("معرف الرسالة هو %d\n", mq_id); /* إرسال رسالة */ memset(mymsg.text, 0, 100); ، 0)؛

يستورد الكود الموجود ملفات الرأس الضرورية ثم يحدد المتغيرات للاستخدام داخل الوظيفة الرئيسية. الخطوة الأولى هي تحديد مفتاح IPC باستخدام /tmp/foo كملف مشترك والرقم 42 كمعرف. لعرض هذا الرقم، يتم استخدام printf(3c) لعرض هذا الرقم. ثم يتم إنشاء قائمة انتظار الرسائل باستخدام msgget. المعلمة الأولى لـ msgget هي مفتاح IPC، والثانية هي مجموعة من العلامات. في المثال، تتمتع العلامات بأذونات ثمانية، مما يسمح لأي شخص لديه مفتاح IPC باستخدام IPC بالكامل، بالإضافة إلى علامة IPC_CREAT، التي تستدعي msgget لإنشاء قائمة الانتظار. مرة أخرى، يتم عرض النتيجة على الشاشة.

إرسال رسالة إلى قائمة الانتظار أمر بسيط. بعد إعادة تعيين مساحة الذاكرة في الرسالة، يتم نسخ السلسلة العادية إلى جزء النص من المخزن المؤقت. يتم تحديد نوع الرسالة على أنه 1، ثم يتم استدعاء msgsnd. يتوقع msgsnd أن يتم تمرير معرف قائمة الانتظار ومؤشر للبيانات وحجم البيانات وعلامة تشير إلى ما إذا كان يجب حظر المكالمة أم لا. إذا تم تعيين علامة IPC_NOWAIT، فسيتم إرجاع المكالمة حتى إذا كانت قائمة الانتظار ممتلئة. إذا تم تعيين العلامة على 0، فسيتم حظر المكالمة حتى تكون هناك مساحة خالية في قائمة الانتظار، أو يتم حذف قائمة الانتظار، أو يتلقى التطبيق إشارة.

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

قائمة 3. رمز لاسترداد رسالة من قائمة الانتظار
#يشمل #يشمل #يشمل #يشمل #يشمل int main (void) ( key_t ipckey; int mq_id; struct ( نوع طويل; نص char; ) mymsg; int تلقى; /* إنشاء مفتاح ipc */ ipckey = ftok("/tmp/foo", 42); printf( "مفتاحي هو %d\n"، ipckey /* إعداد قائمة انتظار الرسائل */ mq_id = msgget(ipckey, 0); sizeof(mymsg), 0, 0); printf("%s (%d)\n", mymsg.text, تلقى )

يشبه إجراء الحصول على مفتاح IPC ومعرف قائمة انتظار الرسائل رمز الخادم. لا يحدد استدعاء msgget أي إشارات لأن الخادم قد قام بالفعل بإنشاء قائمة الانتظار. إذا تم تصميم التطبيقات بحيث يمكن بدء تشغيل العميل قبل الخادم، فسيتعين على كل من العميل والخادم تحديد أذونات الوصول وعلامة IPC_CREAT بحيث يقوم أي تطبيق يبدأ قبل الآخرين بإنشاء قائمة انتظار.

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

  • إذا كان النوع 0، فسيتم إرجاع الرسالة الأولى في قائمة الانتظار.
  • إذا كان النوع عددًا صحيحًا موجبًا، فسيتم إرجاع الرسالة الأولى في قائمة الانتظار التي تحتوي على هذا النوع.
  • إذا كان النوع عددًا صحيحًا سالبًا، فسيتم إرجاع الرسالة الأولى في قائمة الانتظار بأقل قيمة إما أقل من أو تساوي القيمة المطلقة للنوع الذي يتم تعريفه. على سبيل المثال، إذا تمت إضافة 2 ثم 1 إلى قائمة الانتظار، فإن استدعاء msgrcv بالنوع -2 سيعيد 1 لأنه الأصغر، على الرغم من أنه الثاني في قائمة الانتظار.

المعلمة الخامسة لـ msgrcv هي مرة أخرى علامة حظر. يوضح كيفية عمل العميل والخادم.

القائمة 4. نتائج رمز العميل والخادم
sunbox$ ./mq_server مفتاحي هو 704654099 معرف الرسالة هو 2 sunbox$ ./mq_client مفتاحي هو 704654099 معرف الرسالة هو 2 مرحبًا بالعالم! (104)

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

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

قفل الموارد باستخدام الإشارات

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

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

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

  1. sem_num: وصف للإشارة في المجموعة التي يتم التعامل معها.
  2. sem_op: عدد صحيح موقّع يحتوي على الأمر أو الاختبار المطلوب تنفيذه.
  3. sem_flg: مجموعة من علامة IPC_NOWAIT العادية، التي تشير إلى ما إذا كان سيتم تشغيل الاختبار فورًا أو حظره حتى اكتماله، وSEM_UNDO، الذي يلغي عمليات الإشارة إذا انتهت العملية قبل الأوان.

يحتوي sem_op على عدد كبير من التكوينات:

  • إذا كانت قيمة sem_op تساوي 0، فسيتم اختبار sem_num لمعرفة ما إذا كانت القيمة تساوي 0. إذا كانت قيمة sem_num تساوي 0، فسيتم إجراء الاختبار التالي. إذا لم تكن قيمة sem_num 0، فسيتم حظر العملية حتى تصبح قيمة الإشارة 0 ولا يتم تعيين IPC_NOWAIT، أو يتم تخطي الاختبارات المتبقية إذا تم تعيين IPC_NOWAIT.
  • إذا كان sem_op عددًا صحيحًا موجبًا، تتم إضافة قيمة sem_op إلى قيمة الإشارة.
  • إذا كان sem_op عددًا صحيحًا سالبًا وكانت قيمة الإشارة السالبة أكبر من أو تساوي القيمة المطلقة لـ sem_op ، فسيتم طرح القيمة المطلقة من قيمة الإشارة.
  • إذا كان sem_op عددًا صحيحًا سالبًا وكانت قيمة الإشارة أقل من القيمة المطلقة لـ sem_op، فستتوقف الاختبارات فورًا إذا كان IPC_NOWAIT صالحًا، أو يتم حظرها حتى تصبح قيمة الإشارة أكبر من القيمة المطلقة لـ sem_op.

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

القائمة 5. استخدام الإشارة لحماية القسم الحرج
#يشمل #يشمل #يشمل #يشمل /* بالنسبة إلى strerror(3c) */ #include /* للخطأ */ #include /* راند(3ج) */ #include int main (int argc, char **argv) ( key_t ipckey; int Semid; struct sembuf sem; /* sembuf محدد في sys/sem.h */ /* إنشاء مفتاح ipc */ ipckey = ftok("/tmp/ foo"، 42)؛ /* قم بإعداد مجموعة الإشارة. 4 == READ, 2 == ALTER */ Semid = semget(ipckey, 1, 0666 | IPC_CREAT); if (semid< 0) { printf("Error - %s\n", strerror(errno)); _exit(1); } /* These never change so leave them outside the loop */ sem.sem_num = 0; sem.sem_num = 0; sem.sem_flg = SEM_UNDO; /* Release semaphore on exit */ sem.sem_flg = SEM_UNDO; /* Release semaphore on exit */ while(1) { /* loop forever */ printf("[%s] Waiting for the semaphore to be released\n", argv); /* Set up two semaphore operations */ sem.sem_op = 0; /* Wait for zero */ sem.sem_op = 1; /* Add 1 to lock it*/ semop(semid, sem, 2); printf("[%s] I have the semaphore\n", argv); sleep(rand() % 3); /* Critical section, sleep for 0-2 seconds */ sem.sem_op = -1; /* Decrement to unlock */ semop(semid, sem, 1); printf("[%s] Released semaphore\n", argv); sleep(rand() % 3); /* Sleep 0-2 seconds */ } }

يُظهر الطريقة بنفس طريقة مثال قائمة انتظار الرسائل. مثلما يحدد msgget حجم قائمة انتظار الرسائل في المعلمة الثانية، يحدد semget حجم مجموعة الإشارة. مجموعة الإشارة هي مجموعة من الإشارات التي تشترك في مثيل IPC شائع. لا يمكن تغيير عدد الإشارات في المجموعة. إذا تم إنشاء مجموعة إشارة، فسيتم تجاهل المعلمة الثانية لـ semget. إذا قام semget بإرجاع عدد صحيح سالب يشير إلى الفشل، فسيتم طباعة السبب ويخرج البرنامج.

مباشرة قبل حلقة while الرئيسية، تتم تهيئة sem_num وsem_flg، حيث أنهما يظلان ثابتين خلال هذا المثال. يحدد SEM_UNDO أنه إذا خرج مالك الإشارة قبل تحرير الإشارة، فلن يتم حظر كافة التطبيقات الأخرى.

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

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

القائمة 6. برنامجان يستخدمان إشارة لحماية قسم حرج
sunbox$ ./sem_example a & ./sem_example b & [a] في انتظار تحرير الإشارة [a] لدي الإشارة [b] في انتظار تحرير الإشارة [a] تم تحرير الإشارة [b] لدي الإشارة [a] انتظار تحرير الإشارة [b] الإشارة المحررة [a] لدي الإشارة [a] الإشارة المحررة [a] انتظار تحرير الإشارة [a] لدي الإشارة

يظهر حالتين قيد التشغيل، بالاسم a وb على التوالي. أولاً يحصل "أ" على الإشارة وفي نفس الوقت يحاول "ب" الحصول على القفل. بمجرد أن يقوم a بتحرير الإشارة، يتم منح b القفل. وينقلب الوضع عندما ينتظر (أ) أن يكمل (ب) عمله. ينتهي A بإعادة تعيين الإشارة بعد قطع الاتصال بها، نظرًا لأن b ليس في حالة الانتظار.

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

الذاكرة المشتركة

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

القائمة 7. برنامج يوضح استخدام الذاكرة المشتركة
#يشمل #يشمل #يشمل #يشمل #يشمل #يشمل int main(void) ( pid_t pid; int *shared; /* مؤشر إلى shm */ int shmid; shmid = shmget(IPC_PRIVATE, sizeof(int), IPC_CREAT | 0666); if (fork() == 0) ( /* الطفل */ /* إرفاق الذاكرة المشتركة وطباعة المؤشر */ Shared = shmat(shmid, (void *) 0, 0); printf("قيمة الطفل=%d\n", *shared); / مشترك = shmat(shmid, (void *) 0, 0); printf("القيمة الأصلية=%d\n", *shared(shmid, IPC_RMID, 0 ) ؛

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

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

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

القائمة 8. مثال على إخراج الذاكرة المشتركة
sunbox$ ./shared_memory مؤشر الطفل ff390000 قيمة الطفل = 1 مؤشر الأصل ff380000 قيمة الأصل = 1 قيمة الأصل = 42 قيمة الطفل = 42

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

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

خاتمة

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

تعتبر أساليب IPC مفيدة لمطوري التطبيقات لأنها توفر طريقة قياسية للتواصل بين التطبيقات وتتوفر عبر إصدارات مختلفة من UNIX. في المرة القادمة التي تحتاج فيها إلى تأمين الموارد أو نقل البيانات بين العمليات، جرب آليات IPC الخاصة بـ SysV.

اتصال interprocess ( الاتصال بين العمليات (IPC)) عبارة عن مجموعة من الطرق لتبادل البيانات بين سلاسل العمليات. يمكن إطلاق العمليات على نفس الكمبيوتر وعلى أجهزة مختلفة متصلة عبر الشبكة. تأتي IPCs في عدة أنواع: "الإشارة"، "المقبس"، "الإشارة"، "الملف"، "الرسالة" ...

في هذه المقالة أريد أن ألقي نظرة على 3 أنواع فقط من IPC:

الاستطراد: هذه المقالة تعليمية ومخصصة للأشخاص الذين بدأوا للتو في طريق برمجة النظام. هدفها الرئيسي هو التعرف على الطرق المختلفة للتفاعل بين العمليات على نظام تشغيل متوافق مع POSIX.

الأنابيب المسماة

لنقل الرسائل، يمكنك استخدام المقابس والقنوات وD-bus وغيرها من التقنيات. يمكنك أن تقرأ عن المقابس الموجودة في كل زاوية، وتكتب مقالًا منفصلاً عن D-bus. لذلك، قررت التطرق إلى التقنيات غير المعروفة التي تلبي معايير POSIX وتقديم أمثلة عملية.

دعونا نلقي نظرة على تمرير الرسائل عبر الأنابيب المسماة. يبدو النقل بشكل تخطيطي كما يلي:

لإنشاء أنابيب مسماة سنستخدم الدالة، مكفيفو ():
#يشمل int mkfifo(const char *pathname, mode_t mode);
تقوم الوظيفة بإنشاء ملف FIFO خاص باسم اسم المسار، والمعلمة وضعيحدد أذونات الملف.

ملحوظة: وضعتستخدم بالاشتراك مع القيمة الحالية umaskبالطريقة الآتية: (وضع& ~ أوماسك). ستكون نتيجة هذه العملية قيمة جديدة umaskللملف الذي نقوم بإنشائه. لهذا السبب نستخدم 0777 ( S_IRWXO | S_IRWXG | S_IRWXU) حتى لا تتم الكتابة فوق أي جزء من القناع الحالي.
بمجرد إنشاء ملف، يمكن لأي عملية فتح الملف للقراءة أو الكتابة تمامًا كما تفتح ملفًا عاديًا. ومع ذلك، من أجل الاستخدام الصحيح للملف، من الضروري فتحه في وقت واحد من خلال عمليتين/خيطين، أحدهما لتلقي البيانات (قراءة ملف)، والآخر للإرسال (الكتابة إلى ملف).

إذا تم إنشاء ملف FIFO بنجاح، مكفيفو ()ترجع 0 (صفر). في حالة وجود أي أخطاء، تقوم الدالة بإرجاع -1 وتعيين رمز الخطأ إلى متغير خطأ.

الأخطاء النموذجية التي قد تحدث أثناء إنشاء القناة:

  • EACCES- لا توجد حقوق للتشغيل (التنفيذ) في أحد الأدلة الموجودة في المسار اسم المسار
  • موجود- ملف اسم المسارموجود بالفعل، حتى لو كان الملف عبارة عن رابط رمزي
  • لا شيء- لا يوجد دليل مذكور فيه اسم المسار، أو أن الرابط معطل
  • اينوسبك- لا توجد مساحة لإنشاء ملف جديد
  • إينوتدير- أحد الأدلة المذكورة في اسم المسار، في الواقع، فهو ليس كذلك
  • ايروفس- محاولة إنشاء ملف FIFO على نظام ملفات للقراءة فقط
تتم القراءة والكتابة في الملف الذي تم إنشاؤه باستخدام الوظائف يقرأ()و يكتب().

مثال

mkfifo.c
#يشمل #يشمل #يشمل #يشمل #define NAMEDPIPE_NAME "/tmp/my_named_pipe" #define BUFSIZE 50 int main (int argc, char ** argv) ( int fd, len; char buf; if (mkfifo(NAMEDPIPE_NAME, 0777)) ( perror("mkfifo"); return 1 ) printf("تم إنشاء %s\n"، NAMEDPIPE_NAME if ((fd = open(NAMEDPIPE_NAME, O_RDONLY)));<= 0) { perror("open"); return 1; } printf("%s is opened\n", NAMEDPIPE_NAME); do { memset(buf, "\0", BUFSIZE); if ((len = read(fd, buf, BUFSIZE-1)) <= 0) { perror("read"); close(fd); remove(NAMEDPIPE_NAME); return 0; } printf("Incomming message (%d): %s\n", len, buf); } while (1); } [скачать ]

نفتح الملف للقراءة فقط ( O_RDONLY). ويمكن استخدامها O_NONBLOCKمعدّل مصمم خصيصًا لملفات FIFO، حتى لا تنتظر فتح الملف للكتابة على الجانب الآخر. ولكن في الكود المعطى هذه الطريقة غير ملائمة.

نقوم بتجميع البرنامج ثم تشغيله:
$ gcc -o mkfifo mkfifo.c $ ./mkfifo
في النافذة الطرفية التالية نقوم بتنفيذ:
$ echo "مرحبًا، أنبوبي المُسمى!" > /tmp/my_named_pipe
ونتيجة لذلك سنرى المخرجات التالية من البرنامج:
$ ./mkfifo /tmp/my_named_pipe تم إنشاؤه /tmp/my_named_pipe مفتوح الرسالة الواردة (22): مرحبًا، أنبوبي المسمى! إقرأ : النجاح

ذكريات مشتركه

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


لتخصيص الذاكرة المشتركة سوف نستخدم وظيفة POSIX shm_open():
#يشمل int shm_open(const char *name, int oflag, mode_t mode);
تقوم الدالة بإرجاع واصف الملف المرتبط بكائن الذاكرة. يمكن استخدام هذا الواصف لاحقًا بواسطة وظائف أخرى (على سبيل المثال، مماب ()أو مبروتيك()).

يتم الحفاظ على سلامة كائن الذاكرة، بما في ذلك جميع البيانات المرتبطة به، حتى يتم فصل/حذف الكائن ( shm_unlink()). هذا يعني أن أي عملية يمكنها الوصول إلى كائن الذاكرة الخاص بنا (إذا كانت تعرف اسمه) حتى نستدعيه بشكل صريح shm_unlink().

عامل oflagهو bitwise OR من العلامات التالية:

  • O_RDONLY- مفتوح فقط مع أذونات القراءة
  • O_RDWR- فتح مع أذونات القراءة والكتابة
  • O_CREAT- إذا كان الكائن موجودًا بالفعل، فلن يكون للعلم أي تأثير. وبخلاف ذلك، يتم إنشاء الكائن وتعيين حقوق الوصول إليه وفقًا للوضع.
  • O_EXCL- سيؤدي تعيين هذه العلامة مع O_CREATE إلى قيام shm_open بإرجاع خطأ إذا كان مقطع الذاكرة المشتركة موجودًا بالفعل.
كيف يتم تعيين قيمة المعلمة وضعتم وصفه بالتفصيل في الفقرة السابقة "تمرير الرسالة".

بعد إنشاء كائن ذاكرة مشتركة، نقوم بتعيين حجم الذاكرة المشتركة عن طريق الاتصال فترونكات (). إدخال الوظيفة هو واصف الملف الخاص بالكائن والحجم الذي نحتاجه.

مثال

يوضح التعليمة البرمجية التالية إنشاء الذاكرة المشتركة وتعديلها وحذفها. ويوضح أيضًا كيف أنه بعد إنشاء الذاكرة المشتركة، يخرج البرنامج، ولكن في المرة التالية التي نبدأه فيها يمكننا الوصول إليه حتى يتم تنفيذه shm_unlink().
shm_open.c
#يشمل #يشمل #يشمل #يشمل #يشمل #يشمل #define SHARED_MEMORY_OBJECT_NAME "my_shared_memory" #define SHARED_MEMORY_OBJECT_SIZE 50 #define SHM_CREATE 1 #define SHM_PRINT 3 #define SHM_CLOSE 4 void use(const char * s) ( printf("Usage: %s" ["text"]\n"، s); ) int main (int argc, char ** argv) ( int shm, len, cmd, mode = 0; char *addr; if (argc< 2) { usage(argv); return 1; } if ((!strcmp(argv, "create") || !strcmp(argv, "write")) && (argc == 3)) { len = strlen(argv); len = (len<=SHARED_MEMORY_OBJECT_SIZE)?len:SHARED_MEMORY_OBJECT_SIZE; mode = O_CREAT; cmd = SHM_CREATE; } else if (! strcmp(argv, "print")) { cmd = SHM_PRINT; } else if (! strcmp(argv, "unlink")) { cmd = SHM_CLOSE; } else { usage(argv); return 1; } if ((shm = shm_open(SHARED_MEMORY_OBJECT_NAME, mode|O_RDWR, 0777)) == -1) { perror("shm_open"); return 1; } if (cmd == SHM_CREATE) { if (ftruncate(shm, SHARED_MEMORY_OBJECT_SIZE+1) == -1) { perror("ftruncate"); return 1; } } addr = mmap(0, SHARED_MEMORY_OBJECT_SIZE+1, PROT_WRITE|PROT_READ, MAP_SHARED, shm, 0); if (addr == (char*)-1) { perror("mmap"); return 1; } switch (cmd) { case SHM_CREATE: memcpy(addr, argv, len); addr = "\0"; printf("Shared memory filled in. You may run "%s print" to see value.\n", argv); break; case SHM_PRINT: printf("Got from shared memory: %s\n", addr); break; } munmap(addr, SHARED_MEMORY_OBJECT_SIZE); close(shm); if (cmd == SHM_CLOSE) { shm_unlink(SHARED_MEMORY_OBJECT_NAME); } return 0; } [скачать ]

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

هذه المرة تحتاج إلى تجميع التعليمات البرمجية مع الخيار -lrt:
$ دول مجلس التعاون الخليجي -o shm_open -lrt shm_open.c
دعونا نرى ما حدث:
$ ./shm_open قم بإنشاء "مرحبًا، ذاكرتي المشتركة!" الذاكرة المشتركة مملوءة. يمكنك تشغيل "./shm_open print" لمعرفة القيمة. $ ./shm_open print حصلت عليها من الذاكرة المشتركة: مرحبًا، ذاكرتي المشتركة! $ ./shm_open قم بإنشاء "مرحبًا!" الذاكرة المشتركة مملوءة. يمكنك تشغيل "./shm_open print" لمعرفة القيمة. $ ./shm_open print تم الحصول عليها من الذاكرة المشتركة: مرحبًا! $ ./shm_open أغلق $ ./shm_open طباعة shm_open: لا يوجد مثل هذا الملف أو الدليل
في برنامجنا، نستخدم الوسيطة "إنشاء" لإنشاء ذاكرة مشتركة ولتغيير محتوياتها.

بمعرفة اسم كائن الذاكرة، يمكننا تغيير محتويات الذاكرة المشتركة. ولكن يجب أن ندعو shm_unlink()كيف تتوقف الذاكرة عن الوصول إلينا و shm_open()بدون معلمة O_CREATEإرجاع الخطأ "لا يوجد مثل هذا الملف أو الدليل".

إشارة

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

هناك نوعان من الإشارات:

  1. إشارة مع عداد (يحسب الإشارة)، والذي يحدد حد الموارد للعمليات التي تصل إليها
  2. الإشارة الثنائية، التي تحتوي على حالتين "0" أو "1" (في أغلب الأحيان: "مشغول" أو "غير مشغول")
دعونا نلقي نظرة على كلا النوعين من الإشارات.

إشارة مع العداد

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

لذلك، لتنفيذ الإشارات سوف نستخدم وظيفة POSIX sem_open():
#يشمل sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
إلى وظيفة إنشاء الإشارة، نقوم بتمرير اسم الإشارة، التي تم إنشاؤها وفقًا لقواعد معينة، وأعلام التحكم. بهذه الطريقة نحصل على إشارة مسماة.
يتم إنشاء اسم الإشارة على النحو التالي: في البداية يوجد حرف "/" (شرطة مائلة)، متبوعًا بأحرف لاتينية. لا ينبغي استخدام رمز الشرطة المائلة بعد الآن. يمكن أن يصل طول اسم الإشارة إلى 251 حرفًا.

إذا كنا بحاجة إلى إنشاء إشارة، فسيتم تمرير إشارة التحكم O_CREATE. للبدء في استخدام إشارة موجودة بالفعل، إذن oflagيساوي الصفر. إذا جنبا إلى جنب مع العلم O_CREATEتمرير العلم O_EXCL، ثم الدالة sem_open()سيُرجع خطأ إذا كانت الإشارة بالاسم المحدد موجودة بالفعل.

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

لفتح إشارة موجودة بسرعة، نستخدم البناء التالي:
#يشمل sem_t *sem_open(const char *name, int oflag); ، حيث يتم تحديد اسم الإشارة وعلامة التحكم فقط.

مثال على إشارة مع عداد

دعونا نلقي نظرة على مثال لاستخدام الإشارة لمزامنة العمليات. في مثالنا، تعمل إحدى العمليات على زيادة قيمة الإشارة المرورية وتنتظر العملية الثانية لإعادة ضبطها لمواصلة التنفيذ.
sem_open.c
#يشمل #يشمل #يشمل #يشمل #define SEMAPHORE_NAME "/my_named_semaphore" int main(int argc, char ** argv) ( sem_t *sem; if (argc == 2) ( printf("Dropping semaphore...\n"); if ((sem = sem_open (SEMAPHORE_NAME, 0)) == SEM_FAILED) ( perror("sem_open"); return 1; ) sem_post(sem); printf("تم إسقاط الإشارة.\n" ) if ((sem = sem_open(SEMAPHORE_NAME, O_CREAT, 0777, 0)) == SEM_FAILED) ( perror("sem_open"); return 1; ) printf("تم أخذ الإشارة.\nفي انتظار إسقاطها.\n") ;< 0) perror("sem_wait"); if (sem_close(sem) < 0) perror("sem_close"); return 0; } [скачать ]

في وحدة تحكم واحدة نقوم بتشغيل:
$ ./sem_open تم أخذ الإشارة. في انتظار أن يتم إسقاطها.<-- здесь процесс в ожидании другого процесса sem_wait: Success sem_close: Success
في وحدة التحكم التالية نقوم بتشغيل:
$ ./sem_open 1 إسقاط الإشارة... sem_post: إسقاط إشارة النجاح.

إشارة ثنائية

بدلاً من الإشارة الثنائية، التي تُستخدم فيها الدالة sem_open أيضًا، سأفكر في إشارة أكثر استخدامًا تسمى كائن المزامنة (mutex).

كائن المزامنة (mutex) هو في الأساس نفس الشيء مثل الإشارة الثنائية (أي إشارة ذات حالتين: "مشغول" و"غير مشغول"). لكن المصطلح "mutex" يستخدم غالبًا لوصف مخطط يمنع عمليتين من مشاركة البيانات/المتغيرات في نفس الوقت. بينما يُستخدم مصطلح "الإشارة الثنائية" في أغلب الأحيان لوصف البنية التي تقيد الوصول إلى مورد واحد. وهذا يعني أنه يتم استخدام إشارة ثنائية حيث "تحتل" عملية واحدة إشارة، ثم "تحررها" عملية أخرى. بينما يتم تحرير كائن المزامنة (mutex) بنفس العملية/الخيط الذي يشغله.

لا يمكنك الاستغناء عن كائن المزامنة (mutex) عند كتابة قاعدة بيانات، على سبيل المثال، يمكن للعديد من العملاء الوصول إليها.

لاستخدام كائن المزامنة (mutex)، تحتاج إلى استدعاء الدالة pthread_mutex_init():
#يشمل Int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
تقوم الوظيفة بتهيئة كائن المزامنة (variable كائن المزامنة) يصف mutexattr. لو mutexattrيساوي باطل، ثم تتم تهيئة كائن المزامنة (mutex) إلى القيمة الافتراضية. إذا تم تنفيذ الوظيفة بنجاح (رمز الإرجاع 0)، فسيتم اعتبار كائن المزامنة (mutex) مهيأ و"مجانيًا".

الأخطاء النموذجية التي قد تحدث:

  • مرة أخرى- لا توجد موارد ضرورية كافية (باستثناء الذاكرة) لتهيئة كائن المزامنة (mutex).
  • ENOMEM- الذاكرة غير كافية
  • إيبيرم- لا يوجد حقوق لإجراء العملية
  • إيبوسي- محاولة تهيئة كائن المزامنة (mutex) الذي تمت تهيئته بالفعل ولكن لم يتم تدميره
  • إينفال- معنى mutexattrغير صالح
لاحتلال كائن المزامنة أو تحريره، نستخدم الوظائف:
int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex);
وظيفة pthread_mutex_lock()، لو كائن المزامنةلم يحتلها بعد، يحتلها، ويصبح مالكها ويغادر على الفور. إذا كان كائن المزامنة (mutex) مشغولاً، فإنه يمنع تنفيذ العملية وينتظر حتى يتم تحرير كائن المزامنة (mutex).
وظيفة pthread_mutex_trylock()متطابقة في السلوك مع الوظيفة pthread_mutex_lock()مع استثناء واحد - لا يمنع العملية إذا كائن المزامنةمشغول ولكن يعود إيبوسيشفرة.
وظيفة pthread_mutex_unlock()يطلق كائن المزامنة المزدحم.

رموز العودة ل pthread_mutex_lock():

  • EINVAL - لم تتم تهيئة كائن المزامنة (mutex) بشكل صحيح
  • EDEADLK - كائن المزامنة مشغول بالفعل بالعملية الحالية
رموز العودة ل pthread_mutex_trylock():
  • EBUSY - كائن المزامنة (mutex) مشغول بالفعل
رموز العودة ل pthread_mutex_unlock():
  • EINVAL - لم تتم تهيئة كائن المزامنة بشكل صحيح
  • EPERM - لا تمتلك عملية الاستدعاء كائن المزامنة (mutex).

مثال كائن المزامنة

mutex.c
#يشمل #يشمل #يشمل #يشمل عداد كثافة العمليات ثابت؛ // المورد المشترك static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; void incr_counter(void *p) ( do ( usleep(10); // دعنا نحصل على شريحة زمنية بين أقفال كائن المزامنة pthread_mutex_lock(&mutex); counter++; printf("%d\n", counter); Sleep(1) ; اضغط على "Enter" لتهيئة العداد بقيمة جديدة في أي وقت.\n"); Sleep(3); pthread_mutex_unlock(&mutex); // قم بإلغاء حظر كائن المزامنة المحظور حتى يعمل مؤشر ترابط آخر ( if (gets(buf) != buf) return // لا توجد حماية من الخداع! خطر التجاوز! num = atoi(buf); قفل كائن المزامنة باستخدام pthread_mutex_lock().\n"); pthread_mutex_lock(&mutex); else if (rc == 0) ( printf("واو! أنت في الوقت المحدد! تهانينا!\n"); ) else ( printf ("خطأ" : %d\n"، rc)؛ return; counter = num; printf("القيمة الجديدة للعداد هي %d\n"، counter); pthread_mutex_unlock(&mutex); ) بينما (1)؛ ) int main(int argc, char ** argv) ( pthread_t thread_1; pthread_t thread_2; counter = 0; pthread_create(&thread_1, NULL, (void *)&incr_counter, NULL); pthread_create(&thread_2, NULL, (void *)&reset_counter, NULL);

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

إذا لم نستخدم تقنية كائن المزامنة (mutex)، فلن نعرف القيمة التي ستكون في المتغير العام مع الوصول المتزامن بواسطة خيطين. أيضا أثناء بدء التشغيل الفرق بين pthread_mutex_lock()و pthread_mutex_trylock().

تحتاج إلى ترجمة التعليمات البرمجية باستخدام معلمة إضافية -lpthread:
$ دول مجلس التعاون الخليجي -o mutex -lpthread mutex.c
نبدأ ونغير قيمة المتغير ببساطة عن طريق إدخال قيمة جديدة في النافذة الطرفية:
$ ./mutex أدخل الرقم واضغط على "Enter" لتهيئة العداد بقيمة جديدة في أي وقت. 1 2 3 30 <--- новое значение переменной Mutex is already locked by another process. Let"s lock mutex using pthread_mutex_lock(). New value for counter is 30 31 32 33 1 <--- новое значение переменной Mutex is already locked by another process. Let"s lock mutex using pthread_mutex_lock(). New value for counter is 1 2 3

بدلا من الاستنتاج

في المقالات التالية أريد أن ألقي نظرة على تقنيات d-bus وRPC. اذا كنت مهتما اعلمني بذالك.
شكرًا لك.

محدث:تم تحديث الفصل الثالث حول الإشارات. تمت إضافة فصل فرعي حول كائن المزامنة (mutex).

العلامات:

  • لينكس
  • posix
  • ipc
  • برمجة
اضف اشارة