|
helper2416_讲解进程间通信_管道PIPE
[复制链接]
对于进程间通信方法有很多种,下面的文字会详细介绍,参考了linux公社上面的一篇文章,做了部分修改和整理,在helper2416上面测试。
一般我在实际应用中上很少用进程间通信,基本上多线程搞定,偶尔涉及到也是用socket方式解决的,系统的学习了一下管道的方式,当然这里对管道的介绍远远不够,之前看过一个服务器的开源项目,里面大量涉及管道的编程,非用于进程间通信。当然下面的介绍也会让你对管道有一个认识,在需要的时候用就行了,选择适合你的。文字中描述的关于ps -l命令的方式,一般编程中可以直接用shell相关的搞定,当然下面的也是一种方式。
一、进程间通信概念
进程是一个程序的一次执行,是系统资源分配的最小单元.
这里所说的进程一般是指运行在用户态的进程,而由于处于用户态的不同进程间是彼此隔离的.
但是它们很可能需要相互发送一些信息,让对方知道自己的数据/进度等,像这样进程间传递信息就叫进程间通信.
二、进程间通信方式
1、管道及有名管道pipe:
管道可用于具有"血缘"关系进程间(也就是父子进程或者兄弟进程)的通信.有名管道除具有管道所具有的功能外,还允许无"血缘"关系进程间的通信.
2、信号signal:
信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生.其实一个进程收到一个信号与处理器收到一个中断请求效果上可以说是类似的.
3、信号量semaphore:
主要作为进程之间及同一进程的不同线程之间的同步和互斥手段.
4、共享内存share memery:
可以说这是最有效的进程间通信方式,它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新,这种通信方式需要依靠某种同步机制,如互此锁和信号量等.
5、消息队列queue:
消息队列是消息的链表,包括Posix消息队列和System V消息队列. 它克服了前两种通信方式中信息量有限的缺点,具有写权限的进程可以按照一定的规则向消息队列中添加消息,对消息队列具有读权限的进程则可以从消息队列中读取消息。
6、套接字socket:
这个绝对是一种更为一般的进程间通信机制,它除了用于本地进程间通信还可用于网络中不同机器之间的进程间通信,应用很广泛.
三、管道简介
管道是linux中进程间通信的一种方式,它把一个程序的输出直接连接到另一个程序的输入.linux的管道主要包括两种:无名管道和有名管道.
1、无名管道
无名管道是Linux中管道通信的一种原始方法:
1)只能用于具有亲缘关系的进程之间的通信(也就是父子进程或者兄弟进程之间).
2)是一个半双工的通信模式,具有固定的读端和写端.
3)管道也可以看成是一种特殊的文件,对于它的读写也可以使用普通的read()、write()函数.但它不是普通的文件,并不属于其他任何文件系统并且只存在于内存中.
2、有名管道
有名管道是对无名管道的一种改进:
1)可以使互不相关的两个进程间实现彼此通信.
2)可以通过路径名来指出,并且在文件系统中是可见的.在建立了管道之后,两个进程就可以把它当做普通文件一样进行读写操作,使用非常方便.
3)FIFO严格地遵循先进先出规则,对管道及FIFO的读总是从开始处返回数据,对它们的写则是把数据添加到末尾,它们不支持如lseek()等文件定位操作.
四、无名管道
1、管道创建与管道说明
管道是基于文件描述符的通信方式,当一个管道建立时,它会创建两个文件描述符fd[0]和fd[1],其中fd[0]固定用于读管道,而fd[1]固定用于写管道.注意管道是一个半双工的通道.
管道关闭时只需要将这两个文件描述符关闭即可,可使用普通的close()函数逐个关闭各个文件描述符.
2、管道创建函数
创建管道可以调用pipe()来实现
3、管道读写说明
用pipe()创建的管道两端处于同一个进程中,由于管道主要是用于在不同的进程间通信的.因此,在实际应用中没有太大意义.
实际上,通常先是创建一个管道,再调用fork()函数创建一个子进程,该子进程会继承父进程所创建的管道.
父子进程分别拥有自己的读写通道,为了实现父子进程之间的读写,只需把无关的读端或写端的文件描述符关闭即可.
将父进程的写端fd[1]和子进程的读端fd[0]关闭,则父子进程之间就建立起一条"子进程写入父进程读取"的通道.
将父进程的读端fd[0]和子进程的写端fd[1]关闭,则父子进程之间就建立起一条"父进程写入子进程读取"的通道.
另外,父进程还可以创建多个子进程,各个子进程都继承了相应的fd[0]和fd[1].此时,只需要关闭相应的端口就可以建立各子进程之间的的通道.
4、管道读写注意点
1)只有在管道的读端存在时,向管道写入数据才有意义,否则向管道写入数据的进程将收到内核传来的SIGPIPE信号.
2)向管道写入数据时,linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据.如果读进程不读取管道缓冲区中的数据,那么写进程将会一直阻塞.
5、测试程序
首先创建管道,之后父进程使用fork()函数创建子进程,
最后通过关闭父进程的读描述符fd[0]和子进程的写描述符fd[1]来建立一个"父进程写入子进程读取"的管道,从而建立起它们之间的通信.
- int main(int argc, char **argv)
- {
- pid_t pid;
- int pipe_fd[2];
- char rxBuf[RX_BUF_MAX_LEN];
- char pipeMsg[]="__ICP_PIPE";
- /* 创建管道 */
- if(pipe(pipe_fd)<0)
- {
- printf("Pipe create faliure\n");
- exit(1);
- }
-
- pid = fork();
-
- if ( pid>0 ) {
- close(pipe_fd[0]);
-
- /* 父进程写入管道内容 */
- if ( write(pipe_fd[1], pipeMsg, strlen(pipeMsg)) != -1 ) {
- printf("PIPE_WRITE:'%s'\n", pipeMsg);
- }
- close(pipe_fd[1]);
- waitpid(pid, NULL, 0);
- exit(0);
- } else if( pid==0 ) {
- close(pipe_fd[1]);
- memset( (void*)rxBuf, 0, sizeof(rxBuf) );
-
- /* 子进程读取管道内容 */
- if ( read(pipe_fd[0], rxBuf, RX_BUF_MAX_LEN) > 0 ) {
- printf("PIPE_READ: '%s'\n", rxBuf);
- }
- close(pipe_fd[0]);
- exit(0);
- }
-
- return 1;
- }
复制代码
编译并执行
- # arm-linux-gcc pipe.c -o pipe
- # ./pipe
复制代码
五、标准流管道
标准流管道函数与linux的文件操作中有基于文件流的标准I/O操作一样,管道的操作也支持基于文件流的的模式.
这种基于文件流的管道主要是用来创建一个连接到另一个进程的管道,这里的"另一个进程"也就是一个可以进行一定操作的可执行文件.
例如,用户执行"ps -l"或者自己编写的程序"./pipe". 由于这类操作很常用,因此标准流管道就将一系列的创建过程合并到一个函数popen()中完成,它所完成的工作有以下几步:
1)创建一个管道
2)fork()创建一个子进程
3)在父子进程中关闭不需要的文件描述符
4)执行exec函数族调用
5)执行函数中所指定的命令
这个函数的使用可以大大减少代码的编写量,但同时也有一些不利之处.
例如,它不如前面管道创建的函数那样灵活多变,并且用popen()创建的管道必须使用标准I/O函数进行操作,而不能使用前面的read()、write()一类不带缓冲的I/O函数.
与之相对应,关闭用popen()创建的流管道必须使用函数pclose(),该函数关闭标准I/O流,并等待命令执行结束.
- int main(int argc, char **argv)
- {
- FILE *fp;
- char rxBuf[RX_BUF_MAX_LEN];
- char cmd[]="ps -l";
- if ( (fp=popen(cmd, "r")) == NULL ) {
- printf("popen failure\n");
- exit(1);
- }
-
- while( (fgets(rxBuf, RX_BUF_MAX_LEN, fp)) != NULL ) {
- printf("%s", rxBuf);
- }
- pclose(fp);
-
- exit(0);
- return 1;
- }
复制代码
编译并执行
- # arm-linux-gcc popen.c -o popen
- # ./popen
复制代码
六、有名管道
有名管道的创建可以使用函数mkfifo(),该函数类似与文件中的open()操作,可以指定管道的路径和打开的模式.还可以在命令行使用"mknod pipename p"来创建有名管道.
在管道创建成功后,就可以使用open()、write()和read()这些函数了.
与普通文件的开发设置一样,对于为读而打开的管道可在open()中设置O_RDONLY,对于为写而打开的管道可在open()中设置O_WRONLY,在这里与普通文件不同的是阻塞问题.
由于普通文件在读写时不会出现阻塞问题,而在管道的读写中却有阻塞的可能,这里的非阻塞标志可以在open()函数中设定为O_NONBLOCK.
对于读进程:
1)若该管道是阻塞打开,且当前FIFO内没有数据,则对读进程而言将一直阻塞到有数据写入.
2)若该管道是非阻塞打开,则不论FIFO内是否有数据,读进程都会立即执行读操作.也就是如果FIFO内没有数据,则读函数将立刻返回0.
对于写进程:
1)若该管道是阻塞打开,则写操作将一直阻塞到数据可以被写入.
2)若该管道是非阻塞打开而不能写入全部数据,则读操作进行部分写入或者调用失败.
测试程序:
在读管道的程序中创建管道,在写管道程序中写入的内容作为main()函数的参数传入.
读管道的程序会读出用户写入到管道的内容,这两个程序采用的是阻塞式读写管道模式,注意测试时先运行读在运行写.
1)fifo_read.c用于读管道
- /*有名管道文件名*/
- #define ICP_FIFO "/root/icp_fifo"
- /* 在limits.h中有 #define PIPE_BUF 4096 即4个字节大小 */
- #define PIPE_BUF_MAX_LEN PIPE_BUF
- int main(int argc, char **argv)
- {
- char rxBuf[PIPE_BUF_MAX_LEN];
- int fd;
- /* 判断有名管道是否已经存在,若尚未创建则以相应的权限创建 */
- if ( access(ICP_FIFO, F_OK) == -1 ) {
- if ( (mkfifo(ICP_FIFO, 0666)<0) && (errno!=EEXIST) ) {
- printf("Can not create fifo file\n");
- exit(1);
- }
- }
- /* 以只读阻塞方式打开有名管道 */
- fd = open(ICP_FIFO, O_RDONLY);
- if (fd == -1) {
- printf("Open fifo file error\n");
- exit(1);
- }
-
- while (1) {
- memset(rxBuf, 0, sizeof(rxBuf));
- if( read(fd, rxBuf, PIPE_BUF_MAX_LEN) > 0 ) {
- printf("READ FIFO:\n%s\n\n", rxBuf);
- }
- }
- close(fd);
- exit(0);
- }
复制代码
编译并执行
- # arm-linux-gcc fifo_read.c -o fifo_read
- # ./fifo_read
复制代码
2)fifo_write.c用于写管道
- /* 有名管道文件名 */
- #define ICP_FIFO "/root/icp_fifo"
- /* 在limits.h中有 #define PIPE_BUF 4096 即4个字节大小 */
- #define PIPE_BUF_MAX_LEN PIPE_BUF
- int main(int argc, char *argv[])
- {
- int fd;
- char rxBuf[PIPE_BUF_MAX_LEN];
- if (argc <= 1) {
- printf("./fifo_write string\n");
- exit(1);
- }
- sscanf(argv[1], "%s", rxBuf);
-
- /* 以只写阻塞方式打开FIFO管道 */
- fd = open(ICP_FIFO, O_WRONLY);
- if (fd == -1) {
- printf("Open fifo file failure\n");
- exit(1);
- }
-
- /* 向管道中写入字符串 */
- if( write(fd, rxBuf, PIPE_BUF_MAX_LEN) > 0 ) {
- printf("WRITE FIFO:\n%s\n\n", rxBuf);
- }
- close(fd);
- exit(0);
- }
复制代码
编译并执行
- # arm-linux-gcc fifo_read.c -o fifo_write
- # ./fifo_write
复制代码
好了,大家自己体会吧,附件中有程序,便于测试使用。
ipc_FIFO.zip
(12.94 KB, 下载次数: 0, 售价: 1 分芯积分)
|
|