C语言如何获得变量的物理地址以及简单的写时拷贝测试

本文通过分析Linux下的/proc/self/pagemap来获取变量的物理地址,并讨论了vfork和fork在写时拷贝上的区别。实验表明vfork不进行数据段拷贝,父子进程共享虚拟空间,且子进程结束后父进程才能继续执行。而fork即使不修改数据,父子进程的栈也不同。最后提到了_exit不会清除IO缓存。

基本的思路:linux下的/proc/self是对自身进程映射的文件夹,里面的pagemap允许查看到每个虚拟页映射到的物理页。

#include <stdio.h>
#include <unistd.h>
#include <inttypes.h>
intptr_t Mytop(uintptr_t vaddr) 
{
    FILE *pagemap;
    intptr_t paddr = 0;
    int offset = (vaddr / sysconf(_SC_PAGESIZE)) * sizeof(uint64_t);
    uint64_t e;
     // https://www.kernel.org/doc/Documentation/vm/pagemap.txt
    if ((pagemap = fopen("/proc/self/pagemap", "r"))) {
        if (lseek(fileno(pagemap), offset, SEEK_SET) == offset) {
            if (fread(&e, sizeof(uint64_t), 1, pagemap)) {
        if (e & (1ULL << 63)) { // page present ?
                    paddr = e & ((1ULL << 54) - 1); // pfn mask
                    paddr = paddr * sysconf(_SC_PAGESIZE);
            // add offset within page
            paddr = paddr | (vaddr & (sysconf(_SC_PAGESIZE) - 1));
                }   
            }   
        }   
        fclose(pagemap);
    }   
    return paddr;
}  


void TestShow(void *x)
{
  printf("Virtual Address:%u  Physical Address:%u\n",x,Mytop((uintptr_t)x));
}

void main()<pre>
{
  int a;
  TestShow(&a);
}

呃。。。。顺便一提吧,本人后来测试了一下vfork的写时拷贝,大名鼎鼎的写时拷贝….
这里写图片描述
结果发现
这里写图片描述
vfork写时拷贝是完全不存在的。。。。父子进程完全共享虚拟空间,并且经过测试,本人发现了调动vfork后,一定是子进程先结束后父进程才能继续执行!
呃,后来我想了一下,估计原因是这样的: 假设父进程不在子进程调度完成后才能继续玩下跑,那么在父子进程交替着调度往下跑,并且是在同一个堆栈上进行函数调用,栈帧开辟释放,那么结果无疑是一种噩梦般的结果..我有点困扰为什么系统要这么做,于是我去查阅了一下关于vfork的介绍。得到了以下东西:
1. fork要拷贝父进程的数据段;而vfork则不需要完全拷贝父进程的数据段,在子进程没有调用exec和exit之前,子进程与父进程共享数据段
2. fork不对父子进程的执行次序进行任何限制;而在vfork调用中,子进程先运行,父进程挂起,直到子进程调用了exec或exit之后,父子进程的执行次序才不再有限制
顿时有点茅塞顿开的感觉,原来linux内核设计vfork就是和exec配套使用的,写时拷贝更多的是讲一个进程调用vfork确实完全共享进程绝大部分资源,这里是讲共享部分,至于拷贝部分不是说什么蛋疼的程序员,,,比如我。。。无聊的修改了一下局部变量或者是栈上数据,那么系统就要因此给你分配出相应的东西出拷贝一份原有东西再进行你的修改操作,让进程看起来好像是在你(程序员 )写入后系统自动拷贝再去修改。。。。这个是不可能的,内核不可能这么蛋疼专门去检测这些东西,或者说vfork就是为了exec而诞生的,估计内核设计者是这样想的吧,调动vfork的都是极度渴望进行快速exec的程序员同学。所谓的写时拷贝不是说程序员修改栈上数据引发内核专门分配对应资源再进行修改,而是讲的是frok以后调动exec,内核复制相应资源再进行修改!!!
呃。。。。废话有点多,既然测试了vfork发现学习了这么多东西,那么让我们继续进行测试frok吧。。。代码不用贴了吧..
改变点有两个:
1.fork替换了vfork
2.去掉了x = 10;y=20;
结果是发现fork在没有任何修改,父子进程栈上数据就已经是完全不一样了…..这里写图片描述
也就是说:fork以后父子进程拥有的函数调用栈不是同一个的!!而vfork则是同一个!!
呃。。。。然后我觉得有必然去测试一下全部变量或者是其他的……过一段时间再更新吧!!
备注:顺手看到,顺手粘贴的东西
这里写图片描述
_exit不清除IO缓存~~也就是说IO缓冲区的东西是不会输出的~

