第十周课下作业-IPC
题目:研究Linux下IPC机制:原理,优缺点,每种机制至少给一个示例,提交研究博客的链接
- 共享内存
- 管道
- FIFO
- 信号
- 消息队列
共享内存
共享内存允许两个或多个进程进程共享同一块内存(这块内存会映射到各个进程自己独立的地址空间)从而使得这些进程可以相互通信。
在GNU/Linux中所有的进程都有唯一的虚拟地址空间,而共享内存应用编程接口API允许一个进程使用公共内存区段。但是对内存的共享访问其复杂度也相应增加。共享内存的优点是简易性。 使用消息队列时,一个进程要向队列中写入消息,这要引起从用户地址空间向内核地址空间的一次复制,同样一个进程进行消息读取时也要进行一次复制。共享内存的优点是完全省去了这些操作。
共享内存会映射到进程的虚拟地址空间,进程对其可以直接访问,避免了数据的复制过程。因此,共享内存是GNU/Linux现在可用的最快速的IPC机制。
1.shmget函数
该函数用来创建共享内存,它的原型为:
int shmget(key_t key, size_t size, int shmflg);
不相关的进程可以通过该函数的返回值访问同一共享内存,它代表程序可能要使用的某个资源,程序对所有共享内存的访问都是间接的,程序先通过调用shmget函数并提供一个键,再由系统生成一个相应的共享内存标识符(shmget函数的返回值),只有shmget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。
2.shmat函数
第一次创建完共享内存时,它还不能被任何进程访问,shmat函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。它的原型如下:
voidvoid *shmat(int shm_id, const voidvoid *shm_addr, int shmflg);
3.shmdt函数
该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。它的原型如下:
voidvoid *shmat(int shm_id, const voidvoid *shm_addr, int shmflg);
4、shmctl函数
与信号量的semctl函数一样,用来控制共享内存,它的原型如下:
voidvoid *shmat(int shm_id, const voidvoid *shm_addr, int shmflg);
- 首先先使用shmget建立一块共享内存,然后向该内存中写入数据并返回该共享内存shmid
使用另一个程序通过上一程序返回的shmid读该共享内存内的数据
建立共享内存并写入数据的程序
#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <errno.h>
void get_buf(char *buf)
{
int i=0;
while((buf[i]=getchar())!='
'&&i<1024)
i++;
}
int main(void)
{
int shmid;
shmid=shmget(IPC_PRIVATE,sizeof(char)*1024,IPC_CREAT|0666);
if(shmid==-1)
{
perror("shmget");
}
char *buf;
if((int)(buf=shmat(shmid,NULL,0))==-1)
{
perror("shmat");
exit(1);
}
get_buf(buf);
printf("%d
",shmid);
return 0;
}
- 读取数据的程序
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
int main(int argc,char **argv)
{
int shmid;
shmid=atoi(argv[1]);
char *buf;
if((int)(buf=shmat(shmid,NULL,0))==-1)
{
perror("shmat");
exit(1);
}
printf("%s
",buf);
shmdt(buf);
return 0;
}
-
命令行的第一个参数设为第一个程序输出的数字
如:
-
使用完以后可以使用
ipcrm -m 1867788
来删除该共享内存。
有一个很详细的实例:
管道
管道实际是用于进程间通信的一段共享内存,创建管道的进程称为管道服务器,连接到一个管道的进程为管道客户机。一个进程在向管道写入数据后,另一进程就可以从管道的另一端将其读取出来。
- 管道的特点:
1、管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;
2、只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。比如fork或exec创建的新进程,在使用exec创建新进程时,需要将管道的文件描述符作为参数传递给exec创建的新进程。当父进程与使用fork创建的子进程直接通信时,发送数据的进程关闭读端,接受数据的进程关闭写端。
3、单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。
4、数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
-
管道的实现机制:
管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。
管道只能在本地计算机中使用,而不可用于网络间的通信。 -
下面是一个父子进程利用管道通信的实例:
main函数创建两个管道,并用fork生成一个子进程,客户端作为父进程运行,服务器则作为子进程运行。第一个管道用于从客户向服务器发送路径名, 第二个管道用于从服务器向客户发送该文件的内容。
/*
* main函数创建两个管道,并用fork生成一个子进程
* 客户端作为父进程运行,服务器则作为子进程运行
* 第一个管道用于从客户向服务器发送路径名
* 第二个管道用于从服务器向客户发送该文件的内容
*
* cin --客户端写pipe1[1]-----pipe1[0]服务器读
* 服务器写pipe2[1]-----pipe2[0]客户端读
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
//服务器从rfd中读取文件的名字,向wfd中写入文件的内容
void server(int rfd, int wfd)
{
char fileName[1024];
char fileContent[2048];
memset(fileName, 0, 1024);
memset(fileContent, 0, 2048);
//从rfd中读取文件的名字,
int n = read(rfd, fileName, 1024);
fileName[n] = 0;
printf("server receive the file name is %s
", fileName);
int filefd = open(fileName, O_RDONLY);//打开文件
if(filefd < 0){
printf("open error
");
return;
}
else{//读取文件的内容,并写入到wfd中
//读取文件的内容到fileContent中
int num = 0;
while((num = read(filefd, fileContent, 2048)) > 0){
printf("server read the fileContent is: %s", fileContent);
//将fileContent中的内容写入到wfd中
write(wfd, fileContent, num);
}
}
close(filefd);
close(rfd);
close(wfd);
}
//客户端从rfd中读取文件的内容,向wfd中写入文件的名字
void client(int rfd, int wfd)
{
char fileName[1024];
char fileContent[2048];
memset(fileName, 0, 1024);
memset(fileContent, 0, 2048);
printf("输入文件名字:");
//从标准输入输入文件的名字
fgets(fileName, 1024, stdin);
int len = strlen(fileName);
if(fileName[len-1] == '
')
len--;
//向wfd中写入文件的名字
write(wfd, fileName, len);
printf("fileName = %s
", fileName);
//从rfd中读取文件的内容
int n;
while((n = read(rfd, fileContent, 2048)) > 0){
printf("client receive the content is: %s", fileContent);
}
close(rfd);
close(wfd);
}
//主函数
int main()
{
//创建两个管道.
int pipe1[2], pipe2[2];
int ret = pipe(pipe1);
if(ret < 0){
printf("pipe error
");
return -1;
}
ret = pipe(pipe2);
if(ret < 0){
printf("pipe error
");
return -1;
}
//创建一个子进程,作为服务器,用于读取管道1(pipe1[0])中的文件名,并将文件的内容输出到管道2的pipe2[1]中
pid_t child_pid = fork();
if(child_pid < 0){
printf("fork error
");
return -1;
}
else if(child_pid > 0){//父进程
//关闭管道1的写,关闭管道2的读, pipe1[1], pipe2[0]
close(pipe1[1]);
close(pipe2[0]);
server(pipe1[0], pipe2[1]);
}
else if(child_pid == 0){//子进程
//关闭管道1的读,关闭管道2的写, pipe1[0], pipe2[1]
close(pipe1[0]);
close(pipe2[1]);
client(pipe2[0], pipe1[1]);
}
waitpid(child_pid, NULL, 0);//等待子进程退出
return 0;
}
FIFO
有名管道用mkfifo函数创建。
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode)
命名管道和匿名管道区别:
1:管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
2:如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
3:命名管道是一种特殊类型的文件;
使用命名管道实例:
进程1创建命名管道myfifo,并以阻塞方式写打开,进程1从标准输入获得数据并写入到命名管道中;
进程2以阻塞方式读打开该命名管道文件myfifo,进程2从该命名管道中读取数据并输出到标准输出上;
- 进程1:write_fifo.cpp
#include<iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <cstdlib>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
using namespace std;
/*
1:创建命名管道
2:从标准输入输入内容到buffer中
3:打开命名管道,将buffer中的内容写入到命名管道中
*/
int main()
{
int ret = mkfifo("myfifo", 0666);
if(ret < 0){
cerr << "mkfifo error..." << endl;
exit(-1);
}
char buff[1024];
memset(buff, 0, 1024);
int wrfd;
cout << "wating for another process open the myfifo to reading..."<< endl;
wrfd = open("myfifo", O_WRONLY);
if(wrfd == -1){
cerr << "open error..." << endl;
exit(-1);
}
pid_t pid = getpid();
cout << "process " << pid << " write: ";
while(cin.getline(buff, 1024)){
write(wrfd, buff, strlen(buff));
memset(buff, 0, 1024);
cout << "process " << pid << " write: ";
}
close(wrfd);
exit(0);
}
- 进程2:read_fifo.cpp
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <cstdlib>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
using namespace std;
/*
1:创建命名管道
2:从标准输入输入内容到buffer中
3:打开命名管道,将buffer中的内容写入到命名管道中
*/
int main()
{
char buff[1024];
memset(buff, 0, 1024);
int rdfd;
int ret = 0;
rdfd = open("myfifo", O_RDONLY);
if(rdfd < 0){
cout << "open error..." << endl;
exit(-1);
}
cout << "waiting for reading...
";
while(1){
ret = read(rdfd, buff, 1024);
if(ret == 0){
cerr << "end of read..." << endl;
break;
}
cout << "process "<< getpid() << " read: " << buff << endl;
memset(buff, 0, 1024);
}
close(rdfd);
exit(0);
}
运行时就是write_fifo写进程创建管道,并向管道中写入数据,read_fifo读进程从管道中读数据。
信号
信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。
信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。
- 信号的处理——signal函数
程序可用使用signal函数来处理指定的信号,主要通过忽略和恢复其默认行为来工作。signal函数的原型如下:
#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);
- 给出一个例子来说明,源文件名为signal1.c,代码如下:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void ouch(int sig)
{
printf("
OUCH! - I got signal %d
", sig);
//恢复终端中断信号SIGINT的默认行为
(void) signal(SIGINT, SIG_DFL);
}
int main()
{
//改变终端中断信号SIGINT的默认行为,使之执行ouch函数
//而不是终止程序的执行
(void) signal(SIGINT, ouch);
while(1)
{
printf("Hello World!
");
sleep(1);
}
return 0;
}
可以看到,第一次按下终止命令(ctrl+c)时,进程并没有被终止,面是输出OUCH! - I got signal 2,因为SIGINT的默认行为被signal函数改变了,当进程接受到信号SIGINT时,它就去调用函数ouch去处理,注意ouch函数把信号SIGINT的处理方式改变成默认的方式,所以当你再按一次ctrl+c时,进程就像之前那样被终止了。
消息队列
消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。 每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。
- 在Linux中使用消息队列
Linux提供了一系列消息队列的函数接口来让我们方便地使用它来实现进程间的通信。它的用法与其他两个System V PIC机制,即信号量和共享内存相似。 - msgget函数
该函数用来创建和访问一个消息队列。它的原型为:
int msgget(key_t, key, int msgflg);
- msgsnd函数
该函数用来把消息添加到消息队列中。它的原型为:
int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
msgid是由msgget函数返回的消息队列标识符。
- msgrcv函数
该函数用来从一个消息队列获取消息,它的原型为
int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg);
msgid, msg_ptr, msg_st的作用也函数msgsnd函数的一样。
msgreceive和msgsned来表示接收和发送信息。根据正常的情况,允许两个程序都可以创建消息,但只有接收者在接收完最后一个消息之后,它才把它删除。
- 接收信息的程序源文件为msgreceive.c的源代码为:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/msg.h>
struct msg_st
{
long int msg_type;
char text[BUFSIZ];
};
int main()
{
int running = 1;
int msgid = -1;
struct msg_st data;
long int msgtype = 0; //注意1
//建立消息队列
msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
if(msgid == -1)
{
fprintf(stderr, "msgget failed with error: %d
", errno);
exit(EXIT_FAILURE);
}
//从队列中获取消息,直到遇到end消息为止
while(running)
{
if(msgrcv(msgid, (void*)&data, BUFSIZ, msgtype, 0) == -1)
{
fprintf(stderr, "msgrcv failed with errno: %d
", errno);
exit(EXIT_FAILURE);
}
printf("You wrote: %s
",data.text);
//遇到end结束
if(strncmp(data.text, "end", 3) == 0)
running = 0;
}
//删除消息队列
if(msgctl(msgid, IPC_RMID, 0) == -1)
{
fprintf(stderr, "msgctl(IPC_RMID) failed
");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
- 发送信息的程序的源文件msgsend.c的源代码为:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/msg.h>
#include <errno.h>
#define MAX_TEXT 512
struct msg_st
{
long int msg_type;
char text[MAX_TEXT];
};
int main()
{
int running = 1;
struct msg_st data;
char buffer[BUFSIZ];
int msgid = -1;
//建立消息队列
msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
if(msgid == -1)
{
fprintf(stderr, "msgget failed with error: %d
", errno);
exit(EXIT_FAILURE);
}
//向消息队列中写消息,直到写入end
while(running)
{
//输入数据
printf("Enter some text: ");
fgets(buffer, BUFSIZ, stdin);
data.msg_type = 1; //注意2
strcpy(data.text, buffer);
//向队列发送数据
if(msgsnd(msgid, (void*)&data, MAX_TEXT, 0) == -1)
{
fprintf(stderr, "msgsnd failed
");
exit(EXIT_FAILURE);
}
//输入end结束输入
if(strncmp(buffer, "end", 3) == 0)
running = 0;
sleep(1);
}
exit(EXIT_SUCCESS);
}
msgreceive.c文件main函数中定义的变量msgtype(注释为注意1),它作为msgrcv函数的接收信息类型参数的值,其值为0,表示获取队列中第一个可用的消息。再来看看msgsend.c文件中while循环中的语句data.msg_type = 1(注释为注意2),它用来设置发送的信息的信息类型,即其发送的信息的类型为1。所以程序msgreceive能够接收到程序msgsend发送的信息。
消息队列与命名管道的比较
消息队列跟命名管道有不少的相同之处,通过与命名管道一样,消息队列进行通信的进程可以是不相关的进程,同时它们都是通过发送和接收的方式来传递数据的。在命名管道中,发送数据用write,接收数据用read,则在消息队列中,发送数据用msgsnd,接收数据用msgrcv。而且它们对每个数据都有一个最大长度的限制。
与命名管道相比,消息队列的优势在于
1、消息队列也可以独立于发送和接收进程而存在,从而消除了在同步命名管道的打开和关闭时可能产生的困难。
2、同时通过发送消息还可以避免命名管道的同步和阻塞问题,不需要由进程自己来提供同步方法。
3、接收程序可以通过消息类型有选择地接收数据,而不是像命名管道中那样,只能默认地接收。
- 参考