管道
进程间通信(IPC)是多任务操作系统中的关键功能,允许进程之间交换信息。一种自然的 IPC 方式是利用文件系统作为中介:
基于文件的通信:在这种方式中,一个进程通过打开并读写文件来存储信息,而另一个进程则打开同一个文件来读取这些信息。尽管这种方法简单直观,但它依赖于磁盘 I/O 操作,这可能导致较低的通信效率。
为了提高通信效率,操作系统提供了一种特殊的文件系统对象,即 命名管道(Named Pipe,FIFO):
命名管道:这是一种特殊的文件,存在于文件系统中,但它允许进程通过内核维护的缓冲区直接交换数据,而不是通过磁盘。这种方式减少了磁盘 I/O 操作,从而提高了通信效率。
对于存在亲缘关系的进程,它们可以使用另一种 IPC 机制,即 匿名管道:
匿名管道:与有名管道不同,匿名管道不需要在文件系统中创建可见的文件。它们是在进程执行过程中动态创建的,用于连接亲缘进程(如父子进程),并在不需要时自动销毁。匿名管道通常用于简单的、短暂的通信任务。
命名管道
匿名管道
在 Linux 系统中,匿名管道 是一种用于实现父子进程间通信的简单机制。通过 pipe
函数,可以在父子进程之间建立一个单向数据传输通道。
管道的特性
- 单向性:管道是单向的,数据只能在一个方向上流动。这意味着使用匿名管道时,一个进程将数据写入管道的写端(
pipe[1]
),而另一个进程则从管道的读端(pipe[0]
)读取数据。 - 半双工通信:由于管道的单向性,它是一种半双工通信方式,即在任意时刻只能进行发送或接收操作,而不是同时进行。
- 亲缘关系限定:匿名管道主要用于存在亲缘关系的进程间通信,例如父子进程。
管道的创建和使用
- 在一个进程中首先调用
pipe
函数创建管道,然后通过fork
函数创建子进程。这样,父子进程就可以通过管道的读写端进行通信。 - 单个进程使用管道没有太大价值,因为管道的主要目的是实现进程间的通信。
全双工通信
- 为了实现父子进程之间的全双工通信(即同时发送和接收数据),需要调用
pipe
函数两次,创建两条管道:一条用于发送数据,另一条用于接收数据。
pipe
创建匿名管道
pipe
函数是 POSIX 标准中用于创建一个匿名管道(pipe)的系统调用,它允许两个进程(通常是父子进程或兄弟进程)通过一个临时的、半双工的通道进行通信。管道的一端用于写入(write),另一端用于读取(read)。
函数原型:
int pipe(int pipefd[2]);
参数:pipefd
:一个包含两个整型元素的数组,用于存储管道两端的文件描述符(file descriptors)。pipefd[0]
将是管道读取端的文件描述符,pipefd[1]
将是管道写入端的文件描述符。
返回值:
- 成功时,返回 0。
- 如果调用失败,返回 -1,并设置全局变量
errno
以指示错误。
以下是使用 pipe
函数的一个例子:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
int pipefd[2];
pid_t pid;
// 创建一个管道
if (pipe(pipefd) == -1) {
perror("pipe");
return EXIT_FAILURE;
}
// 创建子进程
pid = fork();
if (pid == -1) {
perror("fork");
close(pipefd[0]); // 关闭读取端
close(pipefd[1]); // 关闭写入端
return EXIT_FAILURE;
}
if (pid == 0) {
// 子进程:关闭读取端,写入管道
close(pipefd[0]);
write(pipefd[1], "Hello from child!", 20);
close(pipefd[1]);
} else {
// 父进程:关闭写入端,读取管道
close(pipefd[1]);
char buffer[20];
read(pipefd[0], buffer, 20);
printf("Parent received: %s\n", buffer);
close(pipefd[0]);
}
return EXIT_SUCCESS;
}
在上面的例子中,我们首先调用 pipe
函数创建了一个管道,并获得了两个文件描述符:pipefd[0]
(读取端)和 pipefd[1]
(写入端)。然后,我们使用 fork
创建了一个子进程。在子进程中,我们关闭了读取端,并通过管道发送了一条消息。在父进程中,我们关闭了写入端,并从管道中读取了子进程发送的消息。
使用 pipe
时需要注意的事项:
- 管道是半双工的,数据只能在一个方向上流动。
- 管道是匿名的,只能在创建它的进程及其子进程之间使用。
- 使用完管道后,应该使用
close
函数关闭管道的两个端点。 - 管道通常用于轻量级的进程间通信,特别是当通信双方有直接的亲缘关系时。
pipe
是进程间通信(IPC)的一种基本方式,特别适用于需要单向数据传输的场景。
popen
popen
函数是 C 标准库中的一个函数,用于从 C 程序中执行一个命令并打开一个与之连接的进程。这通常用于在程序中运行 shell 命令,并从标准输出或标准输入中读取数据。
函数原型:
FILE *popen(const char *command, const char *type);
参数:
command
:一个指向以 null 结尾的字符串的指针,表示要执行的命令。type
:一个指向以 null 结尾的字符串的指针,表示打开管道的模式。可以是以下两种模式之一:"r"
:读取模式,从子进程的标准输出读取数据。"w"
:写入模式,向子进程的标准输入写入数据。
返回值:
- 成功时,
popen
返回一个指向新打开的流的FILE
指针,可以像使用标准 I/O 流一样使用这个指针。 - 如果调用失败,返回
NULL
并设置全局变量errno
以指示错误。
以下是使用 popen
函数的一个例子:
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *pipe = popen("ls -l", "r"); // 执行 "ls -l" 命令并读取输出
if (pipe == NULL) {
perror("popen");
return EXIT_FAILURE;
}
char buffer[1024];
while (fgets(buffer, sizeof(buffer), pipe) != NULL) {
printf("%s", buffer); // 打印从管道读取的每行
}
int status = pclose(pipe); // 关闭管道并等待子进程结束
if (status == -1) {
perror("pclose");
return EXIT_FAILURE;
}
printf("Command finished with exit status: %d\n", WEXITSTATUS(status));
return EXIT_SUCCESS;
}
在上面的例子中,我们使用 popen
执行了 ls -l
命令,并以读取模式打开了一个流。然后,我们使用 fgets
函数从这个流中读取数据,并打印输出。完成读取后,我们使用 pclose
函数关闭流,并等待子进程结束。
使用 popen
时需要注意的事项:
popen
创建的子进程默认继承了父进程的环境变量。- 使用
popen
时应该始终配对使用pclose
,以确保子进程正确结束并释放资源。 popen
可以在任何需要执行外部命令并读取其输出的场合使用,但要注意安全性,避免未经检查的输入直接作为命令执行。popen
返回的流应该使用标准 I/O 函数(如fgets
、fscanf
等)进行操作。
popen
是一种方便的方式来执行系统命令,并在 C 程序中处理命令的输出。
共享内存
当进程数量较少时,使用管道进行通信是一种直观的方法。然而,随着进程数量的增加,管道的使用和相关系统调用的数量会急剧增加,这可能导致效率问题。此外,管道通信需要数据在写入和读取时进行两次拷贝,这进一步影响了性能。为了解决这些问题,共享内存 成为了一种更高效的进程间通信方式,特别适用于大量数据的传输。
在操作系统中,每个进程都拥有自己的虚拟地址空间。当进程访问内存时,内存管理单元(MMU)负责将虚拟地址转换为物理地址。这意味着,即使两个进程使用相同的虚拟地址,它们也会被映射到不同的物理内存地址。
共享内存 允许两个或多个进程共享同一块物理存储区域。这是通过将这些进程的虚拟地址映射到相同的物理内存地址来实现的。这种方式显著减少了数据拷贝的需要,从而提高了通信效率。
为了实现内存共享,内核维护了一个专门的数据结构来存储共享内存的信息,包括共享内存的大小、权限和引用进程数量等。
在 Linux 系统中,共享内存可以通过两种常见的接口实现:System V
和 POSIX
接口。这两种接口提供了不同的 API 来创建、附着和销毁共享内存。
System V
ftok
生成一个唯一的键
ftok
函数是 UNIX 和类 UNIX 系统中用于生成一个唯一的键(key)的系统调用,这个键通常用于创建或访问共享内存段、信号量等。键是通过散列提供的文件名和项目标识符(project identifier)来生成的。
函数原型:
key_t ftok(const char *pathname, int proj_id);
参数:
pathname
:一个指向以 null 结尾的字符串的指针,表示一个文件的路径。这个文件名用于生成键的一部分。proj_id
:一个整数,作为项目标识符,用于生成键的另一部分。它通常是一个字符,但也可以是任何整数。
返回值:
- 成功时,返回一个
key_t
类型的值,这是一个用于标识生成的键的整数。 - 如果调用失败,返回
(-1)
,并设置全局变量errno
以指示错误。
ftok
函数生成的键是唯一的,因为它基于文件名和项目标识符。如果 pathname
参数指向的文件存在,并且 proj_id
是唯一的,那么生成的键将是一个唯一的值。
以下是使用 ftok
函数的一个例子:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main() {
const char *filename = "example_file";
int proj_id = 'a'; // 项目标识符,可以是任意整数
key_t key;
// 使用 ftok 生成键
key = ftok(filename, proj_id);
if (key == -1) {
perror("ftok");
return 1;
}
printf("The generated key is: %d\n", key);
// 使用 key 创建共享内存段...
// shmget(key, ...)
return 0;
}
在上面的例子中,我们使用 ftok
函数和指定的文件名及项目标识符来生成一个键。如果 ftok
调用失败,我们打印错误信息并返回。否则,我们打印出生成的键。
使用 ftok
时需要注意的事项:
ftok
函数通常与shmget
或semget
函数一起使用,用于创建或访问共享资源。- 项目标识符
proj_id
应该是唯一的,以确保生成的键的唯一性。 - 如果
pathname
参数指向的文件不存在,ftok
函数仍然可以生成一个键,但通常建议使用实际存在的文件。 ftok
函数生成的键可能在不同的系统或文件系统上有所不同。
shmget
创建共享内存段
shmget
函数是 POSIX 标准中用于创建或获取一个共享内存段的系统调用。共享内存是一种进程间通信(IPC)机制,允许多个进程通过映射同一个内存区域来共享数据。
函数原型:
int shmget(key_t key, size_t size, int shmflg);
参数:
key
:一个key_t
类型的值,用于标识共享内存段。这个键可以由ftok
函数生成,或者使用一些预定义的键值(如IPC_PRIVATE
)。-
size
:共享内存段的大小,以字节为单位。 -
shmflg
:共享内存段的权限和其他控制标志,可以包括:- 权限掩码(如
0666
),表示共享内存段的访问权限。 IPC_CREAT
:如果共享内存段不存在,创建一个新的共享内存段。IPC_EXCL
:与IPC_CREAT
一起使用,如果共享内存段已经存在,则shmget
调用失败。
- 权限掩码(如
返回值:
- 成功时,返回共享内存段的标识符(一个非负整数)。
- 如果调用失败,返回
-1
,并设置全局变量errno
以指示错误。
以下是使用 shmget
函数的一个例子:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main() {
key_t key = ftok("somefile", 'a'); // 创建一个键
size_t size = 1024; // 共享内存段的大小
int shmflg = 0666 | IPC_CREAT; // 设置权限和创建标志
int shmid = shmget(key, size, shmflg); // 创建或获取共享内存段
if (shmid == -1) {
perror("shmget");
return 1;
}
printf("Shared memory segment created with ID: %d\n", shmid);
// 使用共享内存段...
// 完成后清理共享内存段
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl");
}
return 0;
}
在上面的例子中,我们首先使用 ftok
函数创建了一个键。然后,我们调用 shmget
函数,请求创建一个大小为 1024 字节的共享内存段,并设置权限。如果 shmget
调用成功,我们打印出共享内存段的标识符。使用完共享内存段后,我们使用 shmctl
函数与 IPC_RMID
命令清理共享内存段,释放资源。
使用 shmget
时需要注意的事项:
- 共享内存段必须由有足够权限的进程创建,通常需要管理员权限。
- 共享内存段的权限掩码与文件权限类似,用于控制哪些用户可以访问共享内存。
- 使用
IPC_PRIVATE
作为key
可以创建一个私有的共享内存段,这个内存段不能被其他进程通过shmget
访问。 - 共享内存段的生命周期应该由创建者管理,包括在适当的时候清理资源,避免内存泄漏。
查看共享内存命令
ipcs
命令是一个用于监控系统中 IPC 资源状态的工具。通过这个命令,用户可以查看共享内存段、信号量集和消息队列的详细信息。
运行 ipcs
命令将显示当前系统中所有 IPC 资源的概览,包括它们的键(key)、标识符(shmid)、拥有者(owner)、权限(perms)、大小(bytes)、连接数(nattch)和状态(status)。
使用 ipcs -l
命令可以查看系统对 IPC 资源的限制,如共享内存的最大段数、信号量的最大个数等。
当 IPC 资源不再需要时,可以使用 ipcrm
命令手动删除它们。例如,要删除特定的共享内存段,可以使用以下命令:
ipcrm -m shmid
这里,shmid
是共享内存段的标识符。
shmat
获取共享内存段
shmat
函数是 POSIX 标准中用于将一个共享内存段附加(attach)到当前进程的地址空间的系统调用。一旦附加,进程就可以像访问普通内存一样读写共享内存段中的数据,实现进程间通信。
函数原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
shmid
:共享内存段的标识符,由shmget
函数创建并返回。-
shmaddr
:一个可选参数,指向进程希望附加共享内存的地址。如果设置为NULL
,系统将选择一个合适的地址。 -
shmflg
:控制共享内存附加操作的标志,可以是以下选项:SHM_RDONLY
:以只读方式附加共享内存。SHM_RND
:共享内存将附加到shmaddr
指定的地址或按页面大小对齐的地址。SHM_REMAP
:忽略shmaddr
参数,允许系统选择地址。
返回值:
- 成功时,
shmat
返回共享内存段在当前进程地址空间中的地址。 - 如果调用失败,返回
((void *)-1)
,并设置全局变量errno
以指示错误。
以下是使用 shmat
函数的一个例子:
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main() {
key_t key = ftok("somefile", 'a'); // 创建一个键
int shmid = shmget(key, 1024, IPC_CREAT | 0666); // 创建共享内存段
if (shmid == -1) {
perror("shmget");
return EXIT_FAILURE;
}
void *shmaddr = shmat(shmid, NULL, 0); // 附加共享内存
if (shmaddr == (void *)-1) {
perror("shmat");
shmctl(shmid, IPC_RMID, NULL); // 清理共享内存段
return EXIT_FAILURE;
}
printf("Shared memory attached at address: %p\n", shmaddr);
// 使用共享内存...
// 完成后分离共享内存
if (shmdt(shmaddr) == -1) {
perror("shmdt");
}
return EXIT_SUCCESS;
}
在上面的例子中,我们使用 shmget
创建了一个共享内存段,并使用 shmat
将其附加到当前进程的地址空间。如果 shmat
调用成功,我们打印出共享内存的地址。使用完共享内存后,我们使用 shmdt
分离共享内存。
使用 shmat
时需要注意的事项:
- 共享内存段必须先由
shmget
创建。 shmat
返回的地址应该用于访问共享内存。- 使用完共享内存后,应该使用
shmdt
分离共享内存段。 - 分离后,可以使用
shmctl
与IPC_RMID
命令清理共享内存段,释放资源。 - 共享内存、信号量和消息队列等 IPC 资源,即使创建它们的进程已经终止,也不会自动释放。它们需要显式地通过相应的系统调用来清理,或者使用
ipcs
命令查看这些资源的信息。
共享内存通信
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/wait.h>
int main() {
key_t key = ftok("shared_mem", 'a'); // 创建一个键
int shmid = shmget(key, 1024, IPC_CREAT | 0666); // 创建共享内存段
if (shmid == -1) {
perror("shmget");
exit(EXIT_FAILURE);
}
// 父进程创建子进程
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
// 子进程
void *shared_memory = shmat(shmid, NULL, 0); // 附加共享内存
if (shared_memory == (void *)-1) {
perror("shmat");
exit(EXIT_FAILURE);
}
printf("Child process is reading from shared memory.\n");
sleep(2); // 等待父进程写入数据
printf("Message from parent: %s\n", (char *)shared_memory);
// 子进程完成读取,分离共享内存
shmdt(shared_memory);
exit(EXIT_SUCCESS); // 子进程退出
} else {
// 父进程
void *shared_memory = shmat(shmid, NULL, 0); // 附加共享内存
if (shared_memory == (void *)-1) {
perror("shmat");
exit(EXIT_FAILURE);
}
printf("Parent process is writing to shared memory.\n");
sleep(1); // 短暂的延时,确保子进程先准备好读取
strcpy(shared_memory, "Hello, child process!"); // 写入数据
// 父进程完成写入,分离共享内存
shmdt(shared_memory);
wait(NULL); // 等待子进程退出
}
// 清理共享内存段
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl");
exit(EXIT_FAILURE);
}
return EXIT_SUCCESS;
}
在这个例子中,我们首先使用 ftok
创建了一个唯一的键,然后用 shmget
来创建一个大小为 1024 字节的共享内存段。父进程创建了一个子进程,子进程通过 shmat
附加共享内存段,并等待父进程写入数据。父进程也通过 shmat
附加相同的共享内存段,并写入一个字符串。子进程读取这个字符串并打印出来。
关键点说明:
- 使用
ftok
函数创建的键是依赖于文件系统中的文件名和项目 ID 的,所以确保shared_mem
文件存在于文件系统中。 - 父进程和子进程都调用了
shmat
来附加共享内存,附加成功后返回的是共享内存在进程地址空间中的地址。 - 子进程使用
sleep
来等待,确保父进程先写入数据。 - 父进程写入数据后,子进程读取并打印出来。
- 使用
shmdt
来分离共享内存,表示不再需要访问。 - 最后,父进程使用
shmctl
与IPC_RMID
命令来清理共享内存段。
shmdt
解除共享内存映射
shmdt
函数是 POSIX 标准中用于从当前进程的地址空间分离(detach)一个共享内存段的系统调用。当一个共享内存段被 shmat
附加到进程地址空间后,进程就可以访问该内存段。一旦进程完成了对共享内存的操作,它通常会调用 shmdt
来分离该内存段,从而释放相关资源。
函数原型:
int shmdt(const void *shmaddr);
参数:shmaddr
:一个指向共享内存段在当前进程地址空间中起始地址的指针。这个地址是由 shmat
函数返回的。
返回值:
- 成功时,返回 0。
- 如果调用失败,返回 -1,并设置全局变量
errno
以指示错误。
分离共享内存段是一个重要的操作,因为它可以减少内存占用并避免潜在的资源泄露。当共享内存段被分离后,它仍然存在于系统之中,直到没有任何进程附加它,并且最后一个分离它的进程调用了 shmctl
与 IPC_RMID
命令来显式地删除它。
以下是使用 shmdt
函数的一个例子:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main() {
// 假设已经创建了共享内存段并得到了 shmid
int shmid;
// ... 创建共享内存段的代码 ...
// 附加共享内存段
void *shared_memory = shmat(shmid, NULL, 0);
if (shared_memory == (void *)-1) {
perror("shmat");
return 1;
}
// 使用共享内存...
// 分离共享内存段
if (shmdt(shared_memory) == -1) {
perror("shmdt");
// 清理共享内存段
shmctl(shmid, IPC_RMID, NULL);
return 1;
}
printf("Shared memory detached successfully.\n");
// 清理共享内存段
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl");
return 1;
}
return 0;
}
在上面的例子中,我们首先使用 shmat
附加了一个共享内存段。使用完毕后,我们调用 shmdt
来分离共享内存。如果 shmdt
调用失败,我们打印错误信息并清理共享内存段。分离成功后,我们打印一条成功消息。最后,我们使用 shmctl
与 IPC_RMID
命令来清理共享内存段。
使用 shmdt
时需要注意的事项:
- 必须在调用
shmdt
分离共享内存后,才能调用shmctl
与IPC_RMID
命令来删除共享内存段。 - 如果
shmdt
调用失败,应该检查errno
来确定错误原因,可能是由于提供的地址不是由shmat
返回的有效地址。 - 分离共享内存是一个资源管理的重要步骤,应该总是在不再需要共享内存时进行。
shmctl
修改共享内存属性
shmctl
函数是 POSIX 标准中用于对共享内存段进行控制操作的系统调用。它可以用于获取共享内存段的信息、设置共享内存段的属性,或者删除共享内存段。
函数原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid
:共享内存段的标识符,由shmget
函数返回。cmd
:指定要执行的操作。可以是以下命令之一:IPC_STAT
:获取共享内存段的状态信息,将信息存储在buf
指向的结构中。IPC_SET
:设置共享内存段的属性,属性在buf
指向的结构中指定。IPC_RMID
:删除共享内存段,释放所有相关资源。
buf
:指向struct shmid_ds
的指针,这个结构用于存储共享内存段的信息或设置。具体作用取决于cmd
参数。
返回值:
- 成功时,返回 0。
- 如果调用失败,返回 -1,并设置全局变量
errno
以指示错误。
以下是使用 shmctl
函数的一些示例:
获取共享内存段的状态信息:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main() {
// 假设已经创建了共享内存段并得到了 shmid
int shmid;
// 获取共享内存段的状态信息
struct shmid_ds shminfo;
if (shmctl(shmid, IPC_STAT, &shminfo) == -1) {
perror("shmctl IPC_STAT");
return 1;
}
printf("Size of shared memory: %ld\n", shminfo.shm_segsz);
// 打印其他状态信息...
return 0;
}
删除共享内存段
#include <sys/ipc.h>
#include <sys/shm.h>
int main() {
// 假设已经创建了共享内存段并得到了 shmid
int shmid;
// 删除共享内存段
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl IPC_RMID");
return 1;
}
printf("Shared memory segment deleted successfully.\n");
return 0;
}
在使用 shmctl
时需要注意的事项:
- 使用
IPC_STAT
时,需要提供一个有效的struct shmid_ds
指针来接收共享内存段的状态信息。 - 使用
IPC_SET
时,需要提供一个指向包含新设置的struct shmid_ds
结构的指针。这通常用于改变共享内存段的权限或所有者。 - 使用
IPC_RMID
时,不需要buf
参数,因为只需要共享内存段的标识符来删除它。 - 只有当所有进程都分离了共享内存段,并且没有任何进程引用它时,
IPC_RMID
命令才能成功执行。 shmctl
是管理共享内存生命周期的重要工具,应谨慎使用,特别是在删除共享内存段时,以避免潜在的数据丢失。
POSIX
以下是一个使用 POSIX 共享内存 API 的 C 语言示例,展示了如何在父子进程间通过共享内存进行通信。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
// 1. 使用 shm_open 创建或打开共享内存对象
int shm_fd = shm_open("/test", O_CREAT | O_RDWR, 0666);
// 2. 使用 ftruncate 设置共享内存对象的大小
ftruncate(shm_fd, 4096);
// 3. 使用 mmap 将共享内存映射到进程地址空间
void *ptr = mmap(0, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
// 4. 通过 fork 创建新进程
pid_t pid = fork();
if (pid == 0) {
// 5. 子进程向共享内存写入数据
sprintf((char *)ptr, "Hello from child!");
} else {
// 6. 父进程等待子进程结束
wait(NULL);
// 7. 父进程从共享内存读取数据
printf("Shared memory: %s\n", (char *)ptr);
}
// 8. 使用 munmap 解除映射
munmap(ptr, 4096);
// 9. 使用 shm_unlink 删除共享内存对象
shm_unlink("/test");
return 0;
}
编译注意事项:编译上述代码时,需要链接实时库(rt),因为 shm_open
等函数属于实时编程库。例如,使用 gcc 编译器时,应使用以下命令:
gcc test.c -o test -lrt
shm_open
函数的第一个参数是共享内存对象的名称,它不同于文件系统中的路径。该名称应以 /
开头,用于唯一标识共享内存对象。这与 ftok
函数使用的键值不同,ftok
基于文件和项目标识符生成键。
同时写入
共享内存和竞争条件示例
共享内存允许多个进程访问和修改同一数据区域,这要求开发者注意同步和数据一致性问题。以下是一个简单的 C 语言示例,演示了使用 System V 共享内存 API 的潜在问题。
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
// 创建一个新的共享内存段
int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0600);
if (shmid < 0) {
perror("shmget");
exit(1);
}
// 将共享内存附加到进程地址空间
int *p = (int *)shmat(shmid, NULL, 0);
if ((long)p == -1) {
perror("shmat");
exit(1);
}
// 初始化共享内存中的变量
*p = 0;
// 创建子进程
pid_t pid = fork();
if (pid == 0) {
// 子进程执行循环,递增共享内存中的值
for (int i = 0; i < 10000000; i++) {
(*p)++;
}
} else {
// 父进程也执行循环,递增共享内存中的值
for (int i = 0; i < 10000000; i++) {
(*p)++;
}
// 等待子进程结束
wait(NULL);
// 打印最终结果
printf("%d\n", *p);
}
// 从进程地址空间分离共享内存
shmdt(p);
// 删除共享内存段
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
执行上述程序,可能会发现结果与预期的 200000 不符。这是因为发生了 竞争条件(Race Condition),即多个进程同时访问和修改同一资源,而没有适当的同步机制来保证操作的原子性。
当父进程尝试读取共享资源到寄存器时,可能会发生上下文切换到子进程。如果子进程此时修改了该资源并写回共享内存,当控制权返回到父进程时,父进程可能会基于旧的寄存器值继续执行,导致子进程的修改丢失。这种情况需要通过使用锁或其他同步机制来避免。