进程间的通信(1)
进程间的通信IPC(InterProcessCommunication )主要有以下不同形式:
半双工管道和FIFO;全双工管道和命名全双工管道;消息队列,信号量和共享存储;套接字和STREAMS
管道
pipe函数
当从一个进程连接到另一个进程时,我们使用术语管道。我们通常是把一个进程的输出通过管道连接到另一个进程的输入。
管道是由调用pipe函数创建的:
#include<unistd.h>
int pipe(intpipefd[2]);
经由参数pipefd返回两个文件描述符:pipefd[0]为读而打开,pipefd [1]为而打开。pipefd [1]的输出是pipefd [0]的输入。特别要要注意,这里使用的是文件描述符而不是文件流,所有我们必须用底层的read和write调用来访问,而不能用fread和fwrite。
单个进程中的管道几乎没有任何用处。通常,调用pipe的进程接着调用fork,这样就创建了从父进程子进程的IPC通道。调用fork后,对于从父进程到子进程的管道,父进程关闭管道的读端pipefd [0],子进程则关闭写端pipefd [1];对于从子进程到父进程的管道,父进程关闭pipefd [1],子进程关闭pipefd [0]。以下程序说明其用法:
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#define BUFSIZE1024
int main()
{
int data_processed;
int file_pipes[2];
const char some_data[] = "123";
char buffer[BUFSIZE + 1];
pid_t fork_result;
memset(buffer,'\0', sizeof(buffer));
if(pipe(file_pipes) == 0)
{
fork_result = fork();
if(fork_result == -1)
{
fprintf(stderr, "Forkfailure");
exit(EXIT_FAILURE);
}
if(fork_result == 0)
{
close(file_pipes[1]);
data_processed =read(file_pipes[0],buffer,BUFSIZE);
printf("Read %d bytes: %s\n", data_processed, buffer);
exit(EXIT_SUCCESS);
}
else
{
close(file_pipes[0]);
data_processed =write(file_pipes[1], some_data,
strlen(some_data));
printf("Wrote %dbytes\n",data_processed);
}
}
执行结果如下所示:
Wrote 3 bytes
Read 3 bytes:123
在下面例子中,展示了如何在子进程中运行一个与其父进程完全不同的另外一个程序,而不是仅仅运行一个相同的程序,我们用exec调用来完成这一工作。由于调用exec后,子进程内存和父进程不同,所以为了访问创建管道后的文件描述符,需要将文件描述符作为一个参数传递给exec启动程序。
第一个程序是数据生产者,负责创建管道和启动子进程
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#define BUFSIZE1024
int main()
{
int data_processed;
int file_pipes[2];
const char some_data[] = "123";
char buffer[BUFSIZE + 1];
pid_t fork_result;
memset(buffer,'\0', sizeof(buffer));
if(pipe(file_pipes) == 0)
{
fork_result = fork();
if(fork_result == -1)
{
fprintf(stderr, "Forkfailure");
exit(EXIT_FAILURE);
}
if(fork_result == 0)
{
close(file_pipes[1]);
sprintf(buffer, "%d",file_pipes[0]);
(void)execl("pipe2","pipe2",buffer,(char*)0);
exit(EXIT_FAILURE);
}
else
{
close(file_pipes[0]);
data_processed = write(file_pipes[1],some_data,
strlen(some_data));
printf("%d - WROTE %dBYTES\n",getpid(),data_processed);
}
}
exit(EXIT_SUCCESS);
}
第二个程序是数据消费者,负责读取数据
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#define BUFSIZE1024
int main(intargc, char* argv[])
{
int data_processed;
char buffer[BUFSIZE + 1];
int file_descriptor;
memset(buffer, '\0', sizeof(buffer));
sscanf(argv[1], "%d",&file_descriptor);
data_processed = read(file_descriptor,buffer, BUFSIZ);
printf("%d - read %d bytes:%s\n", getpid(), data_processed, buffer);
exit(EXIT_SUCCESS);
}
执行结果如下:
2685 - WROTE 3BYTES
2686 - read 3bytes: 123
我们通过execl调用来启动pipe2,execl的参数是:
1. 要启动的程序
2. argv[0]:程序名
3. argv[1]:包含我们想让被调用程序去读取的文件描述符
4. (char *)0:这个参数的作用是终止被调用程序的参数列表
我们可以把管道的一个文件描述符设置为一个已知值,一般是标准输入0或标准输出1。这样做最大的好处是我们可以调用标准程序,即那些不需要文件描述符为参数的程序。可以使用dup函数。它们的原型如下:
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
duo调用的目的是打开一个新的文件描述符。dup创建的新文件描述符与作为它的参数的那个已有文件描述符指向同一个文件,对于dup函数来说,新的文件描述符总是取最小的可用值,而对于dup2函数来说,它所创建的新文件描述符或者与参数newfd相同,或者是第一个大于该参数的可用值。
我们可以在子程序中把stdin文件描述符替换为我们创建的管道读端文件描述符。程序如下:
#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 == -1)
{
fprintf(stderr,"Fork failure");
exit(EXIT_FAILURE);
}
if(fork_result == 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",getpid(),data_processed);
}
}
exit(EXIT_SUCCESS);
}
执行结果如下:
2886 - WROTE 3 BYTES
0000000 1 2 3
0000003
popen和pclose函数
常见的操作是创建一个管道连接到另一个进程,然后读其输出或向其输入端发送数据,为此,标准I/O库提供了两个函数popen和pclose。这两个函数实现的操作是:创建一个管道,调用fork产生一个子进程,关闭管道的不使用端,执行一个shell以运行命令,然后等待命令终止。
#include<stdio.h>
FILE*popen(const char *command, const char *type);
int pclose(FILE*stream);
函数popen先执行fork,然后调用exec以执行command,并且返回一个标准I/O文件指针。如果type是”r”,则文件指针连接到command的标准输出,返回的文件指针是可读的。
如果type是”w”,则文件指针连接到command的标准输入,返回的文件指针是可写的。
如下程序展示了如何使用popen和pclose函数。
#include <unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define BUFSIZE1024
int main()
{
FILE* read_fp;
char buffer[BUFSIZE + 1];
int chars_read;
memset(buffer, '\0', sizeof(buffer));
read_fp = popen("uname -a","r");
if(read_fp != NULL)
{
chars_read = fread(buffer,sizeof(char), BUFSIZE, read_fp);
if(chars_read > 0)
{
printf("Output was:-\n%s\n", buffer);
}
pclose(read_fp);
exit(EXIT_SUCCESS);
}
exit(EXIT_SUCCESS);
}
执行结果如下:
Output was: -
Linux ubuntu 3.0.0-12-generic #20-UbuntuSMP Fri Oct 7 14:50:42 UTC 2011 i686 i686 i386 GNU/Linux
命名管道
至此我们还只能在相关的程序之间传递数据,即这些程序是由一个共同的祖先进程启动的。但如果我们想在不相关的进程之间交换数据,这是很不方便的。我们可以用FIFO文件来完成这项工作,它通常也被称为命名管道。命名管道是一种特殊类型的文件,它在系统中以文件的形式存在。
我们可以在命令行上直接创建命名管道:
mkfifo filename
也可以在程序中创建:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_tmode);
mkfifo函数中的mode与open函数中的mode相同。
当打开一个FIFO时,非阻塞标志(O_NONBLOCK)产生下列影响:
1. 在一般情况中(没有指定O_NONBLOCK),只读open要阻塞到某个其他进程为写而打开此FIFO。类似的,只写open阻塞到某个其他进程为读而打开它。
2. 如果指定了O_NONBLOCK,则只读open立即返回。但是,如果没有进程已经为读而打开一个FIFO,那么只写open将出错返回-1,其errno是ENXIO。
下面的程序是使用FIFO的客户/服务器应用程序的展示:
1) 首先我们需要一个头文件client.c,它定义了客户和服务程序都会用到的数据。为了方便使用,它包含了必要的系统头文件
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<fcntl.h>
#include<limits.h>
#include<sys/types.h>
#include<sys/stat.h>
#defineSERVER_FIFO_NAME "/tmp/serv_fifo"
#defineCLIENT_FIFO_NAME "/tmp/cli_%d_fifo"
#defineBUFFER_SIZE 20
structdata_to_pass_st{
pid_t client_pid;
char some_data[BUFFER_SIZE-1];
};
2) 现在是服务器程序server.c。在这一部分,我们创建并打开服务器管道。他被设置为只读的阻塞模式。服务器读取客户端发送来的数据,这些数据采用data_to_pass_st结构。然后将接收到的数据转换为大写处理,并把数据发回去。最后关闭服务管道的文件描述符,删除FIFO文件。
#include"client.h"
#include<ctype.h>
intmain()
{
int server_fifo_fd, client_fifo_fd;
struct data_to_pass_st my_data;
int read_res;
charclient_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 fifofailure\n");
exit(EXIT_FAILURE);
}
//sleep(10)
do{
read_res = read(server_fifo_fd,&my_data, sizeof(my_data));
if(read_res>0 )
{
tmp_char_ptr = my_data.some_data;
while(*tmp_char_ptr)
{
*tmp_char_ptr =toupper(*tmp_char_ptr);
tmp_char_ptr++;
}
sprintf(client_fifo,CLIENT_FIFO_NAME,my_data.client_pid);
client_fifo_fd = open(client_fifo,O_WRONLY);
if(client_fifo_fd != -1)
{
write(client_fifo_fd,&my_data, sizeof(my_data));
close(client_fifo_fd);
}
}
}while(read_res > 0);
close(server_fifo_fd);
unlink(SERVER_FIFO_NAME);
exit(EXIT_SUCCESS);
}
3) 下面是客户程序client.c。这个程序的第一部分先检查服务器FIFO文件是否存在,如果存在就打开它。然后它获取自己的进程ID,该进程ID构成要发送给服务器的数据的一部分。接下来,它创建客户FIFO。接着,在循环部分客户将数据发送给服务器,然后打开客户FIFO读处理过的数据。在程序的最后关闭服务器FIFO并将FIFO从内存中删除。
#include"client.h"
#include<ctype.h>
intmain()
{
int server_fifo_fd, client_fifo_fd;
struct data_to_pass_st my_data;
int times_to_send;
char client_fifo[256];
server_fifo_fd = open(SERVER_FIFO_NAME,O_WRONLY);
if(server_fifo_fd == -1)
{
fprintf(stderr, "Sorry, noserver\n");
exit(EXIT_FAILURE);
}
my_data.client_pid = getpid();
sprintf(client_fifo,CLIENT_FIFO_NAME,my_data.client_pid);
if(mkfifo(client_fifo,0777) == -1)
{
fprintf(stderr,"sorry,can't make%s\n",client_fifo);
exit(EXIT_FAILURE);
}
for(times_to_send = 0;times_to_send<5;times_to_send++)
{
sprintf(my_data.some_data,"hellofrom %d",my_data.client_pid);
printf("start5!/n");
printf("%d sent%s,",my_data.client_pid,my_data.some_data);
unlink(SERVER_FIFO_NAME);
write(server_fifo_fd,&my_data,sizeof(my_data));
client_fifo_fd =open(client_fifo,O_RDONLY);
if(client_fifo_fd != -1)
{
if(read(client_fifo_fd,&my_data, sizeof(my_data))>0)
{
printf("received:%s\n",my_data.some_data);
}
close(client_fifo_fd);
}
}
close(server_fifo_fd);
unlink(client_fifo);
exit(EXIT_SUCCESS);
}
执行及输出结果如下:
$ ./server &
[3] 4618
$ for i in 1 2 3 4 5
do
./client &
done
$
[4] 4619
[5] 4620
[6] 4621
[7] 4622
[8] 4623
chen123@ubuntu:~/user/apue.2e$ Sorry, no server
Sorry, no server
Sorry, no server
start5!/n4619 sent hello from 4619,received:HELLO FROM 4619
start5!/n4619 sent hello from 4619,received:HELLO FROM 4619
start5!/n4619 sent hello from 4619,received:HELLO FROM 4619
start5!/n4619 sent hello from 4619,received:HELLO FROM 4619
start5!/n4619 sent hello from 4619,received:HELLO FROM 4619
start5!/n4622 sent hello from 4622,received:HELLO FROM 4622
start5!/n4622 sent hello from 4622,received:HELLO FROM 4622
start5!/n4622 sent hello from 4622,received:HELLO FROM 4622
start5!/n4622 sent hello from 4622,received:HELLO FROM 4622
start5!/n4622 sent hello from 4622,received:HELLO FROM 4622
[3] Done ./server
[4] Done ./client
[5] Exit 1 ./client
[6] Exit 1 ./client
[7] Done ./client
[8]- Exit 1 ./client
可见每个客户都能获得正确的服务器返回给他的处理数据。如果在服务器程序中实现注释部分类似sleep(10)功能使得来自客户的数据排队等待,那么每个客户进程都能打开服务器FIFO。