本文最后更新于 2025-06-25,学习久了要注意休息哟

第五章 Linux 进程间通信

5.1 无名管道

  1. 如果写端存在写管道

  2. 写入的数据量可以是任意多,直到管道写满(通常为 64KB)。此时写操作将阻塞,等待读端消费数据。

  3. 当管道中的数据被读取时,写端操作解除阻塞。

  4. 如果读端不存在写管道

  5. 当管道的读端不存在时(即写端尝试写数据而没有进程在读取),==管道破裂==并产生一个 SIGPIPE 信号,导致写进程异常终止。

  6. 如果写端存在读管道

  7. 读端可以读取任意多的数据。如果读端尝试读取数据而管道中没有可用数据时,读操作将阻塞,直到写端写入数据。

  8. 如果写端不存在读管道

  9. 如果写端不存在,读端尝试读取数据时将立刻返回并结束读取操作。这种情况下,读取是非阻塞的。

读端 不存在 写端存在  ==> 管道破裂
写端 不存在 读端存在  ==> 立刻返回

无名管道只能在亲缘进程间通讯

5.1.1 相关函数

#include <unistd.h>

int pipe(int pipefd[2]);
功能: 
    创建一个无名管道,用于在具有亲缘关系的进程(如父子进程)之间进行单向通信。管道创建后,返回两个文件描述符,分别用于读端和写端。

参数: 
    pipefd: 一个包含两个整数的数组,`pipefd[0]` 是管道的读端,`pipefd[1]` 是管道的写端。

返回值: 
    成功: 返回 0。
    失败: 返回 -1,并设置相应的错误码。
