Linux下进程间通信概述
常用的进程间通信方式
1、传统的进程间通信方式
无名管道(pipe)、有名管道(fifo)和信号(signal)
2、System V IPC对象
共享内存(share memory)、消息队列(message queue)和信号灯(semaphore)
3、BSD
套接字(socket)
无名管道

无名管道特点:
只能用于具有亲缘关系的进程之间的通信;(如父子进程、兄弟进程等)
半双工的通信模式,具有固定的读端和写端;
管道可以看成是一种特殊的文件,对于它的读写可以使用文件IO如read、write函数。
管道创建与关闭


管道读写注意点

#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int fd[2] = {0};
char buf[65536] = {0};
if(pipe(fd) < 0)
{
perror("pipe err");
return -1;
}
/*write(fd[1], "hello", 5);
read(fd[0], buf, 5);
printf("buf:%s\n", buf);
read(fd[0], buf, 5);
printf("buf:%s\n", buf);
*/
write(fd[1],buf, 65536);
read(fd[0], buf, 4095);
printf("befor write\n");
write(fd[1],"a", 1);
printf("after write\n");
printf("%d %d\n", fd[0], fd[1]);
return 0;
}
练习:fork创建进程,父进程中循环从终端输入
子进程负责把数据打印到终端,当输入quit程序退出
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
int fd[2] = {0};
pid_t pid;
char buf[32] = {0};
ssize_t s;
if (pipe(fd) < 0)
{
perror("pipe err");
return -1;
}
if ((pid = fork()) < 0)
{
perror("fork err");
return -1;
}
else if (pid == 0)
{
while (1)
{
s = read(fd[0], buf, 32);
if (strncmp(buf, "quit", 4) == 0)
break;
printf("buf: %s", buf);
}
exit(0);
}
else
{
while (1)
{
fgets(buf, 32, stdin);
write(fd[1], buf, strlen(buf)+1);
if (strncmp(buf, "quit", 4) == 0)
break;
}
wait(NULL);
}
return 0;
}
不使用strlen(buf)+1或者使用
memset(buf, 0, sizeof(buf)); // 清空自定义缓冲区buf
有名管道
FIFO
无名管道只能用于具有亲缘关系的进程之间,这就限制了无名管道的使用范围。
有名管道可以使互不相关的两个进程互相通信。有名管道可以通过路径名来指出,并且在文件系统中可见。
进程通过文件IO来操作有名管道、有名管道遵循先进先出规则、不支持如lseek()操作。

还需要头文件 #include <sys/stat.h>

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
int main(char argc, char *argv[])
{
int fd;
char buf[32] = {0};
if(mkfifo("./fifo", 0666) < 0)
{
if(errno == EEXIST)
printf("fifo file exist\n");
else
{
perror("mkfifo err");
return -1;
}
}
fd = open("./fifo", O_WRONLY);
write(fd, "hello", 5);
read(fd,buf,5);
printf("%s\n",buf);
printf("mkfifo success\n");
return 0;
}
两个进程链接
read.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
int main(char argc, char *argv[])
{
int fd;
char buf[32] = {0};
if(mkfifo("./fifo", 0666) < 0)
{
if(errno == EEXIST)
printf("fifo file exist\n");
else
{
perror("mkfifo err");
return -1;
}
}
fd = open("./fifo", O_WRONLY);
write(fd, "hello", 5);
read(fd,buf,5);
printf("%s\n",buf);
printf("mkfifo success\n");
return 0;
}
write.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
int main(char argc, char *argv[])
{
int fd;
char buf[32] = {0};
if(mkfifo("./fifo", 0666) < 0)
{
if(errno == EEXIST)
printf("fifo file exist\n");
else
{
perror("mkfifo err");
return -1;
}
}
fd = open("./fifo", O_WRONLY);
write(fd, "hello", 5);
read(fd,buf,5);
printf("%s\n",buf);
printf("mkfifo success\n");
return 0;
}
信号
信号通信


使用 kill -l 查看信号


信号发送与捕捉
kill()和raise()
kill函数同读者熟知的kill系统命令一样,可以发送信号给进程或进程组(实际上,kill系统命令只是kill函数的一个用户接口)。
kill–l命令查看系统支持的信号列表。
raise函数允许进程向自己发送信号。
alarm()和pause()
alarm()也称为闹钟函数,它可以在进程中设置一个定时器。当定时器指定的时间到时,内核就向进程发送SIGALARM信号。(以秒为单位,若设置多个定时器,按最后一个执行)
pause()函数是用于将调用进程挂起直到收到信号为止。

#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
/*
//kill(getpid(),SIGINT);
raise(SIGINT);
while(1);
*/
printf("%d\n",alarm(3)); //第一次调用返回0
//sleep(10);
printf("%d\n",alarm(2));//多次调用函数,返回上一次定时剩余时间
pause(); //将进程挂起,直到收到信号终止
pause(); //将进程挂起,直到收到信号终止
return 0;
}
信号的处理


#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
void handler(int sig)
{
if (sig == SIGINT)
printf("ctrl+c\n");
else if (sig == SIGQUIT)
printf("ctrl+/\n");
}
int main(int argc, char *argv[])
{
// 1.忽略信号:当前进程不做任何处理
// signal(SIGINT, SIG_IGN);
// 2.执行缺省(默认)操作:
// signal(SIGINT, SIG_DFL);
// 3.捕捉信号:自定义函数去处理
pid_t pid;
pid = fork();
if (pid == 0)
{
//发送信号
kill(getppid(), SIGINT);
}
else if (pid > 0)
{
signal(SIGINT, handler);
signal(SIGQUIT, handler);
}
pause();
return 0;
}
练习:
用信号的知识实现司机和售票员问题。
1)售票员捕捉SIGINT(代表开车)信号,向司机发送SIGUSR1信号,司机打印(1et's gogogo)
2)售票员捕捉SIGQUIT(代表停车)信号,向司机发送SIGUSR2信号,司机打印(stop the bus)
3)司机捕捉SIGTSTP(代表到达终点站)信号,向售票员发送SIGUSR1信号,售票员打印(please get off the bus)
4)司机等待售票员下车,之后司机再下车。
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
pid_t pid;
void handler_saler(int sig)
{
if(sig == SIGINT)
kill(getppid(),SIGUSR1);
else if(sig == SIGQUIT)
kill(getppid(),SIGUSR2);
else if(sig == SIGUSR1)
{
printf("please get off the bus\n");
exit(0);
}
}
void handler_driver(int sig)
{
if(sig == SIGUSR1)
printf("let's gogogo\n");
else if(sig == SIGUSR2)
printf("stop the bus\n");
else if(sig == SIGTSTP)
kill(pid,SIGUSR1);
}
int main(int argc, char *argv[])
{
if((pid = fork()) < 0)
{
perror("fork err");
return -1;
}
else if (pid == 0)
{
signal(SIGINT, handler_saler);
signal(SIGQUIT,handler_saler);
signal(SIGUSR1,handler_saler);
signal(SIGTSTP,SIG_IGN);
}
else
{
signal(SIGUSR1, handler_driver);
signal(SIGUSR2,handler_driver);
signal(SIGTSTP,handler_driver);
signal(SIGINT,SIG_IGN);
signal(SIGQUIT,SIG_IGN);
wait(NULL);
exit(0);
}
while(1)
{
pause();
}
return 0;
}
共享内存
共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝;
为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间;进程就可以直接读写这一内存区而不需要进行数据的拷贝,从而大大提高的效率。
由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等。
共享内存的使用包括如下步骤:
- 生成 key 值:使用
ftok(pathname, proj_id)生成唯一标识共享内存的 key,若失败则返回 - 1。 - 创建 / 打开共享内存:通过
shmget(key, size, shmflg)创建新段或打开已有段,成功返回shmid(段标识符)。 - 映射内存到进程地址空间:调用
shmat(shmid, shmaddr, shmflg)将共享内存映射到当前进程,返回映射地址(可读写或只读)。 - 数据读写:进程直接操作映射后的内存地址(如指针解引用)。
- 撤销映射:通信完成后,用
shmdt(shmaddr)解除进程与共享内存的关联。 - 删除共享内存对象:调用
shmctl(shmid, IPC_RMID, NULL)删除内存段,避免资源泄露。
一、ftok () 函数
1. 函数原型
#include <sys/types.h>
#include <sys/ipc.h> // 必须包含的头文件
key_t ftok(const char *pathname, int proj_id);
2. 参数说明
| 参数名 | 类型 | 作用 |
|---|---|---|
pathname | const char* | 一个已存在的文件路径(可以是普通文件或目录,建议用绝对路径,如 /tmp/share.tmp)。 |
proj_id | int | 项目 ID(通常取 1~255 之间的非 0 整数,如 123、'A'),作为区分不同 IPC 对象的标识。 |
3. 返回值
- 成功:返回一个非负的
key_t类型键值(用于后续shmget()、msgget()、semget()等函数)。 - 失败:返回
-1,并设置errno(可通过perror()打印错误信息)。
ftok() 失败时会设置 errno,常见错误类型:
| errno 值 | 错误原因 | 解决方案 |
|---|---|---|
| ENOENT | pathname 指定的文件不存在 | 检查路径是否正确,确保文件已创建 |
| EACCES | 无权限访问 pathname 文件 | 确保进程对文件有读权限(chmod +r 文件名) |
| ENOTDIR | pathname 是目录,但权限不足 | 检查目录的执行权限(chmod +x 目录名) |
二、shmget()函数
1.函数原型
#include <sys/types.h> // 定义 key_t、size_t 等类型
#include <sys/ipc.h> // 定义 IPC_* 相关宏(如 IPC_CREAT)
#include <sys/shm.h> // 声明 shmget() 函数
int shmget(key_t key, size_t size, int shmflg);
2.参数说明
1. key_t key:共享内存的 “唯一标识”
- 作用:进程间通过相同的
key识别同一个共享内存(类似 “门锁钥匙”)。 - 来源:
-
由
ftok()生成(推荐,多进程共享场景):如key = ftok("/tmp/share.tmp", 123); -
固定值(如
0x12345678,需手动确保唯一性); -
特殊值
IPC_PRIVATE:创建 “私有共享内存”,仅能由创建进程的父子进程共享(无需ftok(),避免键值冲突)。
-
2. size_t size:共享内存的大小(单位:字节)
- 作用:指定要创建的共享内存的容量,若为 “打开已存在的共享内存”,
size可设为0(忽略大小检查)。 - 关键要求:
- 必须是 系统页大小(page size)的整数倍(内核按 “页” 分配内存,页大小通常为 4KB,可通过
getpagesize()获取); - 若
size不是页大小的整数倍,内核会自动向上对齐到最近的页大小(例如:size=5000 字节,页大小 = 4096,实际分配 8192 字节,浪费 3192 字节); - 最小 size 为 1 页(4096 字节)
- 必须是 系统页大小(page size)的整数倍(内核按 “页” 分配内存,页大小通常为 4KB,可通过
3. int shmflg:操作标志(创建 / 打开模式 + 权限控制)
- 核心是 “标志组合”(用
|连接),分为两类:- 「创建 / 打开标志」:控制函数行为是 “创建” 还是 “打开”;
- 「权限标志」:控制共享内存的访问权限(与文件权限规则一致)。
(1)创建 / 打开标志(常用 2 个)
| 标志 | 作用 |
|---|---|
IPC_CREAT | 若 key 对应的共享内存不存在,则创建;若已存在,则直接打开(“创建或打开”)。 |
IPC_EXCL | 仅与 IPC_CREAT 配合使用:若 key 对应的共享内存已存在,则返回 -1(确保创建 “全新” 的共享内存,避免误打开旧内存)。 |
IPC_NOWAIT | (少用)若共享内存正在被其他进程占用,不阻塞等待,直接返回错误。 |
(2)权限标志(与文件权限一致,常用 0666)
(3)常见标志组合示例
| 组合 | 效果 | ||
|---|---|---|---|
| `IPC_CREAT | 0666` | 创建或打开共享内存,所有用户可读写(最常用)。 | |
| `IPC_CREAT | IPC_EXCL | 0666` | 强制创建新共享内存,若已存在则报错(避免覆盖旧数据)。 |
0(无标志) | 仅打开已存在的共享内存,若不存在则报错。 |
3.返回值与错误处理
1. 返回值
- 成功:返回非负整数
shmid(共享内存唯一标识符,后续操作的核心句柄); - 失败:返回
-1,并设置errno(可通过perror()或strerror(errno)查看错误原因)。
2. 常见错误类型(errno)
| errno 值 | 错误原因 | 解决方案 | |
|---|---|---|---|
| ENOENT | 未指定 IPC_CREAT,且 key 对应的共享内存不存在 | 检查 key 是否正确,或添加 IPC_CREAT 标志 | |
| EEXIST | 指定 `IPC_CREAT | IPC_EXCL,但 key` 对应的共享内存已存在 | 更换 key 或 proj_id(ftok 的参数),或删除旧共享内存(ipcrm -m shmid) |
| EINVAL | size 超过系统限制,或为负数 | 减小 size(参考 /proc/sys/kernel/shmmax 查看最大限制) | |
| EACCES | 共享内存已存在,但当前进程无访问权限 | 检查 shmflg 中的权限标志(如改为 0666),或修改共享内存权限(ipcs -m 查看,ipcrm 删除后重建) | |
| ENOMEM | 系统内存不足,无法分配共享内存 | 释放系统内存,或减小 size |
三、shmat()函数
1.函数原型
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
1.参数说明
| 参数 | 含义 |
|---|---|
shmid | 共享内存段的标识符(由 shmget() 函数创建共享内存时返回)。 |
shmaddr |
指定共享内存附加到进程地址空间的起始地址: - 设为 - 设为非 |
shmflg |
附加标志,控制附加行为和权限: - - - |
3.返回值
- 成功:返回共享内存段在当前进程地址空间中的起始地址(
void*类型,需强制转换为实际数据类型使用); - 失败:返回
(void*)-1,并设置errno标识错误原因。
常见错误码(errno)
| 错误码 | 含义 |
|---|---|
EINVAL | shmid 无效,或 shmaddr 非 NULL 且不符合地址对齐要求,或权限不足。 |
EACCES | 进程没有权限附加该共享内存段(如共享内存是只读的,但进程以读写模式附加)。 |
ENOMEM | 系统内存不足,无法为共享内存分配虚拟地址空间。 |
EIDRM | 共享内存段已被标记为删除(shmctl(shmid, IPC_RMID, ...))。 |
四、shmdt()函数
1.函数原型
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
2.参数说明
| 参数 | 含义 |
|---|---|
shmaddr | 共享内存段在当前进程地址空间中的起始地址(必须是 shmat() 成功返回的有效地址,不能是 NULL 或其他无效地址)。 |
3.返回值
- 成功:返回
0,表示进程已成功与共享内存段分离; - 失败:返回
-1,并设置errno标识错误原因。
常见错误码(errno)
| 错误码 | 含义 |
|---|---|
EINVAL | 无效的 shmaddr(如地址未对齐、不是当前进程附加的共享内存地址,或该地址已被分离过)。 |
EIDRM | 共享内存段已被 shmctl(shmid, IPC_RMID, ...) 标记为删除(但未完全释放,仍可分离)。 |
五、shmctl()函数
1.函数原型
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
2.参数说明
| 参数 | 含义 |
|---|---|
shmid | 共享内存段的标识符(由 shmget() 返回,唯一标识一个共享内存段)。 |
cmd | 控制命令(核心参数),指定对共享内存执行的操作(常用命令见下文详解)。 |
buf |
指向 - 若 - 若 |
3.核心控制命令(cmd 参数详解)
shmctl() 的功能完全由 cmd 决定,以下是最常用的 5 个命令,覆盖绝大多数场景:
| 命令 | 功能描述 | buf 要求 | 权限要求 |
|---|---|---|---|
IPC_STAT | 查询共享内存属性:将 shmid 对应的共享内存属性写入 buf 指向的 struct shmid_ds。 | 必须非 NULL(需提前分配内存) | 进程需有读取权限(如 0444) |
IPC_SET | 修改共享内存属性:用 buf 中的值更新共享内存的属性(仅允许修改 shm_perm.uid、shm_perm.gid、shm_perm.mode)。 | 必须非 NULL(需填充要修改的字段) | 进程需是共享内存所有者,或 root 权限 |
IPC_RMID | 标记共享内存为删除:触发共享内存的 “延迟删除”(核心命令)。 | 设为 NULL 即可 | 同 IPC_SET(所有者或 root) |
SHM_LOCK | 锁定共享内存:禁止系统将该共享内存换出到交换分区(提升访问效率)。 | 设为 NULL 即可 | 仅 root 或创建者可用 |
SHM_UNLOCK | 解锁共享内存:取消 SHM_LOCK 的锁定效果。 | 设为 NULL 即可 | 仅 root 或创建者可用 |
关键命令补充说明(IPC_RMID 重点!)
IPC_RMID不会立即删除共享内存,仅将其标记为 “待删除”;- 只有当所有附加该共享内存的进程都调用
shmdt()分离后,系统才会真正释放共享内存的资源; - 标记后,新的进程无法再通过
shmat()附加该共享内存(会返回EIDRM错误); - 若进程未分离就退出,系统会自动为其分离,最终触发共享内存释放。
4.返回值
- 成功:返回
0(若cmd是IPC_STAT,则buf已填充属性信息); - 失败:返回
-1,并设置errno标识错误原因。
常见错误码(errno)
| 错误码 | 含义 |
|---|---|
EINVAL | shmid 无效,或 cmd 是非法命令,或 buf 为 NULL 但 cmd 要求非 NULL。 |
EACCES | 进程无权限执行该命令(如非所有者调用 IPC_SET,或无读取权限调用 IPC_STAT)。 |
EIDRM | 共享内存已被 IPC_RMID 标记为删除。 |
EPERM | 无足够权限(如普通用户调用 SHM_LOCK)。 |
ENOMEM | 调用 IPC_STAT 时,buf 指向的内存空间不足(极少出现)。 |
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
key_t key;
int shmid;
char *p = NULL;
key = ftok("./app", 'a');
if (key < 0)
{
perror("ftok err");
return -1;
}
// 1.创建共享内存
shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0666);
if (shmid < 0)
{
if (errno == EEXIST)
shmid = shmget(key, 128, 0666);
else
{
perror("shmget err");
return -1;
}
}
// 2.映射,将共享内存地址映射到用户空间
// 参数:NULL:让系统自动完成映射, 0:可读可写
p = (char *)shmat(shmid, NULL, 0);
if (p == (char *)-1)
{
perror("shmat err");
return -1;
}
fgets(p, 32, stdin);
printf("p:%s", p);
printf("shmid:%d\n", shmid);
printf("%#x\n", key);
//3.取消映射
shmdt(p);
//4.删除共享内存
shmctl(shmid, IPC_RMID, NULL);
system("ipcs -m");
return 0;
}
1509

被折叠的 条评论
为什么被折叠?



