Некоторые системные вызовы Unix
Основное средство для организации многозадочности - это вызов
безаргументной функции fork() из <unistd.h>, который создает копию
вызвавшего ее процесса. Копия может быть отличена от оригинала по
возвращаемому значению fork(), которое --- ноль в копии и номер
процесса-копии в исходном, порождающем процессе. Если вызов не удался,
например, из-за нехватки памяти, то fork() возвращает -1.
Вызовы процессов образуют бинарное дерево. Код возврата процесса возвращается
порождающему процессу. Если порождающий процесс завершается раньше
порождённого, то порожденный процесс превращается в зомби-процесс. Он не
может полностью исчезнуть, так как его код возврата может быть востребован
порождающим процессом. Начальный процесс в системе (корень в дереве процессов)
называется init или systemd --- его номер обычно 1.
Собственно вызов новой задачи или, другими словами, установка нового
содержимого для процесса осуществляется функциями семейства exec из <unistd.h>:
execl(), execlp(), execle(), execv(), execvp(). Функции, заканчивающиеся
на p, не требуют точного указания местоположения загружаемого файла ---
они могут найти его по адресам, перечисленным в переменной среды PATH.
Функции, содержащие l, получают параметры вызова через список аргументов
неопределенной длины, а функции, содержащие v, получают такой список через
массив, подобно main(). Последним элементом как списка, так и массива должен
быть 0, а первым --- имя программы. Функция execle() позволяет задавать среду
исполнения процесса.
Пример вызова программы date.
#include<iostream>
#include<unistd.h>
main() {
execlp("date", "date", 0);
std::cerr << "Can't execute program 'date'\n";
}
Программа date вытесняет текущий процесс, поэтому строка с сообщением об
ошибке будет выполнена только в случае, если загрузка date не удалась.
Сохранить текущий процесс при вызове нового можно только с помощью fork().
#include<iostream>
#include<unistd.h>
main() {
if (fork() == 0) {
std::cout << "This is child\n";
execlp("date", "date", 0);
std::cerr << "Can't execute program 'date'\n";
}
else
std::cout << "This is parent\n";
}
Функция wait() из <sys/wait.h> позволяет дождаться окончания выполнения
какого-нибудь порождаемого процесса. Вызовы wait() будут успешны до тех пор,
пока остаются порожденные процессы.
#include<iostream>
#include<unistd.h>
#include<sys/wait.h>
main() {
if (fork() == 0) {
std::cout << "This is child\n";
execlp("sleep", "sleep", "10", 0);
std::cerr << "Can't execute program 'sleep'\n";
}
else {
int status;
std::cout << "This is parent\n";
unsigned cid = wait(&status);
std::cout << "process #" << cid << " is finished with " << (status&255)
<< " exit code\n"; //this message appears after 10 sec
}
}
Значение status, устанавливаемое wait(), содержит, в частности, код возврата
процесса-потомка в младшем байте. Результат wait() --- это номер завершившегося
процесса или -1 в случае ошибки. Можно вызывать wait() с аргументом 0, если
возвращаемое значение неважно.
Ввод-вывод системного уровня обеспечивается функциями из <unistd.h>
read() --- читать из файла,
write() --- писать в файл,
dup() --- создать копию дескриптора файла,
close() --- закрыть файл,
unlink() --- отсоединить, уничтожить жесткий соединитель (файл),
lseek() --- искать позицию в файле.
И функциями из <fcntl.h>
creat() --- создать файл,
open() --- открыть файл.
Некоторые стандартные константы для некоторых из этих функций определены в
<sys/types.h> и <sys/stat.h>.
Файл на низком уровне задается дескриптором --- целым числом, оно
используется, в частности, при перенаправлении потоков ввода-вывода.
Рассмотрим программу, которая печатает два раза все, что будет введено с
клавиатуры.
#include<unistd.h>
main() {
char buf[8], n, cp1;
cp1 = dup(1); //copy of std output
while ((n = read(0, buf, 8)) > 0) {
write(1, buf, n);
write(cp1, buf, n);
}
}
Создание и работа с файлом.
#include<unistd.h>
//#include<sys/types.h>
//#include<sys/stat.h>
#include<fcntl.h>
main() {
char fn[] = "testio.txt", buf[11] = "123456\nok\n";
int fd = creat(fn, 0664); //0664 - mode
for(int i = 0; i < 3; i++)
write(fd, buf, 7);
close(fd);
fd = open(fn, 1); //0=O_RDONLY - read, 1=O_WRONLY - write, 2=O_RDWR - read & write;
//можно устанавливать и другие флаги, см. man 2 open
lseek(fd, 14, 0); //3rd arg is the same as at fseek
buf[0] = '*';
write(fd, buf, 1);
close(fd);
write(1, buf+7, 3); //prints "ok"
} //creates text file 'testio.txt' with 3 lines: 123456 // 123456 // *23456
Для обмена данными между процессами можно использовать трубопроводы,
создаваемые функцией pipe() из <unistd.h>, у которой один аргумент --- массив
из двух целых чисел. Первый элемент массива --- это выход из трубопровода, для
чтения данных, а второй --- это вход.
#include <sys/wait.h>
#include <cstdio>
#include <unistd.h>
#include <cstring>
using namespace std;
int main() {
int pipefd[2];
pid_t cpid;
char buf;
if (pipe(pipefd) == -1) {
fputs("pipe\n", stderr);
return 1;
}
cpid = fork();
if (cpid == -1) {
fputs("fork\n", stderr);
return 2;
}
puts("ok"); //напечатается 2 раза
if (cpid == 0) { //Порожденный процесс читает из трубопровода
close(pipefd[1]); //Закрытие ненужного входа в трубопровод
while (read(pipefd[0], &buf, 1) > 0)
write(STDOUT_FILENO, &buf, 1);
write(STDOUT_FILENO, "\n", 1);
close(pipefd[0]);
return 0;
}
else { //Базовый процесс пишет строку в трубопровод
char string[] = "Hello from the parent to the child";
close(pipefd[0]); //Закрытие ненужного выхода из трубопровода
write(pipefd[1], string, strlen(string));
close(pipefd[1]); //получатель данных получит EOF
wait(0); //ждём завершения работы получателя
return 0;
}
}
Вызов dup в подобных программах позволяет связать концы трубопровода со
стандартными потоками ввода-вывода, что соответствует | в оболочке ОС.
Процессы могут посылать друг другу сигналы. В частности, если порождённый
процесс заканчивается или приостанавливается, то автоматически генерируется
сигнал SIGCHLD. Функция signal() из <signal.h> устанавливает обработчик сигнала
с заданным номером, а функция kill() из этого же заголовка используется для
генерации заданного сигнала.
#include <cstdio>
#include <sys/wait.h>
#include <signal.h>
#include <unistd.h>
using namespace std;
void signalHandler(int signal) {
printf("Cought signal %d!\n", signal);
if (signal == SIGCHLD) {
puts("Child ended");
wait(0); //для совместимости
}
}
int main() {
signal(SIGALRM, signalHandler);
signal(SIGUSR1, signalHandler);
signal(SIGCHLD, signalHandler);
signal(SIGINT, signalHandler); //Control-C
if (!fork()) {
puts("Child running...");
sleep(2);
puts("Child sending SIGALRM...");
kill(getppid(), SIGALRM); //послать сигнал родителю
sleep(10);
puts("Child exitting...");
return 0;
}
printf("Parent running, PID=%d. Press ENTER to exit.\n", getpid());
fgetc(stdin);
puts("Parent exitting...");
return 0;
}
Функции getpid() и getppid() из <unistd.h> возвращают соответственно номера
текущего и порождающего процессов. Функция sleep() из <unistd.h> --- это
задержка на заданное число секунд. При исполнении заданной программы ей можно
посылать сигнал SIGUSR1 для перехвата, например, с командной строки, запуская
kill -SIGUSR1 НОМЕР-ПРОЦЕССА.
Средства взаимодействия процессов (IPC --- Inter-process communication) помимо
сигналов и трубопроводов (с именем и без) включают семафоры, разделяемую память
(shared memory), файлы (обычные и отображаемые в память), сокеты и очереди
сообщений.
Отображаемый в память файл (Memory-mapped file) --- весь или частично доступен
для прямого доступа по заданным адресам оперативной памяти.
Сложности работы с процессами можно проиллюстрировать следующим примером.
#include<iostream>
#include<unistd.h>
main() {
for (int i = 0; i < 2; ++i) {
fork();
std::cout << '.';
//std::cout.flush();
//std::cerr << '.';
}
} //8/6 точек с cout/cerr
Функция clone() в Linux служит для создания сопроцессов или нитей (threads)
--- процессов, разделяющих общую память и другие ресурсы. Управление
сопроцессами проводится при помощи средств, похожих на те, что используются
для управления процессами: семафоры, мьютексы, .... Библиотека NPTL (New Posix
Thread Library) содержит необходимые средства для работы с сопроцессами. Они
вводятся заголовком <pthread.h> си/си++. В стандарте си++ 2011 года для этого
определяются заголовки <thread>, <mutex>, <condition_variable> и <future>.