進程間如何傳遞 f
本文會從文件描述符的定義講到它的原理,然後講解如何在進程之間傳遞文件描述符,其實使用的還是 unix domain socket,在 unix domain socket in Go 詳解 + 示例大集合中關注的是 unix domain socket 的用法和性能,本次使用它能傳遞文件描述符這個不常見的控制數據,最後會使用 Go 實現一個優雅退出的例子,雖然看到最後你可能覺得不夠優雅。
什麼是 fd
大家肯定多少的知道什麼是 fd(file describer),當調用 open 系統調用的時候會返回 int 類型的值,這個值既可以理解爲是文件的 ID,也可理解爲操作文件的句柄(當然理解爲後者更好,前者類比的不恰當)。
1、 文件描述符到底是進程級別還是系統級別?
2、 文件描述符和 inode 之間的關係?
我們先來回答 “文件描述符到底是進程級別還是系統級別?”,我們可以看一個例子:
func main() {
f1, _ := os.Open("/root/go/a.txt")
f2, _ := os.Open("/root/go/a.txt")
fmt.Println(int(f1.Fd()), f2.Fd())
time.Sleep(time.Second * 60 * 50)
}
我們同時啓動兩次來看看分別輸出什麼,都是輸出的 3 5,我們看到打開第一個文件輸出的 3,自然的想到前面的 0、1、2 去哪了?然後在回憶一下我們要把一個命令的輸出 / 錯誤重定向到一個文件中怎麼搞?
是不是使用形如command 2>a.log
這樣的命令。所以這就引出了三個全局的文件描述符(除了他們都是進程級別的了):
我們再把這個問題深挖一下,fd 怎麼對應到真實的文件的呢?,首先需要科普一下 Linux 文件系統的結構 (圖 from introduction-to-the-linux-virtual-filesystem-vfs):
VFS 的存在相當於爲上層提供統一的接口,下層能對接任意的存儲,比如 ext 系列、nfs、xfs 等。inode 存放的是文件的 metadata,比如權限,大小是多大,佔多少個塊等,這個結構相當大。
VFS 還要具有快速通過文件名得到 inode 的能力,總不能把所有的 inode 都緩存下來吧,這誰也受不了了呀,所以就有了 dentry(directory entry),他是一個承上啓下的結構,能通過文件名快速得到 inode,VFS 會把儘量多的 dentry 緩存下來,即 dentry cache。
現在我們對文件系統有了初步的認識,我們在回過頭來看_應用拿到的 fd 和具體負責存儲的 inode 之間的關係_,看下圖(from 文件描述符(File Descriptor)簡介):
進程 A 中的 fd 1 和 fd 30 都指向 global file table 中的 23 號,這是在同一個進程中 open 多次的結果;
進程 A 中 fd 0 指向 global file table 中的 0 號文件句柄和進程 B 中的 fd3 指向的 global file table 中的 86 號文件句柄,它們(global file table 中的 0 號和 86 號)都指向 i-node 表中的 1076 號 entry;
進程 A 中 fd 2 和進程 B 中的 fd 2 都指向的 global file table 中的 73 號文件句柄,這可能有兩種情況:A 和 B 是父子進程關係,子進程默認繼承父繼承的所有 fd;A 和 B 通過 unix domain socket 把這個進程傳遞。
我們通過 open 拿到的是一個指針,指向的是全局由 kernel 維護的 file table,這個裏面記錄着文件以什麼 mode 被打開以及指向的 inode。
如何在進程間傳遞 fd
像 nginx、envoy 重啓的時候都是會先 fork 一個子進程出來,然後這個子進程默認繼承了父進程的文件描述符,老的 worker 處理之後再退出,新 worker 處理新流量。
但是如果子進程要和父進程通信,或者兩個不相關的進程傳遞 fd 就需要採用 IPC,將文件描述符的指針傳遞過去了。read/write 這種最基本的 I/O 函數解決不了傳遞控制數據(control messages、ancillary data),這個時候 recv/send 和 recvmsg/sendmsg 就呼之欲出了,這裏我們只介紹 recvmsg/sendmsg,因爲它是 recv/send 加強版,能夠給內核傳遞的參數更多。我們主要來看下功能最強大的 sendmsg(send、send 以及 sendmsg 之間的差別可以看 recv(2) 和 send(2))。
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
主要來看 msghdr 這個結構:
struct msghdr {
void *msg_name; /* Optional address */
socklen_t msg_namelen; /* Size of address */
struct iovec *msg_iov; /* Scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* Ancillary data, see below */
size_t msg_controllen; /* Ancillary data buffer len */
int msg_flags; /* Flags (unused) */
};
能夠通過 msg_control 傳遞不常見的協議頭、文件描述符以及 unix 認證信息等(老版本的叫 msg_accrights)。這裏就不過多說,有興趣的請看《unix 網絡編程卷 1: API》的 14.5 章,講的很詳細。
現在我們知道通過 socket 傳遞不僅能傳遞 payload 數據還能傳遞附加數據(也叫控制數據),現在我們能寫代碼開搞了。
用 Go 實現進程間傳遞 fd
我們實現如下:
-
進程 A 監聽 9087 端口對外提供服務
-
進程 B 監聽 / tmp/sock.sock 文件,接受其他進程(這裏只有進程 A)傳遞過來已經 listen 文件描述符,繼續進行 accept 處理
-
進程 A 監聽 Interrupt 信號,收到信號將監聽 9087 端口的文件描述符傳遞給 B 進程,然後退出
-
最後 B 進程監聽 9087 端口對外提供服務
我們先看一下執行流程:
1、 啓動 a 程序, 並確認
lsof -i:9087
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
a 82703 helios 5u IPv6 0xd805aa2d675a920f 0t0 TCP *:9087 (LISTEN)
2、 啓動 b 程序
3、 通過 nc localhost 9087 和 a 程序溝通
nc localhost 9087
aaa
a process response
aaa
a process respons
4、通過 kill -2 a 進程的 pid,觸發鏈接遷移,能觀察到兩個現象:步驟三的鏈接斷了以及 9087 是由 b 程序監聽的了,如下:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
b 82725 helios 8u IPv6 0xd805aa2d675a920f 0t0 TCP *:9087 (LISTEN)
b 82725 helios 9u IPv6 0xd805aa2d675a920f 0t0 TCP *:9087 (LISTEN)
5、 再通過 nc localhost 9087 就是和 b 程序進行溝通了
nc localhost 9087
aaa
b process response
aaa
b process respons
我們先來看下 a 的代碼:
const sock = "/tmp/sock.sock"
func main() {
l, _ := net.Listen("tcp", ":9087")
defer l.Close()
// 監聽信號
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt)
// 開啓遷移鏈接的goroutine
go transferConn(ch, l.(*net.TCPListener))
for {
c, err := l.Accept()
if err != nil {
log.Println(err)
break
}
go func() {
for {
var buf = make([]byte, 1)
c.Read(buf)
c.Write([]byte("a process response\n"))
}
}()
}
}
func transferConn(ch <-chan os.Signal, l *net.TCPListener) {
<-ch
log.Print("start transfer listener")
c,err := net.Dial("unix", sock)
unixConn := c.(*net.UnixConn)
linstenFile, err := l.File()
rights := syscall.UnixRights(int(linstenFile.Fd()))
var buf = make([]byte, 1)
unixConn.WriteMsgUnix(buf, rights, nil)
fmt.Println("ending transfer listener")
l.Close()
}
syscall.UnixRights 就是相當於填充的msghdr.msg_control
字段。
再來看看 b 程序:
const sock = "/tmp/sock.sock"
func main() {
_ = syscall.Unlink(sock)
l , _ := net.Listen("unix", sock)
log.Print("listen " + sock)
defer l.Close()
recvListener(l.(*net.UnixListener))
}
func recvListener(unixListener *net.UnixListener) {
unixConn, _ := unixListener.AcceptUnix()
defer unixConn.Close()
var (
buf = make([]byte, 1)
oob = make([]byte, 1024)
)
_, oobn, _, _, _ := unixConn.ReadMsgUnix(buf, oob)
scms, _ := unix.ParseSocketControlMessage(oob[0:oobn])
recvFds, _ := unix.ParseUnixRights(&scms[0])
recvFile := os.NewFile(uintptr(recvFds[0]), "")
recvFileListener, _ := net.FileListener(recvFile)
recvListener := recvFileListener.(*net.TCPListener)
for {
conn, _ := recvListener.Accept()
go func() {
for {
var buf = make([]byte, 1)
conn.Read(buf)
conn.Write([]byte("b process response\n"))
}
}()
}
}
其實看下來挺清晰的,屬於不看不知道一看就會的那種。
總結
看懂了本篇文章,相信你對優雅退出也就不陌生了,但是這也是我開篇說的不夠優雅的地方,因爲這個過程是要斷連的,並沒有對已經建立的連接遷移的過程,其實每個連接也是一個 fd,但是要處理讀寫關係就比較負責,但也不是不能做。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/ts_qsUee6mUFv0FpykaOXQ