进程间通信
进程间通信方式:管道通信、信号、消息队列、共享内存、信号量组
一.管道通信
管道通信分为匿名管道以及有名管道(命名管道),匿名管道用于父进程与子进程之间(具有亲缘的进程之间)。命名管道可以用于非亲缘进程之间进行通信。
匿名管道(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.与其他同步机制的对比
机制
特点
适用场景
信号量组
支持多资源原子操作,跨进程
复杂资源依赖场景
互斥锁
简单的二元锁,不可跨进程(需特殊设置)
单进程内多线程同步
条件变量
结合互斥锁使用,等待特定条件
线程间复杂条件同步
读写锁
允许多个读或单个写
读多写少的场景
煮狗肉放什么调料好吃,煮狗肉的正确方法联通网速测试