Linux学习:进程间通信
内容介绍
IPC通信中的内存映射通信,管道映射通信,消息队列通信以及信号通信。
1. 实现内存映射通信。
内存映射概念:
使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是相比其他通信机制运行效率较低设计的。往往与其它通信机制,如信号量结合使用, 来达到进程间的同步及互斥。
主要代码:
单工通信代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <sys/mman.h>
struct mymsg
{
int type;
char text[512];
};
int main()
{
pid_t result;
mymsg *my_map;
char temp;
my_map = (mymsg* )mmap(NULL, sizeof(mymsg)*10, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
result = fork();
if(result<0)
{
perror("创建子程序失败\n");
exit(0);
}
else if(result == 0)
{
printf("子进程 %d, 开始读出数据\n", getpid());
//sleep(4);
for(int i=0; i<5; i++)
{
printf("子进程读取:第 %d 个数据是: %s \n", i, (*(my_map+i)).text );
}
printf("子进程读出数据结束\n");
munmap(my_map, sizeof(mymsg)*10);
printf("子进程解除内存映射\n");
//exit(0);
}
else
{
int status;
printf("父进程 %d, 开始写入数据\n", getpid());
temp = 'a'-1;
for(int i=0; i<5; i++)
{
temp += 1;
memcpy((*(my_map+i)).text, &temp, 1);
(*(my_map+i)).type = i;
}
//sleep(5);
printf("父进程写入数据结束\n");
printf("父进程解除内存映射\n\n");
munmap(my_map, sizeof(mymsg)*10);
if(waitpid(-1, &status, 0) > 0)
{
printf("\nThe end!\n");
}
}
return 0;
}
双工通信代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/wait.h>
#define SIZE 1024
int main()
{
int shmid ;
char *shmaddr ; //共享内存地址
struct shmid_ds buf ;
int flag = 0 ;
int pid ;
shmid = shmget(IPC_PRIVATE, SIZE, IPC_CREAT|0600 ) ; //获得共享内存地址
if ( shmid < 0 )
{
perror("获取共享内存失败\n") ;
return -1 ;
}
pid = fork() ;
if(pid < 0)
{
perror("创建子进程失败\n");
shmctl(shmid, IPC_RMID, NULL) ;
exit(1);
}
if ( pid == 0 )
{
//子进程
shmaddr = (char *)shmat( shmid, NULL, 0 ) ; //映射共享内存
if ( (long)shmaddr == -1 )
{
perror("共享内存地址失效\n") ;
return -1 ;
}
printf("子进程开始写入数据\n");
strcpy( shmaddr, "这一行是来自子进程的数据\n") ;
printf("子进程写入数据完成\n\n");
shmdt( shmaddr ) ; //撤销内存映射
return 0;
}
else if ( pid > 0)
{
printf("父进程挂起等待子进程结束\n\n");
//父进程
int status;
if(waitpid(-1, &status, 0) < 0) exit(1); //子进程返回出错
flag = shmctl( shmid, IPC_STAT, &buf) ; //获得控制
if ( flag == -1 )
{
perror("获得控制失败\n") ;
return -1 ;
}
printf("共享内存大小 %d bytes\n", buf.shm_segsz ) ;
printf("父进程 pid=%d, shm_cpid = %d \n", getpid(), buf.shm_cpid ) ;
printf("子进程 pid=%d, shm_lpid = %d \n",getpid() , buf.shm_lpid ) ;
shmaddr = (char *) shmat(shmid, NULL, 0 ) ; //内存映射
if ( (long)shmaddr == -1 )
{
perror("内存映射失败\n") ;
return -1 ;
}
printf("父进程读出内容为: %s\n", shmaddr) ;
shmdt( shmaddr ) ; //解除映射
shmctl(shmid, IPC_RMID, NULL) ;
}
return 0 ;
}
结果:
结果分析:
在该实验中,我设计了两个子程序,其实两者是完全独立的。
代码1实现的是单工通信,父进程只能发送数据,然后子进程逐一读出。实现的办法是在程序中通过fork()获取进程的pid,判断是子进程还是父进程。然后采取不同的操作,一个写入,一个读出。难度不大。实验结果如图1所示。
代码2中我寻求的是双向通信,即子进程也可以发送消息给父进程。但是就存在一个问题了。父进程可以创建子进程,然后阻塞,知道子进程完成被唤醒。但是相反则不能。没有机制可以使得子进程阻塞,等待父进程完成任务之后唤醒子进程。因为这涉及到先有父进程后有子进程的问题。所以这里的代码2实现了父进程创建,并创建子进程,然后等待子进程输入,输入结束后,父进程读出数据。
2. 实现管道映射通信。
管道通信概念:
管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
主要代码
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t result;
int r_num;
int pipe_fd[2];
char buf_r[100], buf_w[100]; //读写的缓冲区
memset(buf_r, 0, sizeof(buf_r)); //将写的缓冲区初始化
if(pipe(pipe_fd) < 0) //创建一个管道
{
printf("创建管道失败\n");
return -1;
}
result = fork(); //创建子进程
if(result < 0)
{
perror("创建子进程失败\n");
exit(0);
}
else if(result ==0)
{
//对于子进程
close(pipe_fd[1]); //关闭写通道
if((r_num = read(pipe_fd[0],buf_r, 100)) > 0)
printf("子进程从管道中读取 %d 个字符,读取的字符串是 : %s\n", r_num, buf_r);
close(pipe_fd[0]); //关闭读通道
exit(0);
}
else
{
//对于父进程
close(pipe_fd[0]); //关闭读通道
printf("输入写入管道字符串\n");
scanf("%s", buf_w);
if(write(pipe_fd[1], buf_w, strlen(buf_w))!= -1)
printf("父进程向管道写入:%s\n",buf_w);
close(pipe_fd[1]); //关闭写通道
waitpid(result, NULL, 0); //调用waitpid, 阻塞父进程,等待子进程退出
exit(0);
}
}
实验结果
结果分析
其实本实验实现的是有名管道,可以在父进程和子进程之间进行通信。这里的机制是使用一个数组来表示管道,第一个位表示读通道。第二个位表示写通道。然后在父进程中,先关闭读通道,不让读,然后写入数据在管道中。接着关闭写通道。然后会挂起,等待子进程活动退出再退出。
在子进程中先关闭写通道,不让写,然后读出数据,接着关闭读通道,打印出数据,返回。最后返回父进程,程序结束。实验结果如图3所示。
3. 实现消息队列通信。
消息队列通信概念:
消息队列,就是一个消息的链表,是一系列保存在内核中消息的列表。用户进程可以向消息队列添加消息,也可以向消息队列读取消息。
主要代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <sys/wait.h>
struct mymsg
{
long msg_type; //消息类型
char msg_text[512];
};
int main()
{
int qid;
key_t key;
int len;
struct mymsg msg;
pid_t result;
//创建子进程
result = fork();
if(result < 0)
{
perror("创建子进程失败\n");
exit(0);
}
//调用ftok创建key
if((key=ftok(".",10))==-1)
{
perror("产生标准key出错\n");
exit(1);
}
printf("key= %d\n", key);
//调用msgget创建、打开消息队列
if((qid=msgget(key, IPC_CREAT|0666)) ==-1)
{
perror("创建消息队列出错\n");
exit(1);
}
printf("进程%d 创建、打开的队列号是:%d\n", getpid(), qid); //消息队列准备完毕
if(result==0)
{//对于子进程
if((msgrcv(qid, &msg, 512, 0, 0)) < 0)
{
perror("子进程读取消息出错\n");
exit(1);
}
printf("子进程读取的消息是:%s \n", (&msg)->msg_text);
}
else
{//对于父进程
int status; //子进程状态
sleep(2);
puts("在父进程中输入要加入队列的消息:");
if((fgets((&msg)->msg_text, 512, stdin))==NULL)
{
puts("没有消息");
exit(1);
}
msg.msg_type = getpid();
len = strlen(msg.msg_text);
if((msgsnd(qid, &msg, len, 0)) < 0)
{
perror("父进程添加消息出错\n");
exit(1);
}
if(waitpid(-1, &status, 0) > 0)
{//等待子进程结束再关闭
if(WIFEXITED(status)!=0)
{//返回状态正餐
//删除消息队列
if((msgctl(qid, IPC_RMID, NULL)) < 0)
{
printf("%d 删除消息队列出错\n", getpid());
exit(1);
}
}
}
}
return 0;
}
结果
结果分析
从实验结果图中可以发现,父进程和子进程在创建的时候都使用相同的队列号。个人觉得这里的队列其实有点类似于共享存储通信。因为我实现的方式是采用了一个结构,里面包含了一个消息类型的标识和一块缓冲区用于存储数据。只不过在每次发送消息的时候,都有一个msgsnd()函数用于添加信息。Msgrcv()用于接受信息。这一点有点类似于信件投递。
struct mymsg
{
long msg_type; //消息类型
char msg_text[512];
};
其实消息类型的标识使用的是进程号。在父进程中主要做两件事,一是写入消息,采用msgsnd发送出消息,等待子进程读取数据完成之后删除队列。
子进程则是接受消息,并且打印。实验结果如图4所示。
4. 实现信号通信。
信号映射概念:
信号是比较复杂的通信方式,用于通知接受进程有某种事件生,除了用于进程间通信外,进程还可以发送信号给进程本身。
主要代码
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
static bool flag = true;
void handler(int sig)
{
if(SIGALRM == sig)
{
printf("alarm, I got signal %d\n", sig);
}
else
{
printf("quit, I got signal %d\n", sig); flag = false; //这让子进程退出循环
}
}
int main()
{
printf("SIGALRM的值是 %d \t SIGQUIT的值是 %d\n", SIGALRM, SIGQUIT);
pid_t pid;
pid = fork();
if(pid < 0)
{//进程创建失败
printf("fork() error\n");
}
else if(pid > 0)
{
printf("父进程执行到这里然后开始睡眠\n");
sleep(8);
kill(pid, SIGQUIT); exit(0);
printf("没有执行这里\n");
}
//子进程执行部分
struct sigaction act;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGALRM, &act, 0);
sigaction(SIGQUIT, &act, 0);
while(flag)
{
alarm(2);
printf("子进程执行到这里然后停下等待接受信号\n");
pause(); //此函数用于将进程挂起直到捕捉到信号为止,这个函数很常用,通常用于判断信号是否已到
}
printf("子进程结束\n");
return 0;
}
结果
结果分析
这个实验其实是相对比较难理解的,所以我打印出了SIGALRM的值和SIGQUIT的值,这两者都是头文件中定义的常量。用于标识获取了消息还是退出。很神奇的一个地方是,信号量采用一个结构体sigaction用于通信。这两个常量需要在sigaction中注册。
在代码中使用了pause()函数,这个函数用于将进程挂起直到捕捉到信号为止,这个函数很常用,通常用于判断信号是否已到。
在父进程中,父进程创建子进程,然后睡眠8秒钟。而在子进程中,每两秒会发送和接受一个信号,所以打印出了“子进程执行…”,当父进程结束的时候,子进程也结束,所以此时打印出退出的提示。