Занятия на 26.10.2016 года
ПРОГРАММИРОВАНИЕ НА ЯЗЫКЕ «СИ»
В ОПЕРАЦИОННОЙ СРЕДЕ «UNIX»
Методические указания к лабораторной работе по дисциплине
«Операционные системы»
Рассматриваются вопросы программирования на языке «Си» в операционной системе UNIX. Излагаемый материал является общим для всех разновидностей UNIX – систем. Представлена информация о языке «Си». Приведены задания лабораторных работ и примеры их выполнения.
ВВЕДЕНИЕ
В данном методическом указании рассматриваются основы программирования на языке «Си», а также его особенности в среде «UNIX». Представлена информация об основных командах языка «Си» и некоторые теоретические сведения. Приведены задания лабораторных работ и пример их выполнения.
ЦЕЛЬ ЛАБОРАТОРНОЙ РАБОТЫ
Цель данной лабораторной работы заключается в приобретении навыков программирования на языке «Си» под операционную систему UNIX. Также эта работа является подготовительной для дальнейшего обучения по курсу «операционные системы».
ТЕОРЕТИЧЕСКИЕ СВЕДЕНИЯ
КРАТКИЕ СВЕДЕНИЯ ОБ ОПЕРАЦИОННОЙ СИСТЕМЕ UNIX
Операционные системы играют огромную роль в эксплуатации современных ЭВМ. ЭВМ представляют основные ресурсы для решения задачи. Однако, чтобы сделать эти ресурсы более доступными для программиста, требуется детально разработанная программа, называемая операционной системой (ОС). ОС обычно предоставляет средства для разработки и запуска программ на ЭВМ, средства управления пространством памяти ЭВМ, средства доступа к периферийным устройствам ЭВМ и к некоторой файловой системе. Кроме перечисленных выше средств, более сложные ОС могут предоставлять и другие возможности, делающие ЭВМ еще более удобной для использования: одновременный доступ к ЭВМ множества пользователей (разделение времени), средства связи между пользователями, одновременное выполнение множества различных программ (мультизадачность), средства разработки и поддержки файлов, средства, облегчающие разработку программного обеспечения, средства для ввода и обработки текстов и средства защиты.
В последние годы широкую известность получила операционная система UNIX, разработанная в начале 70-х годов К. Томпсоном и Р. Ритчи в Bell Telephone Laboratories (США) и первоначально предназначенная для проведения исследовательских работ. Однако, концептуальная целостность системы и целый ряд новых, нетрадиционных и прогрессивных решений, заложенных в нее при создании, показали преимущество ОС UNIX перед другими ОС этого класса, основные из которых следующие:
- структурированная многоуровневая файловая система;
- средства разделения времени;
- возможность для любого пользователя выполнять одновременно более одной программы;
- наличие механизмов, позволяющих одной программе передавать ее результаты в другую программу (необязательное использование дополнительного пространства памяти);
- возможность перевода выдачи результатов работы с одного периферийного устройства на другое;
- встроенный командный интерпретатор и язык shell;
- структурированный язык для системного программирования;
- расширенные средства для ввода, внесения изменений и обработки информации и др.
В нашей стране аналогом ОС UNIX являлась ОС ДЕМОС (ИНМОС). Сначала эта система была ориентирована на ЭВМ класса PDP-11 (СМ ЭВМ, Электроника), а затем перенесена и сферу ЕС ЭВМ, и на персональные компьютеры.
В настоящее время на принципах ОС UNIX разработано достаточно большое количество ОС: XENIX, UNIX HP, UNIX SCO, SOLARIS, LINUX, KONIX, FREE BSD и др. Поэтому в дальнейшем под системой UNIX будут подразумеваться любые UNIX-подобные системы.
Все работы в системе UNIX представлены множеством конкурирующих процессов. Процесс - потребитель ресурсов, единица работы и управления. Логически, каждый процесс выполняется своим виртуальным процессором в своем виртуальном адресном пространстве. Каждый процесс имеет уникальный номер, называемый идентификатором процесса, который ставится процессу в соответствие при его рождении. Любые обращения к процессу возможны только через его идентификатор.
Работа процесса состоит в выполнении некоторого файла - программы, состоящей из трех сегментов: процедурного сегмента, сегмента данных и динамического сегмента. Процедурный сегмент содержит машинные команды и константы, сегмент данных - данные, которые могут изменяться в процессе работы, а динамический сегмент выделяется при загрузке выполняемого файла в основную память и содержит данные, не инициализируемые при компиляции. Логическое взаимодействие между процессами реализуется через механизм сигналов.
В UNIX реализован интерфейс для обмена информацией между процессами с дисковыми файлами и внешними устройствами. Это существенно упрощает работу с файлами на пользовательском уровне. Работа с любым внешним устройством с точки зрения пользователя рассматривается как работа с обычным файлом. При этом обмен информацией с любыми файлами осуществляется посредством одних и тех же операций ввода-вывода.
Обмен информацией между родственными процессами может выполняться как традиционными методами, через общие файлы, так и с помощью специального средства - программного канала.
Исключительно большую роль в UNIX играет файловая система. Она имеет многоуровневую структуру, в которой каждый уровень определяется файлом-каталогом. Такую структуру, с некоторыми оговорками, можно назвать древовидной. Помимо файлов-каталогов в состав файловой системы входят обычные и специальные файлы. Любой файл файловой системы может быть указан его полным именем, в котором перечисляются все каталоги, начиная с корневого, через которые проходит путь к данному файлу. Помимо этого, файл может указываться именем относительно текущего каталога. Текущим называется каталог, в котором в данный момент находится пользователь и, который обычно указывает файлы этого пользователя или группы пользователей.
Файловая система обеспечивает защиту файлов от несанкционированного доступа на уровнях пользователя или группы пользователей.
Как с каждым процессом связан идентификатор процесса, так и с каждым файлом в UNIX связан индексный дескриптор, содержащий его основные характеристики. Такой подход делает программы полностью независимыми от местоположения объектов, с которыми они работают.
Все действия в рамках UNIX координируются ядром системы. Ядро во время работы постоянно находится в оперативной памяти и содержит системные программы и управляющие структуры данных. В его функции входит:
- диспетчеризация процессов, включая распределение ресурсов для них;
- выполнение системных вызовов, запросов от процессов к операционной системе;
- управление устройствами при выполнении операций ввода-вывода;
- управление файловой системой;
- распределение и перераспределение оперативной памяти.
Ядро всегда работает в режиме "система". Оно обеспечивает мультипрограммную поддержку в многопользовательском (многотерминальном) режиме, называемым также режимом разделения времени. Количество функций ядра в UNIX минимизировано за счет того, что большое число системных задач решается специальными утилитами.
Пользователь взаимодействует с системой на уровне командного языка. В командном языке каждой команде соответствует свой выполняемый файл, поэтому набор команд может быть легко расширен за счет добавления новых команд и соответствующих им выполняемых файлов. Обработка команд командного языка производится интерпретатором команд shell. По построению командный язык напоминает процедурный язык программирования высокого уровня. В частности, он позволяет организовывать конвейеры команд и содержит конструкции типа условного оператора, цикла и т.п. Это делает программирование в UNIX двухуровневым. Программист может писать свои программы на выбранном языке программирования, например, Си, но он может написать программы и на командном языке. Допускается и смешанный вариант программирования, когда часть программ пишется на языке программирования, а затем обращение к ним производится из программ, написанных на командном языке. При этом надо иметь в виду, что программы, реализованные на командном языке, работают значительно медленнее из-за интерпретации каждой команды.
UNIX поддерживает ряд систем программирования, но наиболее удобной из них является система программирования Си.
Си - язык системного программирования, однако его с успехом можно применять как для задач вычислительного характера, так и для задач обработки данных. Обладая многими свойствами, присущими языкам типа Ассемблера, он остается в то же время процедурным языком программирования, и его использование существенно повышает производительность труда программистов.
Сама по себе система UNIX невелика по объему и довольно проста. Средства системы выразительны и имеют удобную мнемонику. Допускается изучение системы по частям, что дает возможность пользователю ограничиться изучением только того набора средств, который ему в данный момент необходим. Как показала практика, неподготовленный пользователь выходит на режим самостоятельной работы через одну - две недели после начала освоения UNIX.
Инструментальность UNIX проявляется в ее насыщенности различными программными средствами, облегчающими процесс конструирования программного обеспечения. Инструментальные средства UNIX можно разделить на следующие группы:
- утилиты различного назначения, представляющие собой строительные блоки, которые с успехом могут использоваться при разработке сложных программных комплексов. Помимо этого, являясь наполнением некоторых команд командного языка, они в среде интерпретатора shell обеспечивают интерактивную работу пользователя за терминалом;
- средства работы с текстами (к ним относятся строковые и экранные редакторы, форматеры текстов и специальные утилиты, работающие с текстовой информацией);
- средства поддержки разработок программного обеспечения, к которым относятся координатор make и система контроля и документирования текстовых файлов sccs. Они используются для автоматизации процесса формирования сложных многомодульных программ в период их отладки и корректировке, документирование процесса разработки и ведения архива;
- средства генерации программ анализа - генераторы синтаксических и лексических анализаторов уасс и lex используются для автоматического построения соответствующих блоков транслирующих систем, а также специализированных программ обработки символьной информации;
- интерпретатор shell, являющийся важным инструментальным
средством, позволяющим на уровне командного языка строить новые инструментальные средства проблемной направленности.
Свойство мобильности, которое присуще UNIX, означает возможность переноса системы с одной машинной архитектуры на другую с минимальными затратами. Мобильность UNIX обеспечивается разумным выбором соответствующей виртуальной UNIX-машины и уровнем языка, на котором система написана. Виртуальная машина должна быть достаточно абстрактной, чтобы при создании виртуальной системы можно было бы оперировать такими общими понятиями, как процесс, ресурс, файл и т. п., и в то же время не отрываться далеко от реальной ЭВМ, чтобы машинно-зависимая часть системы была наименьшей из возможных. В UNIX на уровне виртуальных понятий рассматриваются: управление процессами, распределение памяти, ввод-вывод, командный язык и др., а машинно-зависимая часть четко выделена.
Наряду с достоинствами, системе UNIX присущ ряд недостатков: не поддерживается режим реального времени; слабая устойчивость к аппаратным сбоям; снижение эффективности при решении однотипных задач. В UNIX слабо развиты средства взаимодействия и синхронизации процессов, отсутствуют средства гибкого управления их приоритетами. Все это приводит к низкой реактивности системы и делает ее использование в режиме реального времени нецелесообразным.
С другой стороны, поскольку UNIX, развивалась с ориентацией на область научных исследований, в ней отсутствовали некоторые возможности, необходимые в коммерческих применениях. Ряд разработчиков программного обеспечения предложили собственные коммерческие версии ОС UNIX, полученные путем ее расширения, например, системы IDRES, CROMIX, COGERENT, ONIX, XENIX. Последняя система - одна из наиболее широко известных, разработана фирмой Microsoft в начале 80-х годов с возможностями широкого коммерческого применения.
Из-за возрастающей популярности системы UNIX перечень прикладных программ, способных работать под ее управлением, постоянно увеличивается. Имеются программы обработки текстов, верстки газет, управления базами данных; трансляторы для языков Бейсик, Фортран, Кобол, Си, Паскаль, Ассемблеров и др. языков программирования. В настоящее время, благодаря техническому прогрессу, микро-ЭВМ имеют достаточно мощные вычислительные ресурсы, чтобы удовлетворить жестким требованиям функционирования ОС UNIX. Стало возможным выполнение большинства программ, которые широко применялись под управлением операционных систем СР/М, MS-DOS, PC-DOS, VAX, WINDOWS 2000. Вследствие возрастания коммерческого и прикладного значения ОС UNIX, а также удобства в эксплуатации, перспективы ее распространения и спроса становятся неограниченными.
ПЕРВАЯ ПРОГРАММА НА СИ
main() {
int first, second, sum;
first=2;
second=3;
sum=first+second;
}
Слово main, которое есть абсолютно в любой Си-программе, показывает компилятору, откуда она начинается. Следом всегда идут открывающая и закрывающая скобки (), внутри которых могут быть дополнительные параметры, но сейчас они нам ни к чему. Сама программа помещается между фигурными скобками {}, которые также обязательны. Строчка int first, second, sum; командует компилятору выделить ячейки памяти для переменных first, second и sum. Имена переменных в языке Си начинаются обязательно с латинской буквы, далее могут идти как буквы, так и цифры. Строчные буквы отличаются от прописных, поэтому Sum и sum — разные имена. Словечко int перед именами переменных показывает, что в выделенных ячейках памяти будут храниться целочисленные переменные, которые могут быть только положительными и отрицательными целыми и нулем. Три последующие строчки
first=2;
second=3;
sum=first+second;
в отличие от предыдущих, где только объявлялись переменные и выделялась память для них, содержат инструкции, то есть описания действий, которые должен выполнить компьютер. Две строчки
first=2;
second=3;
засылают в переменные first и second нужные значения. Целочисленным переменным можно присваивать и дробные значения, например
second=3.5;
Результат (присвоенное значение переменной) все равно будет целым, то есть переменная second окажется равной 3. Это означает, что знак равенства в языке Cи используется не так, как в алгебре. Запись second=3.5 означает в алгебре равенство переменной second числу 3,5. Такая же запись в языке Cи означает присваивание переменной second значения 3.5. Как мы поняли, результат присваивания зависит от типа переменной. Для завершения знакомства с первой программой на языке Си нам осталось только сказать о точке с запятой, которая ставится в конце каждой инструкции. Именно точка с запятой показывает компилятору, что текущая инструкция закончилась и пора переходить к следующей. Это, между прочим, означает, что инструкция может занимать несколько строчек. Например, та же самая инструкция может выглядеть и так:
sum=
first
+
second;
3. ПРОГРАММА, КОТОРАЯ «УМЕЕТ ГОВОРИТЬ»
#include <stdio.h>
main(){
int first, second, sum;
first=2;
second=3;
sum=first+second;
printf("Сумма=%d\n",sum);
}
Загрузив программу увидим на экране компьютера:
Сумма=5
Значит, добавленная нами строчка
printf("Сумма=%d\n",sum);
выводит на экран символы Сумма= и само значение переменной sum (в нашем случае символ ‘5’). С помощью printf() можно выводить на экран самые разные сообщения, например, вывод слова «Мама» выглядит следующим образом:
printf("МАМА");
причем курсор остается справа от последней буквы ‘А’. Чтобы курсор оказался в начале следующей строчки, добавим в конец фразы символ перевода строки '\n':
printf("МАМА\n");
Строка printf("Сумма=%d\n",sum) в программе, устроена чуть сложнее. Кроме букв и управляющего символа '\n' в ней есть еще значок %d. Это шаблон, который обозначает место выводимой на экран переменной (переменная sum будет показана сразу за символами «Сумма=»), причем это будет целое число (буква d задает печать переменной целочисленного типа). Сама переменная указывается правее и отделяется от других знаков, заключенных в кавычки "", запятой. С помощью printf()можно выводить на экран значения нескольких переменных. Для этого нужно сначала поместить в кавычки символы и шаблоны для этих переменных, затем поставить запятую и сами переменные, также разделенные запятыми. Чтобы, например, вывести на экран значения переменных first, second, sum, соответствующая строка программы должна выглядеть так: printf("first=%d second=%d sum=%d\n",
first, second, sum);
В результате мы получим на экране
first=2 second=3 sum=5
Функция printf(), с которой мы только что познакомились, похожа на отдельную программу. На ее вход поступают аргументы, разделенные запятыми. В функции
printf("МАМА\n");
всего один аргумент — последовательность символов, заключенная в кавычки. В функции printf(), выводящей три переменные, уже четыре аргумента, один из которых — это строчка в кавычках, содержащая текст и шаблоны, а три других — разделенные запятыми названия переменных.
Аргументы огораживаются в функции круглыми скобками ().
#include <stdio.h>
Этой строчки не было в нашей первой программе и легко догадаться, что ее появление связано с функцией printf(). Дело в том, что в языке Си все переменные и функции должны быть описаны, иначе компилятор не сможет правильно перевести программу в понятную процессору последовательность команд. До начала работы с переменными компилятор должен знать их тип, иначе он не поймет, сколько ячеек памяти выделять под переменную и как кодировать хранящееся в ней двоичное число. То же самое относится и к функциям. Чтобы правильно обращаться с функцией, компилятор должен знать, какие аргументы ей могут передаваться и как воспользоваться результатами ее работы. Поэтому каждая функция должна быть описана еще до того, как компилятор встретит ее в тексте программы. Если вы сами создаете функцию (а в языке Си это вполне возможно), то сами же должны ее описать. Но если функция стандартная, ее описание хранится в специальном файле. Так, описание printf() хранится в файле stdio.h. Этот файл — неотъемлемая часть компилятора, который, встретив строчку #include <stdio.h>, находит файл stdio.h и вставляет его в текст программы (именно туда, где стоит #include (включить), а уж потом превращает программу, написанную на языке Си в последовательность команд процессора.
ЦИКЛ WHILE
sum=0;
i=1;
while (i <= 2){
sum += i;
i += 1;
}
Цикл while() начинается с проверки: превышает ли переменная i число 2. В нашем случае i равна 1, что явно не больше двух. Значит, будут выполнены инструкции, заключенные в фигурные скобки:
sum += i; /*sum теперь равна единице*/
i += 1; /*i теперь равна 2 */
То есть, к sum прибавится переменная i, равная единице. Затем i увеличится на единицу и станет равной двум. Теперь мы возвращаемся к началу цикла while() и снова проверяем условие i <= 2. Поскольку i теперь равно 2, условие снова выполняется и программа прибавляет к sum уже двойку и снова увеличивает i на единицу (теперь i и sum равны 3), и мы вновь возвращаемся к началу цикла, но теперь проверка i < =2 показывает, что условие не выполняется (i равно теперь 3, а это больше двух!). Значит, не будут выполняться и инструкции в фигурных скобках, а программа (не выполняя больше инструкций в фигурных скобках) перейдет к тому, что следует за циклом while().
МАССИВЫ
Массив — это ряд переменных, у каждой из которых есть специальный номер или индекс. Если сто чисел поместить в массив с именем digits, то первое число будет равно digits[0], второе — digits[1], третье — digits[2], сотое — digits[99]. Номер этих чисел в массиве может храниться в целочисленной переменной, и, скажем, третью переменную можно записать как digits[i], если, конечно, i=2 (нумерация переменных в массиве начинается с нуля). Если все сто переменных загнаны в массив, то их сумму вычислит следующий фрагмент программы:
i=0;
sum=0;
while(i<=99){
sum += digits[i];
i++;
}
При i=0 к переменной sum прибавится первое число, то есть элемент массива digits[0], затем индекс i увеличится на единицу и при следующем обороте цикла к sum прибавится число digits[1]. Последним к sum прибавится число digits[99], после чего i станет равным 100 и цикл завершится.