計算機通信方式主要有如下幾種,本文會詳細(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)系刪除。