操作系統重點知識匯總


目錄

操作系統理論

站在馮諾依曼角度,理解操作系統定位

管理和控制計算機硬件與軟件資源的計算機程序
馮諾伊曼(存儲程序原理)

  1. 馮諾伊曼體系的存儲器指的是內存

  2. 不考慮緩存的情況,CPU只能對內存進行操作,不能訪問外設(輸入或輸出設備)

  3. 外設(輸入輸出設備)如果想輸入輸出數據也只能寫入內存或從內存中讀取

  4. 所有設備只能直接和內存打交道

站在管理角度,理解操作系統[先描述再組織]

  1. 描述:管理軟件的軟件

  2. 組織:如何管理軟件?
    操作系統是最基本的系統軟件,它控制着計算機所有的資源並提供應用程序開發的接口

站在應用者的角度,理解操作系統

  1. 從程序員角度看,操作系統是將程序員從復雜的硬件控制中解脫出來,並為軟件開發者提供了一個虛擬機,從而能更方便的進行程序設計

  2. 從一般用戶角度看,操作系統為他們提供了一個良好的交互界面,使得他們不必了解有關硬件和系統軟件的細節,就能方便地使用計算機

站在操作系統角度,理解系統調用接口

操作系統作為系統軟件,它的任務是為用戶的應用程序提供良好的運行環境。因此,由操作系統內核提供一系列內核函數,通過一組稱為系統調用的接口提供給用戶使用。系統調用的作用是把應用程序的請求傳遞給系統內核,然后調用相應的內核函數完成所需的處理,最終將處理結果返回給應用程序。因此,系統調用是應用程序和系統內核之間的接口

站在操作系統角度,理解操作系統外殼程序定位與作用(Linux shell)

  1. 在操作系統之上提供的一套命令解釋程序叫做外殼程序(shell)

  2. 外殼程序是操作員與操作系統交互的界面,操作系統再負責完成與機器硬件的交互。

  3. 所以操作系統可成為機器硬件的外殼,shell命令解析程序可稱為操作系統的外殼。

  4. 自定義網站/動畫/圖片/flash等、可添加統計代碼、自定義限制運行時間,限制操作等、自定義公告內容、到時自動運行、設置開機啟動、隱藏執行‘、hosts修改、設置主頁

對比系統調用,理解庫函數

  • 一般而言,跟內核功能與操作系統特性緊密相關的服務,由系統調用提供;

  • 具有共通特性的功能一般需要較好的平台移植性,故而由庫函數提供。

  • 庫函數與系統調用在功能上相互補充:

    1. 如進程間通信資源的管理,進程控制等功能與平台特性和內核息息相關,必須由系統調用來實現。

    2. 文件 I/O操作等各平台都具有的共通功能一般采用庫函數,也便於跨平台移植。

  • 某些情況下,庫函數與系統調用也有交集:
    如庫函數中的I/O操作的內部實現依然需要調用系統的I/O方能實現。

  • 庫函數與系統調用主要區別:

    1. 所有 C 函數庫是相同的,而各個操作系統的系統調用是不同的。

    2. 函數庫調用是調用函數庫中的一個程序,而系統調用是調用系統內核的服務。

    3. 函數庫調用是與用戶程序相聯系,而系統調用是操作系統的一個進入點

    4. 函數庫調用是在用戶地址空間執行,而系統調用是在內核地址空間執行

    5. 函數庫調用的運行時間屬於用戶時間,而系統調用的運行時間屬於系統時間

    6. 函數庫調用屬於過程調用,開銷較小,而系統調用需要切換到內核上下文環境然后切換回來,開銷較大

    7. 在C函數庫libc中大約 300 個程序,在 UNIX 中大約有 90 個系統調用

    8. 函數庫典型的 C 函數:system, fprintf, malloc,而典型的系統調用:chdir, fork, write, brk

進程基本概念(重點)

進程概念(PCB[task_struct])

基本概念:程序執行的一個實例,正在運行的程序
基於內核:但當分配系統資源(CPU時間、內存)的實體
基於PCB(Linux下稱為task_struct):task_struct是一種數據結構,他被裝載到RAM里並包含進程信息(信息如下)
1. 標示符:區別和其他進程的唯一標識符號
2. 狀態:任務狀態、退出代碼、退出信號
3. 優先級:相對其他進程的優先級
4. 程序計數器:程序中即將被執行的下一條指令
5. 內存指針:包括程序代碼和進程相關的數據指針,還有和其他進程共享內存的指針
6. 上下文數據:進程執行時處理器的寄存器中的數據
7. I/O狀態信息:包括I/O顯示的請求、分配給I/O的設備、被進程使用的文件列表
8. 記賬信息:可能包括處理器時間總和,使用的時鍾數總和,時間限制,記賬號

進程和程序有什么區別

  • 進程是程序的一次執行過程,是動態概念,程序是一組有序的指令集和,是靜態概念

  • 進程是暫時的,是程序在數據集上的一次執行,可創建可撤銷,程序是永存的

  • 進程具有並發行,程序沒有

  • 進程是競爭計算機資源的最小單位,程序不是

  • 進程與程序不是一一對應,多個進程可執行一個程序,一個程序可執行多個程序

進程標識,進程間關系

  • 進程標識:進程ID,簡稱PID,是大多數操作系統內核用於唯一標識進程的數值

    1. PID的數值是非負整數

    2. 每個進程都有唯一的一個PID

    3. PID可以簡單地表示為主進程表中的一個索引

    4. 當某一進程終止后,其PID可以作為另一個進程的PID

    5. 調度進程的PID固定為0,他按一定原則把處理機分配給進程使用

    6. 初始化進程的PID固定為1,他是Linux系統中其他進程的祖先,是進程的最終控制者

    7. 每個進程都有六個重要的ID:進程ID,父進程ID,有效用戶ID,有效組ID,實際用戶ID,實際組ID

    獲取各類ID的函數
    #include<sys/types.h>
    #include<unistd.h>
    void getpid(void)//返回值:進程ID
    void getppid(void)//返回值:父進程ID
    void getpid(void)//返回值:進程ID
    void getuid(void)//返回值:實際用戶進程ID
    void geteuid(void)//返回值:有效用戶進程ID
    void getgid(void)//返回值:實際組進程ID
    void getegid(void)//返回值:有效組進程ID
    
    

進程狀態

進程在運行中的幾種運行狀態
  • 創建狀態:進程在創建是需要需要申請一個新的PCB,並將控制和管理進程的信息放在PCB里面,從而完成資源分配。如果創建工作無法完成(比如資源無法滿足)就無法被調度運行。

  • 就緒狀態:進程已經准備好了,已經分配到所需資源,只要分配到PCB上就能立即運行。(此狀態允許父進程終止子進程)

  • 執行狀態:處於就緒狀態的進程被調度后就進入到執行狀態

  • 阻塞狀態:正在執行的程序由於受到某些事件(IO請求/申請緩存區失敗)的影響而暫時無法運行,進程受到阻塞。在滿足條件時進程進入就緒狀態等待被調度。(此狀態允許父進程終止子進程)

  • 終止狀態:進程結束,或出現錯誤,或被系統終止,進入終止狀態,無法再執行

-c

Linux下的進程狀態(R、S、D、T、Z、X)
  • R(可執行)狀態:並比意味着進程一定在運行中,它表明要么在運行中,要么在運行隊列里面

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

  • D(不可中斷睡眠)狀態:在這個狀態的進程通常會等待IO結束

  • T(暫停)狀態:可以發送信號SIGSTOP給進程從而來停止進程,這個進程可以通過SIGCONT信號讓進程繼續運行

  • Z(僵屍)狀態:該進程在其父進程沒有讀取到子進程退出返回的代碼時就退出了,該進程就進入了僵屍狀態。僵屍狀態會以終止的狀態保持在進程表中,並一直等待父進程讀取退出狀態碼

  • X(死亡/退出)狀態:這個狀態只是一個返回狀態,在任務列表中看不到這個狀態

進程優先級

  • CPU資源分配的先后順序,就是進程的優先權

  • 優先權高的進程先執行,配置優先權對多任務的Linux環境很有用,可以提高系統的性能

  • 還可以把某個進程指定到某個CPU上,可以提高系統整體的性能

進程創建

fork函數
#include<unistd.h>
pid_t fork(void)

返回值:自進程返回0,父進程返回子進程ID,出錯返回-1。
  • 進程調用fork函數時,當控制轉移到內核中fork代碼后,內核做的事情有:

    1. 分配新的數據塊和數據結構給子進程

    2. 將父進程的數據結構的部分內容拷貝到子進程中

    3. 將子進程添加到系統進程列表

    4. fork返回。開始調度器調度

  • fork之前,父進程獨立執行,fork之后,父子進程兩個執行流分別執行,誰先誰后不確定,完全由調度器決定的。

  • 通常情況下,父子代碼共享,父子在不寫入的情況下都是共享的。當有一方試圖寫入時,便以寫實拷貝各自一份副本進行寫入。

  • 錯誤碼EAGAIN表示達到進程數上線,ENOMEM表示沒有足夠空間給一個新進程分配

  • 所有由父進程打開的文件描述符都被復制到子進程中,父子進程中相同編號的文件描述符在內核中指向同一個file結構體,也就是說file結構體的引用計數要增加

  • fork常用的場景:

    1. 父進程希望復制自己,父子進程同時執行不同代碼段(比如:父進程等待客戶端請求,生成子進程來處理請求)

    2. 一個進程要執行不同的程序(比如:子進程從fork返回后調用exec函數)

