进程间通信方式

2025-08-15 20:31:09 世界杯瑞典 5256

进程间通信

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

一.管道通信

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

匿名管道(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

#include

#include

#include

#include

#include

#include

#include

#include

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

#include

#include

#include

#include

#include

#include

#include

#include

#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

#include

#include

#include

#include

#include

#include

#include

#include

#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

int kill(pid_t pid, int sig);

参数:

pid:目标进程的 PID(若为0,则发送给同一进程组的所有进程)。

sig:要发送的信号编号(如SIGINT)或0(用于检测进程是否存在)。

返回值:

成功返回0,失败返回-1。

2. signal()函数:设置信号处理函数(简单接口)

#include

typedef void (*sighandler_t)(int);

sighandler_t signal(int sig, sighandler_t handler);

参数:

sig:信号编号。

handler:处理函数指针(如SIG_IGN表示忽略,SIG_DFL表示使用默认处理)。

注意:

该接口在 POSIX 标准中已被sigaction()取代,部分系统可能存在兼容性问题。

3. sigaction()函数:设置信号处理函数(推荐接口)

#include

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

#include

#include

// 信号处理函数

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

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

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

int sigpending(sigset_t *set);

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

3.典型应用场景

1.保护关键代码段

在执行不可中断的操作(如共享资源的修改)时,暂时阻塞某些信号。

2.避免竞态条件

在信号处理函数和主程序之间同步时,防止信号在不适当的时机到达。

3.资源清理阶段

在释放重要资源(如文件锁、内存)时,确保不会被信号中断。

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

#include

#include

#include

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

#include

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()。

创建 / 打开消息队列:msgget()

#include

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)

发送消息:msgsnd()

#include

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); // 阻塞发送消息

接收消息:msgrcv()

#include

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

控制队列:msgctl()

#include

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

#include

#include

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

#include

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

#include

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

int shm_unlink(const char *name);

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

示例:

shm_unlink("/my_shared_memory");

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

#include

int ftruncate(int fd, off_t length);

参数:

fd:shm_open()返回的文件描述符。

length:共享内存大小(字节)。

示例:

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

4. 内存映射:mmap()

#include

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

int munmap(void *addr, size_t length);

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

示例:

munmap(data, 4096);

函数使用流程:

#include

#include

// 创建或打开共享内存对象

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

#include

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

#include

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

int shmdt(const void *shmaddr);

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

示例:

shmdt(data);

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

#include

#include

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

常用cmd:

IPC_RMID:删除共享内存段(实际删除发生在所有进程分离后)。

IPC_STAT:获取共享内存状态。

示例:

shmctl(shmid, IPC_RMID, NULL);

函数使用流程:

#include

#include

// 创建或获取共享内存段

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

#include

#include

#include

#include

#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

#include

#include

#include

#include

#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

#include

#include

#include

#include

#include

#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

#include

#include

#include

#include

#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

#include

#include

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

参数:

key:唯一键值(通常由 ftok() 生成)。

nsems:信号量组中的信号量数量。

semflg:标志位(如 IPC_CREAT | 0666 表示创建并设置权限)。

返回值:成功返回信号量组 ID,失败返回 -1。

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

#include

#include

#include

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

#include

#include

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

#include

#include

#include

#include

#include

#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.与其他同步机制的对比

机制

特点

适用场景

信号量组

支持多资源原子操作,跨进程

复杂资源依赖场景

互斥锁

简单的二元锁,不可跨进程(需特殊设置)

单进程内多线程同步

条件变量

结合互斥锁使用,等待特定条件

线程间复杂条件同步

读写锁

允许多个读或单个写

读多写少的场景

煮狗肉放什么调料好吃,煮狗肉的正确方法
联通网速测试