项目名称:进程间通信机制
20135313吴子怡 20135322郑伟
研究时长:两周
内容摘要:管道、FIFO、消息队列、共享内存、信号量、对比机制优缺点
chapter 1 管道与FIFO
part 1 预习
在进程之间进行通信的最简单方法是通过文件,其中一个进程写文件,而另一个进程读文件。 管道是Linux中最常见的IPC机制,它实际上是在进程间开辟一个固定大小的缓冲区,需要发布信息的进程运行写操作,需要接收信息的进程运行读操作。管道是单向的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。 发送进程和接受进程是通过管道进行通信的,因此又称为管道通信。 管道可以和我们生活中具象的管道联系起来理解,就是数据沿着管道从左边流到了右边。
part 2管道的实现机制
管道就是指用于连接一个读进程和一个写进程,以实现他们之间通信的共享文件,又称为pipe文件。
Linux中实现了两种管道,一种是无名管道,一种是命名管道。无名管道没有磁盘节点,仅仅是一个内存对象存在,用完后就销毁了。因为没有文件名和路径,也没有此案节点,因此无名管道没有显式的打开过程,它是在创建的时候自动打开并且声称内存节点inode、目录项对象dentry和两个文件结构对象(读操作、写操作),其内存对象和普通文件一致。无名管道只能由父子进程之间、兄弟进程之间或者其他与偶沁园关系并且都继承了祖先进程的管道文件对象的两个进程间通信使用,命名管道是由文件名和磁盘i的,因此可以由任意两个或多个进程间通信使用,它的使用和普通文件类似,都遵循打开、读、写、关闭这样的过程。
1.无名管道
(1)工作方式
管道以先进先出方式保存一定数量的数据。使用管道的时候一个进程从管道的一端写, 另一个进程从管道的另一端读。在主进程中利用fork()函数创建一个子进程, 然后使用read()和write()函数来进行读写操作。
使用无名管道进行进程间通信的步骤概述如下:
①创建所需的管道 ②生成(多个)子进程 ③关闭/复制文件描述符,使之与相应地管道末端相联系; ④关闭不需要的管道末端; ⑤进行通信活动 ⑥关闭所有剩余的打开文件描述符 ⑦等待子进程结束
(2)无名管道的创建:pipe函数
#include <unistd.h> int pipe(int fd[2])
函数的参数中有两个文件描述符:fd[0]用于管道的read端,fd[1]用于write端。创建成功返回0,否则返回-1。
(3)写管道write函数
ret = write(fd[1],buf,n)
若管道已经满了,则被阻塞,知道管道另一端read将已进入管道的数据取走为止。
(4)读管道read函数
ret = write(fd[0],buf,n)
若管道为空,且写端文件描述字未关闭,则被阻塞。若管道已经关闭则返回0。
管道不为空分两种情况:设管道中实际有m个字节,如n>=m,则读m个;如果n<m则读取n个。实际读取的数目作为read的返回值。
(5)关闭管道close函数
关闭写端则导致读端read调用返回0;关闭读端,则导致写端write调用返回-1,error被设为EPIPE,在写端write函数退出前,进程还会收到SIGPIPE信号(默认处理是种植进程,该信号可以被捕捉)。
(6)文件描述符的复制dup2
int dup2(int fd1,int fd2);
复制文件描述符fd1到fd2。fd2可以是空闲的文件描述符,如果fd2是已打开文件,则关闭fd2;如果fd1不是有效的描述符,则不关闭fd2,调用失败。
(7)注意事项
①管道式半双工方式,数据只能单向传输,若要两个进程之间相互通信,则需要建立两个管道。 ②pipe()调用必须在调用fork()以前进行,否则子进程将无法继承文件描述符。 ③使用无名管道互相连接的任意进程必须位于一个相关的进程家族里。因为管道必须受到内核的限制,所以如果进程没有在管道创建者的家族里面,则该进程将无法访问管道。
这里我们用几个案例来体会一下:
<<案例一:从管道中读取数据
结论:写端不存在时,此时则认为已经读到了数据的末尾,读函数返回的读出字节数为0。
但是当写端存在时,如果请求的字节数目大于PIPE_BUF(ubuntu操作系统为65536),则返回管道中现有的数据字节数,如果请求的字节数目不大于PIPE_BUF,则放回管道中现有数据字节数(此时,管道中数据量小于请求的数据量);或者返回请求的字节数(此时,管道中数据量不小于请求的数据量)。
<<案例二:父进程向管道中写数据,子进程从管道中读取数据
结论:
<1>当写端存在时,管道中没有数据时,读取管道时将阻塞
2.命名管道(FIFO:先进先出)
(1)工作方式
无名管道只能用于具有亲缘关系的进程间通信。而命名管道提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。因此,通过FIFO不想管的进程也能交换数据。FIFO管道的打开时需要用户打开和关闭,因为它是一直存在的。Linux必须处理读进程先于写进程打开管道、读进程在写进程写入数据之前读入这两种情况。
(2)FIFO文件的创建
有两种常见的方法:在shell提示符下使用mknod命令或者在程序中使用mknod()系统调用。如:
shell命令行方式:
$ mknod filename p $ mkfifo a = rw filename
这两个命令都可以创建FIFO文件filename。mkfifo提供了直接改变文件读写权限的功能。mknod创建的文件通过chmod可以改变权限。其中,参数p是所建立的节点,即特殊文件的类型为命名管道。
(3)函数调用
mknod()是一般的设备文件创建函数:
# include <sys/type.h> # include <sys/stat.h> int mknod(const char *filename,mode_t mode,dev_t dev)
其中,filename是被创建文件的名称。mode表示将在该文件上设置的权限位和被创建的文件类型;dev是创建设备文件时使用的值。
mkfifo()函数专门用于创建FIFO。
# include <sys/type.h> # include <sys/stat.h> int mkfifo(const char *filename,mode_t mode)
此两个函数均调用成功后返回0,否则返回-1。
FIFO可以被许多进程同时访问,如果多个进程在写一个管道时,在管道可容纳的范围内,系统将保证各进程所写的数据是分开的。因此在多个进程的读写应用中FIFO非常有用。
(4)管道必须有人读和有人写。如果某进程企图往管道中写数据,而没有进程去读该管道,则内核将向该进程发送SIGPIPE信号。这在管道操作中涉及两个以上进程时非常必要。
3.有名管道的打开方式:有名管道比无名管道多了一个打开操作:open
管道提供了一种从进程向另一种进程传输数据的有效方法。但是也有缺陷:
①因为读数据的同时也向另一种进程传输数据,因此管道不能用来广播数据。 ②若管道有多个读进程,则写进程不能发送数据到指定的读进程,同样,有多个写进程时也无法判别式其中的哪一个。
7*.附加案例
Linux下进程之间通信可以用命名管道FIFO完成。命名管道是一种特殊类型的文件,因为Linux中所有事物都是文件,它在文件系统中以文件名的形式存在。
在程序中,我们可以使用两个不同的函数调用来建立管道:
#include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *filename, mode_t mode); int mknode(const char *filename, mode_t mode | S_IFIFO, (dev_t) 0 );
下面先来创建一个管道:
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> int main() { int res = mkfifo("/tmp/my_fifo", 0777); if (res == 0) { printf("FIFO created/n"); } exit(EXIT_SUCCESS); }
编译并运行程序,再用ls命令查看所创建的管道:
注意:ls命令的输出结果中的第一个字符为p,表示这是一个管道。最后的|符号是由ls命令的-F选项添加的,它也表示是这是一个管道。
虽然,我们所设置的文件创建模式为“0777”,但它被用户掩码(umask)设置(022)给改变了,这与普通文件创建是一样的,所以文件的最终模式为755。
打开FIFO一个主要的限制是,程序不能是O_RDWR模式打开FIFO文件进行读写操作,这样做的后果未明确定义。这个限制是有道理的,因为我们使用FIFO只是为了单身传递数据,所以没有必要使用O_RDWR模式。如果一个管道以读/写方式打开FIFO,进程就会从这个管道读回它自己的输出。如果确实需要在程序之间双向传递数据,最好使用一对FIFO,一个方向使用一个。
当一个Linux进程被阻塞时,它并不消耗CPU资源,这种进程的同步方式对CPU而言是非常有效率的。
chapter2 消息队列
1.消息队列就是一个消息的链表。可以把消息看作一个记录,具有特定的格式。进程可以向中按照一定的规则添加新消息;另一些进程则可以从消息队列中读走消息。
2.消息队列这一通信机制将在内核中开辟的用于保存消息的链表作为竞争的共享资源为有通信需求的多进程公用。消息来源为发送方进程,队列中已有的消息将由指定进程接收。
3.基本思想:
①在内存的操作系统空间设置一组消息缓冲区,用于暂存发送的消息;
②当发送进程要向接收进程发送消息时,首先在自己的内存空间设置一个发送区,将要发送的消息填入其中,并填入消息长度以及本进程标识符等信息,然后调用发送原语 send;
③执行send原语将产生异常,自陷系统;
④内核接收控制权后,则为需要发送的消息分配一个空缓冲区,并将要发送的消息从发送进程的发送区拷贝到其中,然后将该缓冲区链接到接收进程的消息(缓冲)队列上,至此完成消息发送过程;
⑤接收进程在本进程的内存空间设置一个接收区,在接收消息时,通过执行接收原语 receive直接从自己的消息(缓冲)队列上取下第一个消息缓冲区,并将其内容复制到接收区,然后释放该消息缓冲区的空间,至此完成消息接收过程。
3.在队列中,可以把消息看作一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息;对消息队列有读权限的进程则可以从消息队列中读取消息。这样一来,发送方进程不必等待接收进程检查所发消息就可以继续工作;而接收方进程如果没有收到消息也不需要等待。
4.与管道相比,消息队列提供了有格式的数据,这可以减少开发人员的工作量。
5.消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。 每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。Linux用宏MSGMAX和MSGMNB来限制一条消息的最大长度和一个队列的最大长度。
int msgget(key_t, key, int msgflg);
int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
struct my_message{ long int message_type; /* The data you wish to transfer*/ };
int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg);
int msgctl(int msgid, int command, struct msgid_ds *buf);
struct msgid_ds { uid_t shm_perm.uid; uid_t shm_perm.gid; mode_t shm_perm.mode; };
#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); }
#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); }
chapter3 共享内存
1.共享内存是被多个进程共享的一部分物理内存。共享内存是进程间共享数据的一种最快的方法,一个进程向共享内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中的内容。
2.共享内存机制允许两个或多个进程共享一个给定的存储区,这一段存储区可以被两个或两个以上的进程映射至自身的地址空间中,数据不需要在进程间复制,所以这是最快的一种IPC,适用于通信数据量较大的场合。
3.使用共享内存时,多个进程之间对已给定的存储区需进程同步访问。即若一个进程正在将数据放入共享存储区,则在它做完这一操作 之前,其他进程不应当去读取这些数据。通常,信号量被用来实现对共 享存储的访问。
4.采用共享内存通信的一个好处是效率高,因为进程可以直接读写 内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式, 则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝 两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出 文件。
5.一般而言,进程之间在共享内存时,并不总是读写少量数据后就解 除映射,有新的通信时再重新建立共享内存区域;而是保持共享区域, 直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写 回文件。共享内存中的内容往往是解除映射时才写回文件的,因此,采 用共享内存的通信方式效率非常高。
6.实现机制:与信号量一样,在Linux中也提供了一组函数接口用于使用共享内存,而且使用共享共存的接口还与信号量的非常相似,而且比使用信号量的接口来得简单。它们声明在头文件 sys/shm.h中。int shmget(key_t key, size_t size, int shmflg);
void *shmat(int shm_id, const void *shm_addr, int shmflg);
int shmdt(const void *shmaddr);
int shmctl(int shm_id, int command, struct shmid_ds *buf);
struct shmid_ds { uid_t shm_perm.uid; uid_t shm_perm.gid; mode_t shm_perm.mode; };
#ifndef _SHMDATA_H_HEADER #define _SHMDATA_H_HEADER #define TEXT_SZ 2048 struct shared_use_st { int written;//作为一个标志,非0:表示可读,0表示可写 char text[TEXT_SZ];//记录写入和读取的文本 }; #endif 源文件shmread.c的源代码如下: #include <unistd.h>
#include <string.h> #include <stdlib.h> #include <stdio.h> #include <sys/shm.h> #include "shmdata.h" int main() { int running = 1;//程序是否继续运行的标志 void *shm = NULL;//分配的共享内存的原始首地址 struct shared_use_st *shared;//指向shm int shmid;//共享内存标识符 //创建共享内存 shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT); if(shmid == -1) { fprintf(stderr, "shmget failed "); exit(EXIT_FAILURE); } //将共享内存连接到当前进程的地址空间 shm = shmat(shmid, 0, 0); if(shm == (void*)-1) { fprintf(stderr, "shmat failed "); exit(EXIT_FAILURE); } printf(" Memory attached at %X ", (int)shm); //设置共享内存 shared = (struct shared_use_st*)shm; shared->written = 0; while(running)//读取共享内存中的数据 { //没有进程向共享内存定数据有数据可读取 if(shared->written != 0) { printf("You wrote: %s", shared->text); sleep(rand() % 3); //读取完数据,设置written使共享内存段可写 shared->written = 0; //输入了end,退出循环(程序) if(strncmp(shared->text, "end", 3) == 0) running = 0; } else//有其他进程在写数据,不能读取数据 sleep(1); } //把共享内存从当前进程中分离 if(shmdt(shm) == -1) { fprintf(stderr, "shmdt failed "); exit(EXIT_FAILURE); } //删除共享内存 if(shmctl(shmid, IPC_RMID, 0) == -1) { fprintf(stderr, "shmctl(IPC_RMID) failed "); exit(EXIT_FAILURE); } exit(EXIT_SUCCESS); }
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <sys/shm.h> #include "shmdata.h" int main() { int running = 1; void *shm = NULL; struct shared_use_st *shared = NULL; char buffer[BUFSIZ + 1];//用于保存输入的文本 int shmid; //创建共享内存 shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT); if(shmid == -1) { fprintf(stderr, "shmget failed "); exit(EXIT_FAILURE); } //将共享内存连接到当前进程的地址空间 shm = shmat(shmid, (void*)0, 0); if(shm == (void*)-1) { fprintf(stderr, "shmat failed "); exit(EXIT_FAILURE); } printf("Memory attached at %X ", (int)shm); //设置共享内存 shared = (struct shared_use_st*)shm; while(running)//向共享内存中写数据 { //数据还没有被读取,则等待数据被读取,不能向共享内存中写入文本 while(shared->written == 1) { sleep(1); printf("Waiting... "); } //向共享内存中写入数据 printf("Enter some text: "); fgets(buffer, BUFSIZ, stdin); strncpy(shared->text, buffer, TEXT_SZ); //写完数据,设置written使共享内存段可读 shared->written = 1; //输入了end,退出循环(程序) if(strncmp(buffer, "end", 3) == 0) running = 0; } //把共享内存从当前进程中分离 if(shmdt(shm) == -1) { fprintf(stderr, "shmdt failed "); exit(EXIT_FAILURE); } sleep(2); exit(EXIT_SUCCESS); }
chapter4 信号量
1.信号量(又名:信号灯)与其他进程间通信方式不大相同,主要用途是保护临界资源。进程可以根据它判定是否能够访问某些共享资源。除了用于访问控制外,还可用于进程同步。
2.类型:①二值信号灯:信号灯的值只能取0或1,类似于互斥锁。 但两者有不同:信号灯强调共享资源,只要共享资源可用,其他进程同样可以修改信号灯的值;互斥锁更强调进程,占用资源的进程使用完资源后,必须由进程本身来解锁。
3。信号量用来结合其他通信方式实现进程通信的同步与互斥。在操 作系统中,信号量代表了一类物理资源,它是相应物理资源的抽象。
4.信号量在创建时需要设置一个初始值,表示同时能有几个进程访 问该信号量保护的共享资源,初始值为 1 就变成互斥锁(Mutex),即同 时只能有一个进程能访问信号量保护的共享资源。
5.一个进程要想访问共享资源,首先必须得到信号量,获取信号量的 操作将把信号量的值减 1,若当前信号量的值为负数,表明无法获得信 号量,该进程必须挂起在该信号量的等待队列,等待该资源可用;若当 前信号量的值为非负数,表示能获得信号量,因而能即时访问被该信号量保护的共享资源。当进程访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加 1 实现,如果信号量的值为非正数,表明有 任务等待当前信号量,因此它也唤醒所有等待该信号量的进程。
6.Linux 中的信号量表现为一个二元组 sem(v,p),其中 v 为信号量 sem 的值,p 为最近一次因申请共享资源而被阻塞的进程的进程号。在 用信号量控制进程互斥或同步时,使用 semge(t )来获取或创建一个信 号量集;通过 semcnt(l )来控制指定的信号量集,可以为其赋初值,进行 各种设置或删除该信号量集;而在使用过程中对信号量值的修改由 semop()来完成,即通过该函数实现资源的获取或释放。
7.由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:
P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行 V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.
int semget(key_t key, int num_sems, int sem_flags);
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
struct sembuf{ short sem_num;//除非使用一组信号量,否则它为0 short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作, //一个是+1,即V(发送信号)操作。 short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号, //并在进程没有释放该信号量而终止时,操作系统释放信号量 };
int semctl(int sem_id, int sem_num, int command, ...);
union semun{ int val; struct semid_ds *buf; unsigned short *arry; };
#include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { char message = 'X'; int i = 0; if(argc > 1) message = argv[1][0]; for(i = 0; i < 10; ++i) { printf("%c", message); fflush(stdout); sleep(rand() % 3); printf("%c", message); fflush(stdout); sleep(rand() % 2); }
sleep(10); printf(" %d - finished ", getpid()); exit(EXIT_SUCCESS); }
chapter5 各种通信方式的比较
1.Linux 环境下通信机制众多,但各种通信方式都有其适用的场合。
管道是Linux支持的最初Unix IPC机制之一,是实现方法最简单的一种通信机制。但是,只能以半双工的形式在进程间进行通信。 信号是多种通信机制中唯一一种异步方式进行通信的机制。信号方式通信传输的数据量较少,侧重于控制进程根据不同的信号触发不同的行为。
消息队列是在内核中开辟的一组链表,以队列的形式接收和发送信息,适用于传输数据量较少的场合。消息队列与管道通信相比,其优势是对每一个消息可以指定特定消息类型,接收的时候不需要按队列 次序,而是可以根据自定义条件接收特定类型的消息。但在消息信息的发送进程—操作系统内核和内核—接收进程间复制时需要额外占用 CPU 的时间。
共享内存通信机制在进程间可以传送大量的数据,并且由于读写进程均将共享内存块视为本进程所有,可直接读写,因此传输速度最快。但由于多个进程对共享内存块的访问必须以互斥形式进行,因此 还需要信号量机制予以配合。
信号量机制通过信号量值的变化来控制多进程互斥的访问共享资源,或者协调多个进程并发执行的节奏,并未在进程之间实际的传输数 据。
各种机制都有其优缺点,在选择进程间通信机制时,程序设计者应根据问题本身的情况来选择合适的方式。
2.比较:
chapter6 研究心得
这次的项目是对进程间通信的研究,信号量的部分在操作系统中大家都已经理解学习了,因此大家一定也都有一定的功底,另外在《深入理解计算机系统》一书的第十二章中也有专门一个小节在介绍这个模块,内容充实详细,代码材料也很全。而管道,在信息安全系统设计基础第一堂课就提过了,虽说只是对简单用法的熟悉,但是对原理也有了解。这次的研究,我们加入了对命名管道的查询,也对FIFO的概念有所普及。我觉得在后面共享内存和消息队列这部分还是理解起来有些难度的。这部分内容有些抽象,概念也比较多。对底层理解比较高,代码示例也很复杂,经过我们小组的资料查找和代码运行理解,有了一些眉目,但是还是需要更多地时间和资料来完善。我们还接触了娄老师所提供的视频进行补充,最后总结出这一片博客。内容很多,也很抽象,需要慢慢地理解和学习。很难用三言两语说清楚。因此,如果对操作系统的学习有较高的要求,可以在这篇博客的框架基础上再多多扩展学习。
chapter7 参考资料
1.http://m.blog.chinaunix.net/uid-26833883-id-3227144.html
2.《Linux高性能服务器编程》
3.http://blog.csdn.net/ljianhui/article/details/10243617
4.http://blog.csdn.net/ljianhui/article/details/10287879
5.各方式详解http://www.cnblogs.com/skyofbitbit/p/3651750.html