学习资料https://pan.baidu.com/s/1NZpC4NyH0OHGOPPbO9J10g?pwd=h9m2 提取码: h9m2
一. 进程间通信概述
1.1 五大作用
数据传输/共享数据/通知事件/资源共享/进程控制(如 GDB 控制被调试进程)。
1.2 六种方式
| 方式 | 特点 |
|---|---|
| 管道 | 无名管道限亲缘进程;命名管道(FIFO)可任意进程 |
| 信号 | 只通知事件,不能传数据 |
| 消息队列 | 有格式 + 有类型,可无亲缘关系 |
| 共享内存 | 最快,直接访问内存无复制;需配合信号量 |
| 信号量 | 同步与互斥,不传数据 |
| 套接字 | 可跨机器 |
来源:AT&T → System V IPC (单机);BSD → socket;另有 POSIX IPC。Linux 全部继承。
二. 管道通信
- 逻辑上一段管道:一端写、一端读,按写入顺序读出(FIFO 字节流)。
- 单工单向 :方向一旦确定不能改,双向通信需两个管道。
- 管道存在内存 中(不是磁盘);
ps -aux | grep httpd就是管道实例。 - 无名管道 vs 命名管道:前者只能在父子进程间、无名字;后者有名字、任意进程可用、进程退出后仍存在。
2.1 无名管道 pipe
c
#include <unistd.h>
int pipe(int pipe[2]); // pipe[0] 读端,pipe[1] 写端;成功 0,失败 -1
c
int mypipe[2], fd;
if (pipe(mypipe) < 0) { perror("pipe failed"); exit(0); }
if ((fd = fork()) < 0) { perror("fork failed"); exit(0); }
if (fd == 0) { // 子进程:读 → 倒序 → 写回
read(mypipe[0], recbuf, strlen(sendbuf));
reverse(recbuf);
write(mypipe[1], recbuf, strlen(recbuf));
}
if (fd > 0) { // 父进程:写 → 等 → 读
write(mypipe[1], sendbuf, strlen(sendbuf));
sleep(10); // 等子进程把数据取走
read(mypipe[0], parrecbuf, strlen(sendbuf));
wait();
}
- fd 按最低可用 分配:0/1/2 已占 →
pipe[0]=3、pipe[1]=4。 pipe后两端都连父进程;fork后父子共享该管道。
2.2 重定向 dup / dup2
c
newfd = dup(oldfd); // 复制描述符(按最低可用返回新号)
newfd = dup2(oldfd, newfd); // 等价 close(newfd); dup(oldfd);
实现 ps -aux | grep init
c
pipe(mypipe);
pid = fork();
if (pid == 0) { // 子进程:标准输入 ← 管道读端
close(mypipe[1]);
dup2(mypipe[0], 0);
close(mypipe[0]);
execlp("grep", "grep", "init", NULL);
} else { // 父进程:标准输出 → 管道写端
close(mypipe[0]);
dup2(mypipe[1], 1);
close(mypipe[1]);
execlp("ps", "ps", "aux", NULL);
}
分析技巧:
oldfd1=3, oldfd2=4,dup(oldfd1)=5;dup2(oldfd1, 0)先关 0 再复制 → newfd2 = 0(不是 6)。
2.3 popen
c
FILE *popen(const char *command, const char *mode); // mode: "r" 读结果 / "w" 写数据
int pclose(FILE *stream);
c
readfp = popen("ps aux", "r");
writefp = popen("grep init", "w");
while (fgets(buf, 99, readfp) != NULL)
fputs(buf, writefp);
pclose(readfp); pclose(writefp); // ⚠ 必须 pclose,否则产生僵尸进程
内部即:创建管道 →
fork→dup2+close→ 子进程exec。
2.4 命名管道 FIFO
c
int mkfifo(const char *filename, mode_t mode); // 命令:mkfifo mypipe;ls -l 显示为 p
int mknod(const char *filename, mode_t mode | S_IFIFO, (dev_t)0);
打开规则
| 打开方式 | 默认(阻塞) | O_NONBLOCK |
|---|---|---|
| 以读打开 | 阻塞到有进程以写打开 | 立刻成功 |
| 以写打开 | 阻塞到有进程以读打开 | 立刻失败,ENXIO |
- 一般不同时读写 (不用
O_RDWR);双向传数据用一对 FIFO。 - 写端在管道满时阻塞,读端在管道空时阻塞。
- 典型用法:读写两个独立程序
open("/home/mypipe", O_RDONLY/O_WRONLY),先启动的阻塞在open,另一个打开后双方同时返回。
三. System V 信号量
3.1 四个步骤
semget 创建/获取 → semctl(SETVAL) 初始化 → semop 做 P/V → semctl(IPC_RMID) 删除。
| 函数 | 原型 | 要点 |
|---|---|---|
semget |
int semget(key_t key, int nsems, int semflg); |
key 多进程共用同一信号量(IPC_PRIVATE 为私有);semflg 用 `0666 |
semctl |
int semctl(int semid, int semnum, int cmd, union semun arg); |
cmd:SETVAL 设初值/GETVAL 取值/IPC_STAT/IPC_RMID 删除 |
semop |
int semop(int semid, struct sembuf *sops, size_t nsops); |
sem_op = -1 是 P ,+1 是 V ;sem_flg 用 SEM_UNDO |
c
union semun { int val; struct semid_ds *buf; unsigned short *array; }; // 常需自己定义
struct sembuf { short sem_num; short sem_op; short sem_flg; };
临界资源 = 同时只允许有限个进程访问的资源;访问它的代码叫临界区。
3.2 封装好的 P/V 库
c
int sem_p(int sem_id) { // P 操作
struct sembuf sem_b = {0, -1, SEM_UNDO};
if (semop(sem_id, &sem_b, 1) == -1) { perror("p operation failed"); return -1; }
return 0;
}
int sem_v(int sem_id) { // V 操作
struct sembuf sem_b = {0, 1, SEM_UNDO};
if (semop(sem_id, &sem_b, 1) == -1) { perror("v operation failed"); return -1; }
return 0;
}
控制父子进程执行顺序(信号量初值 0)
c
sem_id = semget(ftok(".", 'a'), 1, 0666 | IPC_CREAT); // ftok 由文件 i 节点生成 key
init_sem(sem_id, 0);
result = fork();
if (result == 0) { // 子进程先跑
sleep(DELAY_TIME);
printf("BBBBBBBBBBBB\n");
sem_v(sem_id); // V:值 0 → 唤醒父进程
} else {
sem_p(sem_id); // P:值变 -1,父进程阻塞
printf("AAAAAAAAAAA\n");
del_sem(sem_id);
}
四. POSIX 有名信号量
把信号量值保存在文件 里(通常在 /dev/shm),可用于毫无关系的进程 间同步。头文件 <semaphore.h>,编译加 -lrt 或 -lpthread。
| 函数 | 作用 |
|---|---|
sem_t *sem_open(const char *name, int oflag, mode_t mode, int value); |
创建/打开,O_CREAT(已存在则报错用 ` |
int sem_wait(sem_t *sem); |
P:值为 0 则阻塞,否则减 1 |
int sem_trywait(sem_t *sem); |
P 的非阻塞版,值为 0 返回 EAGAIN |
int sem_post(sem_t *sem); |
V:加 1 并唤醒一个等待者(原子操作) |
int sem_close(sem_t *sem); / int sem_unlink(const char *name); |
关闭/删除(引用数为 0 才真正删) |
c
mutex = sem_open("process1", O_CREAT, 0777, 0); // A 进程先等
mutex2 = sem_open("process2", O_CREAT, 0777, 1); // B 进程先跑
// A 进程(写 ABC) // B 进程(写 DEF)
sem_wait(mutex); sem_wait(mutex2);
write(fd, "ABC\n", 3); write(fd, "DEF\n", 3);
sleep(1); sleep(1);
sem_post(mutex2); sem_post(mutex);
五. 共享内存
最快的 IPC :直接映射到进程虚拟地址空间,省去用户态/内核态之间的数据复制。
- 三步:
shmget创建 →shmat映射 →shmdt撤消映射(+shmctl控制)。 - ⚠ 本身不提供同步保护 ,必须配信号量。
fork后子进程继承父进程的共享内存。
| 函数 | 原型 | 要点 |
|---|---|---|
shmget |
int shmget(key_t key, int size, int shmflg); |
IPC_PRIVATE 私有;返回共享内存 id |
shmat |
char *shmat(int shmid, const void *shmaddr, int shmflg); |
shmaddr=0 由系统分配;SHM_RDONLY 只读;返回映射地址 |
shmdt |
int shmdt(const void *shmaddr); |
解除映射 |
shmctl |
int shmctl(int shmid, int cmd, struct shmid_ds *buf); |
IPC_RMID 删除/IPC_STAT/IPC_SET |
父子进程通过共享内存通信
c
shmid = shmget(IPC_PRIVATE, SIZE, IPC_CREAT | 0600);
pid = fork();
if (pid == 0) { // 子写
shmaddr = (char *)shmat(shmid, NULL, 0);
strcpy(shmaddr, "Hello World!\n");
shmdt(shmaddr);
} else if (pid > 0) { // 父读
sleep(10);
shmaddr = (char *)shmat(shmid, NULL, 0);
strcpy(buff, shmaddr);
printf("I have got from shared memory :%s\n", buff);
shmdt(shmaddr);
shmctl(shmid, IPC_RMID, NULL);
}
5.1 mmap 共享内存
映射同一个普通文件 实现共享,之后像访问内存一样访问文件,不必 read/write。
c
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t len);
int msync(void *addr, size_t len, int flags);
| 参数 | 取值 |
|---|---|
prot |
PROT_READ/PROT_WRITE/PROT_EXEC/PROT_NONE(可或) |
flags |
MAP_SHARED(改动写回文件)/MAP_PRIVATE/MAP_FIXED;fd=-1 + MAP_ANON 为匿名映射(只限亲缘进程) |
msync 的 flags |
MS_ASYNC 异步/MS_SYNC 同步/MS_INVALIDATE 使其他映射失效 |
c
fd = open("stu.txt", O_CREAT | O_RDWR | O_TRUNC, 00777);
lseek(fd, sizeof(student) * 5 - 1, SEEK_SET); write(fd, "", 1); // 先把文件撑大
student *p_map = (student *)mmap(NULL, sizeof(student) * 10,
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
(*(p_map + i)).age = 20 + i; // 像数组一样访问
munmap(p_map, sizeof(student) * 10);
六. 消息队列
消息的链表,消息有格式 和类型(管道没有这两点),可无亲缘关系通信。
- 特性:发送方不必等待 接收方(松耦合);接收可按类型从中间取 ,不必从头;随内核持续(重启或命令删除才消失)。
- 流程:
msgget创建 →msgsnd发送 →msgrcv接收 →msgctl(IPC_RMID)删除。
| 函数 | 原型 | 要点 |
|---|---|---|
msgget |
int msgget(key_t key, int msgflg); |
IPC_CREAT/IPC_EXCL;返回队列 id |
msgsnd |
int msgsnd(int msgid, const void *msgp, size_t msgsz, int msgflg); |
msgsz 不含 4 字节消息类型;msgflg=0 队满挂起,IPC_NOWAIT 立即返回 |
msgrcv |
int msgrcv(int msgid, void *msgp, size_t msgsz, long mtype, int msgflg); |
mtype=0 取驻留最久的那条;读完该消息出队 |
msgctl |
int msgctl(int msgid, int cmd, struct msqid_ds *buf); |
IPC_RMID(等价 ipcrm -q id)/IPC_STAT/IPC_SET |
c
struct my_msg_st {
long my_msg_type; // 消息类型 1/2/3,必须 long 开头
char text[512]; // 消息内容
};
some_data.my_msg_type = rand() % 3 + 1; // 随机类型
msgsnd(msgid, (void *)&some_data, sizeof(some_data), 0);
msgrcv(msgid, (void *)my_data, sizeof(struct my_msg_st), (long)type, IPC_NOWAIT);
ipcs -q查看队列,ipcrm -q <id>删除。
七. 几种方式怎么选
| 方式 | 适用 |
|---|---|
| 共享内存 | 数据量最大、要求最快;耦合性强 |
| 信号量 | 只做同步/互斥,不传数据 |
| 消息队列 | 无亲缘关系、要传多种类型的大量数据 |
| 无名管道 | 仅父子进程 |
| 命名管道 | 任意进程传数据(特殊文件) |
常组合使用:共享内存 + 信号量,实现既快又安全的并发访问。