【Linux學習】(11)進程的概念

前言

在上一章我們知道了什么是進程,并簡單了解了PCB。

本文我們將繼續深入學習進程概念相關知識點:

  1. 學習進程狀態,學會創建進程,掌握僵尸進程和孤兒進程,及其形成原因和危害
  2. 了解進程調度,Linux進程優先級,理解進程競爭性與獨立性,理解并行與并發

一、查看進程

查看進程一般有如下兩種方式:

  1. 使用ps指令查看進程
  2. 通過/proc系統目錄查看

0. 準備工作

  1. 編寫一個簡單的程序,如下:
#include<stdio.h>
#include<unistd.h>int main()
{while(1){printf("我是一個進程!\n");sleep(1);}return 0;
}
  1. 編寫Makefile
myprocess:myprocess.cgcc $^ -o $@
.PHONY:clean
clean:rm -rf myprocess
  1. 執行make命令,生成可執行程序myprocess
  2. 運行myprocess,這個時候myprocess就是一個進程

1. 使用ps指令查看進程

查看所有進程,指令如下:

ps ajx

我們一般不單獨使用ps ajx,因為它會打印所有的進程。我們一般會使用grep篩選出我們想查看的進程,指令如下:

# 篩選包含myprocess關鍵字的所有進程
ps ajx | grep myprocess

在這里插入圖片描述

問題1:為什么會出現grep的進程?——系統指令也是一個可執行程序,執行時當然也是一個進程!

問題2:如何只顯示myprocess進程,指令如下:

ps ajx | grep myprocess | grep -v grep

在這里插入圖片描述

問題3:這些列是什么意思,我們篩選進程之前先打印一下它的第一行列名稱,指令如下:

ps ajx | head -1 && ps ajx | grep myprocess

在這里插入圖片描述

  • 想要執行多條指令,可以使用&&或分號;來連接執行(注意:1. 必須都執行成功,不能只執行成功一條指令;2. 執行順序從左到右。)
  • PID:每一個進程在系統中要被管理起來,必須要有進程的id,即進程的唯一標識符(進程id并不是不變的,因為每重新啟動一次程序,重新分配進程id)
  • PPID:父進程id
  • COMMAND:這個進程運行的時執行的指令


tip:我們可以通過進程的PID,結束進程,指令如下:(ctrl+c也可以結束進程)

kill -9 PID

2. 通過/proc目錄查看進程

在Linux系統中,/proc目錄是一個非常特殊的目錄,它是一個虛擬文件系統,主要用于提供系統運行時的進程信息和內核參數。/proc目錄的內容并不是存儲在磁盤上的,而是由內核動態生成的,反映了系統當前的運行狀態。

如下圖,我們ls查看/proc目錄:

在這里插入圖片描述

如圖我們可以看到再proc中有很多文件,現在我們只關注藍色的文件。這些藍色的文件都是目錄,并且我們發現這些藍色文件的名稱基本都是數字!

這些數字是什么呢——這些數字就是當前進程的pid。

在proc目錄中默認給進程創建一個以它pid為名稱的目錄,這個目錄中保存該進程的大部分屬性!

在這里插入圖片描述

在這個目錄中保存了很多該進程的消息,如圖我們現在需要關注其中兩個消息:

  • exe:exe鏈接文件指向當前進程的可執行文件路徑
  • cwd:current work directory——當前進程的工作目錄。例如我們touch text,為什么就在當前目錄下創建了?——因為touch執行的時候也是一個進程,在進程中有cwd屬性,創建文件的時候會把cwd拼接到text前面,即cwd/text,所以touch創建的文件就在當前目錄下


注:當myprocess進程停止后,我們就不能再通過ll /proc/pid 查到它的進程

在這里插入圖片描述

小結:

  • 這里我們只需要記住proc目錄動態保存了系統中所有進程的信息。
  • touch創建文件的時候,默認在當前目錄下創建,是因為進程中有一個cwd屬性(當前進程的工作目錄)

二、通過系統調用獲取進程標識符(進程id)

  • 進程id(PID)
  • 父進程id(PPID)
  • ps ajx指令打印所有進程的相關常見屬性,其本質是遍歷task_struct雙向循環鏈表,將task_struct的相關屬性拿出來格式化打印
    在這里插入圖片描述
  • 問題:我們怎樣可以自己拿到自己的進程id?

    首先我們知道進程id存在于task_struct,而task_struct存在于操作系統中,其次我們知道操作系統不可以直接訪問,必須通過系統調用接口。所以我們必須通過系統調用接口才能訪問進程id!

1. 通過系統調用接口獲取進程id

  • getpid():系統調用接口,獲取進程id
  • getppid():系統調用接口,獲取父進程id
  • 我們可以通過man手冊查看getpid的使用說明:
man getpid

在這里插入圖片描述

getpid的使用:

  1. 代碼示例:
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>int main()
{while(1){printf("I am a process, my id is: %d, parent: %d\n", getpid(), getppid());sleep(1);}return 0;
}
  1. 監測腳本,每隔1s打印一次進程屬性,指令如下:
while : ; do ps ajx | head -1 ; ps ajx | grep myprocess | grep -v grep; echo "--------"; sleep 1; done
  1. 我們運行發現:每重新啟動一次程序,都要重新分配進程id,但是它的父進程id一直不變!(tip:每一次重新登錄xshell的時候,我們的系統會單獨再創建一個bash進程)
    在這里插入圖片描述

