You are on page 1of 8

Механизм создания процессов в Linux. Продолжение.

В прошлый раз было рассмотрено, как происходит создание нового процесса в


системе, но не было рассказано про выделение памяти процессу и под сам процесс.
Итак мы остановились на функции copy_process(), которая как было сказано
выполняет фактическую работу по созданию нового процесса. В copy_process()
происходит вызов функции dup_task_struct(), которая создает стек ядра и выделяет
память под структуры task_struct и thread_info:

kernel/fork.c:
static struct task_struct *copy_process(...)
{
...
p = dup_task_struct(current);
...
if ((retval = copy_mm(clone_flags, p)))
goto bad_fork_cleanup_signal;
...
}

В dup_task_struct() передается указатель (current — макрос, который


определен в файле arch/x86/include/asm/current.h) на текущий (вызвавший, то есть
«родитель») процесс. Ниже приведен наиболее важный код из dup_task_struct():

kernel/fork.c:
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk;
struct thread_info *ti;
int err;

prepare_to_copy(orig);

tsk = alloc_task_struct();
if (!tsk)
return NULL;
ti = alloc_thread_info(tsk);
if (!ti) {
free_task_struct(tsk);
return NULL;
}
err = arch_dup_task_struct(tsk, orig);
if (err)
goto out;
tsk->stack = ti;
...
setup_thread_stack(tsk, orig);
...
return tsk;
...
}

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.»

При ручной сборке ядра имеется возможность выбрать allocator см. рис.1.

Рис.1. Выбор allocator'а

Итак мы определились с тем, что будем рассматривать интерфейсы slub


allocator'а. Продолжим рассмотрение kmem_cache_alloc(). kmem_cache_alloc()
экспортируется ядром, что позволяет нам его использовать при написании модулей
ядра, в тоже время это обертка для функции slab_alloc():

mm/slub.c:
void *kmem_cache_alloc(struct kmem_cache *s, gfp_t gfpflags)
{
return slab_alloc(s, gfpflags, -1, __builtin_return_address(0));
}
EXPORT_SYMBOL(kmem_cache_alloc);

mm/slub.c:
static __always_inline void *slab_alloc(struct kmem_cache *s,
gfp_t gfpflags, int node, void *addr)
{
void **object;
struct kmem_cache_cpu *c;
...
c = get_cpu_slab(s, smp_processor_id());
...
if (unlikely(!c->freelist || !node_match(c, node)))
object = __slab_alloc(s, gfpflags, node, addr, c);
else {
object = c->freelist;
c->freelist = object[c->offset];
stat(c, ALLOC_FASTPATH);
}
...
return object;
}

Итак, что происходит в slab_alloс()? Узнаем к какому процессору


принадлежит кэш объектов task_struct (get_cpu_slab()). Далее есть два пути:
медленный и быстрый. Медленный: если нет больше свободных объектов, то
выбирается следующий свободный сляб или же, если больше нет свободных слябов -
создается новый сляб, из которого происходит выделение объекта, за это отвечает
функция __slab_alloc(). В противном случае (быстрый путь), что более вероятно,
новый объект получаем из списка свободных объектов: object = c->freelist (на
первый свободный объект в списке указывает переменная freelist). При
необходимости (как например отладка) память под выделенный объект заполняется
нулями (если установлен флаг __GFP_ZERO).

* больше о SLUB allocator можно узнать здесь: http://lwn.net/Articles/229984/

Возвращаясь к 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))

* я не могу понять, зачем передавать указатель на task_struct, если он не используется?

Функция __get_free_pages() выделяет 2^THREAD_ORDER (THREAD_ORDER равен 0,


если при сборке ядра была указана опция CONFIG_4KSTACKS, в противном случае он
равен 1, другими словами выделять одну или две страницы памяти) смежных страниц
физической памяти и возвращает логический адрес первой выделенной страницы.
И последнее, в dup_task_struct() устанавливается стек (см. рис.2):

tsk->stack = ti;
...
setup_thread_stack(tsk, orig);

Функция setup_thread_stack() просто копирует содержимое структуры


thread_info «родителя» в структуру thread_info «ребенка» и связывает thread_info
с task_struct:
include/linux/shed.h:
#define task_thread_info(task) ((struct thread_info *)(task)->stack)
static inline void setup_thread_stack(struct task_struct *p, struct
task_struct *org)
{
*task_thread_info(p) = *task_thread_info(org);
task_thread_info(p)->task = p;
}

Рис.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:
static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
{
struct mm_struct * mm, *oldmm;
...
tsk->mm = NULL;
tsk->active_mm = NULL;

oldmm = current->mm;
if (!oldmm)
return 0;

if (clone_flags & CLONE_VM) {


atomic_inc(&oldmm->mm_users);
mm = oldmm;
goto good_mm;
}
...
mm = dup_mm(tsk);
if (!mm)
goto fail_nomem;
good_mm:
...
tsk->mm = mm;
tsk->active_mm = mm;
return 0;
...
}

Если установлен флаг CLONE_VM, то память под адресное пространство не


выделяется, а используется совместно с родителем, таким образом мы получаем
поток. Если флаг не установлен, то выделение происходит с помощью функции
dup_mm(), которая, к слову сказать, использует ранее рассматривавшуюся функцию
kmem_cache_alloc().
Далее снова берет на себя «бразды правления» функция copy_process(),
заполняя task_struct.
ПРИЛОЖЕНИЕ А. Пример создания нового кэша.
#include <linux/module.h>
#include <linux/kernel.h>
#include <config/slub.h>

static struct kmem_cache *my_cachep;

/* Создание нового кэша slub */


static void init_my_cache(void)
{
my_cachep = kmem_cache_create(
"my_cache", /* Name */
32, /* Object Size */
0, /* Alignment */
SLAB_HWCACHE_ALIGN, /* Flags */
NULL); /* Constructor for the objects */
}
/* Выделение и освобождение объектов */
int slab_test(void)
{
void *object;

printk(KERN_ALERT "Cache name is %s\n", kmem_cache_name(my_cachep));


printk(KERN_ALERT "Cache object size is %d\n",kmem_cache_size(my_cachep));

object = kmem_cache_alloc(my_cachep, GFP_KERNEL);

if (object) {
kmem_cache_free(my_cachep, object);
}

return 0;
}
/* Удаление кэша slub */
static void remove_my_cache(void)
{
if (my_cachep) kmem_cache_destroy(my_cachep);
}

int init_module(void)
{
printk(KERN_ALERT "Start module\n");
init_my_cache();
(void) slab_test();
remove_my_cache();

return 0;
}
void cleanup_module(void)
{
printk(KERN_ALERT "End module\n");
}
MODULE_LICENSE("GPL");
MODULE_AUTHOR("From the site ibm.com");
MODULE_DESCRIPTION("Example of creating a new cache");
Перед Вами модуль ядра, в котором происходит создание нового кэша
используя интерфейсы сля(ю)бового распределителя (slub allocator). Он был взять
с сайта http://www.ibm.com/developerworks/ru/library/l-linux-slab-allocator/.

* на сайте представлено три основных функции init_my_cache, slab_test,


* remove_my_cache, автор предоставляет читателю объединить их в модуль. Есть
* «незначительное» изменение, в статье речь идет о slab allocatore, мы же
* используем slub, поэтому и подключаем библиотеку <config/slub.h>.

Данный модуль можно собрать следующим образом:

echo 'obj-m += module.o' > Makefile


make -C path_to_headers SUBDIRS=$PWD modules

* module.o — соответственно название модуля должно быть module.c


* path_to_headers — путь к заголовкам, если они не установлены, то установите их

Теперь загрузим и сразу выгрузим его:

sudo insmod ./module.ko


sudo rmmod module

Результат работы модуля можно просмотреть командой:

dmesg | tail -n 10
ПРИЛОЖЕНИЕ Б. GDB + DDD + QEMU
Возвращаясь к инструментам отладки. Одним из способов «подглядеть», что
происходит в ядре во время работы, может служить связка gdb + ddd + qemu, где:
• gdb (GNU Debugger) - отладчик;
• ddd (Data Display Debugger) - графический интерфейс к gdb;
• qemu - эмулятор аппаратной платформы x86.
На рисунке ниже показан запущенный ddd (естественно qemu уже работает) в
момент создания нового процесса: