欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 健康 > 养生 > Linux进程间通信方式

Linux进程间通信方式

2025/3/22 5:45:07 来源:https://blog.csdn.net/m0_50499434/article/details/146398234  浏览:    关键词:Linux进程间通信方式

在现代操作系统中,进程是独立运行的基本单位,而进程间通信是确保多个进程协同工作的关键机制。由于不同进程运行在各自的虚拟地址空间中,彼此无法直接访问对方的数据,因此需要IPC机制来交换信息、同步状态,甚至共享内存。在Linux系统中,进程间通信方式多种多样,包括管道(Pipe)、消息队列(MessageQueue)、共享内存(SharedMemory)、信号量(Semaphore)、信号(Signal)、套接字(Socket)等,每种方式适用于不同的应用场景。

本文将从IPC的基本概念出发,介绍不同通信机制的原理、适用场景。我们将重点讨论进程间数据共享、同步控制、性能对比等核心问题,帮助读者深入理解IPC机制在Linux内核中的实现和优化策略。

1.管道

在Linux中,管道是一种经典的进程间通信(IPC)机制,它通过管道符|将多个命令连接在一起,形成一个数据流动的通道。例如:

command1 | command2

这行代码创建了一个管道,它的功能是将前一个命令(command1)的输出作为后一个命令(command2)的输入。从功能描述中可以看出,管道中的数据只能单向流动,即半双工通信。如果想要实现双向通信(即全双工通信),则需要创建两个管道。

在Linux命令行中,管道非常常见。例如:

ps auxf | grep mysql

这行命令将psauxf的输出作为grepmysql的输入,从而筛选出与mysql相关的进程信息。

匿名管道

上述管道是通过管道符|创建的,称为匿名管道。匿名管道的特点如下:

  • 匿名性:没有名字,用完后会被自动销毁。
  • 使用范围:只能用于具有亲缘关系的进程之间,例如父子进程之间的通信。

匿名管道的创建

在Linux中,匿名管道是通过pipe函数创建的。pipe函数的定义如下:

int pipe(int fd[2]);
  • 该函数接受一个大小为2的文件描述符数组
    • fd[0]:指向管道的读端(用于读取数据)。
    • fd[1]:指向管道的写端(用于写入数据)。
    • 数据从fd[1]写入,从fd[0]读取。

匿名管道如何实现进程间的通信

  1. 父进程创建管道
    父进程调用pipe函数创建管道,得到两个文件描述符fd[0]fd[1],分别指向管道的读端和写端。
  2. 父进程创建子进程
    父进程调用fork函数创建子进程。子进程会继承父进程的文件描述符,因此子进程同样拥有指向同一管道的fd[0]fd[1]
  3. 关闭不必要的文件描述符
    为了实现单向通信,父进程关闭fd[0](读端),子进程关闭fd[1](写端)。这样,父进程可以通过fd[1]向管道写入数据,子进程可以通过fd[0]从管道读取数据。
  4. 数据流动
    管道本质上是基于环形队列的缓冲区,数据从写端流入,从读端流出。通过这种方式,父进程和子进程之间实现了单向通信。

其示意图如下:

在这里插入图片描述

命名管道

管道还有另外一个类型是命名管道,也被叫做FIFO,因为数据是先进先出的传输方式。在使用命名管道前,先需要通过mkfifo命令来创建,并且指定管道名字:

mkfifo myPipe

myPipe就是这个管道的名称,基于Linux一切皆文件的理念,所以管道也是以文件的方式存在,我们可以用ls看一下,这个文件的类型是p,也就是pipe的意思:

ls -l|grep myPipe
prw -rw-r--1zxyzxy03月2014:37 myPipe

接下来,我们往myPipe这个管道写入数据:

echo "helloworld" > myPipe

操作后,你会发现命令执行后就停在这了,这是因为管道里的内容没有被读取,只有当管道里的数据被读完后,命令才可以正常退出。

执行另外一个命令来读取这个管道里的数据:

cat <myPipe
hello world

可以看到,管道里的内容被读取出来了,并打印在了终端上,另外一方面,echo那个命令也正常退出了。

我们可以看出,管道这种通信方式效率低,不适合进程间频繁地交换数据

管道的本质

对于管道两端的进程而言,管道就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是单独构成一种文件系统,并且只存在于内存中.内核在内存中开辟了一个缓冲区,这个缓冲区与管道文件相关联,对管道文件的操作,被内核转换成对这块缓冲区的操作。

