首页 > 代码库 > 第11章进程间通信(1)_管道
第11章进程间通信(1)_管道
1. 进程间通信概述
(1)概述
①数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。
②共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。
③通知事件:一个进程需要向另一个(组)进程发送消息,通知它们发生了某种事件(如进程终止时要通知父进程)。
④资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供锁和同步机制。
⑤进程控制:有些进程希望完全控制另一个进程的执行(如Degub进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
(2)现代的进程间通信方式
①管道(pipe)和命名管理(FIFO) ②信号(signal) ③消息队列 ④共享内存 ⑤信号量 ⑥套接字(socket)
2. 管道通信
2.1 概述
(1)管道是针对本地计算机的两个进程之间的通信而设计的通信方法,管道建立后,实际获得的是两个文件描述符:一个用于读取,另一个用于写入。
(2)最常见的IPC机制,通过pipe系统调用
(3)管道是单工的,数据只能向一个方向流动,需要双向通信时,需要建立起两个管道。
(4)数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据(即读取的顺序应与写入的顺序一致!)
2.2 管道的分类和读写
(1)管道的分类
①匿名管道:
A.在关系进程中进程(父进程和子进程,兄弟进程之间)
B.由pipe系统调用,管道由父进程建立
C.管道位于内核空间,其实是一块缓存
②命名管道(FIFO):
A.两个没有任何关系的进程之间通信可通过命名管道进行数据传输,本质上是内核中一块缓存,另在文件系统中以一个特殊的设计文件(管道文件)存在。
B.通过系统调用mkfifo创建。
(2)管道的创建:
头文件 |
#include <unistd.h> |
函数 |
int pipe(int fd[2]); |
功能 |
等待一个或者多个指定信号发生 |
返回值 |
成功返回0,否则返回-1 |
备注 |
①fd[0]:为pipe的读端,用于读取管道。 ②fd[1]:为pipe的写端,用于写入管道。 |
(2)管道的读写
①管道主要用于不同进程间通信。实际上,通常先创建一个管道,再通过fork函数创建另一个子进程。
②注意管道是单工的,所以要关闭父子进程中其中的一些fd(如上图所示)。如果需要双向通信,则需要创建2个管道。
【编程实验】父进程写入,子进程读取
//cal_pipe.c
#include <unistd.h> #include <stdio.h> #include <stdlib.h> /* * 父进程通过管道传输两个数据给子进程 * 由子进程负再从管道中读取并输出 */ int main(void) { int fd[2]; //创建管道 if(pipe(fd) < 0){ perror("fork error"); exit(1); } pid_t pid; if((pid = fork()) < 0){ perror("fork error"); exit(1); }else if(pid >0){ //parent process close(fd[0]); //父进程关闭读端,保留写端。 int start = 1, end = 100; //往管道中写入数据 if(write(fd[1], &start, sizeof(int)) != sizeof(int)){ perror("write error"); exit(1); } if(write(fd[1], &end, sizeof(int)) != sizeof(int)){ perror("write error"); exit(1); } close(fd[1]); wait(pid); }else{ //child process close(fd[1]); //子进程用来读取数据 int start, end; //从管道中读取数据(注意与写入的顺序相同) if(read(fd[0], &start, sizeof(int)) < 0){ perror("read error"); exit(1); } if(read(fd[0], &end, sizeof(int)) < 0){ perror("read error"); exit(1); } close(fd[0]); printf("child process read start: %d, end: %d\n", start, end); } return 0; } /*输出结果: child process read start: 1, end: 100 */
【编程实验】模拟管道命令
①一个子进程执行命令,将命令执行结果写入管道
②另一个子进程从管道中读取命令执行的结果,然后根据关键字过滤(grep)
③分析命令:cat /etc/passwd | grep root。执行该命令时,实际上有3进程:父进程shell,执行cat的子进程和执行grep的子进程!这与本例模拟的情景是一样的,由cat的子进程执行结果写入管道,grep的子进程从管道中读取出来!
//cmd_pipe.c
#include <unistd.h> #include <stdio.h> #include <stdlib.h> char* cmd1[3] = {"/bin/cat", "/etc/passwd", NULL}; char* cmd2[3] = {"/bin/grep", "root", NULL}; //char* cmd2[3] = {"wc", "-l", NULL}; //统计多少用户 int main(void) { int fd[2]; if(pipe(fd) < 0){ perror("pipe error"); exit(1); } int i = 0; pid_t pid; //创建进程扇:1个父进程和2个子进程 for(; i<2; i++){ pid = fork(); if(pid < 0){ perror("fork error"); exit(1); }else if(pid == 0){ //child process if(i == 0){ //第1个子进程,负责往管道写入数据 close(fd[0]); //关闭读端 //注意cat命令默认输出是标准输出(屏幕),因此需输重定向到管道写端 //将标准输出重定向到管道写端,cat执行结果会写入管道 if(dup2(fd[1], STDOUT_FILENO) != STDOUT_FILENO){ //将fd[1]重向定为标准输出 perror("dup2 error"); } close(fd[1]);//标准输出己重定向到管道写端,fd[1]可以关闭 //调用exec函数执行cat命令 if(execvp(cmd1[0], cmd1) < 0){//v数组,p绝对或相对路径 perror("execvp error"); exit(1); } } if(i == 1){ //第2个子进程,负责从管道读取数据 close(fd[1]); //关闭写端 //将标准输入重定向到管道读端,这样grep将从管道读入而不是从标准输入读取 if(dup2(fd[0], STDIN_FILENO) != STDIN_FILENO){ perror("dup2 error"); } close(fd[0]);//标准输入己重定向到管道读端,fd[0]可以关闭 //调用exec函数执行grep命令 if(execvp(cmd2[0], cmd2) < 0){ perror("execvp error"); exit(1); } } break; }else{ //parent process if( i== 1){ //须等第2个子进程创建完毕 //父进程要等到子进程全部创建完毕才去回收 close(fd[0]); close(fd[1]); wait(0); //回收两个子进程 wait(0); } } } if(pid = fork() < 0){ } return 0; } /*输出结果: root:x:0:0:root:/root:/bin/bash operator:x:11:0:operator:/root:/sbin/nologin */
【编程实验】协同进程(两个进程通过两个管道进行双向通信)
①父进程向第1个管道写入x和y。
②子进程从第1个管道读取x和y。并调用add进行相加。
③子进程将计算结果写入第2个管道。
④父进程从第2个管道中读取计算结果,并输出。
//add.c ==> 需单独编译成add.o的可执行文件
#include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(int argc, char* argv[]) { int x = 0, y = 0; if(read(STDIN_FILENO, &x, sizeof(int)) < 0){ perror("read error"); } if(read(STDIN_FILENO, &y, sizeof(int)) < 0){ perror("read error"); } int result = x + y; if(write(STDOUT_FILENO, &result, sizeof(int)) != sizeof(int)){ perror("write error"); } return 0; }
//co_process.c ==> 编译成可执行文件
#include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(void) { int fda[2], fdb[2]; //创建两个管道,以实现双工操作 if( (pipe(fda) < 0) || (pipe(fdb) <0) ){ perror("pipe error"); exit(1); } pid_t pid; pid = fork(); if(pid < 0){ }else if(pid == 0){//child process /* *(1)子进程负责从管道a中读取父进程写入的参数x和y *(2)通过exec函数去调用bin/add程序进行累加 *(3)将累加的结果写入到管道b。 */ close(fda[1]);//只能从a管道读取 close(fdb[0]);//只能向b管道写入 //将标准输入重定向到管道a的读端,则 //(add程序将从管道a的读端读取累加参数x和y) if(dup2(fda[0], STDIN_FILENO) != STDIN_FILENO){ perror("dup2 error"); } //将标准输出重定向到管道b的写端,则 //(add程序累加后的结果会写入管道b中) if(dup2(fdb[1], STDOUT_FILENO) != STDOUT_FILENO){ perror("dup2 error"); } close(fda[0]); //重定向完毕,可以关闭 close(fdb[1]); if(execlp("bin/add", "bin/add", NULL) < 0){ perror("execlp error"); exit(1); } }else{ //parent process /* *(1)从标准输入读取参数x和y *(2)将x和y写入管道a *(3)从管道b中读取累加结果并输出 */ close(fda[0]); close(fdb[1]); int x, y; //(1)读取累加参数x和y printf("please input x and y: "); scanf("%d %d", &x, &y); //(2)将x和y写入管道a if(write(fda[1], &x, sizeof(int)) != sizeof(int)){ perror("write error"); } if(write(fda[1], &y, sizeof(int)) != sizeof(int)){ perror("write error"); } //(3)从管道b中读取结果(注意管道中无数据时会阻塞!) int result; if(read(fdb[0], &result, sizeof(int)) < 0){ perror("read error"); }else{ printf("add result is %d\n", result); } close(fda[1]); close(fdb[0]); wait(0); } return 0; }
2.3 管道的特性
(1)通过打开两个管道来创建一个双向的管道
(2)管道是阻塞性的,当进程从管道中读取数据,若没有数据进程会阻塞。
(3)当一个进程往管道中不断地写入数据,但是没有进程去读取数据,此时只要管道没有满是可以了,但若管道放满数据时则会报错。
(4)不完整管道
①当读一个写端己关闭的管道时,在所有数据被读取后,read返回0,以表示到达了文件的尾部。
②如果写一个读端己被关闭的管道,则产生信号SIGPIPE,如果忽略该信号或捕捉该信号并从处理程序返回,则write返回-1,同时errno设置为EPIPE。
【编程实验】读一个写端己关闭的管道
//broken_pipe_r.c
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> /* *不完整管道:读取一个写端己经关闭的管道 */ int main() { int fd[2]; if(pipe(fd) < 0){ perror("pipe error"); exit(1); } pid_t pid; if((pid = fork()) < 0){ perror("fork error"); exit(1); }else if (pid > 0){ //parent process //父进程从不完整管道中读取数据 close(fd[1]); sleep(5);//等待子进程将管道的写端关闭 while(1){ char c; if(read(fd[0], &c, 1) == 0){ printf("\nwrite-end of pipe closed.\n"); break; }else{ printf("%c", c); } } close(fd[0]); wait(0); }else{ //child process //子进程负再将数据写入管道 close(fd[0]); char* s = "12345"; write(fd[1], s, strlen(s)*sizeof(char)); //写入数据后关闭管道的写端-->变成不完整 //管道,但要确保在父进程读管道之前管道的写端被关闭! close(fd[1]); } return 0; } /*输出结果 12345 write-end of pipe closed. */
【编程实验】写一个读端己关闭的管道
//broken_pipe_w.c
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <errno.h> /* *不完整管道:写一个读端己被关闭的管道 */ //信号处理函数 void sig_handler(int signo) { if(signo == SIGPIPE){ printf("SIGPIEP occured\n"); } } int main(void) { //注册信号处理函数 if(signal(SIGPIPE, sig_handler) == SIG_ERR){ perror("signal sigpipe error"); exit(1); } int fd[2]; if(pipe(fd) < 0){ perror("pipe error"); exit(1); } pid_t pid; if((pid = fork()) < 0){ perror("fork error"); exit(1); }else if(pid > 0){ //parent process //父进程注册信号处理函数 if(signal(SIGPIPE, sig_handler) == SIG_ERR){ perror("signal sigpipe error"); exit(1); } //父进程负责将数据写入不完整管道(读端关闭)中 sleep(5);//让子进程先运行,以保证读端关闭 close(fd[0]); char* s = "12345"; int len = strlen(s) * sizeof(char); if(write(fd[1], s, len ) != len){ fprintf(stderr, "%s, %s\n", strerror(errno), (errno == EPIPE) ? "EPIPE": ", unknow"); } close(fd[1]); wait(0); }else{ //child process close(fd[0]); close(fd[1]); } return 0; } /*输出结果: SIGPIEP occured Broken pipe, EPIPE */
2.4 标准库中的管道操作
头文件 |
#include <stdio.h> |
函数 |
FILE* popen(const char* cmdstring, const char* type); |
参数 |
cmdstring:要执行的命令行参数。 type:r或w。 ①如果type为r,则表示由子进程exec(cmdstring),结果写入管道(子进程内部会将标准输出重定向到管道写端),父进程从管道中读取命令的执行结果。 ②如果type为w,则表示父进程将数据写入管道,子进程从管道中读取数据作为命令执行的输入(内部将标准输入重定到到管道的读端)。 |
返回值 |
成功返回文件指针,出错返回NULL |
功能 |
通过创建一个管道,调用fork()产生一个子进程,然后由子进程执行cmdstring命令。 |
函数 |
int pclose(FILE* fp); |
返回值 |
cmdstring的终止状态,出错返回-1 |
功能 |
关闭管道 |
备注 |
①使用popen()创建的管道必须使用pclose()关闭。其实popen/pclose和标准文件输入/输出流中的fopen/fclose十分相似。 ②封装管道的常用操作。 |
【编程实验】利用标准库操作管道的读写
//popen_rw.c
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <memory.h> /*利用管道操作文件*/ int main(void) { FILE* fp; //命令执行的结果放置在fp指向的结构体缓存中 fp = popen("cat /etc/passwd", "r"); char buf[512]; memset(buf, 0, sizeof(buf)); while(fgets(buf, sizeof(buf), fp) != NULL){ printf("%s", buf); } pclose(fp); printf("-----------------------------------------\n"); //为wc命令提供统计的数据 fp = popen("wc -l", "w"); //向fp指向的缓存写入数据(因为type为"w",所以这些数据会被写入管道中) fprintf(fp, "line1\nline2\nline3\n");//提供3行的数据,作为wc要统计的数据来源! pclose(fp); return 0; }
2.5 命令管道(FIFO)
(1)FIFO的创建
头文件 |
#include <sys/types.h> #include <sys/stat.h> |
函数 |
int mkfifo(const char* pathname, mode_t mode); |
参数 |
①pathname:要创建的管道文件名 ②mode:权限(mode % ~umask) |
返回值 |
(1)成功返回0,出错返回-1 (2)FIFO相关出错信息 ①EACCES(无存取权限) ②EEXIST(指定文件不存在) ③ENAMETOOLONG(路径名太长) ④ENOENT(包含的目录不存在) ⑤ENOSPC(文件系统剩余空间不足) ⑥ENOTDIR(文件路径无效) ⑦EROFS(指定的文件存在于只读文件系统中) |
备注 |
|
(2)注意事项
①只要对FIFO有适当访问权限,FIFO可用在任何两个没有任何关系的进程之间通信。
②本质上内核中的一块缓存,其在文件系统中以一个特殊的设备文件(管道文件)存在。
③在文件系统中只有一个索引块存放文件的路径,没有数据块,所有数据存放在内核中。
④命名管道必须读或写同时打开,否则单独读或单独写会引发阻塞。
⑤命令mkfifo创建命名管道(命令内部调用mkfifo函数)
⑥对FIFO的操作与操作普通文件一样。
⑦一旦己经用mkfifo创建一个FIFO,就可以用open打开它,一般的文件I/O函数(close、read、write和unlink等)都可用于FIFO。
【编程实验】读写管道文件
(1)运行本例子中的两个进程之前,必须先mkfifo创建一个命名管道文件(如s.pipe)
(2)不管先运行读还是写的进程。如果命名管道只被打开一端(读或写),则另一个进程会被阻塞。可以通过先运行读或写进程来观察进程被阻塞的现象。
//fifo_write.c ==>编译成单独的可执行文件
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> /*向命名管道写入数据*/ int main(int argc, char* argv[]) { if(argc < 2){ printf("usage: %s fifo\n", argv[0]); exit(1); } printf("open fifo write...\n"); //打开命名管道 int fd = open(argv[1], O_WRONLY); if( fd < 0 ){ perror("open error"); exit(1); }else{ printf("open fifo success: %d\n", fd); } char* s = "1234567890"; size_t size = strlen(s); if(write(fd, s, size) != size){ perror("write error"); } close(fd); return 0; } /*输出结果: [root@localhost]# bin/fifo_write s.pipe //要先mkfifo s.pipe创建命名管道文件 open fifo write... open fifo success: 3 */
//fifo_read.c ==>编译成单独的可执行文件
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <memory.h> /*从命名管道中读取数据*/ int main(int argc, char* argv[]) { if(argc < 2){ printf("usage: %s fifo\n", argv[0]); exit(1); } printf("open fifo read...\n"); //打开命名管道 int fd = open(argv[1], O_RDONLY); if(fd < 0){ perror("open error"); exit(1); }else{ printf("open file sucess: %d\n", fd); } //从命名管道中读取数据 char buf[512]; memset(buf, 0, sizeof(buf)); while(read(fd, buf, sizeof(buf)) < 0){ perror("read error"); } printf("%s\n", buf); close(fd); return 0; } /*输出结果: [root@localhost]# bin/fifo_read s.pipe open fifo read... open file sucess: 3 1234567890 */
2.6 匿名和命令管道的读写
(1)匿名管道和命名管道读写的相同点
相同点 |
说明 |
阻塞特性 |
默认都是阻塞性读写 |
网络通信 |
都适用于socket的网络通信 |
阻塞不完整管道 |
①单纯读时,在所有数据被读取后,read返回0,以表示到达了文件尾部。 ②单纯写时,则产生信号SIGPIPE,如果忽略该信号或捕捉该信号并从处理程序返回,则write返回-1,同时errno设置为EPIPE。 |
阻塞完整管道 |
①单纯读时,要么阻塞,要么读取到数据 ②单纯写时,写到管道满时会出错 |
非阻塞不完整管道 |
①单纯读时直接报错 ②单纯写时,则产生信号SIGPIPE,如果忽略该信号或捕捉该信号并从处理程序返回,则write返回-1,同时errno设置为EPIPE。 |
非阻塞完整管道 |
①单纯读时直接报错。 ②单纯写时,写到管道满时会出错。 |
(2)匿名管道和命名管道读写的不同点
不同点 |
说明 |
打开方式 |
打开方式不一致 |
设置阻塞特性 |
①pipe通过fcntl系统调用来设置O_NONBLOCK来设置非阻塞性读写。 ②FIFO通过fcntl系统调用或者open函数来设置非阻塞性读写。 |
第11章进程间通信(1)_管道