天天看點

Linux核心剖析 之 程序位址空間(一)緒論程序位址空間記憶體描述符

    核心擷取記憶體方式——直接了當:

    1. 從分區頁框配置設定器擷取記憶體(__get_free_pages()或alloc_pages());

    2. 使用slab配置設定器為專用或通用對象配置設定記憶體(kmem_cache_alloc()或kmalloc());

    3. 使用vmalloc或vmalloc_32擷取一塊非連續記憶體區。

    如果申請的記憶體得以滿足,這些函數傳回一個頁描述符位址或線性位址。

    *核心申請記憶體使用這些簡單方法基于以下兩個原因:

    1.核心是作業系統中優先級最高的成分。如果核心申請動态記憶體,那麼必定有正當理由。是以,沒有理由推遲處理這個請求。

    2.核心信任自己。所有核心函數都是假定沒有錯誤的,是以核心函數不必插入針對程式設計錯誤的任何保護措施。

    *當給使用者态程序配置設定記憶體時,情況完全不同:

    1.程序對動态記憶體的請求被認為是不緊迫的。核心總是盡量推遲給使用者态程序配置設定動态記憶體。

    2.使用者程序是不可信任的,是以,核心必須能随時準備捕捉使用者程序引起的所有尋址錯誤。

主要内容:

    *核心如何實作對程序動态記憶體的推遲配置設定?

    ——使用一種新的資源(線性位址區)

    當使用者程序請求動态記憶體時,并沒有獲得請求的頁框,而是獲得了一個新的線性位址的使用權。這線性位址區域就成為程序位址空間的一部分。

    *為什麼要推遲配置設定?

    *程序怎樣看待動态記憶體。

    *程序位址空間的基本組成。

    *缺頁異常處理程式在推遲給程序配置設定頁框中所起的作用。

    *核心怎樣建立和删除程序的整個位址空間?

    *與程序的位址空間管理有關的api和系統調用。

====>>>>>

    1. 程序位址空間

    2. 記憶體描述符

    3. 線性區

    4. 缺頁異常處理程式

    5. 建立和删除程序的位址空間

    6. 堆的管理

    程序的位址空間(address space)由允許程序使用的全部線性位址組成。一個程序使用的程序位址空間與另一個程序使用的程序位址空間之間沒有關系。如果程序之間共享相同的位址空間,則被稱為線程。

    核心可以通過增加或删除某些線性位址區間來動态修改程序的位址空間。

    核心通過所謂線性區的資源來表示線性位址空間,線性位址空間由起始線性位址、長度以及相應通路權限來描述。

    起始位址和長度都是4096(一頁)的整數倍——提高效率。

    *程序建立新的線性區的典型情況(六種):

    程式執行

    exec()函數

    缺頁異常處理程式

    記憶體映射

    ipc共享記憶體

    malloc()函數—heap

    *與建立、删除線性區相關的系統調用:

系統調用

說明

brk()

改變程序堆的大小

execve()

裝入可執行檔案,進而改變程序的位址空間

_exit()

結束目前程序并撤銷它的程序位址空間

fork()

建立一個新的程序,并建立新的位址空間。

mmap(),mmap2()

為檔案建立一個記憶體映射,進而擴大程序的位址空間

mremap()

擴大或縮小線性區

remap_file_pages()

為檔案建立非線性映射

munmap()

撤銷對檔案的記憶體映射,進而縮小程序的位址空間

shmat()

建立一個共享線性區

shmdt()

撤銷一個共享線性區

    确定程序所擁有的線性位址區是核心的任務,這使得缺頁處理程式能夠有效的處理以下兩種無效的線性位址:

    1. 由程式設計錯誤引發的非法位址通路(如數組越界、非法指針);

    2. 由缺頁(實體頁)引發的無效線性位址,即使這個線性位址屬于程序位址空間,但是核心還未配置設定實體頁。——請求調頁。

    與程序位址空間的全部資訊都包含在一個叫做記憶體描述符(memory descriptor)的資料結構中,這個結構的類型是mm_struct,程序描述符的mm字段指向此結構。

    無論是核心線程還是使用者程序,對于核心來說,都是task_struct這個資料結構的一個執行個體,task_struct被稱為程序描述符(process descriptor),因為它記錄了這個程序所有的上下文(context)。其中有一個被稱為“記憶體描述符”(memory descriptor)的資料結構 mm_struct,該結構抽象并描述了linux視角下管理程序位址空間的所有資訊。

Linux核心剖析 之 程式位址空間(一)緒論程式位址空間記憶體描述符

    [start_code,end_code)表示代碼段的位址空間範圍。 

    [start_data,end_data)表示資料段的位址空間範圍。 

    [start_brk,brk)分别表示heap段的起始空間和目前的heap指針。 

    [start_stack,end_stack)表示stack段的位址空間範圍。 

    mmap_base表示memory mapping段的起始位址。 

    具體結構圖:

Linux核心剖析 之 程式位址空間(一)緒論程式位址空間記憶體描述符

    記憶體描述符:

    mm_strcut定義:

mm_struct字段:

類型

字段

struct vm_area_struct*

mmap

指向線性區域對象的連結清單頭

struct rb_root

mm_rb

指向線性區對象的紅黑樹的根

mmap_cache

指向最後一個引用的線性區對象

unsigned long (*) ()

get_unmapped_area

在程序位址空間中搜尋有效線性位址區

void (*) ()

unmap_area

釋放線性位址區間時調用的方法

unsigned long

free_area_cache 

核心從這個位址開始搜尋程序位址空間中線性位址的空閑區域

pgd_t *

pdg

指向頁全局目錄

atomic_t

mm_users

次使用計數器

mm_count

主使用計數器

int

map_count

線性區的個數

struct rw_semaphore

mmap_sem

線性區的讀/寫信号量

spinlock_t

page_table_lock

線性區的自旋所和頁表的自旋鎖

struct list_head

mmlist

指向記憶體描述符連結清單中的相鄰元素

start_code

可執行代碼的起始位址

end_code

可執行代碼的最後位址

start_data

已初始化資料的起始位址

end_data

已初始化資料的最後位址

start_brk

堆的起始位址

brk

堆的目前最後位址

start_stack

使用者堆棧的起始位址

arg_start

指令行參數的起始位址

arg_end

指令行參數的最後位址

env_start

環境變量的起始位址

env_end

環境變量的最後位址

rss

配置設定給程序的頁框數

anon_rss

非配給匿名記憶體映射的頁框數

total_vm

程序位址空間的大小(頁數)

locked_vm

鎖住而不能換出的頁的個數

shared_vm

共享檔案記憶體映射中的頁數

exec_vm

可執行記憶體映射中的頁數

stack_vm

使用者堆棧中的頁數

reserved_vm

在保留區中的頁數或在特殊線性區中的頁數

def_flags

線性區預設的通路标志

nr_ptes

程序的頁表數

unsigned long []

saved_auxv

開始執行elf程式時使用

unsigned int

dumpable

表示是否可以産生記憶體轉儲資訊的标志

cpumask_t

cpu_vm_mask

用于惰性tlb交換的位掩碼

mm_context_t

context

指向有關特定體系結構資訊的表

swap_token_time

程序在這個時間将有資格獲得交換标志

char

recent_pagein

最近發生了主缺頁,則設定該标志

core_waiters

正在把程序位址空間的内容轉儲到core檔案中的輕量級程序的數目

struct completion *

core_startup_done

指向建立記憶體轉儲檔案的補充原語

struct completion

core_done

rwlock_t

ioctx_list_lock

用于保護異步i/o上下文連結清單的鎖

struct kioctx *

ioctx_list

異步i/o上下文連結清單

struct kioctx

default_kioctx

預設的異步i/o上下文

hiwater_rss

程序所擁有的最大頁框數

hiwater_vm

程序線性區中的最大頁數

    所有的記憶體描述符存放在一個雙向連結清單中。每個記憶體描述符在mmlist字段存放連結清單相鄰元素的位址。連結清單的第一個元素是init_mm的mmlist字段,init_mm是初始化階段程序0使用的記憶體描述符。

    注意了解和比較兩個字段:mm_users和mm_count字段。

    # mm_users字段存放共享mm_struct資料結構的輕量級程序的個數。

    # mm_count字段是記憶體描述符的主使用計數器,在mm_users次使用計數器中的所有使用者在mm_count中隻作為一個單元。每當mm_count遞減時,核心都要檢查它是否變為0,如果是,就要解除這個記憶體描述符,因為不再有使用者使用它。

===>為什麼要設定mm_count字段?見下文。

    核心線程的記憶體描述符

    核心線程就能運作在核心态,是以,它們永遠不會通路低于task_size(等于page_offset)的位址。與普通程序相反,核心線程不用線性區。是以描述符的很多字段對核心線程是沒有意義的。

    核心線程使用前一個程序的記憶體描述符。

    在每個程序描述符中包含了兩種記憶體描述符指針:mm和active_mm。

    程序描述符中的mm字段指向程序所擁有的記憶體描述符,而active_mm字段指向程序運作時所使用的記憶體描述符。對于普通程序,這兩個字段存放相同的指針。但是,對于核心線程,由于核心線程不擁有任何記憶體描述符,是以,它們的mm字段總是null。當核心線程運作時,它的active_mm字段被初始化為前一個運作程序的active_mm值。

這裡,可以解釋上面mm_users字段和mm_count字段的差別:

<<<====>>>

    mm_users字段記錄共享該記憶體描述符的普通程序的個數,mm_count的目的在于支援核心線程級别。

對linux來說,使用者程序和核心線程(kernel thread)都是task_struct的執行個體(tsk),唯一的差別是核心線程是沒有程序位址空間的,核心線程也沒有mm記憶體描述符,是以核心線程的tsk->mm域(其中,struct task_struct * tsk)是空(null)。當核心scheduler在執行程序上下文切換(context switching)時,會根據tsk->mm判斷即将排程的程序是使用者程序還是核心線程。雖然核心線程不通路使用者程序位址空間,但是仍然需要通過page table來通路核心線程的核心位址空間。幸運的是,對于任何使用者程序來說,它們的核心空間都是完全相同的(3g-4g),是以核心可以“借用”上一個被調用的使用者程序的記憶體描述符mm中的頁表來通路核心位址,并将此mm記錄在active_mm。簡而言之,對于使用者程序,tsk->mm == tsk->active_mm;而對于核心線程,tsk->mm == null表示自己核心線程的身份,而tsk->active_mm是使用上一個使用者程序的mm,通過此mm的page table來通路核心空間。

    為了支援核心線程級别,mm_struct裡面引入了另外一個counter,主使用計數器mm_count。前面有mm_users表示這個程序位址空間被多少普通程序共享或者引用,而mm_count則表示這個位址空間被核心線程引用的次數+1(這裡的1,指的是在mm_users次使用計數器中的所有使用者在mm_count中隻作為一個單元)。核心不會因為mm_users == 0而銷毀此mm_struct,核心隻會當mm_count == 0時才會釋放mm_struct,因為這個時候既沒有使用者程序使用這個位址空間,也沒有核心線程引用這個位址空間。

繼續閱讀