2.消息队列

由上述可知,管道由于采用无格式的字节流传输方式,每次通信都需要进行进程同步,而且数据必须按顺序读取,无法实现消息的随机访问,这导致在高频通信场景下效率较低。因此,在需要更高效的数据交换方式时,**消息队列成为了一种更优的选择。

在这里插入图片描述

工作机制

消息队列是由内核管理的消息链表,进程间可以通过发送和接收消息体来实现通信。例如,当进程A需要向进程B传输数据时,它只需将消息写入消息队列,操作完成后即可继续执行,而不必等待B立即处理;同样,B进程在需要时可随时从队列中读取消息体。这种方式类似于邮件通信——A发送消息后不必等待B立即回复,而B可以在需要时查询自己的邮件箱

消息队列vs.管道

与匿名管道不同,消息队列的生命周期不受进程存活状态影响。即使创建消息队列的进程终止,该队列仍然存在,除非显式删除或系统重启。而匿名管道则与创建它的进程绑定,进程退出时,管道随之销毁。此外,消息队列提供结构化的数据存储,每个消息以固定大小的消息体进行存储和传输,而管道则是无格式的字节流,不具备消息边界的概念。

消息队列的局限性

尽管消息队列提升了进程间数据交换的灵活性和效率,但仍然存在一些限制:

  1. 通信延迟
    • 由于消息队列基于存储-转发模式,数据不会实时送达,可能会有短暂的延迟,这一点与邮件通信相似。
  2. 消息大小限制
    • 每个消息体的大小受Linux内核参数MSGMAX限制,而整个队列的存储总量受MSGMNB约束,因此不适合传输大数据
  3. 用户态与内核态的数据拷贝
    • 由于消息队列存储在内核中,进程在写入数据时,需要先将数据从用户态拷贝到内核态,读取时又需从内核态拷贝回用户态。这种双重拷贝造成额外的CPU和内存开销,使其在高吞吐、大数据传输场景下性能受限

3.共享内存

在进程间通信(IPC)机制中,消息队列虽然能提高数据传输的灵活性,但其通信过程中涉及用户态与内核态之间的数据拷贝,导致额外的CPU开销。而**共享内存通过让多个进程映射同一块物理内存,避免了数据在进程间反复拷贝的开销,从而大幅提高通信效率。

在这里插入图片描述

共享内存的工作机制

现代操作系统采用虚拟内存管理,每个进程都有独立的虚拟地址空间,其虚拟地址映射到不同的物理内存区域。因此,即便进程A和进程B的某个虚拟地址相同,它们访问的物理地址通常是不同的,彼此的数据互不影响。

共享内存的核心思想是让多个进程将某个虚拟地址空间映射到同一块物理内存,从而共享数据。例如:

  • 进程A在共享内存区域写入数据。
  • 进程B立即可以在相同地址读取该数据,无需额外的数据拷贝。

这种方式避免了频繁的系统调用和数据拷贝,极大提高了进程间通信的速度,使其成为最快的IPC机制之一

共享内存的优点

  • 高效:数据直接存取,不需要在用户态和内核态之间拷贝。

  • 持久性:共享内存在创建后,即便创建它的进程退出,内存仍然存在,直到显式删除或系统重启。

  • 减少系统调用:除了创建和销毁时涉及系统调用,进程之间的读写操作完全是用户态操作,速度接近普通内存访问。

共享内存的实现

在 Linux 中,System V 共享内存提供了 四个主要的 API 供进程使用:创建共享内存、删除共享内存、关联共享内存、解除共享内存。

1. 创建共享内存

创建共享内存的系统调用是 shmget()

int shmget(key_t key, size_t size, int shmflg);
  • key:共享内存的唯一标识符,通常使用 ftok() 生成唯一 key
  • size:共享内存的大小(以字节为单位)。
  • shmflg:权限标志,如 IPC_CREAT 表示创建新共享内存。
2. 关联共享内存

创建共享内存后,进程需要将其 映射到自己的地址空间,这通过 shmat() 实现:

void *shmat(int shmid, const void *shmaddr, int shmflg);
  • shmid:共享内存 ID。
  • shmaddr:指定映射到进程地址空间的某个地址,通常设为 NULL,让系统自动分配。
  • shmflg:访问权限,如 0 表示默认读写。
