關於C語言進程操作
Linux標准庫 <unistd.h>
符號常量
NULL // Null pointer
SEEK_CUR // Set file offset to current plus offset.
SEEK_END // Set file offset to EOF plus offset.
SEEK_SET // Set file offset to offset.
是POSIX標准定義的unix類系統定義符號常量的頭文件,包含了許多UNIX系統服務的函數原型,例如read函數、write函數和getpid函數。
unistd.h在unix中類似於window中的windows.h。
#ifdef WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif
函數原型
ssize_t read(int, void *, size_t);
int unlink(const char *);
ssize_t write(int, const void *, size_t);
int usleep(useconds_t);
unsigned sleep(unsigned);
int access(const char *, int);
unsigned alarm(unsigned);
int chdir(const char *);
int chown(const char *, uid_t, gid_t);
int close(int);
size_t confstr(int, char *, size_t);
void _exit(int);
pid_t fork(void);
關於管道pipe
管道的概念
管道是一種最基本的IPC機制,作用於有血緣關系的進程之間,完成數據傳遞。調用pipe系統函數即可創建一個管道。有如下特質:
- 其本質是一個偽文件(實為內核緩沖區)
- 由兩個文件描述符引用,一個表示讀端,一個表示寫端。
- 規定數據從管道的寫端流入管道,從讀端流出。
管道的原理
管道實為內核使用環形隊列機制,借助內核緩沖區(4k)實現。
管道的局限性
- 數據自己讀不能自己寫。
- 數據一旦被讀走,便不在管道中存在,不可反復讀取。
- 由於管道采用半雙工通信方式。因此,數據只能在一個方向上流動。
- 只能在有公共祖先的進程間使用管道。
常見的通信方式有,單工通信、半雙工通信、全雙工通信。
pipe 函數
創建管道
int pipe(int pipefd[2]); 成功:0;失敗:-1,設置errno
函數調用成功返回r/w兩個文件描述符。無需open,但需手動close。規定:fd[0] → r; fd[1] → w,就像0對應標准輸入,1對應標准輸出一樣。向管道文件讀寫數據其實是在讀寫內核緩沖區。
管道創建成功以后,創建該管道的進程(父進程)同時掌握着管道的讀端和寫端。如何實現父子進程間通信呢?通常可以采用如下步驟:
- 父進程調用pipe函數創建管道,得到兩個文件描述符fd[0]、fd[1]指向管道的讀端和寫端。
- 父進程調用fork創建子進程,那么子進程也有兩個文件描述符指向同一管道。
- 父進程關閉管道讀端,子進程關閉管道寫端。父進程可以向管道中寫入數據,子進程將管道中的數據讀出。由於管道是利用環形隊列實現的,數據從寫端流入管道,從讀端流出,這樣就實現了進程間通信。
管道的讀寫行為
使用管道需要注意以下4種特殊情況(假設都是阻塞I/O操作,沒有設置O_NONBLOCK標志):
- 如果所有指向管道寫端的文件描述符都關閉了(管道寫端引用計數為0),而仍然有進程從管道的讀端讀數據,那么管道中剩余的數據都被讀取后,再次read會返回0,就像讀到文件末尾一樣。
- 如果有指向管道寫端的文件描述符沒關閉(管道寫端引用計數大於0),而持有管道寫端的進程也沒有向管道中寫數據,這時有進程從管道讀端讀數據,那么管道中剩余的數據都被讀取后,再次read會阻塞,直到管道中有數據可讀了才讀取數據並返回。
- 如果所有指向管道讀端的文件描述符都關閉了(管道讀端引用計數為0),這時有進程向管道的寫端write,那么該進程會收到信號SIGPIPE,通常會導致進程異常終止。當然也可以對SIGPIPE信號實施捕捉,不終止進程。具體方法信號章節詳細介紹。
- 如果有指向管道讀端的文件描述符沒關閉(管道讀端引用計數大於0),而持有管道讀端的進程也沒有從管道中讀數據,這時有進程向管道寫端寫數據,那么在管道被寫滿時再次write會阻塞,直到管道中有空位置了才寫入數據並返回。
總結
-
讀管道:
-
管道中有數據,read返回實際讀到的字節數。
-
管道中無數據:
- 管道寫端被全部關閉,read返回0 (好像讀到文件結尾)
- 寫端沒有全部被關閉,read阻塞等待(不久的將來可能有數據遞達,此時會讓出cpu)
-
-
寫管道:
- 端全部被關閉, 進程異常終止(也可使用捕捉SIGPIPE信號,使進程不終止)
- 管道讀端沒有全部關閉:
- 管道已滿,write阻塞。
- 管道未滿,write將數據寫入,並返回實際寫入的字節數。
當管道進行寫入操作的時候,如果寫入的數據小於128K則是非原子的,如果大於128K字節,緩沖區的數據將被連續地寫入管道,直到全部數據寫完為止,如果沒有進程讀取數據,則將一直阻塞。
命名管道FIFO
管道最大的劣勢就是沒有名字,只能用於有一個共同祖先進程的各個進程之間。FIFO代表先進先出,單它是一個單向數據流,也就是半雙工,和管道不同的是:每個FIFO都有一個路徑與之關聯,從而允許無親緣關系的進程訪問。
關於 stdin、stdout 和 STDOUT_FILENO、STDIN_FILENO
在UNIX系統調用中,標准輸入描述字用stdin
,標准輸出用stdout
,標准出錯用stderr
表示,但在一些調用函數,引用了STDIN_FILENO
表示標准輸入才,同樣,標准出入用STDOUT_FILENO
,標准出錯用STDERR_FILENO
。
stdin
等是FILE *
類型,屬於標准I/O,在<stdio.h>
。
STDIN_FILENO
等是文件描述符,是非負整數,一般定義為0, 1, 2,屬於沒有buffer的I/O,直接調用系統調用,在<unistd.h>
。
關於 perror
perror(s)
用來將上一個函數發生錯誤的原因輸出到標准設備(stderr)。參數s
所指的字符串會先打印出,后面再加上錯誤原因字符串。此錯誤原因依照全局變量errno
的值來決定要輸出的字符串。
在庫函數中有個errno
變量,每個errno
值對應着以字符串表示的錯誤類型。當你調用"某些"函數出錯時,該函數已經重新設置了errno
的值。perror
函數只是將你輸入的一些信息和errno
所對應的錯誤一起輸出。
關於 lockf
lockf()
函數允許將文件區域用作信號量(監視鎖),或用於控制對鎖定進程的訪問(強制模式記錄鎖定)。試圖訪問已鎖定資源的其他進程將返回錯誤或進入休眠狀態,直到資源解除鎖定為止。當關閉文件時,將釋放進程的所有鎖定,即使進程仍然有打開的文件。當進程終止時,將釋放進程保留的所有鎖定。
int lockf(int fd, int cmd, off_t len);
fd
是打開文件的文件描述符cmd
是指定要采取的操作的控制值,允許的值在中定義F_ULOCK
0 //解鎖F_LOCK
1 //互斥鎖定區域F_TLOCK
2 //測試互斥鎖定區域F_TEST
3 //測試區域
F_ULOCK
請求可以完全或部分釋放由進程控制的一個或多個鎖定區域。如果區域未完全釋放,剩余的區域仍將被進程鎖定。如果該表已滿,將會返回[EDEADLK]錯誤,並且不會釋放請求的區域。
使用F_LOCK
或F_TLOCK
鎖定的區域可以完全或部分包含同一個進程以前鎖定的區域,或被同一個進程以前鎖定的區域包含。此時,這些區域將會合並為一個區域。如果請求要求將新元素添加到活動鎖定表中,但該表已滿,則會返回一個錯誤,並且不會鎖定新區域。
F_LOCK
和F_TLOCK
請求僅在采取的操作上有所差異(如果資源不可用)。如果區域已被其他進程鎖定,F_LOCK
將使調用進程進入休眠狀態,直到該資源可用,而F_TLOCK
則會返回[EACCES]錯誤。
F_TEST
用於檢測在指定的區域中是否存在其他進程的鎖定。如果該區域被鎖定,lockf()
將返回 -1,否則返回0;在這種情況下,errno
設置為[EACCES]。F_LOCK
和F_TLOCK
都用於鎖定文件的某個區域(如果該區域可用)。F_ULOCK
用於刪除文件區域的鎖定。
len
是要鎖定或解鎖的連續字節數
要鎖定的資源從文件中當前偏移量開始
對於正len
將向前擴展
對於負len
則向后擴展(直到但不包括當前偏移量的前面的字節數)。
如果len
為零,則鎖定從當前偏移量到文件結尾的區域(即從當前偏移量到現有或任何將來的文件結束標志)。
要鎖定一個區域,不需要將該區域分配到文件中,因為這樣的鎖定可以在文件結束標志之后存在。
返回值
此函數調用成功后,將返回值0
,否則返回−1
,並且設置errno
以表示該錯誤。 由於當文件的某部分被其他進程鎖定后,變量errno
將會設置為[EAGAIN]而不是[EACCES],因此可移植應用程序應對這兩個值進行預計和測試。
關於 wait
編程過程中,有時需要讓一個進程等待另一個進程,最常見的是父進程等待自己的子進程,或者父進程回收自己的子進程資源包括僵屍進程。這里簡單介紹一下系統調用函數:wait()
函數原型
#include <sys/types.h>
#include <wait.h>
int wait(int *status);
函數功能
父進程一旦調用了wait
就立即阻塞自己,由wait
自動分析是否當前進程的某個子進程已經退出,如果讓它找到了這樣一個已經變成僵屍的子進程,wait
就會收集這個子進程的信息,並把它徹底銷毀后返回;如果沒有找到這樣一個子進程,wait
就會一直阻塞在這里,直到有一個出現為止。
當父進程忘了用wait()
函數等待已終止的子進程時,子進程就會進入一種無父進程的狀態,此時子進程就是僵屍進程。
wait()
要與fork()
配套出現,如果在使用fork()
之前調用wait()
,wait()
的返回值則為-1
,正常情況下wait()
的返回值為子進程的PID
。
如果先終止父進程,子進程將繼續正常進行,只是它將由init
進程(PID 1
)繼承,當子進程終止時,init
進程捕獲這個狀態。
參數status
用來保存被收集進程退出時的一些狀態,它是一個指向int
類型的指針。但如果我們對這個子進程是如何死掉毫不在意,只想把這個僵屍進程消滅掉,(事實上絕大多數情況下,我們都會這樣想),我們就可以設定這個參數為NULL
,就像下面這樣:
pid = wait(NULL);
如果成功,wait
會返回被收集的子進程的進程ID,如果調用進程沒有子進程,調用就會失敗,此時wait
返回-1
,同時errno
被置為ECHILD
。
如果參數status
的值不是NULL
,wait
就會把子進程退出時的狀態取出並存入其中, 這是一個整數值(int
),指出了子進程是正常退出還是被非正常結束的,以及正常結束時的返回值,或被哪一個信號結束的等信息。由於這些信息 被存放在一個整數的不同二進制位中,所以用常規的方法讀取會非常麻煩,人們就設計了一套專門的宏(macro)來完成這項工作,下面我們來學習一下其中最常用的兩個:
WIFEXITED(status)
這個宏用來指出子進程是否為正常退出的,如果是,它會返回一個非零值。
請注意,雖然名字一樣,這里的參數status
並不同於wait
唯一的參數–指向整數的指針status
,而是那個指針所指向的整數,切記不要搞混了。
WEXITSTATUS(status)
當WIFEXITED
返回非零值時,我們可以用這個宏來提取子進程的返回值,如果子進程調用exit(5)
退出,WEXITSTATUS(status)
就會返回5;如果子進程調用exit(7)
,WEXITSTATUS(status)
就會返回7。請注意,如果進程不是正常退出的,也就是說,WIFEXITED
返回0,這個值就毫無意義。
關於僵屍進程
僵屍進程是當子進程比父進程先結束,而父進程又沒有回收子進程,釋放子進程占用的資源,此時子進程將成為一個僵屍進程。如果父進程先退出 ,子進程被init
接管,子進程退出后init
會回收其占用的相關資源。
在UNIX 系統中,一個進程結束了,但是他的父進程沒有等待(調用wait / waitpid
)他, 那么他將變成一個僵屍進程。 但是如果該進程的父進程已經先結束了,那么該進程就不會變成僵屍進程, 因為每個進程結束的時候,系統都會掃描當前系統中所運行的所有進程, 看有沒有哪個進程是剛剛結束的這個進程的子進程,如果是的話,就由init
來接管他,成為他的父進程。
一個進程在調用exit
命令結束自己的生命的時候,其實它並沒有真正的被銷毀, 而是留下一個稱為僵屍進程(Zombie)的數據結構(系統調用exit
,它的作用是使進程退出,但也僅僅限於將一個正常的進程變成一個僵屍進程,並不能將其完全銷毀)。
關於 signal.h
signal.h
頭文件定義了一個變量類型sig_atomic_t
、兩個函數調用和一些宏來處理程序執行期間報告的不同信號。
庫變量
sig_atomic_t
這是int
類型,在信號處理程序中作為變量使用。它是一個對象的整數類型,該對象可以作為一個原子實體訪問,即使存在異步信號時,該對象可以作為一個原子實體訪問。
庫宏
以下宏與signal
函數一起使用來定義信號的功能。
SIG_DFL
默認的信號處理程序。SIG_ERR
表示一個信號錯誤。SIG_IGN
忽視信號。
以下宏用於表示以下各種條件的信號碼。
SIGABRT
程序異常終止。SIGFPE
算術運算出錯,如除數為 0 或溢出。SIGILL
非法函數映象,如非法指令。SIGINT
中斷信號,如 ctrl-C。SIGSEGV
非法訪問存儲器,如訪問不存在的內存單元。SIGTERM
發送給本程序的終止請求信號。
庫函數
void (*signal(int sig, void (*func)(int)))(int)
該函數設置一個函數來處理信號,即信號處理程序。
參數
sig
在信號處理程序中作為變量使用的信號碼func
一個指向函數的指針,它可以是一個由程序定義的函數。
int raise(int sig)
該函數會促使生成信號sig
。sig
參數與SIG
宏兼容。
參數
sig
要發送的信號碼。
返回值
如果成功該函數返回零,否則返回非零。