Вперед Назад Содержание

2. Управление процессами и прерываниями

2.1 Структура задачи и таблица процессов

Каждый процесс динамически размещает структуру struct task_struct. Максимальное количество процессов, которое может быть создано в Linux, ограничивается только объемом физической памяти и равно (см. kernel/fork.c:fork_init()):


/* * В качестве максимально возможного числа потоков принимается безопасное * значение: структуры потоков не могут занимать более половины * имеющихся страниц памяти. */ max_threads = mempages / (THREAD_SIZE/PAGE_SIZE) / 2;

что для архитектуры IA32 означает, как правило, num_physpages/4. Например, на машине с 512M памяти, возможно создать 32k потоков. Это значительное усовершенствование по сравнению с 4k-epsilon пределом для ядер 2.2 и более ранних версий. Кроме того, этот предел может быть изменен в процессе исполнения, передачей значения KERN_MAX_THREADS в вызове sysctl(2), или через интерфейс procfs:


# cat /proc/sys/kernel/threads-max 32764 # echo 100000 > /proc/sys/kernel/threads-max # cat /proc/sys/kernel/threads-max 100000 # gdb -q vmlinux /proc/kcore Core was generated by `BOOT_IMAGE=240ac18 ro root=306 video=matrox:vesa:0x118'. #0 0x0 in ?? () (gdb) p max_threads $1 = 100000

Множество процессов в Linux-системе представляет собой совокупность структур struct task_struct, которые взаимосвязаны двумя способами.

  1. как хеш-массив, хешированный по pid, и
  2. как кольцевой двусвязный список, в котором элементы ссылаются друг на друга посредством указателей p->next_task и p->prev_task.

Хеш-массив определен в include/linux/sched.h как pidhash[]:


/* PID hashing. (shouldnt this be dynamic?) */ #define PIDHASH_SZ (4096 >> 2) extern struct task_struct *pidhash[PIDHASH_SZ]; #define pid_hashfn(x) ((((x) >> 8) ^ (x)) & (PIDHASH_SZ - 1))

Задачи хешируются по значению pid, вышеприведенной хеш-функцией, которая равномерно распределяет элементы по диапазону от 0 до PID_MAX-1. Хеш-массив используется для быстрого поиска задачи по заданному pid с помощью inline-функции find_task_by_pid(), определенной в include/linux/sched.h:


static inline struct task_struct *find_task_by_pid(int pid) { struct task_struct *p, **htable = &pidhash[pid_hashfn(pid)]; for(p = *htable; p && p->pid != pid; p = p->pidhash_next) ; return p; }

Задачи в каждом хеш-списке (т.е. хешированные с тем же самым значением) связаны указателями p->pidhash_next/pidhash_pprev, которые используются функциями hash_pid() и unhash_pid() для добавления/удаления заданного процесса в/из хеш-массив. Делается это под блокировкой (spinlock) tasklist_lock, полученной на запись.

Двусвязный список задач организован таким образом, чтобы упростить навигацию по нему, используя указатели p->next_task/prev_task. Для прохождения всего списка задач, в системе предусмотрен макрос for_each_task() из include/linux/sched.h:


#define for_each_task(p) \ for (p = &init_task ; (p = p->next_task) != &init_task ; )

Перед использованием for_each_task() необходимо получить блокировку tasklist_lock на ЧТЕНИЕ. Примечательно, что for_each_task() использует init_task в качестве маркера начала (и конца) списка - благодаря тому, что задача с pid=0 всегда присутствует в системе.

Функции, изменяющие хеш-массив и/или таблицу связей процессов, особенно fork(), exit() и ptrace(), должны получить блокировку (spinlock) tasklist_lock на ЗАПИСЬ. Что особенно интересно - перед записью необходимо запрещать прерывания на локальном процессоре, по той причине, что функция send_sigio(), при прохождении по списку задач, захватывает tasklist_lock на ЧТЕНИЕ, и вызывается она из kill_fasync() в контексте прерывания. Однако, если требуется доступ ТОЛЬКО ДЛЯ ЧТЕНИЯ, запрещать прерывания нет необходимости.

Теперь, когда достаточно ясно представляется как связаны между собой структуры task_struct, можно перейти к рассмотрению полей task_struct.

В других версиях UNIX информация о состоянии задачи разделяется на две части, в одну часть выделяется информация о состоянии задачи (называется 'proc structure', которая включает в себя состояние процесса, информацию планировщика и пр.) и постоянно размещается в памяти, другая часть, необходима только во время работы процесса ('u area', которая включает в себя таблицу дескрипторов, дисковые квоты и пр.) Единственная причина такого подхода - дефицит памяти. Современные операционные системы (не только Linux, но и другие, современная FreeBSD например) не нуждаются в таком разделении и поэтому вся информация о состоянии процесса постоянно хранится в памяти.

Структура task_struct объявлена в include/linux/sched.h и на сегодняшний день занимает 1680 байт.

Поле state объявлено как:


volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ #define TASK_RUNNING 0 #define TASK_INTERRUPTIBLE 1 #define TASK_UNINTERRUPTIBLE 2 #define TASK_ZOMBIE 4 #define TASK_STOPPED 8 #define TASK_EXCLUSIVE 32

Почему константа TASK_EXCLUSIVE имеет значение 32 а не 16? Потому что раньше значение 16 имела константа TASK_SWAPPING и я просто забыл сместить значение TASK_EXCLUSIVE, когда удалял все ссылки на TASK_SWAPPING (когда-то в ядре 2.3.x).

Спецификатор volatile в объявлении p->state означает, что это поле может изменяться асинхронно (в обработчиках прерываний):

  1. TASK_RUNNING: указывает на то, что задача "вероятно" находится в очереди запущенных задач (runqueue). Причина, по которой задача может быть помечена как TASK_RUNNING, но не помещена в runqueue в том, что пометить задачу и вставить в очередь - не одно и то же. Если заполучить блокировку runqueue_lock на чтение-запись и просмотреть runqueue, то можно увидеть, что все задачи в очереди имеют состояние TASK_RUNNING. Таким образом, утверждение "Все задачи в runqueue имеют состояние TASK_RUNNING" не означает истинность обратного утверждения. Аналогично, драйверы могут отмечать себя (или контекст процесса, под которым они запущены) как TASK_INTERRUPTIBLE (или TASK_UNINTERRUPTIBLE) и затем производить вызов schedule(), который удалит их из runqueue (исключая случай ожидания сигнала, тогда процесс остается в runqueue).
  2. TASK_INTERRUPTIBLE: задача в состоянии "сна", но может быть "разбужена" по сигналу или по истечении таймера.
  3. TASK_UNINTERRUPTIBLE: подобно TASK_INTERRUPTIBLE, только задача не может быть "разбужена".
  4. TASK_ZOMBIE: задача, завершившая работу, до того как родительский процесс ("естественный" или "приемный") произвел системный вызов wait(2).
  5. TASK_STOPPED: задача остановлена, либо по управляющему сигналу, либо в результате вызова ptrace(2).
  6. TASK_EXCLUSIVE: не имеет самостоятельного значения и используется только совместно с TASK_INTERRUPTIBLE или с TASK_UNINTERRUPTIBLE (по OR). При наличии этого флага, будет "разбужена" лишь эта задача, избегая тем самым порождения проблемы "гремящего стада" при "пробуждении" всех "спящих" задач.

Флаги задачи представляют не взаимоисключающую информацию о состоянии процесса:


unsigned long flags; /* флаги процесса, определены ниже */ /* * Флаги процесса */ #define PF_ALIGNWARN 0x00000001 /* Print alignment warning msgs */ /* Not implemented yet, only for 486*/ #define PF_STARTING 0x00000002 /* создание */ #define PF_EXITING 0x00000004 /* завершение */ #define PF_FORKNOEXEC 0x00000040 /* создан, но не запущен */ #define PF_SUPERPRIV 0x00000100 /* использует привилегии супер-пользователя */ #define PF_DUMPCORE 0x00000200 /* выполнен дамп памяти */ #define PF_SIGNALED 0x00000400 /* "убит" по сигналу */ #define PF_MEMALLOC 0x00000800 /* Распределение памяти */ #define PF_VFORK 0x00001000 /* "Разбудить" родителя в mm_release */ #define PF_USEDFPU 0x00100000 /* задача использует FPU this quantum (SMP) */