vfork函數
#include<unistd.h>
pid_t vfork(void)

返回值:自進程返回0,父進程返回子進程ID,出錯返回-1。
  • 產生一個子進程,但父子進程共享數據段(共享地址空間)

  • vfork保證子進程先運行,等到子進程調用exec或者exit之后父進程才開始執行

  • 如果在調用exec或exit之前,子進程依賴於父進程進一步動作,則會造成死鎖

  • 改變子進程變量的值,也是的父進程中的值發生改變,如果想改變共享數據段中的變量值,應該先拷貝父進程

fork和vfork的區別
  • fork產生子進程不共享地址空間,vfork產生子進程共享地址空間

  • fork不阻塞,父子進程可以同時執行,vfork阻塞,父進程要等待子進程執行完才能執行

  • fork后子進程和父進程的執行順序不一定,vfork后子進程先執行,待到子進程退出后父進程才開始執行

進程等待

  • 為什么要等待?

    1.子進程退出,如果父進程不管不顧,可能造成僵屍問題,造成內存泄漏

    2.一旦變成僵屍狀態,kill -9都無能為力,因為沒有誰可以殺死一個死去了的進程

    3.父進程需要知道子進程完成任務的情況(對錯與否,有沒有異常退出等)

    4.父進程需要通過進程等待的方式回收子進程的資源,獲取退出信息

  • 怎么等待?(兩個接口函數)

    wait系統調用

    #include<sys/types.h>
    #include<sys/wait.h>
    pid_t wait(int* status)
    

    參數:status輸出型參數,整型指針,指向的空間存放的是子進程退出的狀態,獲取子進程狀態,不關心可以設置為NULL

    返回值:pid_t類型,如果返回值大於0,說明等待成功,返回的是子進程的ID,可以通過status查看子進程的退出狀態;如果返回值等於-1,則說明等待失敗(可能wait的進程本身沒有子進程)

    該方式為阻塞式等待,父進程什么都不做在等待子進程退出,如果沒有子進程退出,父進程會一直等;如果父進程收到SIGCHLD信號,該函數就會立馬返回立馬清理。

    --

    waitpid系統調用

    #include<sys/types.h>
    #include<sys/wait.h>
    pid_t waitpid(pid_t pid, int* status, int options)
    

    參數:pid表示要等待的是哪個進程,status仍然是個輸出型參數,存放子進程的退出碼,options是一個選項,如果options設置為0,那么這個函數就是阻塞式等待,如果設置為WNOHANG,則函數為非阻塞式等待(發現已經沒有退出的子進程可以收集)

    返回值:返回值大於0,等待成功,返回子進程的id,返回值等於0,表示發現等待的子進程沒有退出,返回值等於-1,調用失敗

    如果參數pid設置為-1,則表示等待任意子進程,和wait等效

    --

    wait和waitpid的區別?

    1. wait是阻塞式等待,waitpid可自行選擇(options為0阻塞,options為WNOHANG為非阻塞)
    1. wait等待的是任意子進程(等到誰就是誰),waitpid等待的是參數pid傳進來的確定子進程

進程程序替換

  • 替換原理
    fork創建子進程執行的是和父進程相同的程序(也有可能是某個分支),通常fork出的子進程是為了完成父進程所分配的任務,所以子進程通常會調用一種exec函數(六種中的任何一種)來執行另一個任務。當進程調用exec函數時,當前用戶空間的代碼和數據會被新程序所替換,該進程就會從新程序的啟動歷程開始執行。在這個過程中沒有創建新進程,所以調用exec並沒有改變進程的id。

  • 替換過程(圖解)
    -c

  • 替換函數(exec簇)---六種

#include<unistd.h>
int execl(const char* path, const char* arg, ···)
int execlp(const char* file, const char* arg, ···)
int execle(const char* path, const char* arg, ···, char* const envp[])
int execv(const char* path, char* const argv[])
int execvp(const char* file, char* const argv[])
int execve(const char* path, char* const argv[], char* const envp[])

函數特點

  1. 這些函數如果調用成功,則加載的新程序從啟動代碼開始執行,不再返回
  2. 如果調用出錯返回-1
  3. exec系列函數成功調用沒有返回值,調用失敗才有返回值

命名規律

  1. l(list):表示參數采用列表
  2. v(vector):表示參數采用數組
  3. p(path):自動搜索環境變量PATH
  4. e(env):自己維護環境變量(自己組裝環境變量)
函數名 參數格式 是否帶路徑 是否使用當前環境變量
execl 參數列表
execlp 參數列表
execle 參數列表
execv 參數數組
execvp 參數數組
execve 參數數組
  • 六個函數之間的關系
    事實上,只有execve是系統調用,其他五個最終都調用execve。
    -c

  • 拓展:寫簡易shell的需要循環的步驟

    1. 獲取命令行
    2. 解析命令行
    3. 創建子進程(fork)
    4. 進行程序替換---替換子進程(execve)
    5. 父進程等待子進程退出(wait)

進程終止

  • 終止方式

    1.正常終止,結果正確

    2.正常終止,結果錯誤

    3.異常終止

  • 終止方法

    1.正常終止(可以通過echo $?查看進程退出碼)

    • 調用_exit函數
    #include <unistd.h>
    void _exit(int status)
    
    參數:status定義了進程終止狀態,父進程通過wait來獲取
    注意:雖然status是int,但只有低八位可以被父進程使用
         證明:_exit(-1)時,執行echo $?返回值255 
    
    • 調用exit函數
    #include <unistd.h>
    void exit(int status)
    
    exit函數做了以下事情,最終調用了_exit函數:
    1. 執行用戶通過ataxia或on_exit定義的清理函數
    2. 沖刷緩存,將所有緩存數據寫入,並且關閉所有打開的流
    3. 調用_exit函數
    

    -c

    • main函數返回(return退出)
    return是一種更常見的退出進程的方法
    main函數運行時,exit函數會將main返回值當作參數
    return n則相當於exit(n)。
    

    2.異常退出

      ctrl + c 信號終止
    

進程地址空間

  • 對於一個進程空間分布圖如下:
    進程空間分布圖

  • 引子:猜猜下面輸出結果,為什么呢?

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main() {
     pid_t id = fork();
     if(id < 0){
         perror("fork");
return 0; }
     else if(id == 0){ //child
         printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
     }else{ //parent
         printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
     }
     sleep(1);
     return 0;
}

輸出結果:
parent[2995]: 0 : 0x80497d8
child[2996]: 0 : 0x80497d8
由上可以發現,父子進程變量值和地址一模一樣,因為子進程是以父進程為模版,並且父子進程都沒有對變量進行修改
修改一下代碼(如下),看看結果

