微信公衆號:碼農充電站pro

個人主頁: https://codeshellme.github.io

學編程最有效的方法是動手敲代碼。

目錄

1,什麼是多進程

我們所寫的Python 代碼就是一個 程序 ,Python 程序用Python 解釋器來執行。程序是存儲在磁盤上的一個文件,Python 程序需要通過Python 解釋器將其讀入內存,然後進行 解釋執行

處於 執行運行 )狀態的程序叫做 進程 。進程是由 操作系統 分配資源並進行調度才能執行。操作系統會爲每個進程分配 進程ID (非負整數),作爲進程的 唯一標識

現代操作系統都提供了 多進程 同步執行的機制,也就是操作系統允許多個進程同時運行。操作系統負責進程的管理工作。比如我們在處理 word 文檔的同時還在聽音樂,這就需要有一個 word 程序和一個音樂軟件在同步運行。

多進程機制的硬件支持是由 CPU 提供的,CPU 有 單核多核 之分。

單核CPU 只有一個核心,在同一時刻只能有一個進程在執行,單核CPU 上的多個進程的執行,實際上是 併發 執行。其背後的原理是,CPU 的運行速度是相當快的,多進程執行實際上是每個進程 間隔運行 ,而間隔的時間非常短,人類是無法察覺到這種間隔的,這樣,人類感覺起來就像多個進程同時執行一樣。

多核CPU 有多個核心,每個核心都可以處理進程,這樣每個進程都可以運行在不同的CPU 上,這叫做 並行 執行,是真正的在同一時刻運行。

2, fork 函數

Python 語言也支持多進程編程,以此來支持更加複雜的,高性能的應用。

爲了支持多進程編程,操作系統提供了最原始的系統調用 fork() 函數,使得當前進程可以創建出一個子進程,這樣父進程和子進程就可以處理不同的事務。

Python 中的 fork() 函數被封裝在 os 模塊中,該函數原型很簡單,沒有任何參數,如下:

fork()

與一般函數不同的是,該函數的返回值比較特殊, fork 函數執行一次,返回兩次值:

  • 返回值爲0: 爲子進程範圍,子進程可通過 getppid() 函數得到父進程ID
  • 返回值爲子進程ID: 爲父進程範圍,這樣父進程可得到子進程ID

示例:

#! /usr/bin/env python3

import os

# 這裏是父進程
# 創建子進程
pid = os.fork()

if pid == 0:
    # 子進程範圍,編寫子進程需要處理的事務
    print('這裏是子進程,父進程ID 爲:%s,子進程ID 爲:%s' % (
        os.getppid(), os.getpid()))
else:
    # 父進程範圍,編寫父進程需要處理的事務
    print('這裏是父進程, 父進程ID 爲:%s, 子進程ID 爲:%s' % (
        os.getpid(), pid))

# 父進程和子進程都會執行到這裏
print('進程ID:%s' % os.getpid())

在上面代碼中,我們調用了 fork() 函數,返回值爲 pid

  • pid 爲0 時: 進入了子進程範圍,我們使用 getppid() 函數獲取了父進程ID,使用 getpid() 函數獲取了當前進程(子進程)ID
  • pid 不爲0 時: 進入了父進程範圍,此時 pid 就是子進程ID,我們使用 getpid() 函數獲取了當前進程(父進程)ID

代碼的最後一行 print('進程ID:%s' % os.getpid()) ,父進程和子進程都會執行到。

這段代碼的執行結果如下:

$ python3 Test.py 
這裏是父進程, 父進程ID 爲:1405, 子進程ID 爲:1406
進程ID:1405   # 最後一行代碼的輸出
這裏是子進程,父進程ID 爲:1405,子進程ID 爲:1406
進程ID:1406   # 最後一行代碼的輸出

從上面的執行結果,我們可以看到,父進程ID 爲 1405 ,子進程ID 爲 1406

最後一行代碼,子進程和父進程都能執行到的原因是,在執行了 fork() 函數後,之後的代碼就同時存在於兩個進程(父子進程)空間中。返回值 pid0 時,是子進程空間;返回值 pid 不爲 0 時,是父進程空間。

而最後一行代碼,即屬於 pid == 0 的範圍,又屬於 else 的範圍,所以父子進程都會執行該代碼。

3,孤兒進程與殭屍進程

我們已經知道,在 fork() 函數之後,就會有兩個進程,分別是 父進程子進程 。那這兩個進程是哪個先執行呢?是父進程先於子進程執行,還是子進程先於父進程執行?

答案是 不確定 。因爲父子進程哪個先執行不是程序能夠決定的,而是由操作系統的調度決定的,操作系統先調度到誰,誰就先執行。

另外,在父子進程退出時,由於退出的先後順序不一樣,也會造成 孤兒進程殭屍進程

  • 孤兒進程:父進程先於子進程退出,子進程會變成孤兒進程。孤兒進程會被 系統進程 接管,系統進程變成孤兒進程的父進程。在孤兒進程退出時,系統進程會進行處理。
  • 殭屍進程:如果子進程退出時,其父進程沒有處理子進程的 退出狀態 ,那麼這個進程退出後,其佔用的系統資源就不會 釋放 ,也就是,這個進程即不進行正常的工作,卻依然佔用系統資源,這樣的進程叫做 殭屍進程

下面我們編寫一段會產生殭屍進程的代碼:

#! /usr/bin/env python3

import os
import time

# 這裏是父進程
# 創建子進程
pid = os.fork()

if pid == 0:
    # 子進程範圍,編寫子進程需要處理的事務
    print('這裏是子進程,父進程ID 爲:%s,子進程ID 爲:%s' % (
        os.getppid(), os.getpid()))
else:
    # 父進程範圍,編寫父進程需要處理的事務
    print('這裏是父進程, 父進程ID 爲:%s, 子進程ID 爲:%s' % (
        os.getpid(), pid))
        
    print('父進程正在sleep 600S...')
    time.sleep(600)

# 父進程和子進程都會執行到這裏
print('進程ID:%s' % os.getpid())

上面的代碼中,我們在父進程中 sleep600 秒,這樣,子進程會先於父進程退出,而父進程沒有處理子進程的退出狀態,這必然造成子進程變爲殭屍進程。

我們使用 python3 執行該程序,如下:

$ python3 Test.py 
這裏是父進程, 父進程ID 爲:1524, 子進程ID 爲:1525
父進程正在sleep 600S...
這裏是子進程,父進程ID 爲:1524,子進程ID 爲:1525
進程ID:1525
`注意,這裏父進程在sleep,程序並沒有退出`

從上面的輸出,我們可以知道,父進程ID 爲 1524 ,子進程ID 爲 1525

然後,我們用 ps 命令,來查看當前的 python3 進程,如下:

$ ps -aux| grep python3
1      2    3    4     5      6     7      8      9      10          11
wp   1524  1.0  0.0  23992  6604  pts/2   `S`   09:13   0:00  python3 Test.py
wp   1525  0.0  0.0      0     0  pts/2   `Z`   09:13   0:00  [python3] <defunct>

(爲了方便查看,我在上面的輸出中添加了 列數 ,共11 列。)

其中第 2 列爲進程ID,第 8 列爲進程狀態。我們看到父進程(1524)處於 S 狀態(即休眠狀態),子進程(1525)處於 Z 狀態(即殭屍狀態)。

這說明,子進程先於父進程退出,而父進程又沒有處理子進程的退出狀態,所以使得子進程變爲了 殭屍進程

4,避免殭屍進程

孤兒進程不會造成什麼危害,而殭屍進程會造成系統資源浪費,所以殭屍進程是應該被避免的情況。

既然殭屍進程會導致資源浪費的情況,那麼操作系統爲什麼還要設計殭屍進程的存在呢?

殭屍進程存在的意義是保存了進程退出時的一些狀態,比如進程ID,終止狀態,資源使用情況等信息,這些信息都可以讓其父進程獲取到,來做適當的處理。

所以,在子進程退出後,只有經過父進程的處理才能避免 殭屍進程 的出現。

wait 函數

父進程可以通過 wait() 函數來獲取子進程的退出狀態。需要說明的是,調用 wait() 函數的進程將會阻塞,直到該進程的某個子進程退出。

wait 函數原型如下:

wait()
`
該函數返回一個元組(pid, status)
pid 爲退出進程的ID
status 爲退出進程的狀態
`

父進程調用 wait() 函數有兩種情況,這兩種情況都會正確的避免 殭屍進程 的出現:

  • 父進程在子進程退出 調用 wait()
  • 父進程在子進程退出 調用 wait()

我們分別對這兩種情況進行代碼演示,通過 sleep 函數來控制哪個進程先退出:

  1. 父進程在子進程退出 調用 wait()

代碼:

#! /usr/bin/env python3

import os
import time

# 這裏是父進程
# 創建子進程
pid = os.fork()

if pid == 0:
    # 子進程調用sleep,保證父進程先調用wait
    print('這裏是子進程, 父進程pid:%s, 子進程pid:%s sleep 5 秒' % (
        os.getppid(), os.getpid()
        ))
    time.sleep(5)

else:
    # 父進程調用wait,且出阻塞在這裏
    child_pid, child_status = os.wait()
    print('這裏是父進程, 父進程pid:%s, 子進程pid:%s, 子進程退出狀態:%s' % (
        os.getpid(), child_pid, child_status))

    print('父進程sleep 600 秒, 此時用 ps 命令查看進程狀態')
    time.sleep(600)

