一步一步学linux操作系统: 16 线程是如何创建的以及与进程创建的区别

进程和线程

无论是进程还是线程,在内核里面都是任务
线程不是一个完全由内核实现的机制,它是由内核态和用户态合作完成的。pthread_create 不是一个系统调用,是 Glibc 库的一个函数

Glibc 里的pthread_create

pthread_create 函数
\glibc-2.29\nptl\pthread_create.c


int
__pthread_create_2_1 (pthread_t *newthread, const pthread_attr_t *attr,
		      void *(*start_routine) (void *), void *arg)
{
	STACK_VARIABLES;

	const struct pthread_attr *iattr = (struct pthread_attr *) attr;
	struct pthread_attr default_attr;
	bool free_cpuset = false;
	bool c11 = (attr == ATTR_C11_THREAD);
	if (iattr == NULL || c11)
    {
      lll_lock (__default_pthread_attr_lock, LLL_PRIVATE);	// 线程的属性参数
      default_attr = __default_pthread_attr;
      size_t cpusetsize = default_attr.cpusetsize;
      if (cpusetsize > 0)
		{
		......
		  iattr = &default_attr;
	}

	struct pthread *pd = NULL;	// 维护线程的结构
	int err = ALLOCATE_STACK (iattr, &pd);	// 创建线程栈
	int retval = 0;
	......
	// 用户态的程序从哪里开始运行
	/* Store the address of the start routine and the parameter.  Since
	we do not start the function directly the stillborn thread will
	get the information from its thread descriptor.  */
	pd->start_routine = start_routine;	// 给线程的函数
	pd->arg = arg;	// start_routine 的参数
	pd->c11 = c11;
	......
	/* Copy the parent's scheduling parameters.  The flags will say what
	is valid and what is not.  */
	pd->schedpolicy = self->schedpolicy;	// 调度策略
	pd->schedparam = self->schedparam;
	......

	/* One more thread.  We cannot have the thread do this itself, since it
	might exist but not have been scheduled yet by the time we've returned
	and need to check the value to behave correctly.  We must do it before
	creating the thread, in case it does get scheduled first and then
	might mistakenly think it was the only thread.  In the failure case,
	we momentarily store a false value; this doesn't matter because there
	is no kosher thing a signal handler interrupting us right here can do
	that cares whether the thread count is correct.  */
	atomic_increment (&__nptl_nthreads);	// __nptl_nthreads 加一,说明又多了一个线程

	/* Our local value of stopped_start and thread_ran can be accessed at
	any time. The PD->stopped_start may only be accessed if we have
	ownership of PD (see CONCURRENCY NOTES above).  */
	bool stopped_start = false; bool thread_ran = false;

	/* Start the thread.  */
	if (__glibc_unlikely (report_thread_creation (pd)))
    {
      stopped_start = true;

      /* We always create the thread stopped at startup so we can
	 notify the debugger.  */
      retval = create_thread (pd, iattr, &stopped_start,
			      STACK_VARIABLES_ARGS, &thread_ran);
      if (retval == 0)
		{
		  /* We retain ownership of PD until (a) (see CONCURRENCY NOTES
			 above).  */

		  /* Assert stopped_start is true in both our local copy and the
			 PD copy.  */
		  assert (stopped_start);
		  assert (pd->stopped_start);

		  /* Now fill in the information about the new thread in
			 the newly created thread's data structure.  We cannot let
			 the new thread do this since we don't know whether it was
			 already scheduled when we send the event.  */
		  pd->eventbuf.eventnum = TD_CREATE;
		  pd->eventbuf.eventdata = pd;

		  /* Enqueue the descriptor.  */
		  do
			pd->nextevent = __nptl_last_event;
		  while (atomic_compare_and_exchange_bool_acq (&__nptl_last_event,
								   pd, pd->nextevent)
			 != 0);

		  /* Now call the function which signals the event.  See
			 CONCURRENCY NOTES for the nptl_db interface comments.  */
		  __nptl_create_event ();
		}
    }
	else
	retval = create_thread (pd, iattr, &stopped_start,
				STACK_VARIABLES_ARGS, &thread_ran);

	......
}
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;
}

创建维护线程的结构


struct pthread *pd = NULL;

创建线程栈

int err = ALLOCATE_STACK (iattr, &pd);

ALLOCATE_STACK 宏
\glibc-2.29\nptl\allocatestack.c

/* This is how the function is called.  We do it this way to allow
   other variants of the function to have more parameters.  */
# define ALLOCATE_STACK(attr, pd) allocate_stack (attr, pd, &stackaddr)

在这里插入图片描述

allocate_stack 函数
\glibc-2.29\nptl\allocatestack.c


# 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;
...... 
}
	

在这里插入图片描述

  • 取栈的大小, 在栈末尾加 guardsize防止栈的访问越界
  • 在进程堆中创建线程栈(先尝试调用 get_cached_stack 从缓存回收的线程栈中取用)
  • 若无缓存线程栈, 调用 __mmap 创建
  • 线程栈也是自顶向下生长的
  • 计算 guard 内存位置, 并设置保护
  • 填充 pthread 内容, 其中 specific 存放属于线程的全局变量
  • 线程栈放入 stack_used 链表中(另外 stack_cache 链表记录回收缓存的线程栈)

