1. 程式人生 > >20155236 《信息安全系統設計基礎》第13周學習總結

20155236 《信息安全系統設計基礎》第13周學習總結

在操作 -m 提高效率 加載 構造 類型 並發 多線程程序 %d

20155236 《信息安全系統設計基礎》第13周學習總結

網絡編程

  • 套接字接口概述:
  • 技術分享圖片

並發編程

  • 並發:邏輯控制流在時間上重疊
  • 並發程序:使用應用級並發的應用程序稱為並發程序。
  • 三種基本的構造並發程序的方法:
    • 進程,用內核來調用和維護,有獨立的虛擬地址空間,顯式的進程間通信機制。
    • I/O多路復用,應用程序在一個進程的上下文中顯式的調度控制流。邏輯流被模型化為狀態機。
    • 線程,運行在一個單一進程上下文中的邏輯流。由內核進行調度,共享同一個虛擬地址空間。

基於進程的並發編程

  • 構造並發程序最簡單的方法——用進程。常用函數如下:fork,exec,waitpid
  • 構造並發服務器:在父進程中接受客戶端連接請求,然後創建一個新的子進程來為每個新客戶端提供服務。
  • 需要註意的事情:
    • 父進程需要關閉它的已連接描述符的拷貝(子進程也需要關閉)
    • 必須要包括一個SIGCHLD處理程序來回收僵死子進程的資源
    • 父子進程之間共享文件表,但是不共享用戶地址空間
  • 獨立地址空間的優點是防止虛擬存儲器被錯誤覆蓋,缺點是開銷高,共享狀態信息才需要IPC機制

基於I/O多路復用的並發編程

  • echo服務器必須響應兩個相互獨立的I/O時間:
    • 網絡客戶端發起連接請求
    • 用戶在鍵盤上鍵入命令行
  • I/O多路復用技術的基本思路:使用select函數,要求內核掛起進程,只有在一個或多個I/O事件發生後,才將控制返回給應用程序。
  • 將描述符集合看成是n位位向量:b(n-1),……b1,b0,每個位bk對應於描述符k,當且僅當bk=1,描述符k才表明是描述符集合的一個元素。可以做以下三件事:
    • 分配;
    • 將一個此種類型的變量賦值給另一個變量;
    • 用FDZERO、FDSET、FDCLR和FDISSET宏指令來修改和檢查它們。
  • echo函數:將來自科幻段的每一行回送回去,直到客戶端關閉這個鏈接。
  • 狀態機就是一組狀態、輸入事件和轉移,轉移就是將狀態和輸入時間映射到狀態,自循環是同一輸入和輸出狀態之間的轉移。
  • 事件驅動器的設計優點:
    • 比基於進程的設計給了程序員更多的對程序行為的控制
    • 運行在單一進程上下文中,因此,每個邏輯流都能訪問該進程的全部地址空間,使得流之間共享數據變得很容易。
    • 不需要進程上下文切換來調度新的流。
  • 缺點:
    • 編碼復雜
    • 不能充分利用多核處理器
  • 粒度:每個邏輯流每個時間片執行的指令數量。並發粒度就是讀一個完整的文本行所需要的指令數量。

基於線程的並發編程

  • 線程:運行子啊進程上下文中的邏輯流。
  • 線程有自己的線程上下文,包括一個唯一的整數線程ID、棧、棧指針、程序計數器、通用目的寄存器和條件碼。所有運行在一個進程裏的線程共享該進程的整個虛擬地址空間。

線程執行模型

  • 主線程:每個進程開始生命周期時都是單一線程。
    對等線程:某一時刻,主線程創建的對等線程
  • 線程與進程的不同:
    • 線程的上下文切換要比進程的上下文切換快得多;
    • 和一個進程相關的線程組成一個對等池,獨立於其他線程創建的線程。
    • 主線程和其他線程的區別僅在於它總是進程中第一個運行的線程。
  • 對等池的影響
    • 一個線程可以殺死它的任何對等線程;
    • 等待它的任意對等線程終止;
    • 每個對等線程都能讀寫相同的共享資源。

Posix線程

  • 線程例程:線程的代碼和本地數據被封裝在一個線程例程中。每一個線程例程都以一個通用指針作為輸入,並返回一個通用指針。

