进程间通信:
所谓进程间通信,就是让不同的进程之间可以传播或交换信息,但是不同进程的内存空间是相互独立的(进程的
独占性),一般而言是不能相互访问的,所以多个不同进程间要通信,就必须存在一种访问介质。而这种访问介质,
就是内核下的缓冲区,缓冲区是系统下的“公共场所”,各进程都可以访问,所以当一个进程从用户空间向内核缓冲
区中写入数据,另一个进程则去内核缓冲区中读取数据,这样就实现了不同进程间的通信。内核提供的这种机制称为
进程间通信(IPC InterProcess Communication)。
当然,缓冲区并不是唯一的介质,比如双方都能访问的外设,磁盘上的普通文件,通过“注册表”或其他数据库
的某些表项和记录这些同样能实现数据的交换。广义上说这些都是进程间通信的手段,但是却不把这些称为“进程间
通信”。
一、进程间通信(IPC:Interprocess communication):
进程间通信是一组编程接口,让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、
交换信息。这使得一个程序能够在同一时间里处理许多用户的要求。因为即使只有一个用户发出要求,也可能导致一
个操作系统中多个进程的运行,进程之间必须互相通话。IPC接口就提供了这种可能性。每个IPC方法均有它自己的优
点和局限性,一般,对于单个程序而言使用所有的IPC方法是不常见的。
IPC方法包括管道(PIPE)、消息排队、旗语、共用内存以及套接字(Socket)。进程间通信主要包括了管道、
系统IPC(包括了消息队列、信号以及共享存储)、套接字(SOCKET)。
★这里只讨论管道的相关概念:
管道是一种最基本的 IPC机制,由pipe函数创建:头文件: #include <unistd.h> 函数名:int pipe(int file
des[2]); 调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端,一个写端。然后通 过filedes
参数传出给用户程序两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道的 写端(很好记,就像0是标准输入
1是标准输出一样)。所以管道在用户程序看起来就像一个打开的文件,通过read(filedes[0]);或write(filedes[1]);向这
个文件读写数据其实是在读写内核缓冲区。pipe函数调用成功返回0,调用失败返回-1。
★管道的分类:
①普通管道(PIPE):通常有两种限制,一是单工,即只能单向传输;二是血缘,即常用于父子进程间(或有血缘关
系的进程间)。
②流管道(s_pipe):去除了上述的第一种限制,实现了双向传输。
③命名管道(name_pipe):去除了上述的第二种限制,实现了无血缘关系的不同进程间通信。
▲文件描述符:内核(kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数。打开现存文
件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。在UNIX、
Linux的系统调用中,大量的系统调用都是依赖于文件描述符。
★管道与文件描述符以及文件指针间的关系:
其实管道的使用方法与文件类似,都能使用read,write,open等普通IO函数。管道描述符类似于文件描述符. 事实
上, 管道使用的描述符,文件指针和文件描述符最终都会转化成系统中SOCKET描述符, 都受到系统内核中SOCKET
描述符的限制。本质上LINUX内核源码中管道是通过空文件来实现。
★管道的使用方法:
①pipe:创建一个管道,返回两个管道描述符,通常用于父子进程间的通讯。
②popen,pclose:只返回一个管道描述符,这种方式常用于通信的另一方是stdin或stdout。
③mkpipe:命名管道,在多进程间实现交互。
※开辟管道后可按以下步骤实现两个进程间的通信:
⑴父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端。
⑵父进程调用fork创建子进程,此时子进程也有两个文件描述符指向同一管道。
⑶父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,管道是用循环队列
实现的,数据从写端流入,从读端流出,这样就实现了进程间通信。
相关代码:
★此处的进程间通信是单向通信,即便父进程不关闭写端,子进程不关闭读端,双方都有读端和写端,也无法实
现双向通信?
原因是:管道的读写通过打开的文件描述符来传递,因此要通信的两个进程必须从他们的父进程那里继承管道文件描
述符。上述代码是父进程将文件描述符传给子进程后,父子进程间的通信。当然也可以父进程fork两次,将文件描述
符传给fork出的两个子进程,然后让两个子进程间通信。总之,继承的文件描述符必须能够访问同一管道,这样才能
通信。也就是说,管道通信是需要进程间有关系的。
★使用管道时,须注意以下四种情况:
①如果所有指向管道写端的文件描述符都关闭了,而仍然有进程从管道的读端读取数据,那么管道中剩余的数据被取
完后,在此read后会返回0,因为再无数据可读。
②如果有指向管道写端的文件描述符没关闭,而持有管道写端的进程也没有向管道中写入数据,此时有进程从管道读
端读取数据,那么管道中剩余的数据都被读取后,再次read会发生阻塞,直到管道中有数据可读时才读取数据后返
回。
③如果所有指向管道读端的文件描述符都关闭了,而仍然有进程向管道的写端写入数据,那么该进程会收到SIGPIPE
信号,导致进程的异常终止。
④如果有指向管道读端的文件描述符没关闭,而持有管道读端的进程也没有从管道中读取数据,此时有进程向管道写
端写入数据,那么管道被写满时再次write会发生阻塞,直到管道中存放数据的空位时才写入数据并返回。
★命名管道:
管道的一个不足之处在于管道没有一个确切的名字,所以普通管道只能用于具有血缘关系间的进程间进行通信,
而对于无血缘关系的多个进程间要实现进程间通信,普通管道已无法满足,所以促使了命名管道(name_pipe或
FIFO)的提出,而普通管道(pipe)也就被称为匿名管道。命名管道的不同之处在于其提供了一个路径名与之关
联,以FIFO的文件形式存储于文件系统中。命名管道是一个设备文件,即使多个进程不存在血缘关系,也可以访问该
路径,通过FIFO实现进程间通信。FIFO与调度算法中的FIFO同名,都是first input first output。所以按照先进先
出的原则,第一个被写入的数据首先从管道中读出。
▲命名管道的创建:
linux下命名管道的创建方式有两种:
①在Shell下交互地建立一个命名管道。Shell方式下可使用mknod或mkfifo命令。
②在程序中使用系统函数建立命名管道。相关系统函数有mknod和mkfifo。
1.mknod系统函数:
2.mkfifo系统函数:
注:可以看到在mknod和mkfifo系统函数的声明中,函数参数中的path为创建命名管道的全路径名,mode为创建管
道的模式,指明其存取权限;dev为设备值,该值取决于创建文件的种类,只在创建设备文件时才会用到。两个函数
调用成功都返回0,失败时都返回-1。
★ mknod和mkfifo这两个函数都能创建一个FIFO文件,注意是创建一个真实存在于文件系统中的文件,filename指定了文件名,而mode则指定了文件的读写权限。mknod是比较老的函数,而使用mkfifo函数更加简单
和规范,所以建议在可能的情况下,尽量使用mkfifo而不是mknod。 mkfifo函数的作用是在文件系统中创建一个文
件,该文件用于提供FIFO功能,即命名管道。 前边讲的那些管道都没有名字,因此它们被称为匿名管道,或简称管
道。对文件系统来说, 匿名管道是不可见的,它的作用仅限于在父进程和子进程两个进程间进行通信。而命名管道是
一个可见的文件,因此,它可以用于任何两个进程之间的通信,不管这两个进程是不 是父子进程,也不管这两个进程
之间有没有关系。
fifo read端:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #define _PATH_ "/tmp/file.tmp" #define _SIZE_ 100 int main() { int fd = open(_PATH_, O_RDONLY); if(fd < 0){ printf("open file error!\n"); return 1; } char buf[_SIZE_]; memset(buf, '\0', sizeof(buf)); while(1){ int ret = read(fd, buf, sizeof(buf)); if (ret <= 0)//error or end of file { printf("read end or error!\n"); break; } printf("%s\n", buf); if( strncmp(buf, "quit", 4) == 0 ) { break; } } close(fd); return 0; }
fifo write端:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <string.h> #include <fcntl.h> #define _PATH_ "/tmp/file.tmp" #define _SIZE_ 100 int main() { int ret = mkfifo(_PATH_,0666|S_IFIFO); if(ret == -1){ printf("mkfifo error\n"); return 1; } int fd = open(_PATH_, O_WRONLY); if(fd < 0){ printf("open error\n"); } char buf[_SIZE_]; memset(buf, '\0', sizeof(buf)); while(1){ scanf("%s", buf); int ret = write(fd, buf, strlen(buf)+1); if(ret < 0){ printf("write error\n"); break; } if( strncmp(buf, "quit", 4) == 0 ) { break; } } close(fd); return 0; }
注: fifo write端中的“S_IFIFO|0666”指明创建一个命名管道且存取权限为0666,即为创建者、与创建者同组的用
户、其他用户对该命名管道的访问权限都是读和写(这里要注意umask对生成的管道文件权限的影响)。
▲ 命名管道创建后就可以使用了,其使用方法和管道基本是相同的。只是使用命名管道时,必须先调用open()函数
将其打开。因为命名管道是设备文件,存储于硬盘上的文件,而管道则是存储于内存中的特殊文件。需要注意的是,
调用open()打开命名管道的进程可能会被阻塞,但如果同时用读写方式 (O_RDWR)打开,则一定不会导致阻塞;
如果以只读方式(O_RDONLY)打开,则调 用open()函数的进程将会被阻塞直到有写入数据的一方打开管道;同样
以写方式(O_WRONLY)打开 也会阻塞直到有读取数据的一方打开管道。
流管道:
与linux的文件操作中有基于标准I/O操作一样,管道操作也支持基于文件流的操作,这种基于文件流的管道主要
用来创建一个连接到另一个进程的管道,这里“另一个进程”是可以执行一定操作的可执行文件。例如用户执行“ls
-l”或者./pipe,由于这类操作很常见,所以将一系列创建过程合并到一个函数popen()中完成,而popen()函数会完
成以下的步骤:①创建一个管道;②fork()一个子进程;③在父子进程中关闭不需要的文件描述符;④执行exec()函
数族调用;⑤执行函数中指定的命令。
由此可见,这个函数可以大大减少代码的编写量,但使用不太灵活,不能自己创建管道那么灵活,并且popen()
必须使用标准的I/o函数进行操作,也不能使用read(),wirte()这种不带缓冲的I/O函数,必须使用pclose()来关闭管
道流,该函数关闭标准I/O流,并等待命令执行结束。
注:可以看到,popen()函数的参数中command指向的是一个以null结束符结尾的字符串,这个字符串包含一个shell命令。参数
type中有两部分,即“w”和“r”。“w”表示文件指针连接到command的标准输入,即该命令的结果产生输入;“r”表示文件
指针连接到command的标准输出,即该命令的结果产生输出。成功时返回文件流指针,失败时返回null。pclose()函数的参数只有
一个stream,即要关闭的文件流。成功时返回popen()函数执行的退出码,失败时返回-1。
linux下管道的实现机制及容量:
管道频繁地被用于进程间的通信当中,从本质上来看管道其实就是一种特殊文件。其特殊在管道可以克服使用文
件进行通信时存在的两个问题:①限制管道的大小。实际上,管道是一个固定大小的缓冲区。Linux中该缓冲区的大
小为1页,即4K字节,使得它的大小不像文件那样不加检验地增长。使用单个固定缓冲区也会带来问题,比如在写管
道时可能变满,当这种情况发生时,随后对管道的write()调用将默认地被阻塞,等待某些数据被读取,以便腾出足够
的空间供write()调用写。②读取进程也可能工作得比写进程快。当所有当前进程数据已被读取时,管道变空。当这种
情况发生时,一个随后的read()调用将默认地被阻塞,等待某些数据被写入,这解决了read()调用返回文件结束的问
题。
▲注:从管道读数据是一次性操作,数据一旦被读,它就从管道中被抛弃,释放空间以便写更多的数据。
★ 在Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个file结构指向同一个临时的VFS索引节点,而这个VFS 索引节点又指向一个物理页面而实现的。
管道的架构示意图
关于linux管道结构的具体实现代码可以点击此处链接进行学习: ★★★★★http://blog.chinaunix.net/uid-20498361-id-1940274.html★★★★★
★关于管道的容量:
注:由结果输出可知管道的容量大小为64K,即1024*64=65536。
总的来说管道的特点有以下几点特性:
①单向数据通信;
②管道常用于父子进程间(有血缘关系的进程)---------匿名管道,无血缘关系的进程间通信为命名管道;
③提供一种流式服务(发送和接受不受字节数的限制,可取任意大小,与数据块和数据报区别);
④管道的生命周期是随进程的,进程退出,管道消失;
⑤提供同步与互斥功能,保证数据传输的正确性。