一、馮諾依曼體系結構
我們常見的計算機,如筆記本。我們不常見的計算機,如服務器。它們大部分都遵守馮諾依曼體系。
截至目前,我們所認識的計算機,都是由一個個硬件組件組成。
- 輸入單元:鍵盤、鼠標、掃描儀、寫板等
- 中央處理器(CPU):含有運算器和控制器等
- 輸出單元:顯示器、打印機等
關于馮諾依曼,需要強調以下幾點:
- 這里的存儲器指的是內存
- 不考慮緩存情況,這里的CPU能且只能對內存進行讀寫,不能訪問外設(輸入或輸出設備)
- 外設(輸入或輸出設備)要輸入或輸出數據,也只能寫入內存或者從內存中讀取
- 總的來說,一切設備都只能直接和內存打交道
二、操作系統(Operator System)
2.1 概念
任何計算機系統都包含一個基本的程序集合,稱為操作系統(OS)。籠統的理解,操作系統包括:
- 內核(進程管理,內存管理,文件管理,驅動管理)
- 其他程序(例如函數庫,shell程序等等)
2.2 設計OS的目的
- 對下,與硬件交互,管理所有軟硬件資源
- 對上,為用戶程序(執行程序)提供一個良好的執行環境
2.3 定位
- 在整個計算機軟硬件架構中,操作系統的定位是:一款純正的“搞管理”的軟件
2.4 如何理解“管理”
- 管理的例子 - 學生,輔導員,校長
- 描述被管理對象
- 組織被管理對象
2.5 總結
計算機管理硬件
- 描述起來,用struct結構體
- 組織起來,用鏈表或者其他高效的數據結構
2.6 系統調用和庫函數的概念
- 在開發角度,操作系統對外會表現為一個整體,但是會暴露自己的部分接口,供上層開發使用,這部分由操作系統提供的接口,被稱為系統調用。
- 系統調用在使用上,功能比較基礎,對用戶的要求相對也比較高,所以,有心的開發者可以對部分系統調用進行適度封裝,從而形成庫,有了庫,就很有利于更上層用戶或者開放者進行二次開發。
2.7 承上啟下
那在還沒有學習進程之前,就問?家,操作系統是怎么管理進?進程管理的呢?很簡單,先把進程描述起來,再把進程組織起來!
三、進程
3.1 基本概念與基本操作
- 課本概念:程序的一個執行實例,正在執行的程序等
- 內核觀點:擔當分配系統資源(CPU時間,內存)的實體
- 當前:進程 = 內核數據結構(task_struct) + 自己的程序代碼和數據
3.1.1 描述進程 - PCB
基本概念
- 進程信息被放在一個叫做進程控制塊的數據結構中,可以理解為進程屬性的集合。
- 課本上稱之為PCB(process control block),Linux操作系統下的的PCB是:task_struct。
task_struct - PCB的一種
- 在Linux中描述進程的結構體叫做task_struct
- task_struct是Linux內核的一種數據結構類型,它會被裝載到RAM(內存)里并且包含進程的信息
3.1.2 task_struct
內容分類
- 標識符:描述本進程的唯一標識符,用來區別于其他進程。
- 狀態:任務狀態,退出代碼,退出信號等。
- 優先級:相對于其他進程的優先級。
- 程序計數器:程序中即將被執行的下一條指令的地址。
- 內存指針:包括程序代碼和進程相關數據的指針,還有和其他進程共享的內存塊的指針。
- 上下文數據:進程執行時處理器的寄存器中的數據。
- I / O狀態信息:包括顯示的I/O請求,分配給進程的I/O設備和被進程使用的文件列表。
- 記賬信息:可能包括處理器時間總和,使用的時鐘數總和,時間限制,記賬號等。
- 其他信息
組織進程
可以在內核源代碼里找到它,所有運行在系統里的進程都以 task_struct 雙鏈表的形式存儲在內核里。
3.1.3 查看進程
1. 進程的信息可以通過 /proc 系統文件查看
如:要獲取PID為1的進程信息,需要查看 /proc/1這個文件夾
2. 大多數進程信息同樣可以使用 top 和 ps 這些用戶級工具來獲取
3.1.4 通過系統調用獲取進程標識符
- 進程id(PID)
- 父進程id(PPID)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main()
{printf("pid: %d\n", getpid());printf("ppid: %d\n", getppid());return 0;
}
3.1.5 通過系統調用創建進程 - fork初識
1. 通過執行 man fork 認識fork
fork是Unix/Linux操作系統中的一個系統調用,用于創建當前進程的一個副本(子進程)。子進程與父進程幾乎完全相同,包括代碼、數據、堆棧等。fork調用后,父子進程從同一位置繼續執行,但擁有不同的內存空間(采用寫時拷貝)。
2. fork 有兩個返回值
- 父進程中,fork返回子進程的PID(進程ID)。
- 子進程中,fork返回0。
- 若fork失敗(如系統資源不足),返回-1。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main()
{int ret = fork();printf("hello proc : %d!, ret: %d\n", getpid(), ret);sleep(1);return 0;
}
fork之后通常使用 if 分流
#include <unistd.h>
#include <stdio.h>int main() {pid_t pid = fork();if (pid == -1) {perror("fork failed");return 1;} else if (pid == 0) {printf("This is the child process (PID: %d)\n", getpid());} else {printf("This is the parent process (PID: %d, child PID: %d)\n", getpid(), pid);}return 0;
}
3.2 進程狀態
3.2.1 Linux內核源代碼
為了弄明白正在運行的進程是什么意思,我們需要知道進程的不同的狀態。一個進程可以有以下幾個狀態(在Linux內核里,進程有時也叫做任務)。
下面的狀態在kernel源代碼里定義:
/*
*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 */
};
- R運行狀態(running):并不意味著進程一定在運行中,它表明進程要么是在運行中要么是在運行隊列里。
- S睡眠狀態(sleeping):意味著進程在等待事件完成(這里的睡眠有時候也可以叫做可中斷睡眠(interruptible sleep))。
- D磁盤休眠狀態(Disk sleep):有時候也叫做不可中斷睡眠狀態(uninterruptible sleep),在這個狀態的進程通常會等待IO的結束。
- T停止狀態(stopped):可以通過發送SIGSTOP信號給進程來停止(T)進程。這個暫停的進程可以通過發送SIGCONT信號讓進程繼續運行。
- X死亡狀態(dead):這個狀態只是一個返回狀態,不會在任務列表里看到這個狀態。
3.2.2 進程狀態查看
px aux / ps ajx 命令
- a:顯示一個終端的所有進程,包括其他用戶的進程。
- x:顯示沒有控制終端的進程,例如后臺運行的守護進程。
- j:顯示進程歸屬的進程組ID,會話ID,父進程ID,以及與作業控制相關的信息。
- u:以用戶為中心的格式顯示進程的信息,提供進程的詳細信息,如用戶,CPU和內存使用情況等。
3.2.3 Z(zombie) - 僵死進程
- 僵死狀態(Zombie)是一個比較特殊的狀態。當進程退出并且父進程(使用wait()系統調用)沒有讀取到子進程退出的返回代碼時就會產生僵死進程。
- 僵死進程會以終止狀態保存在進程表中,并且會一直等待父進程讀取退出狀態代碼。
- 所以,只要子進程退出,父進程還在運行,但是父進程沒有讀取子進程狀態,子進程進入Z狀態。
下面創建一個10s的僵死進程的例子:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main()
{pid_t id = fork();if(id < 0){perror("fork");return 1;}else if(id > 0){ //parentprintf("parent[%d] is sleeping...\n", getpid());sleep(10);}else{printf("child[%d] is begin Z...\n", getpid());exit(EXIT_SUCCESS);}return 0;
}
3.2.4 僵尸進程的危害
- 進程的退出狀態必須被維持下去,因為它要告訴關心它的進程(父進程),你交給我的任務,我辦的怎么樣了。但是父進程一直不讀取,子進程就會一直處于Z狀態。
- 維護退出狀態本身就是要用數據維護,也屬于進程基本信息,所以保存在task_struct(PCB)中。換句話說,Z狀態一直不退出,PCB就要一直維護。
- 那一個父進程創建了很多子進程,就是不回收,是不是就會造成內存資源的浪費呢?是的,因為數據結構對象本就要占用內存,就像C中定義一個結構體變量(對象),是要在內存的某個位置開辟空間。所以會造成內存泄漏
- 如何避免呢?使用下面講解的孤兒進程。
3.2.5 孤兒進程
- 如果父進程提前退出,子進程后面推出,進入Z狀態后,那該如何處理呢?
- 父進程先退出,子進程就被稱為“孤兒進程”。
- 孤兒進程被1號init/systemd進程領養,當然就會由init/systemd進程回收。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main()
{pid_t id = fork();if(id < 0){perror("fork");return 1;}else if(id > 0){ //parentprintf("parent[%d] is sleeping...\n", getpid());}else{printf("child[%d] is begin Z...\n", getpid());sleep(10);exit(EXIT_SUCCESS);}return 0;
}
3.3 進程優先級
3.3.1 基本概念
- cpu資源分配的先后順序,就是進程的優先級(priority)。
- 優先級高的進程有優先執行的權利,配置進程的優先級對多任務環境的Linux很有用,可以改善系統性能。
- 還可以把進城運行到指定的cpu上,這樣一來,把不重要的進程安排到某個cpu,可以大大改善系統整體性能。
3.3.2 查看系統進程
在Linux或者Unix系統中,用ps -l命令可以輸出類似以下幾個內容:
我們可以從中看到以下幾個重要信息:
- UID:代表執行者的身份
- PID:代表這個進程的代號
- PPID:代表這個進程是由哪個進程發展衍生而來的,即父進程的代號
- PRI:代表這個進程可被執行的優先級,其值越小越早被執行
- NI:代表這個進程的nice值
3.3.3 PRI 和 NI
- PRI也還是?較好理解的,即進程的優先級,或者通俗點說就是程序被CPU執?的先后順序,此值越?進程的優先級別越?
- 那NI呢 ? 就是我們所要說的nice值了,其表?進程可被執?的優先級的修正數值
- PRI值越?越快被執?,那么加?nice值后,將會使得PRI變為:PRI(new) = PRI(old) + nice
- 這樣,當nice值為負值的時候,那么該程序將會優先級值將變?,即其優先級會變?,則其越快被執?
- 所以,調整進程優先級,在Linux下,就是調整進程nice值
- nice其取值范圍是 - 20?19,?共40個級別。
3.3.4 PRI vs NI
- 需要強調?點的是,進程的nice值不是進程的優先級,他們不是?個概念,但是進程nice值會影響到進程的優先級變化。
- 可以理解nice值是進程優先級的修正數據
3.3.5 查看進程優先級的命令
用top命令更改已存在進程的nice值
- top
- 進入top后按 “r” -> 輸入進程pid -> 輸入nice值
注意:
- 其他調整優先級的命令:nice,renice
- 系統函數
#include <sys/time.h>
#include <sys/resource.h>
int getpriority(int which, int who);
int setpriority(int which, int who, int prio);
3.3.6 補充概念 - 競爭、獨立、并行、并發
- 競爭性: 系統進程數?眾多,?CPU資源只有少量,甚?1個,所以進程之間是具有競爭屬性的。為了?效完成任務,更合理競爭相關資源,便具有了優先級
- 獨?性 : 多進程運?,需要獨享各種資源,多進程運?期間互不?擾
- 并? : 多個進程在多個CPU下分別,同時進?運?,這稱之為并?
- 并發 : 多個進程在?個CPU下采?進程切換的?式,在?段時間之內,讓多個進程都得以推進,稱之為并發
3.4 進程切換
CPU上下?切換:其實際含義是任務切換, 或者CPU寄存器切換。當多任務內核決定運?另外的任務時, 它保存正在運?任務的當前狀態, 也就是CPU寄存器中的全部內容。這些內容被保存在任務??的堆棧中, ?棧?作完成后就把下?個將要運?的任務的當前狀況從該任務的棧中重新裝?CPU寄存器,并開始下?個任務的運?, 這?過程就是context switch。
參考?下Linux內核0.11代碼
注意:
時間片:當代計算機都是分時操作系統,每個進程都有它適合的時間片(其實就是一個計數器)。時間片到達,進程就被操作系統從CPU剝離下來。
3.5?Linux2.6內核進程O(1)調度隊列
上圖是Linux2.6內核中進程隊列的數據結構,之間關系也已經給?家畫出來,?便?家理解
3.5.1 一個cpu擁有一個runqueue
- 如果有多個CPU就要考慮進程個數的負載均衡問題
3.5.2 優先級
- 普通優先級:100?139(我們都是普通的優先級,想想nice值的取值范圍,可與之對應!)
- 實時優先級:0?99(不關?)
3.5.3 活動隊列
- 時間片還沒有結束的所有進程都按照優先級放在該隊列里
- nr_active:總共有多少個運行狀態的進程
- queue[140]:一個元素就是一個進程隊列,相同優先級的進程按照FIFO進行排隊調度,所以,數組下標就是優先級
- 從該結構中選擇一個最適合的進程,過程如下:1. 從0下標開始遍歷queue[140];2. 找到第一個非空隊列,該隊列必定為優先級最高的隊列;3. 拿到選中隊列中的第一個進程,開始運行,調度完成;4. 遍歷queue[140]時間復雜度是常數,但效率還是低了
- bitmap[5]:一共有140個優先級,一共140個進程隊列,為了提高查找非空隊列的效率,就可以用5*32個比特位來表示隊列是否為空,這樣便可以大大提高查找效率
3.5.4 過期隊列
- 過期隊列和活動隊列結構一模一樣
- 過期隊列上放置的進程,都是時間片結束的進程
- 當活動隊列上的進程都被處理完畢之后,對過期隊列上的進程進行時間片重新計算
3.5.5 active指針和expired指針
- active指針永遠指向活動隊列
- expired指針永遠指向過期隊列
- 可是活動隊列上的進程會越來越少,過期隊列上的進程會越來越多,因為進程時間片到期是一直存在的
- 在合適的時候,只要能夠交換active指針和expired指針的內容,就相當于又具有一批新的活動的進程
3.5.6 總結
- 在系統當中查找一個最合適調度的進程的時間復雜度是一個常數,不隨著進程的增加而導致時間成本增加,我們稱之為進程調度O(1)算法
struct rq {spinlock_t lock;/** nr_running and cpu_load should be in the same cacheline because* remote CPUs use both these fields when doing load calculation.*/unsigned long nr_running;unsigned long raw_weighted_load;
#ifdef CONFIG_SMPunsigned long cpu_load[3];
#endifunsigned long long nr_switches;/** This is part of a global counter where only the total sum* over all CPUs matters. A task can increase this counter on* one CPU and if it got migrated afterwards it may decrease* it on another CPU. Always updated under the runqueue lock:*/unsigned long nr_uninterruptible;unsigned long expired_timestamp;unsigned long long timestamp_last_tick;struct task_struct* curr, * idle;struct mm_struct* prev_mm;struct prio_array* active, * expired, arrays[2];int best_expired_prio;atomic_t nr_iowait;
#ifdef CONFIG_SMPstruct sched_domain* sd;/* For active balancing */int active_balance;int push_cpu;struct task_struct* migration_thread;struct list_head migration_queue;
#endif
#ifdef CONFIG_SCHEDSTATS/* latency stats */struct sched_info rq_sched_info;/* sys_sched_yield() stats */unsigned long yld_exp_empty;unsigned long yld_act_empty;unsigned long yld_both_empty;unsigned long yld_cnt;/* schedule() stats */unsigned long sched_switch;unsigned long sched_cnt;unsigned long sched_goidle;/* try_to_wake_up() stats */unsigned long ttwu_cnt;unsigned long ttwu_local;
#endifstruct lock_class_key rq_lock_key;
};
/*
* These are the runqueue data structures:
*/
struct prio_array {unsigned int nr_active;DECLARE_BITMAP(bitmap, MAX_PRIO + 1); /* include 1 bit for delimiter */struct list_head queue[MAX_PRIO];
};