管道
管道是进程间通信的主要手段之一。一个管道实际上就是个只存在于内存中的文件,对这个文件的操作要通过两个已经打开文件进行,它们分别代表管道的两端。管道是一种特殊的文件,它不属于某一种文件系统,而是一种独立的文件系统,有其自己的数据结构。根据管道的适用范围将其分为:无名管道和命名管道。管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。
2.1 编程分类总结
1,血缘进程间通信
这里用无名管道,使用fork来复用无名管道的文件操作符,所以只能实现的是有血缘关系的(比如父子,同祖先等)进程间通信。
1.1 父子进程间
创建管道并连接父子进程(pipe+fork)
【说明】先建立pipe,再fork之后会生成子进程,这时父子进程都“共享了”pipe,那么在父进程中输入,子进程用来读出。
1.2 同祖先进程间
创建管道并连接血缘进程(pipe+fork+exec)
【说明】这里和1.1一样,但是通过exec来替换掉子程序的进程内容,不过需要传递一个参数来表示pipe管道的读端。
创建管道复用标准输入输出连接血缘进程(pipe+fork+dup+exec)
【说明】使用dup主要是用来复用标准输入输出,原理是先关闭标准输入0,那么dup之后会按序找一个可以用的号码(就是0),这样就复用了,这样的目的是不用传递参数了。
2,任意进程间通信
创建有名管道FIFO来实现任意进程间的通信(mkfifo+open+read/write+close)
【说明】这里是建立了一个类似于文件(所以是有名的)的管道,然后用打开open和关闭close来对这个管道进行read/write。这样区别于之前的基于fork的方式来实现了进程间的通信。
2.3 函数popen
popen会调用fork函数建一个子进程
此标准的库函数通过在系统内部调用pipe()来创建一个半双工的管道,然后它创建一个子进程,启动shell,最后在shell上执行command参数中的命令。管道中数据流的方向是由第二个参数type控制的。此参数可以是r或者w,分别代表读或写。但不能同时为读和写。
- 函数原型:
- FILE *popen(char*command,char*type);
- 参数说明:
- FILE*:返回值,如果成功,返回一个新的文件流。如果无法创建进程或者管道,返回NULL。
2.4 函数pclose
- 函数原型:
- int pclose(FILE*stream);
- 参数说明:
- int:返回值,返回系统调用wait4()的状态。-1表示错误。
事实上,popen会fork一个进程,而pclose会wait该子进程并关闭。
【示例代码】:用popen传输大数据:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
-
- int main()
- {
- FILE *read_fp;
- char buffer[BUFSIZ+1];
- int chars_read;
-
- memset(buffer,'\0',sizeof(buffer));
- read_fp = popen("ps -ax","r");
- if(read_fp != NULL)
- {
- chars_read = fread(buffer,sizeof(char),BUFSIZ,read_fp);
- while(chars_read > 0)
- {
- buffer[chars_read] = '\0';
- printf("Reading:-\n %s\n",buffer);
- chars_read = fread(buffer,sizeof(char),BUFSIZ,read_fp);
- }
- pclose(read_fp);
- exit(EXIT_SUCCESS);
- }
- exit(EXIT_FAILURE);
- }
2.5 函数pipe
pipe函数式FIFO方式的底层实现。表示新建了一个管道(定义出了入fd和出fd)。
- 函数原型:
- int pipe(int file_descriptor[2]);
- 参数说明:
- file_descriptor:传出参数,该参数将被函数调用后填写。任何写入file_descriptor[1]的数据可以由file_descriptor[0]中读取。
- int:返回值,-1表示失败。
【示例程序】单独创建了一个管道(pipe)
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
-
- int main()
- {
- int data_processed;
- int file_pipes[2];
- const char some_data[] = "123";
- char buffer[BUFSIZ +1];
-
- memset(buffer,'\0',sizeof(buffer));
-
- if(pipe(file_pipes) == 0)
- {
- data_processed = write(file_pipes[1],some_data,strlen(some_data));
- printf("Wrote %d bytes\n",data_processed);
- data_processed = read(file_pipes[0],buffer,BUFSIZ);
- printf("Read %d bytes: %s\n",data_processed,buffer);
- exit(EXIT_SUCCESS);
- }
- exit(EXIT_FAILURE);
- }
这个程序使用两个文件描述符file_pipes[]来创建一个管道。然后他使用文件描述符file_pipes[1]向管道中写入数据,并且由file_pipes[0]中读取。注意管道有内部缓冲,从而可以在两个调用write与read之间存储数据。
我们应该清楚尝试使用file_pipes[0]写入数据,或是使用file_pipes[1]读取数据的效果是未定义的,所以这样的行为结果会非常奇怪,并且也许会毫无警告的发生改变。
2.6 函数dup
dup和dup2也是两个非常有用的调用,它们的作用都是用来复制一个文件的描述符。它们经常用来重定向进程的stdin、stdout和stderr。
- 函数原型:
- int dup(int file_descriptor);
- 参数说明:
- file_descriptor:传入的已有文件描述符,该函数创建一个新的文件描述符(作为已有的拷贝)
2.7 函数dup2
- 函数原型:
- int dup2(int file_descriptor_one , int file_descriptor_two);
- 参数说明:
- file_descriptor_one:传入的已有文件描述符,dup2它所创建的新文件描述符或者与参数file_descriptor_two相同,或者是第一个大于该参数的可用值。
2.8 无名管道
血缘进程间通信,单向,无名
2.8.1 父子进程间
创建管道并连接父子进程(pipe+fork)
【示例程序】
乍看起来,这个使用管道的例子并无特别之处,它做的工作可以用一个简单的文件来完成。管道的真正优势体现在,当你想在两个进程之间传递数据时,程序用fork创建新进程时,原先打开的文件描述符仍将保持打开的状态。如果在原先的进程中创建一个管道,然后再调用fork创建新进程,我们即可通过管道在两个进程之间传递数据。
[父进程] ===file_pipes[1]===> [管道] ===file_pipes[0]===> [子进程]
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
-
- int main()
- {
- int data_processed;
- int file_pipes[2];
- const char some_data[] = "123";
- char buffer[BUFSIZ + 1];
- pid_t fork_result;
-
- memset(buffer,'\0',sizeof(buffer));
-
- if(pipe(file_pipes)==0)
- {
- fork_result = fork();
- if(fork_result == -1)
- {
- fprintf(stderr,"Fork failure");
- exit(EXIT_FAILURE);
- }
-
-
- if(fork_result == 0)
- {
- data_processed = read(file_pipes[0],buffer,BUFSIZ);
- printf("Read %d bytes: %s\n",data_processed,buffer);
- exit(EXIT_SUCCESS);
- }
- else
- {
- data_processed = write(file_pipes[1],some_data,strlen(some_data));
- printf("Wrote %d bytes\n",data_processed);
- }
- }
- exit(EXIT_SUCCESS);
- }
2.8.2 血缘进程间
创建管道并连接血缘进程(pipe+fork+exec)
【示例程序】
-
- #include <unistd.h>
- #include <stdlib.h>
- #include <stdio.h>
- #include <string.h>
-
- int main()
- {
- int length = 0;
- int pipes[2];
- const char data[] = "MONKEY.D.MENG";
- char buffer[BUFSIZ + 1];
- pid_t child_pid;
-
- memset(buffer, 0, sizeof(buffer));
-
- if (pipe(pipes) == 0)
- {
- child_pid = fork();
- if (child_pid == -1)
- {
- fprintf(stderr, "Fork failure!");
- return EXIT_FAILURE;
- }
-
- if (child_pid == 0)
- {
- sprintf(buffer, "%d", pipes[0]);
- execl("pipe4", "pipe4", buffer, (char *)0);
-
- return EXIT_SUCCESS;
- }
- else
- {
- length = write(pipes[1], data, strlen(data));
- printf("%d - wrote %d bytes/n", getpid(), length);
- }
- }
- return EXIT_SUCCESS;
- }
-
-
- #include <unistd.h>
- #include <stdlib.h>
- #include <stdio.h>
- #include <string.h>
-
- int main(int argc, char *argv[])
- {
- int length = 0;
- int file_descriptor;
- char buffer[BUFSIZ + 1];
-
- memset(buffer, 0, sizeof(buffer));
- sscanf(argv[1], "%d", &file_descriptor);
- length = read(file_descriptor, buffer, BUFSIZ);
-
- printf("%d -read %d bytes: %s/n", getpid(), length, buffer);
- return EXIT_SUCCESS;
- }
- 编译程序:
- gcc pipe4.c –o pipe4
- gcc pipe3.c –o pipe3
- 运行程序:
- ./pipe3
- 10733 - wrote 13 bytes
- 10734 -read 13 bytes: MONKEY.D.MENG
注意这里的两个文件分别是pipe3.c和pipe4.c。在pipe3中先pipe,这样生成了一个管道(有了两个fd名字),然后再fork,在子进程中exec替换为另一个程序pipe4.c。这个时候在pipe4.c中无法知道管道的端口名字,所以必须要用参数传入:execl("pipe4", "pipe4", buffer, (char *)0);其中的buffer就是传入的参数(端口号)。
创建管道复用标准输入输出连接血缘进程(pipe+fork+dup+exec)
原因在于关闭标准输入0后,再dup第一个分配的将是0.
这些进程还只是由一个共同的祖先进程启动的。而不是不相关的进程(因为是用fork)。
【示例程序】
- #include <unistd.h>
- #include <stdlib.h>
- #include <stdio.h>
- #include <string.h>
-
- int main()
- {
- int data_processed;
- int file_pipes[2];
- const char some_data[] = "123";
- pid_t fork_result;
-
- if (pipe(file_pipes) == 0) {
- fork_result = fork();
- if (fork_result == (pid_t)-1) {
- fprintf(stderr, "Fork failure");
- exit(EXIT_FAILURE);
- }
-
- if (fork_result == (pid_t)0) {
- close(0);
- dup(file_pipes[0]);
- close(file_pipes[0]);
- close(file_pipes[1]);
-
- execlp("od", "od", "-c", (char *)0);
- exit(EXIT_FAILURE);
- }
-
- else {
- close(file_pipes[0]);
- data_processed = write(file_pipes[1], some_data,
- strlen(some_data));
- close(file_pipes[1]);
- printf("%d - wrote %d bytes\n", (int)getpid(), data_processed);
- }
- }
- exit(EXIT_SUCCESS);
- }
2.9 有名管道
2.9.1 任意进程间通信
打开FIFO文件和打开普通文件的另一点区别在于:对open_flag的O_NONBLOCK选项的用法。使用这个选项不仅改变open调用的处理方式,还会改变对这次open调用返回的文件描述符进行的读写请求的处理方式:
- open(const char *path, ORDONLY);
- 此时,open调用将阻塞,除非有一个进程以写方式打开同一个FIFO,否则它不会返回。这与前面第一个cat命令的例子类似。
- open(const char *path, ORDONLY | O_NONBLOCK);
- 即使没有其他进程以写方式打开FIFO,这个将成功并立刻返回。
- open(const char *path, O_WRONLY);
- 此时,open调用将阻塞,直到有一个进程以读方式打开同一个FIFO为止。
- open(const char *path, O_WRONLY | O_NONBLOCK);
- 这个函数调用总是立刻返回,但如果没有进程以读方式打开FIFO文件,open调用将返回一个错误-1,并且FIFO也不会被打开。如果确实有一个进程以读方式打开FIFO文件,那么我们就可以通过它返回的文件描述符对这个FIFO文件进行写操作。
创建有名管道FIFO来实现任意进程间的通信(mkfifo+open+read/write+close)
【示例程序】
##client.h定义了头文件和结构信息
- ################client.h
- #include <unistd.h>
- #include <stdlib.h>
- #include <stdio.h>
- #include <string.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <limits.h>
-
- #define SERVER_FIFO_NAME "/tmp/serv_fifo"
- #define CLIENT_FIFO_NAME "/tmp/cli_%d_fifo"
-
- #define BUFFER_SIZE 20
-
- struct message
- {
- pid_t client_pid;
- char data[BUFFER_SIZE + 1];
- };
##server.c
- #include "client.h"
-
- int main()
- {
- int client_fifo_fd;
- int server_fifo_fd;
- struct message msg;
- int read_res = 0;
- char client_fifo[256];
- char * tmp_char_ptr;
-
-
- mkfifo(SERVER_FIFO_NAME, 0777);
- server_fifo_fd = open(SERVER_FIFO_NAME, O_RDONLY);
-
- if (server_fifo_fd == -1)
- {
- fprintf(stderr, "Server fifo failure\n");
- return EXIT_FAILURE;
- }
-
- sleep(3);
-
- do
- {
-
- read_res = read(server_fifo_fd, &msg, sizeof(struct message));
- if (read_res > 0)
- {
-
- tmp_char_ptr = msg.data;
- while(*tmp_char_ptr){
- *tmp_char_ptr = toupper(*tmp_char_ptr);
- tmp_char_ptr++;
- }
-
- sprintf(client_fifo, CLIENT_FIFO_NAME, msg.client_pid);
- client_fifo_fd = open(client_fifo, O_WRONLY);
- if (client_fifo_fd != -1)
- {
- write(client_fifo_fd, &msg, sizeof(struct message));
- close(client_fifo_fd);
- }
- }
- }while(read_res > 0);
-
-
- close(server_fifo_fd);
- unlink(SERVER_FIFO_NAME);
-
- return EXIT_SUCCESS;
- }
##client.c
- #include "client.h"
-
- int main()
- {
- int server_fifo_fd;
- int client_fifo_fd;
- struct message msg;
- char client_fifo[256];
-
-
- server_fifo_fd = open(SERVER_FIFO_NAME, O_WRONLY);
- if (server_fifo_fd == -1)
- {
- fprintf(stderr, "Sorry, no server\n");
- return EXIT_FAILURE;
- }
-
-
- msg.client_pid = getpid();
- sprintf(client_fifo, CLIENT_FIFO_NAME, msg.client_pid);
-
- if (mkfifo(client_fifo, 0777) == -1)
- {
- fprintf(stderr, "Sorry, can not make %s\n", client_fifo);
- return EXIT_FAILURE;
- }
-
-
- sprintf(msg.data, "hello world!");
- printf("%d sent %s\n", msg.client_pid, msg.data);
- write(server_fifo_fd, &msg, sizeof(msg));
-
-
- client_fifo_fd = open(client_fifo, O_RDONLY);
- if (client_fifo_fd != -1)
- {
- if (read(client_fifo_fd, &msg, sizeof(msg)) > 0)
- {
- printf("received : %s\n", msg.data);
- }
- close(client_fifo_fd);
- }
-
-
- close(server_fifo_fd);
- unlink(client_fifo);
-
- return EXIT_SUCCESS;
- }
2.10 函数mkfifo
- 函数原型:
- int mkfifo( const char *filename , mode_t mode);
【代码示例】
- #include <unistd.h>
- #include <stdlib.h>
- #include <stdio.h>
- #include <sys/types.h>
- #include <sys/stat.h>
-
-
- int main()
- {
- int res = mkfifo("/tmp/my_fifo", 0777);
- if (res == 0)
- printf("FIFO created\n");
- exit(EXIT_SUCCESS);
- }
2.11 函数mknod
xxx
2.12 有名管道vs无名管道vs文件
【无名管道】(函数pipe,公用内存)不属于任何文件系统(不可见),只存在于内存中。适合通过fork来复用(利用fork后父子进程继承了打开的描述符),也正因为用fork,所以才决定了它只能在有亲缘关系的进程间通信。
【有名管道】(函数mkfifo,有inode,但数据块只在内存中)是有名有形的,为了使用这种管道,LINUX中设立了一个专门的特殊文件系统--管道文件,它存在于文件系统中,任何进程可以在任何时候通过有名管道的路径和文件名来访问管道。但是在磁盘上的只是一个节点,而文件的数据则只存在于内存缓冲页面中,与普通管道一样。可以用临时文件来替代么??不能,我觉得有名管道FIFO因为有了打开方式的限制(只读,只写,阻塞,非阻塞的各种组合),使得在阻塞方式下保证了管道两端必须一端是读一端是写,实现了同步!
【文件】(有inode,有硬盘数据块)