操作系統原理一:進程管理


進程管理

進程

進程是程序的一次執行

是一個程序及其數據在處理機上順序執行時所發生的活動

是具有獨立功能的程序在一個數據集合上的一次運行過程

是系統進行資源分配和調度的一個基本單位

PCB結構、程序和數據的集合

設備分配只針對現有進程,不會創建進程

進程的特征:

  • 動態性:進程的實質是程序的一次執行過程,因此,動態特征是進程最重要的特征
  • 並發性:沒有為之建立進程的程序是不能並發執行的,僅當為之建立一個進程后才能參加
    並發執行
  • 獨立性:進程是一個能獨立運行的基本單位,同時也是系統分配資源和調度的獨立單位
  • 異步性:由於進程間的相互制約,使進程具有執行的間斷性,即進程按各自獨立的、不可
    預知的速度向前推進
  • 結構特征:為了控制和管理進程,系統為每個進程設立一個進程控制塊--PCB

進程與程序的區別:

  • 程序是進程的靜態文本,進程是執行程序的動態過程
  • 進程與程序之間不是一一對應的,同一程序同時運行於若干不同的數據集合上,它將屬於若干個不同的進程;
    一個進程可以執行多個程序
  • 程序可作為軟件資源長期保存,進程只是一次執行過程,是暫時的
  • 進程是系統分配調度的獨立單位,能與其他進程並發執行

進程狀態及其演變

進程執行的間斷性,決定了進程可能具有多種狀態

基本狀態

運行的進程可能具有就緒、執行、阻塞三種基本狀態

進程已分配到除CPU以外的所有必要資源時,它便處於就緒狀態一旦獲得CPU,便立即執行,進入執行狀態
正在執行的進程,由於發生某個事件而暫時無法執行時,便放棄處理機而進入阻塞狀態
由於執行的進程變為阻塞狀態后,調度程序立即把處理機分配給另一個就緒進程(因此,阻塞進程的事件消失后,進程不會立即恢復到執行狀態,而轉變為就緒狀態,重新等待處理機)

創建和終止

為了管理的需要,還存在着兩種比較常見的進程狀態,即創建狀態和終止狀態

創建狀態:

引起創建的事件:

  1. 用戶登錄
  2. 作業調度:為被調度的作業創建進程
  3. 提供服務:要求打印
  4. 應用請求

創建一個進程一般要通過兩個步驟:

  • 首先,為一個新進程創建PCB,並填寫必要的管理信息;
  • 其次,把該進程轉入就緒狀態並插入就緒隊列之中。

當一個新進程被創建時,系統已為其分配了PCB,填寫了進程標識等信息,但由於該進程所必需的資源或其它信息,如主存資源尚未分配等,一般而言,此時的進程已擁有了自己的PCB,但進程自身還未進入主存,即創建工作尚未完成,進程還不能被調度運行,其所處的狀態就是創建狀態。

引入創建狀態,是為了保證進程的調度必須在創建工作完成后進行,以確保對進程控制塊操作的完整性。同時,創建狀態的引入,也增加了管理的靈活性,操作系統可以根據系統性能或主存容量的限制,推遲創建狀態進程的提交。對於處於創建狀態的進程,獲得了其所必需的資源,以及對其PCB初始化工作完成后,進程狀態便可由創建狀態轉入就緒狀態。

終止狀態:

引起終止的事件:

  1. 正常結束
  2. 異常結束
  3. 外界干預
    1. 系統管理員kill
    2. 父進程終止
    3. 父進程請求

進程的終止也要通過兩個步驟:

  • 首先等待操作系統進行善后處理
  • 然后將其PCB清零,並將PCB 空間返還系統

一個進程到達了自然結束點,或是出現了無法克服的錯誤,或是被操作系統所終結,或是被其他有終止權的進程所終結,它將進入終止狀態

進入終止態的進程以后不能再執行,但在操作系統中依然保留一個記錄,其中保存狀態碼和一些計時統計數據,供其它進程收集。一旦其它進程完成了對終止狀態進程的信息提取之后,操作系統將刪除該進程。

阻塞和喚醒

阻塞是進程自身的一種主動行為

a. 調用block原語

b. 停止執行,修改PCB進入阻塞隊列(一個或多個

喚醒由其他相關進程完成

a. wakeup原語

b. 修改PCB進入就緒隊列

掛起

為了系統和用戶觀察分析的需要,還引入了掛起操作,與掛起對應的是激活操作

當進程被掛起,便會進入靜止狀態:正在執行,便會暫停執行,處於就緒狀態則不接受調度

引入掛起狀態的原因有:

  • 終端用戶的請求:當終端用戶在自己的程序運行期間發現有可疑問題時,希望暫時使自己的程序靜止下來。亦即,使正在執行的進程暫停執行;若此時用戶進程正處於就緒狀態而未執行,則該進程暫不接受調度,以便用戶研究其執行情況或對程序進行修改。我們把這種靜止狀態稱為掛起狀態。
  • 父進程請求:有時父進程希望掛起自己的某個子進程,以便考查和修改該子進程,或者協調各子進程間的活動。
  • 負荷調節的需要:當實時系統中的工作負荷較重,已可能影響到對實時任務的控制時,可由系統把一些不重要的進程掛起,以保證系統能正常運行。
  • 操作系統的需要:操作系統有時希望掛起某些進程,以便檢查運行中的資源使用情況或進行記賬。

在引入掛起狀態后:

  • 活動就緒→靜止就緒:當進程處於未被掛起的就緒狀態時,稱此為活動就緒狀態,表示為Readya
    當用掛起原語Suspend 將該進程掛起后,該進程便轉變為靜止就緒狀態,表示為Readys,處於Readys狀態的進程不再被調度執行
  • 活動阻塞→靜止阻塞:當進程處於未被掛起的阻塞狀態時,稱它是處於活動阻塞狀態,表示為Blockeda。當用Suspend原語將它掛起后,進程便轉變為靜止阻塞狀態,表示為Blockeds。處於該狀態的進程在其所期待的事件出現后,將從靜止阻塞變為靜止就緒
  • 靜止就緒→活動就緒:處於Readys 狀態的進程,若用激活原語Active 激活后,該進程將轉變為Readya 狀態
  • 靜止阻塞→活動阻塞:處於Blockeds 狀態的進程,若用激活原語Active 激活后,該進程將轉變為Blockeda 狀態

五個進程狀態的轉換

  • NULL→創建:一個新進程產生時,該進程處於創建狀態
  • 創建→活動就緒:在當前系統的性能和內存的容量均允許的情況下,完成對進程創建的必要操作后,相應的系統進程將進程的狀態轉換為活動就緒狀態
  • 創建→靜止就緒:考慮到系統當前資源狀況和性能要求,並不分配給新建進程所需資源,主要是主存資源,相應的系統進程將進程狀態轉為靜止就緒狀態,對換到外存,不再參與調度,此時進程創建工作尚未完成
  • 執行→終止:當一個進程到達了自然結束點,或是出現了無法克服的錯誤,或是被操作系統所終結,或是被其他有終止權的進程所終結,進程即進終止狀態

PCB

為了描述和控制進程的運行,系統為每個進程定義了一個數據結構——進程控制塊PCB(Process Control Block),它是進程實體的一部分,是操作系統中最重要的記錄型數據結構

PCB 的作用是使一個在多道程序環境下不能獨立運行的程序(含數據),成為一個能獨立運行的基本單位,一個能與其它進程並發執行的進程

  1. 作為獨立運行基本單位的標志
  2. 能實現間斷性的運行方式
  3. 提供進程管理所需要的信息
  4. 提供進程調度所需要的信息
  5. 實現與其他進程的同步與通信

PCB中的信息:

  1. 進程標識符:用於惟一地標識一個進程,一個進程通常有兩種標識符:
    1. 內部標識符,在所有的操作系統中,都為每一個進程賦予了一個惟一的數字標識符,
      它通常是一個進程的序號。設置內部標識符主要是為了方便系統使用。
    2. 外部標識符,它由創建者提供,通常是由字母、數字組成,往往是由用戶(進程)在訪問該進程時使用。為了描述進程的家族關系,還應設置父進程標識及子進程標識。此外,還可設置用戶標識,以指示擁有該進程的用戶。
  2. 處理機狀態:主要是由處理機的各種寄存器中的內容組成的。處理機在運行時,許多信息都放在寄存器中。當處理機被中斷時,所有這些信息都必須保存在PCB 中,以便在該進程重新執行時,能從斷點繼續執行。這些寄存器包括
    1. 通用寄存器,又稱為用戶可視寄存器,它們是用戶程序可以訪問的,用於暫存信息,在大多數處理機中,有 8~32 個通用寄存器,在RISC 結構的計算機中可超過100 個
    2. 指令計數器,其中存放了要訪問的下一條指令的地址
    3. 程序狀態字PSW,其中含有狀態信息,如條件碼、執行方式、中斷屏蔽標志等
    4. 用戶棧指針,指每個用戶進程都有一個或若干個與之相關的系統棧,用於存放過程和系統調用參數及調用地址,棧指針指向該棧的棧頂。
  3. 進程調度信息:在 PCB中還存放一些與進程調度和進程對換有關的信息,包括
    1. 進程狀態,指明進程的當前狀態,作為進程調度和對換時的依據
    2. 進程優先級,用於描述進程使用處理機的優先級別的一個整數,優先級高的進程應優先獲得處理機
    3. 進程調度所需的其它信息,它們與所采用的進程調度算法有關,比如,進程已等待CPU的時間總和、進程已執行的時間總和等
    4. 事件,指進程由執行狀態轉變為阻塞狀態所等待發生的事件,即阻塞原因。
  4. 進程控制信息
    1. 程序和數據的地址,指進程的程序和數據所在的內存或外存地(首)址,以便再調度到該進程執行時,能從PCB中找到其程序和數據
    2. 進程同步和通信機制,指實現進程同步和進程通信時必需的機制,如消息隊列指針、信號量等,它們可能全部或部分地放在PCB 中
    3. 資源清單,即一張列出了除CPU 以外的、進程所需的全部資源及已經分配到該進程的資源的清單
    4. 鏈接指針,它給出了本進程(PCB)所在隊列中的下一個進程的PCB的首地址。

PCB的組織方式

在一個系統中,通常可擁有數十個、數百個乃至數千個PCB。為了能對它們加以有效的管理,應該用適當的方式將這些PCB組織起來。目前常用的組織方式有以下兩種。

  1. 線性方式:將系統種所有PCB都組織在一張線性表中,將該表首地址存在內存的一個專用區域
    實現簡單,開銷小,但是每次都需要掃描整張表,適合進程數目不多的系統
  2. 鏈接方式:把同一狀態的PCB鏈接成一個隊列,形成就緒隊列、若干個阻塞隊列和空白隊列等
    對其中的就緒隊列常按進程優先級的高低排列,優先級高排在隊前,此外,也可根據阻塞原因的不同而把處於阻塞狀態的進程的PCB排成等待I/O 操作完成的隊列和等待分配內存的隊列等
  3. 索引方式:系統根據所有進程的狀態建立幾張索引表,例如就緒索引表、阻塞索引表等,並把各索引表在內存的首地址記錄在內存的一些專用單元中,在每個索引表的表目中,記錄具有相應狀態的某個PCB在PCB址


進程的控制

進程控制是進程管理最基本的功能,主要包括創建新進程,終止已完成的進程,將發生異常的進程置於阻塞狀態,進程運行中的狀態轉換等

進程創建

參數:進程標識、優先級、進程起始地址、CPU初始狀態、資源需求等等

創建進程的過程

  1. 創建一個空白PCB
  2. 為新進程分配所需資源
  3. 初始化PCB
    1. 標識信息,將系統分配的標識符和父進程標識符填入新PCB
    2. 處理機狀態信息,使程序計數器指向程序入口地址,使棧指針指向棧頂
    3. 處理機控制信息,將進程設為就緒/靜止狀態,通常設為最低優先級
  4. 如果就緒隊列能接納,則插入

進程終止

進程終止的時機/時間:

  1. 正常結束
  2. 異常結束
    1. 越界錯,訪問的存儲區越出該進程的區域
    2. 保護錯,試圖訪問不允許訪問的資源,或以不適當的方式訪問(寫只讀)
    3. 非法指令,試圖執行不存在的指令(可能是程序錯誤地轉移到數據區,數據當成了指令)
    4. 特權指令出錯,用戶進程試圖執行一條只允許OS執行的指令
    5. 運行超時,執行時間超過指定的最大值
    6. 等待超時,進程等待某件事超過指定的最大值
    7. 算數運算錯,試圖執行被禁止的運算(被0除)
    8. I/O故障
  3. 外界干預
    1. 操作員或OS干預(死鎖)
    2. 父進程請求,子進程完成父進程指定的任務時
    3. 父進程終止,所有子進程都應該結束

終止過程

  1. 根據被終止進程的標識符,從PCB集合中檢索出該PCB,讀取進程狀態
  2. 若處於執行狀態:立即終止執行,置調度標志為true,指示該進程被終止后重新調度
  3. 若進程有子孫進程:將其所有子孫進程終止
  4. 全部資源還給父進程/OS
  5. PCB從所在隊列/鏈表中移出

進程阻塞

阻塞的時機/事件

  1. 請求共享資源失敗,系統無足夠資源分配
  2. 等待某種操作完成
  3. 新數據尚未到達(相互合作的進程)
  4. 等待新任務

阻塞過程:進程通過block

進程喚醒

原語wakeup,和阻塞成對使用

喚醒過程:先把被阻塞的進程從該事件阻塞隊列移出,將其PSB狀態改為就緒,再插入就緒隊列

進程同步

制約關系

  • 資源共享關系(間接制約
    • 需要互斥的訪問臨界資源
  • 相互合作關系(直接制約

臨界資源:一次只允許一個進程訪問的資源

  • 引起不可再現性是因為臨界資源沒有互斥的訪問

臨界區

while(1){
    entry;		//進入區
    critical;	//臨界區
    exit;		//退出區
}

各進程應互斥進入相關臨界區,所謂臨界區是指一段代碼,一段程序

同步機制應該遵循:

  • 空閑讓進
  • 忙則等待
  • 有限等待
  • 讓權等待:不能進入臨界區的執行進程放棄cpu執行權

整形信號量

信號量機制是一種進程間的低級通信方式

s是一個整形量,除了初始化外,僅通過兩個原子操作wait(s)和signal(s)訪問(也叫P,V操作)

wait(s){
	while(s<=0);
    s--;
}
signal(s){
	s++;
}

互斥關系:A,B共享一個緩沖區,互斥

PA(){
    while(1){
        wait(s);
        // 臨界區
    	signal(s);
        // 剩余區
    }
}

PB(){
    while(1){
        wait(s);
        // 臨界區
    	signal(s);
        // 剩余區
    }
}

main(){
	s=1;	//init
	// begin
    PA();
    PB();
    // end
}

前驅關系:P1,P2同步,P2→P1

PA(){
    P(s);
    a1;
}

PB(){
    a2;
    V(s);
}

總結:

  • 互斥的模式:PV總是成對出現在同一進程中;
  • 同步的模式:PV總是成對出現在不同進程中;
  • 前驅關系:有多少前驅關系設置多少個信號量,初值為0;有多少前驅做多少P操作,有多少后繼結點做多少V操作,無前驅不做P操作。

信號量表示的是臨界資源數
初值為2,表示初始時有2個可用的資源,若現在為-1,說明這兩個可用資源已經被占用了,而且有一個進程在等待資源
初值為3,表示初始時有3個可用的資源,若現在為1,表示有兩個資源被進程訪問了,可用資源變為1,沒有進程會等待

為了使兩個進程同步運行,至少2個信號量

例題

1.桌上有一空盤,允許存放一只水果,爸爸可向盤內放蘋果或桔子,兒子專等吃桔子,女兒專等吃蘋果

semaphore s,so,sa=1,0,0;
father(){
    while(1) {
        wait(s);
        // 將水果放入盤中;
        if(放入的是桔子)
        then signal(so);
        else signal(sa);
    } 
}
son(){
	while(1){
    	wait(so);
        // 從盤中取出桔子
        signal(s);
        // 吃桔子
    }
}
daughter() {
    while(1){
        wait(sa);
        // 從盤中取出蘋果
        signal(s);
        // 吃蘋果
    }
}
main()
{
    // cobegin
    father();
    son();
    daughter();
    // coend
}

2.某寺廟,有小、老和尚若干,由小和尚提水入缸供老和尚飲用,水缸可容10桶水,水取自同一個井中,水井窄,每次只能容一個桶取水,水桶總數3個,每次入缸取水桶僅為1桶,且不可同時進行,試給出有關取水,入水的算法

mutexj = 1;
mutexg = 1;
empty = 10;
full = 0;
count = 3;

old(){
    while(1) {
        wait(full);		//缸中有無水
        wait(count);	//有無桶
        wait(mutexg);	//取水前
        Fetch from gang;
        signal(mutexg);
        signal(count);
        signal(empty);	//通知小
    }
}

little(){
	while(1) {
        wait(empty);	//缸中有無空
        wait(count);	//有無桶
        wait(mutexj);	//取水前
        Fetch from well;
        signal(mutexj);
        wait(mutexg);	//倒水前
        pour;
        signal(mutexg);
        signal(count);
        signal(full);	//通知老
    }
}

記錄型信號量

整形信號量中S<=0就會不斷地測試,未遵循讓權等待,而是處於忙等

解決方案:建立一個進程鏈表list,連結所有等待該類資源的進程

typedef struct{
    int value;
    struct process_control_block *list
}semaphore;

wait(semaphore *S){
    S->value--;
    if(S->value < 0)
        block(S->list);
}

signal(semaphore *S){
    S->value--;
    if(S->value <= 0)
        wakeup(S->list);
}
  1. S->value>0時,表示系統中可用資源的數目
  2. 當S->value<0時,S->value的絕對值表示阻塞進程的數目
  3. 如果S->value的初值為1,表示只允許一個進程訪問臨界資源,此時的信號量轉化為互斥信號量

AND信號量

要么全分配,要么一個也不分配

不用時有可能發生死鎖

Swait(s1,s2,…,sn){ 
    while(1){ 
        if (s1>=1& …&sn >=1){ 
            for(i=1;i<=n;i++) si--; 
            break;
        }
        else{
        	//將進程放入與找到的第一個si<1的si相關的阻塞隊列中
			//並將該進程的程序計數設置為swait操作的開始
        }
	} 
}

Ssignal(s1,s2,…,sn){ 
    while(1){
        for (i=1;i<=n;i++){ 
            si++;
        	//將與si關聯的隊列中等待的所有進程都移動到就緒隊列中
        } 
    } 
}

信號量集

為提高效率而對AND信號量的擴充

允許一次申請多種資源多個

ti為分配下限值,Si>=ti則不分配,di為該進程需求值

Swait(S1, t1, d1, …, Sn, tn, dn){
	while(1){
        if(Si>=ti& … &Sn>=tn)
    		for (i=1;i<=n;i++) 
                Si=Si-di;
    	else{
            //將進程放在Si<ti的第一個Si的阻塞隊列中
            //並將該進程的程序計數設置為swait操作的開始	
    	}
    } 
} 

Ssignal(S1, d1, …, Sn, dn){
    while(1){ 
        for (i=1;i<=n;i++) {
            Si =Si+di;
        	//將與si關聯的隊列中等待的所有進程都移動到就緒隊列中
        }
    }
}

Swait(S,d,d):允許每次申請d個資源,少於d不分配

Swait(S,1,1):S>1記錄型信號量,S=1互斥形信號量

Swait(S,1,0):可控開關,S>=1時允許同時進入,S<1時不允許

經典進程同步問題

生產者-消費者*

定義數據結構

int n;
typedef item = xxx;	//產品類型
item buffer [n];	//緩沖池
int in = 0,out = 0;	
int counter = 0;	//產品數

緩沖池滿時,生產者等待;為空時,消費者等待

記錄型信號量

semaphore mutex=1,empty=n,full=0
item buffer[n]; 	//緩沖池
int in = 0,out = 0;	

int main()
{
    cobegin:
    producer();
    consumer();
    coend
}

producer(){ 
	while(1){
        …
        produce an item in nextp; 
        …
        wait(empty);
        wait(mutex);
        buffer[in]=nextp;
        in=(in+1)%n;
        signal(mutex);
        signal(full);
    } 
}

consumer(){
	while(1) {
        wait(full);
        wait(mutex);
        nextc=buffer[out];
        out=(out+1)%n;
        signal(mutex);
        signal(empty);
        consumer the item in nextc;
    } 
}

P操作的順序至關重要,順序不當可能導致死鎖(有緩沖池使用權,無緩沖)
V操作的順序無關緊要
當緩沖區只有一個時,mutex可省略

AND信號量

semaphore mutex=1,empty=n,full=0;
item buffer[n];
int in=0,out=0;
main(){
	cobegin
    producer();
    consumer();
    coend
}

producer()
{
    while(1)
    {
        ...
        produce......;
        ...
        Swait(empty,mutex);
        buffer[int]=nextp;
        in = (in+1)mod n;
        Ssignal(mutex,full);
    }
}

consumer(){
    while(1){
    	Swait(full,mutex);
        nextc=buffer[out];
        out=(out+1)%n;
        Ssignal(mutex,empty);
        consumer......;
    }
}

哲學家進餐

5個哲學家圍坐,用5只筷子吃面,筷子交替擺放

記錄型信號量

設5個信號量表示5只筷子

AND信號量

同上

讀-寫*

讀進程可共享對象,寫進程不可

設整形變量readcount讀者數wmutex讀寫互斥,rmutex互斥訪問

記錄型信號量:

semaphore rmutex=1,wmutex=1;
int readcount=0;
int main(){
    cobegin:
    reader();
    writer();
    coend;
}

reader(){
    while(1){
        wait(rmutex);
        if(readcount==0) wait(wmutex);//無人讀才能寫
        readcount++;
        signal(rmutex);
        ...
        read......
        ...
        wait(rmutex);
        readcount--;
        if(readcount==0) signal(wmutex);
        signal(rmutex);
    }
}

writer(){
    while(1){
        wait(wmutex);
        ...
        write......
        ...
        signal(wmutex);
    }
}

寫優先

semaphore rmutex=1,wmutex=1,s=1;
int readcount=0;
int main(){
    cobegin:
    reader();
    writer();
    coend;
}

reader(){
    while(1){
        wait(s);//!
        wait(rmutex);
        if(readcount==0) wait(wmutex);//無人讀
        readcount++;
        signal(rmutex);
        signal(s);//!
        ...
        read......
        ...
        wait(rmutex);
        readcount--;
        if(readcount==0) signal(wmutex);
        signal(rmutex);
    }
}

writer(){
    while(1){
        wait(s);//!
        wait(wmutex);
        ...
        write......
        ...
        signal(wmutex);
        signal(s);//!
    }
}

信號量集

#define RN 20//最大讀者數
semaphore L=RN,mx=1;

int main(){
    cobegin:
    reader();
    writer();
    coend;
}

reader(){
    while(1){
		swait(L,1,1);
        swait(mx,1,0);
        ...
        read......
        ...
        wait(rmutex);
        ssignal(L,1);
    }
}

writer(){
    while(1){
		swait(mx,1,1);
        swait(L,RN,0);
        ...
        write......
        ...
        ssignal(mx,1);
    }
}

管程

將同步操作的機制和臨界資源結合到一起,避免了要使用臨界資源的進程自備同步操作

管程:一個數據結構能為並發進程所執行的一組操作
包括:1. 局部對於管程的共享變量,2. 對該數據結構操作的一組過程,3. 對局部管程數據設初值

Monitor m_name{			//管程名
    variable declarations;	//共享變量說明
    cond declarations;		//條件變量說明
    public:					//能被進程調用的過程
    void P1(…);				//對數據結構的操作過程
    {}					
    void P2(…);				
    {}		
    ...
    void Pn(…);				
    {}
    ...
    {
		//管程主體
        //初始化代碼
    }
}

生產者-消費者

建立管程PC,包括:

  • 二過程:put(item)過程;get(item)過程;
  • 一變量:count>=n時滿,<=0時空
  • 初始值:in=out=count=0
Monitor PC{
    item buffer[N];
    int in out;
    condition notfull,notempty;
    int count;
    public:
    void put(item x){
        if(count>=N)cwait(notfull);
        buffer[in]=x;
        in=(in+1)%N;
        count++;
        csignal(notempty);
    }
    void get(item x){
        if(count<=0)cwait(notempty);
        x=buffer[out];
        out=(out+1)%N;
        count--;
        csignal(notfull);
    }
}

void producer(){
    item x;
    while(1){
        //produce
        PC.put(x);
    }
}

void consumer(){
    item x;
    while(1){
        PC.get(x);
        //consume
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM