进程.线程,内核线程创建的区别



首先,他们在kernel里都会调到do_fork(),do_fork的定义如下:

long do_fork(unsigned long clone_flags,
	      unsigned long stack_start,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr)

如果是进程的话,这里stack_start等于0,表示子进程和父进程使用同一个栈地址,在copy on write时,再复制。

如果是线程的话,这里的stack_start是pthread_create 通过mmap得到的地址,这个地址作为线程的栈,也就是从父进程的地址空间中分配一段作为子线程的栈

区别是对于fork,创建了的进程和父进程有两份页表,页表指向的是不同的物理地址(一开始页表指向的是相同的物理地址,COW时,再指向不同的)
对于pthread_create,创建的线程和父进程共享同一份页表。所以这个线程可以父进程的所有地址空间,包括堆,栈。

如果是内核线程的话,用的是kernel_thread
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
	return do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
		(unsigned long)arg, NULL, NULL);
}
这晨stack_size表示的是内核线程创建好后执行的函数。do_fork会调用copy_mm来处理地址空间

static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
	struct mm_struct *mm, *oldmm;
	int retval;

	tsk->min_flt = tsk->maj_flt = 0;
	tsk->nvcsw = tsk->nivcsw = 0;
#ifdef CONFIG_DETECT_HUNG_TASK
	tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw;
#endif

	tsk->mm = NULL;
	tsk->active_mm = NULL;

	/*
	 * Are we cloning a kernel thread?
	 *
	 * We need to steal a active VM for that..
	 */
	oldmm = current->mm;
	if (!oldmm)
		return 0;

	if (clone_flags & CLONE_VM) {
		atomic_inc(&oldmm->mm_users);
		mm = oldmm;
		goto good_mm;
	}

	retval = -ENOMEM;
	mm = dup_mm(tsk);
	if (!mm)
		goto fail_nomem;

good_mm:
	tsk->mm = mm;
	tsk->active_mm = mm;
	return 0;

fail_nomem:
	return retval;
}
 
对于 oldmm为NULL,表示这是由内核线程(或者叫进程,linux里线程就是进程)创建内核线程,内核进程不需要自己的地址空间,因为它只访问内核地址空间(3G以上的地址空间),这部分地址对于所有的进程都一样的(包括所有的进程和内核线程和init进程)。所以内核线程没有自己的页表,当调度到内核线程运行时,它直接使用被调度出进程页表,这样,还不用刷新TLB

lone_flags & CLONE_VM不为0时,表示共享地址空间,是在创建线程,这是只是增加父进程的地址空间的计数然后把它赋给子线程。
lone_flags & CLONE_VM为0时,表示不共享地址空间,是在创建进程,这时,会通过dum_mm复制父进程的地址空间,包括页表。

另外,对于内核线程的用户进程,它们的返回机制是不一样,用户进程要返回用户态执行,内核线程执行do_fork时指定的函数。这是在copy_thread时进行区的,copy_thread是用来处理进程的context
int
copy_thread(unsigned long clone_flags, unsigned long stack_start,
	    unsigned long stk_sz, struct task_struct *p)
{
	struct thread_info *thread = task_thread_info(p);
	struct pt_regs *childregs = task_pt_regs(p);

	memset(&thread->cpu_context, 0, sizeof(struct cpu_context_save));

	if (likely(!(p->flags & PF_KTHREAD))) {
		*childregs = *current_pt_regs();
		childregs->ARM_r0 = 0;
		if (stack_start)
			childregs->ARM_sp = stack_start;
	} else {
		memset(childregs, 0, sizeof(struct pt_regs));
		thread->cpu_context.r4 = stk_sz;
		thread->cpu_context.r5 = stack_start;
		childregs->ARM_cpsr = SVC_MODE;
	}
	thread->cpu_context.pc = (unsigned long)ret_from_fork;
	thread->cpu_context.sp = (unsigned long)childregs;

	clear_ptrace_hw_breakpoint(p);

	if (clone_flags & CLONE_SETTLS)
		thread->tp_value[0] = childregs->ARM_r3;
	thread->tp_value[1] = get_tpuser();

	thread_notify(THREAD_NOTIFY_COPY, thread);

	return 0;
}


