扒开 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; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.

在这段代码中,首先调用 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; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.

②客户端代码(读进程):

复制
#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; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.

在这个示例中,服务器端首先使用 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()1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.

在这段代码中,首先定义了一个 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("写进程完成")1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.

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);}1.

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; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.

在这段代码中,首先使用 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()1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.

客户端代码:

复制
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()1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.

编译与运行步骤(Linux 环境)

①编译代码:打开终端,分别编译服务端和客户端。

复制
# 编译服务端 gcc unix_socket_server.c -o socket_server # 编译客户端 gcc unix_socket_client.c -o socket_client1.2.3.4.5.

②启动服务端:在第一个终端运行。

复制
./socket_server1.

服务端会显示:服务端已启动,监听路径:/tmp/linux_ipc_socket。

③启动客户端:打开第二个终端运行。

复制
./socket_client1.

客户端会显示连接成功信息,等待用户输入消息。

④通信测试:在客户端输入任意消息并回车,服务端会显示收到的消息并返回响应。输入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); } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.

通过管道实现了父子进程间的单向数据传输,父进程写入的数据被子进程成功读取,管道在这种亲缘关系进程间通信简单高效,但要注意及时关闭不需要的管道端,避免资源浪费和潜在的阻塞问题。

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; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.

(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; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.

(3)编译与运行

复制
# 编译发送进程 gcc msg_sender.c -o msg_sender # 编译接收进程 gcc msg_receiver.c -o msg_receiver1.2.3.4.5.

运行步骤:

先启动接收进程:./msg_receiver再启动发送进程:./msg_sender

运行后输出:

复制
# 接收进程 等待接收消息... 收到消息: Hello from sender process! 收到消息: exit 收到结束消息,退出接收进程 # 发送进程 消息已发送: Hello from sender process! 消息队列已删除1.2.3.4.5.6.7.8.9.

消息队列相比管道的优势是允许非亲缘关系进程通信,且消息可以按类型接收,适合需要异步通信和消息分类的场景。

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; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.

(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; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.

(3)编译与运行

复制
# 编译写进程 gcc shm_writer.c -o shm_writer # 编译读进程 gcc shm_reader.c -o shm_reader1.2.3.4.5.

运行步骤:

先启动写进程:./shm_writer再启动读进程(在另一个终端):./shm_reader

运行后输出:

复制
# 写进程 已向共享内存写入: Hello from shared memory writer! 等待读进程读取数据... 共享内存已清理 # 读进程 从共享内存读取到: Hello from shared memory writer! 读进程完成1.2.3.4.5.6.7.8.

共享内存是所有 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; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.

(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; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.

(3)编译与运行

复制
# 编译接收进程 gcc signal_receiver.c -o signal_receiver # 编译发送进程 gcc signal_sender.c -o signal_sender1.2.3.4.5.

运行步骤:

先启动接收进程:./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信号1.2.3.4.5.6.7.8.9.10.11.12.13.14.

这种通过自定义信号实现的 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; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.

(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; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.

(3)编译与运行步骤

①编译服务器和客户端:

复制
gcc server.c -o server gcc client.c -o client1.2.

②先启动服务器

复制
./server1.

服务器会显示:服务器启动,监听端口 8080...

③再启动客户端(新终端)

复制
./client1.

④运行结果

服务器输出:

复制
服务器启动,监听端口 8080... 收到客户端消息: 你好,服务器! 已向客户端发送回复 服务器已关闭1.2.3.4.

客户端输出:

复制
已向服务器发送消息: 你好,服务器! 收到服务器回复: 服务器已收到消息 客户端已关闭1.2.3.

这个示例实现了基本的 TCP 通信流程,适用于需要可靠数据传输的网络通信场景。如果需要处理多个客户端连接,可以在服务器中添加多线程或多路复用机制进行扩展。

THE END