我对Linux用户空间与内核空间数据传递的几点理解和总结
(1)让我们忽略Linux对段式内存映射的支持。在保护模式下,我们知道无论CPU运行于用户态还是核心态,CPU执行程序所访问的地址都是虚拟地址,MMU必须通过读取控制寄存器CR3中的值作为当前页面目录的指针,进而根据分页内存映射机制(参看相关文档)将该虚拟地址转换为真正的物理地址才能让CPU真正的访问到物理地址。(2)对于32位的Linux,其每一个进程都有4G的寻址空间,但当一个进程访问其虚拟内存空间中的某个地址时又是怎样实现不与其它进程的虚拟空间混淆的呢?每个进程都有其自身的页面目录PGD,Linux将该目录的指针存放在与进程对应的内存结构task_struct.(struct mm_struct)mm->pgd中。每当一个进程被调度(schedule())即将进入运行态时,Linux内核都要用该进程的PGD指针设置CR3(switch_mm())。
(3)当创建一个新的进程时,都要为新进程创建一个新的页面目录PGD,并从内核的页面目录swapper_pg_dir中复制内核区间页面目录项至新建进程页面目录PGD的相应位置,具体过程如下:do_fork()->copy_mm()->mm_init()->pgd_alloc()->set_pgd_fast()->get_pgd_slow()->memcpy(&PGD + USER_PTRS_PER_PGD, swapper_pg_dir + USER_PTRS_PER_PGD, (PTRS_PER_PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t)),这样一来,每个进程的页面目录就分成了两部分,第一部分为“用户空间”,用来映射其整个进程空间(0x0000 0000-0xBFFF FFFF)即3G字节的虚拟地址;第二部分为“系统空间”,用来映射(0xC000 0000-0xFFFF FFFF)1G字节的虚拟地址。可以看出Linux系统中每个进程的页面目录的第二部分是相同的,所以从进程的角度来看,每个进程有4G字节的虚拟空间,较低的3G字节是自己的用户空间,最高的1G字节则为与所有进程以及内核共享的系统空间。
(4)现在假设我们有如下一个情景:
在进程A中通过系统调用sethostname(const char *name,seze_t len)设置计算机在网络中的“主机名”.
在该情景中我们势必涉及到从用户空间向内核空间传递数据的问题,name是用户空间中的地址,它要通过系统调用设置到内核中的某个地址中。让我们看看这个过程中的一些细节问题:系统调用的具体实现是将系统调用的参数依次存入寄存器ebx,ecx,edx,esi,edi(最多5个参数,该情景有两个name和len),接着将系统调用号存入寄存器eax,然后通过中断指令“int 80”使进程A进入系统空间。由于进程的CPU运行级别小于等于为系统调用设置的陷阱门的准入级别3,所以可以畅通无阻的进入系统空间去执行为int 80设置的函数指针system_call()。由于system_call()属于内核空间,其运行级别DPL为0,CPU要将堆栈切换到内核堆栈,即进程A的系统空间堆栈。我们知道内核为新建进程创建task_struct结构时,共分配了两个连续的页面,即8K的大小,并将底部约1k的大小用于task_struct(如#define alloc_task_struct() ((struct task_struct *) __get_free_pages(GFP_KERNEL,1))),而其余部分内存用于系统空间的堆栈空间,即当从用户空间转入系统空间时,堆栈指针esp变成了(alloc_task_struct()+8192),这也是为什么系统空间通常用宏定义current(参看其实现)获取当前进程的task_struct地址的原因。每次在进程从用户空间进入系统空间之初,系统堆栈就已经被依次压入用户堆栈SS、用户堆栈指针ESP、EFLAGS、用户空间CS、EIP,接着system_call()将eax压入,再接着调用SAVE_ALL依次压入ES、DS、EAX、EBP、EDI、ESI、EDX、ECX、EBX,然后调用sys_call_table+4*%EAX,本情景为sys_sethostname()。
(5)在sys_sethostname()中,经过一些保护考虑后,调用copy_from_user(to,from,n),其中to指向内核空间system_utsname.nodename,譬如0xE625A000,from指向用户空间譬如0x8010FE00。现在进程A进入了内核,在系统空间中运行,MMU根据其PGD将虚拟地址完成到物理地址的映射,最终完成从用户空间到系统空间数据的复制。准备复制之前内核先要确定用户空间地址和长度的合法性,至于从该用户空间地址开始的某个长度的整个区间是否已经映射并不去检查,如果区间内某个地址未映射或读写权限等问题出现时,则视为坏地址,就产生一个页面异常,让页面异常服务程序处理。过程如下:copy_from_user()->generic_copy_from_user()->access_ok()+__copy_user_zeroing().
(6)小结:
*进程寻址空间0~4G
*进程在用户态只能访问0~3G,只有进入内核态才能访问3G~4G
*进程通过系统调用进入内核态
*每个进程虚拟空间的3G~4G部分是相同的
*进程从用户态进入内核态不会引起CR3的改变但会引起堆栈的改变
(7)有不准确或错误的地方请不吝指教,谢谢。 task_struct is included in the 8192, why there is a esp == alloc_task_struct() + 8192? esp is not predictable when reaching the point to __copy_user_zeroing, the only knowlege is that, it is within the two page. task_struct is included in the 8192, why there is a esp == alloc_task_struct() + 8192? esp is not predictable when reaching the point to __copy_user_zeroing, the only knowlege is that, it is within the two page.
Yes,I was ever bothered by this question before.But you should know that when a process is trapped,that is to say CPU goes from user space to system space,the ESP will be changed from the process's user STACK to the process's system STACK.
Maybe you will ask why I don't see the implement of this change.In fact,it is the hardware that makes the function work,when CPU changes it's DPL from 3 (user) to 0 (kernel),it will read current process's kernel STACK POINTER from TSS pointerred by TS(Task Regiser).
So... no, i won't ask you that question.
I wonder if MACRO current can get the task_struct pointer correctly according to your explaination. 让我来解释一下,首先一个进程分配空间时都是以连续两个页面即8k字节进行分配的.其中task_struct结构位于低字节,栈底位于最高字节,栈向低字节延伸.
正如sorry30所说,当一个进程刚进入系统态时(被切换进入),tss的设置将esp指向当前系统堆栈的栈顶,其实确切的说,当进程从用户空间转入系统空间时esp并非指向alloc_task_struct()+8192处,原因很简单,首先esp总是指向栈顶,而堆栈转换时sorry30已经提到了,在系统堆栈需要压入一些值,比如用户的返回地址等,但是这只是少量的几个.不过从理解的角度是不能这样认为的,关键是系统堆栈已经有了东东了,虽然很少.至于"esp == alloc_task_struct() + 8192"(首先这不是严格意义上的等于,暂且算吧).很简单,allock_task_struct()返回的是task_struct的首址,也就是8k字节的最低地址,8192是8k,如果按照sorry30的理解,指向栈顶的位置就自然是
"esp == alloc_task_struct() + 8192"了,其实堆栈此时已有数据,esp要向下移,其实应该是少于esp == alloc_task_struct() + 8192
接着我来解释为什么宏current可以得到当前进程的task_struct地址:
static inline struct task_struct * get_current(void)
7 {
8 struct task_struct *current;
9 __asm__("andl %%esp,%0; ":"=r" (current) : "" (~8191UL));
10 return current;
11}
12
13 #define current get_current()
首先current宏肯定是在系统态的用的!
上面的函数的意思就是让当前进程的系统态堆栈指针esp和~8191ul相与.什么意思呢?
esp指向的是当前进程系统堆栈的栈顶位置,相与之后的结过就是esp地址的低13位全变成了0.自然就指向了这个8k字节的最低地址了.于是也就得到了task_struct的地址.
因为给进程分配空间的时候总是以8k字节为单位的.大家按照我上面说的在纸上画画就知道了. 如果zhiwood感兴趣可以看看下面这篇文章:
How does get_current() work ?
static inline struct task_struct * get_current(void)
{
struct task_struct *current;
__asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL));
return current;
}
get_current() is a routine for getting access to the task_struct of the currently executing task. It uses the often confusing inline assembly features of GCC to perform this, as follows :
| __asm__(
This signifies a piece of inline assembly that the compiler must insert into its output code. The __asm__ is the same as asm, but can't be disabled by command line flags.
| "andl %%esp,%0
"%%" is a macro that expands to a "%".
"%0" is a macro that expands to the first input/output specification.
So in this case, it takes the stack pointer (register %esp) and ANDs it into a register that contains 0xFFFFE000, leaving the result in that register.
Basically, the task's task_struct and a task's kernel stack occupy an 8KB block that is 8KB aligned, with the task_struct at the beginning and the stack growing from the end downwards. So you can find the task_struct by clearing the bottom 13 bits of the stack pointer value.
其实在国外的网站上有很多这样的文章.
还有要想知道更深,好好的看<<linux内核源代码情景分析>>估计sorry30也是看的那上面的吧?
还请赐教:) thanks, i see 我觉得<<linux内核源代码情景分析>>这本书还是不错的!
无论什么样的书,我认为应该在读的时候带着问题去看,或许有时候会云里雾里,但不要紧,等到将很多有牵扯的问题汇集在一起的时候,你也许就会将它们串联到了一起而得到一个整体而深入的解释。
我之所以写上面这个帖子也是因为在读系统调用部分的原码时陷入了一时得不到解决就如同失恋的痛苦之中,这时就回忆了以前读到理解(实际未完全消化)的东西,然后写下来以让有同一痛苦的朋友enjoy。 哈哈,我也在研读qjfx,很高兴能认识你,我以前也遇到类似这样的 问题,如果你愿意的话我们可以交流的[email protected]
2)对于32位的Linux,其每一个进程都有4G的寻址空间,但当一个进程访问其虚拟内存空间中的某个地址时又是怎样实现不与其它进程的虚拟空间混淆的呢?每个进程都有其自身的页面目录PGD,Linux将该目录的指针存放在与进程对应的内存结构task_struct.(struct mm_struct)mm->pgd中。每当一个进程被调度(schedule())即将进入运行态时,Linux内核都要用该进程的PGD指针设置CR3(switch_mm())。
(3)当创建一个新的进程时,都要为新进程创建一个新的页面目录PGD,并从内核的页面目录swapper_pg_dir中复制内核区间页面目录项至新建进程页面目录PGD的相应位置,具体过程如下:......
这里面的pgd并不是每个进程一个,pgd是全局的,系统只要维护一张表格就可以了
我也是刚刚才开始琢磨内核,有很多不明白的地方和混淆的地方,希望大家能够耐心的帮助啊,谢谢。 pgd是每个进程一个,这是实现虚拟地址和物理地址
相分离的关键。 daemonx, 或许你还没有看到进程调度的部分。当我们完全理解了Linux的调度机制以及其细节时,我门就不仅会对系统调用而且对用户空间,系统空间包括pgd的相关细节都会有一个很透彻的理解,也会突然有一种似乎很释然的感觉。 我也加入大家的讨论。
我也是刚刚才开始琢磨内核,有很多不明白的地方和混淆的地方,希望大家能够耐心的帮助啊,谢谢。
我的EMAIL:[email protected] 初次读内核,真实有些头大,不过没有关系,坚持!坚持!过一阵子就好了!呵呵!
页:
[1]