該代碼的執行結果如下:

$ python3 Test.py 
這裏是子進程, 父進程pid:1585, 子進程pid:1586 sleep 5 秒
這裏是父進程, 父進程pid:1585, 子進程pid:1586, 子進程退出狀態:0
父進程sleep 600 秒, 此時用 ps 命令查看進程狀態

當打印出 父進程sleep 600 秒, 此時用 ps 命令查看進程狀態 這句話時,證明 子進程 已經退出,我們用 ps 命令查看 python3 進程狀態,如下:

$ ps -aux| grep python3
1     2    3    4     5      6    7    8    9     10          11
wp  1585  0.0  0.0  23992  6604 pts/2  S  10:10  0:00  python3 Test.py

可見此時只有父進程存活,子進程已經成功退出,沒有處於殭屍進程狀態。

  1. 父進程在子進程退出 調用 wait()

代碼:

#! /usr/bin/env python3

import os
import time

# 這裏是父進程
# 創建子進程
pid = os.fork()

if pid == 0:
    # 子進程範圍
    print('這裏是子進程, 父進程pid:%s, 子進程pid:%s' % (
        os.getppid(), os.getpid()
        ))

else:
    # 父進程先 sleep,保證子進程先退出,然後再調用 wait
    time.sleep(5)

    child_pid, child_status = os.wait()
    print('這裏是父進程, 父進程pid:%s, 子進程pid:%s, 子進程退出狀態:%s' % (
        os.getpid(), child_pid, child_status))

    print('父進程sleep 600 秒, 此時用 ps 命令查看進程狀態')
    time.sleep(600)

該代碼執行結果如下:

$ python3 Test.py 
這裏是子進程, 父進程pid:1591, 子進程pid:1592
這裏是父進程, 父進程pid:1591, 子進程pid:1592, 子進程退出狀態:0
父進程sleep 600 秒, 此時用 ps 命令查看進程狀態

當打印出 父進程sleep 600 秒, 此時用 ps 命令查看進程狀態 這句話時,我們用 ps 命令查看 python3 進程狀態,如下:

執行結果:

$ ps -aux| grep python3
1     2    3    4     5      6    7    8    9     10          11
wp  1591  0.2  0.0  23992  6620 pts/2  S  10:20  0:00  python3 Test.py

可見此時只有父進程存活,子進程已經成功退出,沒有處於殭屍進程狀態。

5,使用信號處理殭屍進程

因爲 wait() 函數會導致調用進程阻塞,那就使得調用進程無法處理別的事情。這其實不是很合理,因爲白白浪費了一個進程。

這種情況我們可以使用 信號 來處理。

信號 是一種系統中斷,當進程遇到系統中斷時,就會打斷進程正在執行的正常流程,轉而去處理 中斷函數 。進程處理完中斷函數後,又會回到進程原來的處理流程。

中斷函數 是用戶向系統註冊的一個函數,用於在遇到某個信號時,要做哪些處理。

因爲子進程在退出時會向父進程發送 SIGCHLD 信號,所以父進程可以通過捕獲該信號來處理子進程。

signal 模塊

在Linux 系統中,我們可以通過 kill -l 命令來查看系統中的信號,共 64 個信號:

$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX

在Python 中通過 signal 模塊來處理信號,我們通過 dir(signal) 來查看 signal 模塊都有哪些內容:

>>> dir(signal)
['Handlers', 'ITIMER_PROF', 'ITIMER_REAL', 
'ITIMER_VIRTUAL', 'ItimerError', 'NSIG', 
'SIGABRT', 'SIGALRM', 'SIGBUS', 'SIGCHLD', 
'SIGCLD', 'SIGCONT', 'SIGFPE', 'SIGHUP', 
'SIGILL', 'SIGINT', 'SIGIO', 'SIGIOT', 
'SIGKILL', 'SIGPIPE', 'SIGPOLL', 'SIGPROF', 
'SIGPWR', 'SIGQUIT', 'SIGRTMAX', 'SIGRTMIN', 
'SIGSEGV', 'SIGSTOP', 'SIGSYS', 'SIGTERM', 
'SIGTRAP', 'SIGTSTP', 'SIGTTIN', 'SIGTTOU', 
'SIGURG', 'SIGUSR1', 'SIGUSR2', 'SIGVTALRM', 
'SIGWINCH', 'SIGXCPU', 'SIGXFSZ', 'SIG_BLOCK', 
'SIG_DFL', 'SIG_IGN', 'SIG_SETMASK', 
'SIG_UNBLOCK', 'Sigmasks', 'Signals', 
'_IntEnum', '__builtins__', '__cached__', 
'__doc__', '__file__', '__loader__', 
'__name__', '__package__', '__spec__', 
'_enum_to_int', '_int_to_enum', '_signal', 
'alarm', 'default_int_handler', 'getitimer', 
'getsignal', 'pause', 'pthread_kill', 
'pthread_sigmask', 'set_wakeup_fd', 'setitimer', 
'siginterrupt', 'signal', 'sigpending', 
'sigtimedwait', 'sigwait', 'sigwaitinfo', 
'struct_siginfo']

