Linux 进程间的通信(一)—管道通信(有名管道和无名管道)
Linux 下进程间通信概述
进程间通信就是在不同进程之间传播或交换信息,那么不同进程之间存在着什么双方都可以访问的介质呢?每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)
进程间的通信主要包括:
1.管道及有名管道:管道可用于具有亲缘关系进程间的通信,有名管道:name_pipe, 除了管道的功能外,还可以在许多并不相关的进程之间进行通讯。
2.信号(Signal):信号是比较复杂的通信方式,用于通知接收进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;Linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix标准的信号函数sigaction。
3.报文(Message)队列(消息队列):消息队列是消息的链接表,包括Posix消息队列systemV消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
4.共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
5.信号量:主要作为进程间以及同一进程不同线程之间的同步手段。
6.套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到Linux上。
管道通信
管道就是一种连接一个进程的标准输出到另一个进程的标准输入的方法。管道是最古老
的IPC工具,从UNIX系统一开始就存在。它提供了一种进程之间单向的通信方法。管道在系统中的应用很广泛,即使在shell环境中也要经常使用管道技术。管道通信分为管道和有名管道
,管道可用于具有亲缘关系进程间
的通信,有名管道
, 除了管道的功能外,还可以在许多并不相关的进程
之间进行通讯。
管道 (匿名管道)
管道是一种“无名”、“无形”的文件
当进程创建一个管道时,系统内核设置了两个管道可以使用的文件描述符。一个用于向管道中输入信息(write),另一个用于管道中获取信息(read)。
管道的特点:
·管道时半双工的
,数据只能向一个方向流动。双方通信时,需要建立起两个管道。
·只能用于父子进程或则兄弟进程之间(具有亲缘关系的进程
)
·单独构成一种独立的文件系统
:管道对于管道两端的进程而言,就是一个文件,对于它的读写也可以使用普通的read、write等函数。但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在于内存中
。
·数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
管道的创建
管道是基于文件描述符
的通信方式,当一个管道建立时,它会创建两个文件描述符fd[0]和fd[1],其中fd[0]固定用于读管道
,而fd[1]固定用于写管道
,无名管道的建立比较简单,可以使用pipe()
函数来实现。其函数原型如下:
#include <unistd.h>
int pipe(int fd[2])
说明:参数fd[2]表示管道的两个文件描述符,之后就可以直接操作这两个文件描述符;函数调用成功则返回0,失败返回−1。
管道的关闭
使用pipe()函数创建了一个管道,那么就相当于给文件描述符fd[0]和fd[1]赋值,之后我们对管道的控制就像对文件的操作一样,那么我们就可以使用close()
函数来关闭文件,关闭了fd[0]和fd[1]就关闭了管道。
管道的读/写操作
fd[0] 称为管道读端,fd[1] 称为管道写端。管道的两端是固定了任务的,即一端只能用于读,另一端只能用于写。
pipe.c
/* ************************************************************************
* Filename: pipe.c
* Description:
* Version: 1.0
* Created: 05/05/2020 10:38:37 PM
* Revision: none
* Compiler: gcc
* Author: YOUR NAME (WCT)
* Company:
* ************************************************************************/
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUFFER 80
int main(){
int fd[2], nbytes;
pid_t childpid;
char string[] = "Hello, PIC!\n";
char readbuffer[BUFFER];
if( pipe(fd) < 0 ){ // 创建管道
printf("创建失败!\n");
return -1;
}
if( ( childpid = fork() ) == -1 ){ // 创建一个子进程
perror("fork\n");
exit(1);
}
if( childpid == 0 ){ // 子进程
close(fd[0]); // 子进程关闭读取端
sleep(3); // 暂停确保父进程已关闭相应的写描述符
write( fd[1], string, strlen(string)); // 通过写端发送字符串
close(fd[1]); // 关闭子进程写描述符
exit(0);
}
else{
close(fd[1]); // 父进程关闭写端
memset(readbuffer, 0, sizeof(readbuffer)); // 清空缓冲区防止乱码
nbytes = read( fd[0], readbuffer, sizeof(readbuffer)); // 从管道中读取字符串
printf("Received string : %s\n", readbuffer);
close(fd[0]); // 关闭父进程读描述符
}
return 0;
}
编译运行
出现乱码的情况是字符串没有结束标志,可以在末尾加’\0’,也可以接受前将缓冲区清零(memset(readbuffer, 0, sizeof(readbuffer));
)。
标准流管道
如果你认为上面创建和使用管道的方法过于繁琐的话,你也可以使用下面的简单的方法:
库函数:popen();
原型: FILE *popen ( char *command, char *type);
返回值:如果成功,返回一个新的文件流。如果无法创建进程或者管道,返回 NULL。管道中数据流的方向是由第二个参数type控制的。此参数可以使r(读) 或者 w(写),不能同时读写。Linux 下,管道将会以type 中的第一个字符代表的方式打开。
使用popen()创建的管道必须使用pclose()关闭。其实, popen/pclose和标准文件输入/输出流中的fopen()/fclose()十分相似。
库函数: pclose();
原型: int pclose( FILE *stream );
返回值: 返回popen中执行命令的终止状态 。如果stream无效,或者系统调用失败,则返回-1。
popen函数其实是对管道操作的一些包装,所完成的工作有以下几步:
创建一个管道
fork 一个子进程
在父子进程中关闭不需要的文件描述符
执行 exec 函数族调用
执行函数中所指定的命令
popen.c
/* ************************************************************************
* Filename: popen.c
* Description:
* Version: 1.0
* Created: 05/05/2020 11:33:18 PM
* Revision: none
* Compiler: gcc
* Author: YOUR NAME (WCT),
* Company:
* ************************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define MAXSTRS 5
int main(void){
int cntr;
FILE *pipe_fp;
char *strings[MAXSTRS] = { "echo", "bravo", "alpha","charlie", "delta"};
if (( pipe_fp = popen("sort", "w")) == NULL) /*调用popen创建管道 */
{
perror("popen");
exit(1);
}
for(cntr=0; cntr<MAXSTRS; cntr++) /* 循环处理 */
{
fputs(strings[cntr], pipe_fp);
fputc('\n', pipe_fp);
}
pclose(pipe_fp); /* 关闭管道 */
return(0);
}
编译运行
popen2.c
/* ************************************************************************
* Filename: popen2.c
* Description:
* Version: 1.0
* Created: 05/05/2020 11:33:18 PM
* Revision: none
* Compiler: gcc
* Author: YOUR NAME (WCT),
* Company:
* ************************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define BUFSIZE 200
int main(void){
FILE *pipe_fp;
char buf[BUFSIZE] = { 0 };
if (( pipe_fp = popen("cat 1.txt", "r")) == NULL) /*调用popen创建管道 */
{
perror("popen");
exit(1);
}
while( fgets( buf, BUFSIZE, pipe_fp) != NULL) /* 循环处理 */
{
fputs(buf, stdout);
memset(buf, 0, BUFSIZE);
}
pclose(pipe_fp); /* 关闭管道 */
return(0);
}
编译运行结果
有名管道
有名管道是一种“有名”、“有形”的文件
有名管道,即FIFO管道和一般的管道基本相同,但也有一些显著的不同:
FIFO管道不是临时对象,而是在文件系统中作为一个特殊的设备文件而存在的实体。并且可以通过mkfifo命令来创建。进程只要拥有适当的权限就可以自由的使用FIFO管道
不同祖先的进程之间可以通过管道共享数据
当共享管道的进程执行完所有的 I/O操作以后,命名管道将继续保存在文件系统中以便以后使用
FIFO严格地遵循先进先出规则,对管道及FIFO的读总是从开始处返回数据,对它们的写则是把数据添加到末尾,它们不支持如 lseek()等文件定位操作。命名管道一旦建立,之后它的读、写以及关闭操作都与无名管道完全相同。
·虽然与命名管道对应的FIFO文件inode节点是建立在文件系统中,但是仅是一个节点而已,文件的数据还是存在于内存缓冲页面中,这一点和无名管道相同。
有名管道可以用于任何两个程序间通信,因为有名字可引用。注意管道都是单向的,因此双方通信需要两个管道。
FIFO 的创建
mknod MYFIFO p // 只能通过chmod 修改权限
mkfifo -m a=rw MYFIFO1 // 提供FIFO文件存取的途径
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo( const char *pathname, mode_t mode );
mkfifo函数需要两个参数,第一个参数(pathname)是将要在文件系统中创建的一个专用文件。第二个参数(mode)用来规定FIFO的读写权限。Mkfifo函数如果调用成功的话,返回值为0;如果调用失败返回值为-1。
实例:
fifowrite.c
/* ************************************************************************
* Filename: fifowrite.c
* Description:
* Version: 1.0
* Created: 05/06/2020 03:41:26 AM
* Revision: none
* Compiler: gcc
* Author: YOUR NAME (WCT),
* Company:
* ************************************************************************/
#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include<errno.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#define BUFSIZE 256
int main()
{
char write_fifo_name[] = "lucy";
char read_fifo_name[] = "peter";
int write_fd, read_fd;
char buf[BUFSIZE];
// 创建管道 可读可写
int ret = mkfifo(write_fifo_name, S_IRUSR | S_IWUSR);
printf("%s create!\n", write_fifo_name);
if( ret == -1){
printf("Fail to create FIFO %s: %s",write_fifo_name,strerror(errno)); exit(-1);
return -1;
}
printf("open write %s\n", write_fifo_name);
write_fd = open(write_fifo_name, O_WRONLY); // 写的方式打开
printf("open write %s success!\n", write_fifo_name);
if(write_fd == -1) {
printf("Fail to open FIFO %s: %s",write_fifo_name,strerror(errno));
exit(-1);
}
printf("wait open read %s...\n", read_fifo_name);
while((read_fd = open(read_fifo_name,O_RDONLY)) == -1){
sleep(1); // 等待创建完成
}
printf("wait open read %s success!\n", read_fifo_name);
while(1) {
printf("Lucy: ");
memset(buf, 0, BUFSIZE); // 缓冲区清零
fgets(buf, BUFSIZE, stdin);
buf[strlen(buf)-1] = '\0';
if(strncmp(buf,"quit", 4) == 0)
{
close(write_fd); // 关闭写端
unlink(write_fifo_name); // 删除管道
close(read_fd); // 关闭读端
exit(0);
}
write(write_fd, buf, strlen(buf));
memset(buf, 0, BUFSIZE); // 缓冲区清零
printf("wait %s message!\n", read_fifo_name);
if( read(read_fd, buf, BUFSIZE) > 0 ){
// buf[len] = '\0';
printf("Peter: %s\n", buf);
}
}
}
fiforead.c
/* ************************************************************************
* Filename: fiforead.c
* Description:
* Version: 1.0
* Created: 05/06/2020 04:43:51 AM
* Revision: none
* Compiler: gcc
* Author: YOUR NAME (WCT),
* Company:
* ************************************************************************/
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
#include<stdio.h>
#include<errno.h>
#include<fcntl.h>
#include<stdlib.h>
#define BUFSIZE 256
int main(void)
{
char write_fifo_name[] = "peter";
char read_fifo_name[] = "lucy";
int write_fd, read_fd;
char buf[BUFSIZE];
// 创建管道 可读可写
int ret = mkfifo(write_fifo_name, S_IRUSR | S_IWUSR);
printf("%s create!\n", write_fifo_name);
//sleep(3); // 用于观察管道操作流程
if( ret == -1) // 如果创建失败
{
printf("Fail to create FIFO %s: %s",write_fifo_name,strerror(errno));
exit(-1);
}
// 等待 read_fifo_name 创建完成
printf("wait open read %s...\n", read_fifo_name);
while((read_fd = open(read_fifo_name, O_RDONLY)) == -1)
{
sleep(1);
}
printf("wait open read %s success!\n", read_fifo_name);
// 写的方式打开 write_fifo_name
sleep(3); // 用于观察管道操作流程
printf("open write %s\n", write_fifo_name);
write_fd = open(write_fifo_name, O_WRONLY);
printf("open write %s success!\n", write_fifo_name);
if(write_fd == -1)
{
printf("Fail to open FIFO %s: %s", write_fifo_name, strerror(errno));
exit(-1);
}
while(1)
{
memset(buf, 0, BUFSIZE); // buf 清零
printf("wait %s message!\n", read_fifo_name);
if( read(read_fd, buf, BUFSIZE) > 0)
{
printf("Lucy: %s\n",buf);
}
printf("Peter: ");
fgets(buf, BUFSIZE, stdin);
buf[strlen(buf)-1] = '\0';
if(strncmp(buf,"quit", 4) == 0)
{
close(write_fd);
unlink(write_fifo_name); // 删除管道
close(read_fd);
exit(0);
}
write(write_fd, buf, strlen(buf));
}
}
测试结果
测试结果表明其中的 open(xxx_fifo_name, O_RDONLY) 函数会阻塞,直到确定了 xxx_fifo_name 的两个方向,即读方向和写方向。只有正确的指定了 xxx_fifo_name 管道的方向后 open 函数方可正常执行退出(当然了如果 xxx_fifo_name 两个方向指定后,其他进程的操作同一个 xxx_fifo_name 时open 函数就不会阻塞了
)。对管道的读写操作,当读管道时他会将此时管道中的所有数据读出。当管道中没有数据时 read 会阻塞,直到管道写入数据。
如果将上述 fiforead.c 中的
while((read_fd = open(read_fifo_name, O_RDONLY)) == -1)
改为:
while((read_fd = open(read_fifo_name, O_WRONLY)) == -1)
会出现open函数永久阻塞的局面,出现这个局面主要是只对管道指定一个方向,没有正确指明两个方向。结果如下图: