Подтвердить что ты не робот

Как запланированы/созданы потоки пользовательского уровня и как создаются потоки уровня ядра?

Извиняется, если этот вопрос глуп. Я пытался найти ответ в сети в течение довольно долгого времени, но не мог, и поэтому я спрашиваю здесь. Я изучаю темы, и я проходил через эту ссылку и эту конференцию Linux Plumbers Conference 2013 уровня ядра и уровня пользовательского уровня, и, насколько я понял, использование pthreads создает потоки в пользовательском пространстве, а ядро ​​не знает об этом и рассматривает его только как один процесс, не подозревая о том, сколько потоков внутри. В этом случае

  • кто решает планирование этих пользовательских потоков во время времени, которое получает процесс, поскольку ядро ​​рассматривает его как отдельный процесс и не знает о потоках и как выполняется планирование?
  • Если pthreads создают потоки пользовательского уровня, как создаются потоки уровня ядра или ОС, созданные из программ пользовательского пространства?
  • В соответствии с приведенной выше ссылкой говорится, что ядро ​​операционных систем предоставляет системный вызов для создания и управления потоками. Таким образом, системный вызов clone() создает поток нитей уровня или поток пользовательского уровня?
    • Если он создает поток уровня ядра, то strace простой программы pthreads также показывает использование clone() во время выполнения, но тогда почему это было бы рассмотрено поток пользовательского уровня?
    • Если он не создает поток уровня ядра, то как создаются потоки ядра из программ пользовательского пространства?
  • В соответствии с этой ссылкой он говорит: "Для каждого потока требуется полный блок управления потоками (TCB) для поддержки информации о потоках. В результате возникают значительные накладные расходы и увеличиваются по сложности ядра". Таким образом, в потоках уровня ядра, только куча разделяется, а остальные все являются отдельными для потока?

Изменить:

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

4b9b3361

Ответ 1

Это предваряется верхними комментариями.

Документация, которую вы читаете, является универсальной (не специфичной для Linux) и немного устаревшей. И, более того, он использует другую терминологию. То есть, я считаю, источником путаницы. Итак, читайте дальше...

То, что он называет потоком "пользовательский уровень", является тем, что я называю "устаревшим" потоком LWP. То, что он называет "потоком уровня ядра", является тем, что называется собственным потоком в linux. Под linux то, что называется потоком "ядра", совсем другое. [См. Ниже].

Использование pthreads создает потоки в пользовательском пространстве, и ядро ​​не знает об этом и рассматривает его как только один процесс, не подозревая о том, сколько потоков внутри.

Именно так были созданы потоки пользовательских потоков до NPTL (сборника потоков естественных posix). Это также и то, что SunOS/Solaris называют легким процессом LWP.

Был один процесс, который мультиплексировал себя и создал потоки. IIRC, он назывался процессом мастера потоков [или некоторых таких]. Ядро этого не знал. Ядро еще не поняло или не обеспечило поддержку потоков.

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

Кроме того, перед появлением "родных" потоков у вас может быть 10 процессов. Каждый процесс получает 10% от CPU. Если один из процессов был LWP, в котором было 10 потоков, эти потоки должны были делиться этим 10% и, таким образом, получали только 1% от каждого процессора.

Все это было заменено "родными" потоками, о которых знает планировщик ядра. Это изменение было сделано 10-15 лет назад.

Теперь, с приведенным выше примером, у нас есть 20 потоков/процессов, каждый из которых получает 5% от процессора. И переключатель контекста намного быстрее.

По-прежнему возможно иметь LWP-систему под собственным потоком, но теперь это выбор дизайна, а не необходимость.

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

Тем не менее, реализация до NPTL в glibc также должна была [принудительно] превзойти потоки LWP (т.е. реализовать временное выделение). Я не помню, какой именно механизм использовался, но вот пример. Мастер потока должен был установить будильник, заснуть, проснуться, а затем отправить активный поток на сигнал. Обработчик сигнала будет влиять на контекстный переключатель. Это было грязно, уродливо и несколько ненадежно.

Йоахим упомянул, что функция pthread_create создает поток ядра

Это [технически] неверно, чтобы назвать его нитью ядра. pthread_create создает собственный поток. Это запускается в пользовательском пространстве и противоречит временным рядам на равных началах с процессами. После создания существует небольшая разница между потоком и процессом.

Основное отличие состоит в том, что процесс имеет свое уникальное адресное пространство. Тем не менее, поток - это процесс, который разделяет его адресное пространство с другими процессами/потоками, которые являются частью той же группы потоков.