<think>好的,用户现在想了解C语言中虚拟地址的来源以及虚拟地址空间的大小。首先,我需要回顾之前提供的关于虚拟内存和内存分配机制的详细解释,确保这次回答的一致性和准确性。用户之前的问题涉及进程的独立线性空间和内存分配机制,因此需要将新问题与之前的讨论联系起来。 首先,虚拟地址的来源。根据之前的回答,虚拟地址是由操作系统为每个进程分配的,属于逻辑地址空间的一部分。需要明确说明C语言本身并不直接管理虚拟地址,而是依赖于操作系统的机制。在编程变量、指针等使用的地址都是虚拟地址,由编译器在生成可执行文件分配,并在进程创建由操作系统映射到物理内存。需要引用之前提到的例子,比如进程的独立性、fork后的写复制等案例,来支持这一点。 接下来是虚拟地址空间的大小。用户可能想知道为什么不同系统(如32位和64位)的地址空间大小不同,以及C语言程序在编译如何受到这些限制的影响。需要解释地址空间大小由CPU的位数决定,例如32位系统是4GB(2^32),而64位系统理论上更大,但实际可能受到操作系统或硬件的限制。要提到段的结构,如代码段、数据段、堆栈等,以及它们如何占据虚拟地址空间的不同区域。 另外,用户提供的引用中提到了Linux的虚拟地址空间布局,特别是NULL指针的处理和缺页异常机制。需要将这些内容整合到回答中,例如解释0地址不可访问的原因,以及缺页异常如何触发物理内存的分配。同,用户可能对malloc的行为感兴趣,比如分配大内存虚拟地址和物理内存的关系,可以结合之前的案例说明延迟分配机制。 还需要注意用户可能存在的潜在问题,例如混淆虚拟地址和物理地址,或者认为C语言直接控制虚拟地址。需要澄清这些误解,强调操作系统的作用,以及C语言程序在虚拟地址空间中的运行机制。 最后,生成相关问题,要围绕虚拟地址的来源、大小的影响因素、编程中的注意事项以及相关系统机制展开,帮助用户进一步探索。确保引用之前的引用标记,如[^1]、[^2]等,保持一致性,并正确对应到用户提供的引用内容。</think>### C语言中虚拟地址的来源与虚拟地址空间大小解析 在C语言程序中,**虚拟地址**是由操作系统通过内存管理单元(MMU)和页表机制动态生成的逻辑地址,其核心作用是为进程提供独立、连续的内存访问视图。以下是具体分析: --- #### 一、虚拟地址的来源 1. **操作系统分配机制** - **进程创建生成**:当程序启动,操作系统为其分配独立的虚拟地址空间。例如,在Linux中,可执行文件通过`execve`系统调用加载到内存,此内核为进程构建初始虚拟地址布局(包含代码段、数据段等)[^2]。 - **编译器协作**:编译器在生成可执行文件,会指定代码段、全局变量等逻辑地址的默认虚拟地址范围(如`0x08048000`为32位Linux程序的代码段起始地址)。 2. **动态内存分配的虚拟地址** - **堆内存**:通过`malloc`或`mmap`分配的虚拟地址由操作系统动态管理: ```c int *p = malloc(sizeof(int) * 100); // 返回的指针p为虚拟地址 ``` 此操作系统仅预留虚拟地址空间,物理内存按需分配[^2]。 - **栈内存**:局部变量的虚拟地址由编译器自动生成,例如: ```c void func() { int x; // x的虚拟地址由栈指针寄存器(如ESP)偏移确定 } ``` 3. **虚拟地址独立性验证** - **父子进程相同虚拟地址**: ```c #include <stdio.h> #include <unistd.h> int main() { int val = 10; if (fork() == 0) { val = 20; // 子进程修改val printf("Child: &val=%p\n", &val); // 输出相同虚拟地址 } else { sleep(1); printf("Parent: &val=%p\n", &val); // 虚拟地址相同,物理地址不同 } return 0; } ``` **输出**:父子进程的`&val`虚拟地址相同,但实际映射到不同物理页,通过**写时拷贝(Copy-On-Write)** 实现隔离[^4]。 --- #### 二、虚拟地址空间的大小 虚拟地址空间的大小由**CPU架构位数**决定,具体规则如下: | **系统类型** | **虚拟地址空间大小** | **典型应用场景** | |--------------|---------------------------|--------------------------------| | **32位系统** | $2^{32} = 4\text{GB}$ | 旧版Windows、嵌入式系统 | | **64位系统** | $2^{64} = 16\text{EB}$ | 现代服务器、个人计算机 | 1. **实际可用空间限制** - **操作系统保留区域**:例如,Windows 64位系统用户空间通常为$128\text{TB}$(低47位有效)[^1]。 - **硬件限制**:物理内存和页表层级设计会影响实际可寻址范围。 2. **地址空间布局示例(Linux x86_64)** ``` _________________________________________________________________ | 0x0000000000000000 | 保留区域(禁止访问,如NULL指针)[^1] | 0x00007f0000000000 | 用户栈(向低地址增长) | 0x00007f1234567890 | 堆(向高地址增长) | 0x0000555555554000 | 代码段(.text) | 0x00007fffffffffff | 用户空间结束 ``` 3. **编程中的实际限制** - **代码段与数据段**:编译器静态分配的全局变量和函数地址受可执行文件格式约束。 - **堆扩展上限**:`malloc`最大可申请空间受用户空间剩余虚拟地址范围限制,例如: ```c void *p = malloc(1UL << 40); // 在64位系统中可能成功,但物理内存不足实际使用会触发OOM ``` --- #### 三、虚拟地址与物理地址的映射关系 1. **页表转换机制** - **虚拟地址 → 物理地址**:通过多级页表(如x86_64的四级页表)实现转换,MMU硬件自动完成。 - **缺页异常(Page Fault)**:首次访问虚拟地址,若对应物理页未加载,操作系统触发异常并分配物理内存[^2]。 2. **验证物理内存延迟分配** ```c #include <stdio.h> #include <stdlib.h> int main() { char *p = malloc(1024 * 1024 * 1024); // 分配1GB虚拟地址空间 printf("Virtual address: %p\n", p); // 输出虚拟地址 // 实际物理内存占用接近于0 getchar(); for (int i = 0; i < 1024; i++) p[i * 4096] = 0; // 逐页触发缺页异常,分配物理内存 return 0; } ``` **现象**:程序运行后,虚拟内存占用显示1GB,但物理内存仅增长约4MB(每页4KB,访问1024页)。 --- ### 相关问题 1. **为什么64位系统的虚拟地址空间不是完整的$2^{64}$字节?** (参考:硬件页表设计和操作系统保留区域限制实际寻址范围[^1]) 2. **C语言中如何检测虚拟地址是否有效?** (参考:尝试访问地址可能触发段错误,但需依赖操作系统行为[^1]) 3. **虚拟地址空间碎片化对程序有何影响?** (参考:逻辑地址连续,物理地址可分散,不影响程序运行[^2]) 4. **如何通过C程序直接访问物理地址?** (参考:用户态程序无法直接访问,需内核驱动支持[^1])
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值