1. 程式人生 > >記憶體管理的具體實現

記憶體管理的具體實現

程式與虛擬記憶體和實體記憶體的關係:

這裡寫圖片描述
結構mm_struct可以看成屬於一個程式的包括虛擬記憶體和實體記憶體的所有記憶體的控制塊;vm_area_struct裡有指向相關段虛擬地址的指標vm_end and vm_start和指向操作函式集中的函式指標vm_opst;mm_struct裡有指向實際物理空間的指標pgd。

Linux的空間分配:

這裡寫圖片描述
Linux以PAGE_OFFSET為界將4GB的虛擬記憶體空間分成了兩部分:地址0~3G這段低地址空間為使用者空間,大小為3GB;地址3G-4G這一段高地址空間為核心空間,大小為1GB。

核心空間是所有程式共享的,程式的虛擬使用者空間

是程式私有的。

Linux的程序狀態:

這裡寫圖片描述

Linux有兩類狀態:一類是使用者程序、一類是核心程序
①使用者程序即可以在使用者空間執行、也可以在核心空間執行
②核心程序只可以在核心空間執行

就緒狀態:程序已被掛入執行佇列,處於準備執行狀態,一旦獲得處理器使用權,即可進入執行狀態;
可中斷等待狀態:程序未獲得申請的資源而處於等待狀態,一旦資源有效或者有喚醒訊號就會進入就緒狀態;
不可中斷等待狀態:程序未獲得申請的資源而處於等待狀態,一旦資源有效就會進入就緒狀態,但是不能被喚醒訊號所喚醒;
停止狀態:收到SIGSTOP訊號後,由執行狀態進入停止狀態;收到SINCONT訊號時,進入就緒狀態;
終止狀態

:程序由於某種原因而終止執行,系統對他不再理睬僅僅保留其程序控制塊,這種狀態也叫“僵死”狀態;

Linux的經常控制塊:

Linux可以管理512個程序,每個程序控制塊task_struct的指標都存放在一個數組中;
task_struct中pid代表程序的pid,也就是當前程序的ID號

Linux程序的建立:
Linux中除了系統啟動之後建立的第一個程序[根程序]不是由已存在的程序建立,其餘的程序必須由已經存在的程序來建立,新建立的程序叫做子程序,而建立子程序的程序叫做父程序

函式fork()
建立一個子程序的系統呼叫叫做fork(),呼叫函式fork()的程序就是父程序,以附件為控制塊的程序就是子程序。子程序共享父程序的程式程式碼,有自己的資料區和棧區、系統堆疊區。

呼叫fork()後的程式碼會被執行兩次,但是對自己的資料區、堆疊區中變數的修改互不影響;fork()函式的返回值有-1、0、正整數,“-1”代表建立失敗,“0”代表當前程序為子程序,“正整數”代表當前程序為父程序同時其也是子程序的ID號。

getpid():獲得當前程序的ID
getppid():獲得當前父程序的ID

/*父程序a建立子程序b、c的程式碼示例*/
#include <stdio.h>
#include <sys/types.h>
int main()
{
    pid_t p1,p2;
    p1 = fork();
    if(p1 < 0)
        printf("error in fork !\n");
    else if(p1 == 0)
        printf("child process b \n");
    else
    {
        p2 = fork();
        if(p2 < 0)
            printf("error in fork!\n");
        else if(p2 == 0)
            printf("child process c\n");
        else
            printf("parent process a\n");
    }
    return 0;
}
/*如果執行程式,會發現程序的執行順序有排程器決定,與程序的建立順序無關*/

函式execu(const char *path,cahr *const argv[])
子程序呼叫execu()後,系統會立即為子程序載入執行檔案分配私有記憶體空間。呼叫execu()前,程序有自己的堆疊和資料區,但是沒有自己的程式執行空間;呼叫execu()後除了有自己的堆疊和資料區,也分配了程式執行空間。

子程序呼叫fork(),再呼叫execu()函式後,子程序和父程序不再共享任何系統資源,父程序和子程序唯一存留的關係是父程序有回收子程序的責任。

/*父程序呼叫fork()建立子程序,子程序呼叫execu()執行可執行檔案*/
Hello.c檔案:
#include <stdio.h>
#include <sys/types.h>
int main()
{
    printf("Hello!\n");
    return 0;
}

建立子程序的程式碼檔案:
#include <stdio.h>
#include <sys/types.h>
int main()
{
    pid_t pid;
    if(!(pid = fork()))
    {
        execu("./Hello.o",NULL);
    }
    else
    {
        printf("my pid is %d\n",getpid());
    }
    return 0;
}

wait()函式
按照計算機技術中誰建立誰負責的慣例,在處理父程序與子程序的關係上,那就是等待某個子程序已經退出的資訊;如果父程序得到這個資訊,父程序就會在處理子程序的“後事”之後才會繼續執行。

建立子程序的程式碼檔案:
#include <stdio.h>
#include <sys/types.h>
int main()
{
    pid_t pid;
    if(!(pid = fork()))
    {
        execu("./Hello.o",NULL);
        printf("pid %d:I am back,something is wrong!\n",getppid());
    }
    else
    {
        printf("my pid is %d\n",getpid());
        wait4(pid,NULL,0,NULL);
        for(int i=0;i<10;i++)
        {
            printf("done\n");
        }
    }
    return 0;
}

這裡寫圖片描述

系統呼叫vfork()
與fork()不同的是vfork()有自己的程式程式碼,但是沒有自己的資料區和棧區,fork()、vfork()建立的子程序都有自己的系統堆疊區;以上的特性導致呼叫vfork()之後的程式碼也會被執行兩次,但對資料區和棧區裡變數的修改會互相影響,因為子程序在呼叫execu()函式之前,與父程序使用同一個資料區和使用者堆疊[棧區].同樣也是重要的是,vfork()建立的子程序一定先於父程序執行。