創建線程

  • pthread create函數創建一個新的線程,並帶著一個輸入變量arg,在新線程的上下文中運行線程例程f。新線程可以通過調用pthread _self函數來獲得自己的線程ID。

終止線程

  • 一個線程的終止方式:
    • 當頂層的線程例程返回時,線程會隱式的終止;
    • 通過調用pthread _exit函數,線程會顯示地終止。如果主線程調用pthread _exit,它會等待所有其他對等線程終止,然後再終止主線程和整個進程。

回收已終止線程的資源

  • pthread _join函數會阻塞,直到線程tid終止,回收已終止線程占用的所有存儲器資源。pthread _join函數只能等待一個指定的線程終止。

分離線程

  • 在任何一個時間點上,線程是可結合的或者是分離的。一個可結合的線程能夠被其他線程收回其資源和殺死;一個可分離的線程是不能被其他線程回收或殺死的。它的存儲器資源在它終止時有系統自動釋放。
  • 默認情況下,線程被創建成可結合的,為了避免存儲器漏洞,每個可集合的線程都應該要麽被其他進程顯式的回收,要麽通過調用pthread _detach函數被分離。

初始化線程

  • pthread _once函數允許初始化與線程例程相關的狀態。
  • once _control變量是一個全局或者靜態變量,總是被初始化為PTHREAD _ONCE _INIT

一個基於線程的並發服務器

  • 對等線程的賦值語句和主線程的accept語句之間引入了競爭。

多線程程序中的變量共享

線程存儲器模型

  • 每個線程和其他線程一起共享進程上下文的剩余部分。包括整個用戶虛擬地址空間,是由只讀文本、讀/寫數據、堆以及所有的共享庫代碼和數據區域組成的。線程也共享同樣的打開文件的集合。
  • 任何線程都可以訪問共享虛擬存儲器的任意位置。寄存器是從不共享的,而虛擬存儲器總是共享的。

將變量映射到存儲器

  • 全局變量:虛擬存儲器的讀/寫區域只會包含每個全局變量的一個實例。
  • 本地自動變量:定義在函數內部但沒有static屬性的變量。
  • 本地靜態變量:定義在函數內部並有static屬性的變量。
  • 技術分享圖片

共享變量

  • 變量v是共享的,當且僅當它的一個實例被一個以上的線程引用。

用信號量同步線程

  • 共享變量引入了同步錯誤的可能性。
  • 線程i的循環代碼分解為五部分:
    • Hi:在循環頭部的指令塊
    • Li:加載共享變量cnt到寄存器%eax的指令,%eax表示線程i中的寄存器%eax的值
    • Ui:更新(增加)%eax的指令
    • Si:將%eaxi的更新值存回到共享變量cnt的指令
    • Ti:循環尾部的指令塊。

進度圖

  • 進度圖將指令執行模式化為從一種狀態到另一種狀態的轉換。轉換被表示為一條從一點到相鄰點的有向邊。合法的轉換是向右或者向上。
  • 臨界區:對於線程i,操作共享變量cnt內容的指令構成了一個臨界區。
  • 互斥的訪問:確保每個線程在執行它的臨界區中的指令時,擁有對共享變量的互斥的訪問。
  • 安全軌跡線:繞開不安全區的軌跡線
  • 不安全軌跡線:接觸到任何不安全區的軌跡線就叫做不安全軌跡線
  • 任何安全軌跡線都能正確的更新共享計數器。

信號量

  • 當有多個線程在等待同一個信號量時,你不能預測V操作要重啟哪一個線程。
  • 信號量不變性:一個正在運行的程序絕不能進入這樣一種狀態,也就是一個正確初始化了的信號量有一個負值。
  • 信號量定義:
  • type semaphore=record
     count: integer;
     queue: list of process
    end;
     var s:semaphore;

使用信號量來實現互斥

  • 基本思想是將每個共享變量(或者一組相關的共享變量)與一個信號量s(初始為1)聯系起來,然後用P和V操作將相應的臨界區包圍起來。
  • 幾個概念
    • 二元信號量:用這種方式來保護共享變量的信號量叫做二元信號量,取值總是0或者1.
    • 互斥鎖:以提供互斥為目的的二元信號量
    • 加鎖:對一個互斥鎖執行P操作
    • 解鎖;對一個互斥鎖執行V操作
    • 計數信號量:被用作一組可用資源的計數器的信號量
    • 禁止區:由於信號量的不變性,沒有實際可能的軌跡能夠包含禁止區中的狀態。

利用信號量來調度共享資源

  • 信號量的作用:
    • 提供互斥
    • 調度對共享資源的訪問
  • 生產者—消費者問題:
    • 生產者產生項目並把他們插入到一個有限的緩沖區中,消費者從緩沖區中取出這些項目,然後消費它們。
  • 讀者—寫者問題:
    • 讀者優先,要求不讓讀者等待,除非已經把使用對象的權限賦予了一個寫者。
    • 寫者優先,要求一旦一個寫者準備好可以寫,它就會盡可能地完成它的寫操作。
    • 饑餓就是一個線程無限期地阻塞,無法進展。

使用線程提高並行性

  • 寫順序程序只有一條邏輯流,寫並發程序有多條並發流,並行程序是一個運行在多個處理器上的並發程序。並行程序的集合是並發程序集合的真子集。

其他並發問題

線程安全

  • 線程安全:當且僅當被多個並發線程反復地調用時,它會一直產生正確的結果。
  • 線程不安全:如果一個函數不是線程安全的,就是線程不安全的。
  • 線程不安全的類:
    • 不保護共享變量的函數
    • 保持跨越多個調用的狀態的函數。
    • 返回指向靜態變量的指針的函數。解決辦法:重寫函數和加鎖拷貝。
    • 調用線程不安全函數的函數。

可重入性

  • 可重入函數:當它們被多個線程調用時,不會引用任何共享數據。可重入函數是線程安全函數的一個真子集 。
  • 關鍵思想是我們用一個調用者傳遞進來的指針取代了靜態的next變量。
  • 顯式可重入:沒有指針,沒有引用靜態或全局變量
    隱式可重入:允許它們傳遞指針
  • 可重入性即使調用者也是被調用者的屬性,並不只是被調用者單獨的屬性。

在線程化的程序中使用已存在的庫函數

  • 使用線程不安全函數的可重入版本,名字以_r為後綴結尾。

競爭

  • 競爭發生的原因:
    • 一個程序的正確性依賴於一個線程要在另一個線程到達y點之前到達它的控制流中的x點。也就是說,程序員假定線程會按照某種特殊的軌跡穿過執行狀態空間,忘了一條準則規定:線程化的程序必須對任何可行的軌跡線都正確工作。
  • 消除方法:動態的為每個整數ID分配一個獨立的塊,並且傳遞給線程例程一個指向這個塊的指針

死鎖

  • 死鎖:一組線程被阻塞了,等待一個永遠也不會為真的條件。
  • 程序員使用P和V操作順序不當,以至於兩個信號量的禁止區域重疊。
  • 重疊的禁止區域引起了一組稱為死鎖區域的狀態。
  • 死鎖是一個相當難的問題,因為它是不可預測的。
  • 互斥鎖加鎖順序規則:如果對於程序中每對互斥鎖(s,t),給所有的鎖分配一個全序,每個線程按照這個順序來請求鎖,並且按照逆序來釋放,這個程序就是無死鎖的。

實踐

count.c

代碼

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NLOOP 5000
int counter;
void *doit( void * );
int main(int argc, char **argv)
{
    pthread_t tidA, tidB;

    pthread_create( &tidA ,NULL, &doit, NULL );
    pthread_create( &tidB ,NULL, &doit, NULL );

    pthread_join( tidA, NULL );
    pthread_join( tidB, NULL );

    return 0;
}
void * doit( void * vptr)
{
    int i, val;

    for ( i=0; i<NLOOP; i++ ) {
        val = counter++;
        printf("%x: %d \n", (unsigned int) pthread_self(), val + 1);
        counter = val + 1;
    }

}

技術分享圖片

  • 這是一個不加鎖的情況,兩個線程共享同一變量都實現加1操作的程序,在這個程序中雖然每個線程都給count加了5000,但由於結果的互相覆蓋,最終輸出值不是10000,而是5000。
  • 不過在後續的調試中,也不完全都是5000,有時少於5000有時比5000多,可能因為隨機覆蓋使得counter值不固定。

countwithmutex.c

代碼

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define NLOOP 5000

