极客时间已完结课程限时免费阅读

19 | 线程的创建:如何执行一个新子项目?

19 | 线程的创建:如何执行一个新子项目?-极客时间

19 | 线程的创建:如何执行一个新子项目?

讲述:刘超

时长13:08大小12.00M

上一节,我们了解了进程创建的整个过程,今天我们来看线程创建的过程。
我们前面已经写过多线程编程的程序了,你应该都知道创建一个线程调用的是 pthread_create,可你知道它背后的机制吗?

用户态创建线程

你可能会问,咱们之前不是讲过了吗?无论是进程还是线程,在内核里面都是任务,管起来不是都一样吗?但是问题来了,如果两个完全一样,那为什么咱们前两节写的程序差别那么大?如果不一样,那怎么在内核里面加以区分呢?
其实,线程不是一个完全由内核实现的机制,它是由内核态和用户态合作完成的。pthread_create 不是一个系统调用,是 Glibc 库的一个函数,所以我们还要去 Glibc 里面去找线索。
果然,我们在 nptl/pthread_create.c 里面找到了这个函数。这里的参数我们应该比较熟悉了。
int __pthread_create_2_1 (pthread_t *newthread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg)
{
......
}
versioned_symbol (libpthread, __pthread_create_2_1, pthread_create, GLIBC_2_1);
下面我们依次来看这个函数做了些啥。
首先处理的是线程的属性参数。例如前面写程序的时候,我们设置的线程栈大小。如果没有传入线程属性,就取默认值。
const struct pthread_attr *iattr = (struct pthread_attr *) attr;
struct pthread_attr default_attr;
if (iattr == NULL)
{
......
iattr = &default_attr;
}
接下来,就像在内核里一样,每一个进程或者线程都有一个 task_struct 结构,在用户态也有一个用于维护线程的结构,就是这个 pthread 结构。
struct pthread *pd = NULL;
凡是涉及函数的调用,都要使用到栈。每个线程也有自己的栈。那接下来就是创建线程栈了。
int err = ALLOCATE_STACK (iattr, &pd);
ALLOCATE_STACK 是一个宏,我们找到它的定义之后,发现它其实就是一个函数。只是,这个函数有些复杂,所以我这里把主要的代码列一下。
# define ALLOCATE_STACK(attr, pd) allocate_stack (attr, pd, &stackaddr)
static int
allocate_stack (const struct pthread_attr *attr, struct pthread **pdp,
ALLOCATE_STACK_PARMS)
{
struct pthread *pd;
size_t size;
size_t pagesize_m1 = __getpagesize () - 1;
......
size = attr->stacksize;
......
/* Allocate some anonymous memory. If possible use the cache. */
size_t guardsize;
void *mem;
const int prot = (PROT_READ | PROT_WRITE
| ((GL(dl_stack_flags) & PF_X) ? PROT_EXEC : 0));
/* Adjust the stack size for alignment. */
size &= ~__static_tls_align_m1;
/* Make sure the size of the stack is enough for the guard and
eventually the thread descriptor. */
guardsize = (attr->guardsize + pagesize_m1) & ~pagesize_m1;
size += guardsize;
pd = get_cached_stack (&size, &mem);
if (pd == NULL)
{
/* If a guard page is required, avoid committing memory by first
allocate with PROT_NONE and then reserve with required permission
excluding the guard page. */
mem = __mmap (NULL, size, (guardsize == 0) ? prot : PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
/* Place the thread descriptor at the end of the stack. */
#if TLS_TCB_AT_TP
pd = (struct pthread *) ((char *) mem + size) - 1;
#elif TLS_DTV_AT_TP
pd = (struct pthread *) ((((uintptr_t) mem + size - __static_tls_size) & ~__static_tls_align_m1) - TLS_PRE_TCB_SIZE);
#endif
/* Now mprotect the required region excluding the guard area. */
char *guard = guard_position (mem, size, guardsize, pd, pagesize_m1);
setup_stack_prot (mem, size, guard, guardsize, prot);
pd->stackblock = mem;
pd->stackblock_size = size;
pd->guardsize = guardsize;
pd->specific[0] = pd->specific_1stblock;
/* And add to the list of stacks in use. */
stack_list_add (&pd->list, &stack_used);
}
*pdp = pd;
void *stacktop;
# if TLS_TCB_AT_TP
/* The stack begins before the TCB and the static TLS block. */
stacktop = ((char *) (pd + 1) - __static_tls_size);
# elif TLS_DTV_AT_TP
stacktop = (char *) (pd - 1);
# endif
*stack = stacktop;
......
}
我们来看一下,allocate_stack 主要做了以下这些事情:
如果你在线程属性里面设置过栈的大小,需要你把设置的值拿出来;
为了防止栈的访问越界,在栈的末尾会有一块空间 guardsize,一旦访问到这里就错误了;
其实线程栈是在进程的堆里面创建的。如果一个进程不断地创建和删除线程,我们不可能不断地去申请和清除线程栈使用的内存块,这样就需要有一个缓存。get_cached_stack 就是根据计算出来的 size 大小,看一看已经有的缓存中,有没有已经能够满足条件的;
如果缓存里面没有,就需要调用 __mmap 创建一块新的,系统调用那一节我们讲过,如果要在堆里面 malloc 一块内存,比较大的话,用 __mmap;
线程栈也是自顶向下生长的,还记得每个线程要有一个 pthread 结构,这个结构也是放在栈的空间里面的。在栈底的位置,其实是地址最高位;
计算出 guard 内存的位置,调用 setup_stack_prot 设置这块内存的是受保护的;
接下来,开始填充 pthread 这个结构里面的成员变量 stackblock、stackblock_size、guardsize、specific。这里的 specific 是用于存放 Thread Specific Data 的,也即属于线程的全局变量;
将这个线程栈放到 stack_used 链表中,其实管理线程栈总共有两个链表,一个是 stack_used,也就是这个栈正被使用;另一个是 stack_cache,就是上面说的,一旦线程结束,先缓存起来,不释放,等有其他的线程创建的时候,给其他的线程用。
搞定了用户态栈的问题,其实用户态的事情基本搞定了一半。

内核态创建任务

接下来,我们接着 pthread_create 看。其实有了用户态的栈,接着需要解决的就是用户态的程序从哪里开始运行的问题。
pd->start_routine = start_routine;
pd->arg = arg;
pd->schedpolicy = self->schedpolicy;
pd->schedparam = self->schedparam;
/* Pass the descriptor to the caller. */
*newthread = (pthread_t) pd;
atomic_increment (&__nptl_nthreads);
retval = create_thread (pd, iattr, &stopped_start, STACK_VARIABLES_ARGS, &thread_ran);
start_routine 就是咱们给线程的函数,start_routine,start_routine 的参数 arg,以及调度策略都要赋值给 pthread。
接下来 __nptl_nthreads 加一,说明又多了一个线程。
真正创建线程的是调用 create_thread 函数,这个函数定义如下:
static int
create_thread (struct pthread *pd, const struct pthread_attr *attr,
bool *stopped_start, STACK_VARIABLES_PARMS, bool *thread_ran)
{
const int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM | CLONE_SIGHAND | CLONE_THREAD | CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID | 0);
ARCH_CLONE (&start_thread, STACK_VARIABLES_ARGS, clone_flags, pd, &pd->tid, tp, &pd->tid);
/* It's started now, so if we fail below, we'll have to cancel it
and let it clean itself up. */
*thread_ran = true;
}
这里面有很长的 clone_flags,这些咱们原来一直没注意,不过接下来的过程,我们要特别的关注一下这些标志位。
然后就是 ARCH_CLONE,其实调用的是 __clone。看到这里,你应该就有感觉了,马上就要到系统调用了。
# define ARCH_CLONE __clone
/* The userland implementation is:
int clone (int (*fn)(void *arg), void *child_stack, int flags, void *arg),
the kernel entry is:
int clone (long flags, void *child_stack).
The parameters are passed in register and on the stack from userland:
rdi: fn
rsi: child_stack
rdx: flags
rcx: arg
r8d: TID field in parent
r9d: thread pointer
%esp+8: TID field in child
The kernel expects:
rax: system call number
rdi: flags
rsi: child_stack
rdx: TID field in parent
r10: TID field in child
r8: thread pointer */
.text
ENTRY (__clone)
movq $-EINVAL,%rax
......
/* Insert the argument onto the new stack. */
subq $16,%rsi
movq %rcx,8(%rsi)
/* Save the function pointer. It will be popped off in the
child in the ebx frobbing below. */
movq %rdi,0(%rsi)
/* Do the system call. */
movq %rdx, %rdi
movq %r8, %rdx
movq %r9, %r8
mov 8(%rsp), %R10_LP
movl $SYS_ify(clone),%eax
......
syscall
......
PSEUDO_END (__clone)
如果对于汇编不太熟悉也没关系,你可以重点看上面的注释。
我们能看到最后调用了 syscall,这一点 clone 和我们原来熟悉的其他系统调用几乎是一致的。但是,也有少许不一样的地方。
如果在进程的主线程里面调用其他系统调用,当前用户态的栈是指向整个进程的栈,栈顶指针也是指向进程的栈,指令指针也是指向进程的主线程的代码。此时此刻执行到这里,调用 clone 的时候,用户态的栈、栈顶指针、指令指针和其他系统调用一样,都是指向主线程的。
但是对于线程来说,这些都要变。因为我们希望当 clone 这个系统调用成功的时候,除了内核里面有这个线程对应的 task_struct,当系统调用返回到用户态的时候,用户态的栈应该是线程的栈,栈顶指针应该指向线程的栈,指令指针应该指向线程将要执行的那个函数。
所以这些都需要我们自己做,将线程要执行的函数的参数和指令的位置都压到栈里面,当从内核返回,从栈里弹出来的时候,就从这个函数开始,带着这些参数执行下去。
接下来我们就要进入内核了。内核里面对于 clone 系统调用的定义是这样的:
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
unsigned long, tls)
{
return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls);
}
看到这里,发现了熟悉的面孔 _do_fork,是不是轻松了一些?上一节我们已经沿着它的逻辑过了一遍了。这里我们重点关注几个区别。
第一个是上面复杂的标志位设定,我们来看都影响了什么。
对于 copy_files,原来是调用 dup_fd 复制一个 files_struct 的,现在因为 CLONE_FILES 标识位变成将原来的 files_struct 引用计数加一。
static int copy_files(unsigned long clone_flags, struct task_struct *tsk)
{
struct files_struct *oldf, *newf;
oldf = current->files;
if (clone_flags & CLONE_FILES) {
atomic_inc(&oldf->count);
goto out;
}
newf = dup_fd(oldf, &error);
tsk->files = newf;
out:
return error;
}
对于 copy_fs,原来是调用 copy_fs_struct 复制一个 fs_struct,现在因为 CLONE_FS 标识位变成将原来的 fs_struct 的用户数加一。
static int copy_fs(unsigned long clone_flags, struct task_struct *tsk)
{
struct fs_struct *fs = current->fs;
if (clone_flags & CLONE_FS) {
fs->users++;
return 0;
}
tsk->fs = copy_fs_struct(fs);
return 0;
}
对于 copy_sighand,原来是创建一个新的 sighand_struct,现在因为 CLONE_SIGHAND 标识位变成将原来的 sighand_struct 引用计数加一。
static int copy_sighand(unsigned long clone_flags, struct task_struct *tsk)
{
struct sighand_struct *sig;
if (clone_flags & CLONE_SIGHAND) {
atomic_inc(&current->sighand->count);
return 0;
}
sig = kmem_cache_alloc(sighand_cachep, GFP_KERNEL);
atomic_set(&sig->count, 1);
memcpy(sig->action, current->sighand->action, sizeof(sig->action));
return 0;
}
对于 copy_signal,原来是创建一个新的 signal_struct,现在因为 CLONE_THREAD 直接返回了。
static int copy_signal(unsigned long clone_flags, struct task_struct *tsk)
{
struct signal_struct *sig;
if (clone_flags & CLONE_THREAD)
return 0;
sig = kmem_cache_zalloc(signal_cachep, GFP_KERNEL);
tsk->signal = sig;
init_sigpending(&sig->shared_pending);
......
}
对于 copy_mm,原来是调用 dup_mm 复制一个 mm_struct,现在因为 CLONE_VM 标识位而直接指向了原来的 mm_struct。
static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
struct mm_struct *mm, *oldmm;
oldmm = current->mm;
if (clone_flags & CLONE_VM) {
mmget(oldmm);
mm = oldmm;
goto good_mm;
}
mm = dup_mm(tsk);
good_mm:
tsk->mm = mm;
tsk->active_mm = mm;
return 0;
}
第二个就是对于亲缘关系的影响,毕竟我们要识别多个线程是不是属于一个进程。
p->pid = pid_nr(pid);
if (clone_flags & CLONE_THREAD) {
p->exit_signal = -1;
p->group_leader = current->group_leader;
p->tgid = current->tgid;
} else {
if (clone_flags & CLONE_PARENT)
p->exit_signal = current->group_leader->exit_signal;
else
p->exit_signal = (clone_flags & CSIGNAL);
p->group_leader = p;
p->tgid = p->pid;
}
/* CLONE_PARENT re-uses the old parent */
if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
p->real_parent = current->real_parent;
p->parent_exec_id = current->parent_exec_id;
} else {
p->real_parent = current;
p->parent_exec_id = current->self_exec_id;
}
从上面的代码可以看出,使用了 CLONE_THREAD 标识位之后,使得亲缘关系有了一定的变化。
如果是新进程,那这个进程的 group_leader 就是它自己,tgid 是它自己的 pid,这就完全重打锣鼓另开张了,自己是线程组的头。如果是新线程,group_leader 是当前进程的,group_leader,tgid 是当前进程的 tgid,也就是当前进程的 pid,这个时候还是拜原来进程为老大。
如果是新进程,新进程的 real_parent 是当前的进程,在进程树里面又见一辈人;如果是新线程,线程的 real_parent 是当前的进程的 real_parent,其实是平辈的。
第三,对于信号的处理,如何保证发给进程的信号虽然可以被一个线程处理,但是影响范围应该是整个进程的。例如,kill 一个进程,则所有线程都要被干掉。如果一个信号是发给一个线程的 pthread_kill,则应该只有线程能够收到。
在 copy_process 的主流程里面,无论是创建进程还是线程,都会初始化 struct sigpending pending,也就是每个 task_struct,都会有这样一个成员变量。这就是一个信号列表。如果这个 task_struct 是一个线程,这里面的信号就是发给这个线程的;如果这个 task_struct 是一个进程,这里面的信号是发给主线程的。
init_sigpending(&p->pending);
另外,上面 copy_signal 的时候,我们可以看到,在创建进程的过程中,会初始化 signal_struct 里面的 struct sigpending shared_pending。但是,在创建线程的过程中,连 signal_struct 都共享了。也就是说,整个进程里的所有线程共享一个 shared_pending,这也是一个信号列表,是发给整个进程的,哪个线程处理都一样。
init_sigpending(&sig->shared_pending);
至此,clone 在内核的调用完毕,要返回系统调用,回到用户态。

用户态执行线程

根据 __clone 的第一个参数,回到用户态也不是直接运行我们指定的那个函数,而是一个通用的 start_thread,这是所有线程在用户态的统一入口。
#define START_THREAD_DEFN \
static int __attribute__ ((noreturn)) start_thread (void *arg)
START_THREAD_DEFN
{
struct pthread *pd = START_THREAD_SELF;
/* Run the code the user provided. */
THREAD_SETMEM (pd, result, pd->start_routine (pd->arg));
/* Call destructors for the thread_local TLS variables. */
/* Run the destructor for the thread-local data. */
__nptl_deallocate_tsd ();
if (__glibc_unlikely (atomic_decrement_and_test (&__nptl_nthreads)))
/* This was the last thread. */
exit (0);
__free_tcb (pd);
__exit_thread ();
}
在 start_thread 入口函数中,才真正的调用用户提供的函数,在用户的函数执行完毕之后,会释放这个线程相关的数据。例如,线程本地数据 thread_local variables,线程数目也减一。如果这是最后一个线程了,就直接退出进程,另外 __free_tcb 用于释放 pthread。
void
internal_function
__free_tcb (struct pthread *pd)
{
......
__deallocate_stack (pd);
}
void
internal_function
__deallocate_stack (struct pthread *pd)
{
/* Remove the thread from the list of threads with user defined
stacks. */
stack_list_del (&pd->list);
/* Not much to do. Just free the mmap()ed memory. Note that we do
not reset the 'used' flag in the 'tid' field. This is done by
the kernel. If no thread has been created yet this field is
still zero. */
if (__glibc_likely (! pd->user_stack))
(void) queue_stack (pd);
}
__free_tcb 会调用 __deallocate_stack 来释放整个线程栈,这个线程栈要从当前使用线程栈的列表 stack_used 中拿下来,放到缓存的线程栈列表 stack_cache 中。
好了,整个线程的生命周期到这里就结束了。

总结时刻

线程的调用过程解析完毕了,我画了一个图总结一下。这个图对比了创建进程和创建线程在用户态和内核态的不同。
创建进程的话,调用的系统调用是 fork,在 copy_process 函数里面,会将五大结构 files_struct、fs_struct、sighand_struct、signal_struct、mm_struct 都复制一遍,从此父进程和子进程各用各的数据结构。而创建线程的话,调用的是系统调用 clone,在 copy_process 函数里面, 五大结构仅仅是引用计数加一,也即线程共享进程的数据结构。

课堂练习

你知道如果查看一个进程的线程以及线程栈的使用情况吗?请找一下相关的命令和 API,尝试一下。
欢迎留言和我分享你的疑惑和见解,也欢迎你收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习、进步。
分享给需要的人,Ta购买本课程,你将得20
生成海报并分享

赞 15

提建议

上一篇
18 | 进程的创建:如何发起一个新项目?
下一篇
20 | 内存管理(上):为客户保密,规划进程内存空间布局
 写留言

精选留言(33)

  • Milittle
    2019-05-10
    刘老师,您好,您可以把文档中给出的代码文件定位给出来么,一般在对应看源码的时候,很难定位到老师给的代码点的对应源码文件,谢谢老师~老师讲的真的让我把多年的零散知识可以连贯起来,然后理解的更加透彻,但是也会有不太理解的地方,再次感谢。这个课很值得~。 总结以下进程和线程的异同点: 1. 进程有独立的内存空间,比如代码段,数据段。线程则是共享进程的内存空间。 2. 在创建新进程的时候,会将父进程的所有五大数据结构复制新的,形成自己新的内存空间数据,而在创建新线程的时候,则是引用进程的五大数据结构数据,但是线程会有自己的私有(局部)数据,执行栈空间。 3. 进程和线程其实在cpu看来都是task_struct结构的一个封装,执行不同task即可,而且在cpu看来就是在执行这些task时候遵循对应的调度策略以及上下文资源切换定义,包括寄存器地址切换,内核栈切换,指令指针寄存器的地址切换。所以对于cpu而言,进程和线程是没有区别的。 4. 进程创建的时候直接使用系统调用fork,进行系统调用的链路走,从而进入到_do_fork去创建task,而线程创建在调用_do_fork之前,还需要维护pthread这个数据结构的信息,初始化用户态栈信息。 自己就能意识到这几点,如果有理解不到位,或者不全面的地方,还请老师给予指点,谢谢老师。
    展开
    共 7 条评论
    44
  • jacy
    2019-08-01
    pstree -apl pid看进程树 pstack pid 看栈

    作者回复: 赞

    27
  • why
    2019-05-14
    - 线程的创建 - 线程是由内核态和用户态合作完成的, pthread_create 是 Glibc 库的一个函数 - pthread_create 中 1. 设置线程属性参数, 如线程栈大小 2. 创建用户态维护线程的结构, pthread 3. 创建线程栈 allocate_stack - 取栈的大小, 在栈末尾加 guardsize - 在进程堆中创建线程栈(先尝试调用 get_cached_stack 从缓存回收的线程栈中取用) - 若无缓存线程栈, 调用 `__mmap` 创建 - 将 pthread 指向栈空间中 - 计算 guard 内存位置, 并设置保护 - 填充 pthread 内容, 其中 specific 存放属于线程的全局变量 - 线程栈放入 stack_used 链表中(另外 stack_cache 链表记录回收缓存的线程栈) 4. 设置运行函数, 参数到 pthread 中 5. 调用 create_thread 创建线程 - 设置 clone_flags 标志位, 调用 `__clone` - clone 系统调用返回时, 应该要返回到新线程上下文中, 因此 `__clone` 将参数和指令位置压入栈中, 返回时从该函数开始执行 6. 内核调用 `__do_fork` - 在 copy_process 复制 task_struct 过程中, 五大数据结构不复制, 直接引用进程的 - 亲缘关系设置: group_leader 和 tgid 是当前进程; real_parent 与当前进程一样 - 信号处理: 数据结构共享, 处理一样 7. 返回用户态, 先运行 start_thread 同样函数 - 在 start_thread 中调用用户的函数, 运行完释放相关数据 - 如果是最后一个线程直接退出 - 或调用 `__free_tcb` 释放 pthread 以及线程栈, 从 stack_used 移到 stack_cache 中
    展开
    共 3 条评论
    27
  • why
    2019-05-14
    老师, 多线程的内核栈是共享的吗, 会不会出现问题?

    作者回复: 不共享,进了内核都是单独的任务了

    共 2 条评论
    15
  • humor
    2019-05-30
    老师之前说过进程默认会有一个主线程,意思是在创建进程的时候也会同时创建一个线程吗?

    作者回复: 不会,这个进程的task_struct就代表这个线程

    共 2 条评论
    10
  • 徐凯
    2019-05-10
    "将这个线程栈放到 stack_used 链表中,其实管理线程栈总共有两个链表,一个是 stack_used,也就是这个栈正被使用;另一个是 stack_cache,就是上面说的,一旦线程结束,先缓存起来,不释放,等有其他的线程创建的时候,给其他的线程用。" 这一段是线程池的意思么 如果是的话 既然内部已经有这个设计 我们有时候还要在程序中自己去设计一个呢?

    作者回复: 内核没有线程池的概念,把线程弄一个池子,是业务层做的。这里只是内核栈的复用。

    6
  • 蹦哒
    2020-06-14
    老师、同学们,不知道如下认识是否正确呢: 1.原来线程存在的价值是复用进程的部分内存(引用五大结构),又是一个享元模式(Flyweight Design Pattern)的体现 2.线程函数局部变量在用户态的线程栈中(是在进程的堆里面创建的),独立的内存块,所以多线程之间无需考虑共享数据问题;而进程的全局变量,由于多线程是共享了进程数据,再加上各个线程在内核中是独立的task被调度系统调度,随时会被抢占并且访问同一个全局变量,所以多线程之间需要做共享数据保护
    展开

    作者回复: 是的

    6
  • neohope
    2019-12-10
    关于clone_flags标志位的含义,可以参考一下这里http://man7.org/linux/man-pages/man2/clone.2.html If CLONE_THREAD is set, the child is placed in the same thread group as the calling process. When a clone call is made without specifying CLONE_THREAD, then the resulting thread is placed in a new thread group whose TGID is the same as the thread's TID. This thread is the leader of the new thread group. If CLONE_PARENT is set, then the parent of the new child (as returned by getppid(2)) will be the same as that of the calling process. If CLONE_PARENT is not set, then (as with fork(2)) the child's parent is the calling process.
    展开
    4
  • Geek_b8928e
    2020-03-22
    创建进程的话,调用的系统调用是 fork,在 copy_process 函数里面,会将五大结构 files_struct、fs_struct、sighand_struct、signal_struct、mm_struct 都复制一遍,从此父进程和子进程各用各的数据结构。而创建线程的话,调用的是系统调用 clone,在 copy_process 函数里面, 五大结构仅仅是引用计数加一,也即线程共享进程的数据结构。
    3
  • garlic
    2019-10-12
    进程线程查看命令:ps,top,pidstat,pstree 函数栈查看打印命令: pstack jstack (java) gdb (C/C++/go) kill -SIGQUIT [pid] (go) 相关API: C: glibc backtrace Boost stacktrace libunwind Java: getStackTrace; go: panic debug.PrintStack pprof.Lookup("goroutine").WriteTo runtime.Stack python traceback objects StackSummary Objects 笔记链接 https://garlicspace.com/?p=1678&preview=true
    展开
    3
  • 兔嘟嘟
    2021-08-19
    老师您好,我想请问一下可以这样理解主线程和进程的关系吗: 主线程和进程是绑定的,在内核体现为一个task,在用户态则是进程的运行主线,所以其实没有独立的主线程的概念。 主线程的线程栈就是进程栈,主线程没有pthread,所有的属性都是进程的属性。 因为主线程其实不是独立的,所以主线程结束时,也就是进程结束时。
    2
  • 相逢是缘
    2020-08-15
    1、pthread_create 不是一个系统调用,是 Glibc 库的一个函数 2、在内核里一样,每一个进程或者线程都有一个 task_struct 结构,线程在用户态也有一个用于维护线程的结构,就是这个 pthread 结构 3、创建线程栈 ----用户态int err = ALLOCATE_STACK (iattr, &pd); ---程属性里面设置过栈的大小,需要你把设置的值拿出来 ---为了防止栈的访问越界,在栈的末尾会有一块空间 guardsize ---其实线程栈是在进程的堆里面创建的get_cached_stack ---如果缓存里面没有,就需要调用 __mmap 创建一块新的 ---线程栈也是自顶向下生长的,还记得每个线程要有一个 pthread 结构,这个结构也是放在栈的空间里面的。在栈底的位置,其实是地址最高位。 ---计算出 guard 内存的位置,调用 setup_stack_prot 设置这块内存的是受保护的 ---开始填充 pthread 这个结构里面的成员变量 stackblock、stackblock_size、guardsize、specific。 ---将这个线程栈放到 stack_used 链表中,使用完之后放到stack_cache中 ---其实有了用户态的栈,接着需要解决的就是用户态的程序从哪里开始运行的问题 pd->start_routine = start_routine; pd->arg = arg; pd->schedpolicy = self->schedpolicy; pd->schedparam = self->schedparam; /* Pass the descriptor to the caller. */ *newthread = (pthread_t) pd; atomic_increment (&__nptl_nthreads); retval = create_thread (pd, iattr, &stopped_start, STACK_VARIABLES_ARGS, &thread_ran); start_routine 就是咱们给线程的函数,start_routine,start_routine 的参数 arg,以及调度策略都要赋值给 pthread ----内核态 --系统调用__clone 将线程要执行的函数的参数和指令的位置都压到栈里面,当从内核返回,从栈里弹出来的时候,就从这个函数开始 --在 copy_process 复制 task_struct 过程中, files、fs、sighand、mm、五大数据结构不复制, 直接引用进程的 --亲缘关系:新进程group_leader就是自己,tgid就是他的pid,real_parent 是当前的进程。新线程group_leader是当前进程的,tgid是当前进程的tgid,real_parent 是当前集成的real_parent; --信号处理:共享信号 4、用户态执行线程 --所有的线程统一的入口start_thread --用户的函数执行完毕之后,会释放这个线程相关的数据 a、线程数目也减一,如果这是最后一个线程了,就直接退出进程 b、_free_tcb 用于释放 pthread,__free_tcb 会调用 __deallocate_stack 来释放整个线程栈,放到缓存的线程栈列表 stack_cache 中;
    展开
    2
  • kdb_reboot
    2019-09-28
    三刷: 感觉应该讲清楚这一点,"角色划分" 内核态是用来管理的,用户态是提供给用户用的 这才有了为什么要两个模式来回切换, 以及,真正调度,是内核态来做的,而用户态执行是用户态自己做,这才有了单独的线程栈 内存模型也很重要 另外想请教个问题:从上下文来理解, 所以说,主线程的栈是用整个用户空间的栈?子线程的栈在进程的堆里面?
    展开
    2
  • lfn
    2019-05-10
    所以,线程局部变量其实是存储在每个线程自己的用户栈里咯?

    作者回复: 是的

    2
  • Geek_4c6cb8
    2022-02-23
    老师您好,同一个进程的不同线程,内核栈都是相同的吗
    共 1 条评论
    1
  • 空格
    2019-09-18
    不知道我的理解对不对?线程fork后内核态会创建task_struct,之后还是会尝试wakeup_preempt_entity,然后之后接受调度,调度成功后才会在用户态执行start_thread方法?不知道我这个理解对不对?
    1
  • nora
    2019-05-18
    之前总是认为线程和进程都占用了内核的taskstruct 认为实际上线程进程没啥区别,这篇文章真是醍醐灌顶啊,谢谢老师。

    作者回复: 赞,加油

    1
  • 浅陌
    2022-06-30
    请问这个线程是内核态还是用户态的呀,用户态线程与内核态线程多对多是怎么实现的呀
  • 小鳄鱼
    2022-06-17
    老师,可否讲一下线程模型?还有LWP是如何支持用户线程
  • Samaritan.
    2022-05-02
    老师每一讲的图真的是太用心了,以前有很多模糊的地方,看着图非常容易理解