```
在进行进程间通讯的时候, 一个进程只能拥有 读端 或者 写端
例如 拥有读端的管道 必须关闭写端

5.1.2 实例代码

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main() {
    int pipefd[2];
    pid_t pid;
    char buffer[100];

    // 创建无名管道
    if (pipe(pipefd) == -1) {
        perror("pipe");
        return 1;
    }

    // 创建子进程
    pid = fork();

    if (pid == -1) {
        perror("fork");
        return 1;
    }

    if (pid == 0) {  // 子进程
        close(pipefd[0]); // 关闭读端
        const char *msg = "Hello from child process!";
        write(pipefd[1], msg, strlen(msg) + 1); // 向管道写入数据
        close(pipefd[1]); // 关闭写端
    } else {  // 父进程
        close(pipefd[1]); // 关闭写端
        read(pipefd[0], buffer, sizeof(buffer)); // 从管道读取数据
        printf("Parent process received: %s\n", buffer);
        close(pipefd[0]); // 关闭读端
    }

    return 0;
}

5.2 有名管道

有名管道,也称为 FIFO(First In First Out),是一种进程间通信(IPC)的机制,与无名管道不同,有名管道存在于文件系统中,因此不需要进程之间具有亲缘关系,可以跨进程进行通信。与文件类似,有名管道有一个路径名,可以通过路径在多个进程间共享。

有名管道的基本概念

有名管道与无名管道的区别在于,它存在于文件系统中,并且可以由不相关的进程通过路径名进行通信。它仍然是单向的,但可以通过两个有名管道实现双向通信。

有名管道的创建与使用

使用 mkfifo() 函数创建一个有名管道。创建后,它在文件系统中表现为一个特殊的文件,进程可以通过这个文件进行读写操作。

5.2.1 相关函数

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

功能: 
    创建一个有名管道(FIFO),在文件系统中生成一个特殊文件,用于进程间通信。

参数: 
    pathname: 有名管道的路径名(文件路径)。
    mode: 文件权限,指定管道的访问权限,如 0666 表示所有用户都可以读写。

返回值: 
    成功: 返回 0。
    失败: 返回 -1,并设置相应的错误码。

此外还可以通过 命令来创建管道文件

$mkfifo p_file

5.2.2 示例代码

创建有名管道

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>

int main() {
    const char *fifo_path = "/tmp/my_fifo";

    // 创建有名管道
    if (mkfifo(fifo_path, 0666) == -1) {
        perror("mkfifo");
        return 1;
    }

    printf("有名管道已创建: %s\n", fifo_path);
    return 0;
}

写入进程

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    const char *fifo_path = "/tmp/my_fifo";
    int fd;

    // 打开管道的写端
    fd = open(fifo_path, O_WRONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    // 向管道中写入数据
    write(fd, "Hello from writer", 17);
    close(fd);

    return 0;
}

读取进程

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    const char *fifo_path = "/tmp/my_fifo";
    int fd;
    char buffer[100];

    // 打开管道的读端
    fd = open(fifo_path, O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    // 从管道中读取数据
    read(fd, buffer, sizeof(buffer));
    printf("读取到的数据: %s\n", buffer);
    close(fd);

    return 0;
}

5.2 信号

5.2.1 常见信号

查看信号的命令

kill -l

常见信号

1. SIGHUP (1): 挂起信号,通常在用户终端断开连接时发送给控制进程,通知其重新读取配置文件。

2. SIGINT (2): 中断信号,通常在用户按下 Ctrl + C 时发送,用于终止程序。

3. SIGQUIT (3): 退出信号,通常在用户按下 Ctrl + \ 时发送,程序会生成一个核心转储文件并退出。

4. SIGILL (4): 非法指令信号,当程序执行了非法或未定义的指令时触发。

5. SIGABRT (6): 异常终止信号,通常由 `abort` 函数生成,用于非正常的程序终止。

6. SIGFPE (8): 浮点异常信号,通常在发生数学错误(如除以零)时触发。

7. SIGKILL (9): 杀死信号,无法被捕获、阻塞或忽略,立即终止进程。

8. SIGSEGV (11): 段错误信号,通常在程序尝试访问非法内存地址时触发。

9. SIGPIPE (13): 管道破裂信号,当向一个读端已关闭的管道写入数据时触发。

10. SIGALRM (14): 闹钟信号,通常由 `alarm` 函数触发,用于定时事件。

11. SIGTERM (15): 终止信号,通常用于优雅地结束进程,进程可以捕获该信号进行清理工作。

12. SIGUSR1 (10) 和 SIGUSR2 (12): 用户自定义信号,用户可以在程序中定义这些信号的用途。

13. SIGCHLD (17): 子进程状态变化信号,当子进程结束或停止时发送给父进程,僵尸进程。

14. SIGCONT (18): 继续信号,用于恢复被暂停的进程。

15. SIGSTOP (19): 停止信号,无法被捕获、阻塞或忽略,用于暂停进程。

16. SIGTSTP (20): 终端停止信号,通常在用户按下 Ctrl + Z 时发送,用于暂停进程。

17. SIGTTIN (21) 和 SIGTTOU (22): 后台进程尝试读取或写入终端时触发,通常会暂停进程。

18. SIGBUS (7): 总线错误信号,通常在进程尝试访问未对齐的内存地址时触发。

19. SIGPOLL (29): 轮询信号,表示一个事件在I/O设备上发生。

20. SIGSYS (31): 非法系统调用信号,当进程尝试执行未实现的系统调用时触发。

5.2.2 常用函数

发送信号:

kill(pid, sig):向指定进程发送信号 sig。扩展 atoi函数的使用
raise(sig):向自身进程发送信号 sig。

捕获信号:

signal(sig, handler):设置信号 sig 的处理函数为 handler。
sigaction(sig, &act, &oldact):安装对信号 sig 的处理函数,并获取旧的处理函数。

阻塞和解除阻塞:

sigprocmask(how, &set, &oldset):设置和修改进程的信号屏蔽字,控制对信号的屏蔽和解除屏蔽。
sigpending(&set):获取当前正在等待处理的信号集合。

其他:

pause():挂起进程,直到接收到一个信号。
alarm(seconds):设置定时器,在指定秒数后向进程发送 SIGALRM 信号。

1、发送信号

kill函数

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

功能: 
    用于向指定进程发送信号,可以用于终止、暂停或其他操作。

参数: 
    pid: 目标进程的进程ID。如果为正数,则向该PID对应的进程发送信号;
         如果为0,则向调用进程所在的进程组发送信号;
         如果为-1,则向系统中所有有权限接收到此信号的进程发送信号(但不包括自身);
         如果小于-1,则向目标进程组(由pid的绝对值表示的进程组ID)发送信号。

    sig: 要发送的信号类型,如 SIGKILL, SIGTERM 等。

返回值: 
    成功: 返回0。
    失败: 返回-1,并设置相应的错误码。

raise函数

#include <signal.h>

int raise(int sig);

功能: 
    用于向当前进程发送指定的信号,相当于对当前进程自身调用 `kill(getpid(), sig)`。

参数: 
    sig: 要发送的信号类型,如 SIGINT, SIGTERM 等。

返回值: 
    成功: 返回0。
    失败: 返回非0值。

2、捕获信号

signal函数

#include <signal.h>

typedef void (*sighandler_t)(int); // 函数指针
sighandler_t signal(int signum, sighandler_t handler);

功能: 
    设置对指定信号的处理方式。可以将信号处理设置为忽略、捕获处理或恢复默认行为。
    需要注意的是 这个函数是对信号进行 注册 也就是设置了就好,后面的程序会遇到这个信号发送中断
参数: 
    signum: 要处理的信号编号,如 SIGINT, SIGTERM 等。
    handler: 信号处理函数的指针。
        - 函数指针 当收到对应信号的时候 会运行 所指向的函数
        - SIG_IGN 忽略对应的信号
        - SIG_DFL 保持默认状态

返回值: 
    成功: 返回之前的信号处理函数指针。
    失败: 返回 SIG_ERR,并设置相应的错误码。

sigaction 函数

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);


功能: 
    用于检查或更改指定信号的处理方式。相比 `signal` 函数,`sigaction` 提供了更强大的功能和更精细的控制。

参数: 
    signum: 要处理的信号编号,如 SIGINT, SIGTERM 等。
    act: 指向一个 `struct sigaction` 结构的指针,用于指定新的信号处理行为。如果为 NULL,则表示不修改当前行为。
    oldact: 如果不为 NULL,用于存储先前的信号处理行为。

返回值: 
    成功: 返回 0。
    失败: 返回 -1,并设置相应的错误码。

结构体

struct sigaction 结构体:

struct sigaction {
    void     (*sa_handler)(int);         // 指向信号处理函数的指针或特殊值 (SIG_IGN, SIG_DFL)
    void     (*sa_sigaction)(int, siginfo_t *, void *); // 信号处理函数(带更多参数)
    sigset_t sa_mask;                    // 在处理信号时需要阻塞的信号集
    int      sa_flags;                   // 标志,指定信号处理选项,如 SA_RESTART, SA_SIGINFO 等
    void     (*sa_restorer)(void);       // 已废弃,通常设为 NULL
};

结构体解释

sa_handler:
    类型:void (*)(int)
    功能:指向信号处理函数的指针,用于指定处理接收到的信号的函数。
    值:可以是一个自定义函数的指针,或者是两个特殊的宏值:
    SIG_IGN:忽略该信号。
    SIG_DFL:采用默认的信号处理方式。

sa_sigaction:
    类型:void (*)(int, siginfo_t *, void *)
    功能:这是一个信号处理函数的指针,与 sa_handler 类似,但可以提供更多的上下文信息。使用时,需要在 sa_flags 中指定 SA_SIGINFO 标志。
    参数:
        int:信号编号。
        siginfo_t *:指向包含信号信息的 siginfo_t 结构的指针。
        void *:上下文信息,一般用于保存信号处理时的 CPU 状态。

sa_mask:
    类型:sigset_t
    功能:信号集,指定在信号处理程序执行期间需要阻塞的其他信号,防止它们中断当前的信号处理。

sa_flags:
    类型:int
    功能:标志位,用于指定信号处理的一些选项。常见的标志有:
    SA_RESTART:当信号处理程序执行完毕后,使被信号中断的系统调用自动重新启动。
    SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理程序。
    SA_NOCLDSTOP:防止子进程暂停或继续的状态更改导致 SIGCHLD 信号。
    SA_NOCLDWAIT:防止僵尸进程的产生。

sa_restorer:
    类型:void (*)(void)
    功能:已废弃,不再使用。通常设为 NULL。
    历史上,这个字段用于在信号处理程序返回时恢复信号处理的环境。

参数传递规则

在信号处理函数中,无法直接传递自定义参数。信号处理函数的调用由操作系统内核管理,当信号发生时,操作系统会调用你注册的信号处理函数,并按照标准定义的格式传递固定的参数

格式如下:

void custom_signal_handler(int sig, siginfo_t *info, void *ucontext);

参数介绍
1. int sig 参数
    表示接收到的信号的编号。
    例如,如果信号是 SIGINT,则 sig 的值为 SIGINT 的编号(通常为 2)。
2. siginfo_t *info 参数
    info 是一个指向 siginfo_t 结构体的指针,siginfo_t 是一个包含关于信号的附加信息的结构体。
    结构体 siginfo_t 的部分成员:
        si_signo:信号编号。
        si_errno:如果有错误发生,此成员将保存错误码。
        si_code:描述信号来源的代码(例如用户生成、内核生成等)。
        si_pid:发送信号的进程 ID 。
        si_uid:发送信号的用户 ID 。
        si_value:传递的信号数据(若使用 sigqueue() 等函数发送信号)。
3. void *ucontext 参数
    ucontext 是一个指向 ucontext_t 结构体的指针,保存信号处理时的 CPU 上下文信息。
    一般情况下,这个参数可以忽略,但在某些高级应用中,它可以用于获取信号发生时的寄存器状态等信息。

5.2.3 示例代码

#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>

// 自定义信号处理程序
void custom_signal_handler(int sig, siginfo_t *info, void *ucontext) {
    printf("收到信号 %d\n", sig);  // 打印接收到的信号编号
    printf("信号来自进程: %d\n", info->si_pid);  // 打印发送信号的进程ID
}

int main() {
    struct sigaction sa;

    // 清空结构体,将所有字段初始化为 0
    memset(&sa, 0, sizeof(sa));

    // 使用 sa_sigaction 处理信号,并设置 SA_SIGINFO 标志
    sa.sa_sigaction = custom_signal_handler;
    sa.sa_flags = SA_SIGINFO;

    // 设置处理 SIGINT 信号
    sigaction(SIGINT, &sa, NULL);

    printf("按下 Ctrl+C 以发送 SIGINT 信号...\n");

    // 无限循环等待信号
    while (1) {
        sleep(1);  // 每秒休眠一次
    }

    return 0;
}

3、信号集

一个信号集(sigset_t 类型)可以包含多个信号。信号集用于表示一组信号,可以通过信号集操作函数(如 sigaddsetsigdelsetsigemptysetsigfillset 等)来添加、删除和管理这些信号。

信号集的特性

  • 信号集可以包含多个信号:例如,一个信号集可以同时包含 SIGINTSIGTERMSIGUSR1 等多个信号。
  • 信号集操作函数用于管理信号集:通过这些函数,可以动态地向信号集中添加或删除信号。
  • 信号集通常用于控制进程对信号的阻塞和解除阻塞:通过 sigprocmask 等函数,可以将信号集中的信号设置为阻塞或解除阻塞状态。

sigprocmask函数

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

功能: 
    用于检查或更改当前进程的信号屏蔽字,控制进程可以阻塞哪些信号。信号屏蔽字用于指示进程应阻塞(忽略)哪些信号。

参数: 
    how: 指定如何修改信号屏蔽字。常用值包括:
        SIG_BLOCK: 将 `set` 中的信号添加到当前信号屏蔽字中(阻塞这些信号)。
                    将指定的信号添加到当前的信号屏蔽集中,阻止这些信号被传递到进程。

        SIG_UNBLOCK: 从当前信号屏蔽字中移除 `set` 中的信号(解除阻塞)。
                    从当前的信号屏蔽集中移除指定的信号,使它们可以被传递到进程并由信号处理程序处理。

        SIG_SETMASK: 将当前信号屏蔽字设置为 `set` 中的信号。
                    将当前的信号屏蔽集替换为新的信号屏蔽集。


    set: 指向一个 `sigset_t` 结构体的指针,表示要修改的信号集合。如果为 NULL,则不修改当前屏蔽字。

    oldset: 如果不为 NULL,存储修改前的信号屏蔽字。

返回值: 
    成功: 返回 0。
    失败: 返回 -1,并设置相应的错误码。

示例程序

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void signal_handler(int sig) {
    printf("接收到信号 %d\n", sig);
}

int main() {
    sigset_t new_set, old_set;

    // 初始化信号集
    sigemptyset(&new_set);
    sigaddset(&new_set, SIGINT);  // 将 SIGINT 添加到信号集

    // 设置信号处理程序
    struct sigaction sa;
    sa.sa_handler = signal_handler;
    sa.sa_flags = 0;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGINT, &sa, NULL);

    // 阻塞 SIGINT 信号
    printf("阻塞 SIGINT 信号...\n");
    sigprocmask(SIG_BLOCK, &new_set, &old_set);

    // 等待 10 秒,此时 SIGINT 信号将被阻塞
    printf("等待 10 秒,按下 Ctrl+C 将不会立即退出...\n");
    sleep(10);

    // 解除阻塞 SIGINT 信号
    printf("解除阻塞 SIGINT 信号...\n");
    sigprocmask(SIG_SETMASK, &old_set, NULL);

    // 再次等待信号
    printf("按下 Ctrl+C 将退出程序...\n");
    while (1) {
        sleep(1);  // 每秒休眠一次,等待信号
    }

    return 0;
}

sigpending函数

#include <signal.h>

int sigpending(sigset_t *set);

功能: 
    用于获取当前进程中未决的信号集(即已经发送但由于阻塞而未处理的信号)。

参数: 
    set: 指向一个 `sigset_t` 结构体的指针,用于存储未决的信号集。

返回值: 
    成功: 返回 0。
    失败: 返回 -1,并设置相应的错误码。

sigemptyset函数

初始化一个空的信号集。

#include <signal.h>

int sigemptyset(sigset_t *set);

功能:
    初始化一个空的信号集。调用后,信号集 `set` 将不包含任何信号。此函数常用于信号集操作的开始阶段,以确保信号集中没有任何残留信号。

参数:
    sigset_t *set: 指向 `sigset_t` 类型的信号集的指针,该信号集将被初始化为空。

返回值:
    成功: 返回 0,表示信号集成功初始化为空。
    失败: 返回 -1,并设置 `errno` 来指示错误。

sigaddset函数

将一个信号添加到信号集中。

#include <signal.h>

int sigaddset(sigset_t *set, int signum);

功能:
    将指定的信号添加到信号集中。调用此函数后,信号 `signum` 将被添加到信号集 `set` 中,表示该信号现在是信号集的一部分。

参数:
    sigset_t *set: 指向 `sigset_t` 类型的信号集的指针,该信号集将被修改以包含指定的信号。
    int signum: 要添加到信号集中的信号编号(如 `SIGINT`、`SIGTERM` 等)。

返回值:
    成功: 返回 0,表示信号成功添加到信号集中。
    失败: 返回 -1,并设置 `errno` 来指示错误,例如,如果 `signum` 是无效的信号编号。

sigdelset函数

从信号集中删除一个信号。

#include <signal.h>

int sigdelset(sigset_t *set, int signum);

功能:
    从指定的信号集中删除一个信号。调用此函数后,信号 `signum` 将从信号集 `set` 中移除,表示该信号不再属于该信号集。

参数:
    sigset_t *set: 指向 `sigset_t` 类型的信号集的指针,该信号集将被修改以删除指定的信号。
    int signum: 要从信号集中删除的信号编号(如 `SIGINT`、`SIGTERM` 等)。

返回值:
    成功: 返回 0,表示信号成功从信号集中删除。
    失败: 返回 -1,并设置 `errno` 来指示错误,例如,如果 `signum` 是无效的信号编号。

sigfillset函数

将所有可用信号添加到信号集中。

#include <signal.h>

int sigfillset(sigset_t *set);

功能:
    将所有可用信号添加到信号集中。调用此函数后,信号集 `set` 将包含所有可能的信号,通常用于阻塞所有信号的操作。

参数:
    sigset_t *set: 指向 `sigset_t` 类型的信号集的指针,该信号集将被修改为包含所有信号。

返回值:
    成功: 返回 0,表示信号集成功被填充为包含所有信号。
    失败: 返回 -1,并设置 `errno` 来指示错误。

sigismember函数

检查一个信号是否在信号集中。

#include <signal.h>

int sigismember(const sigset_t *set, int signum);

功能:
    检查指定的信号是否在信号集中。调用此函数后,可以判断信号 `signum` 是否属于信号集 `set`。

参数:
    const sigset_t *set: 指向 `sigset_t` 类型的信号集的指针,该信号集将被检查。
    int signum: 要检查的信号编号(如 `SIGINT`、`SIGTERM` 等)。

返回值:
    成功: 如果信号在信号集中,返回 1;如果信号不在信号集中,返回 0。
    失败: 返回 -1,并设置 `errno` 来指示错误,例如,如果 `signum` 是无效的信号编号。

其他的信号集函数

sigprocmask: 改变或获取进程的信号屏蔽集。
sigpending: 获取当前进程的未决信号集。
sigsuspend: 替换信号屏蔽集并挂起进程,直到捕获到一个信号。
sigwait: 等待一个信号并获取它。
sigwaitinfo: 等待信号并获取详细的信号信息。
sigtimedwait: 等待一个信号,带有超时时间,并获取详细的信号信息

....

4、其他

pause函数

#include <unistd.h>

int pause(void);

功能: 
    使调用进程挂起(暂停执行),直到接收到一个信号并且该信号处理完毕。常用于等待某个信号的发生。

参数: 
    无。

返回值: 
    成功: 无(因为函数只会因为接收到信号而返回)。
    失败: 返回 -1,并设置错误码为 EINTR,表示因信号中断而返回。

alarm函数

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

功能: 
    设置一个定时器,在指定的秒数后发送 SIGALRM 信号给调用进程。如果设置的时间到达且进程未捕获或忽略该信号,默认行为是终止进程。

参数: 
    seconds: 定时器的时间(秒)。如果 `seconds` 为 0,表示取消任何现有的闹钟定时器。

返回值: 
    返回上一个闹钟定时器还剩余的秒数。如果没有设置过闹钟,则返回 0。

5.3 IPC 机制

互斥 死锁

| IPC 机制 | 通信方式 | 适用场景 | 速度 |
| ———— | ———— | ———————————— | ——– |
| 无名管道 | 单向 | 父子进程间简单通信 | 较快 |
| 有名管道 | 单向/双向 | 不相关进程间简单通信 | 较快 |
| 信号 | 异步 | 事件通知、进程控制 | 实时 |
| 消息队列 | 双向 | 复杂多进程通信、消息按优先级传递 | 中等 |
| 共享内存 | 双向 | 高性能、高并发场景 | 非常快 |
| 信号量 | 同步控制 | 进程/线程间同步、互斥 | 较快 |
| 套接字 | 双向 | 网络通信、跨主机通信、本地进程间通信 | 较慢 |
| 文件映射 | 双向 | 大量数据共享、需要文件持久化时 | 快 |

4.3.1 常用命令

$ ipcs  # 查看

--------- 消息队列 -----------
键        msqid      拥有者  权限     已用字节数 消息      

------------ 共享内存段 --------------
键        shmid      拥有者  权限     字节     连接数  状态      
0x00000000 524288     student    600        524288     2          目标       
0x00000000 622593     student    600        16777216   2                       
0x00000000 1376258    student    600        524288     2          目标       
0x00000000 884739     student    600        524288     2          目标       
0x00000000 983044     student    600        524288     2          目标       
0x00000000 1146885    student    600        524288     2          目标       
0x00000000 1802246    student    600        524288     2          目标       
0x00000000 1474567    student    600        524288     2          目标       
0x00000000 1507336    student    600        134217728  2          目标       
0x00000000 1703945    student    600        524288     2          目标       
0x00000000 1835018    student    600        4194304    2          目标       
0x00000000 1933323    student    600        524288     2          目标       
0x00000000 3964940    student    600        524288     2          目标       
0x00000000 3833869    student    600        2097152    2          目标       
0x00000000 2228239    student    600        524288     2          目标       

--------- 信号量数组 -----------
键        semid      拥有者  权限     nsems     


单独查看
$ipcs -q # 查看消息队列
$ipcs -m # 查看信号内存
$ipcs -s # 信号灯集

删除
$ipcrm -q msqid # 删除消息队列
$ipcrm -m shmid # 删除信号内存
$ipcrm -s semid # 删除信号灯集

4.3.2 常用函数

#include <sys/types.h>
#include <sys/ipc.h>

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

功能: 
    生成一个系统V IPC(如消息队列、共享内存、信号量)使用的唯一键值。`ftok` 函数常用于生成跨进程通信时需要的 key 值。
key = (低位 8 位的设备号) << 24 | (低位 8 位的 inode 号) << 16 | (proj_id 的低位 8 位)
        1111 1111 1111 1111 0000 0000 1111 1111
        设备号     inode               proj_id

参数: 
    pathname: 指向一个现有文件的路径名,系统会根据该文件的 i-node 编号和 `proj_id` 生成 key 值。
    proj_id: 项目标识符,通常为一个字符(整数形式),用于进一步确保生成的 key 值的唯一性。它的取值范围通常是 0 到 255。

返回值: 
    成功: 返回生成的 key_t 类型键值。
    失败: 返回 -1,并设置相应的错误码。

5.5 消息队列

消息队列

消息队列(Message Queue)是一种基于内核实现的进程间通信(IPC)机制,它允许多个进程以消息的形式进行数据交换。消息队列由内核维护,消息以消息结构的形式存在,并包含消息类型和消息正文。

消息队列的基本概念

消息队列的工作原理: A进程可以将消息写入消息队列,消息有类型和正文。B进程可以根据消息的类型,从消息队列中读取对应类型的消息。

消息队列的容量: 消息队列的大小默认是16KB。如果消息队列已满,而A进程还要继续写入数据,则写入操作会阻塞,直到队列有空闲空间。

消息的类型: 每条消息包含一个消息类型,用于标识消息的优先级或类别。不同的进程可以根据类型选择性地接收特定的消息。

4.5.1 相关函数

msgget 创建或获取一个消息队列。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);

功能:
    创建一个新的消息队列或获取一个现有的消息队列的标识符。消息队列用于在多个进程之间传递消息。

参数:
    key_t key: 消息队列的键值,用于唯一标识一个消息队列。通常通过 `ftok()` 函数生成。如果设置为 `IPC_PRIVATE`,将创建一个新的私有消息队列。
    int msgflg: 消息队列的创建标志和权限。常见的标志包括:
        - `IPC_CREAT`: 如果消息队列不存在,则创建一个新的消息队列。
        - `IPC_EXCL`: 与 `IPC_CREAT` 一起使用,表示如果消息队列已经存在,则返回错误。
        - 权限标志,如 `0666`,用于设置队列的读写权限。
                IPC_CREAT | 0666
                IPC_CREAT | IPC_EXCL | 0666

返回值:
    成功: 返回消息队列的标识符(`msgid`)。
    失败: 返回 -1,并设置 `errno` 来指示错误。

msgsnd 向消息队列发送消息。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

功能:
    将一条消息发送到指定的消息队列中。消息必须以 `long` 类型的 `mtype` 开头,后面跟随实际的数据。

参数:
    int msqid: 消息队列的标识符,由 `msgget` 返回。
    const void *msgp: 指向要发送的消息的指针。消息必须以 `long` 类型的 `mtype` 开头。
    size_t msgsz: 消息的大小(不包括 `mtype` 字段)。
    int msgflg: 发送消息的操作标志。常用的标志包括:
        - `IPC_NOWAIT`: 如果队列满,则立即返回,而不是等待。

返回值:
    成功: 返回 0。
    失败: 返回 -1,并设置 `errno` 来指示错误,例如,如果消息队列已满或权限不足。

msgrcv 从消息队列接收消息。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

功能:
    从指定的消息队列中接收一条消息。接收到的消息存储在 `msgp` 指向的缓冲区中,消息格式与发送时相同,必须以 `long` 类型的 `mtype` 开头。

参数:
    int msqid: 消息队列的标识符,由 `msgget` 返回。
    void *msgp: 指向接收消息的缓冲区的指针。消息必须以 `long` 类型的 `mtype` 开头。
    size_t msgsz: 消息缓冲区的大小(不包括 `mtype` 字段)。
    long msgtyp: 要接收的消息类型。
        - `msgtyp > 0`: 接收类型为 `msgtyp` 的第一条消息。
        - `msgtyp == 0`: 接收队列中的第一条消息。
        - `msgtyp < 0`: 接收类型等于或小于 `|msgtyp|` 的消息。
               1 2 3 4 80 90 
    int msgflg: 接收消息的操作标志。常用的标志包括:
        - `IPC_NOWAIT`: 如果队列为空,则立即返回,而不是等待。
        - `MSG_NOERROR`: 如果消息大于 `msgsz`,则截断消息。

返回值:
    成功: 返回接收到的消息字节数。
    失败: 返回 -1,并设置 `errno` 来指示错误,例如,如果消息队列为空或权限不足。

msgctl 控制消息队列的属性,如删除消息队列。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

功能:
    对消息队列执行各种控制操作,如删除消息队列、获取或设置消息队列的状态信息等。

参数:
    int msqid: 消息队列的标识符,由 `msgget` 返回。
    int cmd: 控制命令,用于指定要执行的操作。常见的命令包括:
        - `IPC_RMID`: 删除指定的消息队列。 
            当内部有数据 创建者 或者有其他权限
        - `IPC_STAT`: 获取消息队列的状态信息,并将结果存储在 `buf` 指向的 `msqid_ds` 结构中。
        - `IPC_SET`: 设置消息队列的状态信息,`buf` 指向一个包含新状态的 `msqid_ds` 结构。
    struct msqid_ds *buf: 指向 `msqid_ds` 结构的指针,用于保存或设置消息队列的状态信息。根据 `cmd` 的不同,可能会用到或忽略此参数。

返回值:
    成功: 返回 0。
    失败: 返回 -1,并设置 `errno` 来指示错误,例如,如果权限不足或消息队列不存在。
``` c
struct msqid_ds {
    struct ipc_perm msg_perm;   // 消息队列的权限和相关信息
    time_t msg_stime;           // 上次发送消息的时间
    time_t msg_rtime;           // 上次接收消息的时间
    time_t msg_ctime;           // 消息队列的创建或上次修改时间
    unsigned long msg_cbytes;   // 当前队列中所有消息的总字节数
    unsigned long msg_qnum;     // 当前队列中的消息数量
    unsigned long msg_qbytes;   // 队列中允许的最大字节数
    pid_t msg_lspid;            // 最后发送消息的进程ID
    pid_t msg_lrpid;            // 最后接收消息的进程ID
};

struct ipc_perm {
    uid_t uid;    // 拥有者的用户 ID
    gid_t gid;    // 拥有者的组 ID
    uid_t cuid;   // 创建者的用户 ID
    gid_t cgid;   // 创建者的组 ID
    mode_t mode;  // 访问权限(类似文件权限),低 9 位表示权限
    unsigned short __seq;  // 序列号(用于唯一标识 IPC 对象)
    key_t key;    // IPC 键值
};

4.5.2 示例代码

01_创建队列

// 01_create_queue.c
#include <head.h>

#define QUEUE_KEY 1234  // 定义消息队列的键

int main() {
    int msgid;

    // 创建一个消息队列,权限为 0666 (读写权限)
    msgid = msgget(QUEUE_KEY, 0666 | IPC_CREAT);
    if (msgid == -1) {
        perror("msgget 创建消息队列失败");
        exit(EXIT_FAILURE);
    }

    printf("消息队列创建成功,消息队列 ID: %d\n", msgid);
    return 0;
}

02_写入队列

// 02_write_queue.c
#include <head.h>

#define QUEUE_KEY 1234  // 定义消息队列的键
#define MSG_SIZE 100  // 定义消息大小

// 消息结构体,必须以 long 类型的 mtype 开头
struct message {
    long mtype;  // 消息类型
    char mtext[MSG_SIZE];  // 消息内容
};