問題:這個父進程是什么?查看指令如下:
ps ajx | head -1 && ps ajx | grep 28363

在這里插入圖片描述

查看發現這個父進程就是bash命令行解釋器,命令行解釋器它的核心是獲取用戶的輸入,然后幫助用戶進行命令行解釋。

在命令行運行的所有指令都是進程,他們的父進程都是bash!

總結:

  • 在我們登錄xshell的時候,系統會為我們創建一個bash進程,即幫我們創建好一個命令行解釋器的進程,幫我們在顯示器中打印對話框終端
  • 我們在命令行中輸入的所有指令,最后都是bash進程的子進程
  • bash進程只負責命令行解釋,具體執行出問題只會影響它的子進程,這就是為什么在終端中我們運行的進程他們的父進程id一直不變的原因。

2. 小結

  1. 進程關系重點維護父子進程,所以沒有母親進程,爺爺進程
  2. getpid獲取自己進程的id。getppid獲取父進程id
  3. 系統調用接口的使用和使用C接口一樣,直接調用即可
  4. task_struct屬性標識符(進程id):描述本進程的唯一標識符,用來區別其他進程
  5. 我們可以根據進程id殺掉一個進程(kill -9 pid)
  6. 在終端重新啟動程序,進程id會變化,但它的父進程id一般不變
  7. 在終端執行的所有指令他們的父進程都是bash!

三、通過系統調用創建進程-fork

  • 問題:創建進程的方式有哪些?
    • bash創建進程:我們已經知道了在命令行執行指令,系統會為我們創建該指令的進程
    • 自己手動創建進程:使用系統調用接口fork,自己手動創建進程

1. 自己手動創建進程-fork

使用man手冊,查閱fork,指令如下:

man fork

在這里插入圖片描述

  • 庫函數:#include<unistd.h>
  • 函數聲明:pid_t fork(void);
  • 功能:以調用進程為模版創建一個新進程
  • 返回值:如果成功,則在父進程返回子進程的pid,并在子進程中返回0;如果失敗,則在父進程中返回-1,沒有子進程創建。

fork的使用示例:

#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>int main()
{printf("begin: 我是一個進程:pid: %d, ppid: %d\n",getpid(), getppid());//創建一個子進程pid_t id = fork();//如果創建成功,則在父進程中返回子進程的pid,并在子進程中返回0if(0 == id){//子進程while(1){printf("我是子進程,pid:%d, ppid: %d\n",getpid(), getppid());sleep(1);}}else if(id > 0){//父進程while(1){//父進程printf("我是父進程, pid: %d, ppid: %d\n",getpid(), getppid());sleep(1);}}else{//error}return 0;
}

運行結果:

在這里插入圖片描述

問題:按照我們之前所學的知識,代碼從上往下執行,如上代碼運行不應該一直打印id為0的代碼塊嗎,為什么實際運行結果是交叉打印子進程和父進程的代碼塊?

正常情況下我們的執行流是從上往下執行的,但調用fork之后,它會幫我們創建子進程,它會給子進程返回0,父進程中返回子進程id,所以fork之后代碼就變成了兩個執行流,一個執行流執行子進程的代碼,一個執行流執行父進程的代碼。

tip:fork以當前調用進程為模版創建一個子進程!

理解fork:我們需要解決以下幾個問題

問題1:fork函數,究竟在干什么?

  • fork以父進程為模版創建一個子進程
  • fork之后,父子進程代碼共享,數據不共享,采用寫時拷貝。
    在這里插入圖片描述

問題2:為什么fork要給子進程返回0,給父進程返回子進程pid?

  • 返回值為什么不同:因為fork之后的代碼父子共享,所以fork返回不同的返回值,是為了區分,讓不同的執行流,執行不同的代碼塊。
  • 為什么子進程返回0,父進程返回子進程pid:
    • 父進程可以有多個子進程,而子進程只有一個父進程。例如:在現實生活中,一般一個父親可以有多個子女,而一個子女只有一個父親。
    • 父進程將來要對子進程做控制,為了區分不同的子進程,所以在父進程中必須返回子進程的pid,用來標定子進程的唯一性。
    • 而子進程不一樣,它的父進程只有一個通過getppid就可以獲取,所以只需要返回0知道子進程創建成功即可。


問題3:一個函數是如何做到返回兩次的?

  • 一個函數在返回之前,它的核心工作一定都做完了,所以fork的return語句屬于父子進程共享的,父進程返回一次,子進程返回一次
    在這里插入圖片描述

問題4:一個變量怎么會有不同的內容?

  • 因為一、fork之后,父子進程代碼共享;二、fork返回的時候,父子進程返回值不一樣,發生寫時拷貝。所以一個變量會有不同的內容!


補充:如果父子進程被創建好,fork之后,誰先運行?

  • 誰先運行,由調度器決定,是不確定的
  • 調度器讓每個進程公平的被調度

2. 小結

  1. fork創建子進程
  2. 父子進程代碼共享,數據不共享寫時拷貝
  3. fork之后通常要用if進行分流,讓父子進程做不同的事情
  4. bash通過fork創建子進程,fork之后,父進程負責命令行解釋,子進程負責解釋指令

四、進程狀態

1. 操作系統的進程狀態

