进程间通信方式

进程间通信

进程间通信方式:管道通信、信号、消息队列、共享内存、信号量组

一.管道通信

管道通信分为匿名管道以及有名管道(命名管道),匿名管道用于父进程与子进程之间(具有亲缘的进程之间)。命名管道可以用于非亲缘进程之间进行通信。

匿名管道(pipe)

特点:没有名称,所以无法使用open()来创建打开,但是支持read()和write()。pipe函数有一个参数pipefd(是一个数组类型,数组中有两个值)pipefd[0]记录管道读取端的文件描述符pipefd[1]记录管道写入端的文件描述符。当用户将数据写入管道时,数据是暂存在内核的缓冲区的(默认为4M大小)。

匿名管道读取写入实例

/*********************************************************************************
* @Description  : 用于创建匿名管道并进行父子进程间的通信
* @Note         : 
* @retval       : 程序退出状态码
* @Author       : ice_cui
* @Date         : 2025-05-11 21:12:32
* @Email        : Lazypanda_ice@163.com
* @LastEditTime : 2025-05-11 21:34:25
* @Version      : V1.0.0
* @Copyright    : Copyright (c) 2025 Lazypanda_ice@163.com All rights reserved.
**********************************************************************************/
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <string.h>
#include <sys/wait.h>
int main(int argc,char const *argv[])
{
    int fd[2];//管道文件描述符 fd[0]读,fd[1]写
    int ret = pipe(fd);//创建管道
    if(ret == -1){
        fprintf(stderr,
				"pipe error,errno:%d,%s\n",
				errno,strerror(errno));
        return -1;
    }
    pid_t pid = fork();//创建子进程
    if(pid == -1){
        fprintf(stderr,
				"fork error,errno:%d,%s\n",
				errno,strerror(errno));
        return -1;
    }
    if(pid == 0){//子进程
        close(fd[1]);//关闭写端
        char buf[20] = {0};
        int len = read(fd[0],buf,sizeof(buf));//从管道中读取数据
        if(len == -1){
            fprintf(stderr,
			        "read error,errno:%d,%s\n",
					errno,
					strerror(errno));
            return -1;
        }
        printf("child read:%s\n",buf);
        close(fd[0]);//关闭读端
    }
    else{//父进程
        close(fd[0]);//关闭读端
        char *str = "hello world";
        int len = write(fd[1],str,strlen(str));//向管道中写入数据
        if(len == -1){
            fprintf(stderr,
					"write error,errno:%d,%s\n",
					errno,strerror(errno));
            return -1;
        }
        close(fd[1]);//关闭写端
        wait(NULL);//等待子进程结束
    }
    return 0;
}
运行效果
gec@gec-virtaul machine:~/process$ ./pipe_test
child read:hello world

命名管道(fifo)

特点:命名管道有自己的名称,可以被open,同时也支持read/write,但管道无法进行指定位操作,即无法使用lseek操作。同匿名管道不同的是,没有亲缘关系的进程之间可以通过命名管道进行通信,并可以支持多路同时写入。

使用setTime.c获取当前系统时间并将数据写入命名管道

/*********************************************************************************
* @Description  : 用于获取当前时间并向命名管道写入时间字符串。
* @Note         : 
* @retval       : 程序退出状态码
* @Author       : ice_cui
* @Date         : 2025-05-11 15:45:02
* @Email        : Lazypanda_ice@163.com
* @LastEditTime : 2025-05-11 22:04:49
* @Version      : V1.0.0
* @Copyright    : Copyright (c) 2025 Lazypanda_ice@163.com All rights reserved.
**********************************************************************************/
#include <stdio.h>
#include <time.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <string.h>
#define FIFO "/tmp/fifo"
int main(int argc, char const *argv[])
{
    int ret = mkfifo(FIFO,0666);
    if(ret == -1){
        fprintf(stderr,"mkfifo error,errno=%d,%s\n",errno,strerror(errno));
        return -1;
    }
    int fifo_fd = open(FIFO,O_RDWR);//打开管道
    if(fifo_fd == -1){
        fprintf(stderr,"open fifo error,errno=%d,%s\n",errno,strerror(errno));
        return -1;
    }
    while (1)
    {
        time_t rawtime; // 用于存储当前时间的秒数
        struct tm * timeinfo; // 用于存储转换后的时间结构
        char time_str[40]; // 用于存储格式化后的时间字符串
        // 获取当前时间
        time(&rawtime);
        timeinfo = localtime(&rawtime);
        strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S\n", timeinfo); // 格式化时间
        write(fifo_fd,time_str,strlen(time_str));//写入时间
        sleep(1);
    }
    close(fifo_fd);//关闭管道
    return 0;
}

使用getTime.c读取命名管道中的数据然后存入到log.txt文件中

/*********************************************************************************
* @Description  : 用于从管道读取数据并写入日志文件
* @Note         : 
* @retval       : 程序退出状态码
* @Author       : ice_cui
* @Date         : 2025-05-11 15:44:42
* @Email        : Lazypanda_ice@163.com
* @LastEditTime : 2025-05-11 20:49:24
* @Version      : V1.0.0
* @Copyright    : Copyright (c) 2025 Lazypanda_ice@163.com All rights reserved.
**********************************************************************************/
#include <stdio.h>
#include <time.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <string.h>
#define FIFO "/tmp/fifo"
#define LOG_PATH "/tmp/log.txt"
int main(int argc, char const *argv[])
{
    int log_fd = open(LOG_PATH,O_RDWR|O_CREAT,0666);//打开日志文件
    if(log_fd == -1){
        fprintf(stderr,"open log.txt error,errno=%d,%s\n",errno,strerror(errno));//打印错误信息
        return -1;
    }
    int fifo_fd = open(FIFO,O_RDWR);//打开管道
    if(fifo_fd == -1){
        fprintf(stderr,"open fifo error,errno=%d,%s\n",errno,strerror(errno));//打印错误信息
        return -1;
    }
    char time_buf[256] = {0};//定义一个缓冲区
    while(1){
        read(fifo_fd,time_buf,sizeof(time_buf));//读取管道中的数据
        write(log_fd,time_buf,sizeof(time_buf));//写入数据  
        bzero(time_buf,sizeof(time_buf)); //清空缓冲区
        sleep(1);
    }
    close(log_fd);//关闭日志文件
    close(fifo_fd);//关闭管道
    return 0;
}

运行效果

在/tmp目录下生成一个名为fifo的有名管道和一个log.txt文件打开文件里面生成如下内容
2025-05-18 14:40:25
2025-05-18 14:40:26
2025-05-18 14:40:27
2025-05-18 14:40:28
2025-05-18 14:40:29
2025-05-18 14:40:30
2025-05-18 14:40:31
2025-05-18 14:40:32
2025-05-18 14:40:33
2025-05-18 14:40:34
2025-05-18 14:40:35
2025-05-18 14:40:36
2025-05-18 14:40:37

二.信号

1.基本概念

信号: 是一种异步通信机制,用于在操作系统中实现进程间的事件通知和简单控制。它是操作系统向进程发送的软件中断,用于指示某种事件(如程序错误、用户输入中断、定时器超时等)发生。
特点:
轻量级:仅传递事件类型(信号编号),不传递具体数据
异步性:信号的发送和处理不具有严格的时序关系。
可靠性:早期 UNIX 信号(如 POSIX 前的信号)不可靠(可能丢失或重复),现代 POSIX 信号已改进可靠性。

2.信号的生命周期与处理流程

信号从产生到处理的完整流程如下:
信号产生:
由系统内核自动生成(如除零错误触发SIGFPE)。
由其他进程通过kill()函数发送(如kill -信号编号 进程PID)。
由终端输入触发(如Ctrl+C触发SIGINT)。
信号传递:
内核将信号记录到目标进程的信号队列中(对于可靠信号,会维护多个相同信号;对于不可靠信号,可能合并或丢失)。
信号处理:
进程在特定时机(如从内核态返回用户态时)检测信号并执行对应处理动作。
处理动作有三种选择:
默认处理:由系统预设的行为(如终止进程、忽略信号等)。
忽略信号:进程不响应该信号(SIGKILL和SIGSTOP不可忽略)。
自定义处理函数:通过signal()或sigaction()函数注册信号处理函数。

3.关键函数与系统调用

1. kill()函数:发送信号

#include <signal.h>
int kill(pid_t pid, int sig);

参数:
pid:目标进程的 PID(若为0,则发送给同一进程组的所有进程)。
sig:要发送的信号编号(如SIGINT)或0(用于检测进程是否存在)。
返回值:
成功返回0,失败返回-1。
2. signal()函数:设置信号处理函数(简单接口)

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int sig, sighandler_t handler);

参数:
sig:信号编号。
handler:处理函数指针(如SIG_IGN表示忽略,SIG_DFL表示使用默认处理)。
注意:
该接口在 POSIX 标准中已被sigaction()取代,部分系统可能存在兼容性问题。
3. sigaction()函数:设置信号处理函数(推荐接口)

#include <signal.h>
int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);

参数:
sig:信号编号。
act:指向新处理动作的结构体指针。
oldact:指向存储旧处理动作的结构体指针(可设为NULL)。
struct sigaction结构体:

struct sigaction {
    void (*sa_handler)(int);        // 信号处理函数(或sa_sigaction)
    void (*sa_sigaction)(int, siginfo_t *, void *); // 带参数的处理函数(用于可靠信号)
    sigset_t sa_mask;              // 处理信号时阻塞的其他信号集合
    int sa_flags;                   // 标志位(如SA_RESTART、SA_SIGINFO等)
};

优势:支持可靠信号(传递额外信息)、信号掩码设置和更灵活的标志位控制。
4. alarm()与pause()函数:定时器与阻塞
alarm(unsigned int seconds):设置定时器,到期后发送SIGALRM信号。
pause():使进程阻塞直至收到信号(通常与alarm()配合使用)。

4.示例代码-自定义信号处理

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

// 信号处理函数
void sig_handler(int signo) {
    if (signo == SIGUSR1) {
        printf("Received SIGUSR1!\n");
    } else if (signo == SIGINT) {
        printf("Caught SIGINT, exiting...\n");
        _exit(0); // 避免标准IO缓冲区未刷新
    }
}

int main() {
    // 注册信号处理函数
    signal(SIGUSR1, sig_handler);
    signal(SIGINT, sig_handler);

    printf("Process PID: %d\n", getpid());
    printf("Wait for signals...\n");

    while (1) {
        sleep(1); // 阻塞并等待信号
    }PID
    return 0;
}

测试运行
1.运行程序,使用ipcs指令查看当前进程PID并记录 PID。
2.发送自定义信号:kill -SIGUSR1 ,程序将输出Received SIGUSR1!。
3.发送中断信号:kill -SIGINT 或按下Ctrl+C,程序将退出。

总结: 信号是进程间通信中最轻量级的机制,适用于事件通知和简单控制,但不适合大数据量传输。在使用时需注意信号的可靠性、阻塞机制和异步安全问题,合理利用sigaction()等接口实现健壮的信号处理逻辑。

5.信号屏蔽(Signal Masking)详解

信号屏蔽是操作系统中用于控制信号处理时机的机制,允许进程暂时阻塞某些信号的传递和处理。这在需要保证关键代码段不被中断的场景中尤为重要。

1.基本概念

信号掩码(Signal Mask)
每个进程都有一个信号掩码(本质是一个位图),用于指定哪些信号当前被阻塞。
被掩码的信号不会被进程立即处理,而是在掩码解除后才被处理(如果期间信号被发送多次,通常只处理一次)。
阻塞 vs 忽略
阻塞:信号被暂时挂起,不立即处理,但仍会记录信号的到达。
忽略:信号被直接丢弃,不会被处理(通过 signal(SIGXXX, SIG_IGN) 设置)。

2.核心函数

1. 设置信号掩码: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:指向信号集的指针(通过 sigemptyset()/sigfillset() 等函数初始化)。
oldset:用于保存旧的信号掩码(可设为 NULL)。
返回值:成功返回 0,失败返回 -1。
2. 初始化信号集:sigemptyset()/sigfillset()

#include <signal.h>
int sigemptyset(sigset_t *set);  // 清空信号集(所有位为0)
int sigfillset(sigset_t *set);   // 填充信号集(所有位为1)
int sigaddset(sigset_t *set, int signum);  // 添加单个信号
int sigdelset(sigset_t *set, int signum);  // 删除单个信号
int sigismember(const sigset_t *set, int signum);  // 检查信号是否在集合中

3. 检查和获取挂起的信号:sigpending()

#include <signal.h>
int sigpending(sigset_t *set);

作用:将当前被阻塞(挂起)的信号集存入 set 中。

3.典型应用场景

1.保护关键代码段
在执行不可中断的操作(如共享资源的修改)时,暂时阻塞某些信号。
2.避免竞态条件
在信号处理函数和主程序之间同步时,防止信号在不适当的时机到达。
3.资源清理阶段
在释放重要资源(如文件锁、内存)时,确保不会被信号中断。

4.示例代码:信号屏蔽的基本用法

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

void signal_handler(int signo) {
    printf("Caught signal %d\n", signo);
}

int main() {
    sigset_t new_mask, old_mask, pending_mask;

    // 注册信号处理函数
    signal(SIGINT, signal_handler);  // Ctrl+C
    signal(SIGUSR1, signal_handler); // 自定义信号

    // 初始化信号集,添加SIGINT和SIGUSR1
    sigemptyset(&new_mask);
    sigaddset(&new_mask, SIGINT);
    sigaddset(&new_mask, SIGUSR1);

    // 阻塞SIGINT和SIGUSR1
    if (sigprocmask(SIG_BLOCK, &new_mask, &old_mask) == -1) {
        perror("sigprocmask failed");
        return 1;
    }

    printf("SIGINT and SIGUSR1 are now blocked. Try sending them...\n");
    printf("PID: %d\n", getpid());
    sleep(10);  // 在此期间发送的SIGINT和SIGUSR1会被阻塞

    // 检查挂起的信号
    if (sigpending(&pending_mask) == -1) {
        perror("sigpending failed");
        return 1;
    }

    if (sigismember(&pending_mask, SIGINT)) {
        printf("SIGINT is pending (blocked but not processed).\n");
    }
    if (sigismember(&pending_mask, SIGUSR1)) {
        printf("SIGUSR1 is pending (blocked but not processed).\n");
    }

    // 解除阻塞
    if (sigprocmask(SIG_SETMASK, &old_mask, NULL) == -1) {
        perror("sigprocmask failed");
        return 1;
    }

    printf("SIGINT and SIGUSR1 are no longer blocked.\n");
    printf("Sleeping for another 10 seconds...\n");
    sleep(10);  // 在此期间发送的信号会被立即处理

    return 0;
}

