Coding — Механизм создания процессов в Linux. Продолжение.
В прошлый раз было рассмотрено, как происходит создание нового процесса в системе, но не было рассказано про выделение памяти процессу и под сам процесс.
Итак мы остановились на функции copy_process(), которая как было сказано выполняет фактическую работу по созданию нового процесса. В copy_process() происходит вызов функции dup_task_struct(), которая создает стек ядра и выделяет память под структуры task_struct и thread_info:
kernel/fork.c:
В dup_task_struct() передается указатель (current — макрос, который определен в файле arch/x86/include/asm/current.h) на текущий (вызвавший, то есть «родитель») процесс. Ниже приведен наиболее важный код из dup_task_struct():
kernel/fork.c:
prepare_to_copy() отвечает за сохранение регистров процессора в структуре thread_info «родителя», затем они буду скопированы в структуру «ребенка».
alloc_task_struct() - макрос, который возвращает дескриптор процесса для нового процесса и сохраняет его адрес в локальной переменной tsk:
kernel/fork.c:
task_struct_cahep — указатель на структуру struct kmem_cache, то есть кэш объектов task_struct, из которого будет выделяться объект. GFP_KERNEL (GFP — Get Free Page) — это флаг, который означает, что следует зарезервировать блок памяти, выделяя страницы по мере обращения к ним.
Функкция kmem_cache_alloc() представляет для нас наибольший интерес. Дело в том, что в Linux используется механизм управления памятью под названием slab allocator (кусочный или слябовый распределитель, в разных источниках имеет разное название). Но с версии ядра 2.6.22 появилась новая система управления памятью slub allocator, который оптимизирован для SMP-систем, то есть многопроцессорных. По умолчанию, начиная с ядер версии 2.6.23, выбирается именно slub allocator, вот что пишут при конфигурировании ядра:
При ручной сборке ядра имеется возможность выбрать allocator см. рис.1.
Рис.1. Выбор allocator'а
Итак мы определились с тем, что будем рассматривать интерфейсы slub allocator'а. Продолжим рассмотрение kmem_cache_alloc(). kmem_cache_alloc() экспортируется ядром, что позволяет нам его использовать при написании модулей ядра, в тоже время это обертка для функции slab_alloc():
mm/slub.c:
mm/slub.c:
Итак, что происходит в slab_alloс()? Узнаем к какому процессору принадлежит кэш объектов task_struct (get_cpu_slab()). Далее есть два пути: медленный и быстрый. Медленный: если нет больше свободных объектов, то выбирается следующий свободный сляб или же, если больше нет свободных слябов - создается новый сляб, из которого происходит выделение объекта, за это отвечает функция __slab_alloc(). В противном случае (быстрый путь), что более вероятно, новый объект получаем из списка свободных объектов:
object = c->freelist (на первый свободный объект в списке указывает переменная freelist). При необходимости (как например отладка) память под выделенный объект заполняется нулями (если установлен флаг __GFP_ZERO).
Возвращаясь к dup_task_struct(). Итак, выделив память для дескриптора процесса, далее выделяется память под структуру
struct thread_info (данная структура содержит информацию о процессе, которая специфична для архитектуры и определена в файле arch/x86/include/asm/thread_info.h) с помощью макроса alloc_thread_info(), который является оберткой для функции __get_free_pages():
arch/x86/include/asm/thread_info.h:
Функция __get_free_pages() выделяет 2^THREAD_ORDER (THREAD_ORDER равен 0, если при сборке ядра была указана опция CONFIG_4KSTACKS, в противном случае он равен 1, другими словами выделять одну или две страницы памяти) смежных страниц физической памяти и возвращает логический адрес первой выделенной страницы.
И последнее, в dup_task_struct() устанавливается стек (см. рис.2):
Функция setup_thread_stack() просто копирует содержимое структуры thread_info «родителя» в структуру thread_info «ребенка» и связывает thread_info с task_struct:
include/linux/shed.h:
Рис.2. Связь между task_struct, thread_info и стеком процесса (kernel space)
Вернемся к функции copy_process(). У каждого процесса так или иначе есть свое адресное пространство, размер которого зависит от архитектуры (например для 32-х разрядных систем оно составляет 2^32, что составляет около 4 Гб). Процесс имеет доступ только к определенной области памяти (сегмент кода, сегмент данных, стек, bss и т.д.) из адресного пространства, причем на область памяти могут накладываться определенные права (запись, выполнение и т.д., см. рис.3).
Рис.3. Права (флаги) установленные в сегменте кода на секцию инструкций
Адресное пространство описывается структурой struct mm_struct, которая определена в файле include/linux/mm_types.h. Итак в функции copy_process() происходит выделение дескриптора памяти (struct mm_struct) для нового процесса. Выделение происходит посредством вызова функции copy_mm() и передаче ей флагов и дескриптора процесса.
kernel/fork.c:
Если установлен флаг CLONE_VM, то память под адресное пространство не выделяется, а используется совместно с родителем, таким образом мы получаем поток. Если флаг не установлен, то выделение происходит с помощью функции dup_mm(), которая, к слову сказать, использует ранее рассматривавшуюся функцию kmem_cache_alloc().
Далее снова берет на себя «бразды правления» функция copy_process(), заполняя task_struct.
P.S. Первое, за содействие при составлении статьи спасибо eddi. Второе, на написание статьи наталкнул commonD, без его комментария к прошлой статье я бы и не стал разбираться с этим вопросом. И последнее, но не менее важное, в pdf по мимо самой статьи есть еще два небольших приложения.
Итак мы остановились на функции copy_process(), которая как было сказано выполняет фактическую работу по созданию нового процесса. В copy_process() происходит вызов функции dup_task_struct(), которая создает стек ядра и выделяет память под структуры task_struct и thread_info:
kernel/fork.c:
1 2 3 4 5 6 7 8 9 |
static struct task_struct *copy_process(...) |
В dup_task_struct() передается указатель (current — макрос, который определен в файле arch/x86/include/asm/current.h) на текущий (вызвавший, то есть «родитель») процесс. Ниже приведен наиболее важный код из dup_task_struct():
kernel/fork.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
static struct task_struct *dup_task_struct(struct task_struct *orig) |
prepare_to_copy() отвечает за сохранение регистров процессора в структуре thread_info «родителя», затем они буду скопированы в структуру «ребенка».
alloc_task_struct() - макрос, который возвращает дескриптор процесса для нового процесса и сохраняет его адрес в локальной переменной tsk:
kernel/fork.c:
# define alloc_task_struct() kmem_cache_alloc(task_struct_cachep, GFP_KERNEL)
task_struct_cahep — указатель на структуру struct kmem_cache, то есть кэш объектов task_struct, из которого будет выделяться объект. GFP_KERNEL (GFP — Get Free Page) — это флаг, который означает, что следует зарезервировать блок памяти, выделяя страницы по мере обращения к ним.
Функкция kmem_cache_alloc() представляет для нас наибольший интерес. Дело в том, что в Linux используется механизм управления памятью под названием slab allocator (кусочный или слябовый распределитель, в разных источниках имеет разное название). Но с версии ядра 2.6.22 появилась новая система управления памятью slub allocator, который оптимизирован для SMP-систем, то есть многопроцессорных. По умолчанию, начиная с ядер версии 2.6.23, выбирается именно slub allocator, вот что пишут при конфигурировании ядра:
Для SLAB:
«The regular slab allocator that is established and known to work well in all environments. It organizes cache hot objects in per cpu and per node queues.»
Для SLUB:
«SLUB is a slab allocator that minimizes cache line usage instead of managing queues of cached objects (SLAB approach). Per cpu caching is realized using slabs of objects instead of queues of objects. SLUB can use memory efficiently and has enhanced diagnostics. SLUB is the default choice for a slab allocator.»
«The regular slab allocator that is established and known to work well in all environments. It organizes cache hot objects in per cpu and per node queues.»
Для SLUB:
«SLUB is a slab allocator that minimizes cache line usage instead of managing queues of cached objects (SLAB approach). Per cpu caching is realized using slabs of objects instead of queues of objects. SLUB can use memory efficiently and has enhanced diagnostics. SLUB is the default choice for a slab allocator.»
При ручной сборке ядра имеется возможность выбрать allocator см. рис.1.
Рис.1. Выбор allocator'а
Итак мы определились с тем, что будем рассматривать интерфейсы slub allocator'а. Продолжим рассмотрение kmem_cache_alloc(). kmem_cache_alloc() экспортируется ядром, что позволяет нам его использовать при написании модулей ядра, в тоже время это обертка для функции slab_alloc():
mm/slub.c:
1 2 3 4 5 |
void *kmem_cache_alloc(struct kmem_cache *s, gfp_t gfpflags) |
mm/slub.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
static __always_inline void *slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node, void *addr) |
Итак, что происходит в slab_alloс()? Узнаем к какому процессору принадлежит кэш объектов task_struct (get_cpu_slab()). Далее есть два пути: медленный и быстрый. Медленный: если нет больше свободных объектов, то выбирается следующий свободный сляб или же, если больше нет свободных слябов - создается новый сляб, из которого происходит выделение объекта, за это отвечает функция __slab_alloc(). В противном случае (быстрый путь), что более вероятно, новый объект получаем из списка свободных объектов:
object = c->freelist (на первый свободный объект в списке указывает переменная freelist). При необходимости (как например отладка) память под выделенный объект заполняется нулями (если установлен флаг __GFP_ZERO).
* больше о SLUB allocator можно узнать на здесь
Возвращаясь к dup_task_struct(). Итак, выделив память для дескриптора процесса, далее выделяется память под структуру
struct thread_info (данная структура содержит информацию о процессе, которая специфична для архитектуры и определена в файле arch/x86/include/asm/thread_info.h) с помощью макроса alloc_thread_info(), который является оберткой для функции __get_free_pages():
arch/x86/include/asm/thread_info.h:
#define alloc_thread_info(tsk) ((struct thread_info *)__get_free_pages(THREAD_FLAGS, THREAD_ORDER))
Функция __get_free_pages() выделяет 2^THREAD_ORDER (THREAD_ORDER равен 0, если при сборке ядра была указана опция CONFIG_4KSTACKS, в противном случае он равен 1, другими словами выделять одну или две страницы памяти) смежных страниц физической памяти и возвращает логический адрес первой выделенной страницы.
И последнее, в dup_task_struct() устанавливается стек (см. рис.2):
1 2 3 |
tsk->stack = ti; |
Функция setup_thread_stack() просто копирует содержимое структуры thread_info «родителя» в структуру thread_info «ребенка» и связывает thread_info с task_struct:
include/linux/shed.h:
1 2 3 4 5 6 |
#define task_thread_info(task) ((struct thread_info *)(task)->stack) |
Рис.2. Связь между task_struct, thread_info и стеком процесса (kernel space)
Вернемся к функции copy_process(). У каждого процесса так или иначе есть свое адресное пространство, размер которого зависит от архитектуры (например для 32-х разрядных систем оно составляет 2^32, что составляет около 4 Гб). Процесс имеет доступ только к определенной области памяти (сегмент кода, сегмент данных, стек, bss и т.д.) из адресного пространства, причем на область памяти могут накладываться определенные права (запись, выполнение и т.д., см. рис.3).
Рис.3. Права (флаги) установленные в сегменте кода на секцию инструкций
Адресное пространство описывается структурой struct mm_struct, которая определена в файле include/linux/mm_types.h. Итак в функции copy_process() происходит выделение дескриптора памяти (struct mm_struct) для нового процесса. Выделение происходит посредством вызова функции copy_mm() и передаче ей флагов и дескриптора процесса.
* все области памяти для процесса можно просмотреть следующим образом: cat /proc/pid_your_process/maps
kernel/fork.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
static int copy_mm(unsigned long clone_flags, struct task_struct * tsk) |
Если установлен флаг CLONE_VM, то память под адресное пространство не выделяется, а используется совместно с родителем, таким образом мы получаем поток. Если флаг не установлен, то выделение происходит с помощью функции dup_mm(), которая, к слову сказать, использует ранее рассматривавшуюся функцию kmem_cache_alloc().
Далее снова берет на себя «бразды правления» функция copy_process(), заполняя task_struct.
P.S. Первое, за содействие при составлении статьи спасибо eddi. Второе, на написание статьи наталкнул commonD, без его комментария к прошлой статье я бы и не стал разбираться с этим вопросом. И последнее, но не менее важное, в pdf по мимо самой статьи есть еще два небольших приложения.