眾所周知,在Linux中,進程為了能在同一項任務上協調工作,彼此之間必須能夠進行通信。例如,在一個shell管道中,第一個進程的輸出必須傳輸到第二個進程,這樣沿著管道傳遞下去。因此在需要通信的進程之間,應該使用一種結構較好的通信方式。
Linux支持許多不同形式的進程間通信機制,在特定的情況下,它們各有自己的優缺點,如利用管道進行通信的最大缺點就是,只有成為系統調用pipe的進程的子孫後代的進程才可以使用管道,因此,相互無關的進程不能通過管道通信。
OSKit的開發人員們充分考慮了各種通信方式的利弊,揚長避短,采用了如下的兩種線程間通信方式:消息隊列和信號量。雖然這還有很多不足之處,但較之System V以有所改進。
3.1 線程間通信
3.1.1消息隊列
OSKit提供的消息隊列機制於UNIX十分相似,一個和多個線程可向消息隊列寫入消息,而一個或多個線程可從消息隊列中讀取消息,這種線程間通信的機制就像客戶機/服務器一樣,客戶機向服務器發送請求消息,服務器讀取消息並執行相應的請求。
OSKit將消息描述成在內核地址空間的一個內部鏈表,每一個消息隊列由一個IPC標識號唯一的標識。
3.1.2信號
盡管大多數線程間通信是計劃好的,但是同時還需要處理不可預知的通信問題。例如,用戶使用文本編輯器要求列出一個大文件的全部內容,但隨即他認識到該操作並不重要,這是就需要一種方法來中止編輯器的工作,比如說,我們可以按DEL鍵來實現。按DEL鍵實際上是向編輯器發送一個信號,編輯器收到此信號便立即停止打印文件的內容。信號還可以用來報告硬件捕獲到的特定的中斷,如非法指令和浮點運算溢出、超時也通過信號實現的。
在OSKit中,信號是一種重要的線程間通信機制,線程通過信號知道系統中正在出現的事件。信號的一個特點就是它的異步性,這表示線程在它的執行期間的任何時候都可以接受到信號,甚至可能當線程正在執行系統調用的時候接到信號,因此,線程必須隨時為相應信號作好准備。
下面我們再談談信號的生成與交付,當引起信號的事件首次出現時,就認為信號為線程而生成,也就是說信號被發送到線程。這類事件的例子,包括:時間片到時,終端激活,硬件故障的檢測,kill( )的調用。在某些情況下,一個事件為多個可以為多個線程生成信號。每個線程采用樣應的動作來響應系統定義的每種信號(見信號量表3.1)。
在信號的生成與交付之間,該信號被掛起,但在用戶一級是檢測不到這個間隔的。每個線程都擁有一個信號掩碼(sig_mask),它規定了當前被阻塞而不能交付給線程的信號集。線程的信號掩碼是從父線程繼承而來的。我前面提到sigaction( ),sigprocmask( )和sigsuspend( )函數,都是對信號掩碼的操作。
信號的動作與信號有關的動作分為三種:SIG_DFL,SIG_IGN或指向函數的指針。在最開始,進入main( )之前,所有信號都將被置成SIG_DFL或SIG_IGN。
(1)SIG_DFL信號專用的默認動作:
(a)如果默認動作是暫停線程,則該線程的執行被暫時掛起。當線程暫停期間,發送給線程的任何附加信號都不交付,直到該線程開始執行,但是SIGKILL除外。
(b)把掛起信號的信號動作設置成SIG_DFL,且其默認動作是忽略信號 (SIGCHLD)。
(2)SIG_IGN忽略信號
(a)該信號的交付對線程沒有影響
(b)系統不允許把SIGKILL或SIGTOP信號的動作設置為SIG_DFL
(3)指向函數的指針--捕獲信號
(a)信號一經交付,接收線程就在指定地址上執行信號捕獲程序。在信號捕 獲函數返回後,接受線程必須在被中斷點恢復執行。
(b)用C語言函數調用的方法進入信號捕捉程序:
void func (signo)
int signo;
func( )是指定的信號捕捉函數,signo是正被交付信號的編碼
(c)如果SIGFPE,SIGILL或SIGSEGV信號不是由C標准定義的kill( )或raise( )函數所生成,則從信號SIGFPE,SIGILL,SIGSEGV的信號捕獲函數正常返回後線程的行為是未定義的。
(d)系統不允許線程捕獲SIGKILL和SIGSTOP信號。
(e)如果線程為SIGCHLD信號建立信號捕獲函數,而該線程有未被等待的以終止的子線程時,沒有規定是否要生成SIGCHLD信號來指明那個子線程。
每一種信號都被OSKit給予了一個符號名,對於32位的i386平台而言,一個字32位,因而信號有32種。下面的表給出了常用的符號名、描述和它們的信號值。
符號名 信號值 描述 是否符合POSIX
SIGHUP 1 在控制終端上檢測到掛斷或控制線程死亡 是
SIGINT 2 交互注意信號 是
SIGQUIT 3 交互中止信號 是
SIGILL 4 檢測到非法硬件的指令 是
SIGTRAP 5 從陷阱中回朔 否
SIGABRT 6 異常終止信號 是
SIGEMT 7 EMT 指令 否
SIGFPE 8 不正確的算術操作信號 是
SIGKILL 9 終止信號 是
SIGBUS 10 總線錯誤 否
SIGSEGV 11 檢測到非法的內存調用 是
SIGSYS 12 系統call的錯誤參數 否
SIGPIPE 13 在無讀者的管道上寫 是
SIGALRM 14 報時信號 是
SIGTERM 15 終止信號 是
SIGURG 16 IO信道緊急信號 否
SIGSTOP 17 暫停信號 是
SIGTSTP 18 交互暫停信號 是
SIGCONT 19 如果暫停則繼續 是
SIGCHLD 20 子線程終止或暫停 是
SIGTTIN 21 後台線程組一成員試圖從控制終端上讀出 是
SIGTTOU 22 後台線程組的成員試圖寫到控制終端上 是
SIGIO 23 允許I/O信號 否
SIGXCPU 24 超出CPU時限 否
SIGXFSZ 25 超出文件大小限制 否
SIGVTALRM 26 虛時間警報器 否
SIGPROF 27 側面時間警報器 否
SIGWINCH 28 窗口大小的更改 否
SIGINFO 29 消息請求 否
SIGUSR1 30 保留作為用戶自定義的信號1 是
SIGUSR2 31 保留作為用戶自定義的信號 是
請求按默認的規則進行信號處理
SIG_DFL (void (*)(int)) 0
請求忽略信號
SIG_IGN (void (*)(int)) 1
注意: 信號隊列中最多允許有64個信號
3.2 pthreads/pthread_ipc.c
此源碼文件包括了一套完整的消息隊列型線程通信機制,消息隊列是線程通信的主要手段,通過閱讀下面的函數分析,將使讀者了解OSKit到底是用什麼函數來具體實現信號隊列,從而進行線程通信的。
3.2.1 消息發送:
說明:當一個線程要向另一個線程發送消息的時候,OSKit使用下面定義的函數來實現一個線程向另一個線程傳遞消息。
oskit_error_t oskit_ipc_send ( pthread_t dst,void *msg,
oskit_size_t msg_size, oskit_s32_t timeout )
dst:目標地址 *msg:指向消息的指針
msg_size:消息大小(32位無符號整形) timeout:超時
3.2.2 send的算法
發送一個同步消息,直到接收者發送終止消息或超時才結束
為避免死鎖,給沒個人加一個鎖,並關中斷。
assert_interrupts_enabled( );
disable_interrupts( );
pthread_lock (&target->waitlock);
通過條件,檢測目標線程是否在等待接受:
條件: a:目標線程的等待標志 & THREAD_WS_IPCRECV_WAIT(線程接收等待位)
b:目標線程號存在
c:目標線程等於任意一個線程
d:消息大小不為0
條件成立: a && ( b || c) && d == 1 才可以發送
if (target->waitflags & THREAD_WS_IPCRECV_WAIT
&&(target->ipc_state.tid == pthread->tid||target->ipc_state.tid== ANYTID))
{ if (msg_size)
{ Memcpy(target->ipc_state.msg,
msg,MIN(msg_size,target->ipc_state.msg_size));
if (msg_size > target->ipc_state.msg_size)
err = OSKIT_ERANGE;
}
}
把數據的大小和發送者的ID告訴接受線程:
target->ipc_state.msg_size = msg_size;
target->ipc_state.tid = pthread->tid;
清除接受位:
target->waitflags &= ~THREAD_WS_IPCRECV_WAIT;
pthread_unlock ( &target->waitlock );
注意: flagA & = ~flagB 實際上就是將flagB中等於1的位在flagB中清0
發送隊列:
queue_enter ( &target->ipc_state.senders,pthread,pthread_thread_t*,
ipc_state.senders_chain)
&target->ipc_state.senders:發送線程的ID pthread:
pthread_thread_t *:指向消息的指針是pthread_thread_t類型
ipc_state.senders_chain:發送隊列的指針
隊列間參數傳遞:
pthread->ipc_state.msg = msg; /* 消息 */
pthread->ipc_state.msg_size = msg_size; /* 消息的長度 */
pthread->ipc_state.tid = dst; /* 消息的目的地址 */
pthread->waitflags = THREAD_WS_IPCSEND_WAIT;
/* 消息目的等待位置1 */
3.2.3 消息接收
說明:當一個線程要從其消息隊列中開始接受消息的時候,OSKit提供了下面的函數供線程調用。
oskit_error_t oskit_ipc_recv ( pthread_t src,void *msg,
oskit_size_t msg_size, oskit_size_t *actual,oskit_s32_t timeout )
src: 發送線程的ID *msg: 指向消息的指針
msg_size: 消息大小(32位無符號整形)
actual: 實際消息的大小 timeout: 超時
3.2.4 receive的算法
等待從發送線程來的消息
通過條件,檢測是否接受消息
a:有等待發送的線程
b:線程等待位&THREAD_WS_IPCSEND_WAIT(發送等待標志)
c:發送線程存在
條件成立: a&&(b||c)
pthread_lock(&source->waitlock);
if (source->waitflags & THREAD_WS_IPCSEND_WAIT &&
source->ipc_state.tid == pthread->tid) {
*actual = MIN(msg_size, source->ipc_state.msg_size);
if (*actual) {
memcpy(msg, source->ipc_state.msg, *act0ual);
if (source->ipc_state.msg_size > msg_size)
err = OSKIT_ERANGE;
}
}
如果沒有等待發送的進程,則讓接受進程等待
當發送隊列為空且timeout=0時解鎖,如果發送了但沒接受,則重新初始化發送進程,重新發送。
3.3 pthreads/pthread_signal.c
此源碼文件包括了一套完整的信號量型線程通信機制,信號量是線程通信的另一個重要的手段,通過閱讀下面的函數分析,將使讀者了解OSKit到底是用什麼函數來具體實現信號量,從而進行線程通信的。
3.3.1 檢測並更改阻塞的信號
說明:當一個信號被發送出來的時候,並不一定馬上能得到相應,一般來說,它要被阻塞一段時間,但系統也是就不管這些阻塞信號了,系統會定時執行此線程,用以檢測阻塞的到底是什麼信號,它的含義是什麼,然後根據需求作出決定,可以將信號阻塞或者響應。
int pthread_sigmask ( int how, const sigset_t *set, sigset_t *oset )
how: SIG_BLOCK SIG_UNBLOCK SIG_SETMASK
set: 如果是空指針,則指向下一個信號
oset:如果是空指針,則指向上一個信號記錄
3.3.2殺線程信號
說明:OSKit以及所有的操作系統,都包含這個函數,因為它是操作系統所必須具備的基本功能,也就是說,當一個線程出於無響應狀態很長時間,或者因為其他的什麼原因線程不能得到執行,則調用它,將該線程殺死。
int pthread_kill(pthread_t tid, int signo)
3.3.3 關線程鎖
說明:當線程接受到了消息後的響應過程中,有些時候是不希望被其他的線程打斷的,所以OSKit提供了此函數調用,用來將線程鎖起來,也可以理解為將線程保護了起來。
int pthread_kill_locked ( pthread_thread_t *pthread, int signo )
3.3.4 在目標線程的信號等待隊列中加一個信號
說明:在信號發送和接受的過程中,線程間通訊並不一定是同步的,此時,OSKit提供了一個消息等待隊列,將不能及時得到響應的消息用指針鏈接成為一個類似與鏈表的數據結構。當接受線程要響應信號的時候可以直接從消息隊列裡提取,這樣作既可以提高通信效率,有可以增加通信的穩定性。
int sigaddset ( &pthread->sigpending, signo )
3.3.5 信號完成的動作
說明:在用信號方式通信的時候,線程間傳遞的信號並不是要做的動作,而是動作的代碼,而什麼信號對應什麼動作,還要有此漢說來進行解析,這樣作是為了規范線程間通信,同時還可以節約空間,節省發送和接受的時間,從而大大提高系統的效率。
int sigaction ( int sig, const struct sigaction *act, struct sigaction *oact )
sig:指定信號 act:指向與指定信號向聯系的規定信號動作的結構
oact:指向存儲先前與該信號相聯系的動作的結構
3.3.6 測試或改變(或兩者兼有)主調進程的信號掩碼
說明:所謂掩碼就是信號的屏蔽碼,比如說,當一個信號被發送的時候,它被自動加載到接收線程的信號掩碼中,如果此時又向該線程發送了同樣的信號,則阻塞該信號,知道前一個信號響應完畢。
OSKit采用下面的函數完成的對信息掩碼的測試和改變。
int sigprocmask ( int how, const sigset_t *set, sigset_t *oset )
how: 指明改變信號集的方式
SIG_BLOCK: 結果是當前集與實參set所指向的信號集的聯合
SIG_UNBLOCK: 結果集是當前集與實參set所指向的信號集的補集的交
SIG_SETMASK: 結果集是實參set所指向的信號集
set: 指向要用來改變當前被阻塞的信號集 oset: 存儲當前的掩碼
3.3.7 殺線程
說明:此函數和前面介紹過的殺線程信號並不完全一樣,源線程是通過看是否有向目的線程發信號的權限,如果有的話就殺掉它
int kill ( pid_t pid, int signo )
pid: 目標線程 signo: 所要發的信號
3.3.8 等待信號的線程隊列
說明:由於線程間通信不一定同步的原因,調用此函數創建信號等待隊列是十分有必要的。
int sigqueue ( pid_t pid, int signo, const union sigval value )
3.3.9 等待內部信號
說明:OSKit將信號分為內部信號和外部信號,這是十分有意義的,因為很多情況下,它們的處理方式是不一樣的,有的外部消息可以不加理睬,但一般來說,內部消息是一定要及時響應的,系統通過調用下面的函數實現了等待一個內部信號,一旦等到,馬上響應。
oskit_error_t oskit_sigwait_internal ( const sigset_t *set,siginfo_t
*info,const oskit_timespec_t *timeout )
sigset_t: 信號裝置,無符號整型
siginfo_t: 信號信息不會被外部的信號打斷
3.3.10 線程等待信號
說明:在線程調度時,調度程序經常會發送此信號讓執行完畢的線程等待還沒執行完的線程,這是為了保持線程之間的同步。
int sigwait ( const sigset_t *set, int *sig )
3.3.11 線程等待信號(一般指內部信號)
說明:同樣的,核心內的線程一樣也要保持同步。
int sigwaitinfo ( const sigset_t *set, siginfo_t *info )
3.3.12 線程等待信號(有時間限制)
說明:此外為了完善系統的線程管理,OSKit還提供了這條函數,用來設定線程的等待時間,如果在規定的時間內沒有信號到來的話,則不再等待。
int sigtimedwait ( const sigset_t *set,siginfo_t *info,
const struct oskit_timespec *timeout )
3.3.13 線程等待信號,如果無信號發給它,則無限期等待
說明:但有的時候,線程要等待的信號對於該線程來說是十分重要的,所以它將無限期的等待下去,同時調度程序會將該線程阻塞起來。
int sigsuspend ( const sigset_t *sigmask)
3.3.14 從一個陷入發送信號給線程
說明:當系統內部發生陷入的時候,系統會給正在執行的線程發送信號,OSKit正是調用了此函數來實現。
void oskit_libc_sendsig ( int sig, int code, struct sigcontext *scp )
3.3.15 發出線程阻塞信號
說明:如果當前正在執行的線程由於某種突發情況被阻塞了,它將調用此函數,來通知與其相關的線程。
void oskit_deliver_pending_signals ( void )
3.3.16 不會被阻塞的信號發送
說明:在操作系統中,有許多十分重要的消息是不能被阻塞的,OSKit所提供的此條函數調用便是用來發送此種信號的。
void really_deliver_signal ( int sig, siginfo_t *info, struct sigcontext *scp )
3.3.17 信號初始化
說明:與線程類似,信號並不是從天而降,它也要先進行初始化,然後再發送給接受線程。
void pthread_init_signals ( )
3.4 pthreads/pthread_cond.c
此源碼文件包括了一套完整的線程通信的條件變量,它是線程通信的重要組成部分,通過閱讀下面的函數分析,將使讀者了解OSKit到底是用什麼函數來具體實現線程通信的條件變量。
3.4.1條件變量的初始化數據結構
說明:在OSKit的線程間通信機制中,經常用到條件變量。所謂條件變量,就是兩個線程通過條件變量來達到同步。例如,A線程我們讓它等待一個條件變量,B線程我們讓它發送一個信號,滿足A線程所等待的條件,這樣就實現了A、B線程之間的同步。
int pthread_cond_init ( pthread_cond_t *c, const pthread_condattr_t *attr )
pthread_cond_t: 條件變量 pthread_condattr_t: 條件變量屬性
3.4.2撤銷條件變量
說明:條件變量並不是永遠發生作用的,所以有創建就得有撤銷,OSKit正是調用了這個函數來實現條件變量撤銷。
int pthread_cond_destroy(pthread_cond_t *c)
3.4.3 線程等待條件變量
說明:OSKit設定條件變量就是為了通過讓線程等待它,從而實現線程之間的相互協調。
int pthread_cond_wait ( pthread_cond_t *c, pthread_mutex_t *m )
pthread_mutex_t:為什麼而等待
3.4.4 安全的等待
說明:等待條件變量的方式也是多種多樣,此函數提供的是一種安全的等待方式,即等待條件變量的線程不會被中斷。
int pthread_cond_wait_safe ( pthread_cond_t *c, pthread_mutex_t *m )
注:不會被中斷
3.4.5 等待條件變量,但有時間限制
說明:另一種等待條件變量的方式是在一定的時間限制內等待,一旦超時,線程便放棄對該條件變量的請求。
int pthread_cond_timedwait ( pthread_cond_t *c, pthread_mutex_t
*m,oskit_timespec_t *abstime )
struct oskit_timespec {
oskit_time_t tv_sec; /* 秒 */
oskit_s32_t tv_nsec; /* 納秒 */
};
3.4.6 等待捐贈條件
說明:在操作系統中線程之間總有一些比較有趣的調度算法,捐贈就是其中之一,在OSKit裡,出於同一個等待目的的線程之間,為了總體上的最優化,某些線程可以將自己占有的資源捐贈給其他線程,讓其盡早滿足執行的條件。
int pthread_cond_donate_wait ( pthread_cond_t *c, pthread_mutex_t
*m,oskit_timespec_t *abstime, pthread_t donee_tid )
pthread_t donee_tid:被捐贈線程號
3.4.7 等待條件超時
說明:此函數是OSKit的出錯信息,它是在等待條件變量超時的情況下才被調用的。
oskit_error_t pthread_condwait_timeout ( struct oskit_iunknown *listener, void *arg )
3.4.8 線程發送信號條件
說明:在線程間通信中,線程並不是什麼時候都可以發送信號的,OSKit定義了下面這個函數,規定了信號發送是所要等待的條件;換句話說就是只有滿足了此函數調用所設定的條件,線程之間才可進行通信。
int pthread_cond_signal ( pthread_cond_t *c )
3.4.9 線程廣播條件
說明:有些時候,某些線程需要對系統中所有的線程發送同一條消息,這樣就用到了OSKit提供的線程廣播的函數。
int pthread_cond_broadcast ( pthread_cond_t *c )
本 章 小 結
本章從線程間的兩種通信機制入手,系統地闡述了OSKit的線程通信機制。
在第一節主要描述了OSKit所用到的兩種線程間通信機制,以及它為什麼沒有使用UNIX中的管道通信,另外,我還以表格的形式詳細列出了OSKit提供的各種信號量、以及它們與POSIX標准之間的關系。第二節中以源代碼中的 /pthreads/pthread_ipc.c 為例,說明了第一節中消息隊列的具體實現。第三節以源代碼中的 /pthreads/pthread_signal.c 為例,說明了Oskit第一節中信號量的具體實現。第四節中又以源代碼/pthreads/pthread_cond.c為例,闡述了條件變量的概念以及用條件變量進行輔助通信的具體實現。