Ответ 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
и описанную выше ассемблерную обманку для создания гораздо большего стека, если он выполняет чрезвычайно рекурсивную функцию.
См. мой ответ здесь: В чем разница между пользовательским стеком и встроенным стекем при использовании памяти?