5.注意事项

1.不可屏蔽的信号
SIGKILL 和 SIGSTOP 无法被屏蔽或忽略,用于确保系统能强制终止进程。
2.信号处理函数中的屏蔽
当信号处理函数执行时,该信号会自动被添加到进程的信号掩码中(防止递归调用)。
3.恢复旧掩码
使用 sigprocmask 时,建议保存旧掩码并在适当时候恢复,避免意外阻塞其他信号。
4.原子性操作
在解除信号屏蔽的同时检查信号是危险的,可使用 sigwait() 等函数实现原子操作。

6.高级技巧:安全地处理信号

使用 sigprocmask 和 sigwait() 实现信号的同步处理:

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

int main() {
    sigset_t mask;
    int signo;

    // 阻塞SIGINT和SIGUSR1
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGUSR1);
    if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1) {
        perror("sigprocmask failed");
        return 1;
    }

    printf("Waiting for SIGINT or SIGUSR1...\n");

    // 原子性地等待信号
    while (1) {
        if (sigwait(&mask, &signo) == -1) {
            perror("sigwait failed");
            return 1;
        }

        switch (signo) {
            case SIGINT:
                printf("Caught SIGINT via sigwait(). Exiting...\n");
                return 0;
            case SIGUSR1:
                printf("Caught SIGUSR1 via sigwait(). Continuing...\n");
                break;
            default:
                printf("Unexpected signal %d\n", signo);
        }
    }
}

三.消息队列

消息队列(Message Queue) 是一种经典的进程间通信(IPC)方式,允许不同进程通过 发送和接收消息 进行数据交换。它以队列形式存储消息,具备以下特点:

异步通信: 发送方和接收方无需同时运行,消息可在队列中暂存。
消息类型: 每条消息带有类型标签,接收方可以按类型选择性读取(而非先进先出)。
系统管理: 由操作系统内核维护,独立于进程存在(进程退出后队列可保留)。

1.核心概念与原理

1.消息结构
消息由类型(long)数据(正文) 组成,通常通过结构体定义:

struct msgbuf {
    long mtype;       // 消息类型(必须 > 0)
    char mtext[XXX];  // 消息正文(消息类型自定义、长度自定义)
};

2.内核中的消息队列
操作系统通过内核数据结构管理消息队列,每个队列包含:
1.队列 ID(唯一标识)
2.消息计数、队列字节数
3.访问权限(类似文件权限,控制读写权限)

2.消息队列的操作流程(Linux 系统)

在 Linux 中,消息队列通过 System V IPC 接口实现,主要函数包括:
msgget()、msgsnd()、msgrcv()、msgctl()。

  1. 创建 / 打开消息队列:msgget()
#include <sys/msg.h>
int msgget(key_t key, int msgflg);

参数:
key:唯一键值(通常由 ftok() 函数生成,用于标识队列)。
ftok()第一个参数是文件路径,第二个参数是项目ID(1-255)
msgflg:标志位(如 IPC_CREAT 表示创建新队列,0666 表示权限)。
返回值:成功返回队列 ID,失败返回 -1。
示例:

key_t key = ftok("/tmp/myfile", 'A'); // 生成唯一键值
int msqid = msgget(key, 0666 | IPC_CREAT); // 创建队列(权限 0666)
  1. 发送消息:msgsnd()
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

参数:
msqid:队列 ID。
msgp:指向消息结构体的指针(需包含 mtype 和 mtext)。
msgsz:消息正文的字节数(不包含 mtype 的长度)。
msgflg:标志位(如 IPC_NOWAIT 表示非阻塞发送)。
返回值:成功返回 0,失败返回 -1。
示例:

struct msgbuf msg = {
    .mtype = 1,       // 消息类型为 1
    .mtext = "Hello, Message Queue!"
};
msgsnd(msqid, &msg, sizeof(msg.mtext), 0); // 阻塞发送消息
  1. 接收消息:msgrcv()
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long mtype, int msgflg);

参数:
mtype:期望接收的消息类型,可取值:
等于0:接收任意类型的第一条消息。
大于0:接收类型等于 mtype 的第一条消息。
小于0:接收类型小于等于 abs(mtype) 的最小类型消息。
msgflg:标志位(如 IPC_NOWAIT 表示非阻塞接收,MSG_EXCEPT 表示接收类型不等于 mtype 的消息)。
返回值:成功返回消息正文长度,失败返回 -1。
示例:

struct msgbuf msg;
msgrcv(msqid, &msg, sizeof(msg.mtext), 1, 0); // 阻塞接收类型为 1 的消息
printf("Received: %s\n", msg.mtext);
  1. 控制队列:msgctl()
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

常用 cmd:
IPC_STAT:获取队列状态(存储于 buf 中)。
IPC_SET:设置队列属性(如权限)。
IPC_RMID:删除队列(buf 可为 NULL)。

示例:删除队列

msgctl(msqid, IPC_RMID, NULL); // 立即标记队列 for deletion(无进程使用时真正删除)

3.示例:简单的消息队列通信

发送方(sender.c)

#include <stdio.h>
#include <sys/msg.h>
#include <string.h>

struct msgbuf {
    long mtype;
    char mtext[100];
};

int main() {
    key_t key = ftok("msgq.c", 'A');
    int msqid = msgget(key, 0666 | IPC_CREAT);

    struct msgbuf msg = {.mtype = 1, .mtext = "Hello from sender!"};
    msgsnd(msqid, &msg, strlen(msg.mtext), 0);
    printf("Message sent: %s\n", msg.mtext);

    return 0;
}

接收方(receiver.c)

#include <stdio.h>
#include <sys/msg.h>

struct msgbuf {
    long mtype;
    char mtext[100];
};

int main() {
    key_t key = ftok("msgq.c", 'A');
    int msqid = msgget(key, 0666);

    struct msgbuf msg;
    msgrcv(msqid, &msg, sizeof(msg.mtext), 1, 0);
    printf("Received: %s\n", msg.mtext);

    // 清理队列(仅演示,实际中按需保留)
    msgctl(msqid, IPC_RMID, NULL);
    return 0;
}

编译与运行:

gcc sender.c -o sender
gcc receiver.c -o receiver
./sender &  # 后台运行发送方
./receiver  # 前台运行接收方(输出消息)

4.注意事项

消息大小限制:
Linux 中单个消息最大为 MSGMAX(通常为 8192 字节),队列总大小为 MSGMNB(通常为 16384 字节),可通过 sysctl kernel.msgmax 等命令查看 / 修改。
阻塞与非阻塞:
未指定 IPC_NOWAIT 时,msgsnd() 和 msgrcv() 会阻塞等待(队列满或无消息时)。
内存泄漏:
主动调用 msgctl(IPC_RMID) 删除不再使用的队列,避免内核资源泄漏。
权限问题:
确保进程对队列有读写权限(通过 msgget 的权限参数或 chmod 调整)。

四.共享内存

共享内存是一种高效的进程间通信(IPC)机制,允许多个进程直接访问同一块物理内存区域。这种方式避免了数据在进程间的复制,显著提高了通信效率。

1.核心概念

工作原理:
内核创建一块物理内存区域,并将其映射到多个进程的虚拟地址空间中。
进程通过各自的虚拟地址直接读写共享内存,无需通过内核中转。
关键特点
高效性:避免了用户空间与内核空间之间的数据拷贝(相比管道、消息队列等方式)。
同步问题:需要额外的同步机制(如信号量、互斥锁)来协调对共享内存的访问。
生命周期:独立于进程存在,需手动释放(除非设置为随最后一个进程自动销毁)。

2.共享内存的实现方式

1. POSIX 共享内存(推荐)
POSIX 标准提供了更现代、更灵活的共享内存接口,推荐优先使用。
1. 创建 / 打开共享内存对象:shm_open()

#include <fcntl.h>
#include <sys/mman.h>
int shm_open(const char *name, int oflag, mode_t mode);

参数:
name:共享内存对象名称(以/开头,如"/my_shm")。
oflag:标志位(如O_CREAT | O_RDWR表示创建并读写)。
mode:权限位(如0666表示所有用户可读可写)。
返回值:成功返回文件描述符,失败返回-1。
示例:

int fd = shm_open("/my_shared_memory", O_CREAT | O_RDWR, 0666);

2. 删除共享内存对象:shm_unlink()

#include <sys/mman.h>
int shm_unlink(const char *name);

作用:标记共享内存对象待删除(实际删除发生在所有进程解除映射后)。
示例:

shm_unlink("/my_shared_memory");

3. 设置共享内存大小:ftruncate()

#include <unistd.h>
int ftruncate(int fd, off_t length);

参数:
fd:shm_open()返回的文件描述符。
length:共享内存大小(字节)。
示例:

ftruncate(fd, 4096);  // 设置为4KB

4. 内存映射:mmap()

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

参数:
addr:映射地址(通常设为NULL,由系统自动分配)。
length:映射长度(需与ftruncate设置一致)。
prot:保护权限(如PROT_READ | PROT_WRITE)。
flags:标志位(如MAP_SHARED表示修改对其他进程可见)。
fd:文件描述符。
offset:偏移量(通常为0)。
返回值:成功返回映射区域的起始地址,失败返回MAP_FAILED。
示例:

