20155236 《信息安全系統設計基礎》第13周學習總結
阿新 • • 發佈:2017-12-17
在操作 -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周學習總結