目录
- 无名管道
- 关闭未使用的管道文件描述符
- 管道对应的内存大小
- 与shell命令进行通信(popen)
- 命名管道FIFO
- 创建FIFO文件
- 打开FIFO文件
无名管道
管道是最早出现的进程间通信的手段。
管道的作用是在有亲缘关系的进程之间传递消息。所谓有亲缘关系,是指有一个共同的祖先。所以管道并非只能用于父子进程之间,也可以用在兄弟进程之间,还可以用于祖孙进程之间甚至是叔侄进程之间。
管道的本质是内核维护了一块缓冲区与管道文件相关联,对管道文件的操作,被内核转换成对这块缓冲区内存的操作。
在Linux下,可以使用如下接口创建管道:
#include <unistd.h>
int pipe(int pipefd[2]);
如果成功,则返回值是0,如果失败,则返回值是-1,并且设置errno。
errno | 原因 |
---|---|
EMFILE | 该进程使用的文件描述符已经多于MAX_OPEN-2 |
ENFILE | 系统中同时打开的文件已经超过了系统的限制 |
EFAULT | pipefd参数不合法 |
成功调用pipe函数之后,会返回两个打开的文件描述符,一个是管道的读取端描述符pipefd[0],另一个是管道的写入端描述符pipefd[1]。
不应该对读取端描述符调用写操作,也不应该对写入端描述符调用读操作。
对读取端描述符执行write操作,内核就会执行bad_pipe_w函数;对写入端描述符执行read操作,内核就会执行bad_pipe_r函数。这两个函数比较简单,都是直接返回-EBADF。因此对应的read和write调用都会失败,返回-1,并置errno为EBADF。
// 举例:父进程关闭读端进行写数据,子进程关闭写端进行读数据
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <strings.h>
int main()
{
// 管道的文件描述符
int pipefd[2];
// 创建无名管道
if (pipe(pipefd) == -1)
{
perror("pipe");
return -1;
}
int pid = fork();
if (pid < 0)
{
std::cout << "创建子进程失败" << std::endl;
return -1;
}
else if (pid == 0)
{
// 子进程关闭写端
close(pipefd[1]);
char buf[50];
while (1)
{
read(pipefd[0], buf, 50);
std::cout << buf << std::endl;
}
}
else if (pid > 0)
{
// 父进程关闭读端
close(pipefd[0]);
std::string msg = "hello, child process!";
while (1)
{
write(pipefd[1], msg.c_str(), msg.size());
sleep(1);
}
waitpid(pid, nullptr, 0);
}
}
[root@Zhn 管道]# g++ pipe.cpp -o pipe
[root@Zhn 管道]# ./pipe
hello, child process!
hello, child process!
hello, child process!
hello, child process!
hello, child process!
hello, child process!
hello, child process!
hello, child process!
hello, child process!
hello, child process!
hello, child process!
^C
[root@Zhn 管道]#
这么做不仅仅是为了让数据的流向更加清晰,也不仅仅是为了节省文件描述符,更重要的原因是:关闭未使用的管道文件描述符对管道的正确使用影响重大。
以上是父子进程之间通信,也可以兄弟进程之间通信。
步骤就是父进程再fork一个子进程,关闭写端和读端,第二次fork的子进程关闭读端进行写。
关闭未使用的管道文件描述符
这么做不仅仅是为了让数据的流向更加清晰,也不仅仅是为了节省文件描述符,更重要的原因是:关闭未使用的管道文件描述符对管道的正确使用影响重大。
管道有如下三条性质:
- 只有当所有的写入端描述符都已关闭,且管道中的数据都被读出,对读取端描述符调用read函数才会返回0(即读到EOF标志)。
- 如果所有读取端描述符都已关闭,此时进程再次往管道里面写入数据,写操作会失败,errno被设置为EPIPE,同时内核会向写入进程发送一个SIGPIPE的信号。
- 当所有的读取端和写入端都关闭后,管道才能被销毁。
管道对应的内存大小
查看系统管道的容量,单位:字节:
[root@Zhn vscode]# cat /proc/sys/fs/pipe-max-size
1048576
[root@Zhn vscode]#
管道有大小,写入须谨慎,不能连续地写入大量的内容,一旦管道满了,写入就会被阻塞;对于读取端,要及时地读取,防止管道被写满,造成写入阻塞。
与shell命令进行通信(popen)
管道的一个重要作用是和外部命令进行通信。在日常编程中,经常会需要调用一个外部命令,并且要获取命令的输出。而有些时候,需要给外部命令提供一些内容,让外部命令处理这些输入。Linux提供了popen接口来帮助程序员做这些事情。
popen接口定义如下:
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
popen函数会创建一个管道,并且创建一个子进程来执行shell,shell会创建一个子进程来执行command。根据type值的不同,分成以下两种情况。
如果type是r:command执行的标准输出,就会写入管道,从而被调用popen的进程读到。通过对popen返回的FILE类型指针执行read或fgets等操作,就可以读取到command的标准输出,如图所示:
如果type是w:调用popen的进程,可以通过对FILE类型的指针fp执行write、fputs等操作,负责往管道里面写入,写入的内容经过管道传给执行command的进程,作为命令的输入,如图所示:
popen函数成功时,会返回stdio库封装的FILE类型的指针,失败时会返回NULL,并且设置errno。常见的失败有fork失败,pipe失败,或者分配内存失败。
I/O结束了以后,可以调用pclose函数来关闭管道,并且等待子进程的退出。
// 示例
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
#include<sys/wait.h>
#include<signal.h>
#define MAX_LINE_SIZE 8192
void print_wait_exit(int status)
{
printf("\nstatus = %d", status);
if (WIFEXITED(status))
{
printf("\nnormal termination,exit status = %d", WEXITSTATUS(status));
}
else if (WIFSIGNALED(status))
{
printf("abnormal termination,signal number =%d%s\n",
WTERMSIG(status),
#ifdef WCOREDUMP
WCOREDUMP(status) ? "core file generated" : "");
#else
"");
#endif
}
}
int main(int argc, char* argv[])
{
FILE* fp = NULL;
char command[MAX_LINE_SIZE], buffer[MAX_LINE_SIZE];
if (argc != 2)
{
fprintf(stderr, "Usage: %s filename \n", argv[0]);
exit(1);
}
// 执行 cat 文件名 的shell命令
snprintf(command, sizeof(command), "cat %s", argv[1]);
fp = popen(command, "r");
if (fp == NULL)
{
fprintf(stderr, "popen failed (%s)", strerror(errno));
exit(2);
}
// 获取输出
while (fgets(buffer, MAX_LINE_SIZE, fp) != NULL)
{
fprintf(stdout, "%s", buffer);
}
int ret = pclose(fp);
if (ret == 127)
{
fprintf(stderr, "bad command : %s\n", command);
exit(3);
}
else if (ret == -1)
{
fprintf(stderr, "failed to get child status (%s)\n",
strerror(errno));
exit(4);
}
else
print_wait_exit(ret);
exit(0);
}
将文件名作为参数传递给程序,执行cat filename的命令。popen创建子进程来负责执行cat filename的命令,子进程的标准输出通过管道传给父进程,父进程可以通过fgets来读取command的标准输出。
popen函数和system有很多相似的地方,但是也有显著的不同。调用system函数时,shell命令的执行被封装在了函数内部,所以若system函数不返回,调用system的进程就不再继续执行。但是popen函数不同,一旦调用popen函数,调用进程和执行command的进程便处于并行状态。然后pclose函数才会关闭管道,等待执行command的进程退出。换句话说,在popen之后,pclose之前,调用popen的进程和执行command的进程是并行的,这种差异带来了两种显著的不同:
-
在并行期间,调用popen的进程可能会创建其他子进程,所以标准规定popen不能阻塞SIGCHLD信号。这也意味着,popen创建的子进程可能被提前执行的等待操作所捕获。若发生这种情况,调用pclose函数时,已经无法等待command子进程的退出,这种情况下,将返回-1,并且errno为ECHILD。
-
调用进程和command子进程是并行的,所以标准要求popen不能忽略SIGINT和SIGQUIT信号。如果是从键盘产生的上述信号,那么,调用进程和command子进程都会收到信号。
命名管道FIFO
FIFO与管道类似,最大的差别就是有实体文件与之关联。由于存在实体文件,不相关的没有亲缘关系的进程也可以通过使用FIFO来实现进程之间的通信。
创建FIFO文件
创建命名管道的接口定义如下:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
第一个参数是打开的fifo文件名;
第二个参数mode指定了新创建的命名管道的权限。这个参数是一个用于指定文件权限的八进制数字。
在Linux系统中,文件权限由三组权限组成:文件所有者的权限、文件所属组的权限以及其他用户的权限。每组权限都可以分别包括读取(R)、写入(W)和执行(X)权限,用数字表示分别为4、2和1。因此,八进制数字可以用来表示这些权限的组合。
常见的权限值包括:
- 0666:文件所有者、文件所属组和其他用户均有读写权限。
- 0777:文件所有者、文件所属组和其他用户均有读写执行权限。
打开FIFO文件
一旦FIFO文件创建好了,就可以把它用于进程间的通信了。一般的文件操作函数如open、read、write、close、unlink等都可以用在FIFO文件上。
FIFO文件和普通文件相比,有一个明显的不同:程序不应该以O_RDWR模式打开FIFO文件。POSIX标准规定,以O_RDWR模式打开FIFO文件,结果是未定义的。
对FIFO文件推荐的使用方法是,两个进程一个以只读模式(O_RDONLY)打开FIFO文件,另一个以只写模式(O_WRONLY)打开FIFO文件。
在没有进程以写模式(O_RDWR或O_WRONLY)打开FIFO文件的情况下,以O_RDONLY模式打开一个FIFO文件时,调用进程会陷入阻塞,直到另一进程以O_WRONY(或者O_RDWR)的标志位打开该FIFO文件为止。同样的道理,在没有进程以读模式(O_RDONLY或O_RDWR)打开FIFO文件的情况下,如果一个进程以O_WRONLY的标志位打开一个FIFO文件,调用进程也会阻塞,直到另一个进程以O_RDONLY(或者O_RDWR)的标志位打开该FIFO文件为止。也就是说,打开FIFO文件会同步读取进程和写入进程。
乍看之下,O_RDONLY模式打开不能返回,在等写打开,同样O_WRONLY打开不能返回,在等读打开,造成死锁,谁都返回不了。事实上不是这样的。当O_RDONLY打开和O_WRONLY打开的请求都到达FIFO文件时,两者就都能返回了。
FIFO文件提供了O_NONBLOCK标志位,该标志位会显著影响open的行为模式。将O_RDONLY、O_WRONLY及O_NONBLOCK三种标志位结合在一起考虑,共有以下四种组合方式,如表所示:
// 往管道里写
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char* argv[])
{
int fd, i;
char buf[4096];
if (argc < 2)
{
printf("Enter like this: ./a.out fifoname\n");
return -1;
}
fd = open(argv[1], O_WRONLY);
if (fd < 0)
{
perror("open");
exit(1);
}
i = 0;
while (1)
{
sprintf(buf, "hello itcast %d\n", i++);
write(fd, buf, strlen(buf));
sleep(1);
}
close(fd);
return 0;
}
// 往管道里读
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char* argv[])
{
int fd, len;
char buf[4096];
if (argc < 2)
{
printf("./a.out fifoname\n");
return -1;
}
fd = open(argv[1], O_RDONLY);
if (fd < 0)
{
perror("open");
exit(1);
}
while (1)
{
len = read(fd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
sleep(3); //多个读端时应增加睡眠秒数,放大效果.
}
close(fd);
return 0;
}