VI.  Шестая группа вопросов: Системное программирование



1. Системные вызовы. Определение. Применение.
Вce версии UNIX предоставляют строго определенный ограниченный набор входов в ядро операционной системы, через которые прикладные задачи имеют возможность воспользоваться базовыми услугами, предоставляемыми UNIX. Эти точки входа получили название системных вызовов (system calls).
Системный вызов определяет функцию, выполняемую ядром операционной системы от имени процесса, выполнившего вызов, и является интерфейсом самого низкого уровня взаимодействия прикладных процессов с ядром.
В UNIX каждый системный вызов имеет соответствующую функцию с тем же именем, хранящуюся в стандартной библиотеке языка С.
Помимо системных вызовов предлагается большой набор функций общего назначения. Эти функции не являются точками входа в ядро операционной системы, хотя в процессе выполнения многие из них выполняют системные вызовы.
Например, функцияprintf (3 S ) использует системный вызовwrite (2) для записи данных в файл, в то время как функцииstrcpy (3 C ) (копирование строки) или atoi(3C) (преобразование символа в его числовое значение) вообще не прибегают к услугам операционной системы.
Таким образом, часть библиотечных функций является "надстройкой" над системными вызовами, обеспечивающей более удобный способ получения системных услуг.

Заголовки
Использование системных функций обычно требует включения в текст программы заголовочных файлов, содержащих определения функций, число передаваемых аргументов, типы аргументов и типы возвращаемых значений.
Заголовочные файлы включаются в текст программы с помощью директивы #include. При этом имя самого файла заключено в <>. Это значит, что поиск файла будет произведен в общепринятых стандартных каталогах, но если есть необходимость включить файл по абсолютному или относительному имени, или, скажем, заголовочный файл, написанный вручную, то необходимо заключить его имя в двойные кавычки.
Например системный вызов creat(2), используемый для создания файла, объявлен в файле <fcntl.h> следующим образом:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int creat(const char *path, mode_t mode);
т. е. Если вы хотите использовать этот вызов у себя в программе, то необходимо включить все эти заголовочные файлы.

 

2. Схема компиляции программы.

Любой компилятор состоит из транслятора и компоновщика. Часто в качестве компоновщика компилятор использует внешний компоновщик, реализованный в виде самостоятельной программы, а сам выполняет лишь трансляцию исходного текста (по этой причине многие ошибочно считают компилятором разновидность транслятора). Компилятор может быть реализован и как своеобразная программа-менеджер, для трансляции программы вызвающая сооствествующий транслятор (трансляторы - если разные части программы написаны на разных языках программирования) и затем - для компоновки программы, - вызывающая компоновщик. Ярким примером такого компилятора является имеющаяся во всех UNIX-системах (и Linux-системах в том числе) утилита make (имеются реализации утилиты make и в других системах, в частности в Windows-системах).
Процесс компиляции состоит из следующих фаз:
Лексический анализ. На этой фазе последовательность символов исходного файла преобразуется в последовательность лексем.
Синтаксический (грамматический) анализ. Последовательность лексем преобразуется в древо разбора.
Семантический анализ. Древо разбора обрабатывается с целью установления его семантики (смысла) — например, привязка идентификаторов к их определениям, типам данных, проверка совместимости типов данных, определение результирующих типов данных выражений и т. д. Результат обычно называется «промежуточным представлением/кодом», и может быть дополненным древом разбора, новым древом, абстрактным набором команд или чем-то ещё, удобным для дальнейшей обработки.
Оптимизация. Удаляются избыточные команды и упрощается (где это возможно) код с сохранением его смысла, т. е. реализуемого им алгоритма (в том числе предвычисляются (т. е. вычисляются на фазе трансляции) выражения, результаты которых практически являются константами). Оптимизация может быть на разных уровнях и этапах — например, над промежуточным кодом или над конечным машинным кодом.
Генерация кода. Из промежуточного представления порождается код на целевом языке (в том числе выполняется компоновка программы).
В конкретных реализациях компиляторов эти фазы могут быть разделены или, наоборот, совмещены в том или ином виде.
Большинство компиляторов компилирует (в том числе транслирует) программу с некоторого высокоуровневого языка программирования в машинный код, который может быть непосредственно выполнен центральным процессором. Как правило, этот код также ориентирован на исполнение в среде конкретной операционной системы, поскольку использует предоставляемые ею возможности (системные вызовы, библиотеки функций, API). Архитектура или платформа (набор программно-аппаратных средств), для которой производится компиляция, называется целевой машиной или, также, целевой платформой.
Некоторые компиляторы (например, Java) переводят программу не в машинный код, а в программу на некотором специально созданном низкоуровневом интрепретируемом языке.

3. Средства компиляции программ. Утилиты (cc\gcc, make).
В системе UNIX имеются компиляторы с языков C, ФОРТРАН-77 и ПАСКАЛЬ и другие. Команды вызова компилятора имеют вид cc, f77 или fc, pc и т.п.
Параметрами этих команд являются файлы с текстами программ на исходных языках, имена которых должны оканчиваться на .c, .f, .p и т.п. соответственно.
Примеры:
$ cc program.c
$ fc test.f
$ pc example.p
Результатом работы компилятора является файл исполняемого кода, имеющий по умолчанию имя a.out. Если вы хотите другое имя, его можно указать явно ключом -o <имя> при вызове компилятора.
Пример:
$ gcc -o test test.c
$ ls
test
test.c
$
К примеру, если вы захотели создать программу, и исходный файл с ее кодом назвали program.c, то процесс создания исполняемого файла выглядит так:
gcc -o program program.c
Если программный продукт велик, содержит много исходных, объектных и исполняемых модулей, задача его сопровождения и модификации может быть облегчена утилитой make, которая позволяет автоматизировать все рутинные операции по перетрансляции и перелинкированию всех или части модулей при внесении в них изменений.
Утилита make работает с файлом Makefile, в который записывается необходимая информация о всех файлах программы и связях между ними.
Пример:
$ cat Makefile
FILES = test.f check.f prove.f
OBJECTS = test.o check.o prove.o
test: $ {OBJECTS}
ld -o test/lib/frt0.o ${OBJECTS} -lF77
$
Как видно из примера, в файле Makefile помещаются макроопределения, имеющие вид:строка1 = строка2
и правила, имеющие вид:конечный файл :
исходные файлы
команда
Первая строка правила называется зависимостью. Зависимость указывает, что конечный файл является результатом работы команды, указанной во второй строке правила, над исходными файлами. Внутри зависимости можно ссылаться на макроопределения в форме $(строка1).
Подготовив такой Makefile, можно модифицировать программу test одним вызовом команды make, например:
$ make
fc -c test.f
fc -c check.f
fc -c prove.f
ld -o test/lib/frt0.o check.o prove.o -lF77
$
Команда выполняется только в том случае, если дата создания или модификации конечного файла меньше, чем соответствующая дата хотя бы одного исходного файла (то есть если конечный файл устарел).
Возможно изменить только часть программы, например:
$ make prove.o
fc -c prove.f
$
В данном случае утилита make по умолчанию "знает", что файл prove.o зависит от файла prove.f и реализация этой зависимости есть указанный вызов компилятора.
Полезный ключ -n утилиты make позволяет не выполняя предписанных действий предварительно посмотреть, что было бы выполнено, если бы ключ не был подан.
Утилита gcc возьмет первый параметр, переданный ей, и создаст исполняемый файл с этим именем. А исходный текст программы будет взят ею из файла, переданного в качестве второго параметра.
Запустить программу можно будет следующим образом:
./program
В случае, если имеются ошибки, в исходном тексте, то встроенный компилятор cc\gcc (в зависимости от ОС) уведомит, о их наличии, а также укажет номера строк и описание ошибок.

 

4. Параметры, передаваемые gcc/cc.
cc foobar.c
приведет к компиляции foobar.c. Если у вас имеется для компиляции больше одного файла, просто сделайте нечто вроде следующего: cc foo.c bar.c
Заметьте, что синтаксическая проверка - это именно проверка синтаксиса. При этом не выполняется проверка на наличие сделанных вами логических ошибок, типа вхождения в бесконечный цикл или использования пузырьковой сортировки там, где вы хотели использовать двоичную.
Имеется огромное количество параметров для cc, все они описаны на справочной странице. Вот несколько из самых важных, с примерами их использования.
-o filename
Имя выходного файла. Если вы не используете этот параметр, cc сгенерирует выполнимый файл с именем a.out.
cc foobar.c          выполнимый файл называется a.out
cc -o foobar foobar.c выполнимый файл называется foobar
-c
Выполнить только компиляцию файла, без компоновки. Полезно для игрушечных программ, когда вы хотите просто проверить синтаксис, или при использовании Makefile.
 cc -c foobar.c
В результате будет сгенерирован объектный файл (не выполнимый файл) с именем foobar.o. Он может быть скомпонован с другими объектными файлами для получения выполнимого файла.
-g
Создать отладочную версию выполнимого файла. Этот параметр указывает компилятору поместить в выполнимый файл информацию о том, какая строка какого файла с исходным текстом какому вызову функции соответствует. Отладчик может использовать эту информацию для вывода исходного кода при пошаговом выполнении программы, что очень полезно; минусом является то, что вся эта дополнительная информация делает программу гораздо большей. Обычно вы компилируете с параметром -g при работе над программой, а затем, когда убедитесь в работоспособности программы, компилируете "окончательную версию" без параметра -g.
cc -g foobar.c
При этом будет сгенерирована отладочная версия программы.
-O
Создать оптимизированную версию выполнимого файла. Компилятор прибегает к различным ухищрениям для того, чтобы сгенерировать выполнимый файл, выполняющийся быстрее, чем обычно. После опции -O вы можете добавить число, указывающее качество оптимизации, но использование этого не защищено от ошибок оптимизации компилятора.
Обычно оптимизацию используют при компиляции окончательной версии.
cc -O -o foobar foobar.c
По этой команде будет создана оптимизированная версия программы foobar.
Следующие три флага заставят cc проверять ваш код на соответствие известному международномустандарту, часто называемому стандартом ANSI, хотя, строго говоря, это стандарт ISO.
-Wall
Включить выдачу всех предупреждений, которые посчитают нужным выдать авторы cc. Несмотря на свое название, при этом включается выдача не всех предупреждений, на которые способен компилятор cc.
-ansi
Выключить большинство, если не все, не-ANSI расширений языка C, имеющиеся в cc. Несмотря на название, этот параметр не гарантирует, что ваш код будет строго соответствовать стандарту.
-pedantic
Выключить все расширения C, имеющиеся в компиляторе cc, не соответствующие стандарту ANSI.
Без этих флагов компилятор cc позволит вам использовать некоторые нестандартные расширения языка. Некоторые из них весьма полезны, но не будут работать при использовании других компиляторов--фактически одним из назначений стандарта является обеспечение возможности писать код, который будет работать с любым компилятором в любой системе. Такой код называется переносимым кодом.
cc -Wall -ansi -pedantic -o foobar foobar.c      
По этой команде после проверки на соответствие стандарту файла foobar.c будет сгенерирован выполнимый файл foobar.
-llibrary
Задает библиотеку функций, которая будет использоваться на этапе компоновки.
В качестве самого распространенного примера можно привести компиляцию программы, использующей некоторые математические функции на языке C. В отличие от других платформ, имеется отделенная от стандартной библиотека, и вам нужно указать компилятору, что ее нужно использовать.
При этом если библиотека называется libsomething.a, вы передаете программе cc аргумент -lsomething.
cc -o foobar foobar.c -lm  
По этой команде библиотека математических функций будет скомпонована с foobar.
GСС - это свободно доступный оптимизирующий компилятор для языков C, C++. Собственно программа gcc это некоторая надстройка над группой компиляторов, которая способна анализировать имена файлов, передаваемые ей в качестве аргументов, и определять, какие действия необходимо выполнить. Файлы с расширением .cc или .C рассматриваются, как файлы на языке C++, файлы с расширением .c как программы на языке C, а файлы c расширением .o считаются объектными. Можно использовать компилятор gcc для компиляции программ в объектные модули и для компоновки полученных модулей в единую исполняемую программу. Чтобы откомпилировать исходный код C++, находящийся в файле F.cc, и создать объектный файл F.o, необходимо выполнить команду:
gcc -c F.cc//Опция –c означает «только компиляция».
Чтобы скомпоновать один или несколько объектных файлов, полученных из исходного кода - F1.o, F2.o, ... - в единый исполняемый файл F, необходимо ввести команду:
gcc -o F F1.o F2.o //Опция -o задает имя исполняемого файла.
Можно совместить два этапа обработки - компиляцию и компоновку - в один общий этап с помощью команды:
gcc -o F <compile-and-link-options> F1.cc ... -lg++ <other-libraries>
<compile-and-link –options> - возможные дополнительные опции компиляции и компоновки. Опция –lg++ указывает на необходимость подключить стандартную библиотеку языка С++, <other-libraries> - возможные дополнительные библиотеки.
После компоновки будет создан исполняемый файл F, который можно запустить с помощью команды ./F <arguments>. Строка <arguments> определяет аргументы командной строки Вашей программы.
В процессе компоновки очень часто приходится использовать библиотеки. Библиотекой называют набор объектных файлов, сгруппированных в единый файл и проиндексированных. Когда команда компоновки обнаруживает некоторую библиотеку в списке объектных файлов для компоновки, она проверяет, содержат ли уже скомпонованные объектные файлы вызовы для функций, определенных в одном из файлов библиотек. Если такие функции найдены, соответствующие вызовы связываются с кодом объектного файла из библиотеки. Библиотеки могут быть подключены с помощью опции вида -lname. В этом случае в стандартных каталогах, таких как /lib , /usr/lib, /usr/local/lib будет проведен поиск библиотеки в файле с именем libname.a. Библиотеки должны быть перечислены после исходных или объектных файлов, содержащих вызовы к соответствующим функциям.
Опции компиляции
Среди множества опций компиляции и компоновки наиболее часто употребляются следующие:
-c
Эта опция означает, что необходима только компиляция. Из исходных файлов программы создаются объектные файлы в виде name.o. Компоновка не производится.
-Dname=value       
Определить имя name в компилируемой программе, как значение value. Эффект такой же, как наличие строки #define name value в начале программы. Часть `=value' может быть опущена, в этом случае значение по умолчанию равно 1.
-o file-name
Использовать file-name в качестве имени для создаваемого файла.
-lname     
Использовать при компоновке указанную библиотеку.
-Llib-path
-Iinclude-path
Добавить к стандартным каталогам поиска библиотек и заголовочных файлов свои.
-g
Поместить в объектный или исполняемый файл отладочную информацию для отладчика gdb. Опция должна быть указана и для компиляции, и для компоновки. В сочетании –g рекомендуется использовать опцию отключения оптимизации –O0 (см.ниже)
-MM
Вывести заголовочные файлы (но не стандартные заголовочные файлы), используемые в каждом исходном файле, в формате, подходящем для утилиты make. Не создавать объектные или исполняемые файлы.
-pg
Поместить в объектный или исполняемый файл инструкции профилирования для генерации информации, используемой утилитой gprof. Опция должна быть указана и для компиляции, и для компоновки. Собранная с опцией -pg программа при запуске генерирует файл статистики. Программа gprof на основе этого файла создает расшифровку, указывающую время, потраченное на выполнение каждой функции.
-Wall        
Вывод сообщений о всех предупреждениях или ошибках, возникающих во время трансляции программы.
-O1
-O2
-O3
Различные уровни оптимизации.
-O0
Не оптимизировать. Если вы используете многочисленные `-O' опции с номерами или без номеров уровня, действительной является последняя такая опция.

 

5. Параметры, переданные программе (argc, argv[]), переменные окружения.
Функция main() является первой функцией, определенной пользователем (т. е. явно описанной в исходном тексте программы), которой будет передано управление после создания соответствующего окружения запускаемой на выполнение программы.
Традиционно функциятаin() определяется следующим образом:
main(int argc, char *argv[], char *envp[]);
Первый аргумент (argc) определяет число параметров, переданных программе, включая ее имя. Указатели на каждый из параметров передаются в массивеargv [ ], таким образом, через argv[0] адресуется строка, содержащая имя программы, argv[l] указывает на первый параметр и т. д. до argv [ argc-1]. Массив envp [ ] содержит указатели на переменные окружения, передаваемые программе. Каждая переменная представляет собой строку вида
имя_переменной=значение переменной.
Стандарт ANSI С определяет только два первых аргумента функции main() — argc и argv. Стандарт POSIX.1 определяет также аргумент envp, хотя рекомендует передачу окружения программы производить через глобальную переменную environ.
extern char **environ;
Рекомендуется следовать последнему формату передачи для лучшей переносимости программ на другие платформы UNIX.

6. Установка, получение значений переменных окружения.
Начало искать в вопросе выше.
Приведем пример программы, соответствующую стандарту POSIX.1, которая выводит значения всех аргументов, переданных функцииmain(). число переданных параметров, сами параметры и значения первых десяти переменных окружения.
#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<8; i++)
if (environ[i] != NULL)
printf("environ[%d] : %s\n", i, environ[i]);
}
Запустив эту программу на выполнение, мы увидим следующую информацию:
$progname first second 3

argv[l] = first
argv[2] = second
argv[3]= 3

environ[0] LOGNAME=andy
environ[1] MAIL=/var/mail/andy
environ[2] LD_LIBRARY_PATH=/usr/openwin/lib:/usr/ucblib
environ[3] PAGER=/usr/bin/pg
environ[4] TERM=vtlOO
environ[5] PATH=/usr/bin:/bin:/etc:/usr/sbin:/sbin:/usr/ccs/   bin:/usr/local/bin
environ[6] HOME=/home/andy
environ[7] SHELL=/usr/local/bin/bash

Обратите внимание, что argv[0] - имя самой программы. К примеру, если программа, вызывается:
$ program
то ее имя, и соответственно argv[0]=''program''
Для получения и установки значений конкретных переменных окружения используются две функции:get env(3C) иputenv (3C):
#include <stdlib.h>
char *getenv(const char *name) ;
возвращает значение переменной окружения name, a
int putenv(const char *string) ;
помещает переменную и ее значение (var_name=var_value) в окружение программы.
Обычно при запуске программы на выполнение из командной строки shell автоматически устанавливает для нее три стандартных потока ввода/вывода: для ввода данных, для вывода информации и для вывода сообщений об ошибках. Начальную ассоциацию этих потоков с конкретными устройствами производит терминальный сервер в большинстве систем это процесс getty, который открывает специальный файл устройства, связанный с терминалом пользователя, и получает соответствующие дескрипторы. Эти потоки наследует командный интерпретатор shell и передает их запускаемой программе. При этом shell может изменить стандартные направления, если пользователь указал на это с помощью специальных директив перенаправления потока.

 

7. Директивы (include, define)
Директива препроцессора include у нас находится буквально в каждом примере. Она создает копию файла, который мы в ней указываем, и добавляет его в нашу программу. Есть два варианта включения директивы include:
#include <имя файла>
Поиск этого файла будет вестись в каталоге, где расположены библиотечные файлы.
#include "имя файла"
Поиск будет осуществляться в каталоге, где расположена наша программа.
Безусловно, первым предназначением данной директивы препроцессора для нас будет включение стандартных библиотечных файлов. Но эту директиву можно также использовать для включения наших собственных созданных файлов. Все современные программы для читабельности, удобства, экономии разбиваются на несколько составляющих (модулей), и уже эти модули включаются в нашу программу. Директива препроцессора define используется, что бы создать константы. Вот ее синтаксис:
#define идентификатор значение
Теперь давайте разберем принцип работы define. Перед компиляцией программы, во всех местах, где используется наш идентификатор, будет произведена замена его на значение, которое мы указали там же. Вот пример говорящий о его полезности:
#define SIZE 5
for (int i=0; i < SIZE; i++){
for (int j=0; j < SIZE;j++){
     printf("*");
}
printf("\n");
}
Эта программа будет выводить квадрат размеров SIZExSIZE. Она хороша тем, что если нам понадобится изменить размеры нашего квадрата, то достаточно будет только изменить значение нашей заданной константы SIZE на необходимое. Часто совершается одна очень грубая ошибка: во время написания значения для заданной константы ставят знак присвоения (=):
#define SIZE = 6
Это в корне неверно и компилятор выдаст вам ошибку, так как он заменит все константы SIZE на = 6. Т.е. вставит во все места не только цифру 6 но и еще знак присвоения.
Так же директиву препроцессора define можно использовать для задания макросов. Макросы, как и функции, могут задаваться без аргументов или с аргументами. Вот пример:
#define SIZE 5 // без аргументов - получается обычная константа
#define SIZE(x) (x * x) // с аргументами
Но тут есть свои подводные камни. Вот на примере:
SIZE (5); // будет 25
SIZE (1+4); // будет 9
На первый взгляд это покажется удивительно, так как мы передаем в принципе все туже пятерку. Но вот как препроцессор заменит макрос:
5*5 // в первом случае
1+4*1+4 // во втором
Для избегания такой проблемы нужно каждую константу, используемую в макросе брать в скобки. Вот правильное написание нашего макроса:
#define SIZE(x) ( (x) * (x) )
Тут уже никаких ошибок не возникнет, и наша программа заменит все верно.

8. Заголовочные файлы. Определение, применение.
Заголовочный файл (header file), или подключаемый файл — в языках программирования Си и C++ файл содержащий определения типов данных, структуры, прототипы функций, перечисления, макросы препроцессора. Имеет по умолчанию расширение .h; иногда для заголовочных файлов языка C++ используют расширение .hpp. Заголовочный файл используется путём включения его текста в данный файл директивой препроцессора #include. Чтобы избежать повторного включения одного и того же кода, используются директивы #ifndef, #define, #endif
Заголовочный файл в общем случае может содержать любые конструкции языка программирования, но на практике исполняемый код (за исключением inline-функций в C++) в заголовочные файлы не помещают. Например, идентификаторы, которые должны быть объявлены более чем в одном файле, удобно описать в заголовочном файле, а затем его подключать по мере надобности.
Основная цель использования заголовочных файлов — вынесение описания нестандартных типов и функций за пределы основного файла с кодом. На этом же принципе построены библиотеки: в заголовочном файле перечисляются содержащиеся в библиотеке функции и используемые ею структуры/типы, при этом исходный текст библиотеки может находиться отдельно от текста программы, использующей функции библиотеки или вообще быть недоступным. Например, по сложившейся традиции, в заголовочных файлах объявляют функции стандартной библиотеки Си и Си++.

 

9. Система регистрации ошибок.
Обычно в случае возникновения ошибки системные вызовы возвращают ?1 и устанавливают значение переменной errno, указывающее причину возникновения ошибки. Так, например, существует более десятка причин завершения вызова open(2) с ошибкой, и все они могут быть определены с помощью переменной errno. Файл заголовков <errno.h> содержит коды ошибок, зна­чения которых может принимать переменная errno, с краткими комментариями.
Библиотечные функции, как правило, не устанавливают значение переменной errno, а код возврата различен для разных функций.
Поскольку базовым способом получения услуг ядра являются системные вызовы, рассмотрим более подробно обработку ошибок в этом случае.
Переменная errno определена следующим образом:
external int errno;
Следует обратить внимание, что значение errno не обнуляется следующим, нормально завершившимся системным вызовом. Таким образом, значение этой переменной имеет смысл только после системного вызова, который завершился с ошибкой.
Стандарт ANSI С определяет две функции, помогающие сообщить причину ошибочной ситуации: strerror(3C) и реггог(ЗС).
Функция strerror(3C) имеет вид:
#include <string.h>
char *strerror(int errnum);
Функция принимает в качестве аргумента errnum номер ошибки и возвращает указатель на строку, содержащую сообщение о причине ошибочной ситуации.
Функция реггог(зс) объявлена следующим образом:
#include <errno.h>
#include <stdio.h>
void perror(const char *s);
Функция выводит в стандартный поток сообщений об ошибках информацию об ошибочной ситуации, основываясь на значении переменной еrrno. Строка s, передаваемая функции, предваряет это сообщение и может служить дополнительной информацией, добавляя, например, название функции или программы, в которой произошла ошибка.
Использование этих двух функций:
#include <errno.h>
#include <stdio.h>

main(int argc, char *argv[]) {
fprintf(stderr, ?ENOMEM: %s\n", strerror(enomem));
errno = ENOEXEC;
perror(argv[0]); }
Результат выполнения:
ENOMEM: Not enough space
./program: exec format error

 

10. Основные системные вызовы для работы с файлами.
В среде программирования UNIX существуют два основных интерфейса для файлового ввода/вывода:
1. Интерфейс системных вызовов, предлагающий системные функции низкого уровня, непосредственно взаимодействующие с ядром операционной системы.
2. Стандартная библиотека ввода/вывода, предлагающая функции буферизированного ввода/вывода.
Второй интерфейс является "надстройкой" над интерфейсом системных вызовов, предлагающей более удобный способ работы с файлами.
Основные системные функции для работы с файлами
Функция ореn(2)
Открывает указанный файл для чтения или записи и имеет следующий вид:
#include <fcntl.h>
int open(const char *path, int oflag, mode_t mode);
Первый аргумент (path) является указателем на имя файла. Аргумент oflag указывает на режим открытия файла и представляет собой побитное объединение флагов. Аргумент mode, определяющий права доступа к файлу, используется только при создании файла и рассматривается при описании функции creat(2).
Флаги, определяющие режим открытия файла
O_RDONLY   
Открыть файл только для чтения
O_WRONLY
Открыть файл только для записи
О_RDWR
Открыть файл для чтения и записи
О_APPEND
Производить добавление в файл, т. е. устанавливать файловый указатель на конец файла перед каждой записью в файл
O_CREAT
Если указанный файл уже существует, этот флаг не принимается во внимание. В противном случае, создается файл, атрибуты которого установлены по умолчанию, или с помощью аргумента mode
O_EXCL
Если указан совместно с 0_CREAT, то вызовореп(2) завершится с ошибкой, если файл уже существует
O_NOCTTY
Если указанный файл представляет собой терминал, не позволяет ему стать управляющим терминалом
O_SYNC
Все записи в файл, а также соответствующие им изменения в метаданных файла будут сохранены на диске до возврата из вызоваwrite(2)
O_TRUNC
Если файл существует и является обычным файлом, его длина будет установлена равной 0
O_NONBLOCK
Изменяет режим выполнения операций read(2) и write(2) для этого файла на неблокируемый. При невозможности произвести запись или чтение, например, если отсутствуют данные, соответствующие вызовы завершатся с ошибкой EAGAIN
Если операция открытия файла закончилась удачно, то будет возвращен файловый дескриптор — указатель на файл, а в случае неудачи возвратит -1, а глобальная переменная errno будет содержать код ошибки.
Функция creat(2)
Функция служит для создания обычного файла или изменения его атрибутов и имеет следующий вид:
#include <fcntl.h>
int creat(const char *path, mode_t mode);
Как и в случае ореn(2), аргумент path определяет имя файла в файловой системе, a mode — устанавливаемые права доступа к файлу. При этом выполняется ряд правил:
1. Если идентификатор группы (GID) создаваемого файла не совпадай с эффективным идентификатором группы (EGID) или идентификатором одной из дополнительных групп процесса, бит SGID аргумент та mode очищается (если он был установлен)
2. Очищаются все биты, установленные в маске процесса umask.
3. Очищается флаг Sticky bit.
Если файл уже существует, его длина сокращается до 0, а права доступ владельцы сохраняются прежними.
Вызов сгеаt(2) эквивалентен следующему вызову функции ореn(2):
open(path, 0_WRONLY | 0_CREAT | 0_TRUNC, mode);
Фнкция close(2)
Фнкцияclose(2) разрывает связь между файловым дескриптором и открытым файлом, созданную функциями creat(2), open(2), dup(2), pipe(2) или fcntl(2).
Функция имеет вид:
#include <unistd.h>
int close(int fd);
В случае успеха close (2) возвращает нулевое значение, в противном случае возвращается —1. Функция exit(2) автоматически закрывает открытые файлы.
Функцииdup(2) иdup2(2)
Функцияdup(2) используется для дублирования существующего файлового дескриптора:
int dup(int fd);
В случае успешного завершения функции dup(2) возвращается новый файловый деск- риптор, свойства которого идентичны свойствам дескриптора fd. Оба указывают на один и тот же файл, одно и то же смещение, начиная с которого будет производиться следующая операция чтения или записи, и определяют один и тот же режим работы с файлом.
Функция dup2(2) делает то же самое, однако позволяет указать номер файлового дескриптора, который требуется получить после дублирования:
int dup2(int fd, int fd2);
Если дескриптор fd2 уже занят, сначала выполняется функция close(fd2).
Функция lseek(2)
С файловым дескриптором связан файловый указатель, определяющий текущее смещение в файле, начиная с которого будет произведена последующая операция чтения или записи. Каждая операция чтения или записи увеличивают значение файлового указателя на число сщитанных или записаных байт. С помощью функции lseek(2) можно установить файловый указатель на любое место файла.
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
Интерпретация аргумента offset зависит от аргумента whence, который может принимать следующие значения:
SEEK_CUR Указатель смещается на offset байт от текущего положения
SEEK_END Указатель смещается на offset байт от конца файла
SEEK_SET Указатель устанавливается равным offset
В случае успеха функция возвращает положительное целое, равное текущему значению файлового указателя. Смещение, указанное в качестве аргумента lsee k(2), может выходить за пределы файла. В этом случае, последующие операции записи приведут к увеличению размера файла и, в то же время, к образованию дыры — пространства, формально незаполненного данными.
Функцияread(2) иreadv(2)
Функции rеаd(2) иrea d v( 2 ) позволяют считывать данные из файла, на который указывает файловый дескриптор.
Функции имеют следующий вид:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbyte);

#include <sys/types.h>
#include <sys/uio.h>
ssize_t readv(int fd, struct iovec *iov, int iovcnt);
Аргументы, передаваемые функцииrea d( 2 ) , указывают, что следует считать nbyte байт из файла, связанного с дескриптором fds, начиная с текущего значения файлового указателя. Считанные данные помещаются в буфер приложения, указатель на который передается в аргументе buf. После завершения операции значение файлового указателя будет увеличено на nbyte.
Функция readv( 2 ) позволяет выполнить iovcnt последовательных операций чтения за одно обращение к readv( 2 ) . Аргумент iov указывает на массив структур, каждый элемент которого имеет вид:
struct {
void *iov_base; //Указатель на начало буфера
size_t iov_len; //Размер буфера
}iovec;
Функция readv(2) считывает данные из файла и последовательно размещает их в нескольких буферах, определенных массивом iov. Общее число считанных байт в нормальной ситуации равно сумме размеров указанных буферов.
Функции write(2)» writev(2)
Функции write(2) и writev(2) используются для записи данных в файл.
Функции имеют следующий вид:
#include <unistd.h>
ssize_t write(int fd, void *buf, size_t nbyte);

#include <sys/types.h>
#include <sys/uio.h>
ssize_t writev(int fd, struct iovec *iov, int iovcnt);
Аргументы, передаваемые функции write (2), указывают, что следует записать nbyte байт в файл, связанный с дескриптором fd, начиная с текущего значения файлового указателя. Данные для записи находятся в буфере приложения, указанном аргументом buf. После завершения операции значение файлового указателя будет увеличено на nbyte.
Аналогично функции readv(2), функция writev(2) позволяет выполнить iovcnt последовательных операций записи за одно обращение к writev(2).
Функция pipe (2)
Функцияpipe (2) служит для создания однонаправленного (симплексного) канала (также называемого анонимным каналом) обмена данными между двумя родственными процессами. Дело в том, что только родственные процессы (например, родительский и дочерний) имеют возможность получить доступ к одному и тому же каналу.
Функция имеет вид:
#include <unistd.h>
int pipe(int fd[2]);
Функция возвращает два файловых дескриптора в массиве fd [ ], причем fd[0] служит для чтения данных из канала, a fd[1] — для записи данных в канал.
Функцияfcntl (2)
Функцияfcn tl (2) позволяет процессу выполнить ряд действий с файлом, используя его дескриптор, передаваемый в качестве первого аргумента. Это точка доступа к нескольким особым операциям над файлами.
#include <fcntl.h>
int fcntl(int fd, int cmd, ...);
Функция fcntl(2) выполняет действие cmd с файлом, а возможный третий аргумент зависит от конкретного действия.

 

11. Дескрипторы основных потоков.
Стандартная библиотека ввода/вывода
Функции этой библиотеки обеспечивают буферизированный ввод/вывод и более удобный стиль программирования. Для использования функций этой библиотеки в программу должен быть включен файл заголовков< stdio.h>.
Вместо использования файлового дескриптора библиотека определяет указатель на специальную структуру данных (структура FILE), называемый потоком или файловым указателем. Стандартные потоки ввода/вывода обозначаются символическими именами stdin, stdout, stderr соответственно для потоков ввода, вывода и сообщений об ошибках.
Они определены следующим образом:
extern FILE *stdin; 0
extern FILE *stdout; 1
extern FILE *stderr; 2

12. Структура dirent.
dirent - входная структура каталога, независимая от файловой системы
Содержится в:
#include <sys/types.h>
#include <sys/dirent.h>
#include <dirent.h>
Различные файловые системы могут иметь различные входные структуры каталогов. Структура dirent определяет структуру католога, независящую от файловой системы, которая содержит информацию, общую для входных структур каталогов в различных типах файловых систем. Набор этих структур возвращается в результате системного вызова getdents(2).
Структура dirent определена ниже.
struct dirent {
long      d_ino;
off_t     d_off;
unsigned short d_reclen;
char      d_name[1];
};
Здесь d_ino - это номер, уникальный для каждого файла файловой системы. Поле d_off содержит смещение этой структуры каталога в действительном каталоге файловой системы. Поле d_name содержит начало символьного массива, дающего имя этой структуры каталога. Это имя завершается нулем и может содержать самое большее MAXNAMLEN символов. Это реализует структуры каталогов, независимые от файловой системы и являющиеся конструкциями с переменной длиной. Значение d_reclen задает длину записи этой структуры. Эта длина определяется количеством байтов между текущей и следующей структурами, так что следующуя структура оказывается на границе длинного типа.

13. Структура stat, семейство системных вызовов stat.
При программировании на языке C информацию о файлах получают с помощью функций семейства stat(): #include <sys/stat.h>
int stat (const char *restrict path, struct stat *restrict buf);
#include <sys/stat.h>
int fstat (int fildes, struct stat *buf);
#include <sys/stat.h>
int lstat (const char *restrict path, struct stat *restrict buf);
Обратим внимание на использование в описании функций stat() и lstat() ключевого слова restrict из репертуара C99. Оно означает, что по указателям-аргументам доступ осуществляется к непересекающимся областям памяти. Подобная спецификация расширяет оптимизационные возможности компилятора.
Функция stat() предоставляет информацию о поименованном файле: аргумент path указывает на маршрутное имя файла. Чтобы получить эти сведения, достаточно иметь право на поиск для всех компонентов маршрутного префикса. Функция fstat() сообщает данные об открытом файле, задаваемом дескриптором файла fildes. Функция lstat() эквивалентна stat() за одним исключением: если аргумент path задает символьную ссылку, lstat() возвращает информацию о ней, а stat() - о файле, на который эта ссылка указывает.
В случае нормального завершения результат функций семейства stat() равен 0.
Аргумент buf является указателем на структуру типа stat, в которую помещается информация о файле. Согласно стандарту POSIX-2001, в ней содержатся по крайней мере следующие поля:
dev_t st_dev; /* Идентификатор устройства, содержащего файл */
ino_t st_ino; /* Порядковый номер файла в файловой системе */
mode_t st_mode; /* Режим файла */
nlink_t st_nlink; /* Число жестких ссылок на файл */
uid_t st_uid; /* Идентификатор владельца файла */
gid_t st_gid; /* Идентификатор владеющей группы */
off_t st_size; /* Для обычных файлов и символьных ссылок - размер в байтах */
/* Для файлов других типов значение этого поля неспецифицировано */
time_t st_atime; /* Время последнего доступа */
time_t st_mtime; /* Время последнего изменения файла */
time_t st_ctime; /* Время последнего изменения статуса файла */
Некоторые пояснения. Комбинация значений (st_dev, st_ino) должна однозначно определять файл в пределах объединенной (в том числе сетевой) файловой системы. Статус файла меняется, когда модифицируются его атрибуты (например, режим), а не содержимое.
В файле <sys/stat.h> определена не только структура stat, но и константы, полезные для работы с битами режима. Так, S_IFMT выделяет тип файла, S_IFREG обозначает обычные файлы, S_IRWXU - биты режима доступа владельцаи т.д.

14. Межпроцессное взаимодействие (неименованные каналы, именованные каналы, очереди сообщений).
Межпроцессное взаимодействие (Inter-Process Communication) — набор способов обмена данными между множеством потоков в одном или более процессах. Процессы могут быть запущены на одном или более компьютерах, связанных между собой сетью.
Наличие в Unix-системах простых и эффективных средств взаимодействия между процессами играет очень большую роль в системном программировании. Благодаря межпроцессному взаимодействию программист может разбить решение какой-либо задачи на несколько более простых операций, каждая из которых будет реализована отдельной небольшой программой.
Работать с одной большой программой зачастую сложнее, а ее модернизация иногда бывает и вовсе невозможна. Ведь вполне может возникнуть ситуация, когда изменения, которые необходимо внести в программу потребуют ее полного изменения. Именно поэтому целесообразно разбивать ее на некоторое количество функциональных блоков.
Сразу же, конечно, встанет вопрос о взаимодействии и передаче данных между этими блоками. И эта задача достаточно просто решается в Unix-системах с помощью каналов.
Вы уже знакомы с понятием конвейера. Поэтому принцип межпроцессного взаимодействия будет понять достаточно просто. По аналогии с конвейером, данные со стандартного потока вывода одной программы перенаправляются на стандартный поток ввода другой программы, чей стандартный поток вывода может быть также перенаправлен. Но как быть в том случае, если необходимо использовать канал внутри самой программы?
Неименованные каналы.
Один из самых простых способов межпроцессного взаимодействия - внутри-программное использование каналов: программа запускает другую программу и считывает данные, которые запущенная выводит в свой стандартный поток вывода. С помощью этого метода программист может использовать в своей программе функциональность другой программы, не вмешиваясь во внутренние детали ее работы.
Неименованные каналы используются только для передачи данных между процессами, связанными «родственными узами», однако существует возможность использовать их и для передачи данных между совершенно разными процессами.
Для этого воспользуемся механизмом именованных каналов, который позволяет каждому процессу получить свой, собственный дескриптор канала. Передача данных в этих каналах подчиняется принципу FIFO (первым записан - первым прочитан). Именованные каналы отличаются от неименованных наличием имени, то есть идентификатора канала, фактически виден всем процессам системы. Для идентификации именованного канала создается файл специального типа pipe. Файлы именованных каналов являются элементами файловой системы, как и остальные файлы в ОС UNIX, и для них действуют те же права доступа.

15. Организация именованного канала, для взаимодействия процессов.
Передача данных в этих каналах подчиняется принципу FIFO (первым записан - первым прочитан). Именованные каналы отличаются от неименованных наличием имени, то есть идентификатора канала, фактически виден всем процессам системы. Для идентификации именованного канала создается файл специального типа pipe. Файлы именованных каналов являются элементами файловой системы, как и остальные файлы в ОС UNIX, и для них действуют те же права доступа. Файлы именованных каналов создаются функцией mkfifo(3). Первый параметр этой функции - строка, в которой передается имя файла, идентифицирующего канал, второй параметр - маска прав доступа к файлу. Функция mkfifo() создает канал и файл соответствующего типа. Если указанный файл канала уже существует, mkfifo() возвращает -1. После создания файла канала процессы, участвующие в обмене данными, должны открыть этот файл либо для записи, любо для чтения. После закрытия файла канала, файл (и канал) продолжают существовать. Для того, чтобы закрыть сам канал, нужно удалить его файл, например с помощью функции unlink(2).
Рассмотрим работу именованного канала на примере простейшей модели типа клиент- сервер. Программа-сервер создает канал и передает в него текст, вводимый пользователем с клавиатуры. Программа-клиент читает этот текст и выводит его на терминал.
Исходный текст серверной части:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#define fifo "./fifo" //объявление имени именованного канала
int main(int argc, char * argv[]) {
FILE * file_fifo; //структура для работы с файлом
char ch;
mkfifo(fifo, 0700); //создание канала, с именем fifo, с маской прав доступа 0700
file_fifo = fopen(fifo, "w"); //открытие канала на запись
if (file_fifo == NULL) //обработка ошибки открытия
{
perror(argv[0]);
return -1;
}
do {
ch = getchar(); //считать символ
fputc(ch, file_fifo); //запись в канал
if (ch == 10) fflush(file_fifo);//принудительная очистка буферов канала, в результате чего клиент считывает все переданные символы. } while (ch != 'q'); //ввод символов до символа 'q'
fclose(file_fifo); //закрытие файла
unlink(fifo); //удаление канала
return 0; }
Исходный текст клиентской части:
#include <stdio.h>
#define fifo "./fifo"
int main () {
FILE * file_fifo; //структура для работы с файлом
char ch;
file_fifo = fopen(fifo, "r"); //открытие канала на чтение
do {
ch = fgetc(file_fifo); //получение символа из канала
putchar(ch); //вывод этого символа на экран } while (ch != 'q'); //до тех пор пока, символ не 'q'
fclose(file_fifo); //закрытие файла
unlink(fifo); //удаление канала
return 0; }
Для краткости значение, возвращенное mkfifo(), на предмет ошибок не проверяется. В результате вызова mkfifo() с заданными параметрами в рабочей директории программы должен появиться специальный файл fifo. Далее в программе-сервере мы просто открываем созданный файл для записи:
mkfifo(fifo, 0700); //создание канала, с именем fifo, с маской прав доступа 0700
file_fifo = fopen(fifo, "w"); //открытие канала на запись
Считывание данных, вводимых пользователем, выполняется с помощью getchar(), а с помощью функции fputc() данные передаются в канал. Работа сервера завершается, когда пользователь вводит символ ?q?. Клиент открывает файл fifo для чтения как обычный файл.
Символы, передаваемые по каналу, считываются с помощью функции fgetc() и выводятся на экран терминала с помощью putchar(). Каждый раз, когда пользователь наживает ввод, функция fflush(), вызываемая сервером, выполняет принудительную очистку буферов канала, в результате чего клиент считывает все переданные символы. Получение символа ‘q’ завершает работу клиента.

16. Организация неименованного канала, для взаимодействия процессов.
Неименованный канал является средством взаимодействия между связанными процессами - родительским и дочерним. Родительский процесс создает канал при помощи системного вызова:
int pipe(int fd[2]);
Массив из двух целых чисел является выходным параметром этого системного вызова. Если вызов выполнился нормально, то этот массив содержит два файловых дескриптора. fd[0] является дескриптором для чтения из канала, fd[1] - дескриптором для записи в канал. Когда процесс порождает другой процесс, дескрипторы родительского процесса наследуются дочерним процессом, и, таким образом, прокладывается трубопровод между двумя процессами. Естественно, что один из процессов использует канал только для чтения, а другой - только для записи (сами представьте себе, что произойдет, если это правило будет нарушаться). Поэтому, если, например, через канал должны передаваться данные из родительского процесса в дочерний, родительский процесс сразу после запуска дочернего процесса закрывает дескриптор канала для чтения, а дочерний процесс закрывает дескриптор для записи. Если нужен двунаправленный обмен данными между процессами, то родительский процесс создает два канала, один из которых используется для передачи данных в одну сторону, а другой - в другую. После получения процессами дескрипторов канала для работы с каналом используются файловые системные вызовы:
int read(int pipe_fd, void *area, int cnt);
int write(int pipe_fd, void *area, int cnt);
Первый аргумент этих вызовов - дескриптор канала, второй - указатель на область памяти, с которой происходит обмен, третий - количество байт. Оба вызова возвращают число переданных байт (или -1 - при ошибке).
Выполнение этих системных вызовов может переводить процесс в состояние ожидания. Это происходит, если процесс пытается читать данные из пустого канала или писать данные в переполненный канал. Процесс выходит из ожидания, когда в канале появляются данные или когда в канале появляется свободное место, соответственно.
При завершении использования канала процесс выполняет системный вызов:
int close(int pipe_fd);
Если родительский процесс, создавший канал, порождает несколько дочерних процессов, то все дочерние процессы подключены к другому концу канала. Если, например, родительский процесс выводит данные в канал, то они "достанутся" тому дочернему процессу, который раньше выполнит системный вызов read.

17. Fork()
Для обмена данными с внешним приложением функция popen() использует каналы неявным образом. Однако есть возможность использовать каналы и непосредственно. Наиболее распространенный тип каналов - неименованные однонаправленные каналы создаваемые функцией pipe(2). Для программиста такой канал представляется двумя дескрипторами файлов, один из которых служит для чтения данных, а другой - для записи. Каналы не поддерживают произвольный доступ, т. е. данные могут считываться только в том же порядке, в котором они записывались. Неименованные каналы используются преимущественно вместе с функцией fork(2) и служат для обмена данными между родительским и дочерним процессами. Для организации подобного обмена данными, сначала, с помощью функции pipe(), создается канал. Функции pipe() передается единственный параметр - массив типа int, состоящий из двух элементов. В первом элементе массива функция возвращает дескриптор файла, служащий для чтения данных из канала, а во втором - дескриптор для записи. Затем, с помощью функции fork() процесс ?раздваивается?. Дочерний процесс наследует от родительского процесса оба дескриптора, открытых с помощью pipe(), но, также как и родительский процесс, он должен использовать только один из дескрипторов. Направление передачи данных между родительским и дочерним процессом определяется тем, какой дескриптор будет использоваться родительским процессом, а какой - дочерним.
Пример использования функций fork() и pipe():
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#define BUF_SIZE 1024
int main (int argc, char * argv[]) {
int descriptors[2]; //оба дескриптора канала; 0 ? для чтения, 1 ? для записи
int pid; //переменная для вызова fork()
pipe(descriptors); //возврат дескрипторов файлов для чтения и данных из канала
// и записи данных в канал
pid = fork(); //вызов функции ?раздвоения? процесса на родительский и дочерний
if ( pid > 0 ) //родительский процесс
{
char symb[] = "Hello!\n";
int length=sizeof(symb); // размер массива символов
close(descriptors[0]); //закрытие дескриптора для чтения
write(descriptors[1], symb, length + 1); //запись строки в файл, по дескриптору для записи
close(descriptors[1]); //разрыв связи с дескриптором для записи
} else //дочерний процесс
{
char buf[BUF_SIZE];
int len;
close(descriptors[1]); //закрытие дескриптора для записи
while ((len = read(descriptors[0], buf, BUF_SIZE)) != 0) //чтение файла, куда была была записана строка
write(2, buf, len); //вывод строки на экран
close(descriptors[0]); //разрыв связи с дескриптором чтения
} return 0; //код возврата }
Оба дескриптора канала хранятся в массиве descriptors. После вызова fork() процесс раздваивается и родительский процесс (тот, в котором fork() вернула ненулевое значение, равное, кстати, PID дочернего процесса) закрывает дескриптор, открытый для чтения, и записывает данные в канал, используя дескриптор, открытый для записи (descriptors[1]). Дочерний процесс (в котором fork() вернула 0) первым делом закрывает дескриптор, открытый для записи, и затем считывает данные из канала, используя дескриптор, открытый для чтения (descriptors[0]).

18. Семейство функций exec.
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg , ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
Семейство функций exec заменяет текущий образ процесса новым образом процесса. Функции, описанные на этой странице руководства, являются образом функции execve(2). Более детальную информацию о смене текущего процесса можно получить со страниц руководства, описывающих функции execve.
Начальным параметром этих функций будет являться полное имя файла, который необходимо исполнить.
Параметр const char *arg и аналогичные записи в функциях execl, execlp, и execle подразумевают параметры arg0, arg1, ..., argn. Все вместе они описывают один или нескольких указателей на строки, заканчивающиеся NULL, которые представляют собой список параметров, доступных исполняемой программе. Первый параметр, по соглашению, должен указывать на имя, ассоциированное с файлом, который надо исполнить. Список параметров должен заканчиваться NULL.
Функции execv и execvp предоставляют процессу массив указателей на строки, заканчивающиеся null. Эти строки являются списком параметров, доступных новой программе. Первый аргумент, по соглашению, должен указать на имя, ассоциированное с файлом, который необходимо исполнить. Массив указателей должен заканчиваться NULL.
Функция execle также определяет окружение исполняемого процесса, помещая после указателя NULL, заканчивающего список параметров (или после указателя на массив), argv дополнительного параметра. Этот дополнительный параметр является массивом уазателей на строки, завершаемые null, и должен заканчиваться указателем NULL. Другие функции извлекают окружение нового образа процесса из внешней переменной environ текущего процесса.
Некоторые из этих функций имеют особую семантику.
Функции execlp и execvp дублируют действия оболочки, относящиеся к поиску исполняемого файла, если указанное имя файла не содержит символ черты (/). Путь поиска определяется в окружении переменной PATH . Если эта переменная не определена, то используется путь поиска ":/bin:/usr/bin" по умолчанию. Дополнительно обрабатываются некоторые ошибки.
Если запрещен доступ к файлу (при попытке исполнения execve была возвращена ошибка EACCES), то эти функции будут продолжать поиск вдоль оставшегося пути. Если не найдено больше никаких файлов, то по возвращении они установят значение глобальной переменной errno равным EACCES.
Если заголовок файла не распознается (при попытке выполнения функции execve была возвращена ошибка ENOEXEC), то эти функции запустят оболочку (shell) с полным именем файла в качестве первого параметра. (Если и эта попытка будет неудачна, то дальнейший поиск не производится.)
Возвращение значения какой-либо из функций exec приведет к ошибке. При этом возвращаемым значением будет -1 и глобальной переменной errno будет присвоен код соответствующей ошибки. Поведение функций execlp и execvp при ошибках во время попыток исполнения файла сложилось исторически, но при этом не описанно и не определено в стандарте POSIX. BSD (и, возможно, другие системы) выполняют системное ожидание и повтор в случае встретившейся ошибки ETXTBSY. Linux воспринимает это как серьезную ошибку и немедленно ее возвращает.

 

19. Функция popen().
Чтобы избавить программиста от излишнего кода, для работы с трубами введена функция popen(). Синтаксис этой функции:
FILE *popen(const char* command, const char* mode);
Параметр command соответствует системному вызову команды, которая выполняет некоторую программу. Между процессом пользователя и вызванным процессом устанавливается труба для обмена информацией. Режим работы трубы устанавливается параметром mode. "r" означает чтение из трубы, а "w" - запись в трубу. В случае ошибки создания трубы popen() возвращает NULL, иначе возвращается указатель файла. Таким образом, функция popen() выполняет следующие действия:
Вызов pipe():
Вызов fork() (Создание процесса-потомка);
Закрытие ненужных дескрипторов в родителе и потомке;
Замену процесса-потомка с помощью exec.
Пример использования popen():
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#define EXIT(s) {fprintf(stderr, "%s",s); exit(0);}
#define USAGE(s) {fprintf(stderr, "%s Данные для чтения\n",s); exit(0);}
#define MAX 8192
enum {ERROR=-1,SUCCESS};

int main(int argc, char **argv) {
FILE *pipe_writer, *file;
char buffer[MAX];
if(argc!=2) USAGE(argv[0]);
if((file=fopen(argv[1], "r")) == NULL) EXIT("Ошибка открытия файла.........\n");
if(( pipe_writer=popen("./filter" ,"w")) == NULL) EXIT("Ошибка открытия трубы...........\n");
while(1) {
if(fgets(buffer, MAX, file) == NULL) break;
if(fputs(buffer, pipe_writer) == EOF) EXIT("Ошибка записи........\n"); }
pclose(pipe_writer); }

20. Принципиальные различия, между popen и exec*().
1. popen - это библиотечная функция, а exec - системный вызов
2. popen создает новый процесс, а exec заменяет текущий
3. popen запускает сначала оболочку и выполняет в ней команду любой сложности, а exec запускает конкретный исполнимый файл
4. popen требует наличия файла /bin/sh, а exec может выполняться на голой системе (например, если нет ничего, кроме cygwin.dll )

21. Функция pipe() и массив дескрипторов (реализация механизма)
Функция pipe(2) служит для создания однонаправленного (симплексного) канала (также называемого анонимным каналом) обмена данными между двумя родственными процессами. Дело в том, что только родственные процессы (например, родительский и дочерний) имеют возможность получить доступ к одному и тому же каналу.
Функция имеет вид:
#include <unistd.h>
int pipe(int fd[2]);
Функция возвращает два файловых дескриптора в массиве fd [ ], причем fd[0] служит для чтения данных из канала, a fd[1] — для записи данных в канал.
Массив из двух целых чисел является выходным параметром этого системного вызова. Если вызов выполнился нормально, то этот массив содержит два файловых дескриптора. fd[0] является дескриптором для чтения из канала, fd[1] - дескриптором для записи в канал. Когда процесс порождает другой процесс, дескрипторы родительского процесса наследуются дочерним процессом, и, таким образом, прокладывается трубопровод между двумя процессами. Естественно, что один из процессов использует канал только для чтения, а другой - только для записи (сами представьте себе, что произойдет, если это правило будет нарушаться). Поэтому, если, например, через канал должны передаваться данные из родительского процесса в дочерний, родительский процесс сразу после запуска дочернего процесса закрывает дескриптор канала для чтения, а дочерний процесс закрывает дескриптор для записи. Если нужен двунаправленный обмен данными между процессами, то родительский процесс создает два канала, один из которых используется для передачи данных в одну сторону, а другой - в другую.

22. FIFO. Реализация механизма.
FIFO — акроним First In, First Out («первым пришёл — первым ушёл», англ. ), абстрактное понятие в способах организации и манипулирования данными относительно времени и приоритетов. Это выражение описывает принцип технической обработки очереди или обслуживания конфликтных требований путём упорядочения процесса по принципу: “первым пришёл — первым обслужен”. Тот, кто приходит первым, тот и обслуживается первым, пришедший следующим ждёт, пока обслуживание первого не будет закончено, и т.д.
С помощью труб могут общаться только родственные друг другу процессы, полученные с помощью fork(). Именованные каналы FIFO дают возможность обмена данными с абсолютно чужим процессом.
С точки зрения ядра ОС FIFO является одним из вариантов реализации трубы. Системный вызов mkfifo() предоставляет именованную трубу в виде объекта файловой системы. Как и для любого другого объекта, необходимо предоставлять процессам права доступа в FIFO, чтобы определить, кто может писать что-либо в FIFO, и кто может читать из нее. Несколько процессов могут записывать или читать FIFO одновременно. Режим работы с FIFO - полудуплексный, т.е. процессы могут общаться в одном направлении. Типичное применение FIFO - разработка приложений клиент-сервер.
Синтаксис функции для создания FIFO следующий:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *fifoname, mode_t mode);
При возникновении ошибки функция возвращает -1, в противном случае 0. В качестве первого параметра указывается путь, где будет располагаться FIFO. Второй параметр определяет режим работы с FIFO. Пример использования:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
int fd_fifo; /*дескриптор FIFO*/
char buffer[]="Текстовая строка для fifo\n";
char buf[100];
/*Если файл с таким именем существует, удалим его*/
unlink("/tmp/fifo0001.1");
/*Создаем FIFO*/
if((mkfifo("/tmp/fifo0001.1", O_RDWR)) == -1) {
fprintf(stderr, "Невозможно создать fifo.........\n");
exit(0); }
/*Открываем fifo для чтения и записи*/
if((fd_fifo=open("/tmp/fifo0001.1", O_RDWR)) == - 1) {
fprintf(stderr, "Невозможно открыть fifo.....\n");
exit(0); }
write(fd_fifo,buffer,strlen(buffer)) ;
if(read(fd_fifo, &buf, sizeof(buf)) == -1) fprintf(stderr, "Невозможно прочесть из FIFO.......\n");
else printf("Прочитано из FIFO : %s\n",buf);
return 0; }
Если в системе отсутствует функция mkfifo(), можно воспользоваться общей функцией для создания файла
int mknod(char *pathname, int mode, int dev);
Pathname указывает обычное имя каталога Unix и имя FIFO. Режим указывается константой S_IFIFO из заголовочного файла <sys/stat.h>. Здесь же указываются права доступа. Параметр dev не нужен. Пример вызова mknod:
if(mknod("/tmp/fifo0001.1", S_IFIFO | S_IRUSR | S_IWUSR, 0) == - 1) { /*Невозможно создать fifo */ }
Если при открытии FIFO через open не указать режим O_NONBLOCK, открытие FIFO блокируется и для записи, и для чтения. При записи канал блокируется до тех пор, пока другой процесс не откроет FIFO для чтения.При чтении канал снова блокируется до тех пор, пока другой процесс не запишет в FIFO.
Флаг O_NONBLOCK может использоваться только при доступе для чтения. При попытке открыть FIFO с O_NONBLOCK для записи возникает ошибка открытия. Если FIFO закрыть для записи через close или fclose, это значит, что для чтения в FIFO помещается EOF.
Если несколько процессов пишут в один и тот же FIFO, необходимо обратить внимание на то, чтобы сразу не записывалось больше чем PIPE_BUF байтов. Это необходимо, чтобы данные не смешивались друг с другом. Установить пределы записи можно следующей программой:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
unlink("fifo0001");
/*Создаем новый FIFO*/
if((mkfifo("fifo0001", O_RDWR)) == -1) {
fprintf(stderr, "Невозможно создать FIFO\n");
exit(0); }
printf("Можно записать в FIFO сразу %ld байтов\n", pathconf("fifo0001", _PC_PIPE_BUF));
printf("Одновременно можно открыть %ld FIFO \n", sysconf(_SC_OPEN_MAX));
return 0; }
При попытке записи в FIFO, который не открыт в данный момент для чтения ни одним процессом, генерируется сигнал SIGPIPE.

23. Многопоточность.
Многопоточность по своей сути, является прямым решением обеспечения многозадачности и представляет собой логическое развитие концепции разделения ресурсов. В рамках неформального, но простого, определения, поток - это выполнение последовательности машинных инструкций. В многопоточном приложении одновременно работает несколько потоков. Иногда вместо термина “поток” используется термин “нить”, для того, чтобы потоки программ не путались с потоками ввода, вывода и ошибок.
Прежде чем приступать к программированию потоков, следует ответить на вопрос, а нужны ли они вам. Потоки часто становятся источниками программных ошибок особого рода. Эти ошибки возникают при использовании потоками разделяемых ресурсов системы (например, общего адресного пространства) и являются частным случаем более широкого класса ошибок - ошибок синхронизации. Если задача разделена между независимыми процессами, то доступом к их общим ресурсам управляет операционная система, и вероятность ошибок из-за конфликтов доступа снижается. Впрочем, разделение задачи между несколькими независимыми процессами само по себе не защитит вас от других разновидностей ошибок синхронизации. В пользу потоков можно указать то, что накладные расходы на создание нового потока в многопоточном приложении ниже, чем накладные расходы на создание нового самостоятельного процесса. Уровень контроля над потоками в многопоточном приложении выше, чем уровень контроля приложения над дочерними процессами. Кроме того, многопоточные программы не оставляют за собой множество зомби, и “осиротевших” процессов.

24. Поток, функция потока.
В ОС Unix каждый поток является процессом, и для того, чтобы создать новый поток, нужно создать новый процесс. В чем же, в таком случае, заключается различие многопоточности перед многопроцессноси? В многопоточных приложениях для создания дополнительных потоков используются процессы особого типа. Эти процессы представляют собой обычные дочерние процессы главного процесса, но они разделяют с главным процессом адресное пространство, файловые дескрипторы и обработчики сигналов. Для обозначения процессов этого типа, применяется специальный термин - легкие процессы . Прилагательное “легкий” в названии процессов- потоков вполне оправдано. Поскольку этим процессам не нужно создавать собственную копию адресного пространства (и других ресурсов) своего процесса- родителя, создание нового легкого процесса требует значительно меньших затрат, чем создание полновесного дочернего процесса.
У каждого процесса есть идентификатор. Есть он и у процессов-потоков. Но спецификация POSIX требует, чтобы все потоки многопоточного приложения имели один идентификатор. Вызвано это требование тем, что для многих функций системы многопоточное приложение должно представляться как один процесс с одним идентификатором. Проблема единого идентификатора решается следующим образом. Процессы многопоточного приложения группируются в группы потоков. Группе присваивается идентификатор, соответствующий идентификатору первого процесса многопоточного приложения. Именно этот идентификатор группы потоков используется при ?общении? с многопоточным приложением. Функция getpid(2), возвращает значение идентификатора группы потока, независимо от того, из какого потока она вызвана. Функции kill() waitpid() и им подобные по умолчанию также используют идентификаторы групп потоков, а не отдельных процессов. Узнавать собственный идентификатор процесса-потока требуется очень редко, но если надо это сделать, то используется функция gettid(2).
Потоки создаются функцией pthread_create(3), определенной в заголовочном файле <pthread.h>. Первый параметр этой функции представляет собой указатель на переменную типа pthread_t, которая служит идентификатором создаваемого потока. Второй параметр, указатель на переменную типа pthread_attr_t, используется для передачи атрибутов потока. Третьим параметром функции pthread_create() должен быть адрес функции потока. Эта функция играет для потока ту же роль, что функция main() ? для главной программы. Четвертый параметр функции pthread_create() имеет тип void *. Этот параметр может использоваться для передачи значения, возвращаемого функцией потока. Вскоре после вызова pthread_create() функция потока будет запущена на выполнение параллельно с другими потоками программы. Таким образом, собственно, и создается новый поток. Говорят, что новый поток запускается, вскоре, после вызова pthread_create() потому, что перед тем как запустить новую функцию потока, нужно выполнить некоторые подготовительные действия, а поток-родитель между тем продолжает выполняться. Если в ходе создания потока возникла ошибка, функция pthread_create() возвращает ненулевое значение, соответствующее номеру ошибки.
Функция потока должна иметь заголовок вида:
void * func_name(void * arg)
Имя функции, естественно, может быть любым. Аргумент arg, - это тот самый указатель, который передается в последнем параметре функции pthread_create(). Функция потока может вернуть значение, которое затем будет проанализировано заинтересованным потоком, но это не обязательно. Завершение функции потока происходит если:
функция потока вызвала функцию pthread_exit(3);
функция потока достигла точки выхода;
поток был досрочно завершен другим потоком.
Функция pthread_exit() представляет собой потоковый аналог функции exit(). Аргумент функции pthread_exit(), значение типа void *, становится возвращаемым значением функции потока. Как (и кому?) функция потока может вернуть значение, если она не вызывается из программы явным образом? Для того, чтобы получить значение, возвращенное функцией потока, нужно воспользоваться функцией pthread_join(3). У этой функции два параметра. Первый параметр pthread_join(), ? это идентификатор потока, второй параметр имеет тип ?указатель на нетипизированный указатель?. В этом параметре функция pthread_join() возвращает значение, возвращенное функцией потока. Основная же задача функции pthread_join() заключается, в синхронизации потоков. Вызов функции pthread_join() приостанавливает выполнение вызвавшего ее потока до тех пор, пока поток, чей идентификатор передан функции в качестве аргумента, не завершит свою работу. Если в момент вызова pthread_join() ожидаемый поток уже завершился, функция вернет управление немедленно. Функцию pthread_join() можно рассматривать как эквивалент waitpid(2) для потоков. Эта функция позволяет вызвавшему ее потоку дождаться завершения работы другого потока. Попытка выполнить более одного вызова pthread_join() (из разных потоков) для одного и того же потока приведет к ошибке.

25. Семафоры, мьютексы.
Семафоры - счетчики, которые разрешают синхронизировать множественные потоки.
//gcc semaphore.c -D_REENTERANT -I/usr/include/nptl -o semaphore -L/usr/lib/nptl -lpthread (Используйте эту строчку для компиляции в ОС Linux)
//gcc semaphore.c -D_REENTERANT -I/usr/include/nptl -o semaphore -L/usr/lib/nptl -lrt -lthread (Используйте эту строчку для компиляции в ОС Solaris)
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <pthread.h>//заголовочный файл для работы с потоками
#include <semaphore.h>//заголовочный файл для работы с семафорами
sem_t sem; //объявление семафора
void * thread_func(void *arg) {
int i;
int id = * (int *) arg;
sem_post(&sem);//инкремент значения семафора
for (i = 0; i < 4; i++) {
printf("Hello from thread %d\n", id);
sleep(1); }}
int main(int argc, char * argv[]) {
int id=1;
int result;
pthread_t thread1, thread2;
sem_init(&sem, 0, 0);//инициализация семафора, со значением 0
result = pthread_create(&thread1, NULL, thread_func, &id);
if (result != 0) {
perror("While creating thread");
return 1; }
sem_wait(&sem);//приостановление выполнения функции main() до тех пор, пока функция потока не вызовет функцию sem_post()
id = 2;
result = pthread_create(&thread2, NULL, thread_func, &id);
if (result != 0) {
perror("While creating thread");
return 1; }
result = pthread_join(thread1, NULL);
if (result != 0) {
perror("While joining thread");
return 2; }
result = pthread_join(thread2, NULL);
if (result != 0) {
perror("While joining thread");
return 2; }
sem_destroy(&sem); //удаление семафора, освобождение его ресурсов
printf("Done\n");
return 0; }
В новом варианте программы мы используем одну переменную id для передачи значения обоим потокам. Для синхронизации потоков мы задействовали семафоры. Семафоры инициализируются с помощью функции sem_init(3). Первый параметр функции sem_init() ? указатель на переменную типа sem_t, которая служит идентификатором семафора. Второй параметр - pshared - в настоящее время не используется, и мы оставим его равным нулю. В третьем параметре функции sem_init() передается значение, которым инициализируется семафор. Дальнейшая работа с семафором осуществляется с помощью функций sem_wait(3) и sem_post(3). Единственным аргументом функции sem_wait() служит указатель на идентификатор семафора. Функция sem_wait() приостанавливает выполнение вызвавшего ее потока до тех пор, пока значение семафора не станет большим нуля, после чего функция уменьшает значение семафора на единицу и возвращает управление. Функция sem_post() увеличивает значение семафора, идентификатор которого был передан ей в качестве параметра, на единицу. Присвоив семафору значение 0, наша программа создает первый поток и вызывает функцию sem_wait(). Эта функция приостановит выполнение функции main() до тех пор, пока функция потока не вызовет функцию sem_post(), а это случится только после того как функция потока обработает значение переменной id. Таким образом, мы можем быть уверены, что в момент создания второго потока первый поток уже закончит работу с переменной id, и мы сможем использовать эту переменную для передачи данных второму потоку. После завершения обоих потоков мы вызываем функцию sem_destroy(3) для удаления семафора и высвобождения его ресурсов.
Семафоры - не единственное средство синхронизации потоков. Для разграничения доступа к глобальным объектам потоки могут использовать мьютексы.
Мьютекс (англ. mutex, от mutual exclusion - взаимное исключение) - одноместный семафор, служащий в программировании для синхронизации одновременно выполняющихся потоков.
Это один из вариантов семафорных механизмов для организации взаимного исключения. Они реализованы во многих ОС, их основное назначение - организация взаимного исключения для потоков из одного и того же или из разных процессов.
Мьютексы - это простейшие двоичные семафоры, которые могут находиться в одном из двух состояний - отмеченном или неотмеченном (открыт и закрыт соответственно). Когда какой-либо поток, принадлежащий любому процессу, становится владельцем объекта mutex, последний переводится в неотмеченное состояние. Если задача освобождает мьютекс, его состояние становится отмеченным.
Организация последовательного доступа к ресурсам с использованием мьютексов становится несложной, поскольку в каждый конкретный момент только один поток может владеть этим объектом. Для того, чтобы мьютекс стал доступен потокам, принадлежащим разным процессам, при создании ему необходимо присвоить имя. Потом это имя нужно передать,по наследству,задачам, которые должны его использовать для взаимодействия. Для этого вводятся специальные системные вызовы (CreateMutex), в которых указывается начальное значение мьютекса и его имя.
Для работы с мьютексом имеется несколько функций. Помимо уже упомянутой функции создания такого объекта (CreateMutex), есть функции открытия (OpenMutex) и функция освобождения этого объекта (ReleaseMutex). Конкретные обращения к этим функциям и перечни передаваемых и получаемых параметров нужно смотреть в документации на соответствующую ОС.
Единственная задача мьютекса -защита объекта от доступа к нему других потоков, отличных от того, который завладел мьютексом. Если другому потоку будет нужен доступ к переменной, защищённой мьютексом, то этот поток просто засыпает до тех пор, пока мьютекс не будет освобождён.
Все функции и типы данных, имеющие отношение к мьютексам, определены в файле pthread.h.
Мьютекс создается вызовом функции pthread_mutex_init(3). В качестве первого аргумента этой функции передается указатель на переменную pthread_mutex_t, которая играет роль идентификатора нового мьютекса. Вторым аргументом функции pthread_mutex_init() должен быть указатель на переменную типа pthread_mutexattr_t. Эта переменная позволяет установить дополнительные атрибуты мьютекса. Если нам нужен обычный мьютекс, мы можем передать во втором параметре значение NULL. Для того чтобы получить исключительный доступ к некоему глобальному ресурсу, поток вызывает функцию pthread_mutex_lock(3), (в этом случае говорят, что ?поток захватывает мьютекс?). Единственным параметром функции pthread_mutex_lock() должен быть идентификатор мьютекса. Закончив работу с глобальным ресурсом, поток высвобождает мьютекс с помощью функции pthread_mutex_unlock(3), которой также передается идентификатор мьютекса. Если поток вызовет функцию pthread_mutex_lock() для мьютекса, уже захваченного другим потоком, эта функция не вернет управление до тех пор, пока другой поток не высвободит мьютекс с помощью вызова pthread_mutex_unlock() (после этого мьютекс, естественно, перейдет во владение нового потока). Удаление мьютекса выполняется с помощью функции pthread_mutex_destroy(3). Стоит отметить, что в отличие от многих других функций, приостанавливающих работу потока, вызов pthread_mutex_lock() не является точкой останова. Иначе говоря, поток, находящийся в режиме отложенного досрочного завершения, не может быть завершен в тот момент, когда он ожидает выхода из pthread_mutex_lock().

26. Реализация многопоточности.
/*gcc thr.c -D_REENTERANT -I/usr/include/nptl -o thr -L/usr/lib/nptl -lpthread - строка для компиляции данного примера
Команда компиляции включает макрос _REENTERANT. Этот макрос указывает, что вместо обычных функций стандартной библиотеки к программе должны быть подключены их реентерабельные аналоги. Реентерабельный вариант библиотеки glibc написан таким образом, что вы, скорее всего, вообще не обнаружите никаких различий в работе с реентерабельными функциями по сравнению с их обычными аналогами. Мы указываем компилятору путь для поиска заголовочных файлов и путь для поиска библиотек /usr/include/nptl и /usr/lib/nptl соответственно. Наконец, мы указываем компоновщику, что программа должна быть связана с библиотекой libpthread, которая содержит все специальные функции, необходимые для работы с потоками.
Компьютерная программа в целом или её отдельная процедура называется реентера?бельной, если она разработана таким образом, что одна и та же копия инструкций программы в памяти может быть совместно использована несколькими пользователями или процессами. При этом второй пользователь может вызвать реентерабельный код до того, как с ним завершит работу первый пользователь и это как минимум не должно привести к ошибке, а в лучшем случае не должно вызвать потери вычислений (то есть не должно появиться необходимости выполнять уже выполненные фрагменты кода). */
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <pthread.h> //заголовочный файл для работы с потоками
void * thread_func(void *arg) //функция потока
{ int i=0;
int id = * (int *) arg; //преобразование переданного параметра в int
for (i = 0; i < 2; i++) {
printf("Hello from thread %d\n", id); //распечатать номер потока
sleep(1); //задержка в одну секунду }}
int main(int argc, char * argv[]) {
int id1=1; int id2=2; int result;
pthread_t thread1, thread2; //идентификаторы создаваемых потоков
id1 = 1;
result = pthread_create(&thread1, NULL, thread_func, &id1); //создание потока
if (result != 0) //обработка ошибки создания
{ //если идентификатор создаваемого потока не равен нулю -> произошла ошибка
perror("While creating thread");
return 1;}
id2 = 2;
result = pthread_create(&thread2, NULL, thread_func, &id2); //создание потока
if (result != 0) {
perror("While creating thread");
return 1;}
result = pthread_join(thread1, NULL); //функция для получения значения, возвращаемого функцией потока
//а так же для синхронизации потоков (возвращаемое значение передается через указатель, в нашем случае
//указатель не определен(NULL), т.к. поток не возвращает значения.
if (result != 0) {
perror("While joining thread");
return 2;}
result = pthread_join(thread2, NULL);//функция для получения значения, возвращаемого функцией потока
if (result != 0) {
perror("While joining thread");
return 2;}
printf("Done\n");
return 0;}
Рассмотрим сначала функцию thread_func(). Это и есть функция потока. В качестве аргумента ей передается указатель на переменную типа int, в которой содержится номер потока. Функция потока распечатывает этот номер несколько раз с интервалом в одну секунду и завершает свою работу. В функции main() есть две переменные типа pthread_t для создания двух потоков с собственными идентификаторами. Также есть две переменные типа int, id1 и id2, которые используются для передачи функциям потоков их номеров. Сами потоки создаются с помощью функции pthread_create().В этом примере мы не модифицируем атрибуты потоков, поэтому во втором параметре в обоих случаях передаем NULL. Вызывая pthread_create() дважды, мы оба раза передаем в качестве третьего параметра адрес функции thread_func, в результате чего два созданных потока будут выполнять одну и ту же функцию. Функция, вызываемая из нескольких потоков одновременно, должна обладать свойством реентерабельности. Реентерабельная функция, это функция, которая может быть вызвана повторно, в то время, когда она уже вызвана (отсюда и происходит ее название). Реентерабельные функции используют локальные переменные (и локально выделенную память) в тех случаях, когда их не-реентерабельные аналоги могут воспользоваться глобальными переменными.
Мы вызываем последовательно две функции pthread_join() для того, чтобы дождаться завершения обоих потоков. Если мы хотим дождаться завершения всех потоков, порядок вызова функций pthread_join() для разных потоков, очевидно, не имеет значения.
Конечно, возникает вопрос, зачем использовали две разные переменные, id1 и id2, для передачи значений двум потокам? Почему нельзя использовать одну переменную, скажем id, для обоих потоков? Рассмотрим такой кусок кода:
id = 1; pthread_create(&thread1, NULL, thread_func, &id);
id = 2; pthread_create(&thread2, NULL, thread_func, &id);
Конечно, в этом случае оба потока получат указатель на одну и ту же переменную, но ведь значение этой переменной нужно каждому потоку только в самом начале его работы. После того, как поток присвоит это значение своей локальной переменной id, ничто не мешает нам использовать ту же переменную id для другого потока. Все это верно, но проблема заключается в том, что мы не знаем, когда первый поток начнет свою работу. То, что функция pthread_create() вернула управление, не гарантирует нам, что поток уже выполняется. Вполне может случиться так, что первый поток будет запущен уже после того, как переменной id будет присвоено значение 2. Тогда оба потока получат одно и то же значение id. Впрочем, мы можем использовать одну и ту же переменную для передачи данных функциям потока, если воспользуемся средствами синхронизации.


Дата добавления: 2018-04-04; просмотров: 397; Мы поможем в написании вашей работы!

Поделиться с друзьями:






Мы поможем в написании ваших работ!