代码实现
文件:ipc_demo.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/msg.h>
#include <sys/wait.h>
#include <semaphore.h>
#include <signal.h>
#include <fcntl.h>
#define SHM_SIZE 1024
#define MSG_TYPE 1
// 定义消息结构体
struct msg_buffer {
long msg_type;
char msg_text[100];
};
// 全局信号量指针
sem_t *sem;
int shm_id;
void *shm_ptr;
int msgq_id;
int pipe_fd[2];
// 信号处理函数
void sigusr1_handler(int sig) {
printf("进程 %d 收到信号 SIGUSR1\n", getpid());
}
int main() {
// 1. 创建共享内存
shm_id = shmget(IPC_PRIVATE, SHM_SIZE, IPC_CREAT | 0666);
if (shm_id == -1) {
perror("shmget 失败");
exit(1);
}
// 2. 附加共享内存
shm_ptr = shmat(shm_id, NULL, 0);
if (shm_ptr == (void *)-1) {
perror("shmat 失败");
exit(1);
}
// 3. 初始化信号量(进程间共享)
sem = sem_open("/demo_sem", O_CREAT, 0666, 1);
if (sem == SEM_FAILED) {
perror("sem_open 失败");
exit(1);
}
// 4. 创建消息队列
msgq_id = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
if (msgq_id == -1) {
perror("msgget 失败");
exit(1);
}
// 5. 创建管道
if (pipe(pipe_fd) == -1) {
perror("pipe 失败");
exit(1);
}
// 6. 注册信号处理函数
signal(SIGUSR1, sigusr1_handler);
pid_t pid = fork();
if (pid < 0) {
perror("fork 失败");
exit(1);
} else if (pid == 0) { // 子进程
// 从管道读取数据
close(pipe_fd[1]); // 关闭写端
char pipe_data[100];
read(pipe_fd[0], pipe_data, sizeof(pipe_data));
printf("子进程从管道读取: %s\n", pipe_data);
close(pipe_fd[0]);
// 发送消息到消息队列
struct msg_buffer msg;
msg.msg_type = MSG_TYPE;
snprintf(msg.msg_text, sizeof(msg.msg_text), "子进程已启动");
msgsnd(msgq_id, &msg, sizeof(msg.msg_text), 0);
// 等待信号量并写入共享内存
sem_wait(sem);
snprintf((char *)shm_ptr, SHM_SIZE, "子进程写入共享内存");
sem_post(sem);
// 向父进程发送信号
kill(getppid(), SIGUSR1);
exit(0);
} else { // 父进程
// 向管道写入数据
close(pipe_fd[0]); // 关闭读端
write(pipe_fd[1], "来自父进程的管道数据", 100);
close(pipe_fd[1]);
// 从消息队列接收消息
struct msg_buffer msg;
msgrcv(msgq_id, &msg, sizeof(msg.msg_text), MSG_TYPE, 0);
printf("父进程收到消息队列消息: %s\n", msg.msg_text);
// 等待子进程完成共享内存操作
sem_wait(sem);
printf("父进程读取共享内存: %s\n", (char *)shm_ptr);
sem_post(sem);
// 等待子进程结束
wait(NULL);
// 清理资源
shmdt(shm_ptr);
shmctl(shm_id, IPC_RMID, NULL);
sem_close(sem);
sem_unlink("/demo_sem");
msgctl(msgq_id, IPC_RMID, NULL);
}
return 0;
}
编译与运行
- 编译代码
gcc ipc_demo.c -o ipc_demo -lrt -pthread
-lrt
: 链接实时库(共享内存和信号量)。-pthread
: 支持信号量线程安全。
- 运行程序
./ipc_demo
- 输出示例
父进程收到消息队列消息: 子进程已启动
子进程从管道读取: 来自父进程的管道数据
父进程读取共享内存: 子进程写入共享内存
进程 <父进程PID> 收到信号 SIGUSR1
解析
int shmget(key_t key, size_t size, int shmflg);
shmget
函数用于创建或访问一个共享内存段。key
参数用于指定共享内存段的键,以标识共享内存。如果key为IPC_PRIVATE,则创建一个新的共享内存段。size
参数指定共享内存段的大小(以字节为单位)。shmflg
参数是标志位,可以是权限标志(如0666)和控制标志(如IPC_CREAT)的组合。
shm_id = shmget(IPC_PRIVATE, SHM_SIZE, IPC_CREAT | 0666);
IPC_PRIVATE
:创建一个新的共享内存段,并返回一个唯一的标识符(即共享内存ID),这个标识符只在当前进程及其子进程中有效。SHM_SIZE
:这是一个宏或常量,表示共享内存段的大小。在实际代码中,你需要定义这个宏或常量,例如#define SHM_SIZE 1024,表示创建一个1024字节大小的共享内存段。IPC_CREAT | 0666
:IPC_CREAT标志表示如果指定的共享内存段不存在,则创建它。0666是权限标志,指定新创建的共享内存段的访问权限(所有者有读写权限,组用户和其他用户有读权限)。
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmat
函数用于将共享内存段附加到调用进程的地址空间中。shmid
参数是共享内存段的标识符(即之前通过shmget获得的ID)。shmaddr
参数指定共享内存段附加到调用进程地址空间中的位置。如果为NULL,则由系统选择附加地址。shmflg
参数是标志位,用于控制附加行为。如果为0,则默认行为是附加共享内存段,并允许读写。
shm_ptr = shmat(shm_id, NULL, 0);
shm_id
:之前通过shmget函数获得的共享内存段标识符。NULL
:表示让系统选择共享内存段附加到进程地址空间中的位置。0
:标志位,表示默认行为,即附加共享内存段并允许读写。
shmat函数返回共享内存段在调用进程地址空间中的起始地址。如果成功,这个地址被赋值给shm_ptr,用于后续对共享内存的访问。
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
sem_open
函数用于打开或创建一个新的POSIX信号量。name
参数指定信号量的名称,这是一个以null结尾的字符串。在命名空间中,该名称是唯一的。oflag
参数控制函数的行为。若包含O_CREAT,则当信号量不存在时创建它;若同时包含O_EXCL,则仅当信号量不存在时才创建,否则返回错误。mode
参数设置信号量的权限(仅在创建新信号量时有效)。value
参数指定信号量的初始值。
sem = sem_open("/demo_sem", O_CREAT, 0666, 1);
"/demo_sem"
:信号量的名称,它是一个以null结尾的字符串,在POSIX命名空间中唯一。
O_CREAT:若信号量不存在,则创建它。0666
:新创建的信号量的权限设置(所有者有读写权限,组用户和其他用户也有读写权限,但受限于系统的umask值)。1
:信号量的初始值,通常用于表示资源的可用数量。
sem_open函数成功时返回一个指向sem_t类型的指针,该指针代表打开或创建的信号量。若失败,则返回SEM_FAILED。
msgq_id = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
-
msgget
函数用于获取一个消息队列的标识符。如果消息队列不存在,并且指定了IPC_CREAT标志,则创建一个新的消息队列。 -
IPC_PRIVATE
是一个特殊的键值,用于创建一个新的、只由创建者进程及其子进程或明确知道该标识符的进程可见的消息队列。这意味着这个消息队列是私有的,不会与系统中其他消息队列冲突。 -
0666
是消息队列的访问权限(模式),类似于文件系统的权限设置。这里设置为可读可写权限给所有用户(尽管在实际操作中,这些权限可能会受到系统的umask值的影响)。 -
IPC_CREAT
是一个标志,指示如果指定的消息队列不存在,则创建它。如果消息队列已经存在,并且调用进程没有权限创建消息队列,则msgget会失败,除非同时也指定了IPC_EXCL标志(本例中未使用)。 -
if (pipe(pipe_fd) == -1) {:
这行代码检查pipe函数的返回值。如果返回-1,则表示管道创建失败。 -
perror(“pipe 失败”);:
如果管道创建失败,perror函数会被调用。perror函数会打印一个描述最近一次库函数调用失败的错误消息到标准错误输出(stderr)。这里传递的字符串"pipe 失败"会被打印在错误消息之前,提供额外的上下文信息。 -
exit(1);:
如果管道创建失败,程序通过调用exit函数终止执行。exit的参数1通常表示程序异常终止。在UNIX和类UNIX系统中,返回非零值给操作系统通常表示程序遇到了错误或异常情况。
signal(SIGUSR1, sigusr1_handler);
signal
函数:
signal函数用于设置一个信号的处理函数。当指定的信号到达时,操作系统会调用这个函数来处理该信号。
// 定义一个消息缓冲区结构体实例
struct msg_buffer msg;
// 设置消息类型
msg.msg_type = MSG_TYPE;
// 格式化消息文本并存储到消息缓冲区中
snprintf(msg.msg_text, sizeof(msg.msg_text), "子进程已启动");
// 发送消息到消息队列
msgsnd(msgq_id, &msg, sizeof(msg.msg_text), 0);
- 定义消息缓冲区:
struct msg_buffer msg; 定义了一个名为msg的消息缓冲区结构体实例。这里假设msg_buffer结构体已经被定义,并且包含至少两个成员:msg_type(消息类型,通常为长整型)和msg_text(消息文本,通常为字符数组)。 - 设置消息类型:
msg.msg_type = MSG_TYPE; 将消息类型设置为MSG_TYPE。MSG_TYPE是一个预先定义的常量,用于区分消息队列中的不同消息。接收消息时,可以根据消息类型来筛选和处理特定的消息。 - 格式化消息文本:
snprintf(msg.msg_text, sizeof(msg.msg_text), “子进程已启动”); 使用snprintf函数将字符串"子进程已启动"格式化并存储到msg.msg_text中。snprintf函数确保不会超出msg_text数组的边界,从而避免缓冲区溢出。 - 发送消息到消息队列:
msgsnd(msgq_id, &msg, sizeof(msg.msg_text), 0); 调用msgsnd函数将消息发送到消息队列。msgq_id是消息队列的标识符,&msg是指向消息缓冲区的指针,sizeof(msg.msg_text)是消息数据的长度(注意:这里应该传递整个消息结构体的大小,即sizeof(msg),如果消息队列的设计包含了除msg_text之外的其他重要数据。但在这个例子中,为了简化,只发送了msg_text部分的大小。这种做法在实际应用中可能导致数据丢失或错误)。0是发送消息的标志,表示没有特殊选项。 - 重要注意事项:
在调用msgsnd时,应该传递整个消息结构体的大小(sizeof(msg)),而不是仅传递消息文本的大小(sizeof(msg.msg_text)),除非消息队列的设计确实只关心消息文本部分。
如果消息队列的最大消息大小小于要发送的消息大小,msgsnd函数将失败。
发送消息时,如果消息队列已满,msgsnd函数可能会阻塞,除非指定了IPC_NOWAIT标志。在这个例子中,没有使用IPC_NOWAIT标志,所以msgsnd在消息队列满时会阻塞。
综上所述,这段代码的目的是创建一个消息,设置其类型和文本内容,然后将其发送到指定的消息队列中。但是,请确保在实际应用中正确传递消息结构体的大小,并考虑消息队列的状态和可能的阻塞情况。
kill(getppid(), SIGUSR1);
getppid()
函数:
getppid() 函数返回当前进程的父进程ID(PID)。在UNIX和类UNIX系统中,每个进程都有一个唯一的PID,而子进程可以通过调用getppid()来获取其父进程的PID。kill()
函数:
kill() 函数用于向指定进程发送信号。其原型通常如下:
int kill(pid_t pid, int sig);
这里,pid 是目标进程的ID,sig 是要发送的信号编号。
在这个例子中,kill() 函数被用来向父进程(其PID通过getppid()获取)发送SIGUSR1信号。
SIGUSR1
信号:
SIGUSR1 是一个用户定义的信号,它不属于内核定义的、具有特定行为的标准信号集。相反,SIGUSR1 和它的姊妹信号 SIGUSR2 允许用户(即程序员)为进程间通信或进程内事件通知定义任何所需的行为。
接收进程需要有一个信号处理函数(handler)来响应 SIGUSR1 信号,否则,默认行为可能是终止进程(但这可以通过编程来更改)。- 重要注意事项:
在发送信号之前,应确保父进程是存在的,并且正在运行。如果父进程已经终止,kill() 函数可能会失败。
父进程需要有一个注册的信号处理函数来响应 SIGUSR1 信号,否则它可能会按照默认方式处理该信号(例如,终止)。
使用 kill() 函数发送信号时,发送进程和接收进程需要有适当的权限。通常,向自己的子进程发送信号是没有问题的,但向其他用户的进程发送信号可能会受到权限限制。
在进程间通信(IPC)中,进程的执行顺序本质上是异步的,操作系统调度可能导致父子进程交替执行。但在上述综合案例中,通过合理的同步机制和IPC特性确保了操作的顺序性。以下是详细解释:
1. 管道操作的顺序保证
代码逻辑
// 父进程
close(pipe_fd[0]); // 关闭读端
write(pipe_fd[1], data); // 写入数据
close(pipe_fd[1]); // 关闭写端
wait(NULL); // 等待子进程结束
// 子进程
close(pipe_fd[1]); // 关闭写端
read(pipe_fd[0], buffer); // 读取数据
close(pipe_fd[0]);
关键机制
-
管道的阻塞特性:
read
操作在以下两种情况下会立即返回:- 管道中有数据:读取数据。
- 所有写端关闭:返回
0
(EOF)。
write
操作在以下两种情况下会阻塞:- 管道缓冲区满:等待读端消费数据。
- 所有读端关闭:触发
SIGPIPE
信号(通常导致进程终止)。
-
隐式同步:
- 父进程优先写入:
父进程在fork
后立即执行write
,而子进程需要通过read
等待数据。
即使子进程先被调度执行,也会在read
处阻塞,直到父进程写入数据并关闭写端。 - 写端关闭触发EOF:
父进程在写入后关闭写端 (close(pipe_fd[1]
),子进程读取完数据后,read
会返回0
,避免无限阻塞。
- 父进程优先写入:
2. 其他IPC操作的顺序保证
(1) 消息队列的同步
// 子进程发送消息
msgsnd(msgq_id, &msg, sizeof(msg.msg_text), 0);
// 父进程接收消息
msgrcv(msgq_id, &msg, sizeof(msg.msg_text), MSG_TYPE, 0); // 阻塞等待
- 消息队列的特性:
msgrcv
默认处于阻塞模式,父进程会一直等待,直到子进程发送消息。- 父子进程通过消息队列形成了显式的同步点:
父进程必须等到子进程发送消息后才会继续执行后续代码。
(2) 共享内存与信号量
// 子进程写入共享内存
sem_wait(sem); // 加锁
sprintf(shm_ptr, "子进程写入共享内存");
sem_post(sem); // 解锁
// 父进程读取共享内存
sem_wait(sem); // 加锁(等待子进程释放)
printf("父进程读取共享内存: %s\n", (char *)shm_ptr);
sem_post(sem); // 解锁
- 信号量的同步作用:
- 信号量初始值为
1
,子进程通过sem_wait
获得锁并写入共享内存。 - 父进程在读取前调用
sem_wait
,若子进程尚未释放锁,父进程会阻塞直到子进程完成写入并调用sem_post
。 - 通过信号量强制实现了 “先写后读” 的顺序。
- 信号量初始值为
(3) 信号(SIGUSR1)
// 子进程发送信号
kill(getppid(), SIGUSR1);
// 父进程注册处理函数
signal(SIGUSR1, sigusr1_handler);
- 信号的异步性:
父进程可能在任意时刻收到信号,但代码逻辑中信号的发送(子进程)和接收(父进程)通过以下方式保证顺序:- 子进程在完成共享内存写入和消息发送后发送信号。
- 父进程在处理信号前已经执行了
wait(NULL)
,确保子进程已完成所有操作。
3. 竞态条件与解决方案
潜在风险
- 若未使用同步机制,可能存在以下问题:
- 子进程在父进程写入前读取空管道(
read
阻塞直到父进程写入)。 - 父进程读取共享内存时,子进程尚未写入(读取到无效数据)。
- 消息队列的接收端先于发送端执行(父进程阻塞等待,直到子进程发送消息)。
- 子进程在父进程写入前读取空管道(
解决方案
- 管道:依赖
write
和read
的阻塞特性隐式同步。 - 消息队列:通过阻塞模式的
msgrcv
显式同步。 - 共享内存:使用信号量强制实现互斥访问。
- 信号:通过进程执行逻辑(如
wait
)保证时序。
4. 流程时序图
父进程 子进程
| |
| 创建共享内存、消息队列、管道
| 初始化信号量
| fork()
|----------------------->|
| |
| 关闭读端,写入管道 |
| 关闭写端 |
| | 关闭写端,读取管道
| | 发送消息到消息队列
| | 加锁写入共享内存
| | 发送SIGUSR1信号
| |
| 等待子进程结束 (wait) |
| 从消息队列接收消息 |
| 加锁读取共享内存 |
| 处理信号 |
| 资源清理 |
总结
- 管道:通过阻塞读写和关闭描述符的引用计数隐式同步。
- 消息队列:通过阻塞接收显式同步。
- 共享内存:通过信号量强制互斥访问。
- 信号:结合进程等待(
wait
)实现时序控制。 - 竞态避免:综合使用特性+显式同步机制,而非依赖进程调度顺序。
这种设计确保了即使进程执行顺序不确定,关键操作仍按预期顺序执行,适用于需要严格同步的嵌入式场景(如工业控制、传感器数据处理)。
关键知识点总结
IPC 机制 | 特点 | 适用场景 |
---|---|---|
共享内存 | 高速、无拷贝,需同步机制(如信号量) | 大数据量交换(如视频流处理) |
信号量 | 解决资源竞争,支持进程/线程同步 | 临界区保护 |
消息队列 | 结构化数据,支持优先级,内核持久化 | 进程间命令传递 |
管道 | 简单单向通信,内核缓冲区 | 父子进程简单数据流 |
信号 | 异步通知,仅传递信号编号 | 异常处理或事件通知 |
常见问题
-
共享内存未同步导致数据竞争
- 必须使用信号量或互斥锁保护共享内存访问。
-
消息队列或共享内存未清理
- 通过
ipcs
查看残留资源,使用ipcrm
手动删除。
- 通过
-
信号处理函数不可重入
- 避免在信号处理函数中调用
printf
等非异步安全函数。
- 避免在信号处理函数中调用
结语
这里的案例里面,我留了一个坑,有兴趣的可以找出来,这或许对理解IPC有很大的帮助!
参考逻辑
通过以下 IPC 机制实现父子进程协作:
- 共享内存:存储共享数据。
- 信号量:同步共享内存的访问。
- 消息队列:传递控制指令。
- 管道:传输简单数据。
- 信号(SIGUSR1):通知进程处理数据。