進(jìn)程間通信IPC的主要方式有哪些?大佬教你用python實現(xiàn)

計算機通信方式主要有如下幾種,本文會詳細(xì)介紹以下幾種通信方式以及對應(yīng)使用Python的實現(xiàn)方法,能夠在學(xué)習(xí)理論的同時,也結(jié)合到Code層面,學(xué)以致用。

?

前言

每個進(jìn)程的用戶地址空間都是獨立的,一般而言是不能互相訪問的,但內(nèi)核空間是每個進(jìn)程都共享的,所以進(jìn)程之間要通信必須通過內(nèi)核。

如果大家在學(xué)習(xí)中遇到困難,想找一個python學(xué)習(xí)交流環(huán)境,可以加入我們的python圈,裙號930900780,可領(lǐng)取python學(xué)習(xí)資料,會節(jié)約很多時間,減少很多遇到的難題。

?

1.管道

如果你學(xué)過 Linux 命令,那你肯定很熟悉「|」這個豎線。

$ ps auxf | grep mysql

上面命令行里的「|」豎線就是一個管道,它的功能是將前一個命令(ps auxf)的輸出,作為后一個命令(grep mysql)的輸入,從這功能描述,可以看出管道傳輸數(shù)據(jù)是單向的,如果想相互通信,我們需要創(chuàng)建兩個管道才行。

同時,我們得知上面這種管道是沒有名字,所以「|」表示的管道稱為匿名管道,用完了就銷毀。

管道還有另外一個類型是命名管道,也被叫做 FIFO,因為數(shù)據(jù)是先進(jìn)先出的傳輸方式。

在使用命名管道前,先需要通過 mkfifo 命令來創(chuàng)建,并且指定管道名字:

$ mkfifo myPipe

myPipe 就是這個管道的名稱,基于 Linux 一切皆文件的理念,所以管道也是以文件的方式存在,我們可以用 ls 看一下,這個文件的類型是 p,也就是 pipe(管道) 的意思:

$ ls -l

prw-r--r--. 1 root? ? root? ? ? ? 0 Jul 17 02:45 myPipe

復(fù)制代碼

接下來,我們往 myPipe 這個管道寫入數(shù)據(jù):

$ echo "hello" > myPipe? // 將數(shù)據(jù)寫進(jìn)管道

? ? ? ? ? ? ? ? ? ? ? ? // 停住了 ...

復(fù)制代碼

你操作了后,你會發(fā)現(xiàn)命令執(zhí)行后就停在這了,這是因為管道里的內(nèi)容沒有被讀取,只有當(dāng)管道里的數(shù)據(jù)被讀完后,命令才可以正常退出。

于是,我們執(zhí)行另外一個命令來讀取這個管道里的數(shù)據(jù):

$ cat < myPipe? // 讀取管道里的數(shù)據(jù)

hello

復(fù)制代碼

可以看到,管道里的內(nèi)容被讀取出來了,并打印在了終端上,另外一方面,echo 那個命令也正常退出了。

我們可以看出,管道這種通信方式效率低,不適合進(jìn)程間頻繁地交換數(shù)據(jù)。當(dāng)然,它的好處,自然就是簡單,同時也我們很容易得知管道里的數(shù)據(jù)已經(jīng)被另一個進(jìn)程讀取了。

那管道如何創(chuàng)建呢,背后原理是什么?

匿名管道的創(chuàng)建,需要通過下面這個系統(tǒng)調(diào)用:

int pipe(int fd[2])

復(fù)制代碼

這里表示創(chuàng)建一個匿名管道,并返回了兩個描述符,一個是管道的讀取端描述符 fd[0],另一個是管道的寫入端描述符 fd[1]。注意,這個匿名管道是特殊的文件,只存在于內(nèi)存,不存于文件系統(tǒng)中。

?

其實,所謂的管道,就是內(nèi)核里面的一串緩存。從管道的一段寫入的數(shù)據(jù),實際上是緩存在內(nèi)核中的,另一端讀取,也就是從內(nèi)核中讀取這段數(shù)據(jù)。另外,管道傳輸?shù)臄?shù)據(jù)是無格式的流且大小受限。

看到這,你可能會有疑問了,這兩個描述符都是在一個進(jìn)程里面,并沒有起到進(jìn)程間通信的作用,怎么樣才能使得管道是跨過兩個進(jìn)程的呢?

我們可以使用 fork 創(chuàng)建子進(jìn)程,創(chuàng)建的子進(jìn)程會復(fù)制父進(jìn)程的文件描述符,這樣就做到了兩個進(jìn)程各有兩個「 fd[0] 與 fd[1]」,兩個進(jìn)程就可以通過各自的 fd 寫入和讀取同一個管道文件實現(xiàn)跨進(jìn)程通信了。

