前言
筆者將《unix環境高階程式設計》主要內容總結為三篇:檔案篇,程式篇,高階io和程式間通訊三大板塊。本文是unix環境高階程式設計系列文章第三篇:高階IO和程式間通訊篇。該篇主要包括:
高階io
先介紹記錄鎖的概念和記錄鎖的資料結構。然後介紹阻塞io,非阻塞IO,非同步io,IO多路轉接等概念,後者都是針對前者更優的技術。IO多路轉接技術包括:select,peslect,poll。最後介紹儲存對映IO。
程式間通訊
介紹了基本程式間通訊機制,包括兩大類:
- 程式間資料共享:管道,FIFO,訊息佇列和共享儲存
- 程式間資料同步:訊號量
網路程式間通訊
介紹網路間的程式通訊機制:套接字。首先是如何定址。然後介紹socket程式設計的連線建立,資料傳輸等。
高階程式間通訊
高階程式間通訊提供一種可以在程式間傳遞檔案描述符的機制,包括STREAMS管道和unix域套接字
一. 高階IO
1. 非阻塞IO
1.1 概念
- 非阻塞io使得與磁碟io有關的系統呼叫永遠不會被阻塞
- 這些io相關的系統呼叫有:open,read,write
- 如果這種操作不能完成,則呼叫立即出錯返回
1.2 如何指定非阻塞io
- 如果呼叫open獲得檔案描述符,可指定O_NONBLOCK標識
- 對於已經開啟的檔案描述符,可呼叫fcntl,由該函式開啟O_NONBLOCK標識
2. 記錄鎖
2.1 概述
- 概念:當一個程式正在讀或修改檔案的某個部分時,可以阻止其他程式修改同一檔案區
- flock:檔案鎖,早期的unix只支援鎖整個檔案,使用該函式
- fcntl:記錄鎖,允許鎖檔案中的任意位元組數的區域
2.2 fcntl
- cmd:
- F_GETLK:獲取鎖資訊
- F_SETLK:設定鎖資訊
- F_SETLKW:阻塞版本的F_SETLK
- flockptr:指向flock的指標
struct flock{ short l_type;//F_RDLCK共享讀鎖,F_WRLCK獨佔寫鎖,F_UNLCK解鎖 off_t l_start;//加鎖區域其實位置 short l_whence;//和start一起確定加鎖位置 off_t l_len;//加鎖長度 pid_t l_pid;//程式id } 複製程式碼
- 不同鎖的相容性:針對同一把鎖。如果不同鎖,新鎖總是覆蓋舊鎖
2.3 鎖的隱含繼承和釋放
- 程式終止時,所建立的鎖全部釋放
- 關閉檔案描述符時,檔案描述符引用的檔案上的任何一把鎖都被釋放
- fork產生的子程式不繼承父類設定的鎖
- 執行exec後,新程式可以繼承原程式的鎖
2.4 FreeBSD中記錄鎖的資料結構
- v節點表的i節點結構串聯起所有的lockf結構
- 每個lockf結構說明了一個給定程式的一個加鎖區域
- 在父程式中,關閉任意一個檔案描述符,核心都會遍歷i節點各項lockf,並釋放持有的鎖
3. 系統v流機制
3.1 基本概念
- STREAMS是系統V提供的構造核心裝置驅動程式和網路協議包的一種通用方法。不同於標準io中的stream
- 流在使用者程式和裝置驅動程式之間提供一條全雙工通路,流無需和實際硬體裝置之間會話
- 簡單流的基本結構:
3.2 STREAMS訊息
- STREAMS的所有輸入輸出都基於訊息
- 流首和使用者程式進行訊息交換的函式:read,write,ioctl,getmsg,getpmsg,putmsg,putpmsg
- 訊息可以順流而下,也可以逆流而上
- 訊息的組成:訊息型別,控制資訊,資料。控制資訊和資料由strbuf指定:
- 訊息約有25種,但一般使用的只涉及三種:
- M_DATA:使用者資料
- M_PROTO:協議控制資訊
- M_PCPROTO:高優先順序協議控制資訊
- 每個輸入STREAMS模組有兩個輸入佇列,一個來自上面模組的訊息,另一個來自下面模組的訊息
- 流中的訊息都有一個排隊優先順序,通過優先順序波段指定
3.3 putmsg和putpmsg
- 用於將STREAMS訊息寫入流中
- 後者允許指定優先順序波段
3.4 getmsg和getpmsg
- 從流首讀STREAMS訊息
4. IO多路轉接
4.1 阻塞io
- 讀取一個檔案描述符對資料,如果沒有資料就一直阻塞住
- 缺點:長時間阻塞在同一個檔案描述符,另一個檔案描述符雖然有很多資料卻得不到及時處理
4.2 非阻塞io
- 將兩個檔案描述符都設定為非阻塞的
- 對第一個檔案描述符傳送read,如果該輸入上有資料,則讀取並處理。如無資料則立即返回。
- 第二個描述符重複上一步操作
- 若干秒後,重複執行以上步驟,即輪詢
- 缺點:浪費cpu時間,大多數時間實際上上無資料可讀的。輪詢的時間間隔也很難確定
4.3 非同步io
- 當一個檔案描述符已準備好可以進行io時,用一個訊號通知它
- 缺點:併發所有的系統都支援,其次這種訊號對每個程式而言只有一個
4.4 IO多路轉接
- 一種比非同步IO更好的處理IO的技術
- 先構造一張有關描述符的圖表,然後呼叫一個函式,直到這些描述符中至少一個準備好io時,該函式才返回。返回時,告訴哪些檔案描述符已準備好可以io
- 支援IO多路轉接的函式:poll,pselect,select
4.5 select
- readfds:可讀描述符集,每一個檔案描述符佔一位
- 內部結構檢視
- 描述符集的設定函式
- maxfdp1:最大描述符+1,可設定為FD_SETSIZE(1024)
- writefds:可寫描述符集
- exceptfds:異常描述符集
- tvptr:願意等待的時間
- NULL:永遠等待,捕捉到訊號則中斷等待
- 時間每個欄位為0:完全不等待,測試指定的檔案描述符並立即返回
- 不為0:實際等待的時間
- 返回值:
- 返回-1:表示出錯,檔案描述符沒有準備好時收到訊號,此時不修改檔案描述符
- 返回0:已經超時了,指定都檔案描述符都沒有準備好
- 正數:已經準備好的檔案描述符數量(每個檔案描述符讀寫單獨各算一次)
4.6 pselect
pselect與select類似,僅僅少部分有差異,如下:
- 超時值的資料結構不同
- pselect超時值為const,不可改變
- 可使用訊號遮蔽字
4.7 poll
- poll類似與select,不過介面有所不同
- 不是為每個狀態構造檔案描述符集,而是構造一個pollfd的陣列,陣列每個元素指定檔案描述符編號和關心的狀態
- 引數:
- events:使用者設定關心的事件
- reevents:核心返回檔案描述符事件
5. 非同步IO
5.1 概述
非同步io並不像select和poll對所有檔案描述符都生效
- SystemV系統:只對STREAMS裝置和STREAMS管道起作用,傳送SIGPOLL訊號
- BSD系統:只對終端和網路起作用,傳送SIGIO訊號
5.2 SystemV非同步IO
- 啟動非同步IO,需要呼叫ioctl,第二個引數為I_SETSIG
- 同時,在呼叫ioctl之前建立訊號處理程式
5.3 BSD非同步IO
非同步IO是SIGIO(通用非同步io)和SIGURG(通知網路程式資料到達)兩個訊號的組合
- 呼叫signal或signalaction為SIGIO建立訊號處理程式
- 以命令F_SETOWN呼叫fcntl設定程式id和程式組id,將接收對於該描述符的訊號
- 以命令F_SETFL呼叫fcntl設定O_ASYNC檔案狀態標識,使檔案描述符上可以進行非同步IO
6. readv和writev
- 用於在一次函式呼叫中讀寫多個非連續的緩衝區
7. readn和writen
- 按需多此呼叫read和write,直至讀寫了N各位元組資料
- 使用與讀寫管道,網路裝置或終端資料
8. 儲存對映IO
- 使一個磁碟空間與一個儲存空間中的緩衝區對映。當從緩衝區取資料,就相當於讀檔案中的相應位元組。寫資料到緩衝區相當於自動寫入檔案。這樣就可以不用read和write的情況下執行io
- 檔案對映到儲存區:
- addr:儲存對映起始地址,通常設定為0,表示由系統選擇地址然後作為返回值返回
- port:說明對儲存對映區的保護要求,許可權不能超過檔案本身許可權
- PORT_READ:對映區可讀
- PORT_WRITE:對映區可寫
- PORT_EXEC:對映區可執行
- PORT_NONE :對映區不可訪問
- flag:
- MAP_FIXED:返回值必須等於addr,不利於移值
- MAP_SHARED:儲存操作的配置
- MAP_PRIVATE:建立私有副本
- 更改儲存對映區許可權:mprotect
- 重新整理對映儲存區:msync
- 解除儲存對映區:munmap
二. 程式間通訊
程式間通訊機制包括:
- 經典IPC:管道,FIFO,訊息佇列,訊號量,共享儲存
- 網路IPC:套接字
1. 管道
1.1 概述
- 最古老的ipc機制
- 管道有兩個侷限性:
- 歷史上,它是半雙工的,即資料只能在一個方向流動。雖然現在某些系統提供全雙工,但是為了移植性,不假定它有此特性
- 他們只能在具有公共祖先的程式之間使用
- 儘管有侷限性,半雙工管道仍然是最常用的ipc
- 若write寫一個尚無程式為讀而開啟的管道,產生SIGPIPE訊號
- 若管道的最後一個寫程式關閉該管道,則為管道的讀程式產生檔案結束標識
1.2 管道的建立
- 引數fields傳入兩個檔案描述符,field[0]為讀而開啟,field[1]為寫而開啟,field[1]的輸出是field[0]的輸入
- 管道模型:
1.3 popen和pclose
- popen先執行fork,然後呼叫exec以執行cmdstring,並返回標準io檔案指標。如果type=“r“,檔案指標連線到cmdstring的標準輸出。如果type=“w”,檔案指標連線到cmdstring的標準輸入
- pclose關閉標準io流
1.4 FIFO
- FIFO也成為命名管道,通過FIFO,不相關的程式也能交換資料
- 建立FIFO:
- mode引數與open函式一致
- 非阻塞標準O_NONBLOCK:
- 沒有指定該引數:只讀open要阻塞到某個其他程式為寫而開啟此FIFO
- 指定該引數:只讀open立即返回。沒有程式開啟FIFO,將出錯返回-1
- 類似與管道,若write寫一個尚無程式為讀而開啟的FIFO,產生SIGPIPE訊號。若FIFO的最後一個寫程式關閉該FIFO,則為FIFO的讀程式產生檔案結束標識
- PIPE_BUF說明了可被原子寫到FIFO的最大資料量
- FIFO的用途
- 由shell命令使用,以便將資料從一條管道線傳到另一條,無需建立中間臨時檔案
- 用於客戶-伺服器程式中,以在客戶程式和伺服器程式間傳遞資料
2. XSI IPC
訊息佇列,訊號量和共享儲存,這三種IPC稱做XSI IPC,他們之間有很多共性,包括:
2.1 識別符號和鍵
- 識別符號:唯一標識IPC物件的內部名,非負整數
- 鍵:IPC物件的外部名,使多個合作程式能在同一個IPC物件上會合。鍵基本資料型別為key_t
- 客戶程式和伺服器程式在同一IPC上會合的方法:
- 伺服器程式指定鍵IPC_PRIVATE建立一個新的IPC結構,將返回的識別符號放到某處(檔案)給客戶程式使用。缺點:要分別讀寫檔案
- 在公共標頭檔案中定義一個鍵,伺服器程式指定該鍵建立IPC結構。缺點:可能IPC已經存在,獲取時會出錯
- 客戶程式和伺服器程式認同一個路徑名和專案id,接著呼叫ftok將兩個值變換為鍵,再呼叫方法2
2.2 許可權結構
- XSI IPC為每個IPC結構設定了一個ipc_perm結構,規定了許可權和所有者。
2.3 結構限制
- 三種形式的IPC都有內建限制
2.4 優點和缺點
缺點
- IPC結構是在系統範圍內起作用的,沒有訪問計數
- IPC結構在檔案系統中沒有名字,不能修改屬性,不能ls檢視IPC物件,不能用rm刪除,也不能用chmod修改許可權。不能用檔案描述符,也就不能使用select,poll模型
優點
- 可靠
- 流是受控的:緩衝區資源緊張,程式就休眠
- 面向記錄
- 可以用非先進先出方式處理
特徵對比
3. 訊息佇列
3.1 概述
- 訊息的連結表,存放在核心中,由訊息佇列識別符號標識
- 最開始出現的為了提供比一般IPC更高速度的通訊方式,但現在速度上沒有優勢,已經不再使用了
- 建立或開啟佇列:msgget
- 傳送訊息:msgsend
- 獲取訊息:msgrcv,不一定先進先出,可按訊息的型別欄位取
3.2 資料結構
- 每個佇列相關的資料結構
- 訊息佇列在各個系統中的引數限制
3.3 msgctl函式
msgctl函式對佇列執行多裝操作(類似於ioctl,垃圾桶函式)
- cmd:要執行的命令
- IPC_STATE:獲取msgid_ds結構,並放入buf引數
- IPC_SET:按buf值,設定資料
- IPC_RMID:刪除佇列和資料
3.4 msgsend函式
- ptr:指向訊息內容指標,訊息的組成:
- 型別:正長整型型別
- 長度
- 實際資料
- flag:標誌
- IPC_NOWAIT:非阻塞io
3.5 msgrcv函式
- ptr:獲取的資料地址,包括型別和實際資料
- nbytes:資料緩衝區長度
- type:獲取哪種訊息。
- type=0:返回佇列中第一條訊息
- type>0:返回訊息型別為type的第一個訊息
- type<0:返回訊息型別小於等於type絕對值的訊息
- flag:
- IPC_NOWAIT:非阻塞
4. 訊號量
4.1 概述
- 訊號量不同於管道和訊息佇列,它是一個計數器,用於多程式堆共享資料物件的訪問
- 訊號量計數操作必須是原子的,通常在核心中實現
- 使用訊號量獲取共享資源的操作
- 測試該資源的訊號量N
- 若N為正,則程式可以使用該資源。然後N=N-1,表示使用了一個資源單位
- 若N=0,則程式休眠,直到N>0才喚醒,然後第一步
- 當程式不使用共享資源時,N=N+1,如果有程式在休眠等待則喚醒
- XSI訊號量相對複雜一些
- 訊號量併發單個非負值,而是一個或多個訊號量值的集合
- 建立訊號量和賦值是分開的,不能原子的建立訊號集合
- 即使沒有程式在使用訊號量,他仍然存在
- 獲得一個訊號量ID:semget
4.2 資料結構
- 核心為每個訊號量集合設定了一個semid_ds結構
- 每個訊號量的結構
- 訊號量的系統限制
4.3 semctl函式
- 包含多種訊號量操作
- cmd:
- IPC_STAT:取semid_ds結構
- IPC_SET:設定資料
- IPC_RMID:刪除訊號量集合
4.4 訊號量與記錄鎖在liunx的對比
- 記錄鎖比訊號量耗時
- 但如果只鎖一個資源,寧可用記錄鎖。因為他使用簡單,程式終止時會自動清理鎖
5. 共享儲存
5.1 概述
- 共享儲存允許兩個或更多程式共享給定的儲存區
- 資料不需要在程式間複製,是最快的IPC
- 多程式對於同一個儲存區,要注意同步訪問,通常使用訊號量來進行同步
- 獲取共享儲存區域id:shmget
- 共享儲存的位置:棧下面
4.2 資料結構
- 核心為每個共享儲存段設定了shmid_ds結構
- 共享儲存的系統限制
4.3 shmctl函式
- 包含堆共享儲存的多種操作
- 引數同前面
4.4 共享儲存的使用
- shmat函式:程式用於連線共享儲存到其他的地址空間中
- addr引數:
- 為0:連線到由核心選擇的可以地址上,推薦方式
- 非0:且沒有指定SHM_RND,連線到該地址
- 非0:指定SHM_RND,將地址向下取最低邊界地址倍數
- flag:
- SHM_RDONLY:只讀
- 其他:讀寫
4.5 共享儲存的釋放
- shmdt:脫離該段,但並不刪除資料,識別符號還在,直到呼叫shmctl刪除
三. 網路程式間通訊:套接字
1. 套接字描述符
- 套接字是通訊端點的抽象,是用檔案描述符實現的
- 建立套接字描述符:
- domain:套接字域
- type:套接字型別
- protocol:協議,通常為0。表示根據套接字型別預設選擇協議
- 關閉套接字:close
- shutdown:禁止套接字上的輸入/輸出,可只關閉一個方向
2. 定址
2.1 位元組序
- 大端位元組序:最大位元組地址對應於數字最低有效位元組
- 小段位元組需:最小位元組地址對應於數字最低有效位元組
- 各個平臺的位元組序如下:
- 網路傳輸中:tcp/ip使用大端位元組序
2.2 地址格式
- 地址標識了套接字端點,通用地址格式為:
struct sockaddr{
sa_famliy_t sa_famliy;
char sa_data[];
}
複製程式碼
- 套接字實現可以自由新增aa_data欄位以及長度
//linux實現
struct sockaddr{
sa_famliy_t sa_famliy;
char sa_data[14];
}
//freeBSD實現
struct sockaddr{
unsigned char sa_len;
sa_famliy_t sa_famliy;
char sa_data[14];
}
複製程式碼
- ipv4套接字通用地址:<netinnet/in.h>,實現者可以自由新增額外欄位
- ipv6套接字通用地址:實現者可以自由新增額外欄位
- sockaddr_int和sockaddr_int6都會被轉化為sockaddr結構傳入套接字例程中
- 二進位制地址與文字格式地址轉化:inet_ntop,inet_pton
2.3 地址查詢
- 查詢給定計算機主機資訊:gethostent
- 返回的主機資訊資料結構:
- 獲取網路名字和網路號
- 獲取協議名字和協議號
- 服務名字和埠號對映關係查詢
- 將主機名和服務名對映到一個地址
- 地址資訊包含的成員
2.4 將套接字與地址繫結
- 客戶端套接字關聯地址沒有太大意義,可以讓系統選一個預設地址
- 服務端需要給一個客戶端請求的套接字繫結一個眾所周知的地址
- 客戶端繫結服務端地址的方法:
3. 建立連線
3.1 connect
- connect為客戶端呼叫,用於連線請求
- addr為伺服器地址
- 如果sockfd沒有繫結地址,connect會給呼叫者繫結一個預設地址
- 連線可能失敗,應用程式必須能處理connect返回的錯誤
3.2 listen
- listen為服務端呼叫
- 伺服器用listen宣告可以接受連線請求
- backlog:連線請求數量
3.3 accept
- accept獲得連線請求,並建立連線
- 返回的檔案描述符是套接字描述符,描述符連線到呼叫connect到客戶端
- 新的套接字描述符和原始套接字sockfd具有相同的套接字型別和地址族
- 傳給accept的原始套接字沒有關聯到這個連線,而是繼續儲存可以狀態並接受其他連線請求
- 如果沒有連線請求等待處理,accept會阻塞直到有請求到來
4. 資料傳輸
4.1 send
- 傳送資料,類似與write函式
- send比write多了第四個引數flags,用於改變處理資料到傳輸方式
- MSG_DONTROUTE:勿將資料路由出本地網路
- MSG_DONTWAIT:允許非阻塞操作
- MSG_EOR:記錄結束
- MSG_OOB:外帶資料
- sendto函式:類似send。但是sendto允許在勿連線到套接字上指定一個目標地址
4.2 recv
- 獲取資料,類似於read函式
- recv比read多了第四個產生flags,用於控制如何接收資料
- MSG_OOB:接受外帶資料
- MSG_PEEK:返回報文內容而不真正取走報文
- MSG_TRUNC:即使報文被截短,也返回實際的長度
- MSG_WAITALL:等待直到所以資料可用
5. 套接字選項
5.1 套接字選項包括
- 通用選項,工作在所有套接字型別上
- 在套接字層次管理的選項,但是依賴底層協議的支援
- 特定與某種協議的選項,為某個協議獨有
5.2 設定套接字的函式
6. 帶外資料
- 帶外資料是一些通訊協議支援的可選特徵,允許高優先順序的資料比普通資料優先傳輸
- TCP將外帶資料成為“緊急資料”
四. 高階程式間通訊
1. 概述
- Streams管道和unix套接字,這兩種高階IPC,可以在程式間傳遞檔案描述符
- 服務程式可以使他們的開啟檔案描述符與特定的名字相關聯
- 客戶程式可以使用這些名字與伺服器通訊
- 作業系統會為每個客戶程式提供一個獨自的IPC通道
2. STREAMS管道
- Streams pipe是一個全雙工(雙向)通道
- 內部結構如下
3. UNIX域套接字
- 用於在同一臺機器上執行的程式之間通訊