int counter;

pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;

void *doit( void * );

int main(int argc, char **argv)
{
    pthread_t tidA, tidB;

    pthread_create( &tidA ,NULL, &doit, NULL );
    pthread_create( &tidB ,NULL, &doit, NULL );

    pthread_join( tidA, NULL );
    pthread_join( tidB, NULL );

    return 0;
}

void * doit( void * vptr)
{
    int i, val;

    for ( i=0; i<NLOOP; i++ ) {
        pthread_mutex_lock( &counter_mutex );
        val = counter++;
        printf("%x: %d \n", (unsigned int) pthread_self(), val + 1);
        counter = val + 1;
        pthread_mutex_unlock( &counter_mutex );
    }
    return NULL;
}

技術分享圖片

  • 程序首先定義了一個宏PTHREAD_MUTEX_INITIALIZER來靜態初始化互斥鎖。先創建tidA線程後運行doit函數,利用互斥鎖鎖定資源,進行計數,執行完畢後解鎖。後創建tidB,與tidA交替執行。由於定義的NLOOP值為5000,所以程序最後的輸出值為10000.程序的最後還需要分別回收tidA和tidB的資源。
  • 相對於前一個實例,這個代碼中加了“互斥鎖”(Mutex),在其中一個線程(獲得鎖)執行時,另一個(未獲得)只能等待,所以產生了不同於count.c的輸出效果。

share.c

代碼

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
char buf[BUFSIZ];

void *thr_fn1( void *arg )
{
    printf("thread 1 returning %d\n", getpid());
    printf("pwd:%s\n", getcwd(buf, BUFSIZ));
    *(int *)arg = 11;
    return (void *) 1;
}

void *thr_fn2( void *arg )
{
    printf("thread 2 returning %d\n", getpid());
    printf("pwd:%s\n", getcwd(buf, BUFSIZ));
    pthread_exit( (void *) 2 );
}

void *thr_fn3( void *arg )
{
    while( 1 ){
        printf("thread 3 writing %d\n", getpid());
        printf("pwd:%s\n", getcwd(buf, BUFSIZ));
        sleep( 1 );
    }
}
int n = 0;

int main( void )
{
    pthread_t tid;
    void *tret;

    pthread_create( &tid, NULL, thr_fn1, &n);
    pthread_join( tid, &tret );
    printf("n= %d\n",  n );
    printf("thread 1 exit code %d\n", (int) tret );

    pthread_create( &tid, NULL, thr_fn2, NULL);
    pthread_join( tid, &tret );
    printf("thread 2 exit code %d\n", (int) tret );
    
    pthread_create( &tid, NULL, thr_fn3, NULL);
    sleep( 3 );
    pthread_cancel(tid);
    pthread_join( tid, &tret );
    printf("thread 3 exit code %d\n", (int) tret );
    
}

技術分享圖片

  • 該代碼主要是為了獲得線程的終止狀態,thr_fn 1,thr_fn 2和thr_fn 3三個函數對應終止線程的三種方法
    • 從線程函數return
    • 調用pthread_exit終止自己
    • 調用pthread_cancel終止同一進程中的另一個線程

其他(感悟、思考等,可選)

  • 並發是一個之前沒有見過的不同的機制,說沒見過也不可能,我們使用的任何一個操作系統,哪個是只能在一個時間段上運行一個程序嗎,都是可以重疊的,而經過本章節的學習,從程序級的角度了解到了並發,並進行了實踐,這就是對書本理論的一個鞏固。
  • 但感覺自己還是沒有太弄懂,可能是快期末了,事情太多,有些時候精力就顧不上了,還是要提高效率啊,唉……
  • 我對並發的理解:並發執行只是宏觀上的。在操作系統的管理下,所有正在運行的進程輪流使用CPU,每個進程允許占用CPU的時間非常短(比如10毫秒),這樣用戶根本感覺不出來CPU是在輪流為多個進程服務,就好象所有的進程都在不間斷地運行一樣。微觀上一個cpu在同一時間一次還是只能執行一個進程。
  • 然後參考了一下別人的博客

參考資料

  • 《深入理解計算機系統》
  • 2017-2018-1 《信息安全系統設計基礎》教學進程

20155236 《信息安全系統設計基礎》第13周學習總結