```
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main() {
    pid_t id = fork();
    if(id < 0){
        perror("fork");
    return 0; }
    else if(id == 0){ //child
        printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
    }else{ //parent
        printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
    }
    sleep(1);
    return 0;
}

輸出結果:
child[3046]: 100 : 0x80497e8 
parent[3045]: 0 : 0x80497e8
我們發現,父子進程,輸出地址是一致的,但是變量內容不一樣!

```
  • 結合上面兩個例子我們可以總結出以下結論:
    1. 變量內容不一樣,所以⽗子進程輸出的變量絕對不是同⼀個變量
    2. 但地址值是⼀樣的,則該地址絕對不是物理地址
    3. 在Linux地址下,這種地址叫做虛擬地址
    4. 我們在⽤C/C++語⾔言所看到的地址,全部都是虛擬地址,物理地址,⽤戶一概看不到,由OS統⼀管理

  • 早期內存管理原理:

    1. 要運⾏一個程序,會把這些程序全都裝⼊內存
    2. 當計算機同時運⾏多個程序時,必須保證這些程序用到的內存總量要⼩於計算機實際物理內存的⼤⼩問題:
    3. 進程地址空間不隔離。由於程序都是直接訪問物理內存,所以惡意程序可以隨意修改別的進程的內存數據,以達到破壞的目的
    4. 內存使⽤效率低。在 A 和 B 都運⾏的情況下,如果⽤戶⼜運⾏了程序 C ,⽽程序 C 需要 15M ⼤ ⼩的內存才能運⾏,而此時系統只剩下 4M 的空間可供使⽤,所以此時系統必須在已運⾏的程序中 選擇一個將該程序的數據暫時拷⻉到硬盤上,釋放出部分空間來供程序 C 使⽤,然后再將程序 C 的數據全部裝入內存中運⾏
    5. 程序運⾏的地址不確定。當內存中的剩余空間可以滿足程序 C 的要求后,操作系統會在剩余空間中隨機分配一段連續的 20M ⼤⼩的空間給程序 C 使⽤,因為是隨機分配的,所以程序運⾏的地址是不確定的,這種情況下,程序的起始地址都是物理地址,⽽而物理地址都是在加載之后才能確定。

    由於以上機制存在問題,於是后來使用分段來解決這些問題

  • 分段

    1. 在編寫代碼的時候,只要指明了所屬段,代碼段和數據段中出現的所有的地址,都是從0零開始,映射關系完全由操作系統維護
    2. CPU將內存分割成了不同的段,於是指令和數據的有效地址並不是真正的物理地址⽽是相對於段⾸地址的偏移地址

    解決問題:

    1. 因為段寄存器的存在,使得進程的地址空間得以隔離,越界問題很容易被判定出來
    2. 實際代碼和數據中的地址,都是偏移量,所以第一條指令可以從0地址開始,系統會⾃動進⾏轉化映射,也就解決了程序運⾏的地址不確定的問題。
    3. 可是,分段並沒有解決性能問題,在內存空間不⾜的情況下,依舊要換⼊喚出整個程序或者整個段,⽆疑要造成內存和硬盤之間拷⻉⼤量數據的情況,進⽽導致性能問題。

    分段仍然存在一些問題,於是引進了分頁和虛擬地址概念

  • 分頁&虛擬地址空間

    頁表:實現從頁號到物理塊號的地址映射

    虛擬內存基本思想:每個進程有用獨立的邏輯地址空間,內存被分為大小相等的多個塊,稱為頁(Page).每個頁都是一段連續的地址。對於進程來看,邏輯上貌似有很多內存空間,其中一部分對應物理內存上的一塊(稱為頁框,通常頁和頁框大小相等),還有一些沒加載在內存中的對應在硬盤上

    創建進程,虛擬地址和物理地址之間的映射關系
    屏幕快照 2018-08-27 上午11.43.45
    上面的圖說明:同一個變量,地址相同,其實是虛擬地址相同,內容不同其實是被映射到了不同的物理地址!

    過程:當訪問虛擬內存時,會訪問MMU(內存管理單元)去匹配對應的物理地址,而如果虛擬內存的頁並不存在於物理內存中,會產生缺頁中斷,從磁盤中取得缺的頁放入內存,如果內存已滿,還會根據某種算法將磁盤中的頁換出。(MMU中存儲頁表,用來匹配虛擬內存和物理內存)

    二級頁表:因為頁表中每個條目是4字節,現在的32位操作系統虛擬地址空間是2^32次方,假設每頁分為4k,也需(2^32/(4*2^10))*4=4M的空間,為每個進程建立一個4M的頁表並不明智。因此在頁表的概念上進行推廣,產生二級頁表,雖然頁表條目沒有減少,但內存中可以僅僅存放需要使用的二級頁表和一級頁表,大大減少了內存的使用。

    缺頁中斷:虛擬內存的頁並不存在於物理內存中,會產生缺頁中斷。處理中斷地址轉換➕更新(地址轉換:有空閑塊兒,調入頁面,沒有利用置換算法替換出去一個;更新頁表,重啟該命令,檢索頁表,命中物理塊兒,運算得出物理地址)

  • 拓展
    CPU把虛擬地址轉換成物理地址:一個虛擬地址,大小4個字節(32bit),分為3個部分:第22位到第31位這10位(最高10位)是頁目錄中的索引,第12位到第21位這10位是頁表中的索引,第0位到第11位這12位(低12位)是頁內偏移。一個一級頁表有1024項,虛擬地址最高的10bit剛好可以索引1024項(2的10次方等於1024)。一個二級頁表也有1024項,虛擬地址中間部分的10bit,剛好索引1024項。虛擬地址最低的12bit(2的12次方等於4096),作為頁內偏移,剛好可以索引4KB,也就是一個物理頁中的每個字節。

    頁面替換算法:物理內存是極其有限的,當虛擬內存所求的頁不在物理內存中時,將需要將物理內存中的頁替換出去,選擇哪些頁替換出去就顯得尤為重要。

    最佳置換算法(Optimal Page Replacement Algorithm):將未來最久不使用的頁替換出去,這聽起來很簡單,但是無法實現。但是這種算法可以作為衡量其它算法的基准。

    最近不常使用算法(Not Recently Used Replacement Algorithm):這種算法給每個頁一個標志位,R表示最近被訪問過,M表示被修改過。定期對R進行清零。這個算法的思路是首先淘汰那些未被訪問過R=0的頁,其次是被訪問過R=1,未被修改過M=0的頁,最后是R=1,M=1的頁。

    先進先出頁面置換算法(First-In,First-Out Page Replacement Algorithm):淘汰在內存中最久的頁,這種算法的性能接近於隨機淘汰。並不好。

    改進型FIFO算法(Second Chance Page Replacement Algorithm):這種算法是在FIFO的基礎上,為了避免置換出經常使用的頁,增加一個標志位R,如果最近使用過將R置1,當頁將會淘汰時,如果R為1,則不淘汰頁,將R置0.而那些R=0的頁將被淘汰時,直接淘汰。

    時鍾替換算法(Clock Page Replacement Algorithm):雖然改進型FIFO算法避免置換出常用的頁,但由於需要經常移動頁,效率並不高。因此在改進型FIFO算法的基礎上,將隊列首位相連形成一個環路,當缺頁中斷產生時,從當前位置開始找R=0的頁,而所經過的R=1的頁被置0,並不需要移動頁。

    最久未使用算法(LRU Page Replacement Algorithm):LRU算法的思路是淘汰最近最長未使用的頁。這種算法性能比較好,但實現起來比較困難。

進程間通信

一組編程接口,讓程序員能夠協調不同的進程,使之能在一個操作系統里同時運行,並能夠相互傳遞交換信息

為什么要通信(重點)

  • 數據傳輸:一個進程需要將它的數據發送到另一個進程

  • 資源共享:多個進程之間需要共享資源

  • 事件通知:一個進程要向另一個或者一組進程發送消息,通知它(們)發生的事件(比如進程終止要通知父進程)

  • 進程控制:有些進程需要完全控制另一個進程(如Debug進程),此時,控制進程希望能夠攔截它想控制的進程的所有的陷入和異常,並能夠及時知道其狀態的改變

怎么通信(主要三種方式),通信本質(重點)

管道(Unix中最古老的進程間通信的方式)

我們把一個進程連接到另一個進程的一個數據流稱為管道

  • 匿名管道(通常就叫管道)

    創建方法
    #include <unistd.h>
    int pipe(int fd[2])
    
    參數:文件描述符數組,fd[0]表示讀端,fd[1]表示寫端
    返回值:成功返回0,失敗返回錯誤代碼
    

    特點:
    1. 單向傳輸(單工),只能在父子進程間或兄弟進程間使用
    2. 管道是臨時對象
    3. 管道和文件的使用方法類似,都能使用read、write、open等普通IO函數
    4. 管道面向字節流,即提供流式服務
    5. 一般來講,管道生命周期隨進程,進程退出,管道釋放
    6. 一般來講,內核會對管道操作進行同步與互斥
    7. 本質上Linux上的管道是通過空文件夾實現的
    8. 事實上,管道使用的文件描述符、文件描述符、文件指針最終都會轉化成系統內核中SOCKET描述符,都收到了SOCKET描述符的限制
    9. 補充:Linux中,用兩個file數據結構來實現管道

  • 命名管道

    創建方法
    $ mkfifo filename //命令行創建
    
    int mkfifo(count char* filename, mode_t mode)//函數
    

    特點:

    1. 命名管道也是單向傳輸,但它可以在不相關的進程間使用
    2. 命名管道不是臨時對象,它們是文件系統真正的實體
    3. Linux下,在寫進程打開命名管道之前,必須處理讀進程對命名管道的打開,在寫進程寫數據之前,也必須處理處理讀進程對管道的讀
    4. 除了以上的特點與管道不同,其他的都是都與管道一樣,包括數據結構和操作

    匿名管道和命名管道的區別?

    答:匿名管道只能在父子進程間或兄弟進程間使用,命名管道可以在不相關的進程間使用;匿名管道是臨時對象,命名管道是文件系統真正的實體;匿名管道和命名管道打開和關閉方式不同。

  • 管道的缺陷

    1. 管道讀數據的同時也將數據移除,所以管道不能對多個接收者廣播數據
    2. 管道中的數據被當作字節流,所以無法識別信息的邊界
    3. 如果一個進程中有多個讀進程,寫進程無法發送到指定的讀進程,如果有多個寫進程,無法知道數據是哪一個發送的
系統IPC(System V IPC資源生命周期隨內核)
  • 消息隊列

    提供了一個由一個進程向另外一個進程發送一塊數據的方法

    特點:
    1. 消息隊列是隨內核,只有重啟和手動刪除才會被真正的刪除
    2. 每個數據塊都被認為是有個類型,接受者進程接受的數據塊可以是不同類型值
    3. 每個消息是有最大長度的上限的,每個消息隊列的字節數也是有上限的,系統上消息隊列數也有上限
    4. 可用於機器上的任何進程間通信

    消息隊列與管道的區別:
    1. 提供有格式的字節流,減少開發人員的工作量
    2. 消息具有類型,實際應用中,可以當做優先級使用
    3. 消息隊列隨內核,生命周期比管道長,應用空間更大

  • 共享內存

    由一個進程創建,其余進程對這塊內存進行讀寫

    特點:

    1. 最快的IPC形式
    2. 進程間數據傳遞不再涉及到內存(不執行進入系統的內核調用來傳遞彼此的數據)
    3. Linux無法對共享內存進行同步,需要程序自己對共享內存做同步運算,這種運算很多時候就是通過信號量來實現的
  • 信號量

    主要用於同步與互斥

    1. 進程互斥

      1. 由於有些進程需要共享資源,而且需要互斥使用,所以個進程競爭使用這些資源,這種關系被稱為進程互斥
      2. 系統中某些進程一次只能被一個進程使用,稱這樣的資源為臨界資源或者互斥資源
      3. 在進程中涉及互斥資源的的程序段叫做臨界區
    2. 進程同步
      多個進程需要配合完成同一個任務

    3. 信號量和P、V原語

      1. 由Dijkstra提出
      2. 信號量
        1. 互斥:P、V在同一個進程
        2. 同步:P、V在不同的進程
      3. 信號量值的含義
        1. S>0:表示可用資源個數
        2. S=0:表示沒有可以用的資源,無等待進程
        3. S<0:表示等待隊列中有|S|個進程
