Linux多程序程式設計
1.使用計算機或其它裝置時,哪些現象或操作給你的感覺是同時進行或同時發生?
多任務(multitasking)
允許多個程式同時運作。
多使用者(multiuser)
允許多個使用者同時通路系統,每個使用者都可以運作程式。
2.程序基本概念
2.1什麼是程式? //檔案定義: 對系統資源進行通路的一個通用接口。
程式就是一個檔案,該檔案包含了一系列資訊,這些資訊描述了如何在運作時構造一個程序。
程式(檔案)中包含哪些資訊?
描述了可執行檔案的格式(如ELF, Executable and Linking Format),使得核心可以知道檔案的剩餘資訊是什麼含義。
表明程式從哪一條指令(instruction)開始執行。
初始化變量的一些值以及字元串常量等。
描述了程式中函數、變量的名字及位置。
程式運作時所需要的共享庫等資訊。
2.2Linux下可執行程式的分類
可執行目标檔案
經連結器連結後可直接執行的檔案稱為可執行目标檔案。核心一般支援幾種特定格式的可執行檔案。ELF格式是Linux系統中普遍使用的一種标準的可執行檔案格式。
可執行腳本
可執行腳本是一個特殊的文本檔案,它能夠訓示核心啟動一個解釋器去執行後續的内容。這個解釋器必須是可執行目标檔案。一般情況下,腳本的解釋器是Shell,但核心也會檢視腳本檔案第一行,如果前兩個字元是#!,它就會将第一行的剩餘部分解析為啟動解釋器的指令。例如,一個Shell腳本的第一行通常如下:
#!/bin/sh
這樣核心将會啟動/bin/sh作為腳本的解釋器。
2.3程序定義
什麼是程序?
程序是一個程式正在執行的執行個體。每個這樣的執行個體都有自己的位址空間和執行狀态。
核心怎麼區分不同的程序?
程序有一個PID(Process ID,程序辨別),用以區分各個不同的程序。核心記錄程序的PID與狀态,并根據這些資訊來配置設定系統資源(如記憶體等)。
當核心産生一個新的PID,生成對應的用于管理的資料結構,并為運作程式代碼配置設定了必要的資源,一個新的程序就産生了。
2.4 程序的記憶體布局
配置設定給每個程序的記憶體(memory)由一系列段(segments)組成:
text segment
包含機器語言指令。通常是隻讀的、共享的。
initialized data segment
包含初始化的全局變量和靜态(static)變量。
uninitialized data segment (bss segment)
包含未初始化的全局變量和靜态變量。程式運作之前,系統會把這些變量初始化為0。
包含堆棧幀(stack frames)。一個堆棧幀被配置設定給目前的被調函數(called function)。一幀儲存的内容有:局部變量、參數(arguments)、傳回值。
heap
程式運作時,為變量動态配置設定記憶體。
注:可以用size指令顯示一個二進制可執行檔案的text, initialized data, and uninitialized data (bss) segments。
2.5程序狀态
執行狀态
程序正在占用CPU。
就緒狀态
程序已具備一切條件,等待配置設定CPU。
等待狀态
程序不能使用CPU,若等待的事件發生則可将其喚醒。
2.6獲得PID
每個程序都有一個ID(ID是一個正整數),唯一辨別了系統中的這個程序。
獲得調用程序(the calling process)的ID:
#include <unistd.h>
pid_t getpid(void);
每個程序都有一個建立它的父程序(Parent Process),可以通過getppid()獲得父程序的ID:
#include <unistd.h>
pid_t getppid(void);
2.7程序的生命周期
建立
每個程序都由其父程序建立。父程序可以建立子程序,子程序又可以建立子程序的子程序。
運作
多個程序可以同時存在,程序之間可以進行通信。
終止
結束一個程序的運作。
2.8程序的樹狀關系
每個程序有其父程序,父程序又有其父程序...那原始的程序是什麼?
init程序!所有程序的祖先(ancestor)是init程序,其PID為1。可以用pstree指令檢視樹狀關系。
當父程序在子程序之前終止會發生什麼?
當一個子程序(child process)變為孤兒時(由于其父程序終止),此時該子程序會被init程序領養(adopted)。
2.9程序的進一步了解
現代作業系統可以同時執行多個程序。事實上,對于隻有一個CPU的系統來講,在一個給定時刻隻能有一個程序在執行。
核心控制CPU在很短的時間間隔内不斷地在各個程序間切換,輪流執行。因為這個間隔很短,是以使用者感覺到計算機在同時做幾件事情,于是就有了“并發”執行的概念。程序管理是作業系統的核心功能。
一個程序通常具有以下三個核心要素:
程式映像:二進制指令序列。
位址空間:用于存放資料和執行程式。
PCB(Process Control Block,程序控制塊):核心中描述程序的主要資料結構。
2.10 指令行參數
當運作程式時,如何向程式傳遞參數?
利用int argc和char *argv[]
argc表明指令行參數的數目;argv指向指令行參數。
2.11 環境清單
環境清單(簡稱環境)的形式
一個字元串(形式為name=value)數組。name被稱為環境變量(environment variables)。
環境清單的屬性
每個程序都有一個與其相關的環境。
新的程序會繼承其父程序的環境(inherit a copy)。
一種程序間通信的方式(父程序→子程序)。
如何在程式中獲得環境清單
char **environ
int main(int argc, char *argv[], char *envp[])
3.程序控制程式設計
3.1建立一個新程序fork()
include <unistd.h>
pid_t fork(void);
In parent: returns process ID of child on success, or –1 on error;
in successfully created child: always returns 0
當fork()順利完成任務時,就會存在兩個程序,每個程序都從fork()傳回處開始繼續執行。
3.2關于 fork的幾點說明
兩個程序執行相同的代碼(text)段,但是有各自的堆棧(stack)段、資料(data)段以及堆(heap)。
子程序的stack、data、heap segments是從父程序拷貝過來的。
fork()之後,哪一個程序先執行(scheduled to use the CPU)不确定。
fork()之後,不能确定哪個程序先執行,會有什麼隐患嗎?
産生原因
fork()之後,不能确定是父程序還是子程序獲得CPU。
危害
這種bugs很難被發現。
措施
如果需要確定特定的執行順序,需要采用某種同步(synchronization)技術(semaphores,file locks...)。
3.3 程序終止
通常由8種方式使程序終止(terminate)
5種正常終止:
從main函數傳回
調用exit
調用_exit或_Exit
最後一個線程從其啟動例程(start routine)傳回
最後一個線程調用pthread_exit
3種異常終止:
調用abort
接到一個信号并終止
最後一個線程對取消請求做出響應
不管程序如何終止,最後都會執行核心中的同一段代碼:用于關閉所有打開的檔案描述符,釋放記憶體等。
3.4_exit()與exit()
這兩個函數都用于正常終止一個程序:_exit立即進入核心,exit先執行一些清理處理(調用各終止處理程式、關閉所有标準I/O流等),然後進入核心。
#include <stdlib.h>
void exit(int status);
#include <unistd.h>
void _exit(int status);
這兩個函數都有一個整型參數,稱之為終止狀态(exit status)。
注:shell中列印終止狀态的指令為:echo $?
3.5程序的建立與終止
核心使程式執行的方法是調用一個exec函數。
程序自願終止的方法是顯示或隐式(通過exit)地調用_exit。
程序也可非自願地由一個信号使其終止
3.6 exec函數族
Linux系統中有一系列的函數可以将一個程序的執行流程從一個可執行程式轉移到另一個可執行程式,也就是裝載并運作一個程式。這些函數通常被稱為exec函數族:
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execv(const char *path, char *const argv []);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execve(const char *path, char *const argv[], char *const envp[]);
int execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[]);
注意:調用exec并不建立新程序,隻是用一個全新的程式替換了目前程序的正文段、資料、堆和棧。
3.7exec函數族各函數的差別
第一個差別
前4個取路徑名作為參數,後兩個取檔案名作為參數。
第二個差別
與參數傳遞有關(l表示list,v表示vector)。execl、execlp以及execle要求将新程式的每個指令行參數(command-line arguments)都指定為一個單獨的參數,以NULL指針表明參數的結束。另外三個函數(execv、execvp和execve),首先須要建立一個指向各參數的指針數組,然後将該數組的位址作為這三個函數的參數。
第三個差別
與向新程式傳遞環境變量表有關。以e結尾的兩個函數(execle和execve)可以傳遞一個指向環境字元串指針數組的指針。其它四個函數則使用調用程序中的environ變量為新程序複制現有的環境。
3.8 exec函數族的關系圖
這6個函數中,通常隻有execve是核心的系統調用,另外5個隻是庫函數,它們最終都要調用該系統調用。這6個函數的關系如下:
3.9監控子程序
父程序建立子程序後,如何知道子程序什麼時候終止?如何知道子程序怎麼終止(正常or異常)?
措施
wait()或waitpid()...
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
傳回值:若成功傳回程序ID,若出錯傳回-1。
調用wait或waitpid的程序可能發生的情況有:
如果所有子程序都還在運作,則阻塞(Block)。
如果一個子程序已終止,正等待父程序擷取其終止狀态,則取得該子程序的終止狀态立即傳回。
如果它沒有任何子程序,則立即出錯傳回。
3.10 wait和waitpid差別
在一個子程序終止前,wait使其調用者阻塞,而waitpid有一個選項,可使調用者不阻塞。
waitpid并不等待在其調用之後的第一個終止的子程序。它有若幹個選項,可以控制它所等待的程序。
如果一個子程序已經終止,并且是一個僵死程序,wait立即傳回并取得該子程序的狀态,否則wait使其調用者阻塞直到一個子程序終止。如果調用者阻塞并且它有多個子程序,則在其一個子程序終止時,wait就立即傳回。因為wait傳回終止子程序的ID,是以總能了解到是哪一個子程序終止了。
注:僵死程序(zombie),一個已經終止、但是其父程序尚未對其進行善後處理(獲得終止子程序的有關資訊,釋放它仍占用的資)的程序被稱為僵死程序。
3.11 終止狀态的檢視
有4個互斥的宏可以用來擷取程序終止的原因:
WIFEXITED(status)
若子程序正常終止,該宏傳回true。
此時,可以通過WEXITSTATUS(status)擷取子程序的退出狀态(exit status)。
WIFSIGNALED(status)
若子程序由信号殺死,該宏傳回true。
此時,可以通過WTERMSIG(status)擷取使子程序終止的信号值。
WIFSTOPPED(status)
若子程序被信号暫停(stopped),該宏傳回true。
此時,可以通過WSTOPSIG(status)擷取使子程序暫停的信号值。
WIFCONTINUED(status)
若子程序通過SIGCONT恢複,該宏傳回true。
程序和檔案的關系
3.12父子程序間的檔案共享
執行完fork()以後,子程序會得到父程序檔案描述符(file descriptors)的一份拷貝。
父、子程序的檔案描述符指向同一個打開檔案描述(open file description),其中包含目前檔案偏移量(current file offset)和打開檔案狀态标志(open file status flags)。
4.信号
4.1信号基本概念
為什麼程序運作時會出現“段錯誤”?為什麼按下“Ctrl+C”會終止程序?
信号!
信号是核心提供的一種異步消息機制,主要用于核心對程序發送異步通知事件,可以了解為程序執行流程的一個“軟中斷”。
信号總是由核心遞交給程序。但是從應用程式的角度來講,信号的來源是多種多樣的。
4.2常見的信号有哪些呢?
當程序在一個沒有打開的管道上等待時,核心發出SIGPIPE信号。
程序在Shell中前台執行時,使用者按下Ctrl+C組合鍵,将向程序發送SIGINT信号。
使用者使用kill指令向某個程序發送信号。
程序通路非法的記憶體位址時,核心向其發送“段錯誤”信号SIGSEGV。
一個程序使用系統調用向另一個程序發送信号。
發生各種運作時異常(如浮點錯誤)時,核心将向程序發送SIGFPE信号。
...
4.3信号處理機制
在核心對程序進行管理的PCB資訊塊中有若幹個位元組,其中每個比特位用于表示某個信号是否發生。
當需要向某個程序發送一個特定的信号時,就将其PCB資訊塊中對應的比特位置為1。
對信号的處理并不會立刻發生,核心會在程序從核心态傳回使用者态時對目前程序的PCB中表示信号的資料進行檢查,如果有信号發生,則核心會修改目前程序棧中的資訊,使得傳回使用者态後首先執行與信号綁定的處理函數,然後再從目前程序被中斷或進行系統調用的地方繼續執行。
補充:信号何時被處理是應用程式無法預知的。信号處理函數雖然不在程序的正常執行流程中,但也是在使用者态執行的代碼,處于程序的上下文中,可以通路程序的虛拟位址空間。
4.4預設動作
當程序接收到一個信号時,可能執行的預設動作有:
信号被忽略(ignored)。
程序被終止(terminated)。也就是程序的異常終止。
生成一個核心轉儲檔案(core dump file),并且程序被終止。
程序被暫停(stopped)——程序的執行被挂起。
程序的執行被恢複。
4.5 如何向程序發送信号?
程序也可以使用kill給自己發送信号,但在這種情況下,使用raise函數更友善:
#include <signal.h>
int raise(int sig);
使用alarm函數可以在一段指定的時間後給自己發送SIGALRM信号:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
seconds參數表示一個以秒為機關的逾時時間。超過這段時間後,核心将自動向程序發送SIGALRM信号。利用alarm函數可以實作定時操作。
4.6signal()
signal函數是Linux系統上傳統的信号處理接口:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signal函數的作用就是将handler參數所指向的函數注冊成為參數signum所代表的信号的處理函數。signal函數的傳回值是這個信号原來的處理函數,如果傳回SIG_ERR,則說明有錯誤發生。
注冊成功後,所注冊的函數就會在信号被處理時調用,代替了預設的行為,稱為信号被捕捉。
使用signal函數時,應注意以下兩點:
handler參數的值可以是SIG_IGN或SIG_DFL,SIG_IGN表示忽略這個信号,SIG_DFL表示對信号的處理重設為系統的預設方式。
有些信号是不可以忽略或捕獲的,如SIGKILL和SIGSTOP。
4.7sigaction()
signal函數的使用方法比較簡單,但不屬于POSIX标準,在各類UNIX平台上的實作不盡相同,是以其用途收到了一定的限制。POSIX标準定義的信号處理接口是sigaction函數:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
act表示要設定的對信号的新處理方式。oldact表示原來對信号的處理方式。函數執行成功傳回0,失敗傳回-1。
4.8 struct sigaction類型
struct sigaction用來描述對信号的處理,定義如下:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
}
sa_handler與signal函數中的信号處理函數類似。
sa_sigaction是另一個信号處理函數,可以獲得關于信号的更詳細的資訊。
當sa_flags成員的值包含了SA_SIGINFO标志時,系統将使用sa_sigaction函數作為信号處理函數,否則使用sa_handler。
sa_mask成員用來指定在信号處理函數執行期間需要屏蔽的信号。當某個信号被處理時,它自身會被自動放入程序的信号掩碼,是以在信号處理函數執行期間這個信号不會再度發生。
sa_flags成員用于指定信号的處理行為,它可以是以下值的“按位或”組合。
SA_RESTART:使被信号打斷的系統調用自動重新發起。
SA_NOCLDSTOP:使父程序在它的子程序暫停或繼續運作時不會收到SIGCHLD信号。
SA_NOCLDWAIT:使父程序在它的子程序退出時不會收到SIGCHLD信号,這時子程序如果退出也不會成為僵死程序。
SA_NODEFER:使對信号的屏蔽無效,即在信号處理函數執行期間仍能發生這個信号。
SA_RESETHAND:信号處理之後重新設定為預設的處理方式。
SA_SIGINFO:使用sa_sigaction成員而不是sa_handler作為信号處理函數。
sa_restorer成員是一個已廢棄的資料域,一般不使用。
4.9