int main() {
    int msgid;
    struct message msg;

    // 获取现有的消息队列
    msgid = msgget(QUEUE_KEY, 0666);
    if (msgid == -1) {
        perror("msgget 获取消息队列失败");
        exit(EXIT_FAILURE);
    }

    // 设置消息内容
    msg.mtype = 1;  // 消息类型设为1
    strcpy(msg.mtext, "Hello, this is a message.");

    // 发送消息到队列
    if (msgsnd(msgid, &msg, sizeof(msg.mtext), 0) == -1) {
        perror("msgsnd 发送消息失败");
        exit(EXIT_FAILURE);
    }

    printf("消息已发送到队列: %s\n", msg.mtext);
    return 0;
}

03_读取队列

// 03_read_queue.c
#include <head.h>

#define QUEUE_KEY 1234  // 定义消息队列的键
#define MSG_SIZE 100  // 定义消息大小

// 消息结构体,必须以 long 类型的 mtype 开头
struct message {
    long mtype;  // 消息类型
    char mtext[MSG_SIZE];  // 消息内容
};

int main() {
    int msgid;
    struct message msg;

    // 获取现有的消息队列
    msgid = msgget(QUEUE_KEY, 0666);
    if (msgid == -1) {
        perror("msgget 获取消息队列失败");
        exit(EXIT_FAILURE);
    }

    // 从队列中读取消息
    if (msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0) == -1) {
        perror("msgrcv 接收消息失败");
        exit(EXIT_FAILURE);
    }

    printf("接收到的消息: %s\n", msg.mtext);

    // 删除消息队列
    if (msgctl(msgid, IPC_RMID, NULL) == -1) {
        perror("msgctl 删除消息队列失败");
        exit(EXIT_FAILURE);
    }

    printf("消息队列已删除。\n");
    return 0;
}

5.6 共享内存

在内核中创建共享内存 让进程A 和进程B 都可以访问到 通过这段内存进行数据传递
共享内存是所有进程间通讯方式中效率最高的(不需要来回进程数据的拷贝)
共享内存相关的API

所有通讯方式里面 内存是最快的

​ 两台不同的主机之间 套接字

​ 两个不同的进程 内存最快

​ 一个进程之间 内存最快

进程间通讯-共享内存

5.6.2 相关函数

shmget 创建共享内存块

#include <sys/ipc.h>
#include <sys/shm.h>

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

功能:
    `shmget` 函数用于创建一个新的共享内存段或访问一个已经存在的共享内存段。共享内存是一种进程间通信(IPC)机制,允许多个进程共享同一块内存,从而实现高效的数据交换。

参数:
    key_t key: 共享内存段的键值,用于唯一标识一个共享内存段。通常使用 `ftok()` 函数生成。如果设置为 `IPC_PRIVATE`,则创建一个新的私有共享内存段。

    size_t size: 共享内存段的大小(以字节为单位)。如果是创建一个新的共享内存段,则必须指定其大小。
        内存会自动设置为 内存页的整数倍 PAGE_SIZE == > 4k 4k 的整数倍 

    int shmflg: 共享内存段的创建标志和权限。常见的标志包括:
        - `IPC_CREAT`: 如果共享内存段不存在,则创建一个新的共享内存段。
        - `IPC_EXCL`: 与 `IPC_CREAT` 一起使用,表示如果共享内存段已经存在,则返回错误。
        - 权限标志,如 `0666`,用于设置共享内存段的读写权限。
        IPC_CREAT | 0666
返回值:
    成功: 返回共享内存段的标识符(`shmid`)。
    失败: 返回 -1,并设置 `errno` 来指示错误,例如,如果内存不足或权限不足。

shmat 将共享内存 映射到 当前进程的 虚拟内存

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

void * shmat(int shmid, const void *shmaddr, int shmflg);

功能:
    `shmat` 函数用于将一个共享内存段附加到当前进程的地址空间中。通过这个函数,进程可以访问共享内存段中的数据,从而实现进程间通信(IPC)。

参数:
    int shmid: 共享内存段的标识符,由 `shmget` 返回。
    const void *shmaddr: 指定共享内存段应附加到进程地址空间中的地址。
        - 如果设置为 `NULL`,系统会自动选择一个合适的地址进行附加。
        - 如果指定了一个非空地址,系统会尝试将共享内存段附加到该地址,但可能需要指定 `SHM_RND` 标志来进行地址对齐。

    int shmflg: 附加操作的标志。
        - `0`: 默认行为,将共享内存段连接到进程的地址空间,允许读写。
        - `SHM_RDONLY`: 共享内存段为只读方式附加。

返回值:
    成功: 返回一个指向共享内存段的指针,该指针指向共享内存段附加到进程地址空间中的起始位置。
    失败: 返回 `(void *) -1`,并设置 `errno` 来指示错误。

shmdt 将当前进程中的共享内存块给分离出去

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int shmdt(const void *shmaddr);

功能:
    `shmdt` 函数用于将共享内存段从当前进程的地址空间中分离(卸载)。通过这个函数,进程可以停止访问之前通过 `shmat` 附加的共享内存段。

参数:
    const void *shmaddr: 指向共享内存段在进程地址空间中的起始地址。该地址是之前调用 `shmat` 时返回的指针。

返回值:
    成功: 返回 0,表示共享内存段成功从进程地址空间分离。
    失败: 返回 -1,并设置 `errno` 来指示错误,例如,如果地址无效或进程没有附加该地址。

shmctl 内存块控制

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

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

功能:
    `shmctl` 函数用于对共享内存段进行控制操作,如删除共享内存段、获取或设置共享内存段的状态信息等。

参数:
    int shmid: 共享内存段的标识符,由 `shmget` 返回。
    int cmd: 控制命令,用于指定要执行的操作。常见的命令包括:
        - `IPC_RMID`: 删除指定的共享内存段。
        - `IPC_STAT`: 获取共享内存段的状态信息,并将结果存储在 `buf` 指向的 `shmid_ds` 结构中。
        - `IPC_SET`: 设置共享内存段的状态信息,`buf` 指向一个包含新状态的 `shmid_ds` 结构。通常需要超级用户权限。
    struct shmid_ds *buf: 指向 `shmid_ds` 结构的指针,用于保存或设置共享内存段的状态信息。根据 `cmd` 的不同,可能会用到或忽略此参数。

返回值:
    成功: 返回 0。
    失败: 返回 -1,并设置 `errno` 来指示错误,例如,如果权限不足或共享内存段不存在。
