Linux 进程间通信

管道

进程间通信(IPC)是多任务操作系统中的关键功能,允许进程之间交换信息。一种自然的 IPC 方式是利用文件系统作为中介:

基于文件的通信:在这种方式中,一个进程通过打开并读写文件来存储信息,而另一个进程则打开同一个文件来读取这些信息。尽管这种方法简单直观,但它依赖于磁盘 I/O 操作,这可能导致较低的通信效率。

为了提高通信效率,操作系统提供了一种特殊的文件系统对象,即 命名管道(Named Pipe,FIFO):

命名管道:这是一种特殊的文件,存在于文件系统中,但它允许进程通过内核维护的缓冲区直接交换数据,而不是通过磁盘。这种方式减少了磁盘 I/O 操作,从而提高了通信效率。

对于存在亲缘关系的进程,它们可以使用另一种 IPC 机制,即 匿名管道

匿名管道:与有名管道不同,匿名管道不需要在文件系统中创建可见的文件。它们是在进程执行过程中动态创建的,用于连接亲缘进程(如父子进程),并在不需要时自动销毁。匿名管道通常用于简单的、短暂的通信任务。

命名管道

Linux 管道和 IO 多路复用

匿名管道

在 Linux 系统中,匿名管道 是一种用于实现父子进程间通信的简单机制。通过 pipe 函数,可以在父子进程之间建立一个单向数据传输通道。

管道的特性

  • 单向性:管道是单向的,数据只能在一个方向上流动。这意味着使用匿名管道时,一个进程将数据写入管道的写端(pipe[1]),而另一个进程则从管道的读端(pipe[0])读取数据。
  • 半双工通信:由于管道的单向性,它是一种半双工通信方式,即在任意时刻只能进行发送或接收操作,而不是同时进行。
  • 亲缘关系限定:匿名管道主要用于存在亲缘关系的进程间通信,例如父子进程。

管道的创建和使用

  • 在一个进程中首先调用 pipe 函数创建管道,然后通过 fork 函数创建子进程。这样,父子进程就可以通过管道的读写端进行通信。
  • 单个进程使用管道没有太大价值,因为管道的主要目的是实现进程间的通信。

全双工通信

  • 为了实现父子进程之间的全双工通信(即同时发送和接收数据),需要调用 pipe 函数两次,创建两条管道:一条用于发送数据,另一条用于接收数据。

pipe_process.png

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);

参数:

  1. command:一个指向以 null 结尾的字符串的指针,表示要执行的命令。
  2. 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 函数(如 fgetsfscanf 等)进行操作。

popen 是一种方便的方式来执行系统命令,并在 C 程序中处理命令的输出。

共享内存

当进程数量较少时,使用管道进行通信是一种直观的方法。然而,随着进程数量的增加,管道的使用和相关系统调用的数量会急剧增加,这可能导致效率问题。此外,管道通信需要数据在写入和读取时进行两次拷贝,这进一步影响了性能。为了解决这些问题,共享内存 成为了一种更高效的进程间通信方式,特别适用于大量数据的传输。

在操作系统中,每个进程都拥有自己的虚拟地址空间。当进程访问内存时,内存管理单元(MMU)负责将虚拟地址转换为物理地址。这意味着,即使两个进程使用相同的虚拟地址,它们也会被映射到不同的物理内存地址。

共享内存 允许两个或多个进程共享同一块物理存储区域。这是通过将这些进程的虚拟地址映射到相同的物理内存地址来实现的。这种方式显著减少了数据拷贝的需要,从而提高了通信效率。

为了实现内存共享,内核维护了一个专门的数据结构来存储共享内存的信息,包括共享内存的大小、权限和引用进程数量等。

在 Linux 系统中,共享内存可以通过两种常见的接口实现:System VPOSIX 接口。这两种接口提供了不同的 API 来创建、附着和销毁共享内存。

System V

ftok 生成一个唯一的键

ftok 函数是 UNIX 和类 UNIX 系统中用于生成一个唯一的键(key)的系统调用,这个键通常用于创建或访问共享内存段、信号量等。键是通过散列提供的文件名和项目标识符(project identifier)来生成的。

函数原型:

key_t ftok(const char *pathname, int proj_id);