?

管道只能一端寫入,另一端讀出,所以上面這種模式容易造成混亂,因為父進(jìn)程和子進(jìn)程都可以同時寫入,也都可以讀出。那么,為了避免這種情況,通常的做法是:

父進(jìn)程關(guān)閉讀取的 fd[0],只保留寫入的 fd[1];

子進(jìn)程關(guān)閉寫入的 fd[1],只保留讀取的 fd[0];

?

所以說如果需要雙向通信,則應(yīng)該創(chuàng)建兩個管道。

到這里,我們僅僅解析了使用管道進(jìn)行父進(jìn)程與子進(jìn)程之間的通信,但是在我們 shell 里面并不是這樣的。

在 shell 里面執(zhí)行 A | B命令的時候,A 進(jìn)程和 B 進(jìn)程都是 shell 創(chuàng)建出來的子進(jìn)程,A 和 B 之間不存在父子關(guān)系,它倆的父進(jìn)程都是 shell。

?

所以說,在 shell 里通過「|」匿名管道將多個命令連接在一起,實際上也就是創(chuàng)建了多個子進(jìn)程,那么在我們編寫 shell 腳本時,能使用一個管道搞定的事情,就不要多用一個管道,這樣可以減少創(chuàng)建子進(jìn)程的系統(tǒng)開銷。

我們可以得知,對于匿名管道,它的通信范圍是存在父子關(guān)系的進(jìn)程。因為管道沒有實體,也就是沒有管道文件,只能通過 fork 來復(fù)制父進(jìn)程 fd 文件描述符,來達(dá)到通信的目的。

另外,對于命名管道,它可以在不相關(guān)的進(jìn)程間也能相互通信。因為命令管道,提前創(chuàng)建了一個類型為管道的設(shè)備文件,在進(jìn)程里只要使用這個設(shè)備文件,就可以相互通信。

不管是匿名管道還是命名管道,進(jìn)程寫入的數(shù)據(jù)都是緩存在內(nèi)核中,另一個進(jìn)程讀取數(shù)據(jù)時候自然也是從內(nèi)核中獲取,同時通信數(shù)據(jù)都遵循先進(jìn)先出原則,不支持 lseek 之類的文件定位操作。

1.1 Python實現(xiàn)

1.1.1 使用os模塊中的fork創(chuàng)建新的進(jìn)程

python運行時產(chǎn)生的進(jìn)程

在我們運行python程序的時候,系統(tǒng)會生成一個新的python進(jìn)程。使用fork方法來創(chuàng)建一個新進(jìn)程 使用fork創(chuàng)建一個新的進(jìn)程后,新進(jìn)程是原進(jìn)程的子進(jìn)程,原進(jìn)程為父進(jìn)程。如果發(fā)生錯誤,則會拋出OSError異常:

# -*- coding: utf-8 -*-

import time

import os

try:

? ? pid = os.fork()

except OSError, e:

? ? pass

time.sleep(20)

復(fù)制代碼

運行代碼,查看進(jìn)程,在終端輸出如下:

可以看出第二條python進(jìn)程就是第一條的子進(jìn)程。

fork出進(jìn)程后的程序流程

使用fork創(chuàng)建子進(jìn)程后,子進(jìn)程會復(fù)制父進(jìn)程的數(shù)據(jù)信息,而后程序就分兩個進(jìn)程繼續(xù)運行后面的程序,這也是fork(分叉)名字的含義了。在子進(jìn)程內(nèi),這個方法會返回0;在父進(jìn)程內(nèi),這個方法會返回子進(jìn)程的編號PID。 可以使用PID來區(qū)分兩個進(jìn)程:

# -*- coding: utf-8 -*-

import time

import os

#創(chuàng)建子進(jìn)程前聲明的變量

number = 7

try:

? ? pid = os.fork()

? ? if pid == 0:

? ? ? ? print("this is child process")

? ? ? ? number = number - 1

? ? ? ? time.sleep(5)

? ? ? ? print(number)

? ? else:

? ? ? ? print("this is parent process")

except OSError as e:

? ? pass

復(fù)制代碼

上面代碼中,在子進(jìn)程創(chuàng)建前,聲明了一個變量number,然后在子進(jìn)程中自減1,最后打印出number的值,顯然父進(jìn)程打印出來的值為7,子進(jìn)程打印出來的值為6。為了明顯區(qū)分父進(jìn)程和子進(jìn)程,讓子進(jìn)程睡3秒,這樣效果就比較明顯了。

栗子2: 先看代碼:

#!/usr/bin/python

import time

import os

def child(wpipe):

? ? print('hello from child', os.getpid())

