1. 程式人生 > >Unix/Linux程式設計-程序

Unix/Linux程式設計-程序

程序

6.1 程序的開始與終止

6.1.1 mian函式

當核心執行C程式時,在呼叫main前先呼叫一個特殊的啟動例程。可執行程式檔案將此啟動例程指定為程式的其實地址,這時由連線編輯器設定的,而連線編輯器則由C編譯器呼叫。啟動歷程從核心取得命令列引數和環境變數值,然後為按上述方式呼叫main函式做好安排。

6.1.2 程序終止

有8種方式使程序終止,其中1-5為正常終止,6-8為異常終止,它們是:
(1) 從main返回;
(2) 呼叫exit;
(3) 呼叫_exit或_Exit;
(4) 最後一個執行緒從其啟動例程返回;
(5) 從最後一個執行緒呼叫pthread_exit
(6)
呼叫abort
(7) 接到一個訊號
(8) 最後一個執行緒對取消請求做出響應。

6.1.3 退出函式

#include <stdlib.h>

void exit(int status);

void _Exit(int status);

 

#include <unistd.h>

void _exit(int status);

3個函式用於正常終止一個程式;_exit和_Exit立即進入核心,exit則限制性一些清理處理,然後返回核心。exit函式總是執行一個標準I/O庫的清理關閉操作。
3個函式都帶一個整型引數,稱為終止狀態。

6.1.4 函式atexit

按照ISO C的規定,一個程序可登記多至32個函式,這些函式將由exit自動呼叫。我們稱這些函式為終止處理函式,並呼叫atexit函式來登記這些函式

#include <stdlib.h>

int atexit(void (*fun) (void));

返回值:成功返回0,錯處返回非0

引數是一個函式指標,當呼叫此函式無需向他傳遞任何引數,也不期望他返回一個值。exit呼叫這些函式的順序與登記時候的順序相反。
下圖顯示了一個C程式時如何啟動的,以及它終止的各種方式:

6.2 命令列引數與環境表

6.2.1 命令列引數

當執行一個程式時,呼叫 exec的 程序可將命令列引數傳遞給新程式。

6.2.2 環境表

每個程式都接收到一張環境表,環境表是一個字元指標陣列,全域性變數environ則包含了該指標陣列的地址:
extern char **environ;
其中每個字串的結尾處都顯示的有一個null位元組。
6.2.3 環境變數
如同前述,環境字串的形式是:
name=value
Unix核心並不檢視這些字串,它們的解釋完全取決於各個應用程式。
ISO C定義了一個函式getenv,可以用其取環境變數值,但是該標準又稱環境的內容是由實現定義的:

#include <stdlib.h>

char *getenv(const char *name);

返回值:指向與name關聯的value的指標;未找到返回NULL

函式返回一個指標,指向name=value字串中的value。

除了獲取環境變數值,有時也需要設定環境變數。我們可能希望改變現有變數的值,或者是增加新的環境變數,但並不是所有系統都支援這種能力。

#include <stdlib.h>

int putenv(char *str);

返回值:成功返回0,出錯返回非0

int setenv(const char *name, const char *value, int rewite);

int unsetenv(const char *name);

返回值:成功返回0,出錯返回-1


(1) putenv取形式為name=value的字串,將其放到環境表中。如果name已經存在,則先刪除其原來的定義。
(2) setenv將name設定為value。如果在環境中name已經存在,那麼若rewrite非0,則首先刪除其現有的定義,若rewrite為0,則不刪除現有定義(name不設定為新的value,而且也不出錯)。
(3) unsetenv刪除name的定義。即使不存在這種定義也不算出錯。


putenv和setenv的區別:
setenv必須分配儲存空間,以便依據其引數建立name=value字串。putenv可以自由地將傳遞給他的引數字串直接放到環境中。確實,許多實現就是這麼做的。因此,將存放在棧中的字串作為引數傳遞給putenv就會發生錯誤,其原因是,從當前函式返回時,其棧幀佔用的儲存區可能被重用。

6.3 程序標識

每個程序都要一個非負整數表示的唯一程序ID。雖然是唯一的,但是程序ID是可複用的。當一個簡稱終止後,其程序ID就成為複用的候選者。大多數Unix系統實現延遲複用演算法,使得賦予新建程序的ID不同於最近終止程序所使用的ID。
系統中有一些專用程序,ID為0的程序通常是排程程序,場場被稱為交換程序,該程序是核心的一部分,它並不執行任何磁碟上的程式,因此被稱為系統程序。ID為1的程序通常是init程序,在自舉過程結束時由核心呼叫。init通常讀取與系統有關的初始化檔案,並將系統引導到一個狀態,init程序絕不會終止。

#include <unistd>

pid_t getpid(void);

返回值:呼叫程序的程序ID

 

pid_t getppid(void);

返回值:呼叫程序的父程序ID

 

uid_t getuid(void);

返回值:呼叫程序的實際使用者ID

 

uid_t geteuid(void);

返回值:呼叫程序的有效使用者ID

 

gid_t getgid(void);

返回值:呼叫程序的實際組ID

 

gid_t getegid(void);

返回值:呼叫程序的實際組ID

6.4 建立一個新程序:fork

6.4.1 fork函式

#include <unistd.h>

pid_t fork(void);

返回值:子程序返回0,父程序返回子程序ID;出錯返回-1

fork建立的新程序被稱為子程序。fork函式被呼叫一次,但返回兩次(一次在父程序中,一次在子程序中)。兩次返回的區別是子程序的返回值是0,而父程序的返回值則是新建子程序的程序ID。子程序和父程序繼續執行fork呼叫之後的指令。子程序是父程序的副本,子程序獲得父程序的資料空間、堆和棧的副本。父程序和子程序並不共享這些儲存空間部分,共享正文段。
由於fork之後經常跟著exec,所以現在很多實現並不執行一個父程序資料段、堆和棧的完全副本。所謂替代,使用了寫時賦值技術。這些區域由父程序和子程序共享,而且核心將他們的訪問許可權改變為只讀。如果父程序和子整合中的任何一個試圖修改程序共享,則核心只為修改區域的那塊記憶體製作一個副本,通常是虛擬儲存系統中的一頁。
一般來說,在fork之後是父程序先執行還是子程序先執行是不確定的,這取決於核心所使用的排程演算法。

6.4.2 父程序和子程序之間的區別

(1) fork的返回值不同。
(2) 程序ID不同。
(3) 兩個程序的父程序ID不同;子程序的父程序ID是建立它的程序的ID,而父程序的父程序ID不變。
(4) 子程序的tms_utime、tms_stime、tms_cutime和tms_ustime的值設定為0.
(5) 子程序不繼承父程序設定的檔案鎖。
(6) 子程序的未處理訊號集合為空集。

6.4.3 fork失敗的原因

(1) 系統中已經有了太多的程序。
(2) 該實際使用者ID的程序總數超過了系統限制

6.4.4 fork的應用場景

(1) 一個父程序希望複製自己,使父程序和子程序同時執行不同的程式碼段。
(2) 一個程序要執行不同的程式。這種情況下,子程序從fork返回後立即呼叫exec。

6.5 wait和waitpid

當一個程序正常或異常終止時,核心就向其父程序傳送SIGCHLD訊號。子程序終止是非同步時間,所以這種訊號也是核心向父程序發的非同步通知。父程序可以選擇忽略該訊號,或者提供一個該訊號發生時被呼叫的執行函式(訊號處理程式)。對於這種訊號的系統預設動作是忽略它。

#include <sys/wait.h>

pid_t wait(int *statloc);

pid_t waitpid(pid_t pid, int *statloc, int options);

返回值:成功返回程序ID,錯處返回0或-1

我們需要知道,呼叫wait或waitpid的程序可能會發生什麼:
(1) 如果其所有子程序都還在執行,則阻塞。
(2) 如果一個子程序已終止,正等待父程序獲取其終止狀態,則取得該子程序的終止狀態立即返回。
(3) 如果他沒有任何子程序,則立即出錯返回。


引數statloc如果不是一個空指標,則終止程序的終止狀態就存放在它所指向的單元內。終止狀態用第一在<sys/wait.h>中的巨集來檢視。有4個互斥的巨集可用來取得程序終止原因。

巨集

說明

WIFEXITED(status)

若為正常終止程序返回的狀態,則為真。可執行WEXITSTATUS(status),獲取子程序傳送給exit或_exit引數的低8位

WIFSIGNALED(status)

