Linux进程间通信机制:
1.同一主机进程间通信机制:
Unix方式:有名管道FIFO、无名管道PIPE、信号Signal
SystemV方式:信号量、消息队列、共享内存
2.网络通信:RPC(Remote Procedure Call)、Socket
管道
管道是进程间通信中最古老的方式,它包括无名管道和有名管道两种,前者可用于具有亲缘关系进程间的通信,即可用于父进程和子进程间的通信,后者额克服了管道没有名字的限制,因此,除具有前者所具有的功能外,它还允许无亲缘关系进程间的通信,即可用于运行于同一台机器上的任意两个进程间的通信。
无名管道由pipe()函数创建:
#i nclude <unistd.h>
int pipe(int filedis[2]);
参数filedis返回两个文件描述符:filedes[0]为读而打开,filedes[1]为写而打开。filedes[1]的输出是filedes[0]的输入。 无名管道占用两个文件描述符, 不能被非血缘关系的进程所共享, 一般应用在父子进程中.
特点:
半双工单向、父子进程间、一端写一段读、没有名字、缓冲区有限、数据无格式
无名管道常用于父子进程中, 可简单分为单向管道流模型和双向管道流模型. 其中, 单向管道流根据流向分为从父进程流向子进程的管道和从子进程流向父进程的管道.
下面设计一个实例, 数据从父进程流向子进程:父进程向管道写入一行字符, 子进程读取数据并打印到
屏幕上.
[bill@billstone Unix_study]$ cat pipe1.c
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <assert.h>
int main()
{
int fildes[2];
pid_t pid;
int i,j;
char buf[256];
assert(pipe(fildes) == 0); // 创建管道
assert((pid = fork()) >= 0); // 创建子进程
if(pid == 0){ // 子进程
close(fildes[1]); // 子进程关闭管道输出
memset(buf, 0, sizeof(buf));
j = read(fildes[0], buf, sizeof(buf));
fprintf(stderr, "[child] buf=[%s] len[%d]\n", buf, j);
return;
}
close(fildes[0]); // 父进程关闭管道输入
write(fildes[1], "hello!", strlen("hello!"));
write(fildes[1], "world!", strlen("world!"));
return 0;
}
[bill@billstone Unix_study]$ make pipe1
cc pipe1.c -o pipe1
[bill@billstone Unix_study]$ ./pipe1
[child] buf=[hello!world!] len[12] // 子进程一次就可以读出两次父进程写入的数据
[bill@billstone Unix_study]$
从上面可以看出, 在父进程中关闭 fildes[0], 向 fildes[1]写入数据; 在子进程中关闭 filedes[1], 从
fildes[0]中读取数据可实现从父进程流向子进程的管道.
在进程的通信中, 我们无法判断每次通信中报文的字节数, 即无法对数据流进行 自行拆分, 侧耳发生了上例中子进程一次性读取父进程两次通信的报文情况.
管道是进程之间的一种单向交流方法, 要实现进程间的双向交流, 就必须通过两个管道来完成. 双向管道流的创立过程如下:
(1) 创建管道, 返回两个无名管道文件描述符 fildes1 和 fildes2:
(2) 创建子进程, 子进程中继承管道 fildes1 和 fildes2.
(3) 父进程关闭只读文件描述符 fildes1[0], 只写描述符 fildes2[1]
(4) 子进程关闭只写文件描述符 fildes1[1], 只读描述符 fildes2[0]
创建的结果如下:
父进程 --写--> fildes1[1] --管道--> fildes1[0] --读--> 子进程
父进程 <--读-- fildes2[0] <--管道-- fildes2[1] <--写-- 子进程
这里实现一个父子进程间双向通信的实例: 父进程先向子进程发送两次数据, 再接收子进程传送刚来
的两次数据.
为了正确拆分时间留从父进程流向子进程的管道采用'固定长度'方法传送数据; 从子进程流向
父进程的管道采用'显式长度'方法传回数据.
(1) 固定长度方式
char bufG[255];
void WriteG(int fd, char *str, int len){
memset(bufG, 0, sizeof(bufG));
sprintf(bufG, "%s", str);
write(fd, bufG, len);
}
char *ReadG(int fd, int len){
memset(bufG, 0, sizeof(bufG));
read(fd, bufG, len);
return(bufG);
}
在此设计中, 父子程序需要约定好每次发送数据的长度; 且长度不能超过 255 个字符.
(2) 显式长度方式
char bufC[255];
void WriteC(int fd, char str[]){
sprintf(bufC, "%04d%s", strlen(str), str);
write(fd, bufC, strlen(bufC));
}
char *ReadC(int fd){
int i, j;
memset(bufC, 0, sizeof(bufC));
j = read(fd, bufC, 4);
i = atoi(bufC);
j = read(fd, bufC, i);
return(bufC);
}
父子进程约定在发送消息前先指明消息的长度.
(3) 主程序
#include <unistd.h>
#include <stdio.h>
#include <assert.h>
#include <sys/types.h>
int main()
{
int fildes1[2], fildes2[2];
pid_t pid;
char buf[255];
assert(pipe(fildes1) == 0);
assert(pipe(fildes2) == 0);
assert((pid = fork()) >= 0);
if(pid == 0){
close(fildes1[1]);
close(fildes2[0]);
strcpy(buf, ReadG(fildes1[0], 10));
fprintf(stderr, "[child] buf = [%s]\n", buf);
WriteC(fildes2[1], buf);
strcpy(buf, ReadG(fildes1[0], 10));
fprintf(stderr, "[child] buf = [%s]\n", buf);
WriteC(fildes2[1], buf);
return(0);
}
close(fildes1[0]);
close(fildes2[1]);
WriteG(fildes1[1], "hello!", 10);
WriteG(fildes1[1], "world!", 10);
fprintf(stderr, "[father] buf = [%s] \n", ReadC(fildes2[0]));
fprintf(stderr, "[father] buf = [%s] \n", ReadC(fildes2[0]));
return 0;
}
执行结果如下:
[bill@billstone Unix_study]$ make pipe2
cc pipe2.c -o pipe2
[bill@billstone Unix_study]$ ./pipe2
[child] buf = [hello!]
[child] buf = [world!]
[father] buf = [hello!]
[father] buf = [world!]
[bill@billstone Unix_study]$
dup dup2复制文件描述符
在Linux系统下,有名管道可由两种方式创建:命令行方式mknod系统调用和函数mkfifo。
下面的两种途径都在当前目录下生成了一个名为myfifo的有名管道:
方式一:mkfifo("myfifo","rw");
方式二:mknod myfifo p
生成了有名管道后,就可以使用一般的文件I/O函数如open、close、read、write等来对它进行操作。
管道是 UNIX 中最古老的进程间通信工具, 它提供了进程之间的一种单向通信的方法.
popen 模型
从前面的程序可以看出, 创建连接标准 I/O 的管道需要多个步骤, 这需要使用大量的代码, UNIX 为了
简化这个操作, 它提供了一组函数实现之. 原型如下:
#include <stdio.h>
FILE *popen(const char *command, char *type);
int pclose(FILE *stream);
函数 popen 调用成功时返回一个标准的 I/O 的 FILE 文件流, 其读写属性由参数 type 决定.
这里看一个模拟 shell 命令'ps -ef | grep init'的实例.
[bill@billstone Unix_study]$ cat pipe3.c
#include <stdio.h>
#include <assert.h>
int main()
{
FILE *out, *in;
char buf[255];
assert((out = popen("grep init", "w")) != NULL); // 创建写管道流
assert((in = popen("ps -ef", "r")) != NULL); // 创建读管道流
while(fgets(buf, sizeof(buf), in)) // 读取 ps -ef 的结果
fputs(buf, out); // 转发到 grep init
pclose(out);
pclose(in);
return 0;
}
[bill@billstone Unix_study]$ make pipe3
cc pipe3.c -o pipe3
[bill@billstone Unix_study]$ ./pipe3
root 1 0 0 Apr15 ? 00:00:04 init
bill 1392 1353 0 Apr15 ? 00:00:00 /usr/bin/ssh-agent /etc/X11/xinit/Xclients
bill 14204 14203 0 21:33 pts/0 00:00:00 grep init
[bill@billstone Unix_study]$ ps -ef | grep init
root 1 0 0 Apr15 ? 00:00:04 init
bill 1392 1353 0 Apr15 ? 00:00:00 /usr/bin/ssh-agent /etc/X11/xinit/Xclients
bill 14207 1441 0 21:35 pts/0 00:00:00 grep init
[bill@billstone Unix_study]$
读者可以从上面自行比较同 Shell 命令'ps -ef | grep init'的执行结果.
有名管道 FIFO
FIFO 可以在整个系统中使用.
在 Shell 中可以使用 mknod 或者 mkfifo 命令创建管道; 而在 C 程序中, 可以使用 mkfifo 函数创建有名管道.
要使用有名管道, 需要下面几个步骤:
(1) 创建管道文件
(2) 在某个进程中以只写方式打开管道文件, 并写管道
(3) 在某个进程中以只读方式打开管道文件, 并读管道
(4) 关闭管道文件.
低级文件编程库和标准文件编程库都可以操作管道. 管道在执行读写操作之前, 两端必须同时打开, 否
则执行打开管道某端操作的进程将一直阻塞到某个进程以相反方向打开管道为止.
C代码
/*fifoserver.c:向FIFO中写入信息*/
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#define FIFO_SERVER "FIFO4"
main(int argc,char** argv)
{
int fd=0;
char w_buf[4096];
int real_wnum;
memset(w_buf,0,4096);
if((mkfifo(FIFO_SERVER,O_CREAT|O_EXCL|0666)<0)&&(errno!=EEXIST))
printf("cannot create fifoserver\n");
/*此处存在着打开依赖,即若没有读端打开FIFO的话,写端就阻塞在写端*/
fd=open(FIFO_SERVER,O_WRONLY);
if(fd==-1)
printf("open error; no reading process\n");
printf("%d\n",fd);
real_wnum=write(fd,w_buf,2048);
if(real_wnum==-1)
printf("write to fifo error; try later\n");
else
printf("real write num is %d\n",real_wnum);
/*往FIFO写入的数据都是原子的,如果没有足够的空间,则会等待,而不是一点一点的写入。*/
real_wnum=write(fd,w_buf,4096);
if(real_wnum==-1)
printf("write to fifo error; try later\n");
else
printf("real write num is %d\n",real_wnum);
}
C代码
/*fifoclient.c:从FIFO中读出数据*/
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#define FIFO_SERVER "FIFO4"
main(int argc,char** argv)
{
char r_buf[4096];
int fd;
int r_size;
int ret_size;
r_size=atoi(argv[1]);
memset(r_buf,0,sizeof(r_buf));
fd=open(FIFO_SERVER,O_RDONLY);
if(fd==-1)
{
printf("open %s for read error\n");
exit(1);
}
printf("%d\n",fd);
while(1)
{
ret_size=read(fd,r_buf,r_size);
if(ret_size==-1)
printf("no data avlaible\n");
else
printf("real read bytes %d\n",ret_size);
sleep(1);
}
unlink(FIFO_SERVER);
}
下面是一个简单的实例.
首先是写进程: 创建 FIFO 文件, 再打开写端口, 然后读取标准输入并将输入信息发送到管道中, 当键
盘输入'exit'或'quit'时程序退出.
[bill@billstone Unix_study]$ cat fifo1.c
#include <stdio.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/errno.h>
extern int errno;
int main()
{
FILE *fp;
char buf[255];
assert((mkfifo("myfifo", S_IFIFO|0666) > 0) || (errno == EEXIST));
while(1){
assert((fp = fopen("myfifo", "w")) != NULL);
printf("please input: ");
fgets(buf, sizeof(buf), stdin);
fputs(buf, fp);
fclose(fp);
if(strncmp(buf, "quit", 4) == 0 || strncmp(buf, "exit", 4) == 0)
break;
}
return 0;
}
[bill@billstone Unix_study]$ make fifo1
cc fifo1.c -o fifo1
[bill@billstone Unix_study]$
然后是读进程: 打开管道的读端口, 从管道中读取信息(以行为单位), 并将此信息打印到屏幕上. 当读
取到'exit'或者'quit'时程序退出.
[bill@billstone Unix_study]$ cat fifo2.c
#include <stdio.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
FILE *fp;
char buf[255];
while(1){
assert((fp = fopen("myfifo", "r")) != NULL);
fgets(buf, strlen(buf), fp);
printf("gets: [%s]", buf);
fclose(fp);
if(strncmp(buf, "quit", 4) == 0 || strncmp(buf, "exit", 4) == 0)
break;
}
return 0;
}
[bill@billstone Unix_study]$ make fifo2
cc fifo2.c -o fifo2
[bill@billstone Unix_study]$
在一个终端上执行 fifo1, 而在另一个终端上执行 fifo2.
我们先输入'hello', 'world', 然后再输入'exit'退出:
[bill@billstone Unix_study]$ ./fifo1
please input: hello
please input: world
please input: exit
[bill@billstone Unix_study]$
我们可以看到读出结果如下:
[bill@billstone Unix_study]$ ./fifo2
gets: [hello
]gets: [world]gets: [exit][bill@billstone Unix_study]$
看到上面的输出结果, 您可能认为是我写错了. 其实不是的, 读出结果正是如此, 其实按照我们的本意,
正确的输出结果应该是这样的:
[bill@billstone Unix_study]$ ./fifo2
gets: [hello
]gets: [world
]gets: [exit