线程通信相对简单,因为共享空间,所以在线程中已做总结。(同步与互斥)
而进程的相对复杂,所以在此另起炉灶。
UNIX下的基础进程通信分为:无名管道(pipe),有名管道(fifo),和信号(signal)三类
还有三类高级进程间通信SYSTEM V IPC:消息队列,共享内存和信号量,这三类称为IPC对象
Linux IPC继承了System V IPC。
不同点在于: 管道的释放由内核自动控制;
IPC对象因为运行与内核而非文件系统中,所以释放由用户控制。若没有释放,则会一直运行直到重启。
基础进程通信
1.无名管道
特点:
只能在有亲缘关系的进程间通信,通常为父子进程。
单工通信,有固定读端和写端
创建时返回两个文件描述符,分别为读写管道
特性:
写端存在,有数据读正常返回,无数据读阻塞
写端不存在,有数据读正常返回,无数据返回0
读端存在,有空间写正常输入,无空间写阻塞
读端不存在,管道断裂,只进不出,要他何用。
#include <unistd.h>
int pipe(int pfd[2]);
返回值:成功:0;失败:EOF
通过一个整形数组来保存两个文件描述符,pfd[0]为读管道,pfd[1]为写管道
示例:
//省略头文件
int main(void)
{
pid_t pid1,pid2;
char buf[32];
int pfd[2];
if(pipe(pfd) < 0) //创建无名管道
{
perror("pipe");
exit(-1);
}
if((pid1 = fork()) < 0) //建立进程1
{
perror("fork");
exit(-1);
}
else if(pid1 == 0) //子进程1
{ strcpy(buf,"process_1");
write(pfd[1],buf,32);
exit(0);
}
else //子进程1的父进程1
{
if(pid2 = fork()) < 0)
{ //父进程1中建立进程2
perror("fork");
exit(-1);
}
else if(pid2 == 0) //子进程2
{
sleep(1);
strcpy(buf,"process_2");
write(pfd[1].buf,32);
}
else //子进程2的父进程2
{
wait(NULL);
read(pfd[0],buf,32);
printf("%s\n",buf);
wait(NULL);
read(pfd[0],buf,32);
printf("%s\n",buf)
}
}
return 0;
}
1.1白话时间:
以上例程是建立一个进程,产生子进程1和父进程1,然后在父进程1中建立子进程2和父进程2。这样父进程1和父进程2就有了亲缘关系,那么两个子进程也就有了亲缘关系。
例程的过程就是两个子进程分别将内容输入写管道pfd[1],再由父进程通过读管道pfd[0]读取内容打印。
具体使用方法:声明一个两个元素的数组(pfd[2]),再创建管道(pipe(pfd)),之后就可以在每个进程中对两个问件描述符(pfd[0],pfd[1]),进行读写了。
2.有名管道
特点:
有对应管道文件(有名),可用于任意进程通信。
打开管道时指定读写方式,不像无名管道时固定的
通过文件io操作,内容存于内存。
有名管道打开时可能会阻塞
#include <sys/sypes.h>
#include <sys/stat.h>
int mkfifo(const char *path,mode_t mode);
返回值:成功:0;失败:EOF
path:创建的管道文件路径
mode:管道文件权限,如0666
示例:
//创建有名管道:fifo.c
省略头文件
int main(void)
{
if(mkfifo("myfifio",0666) < 0)
{
perror("mkfifo");
exit(-1);
}
return 0;
}
//其他文件通过有名管道进行读写
//write_fifo.c
int main(void)
{
int pfd;
char buf[32];
if((pfd = open("myfifo",O_WRONLY))< 0 )
{
perror("open");
exit(-1);
}
while(1)
{
fgets(buf,32,stdin);
write(pfd,buf,32);
}
close(pfd);
return 0 ;
}
//read_fifo.c
int main(void)
{
char buf[32];
int pfd;
if((pfd = open("myfifo",O_RDONLY)) < 0)
{
perror("open");
exit(-1);
}
while(read(pfd,buf,32) >0)
{
printf("%d\n",strlen(buf));
}
close(pfd);
}
白话部分:
有名管道使用也较为简单,首先创建一个管道文件(mkfifo),之后在别的文件中打开(open)该文件,然后就可以对打开文件的文件描述符(pfd)进行操作了。
3.信号
信号实在软件层面上对中断机制的模拟,是一种异步通信模式。
内核通过信号通知进程,不同信号种类代表不同事件。
进程响应信号有三种方式:缺省,忽略,捕捉。
信号相关命令:
kill [-signal] pid -sig:指定信号类型,pid:发送对象(进程号)
killall [-u user | prog] prog:进程名,user:用户名
发送信号函数:
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid,int sig);
int raise(int sig);
返回值:成功:0,失败:EOF
pid:接收进程进程号,0:同组进程;-1:所有进程
sig:信号类型
int alarm(unisigned int seconds);
返回值:成功:返回上个计时器剩余时间,失败:EOF
seconds:定时时间
一个进程只能设定一个定时器,时间到产生SIGALRM信号
通常被用于超时检测
int pause(void);
进程阻塞,直到被信号中断
终端后返回-1,errno为EINTR;
示例:
int main(void)
{
alarm(3);
pause();
printf("wake up\n");
return 0;
}
//信号响应
#include <unistd.h>
#include <signal.h>
void(*signal(int signo,void(*handler)(int)))(int);
虽然看上去很复杂,但实际上就是将信号与函数关联起来
返回值:成功:返回原先的信号处理函数,失败:SIG_ERR
sign0:要设置的信号类型
handler:指令的信号处理函数,SIG_DFL表示缺省,SIG_IGN表示忽略
示例:
int mian(void)
{
signal(SIGINT,handler);
while(1)
{
pause();
}
return 0;
}
void handler(int signo)
{
if(signo == SIGINT)
printf("I have got SIGINT")
}
白话部分
信号可能比前两个管道略复杂,主要使用的函数便是(signal)将信号与函数联系起来。得到信号,进行相应的处理。
高级进程通信
SYSTEM V IPC:
共享内存(Shm_get),消息队列(Msg_get)和信号灯集(Sem_get)
每个IPC对象有唯一ID
IPC对象创建后一直存在直到被显式删除
每个IPC对象有一个关联的KEY
相关命令ipcs/ipcrm
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *psth,int proj_id);
返回值:成功:合法的key值,失败:EOF
path:可访问的文件路径
proj_id:用于生成key的数字,不等于0
ftok函数详解:
https://blog.csdn.net/andylauren/article/details/78821655
1.共享内存
特点:
最为高效的进程间通信方式,可直接读写内存而不需拷贝数据
在内核空间创建,映射到用户空间访问,使用灵活
同线程类似,由于多个进程可同时访问,所以需要同步和互斥机制
相关命令:
查看共享内存大小限制:ipcs -l;cat/proc/sys/kernel/shmmax
查看共享内存删除时间点:chamctl(shmid,IPC_RMID,NULL)添加删除标记;当nattach变为0时真正删除
使用步骤:
打开/创建共享内存
映射共享内存,将指定的共享内存映射到进程的地址空间
读写
撤销映射
删除共享内存
//创建:由ftok/IPC_PRIVATE获得shmid
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key,int size,int shmflg);
返回值:成功:共享内存id(shmid),失败:EOF
key由函数(ftok)或者IPC_PRIVATE生成
shmflg:共享内存标志位 IPC_CREAT|0666
//映射:由shmid获得共享内存映射地址(shmaddr)
#include <sys/type.h>
#include <sys/shm.h>
void *shmat(int shmid,const void shmaddr,int shmflg);
返回值:成功:映射后地址,失败:(void)-1
shmid:映射的共享内存id,由shmget获得。
shmaddr:映射后地址,NULL表示自动映射
shmflg:标志位;0:可读写;SHM_RDONLY:只读
//读写:通过地址指针进行访问
fgets(shmaddr,N,stdin);
//撤销映射:通过地址撤销映射,但共享内存还在。
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(void *shmaddr);
返回值:成功:0;失败:EOF
不使用共享内存时应撤销
进程结束时自动撤销
//共享内存控制
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid,int cmd,struct shmid_ds *buf);
返回值:成功:0;失败:EOF
shmid:操作的共享内存id
cmd:执行的操作:IPC_STAT;IPC_SET;IPC_RMID
buf:要保存或设置共享内存属性的地址
白话部分:
当需要多个进程访问一片空间时可采用共享内存,就像多个线程共享一个空间一样。使用时先通过(ftok)获得key值,再通过(key值+shmget())获得shmid,再由(shmid+shmat)映射获得一个地址(addr),通过(*addr)进行读写操作,操作完毕通过(shmdt)撤销映射,最后由(shmctl)来决定对其设置或者删除。
消息队列msg
顾名思义,就是消息的列表,用户可以通过消息列表来添加与读取消息,使用它我们可以按照类型来发送/接收消息
使用步骤:
1,打开/创建消息队列
2.向消息队列发送消息
3.从消息队列接收消息
4.控制消息队列
/创建/打开
#include <sys/types.h>
#include <sys/ipc.h>
#incldue <sys/msg.h>
int msgget(key_t key,int msgflg);
返回值:成功:消息队列id;失败:-1;
key:通过ftok或者IPC_PRIVATE获得
msgflg:标志位IPC_CREAT|0666
//发送
#include <sys/types.h>
#include <sys/ipc.h>
#incldue <sys/msg.h>
int msgsnd(int msgid,const void *msgp,size_t size,int msgflg);
返回值:成功:0;失败:-1;
msgid:消息队列id
msgp:消息队列缓冲区地址
size:消息正文长度
msgflg:标志位0或者IPC_NOWAIT
//接收:与发送格式相同,只是多了需要接收的消息类型
#include <sys/types.h>
#include <sys/ipc.h>
#incldue <sys/msg.h>
int msgrcv(int msgid,const void *msgp,size_t size,long msgtype,int msgflg);
返回值:成功:0,失败:-1;
size:指定接受的消息长度
msgtype:指定接收的消息类型
//控制:设置和删除
#include <sys/types.h>
#include <sys/ipc.h>
#incldue <sys/msg.h>
int msgctl(int msgid,int cmd,struct msqid_ds *buf);
返回值:成功:0;失败:-1
cmd:IPC_STAT/IPC_SET/IPC_RMID
buf:存放消息队列属性的地址
示例:
typedef struct{
long mtype;
char mtex[64];
}MSG; //定义消息队列结构体,类型+正文
#define LEN (sizeof(MSG) - sizeof(long)) //正文长度
int main(void)
{
//创建
int msgid;
ket_t key;
if((key = ftok(".",'q')) == -1 )
{
perror("ftok");
exit(-1);
}
if((msgid = msgget(key,IPC_CREAT|0666)) < 0)
{
perror("msgget");
exit(-1);
}
//发送
MSG buf_snd,buf_rcv; //一个名为buf的结构体
buf.mtype = 100; //定义消息类型为100
fgets(buf.mtext,64,stdin); //从键盘输入数据
msgsnd(msgid,&buf_snd,LEN,0); //发送消息
//接收
if(msgrcv(msgid,&buf_rcv,LEN,200,0) < 0) //接收消息类型为200的消息,存入接收缓存区中
{
perror("msgrcv");
exit(-1);
}
}
信号灯(量)集
信号灯也可以叫信号量,之前在线程中也由,但线程中主要是通过p/v操作来实现线程的同步,在进程中也可以通过信号量来实现同步与互斥
“集”表示了这里的system V信号量是由一个或多个信号量组成,也可以同时对多个进行操作,注意使用多个时避免死锁。
使用步骤:
1.打开/创建
2.初始化(信号量特有)
3.p/v操作
4.删除
//创建/打开
#include <sys/types.h>
#include <sys/ipc.h>
#incldue <sys/sem.h>
int semget(key_t key,int nsems,int semflg);
返回值:成功:信号灯id;失败:-1;
ket:管理的key,IPC_PRIVATE或ftok
nsems:包含的信号灯的个数
semflg:标志位IPC_CREAT|0666 IPC_EXCL
//初始化
#include <sys/types.h>
#include <sys/ipc.h>
#incldue <sys/sem.h>
int semctl(int semid,int semnum,int cmd,…);
返回值:成功:0;失败:EOF
semid:信号灯集id
semnum:在要操作信号灯集中操作的信号灯的编号
cmd:执行的操作 SETVAL(初始化) IPC_RMID(删除)
union semun:与cmd有关,通常用于设置和返回信号量的信息
Union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
void *__pad;
}
示例:
//集合中包括两个信号灯:一个初始化为3,一个初始化为0
union semun myun;
myun.val = 3;
if(semctl(semid,0,SETVAL,myun) < 0)
{
perror("semctl");
exit(-1);
}
myun.val = 0;
if(semctl(semid,0,SETVAL,myun) < 0)
{
perror("semctl");
exit(-1);
}
//p/v操作
#include <sys/types.h>
#include <sys/ipc.h>
#incldue <sys/sem.h>
int semop(int semid,struct sembuf *sops,unsigned nsops);
返回值:成功:0;失败:-1
semid:操作的信号灯集id
sops:对信号灯操作的结构体(数组)
nsops:操作的信号灯的个数
struct sembuf
{
short semnum; //信号灯编号
short sem_op; //-1:p(申请资源) 1:v(申请资源)
short sem_flg; //0/IPC_NOWAIT
}