套接字(socket)
  • TCP用主機的ip地址加上主機上的端口號作為TCP連接的端點,這種端點就叫做套接字或插口

  • 用(IP地址:端口號)表示

  • 特點:

    1. 是網絡通信過程中端點的抽象表示,是支持TCP/IP的網絡通信的基本操作單元
    2. 包含進行網絡通信必須的五種信息:連接使用的協議、本地主機的IP地址、本地進程的協議端口、遠地主機的IP地址、遠地進程的協議端口
    3. 在所有提供了TCP/IP協議棧的操作系統都適用,且編程方法幾乎一樣
    4. 要通過Internet進行通信,至少需要一對套接字

進程與文件(重點)

進程打開文件的本質

當我們打開文件時,操作系統在內存中要創建相應的數據結構來描述目標文件。於是就有了file結構體。表⽰⼀個已經打開的文件對象。而進程執行open系統調⽤,所以必須讓進程和文件關聯起來。每個進程都有⼀個指針*files, 指向⼀張表files_struct,該表最重要的部分就是包涵一個指針數組,每個元素都是一個指向打開文件的指針!所以,本質上,文件描述符就是該數組的下標。所以,只要拿着文件描述符,就可以找到對應的文件。

文件描述符的本質

本質是數組元素的下標,每個進程都對應一張文件描述符表,該表可視為指針數組,給數組的元素指向文件表的一個元素,數組元素的下標就是大名鼎鼎的文件描述符

屏幕快照 2018-09-08 上午8.36.04

圖詳解:

1.右側的表稱為i節點表,在整個系統中只有1張。該表可以視為結構體數組,該數組的一個元素對應於一個物理文件。
2.中間的表稱為文件表,在整個系統中只有1張。該表可以視為結構體數組,一個結構體中有很多字段,其中有3個字段比較重要:

file status flags
用於記錄文件被打開來讀的,還是寫的。其實記錄的就是open調用中用戶指定的第2個參數
current file offset
用於記錄文件的當前讀寫位置(指針)。正是由於此字段的存在,使得一個文件被打開並讀取后,
下一次讀取將從上一次讀取的字符后開始讀取
v-node ptr
該字段是指針,指向右側表的一個元素,從而關聯了物理文件。

3.左側的表稱為文件描述符表,每個進程有且僅有1張。該表可以視為指針數組,數組的元素指向文件表的一個元素。最重要的是:數組元素的下標就是大名鼎鼎的文件描述符。
4.open系統調用執行的操作:新建一個i節點表元素,讓其對應打開的物理文件(如果對應於該物理文件的i節點元素已經建立,就不做任何操作);新建一個文件表的元素,根據open的第2個參數設置file status flags字段,將current file offset字段置0,將v-node ptr指向剛建立的i節點表元素;在文件描述符表中,尋找1個尚未使用的元素,在該元素中填入一個指針值,讓其指向剛建立的文件表元素。最重要的是:將該元素的下標作為open的返回值返回。
5.這樣一來,當調用read(write)時,根據傳入的文件描述符,OS就可以找到對應的文件描述符表元素,進而找到文件表的元素,進而找到i節點表元素,從而完成對物理文件的讀寫。

文件描述符與C FILE*的關系,理解系統調用與庫函數

每個進程都有⼀個指針*files, 指向⼀張表files_struct,該表最重要的部分就是包涵一個指針數組,每個元素都是一個指向打開文件的指針,文件描述符就是該數組的下標。

系統調用與庫函數
可以認為,f#系列的函數(庫函數),都是對系統調⽤的封裝,方便⼆次開發。

站在系統角度,常見的文件操作接口的使用

open系統調用執行的操作:新建一個i節點表元素,讓其對應打開的物理文件(如果對應於該物理文件的i節點元素已經建立,就不做任何操作);新建一個文件表的元素,根據open的第2個參數設置file status flags字段,將current file offset字段置0,將v-node ptr指向剛建立的i節點表元素;在文件描述符表中,尋找1個尚未使用的元素,在該元素中填入一個指針值,讓其指向剛建立的文件表元素。最重要的是:將該元素的下標作為open的返回值返回。

read(write)時,根據傳入的文件描述符,OS就可以找到對應的文件描述符表元素,進而找到文件表的元素,進而找到i節點表元素,從而完成對物理文件的讀寫

open close read write lseek都屬於系統提供的接⼝,稱之為系統調⽤接⼝

文件描述符重定向的本質與操作

1.linux用文件描述符來標識每個文件對象,文件描述符是一個非負整數,可以唯一地標識會話中打開的文件,每個過程一次最多可以有9個文件描述符;

2.0=>STDIN=>標准輸入;1=>STDOUT=>標准輸出;2=>STDERR=>標准錯誤;

3.STDIN:STDIN文件描述符代表shell的標准輸入,對終端界面來說,標准輸入是鍵盤,在使用輸入重定向時(<),linux會用重定向指定的文件來替換標准輸入文件描述符,它會讀取文件並提取數據,如同它是在鍵盤上輸入的;

4.STDOUT:STDOUT文件描述符代表標准的shell輸出,在終端界面上,標准輸出就是終端顯示器,shell的所有輸出會被重定向到標准輸出中,也就是顯示器,在使用輸出重定向(>)時,linux會用重定向指定的文件來替換標准輸出文件描述符,>>表示追加到文件;

5.STDERR:STDERR文件描述符代表shell的標准錯誤輸出,默認情況下,STDERR文件描述符會和STDOUT文件描述符指向同樣的地方,即:錯誤消息也會輸出到顯示器輸出中,使用2>file,可以只將錯誤消息輸出至文件file中,使用&>file可將標准輸出和錯誤消息都重定向至文件file;

理解文件系統中inode的概念

  • 概念:inode就是索引節點,它用來存放檔案及目錄的基本信息,包含時間、檔名、使用者及群組等

  • inode 是 UNIX 操作系統中的一種數據結構,其本質是結構體

  • 在 Linux 中,索引節點結構存在於系統內存及磁盤,其可區分成 VFS inode 與實際文件系統的 inode。

  • VFS inode 作為實際文件系統中 inode 的抽象,定義了結構體 inode 與其相關的操作 inode_operations

    Linux 中 VFS inode
    include/linux/fs.h
    
    struct inode { 
    ... 
    const struct inode_operations   *i_op; // 索引節點操作
    unsigned long           i_ino;      // 索引節點號
    atomic_t                i_count;    // 引用計數器
    unsigned int            i_nlink;    // 硬鏈接數目
    ... 
    } 
    
    struct inode_operations { 
    ... 
    int (*create) (struct inode *,struct dentry         *,int, struct nameidata *); 
    int (*link) (struct dentry *,struct inode *,struct dentry *); 
    int (*unlink) (struct inode *,struct dentry *); 
    int (*symlink) (struct inode *,struct dentry *,const char *); 
    int (*mkdir) (struct inode *,struct dentry *,int); 
    int (*rmdir) (struct inode *,struct dentry *); 
    ... 
    }
    
  1. Linux 中 VFS inode

    • 每個文件存在兩個計數器:i_count 與 i_nlink,即引用計數與硬鏈接計數
    • i_count 用於跟蹤文件被訪問的數量,而 i_nlink 則是上述使用 ls -l 等命令查看到的文件硬鏈接數
    • 當文件被刪除時,則 i_nlink 先被設置成 0
    • 文件的這兩個計數器使得 Linux 系統升級或程序更新變的容易
    • 系統或程序可在不關閉的情況下(即文件 i_count 不為 0),將新文件以同樣的文件名進行替換,新文件有自己的 inode 及 data block,舊文件會在相關進程關閉后被完整的刪除
  2. 文件系統 ext4 中的 inode

    ext4 中的 inode
    
    struct ext4_inode { 
    ... 
    __le32  i_atime;        // 文件內容最后一次訪問時間
    __le32  i_ctime;        // inode 修改時間
    __le32  i_mtime;        // 文件內容最后一次修改時間
    __le16  i_links_count;  // 硬鏈接計數
    __le32  i_blocks_lo;    // Block 計數
    __le32  i_block[EXT4_N_BLOCKS];  // 指向具體的 block 
    ... 
    };
    
    • 三個時間的定義可對應與命令 stat 中查看到三個時間

    • i_links_count 不僅用於文件的硬鏈接計數,也用於目錄的子目錄數跟蹤(目錄並不顯示硬鏈接數,命令 ls -ld 查看到的是子目錄數)

    • 文件系統 ext3 對 i_links_count 有限制,其最大數為:32000(該限制在 ext4 中被取消)

理解軟硬鏈接及其區別

