引言:本文主要分析从内核态到用户态的切换,同时理清内核线程、用户空间进行之间的关系。内核进行一系统初始化后,会进入到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晚 于成都