可以看到, signal 模塊中包含了一些信號相關函數,和絕大部分信號。

signal 函數

要想處理信號,則需要使用 signal 模塊中的 signal 函數向系統註冊,捕獲哪個信號,以及處理該信號的函數。

signal 函數原型如下:

signal(signalnum, handler)
  • 該函數接收兩個參數,分別是 signalnumhandler
  • signalnum 是要捕獲的信號
  • handler 是信號處理函數

handler 參數有三種取值:

  • SIG_DFL :表示系統設置的默認值
  • SIG_IGN :表示忽略該信號
  • 一個函數類型的參數:該函數接收兩個參數分別是 信號編號當前的棧幀

接下來,我們編寫代碼,用信號來處理殭屍進程。

示例代碼:

#! /usr/bin/env python3

import os
import time
import signal

# 這裏是父進程

# 信號處理函數
# 該函數須有兩個參數
def sig_handelr(signum, frame):
    # print(frame)

    # 父進程中調用 wait 來處理子進程
    child_pid, child_status = os.wait()
    print('這裏是父進程, 接收到了信號:%s, 此時用 ps 命令查看進程狀態。父進程pid:%s, 子進程pid:%s, 子進程退出狀態:%s' % (
        signum, os.getpid(), child_pid, child_status))

# 父進程註冊信號處理函數
signal.signal(signal.SIGCHLD, sig_handelr)

# 創建子進程
pid = os.fork()

if pid == 0:
    # 子進程範圍

    print('這裏是子進程, 父進程pid:%s, 子進程pid:%s, 子進程 sleep 10 秒' % (
        os.getppid(), os.getpid()
        ))

    # 先讓子進程sleep 10 秒,然後退出
    time.sleep(10)

else:
    print('這裏是父進程, 父進程sleep 600 秒, 保證子進程先退出')
    time.sleep(600)

注意:信號處理函數 signal 的調用,一定要在 fork 函數之前。

執行結果如下:

$ python3 Test.py 
這裏是父進程, 父進程sleep 600 秒, 保證子進程先退出
這裏是子進程, 父進程pid:1651, 子進程pid:1652, 子進程 sleep 10 秒
這裏是父進程, 接收到了信號:17, 此時用 ps 命令查看進程狀態。父進程pid:1651, 子進程pid:1652, 子進程退出狀態:0
`這裏程序並沒有退出,因爲父進程在sleep 600 秒`

等待子進程 sleep 10 秒,退出之後,我們用 ps 命令查看進程狀態:

ps -aux| grep python3
1    2     3    4     5      6    7     8     9      10          11
wp  1651  0.0  0.0  23992  6708 pts/2   S   21:38   0:00  python3 Test.py

通過 ps 命令可以看出,在子進程退出之後,並沒有變成殭屍進程,說明我們的處理沒有問題。

6,忽略 SIGCHLD 信號

更簡單處理辦法是直接將 SIGCHLD 信號 忽略 掉,而不需要爲信號註冊 處理函數忽略信號 也是處理信號的一種,同樣不會使子進程變成殭屍進程。

代碼如下:

#! /usr/bin/env python3

import os
import time
import signal

# 這裏是父進程
# 父進程註冊信號,處理方法是忽略
signal.signal(signal.SIGCHLD, signal.SIG_IGN)

# 創建子進程
pid = os.fork()

if pid == 0:
    # 子進程範圍
    print('這裏是子進程, 父進程pid:%s, 子進程pid:%s, 子進程 sleep 10 秒' % (
        os.getppid(), os.getpid()
        ))

    # 先讓子進程sleep 10 秒,然後退出
    time.sleep(10)

else:
    print('這裏是父進程, 父進程sleep 600 秒, 保證子進程先退出')
    time.sleep(600)

我們將 signal 函數的第二個參數設置爲 signal.SIG_IGN ,意思是 忽略 掉信號。

執行結果如下:

$ python3 Test.py 
這裏是父進程, 父進程sleep 600 秒, 保證子進程先退出
這裏是子進程, 父進程pid:1659, 子進程pid:1660, 子進程 sleep 10 秒
`這裏程序並沒有退出,因爲父進程在sleep 600 秒`

我們再用 ps 命令輸出如下:

$ ps -aux| grep python3
1     2    3    4     5      6     7     8     9     10         11
wp  1659  0.1  0.0  23992  6688  pts/2   S   21:57  0:00  python3 Test.py

可以看到,子進程依然沒有變成殭屍進程。

(完。)

相關文章