Если он не создает поток уровня ядра, то как создаются потоки ядра из пользовательских программ?

Нити ядра не являются потоками пользовательского пространства, NPTL, native или иным образом. Они создаются ядром через функцию kernel_thread. Они запускаются как часть ядра и не связаны ни с какой программой/процессом/потоком в пользовательском пространстве. Они имеют полный доступ к машине. Устройства, MMU и т.д. Потоки ядра выполняются на самом высоком уровне привилегий: ring 0. Они также запускаются в адресном пространстве ядра, а не в адресном пространстве любого пользовательского процесса/потока.

Пользовательская программа/процесс может не создавать поток ядра. Помните, что он создает собственный поток, используя pthread_create, который вызывает syscall clone для этого.

Темы полезны для работы, даже для ядра. Таким образом, он запускает часть своего кода в разных потоках. Вы можете увидеть эти потоки, выполнив ps ax. Посмотрите, и вы увидите kthreadd, ksoftirqd, kworker, rcu_sched, rcu_bh, watchdog, migration и т.д. Это потоки ядра, а не программы/процессы.


UPDATE:

Вы упомянули, что ядро ​​не знает о пользовательских потоках.

Помните, что, как упоминалось выше, существует две "эры".

(1) Прежде чем ядро ​​получит поддержку потоков (около 2004?). Это использовало мастер потоков (который, здесь, я буду называть LWP-планировщик). Ядро просто имело syscall.

(2) Все ядра после этого, которые понимают потоки. Нет мастера потоков, но у нас есть pthreads и syscall clone. Теперь fork реализуется как clone. clone похож на fork, но принимает некоторые аргументы. В частности, аргумент flags и аргумент child_stack.

Подробнее об этом ниже...

то как можно, чтобы потоки уровня пользователя имели отдельные стеки?

В стеке процессора нет ничего "волшебного". Я ограничу обсуждение [главным образом] до x86, но это применимо к любой архитектуре, даже к тем, у которых даже нет регистра стека (например, мэйнфреймы IBM эпохи IBM, такие как IBM System 370)

В x86 указатель стека %rsp. X86 имеет инструкции push и pop. Мы используем их для сохранения и восстановления вещей: push %rcx и [позже] pop %rcx.

Но, допустим, у x86 не было инструкций %rsp или push/pop? Может ли у нас еще стек? Конечно, по соглашению. Мы [как программисты] согласны с тем, что (т.е.) %rbx является указателем стека.

В этом случае "push" из %rcx будет [с использованием AT & T-ассемблера]:

subq    $8,%rbx
movq    %rcx,0(%rbx)

И, "pop" из %rcx будет:

movq    0(%rbx),%rcx
addq    $8,%rbx

Чтобы было проще, я перейду на псевдо-код C. Вот приведенный выше push/pop в псевдокоде:

// push %ecx
    %rbx -= 8;
    0(%rbx) = %ecx;

// pop %ecx
    %ecx = 0(%rbx);
    %rbx += 8;

Чтобы создать поток, планировщик LWP должен был создать область стека с помощью malloc. Затем он должен был сохранить этот указатель в структуре потока, а затем запустить дочерний LWP. Фактический код немного сложный, предположим, что у нас есть функция (например) LWP_create, которая похожа на pthread_create:

typedef void * (*LWP_func)(void *);

// per-thread control
typedef struct tsk tsk_t;
struct tsk {
    tsk_t *tsk_next;                    //
    tsk_t *tsk_prev;                    //
    void *tsk_stack;                    // stack base
    u64 tsk_regsave[16];
};

// list of tasks
typedef struct tsklist tsklist_t;
struct tsklist {
    tsk_t *tsk_next;                    //
    tsk_t *tsk_prev;                    //
};

tsklist_t tsklist;                      // list of tasks

tsk_t *tskcur;                          // current thread

// LWP_switch -- switch from one task to another
void
LWP_switch(tsk_t *to)
{

    // NOTE: we use (i.e.) burn register values as we do our work. in a real
    // implementation, we'd have to push/pop these in a special way. so, just
    // pretend that we do that ...

    // save all registers into tskcur->tsk_regsave
    tskcur->tsk_regsave[RAX] = %rax;
    // ...

    tskcur = to;

    // restore most registers from tskcur->tsk_regsave
    %rax = tskcur->tsk_regsave[RAX];
    // ...

    // set stack pointer to new task stack
    %rsp = tskcur->tsk_regsave[RSP];

    // set resume address for task
    push(%rsp,tskcur->tsk_regsave[RIP]);

    // issue "ret" instruction
    ret();
}