Поля p->has_cpu, p->processor, p->counter, p->priority, p->policy и p->rt_priority связаны с планировщиком и будут рассмотрены позднее.

Поля p->mm и p->active_mm указывают, соответственно, на адресное пространство процесса, описываемое структурой mm_struct и активное адресное пространство, если процесс не имеет своего (например потоки ядра). Это позволяет минимизировать операции с TLB при переключении адресных пространств задач во время их планирования. Так, если запланирован поток ядра (для которого поле p->mm не установлено), то next->active_mm будет установлено в значение prev->active_mm предшествующей задачи, которое будет иметь то же значение, что и prev->mm если prev->mm != NULL. Адресное пространство может разделяться потоками, если в системный вызов clone(2) был передан флаг CLONE_VM, либо был сделан системный вызов vfork(2).

Поле p->fs ссылается на информацию о файловой системе, которая в Linux делится на три части:

  1. корень дерева каталогов и точка монтирования,
  2. альтернативный корень дерева каталогов и точка монтирования,
  3. текущий корень дерева каталогов и точка монтирования.

Эта структура включает в себя так же счетчик ссылок, поскольку возможно разделение файловой системы между клонами, при передаче флага CLONE_FS в вызов clone(2).

Поле p->files ссылается на таблицу файловых дескрипторов, которая так же может разделяться между задачами при передаче флага CLONE_FILES в вызов clone(2).

Поле p->sig содержит ссылку на обработчики сигналов и может разделяться между клонами, которые были созданы с флагом CLONE_SIGHAND.

2.2 Создание и завершение задач и потоков ядра.

В литературе можно встретить самые разные определения термина "процесс", начиная от "экземпляр исполняемой программы" и заканчивая "то, что является результатом работы системного вызова clone(2) или fork(2)". В Linux, существует три типа процессов:

Фоновая задача создается во время компиляции (at compile time) для первого CPU; и затем "вручную" размножается для каждого процессора вызовом fork_by_hand() из arch/i386/kernel/smpboot.c. Фоновая задача имеет общую структуру init_task, но для каждого процессора создается свой собственный TSS, в массиве init_tss. Все фоновые задачи имеют pid = 0 и никакой другой тип задач больше не может разделять pid, т.е. не могут клонироваться с флагом CLONE_PID через clone(2).

Потоки ядра порождаются с помощью функции kernel_thread(), которая делает системный вызов clone(2) в режиме ядра. Потоки ядра обычно не имеют пользовательского адресного пространства, т.е. p->mm = NULL, поэтому они явно вызывают exit_mm(), например через функцию daemonize(). Потоки ядра всегда имеют прямой доступ к адресному пространству ядра. Получают pid из нижнего диапазона. Работают в нулевом кольце защиты и, следовательно, имеют высший приоритет во всех операциях ввода/вывода и имеют преимущество перед планировщиком задач.

Пользовательские задачи создаются через системные вызовы clone(2) или fork(2). И тот и другой обращаются к kernel/fork.c:do_fork().

Давайте рассмотрим что же происходит, когда пользовательский процесс делает системный вызов fork(2). Хотя fork(2) и является аппаратно-зависимым из-за различий в организации стека и регистров, тем не менее основную часть действий выполняет функция do_fork(), которая является переносимой и размещена в kernel/fork.c.

При ветвлении процесса выполняются следующие действия:

  1. Локальной переменной retval присваивается значение -ENOMEM, которое возвращается в случае невозможности распределить память под новую структуру задачи
  2. Если установлен флаг CLONE_PID в параметре clone_flags, тогда возвращается код ошибки (-EPERM). Наличие этого флага допускается только если do_fork() была вызвана из фонового потока (idle thread), т.е. из задачи с pid == 0 (только в процессе загрузки). Таким образом, пользовательские потоки не должны передавать флаг CLONE_PID в clone(2), ибо этот номер все равно не "проскочит".
  3. Инициализируется current->vfork_sem (позднее будет очищен потомком). Он используется функцией sys_vfork() (системный вызов vfork(2), передает clone_flags = CLONE_VFORK|CLONE_VM|SIGCHLD) для того, чтобы "усыпить" родителя пока потомок не выполнит mm_release(), например , в результате исполнения exec() или exit(2).
  4. В памяти размещается новая структура с помощью макроса alloc_task_struct(). На x86 это производится с приоритетом GFP_KERNEL. Это главная причина, по которой системный вызов fork(2) может "заснуть". Если разместить структуру не удалось, то возвращается код ошибки -ENOMEM.
  5. Все поля структуры текущего процесса копируются во вновь созданную структуру посредством присваивания *p = *current. Может быть следует заменить на memset? Позднее, в поля, которые не наследуются потомком, будут записаны корректные значения.
  6. Для сохранения реентерабельности кода, выполняется big kernel lock.
  7. Если "родитель" является пользовательским ресурсом, то проверяется - не превышен ли предел RLIMIT_NPROC, если превышен - тогда возвращается код ошибки -EAGAIN, если нет - увеличивается счетчик процессов для заданного uid p->user->count.
  8. Если превышено системное ограничение на общее число задач - max_threads, возвращается код ошибки -EAGAIN.
  9. Если исполняемый формат программы принадлежит домену исполнения, поддерживаемому на уровне модуля, увеличивается счетчик ссылок соответствующего модуля.
  10. Если исполняемый формат программы принадлежит двоичному формату, поддерживаемому на уровне модуля, увеличивается счетчик ссылок соответствующего модуля.
  11. Потомок помечается как 'has not execed' (p->did_exec = 0)
  12. Потомок помечается как 'not-swappable' (p->swappable = 0)
  13. Потомок переводится в состояние TASK_UNINTERRUPTIBLE, т.е. p->state = TASK_UNINTERRUPTIBLE (TODO: зачем это делается? Я думаю, что в этом нет необходимости - следует избавиться от этого, Linus подтвердил мое мнение)
  14. Устанавливаются флаги потомка p->flags в соответствии с clone_flags; в случае простого fork(2), это будет p->flags = PF_FORKNOEXEC.
  15. Вызовом функции, kernel/fork.c:get_pid(), реализующей быстрый алгоритм поиска, находится pid потомка (p->pid) (TODO: блокировка (spinlock) lastpid_lock может быть опущена, так как get_pid() всегда выполняется под блокировкой ядра (big kernel lock) из do_fork(), так же можно удалить входной параметр flags для get_pid(), патч (patch) отправлен Алану (Alan) 20/06/2000).
  16. Далее инициализируется остальная часть структуры task_struct потомка. В самом конце структура хешируется в таблицу pidhash и потомок активируется (TODO: вызов wake_up_process(p) устанавливает p->state = TASK_RUNNING и добавляет процесс в очередь runqueue, поэтому, вероятно, нет нужды устанавливать p->state в состояние TASK_RUNNING ранее в do_fork()). Обратите внимание на установку p->exit_signal в значение clone_flags & CSIGNAL, которое для fork(2) может быть только SIGCHLD, и на установку p->pdeath_signal в 0. Сигнал pdeath_signal используется когда процесс лишается "родителя" (в случае его "смерти") и может быть получен/установлен посредством команд PR_GET/SET_PDEATHSIG системного вызова prctl(2)

Задача создана. Для завершения задачи имеется несколько способов.

  1. выполнить системный вызов exit(2);
  2. передать сигнал, приказывающий "умереть";
  3. вынужденная "смерть" в результате возникновения некоторых исключений;
  4. вызвать bdflush(2) с func == 1 (эта особенность Linux оставлена для сохранения совместимости со старыми дистрибутивами, которые имели строку 'update' в /etc/inittab - на сегодняшний день эта работа выполняется процессом ядра kupdate).

Имена функций, реализующих системные вызовы, в Linux начинаются с префикса sys_, но они, как правило, ограничиваются только проверкой аргументов или платформо-зависимой передачей информации, а фактически всю работу выполняют функции do_. Это касается и sys_exit(), которая вызываетdo_exit() для выполнения необходимых действий. Хотя, в других частях ядра иногда встречается вызов sys_exit (), на самом деле вызывается do_exit ().

Функция do_exit() размещена в kernel/exit.c. Некоторые примечания по поводу функции do_exit():