目錄
進程終止
errno
exit和_exit
進程等待
wait和waitpid
宏:WIFEXITED
非阻塞等待
進程終止
下面要談的一個話題就是進程終止,就是說一個進程退出了,可能有三種情況
1.進程代碼執行完,結果是正確的
2.進程代碼執行完,結果是錯誤的
3.進程代碼沒有執行完,進程出異常了,中途退出了
其實我們寫的main函數執行起來就是一個進程,而我們一般寫的return 0,就叫做進程的退出碼。一般0表示正確執行,第一種情況;而非0表示執行失敗,第二種情況。因為main函數的return 0就已經是代碼的最后部分了。
為什么用非0表示執行失敗呢?因為成功就是成功了,而失敗可能會有很多種原因。進程的退出碼是給機器看的,要是給人看,就要把退出碼轉化成錯誤描述,這個錯誤描述可以是系統或語言自帶的,也可以自定義,下面我們先用strerror函數看一下系統中的錯誤描述
我們可以看到有很多錯誤描述,到133個了
這時我們就能解釋我們瞎給比如ls 后面一個選項,bash進程,就是命令行解釋器報的錯是什么了,比如:
這不就是上面的二號錯誤嗎,另外,下面的指令可以查看最近一次進程的退出碼
為什么第二次用顯示0呢?因為echo $?也是一個進程,它是成功執行的
我們上面說也可以自定義,那就可以創建幾個字符串枚舉值,并且枚舉值是可以表示整數的,這樣就可以自己去定義錯誤碼了,比如:
上面我們說了前兩種情況,第三種情況是進程沒有執行完,中途出現異常了,只要中間出現異常,其實結果對與否就沒有意義了。其實中途出現異常本質上就是進程收到了異常信號,就是kill -l那一系列的信號
比如:
8號信號對應的是?
SIGFPE
(Floating-Point Exception)信號。這個信號用于指示浮點運算異常,比如除以零或溢出等情況。11號信號對應的是?
SIGSEGV
(Segmentation Fault)信號。這個信號用于指示進程發生了內存段錯誤,即試圖訪問無效的內存地址。
并且有一個細節就是這些錯誤是從1開始的,這跟我們下面的如何用16比特位表示退出碼和收到什么信號是有關系的,并且這些大寫的字母都是宏定義。
所以進程執行的情況可以由兩個數字表示,一個是收到什么信號(0就表示沒有收到信號),一個是退出碼。
errno
除了進程退出,就是main函數退出,我們還有普通函數退出,那我們如何知道它的運行情況呢?我們也有個存放錯誤碼的東西,叫errno,就是說,函數如果執行失敗的話,那么錯誤碼會放在erron這個整形變量里,這個一般庫函數才會有這個錯誤碼,因為庫函數內部一般是有這個賦值的,我們一般寫的函數沒有,比如fopen,可以看一下它的返回值
可以看到errno這個東西,下面寫一個代碼看一下
其實我當前目錄根本就沒有這個文件,并且是以只讀的方式打開文件,所以它肯定會出錯,錯誤信息就存在errno里面,我們也可以看具體字符信息,運行之后就是這樣
exit和_exit
我們如果想讓一個進程退出可以用exit或_exit,前者是庫函數,后者是系統調用,我們可以來查一下
這里的參數status就是你想讓進程退出時的退出碼,我們通過一段代碼來展示一下它們的區別
我們寫這樣一個代碼,運行完后發現什么都不打印,而把_exit改成exit后就會打印,這就說明exit會刷新緩沖區,其實exit就是封裝了_exit,為什么要這樣做呢?
其實我們知道庫函數和系統調用是上下層關系,不同的操作系統的系統調用是不同的,比如Windows下_exit就用不了,所以這時我們把系統調用再封裝一層成為庫函數,不同的操作系統封裝不同的系統調用,但是它們的庫函數的接口就是一樣的了,這就通過庫函數屏蔽掉了系統調用的差異,就實現了語言的可移植性和跨平臺性,所以不同的操作系統就會安裝不同的庫文件。并且這里的緩沖區是庫級別的緩沖區,所以系統調用是無法刷新的,如果是操作系統級別的,那就會刷新,因為操作系統不會讓它白白占著空間的。
進程等待
我們之前說過,父進程要回收子進程的PCB來拿到子進程的退出信息,如果父進程不管不顧,子進程就會進入僵尸狀態,就會造成內存泄漏,這時就算kill -9也無能為力,因為誰也不能殺死一個已經死掉的進程。而父進程回收子進程就是通過進程等待
wait和waitpid
我們有兩種等待方式,分別是wait和waitpid,我們可以man查一下
這里的status是一個輸出型參數,通過給一個整型變量的地址,這個函數內部就可以將退出信息寫入到這個地址中,如果不想讓它寫入可以給NULL;pid就是要等待的子進程的pid,如果是-1的話,那么等待任一子進程,這時與wait等效;options是確定父進程是阻塞等待還是非阻塞等待
我們再看一下返回值是什么意思
就是說:如果成功等待到了子進程結束,就返回子進程的PID;如果等待失敗(如果pid參數指定的子進程不存在或不是當前進程的子進程或是如果調用被一個信號中斷)就返回-1;如果非阻塞等待(WNOHANG)等待后,子進程狀態沒變,那么返回0。
知道了各個參數和返回值是什么意思,那我們就可以簡單的來使用一下wait和waitpid:
我們上面說過,任何進程最終的執行情況可以有兩個數字表明,一個是退出碼,一個是退出信號,如果收到退出信號,那么最終的退出碼沒有意義。waitpid就是通過status這個輸出型參數拿到這兩個數字的,那么這兩個數字是怎么存在一個status中的呢?我們來看一下:
我們只用status中32個比特位中的低16位,也就是0-15位,如果進程沒有收到退出信號,那么8-15位就表示子進程的退出碼,所以我上面獲取退出碼時是先進行位右移8位,然后按位與上0xff;如果收到退出信號,那么0-6位表示收到什么退出信號,第七位表示core dump標志,這個標志先不用管,所以我上面是直接按位與上0x7f。
為了驗證,我們也可以故意給一個錯誤的代碼,比如訪問空指針,這時讓程序運行,看看父進程能否分析出子進程的退出信號
我們可以看到,父進程確實等待到了子進程的退出信號11
宏:WIFEXITED
其實不一定非得像上面那樣進行位操作才可以得到退出碼,我們還可以通過宏來得到退出碼或退出信號,像下面這樣
wait if exited這個表示如果進程是正常終止,就是沒有收到退出信號,那就返回真,所以我們用wait exit status來獲取退出碼;如果為假,我們用wait terminate signal來獲取退出信號。因為只要有退出信號,退出碼就沒意義,退出碼有意義時沒有退出信號,所以它們兩個只要根據不同情況獲取一個即可。
非阻塞等待
我們上面說過waitpid的第三個參數如果是0,那么就是阻塞等待;如果是WNOHANG,就是非阻塞等待,什么是阻塞等待呢?其實就是父進程在等待子進程退出時如果什么都不做,就叫做阻塞等待;如果父進程使用WNOHANG后waitpid后就會立即返回,不管是否有子進程退出,如果等到了子進程退出,返回值就是子進程的pid,如果沒有等到就返回0。因為waitpid只會運行一回,有可能等不到子進程,所以一般把它放進一個循環中,并且父進程執行完waitpid后還可以執行自己的工作,我們一般可以這樣去實現
進程結束之后只留下進程PCB,所以父進程肯定是通過子進程的PCB獲取退出碼和退出信號的,我們在Linux源碼中也確實可以看到