3. 解除共享内存

当进程不再需要共享内存时,可以调用 shmdt() 解除映射:

int shmdt(const void *shmaddr);
  • shmaddr:共享内存地址,即 shmat() 返回的地址。
4. 删除共享内存

由于 System V 共享内存的生命周期与内核相关,进程退出后,内存仍然存在,只有在系统重启或显式删除后才会释放。可以使用 shmctl() 删除:

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • shmid:共享内存 ID。
  • cmdIPC_RMID 表示删除共享内存。

共享内存的局限性

缺乏访问控制:共享内存是进程直接访问的,没有内置同步机制,多个进程可能发生数据竞争,需要额外的信号量或互斥锁进行同步。

生命周期长:除非显式删除或重启系统,否则共享内存会一直驻留在内核中,占用系统资源。

复杂性:进程需要手动管理共享内存,相比消息队列,使用难度更高。

4.信号量和PV操作

在Linux同步机制中介绍。

5.信号

在 Linux 进程间通信(IPC)机制中,信号是唯一一种异步方式的通信机制,它允许在任何时刻发送信号给某个进程,以通知进程某个异步事件的发生,从而触发特定的信号处理程序。信号由用户、内核和进程触发,具有非阻塞、低延迟的特点,因此被广泛用于进程控制、异常处理和事件通知。

信号的基本原理

信号本质上是软件中断机制,当某个事件触发后,操作系统会向目标进程发送一个信号,进程根据该信号的类型决定如何响应:

  • 执行默认操作(如终止、忽略)
  • 捕获并处理(执行自定义信号处理函数)
  • 忽略信号(部分信号无法忽略)

当进程收到信号后,系统会暂停当前执行,转去执行该信号的处理函数。信号处理完毕后,进程恢复之前的执行状态。

Linux中的信号

kill -l1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP6) 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

​ Linux中系统一共支持64种信号,其中1到31号信号为普通信号(也程为不可靠信号),34到64为实时信号(可靠信号)。不可靠主要是不支持信号队列,就是当多个信号发生在进程中的时候(收到信号的速度超过进程处理的速度的时候),这些没来的及处理的信号就会被丢掉,仅仅留下一个信号。可靠信号是多个信号发送到进程的时候(收到信号的速度超过进程处理信号的速度的时候),这些没来的及处理的信号就会排入进程的队列。等进程有机会来处理的时候,依次再处理,信号不丢失。

信号来源

信号可以由不同的 事件 触发,包括:

  1. 硬件异常:CPU 发生 除 0 错误、无效内存访问 时,操作系统向进程发送 SIGFPE、SIGSEGV 等信号。
  2. 用户操作:用户在终端输入 Ctrl + C(发送 SIGINT 信号)、Ctrl + Z(发送 SIGTSTP 信号)等操作。
  3. 软件触发:进程可以调用 kill()raise()sigqueue() 向自己或其他进程发送信号。
  4. 内核事件:定时器超时(SIGALRM)、子进程退出(SIGCHLD)等情况。

信号处理方式

进程收到信号后,可以采取以下处理方式:

  1. 执行默认操作:每种信号都有默认处理方式,如 终止进程、忽略信号、产生核心转储 等。
  2. 捕获信号:自定义信号处理函数,当特定信号到达时,执行用户定义的操作。
  3. 忽略信号:让进程忽略特定信号(SIGKILLSIGSTOP 例外,无法被忽略)。

信号的安装

在 Linux 下,安装信号处理程序可以使用 signal()sigaction(),其中:

  • signal() 适用于简单的信号处理,但功能有限,不支持实时信号。
  • sigaction() 提供更强的控制能力,可屏蔽信号、设置信号处理标志等。
1. 使用 signal() 处理信号
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>void signal_handler(int signum) {printf("Received signal %d\n", signum);
}int main() {signal(SIGINT, signal_handler);  // 捕获 Ctrl + C(SIGINT)while (1) {printf("Running...\n");sleep(2);}return 0;
}
2.使用 sigaction() 处理信号

相比 signal()sigaction() 更强大,可以控制信号的行为,并支持 实时信号

