Некоторые системные вызовы 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>.