likely(!(p->flags & PF_KTHREAD)表示是用户进程,*childregs= *current_pt_regs();这里的regs保存的是用户态的上下文,对于arm,就是arm的所有寄存器,包括pc,sp等,因此,子进程和父进程返执行同样的代码,使用同样的栈,如果指定了stack_start,就把它赋给sp寄存器,这对应的线程的栈。

否则,进入else,表示是内核线程,内核线程的没有用户态的东西,所以及通过memset全部置为0,cpu_context表示内核态的上下文,在这里把要执行函数的地址保存在r5寄存器中,参数保存在r4寄存器中。

对于所有的情况,都把cpu_context中的pc指定为ret_from_fork,这就是在伪造上下文了,因为新创建的进程,从来没有执行过,哪来的context,所以都指定为ret_from_fork
。对于子进程或者子线程,创建好后只是在调度队列中插入了一个task_struct而已,等待着被调度,一旦被调度,就会恢复它们的context,也就是把当前svc模式下的寄存
器的值赋值为cpu_context,这样pc被赋为ret_from_fork,于是跳转到这里:

ENTRY(ret_from_fork)
	bl	schedule_tail
	cmp	r5, #0
	movne	r0, r4
	adrne	lr, BSYM(1f)
	movne	pc, r5
1:	get_thread_info tsk
	b	ret_slow_syscall
ENDPROC(ret_from_fork)

在这里,对于内核线程,r5表示要执行函数的地址,r4为参数,因此cmp r5,#0肯定不等于0,于是把参数传给r0,然后通过movne pc, r5去执行指定的函数。对于用户进程呢,就跳到ret_slow_syscall,在这里最终恢复用户态的context,也就是前面的childregs, 注意,这里childregs->ARM_r0 = 0;这表示对于子进程,它返回到用户态时,返回值为0。
pid = fork();
if(pid ==0)
{
   子进程
}
else{

   父进程
}
 
 

这段代码体现的就是这个意思。


最后,现来看一看fork和pthread_create的实现

首先是fork,位于/bionic/libc/bionic/fork.c

int  fork(void)
{
    int  ret;

    /* Posix mandates that the timers of a fork child process be
     * disarmed, but not destroyed. To avoid a race condition, we're
     * going to stop all timers now, and only re-start them in case
     * of error, or in the parent process
     */
    __timer_table_start_stop(1);
    __bionic_atfork_run_prepare();

    ret = __fork();
    if (ret != 0) {  /* not a child process */
        __timer_table_start_stop(0);
        __bionic_atfork_run_parent();
    } else {
        /* Adjusting the kernel id after a fork */
        (void)__pthread_settid(pthread_self(), gettid());

        /*
         * Newly created process must update cpu accounting.
         * Call cpuacct_add passing in our uid, which will take
         * the current task id and add it to the uid group passed
         * as a parameter.
         */
        cpuacct_add(getuid());
        __bionic_atfork_run_child();
    }
    return ret;
}

直接调_fork(), _fork是个汇编,位于/bionic/libc/arch-arm/syscalls/__fork.S

ENTRY(__fork)
    .save   {r4, r7}
    stmfd   sp!, {r4, r7}
    ldr     r7, =__NR_fork
    swi     #0
    ldmfd   sp!, {r4, r7}
    movs    r0, r0
    bxpl    lr
    b       __set_syscall_errno
END(__fork)

可以看到,swi是系统调用的指令, 系统调用号是_NR_fork,这样就到kernel中去了,注意,swi之后的代码对于子进程和父进程都是一样的,不同的是,对于父进程,r0中是子进程的pid,对于子进程,r0等于0

跟进kernel:

SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
	return do_fork(SIGCHLD, 0, 0, NULL, NULL);
#else
	/* can not support in nommu mode */
	return(-EINVAL);
#endif
}
#endif
SYSCALL_DEFINE0就是定义系统调用的,可以看到fork直接调用do_fork,要注意的是这里的参数,flags只定义了SIGCHLD,其余的都是0,这表示这个进程死后要给父进程发送死亡通知。


pthread_create位于/bionic/libc/bionic/pthread.c

int pthread_create(pthread_t *thread_out, pthread_attr_t const * attr,
                   void *(*start_routine)(void *), void * arg)
{
    int old_errno = errno;

    /* this will inform the rest of the C library that at least one thread
     * was created. this will enforce certain functions to acquire/release
     * locks (e.g. atexit()) to protect shared global structures.
     *
     * this works because pthread_create() is not called by the C library
     * initialization routine that sets up the main thread's data structures.
     */
    __isthreaded = 1;

    pthread_internal_t* thread = calloc(sizeof(*thread), 1);
    if (thread == NULL) {
        return ENOMEM;
    }

    if (attr == NULL) {
        attr = &gDefaultPthreadAttr;
    }

    // make sure the stack is PAGE_SIZE aligned
    size_t stack_size = (attr->stack_size + (PAGE_SIZE-1)) & ~(PAGE_SIZE-1);
    uint8_t* stack = attr->stack_base;
    if (stack == NULL) {
        stack = mkstack(stack_size, attr->guard_size);
        if (stack == NULL) {
            _pthread_internal_free(thread);
            return ENOMEM;
        }
    }

    // Make room for TLS
    void** tls = (void**)(stack + stack_size - BIONIC_TLS_SLOTS*sizeof(void*));

     ...
    tls[TLS_SLOT_THREAD_ID] = thread;

    int flags = CLONE_FILES | CLONE_FS | CLONE_VM | CLONE_SIGHAND |
                CLONE_THREAD | CLONE_SYSVSEM | CLONE_DETACHED;
    int tid = __pthread_clone((int(*)(void*))start_routine, tls, flags, arg);
   
    ...

    *thread_out = (pthread_t) thread;
    pthread_mutex_unlock(start_mutex);

    return 0;
}

