< Linux >:进程地址空间

Posted 脱缰的野驴、

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了< Linux >:进程地址空间相关的知识,希望对你有一定的参考价值。

目录

一、验证进程地址空间

二、感知进程地址空间的存在


一、验证进程地址空间

我们之前学的 C/C++ 程序地址空间是物理内存吗?

        答:不是物理内存,甚至叫做程序地址空间都不太准确,应该叫做进程地址空间,因此根本就不是 C/C++ 上的概念,而是操作系统上的概念、 

在 Linux 操作系统下验证进程地址空间:

在 Winodws 操作系统下无法验证进程地址空间,原因:

1、与 Windows 操作系统本身的设置有关、

2、与 Windows 操作系统所使用的编译器也有关系,编译器会对代码地址空间做调整,防止代码被恶意猜测,比如:栈随机化策略、

[LCC@hjmlcc ~]$ ls
a.out  hjm.c  process.c  test.c
[LCC@hjmlcc ~]$ ./a.out -a -b -c
Code addr           :0x40057d
Init global addr    :0x60103c
Uninit global addr  :0x601044
Heap addr           :0x812010
Stack addr          :0x7ffc3f1db3c0
Argv addr           :0x7ffc3f1db7c6
Argv addr           :0x7ffc3f1db7ce
Argv addr           :0x7ffc3f1db7d1
Argv addr           :0x7ffc3f1db7d4
Env addr            :0x7ffc3f1db7d7
Env addr            :0x7ffc3f1db7ed
Env addr            :0x7ffc3f1db7fd
Env addr            :0x7ffc3f1db808
Env addr            :0x7ffc3f1db818
Env addr            :0x7ffc3f1dbfe6
[LCC@hjmlcc ~]$ 

验证堆区和栈区的增长方向的问题:


二、感知进程地址空间的存在

        注意:使用 Linux 操作系统提供的系统调用接口 fork 创建出当前进程的子进程,若启动当前进程,那么当前进程和当前进程的子进程启动的先后顺序是不确定的,到底是谁先谁后启动,取决于 CPU 先运行了谁,通过实验可知,一般都是父进程先被启动,子进程再被启动,但并代表父进程一定会先被启动,记住即可、

#include<stdio.h>
#include<unistd.h>
int g_val=100;
int main()

  pid_t id=fork();//不考虑创建当前进程子进程失败的情况、
  if(id == 0)
  
    //子进程
    while(1)
    
      printf("我是子进程:%d,ppid:%d,g_val:%d,&g_val:%p\\n",getpid(),getppid(),g_val,&g_val);
      sleep(1);
    
  
  else
    //父进程
    while(1)
    
      printf("我是父进程:%d,ppid:%d,g_val:%d,&g_val:%p\\n",getpid(),getppid(),g_val,&g_val);
      sleep(2);                                                                                                                
                                                         
                                     
   return 0;                                          
    

注意:此时父子进程读取到的普通全局变量是 同一个 变量、