Linux程序的時間片與權重引數

所有程序時間片的總和為一個排程週期,當未被阻塞的程序片都耗盡時,一個排程週期結束,然後由排程器重新分配時間片,開始下一個排程週期。

時間片的初值存放在counter中,counter反應程序時間片的剩餘情況,處理器根據counter值大的優先執行。但是在實際問題中,系統真正用來確定程序的優先權時,使用的依據為權重引數weight,weight大者程序優先執行。

weight 正比 [counter + (20 - nice)]

排程策略:
每個程序的程序控制塊中都有一個域policy來指明程序為何種程序,採用何種策略,SCHED_OTHER說明程序為普通程序,採用普通程序的排程策略。

#if HZ < 200
#define TICK_SCALE(x) ((x) >> 2)
#define NICE_TO_TICKS(nice) (TICK_SCALE(20 - (nice)) + 1)
p->count = (p->counter >> 1) + NICE_TO_TICKS(p->nice);

/*
如果使用者在檔案include/asm-i386/param.h中定義的HZ為100,counter、nice的預設值為0,於是計算結果counter的值為6ticks,即程序片的預設值大小約為60ms。 10100 >> 2 ---> 101 ---> 5 ,5+1等於6。
*/

為實時程序賦值一個遠大於普通程序的固定權重引數weight,以確保實時程序的優先權

weight 正比 [counter + (20 - nice)],這個表示式也體現出程序排程的公平性,上一個排程結束時還處於阻塞狀態的程序,在這一次排程中counter的值會加上上一次剩餘值的一半,使其獲得更高的權重weigth。

Linux檔案系統
磁碟上管理檔案的檔案、資料結構和操作構成了磁碟檔案系統,簡稱檔案系統。

檔案的儲存
通常一個檔案需要佔用多個儲存快,儲存快約為1KB

目錄項記錄檔案的名稱、儲存位置等資訊,一個記錄叫做目錄項
目錄是目錄項的集合,目錄檔案是目錄的集合

目錄檔案是系統自用檔案,也叫做特殊檔案,使用者檔案叫做普通檔案。

檔案塊的索引組織方式
這裡寫圖片描述
以檔案塊的邏輯順序號為陣列元素的下標
以檔案儲存塊的指標為陣列元素的內容
這就形成了一個檔案的索引表

索引表就是檔案塊的邏輯塊號與儲存塊號的對照表

空閒塊的記錄:
點陣圖管理方式:以資料的一個二進位制位表示對應塊的空閒狀態,0表示未佔用、1表示佔用;
連結串列管理方式:把所有儲存塊用連結串列連結起來;
分組連結串列方式:連結串列再連結成一個連結串列;

點陣圖管理方式簡單,分組連結串列方式能夠迅速的找到大量的空閒的儲存塊。

檔案目錄:
這裡寫圖片描述
把一個檔案的所有資訊都放在一個目錄項中叫做一體化目錄

這裡寫圖片描述
分立式目錄一部分只記錄檔名等使用者關係的邏輯資訊[邏輯目錄],另一部分則只記錄檔案所佔用的儲存塊數目、位置等物理資訊[檔案索引結點]。

記錄邏輯目錄的檔案叫做邏輯目錄檔案
記錄檔案索引節點的檔案叫做裝置檔案

多個邏輯目錄檔案的目錄項可以對應多個裝置檔案的檔案索引節點

為了記錄一個檔案有多少個引用,在裝置目錄中有一個引用計數的項

Ext2檔案系統:
在Linux檔案include/linux/ext2_fs.h中定義了檔案索引節點結構ext2_inode:

struct ext2_inode{
_le16 i _mode //檔案模式
_le32 i_block[15] //檔案索引表
};
這裡寫圖片描述
檔案索引表的一個元素的大小為32位,即4個位元組,若Ext2塊大小為1KB,則有256個數據項,那麼Ext2的最大容量:
1KB * 12 + 256 * 256 KB + 256 * 256 * 256KB = 16.842020GB。

虛擬檔案系統:
虛擬檔案系統的目的就是不讓使用者與實際的檔案系統見面,使使用者面對一個具有統一介面的檔案系統,隱藏實際檔案系統的操作細節。

檔案與程序的關聯:
這裡寫圖片描述

管道:

int pipe(int fildes[2]);

這裡寫圖片描述
匿名管道是在具有公共祖父的程序之間進行通訊的手段。

pipe()就是建立一個記憶體檔案,在引數中返回這個檔案的兩個描述符fildes[0]、fildes[1]。fildes[0]是一個具有”只讀“屬性的檔案描述符,fildes[1]是一個具有”只寫“屬性的檔案描述符。使得檔案像一個只能單向流通的管道一樣,一頭專門用來輸入資料,另一頭專門用來輸出資料。

#include <stdio.h>
#include <string.h>
#include <wait.h>
#include <unistd.h>
#define MAX_LINE 80
int main()
{
    int testPipe[2],ret;
    char buf[MAX_LINE + 1];
    const char * testbuf = {"主程序傳送的資料."};
    if(pipe(testPipe) == 0)   //0 正常, -1 不正常
    {
        if(fork() == 0)//子程序fork返回0
        {
            ret = read(testPipe[0],buf,MAX_LINE);//只讀
            buf[ret] = 0;
            printf("子程序讀到的資料為:%s\n",buf);
            clase(testPipe[0]);
        }
        else
        {
            ret = read(testPipe[1],testbuf,strlen(testbuf));//只寫
            ret = wait(NULL);
            clase(testPipe[1]);
        }
    }
    return 0;
}