参数:

  1. pathname:一个指向以 null 结尾的字符串的指针,表示一个文件的路径。这个文件名用于生成键的一部分。
  2. 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 函数通常与 shmgetsemget 函数一起使用,用于创建或访问共享资源。
  • 项目标识符 proj_id 应该是唯一的,以确保生成的键的唯一性。
  • 如果 pathname 参数指向的文件不存在,ftok 函数仍然可以生成一个键,但通常建议使用实际存在的文件。
  • ftok 函数生成的键可能在不同的系统或文件系统上有所不同。

shmget 创建共享内存段

shmget 函数是 POSIX 标准中用于创建或获取一个共享内存段的系统调用。共享内存是一种进程间通信(IPC)机制,允许多个进程通过映射同一个内存区域来共享数据。

函数原型:

int shmget(key_t key, size_t size, int shmflg);

参数:

  1. key:一个 key_t 类型的值,用于标识共享内存段。这个键可以由 ftok 函数生成,或者使用一些预定义的键值(如 IPC_PRIVATE)。

  2. size:共享内存段的大小,以字节为单位。

  3. 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);

参数:

  1. shmid:共享内存段的标识符,由 shmget 函数创建并返回。

  2. shmaddr:一个可选参数,指向进程希望附加共享内存的地址。如果设置为 NULL,系统将选择一个合适的地址。

  3. 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 分离共享内存段。
  • 分离后,可以使用 shmctlIPC_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 来分离共享内存,表示不再需要访问。
  • 最后,父进程使用 shmctlIPC_RMID 命令来清理共享内存段。

shmdt 解除共享内存映射

shmdt 函数是 POSIX 标准中用于从当前进程的地址空间分离(detach)一个共享内存段的系统调用。当一个共享内存段被 shmat 附加到进程地址空间后,进程就可以访问该内存段。一旦进程完成了对共享内存的操作,它通常会调用 shmdt 来分离该内存段,从而释放相关资源。

函数原型:

int shmdt(const void *shmaddr);

参数:shmaddr:一个指向共享内存段在当前进程地址空间中起始地址的指针。这个地址是由 shmat 函数返回的。

返回值:

  • 成功时,返回 0。
  • 如果调用失败,返回 -1,并设置全局变量 errno 以指示错误。

分离共享内存段是一个重要的操作,因为它可以减少内存占用并避免潜在的资源泄露。当共享内存段被分离后,它仍然存在于系统之中,直到没有任何进程附加它,并且最后一个分离它的进程调用了 shmctlIPC_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 调用失败,我们打印错误信息并清理共享内存段。分离成功后,我们打印一条成功消息。最后,我们使用 shmctlIPC_RMID 命令来清理共享内存段。

使用 shmdt 时需要注意的事项:

  • 必须在调用 shmdt 分离共享内存后,才能调用 shmctlIPC_RMID 命令来删除共享内存段。
  • 如果 shmdt 调用失败,应该检查 errno 来确定错误原因,可能是由于提供的地址不是由 shmat 返回的有效地址。
  • 分离共享内存是一个资源管理的重要步骤,应该总是在不再需要共享内存时进行。

shmctl 修改共享内存属性

shmctl 函数是 POSIX 标准中用于对共享内存段进行控制操作的系统调用。它可以用于获取共享内存段的信息、设置共享内存段的属性,或者删除共享内存段。

函数原型:

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数:

  1. shmid:共享内存段的标识符,由 shmget 函数返回。
  2. cmd:指定要执行的操作。可以是以下命令之一:
    • IPC_STAT:获取共享内存段的状态信息,将信息存储在 buf 指向的结构中。
    • IPC_SET:设置共享内存段的属性,属性在 buf 指向的结构中指定。
    • IPC_RMID:删除共享内存段,释放所有相关资源。
  3. 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),即多个进程同时访问和修改同一资源,而没有适当的同步机制来保证操作的原子性。

当父进程尝试读取共享资源到寄存器时,可能会发生上下文切换到子进程。如果子进程此时修改了该资源并写回共享内存,当控制权返回到父进程时,父进程可能会基于旧的寄存器值继续执行,导致子进程的修改丢失。这种情况需要通过使用锁或其他同步机制来避免。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