如下我們百度獲取的兩張進程狀態圖:

在這里插入圖片描述

在這里插入圖片描述

進程狀態:進程狀態反映進程執行過程的變化。這些狀態隨著進程的執行和外界條件的變化而轉換。

  • 在三態模型中,進程狀態分為三個基本狀態,即運行態,就緒態,阻塞態。
  • 在五態模型中,進程分為新建態、終止態,運行態,就緒態,阻塞態。
  • 下面我們只重點學習運行、阻塞、掛起這三個狀態

1、運行態

  • 對于操作系統,我們的任何進程運行時,都需要消耗CPU資源
  • 所有運行在系統里的進程都以task_struct雙向鏈表的形式存在操作系統里
  • 在操作系統中想要運行的進程是非常多的,而CPU資源是少的,所以進程需要去競爭CPU的資源,但調度器需要讓每一個進程都被合理的使用,所以每個CPU都需要維護一個運行隊列struct runqueue
  • 只要鏈入運行隊列的進程,所處的狀態就叫做運行態(R態)。 理解:只要處于運行隊列的進程,都表示我已經準備好了,隨時可以被調度!
  • 問題:一個進程只要把自己放到CPU上開始運行了,是不是就一直要執行完畢,才能自己放下來?

    不是,因為每一個進程都有一個叫做時間片的概念,例如一個進程最多在CPU上運行10ms,時間到了,CPU就會把進程放下來!——并發執行:在一個時間段內,所有的進程都會被執行!
  • 進程切換:在CPU上有大量的把進程拿上去、放下來的動作

2、阻塞態

  • 進程的阻塞狀態?是指進程因等待某些資源或事件而暫時無法繼續執行的狀態。例如:當進程執行I/O操作時,如果I/O設備忙,進程會進入阻塞狀態,等待I/O操作完成。
  • 阻塞態的特點:
    • 暫停執行?:處于阻塞狀態的進程會暫停執行,直到所等待的事件或資源變得可用。
    • ?隊列管理?:通常,處于阻塞狀態的進程會被排成一個隊列,有的系統會根據阻塞原因的不同將進程排成多個隊列。?
  • 每一個外設都有一個等待隊列,在等待隊列中的進程狀態我們叫做阻塞狀態
  • 在阻塞狀態下,進程無法運行

3、掛起態

  • 這是一種極端情況,通常發生在內存資源嚴重不足時,操作系統會將不常用的進程的代碼和數據移動到磁盤的Swap分區,僅保留PCB在內存中。?
  • 掛起態的換出和換入:
    • 換出:將進程的代碼和數據移動到磁盤的swap分區
    • 換入:將進程的代碼和數據從swap中移動到操作系統
  • 掛起態在保證進程的正常調度的情況下,節省了內存資源
  • 一般現在的掛起都是阻塞掛起(也有其他掛起),一般掛起了都是阻塞的,但阻塞不一定是掛起的
  • 系統中是否存在該進程,只取決于進程的PCB是否在內存中(理解:一個人是否是學生,不取決你是否在校,取決于你是否存在學校的學生系統中)

2. Linux操作系統的進程狀態

不同的進程狀態就決定了該進程當下要做什么事情。例如:

  1. 當前我是R狀態,我接下來就是要被調度運行了;
  2. 當前我是阻塞態,我接下來就要等條件就緒等設備準備好,設備準備好之后我要把我的狀態改為運行態投遞到運行隊列中,然后被CPU調度運行;
  3. 當前我是掛起態,我首先要做的就是將swap盤中的代碼和數據換入到內存


為了弄明白正在運行的進程是什么意思,我們需要知道進程的不同狀態。一個進程可以有幾個狀態(在Linux內核里,進程有時候也叫做任務)

在Linux內核源代碼中進程狀態具體分為如下幾種:

/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};

Linux狀態變化圖:

在這里插入圖片描述

2.1 R運行態

  • R運行狀態(running):R狀態就是Linux中的運行狀態,運行狀態并不意味著進程一定在運行中,它表明進程要么是在運行中要么在運行隊列里。
    • 注意:不要用你的感受去于CPU的速度做對比,因為CPU的速度太快了。
  • 例如:我們寫一個C程序死循環打印,將其運行之后,猜測它是什么狀態
#include<stdio.h>int main()
{while(1){printf("hello\n");}return 0;
}

運行該程序后,我們查看該進程狀態:

在這里插入圖片描述

以我們的感受,我們看到該進程一直在打印運行,但是查看該進程的狀態卻是S狀態,不是R狀態!這是為什么呢?

修改代碼,我們不再打印,僅僅是一個空語句死循環。此時我們再運行程序,查看進程狀態就是R狀態了。

需要一直打印的進程狀態是S,不需要打印的進程狀態是R,這是為什么——不要用我們的感受去揣測CPU的速度,因為打印是需要訪問顯示器設備的,當需要一直打印的時候他就需要頻繁的去訪問顯示器,顯示器可能并不是可以直接寫入的狀態,所以進程有很大的概率都是在等待顯示器就緒,所以進程狀態大概率是阻塞態S

tip:

  • 我們看到R+后面有一個+,這表示我們當前進程在前臺運行。(前臺:我們此時運行了的程序,我們的bash解釋器就不再運行了,輸指令沒有反應了)
  • 運行程序的時候在后面空格加&,就代表進程在后臺運行。注意:處于后臺運行的進程【ctrl + C】就不能結束進程了,只能通過kill -9 pid結束進程。

