Читать «Разработка ядра Linux» онлайн - страница 31

Роберт Лав

list_entry(task->tasks.prev, struct task_struct, tasks);

Дна указанных выше выражения доступны также в виде макросов next_task(task) (получить следующую задачу), prev_task(task) (получить предыдущую задачу). Наконец, макрос for_each_process(task) позволяет выполнить цикл по всему списку задач. На каждом шаге цикла переменная task указывает на следующую задачу из списка:

struct task_struct *task;

for_each_process(task) {

 /* просто печатается имя команды и идентификатор PID

    для каждой задачи */

 printk("%s[%d]\n", task->comm, task->pid);

}

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

Создание нового процесса

В операционной системе Unix создание процессов происходит уникальным образом. В большинстве операционных систем для создания процессов используется метод порождения процессов (spawn). При этом создается новый процесс в новом адресном пространстве, в которое считывается исполняемый файл, и после этого начинается исполнение процесса. В ОС Unix используется другой подход, а именно разбиение указанных выше операций на две функции: fork() и exec().

В начале с помощью функции fork() создается порожденный процесс, который является копией текущего задания. Порожденный процесс отличается от родительского только значением идентификатора PID (который является уникальным в системе), значением параметра PPID (идентификатор PID родительского процесса, который устанавливается в значение PID порождающего процесса), некоторыми ресурсами, такими как ожидающие на обработку сигналы (которые не наследуются), а также статистикой использования ресурсов. Вторая функция — exec() — загружает исполняемый файл в адресное пространство процесса и начинает исполнять его. Комбинация функций fork() и exec() аналогична той одной функции создания процесса, которую предоставляет большинство операционных систем.

Копирование при записи

Традиционно при выполнении функции fork() делался дубликат всех ресурсов родительского процесса и передавался порожденному. Такой подход достаточно наивный и неэффективный. В операционной системе Linux вызов fork() реализован с использованием механизма копирования при записи (copy-on-write) страниц памяти. Технология копирования при записи (copy-on-write, COW) позволяет отложить или вообще предотвратить копирование данных. Вместо создания дубликата адресного пространства процесса родительский и порожденный процессы могут совместно использовать одну и ту же копию адресного пространства. Однако при этом данные помечаются особым образом, и если вдруг один из процессов начинает изменять данные, то создается дубликат данных, и каждый процесс получает уникальную копию данных. Следовательно, дубликаты ресурсов создаются только тогда, когда в эти ресурсы осуществляется запись, а до того момента они используются совместно в режиме только для чтения (read-only). Такая техника позволяет задержать копирование каждой страницы памяти до того момента, пока в эту страницу памяти не будет осуществляться запись. В случае, если в страницы памяти никогда не делается запись, как, например, при вызове функции exec() сразу после вызова fork(), то эти страницы никогда и не копируются. Единственные накладные расходы, которые вносит вызов функции fork(), — это копирование таблиц страниц родительского процесса и создание дескриптора порожденного процесса. Данная оптимизация предотвращает ненужное копирование большого количества данных (размер адресного пространства часто может быть более 10 Мбайт), так как процесс после разветвления в большинстве случаев сразу же начинает выполнять новый исполняемый образ. Эта оптимизация очень важна, потому чти идеология операционной системы Unix предусматривает быстрое выполнение процессов.