进程间通信概述





总共有7种文件类型

管道通信


管道的分类

管道的分类


案例1

父进程创建的管道fd[0],fd[1],当fork一个子进程之后,子进程也会有这个fd[0],fd[1]。所以子进程和父进程只有操作一个,另外一个关闭
#include <stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
int fd[2];
// 创建管道
if(pipe(fd)<0)
{
perror("pipi error");
exit(1);
}
pid_t pid;
if((pid=fork())<0)
{
perror("fork error");
exit(1);
}
else if(pid>0)
{
close(fd[0]); // 父进程用来写入数据
int start = 1,end = 100;
// 往管道中写入数据
if(write(fd[1],&start,sizeof(int))!=sizeof(int))
{
perror("write error");
exit(1);
}
if(write(fd[1],&end,sizeof(int))!=sizeof(int))
{
perror("write error");
exit(1);
}
close(fd[1]);
wait(0); //回收子进程
}
else
{
close(fd[1]); // 子进程用来读取数据i
int start,end;
if(read(fd[0],&start,sizeof(int))!=sizeof(int))
{
perror("read error");
exit(1);
}
if(read(fd[0],&end,sizeof(int))!=sizeof(int))
{
perror("read error");
exit(1);
}
close(fd[0]);
printf("child process read start %d,end %d\n",start,end);
}
printf("Hello world\n");
return 0;
}
案例2


#include <stdio.h>
#include<unistd.h>
#include<stdlib.h>
char *cmd1[3] = {"/bin/cat","/etc/passwd",NULL};
char *cmd2[3] = {"/bin/grep","root",NULL};
int main()
{
int fd[2];
if(pipe(fd)<0)
{
perror("pipe error");
exit(1);
}
pid_t pid;
for(int i = 0;i<2;i++)
{
pid = fork();
if(pid<0)
{
perror("fork error");
exit(1);
}
else if(pid==0) //son process
{
if(i==0) // 第一个子进程,负责写入数据
{
// 关闭读端
close(fd[0]);
/*
*将标准输出重定向到管道的写端
下面命令执行结果会写入到管道中
而不是输出到屏幕
* */
if(dup2(fd[1],STDOUT_FILENO)!=STDOUT_FILENO)
{
perror("dup2 error");
}
// 由于上面已经复制了一份fd[1],所以可以关闭fdp[1]
close(fd[1]);
// 调用exec函数执行cat命令
if(execvp(cmd1[0],cmd1)<0)
{
perror("execvp error");
exit(1);
}
break; // 注意这里是进程扇
}
if(i == 1) // 第二个子进程,负责写入数据
{
// 关闭写端
close(fd[1]);
/*
*将标准输入重定向到管道的读端
下面命令grep的执行是从管道的读端开始
读取内容,而不是标准输入读取
* */
if(dup2(fd[0],STDIN_FILENO)!=STDIN_FILENO)
{
perror("dup2 error");
}
close(fd[0]);
// 调用exec函数执行grep命令
if(execvp(cmd2[0],cmd2)<0)
{
perror("execvp error");
exit(1);
}
break;
}
}
else // parent process
{
if(i==1) // 父进程要等到两个子进程全部创建完毕才去回收
{
close(fd[0]);
close(fd[1]);
wait(0);
wait(0);
}
}
}
return 0;
}

管道的读写特性

案例1:读一个写端关闭的管道
#include <stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
int fd[2];
if(pipe(fd)<0)
{
perror("pipe error");
exit(1);
}
pid_t pid;
if((pid=fork())<0)
{
perror("fork error");
exit(1);
}
else if(pid>0)
{// 父进程从不完整管道(写端关闭)中读取数据
sleep(5);// 首先让子进程写入数据并关闭管道的读写
close(fd[1]);
while(1)
{
char c;
if(read(fd[0],&c,1)==0)
{
printf("\n已经读到管道的末尾\n");
break;
}
else
{
printf(" %c ",c);
}
}
close(fd[1]);
wait(0);
}
else
{// 子进程负责将数据写入管道
close(fd[0]);
char *s = "1234";
write(fd[1],s,sizeof(s));
// 写入数据后关闭管道的写端
close(fd[1]);
}
return 0;
}

案例2:写一个读端关闭的管道
#include <stdio.h>
#include<unistd.h>
#include<signal.h>
#include<errno.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
void sig_handler(int signo)
{
if(signo == SIGPIPE)
{
printf("SIGPIPE occred!\n");
}
}
int main()
{
int fd[2];
if(pipe(fd)<0)
{
perror("pipe error");
exit(1);
}
pid_t pid;
if((pid=fork())<0)
{
perror("fork error");
exit(1);
}
else if(pid>0)
{// 父进程从不完整管道(读端关闭)中写入数据
sleep(5);
close(fd[0]);
if(signal(SIGPIPE,sig_handler) == SIG_ERR)
{
perror("signal sigpipe error");
exit(1);
}
char *s = "123";
if(write(fd[1],s,sizeof(s))!=sizeof(s))
{
fprintf(stderr,"%s , %s\n",strerror(errno),
(errno==EPIPE)?"EPPIPE":"UNKNOW");
}
close(fd[1]);
wait(0);
}
else
{// 子进程关闭管道的读端
close(fd[0]);
close(fd[1]);
}
return 0;
}

标准库中的管道操作


#include <stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<memory.h>
int main()
{
FILE *fp;
// 命令执行的结果放置到fp指向的结构体缓存中
fp = popen("cat /etc/passwd" ,"r");
char buf[512];
memset(buf,0,sizeof(buf));
while(fgets(buf,sizeof(buf),fp)!=NULL)
{
printf("%s",buf);
}
pclose(fp);
printf("_____________________________\n");
// 为wc命令提供统计的数据
fp = popen("wc -l","w");
fprintf(fp,"1\n2\n3\n");
pclose(fp);
return 0;
}

命名管道



创建命名管道使用mkfifo函数,命名管道创建后,使用时必须先调用open将其打开。因为命名管道是一个存在于硬盘上的文件,而管道是存在于内存中的特殊文件。调用open打开命名管道的进程可能会阻塞,若同时用读写方式(O_RDWR)打开,则一定不会导致阻塞;以只读方式(O_RDONLY)打开,则调用open函数的进程将会被阻塞直到有写方打开管道;以写方式(O_WRONLY)打开也会阻塞直到有读方式打开管道。
案例1

- 读程序
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<unistd.h>
#include<memory.h>
int main(int argc,char *argv[])
{
if(argc < 2)
{
printf("usage:%s fifo\n",argv[0]);
exit(1);
}
printf("open fifo read....\n");
// 打开命名管道
int fd = open(argv[1],O_RDONLY);
if(fd<0)
{
perror("open error");
exit(1);
}
else
{
printf("open file success : %d\n",fd);
}
// 从命名管道中读取数据
char buf[512];
memset(buf,0,sizeof(buf));
while(read(fd,buf,sizeof(buf))<0)
{
perror("read error");
}
close(fd);
printf("%s\n",buf);
return 0;
}
- 写程序
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<unistd.h>
#include<memory.h>
int main(int argc,char *argv[])
{
if(argc < 2)
{
printf("usage:%s fifo\n",argv[0]);
exit(1);
}
printf("open fifo write....\n");
// 打开命名管道
int fd = open(argv[1],O_WRONLY);
if(fd<0)
{
perror("open error");
exit(1);
}
else
{
printf("open file success : %d\n",fd);
}
// 从命名管道中读取数据
char* s = "123456789";
size_t size = strlen(s);
while(write(fd,s,size != size))
{
perror("write error");
}
close(fd);
return 0;
}



匿名管道 VS 命名管道的区别
相同点
- 重点是非阻塞

不相同点


System V IPC

- 管道的创建在内核中,但是其释放由内核控制。而三个IPC对象的释放需要用户控制。

IPC对象的权限和所有者结构体



消息队列


消息队列属性


创建消息队列

消息队列控制

发送消息


接受消息

案例:发送消息
- 一个发送消息,一个接受消息
#include <stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/msg.h>
typedef struct
{
long type; // 消息类型?
int start; // 消息数据本身(包含start和end)
int end;
}MSG;
/*
* 往消息队列中发送消息
* */
int main(int argc,char *argv[])
{
if(argc<2)
{
printf("参数不足\n");
exit(1);
}
key_t key = atoi(argv[1]);
printf("key: %d\n",key);
// 创建消息队列
int msq_id;
if((msq_id=msgget(key,IPC_CREAT|IPC_EXCL|0777)<0))
{
perror("msgget error");
}
printf("msg_id : %d\n",msq_id);
// 定义要发送的消息
MSG m1 = {4,4,400};
MSG m2 = {2,2,200};
MSG m3 = {1,1,100};
MSG m4 = {6,6,600};
MSG m5 = {6,60,6000};
// 发送消息到消息队列
if(msgsnd(msq_id,&m1,sizeof(MSG)-sizeof(long),
IPC_NOWAIT)<0)
{
perror("msgsnd error ");
}
if(msgsnd(msq_id,&m2,sizeof(MSG)-sizeof(long),
IPC_NOWAIT)<0)
{
perror("msgsnd error ");
}
if(msgsnd(msq_id,&m3,sizeof(MSG)-sizeof(long),
IPC_NOWAIT)<0)
{
perror("msgsnd error ");
}
if(msgsnd(msq_id,&m4,sizeof(MSG)-sizeof(long),
IPC_NOWAIT)<0)
{
perror("msgsnd error ");
}
if(msgsnd(msq_id,&m1,sizeof(MSG)-sizeof(long),
IPC_NOWAIT)<0)
{
perror("msgsnd error ");
}
// 获取消息队列的总数
struct msqid_ds ds;
if(msgctl(msq_id,IPC_STAT,&ds)<0)
{
perror("msgctl error");
}
printf("msg 的消息总数为: %ld\n",ds.msg_qnum);
return 0;
}





- 通过命令删除


案例:接受消息
#include <stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/msg.h>
typedef struct
{
long type; // 消息类型·
int start; // 消息数据本身(包含start和end)
int end;
}MSG;
int main(int argc,char *argv[])
{
if(argc<3)
{
printf("参数不足\n");
exit(1);
}
key_t key = atoi(argv[1]);
long type = atoi(argv[2]);
printf("key: %d\n",key);
// 获得指定的消息队列
int msq_id;
if((msq_id=msgget(key,0777)<0))
{
perror("msgget error");
}
printf("msg_id : %d\n",msq_id);
// 从消息队列中接受指定类型的消息
MSG m1;
if(msgrcv(msq_id,&m1,sizeof(MSG)-sizeof(long),
type,IPC_NOWAIT)<0)
{
perror("msgrcv error ");
}
else
{
printf("type %ld start: %d end %d\n",
m1.type,m1.start,m1.end);
}
return 0;
}







共享内存



共享内存属性


共享内存使用步骤

创建共享内存




共享内存控制

共享内存映射和解除映射





案例(单向同步)
-
二个进程(可以有关系也可以没关系),一个进程操作的时候另外一个进程阻塞。利用管道达到进程间的同步。
-
tell.h
#ifndef __TELL_H__
#define __TELL_H__
// 管道初始化
extern void init();
// 利用管道进行等待
extern void wait_pipi();
// 利用管道进行通知
extern void notify_pipe();
// 销毁管道
extern void destory_pipe();
#endif
- tell.c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include"tell.h"
static int fd[2];
// 管道初始化
void init()
{
if(pipe(fd)<0)
{
perror("pipe error");
}
}
// 利用管道进行等待
void wait_pipi()
{
// 管道:一端没有写数据,而另外一端去读数据,则会阻塞
char c;
// 管道读写默认是阻塞的
if(read(fd[0],&c,1)<0)
{
perror("read pipe error");
}
}
// 利用管道进行通知
void notify_pipe()
{
char c = 'c';
if(write(fd[1],&c,1)!=1)
{
perror("write pipe error");
}
}
// 销毁管道
void destory_pipe()
{
close(fd[0]);
close(fd[1]);
}
- cal_shm.c
#include <stdio.h>
#include<sys/shm.h>
#include<stdlib.h>
#include<string.h>
#include"tell.h"
int main()
{
// 创建共享内存
int shmid;
if((shmid=shmget(IPC_PRIVATE,1024,
IPC_CREAT|IPC_EXCL|0777))<0)
{
perror("shmget error");
exit(1);
}
pid_t pid;
init(); // 初始化管道
if((pid=fork())<0)
{
perror("fork error");
exit(1);
}
else if(pid>0)
{
// 进行共享内存的映射
int *pi = (int*)shmat(shmid,0,0);
if(pi==(int*)-1)
{
perror("shmat error");
exit(1);
}
// 往共享内存中写入数据(通过操作映射的地址即可)
*pi = 100;
*(pi+1) = 20;
// 操作完毕解除共享内存映射
shmdt(pi);
// 写入数据完毕,通知子进程读取数据
notify_pipe();
//关闭fd
destory_pipe();
// 避免僵尸进程
wait(0);
}
else // 子进程
{
// 子进程阻塞,等待父进程写数据
wait_pipe();
// 子进程从共享内存中读取数据
// 子进程进行共享内存的映射
int *pi = (int*)shmat(shmid,0,0);
if(pi==(int*)-1)
{
perror("shmat error");
exit(1);
}
printf("start:%d,end:%d\n",*pi,*(pi+1));
// 解除映射
shmdt(pi);
// 删除共享内存,两个进程一个进行删除即可
shmctl(shmid,IPC_RMID,NULL);
// 子进程复制父进程的fd,也需要关闭
destory_pipe();
}
return 0;
}