create_thread 真正创建线程

nptl\pthread_create.c

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 的参数 arg,以及调度策略都赋值给 pthread。
__nptl_nthreads 加一,说明又多了一个线程

create_thread 函数

\glibc-2.29\sysdeps\unix\sysv\linux\createthread.c


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);

  TLS_DEFINE_INIT_TP (tp, pd);

  if (__glibc_unlikely (ARCH_CLONE (&start_thread, STACK_VARIABLES_ARGS,
				    clone_flags, pd, &pd->tid, tp, &pd->tid)
			== -1))
    return errno;

  /* It's started now, so if we fail below, we'll have to cancel it
     and let it clean itself up.  */
  *thread_ran = true;
......
  return 0;
}
	

在这里插入图片描述
ARCH_CLONE,其实调用的是 __clone

\glibc-2.29\sysdeps\unix\sysv\linux\x86_64\clone.S


# 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)
	

在这里插入图片描述
对于线程来说
除了内核里面有这个线程对应的 task_struct
系统调用返回到用户态的时候,用户态的栈应该是线程的栈,栈顶指针应该指向线程的栈,指令指针应该指向线程将要执行的那个函数
所以将线程要执行的函数的参数和指令的位置都压到栈里面,当从内核返回,从栈里弹出来的时候,就从这个函数开始,带着这些参数执行下去

接下来就要进入内核了

内核态创建任务

clone 系统调用的定义

kernel\fork.c

SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
		 int __user *, parent_tidptr,
		 int __user *, child_tidptr,
		 unsigned long, tls)
#endif
{
	return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls);
}
#endif

在这里插入图片描述

_do_fork 函数可参见 15 进程是如何创建的_fork都做了些什么

创建线程 调用 _do_fork 几个区别

第一个是上面复杂的标志位设定

在这里插入图片描述
glibc 源码中\sysdeps\unix\sysv\linux\createthread.c

copy_files 函数
在这里插入图片描述
原来是调用 dup_fd 复制一个 files_struct 的,现在因为 CLONE_FILES 标识位变成将原来的 files_struct 引用计数加一。
copy_fs 函数
在这里插入图片描述
原来是调用 copy_fs_struct 复制一个 fs_struct,现在因为 CLONE_FS 标识位变成将原来的 fs_struct 的用户数加一
copy_sighand 函数
在这里插入图片描述
原来是创建一个新的 sighand_struct,现在因为 CLONE_SIGHAND 标识位变成将原来的 sighand_struct 引用计数加一。
copy_signal 函数
在这里插入图片描述
原来是创建一个新的 signal_struct,现在因为 CLONE_THREAD 直接返回了。
copy_mm 函数
在这里插入图片描述
原来是调用 dup_mm 复制一个 mm_struct,现在因为 CLONE_VM 标识位而直接指向了原来的 mm_struct

第二个就是对于亲缘关系的影响

要识别多个线程是不是属于一个进程

在这里插入图片描述

  • 如果是新进程,进程的 group_leader 就是它自己,tgid 是它自己的 pid。如果是新线程,group_leader 是当前进程的group_leader,tgid 是当前进程的 tgid,也就是当前进程的 pid
  • 如果是新进程,新进程的 real_parent 是当前的进程;如果是新线程,线程的 real_parent 是当前的进程的 real_parent,是平辈的
第三,对于信号的处理

如何保证发给进程的信号虽然可以被一个线程处理,但是影响范围应该是整个进程的。signal_struct 共享,整个进程里的所有线程共享一个 shared_pending,这也是一个信号列表

用户态执行线程

clone 在内核的调用完毕,要返回系统调用,回到用户态

在glibc 调用__clone时

__clone 的第一个参数,是一个通用的 start_thread,这是所有线程在用户态的统一入口。

glibc 源码 \glibc-2.29\sysdeps\unix\sysv\linux\createthread.c
在这里插入图片描述
\glibc-2.29\nptl\pthread_create.c


#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 入口函数中,才真正的调用用户提供的函数,在用户的函数执行完毕之后,会释放这个线程相关的数据。

__free_tcb 会调用 __deallocate_stack 来释放整个线程栈,这个线程栈要从当前使用线程栈的列表 stack_used 中拿下来,放到缓存的线程栈列表 stack_cache 中。

线程与进程创建对比图

创建进程的话,调用的系统调用是 fork,在 copy_process 函数里面,会将五大结构 files_struct、fs_struct、sighand_struct、signal_struct、mm_struct 都复制一遍,从此父进程和子进程各用各的数据结构。
创建线程的话,调用的是系统调用 clone,在 copy_process 函数里面, 五大结构仅仅是引用计数加一,也即线程共享进程的数据结构

图片来自极客时间趣谈linux操作系统
图片来自极客时间趣谈linux操作系统

参考资料:

趣谈Linux操作系统(极客时间)链接:
http://gk.link/a/10iXZ
欢迎大家来一起交流学习

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

墨1024

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值