参考Linux/Unix系统编程手册44章
【运维角度】liunx运维常见shell命令如:
#ls | wc -l
图:
一个管道就是一个字节流;不存在消息边界;在管道中无法使用lseek()来随机访问数据。
从管道中读取数据
如果从空管道中读取数据, 当从当前为空的管道中读取数据,进程读操作会被阻塞 直到至少一个字节被写入管道;
如果管道的写入端被关闭,从读取端 读取数据的进程在读完管道中剩余的所有数据之后将会接收到 文件结束符,则read 收到结束符后立即返回0
管道是单向的
多个进程写入同一个管道会发生什么情况? 会不会出现数据混合交叉情况?
首先内核会尽量原子性写入管道; 这里有个 常量 PIPE_BUF 在linux 系统中是 4096字节。想想,也对内核不傻,不可能让一个进程一直不停的写一个大的数据到管道,所以超过PIPE_BUF字节时,内核会会把大数据块分解为多个小数据块 <= 4096字节;
得出一个结论: 多个进程往同一个管道写入数据,只要 都写不大于 4096字节(PIPE_BUF)就可以保证写入的原子性,数据不会混合。
向管道写入大数据块?会怎么处理或 有可能会发生什么其他意外情况?
使用write 写入管道,这个调用会阻塞,如果在写一个大数据(大于4096)时,这时write()被信号处理器中断了,则write 会解除阻塞并立刻返回成功传输到管道中的字节数,这个字节数 会少于请求写入的字节数(即出现部分写入的情况)
管道的容量是多大的呢?管道的缓冲器是多大字节的?
LInux 2.6.11 起 管道的存储能力是 65536 字节
Linux 2.6.35开始可以修改管道存储大小
创建管道函数
int pipe(int fields[2]);
Return 0 on success , or -1 on error
管道上可以使用哪些函数;哪些调用?
write ; read; stdio函数 如: printf(), scanf() 等;
管道用途:
精准定义: 可以用于任意两个或多个相关进程之间的通信。
就是让两个进程间通信;
可以让父进程和子进程(父fork出来的);原理图;演化图todo;
可以让进程 和它孙子进程之间使用管道通信。
父子进程想要双向通信怎么办?可以创建两个管道;不过要考虑死锁的问题
非相关进程可以使用管道通信吗? 也可以不过不常见 如: 通过unix domain socket 将管道文件描述符传递给另一个非相关进程。
pipe2()函数 创建的管道 有个flag 参数【不做深入研究】
以下例子演示父进程在一个操作中向管道中写入数据,子进程一小块一小块地从管道中读取数据。
图:
例子代码 44-2
pipes/simple_pipe.c
/* Listing 44-2 */
#include <sys/wait.h>
#include "tlpi_hdr.h"
#define BUF_SIZE 10
int
main(int argc, char *argv[])
{
printf("start:");
int pfd[2]; /* Pipe file descriptors */
char buf[BUF_SIZE];
ssize_t numRead;
if (argc != 2 || strcmp(argv[1], "--help") == 0)
usageErr("%s string\n", argv[0]);
if (pipe(pfd) == -1) /* Create the pipe */
errExit("pipe");
switch (fork()) {
case -1:
errExit("fork");
case 0: /* Child - reads from pipe */
if (close(pfd[1]) == -1) /* Write end is unused */
errExit("close - child");
for (int i=1;;i++) { /* Read data from pipe, echo on stdout */
//printf("iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii: %d",i);
write(STDOUT_FILENO, "8",1);
numRead = read(pfd[0], buf, BUF_SIZE);
if (numRead == -1)
errExit("read");
if (numRead == 0)
break; /* End-of-file */
if (write(STDOUT_FILENO, buf, numRead) != numRead)
fatal("child - partial/failed write");
}
write(STDOUT_FILENO, "\n", 1);
if (close(pfd[0]) == -1)
errExit("close");
_exit(EXIT_SUCCESS);
default: /* Parent - writes to pipe */
if (close(pfd[0]) == -1) /* Read end is unused */
errExit("close - parent");
if (write(pfd[1], argv[1], strlen(argv[1])) != strlen(argv[1]))
fatal("parent - partial/failed write");
if (close(pfd[1]) == -1) /* Child will see EOF */
errExit("close");
wait(NULL); /* Wait for child to finish */
exit(EXIT_SUCCESS);
}
}
测试:
#./simple_pipe helloworld
例子-习题:
使用两个管道来启用父进程和子进程之间的双向通信。父进程循环从标准输入中读取一个文本块 并使用其中一个管道将文本发送给子进程,子进程将文本转换成大写;并通过另一个管道将其返回给父进程。父进程读取子进程数据,并将其反馈到标准输出上;并继续下一个循环。
代码-习题44-1
pipes/change_case.c
/* Solution for Exercise 44-1 */
#include <ctype.h>
#include "tlpi_hdr.h"
#define BUF_SIZE 100 /* Should be <= PIPE_BUF bytes */
int
main(int argc, char *argv[])
{
char buf[BUF_SIZE];
int outbound[2]; /* Pipe to send data from parent to child */
int inbound[2]; /* Pipe to send data from child to parent */
int j;
ssize_t cnt;
if (pipe(outbound) == -1)
errExit("pipe");
if (pipe(inbound) == -1)
errExit("pipe");
switch (fork()) {
case -1:
errExit("fork");
case 0: /* Child */
/* Close unused pipe descriptors */
if (close(outbound[1]) == -1)
errExit("close");
if (close(inbound[0]) == -1)
errExit("close");
/* Read data from outbound pipe, convert to uppercase,
and send back to parent on inbound pipe */
while ((cnt = read(outbound[0], buf, BUF_SIZE)) > 0) {
for (j = 0; j < cnt; j++)
buf[j] = toupper((unsigned char) buf[j]);
if (write(inbound[1], buf, cnt) != cnt)
fatal("failed/partial write(): inbound pipe");
}
if (cnt == -1)
errExit("read");
_exit(EXIT_SUCCESS);
default:
/* Close unused pipe descriptors */
if (close(outbound[0]) == -1)
errExit("close");
if (close(inbound[1]) == -1)
errExit("close");
/* Read data from stdin, send to the child via the
outbound pipe, read the results back from the child
on the inbound pipe, and print them on stdout */
while ((cnt = read(STDIN_FILENO, buf, BUF_SIZE)) > 0) {
if (write(outbound[1], buf, cnt) != cnt)
fatal("failed/partial write(): outbound pipe");
cnt = read(inbound[0], buf, BUF_SIZE);
if (cnt == -1)
errExit("read");
if (cnt > 0)
if (write(STDOUT_FILENO, buf, cnt) != cnt)
fatal("failed/partial write(): STDOUT_FILENO");
}
if (cnt == -1)
errExit("read");
/* Exiting will close write end of outbound pipe, so that
child see EOF */
exit(EXIT_SUCCESS);
}
}
测试:
# ./change_case
nihao
NIHAO
管道是怎么可以作为进程同步的方法呢?【管道用途之一】
这个程序创建了多个进程,每个进程都完成某个动作(本例子中是睡眠一段时间)。父进程等待所有进程完成自己的动作后,再进行自己一些动作,最后父进程退出
例子代码 44-3
pipes/pipe_sync.c
/* Listing 44-3 */
#include "curr_time.h" /* Declaration of currTime() */
#include "tlpi_hdr.h"
int
main(int argc, char *argv[])
{
int pfd[2]; /* Process synchronization pipe */
int j, dummy;
if (argc < 2 || strcmp(argv[1], "--help") == 0)
usageErr("%s sleep-time...\n", argv[0]);
setbuf(stdout, NULL); /* Make stdout unbuffered, since we
terminate child with _exit() */
printf("%s Parent started\n", currTime("%T"));
if (pipe(pfd) == -1)
errExit("pipe");
for (j = 1; j < argc; j++) {
switch (fork()) {
case -1:
errExit("fork %d", j);
case 0: /* Child */
if (close(pfd[0]) == -1) /* Read end is unused */
errExit("close");
/* Child does some work, and lets parent know it's done */
sleep(getInt(argv[j], GN_NONNEG, "sleep-time"));
/* Simulate processing */
printf("%s Child %d (PID=%ld) closing pipe\n",
currTime("%T"), j, (long) getpid());
if (close(pfd[1]) == -1)
errExit("close");
/* Child now carries on to do other things... */
_exit(EXIT_SUCCESS);
default: /* Parent loops to create next child */
break;
}
}
/* Parent comes here; close write end of pipe so we can see EOF */
if (close(pfd[1]) == -1) /* Write end is unused */
errExit("close");
/* Parent may do other work, then synchronizes with children */
if (read(pfd[0], &dummy, 1) != 0)
fatal("parent didn't get EOF");
printf("%s Parent ready to go\n", currTime("%T"));
/* Parent can now carry on to do other things... */
exit(EXIT_SUCCESS);
}
测试:
[root@work pipes]# ./pipe_sync 2 6 3
18:54:10 Parent started
18:54:12 Child 1 (PID=25643) closing pipe
18:54:13 Child 3 (PID=25645) closing pipe
18:54:16 Child 2 (PID=25644) closing pipe
18:54:16 Parent ready to go
[root@work pipes]#
分析/改进:
每个字进程可以向管道写入一条包含其进程ID和一些状态信息的消息,父进程可以计数和分析这些消息。这种方法考虑到子进程意外终止的情况(而不是正常的显式关闭管道的情况)。
使用管道连接 两个程序/两个进程 如 ls wc
图:
在构建完一个管道后,这个程序创建了2个子进程。第一个子进程将其标准输出绑定到管道的写入端,然后执行ls. 第二个子进程将其标准输入绑定到管道的写入端,然后执行wc.
绑定代码如下:
if(pfd[1] != STDOUT_FILENO){
dup2(pfd[1], STDOUT_FILENO);
close(pfd[1]);
}
例子:44-4
pipes/pipe_ls_wc.c
/* Listing 44-4 */
#include <sys/wait.h>
#include "tlpi_hdr.h"
int
main(int argc, char *argv[])
{
int pfd[2]; /* Pipe file descriptors */
if (pipe(pfd) == -1) /* Create pipe */
errExit("pipe");
switch (fork()) {
case -1:
errExit("fork");
case 0: /* First child: exec 'ls' to write to pipe */
if (close(pfd[0]) == -1) /* Read end is unused */
errExit("close 1");
/* Duplicate stdout on write end of pipe; close duplicated descriptor */
if (pfd[1] != STDOUT_FILENO) { /* Defensive check */
if (dup2(pfd[1], STDOUT_FILENO) == -1)
errExit("dup2 1");
if (close(pfd[1]) == -1)
errExit("close 2");
}
execlp("ls", "ls", (char *) NULL); /* Writes to pipe */
errExit("execlp ls");
default: /* Parent falls through to create next child */
break;
}
switch (fork()) {
case -1:
errExit("fork");
case 0: /* Second child: exec 'wc' to read from pipe */
if (close(pfd[1]) == -1) /* Write end is unused */
errExit("close 3");
/* Duplicate stdin on read end of pipe; close duplicated descriptor */
if (pfd[0] != STDIN_FILENO) { /* Defensive check */
if (dup2(pfd[0], STDIN_FILENO) == -1)
errExit("dup2 2");
if (close(pfd[0]) == -1)
errExit("close 4");
}
execlp("wc", "wc", "-l", (char *) NULL);
errExit("execlp wc");
default: /* Parent falls through */
break;
}
/* Parent closes unused file descriptors for pipe, and waits for children */
if (close(pfd[0]) == -1)
errExit("close 5");
if (close(pfd[1]) == -1)
errExit("close 6");
if (wait(NULL) == -1)
errExit("wait 1");
if (wait(NULL) == -1)
errExit("wait 2");
exit(EXIT_SUCCESS);
}
测试:
#./pipe_ls_wc
#ls | wc -l
popen() 通过管道与 shell命令通信; 他是对 fork() pipe() 等的封装
FILE *popen(const char * command, const char *mode);
Return file stream, or NULL on error
int pclose(FILE *stream);
Returns termination status of child process , or -1 on error
popen() 函数创建了一个管道,然后创建了一个子进程来执行shell, 而 shell 又创建了一个子进程来执行command 字符串。
图:
一旦I/O 结束之后可以使用pclose()函数 : 关闭管道;等待子进程中的shell终止; 返回 shell中所执行的最后一条命令的终止状态。
调用进程与被执行的命令(shell命令)是并行执行的。
popen 不忽略 SIGINT 和 SIGQUIT 信号。如果这些信号由键盘产生的,那么调用进程和被执行的shell的命令都会 收到,因为他两位于同一个进程组中。
由于 调用进程在执行popen()和执行 pclose()之间可能会创建其他子进程,所以popen()不能阻塞SIGCHILD信号。
例子: fgets 读取用户输入 正则; fopen 执行ls -d 正则; 分析返回来的文件流
代码44-5
pipes/popen_glob.c
/* Listing 44-5 */
#include <ctype.h>
#include <limits.h>
#include "print_wait_status.h" /* For printWaitStatus() */
#include "tlpi_hdr.h"
#define POPEN_FMT "/bin/ls -d %s 2> /dev/null"
#define PAT_SIZE 50
#define PCMD_BUF_SIZE (sizeof(POPEN_FMT) + PAT_SIZE)
int
main(int argc, char *argv[])
{
char pat[PAT_SIZE]; /* Pattern for globbing */
char popenCmd[PCMD_BUF_SIZE];
FILE *fp; /* File stream returned by popen() */
Boolean badPattern; /* Invalid characters in 'pat'? */
int len, status, fileCnt, j;
char pathname[PATH_MAX];
for (;;) { /* Read pattern, display results of globbing */
printf("pattern: ");
fflush(stdout);
if (fgets(pat, PAT_SIZE, stdin) == NULL)
break; /* EOF */
len = strlen(pat);
if (len <= 1) /* Empty line */
continue;
if (pat[len - 1] == '\n') /* Strip trailing newline */ //把最后一个换行符 替换为字符串字面量的结尾形式
pat[len - 1] = '\0';
/* Ensure that the pattern contains only valid characters,
i.e., letters, digits, underscore, dot, and the shell
globbing characters. (Our definition of valid is more
restrictive than the shell, which permits other characters
to be included in a filename if they are quoted.) */
for (j = 0, badPattern = FALSE; j < len && !badPattern; j++)
if (!isalnum((unsigned char) pat[j]) &&
strchr("_*?[^-].", pat[j]) == NULL)
badPattern = TRUE;
if (badPattern) {
printf("Bad pattern character: %c\n", pat[j - 1]);
continue;
}
/* Build and execute command to glob 'pat' */
snprintf(popenCmd, PCMD_BUF_SIZE, POPEN_FMT, pat);
fp = popen(popenCmd, "r");
if (fp == NULL) {
printf("popen() failed\n");
continue;
}
/* Read resulting list of pathnames until EOF */
fileCnt = 0;
while (fgets(pathname, PATH_MAX, fp) != NULL) { //一行一行读取
printf("%s", pathname);
fileCnt++;
}
/* Close pipe, fetch and display termination status */
status = pclose(fp);
printf(" %d matching file%s\n", fileCnt, (fileCnt != 1) ? "s" : "");
printf(" pclose() status = %#x\n", (unsigned int) status);
if (status != -1)
printWaitStatus("\t", status);
}
exit(EXIT_SUCCESS);
}
执行
./popen_glob
# pop*
# xx*
分析: 真正执行模式匹配的是 shell ; ls 命令仅仅是列出来匹配的文件名
错误检查很有必要,不然让用户乱输入,还执行了,会非常不安全!
管道存在的问题:
管道和 stdio 缓冲
由于popen()调用返回的文件流指针没有引用一个终端,因此stdio库会对这种文件流应用块缓冲。
FIFO 有时也称命名管道
概述: 打开方式和打开一个普通的文件是一样的。这样就能将FIFO用于非相关进程之间的通信。(客户端和服务端)
shell命令创建FIFO
# mkfifo [-m mode] pathname
函数创建:
#include <sys/stat.h>
int mkfifo(const char* pathname, mode_t mode);
return 0 on success, or -1 on error
一旦FIFO被创建,任何进程都可以打开它,只要她通过常规的文件权限检测。
对于那些需要避免在打开FIFO时发生阻塞的需求,open()的O_NONBLOCK标记可以做到。
注意;不要使用O_RDWR标记
使用FIFO 和 tee(1) 创建双重管道线
#mkfifo myfifo
#wc -l < myfifo &
#ls -l | tee myfifo | sort -k5n
图: