前置知识:进程=内存可执行程序+PCB(linux中为 task_struct的结构体)
1.每个进程运行后,都会有一个进程地址空间存在(mm_struct)
(虚拟地址)
虚拟地址与真实物理内存地址之间存在一个映射表,称为页表
fork()创建子进程后,子进程会以父进程为模板,进行pcb拷贝(对子进程特征参数会进行适当改变,如pid 优先级等)。页表也会进行拷贝,当子进程进行变量修改时,会发生写时拷贝,页表中父子进程同一变量映射的真实物理地址会发生改变,因此会导致父子进程中同一变量 同一地址(int i = 0;&i得到的是虚拟地址)却显示不同的值。
2.页表
进程pcb:task_struct结构体中 存在mm_struct*指针,而struct mmmm_struct结构体中存储着栈区、堆区、静态区等各个虚拟地址的区间范围(亦即“每个进程运行后,都会有一个进程地址空间存在”的含义)。
虚拟地址存在的含义:1.有效进行进程访问内存的安全检查!!(访问字段决定虚拟地址是否应用与实际物理地址的映射关系)2. 页表的存在得以让进程与内存分配与程序执行之间进行解耦(进程只管根据虚拟地址做自己的事情,当有需要时os进行分配空间建立虚拟地址与实际内存地址之间的映射关系,亦即pcb根本不用关注实际地址空间只管做自己的事情即可)
398 struct mm_struct {
399 struct vm_area_struct * mmap; /* list of VMAs */ //虚拟地址空间结构体,双向链表包含红黑树节点访问到不能访问的区域。
400 struct rb_root mm_rb; //红黑树的根节点
401 struct vm_area_struct * mmap_cache; /* last find_vma result */ //mmap的高速缓冲器,指的是mmap最后指向的一个虚拟地址区间
402 #ifdef CONFIG_MMU
403 unsigned long (*get_unmapped_area) (struct file *filp,
404 unsigned long addr, unsigned long len,
405 unsigned long pgoff, unsigned long flags);
406 void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
407 #endif
408 unsigned long mmap_base; /* base of mmap area */ //mmap区域的基地址
409 unsigned long mmap_legacy_base; /* base of mmap area in bottom-up allocations */ //自底向上的配置
410 unsigned long task_size; /* size of task vm space */ //进程的虚拟地址空间大小
411 unsigned long cached_hole_size; /* if non-zero, the largest hole below free_area_cache */ //缓冲器的最大的大小
412 unsigned long free_area_cache; /* first hole of size cached_hole_size or larger */ //不受约束的空间大小
413 unsigned long highest_vm_end; /* highest vma end address */ //虚拟地址空间最大结尾地址
414 pgd_t * pgd; //页表的全局目录
415 atomic_t mm_users; /* How many users with user space? */ //有多少用户
416 atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */ //有多少用户引用mm_struct
417 atomic_long_t nr_ptes; /* Page table pages */ //页表
418 int map_count; /* number of VMAs */ //虚拟地址空间的个数
419
420 spinlock_t page_table_lock; /* Protects page tables and some counters */ //保护页表和用户
421 struct rw_semaphore mmap_sem; //读写信号
422
423 struct list_head mmlist; /* List of maybe swapped mm's. These are globally strung
424 * together off init_mm.mmlist, and are protected
425 * by mmlist_lock
426 */
427
428
429 unsigned long hiwater_rss; /* High-watermark of RSS usage */ //标志
430 unsigned long hiwater_vm; /* High-water virtual memory usage */
431
432 unsigned long total_vm; /* Total pages mapped */
433 unsigned long locked_vm; /* Pages that have PG_mlocked set */
434 unsigned long pinned_vm; /* Refcount permanently increased */
435 unsigned long shared_vm; /* Shared pages (files) */
436 unsigned long exec_vm; /* VM_EXEC & ~VM_WRITE */
437 unsigned long stack_vm; /* VM_GROWSUP/DOWN */
438 unsigned long def_flags;
439 unsigned long start_code, end_code, start_data, end_data; //开始代码段,结束代码。开始数据,结束数据
440 unsigned long start_brk, brk, start_stack; //堆的开始和结束。
441 unsigned long arg_start, arg_end, env_start, env_end; //参数的起始和结束,环境变量的起始和终点
442
443 unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */
...
};
3.写时拷贝(变量改变时才重新分配内存空间,不改变就不动)
第二部分提到,子进程的task_struct(pcb)会拷贝父进程的pcb,因此子进程一个变量相应的虚拟地址与父进程中虚拟地址一致,当我们试图修改该变量时,如果子进程页表对应的实际地址不进行修改,而更改了此变量对应实际地址的值,会导致父进程的该变量内容被动修改,这会违反进程独立性原则(进程之间不能相互影响,不然复杂情况下,进程被随意修改,难以追溯),我们不能让子进程影响父进程,因此我们需要重新赋予 子进程页表中该变量对应的实际内存。
那么写时拷贝如何实现呢?
系统在创建子进程进行拷贝父进程pcb之前,会将pcb中的内容改成只读,此时,子进程pcb拷贝完成时权限也是只读,当变量尽相改变时就会报错,在系统底层,我们就知道了报错的地方是需要改变的,此时将权限改成读写,进行更改就可以了。