扒開 Linux 內核看 IPC:進程通信的底層實現與優化
在我們的日常生活中,人與人之間無時無刻不在進行著信息的交流。你和朋友聊天分享日常,通過語言這個 “媒介” 傳遞想法;你給遠方的親人寫信,信件就成了溝通的橋梁。而在 Linux 操作系統的世界里,各個進程就如同生活中的個體,它們也需要相互交流、協同工作,這就離不開進程間通信(Inter - Process Communication,IPC)。想象一下,如果每個進程都是一座孤島,那整個系統的運行將會陷入混亂。
在實際應用中,選擇合適的進程間通信方式至關重要,它直接影響到系統的性能、穩定性和可擴展性。隨著計算機技術的不斷發展,未來的進程間通信技術也將朝著更高性能、更安全、更智能的方向發展。例如,在分布式系統中,通信協議和技術將不斷優化,以解決網絡延遲、數據一致性等問題,實現更高效的分布式協作。同時,隨著人工智能和物聯網技術的興起,進程間通信也將在這些領域發揮重要作用,為智能設備之間的互聯互通提供支持 。今天,咱們就深入探討一下 Linux 進程間通信這個有趣又重要的話題,看看進程們是如何 “交流對話” 的。
一、Linux 進程間通信基礎概念
1.1 為什么要通信
在軟件體系中,進程間通信的原因與人類通信有相似之處。首先,存在需求是關鍵因素。在軟件系統中,多個進程協同完成任務、服務請求或提供消息等情況時有發生。例如,在一個復雜的分布式系統中,不同的進程可能分別負責數據采集、處理和存儲等任務,它們之間需要進行通信以確保整個系統的正常運行。其次,進程間存在隔離。每個進程都有獨立的用戶空間,互相看不到對方的內容。這就如同人與人之間如果身處不同的房間,沒有溝通渠道的話就無法交流信息。所以,為了實現信息的傳遞和任務的協同,進程間通信就顯得尤為必要。
通信方式與人類類似,取決于需求、通信量大小和客觀實現條件。在人類社會中,有烽火、送信鴿、寫信、發電報、打電話、發微信等多種通信方式。在軟件中,也對應著不同的進程間通信方式。比如,對于小量的即時信息傳遞,可以類比為打電話的方式,采用信號這種通信方式;對于大量的數據傳輸,可以類比為寫信的方式,采用消息隊列或共享內存等通信方式。
在 Linux 操作系統中,進程就像是一個個獨立的小世界。每個進程都有自己獨立的地址空間 ,這意味著它們的代碼、數據和堆棧都是相互隔離的。比如,當你在電腦上同時打開瀏覽器、音樂播放器和文檔編輯器時,這些程序各自作為獨立的進程運行。瀏覽器進程不能直接訪問音樂播放器進程的數據,反之亦然。這種獨立性保證了進程之間不會相互干擾,一個進程的崩潰不會影響其他進程的正常運行,就好比每個居民都有自己獨立的房子,不會因為鄰居家的房子出問題而影響到自己的生活。
然而,在實際的系統運行中,進程之間又往往需要相互協作。就拿瀏覽器來說,當你在瀏覽器中輸入一個網址,瀏覽器進程需要與網絡服務進程進行通信,告訴它你想要訪問的網頁地址,網絡服務進程再將獲取到的網頁數據返回給瀏覽器進程,這樣你才能在瀏覽器中看到網頁的內容。如果沒有進程間通信,瀏覽器就無法獲取到網頁數據,它就只是一個空殼,毫無用處。所以,進程間通信是讓各個進程能夠協同工作,完成復雜任務的關鍵,就像人們之間的交流合作,才能共同推動社會的運轉。
我們先拿人來做個類比,人與人之間為什么要通信,有兩個原因。首先是因為你有和對方溝通的需求,如果你都不想搭理對方,那就肯定不用通信了。其次是因為有空間隔離,如果你倆在一起,對方就站在你面前,你有話直說就行了,不需要通信。此時你非要給對方打個電話或者發個微信,是不是顯得非常奇怪、莫名其妙。如果你倆不在一塊,還有事需要溝通,此時就需要通信了。通信的方式有點烽火、送信鴿、寫信、發電報、打電話、發微信等。采取什么樣的通信方式跟你的需求、通信量的大小、以及客觀上能否實現有關。
同樣的,軟件體系中為什么會有進程間通信呢?首先是因為軟件中有這個需求,比如有些任務是由多個進程一起協同來完成的,或者一個進程對另一個進程有服務請求,或者有消息要向另一方提供。其次是因為進程間有隔離,每個進程都有自己獨立的用戶空間,互相看不到對方,所以才需要通信。
1.2 為什么能通信?
內核空間是共享的,雖然多個進程有多個用戶空間,但內核空間只有一個。就像一個公共的資源庫,雖然每個進程都有自己獨立的 “房間”(用戶空間),但它們都可以通過特定的通道訪問這個公共資源庫(內核空間)。
為什么能通信呢?那是因為內核空間是共享的,雖然N個進程都有N個用戶空間,但是內核空間只有一個,雖然用戶空間之間是完全隔離的,但是用戶空間與內核空間并不是完全隔離的,他們之間有系統調用這個通道可以溝通。所以兩個用戶空間就可以通過內核空間這個橋梁進行溝通了。
雖然用戶空間之間完全隔離,但用戶空間與內核空間并非完全隔離,它們之間有系統調用這個通道可以溝通。Linux 使用兩級保護機制:0 級供內核使用,3 級供用戶程序使用。每個進程有各自的私有用戶空間(0~3G),這個空間對系統中的其他進程是不可見的。最高的 1GB 字節虛擬內核空間則為所有進程以及內核所共享。內核空間中存放的是內核代碼和數據,而進程的用戶空間中存放的是用戶程序的代碼和數據。不管是內核空間還是用戶空間,它們都處于虛擬空間中。雖然內核空間占據了每個虛擬空間中的最高 1GB 字節,但映射到物理內存卻總是從最低地址(0x00000000)開始。
通過一副圖講解進程間通信的原理,進程之間雖然有空間隔離,但都和內核連著,可以通過特殊的系統調用和內核溝通,從而達到和其它進程通信的目的。就像不同的房間雖然相互獨立,但都通過管道與一個中央控制室相連。進程就如同各個房間,內核就如同中央控制室。進程雖然不能直接訪問其他進程的用戶空間,但可以通過系統調用與內核進行交互,內核再將信息傳遞給其他進程,從而實現進程間通信。例如,當一個進程需要向另一個進程發送數據時,它可以通過系統調用將數據寫入內核空間的特定區域,內核再通知目標進程從該區域讀取數據。
我們再借助一副圖來講解一下:

雖然這個圖是講進程調度的,但是大家從這個圖里面也能看出來進程之間為什么要通信,因為進程之間都是有空間隔離的,它們之間要想交流信息是沒有辦法的。但是也不是完全沒有辦法,好在它們都和內核是連著的,雖然它們不能隨意訪問內核,但是還有系統調用這個大門,進程之間可以通過一些特殊的系統調用和內核溝通從而達到和其它進程通信的目的。
二、Linux進程間通信的框架
2.1 進程間通信機制的結構
進程間通信機制由存在于內核空間的通信中樞和存在于用戶空間的通信接口組成,兩者關系緊密。通信中樞就如同郵局或基站,為通信提供核心機制;通信接口則像信紙或手機,為用戶提供使用通信機制的方法。
為了更直觀地理解進程間通信機制的結構,我們可以通過以下圖示來展示:

用戶通過通信接口讓通信中樞建立通信信道或傳遞通信信息。例如,在使用共享內存進行進程間通信時,用戶通過特定的系統調用接口(通信接口)請求內核空間的通信中樞為其分配一塊共享內存區域,并建立起不同進程對該區域的訪問路徑。
2.2 進程間通信機制的類型
(1)共享內存式
通信中樞建立好通信信道后,通信雙方之后的通信不需要通信中樞的協助。這就如同兩個房間之間打開了一扇門,雙方可以直接通過這扇門進行交流,而不需要中間人的幫忙。
但是,由于通信信息的傳遞不需要通信中樞的協助,通信雙方需要進程間同步,以保證數據讀寫的一致性。否則,就可能出現數據踩踏或者讀到垃圾數據的情況。比如,多個進程同時對共享內存進行讀寫操作時,需要通過信號量等機制來確保在同一時間只有一個進程能夠進行寫操作,避免數據沖突。
(2)消息傳遞式
通信中樞建立好通信信道后,每次通信還都需要通信中樞的協助。這種方式就像一個中間人在兩個房間之間傳遞信息,每次傳遞都需要經過中間人。
消息傳遞式又分為有邊界消息和無邊界消息。無邊界消息是字節流,發過來是一個一個的字節,要靠進程自己設計如何區分消息的邊界。有邊界消息的發送和接收都是以消息為基本單位,類似于一封封完整的信件,接收方可以明確地知道每個消息的開始和結束位置。
2.3 進程間通信機制的接口設計
按照通信雙方的關系,可分為對稱型通信和非對稱型通信:
- 消息傳遞式進程間通信一般用于非對稱型通信,例如在客戶服務關系中,客戶端向服務端發送請求消息,服務端接收消息并進行處理后返回響應消息,整個通信過程通過通信中樞進行消息的傳遞。
- 共享內存式進程間通信一般用于對稱型通信,也可用于非對稱型通信。在對稱型通信中,通信雙方關系對等,如同兩個平等的伙伴共同使用一塊共享內存進行數據交換。在非對稱型通信中,也可以通過共享內存實現一方寫入數據,另一方讀取數據的模式。
進程間通信機制一般要實現三類接口:
如何建立通信信道,誰去建立通信信道。對于對稱型通信來說,誰去建立通信信道無所謂,有一個人去建立就可以了,后者直接加入通信信道。對于非對稱型通信,一般是由服務端、消費者建立通信信道,客戶端、生產者則加入這個通信信道。不同的進程間通信機制,有不同的接口來創建信道。例如,在使用共享內存時,可以通過特定的系統調用(如 shmget)來創建共享內存區域,建立通信信道。
后者如何找到并加入這個通信信道。一般情況是,雙方通過提前約定好的信道名稱找到信道句柄,通過信道句柄加入通信信道。但是有的是通過繼承把信道句柄傳遞給對方,有的是通過其它進程間通信機制傳遞信道句柄,有的則是通過信道名稱直接找到信道,不需要信道句柄。
如何使用通信信道。一旦通信信道建立并加入成功,進程就需要知道如何正確地使用通信信道進行數據的讀寫操作。例如,在使用管道進行通信時,進程需要明確知道哪個文件描述符是用于讀,哪個是用于寫,以及在讀寫過程中的各種規則和特殊情況的處理。
三、常見通信方式深度解析
3.1 管道(Pipe)
在 Linux 進程間通信的工具庫中,管道是一種非常基礎且常用的方式,它就像是一條連接不同進程的 “數據管道”,數據可以在這個管道中流動,從而實現進程間的通信。管道又分為匿名管道和有名管道,它們各自有著獨特的特點和適用場景。
(1) 匿名管道
匿名管道,從名字就能看出它沒有名字,是一種臨時存在于內存中的單向數據通道。它主要用于有親緣關系的進程之間,比如父子進程。在 Shell 命令中,我們經常使用的 | 就是匿名管道的典型應用,比如 ls -l | grep test,ls -l 命令的輸出通過匿名管道作為 grep test 命令的輸入 ,實現了兩個命令(進程)之間的數據傳遞。
匿名管道的工作原理基于文件描述符。當一個進程調用 pipe 函數創建匿名管道時,會得到兩個文件描述符,fd[0] 用于讀管道,fd[1] 用于寫管道 。這就好比創建了一根兩端開口的管子,一端用來進水(寫數據),一端用來出水(讀數據)。當創建子進程時,子進程會繼承父進程的文件描述符,這樣父子進程就可以通過這兩個文件描述符來操作管道,實現通信。例如父進程關閉讀端 fd[0],子進程關閉寫端 fd[1],就可以實現父進程寫數據,子進程讀數據的單向通信。
下面通過一段代碼來深入理解匿名管道在父子進程通信中的應用:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#define BUFFER_SIZE 1024
int main() {
int pipe_fd[2];
pid_t pid;
char buffer[BUFFER_SIZE];
// 創建匿名管道
if (pipe(pipe_fd) == -1) {
perror("pipe creation failed");
return 1;
}
// 創建子進程
pid = fork();
if (pid == -1) {
perror("fork failed");
return 1;
} else if (pid == 0) {
// 子進程
close(pipe_fd[1]); // 關閉寫端
// 從管道讀取數據
ssize_t bytes_read = read(pipe_fd[0], buffer, BUFFER_SIZE - 1);
if (bytes_read == -1) {
perror("read failed");
exit(1);
}
buffer[bytes_read] = '\0'; // 字符串結束符
printf("Child process received: %s\n", buffer);
close(pipe_fd[0]); // 關閉讀端
exit(0);
} else {
// 父進程
close(pipe_fd[0]); // 關閉讀端
// 向管道寫入數據
const char *message = "Hello, child!";
ssize_t bytes_written = write(pipe_fd[1], message, strlen(message));
if (bytes_written == -1) {
perror("write failed");
return 1;
}
close(pipe_fd[1]); // 關閉寫端
// 等待子進程結束
wait(NULL);
printf("Parent process sent: %s\n", message);
}
return 0;
}在這段代碼中,首先調用 pipe 函數創建匿名管道,返回的 pipe_fd[0] 和 pipe_fd[1] 分別是讀端和寫端的文件描述符。然后通過 fork 函數創建子進程,子進程關閉寫端,從管道讀端讀取父進程寫入的數據;父進程關閉讀端,向管道寫端寫入數據。最后父進程等待子進程結束。
匿名管道的優點是實現簡單,在父子進程這種有親緣關系的進程間通信效率較高,而且它基于內存,不需要額外的磁盤 I/O 操作,速度相對較快。但它也有明顯的缺點,比如它是半雙工通信,數據只能在一個方向上流動,如果需要雙向通信,就需要創建兩個管道;另外,它只能用于有親緣關系的進程之間,適用范圍相對較窄;而且管道的緩沖區大小有限,如果數據量較大,可能需要頻繁讀寫,影響效率。
(2)有名管道(FIFO)
有名管道,也叫 FIFO(First - In - First - Out),與匿名管道不同,它在文件系統中有一個名字,就像一個特殊的文件。這使得它不僅可以用于有親緣關系的進程之間,還可以用于沒有親緣關系的進程之間通信 。比如,在一個多進程協作的系統中,不同的進程可以通過訪問同一個有名管道文件來實現數據交換,就像不同的人可以通過同一個郵箱來傳遞信件。
有名管道的特點在于它有一個文件路徑名,在文件系統中是可見的。雖然它的數據還是存儲在內核緩沖區中,不占用磁盤實際的數據塊空間,但這個可見的名字為不同進程訪問它提供了方便。創建有名管道可以使用 mkfifo 函數,例如 mkfifo("myfifo", 0666) 就創建了一個名為 myfifo 的有名管道,權限為 0666。
下面是一個服務器 - 客戶端模型中使用有名管道進行通信的代碼示例:
①服務器端代碼(寫進程):
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FIFO_NAME "myfifo"
#define BUFFER_SIZE 1024
int main() {
int fd;
char buffer[BUFFER_SIZE];
// 創建有名管道
if (mkfifo(FIFO_NAME, 0666) == -1 && errno != EEXIST) {
perror("mkfifo failed");
return 1;
}
// 打開有名管道進行寫操作
fd = open(FIFO_NAME, O_WRONLY);
if (fd == -1) {
perror("open for write failed");
return 1;
}
// 向管道寫入數據
const char *message = "Hello, client!";
ssize_t bytes_written = write(fd, message, strlen(message));
if (bytes_written == -1) {
perror("write failed");
}
close(fd); // 關閉文件描述符
return 0;
}②客戶端代碼(讀進程):
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define FIFO_NAME "myfifo"
#define BUFFER_SIZE 1024
int main() {
int fd;
char buffer[BUFFER_SIZE];
// 打開有名管道進行讀操作
fd = open(FIFO_NAME, O_RDONLY);
if (fd == -1) {
perror("open for read failed");
return 1;
}
// 從管道讀取數據
ssize_t bytes_read = read(fd, buffer, BUFFER_SIZE - 1);
if (bytes_read == -1) {
perror("read failed");
} else {
buffer[bytes_read] = '\0'; // 字符串結束符
printf("Client received: %s\n", buffer);
}
close(fd); // 關閉文件描述符
return 0;
}在這個示例中,服務器端首先使用 mkfifo 創建有名管道,然后以寫模式打開管道并寫入數據;客戶端以讀模式打開同一個有名管道,讀取服務器端寫入的數據。通過這種方式,兩個沒有親緣關系的進程實現了通信。
有名管道適用于那些需要長期存在,并且不同進程之間需要進行數據傳輸的場景。比如一個日志記錄系統,多個進程可以將日志信息寫入同一個有名管道,而日志處理進程從管道中讀取日志數據進行處理。它的優點是可以在任意進程間通信,并且生命周期不依賴于進程,創建后可以一直存在,直到被刪除;缺點是它也是半雙工通信,雙向通信需要兩個有名管道,而且在使用時需要注意文件系統的操作,相對匿名管道來說,開銷稍大一些。
3.2 信號(Signals)
信號是 Linux 進程間通信中一種異步事件通知機制,它就像是一個緊急通知,當系統中發生某些特定事件時,內核就會向相應的進程發送信號,進程接收到信號后會執行相應的操作。比如,當你在終端中按下 Ctrl + C 組合鍵時,內核會向當前正在運行的前臺進程發送一個 SIGINT(中斷信號),進程接收到這個信號后,默認情況下會終止運行。信號可以用于處理各種異步事件,如硬件中斷、軟件異常等,它能夠及時通知進程重要的事件發生 。
信號的工作機制基于內核和進程之間的交互。內核在檢測到特定事件時,會將信號發送給目標進程。每個信號都有一個唯一的編號和名稱,例如 SIGTERM(終止信號)、SIGKILL(強制終止信號)等。進程可以通過三種方式處理信號:執行默認的信號處理動作、忽略信號、自定義信號處理函數。默認處理動作是系統為每個信號預先定義好的,比如 SIGINT 的默認動作是終止進程;忽略信號就是進程對該信號不做任何處理;自定義信號處理函數則允許程序員根據自己的需求編寫處理信號的代碼,實現特定的功能。
常見的信號及其用途如下:
- SIGINT(2):由鍵盤按下 Ctrl + C 產生,用于中斷當前進程,通常用于終止正在運行的程序。比如,當你運行一個長時間運行的腳本,想中途停止它時,就可以使用 Ctrl + C 發送 SIGINT 信號。
- SIGTERM(15):這是系統 kill 命令默認發送的信號,用于正常終止一個進程。與 SIGKILL 不同,它允許進程在接收到信號后進行一些清理工作,比如關閉文件、釋放資源等,然后再退出。
- SIGKILL(9):這個信號的響應方式不允許改變,它會直接強制終止進程,無論進程當前處于什么狀態。一般用于處理那些無法正常終止的進程,比如進程陷入死循環或者出現嚴重錯誤時。
- SIGCHLD(17):當子進程結束后,會默認給父進程發送該信號,父進程可以通過捕獲這個信號來處理子進程的退出狀態,比如回收子進程的資源,避免出現僵尸進程。
下面是一個信號處理的代碼示例:
import signal
import os
import time
import sys
def handle_sigint(signum, frame):
"""處理SIGINT信號(CTRL+C)"""
print(f"\n接收到SIGINT({signum}) - 用戶中斷")
print("正在執行清理操作...")
# 模擬資源清理
time.sleep(1)
print("清理完成,程序退出")
sys.exit(0)
def handle_sigterm(signum, frame):
"""處理SIGTERM信號(優雅終止)"""
print(f"\n接收到SIGTERM({signum}) - 終止請求")
sys.exit(0)
def handle_sighup(signum, frame):
"""處理SIGHUP信號(終端掛斷)"""
print(f"\n接收到SIGHUP({signum}) - 終端掛斷")
print("重新加載配置文件...")
# 實際應用中這里會重新加載配置
time.sleep(1)
print("配置已重新加載")
def handle_sigchld(signum, frame):
"""處理SIGCHLD信號(子進程狀態變化)"""
print(f"\n接收到SIGCHLD({signum}) - 子進程狀態改變")
# 在實際應用中可以用waitpid()獲取子進程退出狀態
try:
while True:
# 非阻塞方式獲取子進程狀態
pid, status = os.waitpid(-1, os.WNOHANG)
if pid == 0:
break
print(f"子進程 {pid} 退出,狀態碼: {status}")
except OSError:
pass
def main():
# 注冊信號處理函數
signal.signal(signal.SIGINT, handle_sigint)
signal.signal(signal.SIGTERM, handle_sigterm)
signal.signal(signal.SIGHUP, handle_sighup)
signal.signal(signal.SIGCHLD, handle_sigchld)
# 忽略SIGPIPE信號(避免寫入已關閉的管道時崩潰)
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
print(f"進程PID: {os.getpid()}")
print("正在運行,嘗試以下操作測試信號處理:")
print("1. 按CTRL+C發送SIGINT")
print("2. 運行 'kill PID' 發送SIGTERM")
print("3. 運行 'kill -HUP PID' 發送SIGHUP")
print("4. 運行 'kill -CHLD PID' 發送SIGCHLD")
# 創建一個子進程用于測試SIGCHLD
pid = os.fork()
if pid == 0:
# 子進程: 休眠2秒后退出
print(f"子進程({os.getpid()})啟動,2秒后退出")
time.sleep(2)
sys.exit(0)
# 主循環
try:
while True:
time.sleep(1)
except Exception as e:
print(f"發生異常: {e}")
if __name__ == "__main__":
main()在這段代碼中,首先定義了一個 signal_handler 函數作為信號處理函數,當接收到信號時,它會打印出接收到的信號編號,并退出程序。然后在 main 函數中使用 signal 函數注冊信號處理函數,將 SIGINT 信號與 signal_handler 函數關聯起來。之后程序進入一個無限循環,等待信號的到來。當在終端中按下 Ctrl + C 時,就會向該進程發送 SIGINT 信號,進程接收到信號后會調用 signal_handler 函數進行處理。
信號的優點是簡單、高效,能夠快速通知進程發生了某個事件;缺點是信號攜帶的信息量有限,通常只是一個信號編號,而且信號處理函數的執行時機是不確定的,可能會在進程執行的任意時刻被觸發,這可能會影響進程的正常執行流程,所以在使用信號時需要特別小心,避免出現競態條件等問題。
3.3 文件(Files)
文件作為進程間通信的一種方式,原理其實很簡單。不同進程可以通過讀寫同一個文件來實現數據的傳遞和共享。比如,一個進程將數據寫入文件,另一個進程從文件中讀取數據,這樣就完成了一次進程間的通信。這就好比兩個人通過一本筆記本進行交流,一個人在筆記本上寫下信息,另一個人查看筆記本獲取信息。
文件用于進程間通信的場景有很多。例如,在一個多進程的日志系統中,各個進程可以將日志信息寫入同一個日志文件,然后日志分析進程再從這個文件中讀取日志數據進行分析。再比如,在一些配置文件的管理中,不同進程可以讀取和修改同一個配置文件,實現配置信息的共享和更新。
然而,當多個進程同時寫文件時,會出現一些問題。最主要的問題就是數據一致性和競爭條件。假設兩個進程同時向文件中寫入數據,可能會出現數據覆蓋的情況。比如進程 A 寫入一部分數據,還沒來得及寫完,進程 B 就開始寫入,導致進程 A 寫入的數據被破壞。為了解決這個問題,可以采用一些同步機制,比如文件鎖。文件鎖可以分為共享鎖和排他鎖。
共享鎖允許多個進程同時讀取文件,但不允許寫入;排他鎖則只允許一個進程對文件進行讀寫操作,其他進程都不能訪問。通過使用文件鎖,就可以保證在同一時刻只有一個進程能夠對文件進行寫入操作,從而保證數據的一致性。例如,在使用 open 函數打開文件時,可以設置相應的標志位來獲取文件鎖,或者使用 fcntl 函數來操作文件鎖。另外,也可以使用信號量等其他同步機制來協調多進程對文件的訪問 ,確保文件操作的原子性和正確性。
以下是寫進程和讀進程的代碼示例:
import sysv_ipc
import time
import signal
import sys
# 共享內存鍵值和大小
SHM_KEY = 0x123456
SHM_SIZE = 1024
def signal_handler(signal, frame):
"""處理信號,清理資源"""
try:
shm = sysv_ipc.SharedMemory(SHM_KEY)
shm.detach()
shm.remove()
except:
pass
print("\n寫進程退出")
sys.exit(0)
def write_to_shared_memory():
# 注冊信號處理
signal.signal(signal.SIGINT, signal_handler)
try:
# 創建共享內存
shm = sysv_ipc.SharedMemory(SHM_KEY, flags=sysv_ipc.IPC_CREX, size=SHM_SIZE)
print("共享內存創建成功")
except sysv_ipc.ExistError:
# 如果共享內存已存在則連接它
shm = sysv_ipc.SharedMemory(SHM_KEY)
print("連接到已存在的共享內存")
# 寫入數據
try:
for i in range(10):
message = f"這是第 {i+1} 條消息: Hello from write process!".encode('utf-8')
if len(message) >= SHM_SIZE:
print("消息過長,已截斷")
message = message[:SHM_SIZE-1]
# 寫入共享內存
shm.write(message.ljust(SHM_SIZE, b'\x00'))
print(f"已寫入: {message.decode('utf-8')}")
time.sleep(1)
# 發送結束標記
shm.write(b"EOF".ljust(SHM_SIZE, b'\x00'))
print("已發送結束標記")
finally:
# 清理資源
shm.detach()
# 注釋掉remove()以便讀進程可以完成讀取
# shm.remove()
if __name__ == "__main__":
write_to_shared_memory()
time.sleep(2) # 等待讀進程讀取結束標記
# 最后清理共享內存
try:
shm = sysv_ipc.SharedMemory(SHM_KEY)
shm.remove()
except:
pass
print("寫進程完成")3.4 共享內存(Shared Memory)
共享內存允許多個進程訪問同一內存區域,是一種高效的 IPC 機制。可以使用 shmget、shmat、shmdt 和 shmctl 函數來創建共享內存、寫入數據和讀取數據。以下是創建共享內存、寫入數據和讀取數據的代碼示例:
#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/ipc.h>#include <sys/shm.h>#define TEXT_SZ 2048struct shared_use_st { char text[TEXT_SZ];};int main() { int shmid; void *shm = NULL; struct shared_use_st *shared; // 創建共享內存 shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666 | IPC_CREAT); if (shmid == -1) { fprintf(stderr, "shmget failed\n"); exit(EXIT_FAILURE); } // 將共享內存連接到當前進程的地址空間 shm = shmat(shmid, 0, 0); if (shm == (void *)-1) { fprintf(stderr, "shmat failed\n"); exit(EXIT_FAILURE); } // 設置共享內存 shared = (struct shared_use_st *) shm; // 寫入數據 strcpy(shared->text, "Data to be shared"); // 讀取數據 printf("Read from shared memory: %s\n", shared->text); // 把共享內存從當前進程中分離 if (shmdt(shm) == -1) { fprintf(stderr, "shmdt failed\n"); exit(EXIT_FAILURE); } // 刪除共享內存 if (shmctl(shmid, IPC_RMID, 0) == -1) { fprintf(stderr, "shmctl(IPC_RMID) failed"); exit(EXIT_FAILURE); } exit(EXIT_SUCCESS);}3.5 消息隊列(Message Queue)
消息隊列是一種進程間通信的方式,它就像是一個存放消息的 “信箱”。每個進程都可以向這個 “信箱” 中發送消息,也可以從 “信箱” 中接收消息,從而實現進程間的數據傳遞。消息隊列的數據結構本質上是一個鏈表,每個節點存儲一條消息,消息由消息類型和消息內容組成。比如,在一個分布式系統中,不同的微服務進程可以通過消息隊列來傳遞任務請求、響應結果等信息。
消息隊列具有一些獨特的特點和優勢。首先,它是異步通信的,發送進程和接收進程不需要同時運行,發送進程將消息發送到消息隊列后就可以繼續執行其他任務,接收進程在合適的時候從隊列中讀取消息進行處理,這大大提高了系統的并發處理能力。其次,消息隊列可以實現解耦,發送方和接收方不需要直接相互依賴,它們只需要關注消息隊列,這樣可以使系統的架構更加靈活和可擴展。
例如,在一個電商系統中,訂單生成進程將訂單消息發送到消息隊列,而訂單處理進程從消息隊列中獲取訂單消息進行處理,即使訂單生成進程或訂單處理進程發生了變化,只要它們與消息隊列的交互方式不變,整個系統就不會受到太大影響。另外,消息隊列還可以對消息進行排隊,按照先進先出的原則處理消息,保證消息的順序性。
下面是一個使用消息隊列的代碼示例(以 System V 消息隊列為例):
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
#define MSG_SIZE 128
// 定義消息結構
typedef struct msgbuf {
long mtype; // 消息類型
char mtext[MSG_SIZE]; // 消息內容
} message_buf;
int main() {
int msgid;
message_buf msg;
key_t key;
// 創建唯一的鍵值
if ((key = ftok(".", 'a')) == -1) {
perror("ftok");
return 1;
}
// 創建消息隊列
if ((msgid = msgget(key, IPC_CREAT | 0666)) == -1) {
perror("msgget");
return 1;
}
// 填充消息內容
msg.mtype = 1;
strcpy(msg.mtext, "Hello, message queue!");
// 發送消息
if (msgsnd(msgid, &msg, strlen(msg.mtext) + 1, 0) == -1) {
perror("msgsnd");
return 1;
}
printf("Message sent: %s\n", msg.mtext);
// 接收消息
if (msgrcv(msgid, &msg, MSG_SIZE, 1, 0) == -1) {
perror("msgrcv");
return 1;
}
printf("Message received: %s\n", msg.mtext);
// 刪除消息隊列
if (msgctl(msgid, IPC_RMID, NULL) == -1) {
perror("msgctl");
return 1;
}
return 0;
}在這段代碼中,首先使用 ftok 函數創建一個唯一的鍵值,這個鍵值用于標識消息隊列。然后通過 msgget 函數創建消息隊列,如果隊列已經存在,則獲取隊列的標識符。接著填充消息結構,包括消息類型和消息內容,并使用 msgsnd。
3.6 套接字(Sockets)
套接字是一種網絡通信機制,也可用于本地 IPC。可以使用 socket、bind、listen、accept 和 connect 函數來實現服務端和客戶端的通信。以下是服務端和客戶端的代碼示例:
服務端:
import socket
import os
import signal
import sys
# 本地套接字文件路徑
SOCKET_PATH = "/tmp/local_ipc_socket"
def handle_sigint(signum, frame):
"""處理中斷信號,清理資源"""
print("\n接收到中斷信號,清理資源...")
if os.path.exists(SOCKET_PATH):
os.unlink(SOCKET_PATH)
sys.exit(0)
def run_server():
# 注冊信號處理
signal.signal(signal.SIGINT, handle_sigint)
# 確保之前的套接字文件已刪除
if os.path.exists(SOCKET_PATH):
os.unlink(SOCKET_PATH)
# 創建本地套接字
server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
# 綁定到本地路徑
server_socket.bind(SOCKET_PATH)
print(f"服務端綁定到 {SOCKET_PATH}")
# 開始監聽連接
server_socket.listen(1)
print("服務端正在等待連接...")
# 接受客戶端連接
client_socket, addr = server_socket.accept()
print(f"客戶端已連接: {addr}")
try:
while True:
# 接收數據
data = client_socket.recv(1024)
if not data:
print("客戶端斷開連接")
break
print(f"收到客戶端消息: {data.decode('utf-8')}")
# 發送響應
response = f"服務端已收到: {data.decode('utf-8')}"
client_socket.sendall(response.encode('utf-8'))
# 如果收到退出命令,終止服務
if data.decode('utf-8').lower() == 'exit':
print("收到退出命令,服務端將關閉")
break
finally:
client_socket.close()
finally:
server_socket.close()
os.unlink(SOCKET_PATH)
print("服務端已關閉")
if __name__ == "__main__":
run_server()客戶端代碼:
import socket
import sys
# 本地套接字文件路徑,需與服務端一致
SOCKET_PATH = "/tmp/local_ipc_socket"
def run_client():
# 創建本地套接字
client_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
# 連接到服務端
client_socket.connect(SOCKET_PATH)
print(f"已連接到服務端 {SOCKET_PATH}")
while True:
# 獲取用戶輸入
message = input("請輸入要發送的消息(輸入exit退出): ")
if not message:
continue
# 發送消息
client_socket.sendall(message.encode('utf-8'))
# 如果是退出命令,斷開連接
if message.lower() == 'exit':
break
# 接收響應
response = client_socket.recv(1024)
if not response:
print("服務端已斷開連接")
break
print(f"服務端響應: {response.decode('utf-8')}")
except ConnectionRefusedError:
print("連接失敗,請確保服務端已啟動")
finally:
client_socket.close()
print("客戶端已關閉")
if __name__ == "__main__":
run_client()編譯與運行步驟(Linux 環境)
①編譯代碼:打開終端,分別編譯服務端和客戶端。
# 編譯服務端
gcc unix_socket_server.c -o socket_server
# 編譯客戶端
gcc unix_socket_client.c -o socket_client②啟動服務端:在第一個終端運行。
./socket_server服務端會顯示:服務端已啟動,監聽路徑:/tmp/linux_ipc_socket。
③啟動客戶端:打開第二個終端運行。
./socket_client客戶端會顯示連接成功信息,等待用戶輸入消息。
④通信測試:在客戶端輸入任意消息并回車,服務端會顯示收到的消息并返回響應。輸入exit可終止連接。
四、Linux進程間通信案例實戰分析
4.1 管道通信案例
案例描述:父進程創建一個管道,然后創建子進程。父進程向管道寫入數據,子進程從管道讀取數據并打印。
代碼示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#define BUFFER_SIZE 1024
int main() {
int pipefd[2]; // 管道文件描述符數組,pipefd[0]為讀端,pipefd[1]為寫端
pid_t pid;
char buffer[BUFFER_SIZE];
// 1. 創建管道
if (pipe(pipefd) == -1) {
perror("pipe創建失敗");
exit(EXIT_FAILURE);
}
// 2. 創建子進程
pid = fork();
if (pid == -1) {
perror("fork失敗");
exit(EXIT_FAILURE);
}
if (pid == 0) { // 子進程
// 3. 子進程關閉寫端(只需要讀)
close(pipefd[1]);
// 4. 從管道讀取數據
ssize_t bytes_read = read(pipefd[0], buffer, BUFFER_SIZE - 1);
if (bytes_read == -1) {
perror("讀取失敗");
exit(EXIT_FAILURE);
}
// 添加字符串結束符
buffer[bytes_read] = '\0';
// 5. 打印讀取到的數據
printf("子進程讀取到的數據:%s\n", buffer);
// 6. 關閉讀端
close(pipefd[0]);
exit(EXIT_SUCCESS);
} else { // 父進程
// 7. 父進程關閉讀端(只需要寫)
close(pipefd[0]);
// 8. 向管道寫入數據
const char *message = "Hello from parent process!";
if (write(pipefd[1], message, strlen(message)) == -1) {
perror("寫入失敗");
exit(EXIT_FAILURE);
}
// 9. 關閉寫端
close(pipefd[1]);
// 10. 等待子進程結束
wait(NULL);
printf("父進程完成\n");
exit(EXIT_SUCCESS);
}
}通過管道實現了父子進程間的單向數據傳輸,父進程寫入的數據被子進程成功讀取,管道在這種親緣關系進程間通信簡單高效,但要注意及時關閉不需要的管道端,避免資源浪費和潛在的阻塞問題。
4.2 消息隊列通信案例
案例描述:創建一個消息隊列,一個進程向消息隊列發送消息,另一個進程從消息隊列接收消息并打印。
(1)msg_sender.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <unistd.h>
// 消息結構體定義
struct msg_buffer {
long msg_type; // 消息類型(必須 > 0)
char msg_text[1024]; // 消息內容
};
int main() {
key_t key;
int msgid;
struct msg_buffer message;
// 1. 生成唯一鍵值(需與接收進程使用相同鍵值)
key = ftok("msg_queue_demo", 65);
if (key == -1) {
perror("ftok生成鍵值失敗");
exit(EXIT_FAILURE);
}
// 2. 創建或獲取消息隊列
msgid = msgget(key, 0666 | IPC_CREAT);
if (msgid == -1) {
perror("創建消息隊列失敗");
exit(EXIT_FAILURE);
}
// 3. 設置消息類型和內容
message.msg_type = 1; // 消息類型為1
strcpy(message.msg_text, "Hello from sender process!");
// 4. 發送消息到隊列
if (msgsnd(msgid, &message, sizeof(message.msg_text), 0) == -1) {
perror("發送消息失敗");
exit(EXIT_FAILURE);
}
printf("消息已發送: %s\n", message.msg_text);
// 5. 發送結束消息
strcpy(message.msg_text, "exit");
if (msgsnd(msgid, &message, sizeof(message.msg_text), 0) == -1) {
perror("發送結束消息失敗");
exit(EXIT_FAILURE);
}
// 6. 等待接收進程處理完畢后刪除消息隊列
sleep(1);
if (msgctl(msgid, IPC_RMID, NULL) == -1) {
perror("刪除消息隊列失敗");
exit(EXIT_FAILURE);
}
printf("消息隊列已刪除\n");
return 0;
}(2)msg_receiver.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <unistd.h>
// 與發送進程相同的消息結構體定義
struct msg_buffer {
long msg_type; // 消息類型
char msg_text[1024]; // 消息內容
};
int main() {
key_t key;
int msgid;
struct msg_buffer message;
// 1. 生成與發送進程相同的鍵值
key = ftok("msg_queue_demo", 65);
if (key == -1) {
perror("ftok生成鍵值失敗");
exit(EXIT_FAILURE);
}
// 2. 獲取消息隊列(由發送進程創建)
msgid = msgget(key, 0666 | IPC_CREAT);
if (msgid == -1) {
perror("獲取消息隊列失敗");
exit(EXIT_FAILURE);
}
printf("等待接收消息...\n");
// 3. 循環接收消息
while (1) {
// 接收類型為1的消息
if (msgrcv(msgid, &message, sizeof(message.msg_text), 1, 0) == -1) {
perror("接收消息失敗");
exit(EXIT_FAILURE);
}
printf("收到消息: %s\n", message.msg_text);
// 檢查是否為結束消息
if (strcmp(message.msg_text, "exit") == 0) {
printf("收到結束消息,退出接收進程\n");
break;
}
}
return 0;
}(3)編譯與運行
# 編譯發送進程
gcc msg_sender.c -o msg_sender
# 編譯接收進程
gcc msg_receiver.c -o msg_receiver運行步驟:
- 先啟動接收進程:./msg_receiver
- 再啟動發送進程:./msg_sender
運行后輸出:
# 接收進程
等待接收消息...
收到消息: Hello from sender process!
收到消息: exit
收到結束消息,退出接收進程
# 發送進程
消息已發送: Hello from sender process!
消息隊列已刪除消息隊列相比管道的優勢是允許非親緣關系進程通信,且消息可以按類型接收,適合需要異步通信和消息分類的場景。
4.3 共享內存通信案例
案例描述:創建一塊共享內存,一個進程向共享內存寫入數據,另一個進程從共享內存讀取數據并打印。
(1)shm_writer.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define SHM_SIZE 1024 // 共享內存大小
#define SHM_KEY 0x123456 // 共享內存鍵值
int main() {
int shmid;
char *shmaddr;
// 1. 創建共享內存
shmid = shmget(SHM_KEY, SHM_SIZE, 0666 | IPC_CREAT);
if (shmid == -1) {
perror("創建共享內存失敗");
exit(EXIT_FAILURE);
}
// 2. 將共享內存附加到當前進程地址空間
shmaddr = (char*)shmat(shmid, NULL, 0);
if (shmaddr == (char*)-1) {
perror("共享內存附加失敗");
exit(EXIT_FAILURE);
}
// 3. 向共享內存寫入數據
const char *message = "Hello from shared memory writer!";
strncpy(shmaddr, message, SHM_SIZE - 1);
printf("已向共享內存寫入: %s\n", message);
// 等待讀進程讀取數據
printf("等待讀進程讀取數據...\n");
sleep(5); // 給讀進程足夠時間讀取
// 4. 從當前進程地址空間分離共享內存
if (shmdt(shmaddr) == -1) {
perror("共享內存分離失敗");
exit(EXIT_FAILURE);
}
// 5. 刪除共享內存
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("刪除共享內存失敗");
exit(EXIT_FAILURE);
}
printf("共享內存已清理\n");
return 0;
}(2)shm_reader.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define SHM_SIZE 1024 // 共享內存大小,需與寫進程一致
#define SHM_KEY 0x123456 // 共享內存鍵值,需與寫進程一致
int main() {
int shmid;
char *shmaddr;
// 1. 獲取共享內存(由寫進程創建)
shmid = shmget(SHM_KEY, SHM_SIZE, 0666);
if (shmid == -1) {
perror("獲取共享內存失敗,請先啟動寫進程");
exit(EXIT_FAILURE);
}
// 2. 將共享內存附加到當前進程地址空間
shmaddr = (char*)shmat(shmid, NULL, 0);
if (shmaddr == (char*)-1) {
perror("共享內存附加失敗");
exit(EXIT_FAILURE);
}
// 3. 從共享內存讀取數據并打印
printf("從共享內存讀取到: %s\n", shmaddr);
// 4. 從當前進程地址空間分離共享內存
if (shmdt(shmaddr) == -1) {
perror("共享內存分離失敗");
exit(EXIT_FAILURE);
}
printf("讀進程完成\n");
return 0;
}(3)編譯與運行
# 編譯寫進程
gcc shm_writer.c -o shm_writer
# 編譯讀進程
gcc shm_reader.c -o shm_reader運行步驟:
- 先啟動寫進程:./shm_writer
- 再啟動讀進程(在另一個終端):./shm_reader
運行后輸出:
# 寫進程
已向共享內存寫入: Hello from shared memory writer!
等待讀進程讀取數據...
共享內存已清理
# 讀進程
從共享內存讀取到: Hello from shared memory writer!
讀進程完成共享內存是所有 IPC 機制中速度最快的,因為數據不需要在進程間復制,而是直接訪問同一塊物理內存。但需要注意,共享內存本身不提供同步機制,在復雜場景中需要結合信號量等機制來避免競態條件。
4.4 信號通信案例
案例描述:一個進程可以向另一個進程發送自定義信號,接收進程根據接收到的信號執行相應的操作。例如,進程 A 向進程 B 發送 SIGUSR1 信號,進程 B 接收到信號后打印一條消息。
(1)signal_sender.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "用法:%s <接收進程PID>\n", argv[0]);
exit(EXIT_FAILURE);
}
// 將命令行參數轉換為接收進程的PID
pid_t target_pid = atoi(argv[1]);
if (target_pid <= 0) {
fprintf(stderr, "無效的PID:%s\n", argv[1]);
exit(EXIT_FAILURE);
}
printf("發送進程啟動,準備向PID %d 發送信號\n", target_pid);
// 發送SIGUSR1信號
if (kill(target_pid, SIGUSR1) == -1) {
perror("發送SIGUSR1失敗");
exit(EXIT_FAILURE);
}
printf("已發送SIGUSR1信號\n");
// 等待1秒,讓接收進程有時間處理
sleep(1);
// 發送SIGUSR2信號(退出信號)
if (kill(target_pid, SIGUSR2) == -1) {
perror("發送SIGUSR2失敗");
exit(EXIT_FAILURE);
}
printf("已發送SIGUSR2信號\n");
return 0;
}(2)signal_receiver.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
// 信號處理函數:處理SIGUSR1
void handle_sigusr1(int signum) {
printf("接收到SIGUSR1信號(信號編號:%d)\n", signum);
printf("執行操作A:打印當前進程ID - %d\n", getpid());
}
// 信號處理函數:處理SIGUSR2
void handle_sigusr2(int signum) {
printf("接收到SIGUSR2信號(信號編號:%d)\n", signum);
printf("執行操作B:準備退出...\n");
exit(EXIT_SUCCESS);
}
int main() {
// 注冊信號處理函數
if (signal(SIGUSR1, handle_sigusr1) == SIG_ERR) {
perror("無法設置SIGUSR1處理函數");
exit(EXIT_FAILURE);
}
if (signal(SIGUSR2, handle_sigusr2) == SIG_ERR) {
perror("無法設置SIGUSR2處理函數");
exit(EXIT_FAILURE);
}
printf("接收進程啟動,PID:%d\n", getpid());
printf("等待接收SIGUSR1或SIGUSR2信號...\n");
printf("提示:使用命令 kill -USR1 %d 發送SIGUSR1信號\n", getpid());
printf("提示:使用命令 kill -USR2 %d 發送SIGUSR2信號\n", getpid());
// 進入無限循環等待信號
while (1) {
pause(); // 暫停進程,等待信號
}
return 0;
}(3)編譯與運行
# 編譯接收進程
gcc signal_receiver.c -o signal_receiver
# 編譯發送進程
gcc signal_sender.c -o signal_sender運行步驟:
- 先啟動接收進程:./signal_receiver
- 記錄接收進程顯示的 PID(例如 12345)
- 打開新終端,運行發送進程:./signal_sender 12345(替換為實際 PID)
運行后輸出:
# 接收進程
接收進程啟動,PID:12345
等待接收SIGUSR1或SIGUSR2信號...
提示:使用命令 kill -USR1 12345 發送SIGUSR1信號
提示:使用命令 kill -USR2 12345 發送SIGUSR2信號
接收到SIGUSR1信號(信號編號:10)
執行操作A:打印當前進程ID - 12345
接收到SIGUSR2信號(信號編號:12)
執行操作B:準備退出...
# 發送進程
發送進程啟動,準備向PID 12345 發送信號
已發送SIGUSR1信號
已發送SIGUSR2信號這種通過自定義信號實現的 IPC 方式適合簡單的事件通知場景,優點是實現簡單、響應迅速,但不適合傳遞復雜數據,通常用于觸發特定操作(如刷新配置、重新加載數據、優雅退出等)
4.5 套接字通信案例
案例描述:創建一個簡單的服務器進程和客戶端進程,服務器監聽指定端口,客戶端連接服務器后發送消息,服務器接收消息并回復。
(1)服務器端server.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
const char *response = "服務器已收到消息";
// 1. 創建套接字文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket創建失敗");
exit(EXIT_FAILURE);
}
// 2. 設置套接字選項,允許重用端口和地址
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt失敗");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // 監聽所有網絡接口
address.sin_port = htons(PORT); // 設置端口
// 3. 綁定套接字到指定端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("綁定失敗");
exit(EXIT_FAILURE);
}
// 4. 開始監聽,最大等待連接數為5
if (listen(server_fd, 5) < 0) {
perror("監聽失敗");
exit(EXIT_FAILURE);
}
printf("服務器啟動,監聽端口 %d...\n", PORT);
// 5. 接受客戶端連接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("接受連接失敗");
exit(EXIT_FAILURE);
}
// 6. 讀取客戶端消息
ssize_t valread = read(new_socket, buffer, BUFFER_SIZE);
if (valread < 0) {
perror("讀取消息失敗");
exit(EXIT_FAILURE);
}
printf("收到客戶端消息: %s\n", buffer);
// 7. 向客戶端發送回復
send(new_socket, response, strlen(response), 0);
printf("已向客戶端發送回復\n");
// 8. 關閉連接
close(new_socket);
close(server_fd);
printf("服務器已關閉\n");
return 0;
}(2)客戶端client.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
#define SERVER_IP "127.0.0.1" // 服務器IP地址,本地測試用環回地址
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
const char *message = "你好,服務器!";
// 1. 創建套接字
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("套接字創建失敗");
exit(EXIT_FAILURE);
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// 2. 轉換IP地址并設置服務器地址結構
if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
perror("無效的IP地址/地址不支持");
exit(EXIT_FAILURE);
}
// 3. 連接服務器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("連接失敗,請檢查服務器是否啟動");
exit(EXIT_FAILURE);
}
// 4. 向服務器發送消息
send(sock, message, strlen(message), 0);
printf("已向服務器發送消息: %s\n", message);
// 5. 接收服務器回復
ssize_t valread = read(sock, buffer, BUFFER_SIZE);
if (valread < 0) {
perror("讀取回復失敗");
exit(EXIT_FAILURE);
}
printf("收到服務器回復: %s\n", buffer);
// 6. 關閉連接
close(sock);
printf("客戶端已關閉\n");
return 0;
}(3)編譯與運行步驟
①編譯服務器和客戶端:
gcc server.c -o server
gcc client.c -o client②先啟動服務器
./server服務器會顯示:服務器啟動,監聽端口 8080...
③再啟動客戶端(新終端)
./client④運行結果
服務器輸出:
服務器啟動,監聽端口 8080...
收到客戶端消息: 你好,服務器!
已向客戶端發送回復
服務器已關閉客戶端輸出:
已向服務器發送消息: 你好,服務器!
收到服務器回復: 服務器已收到消息
客戶端已關閉這個示例實現了基本的 TCP 通信流程,適用于需要可靠數據傳輸的網絡通信場景。如果需要處理多個客戶端連接,可以在服務器中添加多線程或多路復用機制進行擴展。



