2.2 S睡眠狀態&D磁盤休眠狀態

S睡眠狀態(sleeping): 意味著進程在等待事件完成(這里的睡眠有時候也叫做可中斷睡眠(interruptible sleep))。

因為CPU的速度很快,所以大多數進程狀態都處于等待事件完成,即S態。

在Linux操作系統中的S態對應的就是操作系統中的阻塞狀態。

在Linux操作系統中阻塞狀態不僅僅有S這樣的淺度睡眠狀態,還有D這樣的深度睡眠狀態!

D磁盤休眠狀態(Disk sleep)有時候也叫不可中斷睡眠狀態(uninterruptible sleep),在這個狀態的進程通常會等待IO的結束。

淺度睡眠:可以被喚醒!——理解:隨時可以響應外部的變化。例如可以被kill -9 pid殺掉

深度睡眠:磁盤休眠,讓進程在等待磁盤寫入完畢期間,要保證這個進程不能被任何人殺掉!例如:現在有一個進程,假設他要向磁盤寫1GB數據,此時磁盤需要花時間將這1GB數據寫入,在這一段時間里進程一直在等待磁盤寫入完畢返回響應,響應寫入成功或寫入失敗。問題:假設此時OS壓力很大,內存嚴重不足,OS會殺掉一些它認為不重要的進程,這會OS發現有一個進程一直在等待磁盤寫入,自己一點事不干,就把它殺掉了。但是這會出問題了磁盤寫入數據失敗,返回響應的時候進程又被殺掉了,數據就被丟失了。那數據丟失是誰的責任,找誰負責——1、找OS?OS有管理軟硬件資源權力,并且OS是在內存嚴重不足的情況下殺掉的進程,所以數據丟失與OS無關,OS的職責就是保證系統不掛,并不是保證數據不丟失!2、找磁盤?磁盤就是一個跑腿的,磁盤的工作模式就是如此,磁盤寫數據的時候,就是需要進程等待磁盤返回響應。所以數據丟失不關磁盤,磁盤的工作模式就是如此。3、找進程?進程是被OS殺掉的,不是進程自己跑路了,不在哪里等待的,所以丟失數據也不關進程的事。

淺度睡眠,可以接收外部請求可以被殺死;深度睡眠,不接收任何外部請求不可以被殺死!

當用戶都可以查看到D狀態時,說明操作系統的壓力很大了,快要崩潰了!

2.3 停止/暫停狀態-T/t

T: 可以通過發送 SIGSTOP 信號給進程來停止(T)進程。這個被暫停的進程可以通過發送 SIGCONT 信號讓進程繼續運行。

進程中有一個概念叫做信號,在之前我們就使用過9號信號殺掉進程,我們可以通過以下指令查看進程信號有哪些:

kill -l

在這里插入圖片描述

我們可以使用19號信號暫停進程,如果想重新將暫停的進程運行起來可以使用18號信號。

在這里插入圖片描述

暫停進程及恢復進程,進程會變成后臺進程。

S休眠狀態和T暫停狀態有區別嗎——有,S一定是在等待某種資源,但T可能在等待某種資源也可能并沒有等待某種資源只是單純的控制進程暫停

gdb控制進程暫停——gdb調試

2.4 X死亡狀態

X死亡狀態(dead):這個狀態只是一個返回狀態,你不會在任務列表里看到這個狀態。因為當進程結束時,CPU會回收其資源,包括進程控制塊(PCB)和代碼數據等?

Linux中的X死亡態對應操作系統中的終止態。

2.5 Z僵尸狀態

2.5.1 僵尸進程

一個進程在死亡之后,并不會直接進入X死亡狀態,而是進入Z僵尸狀態

僵死狀態(Zombies)是一個比較特殊的狀態。當進程退出并且父進程(使用wait()系統調用,后面講)沒有讀取到子進程退出的返回代碼時就會產生僵死(尸)進程

理解:在電影之中我們看到發生命案的時候,都是首先警察來查明死因,是正常死亡還是非正常死亡,確認了死因之后才能通知家屬來把人帶走。

查明死因這個時間段所處的狀態就是僵尸狀態,確定死因進入回收所處的狀態就是死亡狀態。

僵死進程會以Z僵尸狀態保持在進程表中,并且會一直在等待父進程讀取退出狀態代碼。

所以,只要子進程退出,父進程還在運行,但父進程沒有讀取子進程狀態,子進程進入Z狀態

僵尸進程代碼示例:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>int main()
{pid_t id = fork();//子進程結束后,父進程沒有主動回收子進程信息if(id == 0){int cnt = 5;while(cnt--){printf("i am child, pid: %d, ppid: %d, cnt: %d\n",getpid(), getppid(), cnt);sleep(1);}exit(0);}else{while(1){printf("i am father, pid: %d, ppid: %d\n", getpid(), getppid());sleep(1);}}return 0;
}

監測進程狀態:

while : ; do ps ajx | head -1 ; ps ajx | grep myprocess | grep -v grep; echo "--------"; sleep 1; done

在這里插入圖片描述

進程一般退出的時候,如果父進程沒有主動回收子進程信息,子進程會一直讓自己處于Z狀態,進程的相關資源尤其是task_struct結構體不能被釋放!

2.5.2 僵尸進程的危害
  • 進程的退出狀態必須被維持下去,因為他要告訴關心它的進程(父進程),你交給我的任務,我辦的怎么樣了。可父進程如果一直不讀取,那子進程就一直處于Z狀態!
  • 維護退出狀態本身就是要用數據維護,也屬于進程基本信息,所以保存在task_struct(PCB)中,換句話說,Z狀態一直不退出,PCB一直都要維護!
  • 那一個父進程創建了很多子進程,就是不回收,就會造成內存資源的浪費!因為數據結構對象本身就要占用內存,想想C中定義一個結構體變量(對象),是要在內存的某個位置進行開辟空間!
  • 總結:如果進程一直處于僵尸狀態,那這個進程的資源會被一直占用,此時就會造成內存泄漏!

3. 孤兒進程

子進程處于僵尸狀態,殺掉父進程時,我們發現父子進程都查不到了。

問題1:父進程不僵尸狀態嗎?

  • 不是,他被bash回收了,任何的父進程只對它的子進程負責。

問題2:父進程如果提前退出,那么子進程后退出,進入Z之后,子進程如何處理呢?
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>int main()
{pid_t id = fork();//父進程提前結束,子進程后結束if(id == 0){int cnt = 100;while(cnt--){printf("我是子進程,pid:%d, ppid: %d\n", getpid(), getppid());sleep(1);}exit(0);}else{int cnt = 5;while(cnt--){printf("我是父進程,pid:%d, ppid: %d\n", getpid(), getppid());sleep(1);}}return 0;
}

在這里插入圖片描述

  • 父進程先退出,子進程就稱之為“孤兒進程”
  • 孤兒進程被1號init進程領養,當然要有init進程回收嘍。(1號進程就是我們操作系統本身)

父子進程,如果父進程先退出,子進程的父進程就會被改成1號進程(操作系統),父進程是1號進程——孤兒進程!

該進程被系統領養!


問題:為什么要被領養?
因為孤兒進程未來也會退出,也要被釋放

4. 補充:task_struct的組織

進程具有父子關系,一個父進程可以有多個子進程,所以Linux中進程PCB結構是一顆多叉樹。

Linux中對于PCB結構體的組織有多種,是怎么做的呢——在PCB結構體中添加鏈接屬性字段,例如雙向循環鏈表的組織

在這里插入圖片描述

五、進程優先級

1. 基本概念

問題1:優先級是什么?

  • 權限決定能不能,優先級是已經能了決定誰先誰后
  • 優先級:對于資源的訪問,誰先訪問,誰后訪問

問題2:為什么要存在優先級?

  • 因為資源是有限,進程是多個的,所以注定了進程之間是競爭關系!——競爭性
  • 因為操作系統必須保證大家良性競爭,所以要確認優先級
  • 如果我們進程長時間得不到CPU資源,那該進程的代碼長時間無法得到推進——進程的饑餓問題

總結:

  • CPU資源分配的先后順序,就是指進程的優先權(priority)
  • 優先權高的進程有優先執行權利。配置進程優先權對多任務環境的Linux很有用,可能改善系統性能(注:不要隨便改變進程的優先級,因為只有調度器可以最公平的幫你調度你的進程)
  • 還可以把進程運行到指定的CPU上,這樣一來,把不重要的進程安排到某個CPU,可以大大改善系統整體性能

問題3:Linux優先級是怎么做的?

  • 在運行隊列中定義了兩個類型為task_struct*的指針數組
  • 這個指針數組的下標對應著進程的優先級,我們根據進程的優先級把進程的PCB鏈入到不同的子隊列中
  • 這兩個數組一個為正在被調度的運行隊列,一個是空閑隊列。已經被調度的進程和增加的新進程就鏈入到空閑隊列中,運行隊列調度完了就交換調度空閑隊列。
  • Linux內核的O(1)調度算法:通過位圖,位圖中的每一個比特位代表一個下標,所以我們只需判斷該比特位是否為01來判斷對應子隊列是否為空,該時間復雜度為O(1)
  • 所有在運行隊列的進程都是R狀態,他們最終會根據優先級打散到我們數組的不同下標處,所以我們就可以使用數組下標的不同從上向下訪問遍歷出來的PCB就是根據優先級調度的進程,所以我們就能做到根據不同優先級先調度那個進程,所以調整優先級的本質就是把進程的PCB鏈入到運行隊列的對應下標處的子隊列

2. 查看進程優先級

  • 查看進程優先級
//查看當前終端下的進程優先級,不能查看其他終端的進程
ps -l
//攜帶-a選項才能查看用戶啟用的所有進程,這就不僅能查看當前終端,還能查看其他終端了
ps -al
//過濾其他干擾進程
ps -al | head -1 && ps -al | grep myproc

在這里插入圖片描述

  • 我們查出的進程屬性有很多,如下:
    • UID:代表執行者的身份(ls攜帶-n選項可以看到文件擁有者的UID)
    • PID:代表這個進程的代號
    • PPID:代表這個進程是由哪個進程發展衍生而來的,即父進程的代號
    • PRI(priority):代表這個進程可被執行的優先級,其值越小越早被執行
    • NI:代表這個進程的nice值,是進程優先級的修正數據

vim批量化注釋:

  • 命令模式下【ctrl+v】,左下角出現V-BLOCK
  • 【h左j下k上l右】選中區域
  • 【shift+i】進入插入模式,輸入//注釋
  • 最后【ESC】

