Как смоделировать стек для системного вызова clone() в linux?

Системный вызов clone() в Linux принимает параметр, указывающий на стек для нового созданного потока. Очевидным способом сделать это является просто malloc некоторое пространство и передать это, но тогда вы должны быть уверены, что malloc'd столько же пространства стека, сколько этот поток будет использовать (трудно предсказать).

Я помнил, что при использовании pthreads мне не пришлось это делать, поэтому мне было любопытно, что он сделал. Я наткнулся на этот сайт, который объясняет: "Лучшим решением, используемым реализацией Linux pthreads, является использование mmap для выделения памяти с указанием флагов область памяти, которая выделяется при ее использовании. Таким образом, память выделяется для стека по мере необходимости, а нарушение сегментации будет происходить, если система не может выделить дополнительную память."

Единственный контекст, который я когда-либо слышал в mmap, используется для сопоставления файлов в памяти и, действительно, считывания man-страницы mmap требуется дескриптор файла. Как это можно использовать для выделения стека динамической длины для предоставления clone()? Этот сайт просто сумасшедший?;)

В любом случае, не нужно ли ядро ​​знать, как найти свободную связку памяти для нового стека, так как это что-то нужно делать все время, когда пользователь запускает новые процессы? Почему указатель стека даже нужно указывать в первую очередь, если ядро ​​уже может это понять?

Ответ 1

Джозеф в ответ на ваш последний вопрос:

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

Если пользователь заменяет текущий выполняемый процесс с помощью exec(), ядро ​​необходимо создать новый стек, но в этом случае это легко, потому что он начинает работать с чистого листа. exec() вытирает пространство памяти процесса и повторно инициализирует его, поэтому ядро ​​скажет "после exec(), стек всегда живет ЗДЕСЬ".

Если, однако, мы используем clone(), то можно сказать, что новый процесс будет разделять пространство памяти со старым процессом (CLONE_VM). В этой ситуации ядро ​​не может покинуть стек, как это было в вызывающем процессе (например, fork()), потому что тогда наши два процесса будут топать друг на друга. Ядро также не может просто поместить его в местоположение по умолчанию (например, exec()), потому что это место уже занято в этом пространстве памяти. Единственное решение - позволить вызывающему процессу найти место для него, что и есть.

Ответ 2

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

Когда люди говорят о том, что стек динамически растет, что они могут иметь в виду, это одна из двух вещей:

  • Страницы стека могут представлять собой копии с нулевыми страницами, которые не получают закрытых копий, пока не будет выполнена первая запись.
  • Нижние части области стека еще не могут быть зарезервированы (и, следовательно, не учитываются в отношении платы за совершение процесса, т.е. количество физической памяти/своп ядра учитывается как зарезервированное для процесса), пока не пострадает страница защиты, и в этом случае ядро ​​совершает больше и перемещает страницу защиты, или убивает процесс, если нет памяти для фиксации.

Попытка полагаться на флаг MAP_GROWSDOWN ненадежна и опасна, потому что она не может защитить вас от mmap, создавая новое сопоставление, только что смежное с вашим стеком, которое затем будет сбито. (Смотрите http://lwn.net/Articles/294001/). Для основного потока ядро ​​автоматически резервирует размер адресного пространства ulimit для стека (не для памяти) ниже стека и предотвращает выделение mmap. (Но будьте осторожны! Некоторые сломанные ядра, обработанные поставщиком, отключили это поведение, приводящее к случайному повреждению памяти!) Для других потоков вы просто должны mmap весь диапазон адресного пространства, который может понадобиться для стека, когда создавая его. Другого пути нет. Вы могли бы сделать большую часть изначально незаписываемой/нечитаемой и изменить это на наличие ошибок, но тогда вам понадобятся обработчики сигналов, и это решение неприемлемо для реализации потоков POSIX, поскольку это будет мешать обработчикам сигналов приложения. (Обратите внимание, что в качестве расширения ядро ​​может предлагать специальные флаги MAP_ для доставки другого сигнала вместо SIGSEGV при незаконном доступе к сопоставлению, а затем реализация потоков может захватывать и действовать по этому сигналу. настоящее не имеет такой возможности.)

Наконец, обратите внимание, что в syscall clone не требуется аргумент указателя стека, потому что он не нужен. Syscall должен выполняться из кода сборки, потому что оболочка пользовательского пространства требуется для изменения указателя стека в потоке "child", чтобы указать на нужный стек, и не записывать ничего в родительский стек.

Фактически, clone принимает аргумент указателя стека, потому что небезопасно ждать, чтобы изменить указатель стека в "дочернем" после возвращения в пользовательское пространство. Если сигналы не заблокированы, обработчик сигналов может работать сразу же в неправильном стеке, а на некоторых архитектурах указатель стека должен быть действительным и указывать на безопасную область для записи в любое время.

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

Ответ 3

Вам нужен флаг MAP_ANONYMOUS для mmap. И MAP_GROWSDOWN, поскольку вы хотите использовать его как стек.

Что-то вроде:

void *stack = mmap(NULL,initial_stacksize,PROT_WRITE|PROT_READ,MAP_PRIVATE|MAP_GROWSDOWN|MAP_ANONYMOUS,-1,0);

Для получения дополнительной информации см. справочную страницу mmap. И помните, что клон - это концепция низкого уровня, которую вы не собираетесь использовать, если вам действительно не нужна то, что она предлагает. И он предлагает большой контроль - например, настройку собственного стека - на случай, если вы захотите совершить какое-либо обманывание (например, наличие стека доступно во всех связанных процессах). Если у вас нет оснований для использования клона, придерживайтесь вилки или pthreads.

Ответ 4

Вот код, который отображает область стека и инструктирует системный вызов clone использовать эту область в качестве стека.

#include <sys/mman.h>
#include <stdio.h>
#include <string.h>
#include <sched.h>

int execute_clone(void *arg)
{
    printf("\nclone function Executed....Sleeping\n");
    fflush(stdout);
    return 0;
}

int main()
{
    void *ptr;
    int rc;
    void *start =(void *) 0x0000010000000000;
    size_t len = 0x0000000000200000;

    ptr = mmap(start, len, PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE|MAP_FIXED|MAP_GROWSDOWN, 0, 0);
    if(ptr == (void *)-1) 
    {
        perror("\nmmap failed");
    }

    rc = clone(&execute_clone, ptr + len, CLONE_VM, NULL);

    if(rc <= 0) 
    {
        perror("\nClone() failed");
    }
}

Ответ 5

mmap больше, чем просто отображение файла в память. Фактически, некоторые реализации malloc будут использовать mmap для больших распределений. Если вы прочитаете страницу с хорошим человеком, вы заметите флаг MAP_ANONYMOUS, и вы увидите, что вам не нужно вообще предоставлять файловый дескриптор.

Что касается того, почему ядро ​​не может просто "найти кучу свободной памяти", хорошо, если вы хотите, чтобы кто-то сделал эту работу для вас, вместо этого используйте fork или используйте pthreads.

Ответ 6

Обратите внимание, что системный вызов clone не принимает аргумент для расположения стека. Он фактически работает так же, как fork. Это просто оболочка glibc, которая принимает этот аргумент.

Ответ 7

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