? ? while True:

? ? ? ? msg = 'how are you\n'.encode()

? ? ? ? os.write(wpipe, msg)

? ? ? ? time.sleep(1)

def parent():

? ? rpipe, wpipe = os.pipe() # os.pipe()返回2個文件描述符(r, w)

? ? pid = os.fork()

? ? if pid == 0:

? ? ? ? os.close(rpipe)

? ? ? ? child(wpipe)

? ? ? ? assert False, 'fork child process error!'

? ? else:

? ? ? ? os.close(wpipe)

? ? ? ? print('hello from parent', os.getpid(), pid)

? ? ? ? fobj = os.fdopen(rpipe, 'r')

? ? ? ? while True:

? ? ? ? ? ? recv = fobj.readline()[:-1]

? ? ? ? ? ? print recv

parent()

復(fù)制代碼

輸出:

('hello from parent', 5108, 5109)

('hello from child', 5109)

how are you

how are you

how are you

復(fù)制代碼

管道是一個單向通道,有點類似共享內(nèi)存緩存.管道有兩端,包括輸入端和輸出端.對于一個進(jìn)程的而言,它只能看到管道一端,即要么是輸入端要么是輸出端.

從文件對象中讀取字符串.這時需要用到os.fdopen()把底層的文件描述符(管道)包裝成文件對象,然后再用文件對象中的readline()方法讀取.這里請注意文件對象的readline()方法總是讀取有換行符’\n’的一行,而且連換行符也讀取出來.還有一點要改進(jìn)的地方是,把父進(jìn)程和子進(jìn)程的管道中不用的一端關(guān)閉掉.

如果要與子進(jìn)程進(jìn)行雙向通信,只有一個pipe管道是不夠的,需要2個pipe管道才行.以下示例在父進(jìn)程新建了2個管道,然后再fork子進(jìn)程.os.dup2()實現(xiàn)輸出和輸入的重定向.spawn功能類似于subprocess.Popen(),既能發(fā)送消息給子進(jìn)程,由能從子子進(jìn)程獲取返回數(shù)據(jù).

#!/usr/bin/python

#coding=utf-8

import os, sys

def spawn(prog, *args):

? ? stdinFd = sys.stdin.fileno()

? ? stdoutFd = sys.stdout.fileno()

? ? parentStdin, childStdout = os.pipe()

? ? childStdin, parentStdout= os.pipe()

? ? pid = os.fork()

? ? if pid:

? ? ? ? os.close(childStdin)

? ? ? ? os.close(childStdout)

? ? ? ? os.dup2(parentStdin, stdinFd)#輸入流綁定到管道,將輸入重定向到管道一端parentStdin

? ? ? ? os.dup2(parentStdout, stdoutFd)#輸出流綁定到管道,發(fā)送到子進(jìn)程childStdin

? ? else:

? ? ? ? os.close(parentStdin)

? ? ? ? os.close(parentStdout)

? ? ? ? os.dup2(childStdin, stdinFd)#輸入流綁定到管道

? ? ? ? os.dup2(childStdout, stdoutFd)

? ? ? ? args = (prog, ) + args

? ? ? ? os.execvp(prog, args)

? ? ? ? assert False, 'execvp failed!'

if __name__ == '__main__':

? ? mypid = os.getpid()

? ? spawn('python', 'pipetest.py', 'spam')

? ? print 'Hello 1 from parent', mypid #打印到輸出流parentStdout, 經(jīng)管道發(fā)送到子進(jìn)程childStdin

? ? sys.stdout.flush()

? ? reply = raw_input()

? ? sys.stderr.write('Parent got: "%s"\n' % reply)#stderr沒有綁定到管道上

? ? print 'Hello 2 from parent', mypid

? ? sys.stdout.flush()

? ? reply = sys.stdin.readline()#另外一種方式獲得子進(jìn)程返回信息

? ? sys.stderr.write('Parent got: "%s"\n' % reply[:-1])

復(fù)制代碼

當(dāng)然,除了os的實現(xiàn)方式,也可以使用multiprocessing來實現(xiàn),具體看代碼:

from multiprocessing import Process, Pipe

"""

multiprocessing.Pipe([duplex])

返回2個連接對象(conn1, conn2),代表管道的兩端,默認(rèn)是雙向通信.

如果duplex=False,conn1只能用來接收消息,conn2只能用來發(fā)送消息.

不同于os.open之處在于os.pipe()返回2個文件描述符(r, w),表示可讀的和可寫的

"""

def send(pipe):

? ? pipe.send(['spam'] + [42, 'egg'])

? ? pipe.close()

def talk(pipe):

? ? pipe.send(dict(name='Bob', spam=42))