案例(互斥访问)
- 银行

- account.h
#ifndef __ACCOUNT_H__
#define __ACCOUNT_H__
#include<pthread.h>
typedef struct{
int code;
double balance;
}Account;
extern double withdraw(Account *a,double amt);
extern double deposit(Account *a,double amt);
extern double get_balance(Account *a);
#endif
- account.h
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include"account.h"
double withdraw(Account *a,double amt)
{
if(amt<0 || amt>a->balance){
return 0.0;
}
double balance = a->balance;
sleep(1);
balance -= amt;
a->balance = balance;
return amt;
}
double deposit(Account *a,double amt)
{
if(amt<0){
return 0.0;
}
double balance = a->balance;
sleep(1);
balance -= amt;
a->balance = balance;
return amt;
}
double get_balance(Account *a)
{
assert(a!=NULL);
double balance = a->balance;
return balance;
}
- account_test.c
#include <stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/shm.h>
#include"account.h"
int main()
{
// 在共享内存中创建银行账户
int shmid;
if((shmid=shmget(IPC_PRIVATE,sizeof(Account),
IPC_CREAT|IPC_EXCL|0777))<0)
{
perror("shmget error");
exit(1);
}
// 进行共享内存映射i(a就是映射的地址)
Account *a = (Account*)shmat(shmid,0,0);
if(a==(Account*)-1)
{
perror("shamt error");
exit(1);
}
a->code = 100001;
a->balance = 10000;
printf("balance : %f\n",a->balance);
pid_t pid;
if((pid=fork())<0)
{
perror("fork error");
exit(1);
}
else if(pid>0) // 父进程
{
//父进程执行取款操作
double amt = withdraw(a,10000);
printf("父进程 pid:%d withdraw: %f from code %d ",
getpid(),amt,a->code);
// 对共享内存的操作要在解除映射之前
printf("balance : %f\n",a->balance);
wait(0);
shmdt(a);
}
else // 子进程
{
double amt = withdraw(a,10000);
// 这里子进程也可以再次映射,也可以不需要(因为子进程可以继承返回后的内存地址)
printf("子进程 pid:%d withdraw: %f from code %d\n",getpid(),amt,a->code);
shmdt(a);
}
return 0;
}
- 注意没有利用同步机制


信号集属性

创建信号量集

信号量集控制


信号量集操作

案例:银行账户案例(互斥访问)
- pc.h
#ifndef __PV_H__
#define __PV_H__
// 初始化semnums个信号灯/信号量的值(value)
extern int I(int semnums,int value);
// 对信号量集(semid)中的信号灯(semnum)做P(value)操作
extern void P(int semid,int semnum,int value);
// 对信号量集(semid)中的信号灯(semnum)做V(value)操作
extern void V(int semid,int semnum,int value);
// 销毁信号量集(semid)
extern void D(int semid);
#endif
- pc.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<sys/sem.h>
#include"pv.h"
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
};
// 创建信号量集,并初始化信号量集
int I(int semnums,int value)
{
// 创建信号集
int semid = semget(IPC_PRIVATE,semnums,
IPC_CREAT|IPC_EXCL|0777);
if(semid < 0)
return -1;
union semun un;
unsigned short *array = (unsigned short*)
calloc(semnums,sizeof(unsigned short));
for(int i = 0;i<semnums;i++)
array[i] = value;
un.array = array;
/*
*初始化信号量集中的所有信号灯的初值
0:表示要初始化所有信号灯
* */
if(semctl(semid,0,SETALL,un)<0)
{
perror("semctl error");
return -1;
}
free(array);
return semid;
}
//对信号量集(semid)中的信号灯(semnum)做P(value)操作
void P(int semid,int semnum,int value)
{
assert(value>=0);
/*
* 定义sembuf类型的结构体数组,放置若干个结构体变量
* 对应要操作的信号量,要作的P或V操作
*/
struct sembuf ops[] = {{semnum,-value,SEM_UNDO}};
// 这里的负号表示做减操作,注意数组只有一个成员表示值操作一个信号量
if(semop(semid,ops,
sizeof(ops)/sizeof(struct sembuf))<0)
{
perror("semop error");
}
}
// 对信号量集(semid)中的信号灯(semnum)做V(value)操作
void V(int semid,int semnum,int value)
{
assert(value>=0);
struct sembuf ops[] = {{semnum,value,SEM_UNDO}};
if(semop(semid,ops,
sizeof(ops)/sizeof(struct sembuf))<0)
{
perror("semop error");
}
}
// 销毁信号量集(semid)
void D(int semid)
{
if(semctl(semid,0,IPC_RMID,NULL)<0)
{
perror("semctl error");
}
}

- account.h
#ifndef __ACCOUNT_H__
#define __ACCOUNT_H__
#include<pthread.h>
typedef struct{
int code;
double balance;
int semid; // 在共享资源上绑定一个信号量集
}Account;
extern double withdraw(Account *a,double amt);
extern double deposit(Account *a,double amt);
extern double get_balance(Account *a);
#endif
- account.c
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include"account.h"
#include"pv.h"
double withdraw(Account *a,double amt)
{
assert(a!=NULL);
// 对信号量集semid中的0号信号量/信号灯作P(1)
P(a->semid,0,1);
if(amt<0 || amt>a->balance){
// 对信号量集semid中的0号信号量/信号灯作V(1)操作
V(a->semid,0,1);
return 0.0;
}
double balance = a->balance;
sleep(1);
balance -= amt;
a->balance = balance;
// 对信号量集semid中的0号信号量/信号灯作V(1)操作
V(a->semid,0,1);
return amt;
}
double deposit(Account *a,double amt)
{
assert(a!=NULL);
P(a->semid,0,1);
if(amt<0){
V(a->semid,0,1);
return 0.0;
}
double balance = a->balance;
sleep(1);
balance -= amt;
a->balance = balance;
V(a->semid,0,1);
return amt;
}
double get_balance(Account *a)
{
assert(a!=NULL);
P(a->semid,0,1);
double balance = a->balance;
V(a->semid,0,1);
return balance;
}
- account_test.c
#include <stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/shm.h>
#include"account.h"
#include"pv.h"
int main()
{
// 在共享内存中创建银行账户
int shmid;
if((shmid=shmget(IPC_PRIVATE,sizeof(Account),
IPC_CREAT|IPC_EXCL|0777))<0)
{
perror("shmget error");
exit(1);
}
// 进行共享内存映射i(a就是映射的地址)
Account *a = (Account*)shmat(shmid,0,0);
if(a==(Account*)-1)
{
perror("shamt error");
exit(1);
}
a->code = 100001;
a->balance = 10000;
// 创建信号量集并初始化(1个信号量/信号灯,初值为1)
a->semid = I(1,1);
if(a->semid < 0)
{
perror("I() init fail");
}
printf("balance : %f\n",a->balance);
pid_t pid;
if((pid=fork())<0)
{
perror("fork error");
exit(1);
}
else if(pid>0) // 父进程
{
//父进程执行取款操作
double amt = withdraw(a,10000);
printf("父进程 pid:%d withdraw: %f from code %d ",
getpid(),amt,a->code);
// 对共享内存的操作要在解除映射之前
printf("balance : %f\n",a->balance);
wait(0);
// 销毁信号量
D(a->semid);
// 解除共享内存的映射
shmdt(a);
// 删除共享内存
shmctl(shmid,IPC_RMID,NULL);
}
else // 子进程
{
double amt = withdraw(a,10000);
// 这里子进程也可以再次映射,也可以不需要(因为子进程可以继承返回后的内存地址)
printf("子进程 pid:%d withdraw: %f from code %d\n",getpid(),amt,a->code);
shmdt(a);
}
return 0;
}

案例:读者写者问题(互斥访问)

📌 选择 IPC 方式
简单父子进程通信 → 管道(Pipe)
多个无关进程的消息传输 → 消息队列(Message Queue)
进程间共享大块数据 → 共享内存(Shared Memory)
控制进程同步 → 信号量(Semaphore)
🚀 实际应用:
Shell 管道 (|) 采用 匿名管道。
Linux 日志系统 使用 消息队列。
数据库缓存 采用 共享内存(如 MySQL 的 InnoDB)。
进程锁(多进程文件访问控制) 使用 信号量。

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