其中mkstack就是用来给线程分配栈的

static void *mkstack(size_t size, size_t guard_size)
{
    pthread_mutex_lock(&mmap_lock);

    int prot = PROMAP_PRIVATET_READ | PROT_WRITE;
    int flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE;
    void* stack = mmap(NULL, size, prot, flags, -1, 0);
    if (stack == MAP_FAILED) {
        stack = NULL;
        goto done;
    }

    if (mprotect(stack, guard_size, PROT_NONE) == -1) {
        munmap(stack, size);
        stack = NULL;
        goto done;
    }

done:
    pthread_mutex_unlock(&mmap_lock);
    return stack;
}

注意,这里mmap实现指了MAP_PRIVATE,但是这并不代表这块区域是线程私有的。如果同一个进程创建的其他线程需要,它可以读写这个线程的栈,也就是局部变量。MAP_PRIVATE主要用于对文件做私有映射。

TLS是线程的私有变量区域,可以看到bionic的实现就是把它们放在栈的最高处。

可以看到调_pthread_clone的flag有很多CLONE_XXX,这就是表示共享的东西,这里没有SIGCHLD,这是因为线程死会,除非它是线程组的最后一个线程,否则不用发送死亡消息给父进程。到这里就要纠正前面的一个错误了,就是相关进程间的父子关系。

假设A进程fork了B进程,则A是B的父进程,假设B进程再pthread_create了C,和D线程,C和D的父进程都是B,C,D属于一个线程组。

接下来 __pthread_clone是个汇编,在bionic/libc/arch0arm/bionic/clone.S里

ENTRY(__pthread_clone)
    @ insert the args onto the new stack
    stmdb r1!, {r0, r3}

    @ do the system call
    @ get flags

    mov     r0, r2

    @ new sp is already in r1

#if __ARM_EABI__
    stmfd   sp!, {r4, r7}
    ldr     r7, =__NR_clone
    swi     #0
#else
    swi     #__NR_clone
#endif

    movs    r0, r0
#if __ARM_EABI__
    ldmnefd sp!, {r4, r7}
#endif
    blt     __error
    bxne    lr


    @ pick the function arg and call address off the stack and jump
    @ to the C __thread_entry function which does some setup and then
    @ calls the thread's start function

    pop     {r0, r1}
    mov     r2, sp			@ __thread_entry needs the TLS pointer
    b       __thread_entry

__error:
    mov     r0, #-1
    bx      lr
END(__pthread_clone)

这里r0保存的是start_routine,  r1对于tls, r2对应flags, r3对应 arg,注意,这里tls就是栈的开始位置了,因为栈地址向下递减,把r0和r3保存到栈中然后调用clone系统调用,跟进kernel

SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
		 int __user *, parent_tidptr,
		 int __user *, child_tidptr,
		 int, tls_val)

{
	return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
} 
这里SYSCALL_DEFINE5表示系统调用有5个参数,但是前面只设置了两个参数,r0中是flags,r1中是sp。怎么办呢,会不会出问题啊?不会的,这些参数因为flags中没有置相应的bit,do_fork不会去读,也不会写。没有问题。

swi之后的代码对于父进程和子线程是一样的。不同的是对于父进程,r0不为0,通过bxne lr返回到pthread_create中通过_pthread_clone的下一行开始执行。对于子线程,从栈上把start_routine和argpop到r0,r1,然后调用__thread_entry

void __thread_entry(int (*func)(void*), void *arg, void **tls)
{
    // Wait for our creating thread to release us. This lets it have time to
    // notify gdb about this thread before we start doing anything.
    //
    // This also provides the memory barrier needed to ensure that all memory
    // accesses previously made by the creating thread are visible to us.
    pthread_mutex_t* start_mutex = (pthread_mutex_t*) &tls[TLS_SLOT_SELF];
    pthread_mutex_lock(start_mutex);
    pthread_mutex_destroy(start_mutex);

    pthread_internal_t* thread = (pthread_internal_t*) tls[TLS_SLOT_THREAD_ID];
    __init_tls(tls, thread);

    if ((thread->internal_flags & kPthreadInitFailed) != 0) {
        pthread_exit(NULL);
    }

    int result = func(arg);
    pthread_exit((void*) result);
}
到这里终于执行到线程的函数中去了


这里android bionic对应的是android4.2的代码,kernle对应的linux3.11.1 这两者的代码不太匹配,不过不影响分析。




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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值