现做如下修改:

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 //普通全局变量、
  4 int g_val=100;//以普通局部变量举例也是可以的、
  5 int main()
  6 
  7   pid_t id=fork();//不考虑创建当前进程子进程失败的情况、
  8   if(id == 0)
  9   
 10     //子进程
 11     int flag=0;
 12     while(1)
 13     
 14       printf("我是子进程:%d,ppid:%d,g_val:%d,&g_val:%p\\n",getpid(),getppid(),g_val,&g_val);
 15       sleep(1);
 16       flag++;
 17       if(flag==5)
 18       
 19         g_val=200;
 20         printf("我是子进程,普通全局变量的值已被修改,请注意\\n");
 21       
 22     
 23                                                                                                                               
 24   else
 25     //父进程
 26     while(1)
 27     
 28       printf("我是父进程:%d,ppid:%d,g_val:%d,&g_val:%p\\n",getpid(),getppid(),g_val,&g_val);
 29       sleep(2);
 30     
 31   
 32   return 0;
 33 

        由上图可知,父子进程读取到的普通全局变量的地址相同,但是读取到的普通全局变量的值不同,因此我们可知,此处普通全局变量的地址一定不是物理地址,不然肯定不会出现这种情况,虽然上述父子进程读取到的普通全局变量的地址相同,但父子进程各自读取到的普通全局变量并不是 同一个 变量,所以,此处的普通全局变量的地址一定是虚拟地址、

        我们之前在 C/C++ 中所谓的地址,都不是物理地址,都是虚拟地址;虚拟地址在 Linux 操作系统下又被称为:线性地址、逻辑地址;这三个概念在 Linux 操作系统下是一样的概念,但是不在Linux 操作系统下,是三个完全不一样的概念,这和 Linux 操作系统本身的空间布局有关、

        本节所讲的进程地址空间,本质上就是虚拟地址空间,因此本小结也可被称为:感知虚拟地址空间的存在、

        操作系统不让我们直接看到或访问物理内存的原因:不安全;内存(物理内存,不存在虚拟内存的概念,物理地址对应物理内存)是一个硬件,不能阻拦我们进行访问,只能被动的进行读取和写入;由野指针或越界等原因造成程序崩溃的情况并不是由内存(物理内存)造成的,而是由操作系统在程序与内存(物理内存)之间添加的软件层造成的、

        每一个进程在启动时,操作系统都会给其创建一个进程地址空间(虚拟地址空间),操作系统会对这若干个进程地址空间进行管理(先描述再组织);所谓的进程地址空间,其实本质上就是内核(操作系统)的一个数据结构,也存在于内存(物理内存)中,在 Linux 操作系统中,也是一个描述进程地址空间的 struct 结构体( struct mm_struct )、        

        所谓的进程地址空间其实就是操作系统通过软件的方式,给进程提供一个软件视角,使得每一个进程都认为其独占操作系统的所有资源(物理内存)、

        前面我们知道进程是具有独立性的,体现在相关的内核数据结构是独立的和进程的代码和数据是独立的;类比于一位海王同时撩三个女的(广撒网,钓大鱼),并对每一个女的说我只中意你,且画大饼说以后对你怎么怎么好……,从而使得每个女的都天真的认为我是不可替代的那个人,这个例子中,海王充当的就是操作系统OS,三个女的就是三个进程,海王画的大饼就是进程地址空间,海王画大饼的原因在于为了维护这三个女(三个进程)的独立性,互补干扰,若有交集则必然乱套、

        所谓的进程就是:进程等于加载到内存(物理内存)中的可执行程序(代码及数据)加上该进程对应的内核数据结构(该进程对应的描述该进程所使用的 struct 结构体(task_struct),其也存在于内存(物理内存)之中)的组合、

        在 Linux 内核中,每个进程都有 task_struct 结构体,该结构体中有个指针指向一个结构mm_struct (进程地址空间),我们假设磁盘上的一个可执行程序被加载到物理内存,之后,操作系统会给每一个进程构建一个页表结构(映射表),我们需要将虚拟地址空间(进程地址空间)和物理内存之间建立映射关系,这种映射关系是通过页表(映射表)的结构完成的,如下图:

// Linux 内核中的进程地址空间、
struct mm_struct

    struct vm_area_struct* mmap;  // list of VMAs
    ...
    ...


strcut vm_area_struct

    struct mm_struct* vm_mm;

    unsigned long vm_start;
    unsigned long vm_end;
    ...
    ...

磁盘中的可执行程序里是有地址的,因为,在链接阶段就是把多个目标文件和链接库通过地址链接起来,从而生成可执行程序的,因此可执行程序里面是由地址的,在该可执行程序中,包括代码区,已初始化全局数据区,未初始化全局数据区等等,这些区域在划分时,采用的不是在物理内存中的地址,采用的是相对地址(相对于整个可执行程序最开始的地址),当该可执行程序被加载到物理内存中(0x00000000 - 0xFFFFFFFF)时,

而堆区和栈区是等该可执行程序被加载到物理内存中才会存在的,

也是有区域的,在 Linux 命令行中输入:readelf -S a.out 即可查看、

转载linux内核笔记之进程地址空间

原文:linux内核笔记之进程地址空间

 

进程的地址空间由允许进程使用的全部线性地址组成,在32位系统中为0~3GB,每个进程看到的线性地址集合是不同的。

内核通过线性区的资源(数据结构)来表示线性地址区间,线性区是由起始线性地址,长度和一些访问权限来描述的。线性区的大小为页框的整数倍,起始地址为4096的整数倍。

下图展示了x86 Linux 进程的地址空间组织结构:

技术分享
  • 正文段 .text ,这是CPU执行的机器指令部分。通常正文段是共享的,而且是只读的,以防止程序修改其自身的指令。
  • 数据段 .data。数据段包含了程序中需要明确赋初值的变量。
  • 非初始化数据段 bss。bss 起始于 IBM 704汇编语言中的 Block Storage Start 指令的首字母缩写,并且沿用至今。

线性区描述符

进程地址空间中的堆、栈等,就是一个线性区,线性区的结构类型为 struct vm_area_struct :

http://lxr.free-electrons.com/source/include/linux/mm_types.h#L299
299structvm_area_struct {
300/* The first cache line has the info for VMA tree walking. */
301
302unsignedlongvm_start;/* Our start address within vm_mm. */
303unsignedlongvm_end;/* The first byte after our end address
304 within vm_mm. */
305
306/* linked list of VM areas per task, sorted by address */
307structvm_area_struct *vm_next, *vm_prev;
308
309structrb_node vm_rb;
310
311/*
312 * Largest free memory gap in bytes to the left of this VMA.
313 * Either between this VMA and vma->vm_prev, or between one of the
314 * VMAs below us in the VMA rbtree and its ->vm_prev. This helps
315 * get_unmapped_area find a free area of the right size.
316 */
317unsignedlongrb_subtree_gap;
318
319/* Second cache line starts here. */
320
321structmm_struct *vm_mm;/* The address space we belong to. */
322pgprot_tvm_page_prot;/* Access permissions of this VMA. */
323unsignedlongvm_flags;/* Flags, see mm.h. */
324
325/*
326 * For areas with an address space and backing store,
327 * linkage into the address_space->i_mmap interval tree.
328 */
329struct{
330structrb_node rb;
331unsignedlongrb_subtree_last;
332} shared;
333
334/*
335 * A file‘s MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
336 * list, after a COW of one of the file pages. A MAP_SHARED vma
337 * can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack
338 * or brk vma (with NULL file) can only be in an anon_vma list.
339 */
340structlist_head anon_vma_chain;/* Serialized by mmap_sem &
341 * page_table_lock */
342structanon_vma *anon_vma;/* Serialized by page_table_lock */
343
344/* Function pointers to deal with this struct. */
345conststructvm_operations_struct *vm_ops;
346
347/* Information about our backing store: */
348unsignedlongvm_pgoff;/* Offset (within vm_file) in PAGE_SIZE
349 units */
350structfile * vm_file;/* File we map to (can be NULL). */
351void* vm_private_data;/* was vm_pte (shared mem) */
352
353#ifndefCONFIG_MMU
354structvm_region *vm_region;/* NOMMU mapping region */
355#endif
356#ifdefCONFIG_NUMA
357structmempolicy *vm_policy;/* NUMA policy for the VMA */
358#endif
359structvm_userfaultfd_ctx vm_userfaultfd_ctx;
360};
  • vm_start :线性区的起始地址
  • vm_end :线性区的结束地址
  • vm_rb :作为红黑树中的一个节点使用
  • vm_mm :指向所在的内存描述符
  • vm_page_prot :线性区中页框的访问权限
  • vm_flags :线性区的标志
  • vm_next, vm_prev :分别指向线性区链表中的下一个和上一个线性区描述符
  • … …

内存描述符

内存描述符中包含了与进程地址空间有关的所有信息,结构类型为 struct mm_struct :

