进程间通信介绍
Linux进程间通信详解
[1. 进程间通信介绍](#1. 进程间通信介绍)
[1.1 进程间通信的概念](#1.1 进程间通信的概念)
[1.2 进程间通信的目的](#1.2 进程间通信的目的)
[1.3 进程间通信的本质](#1.3 进程间通信的本质)
[1.4 进程间通信的分类](#1.4 进程间通信的分类)
[2. 管道](#2. 管道)
[2.1 什么是管道](#2.1 什么是管道)
[2.2 匿名管道](#2.2 匿名管道)
[2.2.1 匿名管道的原理](#2.2.1 匿名管道的原理)
[2.2.2 pipe函数](#2.2.2 pipe函数)
[2.2.3 匿名管道使用步骤](#2.2.3 匿名管道使用步骤)
[2.2.4 管道读写规则](#2.2.4 管道读写规则)
[2.2.5 管道的特点](#2.2.5 管道的特点)
[2.2.6 管道的四种特殊情况](#2.2.6 管道的四种特殊情况)
[2.2.7 管道的大小](#2.2.7 管道的大小)
[2.3 命名管道](#2.3 命名管道)
[2.3.1 命名管道的原理](#2.3.1 命名管道的原理)
[2.3.2 使用命令创建命名管道](#2.3.2 使用命令创建命名管道)
[2.3.3 创建一个命名管道](#2.3.3 创建一个命名管道)
[2.3.4 命名管道的打开规则](#2.3.4 命名管道的打开规则)
[2.3.5 用命名管道实现serve&client通信](#2.3.5 用命名管道实现serve&client通信)
[2.3.6 用命名管道实现派发计算任务](#2.3.6 用命名管道实现派发计算任务)
[2.3.7 用命名管道实现进程遥控](#2.3.7 用命名管道实现进程遥控)
[2.3.8 用命名管道实现文件拷贝](#2.3.8 用命名管道实现文件拷贝)
[2.3.9 命名管道和匿名管道的区别](#2.3.9 命名管道和匿名管道的区别)
[2.4 命令行当中的管道](#2.4 命令行当中的管道)
[3. System V进程间通信](#3. System V进程间通信)
[3.1 System V共享内存](#3.1 System V共享内存)
[3.1.1 共享内存的基本原理](#3.1.1 共享内存的基本原理)
[3.1.2 共享内存数据结构](#3.1.2 共享内存数据结构)
[3.1.3 共享内存的建立与释放](#3.1.3 共享内存的建立与释放)
[3.1.4 共享内存的创建](#3.1.4 共享内存的创建)
[3.1.5 共享内存的释放](#3.1.5 共享内存的释放)
[3.1.6 共享内存的关联](#3.1.6 共享内存的关联)
[3.1.7 共享内存的去关联](#3.1.7 共享内存的去关联)
[3.1.8 用共享内存实现serve&client通信](#3.1.8 用共享内存实现serve&client通信)
[3.1.9 共享内存与管道进行对比](#3.1.9 共享内存与管道进行对比)
[3.2 System V消息队列](#3.2 System V消息队列)
[3.2.1 消息队列的基本原理](#3.2.1 消息队列的基本原理)
[3.2.2 消息队列数据结构](#3.2.2 消息队列数据结构)
[3.2.3 消息队列的创建](#3.2.3 消息队列的创建)
[3.2.4 消息队列的释放](#3.2.4 消息队列的释放)
[3.2.5 向消息队列发送数据](#3.2.5 向消息队列发送数据)
[3.2.6 从消息队列获取数据](#3.2.6 从消息队列获取数据)
[3.3 System V信号量](#3.3 System V信号量)
[3.3.1 信号量相关概念](#3.3.1 信号量相关概念)
[3.3.2 信号量数据结构](#3.3.2 信号量数据结构)
[3.3.3 信号量相关函数](#3.3.3 信号量相关函数)
[3.3.4 进程互斥](#3.3.4 进程互斥)
[3.4 System V IPC联系](#3.4 System V IPC联系)
进程间通信的概念
进程间通信(Inter-Process Communication,简称 IPC)是指在不同进程之间交换或共享信息的一种机制。由于每个进程在操作系统中都是独立运行的,它们各自拥有独立的地址空间,因此进程之间默认是相互隔离的。然而,在许多应用场景中,进程之间需要进行数据交换、资源共享或者协调执行,这就要求进程之间能够相互沟通,而这正是进程间通信的核心所在。
进程间通信的核心在于"交换信息"。例如,一个进程可能需要将自己的数据发送给另一个进程,或者多个进程需要共享某些资源(如打印机、文件等),又或者某个进程需要通知其他进程某件事情已经完成。为了满足这些需求,操作系统提供了多种进程间通信的方式,包括管道、共享内存、消息队列、信号量等。
从本质上讲,进程间通信的本质就是让不同的进程看到同一份资源。由于进程之间的数据是相互隔离的,操作系统必须提供一种机制,使得多个进程能够访问同一块内存区域、文件或者其他数据结构,从而实现信息的交换。这些共享的资源可以是操作系统提供的特殊内存区域,也可以是文件系统中的某个文件,甚至可以是网络上的某个端口。
总的来说,进程间通信的核心目标是实现进程之间的数据传输、资源共享、事件通知以及进程控制。不同的进程间通信方式适用于不同的场景,理解这些方式的原理和特点,有助于我们更高效地编写多进程程序。
进程间通信的目的
进程间通信的主要目的是为了实现不同进程之间的数据交换和协同工作,具体而言,进程间通信的目的可以归结为以下四个方面:
1. 数据传输
一个进程需要将它的数据发送给另一个进程。例如,一个进程负责读取文件,另一个进程负责处理文件内容,这就需要进程之间能够传输数据。
2. 资源共享
多个进程之间需要共享相同的资源。例如,多个进程可能需要同时访问同一个文件、数据库或者硬件设备(如打印机)。通过进程间通信,可以协调对这些资源的访问,避免冲突。
3. 通知事件
一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件。例如,当某个进程完成任务后,它可能需要通知其他进程继续执行后续操作。
4. 进程控制
某些进程希望完全控制另一个进程的执行。例如,在调试过程中,调试器需要拦截目标进程的所有异常和陷阱,并能够实时监控其状态变化。
这些目的的实现依赖于操作系统提供的各种进程间通信机制,如管道、共享内存、消息队列、信号量等。每种机制都有其适用场景,开发者需要根据具体需求选择合适的通信方式,以确保进程之间能够高效、安全地交换信息。
进程间通信的本质
进程间通信的本质在于,让不同的进程看到同一份资源。由于进程的独立性,操作系统默认情况下会为每个进程分配独立的地址空间,这意味着不同进程之间的数据是相互隔离的。然而,如果多个进程需要交换信息或共享资源,就必须找到一种方式,使得它们能够访问同一份数据。
为了实现这一目标,操作系统通常会提供一些共享的资源,如内存区域、文件缓冲区、网络连接等。这些资源可以被多个进程访问,从而实现进程间的通信。例如,共享内存机制允许多个进程映射到同一块物理内存区域,这样它们就可以直接读写这块内存中的数据,而无需经过额外的复制操作。同样,管道机制利用内核提供的缓冲区作为共享资源,使得父子进程之间可以通过读写管道来交换数据。
从更宏观的角度来看,进程间通信的实现依赖于操作系统提供的基础设施。无论是共享内存、管道还是消息队列,它们的核心思想都是通过某种方式,使得多个进程能够访问同一份资源。这种资源可以是内存中的某个区域,也可以是文件系统中的某个特殊文件(如命名管道)。一旦多个进程能够访问同一份资源,它们就可以通过读写该资源来实现数据交换和同步操作。
因此,进程间通信的本质就是通过操作系统提供的共享资源,让不同的进程能够看到并操作同一份数据,从而实现信息的交换和进程之间的协调。
进程间通信的分类
Linux 系统提供了多种进程间通信(IPC)的方式,每种方式都有其适用场景和特点。根据通信机制的不同,Linux 中的进程间通信主要可以分为以下三类:
1. 管道(Pipe)
管道是一种最基础的进程间通信方式,主要用于具有亲缘关系的进程之间(如父子进程)进行数据传输。管道分为两种类型:
- 匿名管道(Anonymous Pipe):仅限于父子进程或兄弟进程之间使用,生命周期随进程结束而终止。
- 命名管道(Named Pipe/FIFO):允许任意两个进程之间进行通信,即使它们没有亲缘关系。命名管道在文件系统中有对应的文件名,但数据仍然存储在内存中,不会写入磁盘。
2. System V IPC
System V IPC 是一组较早的进程间通信机制,主要包括以下三种方式:
- 共享内存(Shared Memory):允许多个进程共享同一块物理内存区域,是最快的进程间通信方式,但需要额外的同步机制(如信号量)来保证数据一致性。
- 消息队列(Message Queue):进程可以通过消息队列发送和接收数据块,每个数据块带有类型信息,接收者可以根据类型选择接收特定的数据。
- 信号量(Semaphore):用于进程间的同步和互斥,主要用于控制对共享资源的访问,防止多个进程同时修改共享数据。
3. POSIX IPC
POSIX IPC 是对 System V IPC 的改进,提供了更加灵活和高效的进程间通信方式,主要包括:
- 消息队列(Message Queue)
- 共享内存(Shared Memory)
- 信号量(Semaphore)
- 互斥量(Mutex)
- 条件变量(Condition Variable)
- 读写锁(Read-Write Lock)
POSIX IPC 提供了更现代的 API,并且支持跨线程和跨进程的同步,相比于 System V IPC 更加灵活和易于使用。
什么是管道
管道是 Unix 系统中最古老的进程间通信方式之一,它的核心思想是通过一个数据流连接两个进程,使得一个进程的输出可以直接作为另一个进程的输入。在 Linux 系统中,管道被广泛用于进程间的数据传输,尤其是在命令行环境中,管道经常被用来组合多个命令,实现数据的连续处理。
例如,考虑一个简单的场景:统计当前登录到系统的用户数量。我们可以使用 who
命令列出所有登录用户,然后通过管道将输出传递给 wc -l
命令来统计行数。整个命令如下:
bash
who | wc -l
在这个例子中,who
命令的输出并不会直接显示在终端上,而是通过管道传递给 wc -l
命令进行处理。管道在这里充当了一个中间媒介,使得两个进程之间能够无缝地交换数据。
管道的工作原理可以类比为一根"数据管道",一端连接写入进程,另一端连接读取进程。写入进程将数据写入管道,而读取进程则从管道中读取数据。由于管道的这种特性,它非常适合用于需要连续处理数据的场景。
管道虽然简单,但它却是进程间通信的基础之一。Linux 提供了两种类型的管道:匿名管道和命名管道。匿名管道主要用于父子进程之间的通信,而命名管道则允许任意两个进程之间进行通信。这两种管道的具体使用方式和适用场景将在后续章节中详细介绍。
匿名管道的原理
匿名管道是一种专门用于本地父子进程之间通信的机制。它的核心原理在于,让两个父子进程共享同一份被打开的文件资源,从而实现数据的传输。
管道的创建与共享
当一个进程调用 pipe
函数创建匿名管道时,操作系统会在内核中创建一个临时的缓冲区,并返回两个文件描述符:一个用于读取数据(读端),另一个用于写入数据(写端)。这两个文件描述符分别对应管道的两端,读端用于从管道中读取数据,写端用于向管道中写入数据。
在创建管道后,父进程通常会调用 fork
函数创建子进程。由于子进程继承了父进程的文件描述符,因此父子进程都拥有指向该管道的读端和写端。为了让管道能够正常工作,通常的做法是让父进程关闭写端,子进程关闭读端,这样就形成了一条单向的数据传输通道:子进程向管道写入数据,父进程从管道读取数据。
内核如何维护管道
虽然管道的使用方式类似于文件操作,但它的实现并不涉及真正的磁盘文件。操作系统维护的管道缓冲区存在于内存中,而不是磁盘上。这意味着,当父子进程通过管道进行数据传输时,数据并不会被写入磁盘,从而避免了不必要的 I/O 操作,提高了通信效率。
此外,内核会自动管理管道的缓冲区。当写入的数据量超过管道的容量时,写操作会被阻塞,直到有进程从管道中读取数据。同样,当管道中没有数据可读时,读操作也会被阻塞,直到有进程写入数据。这种阻塞机制确保了管道的同步性,使得父子进程之间的数据传输更加可靠。
管道的生命周期
匿名管道的生命周期是有限的,它只存在于父子进程之间,并且当所有使用该管道的进程都退出后,管道会被自动释放。这意味着,匿名管道适用于需要短期通信的场景,而不适合用于长时间运行的进程之间的通信。
综上所述,匿名管道通过父子进程共享同一份内存缓冲区的方式,实现了高效的数据传输。它不需要磁盘 I/O,而是由内核在内存中维护数据缓冲区,并通过文件描述符的方式供进程访问。这种方式不仅降低了通信的开销,还确保了数据传输的同步性。
pipe
函数
在 Linux 系统中,pipe
函数用于创建匿名管道,其函数原型如下:
c
int pipe(int pipefd[2]);
pipe
函数的参数是一个输出型参数,即它会返回两个文件描述符,分别用于管道的读端和写端。pipefd
数组的两个元素具有以下含义:
数组元素 | 含义 |
---|---|
pipefd[0] |
管道的读端文件描述符 |
pipefd[1] |
管道的写端文件描述符 |
当 pipe
函数成功执行时,它会创建一个管道,并将两个文件描述符分别存储在 pipefd
数组中。此时,进程可以通过 pipefd[0]
读取管道中的数据,而通过 pipefd[1]
向管道写入数据。
如果 pipe
函数调用失败,则会返回 -1
,并设置相应的错误码。常见的错误包括:
EMFILE
:进程已经打开了太多的文件描述符,无法再创建新的文件描述符。ENFILE
:系统已经打开的文件数量达到了上限,无法再创建新的文件描述符。EFAULT
:pipefd
参数指向的地址无效,导致无法正确存储文件描述符。
pipe
函数的成功执行意味着一个匿名管道已经被创建,此时进程可以通过文件描述符对其进行读写操作。需要注意的是,匿名管道只能用于具有亲缘关系的进程之间的通信,通常是父子进程或兄弟进程之间。
在实际使用中,pipe
函数通常与 fork
函数配合使用,以实现父子进程之间的数据传输。在调用 fork
创建子进程之后,通常的做法是让父进程关闭写端,子进程关闭读端,从而形成单向的数据流,确保数据的正确传输。
匿名管道的使用步骤
匿名管道的使用需要结合 pipe
和 fork
函数,按照一定的顺序完成创建、父子进程的分工以及数据的读写操作。以下是匿名管道的典型使用步骤:
1. 调用 pipe
创建管道
首先,父进程调用 pipe
函数创建一个匿名管道。该函数会返回两个文件描述符:pipefd[0]
用于读取数据,pipefd[1]
用于写入数据。
c
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
2. 调用 fork
创建子进程
接下来,父进程调用 fork
函数创建子进程。由于子进程会继承父进程的文件描述符,因此父子进程都拥有指向管道的读端和写端。
c
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
3. 关闭无关的读写端
为了让管道实现单向通信,通常的做法是让父进程关闭写端,子进程关闭读端。这样,子进程可以向管道写入数据,而父进程可以从管道读取数据。
c
if (pid == 0) { // 子进程
close(pipefd[0]); // 关闭读端
// 子进程向管道写入数据
const char *message = "Hello, parent!";
write(pipefd[1], message, strlen(message) + 1);
close(pipefd[1]); // 写入完成后关闭写端
exit(EXIT_SUCCESS);
} else { // 父进程
close(pipefd[1]); // 关闭写端
// 父进程从管道读取数据
char buffer[128];
read(pipefd[0], buffer, sizeof(buffer));
printf("Received: %s\n", buffer);
close(pipefd[0]); // 读取完成后关闭读端
wait(NULL); // 等待子进程结束
}
4. 数据的读写操作
子进程通过 write
函数向管道的写端写入数据,而父进程通过 read
函数从管道的读端读取数据。由于管道自带同步机制,当管道中没有数据时,读操作会阻塞,直到有数据写入。同样,当管道已满时,写操作也会阻塞,直到有空间可用。
5. 关闭管道的两端
在数据读写完成后,父子进程都需要关闭各自使用的管道端口,以确保资源的正确释放。
通过以上步骤,匿名管道可以在父子进程之间实现简单的数据传输。这种机制虽然简单,但在许多需要父子进程协作的场景中非常实用。
管道读写规则
在使用管道进行进程间通信时,管道的读写操作遵循一定的规则。这些规则决定了进程在读写管道时的行为,包括阻塞、非阻塞模式下的处理方式,以及在特定条件下管道的状态变化。
1. 当没有数据可读时
如果当前管道中没有数据可供读取,进程的读操作可能会产生以下两种情况:
- 阻塞模式(O_NONBLOCK disable) :默认情况下,管道的读端处于阻塞模式。当进程尝试读取空管道时,
read
调用会阻塞,即进程会暂停执行,直到管道中有数据可供读取。 - 非阻塞模式(O_NONBLOCK enable) :如果管道的读端被设置为非阻塞模式,当进程尝试读取空管道时,
read
调用会立即返回-1
,并将errno
设置为EAGAIN
,表示当前没有数据可读,进程可以选择稍后再试。
2. 当管道满的时候
管道的缓冲区大小是有限的,当写入的数据填满管道后,进程的写操作也会受到限制:
- 阻塞模式(O_NONBLOCK disable):如果管道已满,写入进程会进入阻塞状态,直到有其他进程从管道中读取数据,腾出空间。
- 非阻塞模式(O_NONBLOCK enable) :如果管道已满且处于非阻塞模式,写入进程的
write
调用会立即返回-1
,并将errno
设置为EAGAIN
,表示当前无法写入数据。
3. 如果所有管道写端对应的文件描述符被关闭
当所有写端的文件描述符都被关闭后,管道中的数据仍然可以被读取,直到所有数据读取完毕。一旦管道中的数据被全部读取,后续的 read
调用将返回 0
,表示已经读取到文件末尾。
4. 如果所有管道读端对应的文件描述符被关闭
如果所有读端的文件描述符都被关闭,写入进程的 write
调用会触发 SIGPIPE
信号,导致进程终止。为了避免这种情况,写入进程可以忽略 SIGPIPE
信号,或者在写入前确保仍有读端存在。
5. 写入的原子性
Linux 管道在写入数据时,对于小于 PIPE_BUF
(通常为 4096 字节)的写入操作,保证其原子性,即多个进程同时写入时,不会出现数据交叉的情况。然而,如果写入的数据大于 PIPE_BUF
,Linux 将不再保证写入的原子性,不同进程的写入可能会交错,导致数据不一致。
这些规则确保了管道在进程间通信时的可靠性,同时也要求开发者在编写多进程程序时,合理管理管道的读写操作,以避免潜在的阻塞或异常情况。
管道的特点
管道作为一种基础的进程间通信方式,具有以下几个关键特点,这些特点决定了其适用场景和使用方式。
1. 自带同步与互斥机制
管道内部自带同步与互斥机制,这使得多个进程在访问管道时不会发生冲突。同步指的是进程之间的操作顺序协调,例如一个进程在写入数据之前,另一个进程必须等待,直到数据被成功写入;而互斥则是确保同一时刻只有一个进程能够访问共享资源。对于管道来说,这两个机制是紧密相关的。
在管道的使用中,如果一个进程尝试读取数据而管道为空,读操作会阻塞,直到有数据写入。同样,当管道满时,写操作也会被阻塞,直到有空间可用。这种机制确保了进程在读取和写入数据时的协调,避免了数据丢失或损坏的风险。
2. 生命周期随进程
管道的生命周期是短暂的,它的存在依赖于创建它的进程。当所有与管道相关的进程都关闭了它们的文件描述符后,管道将被自动销毁。这意味着,管道不适合用于长期运行的进程之间的通信,而更适合于需要短期数据交换的场景。
3. 提供流式服务
管道提供的是流式服务,这意味着数据在管道中是以连续的字节流形式存在的。进程在写入数据时,数据会被追加到管道的末尾,而在读取时,数据会从管道的开头取出。这种流式服务的特性使得管道非常适合用于连续的数据传输,比如日志记录或实时数据流的处理。
4. 半双工通信
管道是半双工的,数据只能在一个方向上流动。如果两个进程需要进行双向通信,通常需要创建两个管道,一个用于进程A向进程B发送数据,另一个用于进程B向进程A发送数据。这种单向通信的特性使得管道在设计上相对简单,但也限制了其在复杂通信场景中的应用。
通过这些特点,管道为进程间通信提供了一个简单而有效的解决方案,尽管其功能相对基础,但在许多实际应用中仍然发挥着重要作用。😊
管道的四种特殊情况
在使用管道进行进程间通信时,可能会遇到以下四种特殊情况,这些情况反映了管道的同步机制和资源管理特性。
1. 写端进程不写,读端进程一直读
在这种情况下,读端进程会因为管道中没有数据可读而被挂起,直到有数据被写入。此时,读端进程会一直等待,直到写端进程开始向管道写入数据。这种行为确保了进程在读取数据时的同步性,避免了读取空数据的问题。
2. 读端进程不读,写端进程一直写
当写端进程持续向管道写入数据而读端进程不进行读取时,管道会逐渐被填满。一旦管道达到其容量上限,写端进程的写入操作将被阻塞,直到有进程从管道中读取数据,腾出空间。这种机制防止了数据的丢失,并确保了写入进程不会因数据溢出而崩溃。
3. 写端进程写完数据后关闭写端
在这种情况下,读端进程在读取完所有数据后,将不再有新的数据流入。此时,读端进程将继续执行其后续的代码逻辑,而不会被挂起。这种行为表明,当写端关闭后,读端进程的读取操作将正常结束,不会导致进程的无限等待。
4. 读端进程关闭,写端进程仍在写入数据
当读端进程关闭后,写端进程如果继续向管道写入数据,操作系统会检测到这一情况,并向写端进程发送 SIGPIPE
信号,导致写端进程终止。这种机制保护了系统资源,避免了无效的写入操作,确保了进程的稳定性和安全性。
这些特殊情况体现了管道在进程间通信中的同步与互斥机制。通过这些机制,管道能够有效地管理进程之间的数据流动,确保数据传输的可靠性与安全性。😊
管道的大小
管道的容量是有限的,了解其最大容量对于有效使用管道至关重要。在 Linux 系统中,管道的大小可以通过多种方式进行查询和测试。
1. 使用 man
手册查询
根据 man
手册的说明,在 Linux 2.6.11 及更高版本中,管道的最大容量为 65536 字节。这一信息可以通过查阅 pipe
的 man
页面获得。用户可以通过以下命令查看当前系统版本:
bash
uname -r
如果系统版本符合要求,则管道的最大容量为 65536 字节。
2. 使用 ulimit
命令
另一种查询管道大小的方法是使用 ulimit
命令。通过执行以下命令,可以查看当前的资源限制设定:
bash
ulimit -a
在输出中,管道的最大容量通常显示为 512 × 8 = 4096 字节。这一值可能与 man
手册中提到的值不同,具体取决于系统的配置和环境。
3. 自行测试
为了验证管道的实际容量,用户可以通过编写一个简单的测试程序来测试管道的大小。该程序的基本思路是让一个子进程向管道写入数据,直到管道被填满,从而确定管道的实际容量。
以下是一个示例代码:
c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) { // 子进程
close(pipefd[0]); // 关闭读端
char c = 'a';
int count = 0;
while (1) {
write(pipefd[1], &c, 1);
count++;
printf("%d\n", count);
}
close(pipefd[1]);
exit(EXIT_SUCCESS);
} else { // 父进程
close(pipefd[1]); // 关闭写端
wait(NULL); // 等待子进程结束
close(pipefd[0]);
}
return 0;
}
运行此程序后,可以看到写端进程最多能写入 65536 字节的数据,之后会被操作系统挂起,表明管道的容量已达到上限。通过这种方式,用户可以准确地测试管道的实际容量,确保在使用管道时不会超出其限制。😊
命名管道的原理
命名管道(Named Pipe),也称为 FIFO(First In First Out),是一种允许任意两个进程之间进行通信的机制。与匿名管道不同,命名管道不仅限于父子进程之间的通信,而是可以在无关进程之间使用。
命名管道的核心原理在于,它在文件系统中创建了一个特殊的文件,该文件作为进程间通信的桥梁。尽管这个文件在磁盘上存在,但它并不存储实际的数据,而是一个内存中的缓冲区。两个进程通过打开这个命名管道文件,便可以像操作普通文件一样进行读写操作,从而实现数据的交换。
命名管道的创建通过 mkfifo
命令或 mkfifo
函数实现。创建后,进程可以以读写模式打开该文件,进而进行数据传输。一个进程写入的数据会被存储在内存中,直到另一个进程读取这些数据。这种机制使得命名管道成为实现跨进程通信的理想选择。
在使用命名管道时,进程之间的通信可以通过文件名进行标识,这使得即使没有亲缘关系的进程也可以轻松地找到彼此。命名管道的这种特性,使其在多个应用场景中得到了广泛的应用,如服务器与客户端之间的数据交换、进程间的任务调度等。
总之,命名管道通过在文件系统中创建一个特殊的文件,使得不同进程能够共享同一份资源,从而实现高效的进程间通信。😊
使用命令创建命名管道
在 Linux 系统中,可以使用 mkfifo
命令创建命名管道。该命令的使用方式非常简单,只需提供一个文件名作为参数即可。例如,执行以下命令会在当前目录下创建一个名为 fifo
的命名管道:
bash
mkfifo fifo
创建完成后,可以使用 ls -l
命令查看文件的类型。在输出中,文件类型会以 p
标识,表示这是一个命名管道文件。例如:
bash
prw-r--r-- 1 user group 0 Jan 1 00:00 fifo
这里的 p
表示这是一个 FIFO(命名管道)文件,而文件的大小始终为 0,因为命名管道的内容仅存在于内存中,而不会写入磁盘。
创建命名管道后,可以使用两个不同的进程进行通信。例如,一个进程可以向命名管道写入数据,而另一个进程可以从命名管道读取数据。
示例:使用命名管道进行进程间通信
-
启动读端进程 :在一个终端窗口中,使用
cat
命令从命名管道读取数据:bashcat fifo
此时,
cat
命令会阻塞,等待有数据写入管道。 -
启动写端进程 :在另一个终端窗口中,使用
echo
命令向命名管道写入数据:bashecho "Hello, FIFO!" > fifo
写入完成后,
cat
命令会立即输出写入的内容:Hello, FIFO!
这表明两个进程已经成功通过命名管道进行了通信。
通过这种方式,命名管道提供了一种简便的进程间通信机制,使得无关进程之间能够进行数据交换。命名管道的内容仅存在于内存中,不会被写入磁盘,从而避免了不必要的 I/O 操作,提高了通信效率。
创建一个命名管道
在程序中创建命名管道,可以使用 mkfifo
函数。该函数的原型如下:
c
int mkfifo(const char *pathname, mode_t mode);
mkfifo
函数的第一个参数 pathname
是要创建的命名管道文件的路径名。如果 pathname
是一个绝对路径,则命名管道文件将被创建在指定的路径下;如果 pathname
是相对路径,则命名管道文件将被创建在当前工作目录下。
第二个参数 mode
用于指定命名管道文件的访问权限。例如,将 mode
设置为 0666
,则命名管道文件的权限为:
-rw-rw-rw-
然而,实际创建出来的文件权限还会受到 umask
(文件默认掩码)的影响。通常,umask
的默认值为 0002
,因此实际创建出来的文件权限为 mode & (~umask)
。例如,若 mode
为 0666
,则实际权限为 0664
,即:
-rw-rw-r--
为了确保创建出来的命名管道文件具有预期的权限,可以在调用 mkfifo
函数之前使用 umask(0)
将文件默认掩码设置为 0,从而避免 umask
对文件权限的影响。
mkfifo
函数的返回值用于判断命名管道是否创建成功。如果函数调用成功,则返回 0;如果调用失败,则返回 -1,并设置相应的错误码。
以下是一个创建命名管道的示例代码:
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#define FILE_NAME "myfifo"
int main() {
umask(0); // 将文件默认掩码设置为 0
if (mkfifo(FILE_NAME, 0666) < 0) { // 创建命名管道文件
perror("mkfifo");
return 1;
}
// 创建成功
return 0;
}
运行该程序后,当前目录下将创建一个名为 myfifo
的命名管道文件。使用 ls -l
命令查看文件属性时,可以看到该文件的类型为 p
,表示这是一个命名管道文件。
命名管道的打开规则
在使用命名管道进行进程间通信时,命名管道的打开方式决定了进程的行为。命名管道的打开规则主要涉及读写端的打开顺序以及是否启用非阻塞模式。
1. 如果当前打开操作是为读而打开 FIFO
- O_NONBLOCK disable:如果当前打开操作是为读而打开 FIFO,并且没有其他进程以写的方式打开该 FIFO,那么打开操作会阻塞,直到有写端进程打开该 FIFO。
- O_NONBLOCK enable:如果启用了非阻塞模式,打开操作会立即返回成功,而不会等待写端进程打开 FIFO。
2. 如果当前打开操作是为写而打开 FIFO
- O_NONBLOCK disable:如果当前打开操作是为写而打开 FIFO,并且没有其他进程以读的方式打开该 FIFO,那么打开操作会阻塞,直到有读端进程打开该 FIFO。
- O_NONBLOCK enable :如果启用了非阻塞模式,打开操作会立即失败,并返回错误码
ENXIO
,表示没有读端进程存在。
这些规则确保了命名管道在进程间通信时的同步性。在默认情况下,命名管道的读写操作是阻塞的,这意味着进程在没有合适端口打开的情况下会等待,直到条件满足。而在非阻塞模式下,进程可以立即返回,避免了不必要的等待,但需要开发者自行处理可能的错误情况。
通过理解这些打开规则,开发者可以更好地控制命名管道的使用,确保进程在通信时的协调与效率。😊
用命名管道实现 server&client 通信
在 Linux 系统中,命名管道可以用于实现 server 和 client 之间的通信。通过命名管道,server 进程可以监听来自 client 进程的请求,而 client 进程则可以向 server 发送数据。以下是一个简单的实现示例,展示如何利用命名管道进行进程间通信。
server 进程的实现
首先,我们需要创建一个命名管道文件,以便 server 进程能够监听 client 的请求。server 进程将打开该命名管道文件,并以读取模式进行操作。以下是 server 进程的代码示例:
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FILE_NAME "myfifo"
int main() {
umask(0); // 设置文件掩码为0
if (mkfifo(FILE_NAME, 0666) < 0) { // 创建命名管道
perror("mkfifo");
return 1;
}
int fd = open(FILE_NAME, O_RDONLY); // 以只读方式打开命名管道
if (fd < 0) {
perror("open");
return 2;
}
char msg[128];
while (1) {
msg[0] = '\0'; // 清空消息缓冲区
ssize_t s = read(fd, msg, sizeof(msg) - 1); // 从管道中读取数据
if (s > 0) {
msg[s] = '\0'; // 确保字符串结束
printf("client# %s\n", msg); // 输出客户端发送的消息
} else if (s == 0) {
printf("client quit!\n");
break;
} else {
printf("read error!\n");
break;
}
}
close(fd); // 关闭命名管道
unlink(FILE_NAME); // 删除命名管道文件
return 0;
}
client 进程的实现
client 进程将打开已经存在的命名管道文件,并以写入模式进行操作。client 进程将从标准输入中读取用户输入的消息,并将其发送到 server 进程。以下是 client 进程的代码示例:
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FILE_NAME "myfifo"
int main() {
int fd = open(FILE_NAME, O_WRONLY); // 以只写方式打开命名管道
if (fd < 0) {
perror("open");
return 1;
}
char msg[128];
while (1) {
printf("Please Enter# "); // 提示用户输入
fflush(stdout);
ssize_t s = read(0, msg, sizeof(msg) - 1); // 从标准输入读取数据
if (s > 0) {
msg[s] = '\0'; // 确保字符串结束
write(fd, msg, strlen(msg)); // 将数据写入命名管道
} else if (s == 0) {
printf("client quit!\n");
break;
} else {
printf("read error!\n");
break;
}
}
close(fd); // 关闭命名管道
return 0;
}
共享命名管道文件
为了确保 server 和 client 进程能够使用相同的命名管道文件,我们可以将命名管道文件的名称定义在共用的头文件中。以下是一个示例头文件:
c
#pragma once
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FILE_NAME "myfifo" // 命名管道文件名
通过这种方式,server 和 client 进程都可以包含该头文件,从而确保它们使用相同的命名管道文件进行通信。
运行过程
- 启动 server 进程 :运行 server 程序后,它会创建一个名为
myfifo
的命名管道文件,并进入读取状态,等待 client 发送数据。 - 启动 client 进程:运行 client 程序后,它会打开 server 创建的命名管道文件,并等待用户输入消息。
- 通信过程:用户在 client 端输入消息后,client 进程将消息写入命名管道,server 进程则从管道中读取并输出消息。
这种基于命名管道的通信机制,使得 server 和 client 进程能够在不直接关联的情况下进行数据交换,确保了进程间通信的灵活性和可靠性。😊
用命名管道实现派发计算任务
在进程间通信的场景中,命名管道不仅可以用于简单的数据传输,还可以用于派发计算任务。通过命名管道,客户端可以向服务端发送计算请求,服务端接收请求后进行计算,并将结果返回给客户端。
实现步骤
-
服务端监听命名管道:服务端进程创建并打开命名管道,等待客户端的连接。
-
客户端发送计算请求:客户端进程打开命名管道,并向服务端发送包含计算任务的字符串,例如 "3+5" 或 "10*2"。
-
服务端解析并计算:服务端接收到计算请求后,解析字符串中的操作数和运算符,并执行相应的计算操作。
-
服务端返回结果:计算完成后,服务端将结果写入命名管道,客户端读取并输出结果。
示例代码
服务端代码
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FILE_NAME "myfifo"
int main() {
umask(0); // 设置文件掩码为0
if (mkfifo(FILE_NAME, 0666) < 0) {
perror("mkfifo");
return 1;
}
int fd = open(FILE_NAME, O_RDONLY); // 打开命名管道
if (fd < 0) {
perror("open");
return 2;
}
char msg[128];
while (1) {
msg[0] = '\0'; // 清空消息缓冲区
ssize_t s = read(fd, msg, sizeof(msg) - 1); // 从管道中读取数据
if (s > 0) {
msg[s] = '\0'; // 确保字符串结束
printf("client# %s\n", msg);
char *label = "+-*/%";
char *data1 = strtok(msg, label); // 分割操作数
char *data2 = strtok(NULL, label); // 获取第二个操作数
int num1 = atoi(data1); // 转换为整数
int num2 = atoi(data2); // 转换为整数
int result = 0;
switch (msg[strlen(data1)]) {
case '+':
result = num1 + num2;
break;
case '-':
result = num1 - num2;
break;
case '*':
result = num1 * num2;
break;
case '/':
result = num1 / num2;
break;
case '%':
result = num1 % num2;
break;
default:
printf("Invalid operation\n");
continue;
}
printf("%d %c %d = %d\n", num1, msg[strlen(data1)], num2, result); // 输出计算结果
} else if (s == 0) {
printf("client quit!\n");
break;
} else {
printf("read error!\n");
break;
}
}
close(fd); // 关闭命名管道
return 0;
}
客户端代码
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FILE_NAME "myfifo"
int main() {
int fd = open(FILE_NAME, O_WRONLY); // 打开命名管道
if (fd < 0) {
perror("open");
return 1;
}
char msg[128];
while (1) {
printf("Please Enter# ");
fflush(stdout);
ssize_t s = read(0, msg, sizeof(msg) - 1); // 从标准输入读取数据
if (s > 0) {
msg[s - 1] = '\0'; // 去掉换行符
write(fd, msg, strlen(msg)); // 将数据写入命名管道
} else if (s == 0) {
printf("client quit!\n");
break;
} else {
printf("read error!\n");
break;
}
}
close(fd); // 关闭命名管道
return 0;
}
运行过程
-
启动服务端 :运行服务端程序后,它会创建一个名为
myfifo
的命名管道,并等待客户端的连接。 -
启动客户端:运行客户端程序后,它会打开服务端创建的命名管道,并等待用户输入计算请求。
-
发送计算请求 :在客户端输入一个计算表达式,例如
3+5
,客户端将表达式发送到服务端。 -
服务端处理请求:服务端接收到计算请求后,解析表达式并执行相应的计算操作,输出结果。
通过这种方式,命名管道不仅实现了进程间的通信,还能够有效地处理计算任务,展示了其在实际应用中的灵活性和强大功能。😊
用命名管道实现进程遥控
命名管道不仅可以用于数据传输,还可以用于实现进程遥控,即一个进程向另一个进程发送命令并控制其行为。通过命名管道,客户端可以向服务端发送命令,服务端接收命令并执行相应的操作。这种机制可以用于远程控制、任务调度等场景。
实现步骤
-
服务端监听命名管道:服务端进程创建并打开命名管道,等待客户端的连接。
-
客户端发送命令:客户端进程打开命名管道,并向服务端发送命令。
-
服务端执行命令 :服务端接收到命令后,使用
execlp
函数执行命令,并将执行结果返回给客户端(如果需要)。
示例代码
服务端代码
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#define FILE_NAME "myfifo"
int main() {
umask(0); // 设置文件掩码为0
if (mkfifo(FILE_NAME, 0666) < 0) {
perror("mkfifo");
return 1;
}
int fd = open(FILE_NAME, O_RDONLY); // 打开命名管道
if (fd < 0) {
perror("open");
return 2;
}
char msg[128];
while (1) {
msg[0] = '\0'; // 清空消息缓冲区
ssize_t s = read(fd, msg, sizeof(msg) - 1); // 从管道中读取数据
if (s > 0) {
msg[s] = '\0'; // 确保字符串结束
printf("client# %s\n", msg);
pid_t pid = fork(); // 创建子进程执行命令
if (pid == 0) {
execlp(msg, msg, NULL); // 执行命令
exit(1);
}
waitpid(-1, NULL, 0); // 0;
} else if (s == 0) {
printf("client quit!\n");
break;
} else {
printf("read error!\n");
break;
}
}
close(fd); // 关闭命名管道
return 0;
}
客户端代码
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FILE_NAME "myfifo"
int main() {
int fd = open(FILE_NAME, O_WRONLY); // 打开命名管道
if (fd < 0) {
perror("open");
return 1;
}
char msg[128];
while (1) {
printf("Please Enter# ");
fflush(stdout);
ssize_t s = read(0, msg, sizeof(msg) - 1); // 从标准输入读取数据
if (s > 0) {
msg[s - 1] = '\0'; // 去掉换行符
write(fd, msg, strlen(msg)); // 将数据写入命名管道
} else if (s == 0) {
printf("client quit!\n");
break;
} else {
printf("read error!\n");
break;
}
}
close(fd); // 关闭命名管道
return 0;
}
运行过程
-
启动服务端 :运行服务端程序后,它会创建一个名为
myfifo
的命名管道,并等待客户端发送命令。 -
启动客户端:运行客户端程序后,它会打开服务端创建的命名管道,并等待用户输入命令。
-
发送命令 :在客户端输入命令(如
ls -l
),客户端将命令发送到服务端。 -
服务端执行命令 :服务端接收到命令后,使用
execlp
函数执行该命令,并输出执行结果。
通过这种方式,客户端可以向服务端发送任意命令,并由服务端执行,从而实现进程遥控。这种方式可以用于远程控制、自动化任务调度等场景。😊
用命名管道实现文件拷贝
命名管道不仅可以用于进程间的数据传输,还可以用于实现文件的拷贝。通过命名管道,一个进程可以将文件内容发送到管道中,而另一个进程可以从管道中读取数据并将其写入新文件,从而完成文件的拷贝操作。
实现步骤
-
服务端创建命名管道:服务端进程创建一个命名管道文件,并以只读方式打开该文件。
-
客户端打开命名管道:客户端进程以只写方式打开命名管道文件,并打开要拷贝的源文件。
-
客户端发送文件内容:客户端进程读取源文件的内容,并将其写入命名管道。
-
服务端接收数据并写入新文件:服务端进程从命名管道读取数据,并将其写入目标文件中,完成文件拷贝。
示例代码
服务端代码
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FILE_NAME "myfifo"
#define OUTPUT_FILE "file-bat.txt"
int main() {
umask(0); // 设置文件掩码为0
if (mkfifo(FILE_NAME, 0666) < 0) {
perror("mkfifo");
return 1;
}
int fd = open(FILE_NAME, O_RDONLY); // 打开命名管道
if (fd < 0) {
perror("open");
return 2;
}
int fdout = open(OUTPUT_FILE, O_CREAT | O_WRONLY, 0666); // 创建并打开目标文件
if (fdout < 0) {
perror("open");
return 3;
}
char msg[128];
while (1) {
msg[0] = '\0'; // 清空消息缓冲区
ssize_t s = read(fd, msg, sizeof(msg) - 1); // 从管道中读取数据
if (s > 0) {
write(fdout, msg, s); // 将数据写入目标文件
} else if (s == 0) {
printf("client quit!\n");
break;
} else {
printf("read error!\n");
break;
}
}
close(fd); // 关闭命名管道
close(fdout); // 关闭目标文件
return 0;
}
客户端代码
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FILE_NAME "myfifo"
#define INPUT_FILE "file.txt"
int main() {
int fd = open(FILE_NAME, O_WRONLY); // 打开命名管道
if (fd < 0) {
perror("open");
return 1;
}
int fdin = open(INPUT_FILE, O_RDONLY); // 打开源文件
if (fdin < 0) {
perror("open");
return 2;
}
char msg[128];
while (1) {
ssize_t s = read(fdin, msg, sizeof(msg)); // 从源文件读取数据
if (s > 0) {
write(fd, msg, s); // 将数据写入命名管道
} else if (s == 0) {
printf("read end of file!\n");
break;
} else {
printf("read error!\n");
break;
}
}
close(fd); // 关闭命名管道
close(fdin); // 关闭源文件
return 0;
}
运行过程
-
启动服务端 :运行服务端程序后,它会创建一个名为
myfifo
的命名管道,并创建一个名为file-bat.txt
的文件用于存储拷贝的数据。 -
启动客户端 :运行客户端程序后,它会打开服务端创建的命名管道,并打开源文件
file.txt
,开始读取文件内容并写入管道。 -
服务端接收数据并写入文件 :服务端进程从管道中读取数据,并将其写入目标文件
file-bat.txt
。 -
拷贝完成 :当客户端读取完
file.txt
的所有数据后,服务端会完成文件的写入操作,完成文件拷贝。
通过命名管道实现文件拷贝,不仅展示了命名管道在进程间通信中的灵活性,也为实现网络文件传输提供了基础。😊
命名管道和匿名管道的区别
命名管道和匿名管道虽然都用于进程间通信,但它们在创建方式、适用场景以及生命周期等方面存在显著差异。
创建方式
匿名管道通过 pipe
函数创建,通常用于父子进程之间的通信。创建后,父进程和子进程分别持有管道的读端和写端。相对而言,命名管道通过 mkfifo
函数创建,并且在文件系统中有一个显式的文件名,允许任何进程通过文件名打开并进行通信。
适用场景
匿名管道仅限于具有亲缘关系的进程之间进行通信,通常用于父子进程或兄弟进程之间的数据传输。命名管道则适用于任意两个进程之间的通信,即使它们没有亲缘关系。命名管道的这种特性使其在跨进程通信中更为灵活,适用于需要多个无关进程进行数据交换的场景。
生命周期
匿名管道的生命周期与创建它的进程相关,当所有使用管道的进程都退出后,管道将被自动销毁。而命名管道的生命周期则由文件系统管理,只有在显式删除命名管道文件后,才会被销毁。这意味着,命名管道可以在多个进程之间持久存在,直到不再需要为止。
通过这些区别可以看出,匿名管道适合于简单的、短暂的进程间通信,而命名管道则适合于需要在多个无关进程之间进行长期通信的场景。😊
命令行当中的管道
在 Linux 命令行中,管道(|
)是一种非常常见的进程间通信方式。它允许一个命令的输出直接作为另一个命令的输入,从而实现数据的无缝流转。例如,以下命令统计当前登录用户的数量:
bash
who | wc -l
在这个例子中,who
命令的输出被管道传递给 wc -l
命令,后者统计行数,从而得出当前登录用户数量。这种管道的实现方式本质上是匿名管道,因为它用于连接两个具有亲缘关系的进程。
匿名管道与命名管道的区分
匿名管道和命名管道是两种不同的进程间通信机制,它们在实现和使用上各有特点。
匿名管道
匿名管道用于具有亲缘关系的进程之间,通常是父子进程或兄弟进程。通过 pipe
函数创建,管道的生命周期随着进程的终止而结束。匿名管道的数据在内存中传输,不会写入磁盘,因此其效率较高,适合于短暂的进程间通信。
命令行管道的实现
在命令行中,|
符号实际上创建的是匿名管道。例如,当执行 who | wc -l
时,命令行解释器(如 bash
)会创建两个进程,并通过匿名管道连接它们。who
进程的输出被写入管道,wc -l
进程从管道读取数据并进行统计。
命名管道
与匿名管道不同,命名管道(FIFO)允许任意两个进程之间进行通信,而不仅仅限于父子进程。命名管道在文件系统中有一个显式的文件名,进程可以通过文件名打开管道进行通信。命名管道的生命周期与匿名管道不同,它在创建后不会自动销毁,只有在显式删除后才会消失。
0;
在命令行中,管道的使用为我们提供了便捷的进程间通信方式,而匿名管道和命名管道则各自适用于不同的通信需求。匿名管道适合于短暂的、具有亲缘关系的进程通信,而命名管道则适合于需要在任意进程之间进行通信的场景。😊
System V 进程间通信
System V 进程间通信(IPC)是操作系统提供的一组高级通信机制,主要包括共享内存、消息队列和信号量。这些机制的设计目的是为了满足进程间高效的数据传输和同步需求。
共享内存
共享内存是一种高效的进程间通信方式,它允许多个进程共享同一块物理内存区域。通过共享内存,进程可以直接访问共享的内存,避免了频繁的数据复制操作,从而显著提高了通信效率。共享内存的使用通常需要配合其他机制(如信号量)来实现同步,以确保多个进程对共享内存的访问不会导致数据竞争。
消息队列
消息队列是一种较为灵活的进程间通信方式,它允许进程发送和接收消息。每个消息都有一个类型,进程可以根据类型选择接收特定的消息。消息队列的特点是消息的存储和传递是异步的,进程可以在不同的时间发送和接收消息。这种方式适合于需要处理多个消息的场景,能够有效解决进程间的数据传输问题。
信号量
信号量主要用于进程间的同步与互斥,确保多个进程在访问共享资源时的协调。信号量通过维护一个计数器来控制对共享资源的访问,进程在访问资源之前需要获取信号量,访问结束后释放信号量。这种机制可以有效防止多个进程同时对共享资源进行操作,从而避免数据不一致的问题。
System V 进程间通信机制的核心目的是为了实现进程间的高效数据传输和同步。共享内存、消息队列和信号量各自有着不同的应用场景和特点,开发者需要根据具体的通信需求选择合适的机制。😊
System V 共享内存的基本原理
System V 共享内存是一种高效的进程间通信方式,其基本原理在于,让不同的进程共享同一块物理内存区域,从而实现数据的快速交换。
共享内存的实现机制
在 Linux 系统中,共享内存的实现依赖于内核的管理机制。当一个进程申请共享内存时,内核会在物理内存中分配一块指定大小的内存空间,并为该内存空间维护一个数据结构(如 shmid_ds
),用于记录共享内存的状态。
随后,其他进程可以通过相同的键值(key)来访问这块共享内存。内核会为每个进程的虚拟地址空间分配一块映射区域,并建立虚拟地址和物理地址之间的映射关系。这样,多个进程就可以通过各自的虚拟地址访问同一块物理内存,实现数据的共享。
共享内存的优势
共享内存的最大优势在于高效性 。由于进程可以直接读写共享内存,而不需要通过系统调用(如 read
和 write
)进行数据拷贝,因此共享内存是所有进程间通信方式中最快的一种。
然而,共享内存本身并不提供同步机制,多个进程同时访问共享内存可能导致数据竞争问题。因此,在使用共享内存时,通常需要配合信号量等同步机制,以确保数据的一致性和完整性。
通过共享内存,多个进程可以高效地共享数据,这对于需要大量数据交换的场景(如高性能计算、进程间大规模数据传输)非常有用。😊
共享内存的数据结构
在 Linux 系统中,共享内存的管理依赖于内核提供的数据结构,其中最重要的是 shmid_ds
结构体。这个结构体不仅用于描述共享内存的状态,还包含了访问共享内存所需的各种信息。
shmid_ds
结构体
shmid_ds
结构体定义如下:
c
struct shmid_ds {
struct ipc_perm shm_perm; /* 操作权限 */
size_t shm_segsz; /* 共享内存段的大小(字节数) */
__kernel_time_t shm_atime; /* 上一次附加时间 */
__kernel_time_t shm_dtime; /* 上一次分离时间 */
__kernel_time_t shm_ctime; /* 上一次修改时间 */
__kernel_ipc_pid_t shm_cpid; /* 创建共享内存段的进程 PID */
__kernel_ipc_pid_t shm_lpid; /* 最近访问共享内存段的进程 PID */
unsigned short shm_nattch; /* 当前附加到该共享内存段的进程数 */
...
};
shm_perm
结构体
shm_perm
是 shmid_ds
结构体中的一个成员,其定义如下:
c
struct ipc_perm {
__kernel_key_t key; /* IPC 键值 */
__kernel_uid_t uid; /* 拥有者用户 ID */
__kernel_gid_t gid; /* 拥有者组 ID */
__kernel_uid_t cuid; /* 创建者用户 ID */
__kernel_gid_t cgid; /* 创建者组 ID */
__kernel_mode_t mode; /* 访问模式(权限标志) */
unsigned short seq; /* 序列号(用于区分资源) */
};
数据结构的作用
shmid_ds
结构体和 ipc_perm
结构体共同构成了共享内存的管理机制。shmid_ds
提供了共享内存的详细状态信息,包括内存大小、访问权限、附加进程数等。ipc_perm
则负责管理共享内存的权限和所有者信息,确保只有具有适当权限的进程才能访问共享内存。
通过这些数据结构,操作系统能够有效地管理共享内存,确保多个进程能够安全、高效地共享数据。😊
共享内存的建立与释放
共享内存的建立和释放是进程间通信中的重要环节,确保多个进程能够安全地访问共享的物理内存。
共享内存的建立
共享内存的建立大致包括以下两个过程:
- 申请共享内存空间:在物理内存中申请一块共享内存空间。
- 挂接到地址空间:将申请到的共享内存与进程的地址空间建立映射关系。
在 Linux 系统中,使用 shmget
函数创建共享内存,函数原型如下:
c
int shmget(key_t key, size_t size, int shmflg);
key
是共享内存的唯一标识符,通常通过ftok
函数生成。size
是共享内存的大小,单位为字节。shmflg
指定创建共享内存的方式,常用的组合有IPC_CREAT
和IPC_CREAT | IPC_EXCL
。
创建共享内存后,进程需要通过 shmat
函数将共享内存挂接到自己的地址空间。shmat
的函数原型如下:
c
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid
是共享内存的标识符。shmaddr
指定共享内存映射到进程地址空间的地址,通常设置为NULL
,由内核自动选择合适的地址。shmflg
指定附加共享内存的属性,通常为0
,表示默认读写权限。
共享内存的释放
共享内存的释放包括两个步骤:
- 取消映射关系 :使用
shmdt
函数将共享内存与进程的地址空间取消关联。 - 释放共享内存空间 :使用
shmctl
函数释放共享内存空间。
shmdt
的函数原型如下:
c
int shmdt(const void *shmaddr);
shmaddr
是共享内存的起始地址。
shmctl
的函数原型如下:
c
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid
是共享内存的标识符。cmd
指定控制命令,常用的有IPC_STAT
(获取共享内存的当前状态)、IPC_SET
(设置共享内存的当前状态)和IPC_RMID
(删除共享内存段)。buf
是指向shmid_ds
结构体的指针,用于获取或设置共享内存的状态。
通过这些步骤,开发者可以有效地管理共享内存,确保多个进程在访问共享内存时的安全性和效率。😊
共享内存的创建
在 Linux 系统中,创建共享内存的过程涉及多个步骤,主要依赖于 shmget
函数和 ftok
函数。
ftok
函数
ftok
函数用于生成一个唯一的键值(key
),这个键值将在创建共享内存时使用。其函数原型如下:
c
key_t ftok(const char *pathname, int proj_id);
pathname
是一个已存在的文件路径名。proj_id
是一个整数标识符。
ftok
函数的作用是将传入的路径名和整数标识符转换成一个唯一的 key
值,确保多个进程在使用相同的路径名和 proj_id
时,能够生成相同的 key
值,从而访问同一个共享内存。
shmget
函数
shmget
函数用于创建或获取共享内存标识符。其函数原型如下:
c
int shmget(key_t key, size_t size, int shmflg);
key
是ftok
函数生成的键值。size
是共享内存的大小,通常以字节为单位。shmflg
指定了创建共享内存的方式。
常见的 shmflg
组合有:
- IPC_CREAT :如果内核中不存在键值与
key
相同的共享内存,则新建一个共享内存并返回其句柄;如果存在这样的共享内存,则直接返回其句柄。 - IPC_CREAT | IPC_EXCL :如果内核中不存在键值与
key
相同的共享内存,则新建一个共享内存并返回其句柄;如果存在这样的共享内存,则返回错误。
示例代码
以下是一个创建共享内存的示例代码:
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/cl/Linuxcode/IPC/shm/server.c" // 路径名
#define PROJ_ID 0x6666 // 整数标识符
#define SIZE 4096 // 共享内存的大小
int main() {
key_t key = ftok(PATHNAME, PROJ_ID); // 获取 key 值
if (key < 0) {
perror("ftok");
return 1;
}
int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL); // 创建新的共享内存
if (shm < 0) {
perror("shmget");
return 2;
}
printf("key: %x\n", key); // 打印 key 值
printf("shm: %d\n", shm); // 打印共享内存的用户层 id
return 0;
}
在这个示例中,首先使用 ftok
函数生成 key
值,然后使用 shmget
函数创建共享内存。通过 key
值和 shm
句柄,进程可以有效地管理和访问共享内存。
通过这些步骤,开发者可以创建共享内存,并在多个进程之间安全地共享数据。😊
共享内存的释放
在 Linux 系统中,共享内存的释放可以通过命令行工具和程序接口两种方式实现。
使用命令释放共享内存
在命令行中,可以使用 ipcrm
命令来释放共享内存。ipcrm
命令的使用格式如下:
bash
ipcrm -m shmid
其中,shmid
是共享内存的用户层标识符。使用此命令时,用户需要确保提供的 shmid
是有效的,否则会提示错误。
使用程序释放共享内存
在程序中,共享内存的释放涉及到 shmctl
函数的使用。shmctl
函数用于控制共享内存的操作,其函数原型如下:
c
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid
是共享内存的标识符。cmd
指定控制命令,常用的命令有IPC_STAT
(获取共享内存的当前状态)、IPC_SET
(设置共享内存的当前状态)和IPC_RMID
(删除共享内存段)。buf
是指向shmid_ds
结构体的指针,用于获取或设置共享内存的状态。
以下是一个释放共享内存的示例代码:
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/cl/Linuxcode/IPC/shm/server.c" // 路径名
#define PROJ_ID 0x6666 // 整数标识符
#define SIZE 4096 // 共享内存的大小
int main() {
key_t key = ftok(PATHNAME, PROJ_ID); // 获取 key 值
if (key < 0) {
perror("ftok");
return 1;
}
int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); // 创建共享内存
if (shm < 0) {
perror("shmget");
return 2;
}
printf("key: %x\n", key); // 打印 key 值
printf("shm: %d\n", shm); // 打印共享内存的用户层 id
sleep(2);
shmctl(shm, IPC_RMID, NULL); // 释放共享内存
sleep(2);
return 0;
}
在这个示例中,程序首先创建共享内存,随后通过 shmctl
函数的 IPC_RMID
命令释放共享内存。
共享内存的生命周期
共享内存的生命周期与进程的生命周期不同。共享内存的生命周期是由内核管理的,这意味着即使创建共享内存的进程已经退出,共享内存仍然存在,直到显式地调用 shmctl
函数释放共享内存。
共享内存的生命周期可以总结为以下几点:
- 创建 :共享内存通过
shmget
函数创建,只有在调用shmctl
函数或使用ipcrm
命令后,共享内存才会被释放。 - 使用:多个进程可以附加到同一个共享内存段,进行数据的读写操作。
- 释放 :共享内存的释放需要显式调用
shmctl
函数,或使用ipcrm
命令。
通过这些机制,共享内存的生命周期得以延长,直到不再需要为止。这种设计使得共享内存在多个进程间共享时,能够保持数据的持久性,确保进程间的数据交换不会因为某个进程的退出而中断。😊
共享内存的关联
将共享内存连接到进程地址空间的过程,通常使用 shmat
函数。shmat
函数的函数原型如下:
c
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid
是共享内存的用户级标识符,由shmget
函数返回。shmaddr
是共享内存映射到进程地址空间的起始地址。通常设置为NULL
,表示由内核自动选择合适的地址。shmflg
是关联共享内存时设置的属性。常见的选项包括SHM_RDONLY
(关联后只读)、SHM_RND
(自动调整地址)和0
(默认为读写权限)。
shmat
函数返回共享内存映射到进程地址空间的起始地址。如果调用成功,进程可以通过返回的指针直接访问共享内存。
示例代码
以下是一个简单的示例,展示如何使用 shmat
函数将共享内存映射到进程地址空间:
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/cl/Linuxcode/IPC/shm/server.c" // 路径名
#define PROJ_ID 0x6666 // 整数标识符
#define SIZE 4096 // 共享内存的大小
int main() {
key_t key = ftok(PATHNAME, PROJ_ID); // 获取 key 值
if (key < 0) {
perror("ftok");
return 1;
}
int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); // 创建共享内存
if (shm < 0) {
perror("shmget");
return 2;
}
printf("key: %x\n", key); // 打印 key 值
printf("shm: %d\n", shm); // 打印共享内存的用户层 id
printf("attach begin!\n");
sleep(2);
char *mem = shmat(shm, NULL, 0); // 关联共享内存
if (mem == (void *)-1) {
perror("shmat");
return 1;
}
printf("attach end!\n");
sleep(2);
// 在这里可以对共享内存进行操作,例如写入和读取数据
shmdt(mem); // 取消共享内存与进程的关联
shmctl(shm, IPC_RMID, NULL); // 释放共享内存
return 0;
}
在这个示例中,程序首先创建共享内存,然后使用 shmat
函数将其映射到进程的地址空间。通过 shmat
返回的指针 mem
,进程可以直接对共享内存进行读写操作。在操作完成后,使用 shmdt
函数取消共享内存与进程的关联,并使用 shmctl
函数释放共享内存。
通过这种方式,共享内存的关联和去关联操作得以实现,确保了进程能够安全地访问共享内存。😊
共享内存的去关联
取消共享内存与进程地址空间之间的关联,通常使用 shmdt
函数。shmdt
函数的函数原型如下:
c
int shmdt(const void *shmaddr);
shmaddr
是共享内存的起始地址,即调用shmat
函数时返回的地址。
shmdt
函数的返回值用于指示操作的结果。如果调用成功,返回值为 0;如果调用失败,返回值为 -1,并设置相应的错误码。
示例代码
以下是一个简单的示例,展示如何使用 shmdt
函数取消共享内存与进程的关联:
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/cl/Linuxcode/IPC/shm/server.c" // 路径名
#define PROJ_ID 0x6666 // 整数标识符
#define SIZE 4096 // 共享内存的大小
int main() {
key_t key = ftok(PATHNAME, PROJ_ID); // 获取 key 值
if (key < 0) {
perror("ftok");
return 1;
}
int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); // 创建共享内存
if (shm < 0) {
perror("shmget");
return 2;
}
printf("key: %x\n", key); // 打印 key 值
printf("shm: %d\n", shm); // 打印共享内存的用户层 id
printf("attach begin!\n");
sleep(2);
char *mem = shmat(shm, NULL, 0); // 关联共享内存
if (mem == (void *)-1) {
perror("shmat");
return 1;
}
printf("attach end!\n");
sleep(2);
// 在这里可以对共享内存进行操作,例如写入和读取数据
printf("detach begin!\n");
sleep(2);
shmdt(mem); // 共享内存去关联
printf("detach end!\n");
sleep(2);
shmctl(shm, IPC_RMID, NULL); // 释放共享内存
return 0;
}
在这个示例中,程序首先创建共享内存,然后使用 shmat
函数将其映射到进程的地址空间。通过 shmat
返回的指针 mem
,进程可以直接对共享内存进行读写操作。
在操作完成后,使用 shmdt
函数取消共享内存与进程的关联。shmdt
函数的调用非常简单,只需传入 shmat
函数返回的地址即可。
需要注意的是,shmdt
函数的调用不会释放共享内存,仅仅是取消了共享内存与进程之间的映射关系。共享内存的释放需要使用 shmctl
函数,并传入 IPC_RMID
命令,以确保共享内存的生命周期结束。
通过这种方式,共享内存的去关联操作得以实现,确保了进程能够安全地访问共享内存,同时在不再需要时及时释放资源。😊
用共享内存实现 serve&client 通信
在进程间通信中,共享内存是一种高效的通信方式。通过共享内存,客户端和服务端进程可以共享同一块物理内存区域,从而实现数据的快速传输。
服务端代码
服务端负责创建共享内存,并与共享内存进行关联。以下是一个简单的服务端代码示例:
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/cl/Linuxcode/IPC/shm/server.c" // 路径名
#define PROJ_ID 0x6666 // 整数标识符
#define SIZE 4096 // 共享内存的大小
int main() {
key_t key = ftok(PATHNAME, PROJ_ID); // 获取 key 值
if (key < 0) {
perror("ftok");
return 1;
}
int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); // 创建共享内存
if (shm < 0) {
perror("shmget");
return 2;
}
printf("key: %x\n", key); // 打印 key 值
printf("shm: %d\n", shm); // 打印共享内存的用户层 id
char *mem = shmat(shm, NULL, 0); // 关联共享内存
if (mem == (void *)-1) {
perror("shmat");
return 1;
}
while (1) {
// 不进行操作,便于观察共享内存的关联
}
shmdt(mem); // 共享内存去关联
shmctl(shm, IPC_RMID, NULL); // 释放共享内存
return 0;
}
在这个服务端代码中,服务端创建共享内存后,会将其与自身的地址空间关联。通过 shmat
函数,服务端获取共享内存的起始地址,并进入一个死循环,便于观察共享内存的关联情况。
客户端代码
客户端进程需要与服务端创建的共享内存进行关联,以便进行数据的读写操作。以下是一个简单的客户端代码示例:
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/cl/Linuxcode/IPC/shm/server.c" // 路径名
#define PROJ_ID 0x6666 // 整数标识符
#define SIZE 4096 // 共享内存的大小
int main() {
key_t key = ftok(PATHNAME, PROJ_ID); // 获取 key 值
if (key < 0) {
perror("ftok");
return 1;
}
int shm = shmget(key, SIZE, IPC_CREAT); // 获取服务端创建的共享内存的用户层 id
if (shm < 0) {
perror("shmget");
return 2;
}
printf("key: %x\n", key); // 打印 key 值
printf("shm: %d\n", shm); // 打印共享内存的用户层 id
char *mem = shmat(shm, NULL, 0); // 关联共享内存
if (mem == (void *)-1) {
perror("shmat");
return 1;
}
int i = 0;
while (1) {
mem[i] = 'A' + i; // 向共享内存写入数据
i++;
mem[i] = '\0'; // 确保字符串结束
sleep(1);
}
shmdt(mem); // 共享内存去关联
return 0;
}
在这个客户端代码中,客户端通过 ftok
函数生成的 key
值来获取服务端创建的共享内存标识符。客户端通过 shmget
函数获取共享内存的用户层标识符,并使用 shmat
函数将其与自身的地址空间关联。客户端不断向共享内存写入数据,服务端则不断读取共享内存中的数据并输出。
共享内存的通信过程
在共享内存的通信过程中,服务端和客户端的交互过程如下:
-
服务端创建共享内存 :服务端使用
shmget
函数创建共享内存,并通过shmat
函数将其与地址空间关联。 -
客户端获取共享内存 :客户端通过
ftok
函数生成与服务端相同的key
值,并使用shmget
函数获取共享内存的用户层标识符。 -
客户端关联共享内存 :客户端使用
shmat
函数将共享内存与自身的地址空间关联。 -
数据传输:客户端不断向共享内存写入数据,服务端不断读取共享内存中的数据并输出。
-
共享内存的去关联 :服务端和客户端在通信结束后,分别使用
shmdt
函数取消共享内存与地址空间的关联。
通过上述步骤,服务端和客户端能够成功挂接共享内存,实现高效的数据传输。共享内存的高效性在于其直接访问物理内存的特性,使得数据传输不需要通过系统调用进行拷贝,从而提高了通信的效率。
共享内存与管道进行对比
在进程间通信的机制中,共享内存和管道是两种常用的方式,它们在通信效率、同步机制以及生命周期等方面存在显著的差异。
通信效率
共享内存的通信效率远高于管道。共享内存的实现方式是通过将一块物理内存映射到多个进程的地址空间,进程可以直接读写这块共享内存,避免了多次的数据拷贝。具体来说,共享内存的通信过程只需要两次拷贝操作:从输入文件到共享内存,再从共享内存到输出文件。
相较之下,管道的通信效率较低。管道的实现依赖于内核提供的缓冲区,数据在进程之间传输时需要进行四次拷贝操作:从输入文件到服务端的临时缓冲区,再从服务端的缓冲区到管道,接着从管道到客户端的缓冲区,最后从客户端的缓冲区到输出文件。这种多层拷贝操作显著降低了管道的通信效率。
同步机制
共享内存本身并不提供任何同步机制,这意味着多个进程在访问共享内存时,必须依赖外部的同步机制(如信号量)来确保数据的一致性。这种灵活性使得共享内存非常适合需要高效数据传输的场景,但也增加了编程的复杂性,因为开发者需要自行管理进程间的同步问题。
与之相对,管道则自带同步与互斥机制。在管道的使用过程中,写入和读取操作会自动进行阻塞,确保了数据的一致性和完整性。这种内置的同步机制使得管道在实现进程间通信时更为简单,适合于对同步要求较高的场景。
生命周期
共享内存的生命周期是由内核管理的,这意味着共享内存的生命周期与创建它的进程无关。只要共享内存没有被显式删除,它将一直存在,直到系统重启或手动删除。这种特性使得共享内存适合用于需要长期存在的数据共享场景。
而管道的生命周期与创建它的进程密切相关。当最后一个打开管道的进程关闭管道后,管道将被自动销毁。这种生命周期的管理方式使得管道适合于短期的进程间通信,尤其适用于需要一次性数据传输的场景。
通过这些比较可以看出,共享内存和管道各有优劣,选择合适的通信方式应根据具体的通信需求和场景来决定。共享内存适合需要高效数据传输的场景,而管道则适合需要内置同步机制的场景。
System V 消息队列的基本原理
System V 消息队列是一种高效的进程间通信方式,它通过在系统中维护一个队列来实现进程间的数据传输。消息队列的基本原理在于,在系统中创建一个队列,队列中的每个成员都是一个数据块,这些数据块由类型和信息两部分组成。
消息队列的结构类似于链表,队列中的每个节点包含一个数据块。每个数据块由类型和信息组成,类型用于标识数据的来源和目标,信息则是实际的数据内容。通过这种方式,进程可以向队列的队尾添加数据块,或者从队列的队头提取数据块。
使用场景
System V 消息队列适用于需要在多个进程之间进行数据传输的场景。例如,进程 A 可以向队列中添加数据块,而进程 B 则可以从队列中提取这些数据块。
消息队列的使用场景包括:
- 进程间的数据传输:消息队列提供了一个从一个进程向另一个进程发送数据块的机制。
- 多进程协作:多个进程可以通过消息队列进行协作,确保数据的有序传输和处理。
- 资源管理:消息队列可以用来管理共享资源,确保资源的访问顺序和安全性。
资源的生命周期
与共享内存和管道不同,消息队列的资源在创建后,必须由开发者显式删除,否则将一直存在。这种特性使得消息队列适合于需要长时间运行的进程间通信场景。
System V 消息队列的设计使得进程能够有效地管理数据块的发送和接收,确保数据的可靠传输。
消息队列数据结构
在 Linux 系统中,消息队列的管理依赖于内核提供的数据结构。消息队列的数据结构 msqid_ds
包含多个字段,用于维护队列的状态和属性。以下是 msqid_ds