内核态到用户态切换分析(一)

引言:本文主要分析从内核态到用户态的切换,同时理清内核线程、用户空间进行之间的关系。内核进行一系统初始化后,会进入到rest_init,首先会产生一个kernel_init的内核线程,最终切换到用户空间的init进程,从而开始了用户空间初始化流程。

先看下rest_init函数,

static noinline void __init_refok rest_init(void)
    __releases(kernel_lock)
{
    int pid;

    kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
    numa_default_policy();
    pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
    kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
    unlock_kernel();

    /*
     * The boot idle thread must execute schedule()
     * at least once to get things moving:
     */
    init_idle_bootup_task(current);
    rcu_scheduler_starting();
    preempt_enable_no_resched();
    schedule();
    preempt_disable();

    /* Call into cpu_idle with preempt disabled */
    cpu_idle();
}

关键代码:kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
这里先提出几个问题:
1)kernel_init是第几个进程,进程号如何确定?
2)kernel_thread内核线程是什么意思,与用户空间的进程关系如何?
3)如何从内核空间切换到用户空间去的?

要解决以上问题,必须弄清几个概念:
程序:指一组指令的集合
进程:执行中的程序实例。包含两方面的意思,一是拥有大量系统资源(如内在地址空间、文件句柄、堆栈等);二是可以被调度、执行。
任务:泛指软件完成的一项活动。

在Linux内核中,进程通常被称为任务,而运行在用户空间中的程序,称为进程。后边分析以此为准。

针对问题1,先要弄清楚什么时候开始有进程的概,进程号如何分配的?
一般情况下,在Linux里进程都是由父进程通过调用fork()函数产生。那么在内核初始化时,第一个进程如何产生的?进程肯定还是调用fork()一类的函数产生的,但第一个进程的进程控制块(PCB)如何生成的?
很容易想到是人为初始化的,证据当然在代码里。
kernel/init_task.c

/*
 * Initial task structure.
 *
 * All other task structs will be allocated on slabs in fork.c
 */
struct task_struct init_task = INIT_TASK(init_task);

从注释来看,除了init_task这个pcb之外,其它的pcb会自动分配。
继续看INIT_TASK

#define INIT_TASK(tsk)  \
{                                   \
    .flags      = PF_KTHREAD,                   \   
    .pids = {                           \
        [PIDTYPE_PID]  = INIT_PID_LINK(PIDTYPE_PID),        \
        [PIDTYPE_PGID] = INIT_PID_LINK(PIDTYPE_PGID),       \
        [PIDTYPE_SID]  = INIT_PID_LINK(PIDTYPE_SID),        \
}
#define PF_KTHREAD  0x00200000  /* I am a kernel thread */

重点观注几个变量,其中PF_KTHREAD比较明显,代表一个内核线程。其次是pid,看到赋初值情况:

#define INIT_PID_LINK(type)                     \
{                               \
    .node = {                       \
        .next = NULL,                   \
        .pprev = &init_struct_pid.tasks[type].first,    \
    },                          \
    .pid = &init_struct_pid,                \
}

struct pid init_struct_pid = INIT_STRUCT_PID;

#define INIT_STRUCT_PID {                       \
    .count      = ATOMIC_INIT(1),               \
    .rcu        = RCU_HEAD_INIT,                \
    .level      = 0,                        \
    .numbers    = { {                       \
        .nr     = 0,                    \
        .ns     = &init_pid_ns,             \
        .pid_chain  = { .next = NULL, .pprev = NULL },  \
    }, }                                \
}

OK,这里nr=0,即第一个手工pcb结构的pid为0。
继续追问:0号进程的pcb是什么时候初始化的?0号进程的堆栈什么时候初始化的?0号进程最后结局是什么?
这些问题有点烦人,不搞清楚又觉得对不起自己,好吧,由于0号进程的特殊性,必须要从start_kernel之前开始,因为start_kernel已经进入了0号线程范畴内了。根据内核启动流程,start_kernel之前有一小段汇编代码,以x86架构进行分析:
arch/x86/kernel/head_32.S

/*
 * Enable paging
 */
    movl $pa(swapper_pg_dir),%eax
    movl %eax,%cr3      /* set the page table pointer.. */
    movl %cr0,%eax
    orl  $X86_CR0_PG,%eax
    movl %eax,%cr0      /* ..and set paging (PG) bit */
    ljmp $__BOOT_CS,$1f    /* Clear prefetch and normalize %eip */
1:
    /* Set up the stack pointer */
    lss stack_start,%esp

汇编代码也得硬着头皮看下去,Enable paging即打开分页功能,之后内核堆栈初始化:stack_start

.data
ENTRY(stack_start)
    .long init_thread_union+THREAD_SIZE
    .long __BOOT_DS

全局搜索init_thread_union,

arch\x86\kernel\Init_task.c
/*
 * Initial thread structure.

We need to make sure that this is THREAD_SIZE aligned due to the way process stacks are handled. This is done by having a special "init_task" linker map entry..
 */
union thread_union init_thread_union
    __attribute__((__section__(".data.init_task"))) =
        { INIT_THREAD_INFO(init_task) };

include\linux\Sched.h
extern union thread_union init_thread_union;        
union thread_union {
    struct thread_info thread_info;
    unsigned long stack[THREAD_SIZE/sizeof(long)];
}
#define THREAD_SIZE     (PAGE_SIZE << THREAD_ORDER)

从搜出来的结果来看,结合注释init_thread_union是一个thread_union联合体变量,在初始化内核线程结构时,对init_thread_unionI赋值 NIT_THREAD_INFO(init_task)。并把数据放到.data.init_task段,这一动作由特殊的 “init_task” linker map entry完成,这个entry正是前面的stack_start。

#define INIT_THREAD_INFO(tsk)           \
{                       \
    .task       = &tsk,         \
    .exec_domain    = &default_exec_domain, \
    .flags      = 0,            \
    .cpu        = 0,            \
    .preempt_count  = 1,            \
    .addr_limit = KERNEL_DS,        \
    .restart_block = {          \
        .fn = do_no_restart_syscall,    \
    },                  \
}

可以看到INIT_THREAD_INFO宏有个关键的地方.addr_limit = KERNEL_DS,即内核线程的内存地址空间限制为KERNEL_DS。
这样struct task_struct init_task = INIT_TASK(init_task);的初始化的时机就清楚了,即在编译的时候作为一个数据全局数据被初始化的,并且把他们放在.data.init_task数据段中。同时还看到0号线程的thread_info大小unsigned long stack[THREAD_SIZE/sizeof(long)];
#define THREAD_SIZE 8192 /* 2 pages */
这个值一般为8K,两个page,在head_32.S中,将esp指针指向这个已经被初始化了的init_thread_union,并将其以上8k作为栈顶。
OK,前边的三上个追问只剩下一个,o号进程的结局如何?
在rest_init最后调用cpu_idle

/*
 * The idle thread. There's no useful work to be
 * done, so just try to conserve power and have a
 * low exit latency (ie sit in a loop waiting for
 * somebody to say that they'd like to reschedule)
 */
void cpu_idle(void) {
    schedule();

注释已经写得很清楚了,cpu_idle不会做什么有用工作,仅仅是再次调度其它进程,这就是0号进程的结局。
再提一点,在早其的调度初始化中,init_idle(current, smp_processor_id())代码把current=init_task加入到cpu运行队列里,当队列没有其它程序时,idle程序会被执行。


接着来看关于内核线程的相关问题,更具体一些 :
a)内核线程是什么?
b)内核线程如何产生?
c)与用户空间线程的关系如何?
对于a问,内核线程是一个轻量级的进程,拥有自己的pcb结构。
b问, 内核线程产生方式:kthread_create和kernel_thread,其区别为:引用cu博客http://blog.chinaunix.net/uid-24227137-id-3387635.html

(1)最重要的不同:kthread_create创建的内核线程有干净的上那上下文环境,适合于驱动模块或用户空间的程序创建内核线程使用,不会把某些内核信息暴露给用户程序
(2)二者创建的进程的父进程不同: kthread_create创建的进程的父进程被指定为kthreadd, 而kernel_thread创建的进程可以是init或其他内核线程。

对于c问,引用CSDN博客:
http://blog.csdn.net/liuqiyao_01/article/details/38975975

KST: 内核支持线程是在核心空间实现的;内核为每个线程在核心空间中设置了一个线程控制块,用来登记该线程的线程标识符、寄存器值、状态、优先级等信息;所有对线程的操作,如创建、撤消和切换等,都是通过系统功能调用由内核中的相应处理程序完成;设置了内核支持线程的系统,其调度是以线程为单位进行的。
优点:
在多处理器系统中,内核能够同时调度同一进程中多个线程并行执行到多个处理器中;如果进程中的一个线程被阻塞,内核可以调度同一个进程中的另一个线程;内核支持线程具有很小的数据结构和堆栈,线程的切换比较快,切换开销小;内核本身也可以使用多线程的方式来实现。
缺点:
即使CPU在同一个进程的多个线程之间切换,也需要陷入内核,因此其速度和效率不如用户级线程。

ULT: 用户级线程仅存在于用户空间中,与内核无关;就内核而言,它只是管理常规的进程—单线程进程,而感知不到用户级线程的存在;每个线程控制块都设置在用户空间中,所有对线程的操作也在用户空间中由线程库中的函数完成,无需内核的帮助;设置了用户级线程的系统,其调度仍是以进程为单位进行的。
优点:
线程的切换无需陷入内核,故切换开销小,速度非常快;线程库对用户线程的调度算法与OS的调度算法无关,因此,线程库可提供多种调度算法供应用程序选择使用;用户级线程的实现与操作系统平台无关。
缺点:
系统调用的阻塞问题:对应用程序来讲,一个线程的阻塞将导致整个进程中所有线程的阻塞;多线程应用无法享用多处理机系统中多个处理器带来的好处。

最后一个问题,3)如何从内核空间切换到用户空间去的?
留在后文详解。———— 206.05.18晚 于成都

你可能感兴趣的:(android基础,linux基础)