32位linux系统上,进程的地址空间为4G(2的32次方个字节),包括1G的内核地址空间,和3G的用户地址空间。
内核在创建进程的时候,在创建task_struct的同时,会为进程创建相应的堆栈。每个进程会有两个栈,一个用户栈,存在于用户空间,一个内核栈,存在于内核空间。当进程在用户空间运行时,cpu堆栈指针寄存器里面的内容是用户堆栈地址,使用用户栈;当进程在内核空间时,cpu堆栈指针寄存器里面的内容是内核栈空间地址,使用内核栈。
在每一个进程的生命周期中,如果使用了系统调用,那么,进程就会从用户态陷入内核态。在执行系统调用陷入内核之后,这些内核代码所使用的栈并不是原先用户空间中的栈(用户栈),而是一个内核空间的栈,这个称作进程的“内核栈”。即进程的用户栈和内核栈的发送了切换。 进程陷入内核态后,先把用户态堆栈的地址保存在内核栈之中,然后设置堆栈指针寄存器的内容为内核栈的地址,这样就完成了用户栈向内核栈的转换;当进程从内核态恢复到用户态之行时,在内核态执行的最后将保存在内核栈里面的用户栈的地址恢复到堆栈指针寄存器即可。这样就实现了内核栈和用户栈的互相转换。
那么,我们知道从内核转到用户态时用户栈的地址是在陷入内核的时候保存在内核栈里面的,但是在陷入内核的时候,我们是如何知道内核栈的地址的呢?
关键在进程从用户态转到内核态的时候,进程的内核栈总是空的。这是因为,当进程在用户态运行时,使用的是用户栈,当进程陷入到内核态时,内核栈保存进程在内核态运行的相关信息,但是一旦进程返回到用户态后,内核栈中保存的信息无效,会全部恢复,因此每次进程从用户态陷入内核的时候得到的内核栈都是空的。所以在进程陷入内核的时候,直接把内核栈(此时是全局的)的栈顶地址给堆栈指针寄存器就可以了。
内核栈在kernel-2.4和kernel-2.6里面的实现方式是不一样的。
在kernel-2.4内核里面,内核栈的实现是:
Union task_union {
Struct task_struct task; // 进程描述符
Unsigned long stack[INIT_STACK_SIZE/sizeof(long)];
};
其中,INIT_STACK_SIZE的大小只能是8K。
内核为每个进程分配task_struct结构体的时候,实际上分配两个连续的物理页面,底部用作task_struct结构体,结构上面的用作堆栈。使用current()宏能够访问当前正在运行的进程描述符。
注意:这个时候task_struct结构是在内核栈里面的,内核栈的实际能用大小大概有7K。
内核栈在kernel-2.6里面的实现是(kernel-2.6.32):
Union thread_union {
Struct thread_info thread_info;
Unsigned long stack[THREAD_SIZE/sizeof(long)];
};
其中THREAD_SIZE的大小可以是4K,也可以是8K,thread_info占52bytes。
当内核栈为8K时,Thread_info在这块内存的起始地址,内核栈从堆栈末端向下增长。所以此时,kernel-2.6中的current宏是需要更改的。要通过thread_info结构体中的task_struct类型指针thread_info 来获得相关联的task_struct。
struct thread_info {
struct task_struct *task;
struct exec_domain *exec_domain;
__u32 flags;
__u32 status;
__u32 cpu;
......
};
注意:此时(2.6以后的内核)的task_struct结构体已经不在内核栈空间里面了。
内核栈主要是用于进程陷入内核时使用的栈,主要用于进程切换时保存用户态进程信息(寄存器值,一部分硬件上下文等),以及进程在内核执行时分配空间使用。
图中的数据结构均是存放在内核地址空间中,task_struct为进程描述符,也是存放在内核地址空间中,是内核对进程进行管理所使用的数据结构,其中包含了进程几乎所有信息
实际上就是我们所说的进程控制块--PCB
在Linux内核2.6以前,每个进程的进程描述符存放在它们(也就是每个进程)各自内核栈的尾端(所以,2.6内核版本的的进程描述符是存放在内核地址空间中的内核栈里面的)。这样做是为了让那些像x86那样寄存器较少的硬件体系结构只要通过栈指针就能计算出它的位置,而避免使用额外的寄存器专门记录。
由于现在(也就是**2.6以后)**用slab分配器动态生成task_struct(也就是说,2.6以后的进程描述符不需要在内核栈的尾端了),所以,只需要在栈底(对于向下增长的栈来说)或栈顶(对于向上增长的栈来说)创建一个新的结构thread_info。此时(2.6以后的内核)的task_struct结构体已经不在内核栈空间里面了。
一种解释:假设某个进程通过系统调用运行在内核态(使用这个全局内核堆栈),此时如果被抢占,发生一次切换,另一个进程开始运行,如果这个当前进程又通过系统调用陷入内核,那么这个进程也将使用这个全局内核堆栈,这样的话就把以前那个进程的内核空间堆栈给破坏了。 而如果进程使用独立的内核栈,就避免了这种情况的发生。
从内核观点看,进程的目的就是担当分配系统资源(CPU 时间、内存等)的实体。为此目的,操作系统为每个进程维持着一个进程描述符。
每一个进程都有一个进程描述符task_struct,且有一个用来定位它的结构thread_info,thread_info位于其进程内核栈中(有些实现没有用到thread_info,而是使用一个寄存器来记录进程描述符的地址),操作系统使用这个结构中的task指针字段找到进程的进程描述符,从而得到执行一个进程所需的全部信息。
struct thread_info {
struct task_struct *task; //指向当前进程内核栈对应的进程的进程描述符
struct exec_domain *exec_domain;
__u32 flags;
__u32 status;
__u32 cpu;
int preempt_count;
mm_segment_t addr_limit;
struct restart_block restart_block;
void *sysenter_return;
int uaccess_err;
};