线程的实现可以分为两类:用户级线程(User-Level Thread)和内核线线程(Kernel-Level Thread).后者又称为内核支持的线程或轻量级进程.
用户线程指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。
内核线程: 由操作系统内核创建和撤销。内核维护进程及线程的上下文信息以及线程切换。一个内核线程由于I/O操作而阻塞,不会影响其它线程的运行。Windows NT和2000/XP支持内核线程
用户线程:由应用进程利用线程库创建和管理,不以来于操作系统核心。不需要用户态/核心态切换,速度快。操作系统内核不知道多线程的存在,因此一个线程阻塞将使得整个进程(包括它的所有线程)阻塞。由于这里的处理器时间片分配是以进程为基本单位,所以每个线程执行的时间相对减少。
Windows NT和OS/2支持内核线程。Linux 支持内核级的多线程
-----------------------
关于内核线程(kernel_thread)
我们知道Linux内核使用内核线程来将内核分成几个功能模块,
像kswapd,kflushd等,系统中的init进程也是由idle进程调用
kernel_thread()来实现产生的.
我们先来看看内核线程的实现,再来分析内核线程的性质.
int kernel_thread(int(*fn)(void*arg),void *arg,int flags)
{
long retval,d0;
__asm__ __volitate__(
"movl %%esp,%%esi\n\t"
"int $0x80\n\t"
"cmpl %%esp,%%esi\n\t"
"je 1f \n\t"
"movl %4,%%eax\n\t"
"pushl %%eax\n\t"
"call *%5\n\t"
"movl %3,%0\n\t"
"int $0x80\n\t"
"1:\t"
:"=&a"(retval),"=&S"(d0)
:"0"(__NR_clone),"i"(__NR_exit),
"r"(arg),"r"(fn),
"b"(flags | CLONE_VM)
:"memory"
);
return retval;
}
这段代码翻译成直观的ASM码:
{
movl __NR_clone,%0;
movl __NR_exit,%3;
movl arg,%4;
movl fn,%5;
movl flags|CLONE_VM,%ebx;
mov %%esp,%%esi;
int $0x80;
cmpl %%esp,%%esi;
je 1f;
movl %4,%%eax;
pushl %%eax
call *%5;
movl %3,%0;
int $0x80;
1: movl %%eax,retval
movl %%esi,d0
}
它的伪C码为:
int kernel_thread()
{
pid=clone(flags);
if(child)
{
fn(arg);
exit(0);
}
return pid;
}
从上面的代码可以看出,内核线程有以下性质:
1.
内核线程是通过系统调用clone()来实现的,使用CLONE_VM标志(用户还可以
提供其他标志,CLONE_PID,CLONE_FS,CLONE_FILES等),因此内核线程与调用
的进程(current)具有相同的进程空间.
2.
由于调用进程是在内核里调用kernel_thread(),因此当系统调用返回时,子进程也处于
内核态中,而子进程随后调用fn,当fn退出时,子进程调用exit()退出,所以子进程是在
内核态运行的.
3.
由于内核线程是在内核态运行的,因此内核线程可以访问内核中数据,调用内核函数.
运行过程中不能被抢占等等.
请注意在kernel_thread是如何调用系统调用的,我们知道kernel_thread是在内核中
调用,所以他是可以直接调用系统调用的,像sys_open()等,但是在这里kernel_thread
通过系统调用门(int$80)来间接调用clone()函数,就提出以下问题:
1.为什么这样?
2.如果我们直接调用sys_clone()会有什么样的结果呢?
int kernel_thread()
{
int pid;
pid=sys_clone();
if(!pid)
{
exit();
}
return pid;
}
这样,当子进程获取CPU资源时(运行时),从ret_from_fork恢复执行,栈布局对于子进程而言
是不对的,问题在于当子进程运行到RESTORE_ALL的IRET,仔细想一想栈布局的变化.
由sys_clone()的申明可知调用sys_clone需要pt_regs的栈结构,如果我们直接调用sys_clone
是没用办法做到的(如果可以我们也需要精心为它准备栈,//:-(,真是伤神)
同理,其他的类似系统调用,我们也必须通过int$80的系统调用门来实现.
而对于sys_execl,sys_open,sys_close,sys_exit,则可以直接调用.//xixi,我们可以
改动kernel_thread来测试sys_exit是否可以直接调用,同时也可以使用sys_clone的直接调用
来证明我们的分析是否正确.
而如果我们使用系统调用门(int$80)来解决问题,我们使用同样的方法来分析:
A2)
ebx <-- ( esp after save all ,ready for syscalls )
ecx
...
oldeip <-- ( esp before SAVE_ALL which construct stack for syscalls )
oldcs
eflags
d0 <- ( space for local variables )
retval
fn <- ( arguments for kernel_thread )
arg
clone_flags
eip <- ( retore ip for kernel_thread )
..
由于kernel_thread在内核的代码段中,所以没有发生栈切换,所有的压栈/退栈都是在
内核栈中进行的.请注意这样栈中便没有(OLDSS,OLDESP),所以在kernel_thread声明了
两个局部参数(retval,d0),对于retval的意义是明显的,而d0大概是(dummy local
variable
0,...n)的意思吧,:)
B2)子进程运行前:
子进程的TSS,栈布局
ebx <- esp
ecx
...
oldeip
oldcs
eflags
d0 <- (局部变量d0)
retval <- (局部变量retval)
运行到RESTORE_ALL时,将恢复CPU各寄存器,当运行到IRET时,
由于在相同特权等级的转移,所以没有发生特权级切换,所以ESP,SS没有发生变化.
BTW,由上面的分析可知,kernel_thread创建的进程是不能转到用户态运行的.
-------------------------------
Linux 为内核代码和数据结构预留了几个页框。这些页永远不会 被转出到磁盘上。从 0x0 到 0xc0000000 (PAGE_OFFSET) 的线性地址可由用户代码和内核代码进行引用。从 PAGE_OFFSET 到 0xffffffff 的线性地址只能由内核代码进行访问。
这意味着在 4 GB 的内存空间中,只有 3 GB 可以用于用户应用程序。
我已经向您展示了(32 位架构上的) Linux 内核按照 3:1 的比率来划分虚拟内存:3 GB 的虚拟内存用于用户空间,1 GB 的内存用于内核空间。内核代码及其数据结构都必须位于这 1 GB 的地址空间中,但是对于此地址空间而言,更大的消费者是物理地址的虚拟映射。
某一个进程只能运行在用户方式(user mode)或内核方式(kernel mode)下。用户程序运行在用户方式下,而系统调用运行在内核方式下。在这两种方式下所用的堆栈不一样:用户方式下用的是一般的堆栈,而内核方式下用的是固定大小的堆栈(一般为一个内存页的大小)
所有的进程部分运行与用户态,部分运行于系统态。底层的硬件如何支持这些状态各不相同但是通常有一个安全机制从用户态转入系统态并转回来。用户态比系统态的权限低了很多。每一次进程执行一个系统调用,它都从用户态切换到系统态并继续执行。这时让核心执行这个进程。
在fork一个进程的时候,必须建立进程自己的内核页目录项(内核页目录项要
与用户空间的的页目录放在同一个物理地址连续的页面上,所以不能共享,但
所有进程的内核页表与进程0共享
两个代表 (code 和 data/stack)是内核空间从[0xC000 0000] (3 GB)到[0xFFFF FFFF] (4 GB) //线形地址
两个代表 (code 和 data/stack)是用户空间从[0] (0 GB) 到 [0xBFFF FFFF] (3 GB) //线性地址
通常情况下,内核是不会调用用户层的代码,要想实现这逆向的转移,一般做法是在用户进程的核心栈(tss->esp0)压入用户态的SS,ESP,EFLAGS,CS,EIP,伪装成用户进程是通过陷阱门进入核心态,之后通过iret返回用户态。
在 32 位线形地址中的 4 GB 虚拟空间中,其中有 1 GB 作为 内核空间,从 3G—4G。 每个进程都有自己的 3 G 用户空间,它们共享1GB 的内核空间。当一个进程从用户空间进入内核空间时,它就不再有自己的进程空间了。
应用程序在虚拟内存中布局,并且有一块很大的栈空间。当然是用来保存函数调用历史及当前函数中的自动变量的。而相反,内核具有非常小的栈,它可以只和一个 4096 字节的页那样大。我们自己的函数(如 LKM)必须和整个内存空间调用链一同共享这个栈。因此,声明大的自动变量并不是个好注意,若我们要大的结构,则应该在调用时动态分配。
对内核线程的虚拟空间总结一下:
1、创建的时候:
父进程是用户进程,则mm和active_mm均共享父进程的,然后内核线程一般调用daemonize适头舖m
父进程是内核线程,则mm和active_mm均为NULL
总之,内核线程的mm = NULL;进程调度的时候以此为依据判断是用户进程还是内核线程。
2、进程调度的时候
如果切换进来的是内核线程,则置active_mm为切换出去的进程的active_mm;
如果切换出去的是内核线程,则置active_mm为NULL。
linux在创建用户任务的时候,给每个任务都分配了一个kernel mode stack。一个运行在用户态的任务如果被一个IRQ打断,中断处理要做一次堆栈切换。这时linux好像使用了任务的kernel mode stack,也就是说linux系统中没有一个唯一的系统堆栈,而是每一个任务都有一个系统堆栈,中断处理的栈使用的就是被打断任务的系统堆栈。内核线程也是进程,只不过没有自己的用户空间,但task_struct和内核堆栈还是得有的,要不怎么运行呢?
[1.内核在主动进行进程调度时,可以自己设置将要投入运行进程的sp0为TSS段中的sp0,则该用户进程在进入内核后使用的是它自身的系统堆栈,但如果cpu运行在某一用户进程时,而为另一用户进程服务的外部中断发生了,在进入内核后使用的是当前用户进程的系统堆栈,还是中断服务的另一用户进程的系统堆栈呢?
2操作系统映象是否拥有自己的堆栈空间?还是利用用户进程的系统堆栈?
回答: 1。外部中断不是为某个用户进程服务的,是为整个操作系统服务的,它始终用当前进程的核心堆栈。
2。用用户进程的系统堆栈。]