Лабораторная работа. Работа с процессами - efanov/mephi GitHub Wiki

Работа с процессами

Цель

Изучить системные вызовы и программный интерфейс управления процессами. Программы разрабатываются на языке Си.

Системные вызовы, изучаемые в лабораторной работе: fork, exec, wait, kill, getpid, getppid.

Изучение разработки под ОС Linux

Для разработки программ необходимо установить пакет gcc (компилятор).

Создание простейшего исполняемого файла

Создайте файл hello.c следующего содержания, используя текстовый редактор (например, Vim):

#include <stdio.h>

int main(int argc, char const *argv[])
{
  printf("Hello world\n");
  return 0;
}

Перед запуском программу нужно скомпилировать:

gcc -Wall hello.c -o hello

Команда компилирует исходный код из hello.c в машинный код и сохраняет его в исполняемом файле hello. Выходной файл для машинного кода задается с помощью параметра -o. Если он опущен, вывод записывается в файл по умолчанию, a.out.

Теперь исполняемый файл hello можно запустить.

./hello

При каждом изменении исходного файла hello.c программу нужно перекомпилировать заново (команда gcc).

Можно объединить две команды:

gcc hello.c -o hello && ./hello

Дополнительные полезные флаги компилятора

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

Некоторые полезные флаги компилятора:

  • -Wall - включить все предупреждения
  • -Wextra - включить все дополнительные предупреждения
  • -O2 - включить оптимизации для окончательной сборки программы, чтобы она работала быстрее

Обработка аргументов командной строки

В программах на языке Си все аргументы командной строки передаются в качестве массива аргументов в функцию main. Их формирует родительский, вызывающий процесс, после чего операционная система копирует аргументы в адресное пространство нового процесса. В примере выше мы уже предусмотрели передачу аргументов в функцию main, но пока ею не воспользовались. Рассмотрим пример программы, которая выводит на экран все переданные в неё аргументы.

#include <stdio.h>

int main(int argc,          /* кол-во аргументов */
         char const *argv[] /* сам массив аргументов*/)
{
  for(int j = 0; j < argc; j++)
    printf("argv[%d] = %s\n", j, argv[j]);
}

Скомпилируйте эту программу и запустите. Обратите внимание, что в массиве всегда есть хотя бы один элемент с индексом 0 - это имя программы. Запустите эту программу с различными аргументами.

Важно помнить, что все аргументы являются строками и нельзя, например, аргумент 3 использовать в качестве счётчика цикла - его нужно предварительно преобразовать в тип int с помощью, например, sscanf.

Переменные окружения

Аргументы командной строки - не единственный способ передать нашей команде необходимую информацию. Другим не менее распространённым способом изменения поведения программ являются переменные окружения.

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

Рассмотрим пример программы, которая выводи на экран список её переменных окружения. Аналогичным образом работает команда env.

#include <stdio.h>

extern char **environ;
int main(int argc, char *argv[])
{
  char **p;
  for (p = environ; *p != NULL; p++) /* перебор всех элементов массива */
    printf("%s\n", *p); /* разыменование указателя */
}

Задание 1.1. Напишите программу, которая подсчитывает количество переданных ей переменных окружения и выводит на экран результат в виде:

Number of environment variables: 10

Проверьте себя, используя команды Bash. Напишите сценарий для проверки в качестве комментария в вашей программе.

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

Задание 1.3. Напишите программу, которая выводит на экран не более 10 переменных окружения.

Задание 1.4. Доработайте предыдущую программу таким образом, чтобы максимально количество отображаемых на экране переменных окружения передавалось через аргументы командной строки. Иными словами она должна работать также как и следующий сценарий на языке Bash:

env | head -n$1

Справочная информация

Все системные вызовы описаны во втором разделе руководства man. Для того чтобы узнать, какой заголовочный файл нужно подключить для того или иного системного вызова, нужно вызвать man-страницу с данным системным вызовом из второго раздела командой man 2 <название вызова>. Например:

# справка по fork
# требуется подключить unistd.h
man 2 fork
# справка по kill
# требуется подключить sys/types.h и signal.h
man 2 kill

Некоторые системные вызовы, такие как exec, оборачиваются библиотечными функциями. Справка по библиотечным функциям находится в третьем разделе man, таким образом, для получения информации о вызове exec нужно вызвать третий раздел руководства:

man 3 exec

Запуск процесса: fork и exec

Рассмотрим пример с командной оболочкой. Пользователь вводит команду ls, командная оболочка должна выполнить команду, дождаться её завершения и сохранить результат выполнения команды (код возврата).

Такое поведение командной оболочки достигается именно комбинацией системных вызовов fork и exec.

Более наглядно на схематичном изображении:

+--------+
| pid=7  |
| ppid=4 |
| bash   |
+--------+
    |
    | вызывает системный вызов fork
    V
+--------+              +--------+
| pid=7  | ответвляется | pid=22 | <- новый PID назначен системой
| ppid=4 | -----------> | ppid=7 | <- процесс-родитель - bash
| bash   |              | bash   | <- это новый процесс, но всё ещё
+--------+              +--------+    использует адресное пространство bash
    |                       |
    | ожидает PID 22        | вызывает exec и запускает команду ls
    |                       V
    |                   +--------+
    |                   | pid=22 |
    |                   | ppid=7 |
    |                   | ls     | <- вызов exec заменил адресное
    V                   +--------+    пространство bash на ls
+--------+                  |
| pid=7  |   код возврата   | ls завершается
| ppid=4 | <----------------+
| bash   |
+--------+
    |
    | bash продолжает работу
    V

Как видно, одного вызова fork для запуска другого дочернего процесса недостаточно и необходимо выполнить exec, в котором и происходит непосредственно вызов процесса и передача ему аргументов.

fork

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

#include <stdio.h>
#include <unistd.h>

int main(void)
{
  int pid = fork();
  // определить, в каком процессе мы находимся, помогает переменная pid

  if (pid == 0) {
    // дочерний процесс получает в качестве значения 0
    // это не является корректным PID и служит для определения
    // того факта, что данный код выполняется в дочернем процессе
    printf("Это сообщение из дочернего процесса\n");
  } else if (pid > 0) {
    // родительский процесс получает значение PID дочернего, он должен быть > 0
    printf("Это сообщение из родительского процесса.\n"
           "Идентификатор дочернего процесса:  %d\n", pid);
  }

  return 0;
}

Скомпилируйте и запустите эту программу самостоятельно. Обратите внимание, что текст выводится не обязательно в том порядке, в котором он записан в коде программы. Это зависит от значения параметра ядра, которое можно найти в файле /proc/sys/kernel/sched_child_runs_first. Попробуйте изменить его и посмотреть, как изменится результат работы программы.

Задание 2.1 Перепишите программу таким образом, чтобы дочерний процесс выводил на экран значения своего PID и PID родительского процесса.

Задание 2.2 Перепишите программу 2.1 таким образом, чтобы ни дочерний, ни родительский процессы не завершались моментально. Напишите shell-сценарий, который запускает вашу программу и выводит дерево процессов. На дереве процессов должна отображаться цепочка bash -> родитель -> потомок.

Задание 2.3 Перепишите программу 2.2 таким образом, чтобы родительский процесс создавал 10 дочерних. Напишите shell-сценарий, который запускает вашу программу и выводит дерево процессов. На дереве процессов должна отображаться цепочка bash -> родитель -> потомок (x10 штук).

Задание 2.4 Экспериментальным путём установите, ограничено ли число потомков, которые может создавать один процесс?

exec

Системный вызов exec нужно использовать через программные интерфейсы, которых несколько:

int execl(const char *path, const char *arg, ...
                /* (char  *) NULL */);
int execlp(const char *file, const char *arg, ...
                /* (char  *) NULL */);
int execle(const char *path, const char *arg, ...
                /*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
                char *const envp[]);

Каждый вызов exec дополнен одним или более символами:

  • e - передаётся массив указателей на переменные окружения
  • l - аргументы командной строки передаются в виде списка аргументов функции, которые завершаются аргументом NULL
  • p - функция будет искать исполняемый файл в переменной окружения PATH
  • v - аргументы командной строки передаются в виде массива указателей на аргументы

Теперь скомбинируем оба вызова для того чтобы запустить команду ls с аргументами. В отличие от предыдущего примера добавляются два новых системных вызова: exec(3) и wait(3).

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main( void ) {

  int pid = fork();
  int status;

  if(pid == 0) {
    char *cmd = "ls";
    char *argv[3] = {cmd, "-la", NULL};
    execvp(cmd, argv);
    // после exec программный код выполняться не будет, так как адресное
    // пространство перейдёт в новый процесс, где программный код начнёт
    // выполняться с функции main команды ls
  }

  // родительский поток ожидает заврешение дочернего
  wait(&status);
  return 0;
}

Совет: изучите содержимое страницы руководства man 3 wait.

Задание 3.1.

Изучение сигналов

Изучение SUID-бита

Оформление результатов

Все выполненные задания необходимо загрузить в виде исходных c-файлов на GitHub.

⚠️ **GitHub.com Fallback** ⚠️