Структура задачи и таблица процессов
Каждый процесс динамически размещает структуру 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, которые взаимосвязаны двумя способами.
и 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 означает, что это поле может изменяться асинхронно (в обработчиках прерываний):
Флаги задачи представляют не взаимоисключающую информацию о состоянии процесса:
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 делится на три части:
Эта структура включает в себя так же счетчик ссылок, поскольку возможно разделение файловой системы между клонами, при передаче флага CLONE_FS в вызов clone(2).
Поле p->files ссылается на таблицу файловых дескрипторов, которая так же может разделяться между задачами при передаче флага CLONE_FILES в вызов clone(2).
Поле p->sig содержит ссылку на обработчики сигналов и может разделяться между клонами, которые были созданы с флагом CLONE_SIGHAND.