考虑这样一个问题:能否通过管道(fifo)从一个进程A向另一个进程B(A和B之间并无亲属关系)中传递A进程中对象的地址,从而在进程B中访问到A进程的对象呢?
先放结论:传一个进程局部的对象的地址肯定是不行的。因为现代操作系统都具有进程隔离,每个进程具有其独立的虚拟存储器,合法的虚拟地址需要通过MMU来访问物理地址(考虑一下fork后COW的情况),但不合法的虚拟地址会触发缺页异常。
windows下有个例外是如果A进程的ntdll里的API地址 发送给B进程,B进程就可以直接调用,这是个特殊情况(已经不算是进程A独有对象的地址了)。
**[Windows Internals,6th]**Because Ntdll.dll is mapped at the same address, randomly generated at boot, into every process’s address space, the current process can simply pass the address of the function it obtains from its own Ntdll.dll mapping.
然后我们实际来实验一下进程间传递对象地址访问的情况:
0. 创建一个命名管道
- 在进程A中创建一个对象
- 通过管道将A进程中的该对象地址传递给进程B
- 在进程B中通过从管道中接收到的地址来访问该对象
- 在进程A中看B进程执行之后的该对象的值
首先我们先创建一个命名管道:
1 | mkfifo fifofile |
然后我们可以写两个程序,一个通过该管道写,一个通过该管道读:
1 | // writefifo,process A |
然后是读:
1 | // readfifo, process B |
直接来看一下运行结果吧:
可以看到直接是段错误了。
这是因为Linux为每个进程维持了一个单独的虚拟地址空间。
每个进程的虚拟存储器大概如下图:
当MMU在试图翻译某个虚拟地址时,会将虚拟地址作为一个索引来定位实际的物理地址并从存储器中读取它,通过检查有效位来判断DRAM是否命中。
在虚拟存储器中,DRAM缓存不命中被称为缺页(page fault)。缺页异常将导致控制转移到内核的缺页处理程序。
缺页异常的处理程序具有下面三种情况:
- 虚拟地址是否合法?
- 试图进行的存储器访问是否合法?
- 内核知道该缺页是由于对合法的虚拟地址进行合法的操作造成的。
先从第一步开始说:虚拟地址是否合法?(情况一)
就是该虚拟地址是在某个区域结构定义的范围内吗?缺页处理程序会搜索区域结构的链表,把该虚拟地址和每个区域结构中的vm_start和vm_end作比较。如果这个指令不合法,那么缺页处理程序就触发一个段错误,从而终止这个进程。
我们上面的代码就是虚拟地址不合法,该虚拟地址在进程B中并不是一个合法的虚拟地址(在该进程的DRAM中未缓存),所以会触发段错误导致进程终止。
第二步:试图进行的存储器访问是否合法?(情况二)
也就是进程是否有读写或者执行这个区域内页面的权限?如果试图进行的访问时不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。
第三步:内核知道该缺页是由于对合法的虚拟地址进行合法的操作造成的。(情况三)
当内核知道该缺页是由于对合法的虚拟地址进行合法的操作造成的,它会这样来处理这个缺页:选择牺牲一个页面,如果这个牺牲页面被修改过,那么久将他交换出去。换入新的页面并更新新页表。
当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送该虚拟地址到MMU。此时,MMU就能正常地翻译该虚拟地址就不会再产生缺页中断异常了。
三种情况的描述图如下:
更多的内容请详细参照《CASPP》第九章虚拟存储器。