若為異常終止子程序返回的狀態,則為真(接到一個不捕獲的訊號)。可執行WTERMSIG(status),獲取使子程序終止的訊號編號。

WIFSTOPPED(status)

若為當前暫停子程序的返回狀態,則為真。可執行WSTOPSIG(status),獲取使子程序暫停的訊號編號

WIFCONTUNUED(status)

若在作業控制暫停後已經繼續的子程序返回了狀態,則為真。

waitpid函式提供以下功能:
(1) pid==-1,等待任一程序,與wait等效。
(2) pid>0,等待程序ID與pid相等的子程序。
(3) pid==0,等待組ID等於呼叫程序ID的任一子程序。
(4) pid<-1,等待組ID等於pid絕對值的任子程序。
對於waitpid,如果指定的程序或程序組不存在,或者引數pid指定的程序不是呼叫程序的子程序,都可以能出錯。
options引數使我們能進一步控制waitpid的操作,此引數或者是0,或者是下面的常量按位或運算的結果。

常量

說明

WCONTNUED

若實現支援作業控制,那麼由pid指定的任一子程序在停止後已經繼續,但其狀態尚未報告,則返回其狀態。

WNOHANG

若pid指定的子程序並不是立即可用的,則waitpid不阻塞,此時其返回值為0。

6.6 waitid函式

#include <sys/wait.h>

int waited(idtype_t idtype, id_t id, siginfo_t *infop, int options);

返回值:若成功,返回0,出錯返回-1

waitid允許一個程序指定要等待的子程序,但它使用兩個單獨的引數表示要等待的子程序所屬的型別,而不是將此程序ID或程序組ID合成一個引數。id引數的作用域idtype的值相關。該函式支援的idtype型別如下:

常量

說明

P_PID

等待一特定程序:id包含要等待子程序的程序ID

P_PGID

等待一特定程序組中的任一子程序:id包含要等待子程序的程序組ID

P_ALL

等待任一子程序:忽略id

options引數如下各標誌位的按位或運算:

常量

說明

WCONTINUED

等待一程序,它以前曾被停止,此後已繼續,但其狀態尚未報告。

WEXITED

等待已退出的程序。

WNOHANG

如無可用的子程序退出狀態,立即返回而非阻塞。

WNOWAIT

不破壞子程序退出狀態。該子程序退出狀態可由後續的wait、waitid或waitpid呼叫取得。

WSTOPPED

等待一程序,它已經停止,但其狀態尚未報告。

WCONTINUED、WEXITED或WSTOPPED這3個常量之一必須在options引數中指定。
infop引數是指向siginfo結構的指標。該結構包含了在成子程序狀態改變的有關訊號的詳細資訊。

6.7 exec函式族

當程序呼叫一種exec函式時,該程序執行的程式完全替換為新程式,而新程式則從其main函式開始執行。因為呼叫exec並不建立新程序,所以前後的程序ID並未改變。exec只是用磁碟上的一個新程式替換了當前程式的正文段、資料段、堆段和棧段。

#include <unistd.h>

int execl(const char *pathname, const char *arg0, … /* (char *) 0  */);

int execv(const char *pathname, car *const argv[]);

int execle(const char *pathname, const char *arg0,…/* (char *)0 , char *const envp[] */);

int execve(const char *pathname, char *const argv[], char *const envp[]);

int execlp(const char *filename, const char *arg0,… /* (char *) 0 */);

int execvp(const char *filename, char *const srgv[]);

int fexecve(int fd, char *const argv[], char *const envp[]);

出錯返回-1,成功不返回。

前4個函式取路徑名作為引數,後兩個函式則取檔名作為引數,最後一個取檔案描述符作為引數。當指定filename作為引數時:
(1) 如果filename包含/,則就將其視為路徑名;
(2) 否則就按PATH環境變數,在它所指定的各目錄中搜索可執行檔案。


如果execlp或execvp使用路徑字首中的一個找到了一個可執行檔案,但是該檔案不是由連結編輯器產生的機器可執行檔案,則就認為該檔案是一個shell指令碼。
execl、execlp和execle要求將新程式的每一個命令列引數都說明為一個單獨的引數,以空指標結尾。對於另外4個函式,則應先構造一個指向各引數的指標陣列,然後將該陣列地址作為4個函式的引數。
以e結尾的3個函式可以傳遞一個指向環境字串指標陣列的指標。