char *data = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

5. 解除映射:munmap()

#include <sys/mman.h>
int munmap(void *addr, size_t length);

作用:解除内存映射,但不删除共享内存对象。
示例:

munmap(data, 4096);

函数使用流程:

#include <fcntl.h>
#include <sys/mman.h>

// 创建或打开共享内存对象
int shm_open(const char *name, int oflag, mode_t mode);

// 删除共享内存对象(实际释放需等所有进程解除映射)
int shm_unlink(const char *name);

// 映射内存区域
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

// 解除映射
int munmap(void *addr, size_t length);

1、创建或打开共享内存对象(shm_open)。
2、设置对象大小(ftruncate)。
3、将内存映射到进程的地址空间(mmap)。
4、读写共享内存。
5、解除映射(munmap)并删除对象(shm_unlink)。
2. System V 共享内存(传统方式)
System V 标准提供的传统共享内存接口,兼容性强但使用较复杂。
1. 创建 / 获取共享内存段:shmget()

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

参数:
key:唯一键值(由ftok()生成或使用IPC_PRIVATE)。
size:共享内存大小(字节)。
shmflg:标志位(如IPC_CREAT | 0666)。
返回值:成功返回共享内存 ID,失败返回-1。
示例:

key_t key = ftok("/tmp/myfile", 'A');
int shmid = shmget(key, 4096, IPC_CREAT | 0666);

2. 附加共享内存:shmat()

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

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

参数:
shmid:共享内存 ID。
shmaddr:映射地址(通常设为NULL)。
shmflg:标志位(如SHM_RDONLY表示只读)。
返回值:成功返回映射地址,失败返回(void*)-1。
示例:

char *data = shmat(shmid, NULL, 0);

3. 分离共享内存:shmdt()

#include <sys/shm.h>

int shmdt(const void *shmaddr);

作用:断开进程与共享内存的连接。
示例:

shmdt(data);

4. 控制共享内存:shmctl()

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

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

常用cmd:
IPC_RMID:删除共享内存段(实际删除发生在所有进程分离后)。
IPC_STAT:获取共享内存状态。
示例:

shmctl(shmid, IPC_RMID, NULL);

函数使用流程:

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

// 创建或获取共享内存段
int shmget(key_t key, size_t size, int shmflg);

// 连接共享内存段到进程地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg);

// 断开共享内存连接
int shmdt(const void *shmaddr);

// 控制共享内存段(如删除)
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

1、创建或获取共享内存段(shmget)。
2、将内存段附加到进程地址空间(shmat)。
3、读写共享内存。
4、分离内存段(shmdt)。
5、删除共享内存段(shmctl)。

3.POSIX vs System V 共享内存

特性 POSIX 共享内存 System V 共享内存
接口风格 基于文件描述符(更现代) 基于 ID(传统)
创建方式 通过名称(如"/my_shm") 通过键值(ftok()或IPC_PRIVATE)
删除时机 立即标记删除(所有进程解除映射后生效) 需显式调用shmctl(IPC_RMID)
同步支持 需配合信号量或互斥锁 需配合信号量
可移植性 推荐(POSIX 标准) 广泛支持(传统 UNIX 系统)

3.POSIX 共享内存示例

生产者进程(写数据):

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

#define SHM_NAME "/my_shared_memory"
#define SIZE 1024

int main() {
    // 创建共享内存对象
    int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
    if (fd == -1) {
        perror("shm_open failed");
        exit(EXIT_FAILURE);
    }

    // 设置共享内存大小
    if (ftruncate(fd, SIZE) == -1) {
        perror("ftruncate failed");
        exit(EXIT_FAILURE);
    }

    // 映射共享内存
    char *data = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (data == MAP_FAILED) {
        perror("mmap failed");
        exit(EXIT_FAILURE);
    }

    // 写入数据
    sprintf(data, "Hello from producer! Time: %ld", time(NULL));

    // 解除映射
    if (munmap(data, SIZE) == -1) {
        perror("munmap failed");
        exit(EXIT_FAILURE);
    }

    // 关闭文件描述符
    close(fd);

    // 提示消费者可以读取
    printf("Data written to shared memory. Run consumer now.\n");
    return 0;
}

消费者进程(读数据):

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

#define SHM_NAME "/my_shared_memory"
#define SIZE 1024

int main() {
    // 打开共享内存对象
    int fd = shm_open(SHM_NAME, O_RDONLY, 0666);
    if (fd == -1) {
        perror("shm_open failed");
        exit(EXIT_FAILURE);
    }

    // 映射共享内存
    char *data = mmap(NULL, SIZE, PROT_READ, MAP_SHARED, fd, 0);
    if (data == MAP_FAILED) {
        perror("mmap failed");
        exit(EXIT_FAILURE);
    }

    // 读取数据
    printf("Read from shared memory: %s\n", data);

    // 解除映射
    if (munmap(data, SIZE) == -1) {
        perror("munmap failed");
        exit(EXIT_FAILURE);
    }

    // 关闭文件描述符
    close(fd);

    // 删除共享内存对象
    if (shm_unlink(SHM_NAME) == -1) {
        perror("shm_unlink failed");
        exit(EXIT_FAILURE);
    }

    return 0;
}

4.System V 共享内存示例

生产者进程(producer.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

#define SHM_KEY 12345      // 共享内存键值
#define SHM_SIZE 1024      // 共享内存大小(字节)
#define SHM_PERM 0666      // 共享内存权限

int main() {
    int shmid;
    char *shm_addr;
    const char *message = "Hello from producer!";

    // 创建共享内存段
    shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | SHM_PERM);
    if (shmid == -1) {
        perror("shmget failed");
        exit(EXIT_FAILURE);
    }

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

    // 写入数据到共享内存
    strncpy(shm_addr, message, strlen(message) + 1);
    printf("Producer wrote: %s\n", shm_addr);

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

    // 注意:生产者不删除共享内存段,由消费者删除
    printf("Producer detached from shared memory.\n");

    return 0;
}

消费者进程(consumer.c)

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

#define SHM_KEY 12345      // 共享内存键值
#define SHM_SIZE 1024      // 共享内存大小(字节)

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

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

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

    // 从共享内存读取数据
    printf("Consumer read: %s\n", shm_addr);

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

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

    printf("Consumer detached and removed shared memory.\n");

    return 0;
}

编译和运行步骤
编译程序:

gcc producer.c -o producer
gcc consumer.c -o consumer

先运行生产者,再运行消费者:

./producer
./consumer

运行结果:

# 生产者输出
Producer wrote: Hello from producer!
Producer detached from shared memory.

# 消费者输出
Consumer read: Hello from producer!
Consumer detached and removed shared memory.

5.同步与互斥

共享内存本身不提供同步机制,需结合其他工具确保数据一致性:

信号量(Semaphore)
控制对共享资源的访问权限(如 POSIX 信号量 sem_t)。
示例:

// 在共享内存中包含信号量
struct SharedData {
    sem_t mutex;  // 互斥信号量
    int counter;  // 共享数据
};

互斥锁(Mutex)
适用于多线程环境,需设置为进程间共享(pthread_mutexattr_setpshared)。
原子操作
对于简单数据(如计数器),使用原子类型(stdatomic.h)避免竞态条件。

6.优缺点与适用场景

优点 缺点
最高的通信效率(无需数据拷贝) 需手动处理同步问题
适合大量数据的实时传输 内存管理复杂(易泄漏)
支持跨进程的高性能计算 错误处理困难(如进程崩溃可能导致数据损坏)

7.注意事项

内存对齐:确保数据结构按系统要求对齐,避免性能下降。
进程崩溃处理:设计时考虑进程异常退出的情况(如使用信号处理函数释放资源)。
权限控制:通过 shm_open 的 mode 参数设置适当的访问权限。
清理机制:确保不再使用的共享内存被正确删除(避免内核资源泄漏)。

8.与其他 IPC 方式的对比

方式 数据拷贝次数 适用场景
共享内存 0 次 大量数据、高性能需求
消息队列 2 次 异步通信、按类型过滤消息
管道 2 次 流式数据、父子进程通信
套接字 4 次 跨主机通信

五.信号量组

信号量组(Semaphore Set)是操作系统中用于实现进程同步和互斥 的机制,它允许将多个信号量作为一个整体进行管理,以处理复杂的资源依赖关系。以下是关于信号量组的详细介绍:

1.基本概念

信号量(Semaphore)
由荷兰计算机科学家 Edsger Dijkstra 提出,是一种用于协调多进程 / 线程对共享资源访问的计数器。
P 操作(等待):减少信号量值,若结果为负数则阻塞进程。
V 操作(释放):增加信号量值,若之前有进程阻塞则唤醒其中一个。
信号量组
将多个信号量组合在一起,作为一个单元进行操作,用于处理多个资源或复杂的同步关系。
例如:一个进程需要同时获取多个资源(如打印机和磁盘)时,可使用信号量组原子性地操作多个信号量。

2.核心接口(System V IPC)

在 Linux 系统中,信号量组通过 System V IPC 接口实现,主要函数包括:
semget()、semop()、semctl()。

1. 创建 / 获取信号量组:semget()

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);

参数:
key:唯一键值(通常由 ftok() 生成)。
nsems:信号量组中的信号量数量。
semflg:标志位(如 IPC_CREAT | 0666 表示创建并设置权限)。
返回值:成功返回信号量组 ID,失败返回 -1。

2. 操作信号量组:semop()

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned nsops);

参数:
semid:信号量组 ID。
sops:指向 struct sembuf 数组的指针,每个元素表示一个操作:

struct sembuf {
    unsigned short sem_num;  // 信号量在组中的索引(从 0 开始)
    short          sem_op;   // 操作值:-1(P 操作)、+1(V 操作)
    short          sem_flg;  // 标志位(如 IPC_NOWAIT 表示非阻塞)
};

nsops:sops 数组的元素数量。
特点:
所有操作作为原子操作执行,要么全部成功,要么全部失败。
若操作导致信号量值为负,进程默认会阻塞(除非设置 IPC_NOWAIT)。

3. 控制信号量组:semctl()

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);

常用 cmd:
SETVAL:设置单个信号量的值(需传入 union semun 类型的第四个参数)。
IPC_RMID:删除信号量组(无需 semnum,第四个参数可为 NULL)。
GETVAL:获取信号量的当前值。

示例:初始化信号量值

// 定义 union semun(某些系统需要手动定义)
union semun {
    int              val;    // SETVAL 使用的值
    struct semid_ds *buf;    // IPC_STAT/IPC_SET 使用的缓冲区
    unsigned short  *array;  // GETALL/SETALL 使用的数组
};

// 设置第一个信号量的值为 1
union semun arg;
arg.val = 1;
semctl(semid, 0, SETVAL, arg);

3.信号量组的典型应用场景

多资源管理
当一个进程需要同时获取多个资源时,使用信号量组确保原子性操作。
例如:数据库连接池需要同时分配连接和锁。
生产者 - 消费者模型
使用信号量组管理缓冲区的空闲槽位和已占用槽位:

// 信号量组包含两个信号量
// 0: empty(空闲槽位数量,初始值为缓冲区大小)
// 1: full(已占用槽位数量,初始值为 0)

哲学家就餐问题
每个叉子作为一个信号量,使用信号量组避免死锁(如同时获取左右叉子)。

4.示例:使用信号量组实现同步

场景:两个进程通过信号量组协调对共享资源的访问。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>

#define SEM_KEY 12345
#define N_SEMS 2  // 两个信号量:0 用于资源可用,1 用于互斥锁

// P 操作(等待)
void sem_wait(int semid, int semnum) {
    struct sembuf op = {
        .sem_num = semnum,
        .sem_op = -1,
        .sem_flg = 0
    };
    semop(semid, &op, 1);
}

// V 操作(释放)
void sem_signal(int semid, int semnum) {
    struct sembuf op = {
        .sem_num = semnum,
        .sem_op = 1,
        .sem_flg = 0
    };
    semop(semid, &op, 1);
}

int main() {
    // 创建信号量组
    int semid = semget(SEM_KEY, N_SEMS, IPC_CREAT | 0666);
    if (semid == -1) {
        perror("semget failed");
        exit(EXIT_FAILURE);
    }

    // 初始化信号量(仅需执行一次,通常由第一个进程完成)
    union semun arg;
    arg.val = 1;  // 资源可用初始值为 1
    semctl(semid, 0, SETVAL, arg);
    arg.val = 1;  // 互斥锁初始值为 1
    semctl(semid, 1, SETVAL, arg);

    // 模拟对共享资源的访问
    printf("Process %d is waiting for resources...\n", getpid());
    // 先获取资源信号量,再获取互斥锁
    sem_wait(semid, 0);
    sem_wait(semid, 1);
    printf("Process %d is using the shared resource.\n", getpid());
    sleep(2);  // 模拟资源使用
    // 释放锁和资源
    sem_signal(semid, 1);
    sem_signal(semid, 0);
    printf("Process %d has released the resource.\n", getpid());
    // 仅最后一个进程需要删除信号量组
    // semctl(semid, 0, IPC_RMID);

    return 0;
}

5.注意事项

1、信号量的生命周期
信号量组由内核维护,进程退出后不会自动销毁,需手动调用 semctl(IPC_RMID) 删除。
2、死锁预防
对信号量的操作顺序必须一致,避免循环等待。
可使用银行家算法等策略进行资源分配控制。
3、错误处理
semop() 操作失败时,可能需要回滚已执行的部分操作(如通过 SEM_UNDO 标志自动恢复)。
4、跨平台差异
System V 信号量在不同系统中的实现可能略有差异,需注意兼容性。

6.与其他同步机制的对比

机制 特点 适用场景
信号量组 支持多资源原子操作,跨进程 复杂资源依赖场景
互斥锁 简单的二元锁,不可跨进程(需特殊设置) 单进程内多线程同步
条件变量 结合互斥锁使用,等待特定条件 线程间复杂条件同步
读写锁 允许多个读或单个写 读多写少的场景
posted @ 2025-05-18 17:17  ice_cui  阅读(314)  评论(0)    收藏  举报