``` c
struct shmid_ds {
    struct ipc_perm shm_perm;    // 操作权限结构体
    size_t shm_segsz;            // 共享内存段的大小(以字节为单位)
    time_t shm_atime;            // 最后一次附加共享内存的时间
    time_t shm_dtime;            // 最后一次分离共享内存的时间
    time_t shm_ctime;            // 最后一次更改共享内存控制结构的时间
    pid_t shm_cpid;              // 创建共享内存段的进程ID
    pid_t shm_lpid;              // 最后一次操作共享内存段的进程ID
    shmatt_t shm_nattch;         // 当前附加到该共享内存段的进程数
};

struct ipc_perm {
    key_t __key;        // IPC对象的键值
    uid_t uid;          // 所有者的用户ID
    gid_t gid;          // 所有者的组ID
    uid_t cuid;         // 创建者的用户ID
    gid_t cgid;         // 创建者的组ID
    unsigned short mode; // 访问权限标志
    unsigned short __seq; // 序列号,用于内部区分
};

5.6.3 相关示例

01_共享内存创建

// 01_create_shared_memory.c
#include <head.h>

#define SHM_KEY 1234  // 定义共享内存段的键
#define SHM_SIZE 1024  // 共享内存段的大小(1KB)

int main() {
    int shmid;

    // 创建一个共享内存段,大小为 1KB,权限为 0666
    shmid = shmget(SHM_KEY, SHM_SIZE, 0666 | IPC_CREAT);
    if (shmid == -1) {
        perror("shmget 创建共享内存段失败");
        exit(EXIT_FAILURE);
    }

    printf("共享内存段创建成功,ID: %d\n", shmid);
    return 0;
}

02_共享内存写入

// 02_write_shared_memory.c
#include <head.h>

#define SHM_KEY 1234  // 定义共享内存段的键
#define SHM_SIZE 1024  // 共享内存段的大小(1KB)

int main() {
    int shmid;
    char *shared_memory;

    // 获取现有的共享内存段
    shmid = shmget(SHM_KEY, SHM_SIZE, 0666);
    if (shmid == -1) {
        perror("shmget 获取共享内存段失败 ");
        exit(EXIT_FAILURE);
    }

    // 将共享内存段附加到当前进程的地址空间
    shared_memory = (char *)shmat(shmid, NULL, 0);
    if (shared_memory == (char *)-1) {
        perror("shmat 附加共享内存段失败");
        exit(EXIT_FAILURE);
    }

    // 写入数据到共享内存
    strcpy(shared_memory, "Hello, this is a message in shared memory.");
    printf("数据已写入共享内存: %s\n", shared_memory);

    // 分离共享内存段
    if (shmdt(shared_memory) == -1) {
        perror("shmdt 分离共享内存段失败");
        exit(EXIT_FAILURE);
    }

    return 0;
}

03_共享内存读取

// 03_read_shared_memory.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define SHM_KEY 1234  // 定义共享内存段的键
#define SHM_SIZE 1024  // 共享内存段的大小(1KB)

int main() {
    int shmid;
    char *shared_memory;

    // 获取现有的共享内存段
    shmid = shmget(SHM_KEY, SHM_SIZE, 0666);
    if (shmid == -1) {
        perror("shmget 获取共享内存段失败");
        exit(EXIT_FAILURE);
    }

    // 将共享内存段附加到当前进程的地址空间
    shared_memory = (char *)shmat(shmid, NULL, 0);
    if (shared_memory == (char *)-1) {
        perror("shmat 附加共享内存段失败");
        exit(EXIT_FAILURE);
    }

    // 读取共享内存中的数据
    printf("从共享内存读取的数据: %s\n", shared_memory);

    // 分离共享内存段
    if (shmdt(shared_memory) == -1) {
        perror("shmdt 分离共享内存段失败");
        exit(EXIT_FAILURE);
    }

    // 删除共享内存段
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl 删除共享内存段失败");
        exit(EXIT_FAILURE);
    }

    printf("共享内存段已删除。\n");
    return 0;
}

5.7 信号灯集

5.7.1 信号灯集的概念

在 Linux 中,信号灯信号灯集是用于进程间同步的机制。它们主要用于控制多个进程对共享资源的访问,以避免竞争条件或数据不一致的问题。信号灯集是一组信号灯的集合,每个信号灯可以用来管理一个共享资源的访问。

信号灯的基本概念

信号灯是一个非负整数,其值表示可用资源的数量。信号灯提供了两个原子操作:

  • P 操作:如果信号灯的值大于零,将其减一;如果信号灯的值为零,则进程进入等待状态,直到信号灯的值大于零。

  • V 操作:将信号灯的值加一。如果有进程在等待信号灯变为大于零的状态,其中一个进程将被唤醒。

信号灯集

信号灯集是一个包含多个信号灯的结构体,通常用于一个应用程序中需要多个信号灯来管理不同的资源或同步不同的操作。信号灯集的主要特点包括:

  • 标识符(ID):每个信号灯集有一个唯一的标识符,用于操作系统区分不同的信号灯集。
  • 信号灯数组:信号灯集中的信号灯通常存储在一个数组中,可以通过数组索引来访问和操作每个信号灯。

信号灯集

5.7.2 相关函数

 semget 创建信号灯集

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);

功能:
    用于创建一个新的信号灯集或获取一个已有的信号灯集的标识符。
    信号灯集用于在多个进程之间进行同步控制。

参数:
    key: 唯一的键值,用于标识信号灯集。如果 key 为 IPC_PRIVATE,则创建一个新的信号灯集。
    nsems: 信号灯集中的信号灯数量。如果请求的是一个新的信号灯集,则该参数指示信号灯的数量。
    semflg: 控制信号灯集的操作行为,常用的标志包括:
        - IPC_CREAT: 如果不存在,则创建一个新的信号灯集。
        - IPC_EXCL: 与 IPC_CREAT 一起使用,表示如果信号灯集已经存在,则返回错误。
        - 权限位(如 0666): 用于设置信号灯集的访问权限。

返回值:
    成功: 返回信号灯集的标识符(非负整数)。
    失败: 返回 -1,并设置 errno 来指示错误类型(如 EEXIST 表示信号灯集已经存在,EINVAL 表示无效的参数等)。

semop操作信号灯集

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, size_t nsops);

功能:
    对指定的信号灯集执行一组原子操作,包括 P 操作(等待操作)和 V 操作(信号操作)。用于进程间的同步控制。

参数:
    semid: 信号灯集的标识符,由 semget 返回。
    sops: 指向一个或多个信号灯操作结构体数组(struct sembuf),每个结构体描述了对信号灯的一个操作。
    nsops: 要执行的信号灯操作的数量,即 sops 数组的长度。

返回值:
    成功: 返回 0。
    失败: 返回 -1,并设置 errno 指示错误类型(如 EINVAL 表示参数无效,EIDRM 表示信号灯集已被删除等)。

结构体说明

struct sembuf {
    unsigned short sem_num;  // 信号灯集中的信号灯编号(从 0 开始)
    short sem_op;            // 信号灯操作。负值表示P操作(等待),正值表示V操作(释放),0表示等待信号灯值为0
    short sem_flg;           // 操作标志。常用的标志包括IPC_NOWAIT(非阻塞操作)和SEM_UNDO(操作撤销)
};

结构体说明:
    struct sembuf 用于描述信号灯操作的结构体,每个实例定义了一个对信号灯的操作。多个 struct sembuf 结构体可以组合成一个数组,通过 semop 函数对信号灯集执行一组原子操作。

成员:
    sem_num: 指定要操作的信号灯在信号灯集中的编号,从 0 开始。
    sem_op: 指定信号灯的操作:
        - 负值: 执行 P 操作(等待操作),将信号灯的值减少相应的数量。如果信号灯的值小于操作数的绝对值,进程会被阻塞,直到信号灯的值足够。
        - 正值: 执行 V 操作(信号操作),将信号灯的值增加相应的数量。
        - 0: 等待信号灯的值变为 0。
    sem_flg: 操作标志,控制操作行为。
        - IPC_NOWAIT: 如果信号灯操作会导致进程阻塞,则操作立即返回 -1,而不等待。
        - SEM_UNDO: 如果进程异常终止,操作会自动撤销。

semctl控制信号灯集

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, ...);

功能:
    用于控制信号灯集的行为,执行如设置信号灯的值、获取信号灯信息、删除信号灯集等操作。

参数:
    semid: 信号灯集的标识符,由 semget 返回。
    semnum: 信号灯集中信号灯的编号(从 0 开始),当 cmd 操作为 SETVAL 或 GETVAL 时使用。
    cmd: 要执行的控制命令,包括:
        - IPC_RMID: 删除指定的信号灯集。
        - IPC_STAT: 获取信号灯集的状态信息。
        - IPC_SET: 设置信号灯集的状态信息。
        - GETVAL: 获取指定信号灯的当前值。
        - SETVAL: 设置指定信号灯的值。
        - GETALL: 获取信号灯集中所有信号灯的值。
        - SETALL: 设置信号灯集中所有信号灯的值。
        - GETPID: 获取最后一个执行 semop 操作的进程 ID。
        - GETNCNT: 获取等待信号灯值增加的进程数。
        - GETZCNT: 获取等待信号灯值为零的进程数。

返回值:
    成功: 对于不同的命令,返回不同的值,例如返回 0 或信号灯的值。
    失败: 返回 -1,并设置 errno 指示错误类型(如 EINVAL 表示参数无效,EACCES 表示权限被拒绝等)。

第四个参数介绍

第四个参数是一个公用体
union semun {
    int val;                  // 用于 SETVAL 命令设置信号灯的值
    struct semid_ds *buf;     // 用于 IPC_STAT 和 IPC_SET 命令获取和设置信号灯集的状态信息
    unsigned short *array;    // 用于 GETALL 和 SETALL 命令获取和设置信号灯集内所有信号灯的值
    struct seminfo *__buf;    // 用于 IPC_INFO 命令(通常不常用)
};

    在使用 semctl 函数时,第四个参数的类型和用途取决于 cmd 命令的不同情况。通常这是一个可选的参数,可能是 int、union semun 或其他指针类型。以下是常见的情况:

    1. IPC_STAT 和 IPC_SET:
        类型: struct semid_ds *
        用途: 用于获取或设置信号灯集的状态信息。

    2. GETALL 和 SETALL:
        类型: unsigned short *
        用途: 指向一个数组,用于获取或设置信号灯集内所有信号灯的值。

    3. SETVAL:
        类型: int
        用途: 用于设置信号灯的值。

    4. 其他命令:
        不需要第四个参数。

结构体介绍

struct semid_ds {
    struct ipc_perm sem_perm;  // 访问权限结构
    time_t sem_otime;          // 最近一次 semop 操作的时间
    time_t sem_ctime;          // 最近一次 semctl 操作的时间
    unsigned short sem_nsems;  // 信号灯的数量
};


struct ipc_perm {
    key_t __key;               // 信号灯集的键值
    uid_t uid;                 // 所有者的用户ID
    gid_t gid;                 // 所有者的组ID
    uid_t cuid;                // 创建者的用户ID
    gid_t cgid;                // 创建者的组ID
    unsigned short mode;       // 访问权限和其他标志位
    unsigned short __seq;      // 序列号
};

5.7.3 封装二值信号灯集

二值信号灯,又称为互斥锁,是用于进程间同步的一种特殊类型的信号灯,其值只能是 01。二值信号灯主要用于在多个进程之间实现对共享资源的互斥访问,即在同一时间,只有一个进程可以访问某一共享资源。

特点

  • 值域限制:二值信号灯的值只能是 0 或 1。

    • 值为 1:表示资源空闲,可以被占用。

    • 值为 0:表示资源已被占用,其他进程需要等待。

  • 操作类型

    • P 操作(Wait/等待):将信号灯的值从 1 变为 0,表示进程已占用资源。如果信号灯的值已经是 0,则执行 P 操作的进程会被阻塞,直到信号灯的值变为 1

    • V 操作(Signal/信号):将信号灯的值从 0 变为 1,表示进程释放资源。如果有其他进程正在等待资源,则唤醒一个等待的进程。

  • 进程同步:二值信号灯常用于解决临界区问题,保证只有一个进程可以进入临界区(访问共享资源)。

应用场景

  • 互斥访问:确保多个进程在访问共享资源(如文件、缓冲区等)时,不会相互干扰。
  • 进程同步:协调多个进程的执行顺序。例如,某个进程必须等待另一个进程完成某项任务后才能继续执行。

示例程序

/* my_sem.h */
#ifndef __MY_SEM_H__
#define __MY_SEM_H__


// 定义共用体 semun
typedef union semun {
    int val;                  // 信号灯的值
    struct semid_ds *buf;     // 信号灯集的状态信息
    unsigned short *array;    // 信号灯集中所有信号灯的值
} semun;

/*
功能: 初始化信号灯集
参数:
    @__pathname: 路径
    @__proj_id: 字符
    @__nsems: 创建信号灯的个数
返回值:
    成功: 返回信号灯的 ID
    失败: 返回 -1
*/
int my_sem_init(const char *__pathname, int __proj_id, int __nsems);

/*
功能: 申请资源 (P 操作)
参数:
    @__semid: 信号灯集的 ID
    @__semnum: 信号灯编号
返回值:
    成功: 返回 0
    失败: 返回 -1
*/
int P(int __semid, int __semnum);

/*
功能: 释放资源 (V 操作)
参数:
    @__semid: 信号灯集的 ID
    @__semnum: 信号灯编号
返回值:
    成功: 返回 0
    失败: 返回 -1
*/
int V(int __semid, int __semnum);

/*
功能: 删除信号灯集
参数:
    @__semid: 信号灯集的 ID
返回值:
    成功: 返回 0
    失败: 返回 -1
*/
int my_sem_del(int __semid);

#endif /* __MY_SEM_H__ */
```c
#include "my_sem.h"

/*
功能: 初始化信号灯集
参数:
    @__pathname: 路径
    @__proj_id: 字符
    @__nsems: 创建信号灯的个数
返回值:
    成功 返回信号灯的 ID
    失败 返回 -1
*/
int my_sem_init(const char *__pathname, int __proj_id, int __nsems) 
{
    key_t key;
    int semid;
    union semun set_val;

    // 创建键值
    if ((key = ftok(__pathname, __proj_id)) == -1) {
        perror("ftok error");
        return -1;
    }

    // 创建信号灯集,防止重复初始化
    if ((semid = semget(key, __nsems, IPC_CREAT | IPC_EXCL | 0666)) == -1) {
        if (errno == EEXIST) {
            // 如果已经存在,就直接返回
            semid = semget(key, __nsems, IPC_CREAT | 0666);
            return semid;
        } else {
            perror("semget error");
            return -1;
        }
    }

    // 初始化信号灯集的信号,将第一个设置为 1,其他为 0
    for (int i = 0; i < __nsems; i++) {
        set_val.val = (i == 0) ? 1 : 0;
        if (semctl(semid, i, SETVAL, set_val) == -1) {
            perror("semctl error");
            return -1;
        }
    }

    return semid;
}

/*
功能: 申请资源 (P 操作)
参数:
    @__semid: 信号灯集的ID
    @__semnum: 信号灯编号
返回值:
    成功 返回 0
    失败 返回 -1
*/
int P(int __semid, int __semnum) {
    struct sembuf buff = {
        .sem_num = __semnum,
        .sem_op = -1,
        .sem_flg = 0
    };

    if (semop(__semid, &buff, 1) == -1) {
        perror("semop error");
        return -1;
    }
    return 0;
}

/*
功能: 释放资源 (V 操作)
参数:
    @__semid: 信号灯集的ID
    @__semnum: 信号灯编号
返回值:
    成功 返回 0
    失败 返回 -1
*/
int V(int __semid, int __semnum) {
    struct sembuf buff = {
        .sem_num = __semnum,
        .sem_op = 1,
        .sem_flg = 0
    };

    if (semop(__semid, &buff, 1) == -1) {
        perror("semop error");
        return -1;
    }
    return 0;
}

/*
功能: 删除信号灯集
参数:
    @__semid: 信号灯集的ID
返回值:
    成功 返回 0
    失败 返回 -1
*/
int my_sem_del(int __semid) {
    if (semctl(__semid, 0, IPC_RMID) == -1) {
        perror("semctl error");
        return -1;
    }
    return 0;
}

5.7.4 示例程序

使用信号灯集实现共享内存的同步

write

#include "my_sem.h"

typedef struct str
{
    char name[23];
    char sex[5];
    int age;
    int id;
}str;


int main(int argc, char const *argv[])
{
    char  * data_ptr;
    // 挂载内存
    data_ptr = (char  *)shmat( 2228236 , NULL , 0);

    // 创建信号灯集
    int semid = my_sem_init("./" , 'H' , 2);

    while (1)
    {
        P(semid , 1);
        printf("%s\n" , data_ptr);
        V(semid , 0);
    }

    // 卸载内存
    shmdt((void *)data_ptr);
    return 0;
}

read

#include "my_sem.h"

typedef struct str
{
    char name[23];
    char sex[5];
    int age;
    int id;
}str;


int main(int argc, char const *argv[])
{
    char  * data_ptr;
    // 挂载内存
    data_ptr = (char  *)shmat( 2228236 , NULL , 0);

    // 创建信号灯集
    int semid = my_sem_init("./" , 'H' , 2);

    while (1)
    {
        P(semid , 1);
        printf("%s\n" , data_ptr);
        V(semid , 0);
    }

    // 卸载内存
    shmdt((void *)data_ptr);
    return 0;
}