6.8 更改使用者ID和更改組ID

可以用setuid函式設定實際使用者ID和有效使用者ID。於此類似,可以用setgid函式設定實際組ID和有效組ID。

#include <unistd.h>

int setuid(uid_t uid);

int setgid(gid_t gid);

函式返回值:成功返回0,出錯返回-1

更改使用者ID(組ID)的規則:
(1) 若程序具有超級使用者特權,則setuid函式將實際使用者ID、有效使用者ID以及儲存的設定使用者ID設定為uid。
(2) 若程序沒有超級使用者去特權,但是uid等於實際使用者ID或儲存的設定使用者ID,則setuid將有效使用者ID設定為uid。
(3) 若上面兩個條件都不滿足,則errno設定為EPERM,並返回-1。


關於核心所維護的3個使用者ID,還需要注意一下幾點:
(1) 只有超級使用者程序可以更改實際使用者ID。通常,實際使用者ID是在使用者登入時,有login(1)程式設定的,而且決不會改變它。因為login是一個超級使用者程序,呼叫它時,設定所有3個使用者ID。
(2) 僅當對程式檔案設定了設定使用者ID位時,exec才設定有效使用者ID。如果設定使用者ID位沒有設定,exec函式不會改變有效使用者ID,而將維持其現有值。 任何時候都可以呼叫setuid,將有效使用者ID設定為實際使用者ID或儲存的設定使用者ID。自然地,不能將有效使用者ID設定為任一隨機值。
(3) 儲存的設定使用者ID是由exec賦值有效使用者ID而得到的。如果設定了檔案的設定使用者ID位,則在exec根據檔案的使用者ID設定了程序的有效使用者ID以後,這個副本就被儲存起來了。
更改3個使用者ID不同的方法:

ID

exec

setuid

設定使用者ID關閉

設定使用者ID為開啟

超級使用者

非特權使用者

實際使用者ID

不變

不變

設為uid

不變

有效使用者ID

不變

設定為程式檔案的使用者ID

設為uid

設為uid

儲存的設定使用者ID

從有效使用者ID複製

從有效使用者ID複製

設為uid

不變

6.9 程序排程

Unix系統歷史上對程序提供的只是基於排程優先順序的粗粒度的控制,排程策略和排程優先順序是由核心確定的。程序通過調整nice值選擇以更低優先順序執行,只有特權程序允許提高排程許可權。nice值越小,優先順序越高。
程序可以通過nice函式獲取或更改他的nice值,隻影響自己的nice值,不能影響其他任何程序的nice值。

6.9.1 nice函式

#include <unistd.h>

int nice(int incr);

返回值:成功返回新的nice值NZERO,出錯返回-1

incr引數被增加到呼叫程序的nice值上。如果incr太大,系統直接把它降低到最大的合法值,由於-1是合法的成功返回值,在呼叫nice函式之前需要清除errno,在nice函式返回-1時,需要檢查他的值。如果nice呼叫成功,並且返回值為-1,那麼errno仍然為0,。如果errno部位0,說明nice呼叫失敗。

6.9.2 getpriority函式

getpriority函式可以像nice函式那樣用於獲取程序的nice值,但是getpriority還可以獲取一組相關程序的nice值。


#include <sys/resource.h>

int getpriority(int which, id_t who);

返回值:成功返回-NZERO~NZERO-1之間的nice值,出錯返回-1.

which引數可以取一下三個值之一:PRIO_PROCESS表示程序,PRIO_PGRP表示程序組,PRIO_USER表示使用者ID。which引數控制who引數是如何解釋的,who引數選擇感興趣的一個或多個執行緒。如果who引數為0,表示呼叫程序、程序組或使用者(取決於which引數的值)。

6.10 程序時間

任一程序都可以呼叫times函式獲得它自己以及已終止子程序的上述值

#include <sys/timies.h>

clock_t times(struct tms *buf);

車才能更改,返回流逝的牆上時間(以時鐘滴答數為單位),出錯返回-1

tms結構體定義如下:

struct tms {

               clock_t tms_utime;  /* user time */

               clock_t tms_stime;  /* system time */

               clock_t tms_cutime; /* user time of children */

               clock_t tms_cstime; /* system time of children */

           };

此結構沒有包含牆上時鐘時間,times函式返回牆上時鐘時間作為其函式值。