20155326 第十周课下作业-IPC
学习题目:
-
研究Linux下IPC机制:原理,优缺点,每种机制至少给一个示例,提交研究博客的链接
共享内存 管道 FIFO 信号 消息队列
学习过程
-IPC是什么
在linux下的多个进程间的通信机制叫做IPC(Inter-Process Communication),它是多个进程之间相互沟通的一种方法。在linux下有多种进程间通信的方法:半双工管道、命名管道、消息队列、信号、信号量、共享内存、内存映射文件,套接字等等。使用这些机制可以为linux下的网络服务器开发提供灵活而又坚固的框架。
共享内存
-
共享内存是在多个进程之间共享内存区域的一种进程间的通信方式,由IPC为进程创建的一个特殊地址范围,它将出现在该进程的地址空间中。其他进程可以将同一段共享内存连接到自己的地址空间中。所有进程都可以访问共享内存中的地址,就好像它们是malloc分配的一样。如果一个进程向共享内存中写入了数据,所做的改动将立刻被其他进程看到。
-
共享内存是IPC最快捷的方式,因为共享内存方式的通信没有中间过程,而管道、消息队列等方式则是需要将数据通过中间机制进行转换。共享内存方式直接将某段内存段进行映射,多个进程间的共享内存是同一块的物理空间,仅仅映射到各进程的地址不同而已,因此不需要进行复制,可以直接使用此段空间。
-
共享内存本身没有同步机制。
-
原理:
- 使用方法:
共享内存的使用过程可分为 创建->连接->使用->分离->销毁 这几步。
创建使用shmget函数(SHared Memory GET)函数,使用方法如下:
int segment_id = shmget (shm_key, getpagesize (),IPC_CREAT | S_IRUSR | S_IWUSER);
创建后,为了使共享内存可以被当前进程使用,必须紧接着进行连接操作。使用函数shmat(SHared Memory ATtach),参数传入通过shmget返回的共享内存id即可:
[cpp] view plain copy
shared_memory = (char*) shmat (segment_id, 0, 0);
当共享内存使用完毕后,使用函数shmdt (SHared Memory DeTach)进行解连接。
- 示例
下面的示例程序,a进程每一秒的向共享内存写入一个随机数。
#include<stdio.h>
#include<unistd.h>
#include<sys/shm.h>
#include<stdlib.h>
#include<error.h>
int main(){
int shm_id;
int *share;
int num;
srand(time(NULL));
shm_id = shmget (1234, getpagesize(), IPC_CREAT);
if(shm_id == -1){
perror("shmget()");
}
share = (int *)shmat(shm_id, 0, 0);
while(1){
num = random() % 1000;
*share = num;
printf("write a random number %d\n", num);
sleep(1);
}
return 0;
}
运行程序a结果如下:
b进程每隔一秒从该共享内存读出该数。
#include<stdio.h>
#include<unistd.h>
#include<sys/shm.h>
#include<stdlib.h>
#include<error.h>
int main(){
int shm_id;
int *share;
shm_id = shmget (1234, getpagesize(), IPC_CREAT);
if(shm_id == -1){
perror("shmget()");
}
share = (int *)shmat(shm_id, 0, 0);
while(1){
sleep(1);
printf("%d\n", *share);
}
return 0;
}
运行程序b结果如下:
- 优缺点
共享内存简单而高效,但是缺乏同步机制,所以,一般情况下需要配合其他IPC机制共同使用,否则会带来同步问题。
管道
管道是UNIX系统上最古老的IPC方法,管道提供了一种优雅的解决方案:给定两个运行不同程序的进程,在shell中如何让一个进程的输出作为另一个进程的输入?管道可以用来在相关(一个共同的祖先进程创建管道)进程之间传递数据。
eg:统计一个目录中文件的数目,输入命令: ls | wc -l,为了执行上面的命令,shell创建了两个进程来分别执行ls和wc(通过使用fork()和exec()来完成)。如下图所示:
- 特征
1)一个管道是一个字节流(无边界,顺序的)
从管道中读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是什么。此外,通过管道传递的数据是顺序的,从管道中读取出来的字节顺序与它们被写入管道的顺序是完全一样的,在管道中无法使用lseek()来随机地访问数据。
2)从管道中读取数据(读空管道将阻塞,读端遇0为关闭)
试图从一个当前为空的管道中读取数据将会被阻塞直到至少有一个字节被写入到管道中为止。
3)管道是单向的
管道的一端用于写入,另一端用于读取。
4)可以确保写入不超过PIPE_BUF字节的操作是原子的
5)管道的容量是有限的
- 使用
pipe函数原型:
#include <unistd.h>
int pipe(int file_descriptor[2]);//建立管道,该函数在数组上填上两个新的文件描述符后返回0,失败返回-1。
int fd[2];
int result = pipe(fd);
通过使用底层的read和write调用来访问数据。 向file_descriptor[1]写数据,从file_descriptor[0]中读数据。写入与读取的顺序原则是先进先出。
通常,使用管道让两个进程进程通信,为了让两个进程通过管道进行连接,在调用完pipe()之后可以调用fork()。在fork()期间,子进程会继承父进程的文件描述符。在fork()调用之后,其中一个进程应该立即关闭管道的写入端的描述符,另一个进程应该关闭读取端的描述符。
创建一个从父进程到子进程的管道,并且父进程经由该管道向子进程传送数据:
#include <unistd.h>
#include <stdio.h>
#define MAXLINE 1024
int main(void)
{
int n;
int fd[2];
pid_t pid;
char line[MAXLINE];
if (pipe(fd) < 0)
printf("pipe error\n");
if ((pid = fork()) < 0) {
printf("fork error\n");
} else if (pid > 0) { /* parent */
close(fd[0]);
write(fd[1], "hello world\n", 12); /* write data to fd[1] */
} else { /* child */
close(fd[1]);
n = read(fd[0], line, MAXLINE); /* read data from fd[0] */
write(STDOUT_FILENO, line, n); /* write data to standard output */
}
return (0);
}
- 优缺点
从管道中读取数据的进程可以读取任意大小的数据块且通过管道传递的数据是顺序的,但是如果需要在管道中实现离散消息的传递,就必须要在应用程序中完成这些工作,但是对于此类需求,最好使用其他IPC机制,比如,消息队列,数据报socket。且每个数据块有一个最大长度的限制。
共享内存比管道和消息队列效率高的原因
共享内存区是最快的可用IPC形式,一旦这样的内存区映射到共享它的进程的地址空间,这些进程间数据的传递就不再通过执行任何进入内核的系统调用来传递彼此的数据,节省了时间。
FIFO
FIFO有时被称为命名管道,未命名的管道只能在两个相关的进程之间使用,而且这两个相关的进程还要有一个共同的祖先进程。但是,通过FIFO,不相关的进程之间也能交换数据。
- 使用
使用如下函数创建FIFO:
#include <sys/stat.h>
int mkfifo(const char* path, mode_t mode);
int mkfifoat(int fd, const char* path, mode_t mode);
FIFO主要有以下两种用途:
(1)shell命令使用FIFO将数据从一条管道传送到另一条管道,无需创建中间临时文件。
(2)客户进程-服务器进程应用程序中,FIFO用作汇聚点,在客户进程和服务器进程之间传递数据。
- 例子
此为迭代型服务器。若有多个客户端请求,服务器会先处理完一个,再处理下一个。
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#define MAXLINE 1024
#define SERV_FIFO "./fifo.serv"
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) //在文件系统中创建的FIFO的访问权限位
int main(int argc, char** argv)
{
int readfifo ; //从自己的FIFO读
int writefifo ; //向进程对应的FIFO写
int fd ; //文件描述符
char buff[MAXLINE] ;
char fifoname[MAXLINE] ;
char* ptr ;
ssize_t n ;
//创建服务器的FIFO
if ((mkfifo(SERV_FIFO, FILE_MODE) < 0) && (errno != EEXIST))
printf("can't create %s", SERV_FIFO) ;
//打开服务器FIFO的读、写端
readfifo = open(SERV_FIFO, O_RDONLY, 0) ;
//读取来自客户端的请求
while ((n = read(readfifo, buff, MAXLINE)) > 0)
{
if (buff[n-1] == '\n')
buff[--n] = '\0' ;
//获取客户端的进程ID号
if ((ptr = strchr(buff, ' ')) == NULL)
{
printf("bogus request: %s", buff) ; //伪造的客户请求
continue ;
}
*ptr++ = 0 ;
//根据客户端进程ID打开客户端FIFO
snprintf(fifoname, sizeof(fifoname), "./fifo.%s", buff) ;
if ((writefifo = open(fifoname, O_WRONLY, 0)) < 0)
{
printf("cannot open : %s", fifoname) ;
continue ;
}
//向客户端FIFO中写入客户所请求的文件内容
if ((fd = open(ptr, O_RDONLY)) < 0)
{ //打开文件出错,向客户端返回错误信息
snprintf(buff+n, sizeof(buff) - n, ":can't open, %s\n", strerror(errno)) ;
n = strlen(ptr) ;
write(writefifo, ptr, n) ;
close(writefifo) ;
}
while ((n = read(fd, buff, MAXLINE)) > 0)
write(writefifo, buff, n) ;
close(writefifo) ;
close(fd) ;
}//while
exit(0) ;
}
//客户端知道服务器端FIFO的名字,且服务器知道它维护的FIFO的名字是 fifo.进程ID
//
//---------------客户端-------------
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#define MAXLINE 1024
#define SERV_FIFO "./fifo.serv"
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
int
main(int argc, char** argv)
{
int readfifo ;
int writefifo ;
size_t len ;
ssize_t n ;
char* ptr ;
char fifoname[MAXLINE] ;
char buff[MAXLINE] ;
pid_t pid ;
//创建自己的FIFO 用于接收服务器端的数据
pid = getpid() ;
snprintf(fifoname, sizeof(fifoname), "./fifo.%ld", (long)pid) ;
if (mkfifo(fifoname, FILE_MODE) < 0 && (errno != EEXIST))
printf("can't create %s", fifoname) ;
//构造请求消息
snprintf(buff, sizeof(buff), "%ld ", (long)pid) ;
len = strlen(buff) ;
ptr = buff + len ;
fgets(ptr, MAXLINE - len, stdin) ; //从键盘获取请求文件路径
len = strlen(buff) ;
//打开服务器的FIFO 向其中写入消息
writefifo = open(SERV_FIFO, O_WRONLY, 0) ;
write(writefifo, buff, len) ;
//打开自己的FIFO 读取来自服务器的应答数据
readfifo = open(fifoname, O_RDONLY, 0) ;
while ((n = read(readfifo, buff, MAXLINE)) > 0)
write(STDOUT_FILENO, buff, n) ;
close(readfifo) ;
unlink(fifoname) ;
exit(0) ;
}
信号
信号机制是unix系统中最为古老的进程之间的通信机制,用于一个或几个进程之间传递异步信号。信号可以有各种异步事件产生,比如键盘中断等。shell也可以使用信号将作业控制命令传递给它的子进程。
进程可以通过三种方式来响应一个信号:(1)忽略信号,即对信号不做任何处理,其中,有两个信号不能忽略:SIGKILL及SIGSTOP;(2)捕捉信号。定义信号处理函数,当信号发生时,执行相应的处理函数;(3)执行缺省操作,Linux对每种信号都规定了默认操作,
- 使用
发送信号的主要函数有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。
1)1、kill()
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid,int signo)
参数pid的值 信号的接收进程
pid>0 进程ID为pid的进程
pid=0 同一个进程组的进程
pid<0 pid!=-1 进程组ID为 -pid的所有进程
pid=-1 除发送进程自身外,所有进程ID大于1的进程
Kill()最常用于pid>0时的信号发送,调用成功返回 0; 否则,返回 -1。
2)raise()
#include <signal.h>
int raise(int signo)
向进程本身发送信号,参数为即将发送的信号值。调用成功返回 0;否则,返回 -1。
3)sigqueue()
#include <sys/types.h>
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval val)
调用成功返回 0;否则,返回 -1。
sigqueue()主要是针对实时信号提出的(当然也支持前32种),支持信号带有参数,与函数sigaction()配合使用。
sigqueue的第一个参数是指定接收信号的进程ID,第二个参数确定即将发送的信号,第三个参数是一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值。
sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。如果signo=0,将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。
4)alarm()
#include <unistd.h>
unsigned int alarm(unsigned int seconds)
专门为SIGALRM信号而设,在指定的时间seconds秒后,将向进程本身发送SIGALRM信号,又称为闹钟时间。进程调用alarm后,任何以前的alarm()调用都将无效。如果参数seconds为零,那么进程内将不再包含任何闹钟时间。
如果调用alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。
5)setitimer()
#include <sys/time.h>
int setitimer(int which, const struct itimerval *value, struct itimerval*ovalue));
setitimer()比alarm功能强大,支持3种类型的定时器:
ITIMER_REAL: 设定绝对时间;经过指定的时间后,内核将发送SIGALRM信号给本进程;
ITIMER_VIRTUAL 设定程序执行时间;经过指定的时间后,内核将发送SIGVTALRM信号给本进程;
ITIMER_PROF 设定进程执行以及内核因本进程而消耗的时间和,经过指定的时间后,内核将发送ITIMER_VIRTUAL信号给本进程;
6)abort()
#include <stdlib.h>
void abort(void);
向进程发送SIGABORT信号,默认情况下进程会异常退出,当然可定义自己的信号处理函数。即使SIGABORT被进程设置为阻塞信号,调用abort()后,SIGABORT仍然能被进程接收。该函数无返回值。
- 示例
这个例子展示了进程间如何使用信号量来进行通信,两个相同的程序同时向屏幕输出数据,我们可以看到如何使用信号量来使两个进程协调工作,使同一时间只有一个进程可以向屏幕输出数据(屏幕是临界资源)。注意,如果程序是第一次被调用(为了区分,第一次调用程序时带一个要输出到屏幕中的字符作为一个参数),则需要调用set_semvalue函数初始化信号并将message字符设置为传递给程序的参数的第一个字符,同时第一个启动的进程还负责信号量的删除工作。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/sem.h>
union semun
{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
static int sem_id = 0;
static int set_semvalue();
static void del_semvalue();
static int semaphore_p();
static int semaphore_v();
int main(int argc, char *argv[])
{
char message = 'X';
int i = 0;
//创建信号量
sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
if(argc > 1)
{
//程序第一次被调用,初始化信号量
if(!set_semvalue())
{
fprintf(stderr, "Failed to initialize semaphore\n");
exit(EXIT_FAILURE);
}
//设置要输出到屏幕中的信息,即其参数的第一个字符
message = argv[1][0];
sleep(2);
}
for(i = 0; i < 10; ++i)
{
//进入临界区
if(!semaphore_p())
exit(EXIT_FAILURE);
//向屏幕中输出数据
printf("%c", message);
//清理缓冲区,然后休眠随机时间
fflush(stdout);
sleep(rand() % 3);
//离开临界区前再一次向屏幕输出数据
printf("%c", message);
fflush(stdout);
//离开临界区,休眠随机时间后继续循环
if(!semaphore_v())
exit(EXIT_FAILURE);
sleep(rand() % 2);
}
sleep(10);
printf("\n%d - finished\n", getpid());
if(argc > 1)
{
//如果程序是第一次被调用,则在退出前删除信号量
sleep(3);
del_semvalue();
}
exit(EXIT_SUCCESS);
}
static int set_semvalue()
{
//用于初始化信号量,在使用信号量前必须这样做
union semun sem_union;
sem_union.val = 1;
if(semctl(sem_id, 0, SETVAL, sem_union) == -1)
return 0;
return 1;
}
static void del_semvalue()
{
//删除信号量
union semun sem_union;
if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
fprintf(stderr, "Failed to delete semaphore\n");
}
static int semaphore_p()
{
//对信号量做减1操作,即等待P(sv)
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;//P()
sem_b.sem_flg = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_p failed\n");
return 0;
}
return 1;
}
static int semaphore_v()
{
//这是一个释放操作,它使信号量变为可用,即发送信号V(sv)
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;//V()
sem_b.sem_flg = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_v failed\n");
return 0;
}
return 1;
}
运行结果如下:
消息队列
消息队列是内核地址空间中的内部链表,通过linux内核在各个进程直接传递内容,消息顺序地发送到消息队列中,并以几种不同的方式从队列中获得,每个消息队列可以用IPC标识符唯一地进行识别。内核中的消息队列是通过IPC的标识符来区别,不同的消息队列直接是相互独立的。每个消息队列中的消息,又构成一个独立的链表。
Linux的消息队列(queue)实质上是一个链表,它有消息队列标识符(queue ID)。 msgget创建一个新队列或打开一个存在的队列;msgsnd向队列末端添加一条新消息;msgrcv从队列中取消息, 取消息是不一定遵循先进先出的, 也可以按消息的类型字段取消息。
- 使用
对于系统中的每个消息队列,内核维护一个定义在<sys/msg.h>头文件中的信息结构。
struct msqid_ds {
struct ipc_perm msg_perm ;
struct msg* msg_first ; //指向队列中的第一个消息
struct msg* msg_last ; //指向队列中的最后一个消息
……
} ;
调用的第一个函数通常是msgget,其功能是打开一个现存队列或创建一个新队列。
#include <sys/msg.h>
int msgget (key_t key, int oflag) ;
返回值是一个整数标识符,其他三个msg函数就用它来指代该队列。它是基于指定的key产生的,而key既可以是ftok的返回值,也可以是常值IPC_PRIVATE。
oflag是读写权限的组合(用于打开时)。它还可以是IPC_CREATE或IPC_CREATE | IPC_EXCL(用于创建时)。
- 示例
使用消息队列进行进程间通信:
接收信息的程序源文件为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\n", errno);
exit(EXIT_FAILURE);
}
//从队列中获取消息,直到遇到end消息为止
while(running)
{
if(msgrcv(msgid, (void*)&data, BUFSIZ, msgtype, 0) == -1)
{
fprintf(stderr, "msgrcv failed with errno: %d\n", errno);
exit(EXIT_FAILURE);
}
printf("You wrote: %s\n",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\n");
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\n", 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\n");
exit(EXIT_FAILURE);
}
//输入end结束输入
if(strncmp(buffer, "end", 3) == 0)
running = 0;
sleep(1);
}
exit(EXIT_SUCCESS);
}
运行结果如下:
- 优缺点
消息队列克服了信号传递信息少,管道只能支持无格式字节流和缓冲区受限的缺点。
消息队列跟命名管道有不少的相同之处,通过与命名管道一样,消息队列进行通信的进程可以是不相关的进程,同时它们都是通过发送和接收的方式来传递数据的。在命名管道中,发送数据用write,接收数据用read,则在消息队列中,发送数据用msgsnd,接收数据用msgrcv。而且它们对每个数据都有一个最大长度的限制。
与命名管道相比,消息队列的优势在于,1、消息队列也可以独立于发送和接收进程而存在,从而消除了在同步命名管道的打开和关闭时可能产生的困难。2、同时通过发送消息还可以避免命名管道的同步和阻塞问题,不需要由进程自己来提供同步方法。3、接收程序可以通过消息类型有选择地接收数据,而不是像命名管道中那样,只能默认地接收。
- 共享内存和消息队列,FIFO,管道传递消息的区别:
后者,消息队列,FIFO,管道的消息传递方式一般为
1:服务器得到输入
2:通过管道,消息队列写入数据,通常需要从进程拷贝到内核。
3:客户从内核拷贝到进程
4:然后再从进程中拷贝到输出文件
上述过程通常要经过4次拷贝,才能完成文件的传递。
而共享内存只需要
1:从输入文件到共享内存区
2:从共享内存区输出到文件
上述过程不涉及到内核的拷贝,所以花的时间较少。
代码
参考博客
linux基础——linux进程间通信(IPC)机制总结
Linux IPC之管道和FIFO
Linux进程间通信——使用消息队列