vim取消批量化注釋:

  • 命令模式下【ctrl+v】,左下角出現V-BLOCK
  • 【h左j下k上l右】選中區域
  • 最后【d】

3. PRI 和 NI

  • PRI也還是比較好理解的,即進程的優先級,或者通俗點說就是程序被CPU執行的先后順序,此值越小進程的優先級別越高
  • 那NI呢?就是我們所要說的nice值了,其表示進程可被執行的優先級的修正數值
  • PRI值越小越快被執行,那么加入nice值后,將會使得PRI變為:PRI(new)=PRI(old)+nice
  • 這樣,當nice值為負值的時候,那么該程序將會優先級值將變小,即其優先級會變高,則其越快被執行
  • 所以,調整進程優先級,在Linux下,就是調整進程nice值
  • nice其取值范圍是-20至19,一共40個級別

小結:

  1. 進程的優先級可以被調整
  2. 雖然優先級可以被調整,但是為了每一個進程都被公平的調度,Linux不想過多的讓用戶參與優先級的調整,所以優先級的調整范圍nice為[-20,19]
  3. 進程默認的優先級為80,所以進程優先級范圍為[60,99]

注意:

  1. 普通用戶不能調整優先級
  2. 當我們的輸入nice修正值大于邊界值時取邊界值
  3. 每一次調整優先級,默認的PRI(old)都是80

4. 操作系統是如何根據優先級,開展的調度?

  • 位圖就像位運算,其中的每一個比特位都有特定的含義,用01表示不同狀態
  • 一般的位圖就是一串的01序列,用01表示不同的含義
  • 位圖可能有很多比特位,所以位圖結構一般都是結構體里面套數組
//定義具有800比特位的位圖
struct bitmap
{char bits[100];
}
int main()
{//找第N個比特位int i = N / (sizeof(char) * 8);int pos = N % (sizeof(char) * 8);bitmap b;b.bits[i] & pos;
}
  • 在前面我們知道了每一個CPU都要維護一個自己運行隊列,但并沒有講解運行隊列怎么根據不同的優先級調度進程?
    • 在這個運行隊列中我們定義兩個類型為task_struct*的指針數組,這個指針數組的大小為140,下標[0,99]是給其他種類的進程用的,有特殊用途我們不考慮,而下標[100,139]對應我們上面學習的優先級范圍[66,99]
    • 即這個指針數組不同的下標就對應進程不同的優先級,這個指針數組,我們只考慮下標[100,139]的區域
    • 所有在運行隊列的進程都是R狀態,他們最終會根據優先級打散到我們數組的不同下標處,所以我們就可以使用數組下標的不同從上向下訪問遍歷出來的PCB就是根據優先級調度的進程,所以我們就能做到根據不同優先級先調度那個進程,所以調整優先級的本質就是把進程的PCB鏈入到運行隊列的對應下標處的子隊列在這里插入圖片描述
    • 這兩個指針數組,一個為正在調度的運行隊列,一個為空閑隊列。
    • 為什么要有一個空閑隊列——因為要解決在調度運行隊列的時候,已經調度過的進程和不斷新增的進程鏈入在哪里的問題——將調度過的進程和新到的進程鏈入到空閑的隊列中
    • 一個運行隊列調度結束后再調度另一個空閑隊列——通過交換實現
    • Linux內核的O(1)調度算法:通過位圖,位圖中的每一個比特位代表一個下標,所以我們只需判斷該比特位是否為01來判斷對應子隊列是否為空,該時間復雜度為O(1)
//運行隊列
struct runqueue
{//定義一個指針數組,task_struct*指向一個存放進程PCB的地址//下標[0,99]是給其他種類的進程用的,有特殊用途我們不考慮//剛才上面我們學習了優先級的范圍是[66,99]是40個,而下標[100,139]也剛好是40,即優先級的范圍和下標一一對應[66,99] --> [100,139]//這個指針數組的下標就是進程的優先級//這個指針數組,我們只考慮下標[100,139]的區域task_struct *running[140];//問題:我們在調度運行隊列的時候,不僅要將已經調度過的隊列排到后面又會不斷地有新進程鏈入,該怎么解決呢?//1、添加一個鏡像指針數組,將調度過的進程個新到的進程鏈入到空閑的隊列中task_struct *waiting[140];//2、一個運行隊列調度結束和再調度另一個運行隊列——通過交換實現//指向running指針數組task_struct **run;//指向waiting鏡像指針數組task_struct **wait;//交換兩個指針,可以認為run一直指向運行隊列,wait一直指向空閑隊列swap(&run,&wait);//問題:判斷隊列是否為空,我們只能遍歷數組,但時間復雜度太高了,該怎么解決?//通過位圖,位圖中的每一個比特位代表一個下標,所以我們只需判斷該比特位是否為01來判斷對應子隊列是否為空,該時間復雜度為O(1)——Linux內核的O(1)調度算法!//小結:所有在運行隊列的進程都是R狀態,他們最終會根據優先級打散到我們數組的不同下標處,所以我們就可以使用數組下標的不同從上向下訪問遍歷出來的PCB就是根據優先級調度的進程,所以我們就能做到根據不同優先級先調度那個進程,所以調整優先級的本質就是把進程的PCB鏈入到運行隊列的對應下標處的子隊列

六、其他概念

競爭性:系統進程數目眾多,而CPU資源只有少量,甚至只有1個,所以進程之間是具有競爭屬性的。為了高效完成任務,更加合理競爭資源,便有了優先級。

獨立性:多進程運行,需要獨享各種資源,多進程運行期間互不干擾。

并行:多個進程在多個CPU下分別,同時運行,這稱為并行

并發:多個進程在一個CPU下采用進程切換的方式,在一段時間之內, 讓多個進程都得以推進,稱之為并發