#include <stdio.h>
#include <signal.h>
#include <unistd.h>void signal_handler(int signum, siginfo_t *info, void *context) {printf("Received signal %d from PID %d\n", signum, info->si_pid);
}int main() {struct sigaction sa;sa.sa_flags = SA_SIGINFO;  // 使用扩展信息sa.sa_sigaction = signal_handler;sigaction(SIGUSR1, &sa, NULL);  // 处理 SIGUSR1 信号printf("Process PID: %d, waiting for signals...\n", getpid());while (1) pause();  // 持续等待信号
}

6.Socket

在前面的讨论中,我们介绍了 管道、消息队列、共享内存、信号量和信号 等进程间通信(IPC)方式,这些方法都仅限于同一台主机上的进程进行通信。而当进程需要在 不同主机之间通信,甚至跨 不同网络环境 进行数据交换时,就需要使用 Socket(套接字)

实际上,Socket 不仅支持跨网络通信,也可以用于本机进程间通信(IPC),其适用范围更加广泛,因此被广泛应用于 分布式系统、客户端-服务器(C/S)架构、微服务通信等场景

1. Socket 的基本概念

Socket 是 进程间通信的端点,用于在 不同进程之间建立数据连接,无论这些进程是运行在 同一台机器 还是 不同主机 上。Socket 的本质是一个文件描述符,进程可以通过读写这个描述符来发送或接收数据。

2. Socket 的创建

在 Linux 中,可以使用 socket() 系统调用创建一个套接字:

int socket(int domain, int type, int protocal)
  • domain:指定通信的 协议类型,常见的选项:AF_INET使用 IPv4 进行网络通信。AF_INET6:使用 IPv6 进行网络通信。AF_UNIXAF_LOCAL:用于本机进程间通信(IPC)。

  • type:指定 传输模式SOCK_STREAM面向连接,基于 TCP 协议,保证数据完整性和可靠性。SOCK_DGRAM无连接,基于 UDP 协议,适用于低延迟数据传输。

  • protocol:通常设为 0,让系统自动匹配。

示例:创建一个 TCP 套接字

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {perror("socket creation failed");exit(EXIT_FAILURE);
}

3.基于 TCP 的 Socket 通信

TCP是一种 可靠的、基于连接的传输协议.

服务器端流程
  1. 创建套接字 (socket())
  2. 绑定 IP 和端口 (bind())
  3. 监听连接请求 (listen())
  4. 接受客户端连接 (accept())
  5. 数据收发 (send() / recv())
  6. 关闭套接字 (close())
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>int main() {int server_fd, new_socket;struct sockaddr_in address;int addrlen = sizeof(address);char buffer[1024] = {0};// 1. 创建 TCP 套接字server_fd = socket(AF_INET, SOCK_STREAM, 0);if (server_fd == -1) {perror("Socket failed");exit(EXIT_FAILURE);}// 2. 绑定 IP 和端口address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(8080);bind(server_fd, (struct sockaddr *)&address, sizeof(address));// 3. 监听端口,最大等待队列为 5listen(server_fd, 5);printf("Server listening on port 8080...\n");// 4. 等待客户端连接new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);if (new_socket == -1) {perror("Accept failed");exit(EXIT_FAILURE);}// 5. 接收数据recv(new_socket, buffer, sizeof(buffer), 0);printf("Received: %s\n", buffer);// 6. 发送响应send(new_socket, "Hello from Server", strlen("Hello from Server"), 0);// 7. 关闭套接字close(new_socket);close(server_fd);return 0;
}
客户端流程
  1. 创建套接字 (socket())
  2. 连接服务器 (connect())
  3. 数据收发 (send() / recv())
  4. 关闭套接字 (close())
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>int main() {int sock;struct sockaddr_in server_address;char buffer[1024] = {0};// 1. 创建 TCP 套接字sock = socket(AF_INET, SOCK_STREAM, 0);if (sock == -1) {perror("Socket failed");exit(EXIT_FAILURE);}// 2. 设置服务器地址server_address.sin_family = AF_INET;server_address.sin_port = htons(8080);inet_pton(AF_INET, "127.0.0.1", &server_address.sin_addr);// 3. 连接服务器connect(sock, (struct sockaddr *)&server_address, sizeof(server_address));// 4. 发送数据send(sock, "Hello from Client", strlen("Hello from Client"), 0);// 5. 接收服务器响应recv(sock, buffer, sizeof(buffer), 0);printf("Server response: %s\n", buffer);// 6. 关闭套接字close(sock);return 0;
}

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词