? ? reply = pipe.recv()

? ? print('talker got:', reply)

if __name__ == '__main__':

? ? (con1, con2) = Pipe()

? ? sender = Process(target=send, name='send', args=(con1,))

? ? sender.start()

? ? print("con2 got: %s" % con2.recv())? # 從send收到消息

? ? con2.close()

? ? (parentEnd, childEnd) = Pipe()

? ? child = Process(target=talk, name='talk', args=(childEnd,))

? ? child.start()

? ? print('parent got:', parentEnd.recv())

? ? parentEnd.send({x * 2 for x in 'spam'})

? ? child.join()

? ? print('parent exit')

復(fù)制代碼

2.消息隊列

前面說到管道的通信方式是效率低的,因此管道不適合進(jìn)程間頻繁地交換數(shù)據(jù)。

對于這個問題,消息隊列的通信模式就可以解決。比如,A 進(jìn)程要給 B 進(jìn)程發(fā)送消息,A 進(jìn)程把數(shù)據(jù)放在對應(yīng)的消息隊列后就可以正常返回了,B 進(jìn)程需要的時候再去讀取數(shù)據(jù)就可以了。同理,B 進(jìn)程要給 A 進(jìn)程發(fā)送消息也是如此。

再來,消息隊列是保存在內(nèi)核中的消息鏈表,在發(fā)送數(shù)據(jù)時,會分成一個一個獨立的數(shù)據(jù)單元,也就是消息體(數(shù)據(jù)塊),消息體是用戶自定義的數(shù)據(jù)類型,消息的發(fā)送方和接收方要約定好消息體的數(shù)據(jù)類型,所以每個消息體都是固定大小的存儲塊,不像管道是無格式的字節(jié)流數(shù)據(jù)。如果進(jìn)程從消息隊列中讀取了消息體,內(nèi)核就會把這個消息體刪除。

消息隊列生命周期隨內(nèi)核,如果沒有釋放消息隊列或者沒有關(guān)閉操作系統(tǒng),消息隊列會一直存在,而前面提到的匿名管道的生命周期,是隨進(jìn)程的創(chuàng)建而建立,隨進(jìn)程的結(jié)束而銷毀。

消息這種模型,兩個進(jìn)程之間的通信就像平時發(fā)郵件一樣,你來一封,我回一封,可以頻繁溝通了。

但郵件的通信方式存在不足的地方有兩點,一是通信不及時,二是附件也有大小限制,這同樣也是消息隊列通信不足的點。

消息隊列不適合比較大數(shù)據(jù)的傳輸,因為在內(nèi)核中每個消息體都有一個最大長度的限制,同時所有隊列所包含的全部消息體的總長度也是有上限。在 Linux 內(nèi)核中,會有兩個宏定義 MSGMAX 和 MSGMNB,它們以字節(jié)為單位,分別定義了一條消息的最大長度和一個隊列的最大長度。

消息隊列通信過程中,存在用戶態(tài)與內(nèi)核態(tài)之間的數(shù)據(jù)拷貝開銷,因為進(jìn)程寫入數(shù)據(jù)到內(nèi)核中的消息隊列時,會發(fā)生從用戶態(tài)拷貝數(shù)據(jù)到內(nèi)核態(tài)的過程,同理另一進(jìn)程讀取內(nèi)核中的消息數(shù)據(jù)時,會發(fā)生從內(nèi)核態(tài)拷貝數(shù)據(jù)到用戶態(tài)的過程。

3.共享內(nèi)存

消息隊列的讀取和寫入的過程,都會有發(fā)生用戶態(tài)與內(nèi)核態(tài)之間的消息拷貝過程。那共享內(nèi)存的方式,就很好的解決了這一問題。

現(xiàn)代操作系統(tǒng),對于內(nèi)存管理,采用的是虛擬內(nèi)存技術(shù),也就是每個進(jìn)程都有自己獨立的虛擬內(nèi)存空間,不同進(jìn)程的虛擬內(nèi)存映射到不同的物理內(nèi)存中。所以,即使進(jìn)程 A 和 進(jìn)程 B 的虛擬地址是一樣的,其實訪問的是不同的物理內(nèi)存地址,對于數(shù)據(jù)的增刪查改互不影響。

共享內(nèi)存的機制,就是拿出一塊虛擬地址空間來,映射到相同的物理內(nèi)存中。這樣這個進(jìn)程寫入的東西,另外一個進(jìn)程馬上就能看到了,都不需要拷貝來拷貝去,傳來傳去,大大提高了進(jìn)程間通信的速度。

?

4.信號量

