环境变量表具有全局性的原因:
环境变量表之所以具有全局性的特征,主要是因为它们是在进程上下文中维护的,并且在大多数操作系统中,当一个进程创建另一个进程(即父进程创建子进程)时,子进程会继承其父进程的环境变量。这意味着子进程将会获得与父进程相同的环境变量表,除非在创建子进程时明确地改变了某些环境变量。
这种继承机制使得环境变量在整个程序执行期间以及在派生的子进程中保持一致性和可用性。然而,尽管环境变量在一定范围内具有“全局性”,但需要注意的是,每个进程实际上都有它自己独立的一份环境变量副本。也就是说,对某个进程中的环境变量所做的更改不会影响到其他已经存在的进程。这种设计有助于避免不同进程之间的干扰,并提供一定程度的隔离,保证了进程的独立性。
写时拷贝机制:
在 Linux
系统中,当一个进程创建子进程时,子进程确实会继承父进程的环境变量表,但这种继承实际上是通过一种称为 “写时拷贝”(Copy-On-Write, COW)的优化技术来实现的:
前面虽说子进程拥有一份父进程环境变量表的拷贝副本,但这句话其实没有那么准确。实际上,若子进程不修改自己的环境变量表,则子进程和父进程会同时共用同一张环境变量表,这有利于节省内存空间与提高系统效率。
如果子进程修改了自己的环境变量,则会触发系统的写时拷贝机制,系统才会为该子进程分配新的内存页面,并将原来的和父进程共享的环境变量复制到新的内存中。这样,只有在实际需要修改的情况下,才会发生真正的复制。
更加细致的理解:只读数据一般可以被共享
在Linux系统中,环境变量通常是只读的,这意味着在多数情况下,子进程可以直接与父进程共享同一张环境变量表,而不必立即创建一份副本。只有当子进程试图修改环境变量时,系统才会触发写时拷贝(Copy-On-Write, COW)机制,为子进程分配新的内存区域,并复制必要的数据。
写时拷贝机制是不是很聪明!!:你我都不修改,就一起共用;之后谁修改了就为谁分配新的内存存放修改后的新内容。这样只有在真正修改时,才会发生真正的拷贝,大大优化了系统效率。
梳理写时拷贝机制过程
通过父进程创建子进程的例子梳理一下 写时拷贝机制的过程:
1、 创建子进程: 当一个进程通过fork()
创建子进程时,子进程继承了父进程的数据段、堆和栈等内存区域,包括环境变量表。这些内存区域在开始时是共享的,意味着它们指向相同的物理内存页面。
同时这些初始共享区域都是只读的:父进程在继承页表给子进程前,会将页表条目的权限改为只读,然后才继承该页表给子进程。
2、修改检测: 如果子进程试图修改一个共享的内存页面(包括环境变量表),即子进程向只读数据写入时,会触发系统错误:“缺页中断”。
对于子进程写入只读数据产生的缺页中断,操作系统有两种处理情况:
- 对代码区写入:不允许,杀掉该子进程
- 代码区共享,程序的代码在执行时不应该被修改,防止破坏了代码的完整性,而不允许任何一方修改
- 对数据区写入:触发写时拷贝机制,每个进程都有自己的数据副本,实现进程的数据独立性
3、写时拷贝:
- 操作系统会为子进程分配新的内存页面。
- 将原来的页面复制到新的内存页面中。
- 修改后的页面指向新的内存页面,而原来的页面仍然保留不变。
- 父进程和其他未修改的子进程仍然可以继续共享原来的内存页面。
修改的页面各自一份,未修改的部分保持不变(只读且共享)
简化步骤:方便记忆
- 保存上下文数据
- 申请内存空间(找到合适的空间,若空间不足则系统进程页面置换等操作)
- 发生拷贝(拷贝一份待修改数据到新分配的物理内存空间)
- 更新页表映射
- 恢复上下文数据(恢复程序执行)
为什么存放修改数据需要先拷贝一次原数据?
明明修改数据,直接开辟空间存放我修改后的值就好,为什么需要多一步拷贝??
解释:你的写入操作 不等于 对目标区域进行覆盖操作
某些修改程序还是需要使用到历史数据的,而不是单纯的写入新数据
如 count++
:这句代码的底层执行过程是先保存一份旧数据,其次 count+1
,最后返回原数据
父进程修改共享数据会不会写时拷贝?
- 父进程创建子进程时:无论是父进程还是子进程修改共享的内存区域(如数据段中的全局变量或静态变量),都会触发写时拷贝机制,以确保数据的一致性和避免不必要的内存复制。
- 父进程单独运行时:父进程直接修改其拥有的物理内存页面中的数据,不会触发写时拷贝机制。