硬鏈接
  • 概念:硬鏈接(hard link, 也稱鏈接)就是一個文件的一個或多個文件名,其中一個修改后,所有與其有硬鏈接的文件都一起修改了

  • 特點

    (1)文件有相同的 inode 及 data block
    (2)只能對已存在的文件進行創建
    (3)硬鏈接文件不占用存儲空間
    (4)硬鏈接文件不能跨文件系統
    (5)不能對目錄文件進行創建硬鏈接操作
    (6)硬鏈接只能引用同一文件系統中的文件。它引用的是文件在文件系統中的物理索引(也稱為 inode)
    (7)移動或刪除原始文件時,硬鏈接不會被破壞,因為它所引用的是文件的物理數據而不是文件在文件結構中的位置
    (8)硬鏈接的文件不需要用戶有訪問原始文件的權限,也不會顯示原始文件的位置,這樣有助於文件的安全
    (9)刪除的文件有相應的硬鏈接,那么這個文件依然會保留,直到所有對它的引用都被刪除

軟鏈接
  • 概念:軟鏈接又叫符號鏈接,這個文件包含了另一個文件的路徑名。可以是任意文件或目錄,可以鏈接不同文件系統的文件

  • 特點

    (1)軟鏈接有自己的文件屬性及權限等
    (2)可對不存在的文件或目錄創建軟鏈接(鏈接文件可以鏈接不存在的文件,這就產生一般稱之為”斷鏈”的現象)
    (3)軟鏈接可交叉文件系統
    (4)軟鏈接可對文件或目錄創建
    (5)創建軟鏈接時,鏈接計數 i_nlink 不會增加
    (6)鏈接文件可以循環鏈接自己,類似於編程語言中的遞歸。
    (7)刪除軟鏈接並不影響被指向的文件
    (8)軟鏈接文件只是被指向的源文件的一個標記,當刪除了源文件后,鏈接文件不能獨立存在,雖然仍保留文件名,但卻不能查看軟鏈接文件的內容了,成為了死鏈接
    (9)被指向路徑文件被重新創建,死鏈接可恢復為正常的軟鏈接

硬鏈接和軟鏈接的區別
  • 硬鏈接文件有相同的 inode 及 data block;軟連接文件有自己的 inode 及 data block 等文件屬性
  • 硬鏈接文件只能對已存在的文件進行創建;軟鏈接文件可對不存在的文件或目錄創建軟鏈接
  • 硬鏈接不可交叉文件系統;軟鏈接可交叉文件系統
  • 硬鏈接不可對文件或目錄創建;軟鏈接可對文件或目錄創建
  • 移動或刪除原始文件時,硬鏈接不會被破壞;刪除了源文件后,鏈接文件不能獨立存在
  • 軟鏈接文件相比硬鏈接文件占用存儲空間更大

理解動態與靜態庫

靜態庫
  • 概念

    • 靜態庫是指在我們的應用中,有一些公共代碼是需要反復使用,就把這些代碼編譯為“庫”文件;在鏈接步驟中,連接器將從庫文件取得所需的代碼,復制到生成的可執行文件中的這種庫
  • 特點

    • 可執行文件中包含了庫代碼的一份完整拷貝

    • 靜態庫的代碼是在編譯過程中被載入程序中

  • 缺點

    • 就是被多次使用就會有多份冗余拷貝
動態庫(動態鏈接庫)
  • 概念

    • 動態鏈接提供了一種方法,使進程可以調用不屬於其可執行代碼的函數
  • 特點

    • 函數的可執行代碼位於一個 DLL 中,該 DLL 包含一個或多個已被編譯、鏈接並與使用它們的進程分開存儲的函數

    • DLL 還有助於共享數據和資源

    • 多個應用程序可同時訪問內存中單個DLL 副本的內容

    • DLL 是一個包含可由多個程序同時使用的代碼和數據的庫,Windows下動態庫為.dll后綴,在linux下為.so后綴

Linux下靜態庫和動態庫區別
  1. 命名上:靜態庫文件名的命名方式是“libxxx.a”,庫名前加”lib”,后綴用”.a”,“xxx”為靜態庫名;動態庫的命名方式與靜態庫類似,前綴相同,為“lib”,后綴變為“.so”。所以為“libmytime.so”

  2. 鏈接上:靜態庫的代碼是在編譯過程中被載入程序中;動態庫在編譯的時候並沒有被編譯進目標代碼,而是當你的程序執行到相關函數時才調用該函數庫里的相應函數

  3. 更新上:如果所使用的靜態庫發生更新改變,你的程序必須重新編譯;動態庫的改變並不影響你的程序,動態函數庫升級比較方便

  4. 當同一個程序分別使用靜態庫,動態庫兩種方式生成兩個可執行文件時,靜態鏈接所生成的文件所占用的內存要遠遠大於動態鏈接所生成的文件

  5. 內存上:靜態庫每一次編譯都需要載入靜態庫代碼,內存開銷大;系統只需載入一次動態庫,不同的程序可以得到內存中相同的動態庫的副本,內存開銷小

  6. 靜態庫和程序鏈接有關和程序運行無關;動態庫和程序鏈接無關和程序運行有關

進程與線程(重點)

站在操作系統管理角度,理解什么是線程

線程是不擁有獨立資源空間的程序執行流的最小單位

站在進程地址空間角度,理解什么是線程

線程是進程中的實體,是進程內部的控制序列,和該進程內的其他線程共享地址空間和資源

站在執行流角度,理解什么是線程

線程是程序中一個單一的順序控制流程,是程序執行流的最小單位

如何理解線程是進程內部的一個執行分支

  • 原因

    1. 60年代,在OS中能擁有資源和獨立運行的基本單位是進程,然而隨着計算機技術的發展,進程出現了很多弊端,由於進程是資源擁有者,創建、撤消與切換存在較大的時空開銷,因此需要引入輕型進程;

    2. 對稱多處理機(SMP)出現,可以滿足多個運行單位,而多個進程並行開銷過大,因此在80年代,出現了能獨立運行的基本單位——線程(Threads)

  • 進程就是一棵樹,我們的線程就是其中的一個個分支,沒有了線程,進程並不能執行任何操作。我們進程的具體操作最后還是分配給每一個線程來執行。相對於線程,我們甚至可以把進程理解為線程的一個容器,它代表線程來接受分配到的資源,為線程提供執行代碼,所以進程是資源分配的基本單位。

進程與線程有什么區別

體區別點擊這里進來

Linux下線程有什么特點

  • 線程中的實體基本上不擁有系統資源,只是有一點必不可少的、能保證獨立運行的資源。

  • 獨立調度和分派的基本單位

  • 多個線程可並發執行(同個或不同進程中均可以)

  • 多個線程共享同個進程的資源和地址空間。

  • 線程是動態概念,它的動態特性由線程控制塊TCB(線程狀態、當線程不運行時被保存的現場資源、一組執行堆棧、存放每個線程的局部變量主存區、訪問同一個進程中的主存和其它資源)描述。

Linux下,pthread庫如何控制線程

簡單來說是通過pthread簇函數來控制,常見的必不可少的就是創建和終止,具體如下:

創建線程(pthread_create)
#include <pthread.h>
 
int pthread_create(pthread_t *restrict thread,/
                   const pthread_attr_t * restrict attr, /
                   void *(*start_routine)(void *),/
                   void * restrict arg);
                   
返回值:成功返回0,失敗返回錯誤號。
其他的系統函數都是成功返回0,失敗返回-1,而錯誤號保存在全局變量errno中。
pthread庫的函數都是通過返回值返回錯誤號
雖然每個線程也都有一個errno,但這是為了兼容其他函數接口而提供的
pthread庫本身並不使用它,通過返回值返回錯誤碼更加清晰。

注意:gcc編譯時要加上選項 -lpthread
  • 執行過程:在一個線程中調用pthread_create()創建線程后,當前線程從pthread_create()返回繼續往下執行,而新的線程所執行的代碼由我們傳給pthread_create的函數指針start_routine決定。start_routine函數接收一個參數,是通過ptherad_create的arg參數傳遞給它的,該參數的類型為void *,這個指針按什么類型解釋由調用者自己定義。start_routine的返回值類型也是void *,這個指針的含義同樣由調用者自己定義。start_routine返回時,這個線程就退出了,其他線程可以調用pthread_join得到start_routine的返回值,類似於父進程調用wait()得到子進程的退出狀態。

  • 成功返回: pthread_create成功返回后,新創建的線程的id被填寫到thread參數所指向的內存單元。進程id的類型是pid_t,每個進程的id在整個系統中是唯一的,調用getpid()可以獲得當前進程的id,是一個正整數值。線程id的類型是thread_t,它只在當前進程中保證是唯一的,在不同的系統中thread_t這個類型有不同的實現,他可能是一個整數值,也可能是一個結構體,也可能是一個地址,所以不能簡單的當成整數用printf打印,調用pthread_self()可以獲得當前線程的id。

  • 總結

    1. 在linux上,thread_t類型是一個地址值,屬於同一進程的多個線程調用getpid()可以得到相同的進程號,而調用pthread_self()得到的線程號各不相同

    2. 由於pthread_create的錯誤碼不保存在errno中,因此不能直接用perror()打印錯誤信息,可以先用strerror()把錯誤碼轉成錯誤信息再打印

    3. 如果任意一個線程調用了exit或_exit,則整個進程的所有線程都終止,由於從main函數return也相當於調用exit,為了防止新創建的線程還沒有得到執行就終止,我們在main函數return之前延時1秒,但是即使主線程等待1秒鍾,內核也不一定會調度新創建的線程執行。

