====================================================================
IPC ---- Pipes
Related system calls:
popen (high-level, implemented by actually invoking shell and then command)
pclose (redirect shell commands input and output, you can use these two calls to write tests)
pipe
exec
dup
mkfifo
unlink
Basics:
redirect input and output
(parent process) -->file_pipes[1] --> [PIPE] --> file_pipes[0] --> (child process)
The child process may use the exec call to become a different program. You've got to pass the file descriptor as a parameter to the new program.
pipe is unidirectional
By default, file descriptors remain open across an execve(). File descriptors that are marked close-on-exec are closed; see the description of FD_CLOEXEC in fcntl(2).
也就是说,用execve或者exec-like functions来替换掉原进程(A)执行新进程(B)时,A的fd会被保留下来。此时,新进程(B)可以使用这些fd。比如Example 1中,子进程被替换掉后,新进程实际上是有子进程的fd的,之所以要传参数给新进程,是因为它无法知道哪个fd是pipe的read end。如果事先知道的话,是不需要传参数的;比如,我们知道此程序中pipe的read end是3,那么在procB.c中,read(fd, buf, BUFSIZ);改成read(3, buf, BUFSIZ);也是完全正确的。
在所有的fd中,有3个比较特别,0,1,2;它们的语义是预定义的,分别为标准输入,标准输出和标准错误输出。
由以上两点,我们可以想到,如果利用dup函数,复制一个fd,而dup自己产生的fd是0(我们只要先close(0)就可以了),那么,此时标准输入其实是从这个fd指向的文件(inode)中来读取东西。如果之后,我们用execve来替换掉这个进程执行新进程,那么新进程的标准输入,其实是fd指向的文件。如果这个文件是一个pipe,那么新进程的标准输入其实就是pipe的read end。
由上,我们可以在不改变原来某些程序的情况下,编写新程序(pipe,fork, close, dup, execve),来建立一个pipe。
新程序 --》 (pipe) --》 原程序
(note:每个进程有独立的地址空间和fd table,所以此时如果系统运行其他程序,打开一个文件,其fd依然是3,只是这个fd指向的是系统中另外一个inode)。
mkfifo filename (create a named pipe in command line)
命名管道实际上是在系统的file system中创建了一个named file,它的类型是pipe。由于此FIFO存在于file system中,不相关的程序就可以通过它来进行数据交互(不像unnamed pipe对数据交互双方有很强的耦合要求。)
注意,管道是单向的。[procA] --- named pipe ---> [procB]
如果想让数据双向传输,有以下两种方法:
1. [procA] --- named pipe1 ---> [procB]
[procA] <---named pipe2 ---- [procB]
2. [procA] --- named pipe1 ---> [procB]
双方关闭管道,重新打开如下
[procB] --- named pipe1 ---> [procA]
我们通常使用方法1.
注意,打开fifo是可能会block的,如果没有O_NONBLOCK标志的话。
如果有多个writer和一个reader,会出现什么情况呢?
【procA】|
【procB】||=== named pipe ===> 【procD】
【procC】|
ABC的write request就可又能会交错。比如A的一个write request写了一半,B的write request就写进来了。
如何避免这种情况呢?让write request < PIPE_BUF字节,系统就能保证,每个write行为都是atomic的。
Analysis:
==> Unnamed Pipes
pipe IPC机制总算是比signal要强大一点。因为它可以传输的信息量比较大,而不仅仅只是一个integer。
但是,unnamed pipe的IPC机制的使用很有限制,因为它本质上是通过一对fd来进行通讯的。所以,它的应用基本只能局限在以下两种情况:
1. 父子进程,因为子进程有parent进程的fd拷贝。
2. 子进程exec成另外一个进程,同时通过arg将fd传递给新进程。
通信双方的耦合程度不言而喻,基本上比signal还要差。因为signal还有办法通过程序名字获得进程名,从而可以通过kill调用来进行通讯。pipe IPC机制而言,你如何去得到fd?/proc底下某进程有一堆fd,你咋知道哪个是哪个?如果进程A只有一对pipe fd,那么在/proc目录下还可以通过readlink来进行判断,那两个fd属于pipe;但要是A有好几个pipe呢?所以,unnamed pipe的应用场景,基本上就是以上两种。
3. pipe作为标准输入输出使用,达到以下效果【新程序 --》 (pipe) --》 原程序】。分析见上。
(注意:这个应用场景很有用!比如,我们自己写了一个程序,从标准输入读东西来进行处理的,那么我们可以用以上方法,建立pipe在另外一个程序中,使其输入自动化并且可以对输入进行控制。同理,我们可以用dup来替换掉标准输出,从而对某些程序的输出进程自动化控制和分析。)
不过pipe这种单刀直入的思想,在很多应用场景下,很有效果。
==> Named Pipes
Named Pipe机制比unnamed pipe机制要强大一点。原因是它的reader和writer的耦合程度相对较低(只需要知道fifo的存在位置,如/tmp/my_fifo)。另外,从上面的分析可以知道,如果多个writer的write request的字节数少于PIPE_BUF的话,每个write行为都是atomic的。这一点,使named pipe可以作为client/server的通讯。
由此,named pipe的应用场景要比unnamed pipe要广。
可以如下
[client1] |
[client2] | ---- serv_pipe ----> [server]
[client3] |
| --- cli_pipe_1 --> [client1]
[server] ---- | --- cli_pipe_2 --> [client2]
| --- cli_pipe_3 --> [client3]
只要定义好协议格式就可以进行处理了。
另外,open pipe如果没有O_NONBLOCK,是会block的。
比如open(pipe_fd, O_RDONLY);会一种block到另外一个进程open(pipe_fd, O_WRONLY).
同理open(pipe_fd, O_WRONLY);会一种block到另外一个进程open(pipe_fd, O_RDONLY).
所以,在写client/server时要注意对pipe操作的顺序,以免发生两边都blcok了。
Examples:
Example 1 ---- unamed pipes
(procA sends some data to procB)
/**
* procA.c
*
* write to pipe
**/
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void main()
{
int file_pipes[2];
const char some_data[] = "Hello! I'm process A!";
char buf[BUFSIZ+1];
pid_t pid;
int ret;
memset(buf, 0, sizeof(buf));
ret = pipe(file_pipes);
if (ret < 0)
{
perror("create pipe failed");
exit(-1);
}
else /* ret == 0 */
{
pid = fork();
if (pid <0)
{
perror("fork() failed");
exit(-1);
}
else if (pid == 0) /* child process */
{
sprintf(buf, "%d", file_pipes[0]);
execl("procB", "procB", buf, NULL);
exit(-1); /* we should not get here */
}
else /* parent process */
{
write(file_pipes[1], some_data, strlen(some_data));
printf("[%d] ---- write finished! \n", getpid());
}
}
}
/**
* procB.c
*
* read from a pipe, display its contents
**/
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void main(int argc, char *argv[])
{
int fd;
char buf[BUFSIZ+1];
memset(buf, 0, sizeof(buf));
sscanf(argv[1], "%d", &fd);
read(fd, buf, BUFSIZ);
printf("[%d] ---- read data finish: %s \n", getpid(), buf);
}
Example 2 ---- pipes as standard input and standard output
/**
* pipe_as_stdin.c
* procA --[pipe] --> procB
* procA passes a string to procB through a pipe
* procB is 'wc' program, it receives the string as if it was passed in from stdin
**/
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void main()
{
char data[] = "Hello! This is a string to be passed to wc as an input!";
int file_pipes[2];
pid_t pid;
int ret;
ret = pipe(file_pipes);
if (ret < 0)
{
perror("create pipes failed\n");
exit(-1);
}
else
{
pid = fork();
if (pid < 0)
{
perror("fork failed");
exit(-1);
}
else if (pid == 0) /* child process */
{
close(0);
dup(file_pipes[0]);
close(file_pipes[1]);
close(file_pipes[0]);
execlp("wc", "wc", NULL);
exit(-1); /* we should never get here */
}
else /* parent process */
{
close(file_pipes[0]);
write(file_pipes[1], data, sizeof(data));
close(file_pipes[1]);
printf("[%d] ---- write finished!\n", getpid());
}
}
}
/**
* pipe_as_stdout.c
* analyse how many hidden files there are in this directory
* 'ls -a' --[pipe]--> my program
**/
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void main()
{
char buf[BUFSIZ+1];
int file_pipes[2];
pid_t pid;
int ret;
ret = pipe(file_pipes);
if (ret < 0)
{
perror("create pipes failed\n");
exit(-1);
}
else
{
pid = fork();
if (pid < 0)
{
perror("fork failed");
exit(-1);
}
else if (pid == 0) /* child process */
{
close(1);
dup(file_pipes[1]);
close(file_pipes[0]);
close(file_pipes[1]);
execlp("ls", "ls", "-a", NULL);
exit(-1); /* we should never get here */
}
else /* parent process */
{
close(file_pipes[1]);
memset(buf, 0, sizeof(buf));
read(file_pipes[0], buf, BUFSIZ);
close(file_pipes[0]);
printf("[%d] ---- read finished -- \n %s \n", getpid(), buf);
}
}
}
Example 3 --- name pipes (client/server using fifo)
#ifndef _CS_PIPE_COMMON_H
#define _CS_PIPE_COMMON_H
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <ctype.h>
#define SERVER_FIFO_NAME "/tmp/serv_fifo"
#define CLIENT_FIFO_NAME "/tmp/cli_%d_fifo"
#define ACTION_LEN 32
#define DATA_LEN 1024
#define BUFFER_SIZE PIPE_BUF
struct cs_packet_t
{
pid_t cli_pid;
char action[ACTION_LEN];
char data[DATA_LEN];
};
#endif
/**
* client.c
*
* client/server using named pipes (FIFO)
**/
#include "common.h"
static void print_pkt(struct cs_packet_t *pkt)
{
printf("==========packet=================\n");
printf("client pid = %d \n", pkt->cli_pid);
printf("client action = %s \n", pkt->action);
printf("client data = %s \n", pkt->data);
printf("\n");
}
void main()
{
struct cs_packet_t pkt;
pkt.cli_pid = getpid();
/* fifo initialization */
int serv_fd;
int cli_fd;
serv_fd = open(SERVER_FIFO_NAME, O_WRONLY);
if (serv_fd < 0)
{
perror("open server fifo failed");
exit(-1);
}
char client_fifo[32];
sprintf(client_fifo, CLIENT_FIFO_NAME, pkt.cli_pid);
int ret = mkfifo(client_fifo, 0777);
if (ret < 0)
{
perror ("create client fifo failed");
exit(-1);
}
/* loop to send the server uppercase requests and down case requests */
for (;;)
{
printf("<<debugging client>> --- in for loop\n");
sprintf(pkt.action, "upcase");
sprintf(pkt.data, "Data From Client %d", pkt.cli_pid);
write(serv_fd, &pkt, sizeof(pkt));
printf("sent packet finished: \n");
print_pkt(&pkt);
cli_fd = open(client_fifo, O_RDONLY); /* open client fifo for result */
struct cs_packet_t ret_pkt;
int read_res = read(cli_fd, &ret_pkt, sizeof(ret_pkt));
if (read_res > 0)
{
printf("receive packet form server: \n");
print_pkt(&ret_pkt);
}
close(cli_fd);
sleep(2);
sprintf(pkt.action, "downcase");
sprintf(pkt.data, "Data From Client %d", pkt.cli_pid);
write(serv_fd, &pkt, sizeof(pkt));
printf("sent packet finished: \n");
print_pkt(&pkt);
cli_fd = open(client_fifo, O_RDONLY); /* open client fifo for result */
read_res = read(cli_fd, &ret_pkt, sizeof(ret_pkt));
if (read_res > 0)
{
printf("receive packet form server: \n");
print_pkt(&ret_pkt);
}
close(cli_fd);
sleep(2);
}
}
/**
* server.c
*
* client/server using named pipes (FIFO)
**/
#include "common.h"
#include <pthread.h>
static void print_pkt(struct cs_packet_t *pkt)
{
printf("==========packet=================\n");
printf("client pid = %d \n", pkt->cli_pid);
printf("client action = %s \n", pkt->action);
printf("client data = %s \n", pkt->data);
printf("\n");
}
/**
* function: thread_handle_requests
*
* thread which handles client requests, sends back the result to client through cli_pipe_%d pipe and then exits
* client1 --(pkt1)--> | | --> pkt1 (thread1)
* client2 --(pkt2)--> | --> serv_buf --> | --> pkt2 (thread2)
* client3 --(pkt3)--> | | --> pkt3 (thread3)
* This is not a very good design. For practical server, we should queue the requests.
* N threads may loop to take the requests out of the queue and then handles it.
* This avoids the overhead of thead creation and destruction.
**/
void *thread_handle_requests(void *arg) /* arg is of type struct cs_packet_t */
{
struct cs_packet_t *pkt = (struct cs_packet_t*)malloc(sizeof(struct cs_packet_t));
if (pkt == NULL)
{
perror("Not Enough Memory In Heap!");
exit(-1);
}
memcpy(pkt, arg, sizeof(struct cs_packet_t));
printf("<<debugging server>> --- \n");
print_pkt(pkt);
pid_t cli_pid = pkt->cli_pid;
char *action = pkt->action;
char *data = pkt->data;
char client_fifo[32]; /* name of this client's fifo, cli_fifo_%d */
int client_fifo_fd;
memset(client_fifo, 0, sizeof(client_fifo));
sprintf(client_fifo, CLIENT_FIFO_NAME, cli_pid);
client_fifo_fd = open(client_fifo, O_WRONLY);
if (client_fifo_fd == -1)
{
perror("open client pipe failed");
exit(-1);
}
if (!strcmp(pkt->action, "upcase"))
{
char *tmp_char_ptr = data;
while (*tmp_char_ptr)
{
*tmp_char_ptr = toupper(*tmp_char_ptr);
tmp_char_ptr++;
}
write(client_fifo_fd, pkt, sizeof(struct cs_packet_t));
}
else if (!strcmp(pkt->action, "downcase"))
{
char *tmp_char_ptr = data;
while (*tmp_char_ptr)
{
*tmp_char_ptr = tolower(*tmp_char_ptr);
tmp_char_ptr++;
}
write(client_fifo_fd, pkt, sizeof(struct cs_packet_t));
}
else
{
sprintf(data, "Action %s not supported", pkt->action);
write(client_fifo_fd, pkt, sizeof(struct cs_packet_t));
}
close(client_fifo_fd);
free(pkt);
return NULL;
}
void main()
{
char serv_buf[BUFFER_SIZE];
struct cs_packet_t pkt;
int server_fifo_fd;
pthread_t th;
/* make server fifo */
mkfifo(SERVER_FIFO_NAME, 0777);
server_fifo_fd = open(SERVER_FIFO_NAME, O_RDONLY);
if (server_fifo_fd == -1)
{
perror("create server fifo failed");
exit(-1);
}
/* read from server fifo */
for (;;)
{
int read_res = read(server_fifo_fd, &pkt, sizeof(pkt));
if (read_res > 0)
{
printf("<<debugging server>>----received a packet from %d\n", pkt.cli_pid);
/* create a thread to handle this request */
int ret = pthread_create(&th, NULL, thread_handle_requests, (void*)&pkt);
if (ret < 0)
{
perror("create thread failed");
exit(-1);
}
}
}
}