http://lxr.free-electrons.com/source/include/linux/mm_types.h#L395
395structmm_struct {
396structvm_area_struct *mmap;/* list of VMAs */
397structrb_root mm_rb;
398u32 vmacache_seqnum;/* per-thread vmacache */
399#ifdefCONFIG_MMU
400unsignedlong(*get_unmapped_area)(structfile *filp,
401unsignedlongaddr,unsignedlonglen,
402unsignedlongpgoff,unsignedlongflags);
403#endif
404unsignedlongmmap_base;/* base of mmap area */
405unsignedlongmmap_legacy_base;/* base of mmap area in bottom-up allocations */
406unsignedlongtask_size;/* size of task vm space */
407unsignedlonghighest_vm_end;/* highest vma end address */
408pgd_t* pgd;
409atomic_tmm_users;/* How many users with user space? */
410atomic_tmm_count;/* How many references to "struct mm_struct" (users count as 1) */
411atomic_long_tnr_ptes;/* PTE page table pages */
412#ifCONFIG_PGTABLE_LEVELS > 2
413atomic_long_tnr_pmds;/* PMD page table pages */
414#endif
415intmap_count;/* number of VMAs */
416
417spinlock_tpage_table_lock;/* Protects page tables and some counters */
418structrw_semaphore mmap_sem;
419
420structlist_head mmlist;/* List of maybe swapped mm‘s. These are globally strung
421 * together off init_mm.mmlist, and are protected
422 * by mmlist_lock
423 */
424
425
426unsignedlonghiwater_rss;/* High-watermark of RSS usage */
427unsignedlonghiwater_vm;/* High-water virtual memory usage */
428
429unsignedlongtotal_vm;/* Total pages mapped */
430unsignedlonglocked_vm;/* Pages that have PG_mlocked set */
431unsignedlongpinned_vm;/* Refcount permanently increased */
432unsignedlongdata_vm;/* VM_WRITE & ~VM_SHARED & ~VM_STACK */
433unsignedlongexec_vm;/* VM_EXEC & ~VM_WRITE & ~VM_STACK */
434unsignedlongstack_vm;/* VM_STACK */
435unsignedlongdef_flags;
436unsignedlongstart_code, end_code, start_data, end_data;
437unsignedlongstart_brk, brk, start_stack;
438unsignedlongarg_start, arg_end, env_start, env_end;
439
440unsignedlongsaved_auxv[AT_VECTOR_SIZE];/* for /proc/PID/auxv */
441
442/*
443 * Special counters, in some configurations protected by the
444 * page_table_lock, in other configurations by being atomic.
445 */
446structmm_rss_stat rss_stat;
447
448structlinux_binfmt *binfmt;
449
450cpumask_var_tcpu_vm_mask_var;
451
452/* Architecture-specific MM context */
453mm_context_tcontext;
454
455unsignedlongflags;/* Must use atomic bitops to access the bits */
456
457structcore_state *core_state;/* coredumping support */
458#ifdefCONFIG_AIO
459spinlock_tioctx_lock;
460structkioctx_table__rcu *ioctx_table;
461#endif
462#ifdefCONFIG_MEMCG
463/*
464 * "owner" points to a task that is regarded as the canonical
465 * user/owner of this mm. All of the following must be true in
466 * order for it to be changed:
467 *
468 * current == mm->owner
469 * current->mm != mm
470 * new_owner->mm == mm
471 * new_owner->alloc_lock is held
472 */
473structtask_struct__rcu *owner;
474#endif
475
476/* store ref to file /proc/<pid>/exe symlink points to */
477structfile__rcu *exe_file;
478#ifdefCONFIG_MMU_NOTIFIER
479structmmu_notifier_mm *mmu_notifier_mm;
480#endif
481#ifdefined(CONFIG_TRANSPARENT_HUGEPAGE) && !USE_SPLIT_PMD_PTLOCKS
482pgtable_tpmd_huge_pte;/* protected by page_table_lock */
483#endif
484#ifdefCONFIG_CPUMASK_OFFSTACK
485structcpumask cpumask_allocation;
486#endif
487#ifdefCONFIG_NUMA_BALANCING
488/*
489 * numa_next_scan is the next time that the PTEs will be marked
490 * pte_numa. NUMA hinting faults will gather statistics and migrate
491 * pages to new nodes if necessary.
492 */
493unsignedlongnuma_next_scan;
494
495/* Restart point for scanning and setting pte_numa */
496unsignedlongnuma_scan_offset;
497
498/* numa_scan_seq prevents two threads setting pte_numa */
499intnuma_scan_seq;
500#endif
501#ifdefined(CONFIG_NUMA_BALANCING) || defined(CONFIG_COMPACTION)
502/*
503 * An operation with batched TLB flushing is going on. Anything that
504 * can move process memory needs to flush the TLB when moving a
505 * PROT_NONE or PROT_NUMA mapped page.
506 */
507booltlb_flush_pending;
508#endif
509structuprobes_state uprobes_state;
510#ifdefCONFIG_X86_INTEL_MPX
511/* address of the bounds directory */
512void__user *bd_addr;
513#endif
514#ifdefCONFIG_HUGETLB_PAGE
515atomic_long_thugetlb_usage;
516#endif
517#ifdefCONFIG_MMU
518structwork_struct async_put_work;
519#endif
520};
  • mmap :线性区描述符链表中的头元素
  • mm_rb :线性区描述符所在红黑树的根
  • get_unmapped_area :在进程地址空间中搜索有效线性地址区间的方法
  • mmap_base :标识第一个分配的匿名线性区或文件内存映射的线性地址
  • task_size :进程地址空间的大小
  • highest_vm_end :能使用的最高线性地址
  • pgd :指向页全局目录
  • mm_users :次使用计数器
  • mm_count :主使用计数器
  • nr_ptes :页表项数量
  • map_count :线性区数量
  • mmlist :链接内存描述符链表中的相邻描述符
  • … …

线性区相关

进程所拥有的所有线性区通过一个简单地链表链接在一起,链表中的线性区按内存地址升序排列。内核通过进程的内存描述符的 mmap 字段找到线性区链表的第一个线性区。

技术分享

内核频繁执行的一个操作就是查找包含指定线性地址的的线性区,虽然可以通过遍历链表来查找,但是当线性区数量很庞大时,例如面向对象的数据库,此时的效率会变得非常低效。

Linux2.6把内存描述符存放在红黑树的数据结构中,当插入或删除一个线性区描述符时,内核通过红黑树搜索前后元素,并用搜索结果快速更新链表而不用扫描链表。一般来说,红黑树用来确定含有指定地址的线性区,而链表通常在扫描整个线性区集合的时候使用。

内存描述符相关

进程、内存描述符、线性区描述符、线性地址之间的关系如下:

技术分享

所有内存描述符存放在一个双向链表中,每个描述符中的 mmlist 字段存放链表中相邻元素的地址。链表的第一个元素是 init_mm 的 mmlist 字段, init_mm 是初始化阶段进程0所使用的内存描述符。

mm_users 字段存放共享 mm_struct 数据结构的轻量级进程(线程)的个数, mm_count 字段是内存描述符的主使用器, mm_users 的所有使用者在 mm_count 中只占有一个单位,也就是说多个线程只使得 mm_count 的值增加了1。假如一个内核线程使用了该内存描述符,则 mm_count 的值增加1。

对于内核线程来说,因为仅运行在内核态,所以永远不会访问低于 TASK_SIZE (3GB)的地址。每个进程的描述符中包含了两种内存描述符指针: mm 和 active_mm。对于普通进程,两者都指向进程的内存描述符,而内核线程的 active_mm 指向进程的内存描述符, mm 为null。(PS:内核线程使用的全局页表存放在主内存描述符的pgd字段中)

总结

本文简单地描述了进程地址空间中的一些主要数据结构以及之间的联系,关于源码以及更多细节内容在日后整理。

以上是关于< Linux >:进程地址空间的主要内容,如果未能解决你的问题,请参考以下文章

Linux进程概念——下验证进程地址空间的基本排布 | 理解进程地址空间 | 进程地址空间如何映射至物理内存(页表的引出) | 为什么要存在进程地址空间 | Linux2.6内核进程调度队列

Linux进程概念——下验证进程地址空间的基本排布 | 理解进程地址空间 | 进程地址空间如何映射至物理内存(页表的引出) | 为什么要存在进程地址空间 | Linux2.6内核进程调度队列

Linux进程概念——下验证进程地址空间的基本排布 | 理解进程地址空间 | 进程地址空间如何映射至物理内存(页表的引出) | 为什么要存在进程地址空间 | Linux2.6内核进程调度队列

< Linux >:进程地址空间

[ Linux ] 进程地址空间

[OS-Linux]详解Linux的进程2(进程的优先级,环境变量,程序地址空间,进程地址空间,进程调度队列)