Системное программирование под Unix на языке С

Введение

Одной из целей, которые изначально ставились перед разработчиками ОС Unix, являлось создание удобной среды программирования. Во многом это справедливо и сегодня. Ведь действительно, используя не особо сложную организацию sh-скриптов, можно решать практически любые административные задачи. Но иногда бывает необходимость в чем-то, что позволит гораздо более гибкие возможности, чем просто средства интерпретатора командной строки. Например, если необходимо создать свою утилиту, аналогичную ls или внести изменения в само ядро ОС.

Именно этот аспект программирования и назван системным.

Все версии Unix предоставляют строго ограниченный набор входов в ядро ОС, через которые прикладные задачи имеют возможность пользоваться базовыми услугами ОС. Эти точки входа носят название системных вызовов. Системный вызов определяет функцию, выполняемую ядром ОС от имени процесса, выполнившего вызов, и является интерфейсом самого низкого уровня взаимодействия прикладных процессов с ядром.

Таким образом, можно сказать, что системное программирование отличается от прикладного именно этим взаимодействием с ядром, и реализуется оно самим программистом, а не компилятором, а библиотечные функции ? это ?надстройка? над системными вызовами, обеспечивающие более быстрый и удобный способ получения системных услуг.

И если подытожить все вышесказанное, то обусловимся, что системное программирование ? это вид программирования, при котором программист осуществляет вручную обращения к ядру ОС, таким образом решая задачи, которые нельзя решить с помощью прикладного программирования.

Язык С ? ?родной язык? для Unix-систем, поэтому программирование на С, является базовым, если говорить о программировании под Unix.

В самой ОС используется подход, при котором каждый системный вызов имеет соответствующую функцию (или функции) с тем же именем, которые хранятся в стандартной библиотеке языка С.

Однако, повторюсь, что библиотечные функции ? это ?надстройка? над системными вызовами, и поэтому не следует их отождествлять. Например, функция printf(3s) использует системный вызов write(2) для записи данных в файл, и сама она не является системным вызовом. Системный вызов time(2) возвращает время в секундах, прошедшее с 1 января 1970 года, а преобразование этого числа в вид, удобный для восприятия (дата и время) с учетом временной зоны, осуществляется библиотечными функциями (ctime(3c), localtime(3c) и т. п.)


Cхема взаимодействия приложения с ядром ОС при использовании системных вызовов и библиотечных функций.


Заголовки

Использование системных функций обычно требует включения в текст программы заголовочных файлов, содержащих определения функций ? число передаваемых аргументов, типы аргументов и типы возвращаемых значений.

Заголовочные файлы включаются в текст программы с помощью директивы #include. При этом имя самого файла заключено в <>. Это значит, что поиск файла будет произведен в общепринятых стандартных каталогах, но если есть необходимость включить файл по абсолютному или относительному имени, или, скажем, заголовочный файл, написанный вручную, то необходимо заключить его имя в двойные кавычки.

Например системный вызов creat(2), используемый для создания файла, объявлен в файле <fcntl.h> следующим образом:


#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

int creat(const char *path, mode_t mode);

т. е. Если вы хотите использовать этот вызов у себя в программе, то необходимо включить все эти заголовочные файлы.


Компиляция

Схема


К примеру, если вы захотели создать программу, и исходный файл с ее кодом назвали program.c, то процесс создания исполняемого файла выглядит так:

gcc -o program program.c

Утилита gcc возьмет первый параметр, переданный ей, и создаст исполняемый файл с этим именем. А исходный текст программы будет взят ею из файла, переданного в качестве второго параметра.

Запустить программу можно будет следующим образом:

./program

В случае, если имеются ошибки, в исходном тексте, то встроенный компилятор cc\gcc (в зависимости от ОС) уведомит, о их наличии, а также укажет номера строк и описание ошибок.

Программа на языке С. Ее структура и особенности


#include <stddef.h>

// включение заголовочного файла

extern char **environ;

//массив с переданными в программу переменными окружения

main(int argc, char *argv[])

//основная функция

{

int i;

printf("Число параметров переданных программе %s, равно %d\n", argv[0], argc-1);

for (i=1; i<argc; i++)

printf("argv[%d] = %s\n", i, argv[i]);

//печать на экран переданных параметров

for (i=0; i<10; i++)

if (environ[i] != NULL)

printf("environ[%d] : %s\n",i, environ[i]); //печать переменных окружения

}


Функция main() - главная функция, ей передается управление после запуска программы

Традиционно она объявляется следующим образом:

main(int argc, char *argv[])


Первый аргумент, переменная типа int, argc содержит в себе общее число параметров переданных программе, включая ее имя.

argv[] - массив переданных параметров.

Обратите внимание, что argv[0] ? имя самой программы.

К примеру, если программа, вызывается:

$ program

то ее имя, и соответственно argv[0]=''program''


Внимательно изучите листинг программы, написанной выше.

Создайте ее исполняемый модуль и запустите его. Проанализируйте результаты.

Обратите внимание на argv[0].


Завершение С-программы

Существует несколько способов завершения программы. Основные, это ? возврат из функции main() и вызов функции exit(2), оба приводят к завершению выполнения задачи.

Если процесс завершается по независимым от него обстоятельствам, скажем, по получении какого-либо сигнала, то функция exit(2) выполняется от имени процесса.

Системный вызов exit(2) выглядит следующим образом:

#include <unistd.h>

void exit(int status);

Аргумент status ? код завершения программы. По умолчания, если программа завершается успехом, то код ее завершения должен быть равен нулю, иначе должен принимать различные значения, но такие чтобы потом при неудаче программист смог бы разобраться почему именно завершилась его программа, по коду завершения.