// LWP_create -- start a new LWP
tsk_t *
LWP_create(LWP_func start_routine,void *arg)
{
    tsk_t *tsknew;

    // get per-thread struct for new task
    tsknew = calloc(1,sizeof(tsk_t));
    append_to_tsklist(tsknew);

    // get new task stack
    tsknew->tsk_stack = malloc(0x100000)
    tsknew->tsk_regsave[RSP] = tsknew->tsk_stack;

    // give task its argument
    tsknew->tsk_regsave[RDI] = arg;

    // switch to new task
    LWP_switch(tsknew);

    return tsknew;
}

// LWP_destroy -- destroy an LWP
void
LWP_destroy(tsk_t *tsk)
{

    // free the task stack
    free(tsk->tsk_stack);

    remove_from_tsklist(tsk);

    // free per-thread struct for dead task
    free(tsk);
}

С ядром, которое понимает потоки, мы используем pthread_create и clone, но нам все равно нужно создать новый стек потоков. Ядро не создает/не назначает стек для нового потока. Сценарий clone принимает аргумент child_stack. Таким образом, pthread_create должен выделить стек для нового потока и передать его на clone:

// pthread_create -- start a new native thread
tsk_t *
pthread_create(LWP_func start_routine,void *arg)
{
    tsk_t *tsknew;

    // get per-thread struct for new task
    tsknew = calloc(1,sizeof(tsk_t));
    append_to_tsklist(tsknew);

    // get new task stack
    tsknew->tsk_stack = malloc(0x100000)

    // start up thread
    clone(start_routine,tsknew->tsk_stack,CLONE_THREAD,arg);

    return tsknew;
}

// pthread_join -- destroy an LWP
void
pthread_join(tsk_t *tsk)
{

    // wait for thread to die ...

    // free the task stack
    free(tsk->tsk_stack);

    remove_from_tsklist(tsk);

    // free per-thread struct for dead task
    free(tsk);
}

Только процессу или основному потоку назначается его исходный стек ядром, обычно с высоким адресом памяти. Итак, если процесс не использует потоки, как правило, он просто использует этот предварительно назначенный стек.

Но, если поток создается, либо LWP, либо исходный, начальный процесс/поток должен предварительно выделить область для предлагаемого потока с помощью malloc. Замечание: использование malloc является обычным способом, но создатель потока может просто иметь большой пул глобальной памяти: char stack_area[MAXTASK][0x100000];, если он захочет сделать это таким образом.

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

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

См. мой ответ здесь: В чем разница между пользовательским стеком и встроенным стекем при использовании памяти?

Ответ 2

Потоки пользовательского уровня обычно являются сопрограммами в той или иной форме. Переключить контекст между потоками исполнения в пользовательском режиме без участия ядра. Из ядра POV - это один поток. Фактический поток фактически контролируется в пользовательском режиме, и пользовательский режим может приостанавливать, переключать, возобновлять логические потоки выполнения (т.е. сопрограммы). Все это происходит во время квантов, запланированных для фактического потока. Ядро может и бесцеремонно прервать фактический поток (поток ядра) и передать управление процессору другому потоку.

Контексты пользовательского режима требуют совместной многозадачности. Потоки пользовательского режима должны периодически отказываться от контроля над другими потоками режима пользователя (в основном, выполнение изменяет контекст в поток нового пользовательского режима, без потока ядра, когда-либо замечающего что-либо). Обычно происходит то, что код знает намного лучше, когда он хочет освободить контроль над ядром. Плохо закодированная сопрограмма может красть контроль и голодать все остальные сопрограммы.

В исторической реализации используется setcontext, но теперь она устарела. Boost.context предлагает замену для него, но не полностью переносится:

Boost.Context - это основополагающая библиотека, которая предоставляет своего рода кооперативную многозадачность в одном потоке. Предоставляя абстракцию текущего состояния выполнения в текущем потоке, включая стек (с локальными переменными) и указатель стека, все регистры и флаги CPU, а также указатель команд, context_context представляет собой конкретную точку в пути выполнения приложения.

Неудивительно, что Boost.coroutine основан на Boost.context.

Windows предоставила Fibers..Net runtime имеет задачи и async/await.

Ответ 3

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

Источник - интервью Ксавьера Лероя (человека, создавшего LinuxThreads) http://pauillac.inria.fr/~xleroy/linuxthreads/faq.html#K