Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Учеб.пос.СП.doc
Скачиваний:
28
Добавлен:
31.03.2015
Размер:
1.33 Mб
Скачать
    1. Получение и изменение приоритета

Каждому процессу назначается значение параметра nice, посредством которого процесс может влиять на уровень своего приоритета. Чем выше параметр nice, тем более вежлив и тактичен процесс по отношению к другим и тем ниже его приоритет. Меньшее значение nice означает более грубое поведение процесса и более высокий его приоритет. Параметр nice представляет собой положительное значение, обычно число 20 и является смещением от некоторого числа, которое зависит от системы. Процесс стартует с параметром nice равным 20 и может стать как очень дружелюбным, задав параметр nice равным 39, так и совершенно беспардонным, задав параметр nice равным 0.

Для того, чтобы изменить свой приоритет, процесс обращается к системному вызову nice:

nice – изменяет значение параметра nice

#include <unistd.h>

int nice(

int incr /* приращение */

);

/* Возвращает новое значение nice - NZERO или -1 в случае ошибки (код ошибки - в переменной errno)*/

Системный вызов nice добавляет приращение incr к текущему значению параметра nice. Результат должен получиться в диапазоне от 0 до 39 включительно. Если он вышел за пределы указанного диапазона, используется ближайшее допустимое значение. Только суперпользователь может уменьшить значение параметра nice, повысив тем самым приоритет обслуживания.

Контрольные вопросы

  1. Объясните основные принципы реализации системных вызовов fork и exec.

  2. Исследуйте особенности системных вызовов, эквивалентных fork и exec, в других операционных системах.

  3. Перечислите основные различия в использовании системных вызовов wait и waitpid.

  4. Опишите системные вызовы, которые завершают процессы.

  5. Опишите системные вызовы, которые занимаются запуском программ.

Литература

  1. Глас Г., Эйблс К. Unix для программистов и пользователей. / Г. Глас, К. Эйблс – СПб.: БХВ-Петербург, 2004. – 848 с.: ил.

  2. Брюс М. Unix/Linux: Теория и практика программирования. / М.Брюс – Издательство: "Кудиц-Образ", 2004. -576 с.

  3. Роббинс А. Linux. Программирование в примерах. / А. Роббинс. - Издательство: КУДИЦ-Пресс, 2008. – 656 с.

  4. Собель М.Г. Linux. Администрирование и системное программирование. / М.Г. Собель. - Издательство: Питер, 2011. – 880 с.

Глава 5. Механизмы межпроцессного взаимодействия

Мы умеем создавать процессы, но нам хотелось бы связывать их друг с другом, чтобы они могли взаимодействовать. В этой главе мы будем соединять процессы при помощи каналов, используя базовые методики, которые всегда поддерживались всеми версиями UNIX. Каналы известны большинству пользователей UNIX как возможность командной оболочки. Например, для получения отсортированного списка пользователей, вошедших в систему, можно напечатать следующее:

$ who | sort | more

Здесь мы имеем дело с тремя процессами, соединенными посредством двух каналов. Данные перемещаются только в одном направлении: от who к sort и далее к more.

5.1. Каналы

Рассмотрим неименованные каналы, которые реализуются с помощью системного вызова pipe:

pipe – создает канал

#include <unistd.h>

int pipe (

int pfd[2] /* дескрипторы файлов */

);

/* Возвращает 0 или -1 в случае ошибки (код ошибки - в переменной errno)*/

Системный вызов pipe создает канал взаимодействия между процессами, который представляется двумя дескрипторами файлов, возвращаемыми в массиве pfd. При записи данных в элемент pfd[1] они помещаются в канал, а при чтении из pfd[0] данные извлекаются. В UNIX указан параметр PIPE_BUF, который можно считать размером буфера канала. Если несколько процессов или потоков записывают данные в один канал, операция записи блока, размер которого не превышает PIPE_BUF байтов, непременно будет атомарной, то есть данные, записанные в канал разными процессами или потоками, не будут чередоваться. Это очень важно, если несколько процессов записывают в канал структурированные данные, так как в противном случае не было бы никакой гарантии, что читатель получит корректные данные. Значение PIPE_BUF не бывает меньше 512, но если во время выполнения нужно узнать действительное значение этого параметра, нужно воспользоваться вызовом fpathconf:

int pfd[2];

long v;

pipe(pfd);

errno = 0;

v = fpathconf(pfd[0], _PC_PIPE_BUF);

if (errno != 0)

EC_FAIL

else if (v == -1)

printf(“No limit for PIPE_BUF\n”);

else

printf(“PIPE_BUF = %ld\n”, v);

При выполнении этого кода FreeBSD вывела 512, а Linux - 4096. Некоторые системные вызовы, служащие для ввода-вывода, работают с каналами не так, как с обычными файлами, а некоторые вообще ничего не делают. Эти особенности отражены в следующем списке:

  • write - данные записываются в канал в порядке поступления. В нормальных условиях (флаг O_NONBLOCK не установлен) при полном канале вызов write блокируется, пока вызов read не освободит достаточно места (частичная запись невозможна). Если флаг O_NONBLOCK установлен, а объем записываемых данных не превышает PIPE_BUF байтов, вызов write или запишет данные немедленно, или возвратит -1, присвоив при этом errno значение EAGAIN (частичная запись невозможна). Но если размер данных превышает PIPE_BUF байтов, частичная запись возможна;

  • read - данные считываются из канала в порядке поступления, как и были записаны. После прочтения данные могут быть прочитаны повторно или возвращены в канал. В нормальных условиях (флаг O_NONBLOCK не установлен) при пустом канале вызов read блокируется, пока не станет доступным хотя бы один байт данных, если при этом закрыты не все дескрипторы файлов, используемые для записи. Если все дескрипторы файлов, используемые для записи, закрыты, вызов read возвратит нулевой счетчик (обычный признак конца файла). Однако число байтов, указанное при вызове read в качестве третьего аргумента, может быть прочитано не всегда. Будет прочитано столько байтов, сколько имеется в наличии на текущий момент, и будет возвращено соответствующее число. Конечно, число байтов, которые нужно прочитать, никогда не будет превышено, а непрочитанные байты останутся в канале, по крайней мере, до следующей операции чтения. Если флаг O_NONBLOCK установлен, вызов read, выполненный для пустого канала, возвратит -1, присвоив при этом errno значение EAGAIN;

  • close - в случае каналов функциональность этого вызова более широка, чем при работе с файлами. Он не только освобождает дескриптор файла для повторного использования, но и играет для читателя роль признака конца файла, если все дескрипторы файлов, используемые для записи, закрыты. Если все дескрипторы файлов, используемые для чтения, закрыты, выполнение вызова write для дескриптора файла, используемого для записи, приведет к ошибке;

  • fstat - при работе с каналами этот вызов не очень полезен и используется разве что для определения того, что дескриптор файла открыт для канала. Возвращаемое число обычно представляет количество байтов в канале.

  • dup – этот системный вызов и вызов dup2 будут рассмотрены позже;

  • lseek – с каналами не используется.

Отношения между атомарной и неатомарной, блокируемой и неблокируемой, а также полной, частичной и отсроченной записью сравнительно сложны. Чтобы лучше разобраться с этим, изучите таблицу 5.1. Два первых столбца определяют состояние флага O_NONBLOCK и объем записываемых данных, а в трех последних столбцах указано, что при этом происходит в случае полного канала, а также канала, который может принять часть данных немедленно, и канала, который может принять все данные.

Таблица 5.1. Запись в канал

O_NONBLOCK

Объем записываемых данных

Канал полон

Канал может немедленно принять часть данных

Канал может немедленно принять все данные

не установлен

<=PIPE_BUF

блокируется

полная запись

атомарная операция

блокируется

полная запись

атомарная операция

не блокируется

полная запись

атомарная операция

не установлен

>PIPE_BUF

блокируется

полная запись

неатомарная операция

блокируется

полная запись

неатомарная операция

может заблокироваться

полная запись

неатомарная операция

установлен

<=PIPE_BUF

EAGAIN

не блокируется

частичная запись или

EAGAIN

неатомарная операция

не блокируется

полная запись

неатомарная операция

установлен

>PIPE_BUF

EAGAIN

EAGAIN

не блокируется

полная или частичная запись или

EAGAIN

неатомарная операция

Под полной записью в таблице понимается то, что вызов write не возвращает управление, пока не будут записаны все данные. Смысл неатомарной операции в том, что все данные помещаются в канал, но не обязательно последовательно (при этом не гарантируется, что первые PIPE_BUF байтов будут расположены последовательно). EAGAIN означает, что вызов write возвращает -1, присваивая при этом errno значение EAGAIN. Под частичной записью понимается то, что в канал записываются не все данные, запись которых запрошена. Во второй строке последнего столбца написано «может заблокироваться» по той причине, что в начале выполнения write канал может принять все данные, но так как запись не является атомарной, другой процесс или поток может заполнить некоторую часть канала до завершения вызова write, что приведет к его блокировке.

Из вышесказанного вытекает:

  • если запрошенный объем записываемых данных не превышает PIPE_BUF байтов, запись всегда атомарна, то есть никогда не выполняется частично;

  • если флаг O_NONBLOCK не установлен запись никогда не выполняется частично, даже если она неатомарна;

  • частичная запись выполняется только тогда, когда установлен флаг O_NONBLOCK, а запрошенный объем превышает PIPE_BUF байтов.

Следующая таблица 5.2. характеризует поведение вызова read.

O_NONBLOCK

Данные, которые можно немедленно прочитать, отсутствуют

Данные можно сразу же прочитать полностью или частично

не установлен

блокируется, если нет писателей (возвращается 0)

не блокируется

возможно частичное чтение

установлен

EAGAIN, если нет писателей (возвращается 0)

не блокируется

возможно частичное чтение

Вторая таблица гораздо проще, чем первая, потому что атомарность или полнота чтения никогда не гарантируется. Вызов read работает следующим образом:

  • если все дескрипторы файлов, используемые для записи, закрыты, вызов read при пустом канале всегда незамедлительно возвращает 0, что большинством программ рассматривается как конец файла;

  • если дескриптор файла, используемый для записи, открыт, вызов read при пустом канале блокируется или не блокируется - это зависит от того, установлен ли флаг O_NONBLOCK;

  • при непустом канале вызов read всегда возвращает управление немедленно, каким бы ни было отношение между запрошенным объемом и объемом данных, имеющихся в канале. Возвращается число байтов, прочитанных на самом деле.

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

Некоторые дополнительные принципы, о которых следует помнить:

  • если есть один писатель и один читатель ( например, конвейер командной оболочки) и читатель подготовлен к частичному чтению (например, он использует функции ввода-вывода стандартного языка С), то можно записывать любые объемы данных, хотя лучше всего использовать величины, кратные размеру блока;

  • если есть несколько писателей и один читатель, всегда записывайте PIPE_BUF байтов или менее. Нет никакого практического способа заставить каналы корректно работать с более крупными объемами данных, если только не используете другой механизм синхронизации, такой как семафор;

  • о значении PIPE_BUF с уверенностью можно сказать только то, что оно не может быть меньше 512. Если на самом деле нужно узнать его, то используйте вызов fpathconf;

  • в стандартах не требуется, чтобы чтение было атомарным, но если запрошенный объем не превышает PIPE_BUF байтов, оно во всех версиях UNIX атомарно;

  • помните, что признак конца файла (возврат 0 из вызова read) не будет получен, если не будут закрыты все дескрипторы файлов, используемые для записи, в том числе относящиеся к процессу, который выполняет чтение.

Если имеется два процесса, то, как связать их, чтобы один мог читать из канала то, что записывает другой. Как только процессы созданы, связать их нельзя, потому что процесс, создающий канал, не может передать дескриптор файла другому процессу. Конечно, он может передать номер дескриптора файла, однако в другом процессе этот номер не будет корректным. Но если сформировать канал в одном процессе до создания другого процесса, второй процесс унаследует дескрипторы файлов канала, и они будут корректны в обоих процессах. Таким образом, два процесса, взаимодействующие посредством канала, могут быть родительским и дочерним процессами, двумя потомками одного процесса, дедом и внуком. Однако они должны быть связаны родственными отношениями, а канал должен быть назначен процессу при его создании. На практике это может быть серьезным ограничением, потому что после завершения процесса его никак нельзя воссоздать и заново связать с теми же каналами – необходимо завершить и оставшиеся процессы, а затем нужно воссоздать все семейство процессов. В следующем примере один процесс (выполняющий функцию pipewrite) формирует канал, создает дочерний процесс (выполняющий piperead), который наследует канал, после чего записывает в канал некоторые данные, чтобы их прочитал дочерний процесс. Хотя дочерний процесс наследует нужный дескриптор файла, используемый для чтения, номер дескриптора ему неизвестен, поэтому этот номер передается в качестве аргумента:

void pipewrite(void)

{

int pfd[2];

char fdstr[10];

pipe(pfd);

switch (fork()) {

case -1:

EC_FAIL

case 0: /* дочерний процесс */

close(pfd[1]);

snprintf(fdstr, sizeof(fdstr), “%d”, pfd[0]);

execlp(“./piperead”, “piperead”, fdstr, NULL);

EC_FAIL

default: /* родительский процесс */

close(pfd[0]);

write(pfd[1], “hello”, 6);

}

return;

EC_CLEANUP_BGN

EC_FLUSH(“pipewrite”);

EC_CLEANUP_END

}

Рассмотрим дочерний процесс:

int main(int argc, char *argv[ ])

{

int fd;

ssize_t nread;

char s[100];

fd = atoi(argv[1]);

printf(“reading file descriptor %d\n”, fd);

nread = read(fd, s, sizeof(s));

if (nread == 0)

printf(“EOF\n”);

else

printf(“read %ld bytes: %s\n”, (long) nread, s);

exit(EXIT_SUCCESS);

EC_CLEANUP_BGN

exit(EXIT_FAILURE);

EC_CLEANUP_END

}

В результате выполнения этого кода получим:

reading file descriptor 3

read 6 bytes: hello

Некоторые пояснения к примеру:

  • так как дочерний процесс будет только читать данные из канала, но не записывать их, сразу же после создания этого процесса закрываем сторону записи канала (выполняя в родительском процессе вызов close(pfd[1]) ради экономии дескрипторов файлов (если бы дочерний процесс собирался читать больше данных, мы просто обязаны были бы закрыть сторону записи, иначе дочерний процесс никогда не получил бы признак конца файла);

  • родительский процесс не читает данные из канала, поэтому он закрывает pfd[0];

  • функция snprintf преобразует дескриптор файла, используемый для чтения, из целого числа в строку, чтобы его можно было использовать в качестве аргумента программы;

  • дочерняя программа хранится в текущем каталоге, поэтому путь к ней указан как ./piperead, чтобы вызов execlp ее не искал. Это не только экономит время, но и предотвращает случайное выполнение вместо нее какой-то другой программы piperead.

  • ожидать дочерний процесс ни к чему, так как в этом простом примере родительский процесс сразу же выполняет выход;

  • программа piperead - дочерний процесс – просто преобразует свой аргумент обратно в целое число и читает данные с использованием этого дескриптора файла.

Чтобы при помощи канала соединить два процесса для одноправленной коммуникации, нужно сделать следующее:

  1. Создать канал.

  2. Вызвать fork для создания дочернего процесса, который будет читать данные.

  3. Закрыть в дочернем процессе сторону записи канала и выполнить другие подготовительные действия.

  4. Выполнить в дочернем процессе дочернюю программу.

  5. Закрыть в родительском процессе сторону чтения канала.

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

Теперь ясно, почему fork и exec не объединены в один системный вызов. Они разделены, чтобы можно было выполнить вышеуказанный этап 3. Программа piperead читает данные с использованием дескриптора файла, номер которого в нее передан. На самом деле многие программы читают данные из файла с конкретным дескриптором, полагая при этом, что он уже открыт, но этот дескриптор равен нулю (стандартный ввод, STDIN_FILENO) и не передается в качестве аргумента. Аналогично, многие программы записывают данные в файл с дескриптором 1 (стандартный вывод, STDOUT_FILENO). Чтобы связать команды так, как это делает командная оболочка, требуется заставить вызов pipe возвратить в массив pfd дескриптор 0 для стороны чтения и дескриптор 1 для стороны записи. Но pipe не поддерживает такой возможности. Для этих целей используются системные вызовы dup или dup2.