終止線程
  • 只終止線程而不終止進程的方法有三種

    • return(從線程函數return)。主線程return相當於調用了exit。

    • 調用pthread_exit函數。

    • 調用pthread_cancel函數(一個線程可以調用pthread_cancel函數終止同一進程中的另一線程)

  • 終止函數

    #include <pthread.h>
    void pthread_exit(void *value_ptr);
    
     value_ptr是void *類型,和線程函數返回值的用法一樣,其他線程可以調用pthread_join獲得這個指針
     
     pthread_exit或者return返回的指針所指向的內存單元必須是全局的或者是用malloc分配的
     
     不能在線程函數的棧上分配,因為當其他線程得到這個返回指針時線程函數已經退出了
    

    在這里需要用到pthread_join函數所以引入線程等待的概念線程等待

    通常情況下,線程終止后,其終止狀態一直保留到其它線程調用pthread_join獲取它的狀態為止。

    線程也可以被置為detach狀態,這樣的線程一旦終止就立刻回收它所占用的所有資源,而不保留終止狀態。

    不能對一個已經處於detach狀態的線程調用pthread_join,這樣的調用將返回EINVAL。

    對一個尚未detach的線程調用pthread_join或pthread_detach都可以把該線程置為detach狀態

    不能對同一線程重復調用pthread_join

    對一個線程調用pthread_detach就不能再調用pthread_join了。

    #include <pthread.h>
    int pthread_detach(pthread_t tid);
    返回值:成功返回0,失敗返回錯誤號。
    

為什么線程等待,如何等待

  • 為什么需要線程等待(WHY)?

    1. 已經退出的線程,其空間沒有被釋放,仍然在進程的地址空間內。

    2. 創建新的線程不會復⽤剛才退出線程的地址空間

  • 如何等待(HOW)?

    功能:等待線程結束 原型
    #include <pthread.h>
    int pthread_join(pthread_t thread, void **value_ptr);
    
    參數 thread:線程ID
    value_ptr:它指向⼀一個指針,后者指向線程的返回值 
    返回值:成功返回0;失敗返回錯誤碼
    
    
    調用該函數的線程將掛起等待,直到id為thread的線程終止
    
    thread線程以不同的方法終止,通過pthread_join得到的終止狀態是不同的:
    1. 如果thread線程通過return返回,value_ptr所指向的單元里存放的是thread線程函數
    

的返回值

2. 如果thread線程被別的線程調用pthread_cancel異常終止掉,value_ptr所指向的單元

里存放的是常數PTHREAD_CANCELED(pthread庫中常數PTHREAD_CANCELED的值是-1)

define PTHREAD_CANCELED ((void *)-1)

3. 如果thread線程是自己調用pthread_exit終止的,value_ptr所指向的單元存放的是傳

給pthread_exit的參數

注意:如果對thread線程的終止狀態不關心,可以傳NULL給value_ptr參數

```

如何分離線程,為何要分離

  • 什么是線程分離(WHAT)

    簡單來講,線程分離就是當線程被設置為分離狀態后,線程結束時,它的資源會被系統自動的回收,而不再需要在其它線程中對其進行 pthread_join() 操作。

  • 為什么線程分離(WHY)

    在我們使用默認屬性創建一個線程的時候,線程是 joinable 的。 joinable 狀態的線程,必須在另一個線程中使用 pthread_join() 等待其結束,如果一個 joinable 的線程在結束后,沒有使用 pthread_join() 進行操作,這個線程就會變成"僵屍線程"。每個僵屍線程都會消耗一些系統資源,當有太多的僵屍線程的時候,可能會導致創建線程失敗。

  • 怎么分離(HOW)

    在前面講到線程控制時提到了終止線程,其中就提到了detach狀態,下面就是設置為detach狀態的函數

    #include <pthread.h>
    int pthread_detach(pthread_t tid);
    返回值:成功返回0,失敗返回錯誤號。
    
    注意:線程分離可以在創建的時候屬性里面設置
    

    核心思想:

    1. 把一個線程的屬性設置為 detachd 的狀態,讓系統來回收它的資源;
    2. 把一個線程的屬性設置為 joinable 狀態,這樣就可以使用 pthread_join() 來阻塞的等待一個線程的結束,並回收其資源,並且pthread_join() 還會得到線程退出后的返回值,來判斷線程的退出狀態 。

什么叫做臨界區,臨界資源,原子性

  • 臨界資源:臨界資源是一次僅允許一個進程使用的共享資源。各進程采取互斥的方式,實現共享的資源稱作臨界資源。屬於臨界資源的硬件有,打印機,磁帶機等;軟件有消息隊列,變量,數組,緩沖區等。諸進程間采取互斥方式,實現對這種資源的共享。

  • 臨界區:每個進程中訪問臨界資源的那段代碼稱為臨界區(criticalsection),每次只允許一個進程進入臨界區,進入后,不允許其他進程進入。不論是硬件臨界資源還是軟件臨界資源,多個進程必須互斥的對它進行訪問。多個進程涉及到同一個臨界資源的的臨界區稱為相關臨界區。使用臨界區時,一般不允許其運行時間過長,只要運行在臨界區的線程還沒有離開,其他所有進入此臨界區的線程都會被掛起而進入等待狀態,並在一定程度上影響程序的運行性能。

  • 原子性:原子性是指一個操作是不可中斷的,要么全部執行成功要么全部執行失敗,有着“同生共死”的感覺。及時在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程所干擾。

什么叫做互斥與同步,為什么要引入互斥與同步機制

  • 互斥:是指散步在不同任務之間的若干程序片斷,當某個任務運行其中一個程序片段時,其它任務就不能運行它們之中的任一程序片段,只能等到該任務運行完這個程序片段后才可以運行。最基本的場景就是:一個公共資源同一時刻只能被一個進程或線程使用,多個進程或線程不能同時使用公共資源。

  • 同步:是指散步在不同任務之間的若干程序片斷,它們的運行必須嚴格按照規定的某種先后次序來運行,這種先后次序依賴於要完成的特定的任務。最基本的場景就是:兩個或兩個以上的進程或線程在運行過程中協同步調,按預定的先后次序運行。比如 A 任務的運行依賴於 B 任務產生的數據。

  • 引入互斥與同步機制原因:
    現代操作系統基本都是多任務操作系統,即同時有大量可調度實體在運行。在多任務操作系統中,同時運行的多個任務可能:

    1. 都需要訪問/使用同一種資源
    2. 多個任務之間有依賴關系,某個任務的運行依賴於另一個任務

    這兩種情形是多任務編程中遇到的最基本的問題,也是多任務編程中的核心問題,同步和互斥就是用於解決這兩個問題的。

死鎖以及死鎖產生的4個必要條件

  • 所謂死鎖,是指多個進程在運行過程中因爭奪資源而造成的一種僵局,當進程處於這種僵持狀態時,若無外力作用,它們都將無法再向前推進。

  • 產生死鎖的原因:

    可歸結為如下兩點:

    a. 競爭資源

    • 系統中的資源可以分為兩類:

      1. 可剝奪資源,是指某進程在獲得這類資源后,該資源可以再被其他進程或系統剝奪,CPU和主存均屬於可剝奪性資源;
      2. 另一類資源是不可剝奪資源,當系統把這類資源分配給某進程后,再不能強行收回,只能在進程用完后自行釋放,如磁帶機、打印機等。
    • 產生死鎖中的競爭資源之一指的是競爭不可剝奪資源(例如:系統中只有一台打印機,可供進程P1使用,假定P1已占用了打印機,若P2繼續要求打印機打印將阻塞)

    • 產生死鎖中的競爭資源另外一種資源指的是競爭臨時資源(臨時資源包括硬件中斷、信號、消息、緩沖區內的消息等),通常消息通信順序進行不當,則會產生死鎖

    b. 進程間推進順序非法

    • 若P1保持了資源R1,P2保持了資源R2,系統處於不安全狀態,因為這兩個進程再向前推進,便可能發生死鎖

    • 例如,當P1運行到P1:Request(R2)時,將因R2已被P2占用而阻塞;當P2運行到P2:Request(R1)時,也將因R1已被P1占用而阻塞,於是發生進程死鎖

  • 產生死鎖的必要條件:

    • 互斥條件
    • 請求和保持條件
    • 不剝奪條件
    • 環路等待條件。

如何避免死鎖

  • 三種用於避免死鎖的技術:

    1. 加鎖順序(線程按照一定的順序加鎖)

      • 再多線程中,如果一個線程需要鎖,那么他就必須要按照一定順序獲得鎖
      thread a
      lock 1
      lock 2
      
      thread b
          等待 1//等待線程a中1加鎖后才能對3上鎖        
          lock 3(1已經被上鎖)
      
      thread c
          //如果想獲取鎖1,2,3,則
          等待 1
          等待 2
          等待 3
          //然后才輪到該線程獲取
      
      • 缺點:按照順序加鎖是一種有效的死鎖預防機制,這種方式需要你事先知道所有可能會用到的鎖和鎖的順序,但這個很難,並不一定所有的都能知道。
    2. 加鎖時限(線程嘗試獲取鎖的時候加上一定的時限,超過時限則放棄對該鎖的請求,並釋放自己占有的鎖)

      • 在嘗試獲取鎖的時候加一個超時時間,這也就意味着在嘗試獲取鎖的過程中若超過了這個時限該線程則放棄對該鎖請求。

      • 若一個線程沒有在給定的時限內成功獲得所有需要的鎖,則會進行回退並釋放所有已經獲得的鎖,然后等待一段隨機的時間再重試

      • 這段隨機的等待時間讓其它線程有機會嘗試獲取相同的這些鎖,並且讓該應用在沒有獲得鎖的時候可以繼續運行(可以做些其他的事)

      • 缺點:有時為了執行某個任務。某個線程花了很長的時間去執行任務,如果在其他線程看來,可能這個時間已經超過了等待的時限,可能出現了死鎖。

    3. 死鎖檢測

      • 死鎖檢測是一個更好的預防死鎖機制,針對的是那些不可能按序加鎖且鎖超時不可行的場景

      • 每當線程獲取了鎖,會在線程和鎖相關的數據結構(map、graph等)中,同樣,線程請求鎖也需要記錄在這個數據結構中

      • 當一個線程請求失敗時,這個線程可以遍歷鎖的關系圖看看是否有死鎖發生,死鎖一般要比兩個線程互相持有對方的鎖這種情況要復雜的多

      • 當檢測出死鎖時,一個可行的做法是釋放所有鎖,回退,並且等待一段隨機的時間后重試,更好的方案是給這些線程設置優先級,讓一個(或幾個)線程回退,剩下的線程就像沒發生死鎖一樣繼續保持着它們需要的鎖。

最具有代表性的避免死鎖算法是銀行家算法

  • 銀行家算法中的數據結構:
    ① 可利用資源向量 Available
    ② 最大需求矩陣Max
    ③ 分配矩陣 Allocation
    ④ 需求矩陣 Need
    三個矩陣間存在下述關系:

    Needp[i,j] = Max[i,j] –Allocation[i,j]

  • 算法思想:
    (1)如果Request i[j] <= Need[i,j],便轉向步驟 (2);否則認為出錯,因為它所需要的資源數已超過它所宣布的最大值。
    (2)如果Request i[j] <= Available[j],便轉向步驟(3);否則,表示尚無足夠資源,Pi須等待。
    (3)系統試探着把資源分配給進程Pi,並修改下面數據結構中的值:

      Available[j]:=Available[j] – Request i[j];
      Allocation[i,j]:=Allocation[i,j] + Request i[j];
      Need[i,j]:=Need[i,j] – Request i[j];
    

(4)系統執行安全性算法,減產此次算法分配后系統是否處於安全狀態。若安全,才正式將資源分配給進程Pi,以完成本次分配;否則,將本次的試探分配作廢,恢復原來的資源分配狀態,讓進程Pi等待。

  • 安全性算法:
    (1)設置兩個向量:

    ① 工作向量Work,表示系統可提供給進程繼續運行所需的各類資源數目,它含有m個元素,在執行安全算法開始時,Work:=Available。
    ② Finish,表示系統是否有足夠的資源分配給進程,使之運行完成。開始時限做Finish[i]:=false;當有足夠資源分配給進程時,再令Finish[i]:=true。
    (2)從進程集合中找到一個能滿足下述條件的進程:

    ① Finish[i] = false;
    ② Need[i,j] <= Work[j];
    若找到,執行步驟(3),否則,執行步驟(4)

    (3)當進程Pi獲得資源后,可順利執行,直至完成,並釋放出分配給它的資源,故應執行:

      Work[j]:=Work[j] + Allocation[i,j];
      Finish[i]:=true;
      go to step 2;
    

    (4)如果所有進程的Finish[i] = true都滿足,則表示系統處於安全狀態;否則,系統處於不安全狀態。

  • 常用解除死鎖的兩種方法是:① 剝奪資源; ② 撤銷進程。

  • 例題:在銀行家算法中,若出現下述資源分配情況:

Process Allocation Need Available
P0 0 0 3 2 0 0 1 2 1 6 2 2
P1 1 0 0 0 1 7 5 0
P2 1 3 5 4 2 3 5 6
P3 0 3 3 2 0 6 5 2
P4 0 0 1 4 0 6 5 6

試問:
⑴ 該狀態是否安全?
⑵ 若進程P2提出請求Request(1,2,2,2)后,系統能否將資源分配給它?

答: ⑴該狀態是安全的,因為存在一個安全序列< P0P3P4P1P2>。下表為該時刻的安全序列表。

Process Work Need Allocation Work+Allocation Finish
P0 1 6 2 2 0 0 1 2 0 0 3 2 1 6 5 4 true
P3 1 6 5 4 0 6 5 2 0 3 3 3 1 9 8 7 true
P4 1 9 8 7 0 6 5 6 0 0 1 4 1 9 9 11 true
P1 1 9 9 11 1 7 5 0 1 0 0 0 2 9 9 11 true
P2 2 9 9 11 2 3 5 6 1 3 5 4 3 12 14 17 true

⑵若進程P2提出上述請求,系統不能將資源分配給它,因為分配之后系統將進入不安全狀態。

P2請求資源:P2發出請求向量Request2(1,2,2,2),系統按銀行家算法進行檢查:
①Request2(1,2,2,2)≤Need2(2,3,5,6);
②Request2(1,2,2,2)≤Available(1,6,2,2);
③系統暫時先假定可為P2分配資源,並修改P2的有關數據,如下表:

Allocation Need Available
2 5 7 6 1 1 3 4 0 4 0 0

生產者消費者模型的理解

生產者消費者模型也叫緩存綁定問題,是一個經典的、多進程同步問題。

單生產者和單消費者
  • 有兩個進程:一組生產者進程和一組消費者進程共享一個初始為空、固定大小為的緩沖區。

  • 生產者:生產者的工作是制造一段數據,只有緩沖區沒滿時,生產者才能把消息放入到緩沖區,否則必須等待,如此反復;

  • 消費者:只有緩沖區不空時,消費者才能從緩沖區中取出消息,一次消費一段數據(即將其從緩存中移出),否則必須等待。

  • 由於緩沖區是臨界資源,它只允許一個生產者放入消息,或者一個消費者從中取出消息。

  • 需要解決的主要問題

    1. 生產者在緩存還是滿的時候不能向緩存區寫數據;
    2. 消費者不能從空的緩存中取出數據。

    生產者和消費者對緩沖區互斥訪問是互斥關系,同時生產者和消費者又是一個相互協作的關系,只有生產者生產之后,消費者才能消費,他們也是同步關系。

    解決思路:
    對於生產者,如果緩存是滿的就去睡覺。消費者從緩存中取走數據后就叫醒生產者,讓它再次將緩存填滿。若消費者發現緩存是空的,就去睡覺了。下一輪中生產者將數據寫入后就叫醒消費者。

    不完善的解決方案會造成“死鎖”,即兩個進程都在“睡覺”等着對方來“喚醒”。

    只有生產者和消費者兩個進程,正好是這兩個進程存在着互斥關系和同步關系。那么需要解決的是互斥和同步PV操作的位置。使用“進程間通信”,“信號標”semaphore就可以解決喚醒的問題:

    我們使用了兩個信號標:full 和 empty 。信號量mutex作為互斥信號量,它用於控制互斥訪問緩沖池,互斥信號量初值為1;信號量 full 用於記錄當前緩沖池中“滿”緩沖區數,初值為0。信號量 empty 用於記錄當前緩沖池中“空”緩沖區數,初值為n。新的數據添加到緩存中后,full 在增加,而 empty 則減少。如果生產者試圖在 empty 為0時減少其值,生產者就會被“催眠”。下一輪中有數據被消費掉時,empty就會增加,生產者就會被“喚醒”。

    該類問題要注意對緩沖區大小為n的處理,當緩沖區中有空時便可對empty變量執行P 操作,一旦取走一個產品便要執行V操作以釋放空閑區。對empty和full變量的P操作必須放在對mutex的P操作之前。

    1、若生產者進程已經將緩沖區放滿,消費者進程並沒有取產品,即 empty = 0,當下次仍然是生產者進程運行時,它先執行 P(mutex)封鎖信號量,再執行 P(empty)時將被阻塞,希望消費者取出產品后將其喚醒。輪到消費者進程運行時,它先執行 P(mutex),然而由於生產者進程已經封鎖 mutex 信號量,消費者進程也會被阻塞,這樣一來生產者進程與消費者進程都
    將阻塞,都指望對方喚醒自己,陷入了無休止的等待。
    2、若消費者進程已經將緩沖區取空,即 full = 0,下次如果還是消費者先運行,也會出現類似的死鎖。
    不過生產者釋放信號量時,mutex、full 先釋放哪一個無所謂,消費者先釋放 mutex 還是 empty 都可以。

多生產者多消費者
  • 在多個制造商和多個消費者出現的情況下就會造成擁護不堪的情況,會導致兩個或多個進程同時向一個磁道寫入或讀出數據。要理解這種情況是如何出現的,我們可以借助於putItemIntoBuffer()函數。它包含兩個動作:一個來判斷是否有可用磁道,另一個則用來向其寫入數據。如果進程可以由多個制造商並發執行,下面的情況則會出現:
    1. 兩個制造商為emptyCount減值;
    2. 一個制造商判斷緩存中有可用磁道;
    3. 第二個制造商與第一個制造商一樣判斷緩存中有可用磁道;
    4. 兩個制造商同時向同一個磁道寫入數據。

  • 多個生產者向一個緩沖區中存入數據,多個生產者從緩沖區中取數據。這是有界緩沖區問題,隊列改寫,生產者們之間、消費者們之間、生產者消費者之間互相互斥。共享緩沖區作為一個環繞緩沖區,存數據到尾時再從頭開始。

    • 我們使用一個互斥量保護生產者向緩沖區中存入數據。由於有多個生產者,因此需要記住現在向緩沖區中存入的位置。

    • 使用一個互斥量保護緩沖區中消息的數目,這個生產的數據數目作為生產者和消費者溝通的橋梁。

    • 使用一個條件變量用於喚醒消費者。由於有多個消費者,同樣消費者也需要記住每次取的位置。

  • 在選項中選擇生產條目的數目,生產者的線程數目,消費者的線程數目。生產者將條目數目循環放入緩沖區中,消費者從緩沖區中循環取出並在屏幕上打印出來。

  • 為了克服這個問題,我們需要一個方法,以確保一次只有一個制造商在執行調用函數。換個說法來講,我們需要一個有“互斥信號標”(mutal exclusion)的“關鍵扇區”(critical section)。為了實現這一點,我們使用一個叫mutex二位信號標。因為一個二位信號標的值只能是1或0,只有一個進程能執行down(mutex)或up(mutex)。

讀者寫者模型的理解

什么是讀者寫者模型

讀者和寫者模型是操作系統中的一種同步與互斥機制,它與消費者和生產者模型類似,但也有不同的地方,最明顯的一個特點是在讀者寫者模型中,多個多者之間可以共享“倉庫”,讀者與讀者之間采用了並行機制;而在消費者和生產者模型中,消費者只能有一個獨占倉庫,消費者與消費者是競爭關系。

讀者寫者模型的要具有的條件

寫者是排它性的,即在有多個寫者的情況下,只能有一個寫者占有“倉庫”;
讀者的並行機制:可以運行多個讀者去訪問倉庫;
如果讀者占有了倉庫,那么寫者則不能占有;

讀者寫者模型的關系

讀者優先:讀者先來讀取數據,此時寫者處於阻塞狀態,當讀者都讀取完數據后且沒有讀者了時寫者才能訪問倉庫;
寫者優先:類似與讀者優先的情況;
公平情況:寫者與讀者訪問“倉庫”優先級相等,誰先進入優先級隊列誰先訪問;

兩個模型之間的區別

從兩個模型的原理中可以看出,兩個模型最大的區別在於在生產者消費者模型中,生產者與生產者是互斥關系,消費者和消費者是互斥關系,生產者和消費者之間是互斥與同步關系;而在讀者寫者模型中,讀者和讀者沒有關系,寫者和寫者是互斥關系,讀者和寫者是互斥與同步關系。

互斥量,條件變量,信號量,讀寫鎖,自旋鎖

  • 互斥鎖--保護了一個臨界區,在這個臨界區中,一次最多只能進入一個線程。如果有多個進程在同一個臨界區內活動,就有可能產生競態條件(race condition)導致錯誤,其中包含遞歸鎖和非遞歸鎖,(遞歸鎖:同一個線程可以多次獲得該鎖,別的線程必須等該線程釋放所有次數的鎖才可以獲得)。

  • 讀寫鎖--從廣義的邏輯上講,也可以認為是一種共享版的互斥鎖。可以多個線程同時進行讀,但是寫操作必須單獨進行,不可多寫和邊讀邊寫。如果對一個臨界區大部分是讀操作而只有少量的寫操作,讀寫鎖在一定程度上能夠降低線程互斥產生的代價。

  • 條件變量--允許線程以一種無競爭的方式等待某個條件的發生。當該條件沒有發生時,線程會一直處於休眠狀態。當被其它線程通知條件已經發生時,線程才會被喚醒從而繼續向下執行。條件變量是比較底層的同步原語,直接使用的情況不多,往往用於實現高層之間的線程同步。使用條件變量的一個經典的例子就是線程池(Thread Pool)了。

  • 信號量--通過精心設計信號量的PV操作,可以實現很復雜的進程同步情況(例如經典的哲學家就餐問題和理發店問題)。而現實的程序設計中,卻極少有人使用信號量。能用信號量解決的問題似乎總能用其它更清晰更簡潔的設計手段去代替信號量。

  • 自旋鎖--當要獲取一把自旋鎖的時候又被別的線程持有時,不斷循環的去檢索是否可以獲得自旋鎖,一直占CPU資源。

  • 對於這些同步對象,有一些共同點:

    1. 每種類型的同步對象都有一個init的API,它完成該對象的初始化,在初始化過程中會分配該同步對象所需要的資源(注意是為支持這種鎖而需要的資源,不包括表示同步對象的變量本身所需要的內存)
    2. 每種類型的同步對象都一個destory的API,它完成與init相反的工作
    3. 對於使用動態分配內存的同步對象,在使用它之前必須先調用init
    4. 在釋放使用動態分配內存的同步對象所使用的內存時,必須先調用destory釋放系統為其申請的資源
    5. 每種同步對象的默認作用范圍都是進程內部的線程,但是可以通過修改其屬性為PTHREAD_PROCESS_SHARED並在進程共享內存中創建它的方式使其作用范圍跨越進程范圍
    6. 無論是作用於進程內的線程,還是作用於不同進程間的線程,真正參與競爭的都是線程(對於不存在多個線程的進程來說就是其主線程),因而討論都基於線程來
    7. 這些同步對象都是協作性質的,相當於一種君子協定,需要相關線程主動去使用,無法強制一個線程必須使用某個同步對象
  • 總體上來說,可以將它們分為兩類:

    1. 第一類是互斥鎖、讀寫鎖、自旋鎖,它們主要是用來保護臨界區的,也就是主要用於解決互斥問題的,當嘗試上鎖時大體上有兩種情況下會返回:上鎖成功或出錯,它們不會因為出現信號而返回。另外解鎖只能由鎖的擁有着進行
    2. 第二類是條件變量和信號量,它們提供了異步通知的能力,因而可以用於同步和互斥。但是二者又有區別:
      1. 信號量可以由發起P操作的線程發起V操作,也可以由其它線程發起V操作;但是條件變量一般要由其它線程發起signal(即喚醒)操作
      2. 由於條件變量並沒有包含任何需要檢測的條件的信息,因而對這個條件需要用其它方式來保護,所以條件變量需要和互斥鎖一起使用,而信號量本身就包含了相關的條件信息(一般是資源可用量),因而不需要和其它方式一起來使用
      3. 類似於三種鎖,信號量的P操作要么成功返回,要么失敗返回,不會因而出現信號而返回;但是條件變量可能因為出現信號而返回,這也是因為它沒包含相關的條件信息而導致的。

進程關系

守護進程(重點)

  • 概念理解

    1. 守護進程是一個在后台運行並且不受任何終端控制的進程。Unix操作系統有很多典型的守護進程(其數目根據需要或20—50不等),它們在后台運行,執行不同的管理任務。

    2. 守護進程也稱精靈進程(Daemon),是運⾏在后台的⼀種特殊進程。它獨立於控制終端並且周期性地執行 某種任務或等待處理某些發生的事件。守護進程是⼀種很有用的進程。

    3. Linux的⼤多數服務器就是⽤用守護進程實現的。比如,ftp服務器,ssh服務器,Web服務器httpd等。同時,守護進程完成許多系統任務。⽐如,作業規划進程crond等。

    4. Linux系統啟動時會啟動很多系統服務進程,這些系統服務進程沒有控制終端,不能直接和⽤戶交互。其它進程 都是在用戶登錄或運行程序時創建,在運行結束或⽤戶注銷時終止,但系統服務進程(守護進程)不受用戶登錄 注銷的影響,它們一直在運⾏行着。這種進程有⼀個名稱叫守護進程(Daemon)。

    5. 守護進程沒有控制終端,因此當某些情況發生時,不管是一般的報告性信息,還是需由管理員處理的緊急信息,都需要以某種方式輸出。Syslog 函數就是輸出這些信息的標准方法,它把信息發送給 syslogd 守護進程

  • 查看守護進程

    1. ps axj命令查看系統中的進程。參數a表示不僅列出當前⽤戶的進程,也列出所有其他⽤戶的進程,參數x表示不僅列有控制終端的進程,也列出所有⽆控制終端的進程,參數j表示列出與作業控制相關的信息。

       ps axj | more
      
    2. 找出守護進程
      凡是TPGID⼀欄寫着-1的都是沒有控制終端的進程,也就是守護進程。 在COMMAND⼀列⽤[]括起來的名字表⽰內核線程,這些線程在內核⾥創建,沒有⽤戶空間代碼,因此 沒有程序文件名和命令行, 通常采⽤以k開頭的名字,表⽰Kernel。init進程我們已經很熟悉了,udevd負責維護/dev目錄下的設備⽂件,acpid負責電源管理,syslogd負責 維護/var/log下的⽇志文件可以看出,守護進程通常采⽤以d結尾的名字,表⽰Daemon。

特點:

  1. 守護進程最重要的特性是后台運行。

  2. 守護進程必須與其運行前的環境隔離開來(這些環境包括未關閉的文件描述符、控制終端、會話和進程組、工作目錄以及文件創建掩碼等),這些環境通常是守護進程從執行它的父進程(特別是shell)繼承下來的。

  3. 守護進程的啟動方式有其特殊之處。它可以在Linux系統啟動時從啟動腳本/etc/rc.d中啟動,也可以由作業控制進程crond啟動,還可以由用戶終端(通常是shell)執行。


免責聲明!

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



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