關於C語言進程操作


關於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系統函數即可創建一個管道。有如下特質:

  1. 其本質是一個偽文件(實為內核緩沖區)
  2. 由兩個文件描述符引用,一個表示讀端,一個表示寫端。
  3. 規定數據從管道的寫端流入管道,從讀端流出。

管道的原理

管道實為內核使用環形隊列機制,借助內核緩沖區(4k)實現。

管道的局限性

  1. 數據自己讀不能自己寫。
  2. 數據一旦被讀走,便不在管道中存在,不可反復讀取。
  3. 由於管道采用半雙工通信方式。因此,數據只能在一個方向上流動。
  4. 只能在有公共祖先的進程間使用管道。

常見的通信方式有,單工通信、半雙工通信、全雙工通信。

pipe 函數

創建管道

int pipe(int pipefd[2]); 成功:0;失敗:-1,設置errno

函數調用成功返回r/w兩個文件描述符。無需open,但需手動close。規定:fd[0] → r; fd[1] → w,就像0對應標准輸入,1對應標准輸出一樣。向管道文件讀寫數據其實是在讀寫內核緩沖區。

管道創建成功以后,創建該管道的進程(父進程)同時掌握着管道的讀端和寫端。如何實現父子進程間通信呢?通常可以采用如下步驟:

geekfx

  1. 父進程調用pipe函數創建管道,得到兩個文件描述符fd[0]、fd[1]指向管道的讀端和寫端。
  2. 父進程調用fork創建子進程,那么子進程也有兩個文件描述符指向同一管道。
  3. 父進程關閉管道讀端,子進程關閉管道寫端。父進程可以向管道中寫入數據,子進程將管道中的數據讀出。由於管道是利用環形隊列實現的,數據從寫端流入管道,從讀端流出,這樣就實現了進程間通信。

管道的讀寫行為

​ 使用管道需要注意以下4種特殊情況(假設都是阻塞I/O操作,沒有設置O_NONBLOCK標志):

  1. 如果所有指向管道寫端的文件描述符都關閉了(管道寫端引用計數為0),而仍然有進程從管道的讀端讀數據,那么管道中剩余的數據都被讀取后,再次read會返回0,就像讀到文件末尾一樣。
  2. 如果有指向管道寫端的文件描述符沒關閉(管道寫端引用計數大於0),而持有管道寫端的進程也沒有向管道中寫數據,這時有進程從管道讀端讀數據,那么管道中剩余的數據都被讀取后,再次read會阻塞,直到管道中有數據可讀了才讀取數據並返回。
  3. 如果所有指向管道讀端的文件描述符都關閉了(管道讀端引用計數為0),這時有進程向管道的寫端write,那么該進程會收到信號SIGPIPE,通常會導致進程異常終止。當然也可以對SIGPIPE信號實施捕捉,不終止進程。具體方法信號章節詳細介紹。
  4. 如果有指向管道讀端的文件描述符沒關閉(管道讀端引用計數大於0),而持有管道讀端的進程也沒有從管道中讀數據,這時有進程向管道寫端寫數據,那么在管道被寫滿時再次write會阻塞,直到管道中有空位置了才寫入數據並返回。

總結

  1. 讀管道:

    1. 管道中有數據,read返回實際讀到的字節數。

    2. 管道中無數據:

      1. 管道寫端被全部關閉,read返回0 (好像讀到文件結尾)
      2. 寫端沒有全部被關閉,read阻塞等待(不久的將來可能有數據遞達,此時會讓出cpu)
  2. 寫管道:

    1. 端全部被關閉, 進程異常終止(也可使用捕捉SIGPIPE信號,使進程不終止)
    2. 管道讀端沒有全部關閉:
      1. 管道已滿,write阻塞。
      2. 管道未滿,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_LOCKF_TLOCK鎖定的區域可以完全或部分包含同一個進程以前鎖定的區域,或被同一個進程以前鎖定的區域包含。此時,這些區域將會合並為一個區域。如果請求要求將新元素添加到活動鎖定表中,但該表已滿,則會返回一個錯誤,並且不會鎖定新區域。

F_LOCKF_TLOCK請求僅在采取的操作上有所差異(如果資源不可用)。如果區域已被其他進程鎖定,F_LOCK 將使調用進程進入休眠狀態,直到該資源可用,而F_TLOCK則會返回[EACCES]錯誤。

F_TEST用於檢測在指定的區域中是否存在其他進程的鎖定。如果該區域被鎖定,lockf()將返回 -1,否則返回0;在這種情況下,errno設置為[EACCES]。F_LOCKF_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的值不是NULLwait就會把子進程退出時的狀態取出並存入其中, 這是一個整數值(int),指出了子進程是正常退出還是被非正常結束的,以及正常結束時的返回值,或被哪一個信號結束的等信息。由於這些信息 被存放在一個整數的不同二進制位中,所以用常規的方法讀取會非常麻煩,人們就設計了一套專門的宏(macro)來完成這項工作,下面我們來學習一下其中最常用的兩個:

  1. WIFEXITED(status)這個宏用來指出子進程是否為正常退出的,如果是,它會返回一個非零值。

請注意,雖然名字一樣,這里的參數status並不同於wait唯一的參數–指向整數的指針status,而是那個指針所指向的整數,切記不要搞混了。

  1. 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)

該函數會促使生成信號sigsig參數與SIG宏兼容。

參數

  • sig
    要發送的信號碼。

返回值

如果成功該函數返回零,否則返回非零。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM