进程间通信(IPC)
管道
无名管道
特点
1、只能在父子进程,兄弟进程等亲缘进程间进行数据交互
2、无名管道只是一种临时存在于内存中的临时文件,在文件系统中没有其相关的特殊文件,进程双方均具有读写端,可以调用read,write文件的API来对该临时文件进行操作
3、半双工工作方式:同一时刻数据只能在一个方向流动
图解过程:
如图:fd[0]为读端,fd[1]为写端,在某一时刻,父进程把读端关闭,子进程把写端关闭,父进程把数据写入管道,子进程把数据从管道里读出
API
创建无名管道
int pipe(int pipefd[2]);
第一个API可以实现父子进程之间简单的数据交互
编程实现:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
//int pipe(intfd[2])
int main()
{
int fd[2];//通信双方分别通过fd[0],fd[1]来对管道数据进行操作,fd[0]为读操作,fd[1]为写操作
pid_t pid;
char buff[20];
int status;
if(pipe(fd) < 0){
printf("create pipe failed\n");
exit(-1);
}
else{
printf("create pipe success\n");
}
pid = fork();
if(pid < 0){
printf("fork failed\n");
}
else if(pid > 0){
sleep(3);
close(fd[0]);
write(fd[1],"helloworld",strlen("helloworld"));
wait(&status);
}
else if(pid == 0){
close(fd[1]);
read(fd[0],buff,sizeof(buff));
printf("read from father:%s\n",buff);
exit(0);
}
return 0;
}
输出结果:
有名管道(FIFO)
特点
与无名管道相反,有名管道通过在文件系统中生成特殊文件来使两个无亲缘关系进程进行数据交互,与无名管道相同,同样可以调用文件API来进行操作
API
int mkfifo(const char *pathname, mode_t mode);
*pathname:创建管道文件的所在路径
mode:文件权限
编程实现
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include<sys/stat.h>
//int mkfifo(const char *pathname, mode_t mode);
int main()
{
int ret = mkfifo("./file",0600);//调用函数后需要指明生成文件路径以及操作权限为可读可写
if(ret == 0){
printf("mkfifo success\n");//成功会在指定位置生成管道文件
}
else if(ret == -1){
printf("mkfifo failed\n");
}
return 0;
}
执行后在文件系统中生成一个特殊的管道文件(图中黄色file)
两个无关系进程间信息交互`
//读进程
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include <unistd.h>
int main()
{
char buf[30] = {0};
int fd = open("./file",O_RDONLY);
printf("open fifo success\n");
int n_read = read(fd,buf,30);
printf("read %d bytes from fifo,content:%s\n",n_read,buf);
close(fd);
return 0;
}
//写进程
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include <unistd.h>
#include<string.h>
int main()
{
char *buf = "message from fifo";
int fd = open("./file",O_WRONLY);
printf("open fifo success\n");
int n_write = write(fd,buf,strlen(buf));
printf("write success\n");
close(fd);
return 0;
}
结果如下
PS:只有读进程运行时,在调用只读open函数后,程序会进入堵塞状态直到有另外一个程序调用只写open来对管道文件进行打开时才会继续执行下去
消息队列
原理以及过程
消息队列特点:
1以链表形式存放在linux内核中,每个消息队列以队列ID号来进行区分
2消息队列是面向记录的,其中的消息具有特定格式以及优先级
3消息队列独立于发送以及接收进程,当进程终止时消息队列以及其内容不会被删除
4消息队列可实现信息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取
进程使用消息队列进行通信条件:
双方使用同一个消息队列
通信过程
发送信息进程:创建/获取消息队列,把消息写入到消息队列中
接收信息进程:创建/获取消息队列,根据键值找到对应消息队列,并从消息队列中读出消息
对应API
#include<sys/msg.h>//需要包含头文件
创建队列
int msgget(key_t key, int msgflg);
key为索引值,通过索引值在内核中寻找队列
msgflg表示打开队列的方式:如果内核中没有对应键值的消息队列并且msgflg中包含了IPC_CREAT标志位或者key参数为IPC—_PRIVATE时,msgget函数将创建一个新的消息队列,在创建新的消息队列后,需要在msgflg处说明消息队列的权限
返回值:成功返回消息队列ID号,失败返回-1
写入消息到队列中
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
msqid为写入进程在调用msgget函数成功后返回的队列ID
msgp指向一个msgbuf结构体
struct msgbuf{
long mtype;//消息类型,非负
char mtext[1];//消息内容
}
msgsz表示写入的大小,msgflg表示写入的方式
从队列中读出消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
msqid为读取进程在调用msgget函数成功后返回的队列ID
msgp指针为读取进程接受信息的位置
与msgsnd不同的是nsgrcv多了一个msgtyp,用来在消息队列中根据类型寻找消息内容
键值设置
key_t ftok(const char *pathname, int proj_id);
第一个参数指定文件目录,.表示当前目录,第二个参数可根据自己需求来指定
控制消息队列
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
第一个参数为队列ID,第二个参数为需要执行的命令
第三个参数为记录队列信息的结构体
struct msqid_ds {
struct ipc_perm msg_perm; /* Ownership and permissions */
time_t msg_stime; /* Time of last msgsnd(2) */
time_t msg_rtime; /* Time of last msgrcv(2) */
time_t msg_ctime; /* Time of last change */
unsigned long __msg_cbytes; /* Current number of bytes in
queue (nonstandard) */
msgqnum_t msg_qnum; /* Current number of messages
in queue */
msglen_t msg_qbytes; /* Maximum number of bytes
allowed in queue */
pid_t msg_lspid; /* PID of last msgsnd(2) */
pid_t msg_lrpid; /* PID of last msgrcv(2) */
};
一般设置为空,
两进程通过消息队列进行消息收发代码
msgSend
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
//int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
struct msgBuf{
long mtype;
char mtext[128];
};
int main()
{
key_t key = ftok(".",1);
if (key != -1)
{
printf("key:%x\n",key);
}
int msgid = msgget(key,IPC_CREAT|0777);
struct msgBuf sendBuf = {778,"this is message from queue"};
if (msgid == -1)
{
printf("get queue failed\n");
}
else
{
printf("get queue success\n");
printf("queue id:%d\n",msgid);
}
msgsnd(msgid,&sendBuf,strlen(sendBuf.mtext),0);
printf("sending success\n");
msgctl(msgid,IPC_RMID,NULL);
return 0;
}
msgGet
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
struct msgBuf{
long mtype;
char mtext[128];
};
int main()
{
/*
1获取消息队列
2读取消息
*/
key_t key = ftok(".",1);
if (key != -1)
{
printf("key:%x\n",key);
}
int msgid = msgget(key,IPC_CREAT|0777);
struct msgBuf readBuf;
if (msgid == -1)
{
printf("get queue failed\n");
}
else
{
printf("get queue success\n");
printf("queue id:%d\n",msgid);
}
msgrcv(msgid,&readBuf,sizeof(readBuf.mtext),778,0);
printf("receive from queue:%s\n",readBuf.mtext);
msgctl(msgid,IPC_RMID,NULL);
return 0;
}
验证结果:
共享内存
过程
1获取/创建共享内存
2读写进程分别把共享内存挂载到各自的地址空间里
3根据利用strcpy把消息复制到共享内存中
4读进程把消息读出
API
int shmget(key_t key, size_t size, int shmflg);
跟消息队列一样,共享内存也需要key值,第二个参数为共享内存的大小,在设置时必须设置成兆的整数倍,最后一个是获取共享内存的方式,通常设置成IPC_CREAT并带上操作权限,成功返回内存ID,不成功返回-1
void *shmat(int shmid, const void *shmaddr, int shmflg);
挂载函数:把创建成功的shmid号共享内存挂载到进程的地址空间中,最后一个参数默认0为可读可写权限
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
对共享内存进行操作
int shmdt(const void *shmaddr);
卸载掉已挂载的共享内存(不是删除)
编程实现
#include<stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include<stdlib.h>
//int shmget(key_t key, size_t size, int shmflg);
//void *shmat(int shmid, const void *shmaddr, int shmflg);
//int shmctl(int shmid, int cmd, struct shmid_ds *buf);
//int shmdt(const void *shmaddr);
int main()
{
key_t key;
int shmid;
char *shmaddr = NULL;
key = ftok(".",2);
shmid = shmget(key,1024*4,0);
if(shmid == -1){
printf("shmget failed\n");
exit(-1);
}
shmaddr = shmat(shmid,0,0);
printf("allocate success\n");
printf("data:%s\n",shmaddr);
shmdt(shmaddr);
shmctl(shmid,IPC_RMID,0);
printf("quit\n");
return 0;
}
#include<stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include<stdlib.h>
//int shmget(key_t key, size_t size, int shmflg);
//void *shmat(int shmid, const void *shmaddr, int shmflg);
//int shmctl(int shmid, int cmd, struct shmid_ds *buf);
//int shmdt(const void *shmaddr);
int main()
{
key_t key;
int shmid;
char *shmaddr = NULL;
key = ftok(".",2);
shmid = shmget(key,1024*4,0);
if(shmid == -1){
printf("shmget failed\n");
exit(-1);
}
shmaddr = shmat(shmid,0,0);
printf("allocate success\n");
printf("data:%s\n",shmaddr);
shmdt(shmaddr);
shmctl(shmid,IPC_RMID,0);
printf("quit\n");
return 0;
}
结果演示
信号:软中断
对信号有3种处理方式:忽略,捕捉信号,系统默认动作
编程演示:运行程序时键盘按下CTRL+C无效
#include<signal.h>
#include<stdio.h>
//typedef void (*sighandler_t)(int);
//sighandler_t signal(int signum, sighandler_t handler);
void handler(int signum)
{
printf("get signum : %d\n",signum);
printf("SIGINT\n");
}
int main()
{
signal(SIGINT,handler);//注册信号捕捉函数,第一个参数对应需要捕捉的信号编号,第二个为回调函数指针指向捕捉信号后需要执行的函数
while(1);
return 0;
}
在主函数注册信号捕捉函数并定义回调函数之后,在程序运行过程中一旦接收到注册的信号就会指向在主函数外定义的函数并执行所定义的函数
以上所说的函数仅能针对当信号触发时系统的行为
更高级的sigaction函数可以在传输信号时携带信息
关心两个问题:
对于发送端:用什么发以及如何放入信息
对于接收端:用什么收以及如何读出发送端的信息
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
返回值为int型的信号捕捉函数,指定捕获的信号,并执行新的操作,最后一个变量为备份旧操作,如不需要备份赋NULL值即可
定义操作结构体时需要设置一下参数:
1捕捉信号后需要执行的操作(不接收数据)
2信号处理程序,能接受额外数据
3阻塞关键字的信号集
4影响信号的行为标志位
5保留字段,一般设NULL值(已被弃用)
步骤:
首先与初级信号函数一样我们均需要对捕获信号后所需要进行的动作进行定义,要实现收发消息,需要在act结构体中把sa_flag设置为SA_SIGINFO来使能数据接收功能,并且要在设置信号处理函数时设置成sigaction选项,并且要在发送消息的程序中设置好发送的数据或者信息
示例程序:
#include<stdio.h>
#include<signal.h>
//接收端程序
void handler(int signum,siginfo_t *info,void *context)
{
printf("%d process send message\n",signum);
if(context != NULL){
printf("get data : %d\n",info->si_value.sival_int);
}
}
int main()
{
struct sigaction act;
act.sa_sigaction = handler;
act.sa_flags = SA_SIGINFO;
sigaction(SIGUSR1,&act,NULL);
while(1);
return 0;
}
#include<signal.h>
#include<stdio.h>
#include<stdlib.h>
//发送端程序
int main(int argc,char **argv)
{
int pid = atoi(argv[1]);
int signum = atoi(argv[2]);
union sigval value;
value.sival_int = 100;
sigqueue(pid,signum,value);
printf("done\n");
}
实际运行效果
信号量(进程同步)
本质就是一个计数器,用来在临界资源使用中使多个进程操作互斥或者同步
拿生活中的例子来说:只有一台打印机,两个人都想同时用,人为规定只能一个人用完另一个人用,于是就把打印机锁上了,并配备一把钥匙,需要用的时候开锁,用完把钥匙放回等下一个人用。这里的打印机就是临界资源,而钥匙和锁就是信号量的表示
PV操作:原子操作:一旦开始执行必须执行完,不能中断
P操作:信号量减一(拿钥匙)
V操作:信号量加一(放回钥匙)
编程演示:调用linux相关的api来实现信号量的应用
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
//int semget(key_t key, int nsems, int semflg);
//int semctl(int semid, int semnum, int cmd, ...);
//int semop(int semid, struct sembuf *sops, size_t nsops);指定信号量数组的ID,并在结构体中设置好操作参数,最后设置操作个数
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
//保证子进程比父进程先运行(不使用wait函数的情况下)
void PGetKey(int id)
{
struct sembuf set;
set.sem_num = 0;//操作信号量编号
set.sem_op = -1;//信号量操作数(变化)
set.sem_flg = SEM_UNDO;
semop(id,&set,1);
printf("get the key!\n");
}
void VReturnKey(int id)
{
struct sembuf set;
set.sem_num = 0;
set.sem_op = 1;
set.sem_flg = SEM_UNDO;
semop(id,&set,1);
printf("return the key!\n");
}
int main()
{
key_t key;
key = ftok(".",2);
int semid = 0;
union semun initsem;
initsem.val = 0;
semid = semget(key,1,IPC_CREAT|0666);//获取或创建信号量数组并指定信号量数组中信号量个数
if(semid != -1){
printf("semophore create success\n");
printf("semid:%d\n",semid);
}
semctl(semid,0,SETVAL,initsem);//信号量数组赋初值或其他操作
pid_t pid = fork();
if(pid > 0){
PGetKey(semid);
printf("this is father \n");
VReturnKey(semid);
semctl(semid,0,IPC_RMID);
}
else if(pid == 0){
printf("this is child\n");
VReturnKey(semid);
}
else{
printf("fork error\n");
}
return 0;
}
当初始信号量为0时,父进程因为拿不到钥匙而阻塞,而子进程通过一些特殊途径拿到一把钥匙并且把钥匙还回去了,从而使父进程可以拿到钥匙并且运行,由此可以实现不调用wait函数实现让子进程先运行父进程后运行