用了共享內(nèi)存通信方式,帶來新的問題,那就是如果多個進(jìn)程同時修改同一個共享內(nèi)存,很有可能就沖突了。例如兩個進(jìn)程都同時寫一個地址,那先寫的那個進(jìn)程會發(fā)現(xiàn)內(nèi)容被別人覆蓋了。

為了防止多進(jìn)程競爭共享資源,而造成的數(shù)據(jù)錯亂,所以需要保護(hù)機制,使得共享的資源,在任意時刻只能被一個進(jìn)程訪問。正好,信號量就實現(xiàn)了這一保護(hù)機制。

信號量其實是一個整型的計數(shù)器,主要用于實現(xiàn)進(jìn)程間的互斥與同步,而不是用于緩存進(jìn)程間通信的數(shù)據(jù)。

信號量表示資源的數(shù)量,控制信號量的方式有兩種原子操作:

一個是P 操作,這個操作會把信號量減去 -1,相減后如果信號量 < 0,則表明資源已被占用,進(jìn)程需阻塞等待;相減后如果信號量 >= 0,則表明還有資源可使用,進(jìn)程可正常繼續(xù)執(zhí)行。

另一個是?V 操作,這個操作會把信號量加上 1,相加后如果信號量 <= 0,則表明當(dāng)前有阻塞中的進(jìn)程,于是會將該進(jìn)程喚醒運行;相加后如果信號量 > 0,則表明當(dāng)前沒有阻塞中的進(jìn)程;

P 操作是用在進(jìn)入共享資源之前,V 操作是用在離開共享資源之后,這兩個操作是必須成對出現(xiàn)的。

接下來,舉個例子,如果要使得兩個進(jìn)程互斥訪問共享內(nèi)存,我們可以初始化信號量為 1。

?

具體的過程如下:

進(jìn)程 A 在訪問共享內(nèi)存前,先執(zhí)行了 P 操作,由于信號量的初始值為 1,故在進(jìn)程 A 執(zhí)行 P 操作后信號量變?yōu)?0,表示共享資源可用,于是進(jìn)程 A 就可以訪問共享內(nèi)存。

若此時,進(jìn)程 B 也想訪問共享內(nèi)存,執(zhí)行了 P 操作,結(jié)果信號量變?yōu)榱?-1,這就意味著臨界資源已被占用,因此進(jìn)程 B 被阻塞。

直到進(jìn)程 A 訪問完共享內(nèi)存,才會執(zhí)行 V 操作,使得信號量恢復(fù)為 0,接著就會喚醒阻塞中的線程 B,使得進(jìn)程 B 可以訪問共享內(nèi)存,最后完成共享內(nèi)存的訪問后,執(zhí)行 V 操作,使信號量恢復(fù)到初始值 1。

可以發(fā)現(xiàn),信號初始化為 1,就代表著是互斥信號量,它可以保證共享內(nèi)存在任何時刻只有一個進(jìn)程在訪問,這就很好的保護(hù)了共享內(nèi)存。

另外,在多進(jìn)程里,每個進(jìn)程并不一定是順序執(zhí)行的,它們基本是以各自獨立的、不可預(yù)知的速度向前推進(jìn),但有時候我們又希望多個進(jìn)程能密切合作,以實現(xiàn)一個共同的任務(wù)。

例如,進(jìn)程 A 是負(fù)責(zé)生產(chǎn)數(shù)據(jù),而進(jìn)程 B 是負(fù)責(zé)讀取數(shù)據(jù),這兩個進(jìn)程是相互合作、相互依賴的,進(jìn)程 A 必須先生產(chǎn)了數(shù)據(jù),進(jìn)程 B 才能讀取到數(shù)據(jù),所以執(zhí)行是有前后順序的。

那么這時候,就可以用信號量來實現(xiàn)多進(jìn)程同步的方式,我們可以初始化信號量為 0。

?

具體過程:

如果進(jìn)程 B 比進(jìn)程 A 先執(zhí)行了,那么執(zhí)行到 P 操作時,由于信號量初始值為 0,故信號量會變?yōu)?-1,表示進(jìn)程 A 還沒生產(chǎn)數(shù)據(jù),于是進(jìn)程 B 就阻塞等待;

接著,當(dāng)進(jìn)程 A 生產(chǎn)完數(shù)據(jù)后,執(zhí)行了 V 操作,就會使得信號量變?yōu)?0,于是就會喚醒阻塞在 P 操作的進(jìn)程 B;

最后,進(jìn)程 B 被喚醒后,意味著進(jìn)程 A 已經(jīng)生產(chǎn)了數(shù)據(jù),于是進(jìn)程 B 就可以正常讀取數(shù)據(jù)了。

可以發(fā)現(xiàn),信號初始化為 0,就代表著是同步信號量,它可以保證進(jìn)程 A 應(yīng)在進(jìn)程 B 之前執(zhí)行。

5.信號

上面說的進(jìn)程間通信,都是常規(guī)狀態(tài)下的工作模式。對于異常情況下的工作模式,就需要用「信號」的方式來通知進(jìn)程

信號跟信號量雖然名字相似度 66.66%,但兩者用途完全不一樣,就好像 Java 和 JavaScript 的區(qū)別。

在 Linux 操作系統(tǒng)中, 為了響應(yīng)各種各樣的事件,提供了幾十種信號,分別代表不同的意義。我們可以通過 kill -l 命令,查看所有的信號:

$ 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

復(fù)制代碼

運行在 shell 終端的進(jìn)程,我們可以通過鍵盤輸入某些組合鍵的時候,給進(jìn)程發(fā)送信號。例如

Ctrl+C 產(chǎn)生 SIGINT 信號,表示終止該進(jìn)程;

Ctrl+Z 產(chǎn)生 SIGTSTP 信號,表示停止該進(jìn)程,但還未結(jié)束;

如果進(jìn)程在后臺運行,可以通過 kill 命令的方式給進(jìn)程發(fā)送信號,但前提需要知道運行中的進(jìn)程 PID 號,例如:

kill -9 1050 ,表示給 PID 為 1050 的進(jìn)程發(fā)送 SIGKILL 信號,用來立即結(jié)束該進(jìn)程;

所以,信號事件的來源主要有硬件來源(如鍵盤 Cltr+C )和軟件來源(如 kill 命令)。

信號是進(jìn)程間通信機制中唯一的異步通信機制,因為可以在任何時候發(fā)送信號給某一進(jìn)程,一旦有信號產(chǎn)生,我們就有下面這幾種,用戶進(jìn)程對信號的處理方式。

1.執(zhí)行默認(rèn)操作。Linux 對每種信號都規(guī)定了默認(rèn)操作,例如,上面列表中的 SIGTERM 信號,就是終止進(jìn)程的意思。Core 的意思是 Core Dump,也即終止進(jìn)程后,通過 Core Dump 將當(dāng)前進(jìn)程的運行狀態(tài)保存在文件里面,方便程序員事后進(jìn)行分析問題在哪里。

2.捕捉信號。我們可以為信號定義一個信號處理函數(shù)。當(dāng)信號發(fā)生時,我們就執(zhí)行相應(yīng)的信號處理函數(shù)。

3.忽略信號。當(dāng)我們不希望處理某些信號的時候,就可以忽略該信號,不做任何處理。有兩個信號是應(yīng)用進(jìn)程無法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,它們用于在任何時候中斷或結(jié)束某一進(jìn)程。

6.Socket

前面提到的管道、消息隊列、共享內(nèi)存、信號量和信號都是在同一臺主機上進(jìn)行進(jìn)程間通信,那要想跨網(wǎng)絡(luò)與不同主機上的進(jìn)程之間通信,就需要 Socket 通信了

實際上,Socket 通信不僅可以跨網(wǎng)絡(luò)與不同主機的進(jìn)程間通信,還可以在同主機上進(jìn)程間通信。

我們來看看創(chuàng)建 socket 的系統(tǒng)調(diào)用:

int socket(int domain, int type, int protocal)

三個參數(shù)分別代表:

domain 參數(shù)用來指定協(xié)議族,比如 AF_INET 用于 IPV4、AF_INET6 用于 IPV6、AF_LOCAL/AF_UNIX 用于本機;

type 參數(shù)用來指定通信特性,比如 SOCK_STREAM 表示的是字節(jié)流,對應(yīng) TCP、SOCK_DGRAM 表示的是數(shù)據(jù)報,對應(yīng) UDP、SOCK_RAW 表示的是原始套接字;

protocal 參數(shù)原本是用來指定通信協(xié)議的,但現(xiàn)在基本廢棄。因為協(xié)議已經(jīng)通過前面兩個參數(shù)指定完成,protocol 目前一般寫成 0 即可;

根據(jù)創(chuàng)建 socket 類型的不同,通信的方式也就不同:

實現(xiàn) TCP 字節(jié)流通信:socket 類型是 AF_INET 和 SOCK_STREAM;

實現(xiàn) UDP 數(shù)據(jù)報通信:socket 類型是 AF_INET 和 SOCK_DGRAM;

實現(xiàn)本地進(jìn)程間通信:「本地字節(jié)流 socket 」類型是 AF_LOCAL 和 SOCK_STREAM,「本地數(shù)據(jù)報 socket 」類型是 AF_LOCAL 和 SOCK_DGRAM。另外,AF_UNIX 和 AF_LOCAL 是等價的,所以 AF_UNIX 也屬于本地 socket;

接下來,簡單說一下這三種通信的編程模式。

針對 TCP 協(xié)議通信的 socket 編程模型

?

服務(wù)端和客戶端初始化 socket,得到文件描述符;

服務(wù)端調(diào)用 bind,將綁定在 IP 地址和端口;

服務(wù)端調(diào)用 listen,進(jìn)行監(jiān)聽;

服務(wù)端調(diào)用 accept,等待客戶端連接;

客戶端調(diào)用 connect,向服務(wù)器端的地址和端口發(fā)起連接請求;

服務(wù)端 accept 返回用于傳輸?shù)?socket 的文件描述符;

客戶端調(diào)用 write 寫入數(shù)據(jù);服務(wù)端調(diào)用 read 讀取數(shù)據(jù);

客戶端斷開連接時,會調(diào)用 close,那么服務(wù)端 read 讀取數(shù)據(jù)的時候,就會讀取到了 EOF,待處理完數(shù)據(jù)后,服務(wù)端調(diào)用 close,表示連接關(guān)閉。

這里需要注意的是,服務(wù)端調(diào)用 accept 時,連接成功了會返回一個已完成連接的 socket,后續(xù)用來傳輸數(shù)據(jù)。

所以,監(jiān)聽的 socket 和真正用來傳送數(shù)據(jù)的 socket,是「兩個」 socket,一個叫作監(jiān)聽 socket,一個叫作已完成連接 socket。

成功連接建立之后,雙方開始通過 read 和 write 函數(shù)來讀寫數(shù)據(jù),就像往一個文件流里面寫東西一樣。

針對 UDP 協(xié)議通信的 socket 編程模型

UDP 是沒有連接的,所以不需要三次握手,也就不需要像 TCP 調(diào)用 listen 和 connect,但是 UDP 的交互仍然需要 IP 地址和端口號,因此也需要 bind。

對于 UDP 來說,不需要要維護(hù)連接,那么也就沒有所謂的發(fā)送方和接收方,甚至都不存在客戶端和服務(wù)端的概念,只要有一個 socket 多臺機器就可以任意通信,因此每一個 UDP 的 socket 都需要 bind。

另外,每次通信時,調(diào)用 sendto 和 recvfrom,都要傳入目標(biāo)主機的 IP 地址和端口。

針對本地進(jìn)程間通信的 socket 編程模型

本地 socket 被用于在同一臺主機上進(jìn)程間通信的場景:

本地 socket 的編程接口和 IPv4 、IPv6 套接字編程接口是一致的,可以支持「字節(jié)流」和「數(shù)據(jù)報」兩種協(xié)議;

本地 socket 的實現(xiàn)效率大大高于 IPv4 和 IPv6 的字節(jié)流、數(shù)據(jù)報 socket 實現(xiàn);

對于本地字節(jié)流 socket,其 socket 類型是 AF_LOCAL 和 SOCK_STREAM。

對于本地數(shù)據(jù)報 socket,其 socket 類型是 AF_LOCAL 和 SOCK_DGRAM。

本地字節(jié)流 socket 和 本地數(shù)據(jù)報 socket 在 bind 的時候,不像 TCP 和 UDP 要綁定 IP 地址和端口,而是綁定一個本地文件,這也就是它們之間的最大區(qū)別。

總結(jié)

由于每個進(jìn)程的用戶空間都是獨立的,不能相互訪問,這時就需要借助內(nèi)核空間來實現(xiàn)進(jìn)程間通信,原因很簡單,每個進(jìn)程都是共享一個內(nèi)核空間。

Linux 內(nèi)核提供了不少進(jìn)程間通信的方式,其中最簡單的方式就是管道,管道分為「匿名管道」和「命名管道」。

匿名管道顧名思義,它沒有名字標(biāo)識,匿名管道是特殊文件只存在于內(nèi)存,沒有存在于文件系統(tǒng)中,shell 命令中的「|」豎線就是匿名管道,通信的數(shù)據(jù)是無格式的流并且大小受限,通信的方式是單向的,數(shù)據(jù)只能在一個方向上流動,如果要雙向通信,需要創(chuàng)建兩個管道,再來匿名管道是只能用于存在父子關(guān)系的進(jìn)程間通信,匿名管道的生命周期隨著進(jìn)程創(chuàng)建而建立,隨著進(jìn)程終止而消失。