  • 進程切換為什么我們沒有感覺到?——》不要用我們人為的感知來衡量CPU的速度,如果你都能感知到進程切換了,那CPU也太差了!
  • 一個進程被調度后,如果代碼一直沒有跑完會不會一直占用CPU資源?——》不會,因為操作系統不允許,所以每一個進程都有一個時間片的概念。
  • 并發 = 進程切換 + 時間片,即并發是基于進程切換基于時間片輪轉的調度算法。
  • 進程是如何切換的?
    • CPU中包含各種寄存器,寄存器具有對數據臨時保存的能力。例如:函數的返回值是局部變量,函數銷毀的時候也會隨之銷毀,外部能拿到函數的返回值就是通過CPU的寄存器!
    • 程序運行時,它怎么知道當前運行到了哪一行?系統如何得知我們的進程當前執行到哪一行代碼了?——》通過程序計數器(PC指針/eip指令指針):記錄當前進程正在執行指令的下一行指令的地址!
    • CPU中寄存器有很多:通用寄存器(eax,ebx,ecx,edx)、棧幀寄存器(ebp,esp,eip)、狀態寄存器(status)……
    • 為什么CPU中有這么多寄存器,他在其中扮演什么角色?——》提高效率,將進程高頻數據放到寄存器中——》CPU內寄存器保存進程相關數據!
    • 即CPU寄存器保存進程的臨時數據——進程的上下文
    • 進程在從CPU上離開的時候,要將自己的上下文數據保存好,甚至帶走!(保存的目的,就是為了未來的恢復!)
    • 進程切換其實有兩個階段:一、保存上下文;二、恢復上下文。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/91032.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/91032.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/91032.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

UniappDay04

1.登錄模塊-小程序快捷登錄定義接口&#xff0c;封裝 import { http } from /utils/httptype loginParams {code: stringencryptedData: stringiv: string } export const postLoginWxMinAPI (data: loginParams) > {return http({method: POST,url: /login/wxMin,data,})…

NPM/Yarn完全指南:前端開發的“基石“與“加速器“

開篇:當你第一次運行npm install時... "這node_modules文件夾怎么比我的項目代碼還大100倍?!" —— 每個前端新手第一次看到node_modules時的反應都出奇地一致。別擔心,今天我要帶你徹底搞懂這個讓項目"膨脹"的"罪魁禍首",以及如何用NPM/Y…

vue頁面自定義滾動條

效果圖實現思路 固定整個灰色滾動條的長度計算可滾動區域占整個可視視圖的比例&#xff0c;來確定橙色塊的長度監聽頁面滾動&#xff0c;計算橙色塊向右偏移距離 主要代碼 template&#xff1a; <div v-show"showBar" ref"barRef" class"scrollbar…

企業級JWT驗證最佳方案:StringUtils.hasText()

在企業級Java開發中&#xff0c;判斷JWT令牌是否有效的最全面且常用的方式是結合以下兩種方法&#xff1a; ? 推薦方案&#xff1a;StringUtils.hasText(jwt)&#xff08;Spring框架&#xff09; import org.springframework.util.StringUtils;if (!StringUtils.hasText(jwt))…

靈動畫布:快手可靈 AI 推出的多人協作 AI 創意工作臺

靈動畫布&#xff1a;快手可靈 AI 推出的多人協作 AI 創意工作臺 來源&#xff1a;Poixe AI 一、什么是靈動畫布 靈動畫布是快手旗下可靈 AI 于 2025 世界人工智能大會期間發布的全新創意工作臺功能。該功能集無限可視化畫布空間、多人實時協作及 AI 智能輔助于一體&#xf…

【Linux篇】進程間通信:進程IPC

目錄 共享內存空間 共享內存是在用戶空間還是內核空間&#xff1f;——用戶空間 共享內存的生命周期 如何使用共享內存 共享內存的權限 共享內存是進程間通信中&#xff0c;速度最快的方式&#xff1a; 共享內存的缺點&#xff1a; 進程間通信標準&#xff1a; system …

Kubernetes 存儲入門

目錄 Volume 的概念 Volume 的類型 通過 emptyDir 共享數據 編寫 emptyDir 的 Deployment 文件 部署該 Deployment 查看部署結果 登錄 Pod 中的第一個容器 登錄 Pod 中的第二個容器查看 /mnt 下的文件 刪除此 Pod 使用 HostPath 掛載宿主機文件 編寫 Deployment 文件…

深入理解Redission釋放鎖過程

lock.unlock();調用unlock方法&#xff0c;往下追Override public void unlock() {try {// 1. 執行異步解鎖操作并同步等待結果// - 獲取當前線程ID作為鎖持有者標識// - unlockAsync()觸發Lua腳本執行實際解鎖// - get()方法阻塞直到異步操作完成get(unlockAsync(Thread.curre…

四、計算機組成原理——第4章:指令系統

目錄 4.1指令系統 4.1.1指令集體系結構 4.1.2指令的基本格式 1.零地址指令 2.一地址指令 3.二地址指令 4.三地址指令 5.四地址指令 4.1.3定長操作碼指令格式 4.1.4擴展操作碼指令格式 4.1.5指令的操作類型 1.數據傳送 2.算術和邏輯運算 3.移位操作 4.轉移操作 …

RAG面試內容整理-檢索器與生成器的解耦架構

在RAG系統中,檢索器(Retriever)與生成器(Generator)的解耦架構是實現靈活高效的關鍵設計。所謂解耦,即將檢索相關文檔和生成答案兩個步驟分開,由不同的模塊或模型負責。這種架構帶來的直接好處是模塊獨立優化:我們可以針對檢索任務微調或更換檢索模型,而不必影響生成模…

【2026畢業論文鴻蒙系統畢設選題】最新穎的基于HarmonyOS鴻蒙畢業設計選題匯總易過的精品畢設項目分享(建議收藏)?

文章目錄前言最新畢設選題&#xff08;建議收藏起來&#xff09;最新穎的鴻蒙畢業設計選題匯總100套易過的精品畢設項目分享畢設作品推薦&#x1f447;&#x1f447;&#x1f447;文未可免費咨詢畢設相關問題&#xff0c;點贊留言可送系統源碼&#x1f447;&#x1f447;&#…

超全!Linux 面試 100 題精選解析:網絡篇|16 個 Linux 網絡排查與配置必考題詳解

網絡&#xff0c;是 Linux 系統的神經系統。 一臺服務器再強大&#xff0c;沒有網絡連接也如孤島。尤其在實際運維與面試場景中&#xff0c;“網絡相關的問題”是高頻重災區&#xff0c;比如&#xff1a; IP 配置錯亂&#xff0c;連不上公網DNS 無響應&#xff0c;域名解析失敗…

在 CentOS 上安裝 FFmpeg

在 CentOS 上安裝 FFmpeg 可以通過以下兩種推薦方法實現&#xff08;以 CentOS 7/8 為例&#xff09;&#xff1a; 方法一&#xff1a;通過 RPM Fusion 倉庫安裝&#xff08;推薦&#xff09; # 1. 安裝 EPEL 倉庫 sudo yum install epel-release# 2. 啟用 RPM Fusion 倉庫 # C…

數據結構——圖(一、圖的定義)

一、圖的定義1、什么是圖&#xff1f;圖G(V,E) 如圖&#xff0c;無向圖G頂點集V{,,...,}&#xff0c;用|V|表示圖G的頂點個數如&#xff1a;V{A,B,C,D} ,|V|4邊集E{(u,v)|uV, vV}&#xff0c; 用|E|表示圖G的邊的條數如&#xff1a;E{(u,v)|(A,B),(A,D),(A,C),(C,D)}&#xf…

Python 列表推導式與生成器表達式

Python 列表推導式與生成器表達式在 Python 中&#xff0c;列表推導式&#xff08;List Comprehension&#xff09;和生成器表達式&#xff08;Generator Expression&#xff09;是處理序列數據的高效工具。它們不僅能簡化代碼&#xff0c;還能提升數據處理的效率。本文將詳細介…

XCF32PVOG48C Xilinx Platform Flash PROM

XCF32PVOG48C 是 Xilinx 公司推出的一款高容量、低功耗的 Platform Flash PROM&#xff08;平臺閃存配置芯片&#xff09;&#xff0c;專為 Xilinx FPGA 和 CPLD 系列產品提供非易失性配置存儲支持。憑借其 32 Mbit 的大容量與出色的系統兼容性&#xff0c;該芯片成為中高端 FP…

重復文件清理工具,附免費鏈接

鏈接:https://pan.baidu.com/s/1s_Zx1eHp5Y-XnbbGldIgvw?pwdkjex 提取碼:kjex 復制這段內容后打開百度網盤手機App&#xff0c;操作更方便哦

【Spring Boot 快速入門】二、請求與響應

目錄請求響應請求Postman 工具簡單參數請求實體參數請求數組集合參數日期參數JSON 參數路徑參數響應請求響應 請求 Postman 工具 Postman 是一款功能強大的網頁調試與發送網頁 HTTP 請求的 Chrome 插件 作用&#xff1a;常用于進行接口測試 簡單參數請求 原始方式 在原始的…

高并發系統技術架構

&#xff08;點個贊&#xff0c;算法會給你推薦更多類似干貨 ~&#xff09; 口訣&#xff1a; CDN 扛靜態&#xff0c;WAF 防惡意&#xff1b;驗證碼攔機器&#xff1b; Nginx 先限流&#xff0c;Sentinel 再熔斷&#xff1b; Redis 扣庫存&#xff0c;MQ 異步寫&#xff1b; 對…

python任意模塊間采用全局字典來實現借用其他類對象的方法函數來完成任務或數據通信的功能

我們在編寫pthon代碼時&#xff0c;模塊間的數據通信主要采用以下幾種方法&#xff1a;1、采用全局變量。所有模塊都通過引用全局變量&#xff0c;通過本模塊對此全局變量數據的修改值&#xff0c;其他模塊也能訪問并得到此全局變量的當前值&#xff0c;由于全局變量的不可控性…