標籤(空格分隔): 20135321餘佳源html
第十三週(11.30-12.06):
學習計時:共10小時
讀書:5
代碼:2
做業:1
博客:2程序員
1、學習目標web
每一個網絡應用都是基於客戶端一服務器模型的。一個應用是由一個服務器進程和一個或者多個客戶端進程組成。服務器管理某種資源,而且經過操做這種資源來爲它的客戶端提供某種服務。
客戶端一服務器模型中的基本操做是事務,由四步組成:數據庫
一個客戶端-服務器事務:
編程
客戶端和服務器一般運行在不一樣的主機上,而且經過計算機網絡的硬件和軟件資源來通訊。
對於一個主機而言,網絡只是又一種I/O設備,做爲數據源和數據接收方。數組
一個網絡主機的硬件組成:
瀏覽器
物理上而言,網絡是一個按照地理遠近組成的層次系統。最低層是 LAN (Local Area Network,局域網),最流行的局域網技術是以太網 (Ethernet)。
一個以太網段 (Ethernet segment) 包括一些電纜(一般是雙絞線)和一個叫作集線器的小盒子。緩存
每一個以太網適配器都有一個全球惟一的 48 位地址,它存儲在這個適配器的非易失性存儲器上。一臺主機能夠發送 一段位,稱爲幀(frame),到這個網段內其餘任何主機。每一個幀包括一些固定數量的頭部 (header) 位,用來標識此幀的源和目的地址以及此幀的長度,此後緊隨的就是數據位的有效載荷。每一個主機適配器都能看到這個幀,可是隻有目的主機實際讀取它。安全
使用一些電纜和叫作網橋 (bridge) 的小盒子,多個以太網段能夠鏈接成較大的局域網,稱爲橋接以太網 (bridged Ethernet)服務器
橋接以太網:
網橋比集線器更充分地利用了電纜帶寬。
多個不兼容的局域網能夠經過叫作路由器 (router)的特殊計算機鏈接起來,組成一個internet(互聯網絡)。
Internet 和 internet 咱們老是用小寫字母的 internet 描述通常概念, 而用大寫字母的Internet 來描述一種具體的實現,也就是所謂的全球 IP 因特網。
路由器能夠用來由各類局域網和廣域網構建互聯網絡。
互聯網絡相當重要的特性是,它能由採用徹底不一樣和不兼容技術的各類局域網和廣域網組成。
運行在每臺主機和路由器上的協議軟件,它消除了不一樣網絡之間的差別。提供兩種基本能力 :
命名機制。不一樣的局域網技術有不一樣和不兼容的方式來爲主機分配地址。互聯網絡協議經過定義一種一致的主機地址格式消除了這些差別。每臺主機會被分配至少二個這種互聯網絡地址 (internet address),這個地址惟一地標識了這臺主機。
傳送機制。在電纜上編碼位和將這些位封裝成幀方面,不一樣的聯網技術有不一樣的和不兼容的方式。互聯網絡協議經過定義一種把數據位捆紮成不連續的片(稱爲包)的統一方式,從而消除了這些差別。一個包是由包頭和有效載荷組成的,其中包頭包括包的大小以及源主機和目的主機的地址,有效載荷包括從源主機發出的數據位。
在互聯網絡上,數據是如何從一臺主機傳送到另外一臺主機的:
每臺因特網主機都運行實現 TCP/IP 協議 (Transmission Control Protocol/Internet Protocol,傳輸控制協議/互聯網絡協議)的軟件,幾乎每一個現代計算機系統都支持這個協議。
因特網的客戶端和服務器混合使用套接字接口函數和 Unix I/O 函數來進行通訊。套接字函數典型地是做爲會陷入內核的系統調用來實現 的,並調用各類內核模式的 TCP/IP 函數。
TCP/IP 其實是一個協議族,其中每個都提供不一樣的功能。
把因特網看作一個世界範圍的主機集合,知足如下特性:
一個 IP 地址就是一個 32 位無符號整數。
IP地址結構:
由於因特網主機能夠有不一樣的主機字節順序, TCP/IP 爲任意整數數據項定義了統一的網絡字節順序 (network byte order) (大端字節順序)。
IP 地址,它放在包頭中跨過網絡被攜帶。
IP 地址結構中存放的地址老是以(大端法)網絡宇節順序存放的,即便主機字節順序 (host byte order) 是小端法。
網絡和主機字節順序間轉換:
htonl 函數將 32 位整數由主機字節順序轉換爲網絡字節順序。 ntohl 函數將 32 位整數從網絡宇節順序轉換爲主機字節。 htons和 ntohs 函數爲 16 位的整數執行相應的轉換。
IP 地址一般是以一種稱爲點分十進制表示法來表示的。
因特網程序使用 inet_aton 和 inet_ntoa 函數來實現 IP 地址和點分十進制串之間的轉換:
inet_aton 函數將一個點分十進制串 (cp) 轉換爲一個網絡字節順序的 IP 地址 (inp)。類似地, inet_ntoa 函數將一個網絡字節順序的 IP 地址轉換爲它所對應的點分十進制串。
對 inet_aton 的調用傳遞的是指向結構的指針,而對 inet_ntoa 的調用傳遞的是結構自己。
"n" 表示的是網絡(network)。"a" 表示應用(application)。而 "to" 表示轉換。
因特網客戶端和服務器互相通訊時使用的是 IP 地址。
子樹稱爲子域 (subdomain)。 層次結構中的第一層是 一個未命名的根節點。下一層是一組一級域名 (first-level domain name)。
常見的第一層域名包括 com、 edu、 gov、org 和 net。
下一層是二級 (second-level) 域名,例如 cmu. edu
一旦一個組織獲得了一個二級域名,那麼它就能夠在這個子域中建立任何新的域名了。
因特網定義了域名集合和 IP 地址集合之間的映射。
因特網應用程序經過調用 gethostbyname 和 gethostbyaddr 函數,從 DNS 數據庫中檢索任意的主機條目。
gethostbyname 函數返回和域名 name 相關的主機條目。 gethostbyaddr 函數返回和 IP 地址 addr 相關聯的主機條目。第二個參數給出了一個 IP 地址的字節長度,對於目前的因特網而言老是四個字節。對於咱們的要求來講,第三個參數老是0。
每臺因特網主機都有本地定義的域 名localhost,這個域名老是映射爲本地回送地祉 (loopback address) 127.0.0.1
因特網客戶端和服務器經過在鏈接上發送和接收字節流來通訊。
鏈接是點對點的。
從數據能夠同時雙向流動的角度來講,它是全雙工的。可靠的。
一個套接字是鏈接的一個端點。每一個套接字都有相應的套接字地址,是由一個因特網地址和 一個 16 位的整數端口組成的,用"地址 : 端口"來表示。當客戶端發起一個鏈接請求時,客戶 端套接字地址中的端口是由內核自動分配的,稱爲臨時端口(ephemeral port)。服務器套接字地址中的端口一般是某個知名的端口,是和這個服務相對應的。
Web 服務器一般使用端口 80,而電子郵件服務器使用端口 25。
一個鏈接是由它兩端的套接字地址惟一肯定的。這對套接字地址叫作套接字對 (socket pair) ,由下列元組來表示:
cliaddr 是客戶端的 IP 地址, cliport 是客戶端的端口, servaddr 是服務器的 IP 地址, 而 servport 是服務器的端口。
因特網鏈接分析:
套接字接口 (socket interface) 是一組函數,它們和 Unix I/O 函數結合起來,用以建立網絡 應用。
套接字接口描述:
套接字結構:
_in 後綴是互聯網絡 (internet) 的縮寫,而不是輸入 (input) 的縮寫。
connect、 bind 和 accept 函數要求一個指向與協議相關的套接字地址結構的指針。
客戶端和服務器使用 socket 函數來建立一個套接字描述符
調用 socket 函數:
AF_INET 代表咱們正在使用因特網,而 SOCK_STREAM 表示這個套接字是因特網鏈接的一個端點。 socket 返回的 clientfd 描述符僅是部分打開的,還不能用於讀寫。
客戶端經過調用 connect 函數來創建和服務器的鏈接。
connect 函數試圖與套接字地址爲 serv addr 的服務器創建一個因特網鏈接,其中 addrlen 是 sizeof(sockaddr in). connect 函數會阻塞,一直到鏈接成功創建或是發生錯誤。若是成功, sockfd描述符如今就準備好能夠讀寫了,而且獲得的鏈接是由套接字對
(x:y, serv_addr.sin_addr:serv_addr.sin_port)
x 表示客戶端的 IP 地址,而 y 表示臨時端口,它惟一地肯定了客戶端主機上的客戶端進程。
open_clientfd 函數和運行在主機 hostname 上的服務器創建一個鏈接,並在知名端口 port 上監聽鏈接請求。它返回一個打開的套接宇描述符,該描述符準備好了,能夠用 Unix I/O 函數作輸入和輸出。
bind、 listen 和 accept 被服務器用來和客戶端創建鏈接。
bind 函數告訴內核將 my_addr 中的服務器套接字地址和套接字描述符 sockfd 聯繫起來。 參數 addrlen 就是 sizeof(sockaddr_in) 。
客戶端是發起鏈接請求的主動實體。服務器是等待來自客戶端的鏈接請求的被動實體。默認狀況下,內核會認爲 socket 函數建立的描述符對應於主動套接字 (active socket),它存在 於一個鏈接的客戶端。服務器調用 listen 函數告訴內核,描述符是被服務器而不是客戶端使用的。
listen 函數將 sockfd 從一個主動套接字轉化爲一個監聽套接字 (listening socket),該套接字能夠接受來自客戶端的鏈接請求。 backlog 參數暗示了內核在開始拒絕鏈接請求以前,應該放人隊列中等待的未完成鏈接請求的數量。
socket、 bind 和 listen 函數結合成一個叫作。open_listenfd 的輔助函數
open_listenfd 函數打開和返回一個監聽描述符,這個描述符準備好在知名端口 port 上接收鏈接請求。
accept 函數來等待來自客戶端的鏈接請求
accept 函數等待來自客戶端的鏈接請求到達偵聽描述符 listenfd,而後在 addr 中填寫客戶端的套接字地址,並返回一個巳鏈接描述符 (connected descriptor),這個描述符可被用來利用 Unix I/O 函數與客戶端通訊。
監聽描述符是做爲客戶端鏈接請求的一個端點。典型地,它被建立一次,並存在於服務器的整個生命週期。已鏈接描述符是客戶端和服務器之間已經創建起來了的鏈接的一個端點。服務器每次接受鏈接請求時都會建立一次, 它只存在於服務器爲一個客戶端服務的過程當中。
監聽描述符和己鏈接描述符的角色:
Web 客戶端和服務器之間的交互用的是一個基於文本的應用級協議,叫作 HTTP (Hypertext Transfer Protocol,超文本傳輸協議). HTTP 是一個簡單的協議。一個 Web 客戶端(即瀏覽器) 打開一個到服務器的因特網鏈接,而且請求某些內容。服務器響應所請求的內容,而後關閉鏈接。瀏覽器讀取這些內容,並把它顯示在屏幕上。
Web 內容能夠 用一種叫作 HTML (Hypertext Markup Language,超文本標記語言)的語言來編寫。一個 HTML 程序(頁)包含指令(標記),它們告訴瀏覽器如何顯示這頁中的各類文本和圖形對象。
Web 服務器以兩種不一樣的方式向客戶端提供內容:
MIME 類型示例:
每條由 Web 服務器返回的內容都是和它管理的某個文件相關聯的。這些文件中的每個都有一個惟一的名字,叫作 URL (Universal Resource Locator,通用資源定位符)。
URL 的後綴:
由於 HTTP 是基於在因特網鏈接上傳送的文本行的,咱們可使用 Unix 的TELNET程序來和因特網上的任何 Web 服務器執行事務。
一個 HTTP 請求的組成是這樣的:一個請求行 (request line) (第 5 行),後面跟隨零個或更多個請求報頭 (request header) (第 6 行),再跟隨一個空的文本行來終止報頭列表
HTTP 支持許多不一樣的方法,包括 GET、 POST、 OPTIONS、 HEAD、 PUT、 DELETE 和 TRACE。
GET 方法指導服務器生成和返回URI (Uniform Resource Identifier,統一資源標識符)標識的內容。 URI是相應的 URL 的後綴,包括文件名和可選的參數。
代理緩存 (proxy cache) 會使用 Host 報頭,這個代理 緩存有時做爲瀏覽器和管理被請求文件的原始服務器 (origin server) 的中介。客戶端和原始服務器之間,能夠有多個代理,即所謂的代理鏈 (proxy chain)。 Host 報頭中的數據指示了原始服務器 的域名,使得代理鏈中的代理可以判斷它是否能夠在本地緩存中擁有一個被請求內容的副本。
一個 HTTP 響應的組成是這樣的:一個響應行 (response line) (第 8 行)後面跟隨着零個或更多的響應報頭 (response header) (第 9 ~ 13 行), 再跟隨一個終止報頭的空行(第 14 行),再跟隨一個響應主體 (response body)
狀態碼 (status code) 是一個三位的正整數, 指明對請求的處理。狀態消息 (status message) 給出與錯誤代碼等價的英文描述。
每一個網絡應用都是基於客戶端-服務器模型的。
一個應用是由一個服務器和一個或多個客戶端組成的。服務器管理資源,以某種方式操做資源,爲它的客戶端提供服務。
客戶端一服務器模型中的基本操做是客戶端一服務器事務,它是由客戶端請求和跟隨其後的服務器響應組成的。
能夠把因特網當作是一個全球範圍的主機集合,具備如下幾個屬性:1)每一個因特網主機都有一個惟一的 32 位名字,稱爲它的 IP 地址。 2) IP 地址的集合被映射爲一個因特網域名的集合。 3) 不一樣因特 網主機上的進程可以經過鏈接互相通訊。
客戶端和服務器經過使用套接字接口創建鏈接。一個套接字是鏈接的一個端點,鏈接是以文件描述符的形式提供給應用程序的。套接字接口提供了打開和關閉套接宇描述符的函數。客戶端和服務器經過讀寫這些描述符來實現彼此間的通訊。
Web 服務器使用 HTTP 協議和它們的客戶端(例如瀏覽器)彼此通訊。瀏覽器向服務器請 求靜態或者動態的內容。對靜態內容的請求是經過從服務器磁盤取得文件並把它返回給客戶端來服務的。對動態內容的請求是經過在服務器上一個子進程的上下文中運行一個程序並將它的 輸出返回給客戶端來服務的。CGI 標準提供了一組規則,來管理客戶端如何將程序參數傳遞給服務器,服務器如何將這些參數以及其餘信息傳遞給子進程,以及子進程如何將它的輸出發送回客戶端。
邏輯控制流在時間上重疊,那麼它們就是併發的。
併發(concurrency ) ,出如今計算機系統的許多不一樣層面上。
應用級併發是頗有用的:
使用應用級併發的應用程序稱爲併發程序 (concurrent program)。現代操做系統提供了三種基本的構造併發程序的方法:
進程。每一個邏輯控制流都是一個進程,由內核來調度和維護。由於進程 有獨立的虛擬地址空間,想要和其餘流通訊,控制流必須使用某種顯式的進程間通訊 (interprocess communication, IPC) 機制。
I/O 多路複用。在這種形式的併發編程中,應用程序在一個進程的上下文中顯式地調度它們本身的邏輯流。邏輯流被模型化爲狀態機,數據到達文件描述符後,主程序顯式地從一個狀態轉換到另外一個狀態。由於程序是一個單獨的進程,因此全部的流都共享同一個地址空間。
線程。線程是運行在一個單一進程上下文中的邏輯流,由內核進行調度。是其餘兩種方式的混合體,像進程流同樣由內核進行調度,而像I/O 多路複用流同樣共享同一個虛擬地址空間。
構造併發程序最簡單的方法就是用進程。
一個構造併發服務器的天然方法就是,在父進程中接受客戶端鏈接請求,而後建立一個新的子進程來爲每一個新客戶端提供服務。
服務器正在監昕一個監聽描述符(好比描述符 3)上的鏈接請求。如今假設服務器接受了客戶端 1 的鏈接請求, 並返回一個已鏈接描述符(好比描述符4)。
在接受鏈接請求以後,服務器派生一個子進程,這個子進程得到服務器描述符表的完整拷貝。子進程關閉它的拷貝端的鏈接請求中的監聽描述符 3,而父進程關閉它的己鏈接描述符 4 的拷貝,由於再也不須要這些描述符了。
其中子進程正忙於爲客戶端提供服務。由於父、子進程中的已鏈接描述符都指向同一個文件表表項,因此父進程關閉它的已鏈接描述符的拷貝是相當重要的。不然,將永遠不會釋放已鏈接描述符 4 的文件表條目,並且由此 引發的存儲器泄漏將最終消耗盡可用的存儲器,使系統崩潰。
第一步:服務器接受客戶端的鏈接請求:
父進程爲客戶端 1 建立了子進程以後,它接受一個新的客戶端 2 的鏈接請求, 並返回一個新的已鏈接描述符(好比描述符5),而後,父進程又派生另外一個子進程,這個子進程用已鏈接描述符 5 爲它的客戶端提供服務。
此時,父進程正在等待下一個鏈接請求,而兩個子進程正在併發地爲它們各自的客戶端提供服務。
第二步:服務器派生一個子進程爲這個客戶端服務:
第三步:服務器接受另外一個鏈接請求:
第四步:服務器派生另外一個子進程爲新的客戶端服務
基於進程的併發 echo 服務器.父進程派生一個子進程來處理每一個新的鏈接請求:
在父、子進程間共享狀態信息,進程有一個很是清晰的模型 : 共享文件表,可是不共享用戶地址空間。進程有獨立的地址空間既是優勢也是缺點。一個進程不可能不當心覆蓋另外一個進程的虛擬存儲器,這就消除了許多使人迷惑的錯誤一一這是一個明顯的優勢。
另外一方面,獨立的地址空間使得進程共享狀態信息變得更加困難。爲了共享信息,它們必須使用顯式的IPC(進程間通訊)機制。基於進程的設計的另外一個缺點是,它們每每比較慢,由於進程控制和 IPC 的開銷很高。
I/O 多路複用(I/O multiplexing) 技術。基本的思路就是使用 select 函數,要求內核掛起進程,只有在一個或多個I/O事件發生後,纔將控制返回給應用程序
select函數:
select 函數處理類型爲 fd_set 的集合,也叫作描述符集合。邏輯上,咱們將描述符集合當作一個大小爲 n 的位向量。
每一個位 bk對應於描述符 k。當且僅當 bk= 1, 描述符 k才代表是描述符集合的一個元素。只容許你對描述符集合作三件事: 1) 分配它們, 2) 將一個此種類型的變量賦值給另外一個變量, 3) 用 FD_ZERO、 FD_SET、 FD_CLR 和 FD_ISSET 宏指令來修改和檢查它們。
select 函數有兩個輸人 : 一個稱爲讀集合的描述符集合(fdset)和該 讀集合的基數 (n) (其實是任何描述符集合的最大基數). select 函數會一直阻塞,直到讀集合中至少有一個描述符準備好能夠讀。當且僅當一個從該描述符讀取一個字節的請求不會阻塞時,描述符 k就表示準備好能夠讀了。做爲一個反作用, select 修改了參數 fdset 指向的 fd_set,指明讀集合中一個稱爲準備好集合 (ready set) 的子集,這個集合是由讀集合中準備好能夠讀了的描述符組成的。函數返回的值指明瞭準備好集合的基數。注意,因爲這個反作用, 咱們必須在每次調用 select 時都更新讀集合。
使用 I/O 多路複用的 echo 服務器。服務器使用 select 等待監聽描述符上的鏈接請求和標準輸入上的命令:
不是調用 accept 函數來等待一個鏈接請求,而是調用 select 函數,這個函數會一直阻塞,直到監聽描述符或者標準輸入準備好能夠讀。
一旦 select 返回,咱們就用 FD_ISSET 宏指令來判斷哪一個描述符準備好能夠讀了。
一旦它鏈接到某個客戶端,就會連續回送輸入行,直到客戶端關閉這個鏈接中它的那一端。所以,若是你鍵入一個命令到標準輸入,你將不會獲得響應,直到服務器和客戶端之間結束。一個 更好的方法是更細粒度的多路複用,服務器每次循環〈至多)回送一個文本行。
I/O 多路複用能夠用作併發事件驅動 (event-driven) 程序的基礎,在事件驅動程序中,流是由於某種事件而前進的。
將邏輯流模型化爲狀態機。不嚴格地說,一個狀態機 (state machine) 就是一組狀態 (state)、輸入事件(input event) 和轉移他(transition),其中轉移就是將狀態和輸入事件映射到狀態。每一個轉移都將一個(輸入狀態,輸入事件)對映射到一個輸出狀 態。自循環(self-loop) 是同一輸入和輸出狀態之間的轉移。節 點表示狀態,有向弧表示轉移,而弧上的標號表示輸入事件。一個狀態機從某種初始狀態開始執行。每一個輸入事件都會引起一個從當前狀態到下一狀態的轉移。
併發事件驅動 echo 服務器中邏輯流的狀態機:
服務器使用I/O多路複用,藉助 select 函數檢測輸入事件的發生。
服務器調用 select 函數來 檢測兩種不一樣類型的輸人事件: a) 來自一個新客戶端的鏈接請求到達, b) 一個己存在的客戶 端的己鏈接描述符準備好能夠讀了。
基於I/O 多路複用的併發 echo 服務器。每次服務器迭代都回送來自每一個準備好的描述符的文本行:
init_pool 函數初始化客戶端池。 clientfd 數組表示已鏈接描述符的集合, 其中整數 -1 表示一個可用的槽位。初始時,已鏈接描述符集合是空的,並且監聽描述符是 select 讀集合中惟一的描述符。
init_pool: 初始化活動客戶端池:
add_clieht函數添加一個新的客戶端到活動客戶端池中。在 clientfd 數組中找到一個空槽位後,服務器將這個已鏈接描述符添加到數組中,並初始化相應的RIO讀緩衝區,這樣一來咱們就可以對這個描述符調用rio_readlineb。將這個已鏈接描述符添加到 select 讀集合,並更新該池的一些全局屬性。 maxfd 變量記錄了 select 的最大文件描述符。 maxi 變量記錄的 是到 clientfd數組的最大索引,這樣 check_clients 函數就無需搜索整個數組了。
check_clients 函數回送來自每一個準備好的已鏈接描述符的一個文本行。 若是成功地從描述符讀取了一個文本行,那麼咱們就將該文本行回送到客戶端
add_client: 向池中添加一個新的客戶端鏈接:
check_clients: 爲準備好的客戶端鏈接服務:
select 函數檢測到輸入事件,而 add_client 函數建立 一個新的邏輯流(狀態機). check_clients 函數經過回送輸入行來執行狀態轉移,並且當客 戶端完成文本行發送時,它還要刪除這個狀態機。
事件驅動設計的一個優勢是,它比基於進程的設計給了程序員更多的對程序行爲的控制。
另外一個優勢是,一個基於 I/O 多路複用的事件驅動服務器是運行在單一進程上下文中的,因 此每一個邏輯流都能訪問該進程的所有地址空間。
事件驅動設計的一個明顯的缺點就是編碼複雜。咱們的事件驅動的併發 echo 服務器須要的代碼比基於進程的服務器多三倍。不幸的是,隨着併發粒度的減少,複雜性還會上升。這裏的粒度是指每一個邏輯流每一個時間片執行的指令數量。
線程(thread) 就是運行在進程上下文中的邏輯流。
每一個線程都有它本身的線程上下文 (thread context),包括一個惟一的整數線程 (Thread ID, TID)、棧、棧指針、程序計數器、通用目的寄存器和條件碼。全部的運行在一個進程裏的線程共享該進程的整個虛擬地址空間。
基於線程的邏輯流結合了基於進程和基於 I/O 多路複用的流的特性。同進程同樣,線程由內核自動調度,而且內核經過一個整數 ID 來識別線程。同基於 I/O 多路複用的流同樣,多個線程 運行在單一進程的上下文中,所以共享這個進程虛擬地址空間的整個內容,包括它的代碼、數據、堆、共享庫和打開的文件。
每一個進程開始生命週期時都是單一線程,這個線程稱爲主線程 (main thread)。在某一時刻,主線程建立一個對等線程 (peer thread),從這個時間點開始,兩個線程就併發地運行。最後,因 爲主線程執行一個慢速系統調用。或者由於它被系統的間隔計時器中斷, 控制就會經過上下文切換傳遞到對等線程。對等線程會執行一段時間,而後控制傳遞迴主線程,依次類推。
由於一個線程的上下文要比一個進程的上下文小得多,線程的上下文切換要比 進程的上下文切換快得多。另外一個不一樣就是線程不像進程那樣,不是按照嚴格的父子層次來組織的。和一個進程相關的線程組成一個對等(線程)池 (pool),獨立於其餘線程建立的線程。主線程和其餘線程的區別僅在於它老是進程中第一個運行的線程。對等 (線程)池概念的主要影響是,一個線程可 以殺死它的任何對等線程,或者等待它的任意對等線程終止。另外,每一個對等線程都能讀寫相同的共享數據。
併發線程執行:
Posix 線程 (Pthreads) 是在 C 程序中處理線程的一個標準接口。Pthreads 定義了大約 60 個函數,容許程序建立、殺死和回收線程,與對等線程安全地共享數據,還能夠通知對等線程系統狀態的變化。
線程的代碼和本地數據被封裝在一個線程例程(thread routine) 中。若是想傳遞多個參數給錢程例程,那麼你應該將參數放 到一個結構中,並傳遞一個指向該結構的指針。想要線程例程返回多個參數,你能夠返回一個指向一個結構的指針。
線程經過調用 pthread create 函數來建立其餘線程。
pthread_create 函數建立一個新的線程,並帶着一個輸入變量arg,在新線程的上下文中運行線程例程f。能用attr參數來改變新建立線程的默認屬性。
當 pthread_create 返回時,參數 tid包含新建立線程的ID。新線程能夠經過調用 pthread_self 函數來得到它本身的線程 ID.
一個線程是如下列方式之一來終止的 :
經過調用 pthread_exit 函數,線程會顯式地終止。若是主線程調用 pthread_exit , 它會等待全部其餘對等線程終止,而後再終止主線程和整個進程,返回值爲 thread_return。
另外一個對等線程經過以當前線程ID做爲參數調用 pthread_cancle 函數來終止當前線程。
線程經過調用 pthread_join 函數等待其餘線程終止。
pthread_join 函數會阻塞,直到線程 tid 終止,將線程例程返回的 (void*) 指針賦值 爲 thread_return 指向的位置,而後回收己終止線程佔用的全部存儲器資源。
pthread join 函數只能等待一個指定的線程終止。
在任何一個時間點上,線程是可結合的 (joinable) 或者是分離的 (detached)。一個可結合的線程可以被其餘線程收回其資源和殺死。在被其餘線程回收以前,它的存儲器資源(例如棧)是沒有被釋放的。相反,一個分離的線程是不能被其餘線程回收或殺死的。它的存儲器資源在它終止時由系統自動釋放。
默認狀況下,線程被建立成可結合的。爲了不存儲器泄漏,每一個可結合線程都應該要麼被其餘線程顯式地收回,要麼經過調用 pthread_detach 函數被分離。
pthread_detach 函數分離可結合線程 tid. 線程可以經過以 pthread_self ()爲參數 的 pthread_detach 調用來分離它們本身。
pthread_once 函數容許你初始化與線程例程相關的狀態。
once_control 變量是一個全局或者靜態變量,老是被初始化爲 PTHREAD_ONCE_INIT。 當你第一次用參數 once_control 調用 pthread_once 時, 它調用 init_routine,這是一個沒有輸入參數,也不返回什麼的函數。
動態初始化多個線程共享的全局變量時, pthread_once 函數是頗有用的。
調用 pthread_ create 時,如何將已鏈接描述符傳遞給對等線程。最明顯的方法就是傳遞一個指向這個描述符的指針。
對等線程間接引用這個指針,並將它賦值給一個局部變量。
這樣可能會出錯,由於它在對等線程的賦值語句和主線程的 accept 語句間引入了競爭 (race)。若是賦值語句在下一個 accept 以前完成,那麼對等線程中的局部變量 connfd 就獲得正確的描述符值。然而,若是賦值語句是在 accept 以後才完成的,那麼對等線程中的 局部變量 connfd 就獲得下一次鏈接的描述符值。那麼不幸的結果就是,如今兩個線程在同一 個描述符上執行輸入和輸出。爲了不這種潛在的致命競爭,咱們必須將每一個 accept 返回的 已鏈接描述符分配到它本身的動態分配的存儲器塊
是在線程例程中避免存儲器泄漏。既不顯式地收回線程,就必須分離 每一個線程,使得在它終止時它的存儲器資源可以被收回。更進一步,咱們必須當心釋放主線程分配的存儲器塊
一組併發線程運行在一個進程的上下文中。每一個線程都有它本身獨立的線程上下文,包括線程 ID、棧、棧指針、程序計數器、條件碼和通用目的寄存器值。每一個線程和其餘線程一塊兒共享進程上下文的剩餘部分。這包括整個用戶虛擬地址空間,它是由只讀文本(代碼)、讀/寫數據、堆以及全部的共享庫代碼和數據區域組成的。線程也共享一樣的打開文件的集合。
讓一個線程去讀或寫另外一個線程的寄存器值是不可能的。另外一方 面,任何線程均可以訪問共享虛擬存儲器的任意位置。
保存在虛擬地址空間的棧區域中,而且一般是被相應的線程獨立地訪問的。
線程化的 C 程序中變量根據它們的存儲類型被映射到虛擬存儲器:
一個變量 v 是共亭的,當且僅當它的一個實例被一個以上的線程引用。
同步錯誤。
將線程 i 的循環代碼分解成五個部分:
注意頭和尾只操做本地棧變量,而 Li、 Ui 和 Si操做共享計數器變量的內容。
通常而言, 你沒有辦法預測操做系統是否將爲你的線程選擇一個正確的順 序。
進度圖 (progress graph) 的方法來闡明這些正確 的和不正確的指令順序的概念
進度圖 (progress graph) 將 n 個併發線程的執行模型化爲一條 n 維笛卡兒空間中的軌跡線。每條軸 k對應於線程 k 的進度。每一個表明線程 k 已經完成了指令 Ik 這一狀態。圖的原點對應於沒有任何線 程完成一條指令的初始狀態。
進度圖將指令執行模型化爲從一種狀態到另外一種狀態的轉換(transition)。轉換被表示爲一條從一點到相鄰點的有向邊。合法的轉換是向右(線程 1 中的一條指令完成〉或者向上(線程 2 中的一條指令完成)的。兩條指令不能在同一時刻完成一一對角線轉換是不容許的。程序決不會反向運行,因此向下或者向左移動的轉換也是不合法的。
一個程序的執行歷史被模型化爲狀態空間中的一條軌跡線。
繞開不安全區的軌跡線叫作安全軌跡線 (safe trajectory)。 相反,接觸到任何不安全區的軌跡線就叫作不安全軌跡線 (unsafe trajectory)。
一種解決同步不一樣執行線程問題的方法,這種方法是基於一種叫作信號量 (semaphore) 的特殊類型變量的。信號量 s 是具備非負整數值的全 局變量,只能由兩種特殊的操做來處理,這兩種操做稱爲 P 和 V:
P 中的測試和減 1 操做是不可分割的,也就是說,一旦預測信號量s 變爲非零,就會將s 減 1,不能有中斷。V中的加 1 操做也是不可分割的,也就是加載、加 1 和存儲信號量的過程當中沒 有中斷。注意, V的定義中沒有定義等待線程被從新啓動的順序。惟一的要求是V必須只能重啓一個正在等待的線程。所以,當有多個線程在等待同一個信號量時,你不能預測V操做要重啓哪個線程。
P 和 V的定義確保了一個正在運行的程序毫不可能進入這樣一種狀態,也就是一個正確初始化了的信號量有一個負值。這個屬性稱爲信號量不變性 (semaphore invariant),爲控制併發程序的軌跡線提供了強有力的工具
信號量的函數:
sem_init 函數將信號量 sem 初始化爲 value. 每一個信號量在使用前必須初始化。針對我 們的目的,中間的參數老是0。程序分別經過調用 sem_wait 和 sem_post 函數來執行 P 和 V 操做。
P 和 V的包裝函數:
基本思想是將每一個共享變量 (或者一組相關的共享變量)與一個信號量 s (初始爲1)聯繫起來,而後用 P(s) 和V(s) 操做將 相應的臨界區包圍起來。
保護共享變量的信號量叫作二元信號量 (binary semaphore),由於它的值老是 0 或者 1 。以提供互斥爲目的的二元信號量經常也稱爲互斥鎖 (mutex)。在一個互斥鎖上執行 P 操做稱爲對互斥鎖加鎖。相似地,執行 V操做稱爲對互斥鎖解鎖。對一個互斥鎖加了鎖可是尚未解鎖的線程稱爲佔用這個互斥鎖。一個被用做一組可用資源的計數器的信號量稱爲計數信號量。
關鍵思想是這種 P 和 V操做的結合建立了一組狀態, 叫作禁止區 (forbidden region),其中 s<O。由於信號量的不變性,沒有實際可行的軌跡線可以包含禁止區中的狀態。並且,由於禁止區徹底包括了不安全區,因此沒有實際可行的軌跡線可以接觸不安全區的任何部分。所以,每條實際可行的軌跡線都是安全的,並且無論運行時指令順序是怎樣的,程序都會正確地增長計數器的值。
使用信號量來互斥。 s<O 的不可行狀態定義了一個禁止區,禁止區徹底包括了不安全 區,阻止了實際可行的軌跡線接觸到不安全區:
由 P 和 V操做建立的禁止區使得在任什麼時候間點上,在被包圍的臨 界區中,不可能有多個線程在執行指令。換句話說,信號量操做確保了對臨界區的互斥訪問。
除了提供互斥以外,信號量的另外一個重要做用是調度對共享資源的訪問。
生產者一消費者問題。生產者產生項目並把它們插入到一個有限的緩衝區中。消費者從緩衝區中取出這些項目,而後消費它們
由於插入和取出項目都涉及更新共享變量,因此咱們必須保證對緩衝區的訪問是互斥的。可是隻保證互斥訪問是不夠的,咱們還須要調度對緩衝區的訪問。若是緩衝區是滿的(沒有空的槽位),那麼生產者必須等待直到有一個槽位變爲可用。與之類似,若是緩衝區是空的(沒有可取用的項目),那麼消費者必須等待直到有一個項目變爲可用。
基於預線程化 (prethreading) 的有趣的併發服務器。 SBUP 操做類型爲 sbuf_t 的有限緩衝區,項目存放在一個動態分配的 n 項整數數組 (buf) 中。 front 和 rear 索引值記錄該數組中的第一項和最後一項。三個信號量同步對緩 衝區的訪問。 mutex 信號量提供互斥的緩衝區訪問。 slots 和 items 信號量分別記錄空槽位和可用項目的數量。
sbuf_t: SBUF 包使用的有限緩衝區:
sbuf_init 函數爲緩衝區分配堆存儲器,設置 front 和 rear 表示一個空的緩衝區,併爲三個信號量賦初始值。這個函數在調用其餘三個函數中的任何一個以前調用一次。 sbuf_deinit函數是當應用程序使用完緩衝區時,釋放緩衝區存儲的。 sbuf_insert 函數等待一個可用的槽位,對互斥鎖加鎖,添加項目,對互斥鎖解鎖,而後宣佈有一個新項目可用。 sbuf_remove 函數是與 sbuf_insert 函數對稱的。在等待一個可用的緩衝區項目以後,對互斥鎖加鎖,從緩衝區的前面取出該項目,對互斥鎖解鎖,而後發信號通知一個新的槽位可供使用。
SBUF: 同步對有限緩衝區併發訪問的包:
有些線程只讀對象,而其餘的線程只修改對象。修改對象的線程叫作寫者。只讀對象的線程叫作讀者。寫者必須擁有對對象的獨佔的訪問,而讀者能夠和無限多個其餘的讀者共享對象。通常來講,有無限多個併發的讀者和寫者。
第一類讀者-寫者問題,讀者優先,要求不要讓讀者等待,除非已經把使用對象的權限賦予了一個寫者。換句話說,讀者不會由於有一個寫者在等待而等待。第二類讀者一寫者問題,寫者優先,要求一旦一個寫者準備好能夠寫,它就會盡量快地完成它的寫操做。
對這兩種讀者-寫者問題的正確解答可能致使飢餓 (starvation),飢餓就是一個線程無限期地阻塞,沒法進展。
預線程化的併發服務器的組織結構。一組現有的線程不斷地取出和處理來自有限緩衝區的已鏈接描述符:
順序、併發和並行程序集合之間的關係:
並行程序經常被寫爲每一個核上只運行一個線程。
並行程序的加速比 (speedup) 一般定義爲:
p 是處理器核的數量,凡是在 k個核上的運行時間。這個公式有時稱爲強擴展 (strong scaling)。當 T1 是程序順序執行版本的執行時間時, Sp 稱爲絕對加速比.(absolute speedup)。當 T1 是程序並行版本在一個核上的執行時間時, Sp 稱爲相對加速比 (relative speedup)。絕對加速 比比相對加速比能更真實地衡量並行的好處。
效率 (efficiency ) ,定義爲
一般表示爲範圍在 (0, 100] 之間的百分比。效率是對因爲並行化形成的開銷的衡量。具備高 效率的程序比效率低的程序在有用的工做上花費更多的時間,在同步和通訊上花費更少的時間。
加速比還有另一面,稱爲弱擴展 (weak scaling),在增長處理器數量的同時,增長問題的規模,這樣隨着處理器數量的增長,每一個處理器執行的工做量保持不變。加速比和效率被表達爲單位時間完成的工做總量。
當用線程編寫程序時,咱們必須當心地編寫那些具備稱爲線程安全性(thread safety) 屬性的畫數。一個函數被稱爲線程安全的(thread-safe),當且僅當被多個併發線程反覆地調用時,它會一直產生正確的結果。若是一個函數不是線程安全的,咱們就說它是線程不安全的(thread-unsafe)。
四個(不相交的)線程不安全函數類:
第 1 類: 不保護共享變量的函數。thread 函數中對一個未受保護的全局計數器變量加 1. 將這類線程不安全函數變成線程安全的, 相對而言比較容易:利用像P和 V操做這樣的同步操做來保護共享的變量。這個方法的優勢是在調用程序中不須要作任何修改。缺點是同步操做將減慢程序的執行時間。
第 2 類:保持跨越多個調用的狀態的函數。一個僞隨機數生成器是這類線程不安全函數的簡單例子。僞隨機數生成器程序包. rand 函數是線程不安全的,由於當前調用的結果依賴於前次調用的中間結果。當調用 srand 爲 rand 設置了一個種子後,咱們從一個單線程中反覆地調用 rand,可以預期獲得一個可重複的隨機數字序列。然而,若是多線程調用 rand 函數,這種假設就再也不成立了。
使得像 rand這樣的函數線程安全的惟一方式是重寫它,使得它再也不使用任何 static 數據,而是依靠調用者在參數中傳遞狀態信息。這樣作的缺點是,程序員如今還要被迫修改調用程序中的代碼。在一個大的程序中,可能有成百上千個不一樣的調用位置,作這樣的修改將是很是麻煩的,並且容易出錯。
第 3 類:返回指向靜態變量的指針的函數。某些函數,例如 ctime 和 gethostbyname,將計算結果放在一個 static 變量中,而後返回一個指向這個變量的指針。若是咱們從併發線程中調用 這些函數,那麼將可能發生災難,由於正在被一個線程使用的結果會被另外一個線程悄悄地覆蓋了。有兩種方法來處理這類線程不安全函數。一種選擇是重寫函數,使得調用者傳遞存放結果的變量的地址。這就消除了全部共享數據,可是它要求程序員可以修改函數的源代碼。
第 4 類:調用線程不安全函數的函數。若是函數f調用線程不安全函數 g,那麼f就是線程不安全的嗎?不必定。若是 g是第 2 類函數,即依賴於跨越屢次調用的狀態,那麼f也是線程不安全的,並且除了重寫 g 之外,沒有什麼辦法。然而,若是 g 是第 1 類或者第 3 類函數,那麼只 要你用一個互斥鎖保護調用位置和任何獲得的共享數據,f仍然多是線程安全的。
可重入函數 (reentrant function),其特色在於它們具備這 樣一種屬性:當它們被多個線程調用時,不會引用任何共享數據。
可重入函數一般要比不可重人的線程安全的函數高效一些,由於它們不須要同步操做。更進一步來講,將第 2 類線程不安全函數轉化爲線 程安全函數的惟一方法就是重寫它,使之變爲可重入的。
可重入函數、線程安全函數和線程不安全函數之間的集合關係:
檢查某個函數的代碼並先驗地判定它是可重入的。
若是全部的函數參數都是傳值傳遞的(即沒有指針),而且全部的數據引用都是本地的自動棧變量(即沒有引用靜態或全局變量),那麼函數就是顯式可重入的 (explicitly reentrant),也就是說,不管它是被如何調用的,咱們均可以斷言它是可重入的。
咱們老是使用術語可重入的 (reenntrant) 既包括顯式可重入函數也包括隱式可重入函數。然而,認識到可重入性有時既是調用者也是被調用者的屬性,並不僅是被調用者單獨的屬性是很是重要的。
大多數 Unix 函數,包括定義在標準 C 庫中的函數(例如 malloc、 free、 realloc、 printf 和 scanf) 都是線程安全的,只有一小部分是例外。
常見的線程不安全的庫函數:
除了 rand 和 strtok 之外,全部這些線程不安全函數都是第 3 類的,它們返回一個指向靜態變量的指針。若是咱們須要在一個線程化的程序中調用這些函數中的某一個,對調用者來講最不惹麻煩的方法是加鎖-拷貝。然而,加鎖-拷貝方法有許多缺點。首先,額外的同步下降了 程序的速度。其次,像 gethostbyname 這樣的函數返回指向複雜結構的結構的指針,要拷貝整個結構層次,須要深層拷貝 (deep copy) 結構。再次,加鎖-拷貝方法對像 rand 這樣依賴 跨越調用的靜態狀態的第 2 類函數並不有效。 所以,Unix系統提供大多數線程不安全函數的可重人版本。可重入版本的名字老是以"_r" 後綴結尾。
當一個程序的正確性依賴於一個線程要在另外一個線程到達y 點以前到達它的控制流中的 x 點時,就會發生競爭。
發生競爭是由於程序員假定線程將按照某種特殊的軌跡線穿過執行狀態空間,而忘記了另外一條準則規定:線程化的程序必須對任何可行的軌跡線都正確工做。
信號量引人了一種潛在的使人厭惡的運行時錯誤,叫作死鎖(deadlock) ,它指的是一組線程被阻塞了,等待一個永遠也不會爲真的條件。
程序員使用 P 和 V操做順序不當,以致於兩個信號量的禁止區域重疊。若是某個執行軌跡 線碰巧到達了死鎖狀態 d,那麼就不可能有進一步的進展了,由於重疊的禁止區域阻塞了 每一個合法方向上的進展。換句話說,程序死鎖是由於每一個線程都在等待其餘線程執行一個根本不可能發生的V操做。
重疊的禁止區域引發了一組稱爲死所區域(deadlock region )的狀態。若是一個軌跡線碰巧到達了一個死鎖區域中的狀態,那麼死鎖就是不可避免的了。軌跡線能夠進人死鎖區域, 可是它們不可能離開。
死鎖是一個至關困難的問題,由於它不老是可預測的。一些幸運的執行軌跡線將繞開死鎖區域,而其餘的將會陷入這個區域。
程序死鎖有不少緣由,要避免死鎖通常而言是很困難的。然而,當使用二元信號量來實現互斥時,能夠應用下面的簡單而有效的規則來避免死鎖:
互斥鎖加鎖順序規則:若是對於程序中每對互斥鎖 (s, t), 每一個同時佔用 s 和 t 的線程都按照相同的順序對它們加鎖,那麼這個程序就是無死鎖的。
有死鎖程序的進度圖:
無死鎖程序的進度圖:
一個併發程序是由在時間上重疊的一組邏輯流組成的。
三種不一樣的構建併發程序的機制:進程、I/O 多路複用和線程。
進程是由內核自動調度的,並且由於它們有各自獨立的虛擬地址空間,因此要實現共享數 據,必需要有顯式的 IPC 機制。事件驅動程序建立它們本身的併發邏輯流,這些邏輯流被模型化爲狀態機,用I/O 多路複用來顯式地調度這些流。由於程序運行在一個單一進程中,因此在流之間共享數據速度很快並且很容易。線程是這些方法的綜合。同基於進程的流同樣,線程也是由內核自動調度的。同基於 I/O 多路複用的流同樣,線程是運行在一個單一進程的上下文中的,因 此能夠快速而方便地共享數據。
不管哪一種併發機制,同步對共享數據的併發訪問都是一個困難的問題。提出對信號量的 P 和 V操做就是爲了幫助解決這個問題。信號量操做能夠用來提供對共享數據的互斥訪問,也對諸如生產者一消費者程序中有限緩衝區和讀者一寫者系統中的共享對象這樣的資源訪問進行調度。
併發也引人了其餘一些困難的問題。被線程調用的函數必須具備一種稱爲線程安全的屬性。 可重入函數是線程安全函數的一個真子集,它不訪問任何共享數據。可重入函數一般比不可重人函數更爲有效,由於它們不須要任何同步原語。競爭和死鎖是併發程序中出現的另外一些困難的問題。當程序員錯誤地假設邏輯流該如何調度時,就會發生競爭。當一個流等待一個永遠不會發生的事件時,就會產生死鎖。 四類線程不安全的函數。