命名管道突破了匿名管道只能在親緣關(guān)系進(jìn)程間的通信限制,因為使用命名管道的前提,需要在文件系統(tǒng)創(chuàng)建一個類型為 p 的設(shè)備文件,那么毫無關(guān)系的進(jìn)程就可以通過這個設(shè)備文件進(jìn)行通信。另外,不管是匿名管道還是命名管道,進(jìn)程寫入的數(shù)據(jù)都是緩存在內(nèi)核中,另一個進(jìn)程讀取數(shù)據(jù)時候自然也是從內(nèi)核中獲取,同時通信數(shù)據(jù)都遵循先進(jìn)先出原則,不支持 lseek 之類的文件定位操作。

消息隊列克服了管道通信的數(shù)據(jù)是無格式的字節(jié)流的問題,消息隊列實際上是保存在內(nèi)核的「消息鏈表」,消息隊列的消息體是可以用戶自定義的數(shù)據(jù)類型,發(fā)送數(shù)據(jù)時,會被分成一個一個獨立的消息體,當(dāng)然接收數(shù)據(jù)時,也要與發(fā)送方發(fā)送的消息體的數(shù)據(jù)類型保持一致,這樣才能保證讀取的數(shù)據(jù)是正確的。消息隊列通信的速度不是最及時的,畢竟每次數(shù)據(jù)的寫入和讀取都需要經(jīng)過用戶態(tài)與內(nèi)核態(tài)之間的拷貝過程。

共享內(nèi)存可以解決消息隊列通信中用戶態(tài)與內(nèi)核態(tài)之間數(shù)據(jù)拷貝過程帶來的開銷,它直接分配一個共享空間,每個進(jìn)程都可以直接訪問,就像訪問進(jìn)程自己的空間一樣快捷方便,不需要陷入內(nèi)核態(tài)或者系統(tǒng)調(diào)用,大大提高了通信的速度,享有最快的進(jìn)程間通信方式之名。但是便捷高效的共享內(nèi)存通信,帶來新的問題,多進(jìn)程競爭同個共享資源會造成數(shù)據(jù)的錯亂。

那么,就需要信號量來保護(hù)共享資源,以確保任何時刻只能有一個進(jìn)程訪問共享資源,這種方式就是互斥訪問。信號量不僅可以實現(xiàn)訪問的互斥性,還可以實現(xiàn)進(jìn)程間的同步,信號量其實是一個計數(shù)器,表示的是資源個數(shù),其值可以通過兩個原子操作來控制,分別是 P 操作和 V 操作。

與信號量名字很相似的叫信號,它倆名字雖然相似,但功能一點兒都不一樣。信號是進(jìn)程間通信機制中唯一的異步通信機制,信號可以在應(yīng)用進(jìn)程和內(nèi)核之間直接交互,內(nèi)核也可以利用信號來通知用戶空間的進(jìn)程發(fā)生了哪些系統(tǒng)事件,信號事件的來源主要有硬件來源(如鍵盤 Cltr+C )和軟件來源(如 kill 命令),一旦有信號發(fā)生,進(jìn)程有三種方式響應(yīng)信號 1. 執(zhí)行默認(rèn)操作、2. 捕捉信號、3. 忽略信號。有兩個信號是應(yīng)用進(jìn)程無法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,這是為了方便我們能在任何時候結(jié)束或停止某個進(jìn)程。

前面說到的通信機制,都是工作于同一臺主機,如果要與不同主機的進(jìn)程間通信,那么就需要 Socket 通信了。Socket 實際上不僅用于不同的主機進(jìn)程間通信,還可以用于本地主機進(jìn)程間通信,可根據(jù)創(chuàng)建 Socket 的類型不同,分為三種常見的通信方式,一個是基于 TCP 協(xié)議的通信方式,一個是基于 UDP 協(xié)議的通信方式,一個是本地進(jìn)程間通信方式。

以上,就是進(jìn)程間通信的主要機制了。你可能會問了,那線程通信間的方式呢?

同個進(jìn)程下的線程之間都是共享進(jìn)程的資源,只要是共享變量都可以做到線程間通信,比如全局變量,所以對于線程間關(guān)注的不是通信方式,而是關(guān)注多線程競爭共享資源的問題,信號量也同樣可以在線程間實現(xiàn)互斥與同步:

互斥的方式,可保證任意時刻只有一個線程訪問共享資源;

同步的方式,可保證線程 A 應(yīng)在線程 B 之前執(zhí)行;

最后多說一句,小編是一名python開發(fā)工程師,這里有我自己整理了一套最新的python系統(tǒng)學(xué)習(xí)教程。想要這些資料的可以進(jìn)裙930900780領(lǐng)取。

本文章素材來源于網(wǎng)絡(luò),如有侵權(quán)請聯(lián)系刪除。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,431評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,637評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,555評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,900評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,629評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,976評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評論 3 448
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,139評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,686評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,411評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,641評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,820評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,233評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,567評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,362評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,604評論 2 380