Linux系统的线程实现非常特别:他对线程和进程并不特别区分。对linux而言,线程只不过是一种特殊的进程罢了,后面我们会看到,他们都通过do_fork函数创建,只是传入的参数不一样而已。线程创建时,会共享内核资源。
在内核中,各个进程的task_struct存放在他们内核栈的尾端。这样做是为了让那些像x86那些寄存器较少的硬件体系结构只要通过栈指针就能计算出他的位置而避免额外的寄存器专门记录。寄存器较弱的体系结构不是引入thread_info结构的唯一原因。这个新建的结构使在汇编代码中计算其偏移变量非常容易。由于现在用slab分配器动态生成task_struct,所以只需要在栈底(对于向下增长的栈来说)或栈顶(对于向上增长的栈来说)创建一个新的结构struct thread_info。
union thread_union {/*大小为8k*/
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];/*大小为8k*/
};
从这个联合体中可以看到,thread_info结构体和栈存放在两个页面中,而栈的大小正好是两个页面,这也论证了上面所说的。
下面来看看如何获得当前进程的指针
#define current get_current()
#define get_current() (current_thread_info()->task)
static inline struct thread_info *current_thread_info(void)
{
struct thread_info *ti;
/*函数percpu_read_stable(kernel_stack)取得相应当前cpu上 thread_info所在的地址,返回值是整数。*/
ti = (void *)(percpu_read_stable(kernel_stack) +
KERNEL_STACK_OFFSET - THREAD_SIZE);
return ti;
}
其中kernel_stack为内核per CPU变量
DEFINE_PER_CPU(unsigned long, kernel_stack) =
(unsigned long)&init_thread_union - KERNEL_STACK_OFFSET + THREAD_SIZE;
thread_union是一个联合体,里面定义了thread_info结构和堆栈结构,THREAD_SIZE在32位平台上一般定义为4K,所以stack的大小其实就是4KB,这就是初始任务在核心里所拥有的所有空间,除去thread_info和KERNEL_STACK_OFFSET占用的空间后,就是任务在核心里实际拥有堆栈的大小。KERNEL_STACK_OFFSET定义为5*8,由于是unsigned long,所以堆栈底部以上还有5*8*4B=200B的空间用来存放程序运行时相关的环境参数。
内核栈采用每个CPU数据(per_cpu_data)的概念,在每一个处理器中各自维护一个以前在多个CPU之间进行共享的数据,如当前运行的任务结构体,以前就是在多个CPU之间共享的。
关于内核进程初始化
首先我们要知道linux中第一个进程时内核进程,pid为0,他是所有进程的父进程。这个进程也叫swapper,或者idle。这个进程时静态初始化的,定义为:
/*
* Initial thread structure.
*
* We need to make sure that this is 8192-byte aligned due to the
* way process stacks are handled. This is done by making sure
* the linker maps this in the .text segment right after head.S,
* and making head.S ensure the proper alignment.
*
* The things we do for performance..
*/
union thread_union init_thread_union __init_task_data =
{ INIT_THREAD_INFO(init_task) };
/* Attach to the init_task data structure for proper alignment */
#define __init_task_data __attribute__((__section__(".data.init_task")))
这个进程主要用途就是保证至少会有一个进程还在运行,也就是说当没有进程运行的情况下,它是最后的那个进程,就是swaaper 进程.init进程就是由它派生出来的。
然后来看对应的初始化,在header32.s中,这里是将init_thread_union的地址加上THREAD_SIZE(栈的大小)装载进esp,然后__BOOT_DS装载进ss。32为x86默认栈的大小为8k,因此一开始esp的值就是init_thread_union的地址+栈的大小。
/* Set up the stack pointer */
lss stack_start,%esp
.data
ENTRY(stack_start)
.long init_thread_union+THREAD_SIZE
.long __BOOT_DS
关于进程0和进程1(2012.7.12补充)
系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。整个linux系统的所有进程也是一个树形结构。树根是系统自动构造的,即在内核态下执行的0号进程,它是所有进程的祖先。由0号进程创建1号进程(内核态),1号负责执行内核的部分初始化工作及进行系统配置,并创建若干个用于高速缓存和虚拟主存管理的内核线程。随后,1号进程调用execve()运行可执行程序init,并演变成用户态1号进程,即init进程。它按照配置文件/etc/initab的要求,完成系统启动工作,创建编号为1号、2号...的若干终端注册进程getty。
每个getty进程设置其进程组标识号,并监视配置到系统终端的接口线路。当检测到来自终端的连接信号时,getty进程将通过函数execve()执行注册程序login,此时用户就可输入注册名和密码进入登录过程,如果成功,由login程序再通过函数execv()执行shell,该shell进程接收getty进程的pid,取代原来的getty进程。再由shell直接或间接地产生其他进程。
上述过程可描述为:0号进程->1号内核进程->1号用户进程(init进程)->getty进程->shell进程
注意,上述过程描述中提到:1号内核进程调用执行init并演变成1号用户态进程(init进程),这里前者是init是函数,后者是进程。两者容易混淆,区别如下:
1.init()函数在内核态运行,是内核代码
2.init进程是内核启动并运行的第一个用户进程,运行在用户态下。
3.一号内核进程调用execve()从文件/etc/inittab中加载可执行程序init并执行,这个过程并没有使用调用do_fork(),因此两个进程都是1号进程。
上述过程在内核中表现为在内核初始化start_kernel函数最后调用rest_init函数并调用kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);语句来创建内核线程init,内核线程的执行函数kernel_init中最后调用:
if (ramdisk_execute_command) {
run_init_process(ramdisk_execute_command);
printk(KERN_WARNING "Failed to execute %s\n",
ramdisk_execute_command);
}
并最终调用通过excuce函数来启动/init,这也就是用户空间的init进程。
init进程有两项使命:
1、执行系统初始化脚本,创建一系列的进程(它们都是init进程的子孙);
2、在一个死循环中等待其子进程的退出事件,并调用waitid系统调用来完成“收尸”工作;
init进程不会被暂停、也不会被杀死(这是由内核来保证的)。它在等待子进程退出的过程中处于TASK_INTERRUPTIBLE状态, “收尸”过程中则处于TASK_RUNNING状态。
参考资料
http://www.cnblogs.com/justinzhang/archive/2011/07/18/2109923.html
http://www.pagefault.info/?p=36