Лекции Системное ПО. Лекция Структура и основные компоненты вычислительной системы
Скачать 0.71 Mb.
|
Лекция 18 Итак, мы к текущему моменту разобрали два механизма взаимодействия процессов в системе IPC: разделяемую память и механизм сообщений. Давайте попробуем написать программу, в которой первый процесс будет принимать некую текстовую строку и в случае, если некоторая строка начинается с буквы “a”, то эта текстовая строка будет передана процессу A, если “b” — процессу B, если “q”, то сообщение будет передано процессам A и B, и будет осуществлен выход Основной процесс: #include #include #include #include struct { long mtype; char Data[256]; } Message; int main(void) { key_t key; int msgid; char str[256]; key = ftok(“/usr/mash”,’S’); /* создаем ключ для работы с ресурсом — ключ уникальный и однозначно определяет доступ к разделяемому ресурсу одного типа, то есть с одним ключом могут быть связаны разделяемые ресурсы памяти, очередь сообщений и семафоров, но две области памяти связаны одним ключом быть не могут */ msgid = msgget(key, 0666 | IPC_CREAT); /* создаем очередь сообщений, 0666 — права доступа к очереди, разрешают всем читать и писать */ for (;;) { gets(str); /* получаем строку */ strcpy(Message.Data, str); /* копируем ее в буфер сообщения */ switch(str[0]) { case ‘a’: case ‘A’: Message.mtype = 1; /* если строка начинается с “a”, то ставим тип сообщения равным единице, это означает, что приемником будет первый процесс */ msgsnd(msgid, (struct msgbuf *) &Message), 1+strlen(str),0); /* отправляем сообщение */ break; case ‘b’: case ‘B’: Message.mtype = 2; /* посылаем сообщение второму процессу */ msgsnd(msgid, (struct msgbuf *) &Message), 1+strlen(str),0); break; case ‘q’: case ‘Q’: Message.mtype = 1; msgsnd(msgid, (struct msgbuf *) &Message), 1+strlen(str),0); Message.mtype = 2; msgsnd(msgid, (struct msgbuf *) &Message), 1+strlen(str),0); sleep(10); /* берем таймаут для гарантии, что все предыдущие сообщения дошли */ msgctl(msgid, IPC_RMID, NULL); /* убиваем разделяемый ресурс */ exit(0); /* завершаем процесс */ } } Давайте рассмотрим процесс-приемник. Рассмотрим только процесс A, так как B будет аналогичен, за исключением указания типа сообщений для приема. proc_A: ... int main(void) { key_t key; int msgid; key = ftok(“/usr/mash”, ‘S’); /* получаем ключ к очереди по параметрам, аналогичным процессу-отправителю */ msgid = msgget(key, 0666 | IPC_CREAT); /* создаем или подключаемся к очереди сообщений */ for (;;) {msgrcv(msgid, (struct msgbuf *) (&Message), 256, 1, 0); /* принимаем сообщения с типом 1 */ if (Message.Data[0]==’q’) || (Message.Data[0]==’Q’) break; /* если сообщение начинается с “q” — заканчиваем выполнение процесса-получателя */ printf(“...”); } exit(0); } Семафоры. С точки зрения тех проблем, с которыми мы знакомимся — семафоры — законное и существующее понятие. Впервые их ввел достаточно известный ученый Дейкстра. Суть этого объекта заключается в следующем. Семафор — это есть некоторый объект, который имеет целочисленное значениеs, и две операции — P(s) и V(s). P — уменьшает значение семафора на единичку и, если s>=0 после уменьшения процесс продолжает работать, если s<0, то процесс будет приостановлен и встанет в очередь ожидания, связанную с семафором s. Операция V увеличивает семафор на единицу. Если s>0 после увеличения, то процесс продолжает свое выполнение, если s<=0, то разблокируется один из процессов в очереди ожидания. Считается, что операции P и V неделимы, то есть их выполнение не может прерываться. Также бывают двоичные семафоры, максимальное значение которого — 1. При значении 1 считается, что ни один из процессов не находится в критическом участке. При равенстве 0 — один процесс находится в критическом участке, другой работает нормально. Значение “-1” означает, чтоодин семафор находится в очереди ожидания, а другой — в критическом участке. Двоичные семафоры наиболее часто находили практическое применение в аппаратных реализациях. Кроме тех вычислительных машин, которые являются однопроцессорными, бывают и многомашинные, многопроцессорные комплексы, для этих комплексов необходимо внесение в систему команд поддержки семафоров. Это мы рассмотрели семафоры в общем случае. Сейчас же рассмотрим семафоры в системе IPC. Существует разделяемый ресурс массив семафоров. Система позволяет процессам, работающим с этим ресурсом изменять элементы массива на произвольное число. Система позволяет ожидание процессом обнуления одного или нескольких семафоров. И, наконец, система позволяет уменьшать значение семафоров. int semget(key_t key, int n, int flags) Данная функция создает массив размерности n семафоров с заданным ключом и флагами. Функция возвращает идентификатор ресурса или -1, если произошла ошибка. int semop(int semid, struct sembuf *SOPS, int n) semid — идентификатор ресурса семафоров; SOPS — указатель на структуру sembuf; n — количество указателей на эту структуру, которые передаются функции semop, соответственно в структуре sembuf передается вся информация о необходимом действии; struct sembuf {short sem_num; /* номер семафора в массиве семафоров */ short sem_op; /* код операции над семафором */ short sem_flg; /* флаги */ } Все это интерпретируется следующим образом. Пусть значение семафора с номером sem_num есть число sem_val. Если значение операции semop не равно нулю, то оценивается значение сумма sem_val+semop, если эта сумма больше или равна нулю, то значение данного семафора устанавливается новым, равным сумме предыдущего значения плюс код операции (semop), если эта сумма меньше нуля, то действие процесс будет приостановлено до наступления одного из следующих событий: до тех пор, пока значение этой суммы не станет >=0; придет сигнал (при приходе сигнала процесс снимется с ожидание) при этом semop будет равно “-1”. Если же semop=0, то процесс будет ожидать обнуления значения семафора, при этом, если мы обратились к функции semop c нулевым кодом операции, а значение семафора уже было нуль, то ничего не произойдет. Если значение флага равно нулю, то флаги не используются. Флагов на самом деле нет, но, например, есть флаг IPC_NOWAIT, когда процесс ничего ждать не будет. Заметим, что мы можем передать n структур и выполнить действия с n семафорами. Управление массивом семафоров: int semctl(int semid, int n, int cmd, union semun arg); semid — идентификатор ресурса n — номер семафора cmd — команда (команды над семафорами, в том числе IPC_RMID) arg — объединение, содержащее информацию о семафорах. Лекция 19 Мы остановились на средствах синхронизации доступа к разделяемым ресурсам — на семафорах. Мы говорили о том, что семафоры — это тот формализм, который изначально был предложен ученым в области компьютерный наук Дейкстрой, поэтому часто в литературе их называют семафорами Дейкстры. Семафор — это есть некоторая переменная и над ней определены операции P и V. Одна позволяет увеличивать значение семафора, другая — уменьшать. Причем с этими изменениями связаны возможности блокировки процесса и разблокировки процесса. Обратим внимание, что речь идет о неразделяемых операциях, то есть тех операциях, которые не могут быть прерваны, если начались. То есть не может быть так, чтобы во время выполнения P или V пришло прерывание, и система передала управление другому процессу. Это принципиально. Поэтому семафоры можно реализовывать программно, но при этом мы должны понимать, что эта реализация не совсем корректна, так как программа пишется человеком, а прерывается аппаратурой, отсюда возможно нарушение неразделяемости; в развитых вычислительных системах, которые поддерживают многопроцессорную обработку или обработку разделяемых ресурсов в рамках одного процесса, предусмотрены семафорные команды, которые фактически реализовывают операции P и V. Это важно. Мы говорили о реализации семафоров в Unix в системе IPC и о том, что эта система позволяет создать разделяемый ресурс “массив семафоров”, соответственно, как и к любому разделяемому ресурсу, к этому массиву может быть обеспечен доступ со стороны различных процессов, обладающих нужными правами и ключом к данному ресурсу. Каждый элемент массива — семафор. Для управления работой семафора есть функции: semop, которая позволяет реализовывать операции P и V над одним или несколькими семафорами; segctl — управление ресурсом. Под управлением здесь понимается три вещи: - получение информации о состоянии семафора; - возможность создания некоторого режима работы семафора, уничтожение семафора; - изменение значения семафора (под изменением значения здесь понимается установление начальных значений, чтобы использовать в дальнейшем семафоры, как семафоры, а не ящички для передачи значений, другие изменения — только с помощью semop); Давайте приведем пример, которым попытаемся проиллюстрировать использование семафоров на практике. Наша программа будет оперировать с разделяемой памятью. 1 процесс — создает ресурсы “разделяемая память” и “семафоры”, далее он начинает принимать строки со стандартного ввода и записывает их в разделяемую память. 2 процесс — читает строки из разделяемой памяти. Таким образом мы имеем критический участок в момент, когда один процесс еще не дописал строку, а другой ее уже читает. Поэтому следует установить некоторые синхронизации и задержки. Следует отметить, что, как и все программы, которые мы приводим, эта программа не совершенна. Но не потому, что мы не можем ее написать (в крайнем случае можно попросить своих аспирантов или студентов), а потому, что совершенная программа будет занимать слишком много места, и мы сознательно делаем некоторые упрощения. Об этих упрощениях мы постараемся упоминать. 1й процесс: #include #include #include #include int main(void) { key_t key; int semid, shmid; struct sembuf sops; char *shmaddr; char str[256]; key = ftok(“/usr/mash/exmpl”,’S’); /* создаем уникальный ключ */ semid = semget(key,1,0666 | IPC_CREAT); /* создаем один семафор с определенными правами доступа */ shmid = shmget(key,256, 0666 | IPC_CREAT); /*создаем разделяемую память на 256 элементов */ shmaddr = shmat(shmid, NULL, 0); /* подключаемся к разделу памяти, в shaddr — указатель на буфер с разделяемой памятью*/ semctl(semid,0,IPC_SET, (union semun) 0); /*инициализируем семафор со значением 0 */ sops.sem_num = 0; sops.sem_flg = 0; /* запуск бесконечного цикла */ while(1) { printf(“Введите строку:”); if ((str = gets(str)) == NULL) break; sops.sem_op=0; /* ожидание обнуления */ semop(semid, &sops, 1); /* семафора */ strcpy(shmaddr, str); /* копируем строку в разд. память */ sops.sem_op=3; /* увеличение семафора на 3 */ semop(semid, &sops, 1); } shmaddr[0]=’Q’; /* укажем 2ому процессу на то, */ sops.sem_op=3; /* что пора завершаться */ semop(semid, &sops, 1); sops.sem_op = 0; /* ждем, пока обнулится семафор */ semop(semid, &sops, 1); shmdt(shmaddr); /* отключаемся от разд. памяти */ semctl(semid, 0, IPC_RMID, (union semun) 0); /* убиваем семафор */ shmctl(shmid, IPC_RMID, NULL); /* уничтожаем разделяемую память */ exit(0); } 2й процесс: /* здесь нам надо корректно определить существование ресурса, если он есть — подключиться, если нет — сделать что-то еще, но как раз этого мы делать не будем*/ #include #include #include #include int main(void) { key_t key; int semid; struct sembuf sops; char *shmaddr; char st=0; /* далее аналогично предыдущему процессу — инициализации ресурсов */ semid = semget(key,1,0666 | IPC_CREAT); shmid = shmget(key,256, 0666 | IPC_CREAT); shmaddr = shmat(shmid, NULL, 0); sops.sem_num = 0; sops.sem_flg = 0; /* запускаем цикл */ while(st!=’Q’) { printf(“Ждем открытия семафора \n”); /* ожидание положительного значения семафора */ sops.sem_op=-2; semop(semid, &sops, 1); /* будем ожидать, пока “значение семафора”+”значение sem_op” не перевалит за 0, то есть если придет “3”, то “3-2=1” */ /* теперь значение семафора равно 1 */ st = shmaddr[0]; {/*критическая секция — работа с разделяемой памятью — в этот момент первый процесс к разделяемой памяти доступа не имеет*/} /*после работы — закроем семафор*/ sem.sem_op=-1; semop(semid, &sops, 1); /* вернувшись в начало цикла мы опять будем ждать, пока значение семафора не станет больше нуля */ } shmdt(shmaddr); /* освобождаем разделяемую память и выходим */ exit(0); } Это программа, состоящая из двух процессов, синхронно работающих с разделяемой памятью. Понятно, что при наличии интереса можно работать и с сообщениями. На этом мы заканчиваем большую и достаточно важную тему организации взаимодействия процессов в системе. Наша самоцель — не изучение тех средств, которые предоставляет Unix, а изучение принципов, которые предоставляет система для решения тех или иных задач, так как другие ОС предоставляют аналогичные или похожие средства управления процессами. Системы программирования. Мы с вами в начале курса говорили о системах программирования. Определение: Комплекс программных средств, обеспечивающих поддержку технологий проектирования, кодирования, тестирования и отладки, называется системой программирования Проектирование. Было сказано, что на сегодняшний день достаточно сложно, а практически невозможно создавать программное обеспечение без этапа проектирования, такого же долгого, нудного и детального периода, который проходит во время проектирования любого технического объекта. Следует понять, что те программы, которые пишутся студентами в качестве практических и дипломных задач не являются по сути дела программами — это игрушки, так как их сложность невелика, объемы незначительны и такого рода программы можно писать слегка. Реальные же программы так не создаются, так же, как и не создаются сложные технические объекты. Никто никогда не может себе представить, чтобы какая-нибудь авиационная компания продекларировала создание нового самолета и дала команду своим заводам слепить лайнер с такими-то параметрами. Так не бывает. Каждый из элементов такого объекта, как самолет, проходит сложный этап проектирования. Например, фирма Боинг подняла в воздух самолет “Боинг-777”, замечательность этого факта заключается в том, что самолет взлетел без предварительной продувки в аэродинамической трубе. Это означает, что весь самолет был спроектирован и промоделирован на программных моделях, и это проектирование и моделирование было настолько четким и правильным, что позволило сразу же поднять самолет в воздух. Для справки — продувка самолета в аэродинамической трубе стоит сумасшедшие деньги. Примерно та же ситуация происходит при создании сложных современных программных систем. В начале 80х гг была начата СОИ (стратегическая оборонная инициатива), ее идея была в том, чтобы создать сложной технической системы, которая бы в автоматическом режиме установила контроль за пусковыми установками СССР и стран Варшавского блока, и в случае фиксации старта с наших позиций какого-то непродекларированного объекта автоматически начиналась война. То есть запускалисьбы средства уничтожения данного объекта и средства для ответных действий. Реально тот департамент вооруженных сил, который занимался этим проектом, испытал ряд кризисов в связи с тем, что ведущие специалисты в области программного обеспечения отказывалисьучаствовать в реализации этого проекта из-за невозможности корректно его спроектировать, потому что система обладала гигантским потоком входных данных, на основе которых должны были быть приняты однозначные решения, ответственность за которые оценить весьма сложно. На самом деле эта проблема подтолкнула к развитию с одной стороны — языков программирования, которые обладали надежностью, в частности, язык Ада, одной из целью которого было создание безошибочного ПО. В таких языках накладывались ограничения наместа, где наиболее вероятно возникновение ошибки (межмодульные интерфейсы; выражения, где присутствуют разные типы данных и т.п.) Заметим, что язык C не удовлетворяет требованиям безопасности. С другой стороны — к детальному проектированию, которое бы позволяло некоторым формальным образом описывать создаваемый проект и работать с проектом в части его детализации. Причем, переход от детализации к кодированию не имел бы четкой границы. Понятно, что это есть некоторая задача не сегодняшнего, а завтрашнего дня, но реально разработчики программ находятся на пути создания таких средств, которые позволили бы совместить проектирование и кодирование. Сегодняшние системы программирования, которые строятся на объектно ориентированном подходе, частично решают эту проблему. Следующая проблема проектирования. Мы продкларировали модули, объявили их взаимосвязи, как-то описали семантику модулей (это тоже проблема). Но никто не даст гарантии, что этот проект правилен. Для решения этой проблемы используется моделирование программных систем. То есть, когда вместе с построением проекта, который декларирует все интерфейсы, функциональность и прочее, мы можем каким-то образом промоделировать работу всей или частей создаваемой системы. Реально при создании больших программных систем на сегодняшний день нет единых инструментариев для таких действий. Каждые из существующих систем имеют разные подходы. Иногда эти подходы (как и у нас, так и за рубежом) достаточно архаичны. Но тем не менее следует понимать, что период проектирования есть очень важный момент. Кодирование. Если составлен нормальный проект, то с кодированием проблем нет. Но следует обратить внимание на то, что специалист в программировании это не тот, кто быстро пишет на С, а тот, кто хорошо и подробно сможет спроектировать задачу. При современном развитии инструментальных средств закодировать сможет любой школьник, а спроектировать систему — это и есть профессиональная задача людей, занимающихся программированием — выбрать инструментальные средства, составить проект, промоделировать решение. Основной компонент системы кодирования — язык программирования. В голове каждого программиста лежит иерархия языков программирования — от машинного кода и ассемблера до универсальных языков программирования (FORTRAN, Algol, Pascal, C и т.д.), специализированных языков (SQL, HTML, Java и т.д.) Мы имеем ЯП и программу, которая написана в нотации этого языка. Система программирования обеспечивает перевод исходной программы в объектный язык. Этот процесс перевода называется трансляцией. Объектный язык может быть как некоторым языком программирования высокого уровня (трансляция), так и машинный язык (компиляция). Мы можем говорить о трансляторах-компиляторах и трансляторах-интерпретаторах. Компилятор — это транслятор, переводящий текст программы в машинный код. Интерпретатор — это транслятор, который обычно совмещает процесс перевода и выполнения программы (компилятор сначала переводит программу, а только затем ее можно выполнить). Он, грубо говоря, выполняет каждую строчку, при этом машинный код не генерируется, а происходит обращение к некоторой стандартной библиотеке программ интерпретатора. Если результат работы компилятора — код программы на машинном языке, то результат работы транслятора — последовательность обращений к функциям интерпретации. При этом, также как и при компиляции, когда создается оттранслированная программа, у нас тоже может быть создана программа, но в этом интепретируемом коде (последовательности обращений к функциям интерпретации). Понятна разница — компиляторы более эффективны, так как в интерпретаторах невозможна оптимизация и постоянные вызовы функций также не эффективны. Но интерпретаторы более удобны за счет того, что при интерпретации возможно включать в функции интерпретации множество сервисных средств: отладки, возможность интеграции интерпретатора и языкового редактора (компиляция это делать не позволяет). На сегодняшний день каждый из методов — и компиляция и интерпретация занимают свои определенные ниши. |